# CVE-2026-98592: the TX descriptor that survived its own rollback in the SpacemiT K1 EMAC

**Patch:** [2d14720beb58870b15a52b236c3ab0be0e06e915](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2d14720beb58870b15a52b236c3ab0be0e06e915) (mainline, landed in 7.3-rc5)
**Subsystem:** net, SpacemiT K1 ethernet MAC driver
**Affected file:** drivers/net/ethernet/spacemit/k1_emac.c
**CVE record:** [CVE-2026-98592](https://www.cve.org/CVERecord?id=CVE-2026-98592)
**Introduced in:** 6.18, commit bfec6d7f2001c7470c3cd261ae65a3ba8737f226

## The bug in one sentence

When fragment DMA mapping failed mid-transmit, the rollback path freed the buffers and unmapped the DMA mappings but left the already-written ring descriptors claiming the hardware still owned them, with addresses pointing at memory that was no longer mapped.

## Why rollback bugs in TX rings are nasty

The sequence that produces the stale descriptor is subtle enough to be worth walking through slowly.

emac_tx_mem_map() writes TX_DESC_0_OWN (the "hardware owns this slot" bit) into the ring descriptor for every slot beyond old_head, committing each slot with a store as soon as its local memset'd copy is ready, before all of that slot's fragment buffers have necessarily been mapped. If emac_tx_map_frag() then fails on a later fragment, the error path runs emac_free_tx_buf(), which unmaps the DMA mappings and drops the skb, but leaves the descriptor memory untouched. Meanwhile tx_ring->head is never advanced past old_head, because the goto skips the "tx_ring->head = head" store.

So software believes slots between old_head and the rolled-back head are free. The DMA engine, reading the descriptors directly, believes the opposite: OWN is set, and buffer_addr_1 and buffer_addr_2 point at mappings that were just torn down. The next successful transmit only rebuilds the descriptor for old_head itself. If the DMA engine auto-advances into the following descriptor after finishing old_head's packet, and hardware in this class does exactly that, it fetches the stale, unmapped address and DMAs to or from freed memory.

The fix mirrors what emac_tx_clean_desc() already does on the normal reclaim path, where freeing and clearing have always been a pair:

```diff
 	while (i != head) {
 		emac_free_tx_buf(priv, i);
 
+		tx_desc_addr = &((struct emac_desc *)tx_ring->desc_addr)[i];
+		memset(tx_desc_addr, 0, sizeof(*tx_desc_addr));
+
 		if (++i == tx_ring->total_cnt)
 			i = 0;
 	}
```

Full patch: [2d14720beb58](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2d14720beb58870b15a52b236c3ab0be0e06e915). Three added lines: find the descriptor, zero it, continue the loop.

Reaching it needs a fragment mapping failure under memory pressure during transmit, on K1 hardware. When it fires, the symptom space is "the ethernet controller DMA'd into memory nobody owns", which ranges from corrupted packets to corrupted kernel memory depending on what got reallocated.

## How to trigger it

There is no pure userspace PoC here because the entry condition is a fragment DMA mapping failure inside the transmit path, and it needs the actual K1 EMAC. The honest reproducer is fault injection: make emac_tx_map_frag() fail on purpose and keep transmitting so the rolled back descriptor gets reused.

```bash
# boot with slab/page allocation fault injection, e.g. kernel cmdline:
#   failslab=1,probability=50,task-filter=0
# then force multi-fragment transmits (small writes -> skb frags):
iperf3 -c 10.0.0.2 -l 224 -t 600

# watch for the aftermath pre-fix:
dmesg -w | grep -Ei 'k1-emac|emac_tx|DMA'
```

Pre-fix, after a mapping failure the error path frees the buffers and skips the head advance, but the descriptors between old_head and the rolled back head keep TX_DESC_0_OWN and the now unmapped buffer_addr_1/2. If the DMA engine walks into one of those stale descriptors, you see emac TX timeouts, garbage frames, or memory corruption that shows up somewhere else entirely. Post-fix the rollback memsets each freed slot's descriptor, matching what the normal reclaim path always did.

## Score and classification

Unscored by the CNA and by NVD (Received). The spread elsewhere:

* Red Hat: 4.7 MEDIUM, CWE-825, and interestingly a fix deferred rather than not affected on RHEL 10, with a public statement that exploitation needs local access plus SpacemiT network hardware absent from typical enterprise deployments, confining impact to embedded and automotive
* SUSE: 4.7 MEDIUM on v3.1, 5.7 on v4.0
* Tenable published Nessus plugin [367653](https://www.tenable.com/plugins/nessus/367653) rating it High 7.1, but that number is the plugin's generic unpatched-distro scoring, not an analyzed CVSS, and the plugin exists because vendors flagged packages as will-not-be-patched

Hardware-gated, memory-pressure-gated, embedded-only: every factor pushes the honest score down, which is a reasonable summary of why the distros that actually ship K1 boards scored it in the fours.

## Exploit code

No userspace program can order the DMA engine around, so the exploit here is a controlled failure injection: force the fragment mapping failure the rollback path mishandles, and watch the ring after the rollback.

```bash
#!/usr/bin/env bash
# k1_emac_rollback.sh: CVE-2026-98592
# kernel cmdline for the test boots:
#   failslab=1,probability=50,task-filter=0 verbose=0
# (any allocator that emac_tx_map_frag() depends on failing will do; also
#  try fail_page_alloc=... to hit dma_map_page backing allocations)

modprobe k1_emac
ip link set eth0 up

# force multi-fragment transmit skbs: small writes, big aggregate count
iperf3 -c 192.168.1.2 -l 224 -P 4 -t 600 &

# watch for the pre-fix signature: after a failed mapping the descriptors
# between old_head and the rolled back head keep TX_DESC_0_OWN set with
# buffer_addr_1/2 pointing at the mappings emac_free_tx_buf() just tore down
dmesg -w | grep -Ei 'k1-emac|emac_tx|tx timeout|dma'
```

The failure pre-fix is not a clean splat. It is a descriptor the software believes is free while the hardware believes it is owned, holding stale DMA addresses; when the engine walks into it, you see TX timeouts, corrupted frames, or memory that turns out to be damaged somewhere unrelated, which is exactly the kind of bug that takes days off a platform engineer's life on K1 boards. Post-fix the rollback memsets each freed slot's descriptor before moving on, matching the reclaim path, and the stale state is gone. Public status: no KEV, no public PoC before this note; the Tenable plugin 367653 coverage is the most third-party attention this one received.

## Where the fix shipped

* 6.18.55: [ca9986cc824f](https://git.kernel.org/stable/c/ca9986cc824f2feb79b4a83e5f6f4c24c2140448)
* 7.2.9: [3001a103a367](https://git.kernel.org/stable/c/3001a103a367c6051c43aff363b6979c10e65347)
* 7.3-rc5: [2d14720beb58](https://git.kernel.org/stable/c/2d14720beb58870b15a52b236c3ab0be0e06e915)

## References

1. [Upstream commit 2d14720beb58](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2d14720beb58870b15a52b236c3ab0be0e06e915)
2. [CVE announcement on lore](https://lore.kernel.org/linux-cve-announce/2026101836-CVE-2026-98592-d6c3@gregkh/)
3. [Nessus plugin 367653](https://www.tenable.com/plugins/nessus/367653)
4. [Red Hat tracker](https://access.redhat.com/security/cve/cve-2026-98592)
5. [Ubuntu tracker](https://ubuntu.com/security/CVE-2026-98592)
