---
title: "CVE-2026-23274 — Netfilter idletimer use-before-initialization leads to control-flow hijack"
description: "Netfilter idletimer use-before-initialization leads to control-flow hijack (net/netfilter/xt_IDLETIMER.c · idletimer_tg_checkentry)"
url: https://nebusec.ai/buglist/CVE-2026-23274/
published: 2026-04-14T00:00:00.000Z
---

CVE-2026-23274 · Critical

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

Linux kernel · netfilter · Published Apr 14, 2026 · $10,050 kernelCTF bounty

net/netfilter/xt_IDLETIMER.c · idletimer_tg_checkentry

* [CVSS 7.8 · CNA ↗](https://nvd.nist.gov/vuln/detail/CVE-2026-23274)
* Fixed
* Use before init
* Namespace required

Affects · Linux v5.7-rc1+ · COS · CAP_NET_ADMIN · Container workloads

[View reproducer→](#reproducer) · [Research deep-dive](https://nebusec.ai/research/cve-2026-23274-cos.md)

Artifacts · [reproducer](#reproducer) · [exploit.md](#exploit) · [patch.diff](#patch)

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

```c
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:

```c
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:

```c
// 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 →](https://nebusec.ai/research/cve-2026-23274-cos.md)

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

```diff
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 findings →](https://nebusec.ai/buglist/)

[CVE-2026-31418](https://nvd.nist.gov/vuln/detail/CVE-2026-31418)

[`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.](https://nvd.nist.gov/vuln/detail/CVE-2026-31418)

[Out-of-bounds-write · Score 5.5 · NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-31418)

· [CVE-2026-31685](https://nvd.nist.gov/vuln/detail/CVE-2026-31685)

[`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.](https://nvd.nist.gov/vuln/detail/CVE-2026-31685)

[Out-of-bounds-read · Score 9.4 · NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-31685)

· [CVE-2026-31637](https://nvd.nist.gov/vuln/detail/CVE-2026-31637)

[`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.](https://nvd.nist.gov/vuln/detail/CVE-2026-31637)

[Policy-bypass · Score 9.8 · NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-31637)

Find bugs in your code.

Before anyone else does.

[Book a demo](https://nebusec.ai/book-a-demo/)
