From d4798642858c1ee4ef38b6920f9630bf61b24b8e Mon Sep 17 00:00:00 2001 From: Pierre Bertholom Date: Thu, 13 Aug 2026 15:51:22 +0100 Subject: [PATCH 1/4] seccompiler: truncate memfd before each BPF export Reset the shared memfd before each export so a shorter filter cannot retain instructions from the previous filter. Signed-off-by: Pierre Bertholom --- src/seccompiler/src/lib.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/seccompiler/src/lib.rs b/src/seccompiler/src/lib.rs index be003df7fe6..cd96d17e08c 100644 --- a/src/seccompiler/src/lib.rs +++ b/src/seccompiler/src/lib.rs @@ -47,6 +47,8 @@ pub enum CompilationError { MemfdCreate(std::io::Error), /// Cannot rewind memfd: {0} MemfdRewind(std::io::Error), + /// Cannot truncate memfd: {0} + MemfdTruncate(std::io::Error), /// Cannot read from memfd: {0} MemfdRead(std::io::Error), /// Cannot create output file: {0} @@ -158,7 +160,12 @@ pub fn compile_bpf( } } - // SAFETY: Safe as all args are correect. + // `seccomp_export_bpf` does not truncate the output file. + // Reset the memfd so a shorter filter cannot retain instructions from a previous export. + memfd.set_len(0).map_err(CompilationError::MemfdTruncate)?; + memfd.rewind().map_err(CompilationError::MemfdRewind)?; + + // SAFETY: Safe as all args are correct. unsafe { if seccomp_export_bpf(bpf_filter, memfd.as_raw_fd()) != 0 { return Err(CompilationError::LibSeccompExport); From eb14a0306be63e48339facadaa8652d67cf35c87 Mon Sep 17 00:00:00 2001 From: Pierre Bertholom Date: Wed, 29 Jul 2026 11:16:54 +0100 Subject: [PATCH 2/4] test: fix x86 regs for seccomp static analysis Resolve x86 syscall numbers written through partial registers while preserving unaffected bits during backpropagation. Inspect seccomp rules from all thread categories. Signed-off-by: Pierre Bertholom --- tests/framework/static_analysis.py | 59 ++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/tests/framework/static_analysis.py b/tests/framework/static_analysis.py index 6df131b4f8c..5ccd4e0b507 100644 --- a/tests/framework/static_analysis.py +++ b/tests/framework/static_analysis.py @@ -103,6 +103,8 @@ def backpropagate_register( if reg not in affected_registers: return reg + partial_reg = ArchitectureX86_64.partial_reg(reg) + match self.mnemonic: case "mov": if len(self.args) != 2: @@ -113,10 +115,18 @@ def backpropagate_register( if dst == reg: # an immediate load if src.startswith("$"): - return int(src[3:], 16) + value = int(src[3:], 16) + if partial_reg is not None: + full_reg, mask = partial_reg + return full_reg, lambda previous: ( + (previous & ~mask) | (value & mask) + ) + return value # We moved something into our target register. If it's a new register, we understand # what's going on. Anything else, and tough luck if re.match(r"^%\w{2,4}$", src): + if partial_reg is not None: + raise UnsupportedInstructionError(self, reg) return src raise UnsupportedInstructionError(self, reg) return reg @@ -126,6 +136,9 @@ def backpropagate_register( if src == dst: # we know that reg is part of the arguments, and we know that the arguments are identical # Thus we have xor reg,reg, which is effectively zeroing reg + if partial_reg is not None: + full_reg, mask = partial_reg + return full_reg, lambda previous: previous & ~mask return 0 case "push": # a push doesn't do anything @@ -292,21 +305,33 @@ class ArchitectureX86_64( # pylint: disable=invalid-name syscall_argument_registers = ["%rdi", "%rsi", "%rdx", "%r10", "%r8", "%r9"] fn_call_argument_registers = ["%rdi", "%rsi", "%rdx", "%rcx", "%r8", "%r9"] seccomp_arch = seccomp.Arch.X86_64 + REG_GROUPS = [ + ("%rax", "%eax", "%ax", "%al"), + ("%rbx", "%ebx", "%bx", "%bl"), + ("%rcx", "%ecx", "%cx", "%cl"), + ("%rdx", "%edx", "%dx", "%dl"), + ("%rsi", "%esi", "%si", "%sil"), + ("%rdi", "%edi", "%di", "%dil"), + ("%rbp", "%ebp", "%bp", "%bpl"), + ("%rsp", "%esp", "%sp", "%spl"), + ] + [(f"%r{i}", f"%r{i}d", f"%r{i}w", f"%r{i}b") for i in range(8, 16)] + REG_ALIASES = {alias: group for group in REG_GROUPS for alias in group} @staticmethod - def generalize_reg(reg: str) -> list[str]: - suffixes = ["ax", "bx", "cx", "dx", "si", "di", "bp", "sp"] - prefixes = ["%r8", "%r9", "%r10", "%r11", "%r12", "%r13", "%r14", "%r15"] - - for suffix in suffixes: - if reg.endswith(suffix): - return [f"%r{suffix}", f"%e{suffix}", f"%{suffix}"] + def partial_reg(reg: str) -> tuple[str, int] | None: + """Return the full register and mask for a partial register.""" + group = ArchitectureX86_64.REG_ALIASES.get(reg) + if group is None: + return None + if reg == group[2]: + return group[0], 0xFFFF + if reg == group[3]: + return group[0], 0xFF + return None - for prefix in prefixes: - if reg.startswith(prefix): - return [prefix, f"{prefix}d", f"{prefix}w"] - - return [reg] + @staticmethod + def generalize_reg(reg: str) -> list[str]: + return list(ArchitectureX86_64.REG_ALIASES.get(reg, (reg,))) class ArchitectureAarch64(Architecture[InstructionAarch64]): @@ -573,9 +598,11 @@ def load_seccomp_rules(seccomp_path: Path): For 'masked_eq' comparisons, mask is the bitmask value.""" filters = json.loads(seccomp_path.read_text("utf-8")) - all_filters = ( - filters["vcpu"]["filter"] + filters["vmm"]["filter"] + filters["api"]["filter"] - ) + all_filters = [ + seccomp_filter + for thread_filter in filters.values() + for seccomp_filter in thread_filter["filter"] + ] allowlist = defaultdict(list) for seccomp_filter in all_filters: From 5af510446da6a522379c58ec6f72cc2012228d46 Mon Sep 17 00:00:00 2001 From: Pierre Bertholom Date: Thu, 13 Aug 2026 15:51:51 +0100 Subject: [PATCH 3/4] seccomp: add block worker thread filter Add an optional seccomp category and a syscall allowlist for block workers. Keep existing custom filters valid when no block worker is needed. Apply the filter before entering the worker event loop. Signed-off-by: Pierre Bertholom --- .../seccomp/aarch64-unknown-linux-musl.json | 298 ++++++++++++++++++ resources/seccomp/unimplemented.json | 5 + .../seccomp/x86_64-unknown-linux-musl.json | 297 +++++++++++++++++ src/firecracker/src/seccomp.rs | 25 +- .../src/devices/virtio/block/virtio/device.rs | 14 +- .../src/devices/virtio/block/virtio/worker.rs | 13 +- src/vmm/src/seccomp.rs | 1 + 7 files changed, 642 insertions(+), 11 deletions(-) diff --git a/resources/seccomp/aarch64-unknown-linux-musl.json b/resources/seccomp/aarch64-unknown-linux-musl.json index 5a58fd2ccef..a27489f8cf4 100644 --- a/resources/seccomp/aarch64-unknown-linux-musl.json +++ b/resources/seccomp/aarch64-unknown-linux-musl.json @@ -7,6 +7,10 @@ "syscall": "newfstatat", "comment": "Used when creating snapshots in vmm:persist::snapshot_memory_to_file through std::fs::File::metadata" }, + { + "syscall": "epoll_create1", + "comment": "Used by the threaded block worker's EventManager::new(), which runs on the spawned worker thread under the inherited vmm filter before it applies its own blk_worker filter" + }, { "syscall": "epoll_ctl" }, @@ -392,6 +396,28 @@ } ] }, + { + "syscall": "mmap", + "comment": "Used to allocate the threaded block worker's thread stack (spawned from the VMM thread, so checked against this inherited filter)", + "args": [ + { + "index": 3, + "type": "dword", + "op": "eq", + "val": 131106, + "comment": "libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_STACK" + }, + { + "index": 2, + "type": "dword", + "op": { + "masked_eq": 4 + }, + "val": 0, + "comment": "Ensure PROT_EXEC is not set" + } + ] + }, { "syscall": "memfd_create", "comment": "Used by IovDeque ring buffer for net device hotplug", @@ -418,6 +444,19 @@ } ] }, + { + "syscall": "fcntl", + "comment": "Used by EventFd::try_clone() when spawning the threaded block worker (control_evt/queue_evts duplication on the VMM thread)", + "args": [ + { + "index": 1, + "type": "dword", + "op": "eq", + "val": 1030, + "comment": "F_DUPFD_CLOEXEC" + } + ] + }, { "syscall": "rt_sigaction", "comment": "rt_sigaction is used by libc::abort during a panic to install the default handler for SIGABRT", @@ -712,6 +751,70 @@ "comment": "Ensure PROT_EXEC is not set" } ] + }, + { + "syscall": "clone", + "comment": "Used by musl pthread_create to spawn the block worker thread", + "args": [ + { + "index": 0, + "type": "dword", + "op": {"masked_eq": 4290772991}, + "val": 4001536, + "comment": "Require the pthread clone flags and allow only the optional obsolete CLONE_DETACHED bit" + } + ] + }, + { + "syscall": "prctl", + "comment": "Used by musl to set the block worker thread name", + "args": [ + { + "index": 0, + "type": "dword", + "op": "eq", + "val": 15, + "comment": "PR_SET_NAME" + } + ] + }, + { + "syscall": "prctl", + "comment": "Used to set no_new_privs before applying the block worker seccomp filter", + "args": [ + { + "index": 0, + "type": "dword", + "op": "eq", + "val": 38, + "comment": "PR_SET_NO_NEW_PRIVS" + }, + { + "index": 1, + "type": "dword", + "op": "eq", + "val": 1 + } + ] + }, + { + "syscall": "seccomp", + "comment": "Used to apply the block worker seccomp filter", + "args": [ + { + "index": 0, + "type": "dword", + "op": "eq", + "val": 1, + "comment": "SECCOMP_SET_MODE_FILTER" + }, + { + "index": 1, + "type": "dword", + "op": "eq", + "val": 0 + } + ] } ] }, @@ -1349,5 +1452,200 @@ ] } ] + }, + "blk_worker": { + "default_action": "trap", + "filter_action": "allow", + "filter": [ + { + "syscall": "epoll_ctl" + }, + { + "syscall": "epoll_pwait" + }, + { + "syscall": "lseek", + "comment": "SyncFileEngine seeks to the request offset before each read/write" + }, + { + "syscall": "futex", + "comment": "Locking the Arc> data path" + }, + { + "syscall": "mmap", + "comment": "Allocator growth during I/O", + "args": [ + { + "index": 3, + "type": "dword", + "op": "eq", + "val": 34, + "comment": "libc::MAP_ANONYMOUS | libc::MAP_PRIVATE" + }, + { + "index": 2, + "type": "dword", + "op": {"masked_eq": 4}, + "val": 0, + "comment": "Ensure PROT_EXEC is not set" + } + ] + }, + { + "syscall": "mmap", + "comment": "Used by io_uring for mapping the queues during drive patch", + "args": [ + { + "index": 3, + "type": "dword", + "op": "eq", + "val": 32769, + "comment": "libc::MAP_SHARED | libc::MAP_POPULATE" + }, + { + "index": 2, + "type": "dword", + "op": {"masked_eq": 4}, + "val": 0, + "comment": "Ensure PROT_EXEC is not set" + } + ] + }, + { + "syscall": "mmap", + "comment": "Used for reading the timezone in LocalTime::now()", + "args": [ + { + "index": 3, + "type": "dword", + "op": "eq", + "val": 1, + "comment": "libc::MAP_SHARED" + }, + { + "index": 2, + "type": "dword", + "op": {"masked_eq": 4}, + "val": 0, + "comment": "Ensure PROT_EXEC is not set" + } + ] + }, + { + "syscall": "munmap" + }, + { + "syscall": "sigaltstack" + }, + { + "syscall": "rt_sigprocmask" + }, + { + "syscall": "rt_sigaction", + "comment": "rt_sigaction is used by libc::abort during a panic to install the default handler for SIGABRT", + "args": [ + { + "index": 0, + "type": "dword", + "op": "eq", + "val": 6, + "comment": "SIGABRT" + } + ] + }, + { + "syscall": "exit", + "comment": "Worker thread exit on Finish" + }, + { + "syscall": "exit_group", + "comment": "Used by the fatal signal handlers (vmm::signal_handler::exit_with_code) to terminate the process" + }, + { + "syscall": "tkill", + "comment": "tkill is used by libc::abort during a panic to raise SIGABRT", + "args": [ + { + "index": 1, + "type": "dword", + "op": "eq", + "val": 6, + "comment": "SIGABRT" + } + ] + }, + { + "syscall": "read" + }, + { + "syscall": "write" + }, + { + "syscall": "fsync" + }, + { + "syscall": "openat", + "comment": "Used to open the replacement backing file during drive patch and rescan" + }, + { + "syscall": "close" + }, + { + "syscall": "io_uring_enter", + "comment": "Used for submitting io_uring requests" + }, + { + "syscall": "io_uring_setup", + "comment": "Rebuilding the io_uring ring on the worker thread during drive patch (update_file)" + }, + { + "syscall": "io_uring_register", + "comment": "Registering fixed fds/opcode restrictions when rebuilding the ring on drive patch" + }, + { + "syscall": "gettid", + "comment": "Rust std uses it during panic to print the thread id." + }, + { + "syscall": "clock_gettime", + "comment": "Used for metrics and logging, via the helpers in utils/src/time.rs. It's not called on some platforms, because of vdso optimisations." + }, + { + "syscall": "timerfd_settime", + "comment": "Needed for rate limiting and metrics", + "args": [ + { + "index": 1, + "type": "dword", + "op": "eq", + "val": 0 + } + ] + }, + { + "syscall": "fcntl", + "comment": "Used by snapshotting, drive patching and rescanning", + "args": [ + { + "index": 1, + "type": "dword", + "op": "eq", + "val": 2, + "comment": "FCNTL_F_SETFD" + }, + { + "index": 2, + "type": "dword", + "op": "eq", + "val": 1, + "comment": "FCNTL_FD_CLOEXEC" + } + ] + }, + { + "syscall": "fstat", + "comment": "Used by disk update" + } + ] } } diff --git a/resources/seccomp/unimplemented.json b/resources/seccomp/unimplemented.json index a919df15519..e6cbe49383c 100644 --- a/resources/seccomp/unimplemented.json +++ b/resources/seccomp/unimplemented.json @@ -13,5 +13,10 @@ "default_action": "allow", "filter_action": "trap", "filter": [] + }, + "blk_worker": { + "default_action": "allow", + "filter_action": "trap", + "filter": [] } } diff --git a/resources/seccomp/x86_64-unknown-linux-musl.json b/resources/seccomp/x86_64-unknown-linux-musl.json index 35fae0db21b..715d4ebd516 100644 --- a/resources/seccomp/x86_64-unknown-linux-musl.json +++ b/resources/seccomp/x86_64-unknown-linux-musl.json @@ -7,6 +7,10 @@ "syscall": "stat", "comment": "Used when creating snapshots in vmm:persist::snapshot_memory_to_file through std::fs::File::metadata" }, + { + "syscall": "epoll_create1", + "comment": "Used by the threaded block worker's EventManager::new(), which runs on the spawned worker thread under the inherited vmm filter before it applies its own blk_worker filter" + }, { "syscall": "epoll_ctl" }, @@ -392,6 +396,28 @@ } ] }, + { + "syscall": "mmap", + "comment": "Used to allocate the threaded block worker's thread stack (spawned from the VMM thread, so checked against this inherited filter)", + "args": [ + { + "index": 3, + "type": "dword", + "op": "eq", + "val": 131106, + "comment": "libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_STACK" + }, + { + "index": 2, + "type": "dword", + "op": { + "masked_eq": 4 + }, + "val": 0, + "comment": "Ensure PROT_EXEC is not set" + } + ] + }, { "syscall": "memfd_create", "comment": "Used by IovDeque ring buffer for net device hotplug", @@ -418,6 +444,19 @@ } ] }, + { + "syscall": "fcntl", + "comment": "Used by EventFd::try_clone() when spawning the threaded block worker (control_evt/queue_evts duplication on the VMM thread)", + "args": [ + { + "index": 1, + "type": "dword", + "op": "eq", + "val": 1030, + "comment": "F_DUPFD_CLOEXEC" + } + ] + }, { "syscall": "rt_sigaction", "comment": "rt_sigaction is used by libc::abort during a panic to install the default handler for SIGABRT", @@ -724,6 +763,70 @@ "comment": "Ensure PROT_EXEC is not set" } ] + }, + { + "syscall": "clone", + "comment": "Used by musl pthread_create to spawn the block worker thread", + "args": [ + { + "index": 0, + "type": "dword", + "op": {"masked_eq": 4290772991}, + "val": 4001536, + "comment": "Require the pthread clone flags and allow only the optional obsolete CLONE_DETACHED bit" + } + ] + }, + { + "syscall": "prctl", + "comment": "Used by musl to set the block worker thread name", + "args": [ + { + "index": 0, + "type": "dword", + "op": "eq", + "val": 15, + "comment": "PR_SET_NAME" + } + ] + }, + { + "syscall": "prctl", + "comment": "Used to set no_new_privs before applying the block worker seccomp filter", + "args": [ + { + "index": 0, + "type": "dword", + "op": "eq", + "val": 38, + "comment": "PR_SET_NO_NEW_PRIVS" + }, + { + "index": 1, + "type": "dword", + "op": "eq", + "val": 1 + } + ] + }, + { + "syscall": "seccomp", + "comment": "Used to apply the block worker seccomp filter", + "args": [ + { + "index": 0, + "type": "dword", + "op": "eq", + "val": 1, + "comment": "SECCOMP_SET_MODE_FILTER" + }, + { + "index": 1, + "type": "dword", + "op": "eq", + "val": 0 + } + ] } ] }, @@ -1481,5 +1584,199 @@ ] } ] + }, + "blk_worker": { + "default_action": "trap", + "filter_action": "allow", + "filter": [ + { + "syscall": "epoll_ctl" + }, + { + "syscall": "lseek", + "comment": "SyncFileEngine seeks to the request offset before each read/write" + }, + { + "syscall": "futex", + "comment": "Locking the Arc> data path" + }, + { + "syscall": "mmap", + "comment": "Allocator growth during I/O", + "args": [ + { + "index": 3, + "type": "dword", + "op": "eq", + "val": 34, + "comment": "libc::MAP_ANONYMOUS | libc::MAP_PRIVATE" + }, + { + "index": 2, + "type": "dword", + "op": {"masked_eq": 4}, + "val": 0, + "comment": "Ensure PROT_EXEC is not set" + } + ] + }, + { + "syscall": "mmap", + "comment": "Used by io_uring for mapping the queues during drive patch", + "args": [ + { + "index": 3, + "type": "dword", + "op": "eq", + "val": 32769, + "comment": "libc::MAP_SHARED | libc::MAP_POPULATE" + }, + { + "index": 2, + "type": "dword", + "op": {"masked_eq": 4}, + "val": 0, + "comment": "Ensure PROT_EXEC is not set" + } + ] + }, + { + "syscall": "mmap", + "comment": "Used for reading the timezone in LocalTime::now()", + "args": [ + { + "index": 3, + "type": "dword", + "op": "eq", + "val": 1, + "comment": "libc::MAP_SHARED" + }, + { + "index": 2, + "type": "dword", + "op": {"masked_eq": 4}, + "val": 0, + "comment": "Ensure PROT_EXEC is not set" + } + ] + }, + { + "syscall": "munmap" + }, + { + "syscall": "sigaltstack" + }, + { + "syscall": "rt_sigprocmask" + }, + { + "syscall": "rt_sigaction", + "comment": "rt_sigaction is used by libc::abort during a panic to install the default handler for SIGABRT", + "args": [ + { + "index": 0, + "type": "dword", + "op": "eq", + "val": 6, + "comment": "SIGABRT" + } + ] + }, + { + "syscall": "exit", + "comment": "Worker thread exit on Finish" + }, + { + "syscall": "exit_group", + "comment": "Used by the fatal signal handlers (vmm::signal_handler::exit_with_code) to terminate the process" + }, + { + "syscall": "tkill", + "comment": "tkill is used by libc::abort during a panic to raise SIGABRT", + "args": [ + { + "index": 1, + "type": "dword", + "op": "eq", + "val": 6, + "comment": "SIGABRT" + } + ] + }, + { + "syscall": "epoll_pwait" + }, + { + "syscall": "read" + }, + { + "syscall": "write" + }, + { + "syscall": "fsync" + }, + { + "syscall": "open" + }, + { + "syscall": "close" + }, + { + "syscall": "io_uring_enter", + "comment": "Used for submitting io_uring requests" + }, + { + "syscall": "io_uring_setup", + "comment": "Rebuilding the io_uring ring on the worker thread during drive patch (update_file)" + }, + { + "syscall": "io_uring_register", + "comment": "Registering fixed fds/opcode restrictions when rebuilding the ring on drive patch" + }, + { + "syscall": "gettid", + "comment": "Rust std uses it during panic to print the thread id." + }, + { + "syscall": "clock_gettime", + "comment": "Used for metrics and logging, via the helpers in utils/src/time.rs. It's not called on some platforms, because of vdso optimisations." + }, + { + "syscall": "timerfd_settime", + "comment": "Needed for rate limiting and metrics", + "args": [ + { + "index": 1, + "type": "dword", + "op": "eq", + "val": 0 + } + ] + }, + { + "syscall": "fcntl", + "comment": "Used by snapshotting, drive patching and rescanning", + "args": [ + { + "index": 1, + "type": "dword", + "op": "eq", + "val": 2, + "comment": "FCNTL_F_SETFD" + }, + { + "index": 2, + "type": "dword", + "op": "eq", + "val": 1, + "comment": "FCNTL_FD_CLOEXEC" + } + ] + }, + { + "syscall": "fstat", + "comment": "Used by disk update" + } + ] } } diff --git a/src/firecracker/src/seccomp.rs b/src/firecracker/src/seccomp.rs index 421220a7b5f..4fae368fff8 100644 --- a/src/firecracker/src/seccomp.rs +++ b/src/firecracker/src/seccomp.rs @@ -7,7 +7,8 @@ use std::path::Path; use vmm::seccomp::{BpfThreadMap, DeserializationError, deserialize_binary, get_empty_filters}; -const THREAD_CATEGORIES: [&str; 3] = ["vmm", "api", "vcpu"]; +const ALLOWED_THREAD_CATEGORIES: [&str; 4] = ["vmm", "api", "vcpu", "blk_worker"]; +const MANDATORY_THREAD_CATEGORIES: [&str; 3] = ["vmm", "api", "vcpu"]; /// Error retrieving seccomp filters. #[derive(Debug, thiserror::Error, displaydoc::Display)] @@ -76,11 +77,12 @@ fn get_custom_filters(reader: R) -> Result Result { let (filters, invalid_filters): (BpfThreadMap, BpfThreadMap) = map .into_iter() - .partition(|(k, _)| THREAD_CATEGORIES.contains(&k.as_str())); + .partition(|(k, _)| ALLOWED_THREAD_CATEGORIES.contains(&k.as_str())); if !invalid_filters.is_empty() { // build the error message let mut thread_categories_string = @@ -95,7 +97,7 @@ fn filter_thread_categories(map: BpfThreadMap) -> Result Result<(), VirtioBlockError> { + pub(crate) fn spawn_worker( + &mut self, + seccomp_filter: Arc, + ) -> Result<(), VirtioBlockError> { if let BlockState::Configuring(resources, worker_handle @ None) = &mut self.state { let queue_evts = resources .queue_evts @@ -513,8 +517,10 @@ impl VirtioBlock { let name = format!("fc_{}", self.config.drive_id); - *worker_handle = - Some(WorkerHandle::spawn(queue_evts, name).map_err(VirtioBlockError::ThreadSpawn)?); + *worker_handle = Some( + WorkerHandle::spawn(seccomp_filter, queue_evts, name) + .map_err(VirtioBlockError::ThreadSpawn)?, + ); } Ok(()) } @@ -1968,7 +1974,7 @@ mod tests { for threaded in [false, true] { let mut block = default_block(engine); if threaded { - block.spawn_worker().unwrap(); + block.spawn_worker(Arc::new(vec![])).unwrap(); } let mem = default_mem(); diff --git a/src/vmm/src/devices/virtio/block/virtio/worker.rs b/src/vmm/src/devices/virtio/block/virtio/worker.rs index ff1552982c6..507782602d4 100644 --- a/src/vmm/src/devices/virtio/block/virtio/worker.rs +++ b/src/vmm/src/devices/virtio/block/virtio/worker.rs @@ -20,6 +20,7 @@ use crate::devices::virtio::queue::{InvalidAvailIdx, QueueError}; use crate::devices::virtio::transport::VirtioInterruptType; use crate::logger::{IncMetric, error, warn}; use crate::rate_limiter::RateLimiter; +use crate::seccomp::{BpfProgram, apply_filter}; use crate::snapshot::Persist; /// Runtime state and processing logic for an active block device. @@ -329,7 +330,11 @@ impl BlockWorker { impl WorkerHandle { /// Spawn a parked block worker thread. - pub(crate) fn spawn(queue_evts: Vec, name: String) -> Result { + pub(crate) fn spawn( + seccomp_filter: Arc, + queue_evts: Vec, + name: String, + ) -> Result { // handle writes and worker reads the control eventfd let control_evt = EventFd::new(libc::EFD_NONBLOCK)?; let handle_evt = control_evt.try_clone()?; @@ -338,8 +343,14 @@ impl WorkerHandle { let (to_vmm, from_worker) = channel::(); let join = thread::Builder::new().name(name).spawn(move || { + // Create epoll before applying the filter, which does not allow epoll_create1. let event_manager = EventManager::new().expect("Failed to create block worker EventManager"); + + if let Err(err) = apply_filter(&seccomp_filter) { + panic!("Failed to apply seccomp filter on block worker: {err}"); + } + run_worker_loop(event_manager, control_evt, from_vmm, to_vmm); })?; diff --git a/src/vmm/src/seccomp.rs b/src/vmm/src/seccomp.rs index 82e30fa9289..6732f24430a 100644 --- a/src/vmm/src/seccomp.rs +++ b/src/vmm/src/seccomp.rs @@ -42,6 +42,7 @@ pub fn get_empty_filters() -> BpfThreadMap { map.insert("vmm".to_string(), Arc::new(vec![])); map.insert("api".to_string(), Arc::new(vec![])); map.insert("vcpu".to_string(), Arc::new(vec![])); + map.insert("blk_worker".to_string(), Arc::new(vec![])); map } From e3a30b2875b4cd5230a3a605f055c04077dd2824 Mon Sep 17 00:00:00 2001 From: Pierre Bertholom Date: Thu, 13 Aug 2026 15:52:06 +0100 Subject: [PATCH 4/4] docs: document seccomp thread categories Document the allowed and mandatory Firecracker thread categories. Explain when custom filters need the optional block worker category. Signed-off-by: Pierre Bertholom --- docs/seccompiler.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/seccompiler.md b/docs/seccompiler.md index 50f44097cec..81899b39975 100644 --- a/docs/seccompiler.md +++ b/docs/seccompiler.md @@ -77,8 +77,11 @@ This means that Firecracker has a JSON file for each supported target (currently determined by the arch-libc combinations). You can view them in `resources/seccomp`. -At the top level, the file requires an object that maps thread categories (vmm, -api and vcpu) to seccomp filters: +At the top level, the file contains an object that maps thread categories to +seccomp filters. Firecracker allows the `vmm`, `api`, `vcpu`, and `blk_worker` +categories. The `vmm`, `api`, and `vcpu` categories are mandatory. The +`blk_worker` category is optional and is needed only when a block device uses a +dedicated worker thread. ``` { @@ -91,6 +94,7 @@ api and vcpu) to seccomp filters: }, "api": {...}, "vcpu": {...}, + "blk_worker": {...} } ```