Skip to content
Advertisement

Ptrace prevents signal from interrupting pselect() in traced process

I’m trying to monitor syscalls for a binary using ptrace. The binary sleeps in pselect() and without ptrace, a SIGQUIT makes it return from pselect. The mask of blocked signals passed to pselect includes SIGQUIT.

When executed with ptrace, it exits from sys_pselect6 but not all the way out of glibc’s pselect. What am I doing that prevents sys_pselect6 from exiting out to user code ?

Tracer:

#include <stdio.h>
#include <sys/ptrace.h>
#include <sys/reg.h>
#include <err.h>
#include <wait.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int pid = fork(), sys_in = 1, status;

    if (pid == 0) {
        if (ptrace(PTRACE_TRACEME, getppid(), NULL, NULL) < 0)
            err(1, "TRACEME()");

        execl("./child", "./child", NULL);
        err(1, "execl()");
    }

    if (waitpid(pid, &status, 0) != pid) err(1, "wait()");

    for (;; sys_in ^= 1) {
        if (ptrace(PTRACE_SYSCALL, pid, NULL, NULL) < 0) err(1, "SYSCALL");

        if (waitpid(pid, &status, 0) != pid) err(1, "wait()");

        if (sys_in) {
            long long sys_no = ptrace(PTRACE_PEEKUSER, pid, 8 * ORIG_RAX, NULL);
            printf("syscall entry %lldn", sys_no);
        }
        else printf("syscall exitn");
    }
    return 0;
}

Child:

#include <stdio.h>
#include <sys/select.h>
#include <signal.h>
#include <err.h>

void handle_sigquit(int sig, siginfo_t* info, void *ctx)
{
}

int main()
{
    sigset_t mask;
    sigset_t orig_mask;
    struct sigaction sa = {};

    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = handle_sigquit;
    sigaction(SIGQUIT, &sa, NULL);

    sigemptyset(&mask);
    sigaddset(&mask, SIGQUIT);

    if (sigprocmask(SIG_BLOCK, &mask, &orig_mask) < 0) err(1, "sigprocmask()");

    pselect(0, NULL, NULL, NULL, NULL, &orig_mask);
    warn("pselect()");
    return 0;
}

Advertisement

Answer

ptrace(PTRACE_SYSCALL, pid, NULL, NULL)

Whenever your debugger gets a notification, you just assume that that notification is about a system call, and handle it accordingly. That is not the case.

Some of the notifications you receive using wait are for signals that your debugee has received. When those happen, the last NULL in your PTRACE_SYSCALL call eliminates, effectively masks, the signal from arriving at the debugee process.

When processing ptrace results, you need to check the signal that caused your debugger to wake up. At the very least, check if it’s a SIGTRAP or something else. If it is something else, the best bet is to pass it on to the debugee process.

Check out this small program to see a simple way of doing it.

Advertisement