The sandbox that killed my emulator: why I run Cuttlefish with --enable_sandbox=false

· aospcuttlefishcrosvmseccompdebugginglinux


Last time I got the Cuttlefish host packages to build on Ubuntu 26.04. This time the device wouldn’t boot on the same machine — and the failure mode is worth writing down, because it’s not really about Cuttlefish. It’s about what happens to any seccomp-sandboxed program when the kernel and libc underneath it are newer than the sandbox’s allowlist. Everything below is against android-15.0.0_r36 on Ubuntu 26.04, kernel 7.0.0-generic, glibc 2.42.

The symptom: a launch that lies

First launch, by the book:

launch_cvd --daemon

No errors. Host processes running. And adb devices: empty. Nothing to attach to, nothing in logcat, no complaint anywhere I looked at first.

Trying again:

Instance directory files in use. Try `cvd reset`?
Observed PIDs: 9277, 9280, 9281, ...

That second error is actually the first useful clue: those PIDs were the previous launch, still alive. run_cvd, the modem simulator, the adb connector, log tees — the whole host-side entourage was up and holding the instance directory. The launcher considered the launch a success. Something more specific than “it didn’t work” was going on.

Narrowing it down

The Cuttlefish host stack is a crowd of cooperating processes, so “is it running?” is the wrong question — the right one is “which layer is dead?” Three checks, each one level deeper:

1. Is anything listening where adb should connect? Cuttlefish exposes the guest’s adbd on TCP 6520 via a proxy:

ss -ltnp | grep 6520
# nothing

The adb server was running; the device port had no listener. So the guest side of that proxy never came up.

2. What does the launcher log say? ~/cuttlefish/instances/cvd-1/logs/launcher.log was an infinite loop of two processes politely failing:

webRTC ... vsock_connection.cpp:222] Failed to connect: No such device
adb_connector ... transport message failed, response body: device '0.0.0.0:6520' not found

vsock is the host↔guest socket family — “No such device” from a vsock connect means there is no guest. The host services weren’t broken; they were waiting, forever, for a VM that didn’t exist.

3. Did the VM ever boot? The definitive check:

wc -l ~/cuttlefish/instances/cvd-1/logs/kernel.log
# 0

Zero bytes of kernel output. The guest never executed a single instruction. And pgrep crosvm — the actual hypervisor process — came back empty, while everything around it lived on.

The actual error, one second in

Grepping the launcher log for crosvm’s own lines found the death certificate, timestamped one second after launch:

ERROR crosvm] exiting with error 1: the architecture failed to build the vm
    failed to create a PCI root hub: failed to create proxy device:
    Failed to configure tube: failed to receive packet:
    Connection reset by peer (os error 104)

“Proxy device” is the tell. crosvm runs in multiprocess mode: each virtual device (block, net, serial, rng, …) is forked into its own child process, and each child is locked into a minijail sandbox with a seccomp policy — a per-device allowlist of syscalls, shipped as .policy files next to the crosvm binary:

out/host/linux-x86/usr/share/crosvm/x86_64-linux-gnu/seccomp/
├── block_device.policy
├── net_device.policy
├── serial_device.policy
└── ...

Under seccomp’s default-deny, a syscall that isn’t on the list kills the process with SIGSYS. No stderr, no exception, no exit message — the child just stops existing. The parent’s next read on the control socket gets ECONNRESET, which it can only report as “Connection reset by peer”. That’s why the error message describes the consequence (a dead pipe) and not the cause (a forbidden syscall).

And why were syscalls forbidden? Because the allowlist is frozen and my machine is not. Those policy files were written for the kernels and glibc versions Android’s build infrastructure ran when android-15.0.0_r36 was cut. glibc 2.42 on kernel 7.0 happily uses syscalls that are newer than the policy — modern glibc aggressively adopts things like fresh clone3/futex variants and new *at2-style entry points as the kernel grows them. The program didn’t change; the floor under it did. First device child makes an unlisted syscall during setup, SIGSYS, dead, and the VM build aborts.

I couldn’t even catch the SIGSYS directly — audit logging was disabled on the box, so the kill is invisible in the journal too. The diagnosis rests on the shape of the failure: multiprocess sandbox + instant child death + ECONNRESET at proxy-device creation + a distro years newer than the toolchain. Every piece points the same way.

The fix, and why it’s the right one for a dev box

launch_cvd --daemon --enable_sandbox=false

Cuttlefish passes --disable-sandbox down to crosvm; device processes run unjailed; the VM builds; the device boots and shows up as 0.0.0.0:6520.

Now the opinionated part: on a developer machine running a bleeding-edge kernel, this sandbox is not worth keeping. Not because sandboxing is bad — because this sandbox’s threat model and failure mode are both wrong for this machine:

The general rule I’m taking from this: pinned userspace + rolling kernel = seccomp time bomb. AOSP is the extreme case — its whole philosophy is hermetic, pinned, prebuilt everything — but the same applies to any older sandboxed binary you carry onto a new distro: browsers’ zygotes, container runtimes with hand-rolled seccomp profiles, gVisor-ish shims. If a multiprocess program dies at startup with a broken-pipe-flavored error and its children use seccomp, suspect the allowlist before you suspect the code.

What I’m taking away