sbsh: frames with total length 1-4 bytes cause an unbounded repack loop in encap_frame() - permanent kernel spin holding the interface serializer
Summary
The loop-top 'm->m_len < 5' triggers repack(), which rebuilds a single mbuf with m_len = pkthdr.len - still < 5 for a 1-4 byte frame - and goto re-tests the same condition forever: allocate/copy/free spin in the sender's context holding if_serializer (one CPU pinned, every serializer path blocks). Unpriv-reachable when sbsh0 is a bridge member (runt frame from a LAN peer); BPF path is root-gated. Repack-refuse-sub-5 + free-on-fail fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4498 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | 2-byte runt BPF injector | 1.7 KB | view raw |
| build.sh | build-script | compile trigger | 65 B | view raw |
| run.sh | run-script | device-absence aware runner | 89 B | view raw |
| build.log | build-log | guest compile BUILD_OK | 124 B | view raw |
| run.log | run-log | BIOCSETIF: Device not configured | 51 B | view raw |
| env.txt | env | uname + pciconf absence proof | 222 B | view raw |
| VERDICT.md | verdict | loop proof + vectors | 3.1 KB | β raw |
| fix.diff | verified-fix | repack <5 guard + leak fix | 892 B | view raw |
| verdict.json | verdict-json | machine verdict (strict status set) | 2.3 KB | view raw |
VERDICT DF-4498 β sbsh encap_frame() infinite repack loop on 1β4 byte frames
Bottom line
INCONCLUSIVE at runtime (hardware-gated), defect CONFIRMED at source level.
The loop is provable by pure code logic β no race, no device misbehavior
needed, only a frame with 0 < m_pkthdr.len < 5 in if_snd while the unit is
ACTIVE. The QEMU guest has no Granch SBNI12/16 PCI device (env.txt), so the
driver never attaches and the frame can never be enqueued.
Source-level proof
-
start_xmit_frames()dequeues any mbuf with nonzerom_pkthdr.lenand callsencap_frame()(if_sbsh.c:636-643). -
encap_frame()loop head (:660-672):c look_for_nonzero: for (m = m_head; !m->m_len; m = m->m_next) ; cur_tbd = sc->regs->LTDR & 0x7f; do { if (m->m_len < 5 || cur_tbd == ((sc->head_tdesc - 1) & 0x7f)) { if ((m_head = repack(sc, m_head)) != NULL) goto look_for_nonzero; else return; } -
repack()always producesm_new->m_pkthdr.len = m_new->m_len = m->m_pkthdr.len(:720-721). For a 1β4 byte frame the repacked mbuf hasm_len < 5again, so thegoto look_for_nonzeropath re-tests the same condition on a fresh 1β4 byte mbuf β unbounded allocate / copy / free cycle. -
The cycle runs in the caller's context holding
ifp->if_serializer(network thread / syscall path viasbsh_start,:313-321), withM_NOWAITallocations that keep succeeding under normal memory pressure β the only exit is mbuf exhaustion, which the loop itself prevents by freeing what it allocates. Result: permanent kernel-mode spin on one CPU with the interface serializer held (livelock DoS; everything that touches the ifnet or takes that serializer blocks). -
Secondary defect on the same lines: when
repack()fails (returns NULL),encap_frame()returns without freeingm_headβ the dequeued packet is leaked (:668-671).
Injection vectors (why Low and not Medium)
/dev/bpfwrite of a runt frame β requires root/group on the bpf node (default root-only on DF).- Bridge member ingress: if
sbsh0is a bridge member, an unprivileged LAN peer on another member port can emit a runt frame thatbridge_output()forwards intoif_snd(bridges do not enforce a minimum frame size). Config-dependent. - Normal stack paths (ether_output β₯ 14 bytes) cannot produce it.
Why not reproduced
sbsh0 cannot exist on this guest (PCI 0x55:0x9d absent β env.txt);
trigger compiled (build.log) and failed only at BIOCSETIF
(run.log).
Fix
fix.diff: (1) repack() refuses frames < 5 bytes (returns NULL β caller
drops); (2) encap_frame() frees m_head on the repack-failure path, fixing
the leak as well.
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 post-verification against read-only sys/; no runtime baseline possible on this guest (device absent)
fix.diff
Confirmed kernel references
Detail
Exploit chain
runt frame into if_snd (bpf write as root, or unprivileged LAN peer via a bridge member) -> one CPU spins forever in kernel holding the interface serializer -> permanent local/remote DoS
Evidence (decisive lines)
VERDICT.md (line-by-line loop proof); run.log (BIOCSETIF: Device not configured); env.txt (pciconf absence); fix.diff (repack refuses <5 byte frames + drop-path m_freem fixing the packet leak)
PoC changes
authored trigger from scratch (2-byte BPF runt injection); include set for DF: sys/socket.h + net/if.h + net/bpf.h
Verified recommended fix
repack(): return NULL for m_pkthdr.len < 5; encap_frame(): m_freem(m_head) on repack failure
Verdict
Logic defect confirmed at source level: a frame with 0 < m_pkthdr.len < 5 makes encap_frame() repack it forever (repack() preserves the sub-5 length at if_sbsh.c:721, the m_len<5 test at :667 re-fires after every goto look_for_nonzero), an unbounded alloc/copy/free spin holding ifp->if_serializer. Runtime execution impossible on the audit guest: the Granch SBNI12/16 PCI device is absent, sbsh0 never exists; trigger compiled and failed only at BIOCSETIF.
No comments yet.