Skip to content

Commit e1b39cd

Browse files
committed
Make pagemap integration resilient across forks
1 parent 57e55cc commit e1b39cd

File tree

3 files changed

+220
-2
lines changed

3 files changed

+220
-2
lines changed

crates/wasmtime/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ wasmtime-versioned-export-macros = { workspace = true, optional = true }
113113
name = "host_segfault"
114114
harness = false
115115

116+
[[test]]
117+
name = "engine_across_forks"
118+
harness = false
119+
116120
# =============================================================================
117121
#
118122
# Features for the Wasmtime crate.

crates/wasmtime/src/runtime/vm/sys/unix/pagemap.rs

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,100 @@ use crate::prelude::*;
77

88
use self::ioctl::{Categories, PageMapScanBuilder};
99
use crate::runtime::vm::{HostAlignedByteCount, host_page_size};
10+
use rustix::fd::AsRawFd;
1011
use rustix::ioctl::ioctl;
1112
use std::fs::File;
1213
use std::mem::MaybeUninit;
1314
use std::ptr;
15+
use std::sync::LazyLock;
16+
17+
/// A static file-per-process which represents this process's page map file.
18+
///
19+
/// Note that this is required to be updated on a fork because otherwise this'll
20+
/// refer to the parent process's page map instead of the child process's page
21+
/// map. Thus when first initializing this file the `pthread_atfork` function is
22+
/// used to hook the child process to update this.
23+
///
24+
/// Also note that updating this is not done via mutation but rather it's done
25+
/// with `dup2` to replace the file descriptor that `File` points to in-place.
26+
/// The local copy of of `File` is then closed in the atfork handler.
27+
static PROCESS_PAGEMAP: LazyLock<Option<File>> = LazyLock::new(|| {
28+
let pagemap = File::open("/proc/self/pagemap").ok()?;
29+
30+
// SAFETY: all libc functions are unsafe by default, and we're basically
31+
// going to do our damndest to make sure this invocation of `pthread_atfork`
32+
// is safe, namely the handler registered here is intentionally quite
33+
// minimal and only accesses the `PROCESS_PAGEMAP`.
34+
let rc = unsafe { libc::pthread_atfork(None, None, Some(after_fork_in_child)) };
35+
if rc != 0 {
36+
return None;
37+
}
38+
39+
return Some(pagemap);
40+
41+
/// Hook executed as part of `pthread_atfork` in the child process after a
42+
/// fork.
43+
///
44+
/// # Safety
45+
///
46+
/// This function is not safe to call in general and additionally has its
47+
/// own stringent safety requirements. This is after a fork but before exec
48+
/// so all the safety requirements of `Command::pre_exec` in the standard
49+
/// library apply here. Effectively the standard library primitives are
50+
/// avoided here as they aren't necessarily safe to execute in this context.
51+
unsafe extern "C" fn after_fork_in_child() {
52+
let Some(parent_pagemap) = PROCESS_PAGEMAP.as_ref() else {
53+
// This should not be reachable, but to avoid panic infrastructure
54+
// here this is just skipped instead.
55+
return;
56+
};
57+
58+
// SAFETY: see function documentation.
59+
//
60+
// Here `/proc/self/pagemap` is opened in the child. If that fails for
61+
// whatever reason then the pagemap is replaced with `/dev/null` which
62+
// means that all future ioctls for `PAGEMAP_SCAN` will fail. If that
63+
// fails then that's left to abort the process for now. If that's
64+
// problematic we may want to consider opening a local pipe and then
65+
// installing that here? Unsure.
66+
//
67+
// Once a fd is opened the `dup2` syscall is used to replace the
68+
// previous file descriptor stored in `parent_pagemap`. That'll update
69+
// the pagemap in-place in this child for all future use in case this is
70+
// further used in the child.
71+
//
72+
// And finally once that's all done the `child_pagemap` is itself
73+
// closed since we have no more need for it.
74+
unsafe {
75+
let flags = libc::O_CLOEXEC | libc::O_RDONLY;
76+
let mut child_pagemap = libc::open(c"/proc/self/pagemap".as_ptr(), flags);
77+
if child_pagemap == -1 {
78+
child_pagemap = libc::open(c"/dev/null".as_ptr(), flags);
79+
}
80+
if child_pagemap == -1 {
81+
libc::abort();
82+
}
83+
84+
let rc = libc::dup2(child_pagemap, parent_pagemap.as_raw_fd());
85+
if rc == -1 {
86+
libc::abort();
87+
}
88+
let rc = libc::close(child_pagemap);
89+
if rc == -1 {
90+
libc::abort();
91+
}
92+
}
93+
}
94+
});
1495

1596
#[derive(Debug)]
16-
pub struct PageMap(File);
97+
pub struct PageMap(&'static File);
1798

1899
impl PageMap {
19100
#[cfg(feature = "pooling-allocator")]
20101
pub fn new() -> Option<PageMap> {
21-
let file = File::open("/proc/self/pagemap").ok()?;
102+
let file = PROCESS_PAGEMAP.as_ref()?;
103+
22104
// Check if the `pagemap_scan` ioctl is supported.
23105
let mut regions = vec![MaybeUninit::uninit(); 1];
24106
let pm_scan = PageMapScanBuilder::new(ptr::slice_from_raw_parts(ptr::null_mut(), 0))
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
use wasmtime::Result;
2+
3+
fn main() -> Result<()> {
4+
#[cfg(unix)]
5+
if true {
6+
use libtest_mimic::{Arguments, Trial};
7+
8+
let mut trials = Vec::new();
9+
for (name, test) in linux::TESTS {
10+
trials.push(Trial::test(*name, || {
11+
test().map_err(|e| format!("{e:?}").into())
12+
}));
13+
}
14+
15+
let mut args = Arguments::from_args();
16+
// I'll be honest, I'm scared of threads + fork, so I'm just
17+
// preemptively disabling threads here.
18+
args.test_threads = Some(1);
19+
libtest_mimic::run(&args, trials).exit()
20+
}
21+
22+
Ok(())
23+
}
24+
25+
mod linux {
26+
use rustix::fd::AsRawFd;
27+
use rustix::process::{Pid, WaitOptions, waitpid};
28+
use std::io::{self, BufRead, BufReader};
29+
use wasmtime::*;
30+
31+
pub const TESTS: &[(&str, fn() -> Result<()>)] = &[
32+
("smoke", smoke),
33+
("pooling_allocator_reset", pooling_allocator_reset),
34+
];
35+
36+
fn smoke() -> Result<()> {
37+
let engine = Engine::default();
38+
let module = Module::new(&engine, r#"(module (func (export "")))"#)?;
39+
run_in_child(|| {
40+
let mut store = Store::new(&engine, ());
41+
let instance = Instance::new(&mut store, &module, &[])?;
42+
let export = instance.get_typed_func::<(), ()>(&mut store, "")?;
43+
export.call(&mut store, ())?;
44+
Ok(())
45+
})?;
46+
Ok(())
47+
}
48+
49+
fn pooling_allocator_reset() -> Result<()> {
50+
let mut pooling = PoolingAllocationConfig::new();
51+
pooling.linear_memory_keep_resident(4096);
52+
let mut config = Config::new();
53+
config.allocation_strategy(pooling);
54+
let engine = Engine::new(&config)?;
55+
let module = Module::new(
56+
&engine,
57+
r#"
58+
(module
59+
(memory (export "") 1 1)
60+
(data (i32.const 0) "\0a")
61+
)
62+
"#,
63+
)?;
64+
65+
let assert_pristine = || {
66+
let mut store = Store::new(&engine, ());
67+
let instance = Instance::new(&mut store, &module, &[])?;
68+
let memory = instance.get_memory(&mut store, "").unwrap();
69+
let data = memory.data(&store);
70+
assert_eq!(data[0], 0x0a);
71+
anyhow::Ok((store, memory))
72+
};
73+
run_in_child(|| {
74+
// Allocate a memory, and then mutate it.
75+
let (mut store, memory) = assert_pristine()?;
76+
let data = memory.data_mut(&mut store);
77+
data[0] = 0;
78+
drop(store);
79+
80+
// Allocating the memory again should reuse the same pooling
81+
// allocator slot but it should be reset correctly.
82+
assert_pristine()?;
83+
assert_pristine()?;
84+
Ok(())
85+
})?;
86+
Ok(())
87+
}
88+
89+
fn run_in_child(closure: impl FnOnce() -> Result<()>) -> Result<()> {
90+
let (read, write) = io::pipe()?;
91+
let child = match unsafe { libc::fork() } {
92+
-1 => return Err(io::Error::last_os_error().into()),
93+
94+
0 => {
95+
// If a panic happens, don't let it go above this stack frame.
96+
let _bomb = Bomb;
97+
98+
drop(read);
99+
unsafe {
100+
assert!(libc::dup2(write.as_raw_fd(), 1) == 1);
101+
assert!(libc::dup2(write.as_raw_fd(), 2) == 2);
102+
}
103+
drop(write);
104+
105+
closure().unwrap();
106+
std::process::exit(0);
107+
}
108+
109+
pid => pid,
110+
};
111+
112+
drop(write);
113+
114+
for line in BufReader::new(read).lines() {
115+
println!("CHILD: {}", line?);
116+
}
117+
118+
let (_pid, status) =
119+
waitpid(Some(Pid::from_raw(child).unwrap()), WaitOptions::empty())?.unwrap();
120+
assert_eq!(status.as_raw(), 0);
121+
122+
Ok(())
123+
}
124+
125+
struct Bomb;
126+
127+
impl Drop for Bomb {
128+
fn drop(&mut self) {
129+
std::process::exit(1);
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)