vmm/vmm_config/
machine_config.rs

1// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3use std::fmt::Debug;
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7use crate::cpu_config::templates::{CpuTemplateType, CustomCpuTemplate, StaticCpuTemplate};
8
9/// The default memory size of the VM, in MiB.
10pub const DEFAULT_MEM_SIZE_MIB: usize = 128;
11/// Firecracker aims to support small scale workloads only, so limit the maximum
12/// vCPUs supported.
13pub const MAX_SUPPORTED_VCPUS: u8 = 32;
14
15/// Errors associated with configuring the microVM.
16#[rustfmt::skip]
17#[derive(Debug, thiserror::Error, displaydoc::Display, PartialEq, Eq)]
18pub enum MachineConfigError {
19    /// The memory size (MiB) is smaller than the previously set balloon device target size.
20    IncompatibleBalloonSize,
21    /// The memory size (MiB) is either 0, or not a multiple of the configured page size.
22    InvalidMemorySize,
23    /// The number of vCPUs must be greater than 0, less than {MAX_SUPPORTED_VCPUS:} and must be 1 or an even number if SMT is enabled.
24    InvalidVcpuCount,
25    /// Could not get the configuration of the previously installed balloon device to validate the memory size.
26    InvalidVmState,
27    /// Enabling simultaneous multithreading is not supported on aarch64.
28    #[cfg(target_arch = "aarch64")]
29    SmtNotSupported,
30    /// Could not determine host kernel version when checking hugetlbfs compatibility
31    KernelVersion,
32}
33
34/// Describes the possible (huge)page configurations for a microVM's memory.
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
36pub enum HugePageConfig {
37    /// Do not use hugepages, e.g. back guest memory by 4K
38    #[default]
39    None,
40    /// Back guest memory by 2MB hugetlbfs pages
41    #[serde(rename = "2M")]
42    Hugetlbfs2M,
43}
44
45impl HugePageConfig {
46    /// Checks whether the given memory size (in MiB) is valid for this [`HugePageConfig`], e.g.
47    /// whether it is a multiple of the page size
48    fn is_valid_mem_size(&self, mem_size_mib: usize) -> bool {
49        let divisor = match self {
50            // Any integer memory size expressed in MiB will be a multiple of 4096KiB.
51            HugePageConfig::None => 1,
52            HugePageConfig::Hugetlbfs2M => 2,
53        };
54
55        mem_size_mib % divisor == 0
56    }
57
58    /// Returns the flags required to pass to `mmap`, in addition to `MAP_ANONYMOUS`, to
59    /// create a mapping backed by huge pages as described by this [`HugePageConfig`].
60    pub fn mmap_flags(&self) -> libc::c_int {
61        match self {
62            HugePageConfig::None => 0,
63            HugePageConfig::Hugetlbfs2M => libc::MAP_HUGETLB | libc::MAP_HUGE_2MB,
64        }
65    }
66
67    /// Returns `true` iff this [`HugePageConfig`] describes a hugetlbfs-based configuration.
68    pub fn is_hugetlbfs(&self) -> bool {
69        matches!(self, HugePageConfig::Hugetlbfs2M)
70    }
71
72    /// Gets the page size in bytes of this [`HugePageConfig`].
73    pub fn page_size(&self) -> usize {
74        match self {
75            HugePageConfig::None => 4096,
76            HugePageConfig::Hugetlbfs2M => 2 * 1024 * 1024,
77        }
78    }
79}
80
81impl From<HugePageConfig> for Option<memfd::HugetlbSize> {
82    fn from(value: HugePageConfig) -> Self {
83        match value {
84            HugePageConfig::None => None,
85            HugePageConfig::Hugetlbfs2M => Some(memfd::HugetlbSize::Huge2MB),
86        }
87    }
88}
89
90/// Struct used in PUT `/machine-config` API call.
91#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
92#[serde(deny_unknown_fields)]
93pub struct MachineConfig {
94    /// Number of vcpu to start.
95    pub vcpu_count: u8,
96    /// The memory size in MiB.
97    pub mem_size_mib: usize,
98    /// Enables or disabled SMT.
99    #[serde(default)]
100    pub smt: bool,
101    /// A CPU template that it is used to filter the CPU features exposed to the guest.
102    // FIXME: once support for static CPU templates is removed, this field can be dropped altogether
103    #[serde(
104        default,
105        skip_serializing_if = "is_none_or_custom_template",
106        deserialize_with = "deserialize_static_template",
107        serialize_with = "serialize_static_template"
108    )]
109    pub cpu_template: Option<CpuTemplateType>,
110    /// Enables or disables dirty page tracking. Enabling allows incremental snapshots.
111    #[serde(default)]
112    pub track_dirty_pages: bool,
113    /// Configures what page size Firecracker should use to back guest memory.
114    #[serde(default)]
115    pub huge_pages: HugePageConfig,
116    /// Enables nested virtualization features when supported by the host.
117    #[serde(default)]
118    pub enable_nested_virt: bool,
119    /// GDB socket address.
120    #[cfg(feature = "gdb")]
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub gdb_socket_path: Option<String>,
123}
124
125fn is_none_or_custom_template(template: &Option<CpuTemplateType>) -> bool {
126    matches!(template, None | Some(CpuTemplateType::Custom(_)))
127}
128
129fn deserialize_static_template<'de, D>(deserializer: D) -> Result<Option<CpuTemplateType>, D::Error>
130where
131    D: Deserializer<'de>,
132{
133    Option::<StaticCpuTemplate>::deserialize(deserializer)
134        .map(|maybe_template| maybe_template.map(CpuTemplateType::Static))
135}
136
137fn serialize_static_template<S>(
138    template: &Option<CpuTemplateType>,
139    serializer: S,
140) -> Result<S::Ok, S::Error>
141where
142    S: Serializer,
143{
144    let Some(CpuTemplateType::Static(template)) = template else {
145        // We have a skip_serializing_if on the field
146        unreachable!()
147    };
148
149    template.serialize(serializer)
150}
151
152impl Default for MachineConfig {
153    fn default() -> Self {
154        Self {
155            vcpu_count: 1,
156            mem_size_mib: DEFAULT_MEM_SIZE_MIB,
157            smt: false,
158            cpu_template: None,
159            track_dirty_pages: false,
160            huge_pages: HugePageConfig::None,
161            enable_nested_virt: false,
162            #[cfg(feature = "gdb")]
163            gdb_socket_path: None,
164        }
165    }
166}
167
168/// Struct used in PATCH `/machine-config` API call.
169/// Used to update `MachineConfig` in `VmResources`.
170/// This struct mirrors all the fields in `MachineConfig`.
171/// All fields are optional, but at least one needs to be specified.
172/// If a field is `Some(value)` then we assume an update is requested
173/// for that field.
174#[derive(Clone, Default, Debug, PartialEq, Eq, Deserialize)]
175#[serde(deny_unknown_fields)]
176pub struct MachineConfigUpdate {
177    /// Number of vcpu to start.
178    #[serde(default)]
179    pub vcpu_count: Option<u8>,
180    /// The memory size in MiB.
181    #[serde(default)]
182    pub mem_size_mib: Option<usize>,
183    /// Enables or disabled SMT.
184    #[serde(default)]
185    pub smt: Option<bool>,
186    /// A CPU template that it is used to filter the CPU features exposed to the guest.
187    #[serde(default)]
188    pub cpu_template: Option<StaticCpuTemplate>,
189    /// Enables or disables dirty page tracking. Enabling allows incremental snapshots.
190    #[serde(default)]
191    pub track_dirty_pages: Option<bool>,
192    /// Configures what page size Firecracker should use to back guest memory.
193    #[serde(default)]
194    pub huge_pages: Option<HugePageConfig>,
195    /// Enables nested virtualization features when supported by the host.
196    #[serde(default)]
197    pub enable_nested_virt: Option<bool>,
198    /// GDB socket address.
199    #[cfg(feature = "gdb")]
200    #[serde(default)]
201    pub gdb_socket_path: Option<String>,
202}
203
204impl MachineConfigUpdate {
205    /// Checks if the update request contains any data.
206    /// Returns `true` if all fields are set to `None` which means that there is nothing
207    /// to be updated.
208    pub fn is_empty(&self) -> bool {
209        self == &Default::default()
210    }
211}
212
213impl From<MachineConfig> for MachineConfigUpdate {
214    fn from(cfg: MachineConfig) -> Self {
215        MachineConfigUpdate {
216            vcpu_count: Some(cfg.vcpu_count),
217            mem_size_mib: Some(cfg.mem_size_mib),
218            smt: Some(cfg.smt),
219            cpu_template: cfg.static_template(),
220            track_dirty_pages: Some(cfg.track_dirty_pages),
221            huge_pages: Some(cfg.huge_pages),
222            enable_nested_virt: Some(cfg.enable_nested_virt),
223            #[cfg(feature = "gdb")]
224            gdb_socket_path: cfg.gdb_socket_path,
225        }
226    }
227}
228
229impl MachineConfig {
230    /// Sets cpu tempalte field to `CpuTemplateType::Custom(cpu_template)`.
231    pub fn set_custom_cpu_template(&mut self, cpu_template: CustomCpuTemplate) {
232        self.cpu_template = Some(CpuTemplateType::Custom(cpu_template));
233    }
234
235    fn static_template(&self) -> Option<StaticCpuTemplate> {
236        match self.cpu_template {
237            Some(CpuTemplateType::Static(template)) => Some(template),
238            _ => None,
239        }
240    }
241
242    /// Updates [`MachineConfig`] with [`MachineConfigUpdate`].
243    /// Mapping for cpu template update:
244    /// StaticCpuTemplate::None -> None
245    /// StaticCpuTemplate::Other -> Some(CustomCpuTemplate::Static(Other)),
246    /// Returns the updated `MachineConfig` object.
247    pub fn update(
248        &self,
249        update: &MachineConfigUpdate,
250    ) -> Result<MachineConfig, MachineConfigError> {
251        let vcpu_count = update.vcpu_count.unwrap_or(self.vcpu_count);
252
253        let smt = update.smt.unwrap_or(self.smt);
254
255        #[cfg(target_arch = "aarch64")]
256        if smt {
257            return Err(MachineConfigError::SmtNotSupported);
258        }
259
260        if vcpu_count == 0 || vcpu_count > MAX_SUPPORTED_VCPUS {
261            return Err(MachineConfigError::InvalidVcpuCount);
262        }
263
264        // If SMT is enabled or is to be enabled in this call
265        // only allow vcpu count to be 1 or even.
266        if smt && vcpu_count > 1 && vcpu_count % 2 == 1 {
267            return Err(MachineConfigError::InvalidVcpuCount);
268        }
269
270        let mem_size_mib = update.mem_size_mib.unwrap_or(self.mem_size_mib);
271        let page_config = update.huge_pages.unwrap_or(self.huge_pages);
272
273        if mem_size_mib == 0 || !page_config.is_valid_mem_size(mem_size_mib) {
274            return Err(MachineConfigError::InvalidMemorySize);
275        }
276
277        let cpu_template = match update.cpu_template {
278            None => self.cpu_template.clone(),
279            Some(StaticCpuTemplate::None) => None,
280            Some(other) => Some(CpuTemplateType::Static(other)),
281        };
282
283        Ok(MachineConfig {
284            vcpu_count,
285            mem_size_mib,
286            smt,
287            cpu_template,
288            track_dirty_pages: update.track_dirty_pages.unwrap_or(self.track_dirty_pages),
289            huge_pages: page_config,
290            enable_nested_virt: update.enable_nested_virt.unwrap_or(self.enable_nested_virt),
291            #[cfg(feature = "gdb")]
292            gdb_socket_path: update.gdb_socket_path.clone(),
293        })
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use crate::cpu_config::templates::{CpuTemplateType, CustomCpuTemplate, StaticCpuTemplate};
300    use crate::vmm_config::machine_config::MachineConfig;
301
302    // Ensure the special (de)serialization logic for the cpu_template field works:
303    // only static cpu templates can be specified via the machine-config endpoint, but
304    // we still cram custom cpu templates into the MachineConfig struct if they're set otherwise
305    // Ensure that during (de)serialization we preserve static templates, but we set custom
306    // templates to None
307    #[test]
308    fn test_serialize_machine_config() {
309        #[cfg(target_arch = "aarch64")]
310        const TEMPLATE: StaticCpuTemplate = StaticCpuTemplate::V1N1;
311        #[cfg(target_arch = "x86_64")]
312        const TEMPLATE: StaticCpuTemplate = StaticCpuTemplate::T2S;
313
314        let mconfig = MachineConfig {
315            cpu_template: None,
316            ..Default::default()
317        };
318
319        let serialized = serde_json::to_string(&mconfig).unwrap();
320        let deserialized = serde_json::from_str::<MachineConfig>(&serialized).unwrap();
321
322        assert!(deserialized.cpu_template.is_none());
323
324        let mconfig = MachineConfig {
325            cpu_template: Some(CpuTemplateType::Static(TEMPLATE)),
326            ..Default::default()
327        };
328
329        let serialized = serde_json::to_string(&mconfig).unwrap();
330        let deserialized = serde_json::from_str::<MachineConfig>(&serialized).unwrap();
331
332        assert_eq!(
333            deserialized.cpu_template,
334            Some(CpuTemplateType::Static(TEMPLATE))
335        );
336
337        let mconfig = MachineConfig {
338            cpu_template: Some(CpuTemplateType::Custom(CustomCpuTemplate::default())),
339            ..Default::default()
340        };
341
342        let serialized = serde_json::to_string(&mconfig).unwrap();
343        let deserialized = serde_json::from_str::<MachineConfig>(&serialized).unwrap();
344
345        assert!(deserialized.cpu_template.is_none());
346    }
347}