diff --git a/apps/starry/README.md b/apps/starry/README.md index af43ed3c36..61ec584006 100644 --- a/apps/starry/README.md +++ b/apps/starry/README.md @@ -87,14 +87,24 @@ Stress configs are available through explicit QEMU config variants; see ## GDB Smoke The `gdb-smoke` case is a RISC-V QEMU app workflow that prepares a temporary -rootfs overlay with GDB, GDBServer, and two tiny target programs. +rootfs overlay with GDB, GDBServer, and tiny debugger smoke targets. ```bash -cargo xtask starry app run -t gdb-smoke --arch riscv64 -cargo xtask starry app run -t gdb-smoke --arch riscv64 \ +cargo xtask starry app qemu -t gdb-smoke --arch riscv64 +cargo xtask starry app qemu -t gdb-smoke --arch riscv64 \ --qemu-config qemu-riscv64-gdbserver.toml +cargo xtask starry app qemu -t gdb-smoke --arch riscv64 \ + --qemu-config qemu-riscv64-threads.toml +cargo xtask starry app qemu -t gdb-smoke --arch riscv64 \ + --qemu-config qemu-riscv64-stress.toml +cargo xtask starry app qemu -t gdb-smoke --arch riscv64 \ + --qemu-config qemu-riscv64-gdbserver-manual.toml ``` +When using the long-lived Docker container for a `*-manual.toml` entry, run the +same command through `docker exec -it tgoskits-dev ...` so the QEMU serial +console stays interactive. + ## MariaDB The `mariadb` case is a QEMU app workflow that installs MariaDB in the guest, diff --git a/apps/starry/gdb-smoke/README.md b/apps/starry/gdb-smoke/README.md new file mode 100644 index 0000000000..edae7807fb --- /dev/null +++ b/apps/starry/gdb-smoke/README.md @@ -0,0 +1,207 @@ +# StarryOS GDB Smoke + +This app prepares a RISC-V Alpine rootfs overlay with guest `gdb`, `gdbserver`, +and tiny target programs for StarryOS user-space debugger smoke testing. + +## Batch Native GDB Smoke + +Use this command for the automated native GDB batch smoke: + +```bash +cargo xtask starry app qemu -t gdb-smoke --arch riscv64 +``` + +The batch script runs: + +```gdb +break native_marker +run +bt +info proc mappings +info files +info auxv +shell pid="$(pidof gdb-native-smoke-target)" && cat "/proc/$pid/status" +info registers +x/4gx $sp +stepi +continue +``` + +Success requires all `GDB_NATIVE_*` markers from +`native/gdb-native-smoke.gdb`. + +## Manual Native GDB Demo + +Use this entry when you want an interactive StarryOS shell: + +```bash +cargo xtask starry app qemu -t gdb-smoke --arch riscv64 \ + --qemu-config qemu-riscv64-manual.toml +``` + +When running through the long-lived Docker container, keep stdin and a TTY +attached: + +```bash +docker exec -it tgoskits-dev cargo xtask starry app qemu -t gdb-smoke --arch riscv64 \ + --qemu-config qemu-riscv64-manual.toml +``` + +This keeps QEMU's serial console interactive inside Docker. Use `Ctrl+A`, then +`x`, to leave the QEMU console after the manual demo. + +Inside StarryOS: + +```bash +gdb /usr/bin/gdb-native-smoke-target +``` + +This starts the guest-side GDB and loads symbols for the native smoke target. + +Inside GDB: + +```gdb +break native_marker +run +bt +info registers +stepi +continue +quit +``` + +These commands set a breakpoint, run to it, print a backtrace and registers, +single-step once, then continue the target to normal exit. + +The native target uses a clear call chain: + +```text +main -> demo_entry -> demo_worker -> native_marker +``` + +## Native Thread GDB Smoke + +Use this command for the native GDB thread smoke: + +```bash +cargo xtask starry app qemu -t gdb-smoke --arch riscv64 \ + --qemu-config qemu-riscv64-threads.toml +``` + +The thread script enables GDB's multi-thread scheduling, breaks on +`thread_marker`, runs `info threads`, lists `/proc//task`, prints a +backtrace, deletes the breakpoint, and continues the target to normal exit. + +## GDBServer Smoke + +Use this command for the guest-internal gdbserver smoke: + +```bash +cargo xtask starry app qemu -t gdb-smoke --arch riscv64 \ + --qemu-config qemu-riscv64-gdbserver.toml +``` + +The default gdbserver script connects to `127.0.0.1:1234`, breaks on +`compute_value`, prints a backtrace, deletes the breakpoint, and continues the +remote target. + +Remote pthread gdbserver coverage is opt-in because it is slower and exercises +the heavier clone/thread event path: + +```bash +cargo xtask starry app qemu -t gdb-smoke --arch riscv64 \ + --qemu-config qemu-riscv64-gdbserver-debug.toml +``` + +Set `GDBSERVER_SMOKE_SERVER_DEBUG=1` in the QEMU config when gdbserver's own +debug trace is needed for a focused investigation. + +## GDB Stress + +Use this opt-in entry for the heavier ptrace/GDB stress path: + +```bash +cargo xtask starry app qemu -t gdb-smoke --arch riscv64 \ + --qemu-config qemu-riscv64-stress.toml +``` + +This stress target exercises a multi-threaded tracee with clone events, a +software breakpoint written through `/proc//mem`, register access, +single-step, and delayed tracer scheduling. It is intentionally kept out of the +default batch smoke and remote CI paths. + +## Host-To-Guest Remote GDB Demo + +Use this entry when you want the host to connect to guest `gdbserver` through +QEMU user-network port forwarding: + +```bash +cargo xtask starry app qemu -t gdb-smoke --arch riscv64 \ + --qemu-config qemu-riscv64-gdbserver-manual.toml +``` + +When running through the long-lived Docker container, keep stdin and a TTY +attached: + +```bash +docker exec -it tgoskits-dev cargo xtask starry app qemu -t gdb-smoke --arch riscv64 \ + --qemu-config qemu-riscv64-gdbserver-manual.toml +``` + +This manual config keeps the guest shell open and forwards host TCP port 1234 to +guest TCP port 1234. + +Inside StarryOS, start `gdbserver` and leave it waiting for the host GDB: + +```bash +gdbserver 0.0.0.0:1234 /usr/bin/gdbserver-smoke-target +``` + +`0.0.0.0:1234` is required for the QEMU host-forwarded connection. The command +blocks until the host GDB connects. + +On the host side, use the copied symbol file produced by `prebuild.sh` and keep +GDB interactive: + +```bash +gdb-multiarch -q -x apps/starry/gdb-smoke/gdbserver/host-manual.gdb \ + target/gdb-smoke-host/gdbserver-smoke-target +``` + +`host-manual.gdb` sets the riscv64 remote debugging defaults and connects to +`:1234`, but leaves you at the GDB prompt for manual commands. + +Inside host GDB: + +```gdb +break compute_value +continue +bt +info registers +detach +quit +``` + +These commands prove host-to-guest remote debugging: insert a breakpoint in the +guest process, continue to it, inspect stack/registers, then detach cleanly so +the guest target can finish. + +For the reproducible host-to-guest batch demo, start the guest-side +`gdbserver` with: + +```bash +cargo xtask starry app qemu -t gdb-smoke --arch riscv64 \ + --qemu-config qemu-riscv64-gdbserver-host.toml +``` + +This automatic config starts guest `gdbserver` for you and is intended for +repeatable logs rather than manual interaction. + +Then run the batch host script: + +```bash +gdb-multiarch -q -batch -x apps/starry/gdb-smoke/gdbserver/host-remote.gdb \ + target/gdb-smoke-host/gdbserver-smoke-target +``` + +`-batch` runs the scripted host GDB flow and exits after the marker output. diff --git a/apps/starry/gdb-smoke/gdbserver/gdbserver-smoke.gdb b/apps/starry/gdb-smoke/gdbserver/gdbserver-smoke.gdb index 3378e070d0..7a6858c131 100644 --- a/apps/starry/gdb-smoke/gdbserver/gdbserver-smoke.gdb +++ b/apps/starry/gdb-smoke/gdbserver/gdbserver-smoke.gdb @@ -4,6 +4,14 @@ set debuginfod enabled off set sysroot / set solib-search-path /lib:/usr/lib set remotetimeout 10 +set remote hostio-open-packet off +set remote hostio-pread-packet off target remote 127.0.0.1:1234 +break compute_value continue +bt +echo GDBSERVER_BREAKPOINT_DONE\n +delete breakpoints +continue +echo GDBSERVER_CONTINUE_DONE\n quit diff --git a/apps/starry/gdb-smoke/gdbserver/gdbserver-smoke.sh b/apps/starry/gdb-smoke/gdbserver/gdbserver-smoke.sh index 6349680d49..40cdd9a58e 100755 --- a/apps/starry/gdb-smoke/gdbserver/gdbserver-smoke.sh +++ b/apps/starry/gdb-smoke/gdbserver/gdbserver-smoke.sh @@ -2,34 +2,76 @@ set -eu log=/tmp/gdbserver-smoke.log -rm -f "$log" +gdb_timeout_seconds=${GDBSERVER_SMOKE_TIMEOUT:-90} -/usr/bin/gdbserver 0.0.0.0:1234 /usr/bin/gdbserver-smoke-target >"$log" 2>&1 & -server_pid=$! -trap 'kill "$server_pid" 2>/dev/null || true' EXIT +run_gdb_batch() { + script=$1 + target=$2 -listening=0 -for _ in 1 2 3 4 5 6 7 8 9 10; do - if grep -q "Listening on port 1234" "$log"; then - listening=1 - break + set -- /usr/bin/gdb -q -batch + if [ "${GDBSERVER_SMOKE_GDB_DEBUG:-0}" = 1 ]; then + set -- "$@" -ex "set debug remote 1" -ex "set debug infrun 1" fi - sleep 1 -done + set -- "$@" -x "$script" "$target" -cat "$log" -if [ "$listening" -ne 1 ]; then - echo "FAIL: gdbserver did not start listening" - exit 1 -fi + if command -v timeout >/dev/null 2>&1; then + timeout -s KILL "$gdb_timeout_seconds" "$@" + else + "$@" + fi +} -/usr/bin/gdb -q -batch -x /usr/bin/gdbserver-smoke.gdb /usr/bin/gdbserver-smoke-target +run_remote_smoke() { + target=$1 + script=$2 + + echo "GDBSERVER_PHASE_START target=$target script=$script" + rm -f "$log" + set -- /usr/bin/gdbserver + if [ "${GDBSERVER_SMOKE_SERVER_DEBUG:-0}" = 1 ]; then + set -- "$@" --debug + fi + set -- "$@" 0.0.0.0:1234 "$target" + "$@" >"$log" 2>&1 & + server_pid=$! + trap 'kill "$server_pid" 2>/dev/null || true' EXIT + + listening=0 + for _ in 1 2 3 4 5 6 7 8 9 10; do + if grep -q "Listening on port 1234" "$log"; then + listening=1 + break + fi + sleep 1 + done -if ! wait "$server_pid"; then cat "$log" - exit 1 + if [ "$listening" -ne 1 ]; then + echo "FAIL: gdbserver did not start listening for $target" + exit 1 + fi + + if ! run_gdb_batch "$script" "$target"; then + echo "FAIL: gdb batch failed for $target with $script" + cat "$log" + exit 1 + fi + + if ! wait "$server_pid"; then + cat "$log" + exit 1 + fi + trap - EXIT + + cat "$log" + echo "GDBSERVER_PHASE_DONE target=$target" +} + +run_remote_smoke /usr/bin/gdbserver-smoke-target /usr/bin/gdbserver-smoke.gdb +if [ "${GDBSERVER_SMOKE_THREADS:-0}" = 1 ]; then + run_remote_smoke /usr/bin/gdb-native-thread-target /usr/bin/gdbserver-threads.gdb +else + echo "GDBSERVER_THREADS_SKIPPED" fi -trap - EXIT -cat "$log" echo GDBSERVER_SMOKE_DONE diff --git a/apps/starry/gdb-smoke/gdbserver/gdbserver-threads.gdb b/apps/starry/gdb-smoke/gdbserver/gdbserver-threads.gdb new file mode 100644 index 0000000000..b98c06e697 --- /dev/null +++ b/apps/starry/gdb-smoke/gdbserver/gdbserver-threads.gdb @@ -0,0 +1,27 @@ +set pagination off +set confirm off +set debuginfod enabled off +set sysroot / +set solib-search-path /lib:/usr/lib +set remotetimeout 10 +set remote hostio-open-packet off +set remote hostio-pread-packet off +target remote 127.0.0.1:1234 +echo GDBSERVER_THREADS_CONNECTED\n +break thread_marker +echo GDBSERVER_THREADS_BREAKPOINT_SET\n +info threads +echo GDBSERVER_THREADS_INITIAL_LIST_DONE\n +continue +echo GDBSERVER_THREADS_BREAKPOINT_HIT\n +info threads +echo GDBSERVER_THREADS_LIST_DONE\n +bt +echo GDBSERVER_THREADS_BT_DONE\n +delete breakpoints +echo GDBSERVER_THREADS_BREAKPOINTS_DELETED\n +continue +echo GDBSERVER_THREADS_PENDING_TRAP_CONSUMED\n +continue +echo GDBSERVER_THREADS_CONTINUE_DONE\n +quit diff --git a/apps/starry/gdb-smoke/gdbserver/host-manual.gdb b/apps/starry/gdb-smoke/gdbserver/host-manual.gdb new file mode 100644 index 0000000000..0ae6ea3442 --- /dev/null +++ b/apps/starry/gdb-smoke/gdbserver/host-manual.gdb @@ -0,0 +1,11 @@ +set pagination off +set confirm off +set debuginfod enabled off +set architecture riscv:rv64 +set sysroot / +set solib-search-path /lib:/usr/lib +set remotetimeout 10 +set remote hostio-open-packet off +set remote hostio-pread-packet off +target remote :1234 +echo HOST_GDB_REMOTE_MANUAL_CONNECTED\n diff --git a/apps/starry/gdb-smoke/gdbserver/host-remote.gdb b/apps/starry/gdb-smoke/gdbserver/host-remote.gdb new file mode 100644 index 0000000000..7657fa8cdb --- /dev/null +++ b/apps/starry/gdb-smoke/gdbserver/host-remote.gdb @@ -0,0 +1,18 @@ +set pagination off +set confirm off +set debuginfod enabled off +set architecture riscv:rv64 +set sysroot / +set solib-search-path /lib:/usr/lib +set remotetimeout 10 +set remote hostio-open-packet off +set remote hostio-pread-packet off +target remote :1234 +echo HOST_GDB_REMOTE_CONNECTED\n +break compute_value +continue +bt +echo HOST_GDB_REMOTE_BT_DONE\n +detach +echo HOST_GDB_REMOTE_DETACH_DONE\n +quit diff --git a/apps/starry/gdb-smoke/gdbserver/src/main.c b/apps/starry/gdb-smoke/gdbserver/src/main.c index a4d22467a1..39b445309c 100644 --- a/apps/starry/gdb-smoke/gdbserver/src/main.c +++ b/apps/starry/gdb-smoke/gdbserver/src/main.c @@ -3,7 +3,7 @@ #define RISCV_HWCAP_ISA_D (1UL << ('D' - 'A')) -static int compute_value(void) +__attribute__((noinline)) static int compute_value(void) { return 40 + 2; } diff --git a/apps/starry/gdb-smoke/native-thread/gdb-native-threads.gdb b/apps/starry/gdb-smoke/native-thread/gdb-native-threads.gdb new file mode 100644 index 0000000000..e42022e2ed --- /dev/null +++ b/apps/starry/gdb-smoke/native-thread/gdb-native-threads.gdb @@ -0,0 +1,15 @@ +set pagination off +set confirm off +set debuginfod enabled off +set schedule-multiple on +break thread_marker +run +info threads +echo GDB_NATIVE_THREADS_LIST_DONE\n +shell pid="$(pidof gdb-native-thread-target)" && echo GDB_NATIVE_THREADS_TASK_BEGIN && ls "/proc/$pid/task" && echo GDB_NATIVE_THREADS_TASK_DONE +bt +echo GDB_NATIVE_THREADS_BT_DONE\n +delete breakpoints +continue +echo GDB_NATIVE_THREADS_DONE\n +quit diff --git a/apps/starry/gdb-smoke/native-thread/src/main.c b/apps/starry/gdb-smoke/native-thread/src/main.c new file mode 100644 index 0000000000..62ad14d16b --- /dev/null +++ b/apps/starry/gdb-smoke/native-thread/src/main.c @@ -0,0 +1,46 @@ +#include +#include + +struct worker_arg { + int id; + int result; +}; + +__attribute__((noinline)) static int thread_marker(int id) +{ + volatile int marker_id = id; + return marker_id + 100; +} + +static void *thread_worker(void *arg) +{ + struct worker_arg *worker = arg; + worker->result = thread_marker(worker->id); + return NULL; +} + +int main(void) +{ + pthread_t threads[2]; + struct worker_arg args[2] = { + {.id = 1, .result = 0}, + {.id = 2, .result = 0}, + }; + + for (int i = 0; i < 2; i++) { + if (pthread_create(&threads[i], NULL, thread_worker, &args[i]) != 0) { + perror("pthread_create"); + return 1; + } + } + + for (int i = 0; i < 2; i++) { + if (pthread_join(threads[i], NULL) != 0) { + perror("pthread_join"); + return 1; + } + } + + printf("gdb-native-thread-target results=%d,%d\n", args[0].result, args[1].result); + return args[0].result == 101 && args[1].result == 102 ? 0 : 1; +} diff --git a/apps/starry/gdb-smoke/native/gdb-native-smoke.gdb b/apps/starry/gdb-smoke/native/gdb-native-smoke.gdb index cf919ce3d3..d4c20df306 100644 --- a/apps/starry/gdb-smoke/native/gdb-native-smoke.gdb +++ b/apps/starry/gdb-smoke/native/gdb-native-smoke.gdb @@ -4,6 +4,20 @@ set debuginfod enabled off break native_marker run bt +echo GDB_NATIVE_BACKTRACE_DONE\n +info proc mappings +echo GDB_NATIVE_PROC_MAPPINGS_DONE\n +info files +echo GDB_NATIVE_PROC_FILES_DONE\n +info auxv +echo GDB_NATIVE_PROC_AUXV_DONE\n +shell pid="$(pidof gdb-native-smoke-target)" && echo GDB_NATIVE_PROC_STATUS_BEGIN && cat "/proc/$pid/status" && echo GDB_NATIVE_PROC_STATUS_DONE +info registers +echo GDB_NATIVE_REGS_DONE\n +x/4gx $sp +echo GDB_NATIVE_MEMORY_DONE\n +stepi +echo GDB_NATIVE_STEPI_DONE\n continue echo GDB_NATIVE_SMOKE_DONE\n quit diff --git a/apps/starry/gdb-smoke/native/src/main.c b/apps/starry/gdb-smoke/native/src/main.c index 93e014cd8e..8c34a9a719 100644 --- a/apps/starry/gdb-smoke/native/src/main.c +++ b/apps/starry/gdb-smoke/native/src/main.c @@ -5,9 +5,21 @@ __attribute__((noinline)) static int native_marker(int value) return value + 1; } +__attribute__((noinline)) static int demo_worker(int value) +{ + volatile int worker_value = value; + return native_marker(worker_value); +} + +__attribute__((noinline)) static int demo_entry(int value) +{ + volatile int entry_value = value; + return demo_worker(entry_value); +} + int main(void) { - int value = native_marker(41); + int value = demo_entry(41); printf("gdb-native-smoke-target value=%d\n", value); return value == 42 ? 0 : 1; } diff --git a/apps/starry/gdb-smoke/prebuild.sh b/apps/starry/gdb-smoke/prebuild.sh index 56de27a332..a49ba5141a 100755 --- a/apps/starry/gdb-smoke/prebuild.sh +++ b/apps/starry/gdb-smoke/prebuild.sh @@ -6,6 +6,7 @@ base_rootfs="${STARRY_ROOTFS:-}" staging_root="${STARRY_STAGING_ROOT:-}" overlay_dir="${STARRY_OVERLAY_DIR:-}" apk_cache="${STARRY_WORKSPACE:-$(cd "$app_dir/../../.." && pwd)}/target/gdb-smoke-apk-cache" +host_artifact_dir="${STARRY_WORKSPACE:-$(cd "$app_dir/../../.." && pwd)}/target/gdb-smoke-host" qemu_runner="" lld_linker="" lld_linker_dir="" @@ -196,10 +197,20 @@ populate_overlay() { "$app_dir/native/src/main.c" \ /usr/bin/gdb-native-smoke-target \ -Wall -Wextra -Werror -O0 -g + compile_target \ + "$app_dir/native-thread/src/main.c" \ + /usr/bin/gdb-native-thread-target \ + -Wall -Wextra -Werror -O0 -g -pthread compile_target \ "$app_dir/gdbserver/src/main.c" \ /usr/bin/gdbserver-smoke-target \ - -Wall -Wextra -Werror -g0 -s + -Wall -Wextra -Werror -O0 -g + compile_target \ + "$app_dir/stress/src/thread-breakpoint-wall.c" \ + /usr/bin/gdb-ptrace-thread-breakpoint-stress \ + -Wall -Wextra -Werror -O0 -g -pthread + install -Dm0755 "$overlay_dir/usr/bin/gdbserver-smoke-target" \ + "$host_artifact_dir/gdbserver-smoke-target" copy_file_to_overlay /usr/bin/gdb 0755 copy_file_to_overlay /usr/bin/gdbserver 0755 @@ -216,8 +227,12 @@ populate_overlay() { install -Dm0755 "$app_dir/native/gdb-native-smoke.gdb" \ "$overlay_dir/usr/bin/gdb-native-smoke.gdb" + install -Dm0755 "$app_dir/native-thread/gdb-native-threads.gdb" \ + "$overlay_dir/usr/bin/gdb-native-threads.gdb" install -Dm0644 "$app_dir/gdbserver/gdbserver-smoke.gdb" \ "$overlay_dir/usr/bin/gdbserver-smoke.gdb" + install -Dm0644 "$app_dir/gdbserver/gdbserver-threads.gdb" \ + "$overlay_dir/usr/bin/gdbserver-threads.gdb" install -Dm0755 "$app_dir/gdbserver/gdbserver-smoke.sh" \ "$overlay_dir/usr/bin/gdbserver-smoke.sh" } diff --git a/apps/starry/gdb-smoke/qemu-riscv64-gdbserver-debug.toml b/apps/starry/gdb-smoke/qemu-riscv64-gdbserver-debug.toml new file mode 100644 index 0000000000..3db7d21169 --- /dev/null +++ b/apps/starry/gdb-smoke/qemu-riscv64-gdbserver-debug.toml @@ -0,0 +1,28 @@ +args = [ + "-nographic", + "-m", "512M", + "-cpu", "rv64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "GDBSERVER_SMOKE_THREADS=1 GDBSERVER_SMOKE_TIMEOUT=90 /usr/bin/gdbserver-smoke.sh" +success_regex = ["GDBSERVER_SMOKE_DONE"] +fail_regex = [ + '(?i)\bpanic(?:ked)?\b', + '(?m)FAIL', + 'gdb: not found', + 'Python Exception', + 'Python initialization failed', + 'Cannot insert breakpoint', + 'Remote communication error', + 'ptrace: Function not implemented', + 'No stack', + 'Program received signal SIGSEGV', + '(?i)Segmentation fault', +] +timeout = 180 diff --git a/apps/starry/gdb-smoke/qemu-riscv64-gdbserver-host.toml b/apps/starry/gdb-smoke/qemu-riscv64-gdbserver-host.toml new file mode 100644 index 0000000000..db05d7204f --- /dev/null +++ b/apps/starry/gdb-smoke/qemu-riscv64-gdbserver-host.toml @@ -0,0 +1,27 @@ +args = [ + "-nographic", + "-m", "512M", + "-cpu", "rv64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0,hostfwd=tcp::1234-:1234", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "gdbserver 0.0.0.0:1234 /usr/bin/gdbserver-smoke-target; echo __HOST_REMOTE_GDBSERVER_DONE__" +success_regex = [ + "(?m)^__HOST_REMOTE_GDBSERVER_DONE__$", +] +fail_regex = [ + '(?i)\bpanic(?:ked)?\b', + '(?m)FAIL', + 'gdbserver: not found', + 'Cannot insert breakpoint', + 'Remote communication error', + 'ptrace: Function not implemented', + 'Program received signal SIGSEGV', + '(?i)Segmentation fault', +] +timeout = 240 diff --git a/apps/starry/gdb-smoke/qemu-riscv64-gdbserver-manual.toml b/apps/starry/gdb-smoke/qemu-riscv64-gdbserver-manual.toml new file mode 100644 index 0000000000..a64339fb8b --- /dev/null +++ b/apps/starry/gdb-smoke/qemu-riscv64-gdbserver-manual.toml @@ -0,0 +1,19 @@ +args = [ + "-nographic", + "-m", "512M", + "-cpu", "rv64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0,hostfwd=tcp::1234-:1234", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +success_regex = [] +fail_regex = [ + '(?i)\bpanic(?:ked)?\b', + '(?m)FAIL', + 'gdbserver: not found', +] +timeout = 0 diff --git a/apps/starry/gdb-smoke/qemu-riscv64-gdbserver.toml b/apps/starry/gdb-smoke/qemu-riscv64-gdbserver.toml index 05ea33471d..b5855d2072 100644 --- a/apps/starry/gdb-smoke/qemu-riscv64-gdbserver.toml +++ b/apps/starry/gdb-smoke/qemu-riscv64-gdbserver.toml @@ -18,7 +18,11 @@ fail_regex = [ 'gdb: not found', 'Python Exception', 'Python initialization failed', + 'Cannot insert breakpoint', 'Remote communication error', 'ptrace: Function not implemented', + 'No stack', + 'Program received signal SIGSEGV', + '(?i)Segmentation fault', ] -timeout = 120 +timeout = 180 diff --git a/apps/starry/gdb-smoke/qemu-riscv64-manual.toml b/apps/starry/gdb-smoke/qemu-riscv64-manual.toml new file mode 100644 index 0000000000..9310693d06 --- /dev/null +++ b/apps/starry/gdb-smoke/qemu-riscv64-manual.toml @@ -0,0 +1,21 @@ +args = [ + "-nographic", + "-m", "512M", + "-cpu", "rv64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +success_regex = [] +fail_regex = [ + '(?i)\bpanic(?:ked)?\b', + '(?m)FAIL', + 'gdb: not found', + 'Python Exception', + 'Python initialization failed', +] +timeout = 0 diff --git a/apps/starry/gdb-smoke/qemu-riscv64-stress.toml b/apps/starry/gdb-smoke/qemu-riscv64-stress.toml new file mode 100644 index 0000000000..80a1faa0b1 --- /dev/null +++ b/apps/starry/gdb-smoke/qemu-riscv64-stress.toml @@ -0,0 +1,19 @@ +args = [ + "-nographic", + "-m", "512M", + "-cpu", "rv64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/gdb-ptrace-thread-breakpoint-stress" +success_regex = ["DONE: 1 pass, 0 fail"] +fail_regex = [ + '(?i)\bpanic(?:ked)?\b', + '(?m)FAIL', +] +timeout = 120 diff --git a/apps/starry/gdb-smoke/qemu-riscv64-threads.toml b/apps/starry/gdb-smoke/qemu-riscv64-threads.toml new file mode 100644 index 0000000000..726d760798 --- /dev/null +++ b/apps/starry/gdb-smoke/qemu-riscv64-threads.toml @@ -0,0 +1,27 @@ +args = [ + "-nographic", + "-m", "512M", + "-cpu", "rv64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/gdb -q -batch -x /usr/bin/gdb-native-threads.gdb /usr/bin/gdb-native-thread-target" +success_regex = ["GDB_NATIVE_THREADS_DONE"] +fail_regex = [ + '(?i)\bpanic(?:ked)?\b', + '(?m)FAIL', + 'gdb: not found', + 'Python Exception', + 'Python initialization failed', + 'Cannot insert breakpoint', + 'ptrace: Function not implemented', + 'No stack', + 'Program received signal SIGSEGV', + '(?i)Segmentation fault', +] +timeout = 180 diff --git a/apps/starry/gdb-smoke/stress/src/thread-breakpoint-wall.c b/apps/starry/gdb-smoke/stress/src/thread-breakpoint-wall.c new file mode 100644 index 0000000000..803245f6af --- /dev/null +++ b/apps/starry/gdb-smoke/stress/src/thread-breakpoint-wall.c @@ -0,0 +1,438 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef NT_PRSTATUS +#define NT_PRSTATUS 1 +#endif +#ifndef PTRACE_SETOPTIONS +#define PTRACE_SETOPTIONS 0x4200 +#endif +#ifndef PTRACE_GETEVENTMSG +#define PTRACE_GETEVENTMSG 0x4201 +#endif +#ifndef PTRACE_GETSIGINFO +#define PTRACE_GETSIGINFO 0x4202 +#endif +#ifndef PTRACE_GETREGSET +#define PTRACE_GETREGSET 0x4204 +#endif +#ifndef PTRACE_SETREGSET +#define PTRACE_SETREGSET 0x4205 +#endif +#ifndef PTRACE_SINGLESTEP +#define PTRACE_SINGLESTEP 9 +#endif +#ifndef PTRACE_O_TRACECLONE +#define PTRACE_O_TRACECLONE 0x00000008 +#endif +#ifndef PTRACE_EVENT_CLONE +#define PTRACE_EVENT_CLONE 3 +#endif +#ifndef __WALL +#define __WALL 0x40000000 +#endif + +#define RISCV_EBREAK_INSN 0x00100073UL + +struct trace_addrs { + uintptr_t text_addr; +}; + +struct riscv_user_regs { + unsigned long pc; + unsigned long ra; + unsigned long sp; + unsigned long gp; + unsigned long tp; + unsigned long t0; + unsigned long t1; + unsigned long t2; + unsigned long s0; + unsigned long s1; + unsigned long a0; + unsigned long a1; + unsigned long a2; + unsigned long a3; + unsigned long a4; + unsigned long a5; + unsigned long a6; + unsigned long a7; + unsigned long s2; + unsigned long s3; + unsigned long s4; + unsigned long s5; + unsigned long s6; + unsigned long s7; + unsigned long s8; + unsigned long s9; + unsigned long s10; + unsigned long s11; + unsigned long t3; + unsigned long t4; + unsigned long t5; + unsigned long t6; +}; + +struct worker_arg { + int id; + int result; +}; + +__attribute__((noinline, aligned(8))) static int thread_marker(int id) +{ + volatile int marker_id = id; + return marker_id + 100; +} + +static void *worker_main(void *arg) +{ + struct worker_arg *worker = arg; + worker->result = thread_marker(worker->id); + return NULL; +} + +static int fail(const char *msg) +{ + printf("FAIL: %s: errno=%d (%s)\n", msg, errno, strerror(errno)); + return 1; +} + +static int mem_read_word(int fd, uintptr_t addr, unsigned long *out) +{ + ssize_t bytes = pread(fd, out, sizeof(*out), (off_t)addr); + if (bytes != (ssize_t)sizeof(*out)) { + errno = bytes < 0 ? errno : EIO; + return -1; + } + return 0; +} + +static int mem_write_word(int fd, uintptr_t addr, unsigned long word) +{ + ssize_t bytes = pwrite(fd, &word, sizeof(word), (off_t)addr); + if (bytes != (ssize_t)sizeof(word)) { + errno = bytes < 0 ? errno : EIO; + return -1; + } + return 0; +} + +static int get_regs(pid_t tid, struct riscv_user_regs *regs) +{ + struct iovec iov = {.iov_base = regs, .iov_len = sizeof(*regs)}; + if (ptrace(PTRACE_GETREGSET, tid, (void *)NT_PRSTATUS, &iov) != 0) { + return fail("getregset stopped tid"); + } + if (iov.iov_len == 0 || regs->pc == 0) { + printf("FAIL: invalid stopped tid regs iov_len=%ld pc=%#lx\n", (long)iov.iov_len, + regs->pc); + return 1; + } + return 0; +} + +static int set_regs(pid_t tid, struct riscv_user_regs *regs) +{ + struct iovec iov = {.iov_base = regs, .iov_len = sizeof(*regs)}; + if (ptrace(PTRACE_SETREGSET, tid, (void *)NT_PRSTATUS, &iov) != 0) { + return fail("setregset stopped tid"); + } + return 0; +} + +static int inspect_and_continue_stopped_thread(pid_t new_tid) +{ + siginfo_t siginfo; + if (ptrace(PTRACE_GETSIGINFO, new_tid, NULL, &siginfo) != 0) { + return fail("getsiginfo new tid"); + } + if (siginfo.si_signo != SIGSTOP) { + printf("FAIL: expected new tid SIGSTOP siginfo, got signo=%d\n", siginfo.si_signo); + return 1; + } + + struct riscv_user_regs regs = {0}; + if (get_regs(new_tid, ®s) != 0) { + return 1; + } + if (ptrace(PTRACE_SETOPTIONS, new_tid, NULL, (void *)PTRACE_O_TRACECLONE) != 0) { + return fail("setoptions new tid"); + } + if (ptrace(PTRACE_CONT, new_tid, NULL, NULL) != 0) { + return fail("cont new tid"); + } + + printf("INFO: inspected and continued new tid=%ld pc=%#lx\n", (long)new_tid, regs.pc); + return 0; +} + +static int inspect_and_continue_new_thread(unsigned long new_tid) +{ + int status = 0; + pid_t stopped = -1; + for (int tries = 0; tries < 50; tries++) { + stopped = waitpid((pid_t)new_tid, &status, __WALL | WNOHANG); + if (stopped != 0) { + break; + } + usleep(1000); + } + if (stopped != (pid_t)new_tid || !WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) { + printf("FAIL: expected new tid=%lu initial SIGSTOP before parent cont, got pid=%ld " + "status=%#x\n", + new_tid, (long)stopped, status); + return 1; + } + + if (inspect_and_continue_stopped_thread(stopped) != 0) { + return 1; + } + return 0; +} + +static int handle_clone_event(pid_t stopped, int *clone_count) +{ + unsigned long new_tid = 0; + if (ptrace(PTRACE_GETEVENTMSG, stopped, NULL, &new_tid) != 0) { + return fail("get clone event msg"); + } + if (inspect_and_continue_new_thread(new_tid) != 0) { + return 1; + } + if (ptrace(PTRACE_CONT, stopped, NULL, NULL) != 0) { + return fail("cont thread after clone event"); + } + + (*clone_count)++; + printf("INFO: consumed clone event %d for tid=%lu\n", *clone_count, new_tid); + return 0; +} + +static int handle_breakpoint_stop(pid_t stopped, uintptr_t marker_addr, int mem_fd, + unsigned long text_word, int *breakpoint_done) +{ + siginfo_t siginfo; + if (ptrace(PTRACE_GETSIGINFO, stopped, NULL, &siginfo) != 0) { + return fail("getsiginfo breakpoint tid"); + } + if (siginfo.si_signo != SIGTRAP) { + printf("FAIL: expected SIGTRAP siginfo for breakpoint tid=%ld, got signo=%d\n", + (long)stopped, siginfo.si_signo); + return 1; + } + + struct riscv_user_regs regs = {0}; + if (get_regs(stopped, ®s) != 0) { + return 1; + } + if (regs.pc != marker_addr) { + printf("FAIL: expected marker pc=%#lx from tid=%ld, got pc=%#lx\n", + (unsigned long)marker_addr, (long)stopped, regs.pc); + return 1; + } + if (*breakpoint_done) { + printf("FAIL: observed second breakpoint stop from tid=%ld after text restore\n", + (long)stopped); + return 1; + } + + printf("INFO: breakpoint stop from tid=%ld pc=%#lx\n", (long)stopped, regs.pc); + if (mem_write_word(mem_fd, marker_addr, text_word) != 0) { + return fail("restore marker text"); + } + + regs.pc = marker_addr; + if (set_regs(stopped, ®s) != 0) { + return 1; + } + if (ptrace(PTRACE_SINGLESTEP, stopped, NULL, NULL) != 0) { + return fail("singlestep stopped tid"); + } + + int status = 0; + if (waitpid(stopped, &status, __WALL) != stopped || !WIFSTOPPED(status) + || WSTOPSIG(status) != SIGTRAP) { + printf("FAIL: expected singlestep SIGTRAP from tid=%ld, status=%#x\n", (long)stopped, + status); + return 1; + } + if (ptrace(PTRACE_CONT, stopped, NULL, NULL) != 0) { + return fail("cont stopped tid after singlestep"); + } + + *breakpoint_done = 1; + return 0; +} + +static int run_trace_loop(pid_t leader, uintptr_t marker_addr, int mem_fd, unsigned long text_word) +{ + int clone_count = 0; + int breakpoint_done = 0; + + for (;;) { + int status = 0; + pid_t stopped = waitpid(-1, &status, __WALL); + if (stopped < 0) { + return fail("wait trace loop"); + } + if (stopped == leader && WIFEXITED(status)) { + if (WEXITSTATUS(status) != 42) { + printf("FAIL: expected child exit 42, status=%#x\n", status); + return 1; + } + if (clone_count != 2 || !breakpoint_done) { + printf("FAIL: child exited before expected events, clone_count=%d breakpoint=%d\n", + clone_count, breakpoint_done); + return 1; + } + return 0; + } + if (!WIFSTOPPED(status)) { + printf("FAIL: expected stop or exit, got pid=%ld status=%#x\n", (long)stopped, + status); + return 1; + } + if (WSTOPSIG(status) == SIGSTOP) { + if (inspect_and_continue_stopped_thread(stopped) != 0) { + return 1; + } + continue; + } + if (WSTOPSIG(status) != SIGTRAP) { + printf("FAIL: expected SIGTRAP stop or exit, got pid=%ld status=%#x\n", + (long)stopped, status); + return 1; + } + + unsigned int event = (unsigned int)status >> 16; + if (event == PTRACE_EVENT_CLONE) { + if (handle_clone_event(stopped, &clone_count) != 0) { + return 1; + } + continue; + } + if (event != 0) { + printf("FAIL: unexpected event=%u from pid=%ld status=%#x\n", event, (long)stopped, + status); + return 1; + } + if (handle_breakpoint_stop(stopped, marker_addr, mem_fd, text_word, &breakpoint_done) + != 0) { + return 1; + } + } +} + +static void run_child(int write_fd) +{ + if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) != 0) { + _exit(101); + } + + struct trace_addrs addrs = {.text_addr = (uintptr_t)thread_marker}; + if (write(write_fd, &addrs, sizeof(addrs)) != (ssize_t)sizeof(addrs)) { + _exit(102); + } + close(write_fd); + + if (kill(getpid(), SIGSTOP) != 0) { + _exit(103); + } + + pthread_t threads[2]; + struct worker_arg args[2] = { + {.id = 1, .result = 0}, + {.id = 2, .result = 0}, + }; + + for (int i = 0; i < 2; i++) { + if (pthread_create(&threads[i], NULL, worker_main, &args[i]) != 0) { + _exit(104); + } + } + for (int i = 0; i < 2; i++) { + if (pthread_join(threads[i], NULL) != 0) { + _exit(105); + } + } + + _exit(args[0].result == 101 && args[1].result == 102 ? 42 : 106); +} + +int main(void) +{ + int pipefd[2]; + if (pipe(pipefd) != 0) { + return fail("pipe"); + } + + pid_t pid = fork(); + if (pid < 0) { + return fail("fork"); + } + if (pid == 0) { + close(pipefd[0]); + run_child(pipefd[1]); + } + + close(pipefd[1]); + struct trace_addrs addrs = {0}; + if (read(pipefd[0], &addrs, sizeof(addrs)) != (ssize_t)sizeof(addrs)) { + return fail("read trace addrs"); + } + close(pipefd[0]); + + int status = 0; + if (waitpid(pid, &status, __WALL) != pid || !WIFSTOPPED(status) + || WSTOPSIG(status) != SIGSTOP) { + printf("FAIL: expected initial SIGSTOP, status=%#x\n", status); + return 1; + } + if (ptrace(PTRACE_SETOPTIONS, pid, NULL, (void *)PTRACE_O_TRACECLONE) != 0) { + return fail("set traceclone option"); + } + + char mem_path[64]; + snprintf(mem_path, sizeof(mem_path), "/proc/%d/mem", pid); + int mem_fd = open(mem_path, O_RDWR); + if (mem_fd < 0) { + return fail("open proc mem"); + } + + unsigned long text_word = 0; + if (mem_read_word(mem_fd, addrs.text_addr, &text_word) != 0) { + close(mem_fd); + return fail("read marker text"); + } + unsigned long breakpoint_word = (text_word & ~0xffffffffUL) | RISCV_EBREAK_INSN; + if (mem_write_word(mem_fd, addrs.text_addr, breakpoint_word) != 0) { + close(mem_fd); + return fail("write marker breakpoint"); + } + + if (ptrace(PTRACE_CONT, pid, NULL, NULL) != 0) { + close(mem_fd); + return fail("cont child to clone events"); + } + usleep(100000); + if (run_trace_loop(pid, addrs.text_addr, mem_fd, text_word) != 0) { + close(mem_fd); + return 1; + } + close(mem_fd); + + puts("DONE: 1 pass, 0 fail"); + return 0; +} diff --git a/os/StarryOS/kernel/src/pseudofs/proc.rs b/os/StarryOS/kernel/src/pseudofs/proc.rs index d6a9300880..8da163b35e 100644 --- a/os/StarryOS/kernel/src/pseudofs/proc.rs +++ b/os/StarryOS/kernel/src/pseudofs/proc.rs @@ -16,6 +16,7 @@ use core::{ }; use ax_lazyinit::LazyInit; +use ax_memory_addr::{MemoryAddr, VirtAddr}; use ax_runtime::hal::{ paging::MappingFlags, time::{monotonic_time, wall_time}, @@ -31,8 +32,8 @@ use crate::{ file::FD_TABLE, mm::{BackendFileInfo, ProcessMemStats}, pseudofs::{ - DirMaker, DirMapping, NodeOpsMux, RwFile, SeqObject, SimpleDir, SimpleDirOps, SimpleFile, - SimpleFileOperation, SimpleFs, SpecialFsFile, + DirMaker, DirMapping, DirectRwFsFileOps, NodeOpsMux, RwFile, SeqObject, SimpleDir, + SimpleDirOps, SimpleFile, SimpleFileOperation, SimpleFs, SpecialFsFile, }, task::{ AsThread, ProcessData, TaskStat, Thread, get_process_data, get_task, processes, tasks, @@ -370,24 +371,60 @@ impl SimpleDirOps for ProcessTaskDir { fn task_status(task: &AxTaskRef) -> String { let thread = task.as_thread(); let cred = thread.cred(); + let name = task.name(); let num_threads = thread.proc_data.proc.threads().len() as u32; let mem = ProcessMemStats::collect(&thread.proc_data.aspace().lock()); + let tracer_pid = thread.proc_data.ptrace_tracer_pid().unwrap_or(0); + let ppid = thread + .proc_data + .proc + .parent() + .map_or(0, |parent| parent.pid()); render_task_status( - thread.proc_data.proc.pid(), - thread.tid() as u64, - &cred, - num_threads, + TaskStatusBase { + name: &name, + state: task_status_state(task), + tgid: thread.proc_data.proc.pid(), + pid: thread.tid() as u64, + ppid, + tracer_pid, + cred: &cred, + num_threads, + }, task.cpumask(), ax_runtime::hal::cpu_num(), &mem, ) } -fn render_task_status( +fn task_status_state(task: &AxTaskRef) -> &'static str { + match task.state() { + TaskState::Running | TaskState::Ready => "R (running)", + TaskState::Blocked => "S (sleeping)", + TaskState::Exited => "Z (zombie)", + } +} + +struct TaskStatusBase<'a> { + name: &'a str, + state: &'a str, tgid: u32, pid: u64, - cred: &crate::task::Cred, + ppid: u32, + tracer_pid: u32, + cred: &'a crate::task::Cred, num_threads: u32, +} + +struct TaskStatusFields<'a> { + base: TaskStatusBase<'a>, + cpus_allowed: &'a str, + cpus_allowed_list: &'a str, + mem: &'a ProcessMemStats, +} + +fn render_task_status( + base: TaskStatusBase<'_>, cpumask: AxCpuMask, cpu_num: usize, mem: &ProcessMemStats, @@ -395,27 +432,17 @@ fn render_task_status( let cpus_allowed = format_cpumask_hex(cpumask, cpu_num); let cpus_allowed_list = format_cpumask_list(cpumask, cpu_num); - render_task_status_fields( - tgid, - pid, - cred, - num_threads, - &cpus_allowed, - &cpus_allowed_list, + render_task_status_fields(&TaskStatusFields { + base, + cpus_allowed: &cpus_allowed, + cpus_allowed_list: &cpus_allowed_list, mem, - ) + }) } #[rustfmt::skip] -fn render_task_status_fields( - tgid: u32, - pid: u64, - cred: &crate::task::Cred, - num_threads: u32, - cpus_allowed: &str, - cpus_allowed_list: &str, - mem: &ProcessMemStats, -) -> String { +fn render_task_status_fields(status: &TaskStatusFields<'_>) -> String { + let base = &status.base; // NOTE: `Threads:\t` is REQUIRED by psutil. `Process.num_threads()` // does `int(re.compile(br'Threads:\t(\d+)').findall(data)[0])`, which // raises an *uncaught* IndexError (not NoSuchProcess/AccessDenied/ @@ -424,19 +451,32 @@ fn render_task_status_fields( // The tab-separated `Uid:`/`Gid:` lines are likewise mandatory for // `Process.uids()`/`gids()`, which also index `findall(...)[0]` blindly. format!( - "Tgid:\t{tgid}\n\ - Pid:\t{pid}\n\ + "Name:\t{}\n\ + State:\t{}\n\ + Tgid:\t{}\n\ + Pid:\t{}\n\ + PPid:\t{}\n\ + TracerPid:\t{}\n\ Uid:\t{}\t{}\t{}\t{}\n\ Gid:\t{}\t{}\t{}\t{}\n\ - Threads:\t{num_threads}\n\ - {vm_lines}\ - Cpus_allowed:\t{cpus_allowed}\n\ - Cpus_allowed_list:\t{cpus_allowed_list}\n\ + Threads:\t{}\n\ + {}\ + Cpus_allowed:\t{}\n\ + Cpus_allowed_list:\t{}\n\ Mems_allowed:\t1\n\ Mems_allowed_list:\t0", - cred.uid, cred.euid, cred.suid, cred.fsuid, - cred.gid, cred.egid, cred.sgid, cred.fsgid, - vm_lines = mem.format_status_vm_lines(), + base.name, + base.state, + base.tgid, + base.pid, + base.ppid, + base.tracer_pid, + base.cred.uid, base.cred.euid, base.cred.suid, base.cred.fsuid, + base.cred.gid, base.cred.egid, base.cred.sgid, base.cred.fsgid, + base.num_threads, + status.mem.format_status_vm_lines(), + status.cpus_allowed, + status.cpus_allowed_list, ) } @@ -737,6 +777,74 @@ fn render_thread_auxv(task: &AxTaskRef) -> Vec { bytes } +struct ProcMemFile { + proc_data: Arc, +} + +impl ProcMemFile { + fn check_access(&self) -> VfsResult<()> { + let current_task = current(); + let current_proc = ¤t_task.as_thread().proc_data; + if current_proc.proc.pid() == self.proc_data.proc.pid() { + return Ok(()); + } + + let is_tracer = (self.proc_data.is_ptrace_traceme() || self.proc_data.is_ptrace_attached()) + && self + .proc_data + .ptrace_tracer_pid() + .is_some_and(|pid| pid == current_proc.proc.pid()) + && self.proc_data.ptrace_stop_signo().is_some(); + if is_tracer { + Ok(()) + } else { + Err(VfsError::PermissionDenied) + } + } + + fn populate_remote_range(&self, addr: usize, len: usize, flags: MappingFlags) -> VfsResult<()> { + if len == 0 { + return Ok(()); + } + let start = VirtAddr::from_usize(addr); + let end = VirtAddr::from_usize(addr.checked_add(len).ok_or(VfsError::BadAddress)?); + let page_start = start.align_down_4k(); + let page_end = end.align_up_4k(); + let aspace = self.proc_data.aspace(); + let mut aspace = aspace.lock(); + aspace.populate_area(page_start, page_end - page_start, flags) + } +} + +impl DirectRwFsFileOps for ProcMemFile { + fn read_at(&self, buf: &mut [u8], offset: u64) -> VfsResult { + self.check_access()?; + if buf.is_empty() { + return Ok(0); + } + let addr = usize::try_from(offset).map_err(|_| VfsError::BadAddress)?; + self.populate_remote_range(addr, buf.len(), MappingFlags::READ)?; + let aspace = self.proc_data.aspace(); + let aspace = aspace.lock(); + aspace.read(VirtAddr::from_usize(addr), buf)?; + Ok(buf.len()) + } + + fn write_at(&self, buf: &[u8], offset: u64) -> VfsResult { + self.check_access()?; + if buf.is_empty() { + return Ok(0); + } + let addr = usize::try_from(offset).map_err(|_| VfsError::BadAddress)?; + self.populate_remote_range(addr, buf.len(), MappingFlags::WRITE)?; + let aspace = self.proc_data.aspace(); + let aspace = aspace.lock(); + aspace.write(VirtAddr::from_usize(addr), buf)?; + ax_runtime::hal::cpu::asm::flush_icache_all(); + Ok(buf.len()) + } +} + impl SimpleDirOps for ThreadDir { fn child_names<'a>(&'a self) -> Box> + 'a> { Box::new( @@ -747,6 +855,7 @@ impl SimpleDirOps for ThreadDir { "oom_score_adj", "task", "maps", + "mem", "auxv", "mounts", "cmdline", @@ -789,13 +898,25 @@ impl SimpleDirOps for ThreadDir { if let Some(pid) = procfs_pid { let thread = task.as_thread(); let cred = thread.cred(); + let name = task.name(); let num_threads = thread.proc_data.proc.threads().len() as u32; let mem = ProcessMemStats::collect(&thread.proc_data.aspace().lock()); + let ppid = thread + .proc_data + .proc + .parent() + .map_or(0, |parent| parent.pid()); Ok(render_task_status( - pid, - pid as u64, - &cred, - num_threads, + TaskStatusBase { + name: &name, + state: task_status_state(&task), + tgid: pid, + pid: pid as u64, + ppid, + tracer_pid: thread.proc_data.ptrace_tracer_pid().unwrap_or(0), + cred: &cred, + num_threads, + }, task.cpumask(), ax_runtime::hal::cpu_num(), &mem, @@ -843,6 +964,14 @@ impl SimpleDirOps for ThreadDir { ) .into() } + "mem" => SpecialFsFile::new_regular_with_perm( + fs, + ProcMemFile { + proc_data: task.as_thread().proc_data.clone(), + }, + NodePermission::from_bits_truncate(0o600), + ) + .into(), "auxv" => SimpleFile::new_regular(fs, move || Ok(render_thread_auxv(&task))).into(), "mounts" => SimpleFile::new_regular(fs, move || { Ok("proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0\n") @@ -1289,8 +1418,8 @@ mod tests { use alloc::{format, string::String}; use super::{ - collect_cpu_presence, format_cpu_presence_hex, format_cpu_presence_list, - render_task_status_fields, + TaskStatusBase, TaskStatusFields, collect_cpu_presence, format_cpu_presence_hex, + format_cpu_presence_list, render_task_status_fields, }; use crate::{mm::ProcessMemStats, task::Cred}; @@ -1315,15 +1444,21 @@ mod tests { let cpus_allowed = format_cpu_presence_hex(&cpu_presence); let cpus_allowed_list = format_cpu_presence_list(&cpu_presence); - render_task_status_fields( - tgid, - pid, - &Cred::root(), - 1, - &cpus_allowed, - &cpus_allowed_list, - &sample_mem_stats(), - ) + render_task_status_fields(&TaskStatusFields { + base: TaskStatusBase { + name: "proc-status-test", + state: "R (running)", + tgid, + pid, + ppid: 0, + tracer_pid: 0, + cred: &Cred::root(), + num_threads: 1, + }, + cpus_allowed: &cpus_allowed, + cpus_allowed_list: &cpus_allowed_list, + mem: &sample_mem_stats(), + }) } #[test] @@ -1363,6 +1498,9 @@ mod tests { assert!(status.contains("Tgid:\t42\n")); assert!(status.contains("Pid:\t84\n")); + assert!(status.contains("Name:\tproc-status-test\n")); + assert!(status.contains("State:\tR (running)\n")); + assert!(status.contains("PPid:\t0\n")); assert!(status.contains("Cpus_allowed:\t0000000a\n")); assert!(status.contains("Cpus_allowed_list:\t1,3\n")); } @@ -1375,19 +1513,51 @@ mod tests { let cpu_presence = collect_cpu_presence([0usize], 1); let cpus_allowed = format_cpu_presence_hex(&cpu_presence); let cpus_allowed_list = format_cpu_presence_list(&cpu_presence); - let status = render_task_status_fields( - 1, - 1, - &Cred::root(), - 3, - &cpus_allowed, - &cpus_allowed_list, - &sample_mem_stats(), - ); + let status = render_task_status_fields(&TaskStatusFields { + base: TaskStatusBase { + name: "proc-status-test", + state: "S (sleeping)", + tgid: 1, + pid: 1, + ppid: 0, + tracer_pid: 0, + cred: &Cred::root(), + num_threads: 3, + }, + cpus_allowed: &cpus_allowed, + cpus_allowed_list: &cpus_allowed_list, + mem: &sample_mem_stats(), + }); assert!(status.contains("Threads:\t3\n")); // Tab-separated, exactly as the psutil regex expects (not space). assert!(!status.contains("Threads: 3")); + assert!(status.contains("State:\tS (sleeping)\n")); + } + + #[test] + fn task_status_reports_tracer_pid_for_debuggers() { + let cpu_presence = collect_cpu_presence([0usize], 1); + let cpus_allowed = format_cpu_presence_hex(&cpu_presence); + let cpus_allowed_list = format_cpu_presence_list(&cpu_presence); + let status = render_task_status_fields(&TaskStatusFields { + base: TaskStatusBase { + name: "proc-status-test", + state: "R (running)", + tgid: 10, + pid: 11, + ppid: 9, + tracer_pid: 42, + cred: &Cred::root(), + num_threads: 1, + }, + cpus_allowed: &cpus_allowed, + cpus_allowed_list: &cpus_allowed_list, + mem: &sample_mem_stats(), + }); + + assert!(status.contains("PPid:\t9\n")); + assert!(status.contains("TracerPid:\t42\n")); } #[test] @@ -1400,15 +1570,21 @@ mod tests { resident_pages: 128, ..Default::default() }; - let status = render_task_status_fields( - 1, - 1, - &Cred::root(), - 1, - &cpus_allowed, - &cpus_allowed_list, - &mem, - ); + let status = render_task_status_fields(&TaskStatusFields { + base: TaskStatusBase { + name: "proc-status-test", + state: "R (running)", + tgid: 1, + pid: 1, + ppid: 0, + tracer_pid: 0, + cred: &Cred::root(), + num_threads: 1, + }, + cpus_allowed: &cpus_allowed, + cpus_allowed_list: &cpus_allowed_list, + mem: &mem, + }); assert!(status.contains("VmSize:\t512 kB\n")); assert!(status.contains("VmRSS:\t512 kB\n")); diff --git a/os/StarryOS/kernel/src/syscall/task/clone.rs b/os/StarryOS/kernel/src/syscall/task/clone.rs index 38db20111a..9411ed25a1 100644 --- a/os/StarryOS/kernel/src/syscall/task/clone.rs +++ b/os/StarryOS/kernel/src/syscall/task/clone.rs @@ -372,20 +372,20 @@ impl CloneArgs { } else { super::ptrace::PTRACE_EVENT_FORK }; - let trace_clone = super::ptrace::ptrace_notify_clone(parent_pid, tid as Pid, ptrace_event); - if trace_clone - && !flags.contains(CloneFlags::THREAD) - && let Some(tracer_pid) = curr.as_thread().proc_data.ptrace_tracer_pid() - { - new_proc_data.set_ptrace_tracer_pid(tracer_pid); - new_proc_data.set_ptrace_attached(); - new_proc_data.set_ptrace_stop(starry_signal::Signo::SIGSTOP, &new_uctx); + let trace_clone = + super::ptrace::ptrace_notify_clone(parent_pid, parent_tid, tid as Pid, ptrace_event); + if trace_clone && let Some(tracer_pid) = curr.as_thread().proc_data.ptrace_tracer_pid() { + if !flags.contains(CloneFlags::THREAD) { + new_proc_data.set_ptrace_tracer_pid(tracer_pid); + new_proc_data.set_ptrace_attached(); + } + new_proc_data.set_ptrace_stop(tid, starry_signal::Signo::SIGSTOP, &new_uctx); } let task = spawn_task(new_task); add_task_to_table(&task); - if trace_clone { + if trace_clone && needs_vfork_block { let _ = crate::task::send_signal_to_thread( None, parent_tid, @@ -402,15 +402,7 @@ impl CloneArgs { // Block the parent until the child exec's or exits. if needs_vfork_block { new_proc_data.wait_vfork_done(); - if super::ptrace::ptrace_notify_vfork_done(parent_pid, tid as Pid) { - let _ = crate::task::send_signal_to_thread( - None, - parent_tid, - Some(starry_signal::SignalInfo::new_kernel( - starry_signal::Signo::SIGTRAP, - )), - ); - } + let _ = super::ptrace::ptrace_notify_vfork_done(parent_pid, tid as Pid); } Ok(tid as _) diff --git a/os/StarryOS/kernel/src/syscall/task/ptrace.rs b/os/StarryOS/kernel/src/syscall/task/ptrace.rs index ca3903eb72..6c13cc3756 100644 --- a/os/StarryOS/kernel/src/syscall/task/ptrace.rs +++ b/os/StarryOS/kernel/src/syscall/task/ptrace.rs @@ -13,7 +13,7 @@ use starry_vm::{VmMutPtr, VmPtr, vm_read_slice, vm_write_slice}; use crate::{ mm::{AddrSpace, IoVec}, - task::{AsThread, ProcessData, get_process_data}, + task::{AsThread, ProcessData, get_process_data, get_task}, }; const PTRACE_TRACEME: u32 = 0; @@ -125,7 +125,7 @@ pub fn sys_ptrace(request: u32, pid: usize, addr: usize, data: usize) -> AxResul PTRACE_ATTACH => ptrace_attach(pid), PTRACE_DETACH => ptrace_detach(pid, data), PTRACE_SYSCALL => ptrace_syscall(pid, data), - PTRACE_SETOPTIONS => ptrace_setoptions(pid, addr), + PTRACE_SETOPTIONS => ptrace_setoptions(pid, data), PTRACE_GETEVENTMSG => ptrace_geteventmsg(pid, data), PTRACE_GETSIGINFO => ptrace_getsiginfo(pid, data), PTRACE_SETSIGINFO => ptrace_setsiginfo(pid, data), @@ -162,10 +162,11 @@ fn ptrace_resume_signo(data: usize) -> AxResult { fn ptrace_cont(pid: usize, data: usize) -> AxResult { let signo = ptrace_resume_signo(data)?; - let tracee = ptrace_stopped_tracee(pid)?; - tracee.set_ptrace_singlestep(false); - tracee.set_ptrace_syscall_trace(false); - tracee.resume_ptrace_stop_with_signal(signo); + let (tracee, tid) = ptrace_stopped_tracee_with_tid(pid)?; + tracee.set_ptrace_singlestep_for(tid, false); + tracee.set_ptrace_syscall_trace_for(tid, false); + tracee.resume_ptrace_stop_with_signal_for(tid, signo); + ax_task::yield_now(); Ok(0) } @@ -186,10 +187,10 @@ fn ptrace_kill(pid: usize) -> AxResult { fn ptrace_singlestep(pid: usize, data: usize) -> AxResult { let signo = ptrace_resume_signo(data)?; - let tracee = ptrace_stopped_tracee(pid)?; - tracee.set_ptrace_singlestep(true); - tracee.set_ptrace_syscall_trace(false); - tracee.resume_ptrace_stop_with_signal(signo); + let (tracee, tid) = ptrace_stopped_tracee_with_tid(pid)?; + tracee.set_ptrace_singlestep_for(tid, true); + tracee.set_ptrace_syscall_trace_for(tid, false); + tracee.resume_ptrace_stop_with_signal_for(tid, signo); Ok(0) } @@ -219,23 +220,23 @@ fn ptrace_attach(pid: usize) -> AxResult { fn ptrace_detach(pid: usize, data: usize) -> AxResult { let signo = ptrace_resume_signo(data)?; - let tracee = ptrace_stopped_tracee(pid)?; + let (tracee, tid) = ptrace_stopped_tracee_with_tid(pid)?; tracee.clear_ptrace_traceme(); tracee.clear_ptrace_attached(); tracee.clear_ptrace_tracer_pid(); - tracee.set_ptrace_singlestep(false); - tracee.set_ptrace_syscall_trace(false); + tracee.set_ptrace_singlestep_for(tid, false); + tracee.set_ptrace_syscall_trace_for(tid, false); tracee.set_ptrace_options(0); - tracee.resume_ptrace_stop_with_signal(signo); + tracee.resume_ptrace_stop_with_signal_for(tid, signo); Ok(0) } fn ptrace_syscall(pid: usize, data: usize) -> AxResult { let signo = ptrace_resume_signo(data)?; - let tracee = ptrace_stopped_tracee(pid)?; - tracee.set_ptrace_singlestep(false); - tracee.set_ptrace_syscall_trace(true); - tracee.resume_ptrace_stop_with_signal(signo); + let (tracee, tid) = ptrace_stopped_tracee_with_tid(pid)?; + tracee.set_ptrace_singlestep_for(tid, false); + tracee.set_ptrace_syscall_trace_for(tid, true); + tracee.resume_ptrace_stop_with_signal_for(tid, signo); Ok(0) } @@ -256,16 +257,16 @@ fn ptrace_setoptions(pid: usize, options: usize) -> AxResult { } fn ptrace_geteventmsg(pid: usize, data: usize) -> AxResult { - let tracee = ptrace_stopped_tracee(pid)?; - let msg = tracee.ptrace_event_msg(); + let (tracee, tid) = ptrace_stopped_tracee_with_tid(pid)?; + let msg = tracee.ptrace_event_msg_for(tid); (data as *mut usize).vm_write(msg)?; Ok(0) } fn ptrace_getsiginfo(pid: usize, data: usize) -> AxResult { - let tracee = ptrace_stopped_tracee(pid)?; + let (tracee, tid) = ptrace_stopped_tracee_with_tid(pid)?; let siginfo = tracee - .ptrace_stop_siginfo() + .ptrace_stop_siginfo_for(tid) .ok_or_else(|| AxError::from(LinuxError::ESRCH))?; #[cfg(target_arch = "riscv64")] @@ -292,10 +293,10 @@ fn ptrace_setsiginfo(pid: usize, data: usize) -> AxResult { if data == 0 { return Err(AxError::InvalidInput); } - let tracee = ptrace_stopped_tracee(pid)?; + let (tracee, tid) = ptrace_stopped_tracee_with_tid(pid)?; let siginfo = ptrace_read_user_siginfo(data)?; let signo = ptrace_siginfo_signo(&siginfo)?; - if !tracee.set_ptrace_stop_siginfo(signo, starry_signal::SignalInfo(siginfo)) { + if !tracee.set_ptrace_stop_siginfo_for(tid, signo, starry_signal::SignalInfo(siginfo)) { return Err(AxError::from(LinuxError::ESRCH)); } Ok(0) @@ -481,9 +482,9 @@ fn ptrace_setregset_prstatus(pid: usize, data: usize) -> AxResult { #[cfg(target_arch = "riscv64")] fn ptrace_read_stopped_user_regs(pid: usize) -> AxResult { - let tracee = ptrace_stopped_tracee(pid)?; + let (tracee, tid) = ptrace_stopped_tracee_with_tid(pid)?; let uctx = tracee - .ptrace_stop_user_context() + .ptrace_stop_user_context_for(tid) .ok_or_else(|| AxError::from(LinuxError::ESRCH))?; Ok(RiscvUserRegs::from(&uctx)) } @@ -503,12 +504,12 @@ fn ptrace_read_user_regs(data: usize) -> AxResult { #[cfg(target_arch = "riscv64")] fn ptrace_write_stopped_user_regs(pid: usize, regs: RiscvUserRegs) -> AxResult { - let tracee = ptrace_stopped_tracee(pid)?; + let (tracee, tid) = ptrace_stopped_tracee_with_tid(pid)?; let mut uctx = tracee - .ptrace_stop_user_context() + .ptrace_stop_user_context_for(tid) .ok_or_else(|| AxError::from(LinuxError::ESRCH))?; regs.write_to(&mut uctx); - if !tracee.set_ptrace_stop_user_context(uctx) { + if !tracee.set_ptrace_stop_user_context_for(tid, uctx) { return Err(AxError::from(LinuxError::ESRCH)); } Ok(0) @@ -557,9 +558,9 @@ fn ptrace_setregset_fpregset(pid: usize, data: usize) -> AxResult { #[cfg(target_arch = "riscv64")] fn ptrace_read_stopped_fp_regs(pid: usize) -> AxResult { - let tracee = ptrace_stopped_tracee(pid)?; + let (tracee, tid) = ptrace_stopped_tracee_with_tid(pid)?; let fp_data = tracee - .ptrace_stop_fp_data() + .ptrace_stop_fp_data_for(tid) .ok_or_else(|| AxError::from(LinuxError::ESRCH))?; Ok(RiscvFpRegs { f: fp_data.0, @@ -601,8 +602,8 @@ fn ptrace_siginfo_signo(siginfo: &linux_raw_sys::general::siginfo_t) -> AxResult #[cfg(target_arch = "riscv64")] fn ptrace_write_stopped_fp_regs(pid: usize, regs: RiscvFpRegs) -> AxResult { - let tracee = ptrace_stopped_tracee(pid)?; - if !tracee.set_ptrace_stop_fp_data((regs.f, regs.fcsr)) { + let (tracee, tid) = ptrace_stopped_tracee_with_tid(pid)?; + if !tracee.set_ptrace_stop_fp_data_for(tid, (regs.f, regs.fcsr)) { return Err(AxError::from(LinuxError::ESRCH)); } Ok(0) @@ -762,6 +763,12 @@ fn process_vm_tracee(pid: usize) -> AxResult> { } } +fn ptrace_tracee_by_pid_or_tid(pid: Pid) -> AxResult> { + get_process_data(pid) + .or_else(|_| get_task(pid).map(|task| task.as_thread().proc_data.clone())) + .map_err(|_| AxError::from(LinuxError::ESRCH)) +} + fn read_iovecs(iov: *const IoVec, iovcnt: usize) -> AxResult> { if iovcnt > 1024 { return Err(AxError::InvalidInput); @@ -809,9 +816,13 @@ fn remote_write(tracee: &ProcessData, addr: usize, data: &[u8]) -> AxResult { } fn ptrace_stopped_tracee(pid: usize) -> AxResult> { + ptrace_stopped_tracee_with_tid(pid).map(|(tracee, _tid)| tracee) +} + +fn ptrace_stopped_tracee_with_tid(pid: usize) -> AxResult<(Arc, u32)> { let pid = Pid::try_from(pid).map_err(|_| AxError::from(LinuxError::ESRCH))?; let tracer_pid = current().as_thread().proc_data.proc.pid(); - let tracee = get_process_data(pid).map_err(|_| AxError::from(LinuxError::ESRCH))?; + let tracee = ptrace_tracee_by_pid_or_tid(pid)?; let is_tracer = (tracee.is_ptrace_traceme() || tracee.is_ptrace_attached()) && tracee .ptrace_tracer_pid() @@ -819,19 +830,30 @@ fn ptrace_stopped_tracee(pid: usize) -> AxResult> { if !is_tracer || tracee.ptrace_stop_signo().is_none() { return Err(AxError::from(LinuxError::ESRCH)); } - Ok(tracee) + if pid == tracee.proc.pid() { + if tracee.ptrace_stop_signo_for(pid).is_some() { + tracee.select_ptrace_stop(pid); + } + } else if !tracee.select_ptrace_stop(pid) { + return Err(AxError::from(LinuxError::ESRCH)); + } + let tid = tracee + .selected_ptrace_stop_tid() + .ok_or_else(|| AxError::from(LinuxError::ESRCH))?; + Ok((tracee, tid)) } #[cfg(target_arch = "riscv64")] pub fn ptrace_setup_singlestep( tracee: &ProcessData, + tid: Pid, uctx: &mut ax_runtime::hal::cpu::uspace::UserContext, ) { let pc = uctx.ip(); let aspace = tracee.aspace(); let mut aspace = aspace.lock(); - let saved = tracee.take_ptrace_ss_saved_insn(); + let saved = tracee.take_ptrace_ss_saved_insn_for(tid); if let Some((saved_addr, saved_insn)) = saved { let _ = ptrace_write_u16_unlocked(&mut aspace, saved_addr, saved_insn as u16); } @@ -839,7 +861,7 @@ pub fn ptrace_setup_singlestep( let first_half = match ptrace_read_u16_unlocked(&aspace, pc) { Ok(half) => half, Err(_) => { - tracee.set_ptrace_ss_saved_insn(None); + tracee.set_ptrace_ss_saved_insn_for(tid, None); return; } }; @@ -850,31 +872,31 @@ pub fn ptrace_setup_singlestep( match ptrace_read_u32_unlocked(&aspace, pc) { Ok(word) => word, Err(_) => { - tracee.set_ptrace_ss_saved_insn(None); + tracee.set_ptrace_ss_saved_insn_for(tid, None); return; } } }; let next_insn_addr = riscv_next_pc(current_insn, insn_len, pc, uctx); if next_insn_addr == pc { - tracee.set_ptrace_ss_saved_insn(None); + tracee.set_ptrace_ss_saved_insn_for(tid, None); return; } let orig_insn = match ptrace_read_u16_unlocked(&aspace, next_insn_addr) { Ok(half) => half, Err(_) => { - tracee.set_ptrace_ss_saved_insn(None); + tracee.set_ptrace_ss_saved_insn_for(tid, None); return; } }; if orig_insn == EBREAK_INSN { - tracee.set_ptrace_ss_saved_insn(None); + tracee.set_ptrace_ss_saved_insn_for(tid, None); ax_runtime::hal::cpu::asm::flush_icache_all(); return; } let _ = ptrace_write_u16_unlocked(&mut aspace, next_insn_addr, EBREAK_INSN); - tracee.set_ptrace_ss_saved_insn(Some((next_insn_addr, orig_insn as usize))); + tracee.set_ptrace_ss_saved_insn_for(tid, Some((next_insn_addr, orig_insn as usize))); ax_runtime::hal::cpu::asm::flush_icache_all(); } @@ -1080,7 +1102,7 @@ fn ptrace_write_u16_unlocked(aspace: &mut AddrSpace, addr: usize, data: u16) -> Ok(()) } -pub fn ptrace_notify_clone(parent_pid: Pid, child_pid: Pid, event: u32) -> bool { +pub fn ptrace_notify_clone(parent_pid: Pid, parent_tid: Pid, child_pid: Pid, event: u32) -> bool { let Ok(parent) = get_process_data(parent_pid) else { return false; }; @@ -1097,8 +1119,7 @@ pub fn ptrace_notify_clone(parent_pid: Pid, child_pid: Pid, event: u32) -> bool if options & option_flag == 0 { return false; } - parent.set_ptrace_event_msg(child_pid as usize); - parent.set_ptrace_event(event); + parent.set_ptrace_pending_event(parent_tid, event, child_pid as usize); true } @@ -1110,8 +1131,7 @@ pub fn ptrace_notify_exec(tracee_pid: Pid) -> bool { if options & PTRACE_O_TRACEEXEC == 0 { return false; } - tracee.set_ptrace_event_msg(tracee_pid as usize); - tracee.set_ptrace_event(PTRACE_EVENT_EXEC); + tracee.set_ptrace_pending_event(tracee_pid, PTRACE_EVENT_EXEC, tracee_pid as usize); true } @@ -1126,8 +1146,7 @@ pub fn ptrace_notify_exit(tracee_pid: Pid, exit_code: i32) -> bool { if options & PTRACE_O_TRACEEXIT == 0 { return false; } - tracee.set_ptrace_event_msg(exit_code as usize); - tracee.set_ptrace_event(PTRACE_EVENT_EXIT); + tracee.set_ptrace_pending_event(tracee_pid, PTRACE_EVENT_EXIT, exit_code as usize); true } @@ -1142,8 +1161,7 @@ pub fn ptrace_notify_vfork_done(parent_pid: Pid, child_pid: Pid) -> bool { if options & PTRACE_O_TRACEVFORKDONE == 0 { return false; } - parent.set_ptrace_event_msg(child_pid as usize); - parent.set_ptrace_event(PTRACE_EVENT_VFORK_DONE); + parent.set_ptrace_pending_event(parent_pid, PTRACE_EVENT_VFORK_DONE, child_pid as usize); true } diff --git a/os/StarryOS/kernel/src/syscall/task/wait.rs b/os/StarryOS/kernel/src/syscall/task/wait.rs index 436b9d598b..14a1a75514 100644 --- a/os/StarryOS/kernel/src/syscall/task/wait.rs +++ b/os/StarryOS/kernel/src/syscall/task/wait.rs @@ -71,6 +71,31 @@ impl WaitTarget { WaitTarget::Pgid(pgid) => child.group().pgid() == *pgid, } } + + fn matches_process_or_thread(&self, child: &Process) -> bool { + self.matches(child) || matches!(self, WaitTarget::Pid(pid) if child.threads().contains(pid)) + } + + fn ptrace_report_pid(&self, child: &Process, data: &ProcessData) -> Pid { + match self { + WaitTarget::Pid(pid) if *pid == child.pid() || child.threads().contains(pid) => *pid, + _ => data.ptrace_stop_tid().unwrap_or(child.pid()), + } + } + + fn ptrace_preferred_stop_tid(&self, child: &Process) -> Option { + match self { + WaitTarget::Pid(pid) if *pid != child.pid() && child.threads().contains(pid) => { + Some(*pid) + } + WaitTarget::Pid(pid) if *pid == child.pid() => Some(*pid), + _ => None, + } + } + + fn ptrace_requires_exact_stop(&self, child: &Process) -> bool { + matches!(self, WaitTarget::Pid(pid) if *pid != child.pid() && child.threads().contains(pid)) + } } fn waitid_pidfd_target(fd: i32) -> AxResult { @@ -129,7 +154,7 @@ fn waitable_processes(proc: &Process, target: WaitTarget, tracer_pid: Pid) -> Ve let traced = data.ptrace_tracer_pid() == Some(tracer_pid); let proc = data.proc.clone(); if traced - && target.matches(&proc) + && target.matches_process_or_thread(&proc) && !candidates .iter() .any(|candidate| candidate.pid() == proc.pid()) @@ -168,8 +193,6 @@ pub fn sys_waitpid(pid: i32, exit_code: *mut i32, options: u32) -> AxResult AxResult, + uctx: UserContext, + siginfo: Option, + is_syscall: bool, + reported: bool, + event: u32, + event_msg: usize, +} + +struct PtracePendingEvent { + event: u32, + msg: usize, +} use crate::mm::AddrSpace; /// Size of the syscall instruction for the current architecture. @@ -632,32 +647,23 @@ pub struct ProcessData { /// stops from this process. ptrace_traceme: AtomicBool, - /// Non-zero signal number while this traced process is stopped. - ptrace_stop_signo: AtomicU32, - - /// User register snapshot captured when entering the current ptrace stop. - ptrace_stop_uctx: SpinNoIrq>, - - /// siginfo for the current ptrace stop, used by `PTRACE_GETSIGINFO`. - ptrace_stop_siginfo: SpinNoIrq>, - - /// True when the current SIGTRAP stop is a PTRACE_SYSCALL entry/exit stop. - ptrace_stop_is_syscall: AtomicBool, + /// Current ptrace stop records, keyed by stopped TID. + ptrace_stop: SpinNoIrq>, - /// True after wait* has already reported the current ptrace stop. - ptrace_stop_reported: AtomicBool, + /// TID selected by the most recent ptrace request. + ptrace_stop_tid: AtomicU32, /// Wakes a traced task that is sleeping in a ptrace stop. ptrace_stop_event: Arc, - /// Signal number to deliver on resume, set by `PTRACE_CONT(sig)`. + /// Signal number to deliver on resume, keyed by resumed TID. /// 0 means suppress the signal; non-zero means deliver that signal. - ptrace_resume_signo: AtomicU32, + ptrace_resume_signo: SpinNoIrq>, /// One-shot signal number that came from ptrace resume injection. /// The signal subsystem still handles disposition and handlers, but the /// next matching signal delivery must not stop for ptrace again. - ptrace_resume_signal_bypass: AtomicU32, + ptrace_resume_signal_bypass: SpinNoIrq>, /// Set by `execve` when the calling thread was `PTRACE_TRACEME`. /// Cleared after the exec-stop is delivered in the user-return loop. @@ -666,27 +672,24 @@ pub struct ProcessData { /// Set by `PTRACE_ATTACH` / `PTRACE_SEIZE`. ptrace_attached: AtomicBool, - /// Set by `PTRACE_SINGLESTEP`; causes a temporary EBREAK insertion. - ptrace_singlestep: AtomicBool, + /// TID selected by `PTRACE_SINGLESTEP`; causes a temporary EBREAK insertion. + ptrace_singlestep_tid: AtomicU32, - /// Set by `PTRACE_SYSCALL`; causes syscall-entry/exit stops. - ptrace_syscall_trace: SpinNoIrq, + /// Set by `PTRACE_SYSCALL`; causes syscall-entry/exit stops, keyed by TID. + ptrace_syscall_trace: SpinNoIrq>, /// Bitmask of PTRACE_O_* options set via `PTRACE_SETOPTIONS`. ptrace_options: AtomicUsize, - /// Event message stored by `PTRACE_EVENT_*` (e.g. new child PID). - ptrace_event_msg: AtomicUsize, + /// Pending ptrace events that have not yet been bound to their owner TID stops. + ptrace_pending_event: SpinNoIrq>, - /// Pending ptrace event code (PTRACE_EVENT_FORK etc.), or 0 for none. - ptrace_event: AtomicU32, + /// Saved instruction overwritten by single-step EBREAK, keyed by TID. + ptrace_ss_saved_insn: SpinNoIrq>, - /// Saved instruction overwritten by single-step EBREAK, if any. - ptrace_ss_saved_insn: SpinNoIrq>, - - /// FP register snapshot captured when entering ptrace stop. + /// FP register snapshot captured when entering ptrace stop, keyed by TID. /// Stored as raw bytes to avoid arch-specific crate dependency. - ptrace_stop_fp_data: SpinNoIrq>, + ptrace_stop_fp_data: SpinNoIrq>, /// Linux process personality flags. Starry does not randomize userspace /// mappings yet, but debuggers still probe and set ADDR_NO_RANDOMIZE. @@ -766,23 +769,19 @@ impl ProcessData { ptrace_tracer_pid: AtomicU32::new(0), ptrace_traceme: AtomicBool::new(false), - ptrace_stop_signo: AtomicU32::new(0), - ptrace_stop_uctx: SpinNoIrq::new(None), - ptrace_stop_siginfo: SpinNoIrq::new(None), - ptrace_stop_is_syscall: AtomicBool::new(false), - ptrace_stop_reported: AtomicBool::new(false), + ptrace_stop: SpinNoIrq::new(BTreeMap::new()), + ptrace_stop_tid: AtomicU32::new(0), ptrace_stop_event: Arc::default(), - ptrace_resume_signo: AtomicU32::new(0), - ptrace_resume_signal_bypass: AtomicU32::new(0), + ptrace_resume_signo: SpinNoIrq::new(BTreeMap::new()), + ptrace_resume_signal_bypass: SpinNoIrq::new(BTreeMap::new()), ptrace_exec_stop_pending: AtomicBool::new(false), ptrace_attached: AtomicBool::new(false), - ptrace_singlestep: AtomicBool::new(false), - ptrace_syscall_trace: SpinNoIrq::new(SyscallTraceState::None), + ptrace_singlestep_tid: AtomicU32::new(0), + ptrace_syscall_trace: SpinNoIrq::new(BTreeMap::new()), ptrace_options: AtomicUsize::new(0), - ptrace_event_msg: AtomicUsize::new(0), - ptrace_event: AtomicU32::new(0), - ptrace_ss_saved_insn: SpinNoIrq::new(None), - ptrace_stop_fp_data: SpinNoIrq::new(None), + ptrace_pending_event: SpinNoIrq::new(BTreeMap::new()), + ptrace_ss_saved_insn: SpinNoIrq::new(BTreeMap::new()), + ptrace_stop_fp_data: SpinNoIrq::new(BTreeMap::new()), personality: AtomicUsize::new(0), @@ -1045,83 +1044,239 @@ impl ProcessData { } /// Record that this tracee is stopped by `signo`. - pub fn set_ptrace_stop(&self, signo: Signo, uctx: &UserContext) { - *self.ptrace_stop_uctx.lock() = Some(*uctx); - *self.ptrace_stop_siginfo.lock() = Some(SignalInfo::new_kernel(signo)); - self.ptrace_stop_is_syscall.store(false, Ordering::Release); - self.ptrace_stop_reported.store(false, Ordering::Release); - self.ptrace_stop_signo - .store(signo as u32, Ordering::Release); + pub fn set_ptrace_stop(&self, tid: u32, signo: Signo, uctx: &UserContext) { + let pending_event = self.ptrace_pending_event.lock().remove(&tid); + self.ptrace_stop.lock().insert( + tid, + PtraceStopRecord { + signo: Some(signo), + uctx: *uctx, + siginfo: Some(SignalInfo::new_kernel(signo)), + is_syscall: false, + reported: false, + event: pending_event.as_ref().map_or(0, |event| event.event), + event_msg: pending_event.as_ref().map_or(0, |event| event.msg), + }, + ); + self.ptrace_stop_tid.store(tid, Ordering::Release); } /// Record that this tracee is stopped at a syscall entry or exit boundary. - pub fn set_ptrace_syscall_stop(&self, signo: Signo, uctx: &UserContext) { - self.set_ptrace_stop(signo, uctx); - self.ptrace_stop_is_syscall.store(true, Ordering::Release); + pub fn set_ptrace_syscall_stop(&self, tid: u32, signo: Signo, uctx: &UserContext) { + self.set_ptrace_stop(tid, signo, uctx); + if let Some(stop) = self.ptrace_stop.lock().get_mut(&tid) { + stop.is_syscall = true; + } + } + + pub fn ptrace_stop_tid(&self) -> Option { + let stops = self.ptrace_stop.lock(); + stops + .iter() + .find_map(|(tid, stop)| (!stop.reported && stop.signo.is_some()).then_some(*tid)) + .or_else(|| stops.keys().next().copied()) + } + + pub fn select_ptrace_stop(&self, tid: u32) -> bool { + if self.ptrace_stop.lock().contains_key(&tid) { + self.ptrace_stop_tid.store(tid, Ordering::Release); + true + } else { + false + } + } + + pub fn selected_ptrace_stop_tid(&self) -> Option { + let selected = self.ptrace_stop_tid.load(Ordering::Acquire); + let stops = self.ptrace_stop.lock(); + if selected != 0 + && stops + .get(&selected) + .is_some_and(|stop| stop.signo.is_some()) + { + Some(selected) + } else { + stops + .iter() + .find_map(|(tid, stop)| stop.signo.is_some().then_some(*tid)) + } + } + + pub fn has_ptrace_stop(&self, tid: u32) -> bool { + self.ptrace_stop.lock().contains_key(&tid) + } + + pub fn ptrace_stop_signo_for(&self, tid: u32) -> Option { + self.ptrace_stop + .lock() + .get(&tid) + .and_then(|stop| stop.signo) + } + + pub fn ptrace_unreported_stop(&self, preferred_tid: Option) -> Option<(u32, Signo)> { + { + let stops = self.ptrace_stop.lock(); + if let Some(tid) = preferred_tid + && let Some(stop) = stops.get(&tid) + && !stop.reported + && let Some(signo) = stop.signo + { + return Some((tid, signo)); + } + if let Some((tid, stop)) = stops + .iter() + .find(|(_, stop)| !stop.reported && stop.signo.is_some() && stop.event != 0) + { + return stop.signo.map(|signo| (*tid, signo)); + } + } + + if !self.ptrace_pending_event.lock().is_empty() { + return None; + } + + self.ptrace_stop.lock().iter().find_map(|(tid, stop)| { + (!stop.reported) + .then_some(stop.signo) + .flatten() + .map(|signo| (*tid, signo)) + }) + } + + pub fn ptrace_unreported_stop_for(&self, tid: u32) -> Option<(u32, Signo)> { + self.ptrace_stop.lock().get(&tid).and_then(|stop| { + (!stop.reported) + .then_some(stop.signo) + .flatten() + .map(|signo| (tid, signo)) + }) } pub fn is_ptrace_syscall_stop(&self) -> bool { - self.ptrace_stop_is_syscall.load(Ordering::Acquire) + let Some(tid) = self.selected_ptrace_stop_tid() else { + return false; + }; + self.is_ptrace_syscall_stop_for(tid) + } + + pub fn is_ptrace_syscall_stop_for(&self, tid: u32) -> bool { + self.ptrace_stop + .lock() + .get(&tid) + .is_some_and(|stop| stop.is_syscall) } /// Return the siginfo for the current ptrace stop. pub fn ptrace_stop_siginfo(&self) -> Option { - self.ptrace_stop_siginfo.lock().clone() + let tid = self.selected_ptrace_stop_tid()?; + self.ptrace_stop_siginfo_for(tid) + } + + pub fn ptrace_stop_siginfo_for(&self, tid: u32) -> Option { + self.ptrace_stop + .lock() + .get(&tid) + .and_then(|stop| stop.siginfo.clone()) } /// Replace the siginfo held for the current ptrace stop. pub fn set_ptrace_stop_siginfo(&self, signo: Signo, siginfo: SignalInfo) -> bool { - let mut saved = self.ptrace_stop_siginfo.lock(); - if saved.is_none() { + let Some(tid) = self.selected_ptrace_stop_tid() else { return false; - } - *saved = Some(siginfo); - self.ptrace_stop_signo - .store(signo as u32, Ordering::Release); + }; + self.set_ptrace_stop_siginfo_for(tid, signo, siginfo) + } + + pub fn set_ptrace_stop_siginfo_for(&self, tid: u32, signo: Signo, siginfo: SignalInfo) -> bool { + let mut stops = self.ptrace_stop.lock(); + let Some(stop) = stops.get_mut(&tid) else { + return false; + }; + stop.signo = Some(signo); + stop.siginfo = Some(siginfo); true } /// Return the current ptrace stop signal, if any. pub fn ptrace_stop_signo(&self) -> Option { - let signo = self.ptrace_stop_signo.load(Ordering::Acquire); - if signo == 0 { - None - } else { - Signo::from_repr(signo as u8) - } + let stops = self.ptrace_stop.lock(); + stops + .values() + .find_map(|stop| (!stop.reported).then_some(stop.signo).flatten()) + .or_else(|| stops.values().find_map(|stop| stop.signo)) + } + + pub fn claim_ptrace_stop(&self, tid: u32) -> bool { + !self.ptrace_stop.lock().contains_key(&tid) } /// Return the saved user context for the current ptrace stop. pub fn ptrace_stop_user_context(&self) -> Option { - *self.ptrace_stop_uctx.lock() + let tid = self.selected_ptrace_stop_tid()?; + self.ptrace_stop_user_context_for(tid) + } + + pub fn ptrace_stop_user_context_for(&self, tid: u32) -> Option { + self.ptrace_stop.lock().get(&tid).map(|stop| stop.uctx) } pub fn ptrace_stop_reported(&self) -> bool { - self.ptrace_stop_reported.load(Ordering::Acquire) + self.ptrace_stop + .lock() + .values() + .all(|stop| stop.reported || stop.signo.is_none()) } pub fn mark_ptrace_stop_reported(&self) { - self.ptrace_stop_reported.store(true, Ordering::Release); + let mut stops = self.ptrace_stop.lock(); + if let Some(stop) = stops + .values_mut() + .find(|stop| !stop.reported && stop.signo.is_some()) + { + stop.reported = true; + } + } + + pub fn mark_ptrace_stop_reported_for(&self, tid: u32) { + if let Some(stop) = self.ptrace_stop.lock().get_mut(&tid) { + stop.reported = true; + } } /// Replace registers held for a stopped tracee. pub fn set_ptrace_stop_user_context(&self, uctx: UserContext) -> bool { - let mut saved = self.ptrace_stop_uctx.lock(); - if saved.is_none() { + let Some(tid) = self.selected_ptrace_stop_tid() else { return false; - } - *saved = Some(uctx); + }; + self.set_ptrace_stop_user_context_for(tid, uctx) + } + + pub fn set_ptrace_stop_user_context_for(&self, tid: u32, uctx: UserContext) -> bool { + let mut stops = self.ptrace_stop.lock(); + let Some(stop) = stops.get_mut(&tid) else { + return false; + }; + stop.uctx = uctx; true } /// Resume the stopped task, optionally injecting a signal. pub fn resume_ptrace_stop_with_signal(&self, signo: u32) { - self.ptrace_resume_signo.store(signo, Ordering::Release); - *self.ptrace_stop_siginfo.lock() = None; - self.ptrace_stop_is_syscall.store(false, Ordering::Release); - self.ptrace_stop_reported.store(false, Ordering::Release); - self.ptrace_event.store(0, Ordering::Release); - self.ptrace_stop_signo.store(0, Ordering::Release); + if let Some(tid) = self.selected_ptrace_stop_tid() { + self.resume_ptrace_stop_with_signal_for(tid, signo); + } + } + + pub fn resume_ptrace_stop_with_signal_for(&self, tid: u32, signo: u32) { + if let Some(stop) = self.ptrace_stop.lock().get_mut(&tid) { + self.ptrace_resume_signo.lock().insert(tid, signo); + stop.signo = None; + stop.siginfo = None; + stop.is_syscall = false; + stop.reported = false; + stop.event = 0; + stop.event_msg = 0; + } self.ptrace_stop_event.wake(); } @@ -1131,35 +1286,52 @@ impl ProcessData { } /// Consume the signal chosen by the tracer on resume. - pub fn take_ptrace_resume_signo(&self) -> Option { - let signo = self.ptrace_resume_signo.swap(0, Ordering::AcqRel); + pub fn take_ptrace_resume_signo_for(&self, tid: u32) -> Option { + let signo = self.ptrace_resume_signo.lock().remove(&tid).unwrap_or(0); Signo::from_repr(signo as u8) } - pub fn set_ptrace_resume_signal_bypass(&self, signo: Signo) { + pub fn set_ptrace_resume_signal_bypass_for(&self, tid: u32, signo: Signo) { self.ptrace_resume_signal_bypass - .store(signo as u32, Ordering::Release); + .lock() + .insert(tid, signo as u32); } - pub fn take_ptrace_resume_signal_bypass(&self, signo: Signo) -> bool { - self.ptrace_resume_signal_bypass - .compare_exchange(signo as u32, 0, Ordering::AcqRel, Ordering::Acquire) - .is_ok() + pub fn take_ptrace_resume_signal_bypass_for(&self, tid: u32, signo: Signo) -> bool { + let mut bypass = self.ptrace_resume_signal_bypass.lock(); + if bypass.get(&tid).copied() == Some(signo as u32) { + bypass.remove(&tid); + true + } else { + false + } } /// Take registers once the stopped task resumes. pub fn take_ptrace_stop_user_context(&self) -> Option { - self.ptrace_stop_uctx.lock().take() + let tid = self.selected_ptrace_stop_tid()?; + self.take_ptrace_stop_user_context_for(tid) + } + + pub fn take_ptrace_stop_user_context_for(&self, tid: u32) -> Option { + let uctx = self.ptrace_stop.lock().remove(&tid).map(|stop| stop.uctx); + if uctx.is_some() && self.ptrace_stop_tid.load(Ordering::Acquire) == tid { + self.ptrace_stop_tid.store(0, Ordering::Release); + } + uctx } /// Cancel the current ptrace stop and discard its saved registers. pub fn clear_ptrace_stop(&self) { - *self.ptrace_stop_uctx.lock() = None; - *self.ptrace_stop_siginfo.lock() = None; - self.ptrace_stop_is_syscall.store(false, Ordering::Release); - self.ptrace_stop_reported.store(false, Ordering::Release); - self.ptrace_event.store(0, Ordering::Release); - self.ptrace_stop_signo.store(0, Ordering::Release); + self.ptrace_stop.lock().clear(); + self.ptrace_stop_tid.store(0, Ordering::Release); + self.ptrace_resume_signo.lock().clear(); + self.ptrace_resume_signal_bypass.lock().clear(); + self.ptrace_pending_event.lock().clear(); + self.ptrace_singlestep_tid.store(0, Ordering::Release); + self.ptrace_syscall_trace.lock().clear(); + self.ptrace_ss_saved_insn.lock().clear(); + self.ptrace_stop_fp_data.lock().clear(); self.ptrace_stop_event.wake(); } @@ -1191,27 +1363,57 @@ impl ProcessData { } pub fn set_ptrace_singlestep(&self, val: bool) { - self.ptrace_singlestep.store(val, Ordering::Release); + if !val { + self.ptrace_singlestep_tid.store(0, Ordering::Release); + } else if let Some(tid) = self.selected_ptrace_stop_tid() { + self.ptrace_singlestep_tid.store(tid, Ordering::Release); + } + } + + pub fn set_ptrace_singlestep_for(&self, tid: u32, val: bool) { + self.ptrace_singlestep_tid + .store(if val { tid } else { 0 }, Ordering::Release); } pub fn is_ptrace_singlestep(&self) -> bool { - self.ptrace_singlestep.load(Ordering::Acquire) + self.ptrace_singlestep_tid.load(Ordering::Acquire) != 0 + } + + pub fn is_ptrace_singlestep_for(&self, tid: u32) -> bool { + self.ptrace_singlestep_tid.load(Ordering::Acquire) == tid } pub fn set_ptrace_syscall_trace(&self, trace: bool) { - *self.ptrace_syscall_trace.lock() = if trace { - SyscallTraceState::Entry - } else { - SyscallTraceState::None - }; + if let Some(tid) = self.selected_ptrace_stop_tid() { + self.set_ptrace_syscall_trace_for(tid, trace); + } } - pub fn set_ptrace_syscall_trace_state(&self, state: SyscallTraceState) { - *self.ptrace_syscall_trace.lock() = state; + pub fn set_ptrace_syscall_trace_for(&self, tid: u32, trace: bool) { + self.set_ptrace_syscall_trace_state_for( + tid, + if trace { + SyscallTraceState::Entry + } else { + SyscallTraceState::None + }, + ); } - pub fn take_ptrace_syscall_trace(&self) -> SyscallTraceState { - core::mem::take(&mut *self.ptrace_syscall_trace.lock()) + pub fn set_ptrace_syscall_trace_state_for(&self, tid: u32, state: SyscallTraceState) { + let mut traces = self.ptrace_syscall_trace.lock(); + if matches!(state, SyscallTraceState::None) { + traces.remove(&tid); + } else { + traces.insert(tid, state); + } + } + + pub fn take_ptrace_syscall_trace_for(&self, tid: u32) -> SyscallTraceState { + self.ptrace_syscall_trace + .lock() + .remove(&tid) + .unwrap_or_default() } pub fn set_ptrace_options(&self, opts: usize) { @@ -1222,50 +1424,100 @@ impl ProcessData { self.ptrace_options.load(Ordering::Acquire) } - pub fn set_ptrace_event_msg(&self, msg: usize) { - self.ptrace_event_msg.store(msg, Ordering::Release); + pub fn ptrace_event_msg(&self) -> usize { + if let Some(tid) = self.selected_ptrace_stop_tid() { + return self.ptrace_event_msg_for(tid); + } + 0 } - pub fn ptrace_event_msg(&self) -> usize { - self.ptrace_event_msg.load(Ordering::Acquire) + pub fn ptrace_event_msg_for(&self, tid: u32) -> usize { + self.ptrace_stop + .lock() + .get(&tid) + .map_or(0, |stop| stop.event_msg) } - pub fn set_ptrace_event(&self, event: u32) { - self.ptrace_event.store(event, Ordering::Release); + pub fn set_ptrace_pending_event(&self, tid: u32, event: u32, msg: usize) { + self.ptrace_pending_event + .lock() + .insert(tid, PtracePendingEvent { event, msg }); + } + + pub fn has_ptrace_pending_event_for(&self, tid: u32) -> bool { + self.ptrace_pending_event.lock().contains_key(&tid) } pub fn ptrace_event(&self) -> Option { - let event = self.ptrace_event.load(Ordering::Acquire); + if let Some(tid) = self.selected_ptrace_stop_tid() { + return self.ptrace_event_for(tid); + } + let stops = self.ptrace_stop.lock(); + let event = stops + .values() + .find_map(|stop| (!stop.reported && stop.event != 0).then_some(stop.event)) + .or_else(|| { + stops + .values() + .find_map(|stop| (stop.event != 0).then_some(stop.event)) + }) + .unwrap_or(0); if event == 0 { None } else { Some(event) } } + pub fn ptrace_event_for(&self, tid: u32) -> Option { + let event = self + .ptrace_stop + .lock() + .get(&tid) + .map_or(0, |stop| stop.event); + (event != 0).then_some(event) + } + pub fn take_ptrace_event(&self) -> Option { - let event = self.ptrace_event.swap(0, Ordering::AcqRel); + let tid = self.selected_ptrace_stop_tid()?; + self.take_ptrace_event_for(tid) + } + + pub fn take_ptrace_event_for(&self, tid: u32) -> Option { + let event = self.ptrace_stop.lock().get_mut(&tid).map_or(0, |stop| { + let event = stop.event; + stop.event = 0; + stop.event_msg = 0; + event + }); if event == 0 { None } else { Some(event) } } - pub fn set_ptrace_ss_saved_insn(&self, saved: Option<(usize, usize)>) { - *self.ptrace_ss_saved_insn.lock() = saved; + pub fn set_ptrace_ss_saved_insn_for(&self, tid: u32, saved: Option<(usize, usize)>) { + let mut saved_insns = self.ptrace_ss_saved_insn.lock(); + if let Some(saved) = saved { + saved_insns.insert(tid, saved); + } else { + saved_insns.remove(&tid); + } } - pub fn take_ptrace_ss_saved_insn(&self) -> Option<(usize, usize)> { - self.ptrace_ss_saved_insn.lock().take() + pub fn take_ptrace_ss_saved_insn_for(&self, tid: u32) -> Option<(usize, usize)> { + self.ptrace_ss_saved_insn.lock().remove(&tid) } #[cfg(target_arch = "riscv64")] - pub fn save_current_fp_for_ptrace(&self) { + pub fn save_current_fp_for_ptrace(&self, tid: u32) { let mut fp = ax_cpu::FpState::default(); fp.save(); fp.fs = riscv::register::sstatus::read().fs(); - *self.ptrace_stop_fp_data.lock() = Some((fp.fp, fp.fcsr)); + self.ptrace_stop_fp_data + .lock() + .insert(tid, (fp.fp, fp.fcsr)); } #[cfg(not(target_arch = "riscv64"))] - pub fn save_current_fp_for_ptrace(&self) {} + pub fn save_current_fp_for_ptrace(&self, _tid: u32) {} #[cfg(target_arch = "riscv64")] - pub fn restore_current_fp_for_ptrace(&self, uctx: &mut UserContext) { - let Some((fp, fcsr)) = self.ptrace_stop_fp_data() else { + pub fn restore_current_fp_for_ptrace(&self, tid: u32, uctx: &mut UserContext) { + let Some((fp, fcsr)) = self.ptrace_stop_fp_data.lock().remove(&tid) else { return; }; @@ -1283,19 +1535,14 @@ impl ProcessData { } #[cfg(not(target_arch = "riscv64"))] - pub fn restore_current_fp_for_ptrace(&self, _uctx: &mut UserContext) {} + pub fn restore_current_fp_for_ptrace(&self, _tid: u32, _uctx: &mut UserContext) {} - pub fn ptrace_stop_fp_data(&self) -> Option<([u64; 32], usize)> { - *self.ptrace_stop_fp_data.lock() + pub fn ptrace_stop_fp_data_for(&self, tid: u32) -> Option<([u64; 32], usize)> { + self.ptrace_stop_fp_data.lock().get(&tid).copied() } - pub fn set_ptrace_stop_fp_data(&self, data: ([u64; 32], usize)) -> bool { - let mut guard = self.ptrace_stop_fp_data.lock(); - if guard.is_none() { - return false; - } - *guard = Some(data); - true + pub fn set_ptrace_stop_fp_data_for(&self, tid: u32, data: ([u64; 32], usize)) -> bool { + self.ptrace_stop_fp_data.lock().insert(tid, data).is_some() } pub fn personality(&self) -> usize { diff --git a/os/StarryOS/kernel/src/task/signal.rs b/os/StarryOS/kernel/src/task/signal.rs index 972e80bd43..0883730e6a 100644 --- a/os/StarryOS/kernel/src/task/signal.rs +++ b/os/StarryOS/kernel/src/task/signal.rs @@ -196,6 +196,37 @@ pub fn ptrace_syscall_stop_current( ptrace_stop_current_impl(thr, signo, uctx, true) } +pub fn wait_existing_ptrace_stop_current(thr: &Thread, uctx: &mut UserContext) { + let tid = thr.tid(); + if let Some(signo) = thr.proc_data.ptrace_stop_signo_for(tid) { + notify_ptrace_waiter(thr, signo); + } + wait_ptrace_resume(thr, tid, uctx); +} + +fn wait_ptrace_resume(thr: &Thread, tid: u32, uctx: &mut UserContext) { + current().clear_interrupt(); + let wait_result = block_on(interruptible(poll_fn(|cx| { + if thr.proc_data.ptrace_stop_signo_for(tid).is_none() { + Poll::Ready(()) + } else { + thr.proc_data.register_ptrace_stop_waker(cx.waker()); + if thr.proc_data.ptrace_stop_signo_for(tid).is_none() { + Poll::Ready(()) + } else { + Poll::Pending + } + } + }))); + + if wait_result.is_err() { + thr.proc_data.clear_ptrace_stop(); + } else if let Some(resume_uctx) = thr.proc_data.take_ptrace_stop_user_context_for(tid) { + *uctx = resume_uctx; + thr.proc_data.restore_current_fp_for_ptrace(tid, uctx); + } +} + fn ptrace_stop_current_impl( thr: &Thread, signo: Signo, @@ -206,16 +237,38 @@ fn ptrace_stop_current_impl( return None; } + let tid = thr.tid(); + while !thr.proc_data.claim_ptrace_stop(tid) { + block_on(poll_fn(|cx| { + if !thr.proc_data.has_ptrace_stop(tid) { + Poll::Ready(()) + } else { + thr.proc_data.register_ptrace_stop_waker(cx.waker()); + if !thr.proc_data.has_ptrace_stop(tid) { + Poll::Ready(()) + } else { + Poll::Pending + } + } + })); + } + #[cfg(target_arch = "riscv64")] { - thr.proc_data.save_current_fp_for_ptrace(); + thr.proc_data.save_current_fp_for_ptrace(tid); } if is_syscall_stop { - thr.proc_data.set_ptrace_syscall_stop(signo, uctx); + thr.proc_data.set_ptrace_syscall_stop(tid, signo, uctx); } else { - thr.proc_data.set_ptrace_stop(signo, uctx); + thr.proc_data.set_ptrace_stop(tid, signo, uctx); } + notify_ptrace_waiter(thr, signo); + + wait_ptrace_resume(thr, tid, uctx); + Some(thr.proc_data.take_ptrace_resume_signo_for(tid)) +} +fn notify_ptrace_waiter(thr: &Thread, signo: Signo) { let waiter_pid = thr .proc_data .ptrace_tracer_pid() @@ -232,28 +285,6 @@ fn ptrace_stop_current_impl( let _ = send_signal_to_process(waiter_pid, Some(sigchld)); parent_data.child_exit_event.wake(); } - - current().clear_interrupt(); - let wait_result = block_on(interruptible(poll_fn(|cx| { - if thr.proc_data.ptrace_stop_signo().is_none() { - Poll::Ready(()) - } else { - thr.proc_data.register_ptrace_stop_waker(cx.waker()); - if thr.proc_data.ptrace_stop_signo().is_none() { - Poll::Ready(()) - } else { - Poll::Pending - } - } - }))); - - if wait_result.is_err() { - thr.proc_data.clear_ptrace_stop(); - } else if let Some(resume_uctx) = thr.proc_data.take_ptrace_stop_user_context() { - *uctx = resume_uctx; - thr.proc_data.restore_current_fp_for_ptrace(uctx); - } - Some(thr.proc_data.take_ptrace_resume_signo()) } pub fn check_signals( @@ -308,13 +339,16 @@ pub fn check_signals( let signo = sig.signo(); if signo != Signo::SIGKILL - && !thr.proc_data.take_ptrace_resume_signal_bypass(signo) + && !thr + .proc_data + .take_ptrace_resume_signal_bypass_for(thr.tid(), signo) && let Some(resume_signo) = ptrace_stop_current(thr, signo, uctx) { match resume_signo { None => return true, Some(new_signo) if new_signo != signo => { - thr.proc_data.set_ptrace_resume_signal_bypass(new_signo); + thr.proc_data + .set_ptrace_resume_signal_bypass_for(thr.tid(), new_signo); let _ = thr.signal.send_signal(SignalInfo::new_kernel(new_signo)); return true; } diff --git a/os/StarryOS/kernel/src/task/user.rs b/os/StarryOS/kernel/src/task/user.rs index 5d6ce6b411..a62485b765 100644 --- a/os/StarryOS/kernel/src/task/user.rs +++ b/os/StarryOS/kernel/src/task/user.rs @@ -8,7 +8,7 @@ use syscalls::Sysno; use super::{ AsThread, SyscallRestartInfo, SyscallTraceState, TimerState, check_signals, ptrace_stop_current, ptrace_syscall_stop_current, raise_signal_fatal, set_timer_state, - unblock_next_signal, + unblock_next_signal, wait_existing_ptrace_stop_current, }; use crate::syscall::{handle_syscall, syscall_allows_signal_restart}; @@ -25,15 +25,19 @@ pub fn new_user_task(name: &str, mut uctx: UserContext, set_child_tid: usize) -> info!("Enter user space: ip={:#x}, sp={:#x}", uctx.ip(), uctx.sp()); let thr = curr.as_thread(); - if thr.proc_data.ptrace_stop_signo().is_some() { + if thr.proc_data.ptrace_stop_signo_for(thr.tid()).is_some() { + wait_existing_ptrace_stop_current(thr, &mut uctx); + } else if thr.tid() == thr.proc_data.proc.pid() + && thr.proc_data.ptrace_stop_signo().is_some() + { let _ = ptrace_stop_current(thr, Signo::SIGSTOP, &mut uctx); } while !thr.pending_exit() { - if thr.proc_data.is_ptrace_singlestep() + if thr.proc_data.is_ptrace_singlestep_for(thr.tid()) && (thr.proc_data.is_ptrace_traceme() || thr.proc_data.is_ptrace_attached()) { #[cfg(target_arch = "riscv64")] - crate::syscall::ptrace_setup_singlestep(&thr.proc_data, &mut uctx); + crate::syscall::ptrace_setup_singlestep(&thr.proc_data, thr.tid(), &mut uctx); } let reason = uctx.run(); @@ -46,14 +50,18 @@ pub fn new_user_task(name: &str, mut uctx: UserContext, set_child_tid: usize) -> match reason { ReturnReason::Syscall => { - let trace_state = thr.proc_data.take_ptrace_syscall_trace(); + let tid = thr.tid(); + let trace_state = thr.proc_data.take_ptrace_syscall_trace_for(tid); if matches!(trace_state, SyscallTraceState::Entry) && ptrace_syscall_stop_current(thr, Signo::SIGTRAP, &mut uctx).is_some() { - match thr.proc_data.take_ptrace_syscall_trace() { - SyscallTraceState::Entry | SyscallTraceState::Exit => thr - .proc_data - .set_ptrace_syscall_trace_state(SyscallTraceState::Exit), + match thr.proc_data.take_ptrace_syscall_trace_for(tid) { + SyscallTraceState::Entry | SyscallTraceState::Exit => { + thr.proc_data.set_ptrace_syscall_trace_state_for( + tid, + SyscallTraceState::Exit, + ) + } SyscallTraceState::None => {} } } @@ -68,8 +76,14 @@ pub fn new_user_task(name: &str, mut uctx: UserContext, set_child_tid: usize) -> } handle_syscall(&mut uctx); + if thr.proc_data.has_ptrace_pending_event_for(tid) + && let Some(_resume_sig) = + ptrace_stop_current(thr, Signo::SIGTRAP, &mut uctx) + { + continue; + } if matches!( - thr.proc_data.take_ptrace_syscall_trace(), + thr.proc_data.take_ptrace_syscall_trace_for(tid), SyscallTraceState::Exit ) { let _ = ptrace_syscall_stop_current(thr, Signo::SIGTRAP, &mut uctx); @@ -127,7 +141,7 @@ pub fn new_user_task(name: &str, mut uctx: UserContext, set_child_tid: usize) -> && (thr.proc_data.is_ptrace_traceme() || thr.proc_data.is_ptrace_attached()) { - let saved_insn = thr.proc_data.take_ptrace_ss_saved_insn(); + let saved_insn = thr.proc_data.take_ptrace_ss_saved_insn_for(thr.tid()); if let Some((addr, insn)) = saved_insn { if addr == uctx.ip() { let aspace = thr.proc_data.aspace(); @@ -139,7 +153,10 @@ pub fn new_user_task(name: &str, mut uctx: UserContext, set_child_tid: usize) -> #[cfg(target_arch = "riscv64")] ax_runtime::hal::cpu::asm::flush_icache_all(); } else { - thr.proc_data.set_ptrace_ss_saved_insn(Some((addr, insn))); + thr.proc_data.set_ptrace_ss_saved_insn_for( + thr.tid(), + Some((addr, insn)), + ); } } if let Some(_resume_sig) = diff --git a/test-suit/starryos/normal/qemu-smp1/test-proc-status-tracerpid/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/test-proc-status-tracerpid/c/CMakeLists.txt new file mode 100644 index 0000000000..218871eb2e --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-proc-status-tracerpid/c/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) +project(test-proc-status-tracerpid C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS ON) + +add_executable(test-proc-status-tracerpid src/main.c) +target_compile_options(test-proc-status-tracerpid PRIVATE -Wall -Wextra -Werror) + +install(TARGETS test-proc-status-tracerpid RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/test-proc-status-tracerpid/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/test-proc-status-tracerpid/c/src/main.c new file mode 100644 index 0000000000..238f03bc7e --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-proc-status-tracerpid/c/src/main.c @@ -0,0 +1,96 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include + +static int fail(const char *msg) +{ + printf("FAIL: %s: errno=%d (%s)\n", msg, errno, strerror(errno)); + return 1; +} + +static int read_tracer_pid(pid_t tracee) +{ + char path[64]; + snprintf(path, sizeof(path), "/proc/%ld/status", (long)tracee); + + FILE *file = fopen(path, "r"); + if (file == NULL) { + return -1; + } + + char line[256]; + while (fgets(line, sizeof(line), file) != NULL) { + int tracer_pid = 0; + if (sscanf(line, "TracerPid:\t%d", &tracer_pid) == 1) { + fclose(file); + return tracer_pid; + } + } + + fclose(file); + errno = ENOENT; + return -1; +} + +int main(void) +{ + pid_t child = fork(); + if (child < 0) { + return fail("fork"); + } + + if (child == 0) { + if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) != 0) { + _exit(101); + } + if (kill(getpid(), SIGSTOP) != 0) { + _exit(102); + } + _exit(0); + } + + int status = 0; + if (waitpid(child, &status, 0) != child) { + return fail("waitpid child stop"); + } + if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) { + printf("FAIL: expected traced child SIGSTOP, status=%#x\n", status); + return 1; + } + + int tracer_pid = read_tracer_pid(child); + if (tracer_pid < 0) { + int saved_errno = errno; + kill(child, SIGKILL); + waitpid(child, &status, 0); + errno = saved_errno; + return fail("read TracerPid from /proc child status"); + } + + pid_t expected = getpid(); + if (tracer_pid != expected) { + printf("FAIL: expected TracerPid %ld, got %d\n", (long)expected, tracer_pid); + kill(child, SIGKILL); + waitpid(child, &status, 0); + return 1; + } + + if (kill(child, SIGKILL) != 0) { + return fail("kill child"); + } + if (waitpid(child, &status, 0) != child || !WIFSIGNALED(status) + || WTERMSIG(status) != SIGKILL) { + printf("FAIL: expected child SIGKILL, status=%#x\n", status); + return 1; + } + + printf("DONE: 1 pass, 0 fail\n"); + return 0; +} diff --git a/test-suit/starryos/normal/qemu-smp1/test-proc-status-tracerpid/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/test-proc-status-tracerpid/qemu-riscv64.toml new file mode 100644 index 0000000000..b6122140c4 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-proc-status-tracerpid/qemu-riscv64.toml @@ -0,0 +1,14 @@ +args = [ + "-nographic", "-cpu", "rv64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-proc-status-tracerpid" +success_regex = ["DONE: 1 pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 60 diff --git a/test-suit/starryos/normal/qemu-smp1/test-proc-task-status-tid/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/test-proc-task-status-tid/c/CMakeLists.txt new file mode 100644 index 0000000000..f290794612 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-proc-task-status-tid/c/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.16) +project(test-proc-task-status-tid C) + +add_executable(test-proc-task-status-tid src/main.c) +target_compile_options(test-proc-task-status-tid PRIVATE -Wall -Wextra -Werror) +target_link_libraries(test-proc-task-status-tid PRIVATE pthread) +install(TARGETS test-proc-task-status-tid RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/test-proc-task-status-tid/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/test-proc-task-status-tid/c/src/main.c new file mode 100644 index 0000000000..93a8a032c2 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-proc-task-status-tid/c/src/main.c @@ -0,0 +1,129 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include + +struct thread_sync { + pthread_mutex_t lock; + pthread_cond_t ready; + pthread_cond_t done; + pid_t tid; + int should_exit; +}; + +static int fail(const char *msg) +{ + printf("FAIL: %s: errno=%d (%s)\n", msg, errno, strerror(errno)); + return 1; +} + +static pid_t raw_gettid(void) +{ + return (pid_t)syscall(SYS_gettid); +} + +static void *worker_main(void *arg) +{ + struct thread_sync *sync = arg; + pid_t tid = raw_gettid(); + + pthread_mutex_lock(&sync->lock); + sync->tid = tid; + pthread_cond_signal(&sync->ready); + while (!sync->should_exit) { + pthread_cond_wait(&sync->done, &sync->lock); + } + pthread_mutex_unlock(&sync->lock); + return NULL; +} + +static int read_status_value(pid_t tid, const char *key) +{ + char path[128]; + snprintf(path, sizeof(path), "/proc/self/task/%ld/status", (long)tid); + + FILE *file = fopen(path, "r"); + if (file == NULL) { + return -1; + } + + char line[256]; + int value = -1; + while (fgets(line, sizeof(line), file) != NULL) { + if (sscanf(line, "Tgid:\t%d", &value) == 1 && strcmp(key, "Tgid") == 0) { + fclose(file); + return value; + } + if (sscanf(line, "Pid:\t%d", &value) == 1 && strcmp(key, "Pid") == 0) { + fclose(file); + return value; + } + } + + fclose(file); + errno = ENOENT; + return -1; +} + +static void stop_worker(struct thread_sync *sync, pthread_t worker) +{ + pthread_mutex_lock(&sync->lock); + sync->should_exit = 1; + pthread_cond_signal(&sync->done); + pthread_mutex_unlock(&sync->lock); + pthread_join(worker, NULL); +} + +int main(void) +{ + struct thread_sync sync = { + .lock = PTHREAD_MUTEX_INITIALIZER, + .ready = PTHREAD_COND_INITIALIZER, + .done = PTHREAD_COND_INITIALIZER, + .tid = 0, + .should_exit = 0, + }; + pthread_t worker; + + if (pthread_create(&worker, NULL, worker_main, &sync) != 0) { + return fail("pthread_create"); + } + + pthread_mutex_lock(&sync.lock); + while (sync.tid == 0) { + pthread_cond_wait(&sync.ready, &sync.lock); + } + pid_t worker_tid = sync.tid; + pthread_mutex_unlock(&sync.lock); + + int tgid = read_status_value(worker_tid, "Tgid"); + int pid = read_status_value(worker_tid, "Pid"); + if (tgid < 0 || pid < 0) { + int saved_errno = errno; + stop_worker(&sync, worker); + errno = saved_errno; + return fail("read /proc/self/task//status"); + } + + pid_t process_pid = getpid(); + if (tgid != process_pid) { + printf("FAIL: expected task status Tgid %ld, got %d\n", (long)process_pid, tgid); + stop_worker(&sync, worker); + return 1; + } + if (pid != worker_tid) { + printf("FAIL: expected task status Pid %ld, got %d\n", (long)worker_tid, pid); + stop_worker(&sync, worker); + return 1; + } + + stop_worker(&sync, worker); + + printf("DONE: 1 pass, 0 fail\n"); + return 0; +} diff --git a/test-suit/starryos/normal/qemu-smp1/test-proc-task-status-tid/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/test-proc-task-status-tid/qemu-riscv64.toml new file mode 100644 index 0000000000..3b65f9bbdd --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-proc-task-status-tid/qemu-riscv64.toml @@ -0,0 +1,14 @@ +args = [ + "-nographic", "-cpu", "rv64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-proc-task-status-tid" +success_regex = ["DONE: 1 pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 60 diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-gdb/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/test-ptrace-gdb/c/src/main.c index 6b769bd0f8..544be936e1 100644 --- a/test-suit/starryos/normal/qemu-smp1/test-ptrace-gdb/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-gdb/c/src/main.c @@ -77,6 +77,9 @@ #ifndef PTRACE_O_TRACEVFORKDONE #define PTRACE_O_TRACEVFORKDONE 0x00000020 #endif +#ifndef __WALL +#define __WALL 0x40000000 +#endif #define PTRACE_EVENT_FORK 1 #define PTRACE_EVENT_CLONE 3 @@ -304,6 +307,17 @@ static int check_singlestep_stops_before_target_side_effect(pid_t pid, if (set_regs(pid, regs) != 0) { return fail("setregset control-flow singlestep target"); } + memset(regs, 0, sizeof(*regs)); + if (get_regs(pid, regs) != 0) { + return fail("getregset after setting control-flow singlestep target"); + } + if (regs->pc != pc || regs->a0 != a0 || regs->a1 != a1 || regs->a2 != 0 + || regs->a3 != a3 || regs->s0 != s0) { + printf("FAIL: setregset control-flow readback pc=%#lx a0=%#lx a1=%#lx a2=%#lx " + "a3=%#lx s0=%#lx\n", + regs->pc, regs->a0, regs->a1, regs->a2, regs->a3, regs->s0); + return 1; + } if (ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL) != 0) { return fail("singlestep control-flow instruction"); @@ -316,7 +330,8 @@ static int check_singlestep_stops_before_target_side_effect(pid_t pid, return fail("getregset after control-flow singlestep"); } if (regs->a2 != 0) { - printf("FAIL: singlestep ran target side effect early, a2=%#lx\n", regs->a2); + printf("FAIL: singlestep ran target side effect early, pc=%#lx s0=%#lx a2=%#lx\n", + regs->pc, regs->s0, regs->a2); return 1; } @@ -653,7 +668,7 @@ static int test_setoptions(void) return 1; } - if (ptrace(PTRACE_SETOPTIONS, pid, (void *)(long)PTRACE_O_TRACEFORK, NULL) != 0) { + if (ptrace(PTRACE_SETOPTIONS, pid, NULL, (void *)(long)PTRACE_O_TRACEFORK) != 0) { return fail("setoptions"); } @@ -846,7 +861,7 @@ static int test_syscall_trace(void) printf("FAIL: initial stop\n"); return 1; } - if (ptrace(PTRACE_SETOPTIONS, pid, (void *)(long)PTRACE_O_TRACESYSGOOD, NULL) != 0) { + if (ptrace(PTRACE_SETOPTIONS, pid, NULL, (void *)(long)PTRACE_O_TRACESYSGOOD) != 0) { return fail("setoptions TRACESYSGOOD"); } @@ -1207,7 +1222,7 @@ static int test_traceexit_event(void) return 1; } - if (ptrace(PTRACE_SETOPTIONS, pid, (void *)(long)PTRACE_O_TRACEEXIT, NULL) != 0) { + if (ptrace(PTRACE_SETOPTIONS, pid, NULL, (void *)(long)PTRACE_O_TRACEEXIT) != 0) { return fail("setoptions TRACEEXIT"); } if (ptrace(PTRACE_CONT, pid, NULL, NULL) != 0) { @@ -1305,7 +1320,7 @@ static int test_traceclone_thread_event(void) return 1; } - if (ptrace(PTRACE_SETOPTIONS, pid, (void *)(long)PTRACE_O_TRACECLONE, NULL) != 0) { + if (ptrace(PTRACE_SETOPTIONS, pid, NULL, (void *)(long)PTRACE_O_TRACECLONE) != 0) { return fail("setoptions TRACECLONE"); } if (ptrace(PTRACE_CONT, pid, NULL, NULL) != 0) { @@ -1335,6 +1350,15 @@ static int test_traceclone_thread_event(void) return 1; } + if (waitpid((pid_t)event_msg, &status, __WALL) != (pid_t)event_msg + || !WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) { + printf("FAIL: expected traced clone tid %lu initial SIGSTOP, status=%#x\n", event_msg, + status); + return 1; + } + if (ptrace(PTRACE_CONT, (pid_t)event_msg, NULL, NULL) != 0) { + return fail("cont traceclone child tid"); + } if (ptrace(PTRACE_CONT, pid, NULL, NULL) != 0) { return fail("cont after traceclone event"); } @@ -1378,7 +1402,7 @@ static int test_tracevforkdone_event(void) return 1; } - if (ptrace(PTRACE_SETOPTIONS, pid, (void *)(long)PTRACE_O_TRACEVFORKDONE, NULL) != 0) { + if (ptrace(PTRACE_SETOPTIONS, pid, NULL, (void *)(long)PTRACE_O_TRACEVFORKDONE) != 0) { return fail("setoptions TRACEVFORKDONE"); } if (ptrace(PTRACE_CONT, pid, NULL, NULL) != 0) { @@ -1487,7 +1511,7 @@ static int test_sigkill_event_stopped_tracee(void) printf("FAIL: initial event-kill stop status=%#x\n", status); return 1; } - if (ptrace(PTRACE_SETOPTIONS, pid, (void *)(long)PTRACE_O_TRACEFORK, NULL) != 0) { + if (ptrace(PTRACE_SETOPTIONS, pid, NULL, (void *)(long)PTRACE_O_TRACEFORK) != 0) { return fail("setoptions event-kill TRACEFORK"); } if (ptrace(PTRACE_CONT, pid, NULL, NULL) != 0) { diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-thread-traceclone-wall/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/test-ptrace-thread-traceclone-wall/c/CMakeLists.txt new file mode 100644 index 0000000000..40cdae6a69 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-thread-traceclone-wall/c/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.16) +project(test-ptrace-thread-traceclone-wall C) + +add_executable(test-ptrace-thread-traceclone-wall src/main.c) +target_compile_options(test-ptrace-thread-traceclone-wall PRIVATE -Wall -Wextra -Werror) +target_link_libraries(test-ptrace-thread-traceclone-wall PRIVATE pthread) +install(TARGETS test-ptrace-thread-traceclone-wall RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-thread-traceclone-wall/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/test-ptrace-thread-traceclone-wall/c/src/main.c new file mode 100644 index 0000000000..00e901fcf9 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-thread-traceclone-wall/c/src/main.c @@ -0,0 +1,246 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef NT_PRSTATUS +#define NT_PRSTATUS 1 +#endif +#ifndef PTRACE_SETOPTIONS +#define PTRACE_SETOPTIONS 0x4200 +#endif +#ifndef PTRACE_GETEVENTMSG +#define PTRACE_GETEVENTMSG 0x4201 +#endif +#ifndef PTRACE_GETSIGINFO +#define PTRACE_GETSIGINFO 0x4202 +#endif +#ifndef PTRACE_GETREGSET +#define PTRACE_GETREGSET 0x4204 +#endif +#ifndef PTRACE_O_TRACECLONE +#define PTRACE_O_TRACECLONE 0x00000008 +#endif +#ifndef PTRACE_EVENT_CLONE +#define PTRACE_EVENT_CLONE 3 +#endif +#ifndef __WALL +#define __WALL 0x40000000 +#endif + +struct riscv_user_regs { + unsigned long pc; + unsigned long ra; + unsigned long sp; + unsigned long gp; + unsigned long tp; + unsigned long t0; + unsigned long t1; + unsigned long t2; + unsigned long s0; + unsigned long s1; + unsigned long a0; + unsigned long a1; + unsigned long a2; + unsigned long a3; + unsigned long a4; + unsigned long a5; + unsigned long a6; + unsigned long a7; + unsigned long s2; + unsigned long s3; + unsigned long s4; + unsigned long s5; + unsigned long s6; + unsigned long s7; + unsigned long s8; + unsigned long s9; + unsigned long s10; + unsigned long s11; + unsigned long t3; + unsigned long t4; + unsigned long t5; + unsigned long t6; +}; + +struct worker_arg { + int id; + volatile int result; +}; + +static int fail(const char *msg) +{ + printf("FAIL: %s: errno=%d (%s)\n", msg, errno, strerror(errno)); + return 1; +} + +static void *worker_main(void *arg) +{ + struct worker_arg *worker = arg; + worker->result = worker->id + 100; + return NULL; +} + +static void run_child(void) +{ + if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) != 0) { + _exit(101); + } + if (kill(getpid(), SIGSTOP) != 0) { + _exit(102); + } + + pthread_t threads[2]; + struct worker_arg args[2] = { + {.id = 1, .result = 0}, + {.id = 2, .result = 0}, + }; + + for (int i = 0; i < 2; i++) { + if (pthread_create(&threads[i], NULL, worker_main, &args[i]) != 0) { + _exit(103); + } + } + for (int i = 0; i < 2; i++) { + if (pthread_join(threads[i], NULL) != 0) { + _exit(104); + } + } + + _exit(args[0].result == 101 && args[1].result == 102 ? 42 : 105); +} + +static int inspect_and_continue_new_thread(unsigned long new_tid) +{ + siginfo_t wait_info = {0}; + if (waitid(P_PID, (id_t)new_tid, &wait_info, WSTOPPED | __WALL) != 0) { + return fail("waitid exact new tid SIGSTOP"); + } + if (wait_info.si_pid != (pid_t)new_tid || wait_info.si_code != CLD_TRAPPED + || wait_info.si_status != SIGSTOP) { + printf("FAIL: expected exact waitid for new tid=%lu SIGSTOP, got pid=%ld code=%d " + "status=%d\n", + new_tid, (long)wait_info.si_pid, wait_info.si_code, wait_info.si_status); + return 1; + } + + wait_info.si_pid = -1; + if (waitid(P_PID, (id_t)new_tid, &wait_info, WSTOPPED | WNOHANG | __WALL) != 0) { + return fail("waitid exact new tid WNOHANG"); + } + if (wait_info.si_pid != 0) { + printf("FAIL: repeated waitid for new tid=%lu should not report same stop again, " + "got pid=%ld code=%d status=%d\n", + new_tid, (long)wait_info.si_pid, wait_info.si_code, wait_info.si_status); + return 1; + } + + siginfo_t siginfo; + if (ptrace(PTRACE_GETSIGINFO, (pid_t)new_tid, NULL, &siginfo) != 0) { + return fail("getsiginfo new tid"); + } + if (siginfo.si_signo != SIGSTOP) { + printf("FAIL: expected new tid siginfo SIGSTOP, got signo=%d\n", siginfo.si_signo); + return 1; + } + + struct riscv_user_regs regs = {0}; + struct iovec iov = {.iov_base = ®s, .iov_len = sizeof(regs)}; + if (ptrace(PTRACE_GETREGSET, (pid_t)new_tid, (void *)NT_PRSTATUS, &iov) != 0) { + return fail("getregset new tid"); + } + if (iov.iov_len == 0 || regs.pc == 0) { + printf("FAIL: expected non-empty regs for new tid, iov_len=%ld pc=%#lx\n", + (long)iov.iov_len, regs.pc); + return 1; + } + + if (ptrace(PTRACE_SETOPTIONS, (pid_t)new_tid, NULL, (void *)PTRACE_O_TRACECLONE) != 0) { + return fail("setoptions new tid"); + } + if (ptrace(PTRACE_CONT, (pid_t)new_tid, NULL, NULL) != 0) { + return fail("cont new tid"); + } + + printf("INFO: inspected and continued new traced tid=%lu pc=%#lx\n", new_tid, regs.pc); + return 0; +} + +static int consume_clone_event(pid_t pid, int *clone_count) +{ + int status = 0; + pid_t stopped = waitpid(pid, &status, __WALL); + if (stopped != pid || !WIFSTOPPED(status) || WSTOPSIG(status) != SIGTRAP) { + printf("FAIL: expected clone SIGTRAP from pid=%d, got pid=%ld status=%#x\n", pid, + (long)stopped, status); + return 1; + } + + unsigned int event = (unsigned int)status >> 16; + if (event != PTRACE_EVENT_CLONE) { + printf("FAIL: expected PTRACE_EVENT_CLONE, got event=%u status=%#x\n", event, status); + return 1; + } + + unsigned long new_tid = 0; + if (ptrace(PTRACE_GETEVENTMSG, pid, NULL, &new_tid) != 0) { + return fail("get clone event msg"); + } + if (inspect_and_continue_new_thread(new_tid) != 0) { + return 1; + } + if (ptrace(PTRACE_CONT, pid, NULL, NULL) != 0) { + return fail("cont after clone event"); + } + + (*clone_count)++; + printf("INFO: consumed clone event %d for tid=%lu\n", *clone_count, new_tid); + return 0; +} + +int main(void) +{ + pid_t pid = fork(); + if (pid < 0) { + return fail("fork"); + } + if (pid == 0) { + run_child(); + } + + int status = 0; + if (waitpid(pid, &status, __WALL) != pid || !WIFSTOPPED(status) + || WSTOPSIG(status) != SIGSTOP) { + printf("FAIL: expected initial SIGSTOP, status=%#x\n", status); + return 1; + } + if (ptrace(PTRACE_SETOPTIONS, pid, NULL, (void *)PTRACE_O_TRACECLONE) != 0) { + return fail("set traceclone option"); + } + if (ptrace(PTRACE_CONT, pid, NULL, NULL) != 0) { + return fail("cont child"); + } + + int clone_count = 0; + while (clone_count < 2) { + if (consume_clone_event(pid, &clone_count) != 0) { + return 1; + } + } + + if (waitpid(pid, &status, __WALL) != pid || !WIFEXITED(status) || WEXITSTATUS(status) != 42) { + printf("FAIL: expected traced child exit 42, status=%#x\n", status); + return 1; + } + + puts("DONE: 1 pass, 0 fail"); + return 0; +} diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-thread-traceclone-wall/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/test-ptrace-thread-traceclone-wall/qemu-riscv64.toml new file mode 100644 index 0000000000..8c8b06dc87 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-thread-traceclone-wall/qemu-riscv64.toml @@ -0,0 +1,14 @@ +args = [ + "-nographic", "-cpu", "rv64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-ptrace-thread-traceclone-wall" +success_regex = ["DONE: 1 pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 60 diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-traceme-stop/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/test-ptrace-traceme-stop/c/src/main.c index 4892670572..da69769bd9 100644 --- a/test-suit/starryos/normal/qemu-smp1/test-ptrace-traceme-stop/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-traceme-stop/c/src/main.c @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -100,6 +101,50 @@ static int fail(const char *msg) return 1; } +static int remote_read_word(pid_t pid, uintptr_t addr, unsigned long *out) +{ + struct iovec local = {.iov_base = out, .iov_len = sizeof(*out)}; + struct iovec remote = {.iov_base = (void *)addr, .iov_len = sizeof(*out)}; + ssize_t bytes = process_vm_readv(pid, &local, 1, &remote, 1, 0); + if (bytes != (ssize_t)sizeof(*out)) { + errno = bytes < 0 ? errno : EIO; + return -1; + } + return 0; +} + +static int remote_write_word(pid_t pid, uintptr_t addr, unsigned long word) +{ + struct iovec local = {.iov_base = &word, .iov_len = sizeof(word)}; + struct iovec remote = {.iov_base = (void *)addr, .iov_len = sizeof(word)}; + ssize_t bytes = process_vm_writev(pid, &local, 1, &remote, 1, 0); + if (bytes != (ssize_t)sizeof(word)) { + errno = bytes < 0 ? errno : EIO; + return -1; + } + return 0; +} + +static int proc_mem_read_word(int fd, uintptr_t addr, unsigned long *out) +{ + ssize_t bytes = pread(fd, out, sizeof(*out), (off_t)addr); + if (bytes != (ssize_t)sizeof(*out)) { + errno = bytes < 0 ? errno : EIO; + return -1; + } + return 0; +} + +static int proc_mem_write_word(int fd, uintptr_t addr, unsigned long word) +{ + ssize_t bytes = pwrite(fd, &word, sizeof(word), (off_t)addr); + if (bytes != (ssize_t)sizeof(word)) { + errno = bytes < 0 ? errno : EIO; + return -1; + } + return 0; +} + int main(void) { struct sigaction chld_action; @@ -255,29 +300,67 @@ int main(void) if (ptrace(PTRACE_POKEDATA, pid, (void *)addrs.text_addr, (void *)breakpoint_word) != 0) { return fail("ptrace poke breakpoint"); } + if (ptrace(PTRACE_POKEDATA, pid, (void *)addrs.text_addr, (void *)text_word) != 0) { + return fail("ptrace restore text before process_vm"); + } + + unsigned long vm_text_word = 0; + if (remote_read_word(pid, addrs.text_addr, &vm_text_word) != 0 || + vm_text_word != (unsigned long)text_word) { + return fail("process_vm_readv text"); + } + if (remote_write_word(pid, addrs.text_addr, breakpoint_word) != 0) { + return fail("process_vm_writev breakpoint"); + } + if (remote_write_word(pid, addrs.text_addr, (unsigned long)text_word) != 0) { + return fail("process_vm_writev restore before proc mem"); + } + + char mem_path[64]; + snprintf(mem_path, sizeof(mem_path), "/proc/%d/mem", pid); + int mem_fd = open(mem_path, O_RDWR); + if (mem_fd < 0) { + return fail("open proc mem"); + } + unsigned long proc_mem_text_word = 0; + if (proc_mem_read_word(mem_fd, addrs.text_addr, &proc_mem_text_word) != 0 || + proc_mem_text_word != (unsigned long)text_word) { + close(mem_fd); + return fail("proc mem pread text"); + } + if (proc_mem_write_word(mem_fd, addrs.text_addr, breakpoint_word) != 0) { + close(mem_fd); + return fail("proc mem pwrite breakpoint"); + } if (ptrace(PTRACE_CONT, pid, NULL, NULL) != 0) { + close(mem_fd); return fail("ptrace cont to breakpoint"); } if (waitpid(pid, &status, WUNTRACED) != pid || !WIFSTOPPED(status) || WSTOPSIG(status) != SIGTRAP) { printf("FAIL: expected breakpoint SIGTRAP, status=%#x\n", status); + close(mem_fd); return 1; } memset(®s, 0, sizeof(regs)); iov.iov_len = sizeof(regs); if (ptrace(PTRACE_GETREGSET, pid, (void *)NT_PRSTATUS, &iov) != 0) { + close(mem_fd); return fail("ptrace getregset at breakpoint"); } if (regs.pc != addrs.text_addr) { printf("FAIL: expected breakpoint pc=%#lx, got %#lx\n", (unsigned long)addrs.text_addr, regs.pc); + close(mem_fd); return 1; } - if (ptrace(PTRACE_POKEDATA, pid, (void *)addrs.text_addr, (void *)text_word) != 0) { - return fail("ptrace restore text"); + if (proc_mem_write_word(mem_fd, addrs.text_addr, (unsigned long)text_word) != 0) { + close(mem_fd); + return fail("proc mem pwrite restore text"); } + close(mem_fd); regs.pc = addrs.text_addr; iov.iov_len = sizeof(regs); if (ptrace(PTRACE_SETREGSET, pid, (void *)NT_PRSTATUS, &iov) != 0) { diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-wait-wall/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/test-ptrace-wait-wall/c/CMakeLists.txt new file mode 100644 index 0000000000..488c6590b2 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-wait-wall/c/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) +project(test-ptrace-wait-wall C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS ON) + +add_executable(test-ptrace-wait-wall src/main.c) +target_compile_options(test-ptrace-wait-wall PRIVATE -Wall -Wextra -Werror) + +install(TARGETS test-ptrace-wait-wall RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-wait-wall/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/test-ptrace-wait-wall/c/src/main.c new file mode 100644 index 0000000000..739ec9d65e --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-wait-wall/c/src/main.c @@ -0,0 +1,58 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include + +#ifndef __WALL +#define __WALL 0x40000000 +#endif + +static int fail(const char *msg) +{ + printf("FAIL: %s: errno=%d (%s)\n", msg, errno, strerror(errno)); + return 1; +} + +int main(void) +{ + pid_t child = fork(); + if (child < 0) { + return fail("fork"); + } + + if (child == 0) { + if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) != 0) { + _exit(101); + } + if (kill(getpid(), SIGSTOP) != 0) { + _exit(102); + } + _exit(0); + } + + int status = 0; + if (waitpid(child, &status, __WALL) != child) { + return fail("waitpid __WALL ptrace stop"); + } + if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) { + printf("FAIL: expected ptrace SIGSTOP through __WALL, status=%#x\n", status); + return 1; + } + + if (kill(child, SIGKILL) != 0) { + return fail("kill child"); + } + if (waitpid(child, &status, __WALL) != child || !WIFSIGNALED(status) + || WTERMSIG(status) != SIGKILL) { + printf("FAIL: expected SIGKILL through __WALL, status=%#x\n", status); + return 1; + } + + printf("DONE: 1 pass, 0 fail\n"); + return 0; +} diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-wait-wall/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/test-ptrace-wait-wall/qemu-riscv64.toml new file mode 100644 index 0000000000..2243661d21 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-wait-wall/qemu-riscv64.toml @@ -0,0 +1,14 @@ +args = [ + "-nographic", "-cpu", "rv64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-ptrace-wait-wall" +success_regex = ["DONE: 1 pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 60