diff --git a/os/StarryOS/kernel/src/syscall/task/execve.rs b/os/StarryOS/kernel/src/syscall/task/execve.rs index 3c762cb3ab..6dc3abd1e7 100644 --- a/os/StarryOS/kernel/src/syscall/task/execve.rs +++ b/os/StarryOS/kernel/src/syscall/task/execve.rs @@ -385,6 +385,14 @@ fn do_execve( proc_data.proc.rename_thread(my_tid, tgid); } + // All ptrace tracees (both TRACEME and ATTACH) unconditionally + // stop with SIGTRAP on execve (Linux ptrace(2)). PTRACE_O_TRACEEXEC + // only controls whether the stop carries PTRACE_EVENT_EXEC data, + // not whether the stop itself occurs. + if proc_data.is_ptrace_traceme() || proc_data.is_ptrace_attached() { + proc_data.set_ptrace_exec_stop_pending(); + } + // Reset every user-visible register to a fresh-process state, not // just IP/SP. Linux's `start_thread()` clears all GP registers, // resets the TLS pointer, and clobbers any FP/SIMD state to the @@ -407,14 +415,6 @@ fn do_execve( has_ldso, ); - // All ptrace tracees (both TRACEME and ATTACH) unconditionally - // stop with SIGTRAP on execve (Linux ptrace(2)). PTRACE_O_TRACEEXEC - // only controls whether the stop carries PTRACE_EVENT_EXEC data, - // not whether the stop itself occurs. - if proc_data.is_ptrace_traceme() || proc_data.is_ptrace_attached() { - proc_data.set_ptrace_exec_stop_pending(); - } - // Unblock a vfork parent waiting for this child to exec. // Must be last: by now CLOEXEC fds are closed so the parent's pipe // read will see EOF correctly. diff --git a/os/StarryOS/kernel/src/syscall/task/ptrace.rs b/os/StarryOS/kernel/src/syscall/task/ptrace.rs index 816a083548..92c17d2bd9 100644 --- a/os/StarryOS/kernel/src/syscall/task/ptrace.rs +++ b/os/StarryOS/kernel/src/syscall/task/ptrace.rs @@ -58,6 +58,7 @@ const PTRACE_GETREGSET: u32 = 0x4204; const PTRACE_SETREGSET: u32 = 0x4205; const PTRACE_SEIZE: u32 = 0x4206; const PTRACE_INTERRUPT: u32 = 0x4207; +const PTRACE_LISTEN: u32 = 0x4208; const NT_PRSTATUS: usize = 1; #[cfg(any( @@ -76,6 +77,8 @@ const PTRACE_O_TRACECLONE: usize = 1 << 3; const PTRACE_O_TRACEEXEC: usize = 1 << 4; const PTRACE_O_TRACEVFORKDONE: usize = 1 << 5; const PTRACE_O_TRACEEXIT: usize = 1 << 6; +const PTRACE_O_TRACESECCOMP: usize = 1 << 7; +const PTRACE_O_EXITKILL: usize = 1 << 20; pub const PTRACE_EVENT_FORK: u32 = 1; pub const PTRACE_EVENT_VFORK: u32 = 2; @@ -83,6 +86,7 @@ pub const PTRACE_EVENT_CLONE: u32 = 3; const PTRACE_EVENT_EXEC: u32 = 4; pub const PTRACE_EVENT_VFORK_DONE: u32 = 5; const PTRACE_EVENT_EXIT: u32 = 6; +const PTRACE_EVENT_STOP: u32 = 128; #[cfg(target_arch = "riscv64")] const EBREAK_INSN: u16 = 0x9002; @@ -260,8 +264,9 @@ pub fn sys_ptrace(request: u32, pid: usize, addr: usize, data: usize) -> AxResul PTRACE_SETSIGINFO => ptrace_setsiginfo(pid, data), PTRACE_GETREGSET => ptrace_getregset(pid, addr, data), PTRACE_SETREGSET => ptrace_setregset(pid, addr, data), - PTRACE_SEIZE => ptrace_seize(pid, addr), + PTRACE_SEIZE => ptrace_seize(pid, addr, data), PTRACE_INTERRUPT => ptrace_interrupt(pid), + PTRACE_LISTEN => ptrace_listen(pid), _ => Err(AxError::Unsupported), } } @@ -376,7 +381,9 @@ fn ptrace_setoptions(pid: usize, options: usize) -> AxResult { | PTRACE_O_TRACECLONE | PTRACE_O_TRACEEXEC | PTRACE_O_TRACEVFORKDONE - | PTRACE_O_TRACEEXIT; + | PTRACE_O_TRACEEXIT + | PTRACE_O_TRACESECCOMP + | PTRACE_O_EXITKILL; if options & !valid_mask != 0 { return Err(AxError::InvalidInput); } @@ -598,7 +605,7 @@ fn ptrace_setfpregs(pid: usize, data: usize) -> AxResult { Err(AxError::Unsupported) } -fn ptrace_seize(pid: usize, _addr: usize) -> AxResult { +fn ptrace_seize(pid: usize, _addr: usize, data: usize) -> AxResult { let tracer_pid = current().as_thread().proc_data.proc.pid(); let tracee_pid = Pid::try_from(pid).map_err(|_| AxError::from(LinuxError::ESRCH))?; if tracee_pid == tracer_pid { @@ -613,6 +620,22 @@ fn ptrace_seize(pid: usize, _addr: usize) -> AxResult { } tracee.set_ptrace_tracer_pid(tracer_pid); tracee.set_ptrace_attached(); + // PTRACE_SEIZE accepts ptrace options in the data argument; + // store them immediately (strace passes ptrace_setoptions here). + if data != 0 { + let valid_mask = PTRACE_O_TRACESYSGOOD + | PTRACE_O_TRACEFORK + | PTRACE_O_TRACEVFORK + | PTRACE_O_TRACECLONE + | PTRACE_O_TRACEEXEC + | PTRACE_O_TRACEVFORKDONE + | PTRACE_O_TRACEEXIT + | PTRACE_O_TRACESECCOMP + | PTRACE_O_EXITKILL; + if data & !valid_mask == 0 { + tracee.set_ptrace_options(data); + } + } Ok(0) } @@ -653,6 +676,10 @@ fn ptrace_interrupt(pid: usize) -> AxResult { if tracee.ptrace_stop_signo().is_some() { return Ok(0); } + // Tag the upcoming stop with PTRACE_EVENT_STOP so the tracer sees + // EVENT_STOP(128) in the upper 16 bits of the wait status and + // handles it as a group-stop (TE_GROUP_STOP / TE_RESTART). + tracee.set_ptrace_pending_event(tracee_pid, PTRACE_EVENT_STOP, 0); use starry_signal::SignalInfo; let _ = crate::task::send_signal_to_process( tracee_pid, @@ -661,6 +688,24 @@ fn ptrace_interrupt(pid: usize) -> AxResult { Ok(0) } +#[cfg(any( + target_arch = "riscv64", + target_arch = "aarch64", + target_arch = "loongarch64", + target_arch = "x86_64" +))] +fn ptrace_listen(pid: usize) -> AxResult { + let (tracee, tid) = ptrace_stopped_tracee_with_tid(pid)?; + let signo = tracee + .ptrace_stop_signo_for(tid) + .ok_or_else(|| AxError::from(LinuxError::ESRCH))?; + tracee.set_ptrace_singlestep_for(tid, false); + tracee.set_ptrace_syscall_trace_for(tid, false); + tracee.resume_ptrace_stop_with_signal_for(tid, signo as u32); + ax_task::yield_now(); + Ok(0) +} + #[cfg(any( target_arch = "riscv64", target_arch = "aarch64", diff --git a/os/StarryOS/kernel/src/task/signal.rs b/os/StarryOS/kernel/src/task/signal.rs index 76b37d8a5f..418a115504 100644 --- a/os/StarryOS/kernel/src/task/signal.rs +++ b/os/StarryOS/kernel/src/task/signal.rs @@ -344,6 +344,14 @@ pub fn check_signals( let signo = sig.signo(); + // Signals whose disposition is Continue (SIGCONT) or that are + // unconditionally forwarded (SIGKILL, already checked above) must + // never create a ptrace-stop — they are consumed silently so that + // tracers observe only "real" stops. + if matches!(os_action, SignalOSAction::Continue) { + return true; + } + if signo != Signo::SIGKILL && !thr .proc_data diff --git a/os/StarryOS/kernel/src/task/user.rs b/os/StarryOS/kernel/src/task/user.rs index d4fd6d0994..5d2ba38bd9 100644 --- a/os/StarryOS/kernel/src/task/user.rs +++ b/os/StarryOS/kernel/src/task/user.rs @@ -65,6 +65,24 @@ pub fn new_user_task(name: &str, mut uctx: UserContext, set_child_tid: usize) -> } } + // Linux stops tracees *before* execve so the tracer + // can read the original syscall arguments (path, argv, + // envp) from the trap frame. When no syscall-trace was + // active (trace_state != Entry) we must create that + // pre-execve stop here. With syscall-trace active the + // entry-stop above already covered it. + if !matches!(trace_state, SyscallTraceState::Entry) + && (thr.proc_data.is_ptrace_traceme() + || thr.proc_data.is_ptrace_attached()) + && matches!( + Sysno::new(saved_sysno), + Some(Sysno::execve | Sysno::execveat) + ) + { + crate::syscall::ptrace_notify_exec(thr.proc_data.proc.pid()); + let _ = ptrace_stop_current(thr, Signo::SIGTRAP, &mut uctx); + } + if let Some(exit_code) = ptrace_exit_event_code(saved_sysno, saved_a0) && crate::syscall::ptrace_notify_exit( thr.proc_data.proc.pid(), @@ -81,20 +99,26 @@ pub fn new_user_task(name: &str, mut uctx: UserContext, set_child_tid: usize) -> { continue; } - if matches!( - thr.proc_data.take_ptrace_syscall_trace_for(tid), - SyscallTraceState::Exit - ) { - let _ = ptrace_syscall_stop_current(thr, Signo::SIGTRAP, &mut uctx); - } - if thr.proc_data.take_ptrace_exec_stop_pending() { - let _is_event = + let exec_pending = thr.proc_data.take_ptrace_exec_stop_pending(); + let syscall_trace = thr.proc_data.take_ptrace_syscall_trace_for(tid); + match (exec_pending, syscall_trace) { + // Normal syscall exit — non-execve path. + (false, SyscallTraceState::Exit) => { + let _ = ptrace_syscall_stop_current(thr, Signo::SIGTRAP, &mut uctx); + } + // Execve with syscall-trace active (ATTACH/SEIZE), + // or enabled during the pre-execve stop (TRACEME + + // PTRACE_SYSCALL). Deliver the exit-stop with the + // return value and the PTRACE_EVENT_EXEC event. + (true, SyscallTraceState::Exit | SyscallTraceState::Entry) => { crate::syscall::ptrace_notify_exec(thr.proc_data.proc.pid()); - if let Some(_resume_sig) = - ptrace_stop_current(thr, Signo::SIGTRAP, &mut uctx) - { - continue; + let _ = ptrace_syscall_stop_current(thr, Signo::SIGTRAP, &mut uctx); } + // Execve after pre-execve stop, but tracer used + // PTRACE_CONT (no syscall-trace). The pre-execve + // stop already notified the tracer; no extra stop. + (true, _) => {} + (false, _) => {} } } ReturnReason::PageFault(addr, flags) => { diff --git a/test-suit/starryos/qemu-smp1/system/test-ptrace-listen/CMakeLists.txt b/test-suit/starryos/qemu-smp1/system/test-ptrace-listen/CMakeLists.txt new file mode 100644 index 0000000000..86629e0c24 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/test-ptrace-listen/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.20) +project(test-ptrace-listen C) +include(${CMAKE_CURRENT_LIST_DIR}/../common/starry_arch_filter.cmake) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +starry_arch_filtered_executable(test-ptrace-listen "riscv64" "test-ptrace-listen skipped on unsupported architecture" src/main.c) +target_include_directories(test-ptrace-listen PRIVATE src) +target_compile_options(test-ptrace-listen PRIVATE -Wall -Wextra -Werror) + +install(TARGETS test-ptrace-listen RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu-smp1/system/test-ptrace-listen/src/main.c b/test-suit/starryos/qemu-smp1/system/test-ptrace-listen/src/main.c new file mode 100644 index 0000000000..20a99d537c --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/test-ptrace-listen/src/main.c @@ -0,0 +1,179 @@ +/* + * test-ptrace-listen + * + * Verify PTRACE_LISTEN semantics per man 2 ptrace: + * - Fails with ESRCH on a non-traced process. + * - Fails with ESRCH on a SEIZE'd but still-running tracee. + * - After a group-stop (SIGSTOP): LISTEN releases the ptrace-stop and + * puts the tracee into a job-control stop. A subsequent SIGCONT + * resumes the tracee and waitpid(WCONTINUED) reports the continue. + */ + +#include "test_framework.h" +#include +#include +#include +#include + +#ifndef PTRACE_SEIZE +#define PTRACE_SEIZE 0x4206 +#endif +#ifndef PTRACE_LISTEN +#define PTRACE_LISTEN 0x4208 +#endif + +static void kill_and_reap(pid_t pid) +{ + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); +} + +/* --- test 1: LISTEN on non-traced child returns ESRCH --- */ +static int test_listen_not_traced(void) +{ + TEST_START("LISTEN on non-traced child returns ESRCH"); + + pid_t pid = fork(); + CHECK(pid >= 0, "fork"); + if (pid < 0) TEST_DONE(); + if (pid == 0) { + pause(); + _exit(0); + } + + CHECK_ERR(ptrace(PTRACE_LISTEN, pid, NULL, NULL), ESRCH, + "LISTEN on non-traced child"); + + kill_and_reap(pid); + TEST_DONE(); +} + +/* --- test 2: LISTEN on SEIZE'd but running tracee returns ESRCH --- */ +static int test_listen_running_seized(void) +{ + TEST_START("LISTEN on running SEIZE'd tracee returns ESRCH"); + + pid_t pid = fork(); + CHECK(pid >= 0, "fork"); + if (pid < 0) TEST_DONE(); + if (pid == 0) { + pause(); + _exit(0); + } + + CHECK_RET(ptrace(PTRACE_SEIZE, pid, NULL, NULL), 0, "ptrace SEIZE"); + CHECK_ERR(ptrace(PTRACE_LISTEN, pid, NULL, NULL), ESRCH, + "LISTEN on running seized tracee"); + + kill_and_reap(pid); + TEST_DONE(); +} + +/* --- test 3: LISTEN after group-stop, SIGCONT resumes --- */ +static int test_listen_group_stop_then_cont(void) +{ + TEST_START("LISTEN after group-stop, then SIGCONT resumes"); + + int sync_pipe[2]; + CHECK(pipe(sync_pipe) == 0, "pipe"); + if (sync_pipe[0] < 0) { + /* pipe failed; CHECK already recorded the failure */ + TEST_DONE(); + } + + pid_t pid = fork(); + CHECK(pid >= 0, "fork"); + if (pid < 0) { + close(sync_pipe[0]); close(sync_pipe[1]); + TEST_DONE(); + } + + if (pid == 0) { + /* child side */ + close(sync_pipe[0]); + char r = 1; + if (write(sync_pipe[1], &r, 1) != 1) _exit(99); /* signal ready */ + if (read(sync_pipe[1], &r, 1) != 1) _exit(98); /* wait for SEIZE */ + close(sync_pipe[1]); + raise(SIGSTOP); /* enter group-stop */ + _exit(42); + } + + /* parent side */ + close(sync_pipe[1]); + + /* wait for child to signal ready */ + { + char r; + CHECK(read(sync_pipe[0], &r, 1) == 1, "sync read from child"); + } + + CHECK_RET(ptrace(PTRACE_SEIZE, pid, NULL, NULL), 0, "ptrace SEIZE"); + + /* tell child to raise(SIGSTOP) */ + { + char r = 1; + CHECK(write(sync_pipe[0], &r, 1) == 1, "sync write to child"); + } + close(sync_pipe[0]); + + /* wait for the group-stop notification */ + { + int status = 0; + pid_t got = waitpid(pid, &status, WUNTRACED); + CHECK(got == pid, "waitpid returns pid after SIGSTOP"); + if (got == pid) { + CHECK(WIFSTOPPED(status), "WIFSTOPPED"); + if (WIFSTOPPED(status)) { + CHECK(WSTOPSIG(status) == SIGSTOP, "WSTOPSIG == SIGSTOP"); + } + } + } + + /* LISTEN — release ptrace-stop, enter job-control stop */ + CHECK_RET(ptrace(PTRACE_LISTEN, pid, NULL, NULL), 0, "PTRACE_LISTEN"); + + /* After LISTEN the tracee is NOT in ptrace-stop. Pass a non-null + * data pointer so the kernel reaches the ptrace-stopped check + * (null data -> EINVAL, which would mask the ESRCH we want). */ + { + struct riscv_user_regs { unsigned long r[32]; } regs; + CHECK_ERR(ptrace(PTRACE_GETREGS, pid, NULL, ®s), ESRCH, + "GETREGS fails after LISTEN (not in ptrace-stop)"); + } + + /* SIGCONT resumes the tracee from job-control stop */ + CHECK_RET(kill(pid, SIGCONT), 0, "kill SIGCONT"); + + { + int status = 0; + pid_t got = waitpid(pid, &status, WCONTINUED); + CHECK(got == pid, "waitpid WCONTINUED returns pid"); + if (got == pid) { + CHECK(WIFCONTINUED(status), "WIFCONTINUED"); + } + } + + /* After SIGCONT the child resumes from raise(SIGSTOP) and exits */ + { + int status = 0; + pid_t got = waitpid(pid, &status, 0); + CHECK(got == pid, "waitpid reaps exited child"); + if (got == pid) { + CHECK(WIFEXITED(status), "WIFEXITED"); + if (WIFEXITED(status)) { + CHECK(WEXITSTATUS(status) == 42, "WEXITSTATUS == 42"); + } + } + } + + TEST_DONE(); +} + +int main(void) +{ + test_listen_not_traced(); + test_listen_running_seized(); + test_listen_group_stop_then_cont(); + return __fail > 0 ? 1 : 0; +} diff --git a/test-suit/starryos/qemu-smp1/system/test-ptrace-listen/src/test_framework.h b/test-suit/starryos/qemu-smp1/system/test-ptrace-listen/src/test_framework.h new file mode 100644 index 0000000000..2f47878f32 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/test-ptrace-listen/src/test_framework.h @@ -0,0 +1,86 @@ +#pragma once + +/* 必须在最前面定义,确保 pipe2/gettid 等可用 */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +/* + * StarryOS Syscall Test Framework + * + * 极简独立测试框架:每个文件测一个 syscall,独立编译运行。 + * 目标:出错时精确定位到 源文件:行号 -> 哪个调用 -> 什么结果 + * + * 用法: + * TEST_START("测试名"); + * CHECK(call == expected, "描述"); + * CHECK_ERR(call, EBADF, "描述"); + * TEST_DONE(); + */ + +#include +#include +#include +#include + +static int __pass = 0; +static int __fail = 0; + +/* ---- 核心: 带文件名+行号的检查宏 ---- */ + +/* 检查条件为真 */ +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + __fail++; \ + } \ +} while (0) + +/* 检查 syscall 返回特定值 */ +#define CHECK_RET(call, expected, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + long _e = (long)(expected); \ + if (_r == _e) { \ + printf(" PASS | %s:%d | %s (ret=%ld)\n", \ + __FILE__, __LINE__, msg, _r); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, _e, _r, errno, strerror(errno)); \ + __fail++; \ + } \ +} while (0) + +/* 检查 syscall 失败且 errno 符合预期 */ +#define CHECK_ERR(call, exp_errno, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + if (_r == -1 && errno == (exp_errno)) { \ + printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \ + __FILE__, __LINE__, msg, errno); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, \ + strerror(errno)); \ + __fail++; \ + } \ +} while (0) + +/* ---- 测试边界 ---- */ +#define TEST_START(name) \ + printf("================================================\n"); \ + printf(" TEST: %s\n", name); \ + printf(" FILE: %s\n", __FILE__); \ + printf("================================================\n") + +#define TEST_DONE() \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail\n", __pass, __fail); \ + printf("================================================\n\n"); \ + return __fail > 0 ? 1 : 0