Vega VEGA / Bug List / CVE-2026-23274
Finding · Linux kernel · netfilter · Apr 14, 2026
CVE-2026-23274

Netfilter idletimer use-before-initialization leads to control-flow hijack

net/netfilter/xt_IDLETIMER.c · idletimer_tg_checkentry
Affects Linux v5.7-rc1+ COS CAP_NET_ADMIN Container workloads
Disclosure timeline
  1. Reported
    Mar 01, 2026
  2. Confirmed
    Mar 08, 2026
  3. Patched
    Apr 01, 2026
  4. Disclosed
    Apr 14, 2026

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_ADMIN in 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 outline
create_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 object

The 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:

  1. leak the kernel base,
  2. use the uninitialized timer_list in mod_timer() to seize the callback path,
  3. pivot into a fake stack staged in writable kernel memory,
  4. queue a fake work item so the final payload runs in process context,
  5. 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.

Read the deep-dive

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_ADMIN into untrusted containers,
  • disable or tightly restrict unprivileged user namespaces where that is still operationally acceptable,
  • audit uses of xt_IDLETIMER and 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.

Related findings

See all ↗
CVE-2026-31418pending

`mtype_del()` can shrink a bucket with `n->size == AHASH_INIT_SIZE` down to `tmp->size = 0` (`n->size - AHASH_INIT_SIZE`) while leaving an empty bucket object installed. Later, if the set is full and forceadd is enabled, `mtype_add()` takes the `reuse || forceadd` branch before the grow-path, forces `j = 0`, and computes `data = ahash_data(n, j, set->dsize)` even when `n->size == 0` and `n->pos == 0`. It then calls `ip_set_ext_destroy(set, data)` and eventually `memcpy(data, d, sizeof(struct mtype_elem))`, causing out-of-bounds heap access/write past the zero-element bucket allocation.

out-of-bound
CVE-2026-31685pending

`eui64_mt6()` only rejects an invalid or unset MAC header when `par->fragoff != 0`. For non-fragment packets, it still reaches `eth_hdr(skb)` and dereferences `h_proto`/`h_source` even if `skb_mac_header(skb)` does not describe a valid Ethernet header. A crafted skb on the matcher's `PRE_ROUTING`/`LOCAL_IN`/`FORWARD` hooks can therefore trigger out-of-bounds reads from skb headroom or unrelated memory.

out-of-bound
CVE-2026-31637CNA 9.8

`rxkad_decrypt_ticket()` never checks the return value of `crypto_skcipher_decrypt()`. `rxkad_verify_response()` only constrains `ticket_len` to 4..1024, so a non-block- aligned ticket can make decryption fail while the function continues parsing attacker- controlled bytes as if they were a plaintext ticket and session key. An attacker can then craft the RESPONSE body around that chosen session key and bypass the server secret.

others
Vega · AI security research

Find bugs in your code. Before anyone else does.

Join the waitlist