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

seq_sync() kernel stack overflow: sync[16] vs midi_number up to 32 β€” unprivileged kernel control-flow corruption (RIP=0) via SNDCTL_SEQ_SYNC/close on the 0666 sequencer

Summary

seq_sync declares sync[16] but loops i < scp->midi_number, a count seq_open derives from attached snd_midi units bounded only by the 32-entry midis[] allocation. 17..32 MIDI units write 4..64 bytes of 0x00000001 words past sync[16] on the kernel stack of the calling unprivileged thread (0666 node), via SNDCTL_SEQ_SYNC or close. LIVE-REPRODUCED with a fakempu.ko harness (32 fake MPU units + 2-line PoC-wiring patch making the dead personality live): Fatal trap with RIP=0/FP=0 - return-address smash; fix validated in the same run (shared MIDIDEV_MAX bounds arrays and loops; survived 3 cycles). Low because the sequencer personality is DEAD CODE on stock DragonFly (midi.c:1509 '#if 0', verified live - only /dev/midistat appears) and the written content is the constant 1; becomes Medium the moment the wiring is restored. The open-loop clamp also closes DF-1394's >32-unit heap window.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4129 Β· 19 files
FileTypeDescriptionSize
seq_sync_oob.c β€” 2.5 KB view raw
fakempu.c β€” 2.9 KB view raw
wiring_enable.diff β€” 438 B view raw
fix.diff β€” 1.3 KB view raw
baseline_stock.log β€” 304 B view raw
wired_load.log β€” 239 B view raw
fakempu_load.log β€” 168 B view raw
panic.txt β€” 1.4 KB view raw
run.log β€” 1.5 KB view raw
fix_setup.log β€” 25 B view raw
fix_run.log β€” 419 B view raw
build.log β€” 5.6 MB ↓ download
fix_build.log β€” 5.6 MB ↓ download
env.txt β€” 547 B view raw
sha256.txt β€” 199 B view raw
VERDICT.md β€” 6.0 KB ↓ raw
README.md β€” 6.1 KB ↓ raw
verdict.json β€” 4.4 KB view raw
manifest.json β€” 1.1 KB view raw

DF-4129 β€” seq_sync() kernel stack overflow: sync[16] vs up to 32 MIDI units

File: sys/dev/sound/midi/sequencer.c:2063, 2122-2131 β€” pass 2 (GLM 5.3). Verified live on the QEMU DragonFly 6.5-DEVELOPMENT guest on 2026-09-18.

THE BUG seq_sync() declares a 16-entry per-unit flag array

    int i, rl, sync[16], done;                /* sequencer.c:2063 */

but sizes its loops by scp->midi_number, which seq_open()
(sequencer.c:766-785) sets to one per attached snd_midi unit and which
is bounded only by the 32-entry scp->midis[] allocation
(sequencer.c:541-543):

    for (i = 0; i < scp->midi_number; i++)    /* :2123 */
            sync[i] = 1;                      /* :2124  OOB WRITE i>=16 */
    ...
    for (i = 0; i < scp->midi_number; i++)    /* :2128 */
        if (sync[i]) {                        /* :2129  OOB READ  i>=16 */

17..32 attached MIDI units => 4..64 bytes of 0x00000001 words written
past sync[16] on the KERNEL STACK of the calling thread.  Reachable
unprivileged (the /dev/sequencer node is created 0666, sequencer.c:563)
via
    * ioctl(fd, SNDCTL_SEQ_SYNC)   -> sequencer.c:1202-1210
    * close(fd) -> seq_close       -> sequencer.c:836-837
Distinct from known DF-1394 (heap midis[] write, needs >32 units) and
DF-1393 (seq_fetch_mid off-by-one NULL deref): different array, kernel
stack not heap, triggers in the 17..32 window.

REACHABILITY ON STOCK DRAGONFLY (pass-2 discovery) The sequencer personality is DEAD CODE on DragonFly: the only caller of seq_modevent() is compiled out (#if 0 at midi.c:1509 and :1517) and sequencer.c registers no module itself, so seq_addunit() never runs and /dev/sequencer + /dev/music are never created. Verified live: stock kldload sound creates only /dev/midistat (baseline_stock.log). The PoC therefore first applies a PoC-WIRING patch (wiring_enable.diff: flips the two #if 0 to #if 1, FreeBSD semantics β€” the vulnerable sequencer.c itself is untouched) and then triggers the overflow. If the wiring is ever re-enabled (it is one #if away, and FreeBSD ships it enabled), stock sequencer.c is vulnerable as-is.

LIVE VERIFICATION (this pack) * fakempu.ko β€” harness kld registering 32 fake MPU-401 UART units through the exported midi_init() (midi.c:274): midi_number == 32, the maximum that does NOT trip the DF-1394 heap bound (>32). * seq_sync_oob β€” unprivileged trigger: open("/dev/sequencer0"), SNDCTL_SEQ_SYNC, close. * VULNERABLE kernel (wired, unmodified sequencer.c): run as unprivileged user "maxx" -> Fatal trap 12, "Fatal user address access from kernel mode from seq_sync_oob at 0000000000000000", RIP=0, frame pointer=0x0 β€” the smashed return address takes the kernel into 0x0 inside the unprivileged user's process (panic.txt; the preceding "noinsync" lines are the OOB read loop executing over i>=16). * FIXED kernel (wiring + fix.diff: shared MIDIDEV_MAX=32 constant sizing sync[]/midis/midi_flags + enumeration clamp): identical trigger completes 3 open/sync/close cycles, rc=0, guest stays up (fix_run.log). * The fix validation run used fakempu with a 1024-byte out-queue; the first fixed-kernel attempt with 128-byte queues hung in midisynth_writeraw during seq_reset β€” a HARNESS artifact (fake UART never drains its tx queue; real hardware drains by IRQ), unrelated to the overflow and absent on the vulnerable kernel (it panics inside the ioctl before seq_reset ever runs).

BUILD (guest, root for module/kernel, user for trigger) sh /tmp/df4129/build.sh # cc trigger + fakempu.ko (kernel obj tree) # sound.ko rebuilt via: cd /usr/src && make -j6 nativekernel # KERNCONF=X86_64_GENERIC (after wiring_enable.diff [+ fix.diff])

RUN kldload sound # wired module -> /dev/sequencer0 0666 kldload /tmp/fakempu.ko # 32 fake units -> /dev/midi0.1..31.32 su - maxx -c '/tmp/seq_sync_oob 3' # unprivileged trigger

EXPECTED vulnerable: fatal trap 12 / panic (RIP=0), guest dies, serial console shows "from seq_sync_oob at 0x0" fixed: "[+] survived 3 open/sync/close cycle(s)", rc=0, guest up

EXPLOITABILITY CEILING The overflow writes a CONSTANT value (1) into attacker-COUNTED (midi_number-16) 4-byte slots. That smashes saved registers, frame pointer and return address (demonstrated: RIP/FP => 0x...0100000001- style values -> controlled fault), but the written content is not attacker-chosen, so the demonstrated primitive is kernel control-flow corruption to a fixed invalid address = reliable local kernel panic (DoS). Turning it into uid=0 would require controlling the smashed words (not possible through this write) or a separate info-leak to pair with register corruption β€” no such pairing was found. Combined with the dead-code wiring on stock DragonFly the practical severity today is LOW; it becomes MEDIUM the moment the sequencer is wired.

FIX fix.diff β€” one shared MIDIDEV_MAX (32) used for the midis/midi_flags allocations, the sync[] array, and as a clamp on the seq_open enumeration loop (also closes the DF-1394 heap window). Validated: baseline panics, patched survives.

ARTIFACTS seq_sync_oob.c / fakempu.c trigger + harness sources wiring_enable.diff PoC wiring (NOT a fix) fix.diff verified fix (validated in-guest) baseline_stock.log stock sound.ko: no /dev/sequencer (dead wiring) wired_load.log wired sound.ko: /dev/sequencer0 + /dev/music0 0666 fakempu_load.log 32 fake units registered panic.txt vulnerable-kernel serial console (fatal trap 12) fix_setup.log / fix_run.log fixed-kernel setup + clean trigger run build.log / fix_build.log full untrimmed kernel/module build logs env.txt / sha256.txt guest environment, source hashes

VERDICT.md
↓ download raw

VERDICT β€” DF-4129 (sys/dev/sound/midi/sequencer.c seq_sync stack OOB)

STATUS: reproduced (vulnerable kernel, wired sequencer) / fixed (fix.diff validated in the same run). Impact: kernel stack control-flow corruption demonstrated as a reliable local kernel panic from an unprivileged process; uid=0 not reached (write content is the constant 1 β€” see ceiling below).

  1. Root cause (source certainty) seq_sync() (sequencer.c:2060-2142) sizes its per-unit scratch array at 16 (int i, rl, sync[16], done;, :2063) but iterates i < scp->midi_number (:2123, :2128) β€” a count that seq_open (:766-785) derives from the number of registered snd_midi units, bounded only by the 32-entry midis[] kmalloc (:541-543). Every attached MIDI unit beyond the 16th therefore writes 1 past sync[16] on the calling thread's kernel stack (:2124) and reads it back (:2129). Both SNDCTL_SEQ_SYNC (via seq_ioctl :1202-1210) and close() (via seq_close :836-837) call seq_sync with seq_lock released at the write point (:2122) β€” no lock ordering involvement. Not a re-report of DF-1394 (that is the midis[] HEAP write at :773, needs >32 units) or DF-1393 (seq_fetch_mid off-by-one at :724): this is the sync[] STACK write in the 17..32 window.

  2. Reachability β€” the pass-2 discovery that gates everything seq_addunit() has no live caller: the only seq_modevent() call sits inside #if 0 (midi.c:1509, :1517) and sequencer.c registers no module. Live proof on the stock guest: kldload sound yields only /dev/midistat β€” no /dev/sequencer, /dev/music (baseline_stock.log). => on stock DragonFly the entire sequencer surface (this bug, DF-1393, DF-1394) is dead code today. The PoC re-enables the wiring with a 2-line patch (wiring_enable.diff β€” the FreeBSD wiring) and leaves sequencer.c untouched, demonstrating that the vulnerability is one #if away from live. If that wiring is ever restored, 17+ MIDI units (multi-port USB/PCIe MIDI interfaces enumerate one snd_midi per port; 8x8-port boxes reach 64) satisfy the precondition.

  3. Reproduction narrative (guest: DragonFly 6.5-DEVELOPMENT #0, X86_64_GENERIC) a) baseline: stock sound.ko loaded -> no sequencer nodes. (Also observed: kldunload sound panics in sndstat_sysuninit() β€” PRE-EXISTING sndstat.c bug already on file from DF-4115's pack; not mine.) b) wiring_enable.diff applied to /usr/src midi.c; make nativekernel; /usr/obj/.../sound.ko installed -> kldload sound creates /dev/sequencer0 and /dev/music0, mode 0666 (wired_load.log). c) fakempu.ko built inside the kernel module obj tree (mpu_if KOBJ class, 8 methods; 32 units via exported midi_init(); midi.c's midimapper_open() then reports 32). Loaded: "fakempu: 32/32 fake MIDI units registered", 32 /dev/midiU.C nodes (fakempu_load.log; ls /dev/midi* | wc -l = 33 incl. midistat). d) TRIGGER as unprivileged user maxx (/tmp/seq_sync_oob 3): serial console shows the synth_if default stubs "noinsync" x N (the OOB read loop at :2129 executing for i>=16), then

      Fatal user address access from kernel mode from seq_sync_oob
          at 0000000000000000
      Fatal trap 12: page fault while in kernel mode
      instruction pointer = 0x8:0x0
      frame pointer       = 0x10:0x0
      current process     = 42761        (the unprivileged trigger)
    

    (panic.txt). Guest dead at db> prompt. The smashed stack took RIP and RBP to ~0 β€” kernel control flow hijacked from an unprivileged process; the 0x00000001-word pattern of the sync[i]=1 writes is the corruption source.

  4. Fix validation (same run, same harness) fix.diff: introduce MIDIDEV_MAX=32; use it for the midis/midi_flags allocations (:544-546), size sync[MIDIDEV_MAX] (:2068), and clamp the seq_open enumeration (i < scp->maxunits && scp->midi_number < MIDIDEV_MAX, :776 β€” also closing the DF-1394 >32 heap window). Applied together with the wiring, kernel rebuilt, sound.ko reinstalled: the identical unprivileged trigger now completes "[+] survived 3 open/sync/close cycle(s)" with rc=0 and the guest stays up (fix_run.log). First fixed-kernel attempt with 128-byte fake out-queues hung in midisynth_writeraw ("Always blocking") during seq_reset's 16-channel controller flush β€” a pure harness artifact (the fake UART has no IRQ-driven drain; real hardware drains), not kernel-path behavior of the fix; rerun with 1024-byte out-queues is clean. On the vulnerable kernel this path never even runs (panic fires inside the ioctl first).

  5. Exploit ceiling β€” why panic, not uid0 Primitive: 4..64 bytes of constant 0x00000001 written past a stack array, count attacker-influenced (attached-unit count minus 16), content NOT attacker-controlled. Demonstrated effect: saved RBP/RIP overwritten with 1-pattern values => return to a fixed invalid address => fatal trap. For uid=0 one would need either attacker-chosen data in the smashed slots (this write cannot), or a register-only corruption that survives to a useful gadget (no attacker-controlled pivot available from a constant). No pairing leak was found in this dead-wired surface. Verdict: reliable local kernel DoS / memory corruption class, no demonstrated escalation.

  6. Bottom line Real, source-certain, live-demonstrated kernel stack overflow (reproduced/panic on the wired sequencer, fixed by fix.diff in the same run), currently gated by the sequencer personality being dead code on DragonFly (midi.c:1509). File the fix; if the sequencer is ever wired back on, this becomes a live unprivileged kernel-smash.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (MIDIDEV_MAX=32 for sync[]/midis/midi_flags + enumeration clamp in seq_open) applied in-guest on top of the PoC wiring, kernel+sound.ko rebuilt: the exact trigger that panicked (fatal trap 12, RIP=0) now completes 3 open/SNDCTL_SEQ_SYNC/close cycles with rc=0 and the guest stays up. The clamp also closes the DF-1394 >32-unit heap window.

baseline: panic.txt (fatal trap 12 from seq_sync_oob, RIP=0x0) | patched: fix_run.log ('[*] SNDCTL_SEQ_SYNC -> rc=0' x3, '[+] survived 3 open/sync/close cycle(s)', TRIGGER-RC=0, vm status up)
↓ fix.diffper-fix-DF-4129

Confirmed kernel references

Detail

Exploit chain

unpriv open(/dev/sequencer0,0666) -> ioctl(SNDCTL_SEQ_SYNC) -> seq_sync writes sync[16..31]=1 past the array, smashing saved regs/RBP/RIP with 1-pattern words -> return to ~0 -> Fatal trap 12 in the unprivileged process. Content is the constant 1 (count attacker-set, data not), so the demonstrated ceiling is reliable kernel-stack control-flow corruption/panic; no uid=0 route found (no data control, no pairing leak). Precondition: >=17 registered snd_midi units AND the sequencer personality being wired (dead code on stock DragonFly: midi.c:1509 #if 0 β€” verified live, kldload sound creates no /dev/sequencer).

Evidence (decisive lines)

panic.txt (fatal trap 12, RIP=0x0, frame pointer=0x0, 'from seq_sync_oob at 0000000000000000', preceding noinsync stub prints = the OOB read loop); baseline_stock.log (stock sound: no /dev/sequencer β€” dead wiring); wired_load.log (/dev/sequencer0+/dev/music0 0666 after wiring); fakempu_load.log (32/32 units); fix_run.log ('[+] survived 3 open/sync/close cycle(s)', rc=0, guest up on fix.diff)

PoC changes

PoC authored fresh (no seed): unprivileged trigger open+SYNC+close; fakempu.ko harness kld registering 32 fake MPU-401 UARTs via exported midi_init() (max units that avoids the known DF-1394 heap bound); 2-line PoC-wiring patch re-enabling the dead-coded sequencer personality (midi.c #if 0 -> 1, sequencer.c untouched); first fixed-kernel harness used 128B out-queues and hung in midisynth_writeraw during seq_reset (fake UART never drains) β€” harness artifact, rerun with 1024B queues clean.

Verified recommended fix

size sync[] (and midis/midi_flags + the seq_open enumeration) with one shared MIDIDEV_MAX=32 constant β€” fix.diff validated

Verdict

seq_sync() sizes its per-unit flag array at 16 (sequencer.c:2063) but loops over scp->midi_number, bounded only by the 32-entry midis[] allocation β€” 17..32 attached MIDI units write 4..64 bytes of 0x1 past sync[16] on the kernel stack of an unprivileged thread via SNDCTL_SEQ_SYNC or close(). Live-reproduced on the guest: 32 harness fake MPU units + wired sequencer (stock DragonFly has the sequencer dead-coded at midi.c:1509) -> 'Fatal user address access from kernel mode from seq_sync_oob at 0x0', RIP=0, FP=0x0 (kernel control-flow corruption demonstrated as reliable local panic). fix.diff (shared MIDIDEV_MAX=32 sizing + enumeration clamp) validated in the same run: identical trigger completes cleanly, guest up.