[PATCH 8/8] kprobes: Add KUnit test cases for kprobes regressions
From: Masami Hiramatsu (Google)
Date: Tue Nov 03 2026 - 19:04:10 EST
From: Masami Hiramatsu (Google) <mhiramat@xxxxxxxxxx>
Add KUnit test cases to lib/tests/test_kprobes.c to verify that the
recent regressions in kprobes registration, ftrace ops switching, and
optimization state transitions are resolved:
1. test_kretprobe_unregistered_cleanup:
Verifies that unregistering an uninitialized or already-unregistered
kretprobe does not cause a NULL pointer dereference or double free.
2. test_kretprobe_rereg_by_symbol:
Verifies that attempting to re-register an active kretprobe by symbol
name fails with -EINVAL and does not destroy the active probe's
underlying rethook state.
3. test_kprobe_ftrace_post_handler_switch:
Verifies that adding a second kprobe with a post_handler to a probe
point armed with kprobe_ftrace_ops correctly switches to
kprobe_ipmodify_ops and both probes cleanly unregister without
disarming errors (-ENOENT).
4. test_optprobe_rearm_unoptimizing:
Verifies that disabling an optimized kprobe and immediately
re-enabling it before the optimizer thread runs does not overwrite the
jump instruction opcode or crash on execution.
Signed-off-by: Masami Hiramatsu (Google) <mhiramat@xxxxxxxxxx>
---
lib/tests/test_kprobes.c | 123 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 123 insertions(+)
diff --git a/lib/tests/test_kprobes.c b/lib/tests/test_kprobes.c
index 06e729e4de05..c20f70cc63d7 100644
--- a/lib/tests/test_kprobes.c
+++ b/lib/tests/test_kprobes.c
@@ -358,8 +358,123 @@ static void test_stacktrace_on_nested_kretprobe(struct kunit *test)
}
#endif /* CONFIG_ARCH_CORRECT_STACKTRACE_ON_KRETPROBE */
+/* Test for: kprobes: Fix double free and NULL deref in unregister_kretprobes() */
+static void test_kretprobe_unregistered_cleanup(struct kunit *test)
+{
+ struct kretprobe unreg_rp = {
+ .kp.symbol_name = "kprobe_target",
+ };
+
+ current_test = test;
+
+ /* Unregistering a probe that was never registered must return cleanly */
+ unregister_kretprobe(&unreg_rp);
+
+ /* Double unregister of a valid registered probe */
+ KUNIT_ASSERT_EQ(test, 0, register_kretprobe(&unreg_rp));
+ unregister_kretprobe(&unreg_rp);
+ /* Second unregister must not cause double-free or NULL pointer dereference */
+ unregister_kretprobe(&unreg_rp);
+}
+
+/* Test for: kprobes: Validate re-registration earlier in register_kretprobe() */
+static void test_kretprobe_rereg_by_symbol(struct kunit *test)
+{
+ struct kretprobe test_rp = {
+ .kp.symbol_name = "kprobe_target",
+ .handler = return_handler,
+ .entry_handler = entry_handler,
+ };
+
+ current_test = test;
+ KUNIT_ASSERT_EQ(test, 0, register_kretprobe(&test_rp));
+
+ /* Verify the probe is live and functional */
+ krph_val = 0;
+ target(rand1);
+ KUNIT_EXPECT_EQ(test, krph_val, rand1);
+
+ /*
+ * Attempt re-registration with symbol_name (with kp.addr cleared).
+ * Must fail with -EINVAL and NOT destroy the live probe's rethook.
+ */
+ test_rp.kp.addr = NULL;
+ KUNIT_EXPECT_EQ(test, -EINVAL, register_kretprobe(&test_rp));
+
+ /* The original kretprobe must still be functional */
+ krph_val = 0;
+ target(rand1);
+ KUNIT_EXPECT_EQ(test, krph_val, rand1);
+
+ unregister_kretprobe(&test_rp);
+}
+
#endif /* CONFIG_KRETPROBES */
+#if defined(CONFIG_KPROBES_ON_FTRACE)
+/* Test for: kprobes: Switch ftrace ops when adding probe with post_handler */
+static void test_kprobe_ftrace_post_handler_switch(struct kunit *test)
+{
+ struct kprobe kp_entry = {
+ .symbol_name = "kprobe_target",
+ .pre_handler = kp_pre_handler,
+ };
+ struct kprobe kp_post = {
+ .symbol_name = "kprobe_target",
+ .post_handler = kp_post_handler,
+ };
+
+ current_test = test;
+
+ /* 1. Register first probe without post_handler (armed with kprobe_ftrace_ops) */
+ KUNIT_ASSERT_EQ(test, 0, register_kprobe(&kp_entry));
+
+ /* 2. Register second probe with post_handler at same IP (must switch ops) */
+ KUNIT_ASSERT_EQ(test, 0, register_kprobe(&kp_post));
+
+ /* 3. Execute target function to trigger both */
+ preh_val = 0;
+ posth_val = 0;
+ target(rand1);
+ KUNIT_EXPECT_NE(test, 0, preh_val);
+ KUNIT_EXPECT_NE(test, 0, posth_val);
+
+ /* 4. Unregistering must successfully disarm without -ENOENT */
+ unregister_kprobe(&kp_post);
+ unregister_kprobe(&kp_entry);
+}
+#endif
+
+#if defined(CONFIG_OPTPROBES)
+/* Test for: kprobes: Do not overwrite jump instruction when re-arming unoptimizing probe */
+static void test_optprobe_rearm_unoptimizing(struct kunit *test)
+{
+ struct kprobe opt_kp = {
+ .symbol_name = "kprobe_target",
+ .pre_handler = kp_pre_handler,
+ };
+
+ current_test = test;
+ KUNIT_ASSERT_EQ(test, 0, register_kprobe(&opt_kp));
+
+ /* Allow background optimizer thread to jump-optimize the probe */
+ wait_for_kprobe_optimizer();
+
+ /* Temporarily disable probe (queues on unoptimizing_list, leaves jump) */
+ KUNIT_ASSERT_EQ(test, 0, disable_kprobe(&opt_kp));
+
+ /* Immediately re-enable (re-arm) before optimizer thread unoptimizes */
+ KUNIT_ASSERT_EQ(test, 0, enable_kprobe(&opt_kp));
+
+ /* Verify jump opcode was not corrupted by an unaligned INT3 overwrite */
+ preh_val = 0;
+ target(rand1);
+ KUNIT_EXPECT_NE(test, 0, preh_val);
+
+ unregister_kprobe(&opt_kp);
+}
+#endif
+
static int kprobes_test_init(struct kunit *test)
{
KP_CLEAR(kp);
@@ -391,10 +506,18 @@ static struct kunit_case kprobes_testcases[] = {
#ifdef CONFIG_KRETPROBES
KUNIT_CASE(test_kretprobe),
KUNIT_CASE(test_kretprobes),
+ KUNIT_CASE(test_kretprobe_unregistered_cleanup),
+ KUNIT_CASE(test_kretprobe_rereg_by_symbol),
#ifdef CONFIG_ARCH_CORRECT_STACKTRACE_ON_KRETPROBE
KUNIT_CASE(test_stacktrace_on_kretprobe),
KUNIT_CASE(test_stacktrace_on_nested_kretprobe),
#endif
+#endif
+#if defined(CONFIG_KPROBES_ON_FTRACE)
+ KUNIT_CASE(test_kprobe_ftrace_post_handler_switch),
+#endif
+#if defined(CONFIG_OPTPROBES)
+ KUNIT_CASE(test_optprobe_rearm_unoptimizing),
#endif
{}
};