Summary
xt_IDLETIMER revision 0 can reuse an object created by revision 1 without
revalidating the timer backend. If the first rule used
XT_IDLETIMER_ALARM, the rev1 path initializes the alarm state but never calls
timer_setup() for the embedded timer_list. A later rev0 rule with the same
label then calls mod_timer() on that uninitialized timer object.
That turns a netfilter rule-reuse bug into a real memory-corruption primitive.
Because the uninitialized timer_list sits inside a kmalloc-256 object, an
attacker who can reclaim that slab can steer the callback path and turn the bug
into control-flow hijack.
Impact & exploitability
The interesting part of this bug is not just that it corrupts timer internals;
it does so on attacker-influenced memory in a shape that is directly useful for
exploitation. The affected path is reachable from CAP_NET_ADMIN context, and
in practice that means container or namespace-enabled environments are the most
interesting targets.
In our kernelCTF exploit, the bug yielded a controllable function pointer
inside a forged timer_list, which was enough to pivot into a longer ROP chain.
This was not a “panic only” bug or a hypothetical slab hazard. We used it to
reach code execution in kernel context and read the challenge flag.
- Attacker model: local attacker with
CAP_NET_ADMINin the target networking context - Primitive: use-before-initialization on
struct timer_list - Practical impact: control-flow hijack, then process-context file read via queued work item
Root cause
The rev1 creation path supports an alarm-backed timer mode. In that mode the
kernel initializes info->timer->alarm, but it never initializes
info->timer->timer:
if (info->timer->timer_type & XT_IDLETIMER_ALARM) { ktime_t tout; alarm_init(&info->timer->alarm, ALARM_BOOTTIME, idletimer_tg_alarmproc); info->timer->alarm.data = info->timer; tout = ktime_set(info->timeout, 0); alarm_start_relative(&info->timer->alarm, tout);} else { timer_setup(&info->timer->timer, idletimer_tg_expired, 0); mod_timer(&info->timer->timer, msecs_to_jiffies(info->timeout * 1000) + jiffies);}That would be fine if later users of the shared object always respected the
backend type. The bug is that rev0 does not. When a rule with the same label is
installed through the rev0 path, idletimer_tg_checkentry() reuses the object
and calls mod_timer() unconditionally:
info->timer = __idletimer_tg_find_by_label(info->label);if (info->timer) { info->timer->refcnt++; mod_timer(&info->timer->timer, msecs_to_jiffies(info->timeout * 1000) + jiffies);}Rev1 already had a timer-type consistency check. Rev0 did not. That gap is the
entire vulnerability: one path creates an alarm-only object, the other path
assumes a regular timer_list, and the shared-label reuse logic lets the two
worlds collide.
Reproducer
The minimal reproducer is the two-rule sequence that creates the mismatched state:
// high-level reproducer outlinecreate_rev1_rule({ .label = "shared-idletimer", .timer_type = XT_IDLETIMER_ALARM,});
create_rev0_rule({ .label = "shared-idletimer",});
// rev0 reuses the rev1-created object and calls mod_timer() on an// uninitialized timer_list embedded in that objectThe order matters. Creating the rev1 ALARM object first and reusing it from
rev0 is what exposes the uninitialized timer_list; doing it the other way
around does not hit the same bug.
Exploit
Our public exploit chain was:
- leak the kernel base,
- use the uninitialized
timer_listinmod_timer()to seize the callback path, - pivot into a fake stack staged in writable kernel memory,
- queue a fake work item so the final payload runs in process context,
- open and read
/flag, then print it to the kernel log.
The key exploitation detail is that __mod_timer() rewrites parts of the timer
object, so the cleanest target is the callback path itself. We used the timer
corruption to obtain an arb_function(EVIL_TIMER_LIST)-style primitive, then
used that to build a second-stage pivot into a larger ROP chain.
The full kernelCTF walk-through, including the gadget chain, fake work item, and process-context pivot, lives in the existing blog post.
Patch
The fix is conceptually simple: rev0 must stop reusing an existing timer object
unless the backend type matches. In practice that means pulling the same
timer-type consistency check into the rev0 reuse path before it calls
mod_timer().
Representative patch shape:
info->timer = __idletimer_tg_find_by_label(info->label);if (info->timer) { if (info->timer->timer_type != info->timer_type) { mutex_unlock(&list_mutex); return -EINVAL; } info->timer->refcnt++; mod_timer(&info->timer->timer, msecs_to_jiffies(info->timeout * 1000) + jiffies);}Upstream fixed the issue after the kernelCTF submission, and the blog write-up
notes that the patch landed in v7.0-rc4.
Mitigation
The correct mitigation is to deploy the upstream fix. Short of that, the
practical exposure comes from environments that let an attacker reach the
xt_IDLETIMER configuration path with sufficient networking privileges.
If you need an interim reduction in risk:
- avoid delegating
CAP_NET_ADMINinto untrusted containers, - disable or tightly restrict unprivileged user namespaces where that is still operationally acceptable,
- audit uses of
xt_IDLETIMERand avoid mixed rev0/rev1 label reuse on affected kernels.
For most operators, this is not a tuning problem. It is a patching problem.
Timeline & credit
We discovered the bug in March 2026, validated exploitation against the kernelCTF target, and disclosed it after the upstream fix was ready. The public deep-dive and exploit notes went live on Apr 14, 2026.
This per-CVE page is the short-form reference. The existing blog post remains the long-form disclosure, including the exploitation narrative and the full ROP appendix.