Skip to content

Android x86_64: SIGSYS on startup — epoll_wait is blocked by seccomp #81

Description

@philipreese

Summary

Any app using this package crashes on startup on Android/x86_64 — i.e. any standard emulator — before Flutter renders a frame:

F libc  : Fatal signal 31 (SIGSYS), code 1 (SYS_SECCOMP), syscall 232
F DEBUG : Cause: seccomp prevented call to disallowed x86_64 system call 232

Syscall 232 on x86_64 is epoll_wait, which Android's seccomp filter denies for app processes. epollReactorPoller.Wait calls it via unix.EpollWait (go/reactor_linux.go:113). Because Tailscale.init() starts the Go runtime, the reactor comes up before any UI does, so the process dies immediately with no actionable error.

Reproduced on 0.7.1; main has the same call site.

Why this hasn't shown up on devices

golang.org/x/sys/unix maps EpollWait onto a different syscall per architecture:

// syscall_linux_amd64.go:9
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)

// syscall_linux_arm64.go:11
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) = SYS_EPOLL_PWAIT

arm64 Linux has no epoll_wait syscall at all, so on arm64 unix.EpollWait is already epoll_pwait — which seccomp permits. Real arm64 devices therefore work fine and only x86_64 hits the blocked syscall, so the failure reproduces on emulators and most CI runners while physical devices stay green.

Suggested fix

x/sys exports no EpollPwait wrapper (only EpollCreate, EpollCtl, EpollWait), so the syscall has to be issued directly. This mirrors the call form x/sys itself generates for arm64 verbatim — the trailing 0, 0 is a nil sigmask and zero sigsetsize, which the kernel ignores entirely when the mask is NULL, making it semantically identical to EpollWait:

// epollPwait issues epoll_pwait rather than epoll_wait.
//
// unix.EpollWait maps to SYS_EPOLL_WAIT on linux/amd64, which Android's seccomp filter denies
// for app processes: an x86_64 emulator is killed with SIGSYS (syscall 232) before any UI
// renders. arm64 Linux has no epoll_wait syscall at all, so x/sys already maps EpollWait to
// SYS_EPOLL_PWAIT there (syscall_linux_arm64.go), which is why real arm64 devices are
// unaffected and this went unnoticed.
//
// x/sys exports no EpollPwait wrapper, so this mirrors the call form it generates for arm64
// verbatim. A nil sigmask (the trailing 0, 0) makes it semantically identical to EpollWait.
func epollPwait(epfd int, events []unix.EpollEvent, msec int) (int, error) {
	var zero byte
	p := unsafe.Pointer(&zero)
	if len(events) > 0 {
		p = unsafe.Pointer(&events[0])
	}
	r0, _, e1 := unix.Syscall6(unix.SYS_EPOLL_PWAIT, uintptr(epfd), uintptr(p), uintptr(len(events)), uintptr(msec), 0, 0)
	if e1 != 0 {
		return int(r0), e1
	}
	return int(r0), nil
}

Then in Wait, plus adding "unsafe" to the imports:

-	n, err := unix.EpollWait(p.epfd, events, timeoutMillis)
+	n, err := epollPwait(p.epfd, events, timeoutMillis)

SYS_EPOLL_PWAIT is defined on both amd64 and arm64, so this stays a single arch-independent code path rather than a build-tagged fork. Error semantics are unchanged — e1 is a syscall.Errno, so the existing err == unix.EINTR check still matches.

Verification

Controlled A/B on one emulator (google_apis, x86_64, API 35), same toolchain, same build, only this change differing:

Unpatched Patched
Process after launch dead alive
SIGSYS in logcat 2 0
Evidence seccomp prevented call to disallowed x86_64 system call 232 Impeller rendering backend, Dart VM service listening

Also: gofmt clean, and typechecks under GOOS=linux for both GOARCH=amd64 and GOARCH=arm64.

What I did not run: your full tool/test_pr_gate.sh. I'm on a Windows host, where go test ./... can't build the package (platform files are excluded) and the Headscale E2E needs Docker I don't have running — so I filed this rather than send a PR I couldn't put through your own gate. Happy to open one if you'd prefer, or if the fix looks right it's small enough to take directly.

Repro

  1. avdmanager create avd -n test -k "system-images;android-35;google_apis;x86_64"
  2. Build and install any app calling Tailscale.init()
  3. Launch — the process dies with the SIGSYS above

Thanks for the package — the embedded-tsnet approach is exactly what we needed, and this was the only thing standing between us and emulator/CI coverage.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions