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)
PoC verification
Evidence pack
findings/poc/DF-4115 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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
-
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.
-
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.
-
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.
-
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.
-
CROSS-FILE OBSERVATION (for the orchestrator, not DF-4115) Root
kldunload snd_hdaafter 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_testablefix.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
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.
No comments yet.