diff --git a/include/linux/ptrace.h b/include/linux/ptrace.h index dc1fb15fa8f45670c743ff9994040cecf1ad7abc..c60edd4f2aedbed2143329eec8d943fe83642009 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 /* for syscall_get_* */ @@ -1279,9 +1280,30 @@ 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); +static int ptrace_run_pre_hook(long request, long pid, struct task_struct *task, + long addr, long data) +{ + ptrace_pre_hook_t *hook; + int ret = 0; + + /* + * The hook may live in a loadable module. Keep the load and the + * call in the same RCU read-side critical section so that such a + * module can use synchronize_rcu() to wait for an in-flight call + * before its text is freed. + */ + rcu_read_lock(); + hook = rcu_dereference(ptrace_pre_hook); + if (hook) + ret = hook(request, pid, task, addr, data); + rcu_read_unlock(); + + return ret; +} + SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, unsigned long, data) { @@ -1299,11 +1321,9 @@ 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 = ptrace_run_pre_hook(request, pid, child, addr, data); + if (ret) + goto out_put_task_struct; if (request == PTRACE_ATTACH || request == PTRACE_SEIZE) { ret = ptrace_attach(child, request, addr, data); @@ -1444,11 +1464,9 @@ 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 = ptrace_run_pre_hook(request, pid, child, addr, data); + 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..e8ad70976d6dcb6658e10c86b47e5c1ae6510f43 100644 --- a/kernel/tkernel/ttools/ttools_module.c +++ b/kernel/tkernel/ttools/ttools_module.c @@ -16,6 +16,7 @@ #include #include #include +#include #include "ttools.h" #define TTOOLS_MINOR 254 @@ -258,8 +259,7 @@ static int ttools_init(void) if (ret) return ret; - ptrace_pre_hook = ttools_ptrace_hook; - smp_wmb(); + rcu_assign_pointer(ptrace_pre_hook, ttools_ptrace_hook); smp_call_function(flush_icache_1, NULL, 1); pr_info("ttools " TTOOLS_VER " loaded\n"); return 0; @@ -267,8 +267,14 @@ static int ttools_init(void) static void ttools_exit(void) { - ptrace_pre_hook = NULL; - smp_wmb(); + /* + * The ptrace hook is not covered by this module's refcount: a task + * may have loaded the exported pointer just before exit runs. Clear + * it under RCU and wait for such readers before the module text can + * be freed by module unload. + */ + rcu_assign_pointer(ptrace_pre_hook, NULL); + synchronize_rcu(); smp_call_function(flush_icache_1, NULL, 1); misc_deregister(&ttools_dev); ttools_clean_task_list(); diff --git a/tools/testing/selftests/tkernel/ttools_ptrace.c b/tools/testing/selftests/tkernel/ttools_ptrace.c index cc14bffa15235875366eaa90b12ed6cd17842b8c..bba4cb56271ac36c9502e443d949ad77930d0ae9 100644 --- a/tools/testing/selftests/tkernel/ttools_ptrace.c +++ b/tools/testing/selftests/tkernel/ttools_ptrace.c @@ -14,6 +14,10 @@ * compat PTRACE_ATTACH (ttools_ptrace_32 helper) * compat PTRACE_SEIZE (ttools_ptrace_32 helper) * + * When the module is unloadable, a fifth test keeps PTRACE_ATTACH in + * flight while ttools is repeatedly unloaded and reloaded, then checks + * the kernel log for KASAN and oops reports. + * * 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(). @@ -30,6 +34,7 @@ #include #include #include +#include #ifdef TTOOLS_PTRACE_TRACER @@ -133,15 +138,146 @@ static void cleanup(void) } } +#define TTOOLS_RELOAD_CYCLES 50 + +static int count_dmesg_lines(void) +{ + char line[2048]; + FILE *fp; + int lines = 0; + + fp = popen("dmesg 2>/dev/null", "r"); + if (!fp) + return -1; + while (fgets(line, sizeof(line), fp)) + lines++; + if (pclose(fp) != 0) + return -1; + return lines; +} + +static int dmesg_has_new_failure(int skip) +{ + static const char *const patterns[] = { + "BUG: KASAN", + "Oops:", + "general protection fault", + "Unable to handle kernel", + "kernel NULL pointer dereference", + }; + char line[2048]; + FILE *fp; + int lines = 0; + int found = 0; + size_t i; + + fp = popen("dmesg 2>/dev/null", "r"); + if (!fp) + return -1; + while (fgets(line, sizeof(line), fp)) { + if (++lines <= skip) + continue; + for (i = 0; i < ARRAY_SIZE(patterns); i++) { + if (strstr(line, patterns[i])) { + found = 1; + break; + } + } + if (found) + break; + } + if (pclose(fp) != 0 && !found) + return -1; + return found; +} + +/* + * Keep issuing PTRACE_ATTACH while another process unloads and reloads + * ttools. The tracee holds no /dev/ttools reference, so the module can + * reach a zero refcount while this process is between reading + * ptrace_pre_hook and calling into the module. + */ +static int reload_stress(unsigned long *attempts_out) +{ + pid_t tracee_pid, cycler; + int status, i, rc = 0; + int cycler_reaped = 0; + unsigned long attempts = 0; + + tracee_pid = fork(); + if (tracee_pid < 0) + return -1; + if (tracee_pid == 0) { + prctl(PR_SET_PDEATHSIG, SIGKILL); + for (;;) + pause(); + } + + cycler = fork(); + if (cycler < 0) { + kill(tracee_pid, SIGKILL); + waitpid(tracee_pid, NULL, 0); + return -1; + } + if (cycler == 0) { + prctl(PR_SET_PDEATHSIG, SIGKILL); + for (i = 0; i < TTOOLS_RELOAD_CYCLES; i++) { + if (system("modprobe -r ttools 2>/dev/null") || + system("modprobe ttools 2>/dev/null")) + _exit(1); + } + _exit(0); + } + + for (;;) { + pid_t ret; + + if (ptrace(PTRACE_ATTACH, tracee_pid, NULL, NULL) == 0) { + ptrace(PTRACE_DETACH, tracee_pid, NULL, NULL); + kill(tracee_pid, SIGCONT); + } else if (errno != EPERM) { + rc = -1; + break; + } + attempts++; + + ret = waitpid(cycler, &status, WNOHANG); + if (ret == cycler) { + cycler_reaped = 1; + if (!WIFEXITED(status) || WEXITSTATUS(status)) + rc = -1; + break; + } + if (ret < 0 && errno != EINTR) { + rc = -1; + break; + } + } + + if (!cycler_reaped) { + kill(cycler, SIGKILL); + waitpid(cycler, &status, 0); + } + kill(tracee_pid, SIGKILL); + waitpid(tracee_pid, NULL, 0); + if (!attempts) + rc = -1; + *attempts_out = attempts; + return rc; +} + int main(void) { int ready[2]; char ready_byte; const char *helper; + const char *stress; + unsigned long attempts = 0; + int log_baseline; int rc; ksft_print_header(); - ksft_set_plan(4); + ksft_set_plan(5); helper = getenv("TTOOLS_PTRACE_32"); if (!helper || access(helper, X_OK)) @@ -175,7 +311,22 @@ int main(void) rc = helper_trace(helper, "SEIZE"); ksft_test_result(rc == 1, "compat PTRACE_SEIZE on a protected tracee is denied\n"); - return ksft_exit_pass(); + stress = getenv("TTOOLS_RELOAD_STRESS"); + if (stress && !strcmp(stress, "1")) { + log_baseline = count_dmesg_lines(); + rc = reload_stress(&attempts); + if (rc == 0 && log_baseline >= 0) { + if (dmesg_has_new_failure(log_baseline) > 0) + rc = -1; + } + ksft_test_result(rc == 0, + "ttools unload/reload under ptrace load stays safe (%lu attempts)\n", + attempts); + } else { + ksft_test_result_skip("ttools reload stress is unavailable\n"); + } + + ksft_finished(); } #endif diff --git a/tools/testing/selftests/tkernel/ttools_ptrace.sh b/tools/testing/selftests/tkernel/ttools_ptrace.sh index 5d193ef5a14c5c09cc9902b3e942cbacb15f2701..effe6cd94868cc355859a913d99f369d6b8f089e 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 +TESTS=5 helper32= +reload_stress=0 script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) driver="$script_dir/ttools_ptrace" @@ -27,8 +28,16 @@ if [ ! -e "$DEV" ] && command -v modprobe >/dev/null 2>&1; then fi [ -c "$DEV" ] || skip_unavailable "CONFIG_TKERNEL_TTOOLS is not enabled" +if command -v modprobe >/dev/null 2>&1 && modprobe -r ttools 2>/dev/null; then + if modprobe ttools 2>/dev/null; then + reload_stress=1 + else + skip_unavailable "cannot reload ttools after unloading it" + fi +fi + echo "TAP version 13" -TTOOLS_PTRACE_32="$helper32" "$driver" +TTOOLS_PTRACE_32="$helper32" TTOOLS_RELOAD_STRESS="$reload_stress" "$driver" rc=$? [ "$rc" -ne 2 ] || ksft_skip_all "the ttools_ptrace driver hit a harness error" exit "$rc"