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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4498 Β· 9 files
FileTypeDescriptionSize
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.md verdict loop proof + vectors
↓ download 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

  1. start_xmit_frames() dequeues any mbuf with nonzero m_pkthdr.len and calls encap_frame() (if_sbsh.c:636-643).

  2. 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; }

  3. repack() always produces m_new->m_pkthdr.len = m_new->m_len = m->m_pkthdr.len (:720-721). For a 1–4 byte frame the repacked mbuf has m_len < 5 again, so the goto look_for_nonzero path re-tests the same condition on a fresh 1–4 byte mbuf β€” unbounded allocate / copy / free cycle.

  4. The cycle runs in the caller's context holding ifp->if_serializer (network thread / syscall path via sbsh_start, :313-321), with M_NOWAIT allocations 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).

  5. Secondary defect on the same lines: when repack() fails (returns NULL), encap_frame() returns without freeing m_head β€” the dequeued packet is leaked (:668-671).

Injection vectors (why Low and not Medium)

  • /dev/bpf write of a runt frame β€” requires root/group on the bpf node (default root-only on DF).
  • Bridge member ingress: if sbsh0 is a bridge member, an unprivileged LAN peer on another member port can emit a runt frame that bridge_output() forwards into if_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_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored post-verification against read-only sys/; no runtime baseline possible on this guest (device absent)

fix.diff
↓ fix.diffper-fix-DF-4498

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.