lge_jref() check-then-increment runs without the jslot serializer: racing lge_jfree() recycles the slot onto the free list with inuse==1 β double ownership of a 9KB RX jumbo cluster (DF-1384 nge twin)
Summary
lge_jref reads inuse==0 and then atomic-adds WITHOUT the jslot serializer, while lge_jfree decrements and recycles UNDER it. Interleaving: jref passes the check -> jfree drops to 0 and frees the slot -> jref's add makes it 1 while on the free list -> jalloc pops the slot and newbuf hands the SAME 9KB cluster to a new RX descriptor while the shared mbuf still references it: NIC DMA lands in a buffer the stack is concurrently reading (cross-packet bleed), and the eventual second free panics or corrupts the freelist. Byte-for-byte the DF-1384 nge_jref defect in lge's copy of the jumbo-pool template. Serialize-the-check fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4317 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| fix.diff | β | 911 B | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.1 KB | view raw |
DF-4317 β lge_jref check-then-increment runs without the jslot serializer (DF-1384 nge twin)
What: lge_jref() (if_lge.c:801-814) validates entry->lge_inuse != 0
(:810) and then does atomic_add_int(&entry->lge_inuse, 1) (:813) without
holding sc->lge_jslot_serializer, while lge_jfree()
(:819-841) performs its decrement-and-recycle under that serializer
(:833-839). This is byte-for-byte the unlocked check-then-act that DF-1384
found in nge(4) (if_nge.c nge_jref).
Race (needs two CPUs):
1. RX jumbo mbuf m (ext_arg = jslot E) is shared, e.g. by an
m_copym/BPF-style copy, so the cluster refcount path calls
lge_jref(E) (m_ext.ext_ref, wired at if_lge.c:688).
2. CPU0 (lge_jref): reads E->lge_inuse == 1 β check passes.
3. CPU1 (lge_jfree on the original mbuf): takes the serializer,
atomic_subtract_int(&E->lge_inuse, 1) β 0, SLIST_INSERT_HEAD(free_list,
E) (:836-837), releases.
4. CPU0 (lge_jref): atomic_add_int(&E->lge_inuse, 1) β E is now
inuse == 1 while sitting on the free list.
5. lge_jalloc() (:777-794, serialized) pops E and hands the same 9 KB DMA
buffer to a fresh RX descriptor (lge_newbuf :661-729) while the shared
mbuf still references it β NIC DMA lands in a buffer the network stack is
reading β packet data corruption; eventual double-free of E either trips
panic("lge_jfree: buffer already free!") (:831) or corrupts the jslot
freelist.
Trigger: any concurrent share (ext_ref) and free of an RX jumbo cluster.
Reachability of ext_ref: DragonFly mbuf code invokes ext_ref whenever an
external cluster is shared (m_copym of the full buffer, m_dup_pkthdr, BPF
copies). Concurrent free comes from the normal RX path on another CPU.
Hardware-gated: requires an lge(4) NIC passing traffic (guest has none:
ifconfig -l β vtnet0 lo0).
Reproduce / verification status
Source-certain by inspection (the code path is driver-local, deterministic to
read); the runtime race is hardware-gated β the audit QEMU guest has no
Level-One LXT1001 NIC, so no live PoC is possible. Status untested,
confidence likely (identical code shape to DF-1384, which was verified as a
class; the DF-1384 fix is directly transferable).
Cross-check the twin: sys/dev/netif/nge/if_nge.c nge_jref/nge_jfree
(DF-1384), and note lge_jalloc (:782-792) is correctly serialized β only
lge_jref is the unlocked actor.
Fix
fix.diff β take sc->lge_jslot_serializer around the check+increment in
lge_jref() (mirrors the DF-1384 fix): the serializer is per-softc and
never taken in an order that can recurse here (jref is called from mbuf
code with no driver locks held).
Files
README.md(this),VERDICT.md,fix.diff,manifest.json,verdict.json
DF-4317 β VERDICT
Status: hardware-gated β untested on this guest (no LGE NIC:
ifconfig -l β vtnet0 lo0). Source analysis certain; runtime likelihood
of the race, once traffic flows and clusters get shared, follows the
DF-1384 (nge twin) precedent: Low severity, local DoS / data-corruption
ceiling.
Root cause (path:line)
sys/dev/netif/lge/if_lge.c:685-688β every RX jumbo mbuf wiresm_ext.ext_ref = lge_jref; the mbuf layer calls it whenever the external cluster is shared (m_copym full-buffer copy, m_dup_pkthdr, BPF copies).sys/dev/netif/lge/if_lge.c:801-814βlge_jref()::807membership sanity check (fine),:810unlocked readentry->lge_inuse == 0β panic, else:813atomic_add_int(&entry->lge_inuse, 1)β the increment is atomic but the check-then-act pair is not serialized.sys/dev/netif/lge/if_lge.c:819-841βlge_jfree()correctly takessc->lge_jslot_serializer(:833) aroundatomic_subtract_int(&entry->lge_inuse, 1)(:834) and theSLIST_INSERT_HEAD(&sc->lge_jfree_listhead, entry, ...)(:836-837) that recycles the slot when the count hits 0.sys/dev/netif/lge/if_lge.c:777-794βlge_jalloc()is serialized (:782) and pops fromlge_jfree_listhead(:783-785).
Interleaving (CPU0 = lge_jref via shared mbuf, CPU1 = lge_jfree via original mbuf):
CPU0: lge_jref(E): reads inuse == 1 (check passes, no lock) CPU1: lge_jfree(E): [serializer] inuse 1->0; SLIST_INSERT_HEAD(free, E) CPU0: lge_jref(E): atomic_add inuse 0->1 (E: on free list AND inuse) CPU1: lge_jalloc(): [serializer] pops E -> new lge_newbuf RX descriptor
Consequences: two owners of one 9 KB jumbo cluster β the NIC DMAs received
frames into a buffer the stack is concurrently reading (data corruption /
cross-packet bleed), and the second eventual lge_jfree either panics
(lge_jfree: buffer already free!, :831) or double-inserts the slot,
corrupting lge_jfree_listhead.
This is the same defect DF-1384 established for nge(4)
(nge_jref unlocked check-then-add vs serialized nge_jfree), in lge's
copy of the Bill Paul jumbo-pool template. Only lge_jref is the unlocked
actor here; lge_jalloc/lge_jfree are correctly serialized.
Why not reproducible on this guest
The race requires a Level-One LXT1001 NIC serving RX traffic whose jumbo
clusters get shared and freed concurrently. The audit guest is virtio-based
(ifconfig -l β vtnet0 lo0); the driver cannot even attach. The code shape
is driver-local and fully verifiable by inspection; the twin finding DF-1384
documents the same interleaving.
Fix
fix.diff β serialize the check+increment in lge_jref() with
sc->lge_jslot_serializer (identical to the DF-1384 fix for nge_jref). No
recursion risk: lge_jref runs from mbuf code with no driver locks held;
lge_jfree only takes the same serializer (same thread cannot be inside both).
fix_status=not_testable (no lge hardware in guest).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablefix.diff authored against the read-only sys/ tree (applies cleanly to sys/dev/netif/lge/if_lge.c); cannot be validated without lge hardware.
findings/poc/DF-4317/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-4317/VERDICT.md (interleaving diagram + path:line proof); findings/poc/DF-4317/fix.diff (serialized check+increment, mirrors the DF-1384 fix); twin precedent DF-1384 row in audit.db
PoC changes
No seed PoC. Hardware-gated analysis pack: no LGE NIC exists in the QEMU guest, so verification is by source-path inspection against the DF-1384 twin whose class was already established.
Verified recommended fix
Take sc->lge_jslot_serializer around the check+increment in lge_jref() (findings/poc/DF-4317/fix.diff), mirroring the DF-1384 nge fix.
Verdict
Hardware-gated: the audit guest has no Level-One LXT1001 NIC (ifconfig -l -> 'vtnet0 lo0'), so no runtime PoC is possible -> untested. Source analysis is certain: lge_jref() (sys/dev/netif/lge/if_lge.c:801-814) performs an unlocked check-then-increment (inuse!=0 read at :810, atomic_add at :813) while lge_jfree() (:819-841) decrements and recycles the slot onto lge_jfree_listhead under sc->lge_jslot_serializer (:833-837). A racing jref/jfree interleaving puts a slot on the free list with inuse==1; lge_jalloc() (:777-794, serialized) then hands the same 9KB jumbo cluster to a new RX descriptor while a shared mbuf still references it: NIC DMA into a live stack buffer (cross-packet data bleed), and the later second free either panics ('lge_jfree: buffer already free!' :831) or double-inserts the slot, corrupting the freelist. Byte-for-byte the same defect as DF-1384 (nge_jref, sys/dev/netif/nge/if_nge.c) in lge's copy of the Bill Paul jumbo-pool template; only lge_jref is the unlocked actor (lge_jalloc/lge_jfree are correctly serialized). ext_ref reachability: wired for every RX jumbo mbuf at :685-688; the mbuf layer calls it on m_copym full-buffer copies / m_dup_pkthdr / BPF copies. Requires >=2 CPUs, RX traffic, and a cluster share+free race -> Low severity local DoS / data-corruption ceiling.
No comments yet.