Skip to content

Commit

Permalink
Improve support for ptrace emulation during group stops. (#3926)
Browse files Browse the repository at this point in the history
Firefox's minidump writer expects to be able to attach to tasks in group stops
and invoke ptrace commands on them (e.g. PTRACE_GETREGS)

Fixes #3741.
  • Loading branch information
khuey authored Mar 1, 2025
1 parent abc62ca commit ec66b21
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 23 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,7 @@ set(BASIC_TESTS
x86/ptrace_debug_regs
ptrace_exec
x86/ptrace_exec32
ptrace_group_stop
ptrace_kill_grandtracee
x86/ptrace_tls
ptrace_seize
Expand Down
49 changes: 29 additions & 20 deletions src/RecordTask.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1185,34 +1185,28 @@ void RecordTask::emulate_SIGCONT() {
}

void RecordTask::signal_delivered(int sig) {
bool needs_SIGCHLD = true;
Sighandler& h = sighandlers->get(sig);
if (h.resethand) {
reset_handler(&h, arch());
}

if (!is_sig_ignored(sig)) {
switch (sig) {
case SIGTSTP:
case SIGTTIN:
case SIGTTOU:
if (h.disposition() == SIGNAL_HANDLER) {
break;
}
RR_FALLTHROUGH;
case SIGSTOP:
// All threads in the process are stopped.
for (Task* t : thread_group()->task_set()) {
auto rt = static_cast<RecordTask*>(t);
rt->apply_group_stop(sig);
}
break;
case SIGCONT:
emulate_SIGCONT();
break;
if (is_sig_stopping(sig)) {
// All threads in the process are stopped.
for (Task* t : thread_group()->task_set()) {
auto rt = static_cast<RecordTask*>(t);
rt->apply_group_stop(sig);
}
// apply_group_stop calls send_synthetic_SIGCHLD_if_necessary(). Don't
// do it again.
needs_SIGCHLD = false;
} else if (sig == SIGCONT && !is_sig_ignored(sig)) {
emulate_SIGCONT();
}

send_synthetic_SIGCHLD_if_necessary();
if (needs_SIGCHLD) {
send_synthetic_SIGCHLD_if_necessary();
}
}

bool RecordTask::signal_has_user_handler(int sig) const {
Expand Down Expand Up @@ -1259,6 +1253,21 @@ bool RecordTask::is_sig_ignored(int sig) const {
}
}

bool RecordTask::is_sig_stopping(int sig) const {
switch (sig) {
case SIGTSTP:
case SIGTTIN:
case SIGTTOU:
if (sig_disposition(sig) != SIGNAL_DEFAULT) {
break;
}
RR_FALLTHROUGH;
case SIGSTOP:
return true;
}
return false;
}

SignalDisposition RecordTask::sig_disposition(int sig) const {
return sighandlers->get(sig).disposition();
}
Expand Down
4 changes: 4 additions & 0 deletions src/RecordTask.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ class RecordTask final : public Task {
* default disposition is "ignore".
*/
bool is_sig_ignored(int sig) const;
/**
* Return true iff |sig| is a stopping signal.
*/
bool is_sig_stopping(int sig) const;
/**
* Return the applications current disposition of |sig|.
*/
Expand Down
21 changes: 18 additions & 3 deletions src/Scheduler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,22 @@ bool Scheduler::is_task_runnable(RecordTask* t, WaitAggregator& wait_aggregator,
WaitAggregator::try_wait_exit(t);
// N.B.: If we supported ptrace exit notifications for killed tracee's
// that would need handling here, but we don't at the moment.
return t->seen_ptrace_exit_event();
if (t->seen_ptrace_exit_event()) {
LOGM(debug) << " ... but it died";
return true;
}
if (t->is_stopped()) {
return false;
}
// If we're not stopped, we need to get to the stop.
// AFAIK we can only get here with group stops, which are eagerly applied
// to every task in the group. If I'm wrong, die here.
ASSERT(t, t->emulated_stop_type == GROUP_STOP);
LOGM(debug) << " interrupting and waiting";
t->do_ptrace_interrupt();
// Wait on the task to get the kernel to kick it into the group stop.
// If it died, we can deal with it later.
return t->wait();
}
}

Expand Down Expand Up @@ -787,8 +802,8 @@ Scheduler::Rescheduled Scheduler::reschedule(Switchable switchable) {
#ifdef MONITOR_UNSWITCHABLE_WAITS
double wait_duration = monotonic_now_sec() - now;
if (wait_duration >= 0.010) {
log_warn("Waiting for unswitchable %s took %g ms",
strevent(current_->event), 1000.0 * wait_duration);
LOGM(warn) << "Waiting for unswitchable " << current_->ev()
<< " took " << 1000.0 * wait_duration << "ms";
}
#endif
}
Expand Down
64 changes: 64 additions & 0 deletions src/test/ptrace_group_stop.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* -*- Mode: C; tab-width: 8; c-basic-offset: 2; indent-tabs-mode: nil; -*- */

#include "util.h"
#include "ptrace_util.h"

static void* do_thread(void* arg) {
int pipe_fd = *(int*)arg;
uint32_t tid = gettid();

write(pipe_fd, &tid, 4);
/* Sleep long enough that it will be noticed if it's not interrupted. */
sleep(1000);

return NULL;
}

int main(void) {
pid_t child, child2;
uint32_t msg;
int status;
int pipe_fds[2];
struct user_regs_struct regs;

test_assert(0 == pipe(pipe_fds));

if (0 == (child = fork())) {
pthread_t t;

pthread_create(&t, NULL, do_thread, &pipe_fds[1]);
pthread_join(t, NULL);

return 77;
}

test_assert(4 == read(pipe_fds[0], &msg, 4));
child2 = (pid_t)msg;
close(pipe_fds[0]);
sched_yield();

/* Hit the entire process group with a SIGSTOP. */
tgkill(child, child, SIGSTOP);

/* Force the rr scheduler to run. */
sched_yield();

/* Now seize the stopped task. */
test_assert(0 == ptrace(PTRACE_SEIZE, child2, 0, 0));
test_assert(child2 == waitpid(child2, &status, 0));
test_assert(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP);

/* Do something that requires the task to be stopped. */
ptrace_getregs(child2, &regs);

/* Verify that we can resume from group stops. */
test_assert(0 == ptrace(PTRACE_CONT, child2, 0, 0));
/* Force the rr scheduler to run. */
sched_yield();
test_assert(0 == ptrace(PTRACE_INTERRUPT, child2, 0, 0));
test_assert(child2 == waitpid(child2, &status, 0));
test_assert(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP);

atomic_puts("EXIT-SUCCESS");
return 0;
}

0 comments on commit ec66b21

Please sign in to comment.