vmm: add memory reserve option to opt out of MAP_NORESERVE

Cloud Hypervisor maps guest RAM with MAP_NORESERVE, so the kernel never
reserves the backing pages at mmap time. On a host whose hugepage pool
cannot satisfy every guest, a VM is created successfully and then takes
a SIGBUS when the guest faults a page the pool can no longer back. This
is the failure mode reported in #5730 and #7387. As noted on #5730,
checking free pool headroom up front is not a reliable fix: another
process can consume pages between the check and the fault.

Add a reserve=on parameter to --memory and --memory-zone (default off,
preserving the current MAP_NORESERVE behaviour). When set, the region
is mapped without MAP_NORESERVE, so the kernel reserves the backing
pages (swap, or huge pages for hugepage-backed memory) at mmap time,
atomically with the mapping. An over-committed configuration then
fails cleanly at VM creation with an mmap ENOMEM instead of crashing
the guest later. Unlike prefault it does not fault the memory in, so
it does not slow down boot.

This mirrors QEMU's memory-backend reserve property, which has the same
name and meaning (reserve=off maps with MAP_NORESERVE). reserve is
threaded through the same mmap paths as the existing prefault option,
and is exposed in the OpenAPI schema, CLI help and docs. The top-level
--memory reserve=on path is unchanged: the default zone is synthesised
from MemoryConfig and inherits its reserve value.

Assisted-by: Claude Code (Opus 4.8)
Signed-off-by: Ian Klemm <hi@ianklemm.de>
This commit is contained in:
Ian Klemm
2026-06-05 19:42:29 +01:00
committed by Rob Bradford
parent 38bee23d89
commit 8d05407799
10 changed files with 126 additions and 5 deletions

View File

@@ -321,7 +321,7 @@ fn get_cli_options_sorted(
hotplug_method=acpi|virtio-mem,\
hotplug_size=<hotpluggable_memory_size>,\
hotplugged_size=<hotplugged_memory_size>,\
prefault=on|off,thp=on|off\"",
prefault=on|off,reserve=on|off,thp=on|off\"",
)
.default_value(default_memory)
.group("vm-config"),
@@ -335,7 +335,7 @@ fn get_cli_options_sorted(
host_numa_node=<node_id>,\
id=<zone_identifier>,hotplug_size=<hotpluggable_memory_size>,\
hotplugged_size=<hotplugged_memory_size>,\
prefault=on|off\"",
prefault=on|off,reserve=on|off\"",
)
.num_args(1..)
.action(ArgAction::Append)
@@ -1003,6 +1003,7 @@ mod unit_tests {
hugepages: false,
hugepage_size: None,
prefault: false,
reserve: false,
zones: None,
thp: true,
},

View File

@@ -20,13 +20,14 @@ struct MemoryConfig {
hugepages: bool,
hugepage_size: Option<u64>,
prefault: bool,
reserve: bool,
thp: bool,
zones: Option<Vec<MemoryZoneConfig>>,
}
```
```
--memory <memory> Memory parameters "size=<guest_memory_size>,mergeable=on|off,shared=on|off,hugepages=on|off,hugepage_size=<hugepage_size>,hotplug_method=acpi|virtio-mem,hotplug_size=<hotpluggable_memory_size>,hotplugged_size=<hotplugged_memory_size>,prefault=on|off,thp=on|off" [default: size=512M,thp=on]
--memory <memory> Memory parameters "size=<guest_memory_size>,mergeable=on|off,shared=on|off,hugepages=on|off,hugepage_size=<hugepage_size>,hotplug_method=acpi|virtio-mem,hotplug_size=<hotpluggable_memory_size>,hotplugged_size=<hotplugged_memory_size>,prefault=on|off,reserve=on|off,thp=on|off" [default: size=512M,thp=on]
```
### `size`
@@ -177,6 +178,32 @@ _Example_
--memory size=1G,prefault=on
```
### `reserve`
Specifies if guest memory should be `mmap(2)`-ed _without_ the `MAP_NORESERVE`
flag, asking the kernel to reserve the backing pages (swap space, or huge pages
for hugepage-backed memory) for the whole region up front at `mmap` time.
By default Cloud Hypervisor maps guest memory with `MAP_NORESERVE`, so VM
creation succeeds even when the backing pool cannot satisfy the full guest size.
The shortfall then surfaces only later, as a `SIGBUS` delivered to the guest when
it faults a page the pool cannot back. With `reserve=on` the reservation is made
when the memory is mapped, so an over-committed configuration fails cleanly at VM
creation with an out-of-memory error instead of crashing the guest at run time.
This is most useful for hugepage-backed memory, where the huge pages are reserved
from the pool.
Unlike `prefault`, this does not populate or fault in the memory, so it does not
slow down boot; it only reserves it.
By default this option is turned off.
_Example_
```
--memory size=1G,hugepages=on,reserve=on
```
### `thp`
Specifies if private anonymous memory for the guest (i.e. `shared=off` and no
@@ -214,12 +241,13 @@ struct MemoryZoneConfig {
hotplug_size: Option<u64>,
hotplugged_size: Option<u64>,
prefault: bool,
reserve: bool,
mergeable: bool,
}
```
```
--memory-zone <memory-zone> User defined memory zone parameters "size=<guest_memory_region_size>,file=<backing_file>,shared=on|off,hugepages=on|off,hugepage_size=<hugepage_size>,host_numa_node=<node_id>,id=<zone_identifier>,hotplug_size=<hotpluggable_memory_size>,hotplugged_size=<hotplugged_memory_size>,prefault=on|off,mergeable=on|off"
--memory-zone <memory-zone> User defined memory zone parameters "size=<guest_memory_region_size>,file=<backing_file>,shared=on|off,hugepages=on|off,hugepage_size=<hugepage_size>,host_numa_node=<node_id>,id=<zone_identifier>,hotplug_size=<hotpluggable_memory_size>,hotplugged_size=<hotplugged_memory_size>,prefault=on|off,reserve=on|off,mergeable=on|off"
```
This parameter expects one or more occurrences, allowing for a list of memory
@@ -423,6 +451,34 @@ _Example_
--memory-zone id=mem0,size=1G,prefault=on
```
### `reserve`
Specifies if the memory for this zone should be `mmap(2)`-ed _without_ the
`MAP_NORESERVE` flag, asking the kernel to reserve the backing pages (swap space,
or huge pages for hugepage-backed memory) for the whole zone up front at `mmap`
time.
By default Cloud Hypervisor maps guest memory with `MAP_NORESERVE`, so VM
creation succeeds even when the backing pool cannot satisfy the full zone size.
The shortfall then surfaces only later, as a `SIGBUS` delivered to the guest when
it faults a page the pool cannot back. With `reserve=on` the reservation is made
when the memory is mapped, so an over-committed configuration fails cleanly at VM
creation with an out-of-memory error instead of crashing the guest at run time.
This is most useful for hugepage-backed zones, where the huge pages are reserved
from the pool.
Unlike `prefault`, this does not populate or fault in the memory, so it does not
slow down boot; it only reserves it.
By default this option is turned off.
_Example_
```
--memory size=0
--memory-zone id=mem0,size=1G,hugepages=on,reserve=on
```
### `mergeable`
Specifies if the pages from this memory zone must be marked as _mergeable_,

View File

@@ -150,6 +150,7 @@ impl RequestHandler for StubApiRequestHandler {
hugepages: false,
hugepage_size: None,
prefault: false,
reserve: false,
zones: None,
thp: true,
},

View File

@@ -148,6 +148,7 @@ fn create_dummy_virtio_mem(bytes: &[u8; VIRTIO_MEM_DATA_SIZE]) -> (Mem, Arc<Gues
false,
false,
false,
false,
None,
numa_id,
None,

View File

@@ -862,6 +862,9 @@ components:
prefault:
type: boolean
default: false
reserve:
type: boolean
default: false
MemoryConfig:
required:
@@ -895,6 +898,9 @@ components:
prefault:
type: boolean
default: false
reserve:
type: boolean
default: false
thp:
type: boolean
default: true

View File

@@ -1060,6 +1060,7 @@ impl MemoryConfig {
.add("hugepages")
.add("hugepage_size")
.add("prefault")
.add("reserve")
.add("thp");
parser.parse(memory).map_err(Error::ParseMemory)?;
@@ -1104,6 +1105,11 @@ impl MemoryConfig {
.map_err(Error::ParseMemory)?
.unwrap_or(Toggle(false))
.0;
let reserve = parser
.convert::<Toggle>("reserve")
.map_err(Error::ParseMemory)?
.unwrap_or(Toggle(false))
.0;
let thp = parser
.convert::<Toggle>("thp")
.map_err(Error::ParseMemory)?
@@ -1125,6 +1131,7 @@ impl MemoryConfig {
.add("hotplug_size")
.add("hotplugged_size")
.add("prefault")
.add("reserve")
.add("mergeable");
parser.parse(memory_zone).map_err(Error::ParseMemoryZone)?;
@@ -1166,6 +1173,11 @@ impl MemoryConfig {
.map_err(Error::ParseMemoryZone)?
.unwrap_or(Toggle(false))
.0;
let reserve = parser
.convert::<Toggle>("reserve")
.map_err(Error::ParseMemoryZone)?
.unwrap_or(Toggle(false))
.0;
let mergeable = parser
.convert::<Toggle>("mergeable")
.map_err(Error::ParseMemoryZone)?
@@ -1183,6 +1195,7 @@ impl MemoryConfig {
hotplug_size,
hotplugged_size,
prefault,
reserve,
mergeable,
});
}
@@ -1201,6 +1214,7 @@ impl MemoryConfig {
hugepages,
hugepage_size,
prefault,
reserve,
zones,
thp,
})
@@ -3974,6 +3988,20 @@ mod unit_tests {
..Default::default()
}
);
// reserve=on on a zone
assert_eq!(
MemoryConfig::parse("size=0", Some(vec!["id=mem0,size=1G,reserve=on"]))?,
MemoryConfig {
size: 0,
zones: Some(vec![MemoryZoneConfig {
id: "mem0".to_string(),
size: 1 << 30,
reserve: true,
..Default::default()
}]),
..Default::default()
}
);
Ok(())
}
@@ -4038,6 +4066,16 @@ mod unit_tests {
..Default::default()
}
);
// reserve=on opts out of MAP_NORESERVE
assert_eq!(
MemoryConfig::parse("size=1G,hugepages=on,reserve=on", None)?,
MemoryConfig {
size: 1 << 30,
hugepages: true,
reserve: true,
..Default::default()
}
);
Ok(())
}
@@ -5227,6 +5265,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
hugepages: false,
hugepage_size: None,
prefault: false,
reserve: false,
zones: None,
thp: true,
},

View File

@@ -5387,6 +5387,7 @@ impl IvshmemOps for IvshmemHandler {
0,
size,
false,
false,
true,
false,
None,

View File

@@ -2731,6 +2731,7 @@ mod unit_tests {
hugepages: false,
hugepage_size: None,
prefault: false,
reserve: false,
zones: None,
thp: true,
},

View File

@@ -278,6 +278,7 @@ pub struct MemoryManager {
hugepages: bool,
hugepage_size: Option<u64>,
prefault: bool,
reserve: bool,
thp: bool,
user_provided_zones: bool,
snapshot_memory_ranges: MemoryRangeTable,
@@ -727,6 +728,7 @@ impl MemoryManager {
region_start,
region_size as usize,
prefault.unwrap_or(zone.prefault),
zone.reserve,
zone.shared,
zone.hugepages,
zone.hugepage_size,
@@ -829,6 +831,7 @@ impl MemoryManager {
GuestAddress(guest_ram_mapping.gpa),
guest_ram_mapping.size as usize,
prefault.unwrap_or(zone_config.prefault),
zone_config.reserve,
zone_config.shared,
zone_config.hugepages,
zone_config.hugepage_size,
@@ -1528,6 +1531,7 @@ impl MemoryManager {
hotplug_size: config.hotplug_size,
hotplugged_size: config.hotplugged_size,
prefault: config.prefault,
reserve: config.reserve,
mergeable: config.mergeable,
}];
@@ -1762,6 +1766,7 @@ impl MemoryManager {
start_addr,
hotplug_size as usize,
prefault.unwrap_or(zone.prefault),
zone.reserve,
zone.shared,
zone.hugepages,
zone.hugepage_size,
@@ -1875,6 +1880,7 @@ impl MemoryManager {
hugepages: config.hugepages,
hugepage_size: config.hugepage_size,
prefault: config.prefault,
reserve: config.reserve,
user_provided_zones,
snapshot_memory_ranges: MemoryRangeTable::default(),
memory_zones,
@@ -2042,6 +2048,7 @@ impl MemoryManager {
file_offset: u64,
size: usize,
prefault: bool,
reserve: bool,
shared: bool,
hugepages: bool,
hugepage_size: Option<u64>,
@@ -2049,7 +2056,7 @@ impl MemoryManager {
existing_memory_file: Option<File>,
thp: bool,
) -> Result<MmapRegion<AtomicBitmap>, Error> {
let mut mmap_flags = libc::MAP_NORESERVE;
let mut mmap_flags = if reserve { 0 } else { libc::MAP_NORESERVE };
// The duplication of mmap_flags ORing here is unfortunate but it also makes
// the complexity of the handling clear.
@@ -2179,6 +2186,7 @@ impl MemoryManager {
start_addr: GuestAddress,
size: usize,
prefault: bool,
reserve: bool,
shared: bool,
hugepages: bool,
hugepage_size: Option<u64>,
@@ -2191,6 +2199,7 @@ impl MemoryManager {
file_offset,
size,
prefault,
reserve,
shared,
hugepages,
hugepage_size,
@@ -2306,6 +2315,7 @@ impl MemoryManager {
start_addr,
size,
self.prefault,
self.reserve,
self.shared,
self.hugepages,
self.hugepage_size,

View File

@@ -245,6 +245,8 @@ pub struct MemoryZoneConfig {
#[serde(default)]
pub prefault: bool,
#[serde(default)]
pub reserve: bool,
#[serde(default)]
pub mergeable: bool,
}
@@ -293,6 +295,8 @@ pub struct MemoryConfig {
#[serde(default)]
pub prefault: bool,
#[serde(default)]
pub reserve: bool,
#[serde(default)]
pub zones: Option<Vec<MemoryZoneConfig>>,
#[serde(default = "default_memoryconfig_thp")]
pub thp: bool,
@@ -312,6 +316,7 @@ impl Default for MemoryConfig {
hugepages: false,
hugepage_size: None,
prefault: false,
reserve: false,
zones: None,
thp: true,
}