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

tulip_txput_setup() ring-full guard off-by-one (`ri_free == 1`): ri_free underflows to -1 and the setup frame is programmed over a chip-owned TX descriptor

Summary

The deferral guard tests 'ri->ri_free == 1' only. When all 128 TX descriptors are chip-owned (ri_free == 0 and tulip_tx_intr() reclaims nothing), the function falls through: ri_free underflows to -1, the setup frame overwrites a descriptor the NIC owns and ORs TxSETUPPKT|TxLASTSEG into an in-flight packet descriptor; on completion tulip_tx_intr() takes the SETUPPKT branch and never frees that packet's mbuf -> mbuf+cluster leak; descriptor reuse then trips KASSERT 'clobbering earlier tx mbuf' -> panic on INVARIANTS kernels; ring accounting corrupted by the negative ri_free. HARNESS: guard falls through at ri_free==0, ri_free=-1 UNDERFLOW, 'setup frame programmed over descriptor 0 (chip-owned: YES - ownership violation)'. On any machine with a de(4) NIC: unprivileged local user (IP_ADD_MEMBERSHIP -> SIOCADDMULTI needs no privilege on the join) combined with TX saturation making the ring momentarily 100% chip-owned: local DoS + driver-state corruption; timing-window dependent.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3757 Β· 10 files
FileTypeDescriptionSize
txput_setup_guard_model.c β€” 5.5 KB view raw
build.sh β€” 120 B view raw
run.sh β€” 120 B view raw
build.log β€” 143 B view raw
run.log β€” 909 B view raw
env.txt β€” 865 B view raw
fix.diff β€” 418 B view raw
VERDICT.md β€” 4.9 KB ↓ raw
manifest.json β€” 1.0 KB view raw
verdict.json β€” 3.0 KB view raw
VERDICT.md
↓ download raw

DF-3757 VERDICT β€” tulip_txput_setup() misses the ri_free == 0 case

Status: not_reproduced (requires live de(4) hardware under TX saturation;

the guard defect is source-proven and logic-model-CONFIRMED)

Root cause

tulip_txput_setup() needs one free TX descriptor to emit the multicast setup frame. Its deferral guard tests only for exactly one free descriptor:

/* sys/dev/netif/de/if_de.c:3631-3636 */
if (ri->ri_free < 2)
    tulip_tx_intr(sc);                                   /* try to reclaim */
if ((sc->tulip_flags & TULIP_DOINGSETUP) || ri->ri_free == 1) {
    sc->tulip_flags |= TULIP_WANTTXSTART;
    return;                                              /* defer */
}
...
ri->ri_free--;                                           /* if_de.c:3644 */
nextout = ri->ri_nextout->di_desc;                       /* if_de.c:3645 */
...
nextout->d_status = TULIP_DSTS_OWNER;                    /* if_de.c:3675 */

== 1 must be < 2. When the TX ring is completely full of chip-owned descriptors β€” 128/128 in flight, ri_free == 0, and tulip_tx_intr() reclaims nothing because every descriptor still has TULIP_DSTS_OWNER set (if_de.c:3169-3170) β€” the guard does not defer, and:

  1. ri->ri_free-- underflows to -1 (if_de.c:3644) β€” the ring accounting is now permanently negative; e.g. the free == 0 reclaim at if_de.c:3445 in tulip_txput() is skipped (free starts at -1);
  2. the setup frame is programmed over a descriptor the chip currently owns (if_de.c:3645-3675), OR-ing TxSETUPPKT|TxLASTSEG into an in-flight packet descriptor β€” a descriptor-ownership violation;
  3. when the chip later completes that descriptor, tulip_tx_intr() takes the SETUPPKT branch (if_de.c:3176-3196) and never frees the packet's mbuf β€” the m_freem() only exists in the non-setup branch (if_de.c:3197-3202) β†’ mbuf+cluster leak per occurrence;
  4. once the clobbered descriptor is re-used, tulip_txput()'s KASSERT(eop->di_mbuf == NULL, "clobbering earlier tx mbuf") (if_de.c:3544) fires on INVARIANTS kernels β†’ panic; on production kernels the mbuf accounting corruption continues (leak/double-handling).

Trigger path (unprivileged once the NIC exists)

  1. Any local user floods traffic through the de(4) interface until the 128-entry TX ring is momentarily 100% chip-owned (saturation; no privilege needed to transmit).
  2. The same or another user performs a multicast join: setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, …) β†’ in_addmulti() (sys/netinet/in.c:1393) β†’ if_addmulti() β†’ if_addmulti_serialized() calls ifp->if_ioctl(ifp, SIOCADDMULTI, 0, NULL) with no privilege check on the join itself (sys/net/if.c:2738-2739).
  3. tulip_ifioctl (if_de.c:3714-3722) β†’ tulip_addr_filter() sets TULIP_WANTSETUP β†’ tulip_init() (if_de.c:2960-2961) β†’ tulip_txput_setup() with ri_free == 0 β†’ defect.

The same call sequence also arrives from the privileged SIOCADDMULTI / SIOCSIFFLAGS ioctls, but the join path needs no privileges.

Verification narrative

  1. Source walk of tulip_txput_setup, tulip_tx_intr, tulip_txput (cited lines) β€” the == 1 guard and its consequences follow directly; no other guard exists between 3636 and 3644.
  2. Guest check (env.txt): de(4) compiled into the kernel but zero DEC 2114x hardware (hw.de.* count 0) β†’ the kernel path cannot execute on this rig at all β€” a TX ring can never exist.
  3. Built and ran the 1:1 logic model on the guest (build.log, run.log): with a 100% chip-owned ring the guard falls through, ri_free becomes -1, and a chip-owned descriptor is reprogrammed β€” confirmed (exit 0).

Classification: not_reproduced / reproduced=0 / impact=none on this guest. On hardware: unprivileged-triggerable DoS (INVARIANTS panic / mbuf-exhaustion) plus driver-state corruption; timing-window dependent (ring must be 100% in-flight at the join instant), hence AC:H and confidence "likely" rather than "certain" for real-world manifestation.

if_de.c:3633: change ri->ri_free == 1 to ri->ri_free < 2 (fix.diff). One-line, semantics otherwise unchanged.

Kernel references

  • sys/dev/netif/de/if_de.c:3631-3636 (the guard), 3644 (underflow), 3645-3675 (chip-owned descriptor rewrite), 3165-3172 (reclaim semantics), 3176-3202 (SETUPPKT branch skips m_freem), 3445 (free==0 reclaim skipped), 3544 (KASSERT panic)
  • sys/net/if.c:2738-2739 (unprivileged SIOCADDMULTI notify into driver)
  • sys/netinet/in.c:1393 (IP_ADD_MEMBERSHIP β†’ if_addmulti)

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 (== 1 -> < 2 at if_de.c:3633); not kernel-tested: no de(4) hardware on the guest, baseline itself cannot run.

fix.diff; logic model encodes both the buggy guard and the corrected < 2 semantics.
↓ fix.diffper-fix-DF-3757

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: 'ri->ri_free = -1 (UNDERFLOW)' + 'setup frame programmed over descriptor 0 (chip-owned before write: YES)'; VERDICT.md cites the guard line 3633, underflow 3644, SETUPPKT skip 3176-3202, KASSERT 3544, and the unprivileged join path if.c:2739.

PoC changes

No seed PoC; authored 1:1 guard/reclaim model from scratch, compiled clean on first attempt (gcc 8.3 guest).

Verified recommended fix

if_de.c:3633: change ri->ri_free == 1' tori->ri_free < 2' so a completely full ring defers instead of underflowing.

Verdict

tulip_txput_setup() (if_de.c:3631-3636) defers on ri->ri_free == 1' but not onri->ri_free == 0'. With all 128 TX descriptors chip-owned (saturation; tulip_tx_intr reclaims nothing, if_de.c:3169), the setup-frame path falls through: ri_free underflows to -1 (if_de.c:3644), the setup frame is programmed over a chip-owned in-flight descriptor (if_de.c:3645-3675), tulip_tx_intr's SETUPPKT branch then skips m_freem for the clobbered packet (if_de.c:3176-3202) leaking mbuf+cluster, and KASSERT 'clobbering earlier tx mbuf' (if_de.c:3544) panics INVARIANTS kernels on descriptor reuse. Unprivileged trigger on hardware: IP_ADD_MEMBERSHIP -> if_addmulti -> ifp->if_ioctl(SIOCADDMULTI) with no join privilege (if.c:2738-2739) while the ring is 100% in flight. A 1:1 logic model built and run on the audit guest CONFIRMED the underflow and descriptor-ownership violation (run.log, exit 0); the kernel path needs a live de(4) NIC under TX saturation and the guest has zero DEC 2114x hardware (env.txt), hence not_reproduced. Expected real-world impact: unprivileged local DoS (panic/mbuf exhaustion) on machines with a de(4) NIC, timing-window dependent.