crates/heimdall-wasm/tests/no_memfd.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! The wasm host needs no `memfd_create`: Heimdall's own container seccomp
//! profile refuses it, and a guest run inside a HeimdallRunner job must still
//! load and run (Holger's split verify-bundle guest trapped with "cannot
//! create a memfd"). Heimdall is a defensive sandbox; this test filters only
//! its own child process.
//!
//! The parent test re-runs this test binary as a child with
//! `HEIMDALL_NO_MEMFD_CHILD=1`; the child installs a seccomp filter on every
//! thread that makes `memfd_create` fail with EPERM, proves the filter is on,
//! and runs a guest with data segments (the case that wasmtime's
//! copy-on-write memory images back with a memfd).
mod common;
use common::*;
use heimdall_wasm::{RunLimits, Verdict};
const CHILD: &str = "HEIMDALL_NO_MEMFD_CHILD";
#[cfg(target_arch = "x86_64")]
const AUDIT_ARCH: u32 = 0xC000_003E; // AUDIT_ARCH_X86_64
#[cfg(target_arch = "aarch64")]
const AUDIT_ARCH: u32 = 0xC000_00B7; // AUDIT_ARCH_AARCH64
/// seccomp: `memfd_create` fails with EPERM, every other call is allowed;
/// a foreign arch is killed. Applied to every thread (TSYNC).
fn refuse_memfd_create() {
use libc::{sock_filter, sock_fprog};
const LD_W_ABS: u16 = 0x20; // BPF_LD | BPF_W | BPF_ABS
const JEQ_K: u16 = 0x15; // BPF_JMP | BPF_JEQ | BPF_K
const RET_K: u16 = 0x06; // BPF_RET | BPF_K
let f = |code, jt, jf, k| sock_filter { code, jt, jf, k };
let prog = [
f(LD_W_ABS, 0, 0, 4), // seccomp_data.arch
f(JEQ_K, 1, 0, AUDIT_ARCH),
f(RET_K, 0, 0, libc::SECCOMP_RET_KILL_PROCESS),
f(LD_W_ABS, 0, 0, 0), // seccomp_data.nr
f(JEQ_K, 0, 1, libc::SYS_memfd_create as u32),
f(RET_K, 0, 0, libc::SECCOMP_RET_ERRNO | libc::EPERM as u32),
f(RET_K, 0, 0, libc::SECCOMP_RET_ALLOW),
];
let fprog = sock_fprog {
len: prog.len() as u16,
filter: prog.as_ptr() as *mut sock_filter,
};
// SAFETY: plain prctl/seccomp calls with a valid, live filter program.
unsafe {
assert_eq!(libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
let r = libc::syscall(
libc::SYS_seccomp,
libc::SECCOMP_SET_MODE_FILTER,
libc::SECCOMP_FILTER_FLAG_TSYNC,
&fprog as *const sock_fprog,
);
assert_eq!(r, 0, "seccomp: {}", std::io::Error::last_os_error());
// The filter is on: memfd_create now fails with EPERM.
let name = b"probe\0";
let fd = libc::syscall(libc::SYS_memfd_create, name.as_ptr(), 0u32);
assert_eq!(fd, -1);
assert_eq!(
std::io::Error::last_os_error().raw_os_error(),
Some(libc::EPERM)
);
}
}
#[test]
fn a_guest_with_data_segments_loads_and_runs_when_memfd_create_is_refused() {
if std::env::var_os(CHILD).is_some() {
refuse_memfd_create();
let kit = Kit::new("no-memfd");
let g = kit.guest("good", &good_wat());
// Twice: the second instance would reuse a memory image if one existed.
for _ in 0..2 {
let o = g.run(b"hello bundle", &none(), &RunLimits::default());
assert_eq!(
o.verdict,
Verdict::Accepted {
note: "accepted".into()
},
"{o:?}"
);
assert_eq!(o.files[0].bytes, b"hello bundle");
}
return;
}
let out = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"a_guest_with_data_segments_loads_and_runs_when_memfd_create_is_refused",
"--nocapture",
"--test-threads=1",
])
.env(CHILD, "1")
.output()
.expect("the child test runs");
let text = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(out.status.success(), "child failed:\n{text}");
assert!(
text.contains("1 passed"),
"the child did not run the test:\n{text}"
);
}