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)
PoC verification
Evidence pack
findings/poc/DF-3757 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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:
ri->ri_free--underflows to -1 (if_de.c:3644) β the ring accounting is now permanently negative; e.g. thefree == 0reclaim atif_de.c:3445intulip_txput()is skipped (freestarts at -1);- the setup frame is programmed over a descriptor the chip currently owns
(
if_de.c:3645-3675), OR-ingTxSETUPPKT|TxLASTSEGinto an in-flight packet descriptor β a descriptor-ownership violation; - 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 β them_freem()only exists in the non-setup branch (if_de.c:3197-3202) β mbuf+cluster leak per occurrence; - once the clobbered descriptor is re-used,
tulip_txput()'sKASSERT(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)
- 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).
- 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()callsifp->if_ioctl(ifp, SIOCADDMULTI, 0, NULL)with no privilege check on the join itself (sys/net/if.c:2738-2739). tulip_ifioctl(if_de.c:3714-3722) βtulip_addr_filter()setsTULIP_WANTSETUPβtulip_init()(if_de.c:2960-2961) βtulip_txput_setup()withri_free == 0β defect.
The same call sequence also arrives from the privileged SIOCADDMULTI / SIOCSIFFLAGS ioctls, but the join path needs no privileges.
Verification narrative
- Source walk of
tulip_txput_setup,tulip_tx_intr,tulip_txput(cited lines) β the== 1guard and its consequences follow directly; no other guard exists between 3636 and 3644. - 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. - 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_freebecomes -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.
Recommended fix
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_testablefix.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.
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.
No comments yet.