---
id: "CVE-2026-98547"
kind: "vulnerability"
aliases: ["CVE-2026-98547"]
title: "Linux kernel: af_packet integer overflow lets local user spin CPU"
severity: "high"
cvss: null
epss: null
exploited: false
status: "open"
category: "runtime"
products: []
ecosystems: []
tags: ["containers", "linux-kernel", "network-host", "runtime", "self-hosted"]
not_affected_if: ["kernel-patched", "no-cap-net-raw", "non-1gbps-nic"]
published_at: "2026-10-18T16:17:06+00:00"
first_seen_at: "2026-10-23T21:57:15+00:00"
updated_at: "2026-10-25T05:23:36+00:00"
sources: [{"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-98547", "first_seen_at": "2026-10-23T21:57:15+00:00"}]
---

# Linux kernel: af_packet integer overflow lets local user spin CPU

An integer overflow in the AF_PACKET TPACKET_V3 ring setup can produce a negative retire timeout on a 1 Gbps NIC, causing a high-rate hrtimer loop in softirq context. An unprivileged user with CAP_NET_RAW can exhaust a CPU and, with several rings, take the machine down.

_Summary, check and fix written by a model from the sources; labelled as such._

## How to check

- Run `uname -r` and compare against the kernel version that carries the fix; the sources do not list affected versions.
- Check whether unprivileged users or containers hold CAP_NET_RAW, for example with `capsh --print` inside a `--network host` container.

## Fix

- Update the Linux kernel to a release that includes the fix that computes prb_calc_retire_blk_tmo() in 64-bit arithmetic; the sources do not name the fixed version.
- Until patched, restrict CAP_NET_RAW for untrusted workloads.

## Timeline

- 2026-10-23T21:57:15+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-98547

## From the sources

In the Linux kernel, the following vulnerability has been resolved:

af_packet: fix integer overflow in prb_calc_retire_blk_tmo()

prb_calc_retire_blk_tmo() computes in 32-bit int arithmetic:

        mbits = (blk_size_in_bytes * 8) / (1024 * 1024);

If I'm reading the validation right, tp_block_size is user
controlled and packet_set_ring() only rejects values that are <= 0
as int or not page aligned, so a 256MiB block goes right through
(and alloc_one_pg_vec_page() even has a vzalloc fallback for it).
0x10000000 * 8 wraps to INT_MIN, and on a NIC reporting 1 Gbps
(div == 1) the function ends up returning -2047.

The condition is actually (8 * size) mod 2^32 >= 2^31 && div == 1,
so the trigger set is [256,512), [768,1024), [1280,1536) and
[1792,2048) MiB. Other sizes wrap to non-negative values and faster
links divide the unsigned value back below 2^31, which is why this
doesn't blow up for everyone.

What makes it fatal is what happens next in init_prb_bdqc():

        p1->interval_ktime = ms_to_ktime(prb_calc_retire_blk_tmo(...));
        hrtimer_start(&p1->retire_blk_timer, p1->interval_ktime,
                      HRTIMER_MODE_REL_SOFT);

A negative relative timeout expires immediately. The callback
unconditionally returns HRTIMER_RESTART, and hrtimer_forward() turns
the negative interval into hrtimer_resolution:

        if (interval < hrtimer_resolution)
                interval = hrtimer_resolution;

So the SOFT timer re-fires at the maximum rate forever, holding
sk_receive_queue.lock each pass. One CPU spins in softirq until the
socket is closed. Repeat with more rings and the machine is gone.

The overflow itself is ancient - it was introduced together with
TPACKET_V3 in f6fb8f100b80 ("af-packet: TPACKET_V3 flexible buffer
implementation."). Its effect prior to f7460d2989fa ("net:
af_packet: Use hrtimer to do the retire operation", v6.18) was not
as clear-cut, though: the return value was stored into an unsigned
short retire_blk_tov, so a negative result was truncated, and a
0-jiffy delay loop could be programmed as well. Neither is nearly
as detrimental as the immediate maximum-rate spin the hrtimer
conversion turned it into.

(Unrelated to CVE-2019-20812 - that one was the ethtool failure path
returning 0, which now returns DEFAULT_PRB_RETIRE_TOV.)

Reproducer, needs CAP_NET_RAW (a --network host container has it by
default) and a 1 Gbps NIC (QEMU e1000 works):

        int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
        bind(fd, ...);
        int v = TPACKET_V3;
        setsockopt(fd, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
        struct tpacket_req3 req = {
                .tp_block_size = 0x10000000,
                .tp_block_nr = 1,
                .tp_frame_size = 2048,
                .tp_frame_nr = 0x10000000 / 2048,
                .tp_retire_blk_tov = 0,
        };
        setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req));

Compute in 64 bits instead. The operands are already bounded by the
existing validation, so nothing else changes. If you'd prefer a
different fix, just say so and I'll respin.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
