Skip to content

Commit 78f738e

Browse files
fix: Unmap initial state data (#10695)
The mmapped initial state data is never munmapped, causing unnecessary increases in resident memory after canister installs and upgrades. This wasn't a large problem, as the memory could be reclaimed if necessary, but it's still cleaner to stop mapping it as soon as we know that we won't need it anymore.
1 parent 65ad179 commit 78f738e

2 files changed

Lines changed: 87 additions & 9 deletions

File tree

rs/canister_sandbox/src/sandbox_manager.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -498,12 +498,13 @@ impl SandboxManager {
498498
// and later or concurrent uses of the same cache entry would fail. But
499499
// we can mmap the data without mutating the fd.
500500
let initial_state_data: InitialStateData = {
501-
use nix::sys::mman::{MapFlags, ProtFlags, mmap};
501+
use nix::sys::mman::{MapFlags, ProtFlags, mmap, munmap};
502502
use std::os::{fd::AsRawFd, unix::fs::MetadataExt};
503503

504504
let mmap_size = initial_state_data.metadata().unwrap().size() as usize;
505-
let data = if mmap_size == 0 {
506-
&[]
505+
if mmap_size == 0 {
506+
let data: &[u8] = &[];
507+
bincode::deserialize(data).unwrap()
507508
} else {
508509
// SAFETY: The address is valid because it is null, we have checked
509510
// the size is positive and the fd is valid since it comes from a
@@ -523,9 +524,16 @@ impl SandboxManager {
523524
// SAFETY: We've mmapped `mmap_size` and gotten a succesful
524525
// reply at address `mmap_ptr` and the mapping is readonly
525526
// private.
526-
unsafe { std::slice::from_raw_parts(mmap_ptr, mmap_size) }
527-
};
528-
bincode::deserialize(data).unwrap()
527+
let data = unsafe { std::slice::from_raw_parts(mmap_ptr, mmap_size) };
528+
let initial_state_data = bincode::deserialize(data).unwrap();
529+
// SAFETY: `mmap_ptr`/`mmap_size` are exactly what `mmap` returned;
530+
// `data` is not used past this point.
531+
unsafe {
532+
munmap(mmap_ptr as *mut std::ffi::c_void, mmap_size)
533+
.expect("Unable to unmap initial state data file");
534+
}
535+
initial_state_data
536+
}
529537
};
530538

531539
let (wasm_memory_modifications, exported_globals) = self

rs/embedders/src/serialized_module.rs

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use ic_interfaces::execution_environment::{HypervisorError, HypervisorResult};
1313
use ic_replicated_state::canister_state::execution_state::WasmMetadata;
1414
use ic_types::{DiskBytes, NumInstructions, methods::WasmMethod};
1515
use ic_wasm_types::WasmEngineError;
16-
use nix::sys::mman::{MapFlags, ProtFlags, mmap};
16+
use nix::sys::mman::{MapFlags, ProtFlags, mmap, munmap};
1717
use serde::{Deserialize, Serialize};
1818
use wasmtime::Module;
1919

@@ -259,8 +259,15 @@ impl OnDiskSerializedModule {
259259
}) as *mut u8;
260260
// Safety: allocation was made with length `mmap_size`.
261261
let data = unsafe { std::slice::from_raw_parts(mmap_ptr, mmap_size) };
262-
bincode::deserialize::<InitialStateData>(data)
263-
.expect("Error parsing initial state data file")
262+
let initial_state_data = bincode::deserialize::<InitialStateData>(data)
263+
.expect("Error parsing initial state data file");
264+
// Safety: `mmap_ptr`/`mmap_size` are the pointer and length returned by
265+
// the `mmap` above; `data` is not used past this point.
266+
unsafe {
267+
munmap(mmap_ptr as *mut std::ffi::c_void, mmap_size)
268+
.expect("Unable to unmap initial state data file");
269+
}
270+
initial_state_data
264271
}
265272
}
266273

@@ -427,4 +434,67 @@ mod test {
427434
});
428435
}
429436
}
437+
438+
#[cfg(target_os = "linux")]
439+
#[test]
440+
fn initial_state_data_does_not_leak_mappings() {
441+
let module = SerializedModule {
442+
bytes: Arc::new(SerializedModuleBytes(vec![0_u8; 4096])),
443+
exported_functions: BTreeSet::new(),
444+
data_segments: vec![(0_usize, vec![7_u8; 4096 * 8])].into_iter().collect(),
445+
wasm_metadata: WasmMetadata::new(std::collections::BTreeMap::new()),
446+
compilation_cost: NumInstructions::from(0),
447+
imports_details: WasmImportsDetails {
448+
imports_call_cycles_add: false,
449+
imports_canister_cycle_balance: false,
450+
imports_msg_cycles_available: false,
451+
imports_msg_cycles_refunded: false,
452+
imports_msg_cycles_accept: false,
453+
imports_mint_cycles: false,
454+
},
455+
is_wasm64: false,
456+
};
457+
458+
let dir = tempfile::tempdir().unwrap();
459+
let mut bytes_path: PathBuf = dir.path().into();
460+
let mut data_path: PathBuf = dir.path().into();
461+
bytes_path.push("bytes");
462+
data_path.push("data");
463+
let on_disk =
464+
OnDiskSerializedModule::from_serialized_module(module, &bytes_path, &data_path);
465+
466+
let map_size = {
467+
let len = on_disk.initial_state_data.metadata().unwrap().len() as usize;
468+
len.div_ceil(4096) * 4096
469+
};
470+
let count_leaked_maps = || {
471+
std::fs::read_to_string("/proc/self/maps")
472+
.unwrap()
473+
.lines()
474+
.filter(|line| {
475+
line.split_once(' ')
476+
.and_then(|(range, _)| range.split_once('-'))
477+
.and_then(|(s, e)| {
478+
Some(
479+
usize::from_str_radix(e, 16).ok()?
480+
- usize::from_str_radix(s, 16).ok()?,
481+
)
482+
})
483+
== Some(map_size)
484+
})
485+
.count()
486+
};
487+
488+
let before = count_leaked_maps();
489+
const ITERS: usize = 500;
490+
for _ in 0..ITERS {
491+
let _ = on_disk.initial_state_data();
492+
}
493+
let after = count_leaked_maps();
494+
495+
assert!(
496+
after <= before + 2,
497+
"initial_state_data() leaked mappings: {before} -> {after} over {ITERS} calls"
498+
);
499+
}
430500
}

0 commit comments

Comments
 (0)