---
id: "CVE-2026-98797"
kind: "vulnerability"
aliases: ["CVE-2026-98797"]
title: "Linux kernel: stale queue mapping lets a local user crash taprio"
severity: "high"
cvss: null
epss: null
exploited: false
status: "open"
category: "runtime"
products: []
ecosystems: []
tags: ["containers", "linux-kernel", "multi-tenant", "runtime", "taprio"]
not_affected_if: ["no-taprio", "no-untrusted-local-users"]
published_at: "2026-10-18T16:17:33+00:00"
first_seen_at: "2026-10-23T21:57:16+00:00"
updated_at: "2026-10-25T05:15:10+00:00"
sources: [{"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-98797", "first_seen_at": "2026-10-23T21:57:16+00:00"}]
---

# Linux kernel: stale queue mapping lets a local user crash taprio

A local user with CAP_NET_ADMIN in a network namespace can redirect packets between devices with different transmit queue counts, leaving a stale queue index that makes the taprio scheduler read past its allocated array. Under KASAN this is reported as an out-of-bounds access in taprio_enqueue, so the impact is a kernel memory fault and possible privilege escalation.

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

## How to check

- `uname -r` and compare against the kernel build that contains the fix, which the sources do not name
- `lsmod | grep sch_taprio` or `zgrep CONFIG_NET_SCH_TAPRIO /proc/config.gz` to see whether taprio is built in or loaded
- `zgrep CONFIG_NET_ACT_SKBEDIT /proc/config.gz` and `zgrep CONFIG_NET_ACT_MIRRED /proc/config.gz` to see whether the required actions are present

## Fix

- Update the Linux kernel to a stable release that includes the upstream commit resolving this issue; the exact fixed version is unknown from the sources
- Until patched, unload or avoid the taprio qdisc where it is not needed, and restrict CAP_NET_ADMIN in untrusted network namespaces

## Timeline

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

## From the sources

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

net: cap skb->queue_mapping when the tx queue is picked

skbedit can set skb->queue_mapping and raise the per-CPU skip_txqueue
flag so __dev_queue_xmit() honours the mapping. __dev_queue_xmit()
cleared the flag before sch_handle_egress() and only read it afterwards,
so the flag was not confined to the xmit that set it: a nested xmit
(mirred redirect or mirror, or a drop after skbedit) could set the flag
and the outer xmit would consume it for an skb that never went through
skbedit.

A forwarded packet still carries the ingress NIC's rx_queue + 1 in
skb->queue_mapping, so the outer device then indexes its tx queue state
with that stale value. Taprio's child array q->qdiscs[] is sized to the
device's queue count, so taprio_enqueue() indexes past its allocation
and dereferences the result as a struct Qdisc *.

We (ab)use the skb->nf_skip_egress which means "skip netfilter egress
for this packet" to tag to "am I in tc egress?". Despite the overload
I dont see it as a conflict since the marker is set only around the
single sch_handle_egress() call and ingress path is guarded by
tc_at_ingress.
I will send a followup(net-next) patch once this hits net-next to
rename the skb->nf_skip_egress bit/flag to skb->skip_egress

Arm the flag only from the egress classifier that can use it: raise
skip_txqueue from tcf_skbedit_act() only when it runs inside
sch_handle_egress(), thanks to skb->nf_skip_egress. An egress qdisc
classifier runs in q->enqueue(), after the tx queue has been picked,
so a mapping it sets cannot affect the current packet; arming the flag
there only pollutes it for a later xmit. Then own the flag for the xmit
frame the egress hook runs in: save the incoming value and clear it just
before sch_handle_egress(), and restore it after the hook - on the
consumed (drop) path, or, in the same call that reads it, on the
surviving path. The save and the restores stay inside the
egress_needed_key static branch, so a packet pays for them only when
egress hooks are active (2f1e85b1aee4).

Store the value netdev_cap_txqueue() selected back into skb->queue_mapping
in netdev_tx_queue_mapping(), as netdev_core_pick_tx() already does, so
the skip_txqueue path never hands a later reader on the xmit path a
mapping the device cannot serve. A store made still later in the same
frame, by a tc BPF program attached to a transmit qdisc, is outside this
path and is not re-capped; a separate followup will resolve that path.

netdev_xmit_skip_txqueue() returns the previous flag value so the
save-and-clear is one call, and a no-op stub is provided when
CONFIG_NET_EGRESS is disabled. skb->nf_skip_egress is compiled under
CONFIG_NET_EGRESS rather than CONFIG_NETFILTER_SKIP_EGRESS, so
skb_at_tc_egress() is valid whenever the egress path is built.

A local user in a network namespace can redirect a packet from a device
with more TX queues to one with fewer after setting a mapping valid only
on the larger device. That reaches these reads and, under KASAN, faults
with "slab-out-of-bounds in taprio_enqueue".

Conditions to recreate the bug: the report's own trigger is a local user
with CAP_NET_ADMIN in a network namespace, so no eBPF program is needed.
With CONFIG_NET_SCH_TAPRIO=y, CONFIG_NET_ACT_SKBEDIT=y,
CONFIG_NET_ACT_MIRRED=y, CONFIG_NET_CLS_MATCHALL=y,
CONFIG_NET_SCH_PRIO=y and KASAN enabled, create qa (3 queues), qb
(2 queues) and qc (1 queue) as dummy devices; put a taprio root on qb
(num_tc 1, queues 2@0) and clsact on all three; then add an egress
matchall filter on every device. On qa: "action skbedit queue_mapping 2
pipe action mirred egress redirect dev qb". On qb: "action mirred egress
mirror dev qc". On qc: "action skbedit queue_mapping 0 pipe". Send one
packet out qa. qc's skbedit sets the flag while qb's outer xmit is in
flight; without the fix qb consumes it and reads its two-entry taprio
child array with the forwarded packet's stale mapping. A qc whose
skb
---truncated---

---

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