# 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:

```c
/* 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.

## 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.
