Skip to content

Commit ce4f78f

Browse files
ancientmodernpalmer-dabbelt
authored andcommitted
riscv: signal: handle syscall restart before get_signal
In the current riscv implementation, blocking syscalls like read() may not correctly restart after being interrupted by ptrace. This problem arises when the syscall restart process in arch_do_signal_or_restart() is bypassed due to changes to the regs->cause register, such as an ebreak instruction. Steps to reproduce: 1. Interrupt the tracee process with PTRACE_SEIZE & PTRACE_INTERRUPT. 2. Backup original registers and instruction at new_pc. 3. Change pc to new_pc, and inject an instruction (like ebreak) to this address. 4. Resume with PTRACE_CONT and wait for the process to stop again after executing ebreak. 5. Restore original registers and instructions, and detach from the tracee process. 6. Now the read() syscall in tracee will return -1 with errno set to ERESTARTSYS. Specifically, during an interrupt, the regs->cause changes from EXC_SYSCALL to EXC_BREAKPOINT due to the injected ebreak, which is inaccessible via ptrace so we cannot restore it. This alteration breaks the syscall restart condition and ends the read() syscall with an ERESTARTSYS error. According to include/linux/errno.h, it should never be seen by user programs. X86 can avoid this issue as it checks the syscall condition using a register (orig_ax) exposed to user space. Arm64 handles syscall restart before calling get_signal, where it could be paused and inspected by ptrace/debugger. This patch adjusts the riscv implementation to arm64 style, which also checks syscall using a kernel register (syscallno). It ensures the syscall restart process is not bypassed when changes to the cause register occur, providing more consistent behavior across various architectures. For a simplified reproduction program, feel free to visit: https://github.com/ancientmodern/riscv-ptrace-bug-demo. Signed-off-by: Haorong Lu <ancientmodern4@gmail.com> Link: https://lore.kernel.org/r/20230803224458.4156006-1-ancientmodern4@gmail.com Signed-off-by: Palmer Dabbelt <palmer@rivosinc.com>
1 parent 0619ff9 commit ce4f78f

1 file changed

Lines changed: 46 additions & 39 deletions

File tree

arch/riscv/kernel/signal.c

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -391,30 +391,6 @@ static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
391391
sigset_t *oldset = sigmask_to_save();
392392
int ret;
393393

394-
/* Are we from a system call? */
395-
if (regs->cause == EXC_SYSCALL) {
396-
/* Avoid additional syscall restarting via ret_from_exception */
397-
regs->cause = -1UL;
398-
/* If so, check system call restarting.. */
399-
switch (regs->a0) {
400-
case -ERESTART_RESTARTBLOCK:
401-
case -ERESTARTNOHAND:
402-
regs->a0 = -EINTR;
403-
break;
404-
405-
case -ERESTARTSYS:
406-
if (!(ksig->ka.sa.sa_flags & SA_RESTART)) {
407-
regs->a0 = -EINTR;
408-
break;
409-
}
410-
fallthrough;
411-
case -ERESTARTNOINTR:
412-
regs->a0 = regs->orig_a0;
413-
regs->epc -= 0x4;
414-
break;
415-
}
416-
}
417-
418394
rseq_signal_deliver(ksig, regs);
419395

420396
/* Set up the stack frame */
@@ -428,35 +404,66 @@ static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
428404

429405
void arch_do_signal_or_restart(struct pt_regs *regs)
430406
{
407+
unsigned long continue_addr = 0, restart_addr = 0;
408+
int retval = 0;
431409
struct ksignal ksig;
410+
bool syscall = (regs->cause == EXC_SYSCALL);
432411

433-
if (get_signal(&ksig)) {
434-
/* Actually deliver the signal */
435-
handle_signal(&ksig, regs);
436-
return;
437-
}
412+
/* If we were from a system call, check for system call restarting */
413+
if (syscall) {
414+
continue_addr = regs->epc;
415+
restart_addr = continue_addr - 4;
416+
retval = regs->a0;
438417

439-
/* Did we come from a system call? */
440-
if (regs->cause == EXC_SYSCALL) {
441418
/* Avoid additional syscall restarting via ret_from_exception */
442419
regs->cause = -1UL;
443420

444-
/* Restart the system call - no handlers present */
445-
switch (regs->a0) {
421+
/*
422+
* Prepare for system call restart. We do this here so that a
423+
* debugger will see the already changed PC.
424+
*/
425+
switch (retval) {
446426
case -ERESTARTNOHAND:
447427
case -ERESTARTSYS:
448428
case -ERESTARTNOINTR:
449-
regs->a0 = regs->orig_a0;
450-
regs->epc -= 0x4;
451-
break;
452429
case -ERESTART_RESTARTBLOCK:
453-
regs->a0 = regs->orig_a0;
454-
regs->a7 = __NR_restart_syscall;
455-
regs->epc -= 0x4;
430+
regs->a0 = regs->orig_a0;
431+
regs->epc = restart_addr;
456432
break;
457433
}
458434
}
459435

436+
/*
437+
* Get the signal to deliver. When running under ptrace, at this point
438+
* the debugger may change all of our registers.
439+
*/
440+
if (get_signal(&ksig)) {
441+
/*
442+
* Depending on the signal settings, we may need to revert the
443+
* decision to restart the system call, but skip this if a
444+
* debugger has chosen to restart at a different PC.
445+
*/
446+
if (regs->epc == restart_addr &&
447+
(retval == -ERESTARTNOHAND ||
448+
retval == -ERESTART_RESTARTBLOCK ||
449+
(retval == -ERESTARTSYS &&
450+
!(ksig.ka.sa.sa_flags & SA_RESTART)))) {
451+
regs->a0 = -EINTR;
452+
regs->epc = continue_addr;
453+
}
454+
455+
/* Actually deliver the signal */
456+
handle_signal(&ksig, regs);
457+
return;
458+
}
459+
460+
/*
461+
* Handle restarting a different system call. As above, if a debugger
462+
* has chosen to restart at a different PC, ignore the restart.
463+
*/
464+
if (syscall && regs->epc == restart_addr && retval == -ERESTART_RESTARTBLOCK)
465+
regs->a7 = __NR_restart_syscall;
466+
460467
/*
461468
* If there is no signal to deliver, we just put the saved
462469
* sigmask back.

0 commit comments

Comments
 (0)