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

hdac_stream_alloc() bidirectional-stream tag arithmetic: '+ num_oss' should be '- num_oss' (missing upstream FreeBSD fix) β€” wrong/colliding/negative HDA stream tags on BSS-capable controllers

Summary

hdac.c:1934 computes the stream tag for a bidirectional (BSS) stream as 15 - (ss - num_iss + num_oss), which C evaluates as 15 - 2*num_oss - j for BSS index j, instead of the intended 15 - j. FreeBSD fixed this between 11.1R and 11.2R (verified against fetched 11.0-14.x sources); DragonFly imported pre-fix r275101 and never merged the correction. With num_oss >= 8 the tag goes negative; tags also collide with the OSS tag space - garbage/negative tag into SDCTL2, wrong codec converter stream verbs, tag collisions between live streams. No kernel memory-safety consequence (equality lookups + byte-wide register writes). BSS=0 controllers (incl. QEMU ICH6, runtime-verified) structurally unaffected. LIVE ioctl battery (1310 rounds + real stream starts + hostile fragments) on the emulated intel-hda guest held clean; the wrong-tag branch itself hardware-gated (BSS 0). One-token upstream fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4115 Β· 17 files
FileTypeDescriptionSize
README.md β€” 4.2 KB ↓ raw
VERDICT.md β€” 5.1 KB ↓ raw
fix.diff β€” 416 B view raw
dsp_battery.c β€” 5.5 KB view raw
frag_start.c β€” 1.9 KB view raw
hblkcnt_max.c β€” 2.1 KB view raw
build.sh β€” 131 B view raw
run.sh β€” 90 B view raw
build.log β€” 37 B view raw
run.log β€” 1.5 KB view raw
dmesg_verbose.txt β€” 2.6 KB view raw
caps.txt β€” 68 B view raw
env.txt β€” 348 B view raw
panic.sndstat.txt β€” 2.8 MB ↓ download
sha256.txt β€” 239 B view raw
verdict.json β€” 3.2 KB view raw
manifest.json β€” 961 B view raw

DF-4115 β€” hdac_stream_alloc(): bidirectional-stream numbering sign error

File: sys/dev/sound/pci/hda/hdac.c:1933-1934 β€” pass 2 (GLM 5.3). Verified 2026-09-18 against a live QEMU intel-hda (ICH6) + hda-duplex guest.

THE BUG hdac_stream_alloc() computes the HDA stream tag for a BIDIRECTIONAL stream (index ss = num_iss + num_oss + j, j in [0, num_bss)) as

    stream = 15 - (ss - sc->num_iss + sc->num_oss);   /* hdac.c:1934 */

The parenthesised expression evaluates to ((ss - num_iss) + num_oss)
= 2*num_oss + j, not the intended (ss - (num_iss + num_oss)) = j.
Result: stream = 15 - 2*num_oss - j instead of 15 - j.

FreeBSD fixed this upstream between 11.1R and 11.2R (present as
`- sc->num_oss' in FreeBSD 11.2/11.3/11.4/12.x/13.x/14.x); DragonFly
carries the pre-fix FreeBSD r275101 code and never picked the fix up.
Proven by fetching FreeBSD release sources (see VERDICT.md).

IMPACT On controllers with num_oss >= 1 and BSS streams, every bidirectional stream gets a wrong tag: colliding with the OSS tag space, and for 2*num_oss + j > 15 a NEGATIVE stream tag. The tag flows into SDCTL2 programming (hdac_stream_start(), hdac.c:2010), into codec SET_CONVERTER_STREAM verbs and stream-interrupt dispatch β€” misprogrammed controller/codec registers, broken audio, possible tag collisions between two active streams (both functional corruption), no kernel memory-safety violation (stream is only used for equality lookups and byte-wide register writes). Severity: Low (functional correctness on BSS-capable controllers; untriggerable on BSS=0 controllers such as QEMU ICH6).

LIVE VERIFICATION ATTEMPT (this pack) The guest CAN attach an emulated controller (DF-3879 pattern: -audiodev none,id=snd0 -device intel-hda -device hda-duplex), and /dev/dsp is 0666, so the alloc path was hammered live: * dsp_battery.c β€” 1310 rounds: extreme SETFRAGMENT (incl. 0x7fff0000, 0xffffffff), format/speed/channel churn, 6 concurrent open/play/close workers. No panic, guest alive. * frag_start.c β€” 11 hostile fragment shapes each with an actual stream START (write) so hdac_stream_start()'s BDL walk runs. * hblkcnt_max.c β€” double-fragment trick that drives the pcm core's hardware request above HDA_BDL_MAX (hblkcnt up to 512 after the maxsize shrink), proving the hdaa.c:2096 clamp is the operative bound; hard sndbuf never exceeded blkcnt=2 live (dmesg_verbose.txt). Runtime controller caps: "OSS 4, ISS 4, BSS 0" (caps.txt) β€” with num_bss == 0 the buggy branch is unreachable on the only available HDA hardware, so the wrong-tag behaviour itself could not be triggered live. Source-level certainty stands on the upstream diff.

BUILD (on the guest, unprivileged compile) cc -O2 -Wall -o /tmp/dsp_battery /tmp/dsp_battery.c cc -O2 -Wall -o /tmp/frag_start /tmp/frag_start.c cc -O2 -Wall -o /tmp/hblkcnt_max /tmp/hblkcnt_max.c

RUN (unprivileged) /tmp/dsp_battery ; /tmp/frag_start ; /tmp/hblkcnt_max

EXPECTED all three complete, rc=0, guest stays up, no hdac panic; the BSS branch itself never executes (BSS 0).

FIX fix.diff β€” adopt the upstream FreeBSD correction (- sc->num_oss). Cannot be runtime-validated on this guest (no BSS controller); fix_status=not_testable for that reason (baseline behaviour is also unobservable here).

ARTIFACTS dsp_battery.c / frag_start.c / hblkcnt_max.c live-test sources build.log, run.log clean final run dmesg_verbose.txt hw-sndbuf clamp evidence caps.txt runtime GCAP (BSS 0) env.txt guest + controller info panic.sndstat.txt CROSS-FILE observation (NOT DF-4115): root-only kldunload snd_hda after use panics in sndstat_sysuninit() ("sbuf_delete called with uninitialized or corrupt sbuf", sys/dev/sound/pcm/sndstat.c) β€” hdac0 detaches cleanly first; filed for the orchestrator to attribute to sndstat.c.

VERDICT.md
↓ download raw

DF-4115 VERDICT β€” hdac_stream_alloc BSS stream-number sign error

STATUS: not_reproduced (live trigger hardware-gated: no BSS-capable controller available); root cause CERTAIN from source + upstream diff. impact: none (functional wrong-tag on BSS controllers; no memory safety) confidence: certain

  1. ROOT CAUSE (proven) sys/dev/sound/pci/hda/hdac.c:1934

    if (ss >= sc->num_iss + sc->num_oss) stream = 15 - (ss - sc->num_iss + sc->num_oss);

C evaluates the parenthesised expression left-to-right: (ss - num_iss) + num_oss. For a BSS stream ss = num_iss+num_oss+j this is 2num_oss + j, so stream = 15 - 2num_oss - j. Intended (and correct upstream): 15 - (ss - (num_iss + num_oss)) = 15 - j.

Upstream provenance (fetched 2026-09-18 from raw.githubusercontent.com/freebsd/freebsd-src release tags): 11.0.0 / 11.1.0 : stream = 15 - (ss - sc->num_iss + sc->num_oss); 11.2.0 / 11.3.0 / 11.4.0 / 12.0.0 : ... - sc->num_oss - ... i.e. FreeBSD fixed it in stable/11 between 11.1R (2016) and 11.2R (2018). DragonFly's hdac.c is an import of FreeBSD r275101 (the $FreeBSD$ tag at line 51) which predates the fix; DF never merged it. DF also verified to carry other deliberate DF-local diffs (device table additions, taskqueue_hdac, tsleep hack in detach), so this is a missed-upstream-fix class bug, not a DF typo.

  1. WHY IT IS ONLY 'Low' stream is used for: sc->streams[ss].stream (equality lookup key in hdac_find_stream, hdac.c:1861-1893), the SDCTL2 STRIPE/STRM byte register write (hdac_stream_start, hdac.c:2010), codec SET_CONVERTER_STREAM verbs and interrupt dispatch (hdac_one_intr:392-395). None of these index kernel memory. With num_oss >= 8 and num_bss large, stream goes negative (e.g. 15 - 16 - j) β†’ garbage tag in SDCTL2 and wrong equality key; audio breaks; on some controllers two streams can share a tag (OSS stream n collides with BSS stream tag 15-2*oss-j) β†’ cross-stream data corruption at the hardware level. No host-kernel memory-safety consequence found.

  2. LIVE VERIFICATION NARRATIVE (guest: DF 6.5-DEVELOPMENT X86_64_GENERIC, QEMU intel-hda ICH6 + hda-duplex, snd_hda.ko loaded at runtime) a) dmesg: hdac0/hdacc0/hdaa0/pcm0 attach; /dev/dsp{,0} + /dev/mixer 0666 β†’ all tested paths unprivileged. b) dsp_battery.c (1310 rounds: hostile SETFRAGMENT incl. 0x7fff0000/0xffffffff/0x00000000, fmt/speed/channel churn incl. invalid values, 6 concurrent open/play/close workers): no panic, rc=1 only because of my own >4096 fragstotal sanity check tripping on legit soft-buffer values (16/8192/131072). c) frag_start.c + hblkcnt_max.c: hostile fragments with REAL stream starts (write()). Key result (dmesg_verbose.txt): the pcm core can hand the driver a hardware blkcnt request > HDA_BDL_MAX (channel.c:1832 hblkcnt = round_pow2(prev soft blkcnt) ≀ 8192; after the maxsize shrink loop up to 512), and the ONLY bound on hdac_stream_start()'s BDL walk (hdac.c:1989, buffer sized 256 entries at hdac.c:1306) is hdaa.c:2096's clamp β€” which held every time (live hard-sndbuf blkcnt observed only 2; soft buffer reached 8192 freely). BDL-overflow family: KILLED on this guest, with runtime evidence. d) polling mode (dev.hdac.0.polling=1): frag_start completed (hdac_mdata_rate division, hdac_poll_reinit, polled hdac_intr_handler exercised; no div0/spin). The big format-churn battery under polling merely runs slow (100Hz poll Γ— 256B hw buffer) and tripped its own 240s watchdog β€” not a deadlock. e) Runtime caps: "OSS 4, ISS 4, BSS 0, NSDO 1, 64bit, CORB 256, RIRB 256" (caps.txt). num_bss == 0 β‡’ hdac.c:1933 branch is structurally unreachable on the only available HDA hardware β‡’ the wrong-tag behaviour cannot be triggered here. Hence not_reproduced with certain source-level root cause.

  3. FIX fix.diff = upstream's one-line correction. Kernel-level fix validation impossible on this guest (no BSS controller; the patched branch never executes), so fix_status=not_testable.

  4. CROSS-FILE OBSERVATION (for the orchestrator, not DF-4115) Root kldunload snd_hda after use panics the guest in sndstat_sysuninit() β€” "sbuf_delete called with uninitialized or corrupt sbuf" β€” via linker_file_unload (panic.sndstat.txt; serial console capture). hdac0/hdacc0/hdaa0 all detach cleanly BEFORE the panic; hdac_detach's own paths are not implicated. Root-only trigger. Belongs in sys/dev/sound/pcm/sndstat.c (or sound module teardown) β€” recommend a separate finding if that file isn't audited yet. Guest was vm.sh-reset (with-src) afterwards.

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 applies the proven upstream one-line correction. Kernel-level validation impossible on the only available hardware (BSS 0 - the patched branch never executes, baseline behaviour equally unobservable); the fix is byte-identical to FreeBSD 11.2+ which has shipped it for 8+ years.

fix.diff; VERDICT.md section 4; upstream tags 11.1.0 (+) vs 11.2.0 (-) fetched 2026-09-18
↓ fix.diffper-fix-DF-4115

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (upstream diff + mechanism); dmesg_verbose.txt (hard sndbuf blkcnt<=2 under hostile fragments, hblkcnt>256 request clamped at hdaa.c:2096); caps.txt (runtime 'BSS 0' - trigger structurally absent); run.log (1310-round battery + 11 hostile frag shapes with real stream starts, guest alive); panic.sndstat.txt (cross-file root-only kldunload panic in sndstat_sysuninit, hdac detaches cleanly - for the orchestrator)

PoC changes

No PoC seed existed. Authored three live-test programs (dsp_battery.c, frag_start.c, hblkcnt_max.c); trigger for the finding itself is unreachable on the guest (BSS 0), so the pack instead proves the adjacent user-reachable paths safe and documents the hardware gate.

Verified recommended fix

Adopt upstream FreeBSD fix: stream = 15 - (ss - sc->num_iss - sc->num_oss); (see fix.diff)

Verdict

hdac.c:1934 computes bidirectional-stream tags as 15-(ss-num_iss+num_oss)=15-2*num_oss-j instead of the intended 15-j; proven by source analysis and by the upstream FreeBSD fix (present since 11.2R/12.x, missing from DF's r275101 import). Impact is functional (wrong/colliding/negative HDA stream tags -> misprogrammed SDCTL2/codec registers on BSS-capable controllers), not memory safety. Live trigger is hardware-gated: the only available controller (QEMU ICH6) reports 'Caps: OSS 4, ISS 4, BSS 0' so the buggy branch never executes; the surrounding user-reachable paths (stream alloc/start/free, hostile SETFRAGMENT, BDL-walk clamps, polling mode) were live-exercised unprivileged with no panic and the clamp chain proven to hold.