Restore original sysnum in unmodifiable-sysnum workaround#348
Open
Ebola-Chan-bot wants to merge 2 commits intotermux:masterfrom
Open
Restore original sysnum in unmodifiable-sysnum workaround#348Ebola-Chan-bot wants to merge 2 commits intotermux:masterfrom
Ebola-Chan-bot wants to merge 2 commits intotermux:masterfrom
Conversation
When push_specific_regs fails because the kernel lacks the NT_ARM_SYSTEM_CALL regset (so NT_PRSTATUS push carrying x8=-1/PR_void is rejected with EINVAL), proot falls back to poking all six syscall argument registers to -1 and re-pushes NT_PRSTATUS. Previously x8 was kept at -1 through this second push, so the kernel saw an illegal sysnum and rejected the call with ENOSYS; the real error code was then written to x0 at sysexit. On some kernels this does not work: x8=-1 on restart triggers a non-standard signal delivery path that synthesizes a SIGSEGV and kills the tracee before it executes a single user-mode instruction. Restore x8 to the original sysnum before re-pushing, and keep poking all six argument registers to -1. The kernel then actually runs the original syscall, but with all arguments -1 it fails naturally inside the kernel (EFAULT/EBADF/EINVAL) without side effects, and proot overrides the returned x0 at sysexit with the real error code. The PR_brk special-case (SYSARG_1=0) is also removed. It was a defensive path against a non-compliant kernel mishandling brk(-1); with x8 now explicitly restored and POSIX brk(addr) returning the current brk without mutation when addr is out of range, the extra poke is unnecessary.
There was a problem hiding this comment.
Pull request overview
Updates the “unmodifiable syscall-number” fallback logic so that when the kernel refuses syscall-number modification, PRoot restores the original syscall number before re-pushing registers while still poisoning the syscall arguments to force an in-kernel failure, and removes the brk-specific argument override.
Changes:
- Restore original syscall number before re-pushing registers in the sysnum-unmodifiable workaround path.
- Keep poisoning all six syscall argument registers to
-1to induce a natural kernel-side failure. - Remove the
brk()special-case that forcedSYSARG_1=0.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Ebola-Chan-bot
pushed a commit
to Ebola-Chan-bot/proot
that referenced
this pull request
Apr 18, 2026
…ability Two follow-ups to the unmodifiable-sysnum workaround: 1) Rewrite the workaround comment in src/syscall/syscall.c to reflect the real failure point. The previous wording blamed the NT_PRSTATUS push carrying x8=-1, but push_specific_regs() bails out earlier: on arm64 it returns as soon as PTRACE_SETREGSET(NT_ARM_SYSTEM_CALL) returns EINVAL, so the general-register push is never even attempted. The comment now describes the syscall-number regset / register in architecture-agnostic terms (x8/x0 references kept only as concrete arm64 examples inside the explanation) and records why a "known-unsafe syscall" guard proposed by the reviewer is not added: (a) the legacy "keep sysnum=PR_void" path is strictly worse on affected kernels because it causes SIGSEGV and kills the tracee; (b) poisoning all six arg registers to -1 already traps the vast majority of side-effectful syscalls at the kernel's parameter-validation stage; (c) we have no empirically grounded list of syscalls that both reach this suppression path and cause harmful side effects when invoked with poisoned args, so a speculative allow/deny list would be dead code. sysexit still overrides the return-value register with the real error code. 2) Cache NT_ARM_SYSTEM_CALL availability in src/tracee/reg.c. The regset is a kernel-global capability: once PTRACE_SETREGSET rejects it with EINVAL, it will never succeed under the same running kernel. A process-local static bool short-circuits subsequent requests so the caller hits its workaround path immediately, without paying the cost of a guaranteed-failing ptrace call on every intercepted syscall. Only EINVAL is cached; ESRCH/EPERM/ EFAULT are per-tracee state errors and are not treated as kernel- capability signals. The cache is memory-only and not persisted, so a new proot run always re-probes — this keeps behaviour correct across reboots, kernel upgrades, or chroots into different kernels, at the cost of a single failing ptrace per proot start on affected kernels.
…ability Two follow-ups to the unmodifiable-sysnum workaround: 1) Rewrite the workaround comment in src/syscall/syscall.c to reflect the real failure point. The previous wording blamed the NT_PRSTATUS push carrying x8=-1, but push_specific_regs() bails out earlier: on arm64 it returns as soon as PTRACE_SETREGSET(NT_ARM_SYSTEM_CALL) returns EINVAL, so the general-register push is never even attempted. The comment now describes the syscall-number regset / register in architecture-agnostic terms (x8/x0 references kept only as concrete arm64 examples inside the explanation) and records why a "known-unsafe syscall" guard proposed by the reviewer is not added: (a) the legacy "keep sysnum=PR_void" path is strictly worse on affected kernels because it causes SIGSEGV and kills the tracee; (b) poisoning all six arg registers to -1 already traps the vast majority of side-effectful syscalls at the kernel's parameter-validation stage; (c) we have no empirically grounded list of syscalls that both reach this suppression path and cause harmful side effects when invoked with poisoned args, so a speculative allow/deny list would be dead code. sysexit still overrides the return-value register with the real error code. 2) Cache NT_ARM_SYSTEM_CALL availability in src/tracee/reg.c. The regset is a kernel-global capability: once PTRACE_SETREGSET rejects it with EINVAL, it will never succeed under the same running kernel. A process-local static bool short-circuits subsequent requests so the caller hits its workaround path immediately, without paying the cost of a guaranteed-failing ptrace call on every intercepted syscall. Only EINVAL is cached; ESRCH/EPERM/ EFAULT are per-tracee state errors and are not treated as kernel- capability signals. The cache is memory-only and not persisted, so a new proot run always re-probes — this keeps behaviour correct across reboots, kernel upgrades, or chroots into different kernels, at the cost of a single failing ptrace per proot start on affected kernels.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes tracee death (SIGSEGV) on kernels that both (a) reject modification of the syscall-number register via ptrace and (b) treat a restart with the syscall-number register set to
PR_void(-1on arm64, i.e.x8 = -1) as a fatal non-standard signal-delivery event. Also avoids a guaranteed-failing ptrace call on every intercepted syscall under the same kernel.Background
When PRoot decides to suppress a syscall (turn it into a no-op whose return value it will later override at sysexit), it rewrites the syscall-number register to
PR_void. On arm64 this requiresPTRACE_SETREGSET(..., NT_ARM_SYSTEM_CALL, ...)because the regular general-register push does not carry the syscall number. Some kernels — notably certain vendor-modified arm64 kernels — do not accept writes toNT_ARM_SYSTEM_CALLand returnEINVAL. PRoot detects this and falls back to a workaround that still tries to make the syscall fail inside the kernel by poisoning its argument registers, then relies on sysexit to substitute the real error code.The legacy workaround kept the syscall-number register at
PR_void(-1) and poked all six arg registers to-1, expecting the kernel to simply reject the illegal syscall number withENOSYS. On affected kernels this expectation is wrong: restarting with the syscall-number register set to-1triggers a non-standard signal-delivery path that synthesizes aSIGSEGVand kills the tracee before it executes a single user-mode instruction.Changes
src/syscall/syscall.c— unmodifiable-sysnum workaroundNet behaviour change:
PR_void), the kernel actually runs the rejected syscall. Combined with every argument register set to-1, the overwhelming majority of side-effectful syscalls fail inside the kernel at the parameter-validation stage (EBADF/EFAULT/EINVAL). sysexit still overrides the return-value register with the real error code PRoot intended, so upper-layer semantics are preserved.PR_brkspecial case that forcedSYSARG_1 = 0. This was a defensive workaround against a non-compliant kernel mishandlingbrk(-1). With the syscall number now explicitly restored and POSIXbrk(addr)returning the current break without mutation whenaddris out of range, the extra arg-0 poke is unnecessary and was removed to keep the fallback path uniform across syscalls.src/tracee/reg.c— cacheNT_ARM_SYSTEM_CALLavailabilityNT_ARM_SYSTEM_CALLacceptance is a kernel-global property. Once the running kernel rejects it withEINVAL, it will never succeed for the lifetime of that kernel. Without a cache, every intercepted syscall that needs sysnum modification pays the cost of a guaranteed-failingptracecall before hitting the workaround.Implementation:
static bool sysnum_regset_unavailableinsidepush_specific_regs()records the capability.PTRACE_SETREGSET(NT_ARM_SYSTEM_CALL)returningEINVALsets the flag. Subsequent calls that would require sysnum modification short-circuit witherrno = EINVAL; return -1;, so the existing caller (syscall.c:translate_syscall) hits its workaround path immediately.EINVALis cached.ESRCH/EPERM/EFAULTare per-tracee state errors (tracee exited, wrong ptrace state, bad buffer) and are not treated as kernel-capability signals. Caching them would incorrectly disable the regset for unaffected tracees.prootrun always re-probes. This keeps behaviour correct across reboots, kernel upgrades, or chroots into different kernels, at the cost of one failingptraceperprootstart on affected kernels — negligible compared to proot's baseline ptrace volume.