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)
PoC verification
Evidence pack
findings/poc/DF-4129 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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).
-
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 iteratesi < 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 writes1past 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. -
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 soundyields 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#ifaway 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. -
Reproduction narrative (guest: DragonFly 6.5-DEVELOPMENT #0, X86_64_GENERIC) a) baseline: stock sound.ko loaded -> no sequencer nodes. (Also observed:
kldunload soundpanics 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), thenFatal 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]=1writes is the corruption source. -
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). -
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.
-
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
fixedfix.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)
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.
No comments yet.