Technical Root Cause Analysis
Overview
The reported bug is a post-decrement slab-use-after-free in put_ctx() at kernel/events/core.c:1394. A thread dropping a reference to struct perf_event_context accesses ctx->task after refcount_dec_and_test(&ctx->refcount) returns false. Because the thread no longer owns a reference to ctx and does not hold an RCU read-side critical section, a concurrent thread can release the final reference, leading to RCU reclamation and freeing of ctx before the first thread completes its access.
Key Entities and Files Involved
- Source File:
kernel/events/core.c
- Functions:
put_ctx() (kernel/events/core.c:1384–1397)
__free_event() (kernel/events/core.c:5812–5818)
perf_event_exit_task_context() (kernel/events/core.c:14744–14820)
free_ctx() (kernel/events/core.c:1376–1382)
- Structures & Fields:
struct perf_event_context:
refcount (refcount_t): reference counter for the context object.
task (struct task_struct *, offset 296): pointer to the task tracked by this context (or TASK_TOMBSTONE when dead).
rcu_head: RCU callback head for deferred freeing via free_ctx().
Detailed Trigger Mechanism & Invariant Violation
In kernel/events/core.c, put_ctx() is defined as follows:
static void put_ctx(struct perf_event_context *ctx)
{
if (refcount_dec_and_test(&ctx->refcount)) {
if (ctx->parent_ctx)
put_ctx(ctx->parent_ctx);
if (ctx->task && ctx->task != TASK_TOMBSTONE)
put_task_struct(ctx->task);
call_rcu(&ctx->rcu_head, free_ctx);
} else {
smp_mb__after_atomic(); /* pairs with wait_var_event() */
if (ctx->task == TASK_TOMBSTONE)
wake_up_var(&ctx->refcount);
}
}
Standard reference counting rules dictate that once a caller calls refcount_dec_and_test(&ctx->refcount) and receives false, it has relinquished its reference to ctx. Unless protected by another mechanism (such as holding rcu_read_lock() or holding a lock embedded in a parent container), the caller cannot safely dereference ctx after the decrement because another holder can concurrently drop the final reference and free the object.
Here, the post-decrement check was added to wake up wait_var_event(&ctx->refcount, ...) in perf_event_exit_task_context() (which waits for event references to be dropped when !exit). However, this violates memory safety during task exit:
-
Context State:
A task context (struct perf_event_context *ctx) has two references:
- One held by an open perf event (
event->ctx).
- One held by the target task's exit sequence via
perf_pin_task_context() in perf_event_exit_task_context(task, exit=true) (after dropping the task's task->perf_event_ctxp reference at line 14779).
ctx->task has been marked WRITE_ONCE(ctx->task, TASK_TOMBSTONE) at line 14780.
-
Concurrent Decrement:
- Task A (PID 8324, event closer): Closes the event file (
__fput $\to$ perf_release $\to$ perf_event_release_kernel $\to$ __free_event), which invokes put_ctx(event->ctx) at line 5817.
refcount_dec_and_test(&ctx->refcount) decrements ctx->refcount from 2 to 1 and returns false.
- Task A enters the
else branch. In a preemptible kernel (PREEMPT(full)), Task A is preempted or delayed right before line 1394 without holding an RCU read lock.
-
Final Free:
- Task B (PID 8338, exiting target task): In
perf_event_exit_task_context(task, true), because exit == true, it skips wait_var_event() and calls put_ctx(ctx) at line 14819.
refcount_dec_and_test(&ctx->refcount) decrements ctx->refcount from 1 to 0 and returns true.
- Task B calls
call_rcu(&ctx->rcu_head, free_ctx).
- The RCU grace period elapses, and the RCU core executes
free_ctx(), which calls kfree(ctx).
-
Use-After-Free: