feat(runtime): seccomp blocklist filter via optional seccomp feature

Add seccomp feature flag (seccompiler + libc, Linux-only, optional).

When compiled with --features seccomp, weft-runtime installs a
SECCOMP_MODE_FILTER immediately after argument parsing, before any
package resolution or WASM execution.

Filter strategy: default-allow with explicit KillProcess rules for
high-risk syscalls a WASM runtime process has no legitimate need for:
ptrace, process_vm_readv/writev, kexec_load, personality, syslog,
reboot, mount/umount2, setuid/setgid/setreuid/setregid/setresuid/
setresgid, chroot, pivot_root, init_module/finit_module/delete_module,
bpf, perf_event_open, acct.

The feature is off by default so the standard build and tests are
unaffected. Enable in production service builds with --features seccomp.
This commit is contained in:
Marco Allegretti 2026-03-11 15:34:21 +01:00
parent ec4cc272af
commit 98a21da734
2 changed files with 59 additions and 0 deletions

View file

@ -11,6 +11,7 @@ path = "src/main.rs"
[features]
default = []
wasmtime-runtime = ["dep:wasmtime", "dep:wasmtime-wasi", "dep:cap-std"]
seccomp = ["dep:seccompiler", "dep:libc"]
[dependencies]
anyhow = "1.0"
@ -19,3 +20,5 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
wasmtime = { version = "30", optional = true }
wasmtime-wasi = { version = "30", optional = true }
cap-std = { version = "3", optional = true }
seccompiler = { version = "0.4", optional = true }
libc = { version = "0.2", optional = true }

View file

@ -50,6 +50,9 @@ fn main() -> anyhow::Result<()> {
i += 1;
}
#[cfg(feature = "seccomp")]
apply_seccomp_filter().context("apply seccomp filter")?;
tracing::info!(session_id, %app_id, "weft-runtime starting");
let pkg_dir = resolve_package(app_id)?;
@ -168,6 +171,59 @@ fn run_module(
.map_err(|()| anyhow::anyhow!("wasm component run exited with error"))
}
#[cfg(feature = "seccomp")]
fn apply_seccomp_filter() -> anyhow::Result<()> {
use seccompiler::{BpfProgram, SeccompAction, SeccompFilter, SeccompRule};
use std::collections::BTreeMap;
use std::convert::TryInto;
#[cfg(target_arch = "x86_64")]
let arch = seccompiler::TargetArch::x86_64;
#[cfg(target_arch = "aarch64")]
let arch = seccompiler::TargetArch::aarch64;
let blocked: &[i64] = &[
libc::SYS_ptrace,
libc::SYS_process_vm_readv,
libc::SYS_process_vm_writev,
libc::SYS_kexec_load,
libc::SYS_personality,
libc::SYS_syslog,
libc::SYS_reboot,
libc::SYS_mount,
libc::SYS_umount2,
libc::SYS_setuid,
libc::SYS_setgid,
libc::SYS_setreuid,
libc::SYS_setregid,
libc::SYS_setresuid,
libc::SYS_setresgid,
libc::SYS_chroot,
libc::SYS_pivot_root,
libc::SYS_init_module,
libc::SYS_finit_module,
libc::SYS_delete_module,
libc::SYS_bpf,
libc::SYS_perf_event_open,
libc::SYS_acct,
];
let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();
for &syscall in blocked {
rules.insert(syscall, vec![SeccompRule::new(vec![])?]);
}
let filter = SeccompFilter::new(
rules,
SeccompAction::Allow,
SeccompAction::KillProcess,
arch,
)?;
let bpf: BpfProgram = filter.try_into()?;
seccompiler::apply_filter(&bpf)?;
Ok(())
}
fn package_store_roots() -> Vec<PathBuf> {
if let Ok(explicit) = std::env::var("WEFT_APP_STORE") {
return vec![PathBuf::from(explicit)];