Skip to content

Commit 763f2e0

Browse files
author
埃博拉酱
committed
Address Copilot review on PR termux#348 and cache sysnum-regset availability
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.
1 parent 970d51c commit 763f2e0

2 files changed

Lines changed: 55 additions & 17 deletions

File tree

src/syscall/syscall.c

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -243,28 +243,48 @@ void translate_syscall(Tracee *tracee)
243243
tracee->restart_how = PTRACE_SYSCALL;
244244
}
245245

246-
/* Handle syscall rejection when sysnum can't be modified.
246+
/* Handle syscall rejection when the syscall number can't be modified.
247247
*
248-
* Normal path: proot sets sysnum to PR_void so the kernel runs
249-
* a harmless no-op, then overrides x0 at sysexit with the real
250-
* error code. On some kernels the NT_ARM_SYSTEM_CALL regset is
251-
* absent, so the NT_PRSTATUS push carrying x8=-1 (PR_void) is
252-
* rejected with EINVAL and we land in this workaround branch.
248+
* Normal path: proot sets the syscall number to PR_void so the
249+
* kernel runs a harmless no-op, then overrides the return-value
250+
* register at sysexit with the real error code. On some kernels
251+
* the dedicated syscall-number regset is absent/refused (on
252+
* arm64 this is PTRACE_SETREGSET(NT_ARM_SYSTEM_CALL) returning
253+
* EINVAL; see push_specific_regs() in tracee/reg.c which bails
254+
* out before even attempting the general-register push), and
255+
* we land in this workaround branch.
253256
*
254257
* Legacy strategy was to poke all 6 syscall args to -1 and
255-
* re-push NT_PRSTATUS, keeping x8=-1 so the kernel still saw
256-
* an illegal sysnum and rejected the call with ENOSYS. That
257-
* works on stock kernels, but on some kernels x8=-1 on restart
258-
* triggers a non-standard signal delivery path that
259-
* synthesizes a SIGSEGV and kills the tracee before it
260-
* executes a single user-mode instruction.
258+
* re-push the general register state while keeping the syscall
259+
* number set to PR_void, so the kernel still saw an illegal
260+
* syscall number and rejected the call with ENOSYS. That works
261+
* on stock kernels, but on some kernels restarting with the
262+
* syscall-number register set to PR_void triggers a
263+
* non-standard signal delivery path that synthesizes a SIGSEGV
264+
* and kills the tracee before it executes a single user-mode
265+
* instruction.
261266
*
262-
* Correct strategy: restore x8 to the original sysnum so the
267+
* Correct strategy: restore the original syscall number so the
263268
* kernel actually runs the rejected syscall, and poke all 6
264269
* args to -1 so the syscall fails naturally inside the kernel
265-
* (EFAULT/EBADF/EINVAL). The real error code is written to x0
266-
* by proot at sysexit. */
267-
poke_reg(tracee, SYSARG_NUM, orig_sysnum); /* restore sysnum; x8=-1 triggers non-standard signal path on some kernels */
270+
* (EFAULT/EBADF/EINVAL). The real error code is written to the
271+
* return-value register by proot at sysexit.
272+
*
273+
* Known limitation:
274+
* syscalls that ignore arguments (e.g. getpid/sync) or take
275+
* fewer than 6 args will not necessarily fail inside the
276+
* kernel, so they will actually execute with whatever state
277+
* the tracee already has. We accept this: (a) the legacy
278+
* "keep sysnum=PR_void" path is strictly worse on affected
279+
* kernels — it kills the tracee with SIGSEGV; (b) -1 in every
280+
* arg slot already traps the overwhelming majority of
281+
* side-effectful syscalls at the kernel's parameter-validation
282+
* stage (EBADF/EFAULT/EINVAL); (c) we have no empirically
283+
* grounded list of syscalls that both reach this suppression
284+
* path and cause harmful side effects when run with poisoned
285+
* args, so a speculative allow/deny list would be dead code.
286+
* The real return value is still overridden at sysexit. */
287+
poke_reg(tracee, SYSARG_NUM, orig_sysnum); /* restore original sysnum; PR_void in the syscall-number register triggers a non-standard SIGSEGV path on some kernels */
268288
poke_reg(tracee, SYSARG_1, -1);
269289
poke_reg(tracee, SYSARG_2, -1);
270290
poke_reg(tracee, SYSARG_3, -1);

src/tracee/reg.c

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,30 @@ int push_specific_regs(Tracee *tracee, bool including_sysnum)
332332

333333
/* Update syscall number if needed. On arm64, a new
334334
* subcommand has been added to PTRACE_{S,G}ETREGSET
335-
* to allow write/read of current sycall number. */
335+
* to allow write/read of current sycall number.
336+
*
337+
* Kernel-capability cache: NT_ARM_SYSTEM_CALL is a
338+
* kernel-global feature — if it is rejected once with
339+
* EINVAL, it will never succeed under the same running
340+
* kernel. We short-circuit subsequent requests so the
341+
* caller (see syscall.c unmodifiable-sysnum workaround)
342+
* hits its fallback path immediately without paying the
343+
* cost of a guaranteed-failing ptrace on every intercepted
344+
* syscall. Only EINVAL is cached: ESRCH/EPERM/EFAULT are
345+
* per-tracee state issues, not kernel capability. Memory
346+
* only (not persisted); a new proot run re-probes. */
347+
static bool sysnum_regset_unavailable = false;
336348
if (including_sysnum && current_sysnum != REG(tracee, ORIGINAL, SYSARG_NUM)) {
349+
if (sysnum_regset_unavailable) {
350+
errno = EINVAL;
351+
return -1;
352+
}
337353
regs.iov_base = &current_sysnum;
338354
regs.iov_len = sizeof(current_sysnum);
339355
status = ptrace(PTRACE_SETREGSET, tracee->pid, NT_ARM_SYSTEM_CALL, &regs);
340356
if (status < 0) {
357+
if (errno == EINVAL)
358+
sysnum_regset_unavailable = true;
341359
//note(tracee, WARNING, SYSTEM, "can't set the syscall number");
342360
return status;
343361
}

0 commit comments

Comments
 (0)