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
avdmanager create avd -n test -k "system-images;android-35;google_apis;x86_64"
- Build and install any app calling
Tailscale.init()
- 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.
Summary
Any app using this package crashes on startup on Android/x86_64 — i.e. any standard emulator — before Flutter renders a frame:
Syscall 232 on x86_64 is
epoll_wait, which Android's seccomp filter denies for app processes.epollReactorPoller.Waitcalls it viaunix.EpollWait(go/reactor_linux.go:113). BecauseTailscale.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;
mainhas the same call site.Why this hasn't shown up on devices
golang.org/x/sys/unixmapsEpollWaitonto a different syscall per architecture:arm64 Linux has no
epoll_waitsyscall at all, so on arm64unix.EpollWaitis alreadyepoll_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/sysexports noEpollPwaitwrapper (onlyEpollCreate,EpollCtl,EpollWait), so the syscall has to be issued directly. This mirrors the call formx/sysitself generates for arm64 verbatim — the trailing0, 0is a nil sigmask and zero sigsetsize, which the kernel ignores entirely when the mask is NULL, making it semantically identical toEpollWait:Then in
Wait, plus adding"unsafe"to the imports:SYS_EPOLL_PWAITis 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 —e1is asyscall.Errno, so the existingerr == unix.EINTRcheck still matches.Verification
Controlled A/B on one emulator (
google_apis, x86_64, API 35), same toolchain, same build, only this change differing:SIGSYSin logcatseccomp prevented call to disallowed x86_64 system call 232Impeller rendering backend,Dart VM service listeningAlso:
gofmtclean, and typechecks underGOOS=linuxfor bothGOARCH=amd64andGOARCH=arm64.What I did not run: your full
tool/test_pr_gate.sh. I'm on a Windows host, wherego 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
avdmanager create avd -n test -k "system-images;android-35;google_apis;x86_64"Tailscale.init()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.