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.

flowchart LR JSON["Target JSON policy"] --> Compiler["seccompiler-bin"] Compiler --> Blob["Bitcode-serialized BPF map"] Blob --> Binary["Embedded in Firecracker"] Binary --> API["api filter"] Binary --> VCPU["vcpu filter per vCPU"] Binary --> VMM["vmm filter"]

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:

  1. Deserialize the JSON into a BTreeMap<String, Filter>. Ordered keys keep the serialized output reproducible.
  2. Create a libseccomp context with the filter's default action and target architecture.
  3. Resolve syscall names and add either an unconditional syscall rule or a rule with libseccomp argument comparators.
  4. Export each classic BPF program through a memfd as a Vec<u64>.
  5. 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:

  1. prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
  2. 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