Skip to content

fix: run Claude Code where Android refuses its syscall - #378

Merged
rmyndharis merged 5 commits into
mainfrom
fix/claude-code-sigsys-message
Aug 24, 2026
Merged

fix: run Claude Code where Android refuses its syscall#378
rmyndharis merged 5 commits into
mainfrom
fix/claude-code-sigsys-message

Conversation

@rmyndharis

@rmyndharis rmyndharis commented Aug 24, 2026

Copy link
Copy Markdown
Owner

The Claude Code sign-in panel reported Claude Code process terminated by signal SIGSYS on Android 13 and 14, and a terminal printed Bad system call. Neither
says what happened, and both read as if VSCodroid broke something.

Root cause

An Android app may make only the system calls bionic exposes in SYSCALLS.TXT,
and epoll_pwait2 (441 on aarch64) appears there in android15: it is absent
from android13-release and android14-release. The CLI's runtime calls it as
soon as its event loop starts, so the process was stopped there. That is why
claude --version printed and exited happily while anything that ran the loop
died.

Established with a ptrace tracer run inside the app (it has to live in
nativeLibraryDir, since SELinux refuses execve under filesDir):

TRACER: SIGSYS in tid 21248 -- si_syscall=441 x8=441

Each alternative was measured and ruled out, not argued away: the extension
build from four months earlier fails identically; both emulators run a
byte-identical APK; DNS and HTTPS resolve on the failing device; targetSdk has
been 36 since 1.1.0; and the failure reproduces with the CLI sandbox both on and
off.

The fix

Android refuses with SECCOMP_RET_TRAP rather than a kill, so the refusal
arrives as a catchable SIGSYS: the syscall does not run, and a handler that
returns resumes the thread with whatever it left in x0. That is enough to
answer the call.

  • scripts/seccomp-shim.c builds libseccomp-shim.so, preloaded into the CLI.
    It emulates epoll_pwait2 with epoll_pwait, rounding the timespec up to the
    next millisecond so a sub-millisecond wait does not become a spin. Any other
    refused call is left to die as the platform intended: the handler restores
    SIG_DFL and returns.
  • It also interposes sigaction. The CLI installs a SIGSYS handler of its own
    that re-raises through kill(); without the interposition the emulation
    answers three calls and the fourth is fatal. Every other signal is passed
    through with the layout translated in both directions, since musl's
    struct sigaction is not the kernel's.
  • scripts/claude-launch.c builds libclaude-launch.so, which is what
    claudeCode.claudeProcessWrapper now names. It appends the shim to
    LD_PRELOAD and execs musl's loader. Setting LD_PRELOAD globally would put
    the shim in front of Bionic children too, which would read its musl-shaped
    sigaction translation and corrupt their own.
  • The shim links with --pack-dyn-relocs=none. Targeting API 30 or newer, lld
    emits relative relocations in Android's packed form (DT_ANDROID_RELR), which
    musl 1.2.5 does not implement: the constructor pointer in .init_array keeps
    its link-time address and the CLI dies with SIGSEGV before running a line. The
    build fails on either that or a NEEDED entry, which would put a second libc
    in a musl process.
  • Both files are requiredJniLibs, so an APK cannot ship without them, and
    build-all.sh, build.yml and release.yml build them alongside the other
    native pieces.

docs/USER_GUIDE.md now says the platform limit is handled and what to report if
a future CLI reaches for a call the shim does not cover. The claude wrapper
keeps its 159 check for the same case.

Verification

Measured on an API 33 emulator with an API 37 control, through the shipped
launcher rather than a hand-built one:

before after
claude --version killed, signal 31 2.1.240 (Claude Code), status 0
stream-json session killed, signal 31 full init JSON, then its own auth prompt
sign-in panel terminated by signal SIGSYS signs in
SIGSYS in the log on every attempt none

API 37 is unchanged: the syscall is allowed there, so the handler never fires.

2227 JVM tests, 0 failures; lint clean; the punctuation, document-date,
build-step, workflow-step, translatable-string, bundled-extension, binary-list,
checklist and instrumented-inventory gates and the nine JavaScript self-checks
all exit 0.

Sources: bionic SYSCALLS.TXT on the android13, android14 and android15
branches; asm-generic/unistd.h for the syscall number.

The Claude Code CLI asks for epoll_pwait2. Android refuses system calls an
app is not on the list for, and a refusal there is a kill rather than the
ENOSYS a runtime could fall back from, so the program dies the moment its
event loop starts. The sign-in panel reports "Claude Code process
terminated by signal SIGSYS" and a terminal prints "Bad system call",
neither of which tells the user what happened or that nothing here can
change it.

Measured with a ptrace tracer against both emulators, same VSCodroid build
(byte-identical APK) and same extension build on each: the refused call is
441, epoll_pwait2. It fails on Android 13 and runs on Android 17. An
extension release four months older fails identically on Android 13, so
this is the platform and not the extension version, and targetSdk has not
moved since 1.1.0.

The wrapper now reads the 159 the shell reports for a SIGSYS kill and says
what was refused; the user guide records which versions were measured and
that the filter is Android's to set, not ours.
…e call

bionic's SYSCALLS.TXT gains epoll_pwait2 in android15 and does not carry it
on the android13 or android14 branches, so the boundary is Android 15 rather
than the open range the first version of this message described. The guide
and the wrapper now say that instead of naming two measured points and
leaving the middle unstated.
An app may make only the system calls bionic exposes in SYSCALLS.TXT, and
epoll_pwait2 (441) is there from android15: absent on the android13 and
android14 branches. The Claude Code CLI's runtime calls it as soon as its
event loop starts, so on Android 13 and 14 the process was stopped there.
The sign-in panel reported "Claude Code process terminated by signal
SIGSYS" and a terminal printed "Bad system call", neither of which says
what happened or that nothing on the device could change it.

Android refuses with SECCOMP_RET_TRAP rather than a kill, so the refusal
arrives as a catchable SIGSYS and can be answered.

- libseccomp-shim.so is preloaded into the CLI and emulates that one call
  with epoll_pwait, rounding the timespec up so a sub-millisecond wait
  does not become a spin. Anything else refused is left to die as the
  platform intended.
- It also interposes sigaction, because the CLI installs a SIGSYS handler
  of its own that re-raises through kill(): without this the emulation
  answers three calls and the fourth is fatal.
- libclaude-launch.so is what claudeCode.claudeProcessWrapper now names.
  It puts the shim into LD_PRELOAD and execs musl's loader, rather than
  setting LD_PRELOAD globally, where a Bionic child would read the shim's
  musl-shaped sigaction translation and corrupt its own.
- The shim links with --pack-dyn-relocs=none. Targeting API 30 or newer,
  lld emits DT_ANDROID_RELR, which musl 1.2.5 ignores, leaving its
  constructor pointer unrelocated and the CLI dead with SIGSEGV before it
  runs a line. Both that and any NEEDED entry fail the build.

Verified on an API 33 emulator against an API 37 control, through the
shipped launcher: claude --version exits 0 and a stream-json session
reaches its own auth prompt, where both died with signal 31, and sign-in
completes. No SIGSYS is left in the log.
@rmyndharis rmyndharis changed the title fix: say why Claude Code dies where the platform refuses its syscall fix: run Claude Code where Android refuses its syscall Aug 24, 2026
Both are compiled from this repository's own source, so they carry no
third-party obligation, but the attribution check knows only what it is
told and refuses anything it cannot classify. The seccomp shim links
against no library at all, which is what leaves it nothing to attribute
beyond the root LICENSE.
@rmyndharis
rmyndharis merged commit 82d393c into main Aug 24, 2026
4 checks passed
@rmyndharis
rmyndharis deleted the fix/claude-code-sigsys-message branch August 24, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant