Re: [REGRESSION] x86/entry: TIF_SINGLESTEP handling is still broken

From: Kyle Huey
Date: Sun Jan 31 2021 - 18:20:29 EST


On Sun, Jan 31, 2021 at 2:27 PM Kyle Huey <me@xxxxxxxxxxxx> wrote:
>
> On Sun, Jan 31, 2021 at 2:20 PM Andy Lutomirski <luto@xxxxxxxxxxxxxx> wrote:
> >
> >
> >
> > > On Jan 31, 2021, at 2:08 PM, Kyle Huey <me@xxxxxxxxxxxx> wrote:
> > >
> > > On Sun, Jan 31, 2021 at 2:04 PM Andy Lutomirski <luto@xxxxxxxxxxxxxx> wrote:
> > >> Indeed, and I have tests for this.
> > >
> > > Do you mean you already have a test case or that you would like a
> > > minimized test case?
> >
> > A smallish test that we could stick in selftests would be great if that’s straightforward.
>
> I'll look into it.
>
> - Kyle

A minimal test case follows.

The key to triggering this bug is to enter a ptrace syscall stop and
then use PTRACE_SINGLESTEP to exit it. On a good kernel this will not
result in any userspace code execution in the tracee because on the
way out of the kernel's syscall handling path the singlestep trap will
be raised immediately. On a bad kernel that stop will not be raised,
and in the example below, the program will crash.

- Kyle

---

#include <assert.h>
#include <stdio.h>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <unistd.h>

void do_child() {
/* Synchronize with the parent */
kill(getpid(), SIGSTOP);
/* Do a syscall */
printf("child is alive\n");
/* Return and exit */
}

int main() {
pid_t child = -1;
int status = 0;
unsigned long long previous_rip = 0;
struct user_regs_struct regs;

if ((child = fork()) == 0) {
do_child();
return 0;
}

/* Adds 0x80 to syscall stops so we can see them easily */
intptr_t options = PTRACE_O_TRACESYSGOOD;
/* Take control of the child (which should be waiting */
assert(ptrace(PTRACE_SEIZE, child, NULL, options) == 0);
assert(waitpid(child, &status, 0) == child);
assert(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP);

/* Advance to the syscall stop for the write underlying
* the child's printf.
*/
assert(ptrace(PTRACE_SYSCALL, child, NULL, 0) == 0);
assert(waitpid(child, &status, 0) == child);
/* Should be a syscall stop */
assert(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP | 0x80);

/* Mess with the child's registers, so it will crash if
* it executes any code
*/
assert(ptrace(PTRACE_GETREGS, child, NULL, &regs) == 0);
previous_rip = regs.rip;
regs.rip = 0xdeadbeef;
assert(ptrace(PTRACE_SETREGS, child, NULL, &regs) == 0);
/* Singlestep. This should trap without executing any code */
assert(ptrace(PTRACE_SINGLESTEP, child, NULL, 0) == 0);
assert(waitpid(child, &status, 0) == child);
/* Should be at a singlestep SIGTRAP. In a buggy kernel,
* the SIGTRAP is skipped, execution resumes, and we
* get a SIGSEGV at the invalid address.
*/
assert(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP);

/* Restore registers */
assert(ptrace(PTRACE_GETREGS, child, NULL, &regs) == 0);
regs.rip = previous_rip;
assert(ptrace(PTRACE_SETREGS, child, NULL, &regs) == 0);

/* Continue to the end of the program */
assert(ptrace(PTRACE_CONT, child, NULL, 0) == 0);
assert(waitpid(child, &status, 0) == child);
/* Verify the child exited cleanly */
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);

printf("SUCCESS\n");

return 0;
}