Chapter 19: Seccomp In Firecracker
A guest normally reaches the host kernel through KVM exits handled by Firecracker. If a KVM or VMM flaw instead gives the guest control of a host thread, that thread can try to enter the host kernel directly. Firecracker reduces that syscall surface with a different seccomp-BPF program for each thread category.
Seccomp is a limit, not a second hypervisor. A compromised thread still shares Firecracker's address space and existing file descriptors with the rest of the process. An allowed syscall can still contain a kernel bug, and a thread may be able to corrupt shared process state. The filter removes syscall operations that the thread's normal job does not require; it does not make arbitrary code execution harmless.
The containerd book covers the general seccomp mechanism, including
struct seccomp_data, classic BPF evaluation, action precedence, and OCI
profiles. This chapter stays with Firecracker's host-side choices: three policy
categories, argument checks, installation timing, and failure behavior.
Three Policy Categories
Firecracker's filter map must contain exactly these keys:
| Key | Normal user | Installation point |
|---|---|---|
api |
API server thread | In ApiServer::run, before start_server |
vcpu |
Every vCPU thread | In KvmVcpu::run, before the vCPU state machine |
vmm |
Main VMM thread | After the builder returns and before the event loop |
These are categories, not a claim that the process has only three threads.
There may be several vCPU threads, and a configuration run with --no-api has
no API thread. Threads created after a filter is installed inherit their
creator's seccomp state.
The timing preserves one important invariant: the relevant filters are active
before guest code runs. A vCPU thread registers its kick-signal handler and
waits at the startup barrier, then KvmVcpu::run calls apply_filter before
entering its state machine. The VMM applies its own filter after microVM
construction and before it begins processing guest-facing events. The API
socket is created before the API thread starts, but the API thread applies its
filter before it starts serving requests.
An installation failure is fatal. API and vCPU call sites panic; the VMM path returns an error and does not enter the event loop. There is no fallback from a requested filter to unrestricted execution.
Building The Default Policy
Release builds for supported musl targets select
resources/seccomp/<target-triple>.json. Firecracker currently carries
policies for x86_64-unknown-linux-musl and
aarch64-unknown-linux-musl. Its build script invokes the workspace's
seccompiler library and embeds the resulting seccomp_filter.bpf from
Cargo's output directory.
The JSON root maps each thread category to a filter:
{
"vcpu": {
"default_action": "trap",
"filter_action": "allow",
"filter": [
{ "syscall": "write" },
{
"syscall": "ioctl",
"args": [
{
"index": 1,
"type": "dword",
"op": "eq",
"val": 44672,
"comment": "KVM_RUN"
}
]
}
]
}
}
All three production filters use trap for the default action and allow for
a matching rule. A rule without args admits that syscall regardless of its
arguments. Conditions within one rule are ANDed; separate rules are
alternatives. The compiler accepts argument indices 0 through 5, 32-bit
dword and 64-bit qword values, and equality, ordering, inequality, and
masked-equality operators. Numeric syscall arguments are policy ABI: the
comments name constants for readers, but the compiler ignores them.
The compiler performs these steps:
- Deserialize the JSON into a
BTreeMap<String, Filter>. Ordered keys keep the serialized output reproducible. - Create a libseccomp context with the filter's default action and target architecture.
- Resolve syscall names and add either an unconditional syscall rule or a rule with libseccomp argument comparators.
- Export each classic BPF program through a
memfdas aVec<u64>. - Serialize the category-to-program map with
bitcode.
The compiler changed from Firecracker's in-house BPF backend to libseccomp in v1.11.0. That detail matters to builders: compilation links to the host's libseccomp, while the Firecracker process consumes already-compiled BPF and does not invoke libseccomp at runtime.
Argument Checks
The most consequential rules constrain ioctl. A name-only ioctl rule would
admit every request the target file descriptor understands. Firecracker instead
uses separate rules that compare argument 1, the request number, with specific
KVM, TUN/TAP, terminal, or nonblocking-I/O constants.
dword equality has a special implementation. Syscall arguments occupy
64-bit slots in seccomp_data, but a C argument such as an ioctl request is
only 32 bits. Its unused upper register bits need not be zero. The compiler
therefore turns dword equality into libseccomp SCMP_CMP_MASKED_EQ with mask
0x00000000ffffffff. A qword equality compares all 64 bits.
Other calls also receive argument checks. In the current x86-64 policy, for
example, the vCPU filter admits only selected futex operations; restricts
madvise to MADV_DONTNEED; admits two expected mmap forms while excluding
PROT_EXEC; and permits tkill only for SIGABRT. The API filter restricts
socket to a close-on-exec Unix stream socket and accept4 to
SOCK_CLOEXEC.
Argument filtering still has limits. Most checks do not bind an operation to a
particular file descriptor, and many calls, including open, read, write,
and some message operations, are name-only in one or more policies. The jail's
filesystem and inherited-descriptor set therefore remain part of the security
boundary.
The Current X86-64 Allowlists
The policy changes as Firecracker changes. At Firecracker commit
fb53569b807c8e1349e4d2b49d5300e463a91d73, checked on 2026-07-10, the
x86-64 musl policy contains:
| Category | Unique syscall names | ioctl rules |
|---|---|---|
vmm |
50 | 14 |
api |
31 | 1 |
vcpu |
27 | 18 |
Those counts are a source snapshot, not an interface guarantee. They are useful
because they correct an easy misconception: the vCPU policy is narrow, but it
is not only KVM_RUN, timers, and futexes. It currently includes name-only
open, io_uring_enter, and sendmsg, plus selected mmap, futex, signal,
and ioctl forms. It does not include socket, connect, clone, execve,
or mount.
The 18 x86-64 vCPU ioctl rules include KVM_RUN, register and interrupt
state getters used by pause and snapshot paths, KVM_GET_TSC_KHZ,
KVM_KVMCLOCK_CTRL, KVM_CHECK_EXTENSION for KVM_CAP_MSI_DEVID,
KVM_SET_GSI_ROUTING, KVM_IRQFD, and TUNSETOFFLOAD. The request number is
checked; the file descriptor generally is not. The aarch64 policy has the same
27 unique syscall names but a different, shorter architecture-specific ioctl
set, including KVM_GET_ONE_REG and KVM_GET_REG_LIST.
The VMM policy is broader because the main event loop owns device I/O,
snapshot work, memory mappings, and runtime reconfiguration. Its current list
includes all three io_uring setup and operation calls, Unix-socket calls, and
selected KVM and TUN/TAP ioctls. The API policy has Unix-socket and HTTP-server
operations but only the FIONBIO ioctl rule.
Installing A Program
vmm::seccomp::apply_filter receives one program as a slice of u64. Each
classic BPF instruction is eight bytes; u64 supplies sufficient size and
alignment for the sock_fprog pointer passed to the kernel.
The function first accepts an empty slice as an explicit no-op. Otherwise it
rejects more than 4,096 instructions, converts the length to u16, and makes
two host calls:
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)seccomp(SECCOMP_SET_MODE_FILTER, 0, &sock_fprog)
The zero flags are significant. Firecracker does not request
SECCOMP_FILTER_FLAG_TSYNC; each thread installs the program appropriate to
its own category. It also does not request a userspace notification listener
or kernel logging flag.
When no rule matches a production allowlist, SECCOMP_RET_TRAP delivers
SIGSYS. Firecracker's registered handler verifies SYS_SECCOMP, records the
seccomp.num_faults metric, logs the offending syscall number, writes metrics,
and terminates the process with its BadSyscall exit code via _exit. This is
an abrupt process exit, not a graceful guest shutdown.
Custom And Empty Policies
--seccomp-filter <path> replaces the embedded map with a file previously
produced by seccompiler-bin. Deserialization reads at most 100,000 bytes,
lowercases category names, rejects unknown categories, and requires all three
expected categories. The per-program 4,096-instruction check still occurs at
installation.
seccompiler-bin also retains two specialist switches. --split-output
writes raw <thread>.bpf files for tests. The deprecated --basic switch is
still accepted; it discards every argument condition and emits name-only rules.
That weakens the policy and should not be mistaken for a compatibility-safe
conversion.
Debug builds and targets without a matching policy file use
resources/seccomp/unimplemented.json, whose empty rule lists have
default_action: allow. They therefore receive no syscall restriction from
the default policy. --no-seccomp is different in implementation: it supplies
three empty BPF vectors, so apply_filter returns before setting
no_new_privs or calling seccomp. Firecracker documents both situations as
unsuitable for production.
Two Kernels, Two Layers
Firecracker's filters govern syscalls made by the VMM process to the host kernel. If the guest runs containers, an OCI runtime may independently install seccomp filters on container processes inside the guest. The guest kernel evaluates those filters; the host kernel evaluates Firecracker's filters. A guest syscall does not become the same-numbered host syscall.
The Jailer complements the host-side filter. Its filesystem view, uid/gid transition, resource limits, optional cgroups, and optional namespaces restrict what the process can reach and consume. Seccomp restricts which kernel entry points each Firecracker thread can invoke. Neither control repairs a KVM flaw, and neither removes the need to minimize inherited descriptors and allowed operations. Together they reduce different parts of the post-escape surface.
Sources And Further Reading
- Research note for this chapter
- Firecracker seccomp documentation
- Firecracker seccompiler documentation
- Default x86-64 musl policy
- Filter loading and category validation
apply_filter- Seccompiler implementation
- Seccompiler types and argument conversion
- Firecracker signal handlers
- Kernel seccomp filter documentation
seccomp(2)