Skip to content

Restore original sysnum in unmodifiable-sysnum workaround#348

Open
Ebola-Chan-bot wants to merge 2 commits intotermux:masterfrom
Ebola-Chan-bot:fix-unmodifiable-sysnum-workaround-x8-restore
Open

Restore original sysnum in unmodifiable-sysnum workaround#348
Ebola-Chan-bot wants to merge 2 commits intotermux:masterfrom
Ebola-Chan-bot:fix-unmodifiable-sysnum-workaround-x8-restore

Conversation

@Ebola-Chan-bot
Copy link
Copy Markdown

@Ebola-Chan-bot Ebola-Chan-bot commented Apr 17, 2026

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 (-1 on 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 requires PTRACE_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 to NT_ARM_SYSTEM_CALL and return EINVAL. 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 with ENOSYS. On affected kernels this expectation is wrong: restarting with the syscall-number register set to -1 triggers a non-standard signal-delivery path that synthesizes a SIGSEGV and kills the tracee before it executes a single user-mode instruction.

Changes

src/syscall/syscall.c — unmodifiable-sysnum workaround

Net behaviour change:

  • Restore the original syscall number before re-pushing the general register state. With the syscall-number register back to the original value (not 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.
  • Remove the PR_brk special case that forced SYSARG_1 = 0. This was a defensive workaround against a non-compliant kernel mishandling brk(-1). With the syscall number now explicitly restored and POSIX brk(addr) returning the current break without mutation when addr is 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 — cache NT_ARM_SYSTEM_CALL availability

NT_ARM_SYSTEM_CALL acceptance is a kernel-global property. Once the running kernel rejects it with EINVAL, 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-failing ptrace call before hitting the workaround.

Implementation:

  • A process-local static bool sysnum_regset_unavailable inside push_specific_regs() records the capability.
  • First PTRACE_SETREGSET(NT_ARM_SYSTEM_CALL) returning EINVAL sets the flag. Subsequent calls that would require sysnum modification short-circuit with errno = EINVAL; return -1;, so the existing caller (syscall.c:translate_syscall) hits its workaround path immediately.
  • Only EINVAL is cached. ESRCH / EPERM / EFAULT are 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.
  • Memory-only, not persisted. A new proot run always re-probes. This keeps behaviour correct across reboots, kernel upgrades, or chroots into different kernels, at the cost of one failing ptrace per proot start on affected kernels — negligible compared to proot's baseline ptrace volume.
  • Scope is deliberately process-global (function-static) rather than per-tracee: all tracees share the same running kernel, so a per-tracee flag would re-probe uselessly on every new child.

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.
Copilot AI review requested due to automatic review settings April 17, 2026 19:21
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 -1 to induce a natural kernel-side failure.
  • Remove the brk() special-case that forced SYSARG_1=0.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/syscall/syscall.c Outdated
Comment thread src/syscall/syscall.c Outdated
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.
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.

2 participants