diff --git a/include/linux/ptrace.h b/include/linux/ptrace.h index dc1fb15fa8f45670c743ff9994040cecf1ad7abc..fcf82138589abe3b0f95a6dbaed7ae12e5198c7b 100644 --- a/include/linux/ptrace.h +++ b/include/linux/ptrace.h @@ -52,7 +52,14 @@ extern int ptrace_access_vm(struct task_struct *tsk, unsigned long addr, #define PT_BLOCKSTEP_BIT 30 #define PT_BLOCKSTEP (1< #include #include +#include #include #include #include @@ -1279,13 +1280,14 @@ int ptrace_request(struct task_struct *child, long request, return ret; } -int (*ptrace_pre_hook)(long request, long pid, struct task_struct *task, long addr, long data); +ptrace_pre_hook_t __rcu *ptrace_pre_hook; EXPORT_SYMBOL(ptrace_pre_hook); SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, unsigned long, data) { struct task_struct *child; + ptrace_pre_hook_t *hook; long ret; if (request == PTRACE_TRACEME) { @@ -1299,11 +1301,14 @@ SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, goto out; } - if (ptrace_pre_hook) { - ret = ptrace_pre_hook(request, pid, child, addr, data); - if (ret) - goto out_put_task_struct; - } + ret = 0; + rcu_read_lock(); + hook = rcu_dereference(ptrace_pre_hook); + if (hook) + ret = hook(request, pid, child, addr, data); + rcu_read_unlock(); + if (ret) + goto out_put_task_struct; if (request == PTRACE_ATTACH || request == PTRACE_SEIZE) { ret = ptrace_attach(child, request, addr, data); @@ -1431,6 +1436,7 @@ COMPAT_SYSCALL_DEFINE4(ptrace, compat_long_t, request, compat_long_t, pid, compat_long_t, addr, compat_long_t, data) { struct task_struct *child; + ptrace_pre_hook_t *hook; long ret; if (request == PTRACE_TRACEME) { @@ -1444,11 +1450,14 @@ COMPAT_SYSCALL_DEFINE4(ptrace, compat_long_t, request, compat_long_t, pid, goto out; } - if (ptrace_pre_hook) { - ret = ptrace_pre_hook(request, pid, child, addr, data); - if (ret) - goto out_put_task_struct; - } + ret = 0; + rcu_read_lock(); + hook = rcu_dereference(ptrace_pre_hook); + if (hook) + ret = hook(request, pid, child, addr, data); + rcu_read_unlock(); + if (ret) + goto out_put_task_struct; if (request == PTRACE_ATTACH || request == PTRACE_SEIZE) { ret = ptrace_attach(child, request, addr, data); diff --git a/kernel/tkernel/ttools/ttools_module.c b/kernel/tkernel/ttools/ttools_module.c index e1302187a15b18f9971f54ea7344d8c6666abb8b..5ab89b2877f1f65025ebee4be86cb8e55848c8e1 100644 --- a/kernel/tkernel/ttools/ttools_module.c +++ b/kernel/tkernel/ttools/ttools_module.c @@ -15,7 +15,7 @@ #include #include #include -#include +#include #include "ttools.h" #define TTOOLS_MINOR 254 @@ -241,14 +241,6 @@ static struct miscdevice ttools_dev = { }; -static void flush_icache_1(void *info) -{ - smp_mb(); -#ifdef CONFIG_X86 - sync_core(); -#endif -} - static int ttools_init(void) { int ret; @@ -258,18 +250,21 @@ static int ttools_init(void) if (ret) return ret; - ptrace_pre_hook = ttools_ptrace_hook; - smp_wmb(); - smp_call_function(flush_icache_1, NULL, 1); + rcu_assign_pointer(ptrace_pre_hook, ttools_ptrace_hook); pr_info("ttools " TTOOLS_VER " loaded\n"); return 0; } static void ttools_exit(void) { - ptrace_pre_hook = NULL; - smp_wmb(); - smp_call_function(flush_icache_1, NULL, 1); + /* + * Publish the removal first, then wait for every ptrace caller that + * already loaded the old pointer. Returning before this grace period + * would let the module text be freed while such a caller is still + * about to execute ttools_ptrace_hook. + */ + rcu_assign_pointer(ptrace_pre_hook, NULL); + synchronize_rcu(); misc_deregister(&ttools_dev); ttools_clean_task_list(); pr_info("ttools " TTOOLS_VER " unloaded\n"); diff --git a/tools/testing/selftests/tkernel/ttools_ptrace.c b/tools/testing/selftests/tkernel/ttools_ptrace.c index cc14bffa15235875366eaa90b12ed6cd17842b8c..445f6c981294651026bb1ad798fbba03ff86ff5c 100644 --- a/tools/testing/selftests/tkernel/ttools_ptrace.c +++ b/tools/testing/selftests/tkernel/ttools_ptrace.c @@ -17,6 +17,10 @@ * Every request must fail with EPERM. Before the compat fix the * 32-bit tracer attached successfully to the protected tracee, * because compat_sys_ptrace() never invoked ptrace_pre_hook(). + * + * The shell wrapper can also run the --race stress mode, where several + * workers repeatedly PTRACE_ATTACH a plain tracee while ttools is + * unloaded and reloaded. */ #define _GNU_SOURCE #include @@ -29,6 +33,7 @@ #include #include #include +#include #include #ifdef TTOOLS_PTRACE_TRACER @@ -133,15 +138,138 @@ static void cleanup(void) } } -int main(void) +#define TTOOLS_RACE_WORKERS 4 +#define TTOOLS_RACE_DEFAULT_MS 4000 + +/* Keep a plain task alive so the stress workers can repeatedly enter the + * ptrace hook path while the module is being unloaded and reloaded. + */ +static pid_t spawn_race_tracee(void) +{ + int ready[2]; + char byte; + pid_t pid; + + if (pipe(ready)) + return -1; + + pid = fork(); + if (pid < 0) { + close(ready[0]); + close(ready[1]); + return -1; + } + if (pid == 0) { + close(ready[0]); + if (write(ready[1], "R", 1) != 1) + _exit(2); + close(ready[1]); + for (;;) + pause(); + } + + close(ready[1]); + if (read(ready[0], &byte, 1) != 1) { + close(ready[0]); + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + return -1; + } + close(ready[0]); + return pid; +} + +static int race_attach_loop(pid_t pid, long duration_ms) +{ + struct timespec start, now; + long elapsed; + int status; + + if (clock_gettime(CLOCK_MONOTONIC, &start)) + return 2; + + do { + if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == 0) { + if (waitpid(pid, &status, 0) != pid || + !WIFSTOPPED(status)) + return 2; + if (ptrace(PTRACE_DETACH, pid, NULL, NULL)) + return 2; + } else { + /* Another worker may currently own the shared tracee. */ + if (errno != EPERM && errno != EINTR) + return 2; + } + + if (clock_gettime(CLOCK_MONOTONIC, &now)) + return 2; + elapsed = (now.tv_sec - start.tv_sec) * 1000 + + (now.tv_nsec - start.tv_nsec) / 1000000; + } while (elapsed < duration_ms); + + return 0; +} + +static int race_main(void) +{ + pid_t workers[TTOOLS_RACE_WORKERS]; + pid_t tracee; + const char *env; + char *end; + long duration_ms = TTOOLS_RACE_DEFAULT_MS; + int started = 0; + int rc = 0; + int i; + + env = getenv("TTOOLS_PTRACE_RACE_MS"); + if (env && *env) { + long value = strtol(env, &end, 10); + + if (!*end && value > 0) + duration_ms = value; + } + + tracee = spawn_race_tracee(); + if (tracee < 0) + return 2; + + for (i = 0; i < TTOOLS_RACE_WORKERS; i++) { + pid_t worker = fork(); + + if (worker < 0) { + rc = 2; + break; + } + if (worker == 0) + _exit(race_attach_loop(tracee, duration_ms)); + workers[started++] = worker; + } + + for (i = 0; i < started; i++) { + int status; + + if (waitpid(workers[i], &status, 0) != workers[i] || + !WIFEXITED(status) || WEXITSTATUS(status)) + rc = 2; + } + + kill(tracee, SIGKILL); + waitpid(tracee, NULL, 0); + return rc; +} + +int main(int argc, char **argv) { int ready[2]; char ready_byte; const char *helper; int rc; + if (argc > 1 && !strcmp(argv[1], "--race")) + return race_main(); + ksft_print_header(); - ksft_set_plan(4); + ksft_set_plan(getenv("TTOOLS_PTRACE_EXTRA_TEST") ? 5 : 4); helper = getenv("TTOOLS_PTRACE_32"); if (!helper || access(helper, X_OK)) diff --git a/tools/testing/selftests/tkernel/ttools_ptrace.sh b/tools/testing/selftests/tkernel/ttools_ptrace.sh index 5d193ef5a14c5c09cc9902b3e942cbacb15f2701..cca06fb20cf3c28b7b5ce7836bd1f2d619e4dd44 100644 --- a/tools/testing/selftests/tkernel/ttools_ptrace.sh +++ b/tools/testing/selftests/tkernel/ttools_ptrace.sh @@ -2,8 +2,9 @@ # SPDX-License-Identifier: GPL-2.0 DEV=/dev/ttools -TESTS=4 helper32= +race_pid= +race_module_unloaded=0 script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) driver="$script_dir/ttools_ptrace" @@ -15,6 +16,19 @@ skip_unavailable() ksft_skip_all "$1" } +cleanup() +{ + if [ -n "$race_pid" ]; then + kill "$race_pid" 2>/dev/null + wait "$race_pid" 2>/dev/null + race_pid= + fi + if [ "$race_module_unloaded" -eq 1 ] && + command -v modprobe >/dev/null 2>&1; then + modprobe ttools 2>/dev/null || true + fi +} + if [ "$(id -u)" -ne 0 ]; then skip_unavailable "root privileges are required" fi @@ -27,8 +41,62 @@ if [ ! -e "$DEV" ] && command -v modprobe >/dev/null 2>&1; then fi [ -c "$DEV" ] || skip_unavailable "CONFIG_TKERNEL_TTOOLS is not enabled" -echo "TAP version 13" -TTOOLS_PTRACE_32="$helper32" "$driver" +trap cleanup EXIT INT TERM + +race_available=0 +if command -v modprobe >/dev/null 2>&1 && dmesg >/dev/null 2>&1; then + race_available=1 +fi + +if [ "$race_available" -eq 1 ]; then + TTOOLS_PTRACE_EXTRA_TEST=1 TTOOLS_PTRACE_32="$helper32" "$driver" +else + TTOOLS_PTRACE_32="$helper32" "$driver" +fi rc=$? [ "$rc" -ne 2 ] || ksft_skip_all "the ttools_ptrace driver hit a harness error" + +if [ "$rc" -eq 1 ] && [ "$race_available" -eq 1 ]; then + echo "ok 5 - ptrace survives concurrent ttools reload # SKIP driver failed" +fi + +if [ "$rc" -eq 0 ] && [ "$race_available" -eq 1 ]; then + if ! modprobe -r ttools 2>/dev/null; then + echo "ok 5 - ptrace survives concurrent ttools reload # SKIP no unload" + exit "$rc" + fi + + race_module_unloaded=1 + dmesg_before=$(dmesg | wc -l | tr -d ' ') + TTOOLS_PTRACE_RACE_MS=4000 "$driver" --race >/dev/null 2>&1 & + race_pid=$! + sleep 1 + rounds=0 + while kill -0 "$race_pid" 2>/dev/null; do + modprobe ttools 2>/dev/null || break + modprobe -r ttools 2>/dev/null || break + rounds=$((rounds + 1)) + done + wait "$race_pid" + race_rc=$? + race_pid= + modprobe ttools 2>/dev/null || true + race_module_unloaded=0 + + new_dmesg=$(dmesg | tail -n +$((dmesg_before + 1))) + race_ok=0 + if [ "$race_rc" -eq 0 ] && [ "$rounds" -gt 0 ] && + ! printf '%s\n' "$new_dmesg" | + grep -Eq 'BUG:|KASAN:|Oops:|general protection fault|use-after-free'; then + race_ok=1 + fi + + if [ "$race_ok" -eq 1 ]; then + echo "ok 5 - ptrace survives concurrent ttools reload" + else + echo "not ok 5 - ptrace survives concurrent ttools reload" + rc=1 + fi +fi + exit "$rc"