β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4317

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4317 Β· 5 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

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 wires m_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():
  • :807 membership sanity check (fine),
  • :810 unlocked read entry->lge_inuse == 0 β†’ panic, else
  • :813 atomic_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 takes sc->lge_jslot_serializer (:833) around atomic_subtract_int(&entry->lge_inuse, 1) (:834) and the SLIST_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 from lge_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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-4317

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.