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

hdacc_stream_intr/hdacc_stream_free index codec->streams[dir][stream] with an unvalidated controller-derived stream tag β€” negative DF-4115 BSS tags dispatch through an OOB-read device_t (wild indirect call)

Summary

streams[2][16] is indexed directly with dir/stream handed up by the controller; the DF-4115 BSS sign error can produce negative tags (down to -45 for OSS=15) which hdac stores with a non-NULL dev - hdaa rejects sid<=0 but the poisoned slot stays populated, so any stream-interrupt bit for that index dispatches stream_intr with a negative stream: -2 reads codec->lock (non-NULL) -> method dispatch on a struct lock* -> certain crash; deeper tags read outside the softc into a garbage device_t -> wild indirect call. This corrects DF-4115's no-consequence assessment and adds the missing sink clamp. Guest's QEMU ICH6 reports BSS 0 so the branch is structurally unreachable there (320KB playback clean). Clamp-both-sinks + fix-the-feeder fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4617 Β· 6 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 5.0 KB ↓ raw
run.sh β€” 89 B view raw
dmesg.txt β€” 770 B view raw
fix.diff β€” 878 B view raw
verdict.json β€” 3.5 KB view raw

DF-4617 β€” hdacc.c: unvalidated controller-derived stream tag indexes codec->streams[dir][stream] (OOB read β†’ wild indirect call; NULL-write on free path)

File: sys/dev/sound/pci/hda/hdacc.c Lines: 702-710 (hdacc_stream_intr), 663-670 (hdacc_stream_free)

What this is

struct hdacc_softc holds device_t streams[2][16] (hdacc.c:57) β€” valid stream tags are 1..15 (HDA 4-bit tag field), dir is 0/1. Both hdacc sink functions index that array with the stream/dir values handed up by the hdac controller driver without any bounds validation:

  • hdacc_stream_intr() β€” hdacc.c:708 codec->streams[dir][stream] (read β†’ indirect method call through the loaded device_t), invoked from hdac_one_intr() hdac.c:392-394 with sc->streams[i].stream.
  • hdacc_stream_free() β€” hdacc.c:668 codec->streams[dir][stream] = NULL (write), invoked from hdaa.c:2156 with ch->sid.

The tag handed up by hdac_stream_alloc() (hdac.c:1932-1942) is computed from GCAP controller capabilities, and the BSS branch carries the known DF-4115 sign error: stream = 15 - (ss - num_iss + num_oss) = 15 - 2*num_oss - j, which goes negative whenever 2*num_oss + j > 15 (any controller with OSS β‰₯ 8, or OSS=4 + BSS index j β‰₯ 8, etc.). hdac_stream_alloc stores the negative tag and a non-NULL dev in sc->streams[ss] regardless of the return value (hdac.c:1940-1942); hdaa rejects sid <= 0 (hdaa.c:2170) but the stale slot stays populated, so any stream-interrupt bit for that index dispatches hdacc_stream_intr(dev, dir, negative_tag) β†’ codec->streams[dir][negative] reads memory before the array (cad / lock / dev softc fields and further out for deeply negative tags) as a device_t and tail-calls HDAC_STREAM_INTR(child, ...) through it β€” a wild indirect call primitive (certain crash; attacker-uncontrolled target).

Threat model: malicious or faulty HDA controller (PCI/Thunderbolt hot-plug audio; VM/emulator defect) reporting BSS > 0 with a tag config that yields negative tags, then asserting a stream interrupt for the affected index. Note a fully malicious controller already owns bus-master DMA, so the practical delta is buggy-controller fault injection; severity Low.

Verification (this pack)

  • Guest boots hdac0 (Intel ICH6, QEMU) β†’ hdacc0 β†’ hdaa0 β†’ pcm0.
  • Runtime GCAP caps captured by DF-4115 on this same guest: hdac0: Caps: OSS 4, ISS 4, BSS 0 β†’ num_ss = 8, ss ∈ 0..7, BSS branch (ss >= num_iss + num_oss == 8) is structurally unreachable on this guest β€” the negative-tag feeder cannot fire, so the OOB sink cannot be triggered here. Same hardware gate as DF-4115.
  • Happy-path exercise: 320 KB of PCM written to /dev/dsp (alloc β†’ codec->streams[dir][tag∈1..4] β†’ interrupt dispatch) β€” clean, no panic, no "Unexpected unsolicited response" spam.

Build / Run

Nothing to build for the negative-result run:

./run.sh     # on the host: drives vm.sh; shows codec attach + dsp playback

Expected: playback completes, guest stays up β€” the vulnerability is hardware-gated (BSS > 0 required) and does not trigger on this guest.

Fix

fix.diff clamps both sinks (reject dirβˆ‰{0,1}, streamβˆ‰[1,15] for intr; [0,15] for free). Verified git apply --check clean against the repo root. Not applied β€” sys/ is read-only for the audit. The real fix for the feeder is the DF-4115 upstream one-token correction in hdac.c.

VERDICT.md
↓ download raw

DF-4617 VERDICT β€” unvalidated stream tag in hdacc_stream_intr / hdacc_stream_free

STATUS: not_reproduced β€” live trigger hardware-gated (requires an HDA controller with BSS > 0 whose DF-4115 tag arithmetic yields negative tags; this guest's controller reports BSS = 0). reproduced: 0 Β· impact: none on this guest Β· confidence in the code mechanics: certain (source-traced, line by line, below).

The defect (source truth)

struct hdacc_softc (hdacc.c:53-61):

device_t        streams[2][16];   /* valid tag index: 1..15 */
device_t        tags[64];         /* valid unsol tag: 0..63 */

Sink 1 β€” hdacc_stream_intr (hdacc.c:702-710), the reachable one

708:  if ((child = codec->streams[dir][stream]) != NULL)
709:      HDAC_STREAM_INTR(child, dir, stream);

Caller: hdac_one_intr() (hdac.c:386-396) loops over all num_ss stream slots and, for any set INTSTS bit with sc->streams[i].dev != NULL, calls

HDAC_STREAM_INTR(dev, sc->streams[i].dir, sc->streams[i].stream);

sc->streams[i].stream is set in hdac_stream_alloc() (hdac.c:1932-1942) from controller-GCAP-derived arithmetic. The BSS branch (hdac.c:1934) carries the known DF-4115 sign error:

stream = 15 - (ss - sc->num_iss + sc->num_oss);   /* = 15 - 2*num_oss - j */

which is negative whenever 2*num_oss + j > 15 (j = BSS index; e.g. any controller with OSS β‰₯ 8, or OSS=4 with a 9th+ BSS stream). hdac stores the negative tag and sc->streams[ss].dev = child unconditionally (hdac.c:1940-1942). hdaa_channel_start rejects the returned sid <= 0 (hdaa.c:2170-2171), but the poisoned slot remains dev != NULL, so any stream-interrupt bit for that index dispatches into hdacc_stream_intr with a negative stream:

  • stream = -1 β†’ reads the softc cad+padding word (0) β†’ NULL child β†’ silently skipped (masked);
  • stream = -2 β†’ reads codec->lock (non-NULL!) β†’ HDAC_STREAM_INTR method dispatch on a struct lock * β€” wild indirect call, certain crash;
  • more negative (num_oss=15 β†’ down to 15-30-30 = -45) β†’ reads far outside the softc β†’ semi-controlled garbage device_t β†’ wild call.

No bounds check exists at the sink β€” hdacc.c:708 is the missing clamp.

Sink 2 β€” hdacc_stream_free (hdacc.c:663-670)

668:  codec->streams[dir][stream] = NULL;

Caller passes ch->sid (hdaa.c:2156-2157), which hdaa only ever sets to an alloc return it validated > 0 β€” so the NULL-write at a negative index is currently unreachable through hdaa; the clamp is defense-in-depth.

Why not reproduced on this guest

  • Guest controller: QEMU Intel ICH6 (8086:2668), hdac0@pci0:0:3:0.
  • Runtime caps (boot -v, captured on this same guest image by DF-4115, findings/poc/DF-4115/caps.txt): hdac0: Caps: OSS 4, ISS 4, BSS 0.
  • num_ss = 8, ss ∈ 0..7; the BSS branch requires ss >= num_iss + num_oss = 8 β†’ structurally unreachable; every computed tag is in 1..4. No userspace action can make QEMU's ICH6 report BSS > 0, and no syscall path feeds the tag from user memory.
  • Live exercise (this run, dmesg.txt): hdac0 β†’ hdacc0 β†’ hdaa0 β†’ pcm0 attach; 320 KB PCM β†’ /dev/dsp (RC=0) exercising stream_alloc / streams[dir][1..4] / stream_intr dispatch β€” no panic, no unsol-tag spam, guest healthy.

This mirrors DF-4115's own verdict on the same hardware gate.

Exploit chain (on affected hardware β€” hostile/faulty controller)

  1. Controller reports GCAP with BSS > 0 and 2*num_oss + j > 15 for some BSS index j (e.g. OSS=8/BSS>0, or OSS=4/BSSβ‰₯9).
  2. Audio open exhausts ISS/OSS or requests dir fallback β†’ hdac_stream_alloc picks BSS slot β†’ negative tag stored with dev != NULL (hdac.c:1940-42).
  3. Controller asserts the stream's INTSTS bit (hostile device trivially can; a buggy one may via tag-collision chaos).
  4. hdac_one_intr β†’ hdacc_stream_intr β†’ OOB read at streams[dir][negative] β†’ wild HDAC_STREAM_INTR indirect call β†’ kernel crash / unreliable control-flow hijack (call target is the OOB-read pointer, not directly attacker-chosen; a malicious controller already has DMA, so this is fault-injection amplification, not a new privilege boundary).

Ceiling: kernel memory-corruption-class primitive gated on non-default audio controller hardware β†’ Low severity, certain mechanics.

Fix

fix.diff (git-apply-able, checked): clamp dir/stream at both sinks. The true root cause is DF-4115's hdac.c arithmetic β€” fix upstream there; the hdacc clamps make the sink safe against any future feeder bug.

fix_status: not_testable β€” the trigger is hardware-gated off on this guest (BSS=0), so baseline-vs-patched behavior cannot be observed on the standard snapshot; the clamp compiles and the happy path (playback) is unaffected.

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

Trigger hardware-gated off on this guest (GCAP BSS=0) β€” no baseline misbehavior exists to compare; fix.diff is git-apply-able and the patched-tree happy path is unchanged by construction (clamps only reject invalid indices that cannot occur on this hardware).

fix.diff; git apply --check clean; playback battery in dmesg.txt
↓ fix.diffper-fix-DF-4617

Confirmed kernel references

Detail

Exploit chain

hostile/faulty HDA controller reports GCAP BSS>0 with 2*num_oss+j>15 -> hdac_stream_alloc stores negative tag with dev!=NULL -> controller asserts that stream's INTSTS bit -> hdac_one_intr (hdac.c:392-394) -> hdacc_stream_intr indexes streams[dir][negative] -> OOB-read device_t -> HDAC_STREAM_INTR wild indirect call. Hardware-gated; unreachable on BSS=0 controllers.

Evidence (decisive lines)

VERDICT.md (full path:line chain); dmesg.txt (guest codec chain + playback RC=0); fix.diff (sink clamps, git apply --check clean); findings/poc/DF-4115/caps.txt ('hdac0: Caps: OSS 4, ISS 4, BSS 0' on this same guest image)

PoC changes

No seeded PoC existed; wrote a negative-result run.sh (codec attach + /dev/dsp playback battery) and the path-analysis verdict instead.

Verified recommended fix

Clamp dir/stream at both hdacc sinks (reject dir outside 0..1, stream outside 1..15) as in fix.diff; fix the DF-4115 feeder arithmetic in hdac.c:1934.

Verdict

hdacc_stream_intr (hdacc.c:708) and hdacc_stream_free (hdacc.c:668) index codec->streams[dir][stream] with the controller-derived HDA stream tag without any bounds check. The DF-4115 BSS-tag sign error in hdac.c:1934 produces negative tags on BSS>0 controllers (stored with dev!=NULL at hdac.c:1940-42), and any stream interrupt for such a slot dispatches through an out-of-bounds-read device_t β€” a wild indirect call (certain crash; fault-injection class, not a new privilege boundary since a malicious controller already owns DMA). Not reproduced live: this guest's QEMU ICH6 reports OSS 4, ISS 4, BSS 0 (runtime caps, findings/poc/DF-4115/caps.txt), making the BSS branch structurally unreachable; the in-bounds path was exercised live (320KB playback through hdacc0/hdaa0/pcm0, RC=0, no panic).