sco_ctloutput PRCO_SETOPT unconditionally NULL-dereferences on every setsockopt
Summary
sco_ctloutput PRCO_SETOPT (sco_socket.c:123-135): line 124 m=m_get(M_WAITOK,MT_DATA) sets m->m_len=0 (uipc_mbuf.c:1040). Line 125 sopt_to_kbuf(sopt,mtod(m),m->m_len,m->m_len) copies 0 bytes (m_len==0). Line 127 if(m->m_len==0) ALWAYS TRUE. Lines 128-130 m_freem(m);m=NULL;err=EIO. NO break. Line 133 err=sco_setopt(pcb,sopt_name,mtod(m,uint8_t*)) m==NULL mtod(NULL)->NULL->m_data -> UNCONDITIONAL PANIC on EVERY setsockopt regardless of optname/value. More reliable than DF-0687: no specific opt needed. Attacker: unpriv local socket(AF_BLUETOOTH,SOCK_SEQPACKET,BTPROTO_SCO) then ANY setsockopt(s,BTPROTO_SCO,0,&val,sizeof(val)) -> instant panic. Fix: replace mbuf dance with stack buffer like l2cap_setopt2.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0688 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| sco_setsockopt_panic.c | trigger-source | minimal one-setsockopt trigger | 2.1 KB | view raw |
| build.sh | build-script | cc -O2 -Wall | 201 B | view raw |
| run.sh | run-script | kldload netbt.ko + run | 590 B | view raw |
| run.log | run-log | decisive run as maxx; panic | 789 B | view raw |
| panic.txt | panic-signature | Fatal trap 12 va=0x10 sco_ctloutput.cold.2 | 1.4 KB | view raw |
| pre_boot.log | boot-log | boot.log snapshot before trigger | 12.9 KB | view raw |
| boot_tail.txt | boot-log | last 60 lines of boot.log with panic | 2.1 KB | view raw |
| env.txt | environment | uname, cc, kldstat | 345 B | view raw |
| fix.diff | suggested-fix | add missing break in m_len==0 block | 371 B | view raw |
| fix_build.log | build-log | make nativekernel output (35k lines) | 5.6 MB | β download |
| fix_run.log | run-log | post-fix run: setsockopt returns EIO, no panic | 1.2 KB | view raw |
| VERDICT.md | verdict | this analysis | 3.7 KB | β raw |
| README.md | readme | human reproduce doc | 1.5 KB | β 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 |
DF-0688 β sco_ctloutput PRCO_SETOPT unconditional NULL-deref
Summary
sys/netbt/sco_socket.c:123-135 sco_ctloutput() PRCO_SETOPT path
unconditionally dereferences NULL on every setsockopt, because line 124
m = m_get(M_WAITOK, MT_DATA) yields m->m_len == 0, the
if (m->m_len == 0) branch at line 127 therefore always fires (freeing
m and setting m = NULL), and the missing break at line 132 lets
control fall through to sco_setopt(pcb, sopt->sopt_name, mtod(m, uint8_t *))
which expands mtod(NULL,...) and reads offsetof(struct mbuf, m_data)
== 0x10 from NULL β page-fault panic.
How to reproduce
./build.sh ssh dfbsd 'kldload netbt.ko' # admin Bluetooth setup (realistic precondition) ssh dfbsd-maxx 'cd poc/DF-0688 && ./run.sh' # expect: guest dies; panic in dfbsd-qemu/boot.log
Expected (bug present)
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x10 Stopped at sco_ctloutput.cold.2+0x19: movq 0x10,%rax
Expected (after fix)
setsockopt returns -1 with errno=EIO ("Input/output error"). Guest
stays up. No panic.
Preconditions
netbt.koloaded by an admin (realistic Bluetooth deployment).- Attacker is any local user (no special group);
socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO)is allowed for any user once the domain is registered.
Impact
Local unprivileged DoS (kernel panic). No corruption primitive (the m
pointer is NULL, not stale). A single setsockopt kills the kernel.
DF-0688 β sco_ctloutput PRCO_SETOPT unconditional NULL-deref
Verdict: REPRODUCED (panic, unprivilegedβkernel DoS)
Mechanism (sys/netbt/sco_socket.c:123-135)
In sco_ctloutput() for the PRCO_SETOPT case:
case PRCO_SETOPT:
m = m_get(M_WAITOK, MT_DATA); /* line 124: m->m_len == 0 */
err = soopt_to_kbuf(sopt, mtod(m,void*), m->m_len, m->m_len); /* copies 0 bytes */
if (m->m_len == 0) { /* line 127: ALWAYS TRUE */
m_freem(m);
m = NULL;
err = EIO;
/* no `break` here β falls through */
}
err = sco_setopt(pcb, sopt->sopt_name, mtod(m, uint8_t *)); /* line 133: mtod(NULL) */
m_freem(m);
break;
m_get(M_WAITOK, MT_DATA) always yields m->m_len == 0 (uipc_mbuf.c:1040).
sopt_to_kbuf(... , m->m_len, m->m_len) therefore copies 0 bytes. The
if (m->m_len == 0) branch always fires, sets m = NULL; err = EIO, but β
because the reviewer omitted break β execution falls through to line 133
where mtod(m, uint8_t *) expands to ((uint8_t *)((m)->m_data)), i.e.
((uint8_t *)((NULL)->m_data)). That reads offsetof(struct mbuf, m_data)
== 0x10 from NULL, which on this guest traps as a page fault.
The result is unconditional: ANY setsockopt on a SCO socket panics,
regardless of optname or optval.
Trigger (unprivileged, after admin Bluetooth setup)
int s = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO);
int v = 1;
setsockopt(s, BTPROTO_SCO, 0, &v, sizeof(v)); /* unconditional panic */
Precondition: netbt.ko must be loaded. This is the realistic
admin-setup case (an admin deploying Bluetooth loads the module, e.g. via
kldload netbt.ko or /boot/loader.conf). Once loaded, the AF_BLUETOOTH
domain is registered and any unprivileged user can create the SCO socket
that triggers the panic. This is analogous to the ACCEPTABLE precondition
of an admin mounting/making-mountable a filesystem image.
Evidence
panic.txt (excerpt from dfbsd-qemu/boot.log):
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x10 <- offsetof(struct mbuf, m_data) Stopped at sco_ctloutput.cold.2+0x19: movq 0x10,%rax db>
The .cold.2 symbol is the GCC-outlined cold path of sco_ctloutput β
exactly the m_len==0 branch that the missing break falls out of.
Fix
fix.diff adds the missing break; in the if (m->m_len == 0) block:
@@ -128,6 +128,7 @@
m_freem(m);
m = NULL;
err = EIO;
+ break; /* DF-0688: missing bail-out */
}
After the fix, setsockopt on a SCO socket returns EIO (matching the
apparent intent of the bail-out block β the mbuf-dance was already broken
in a way that could never deliver a valid option to sco_setopt).
Restoring actual option delivery would require a further rewrite (replace
the mbuf dance with a stack buffer as l2cap_setopt2 does), but that is
a feature, not a security fix.
Fix-validation
Built the patched netbt.ko against the read-only audit source
(make -f Makefile in sys/netbt/); installed to /boot/kernel/netbt.ko
(sha256 0f2c251b...); kept the kernel itself on the unpatched baseline
(#0); re-ran the PoC as maxx:
BEFORE: Fatal trap 12, fault va=0x10, Stopped at sco_ctloutput.cold.2+0x19
AFTER: setsockopt returns EIO ("Input/output error"); guest stays up.
Three repeat runs are byte-identical (deterministic fix). See fix_run.log.
Threat-model note
This is a local DoS that an unprivileged user can trigger after a
legitimate admin setup (kldload netbt.ko for Bluetooth). No primitive
beyond panic β the m pointer is NULL (not stale), so this is not a UAF
or arbitrary-write primitive. There is no escalation chain to develop.
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
REPRODUCED (live panic). sco_ctloutput PRCO_SETOPT missing break -> mtod(NULL) fault 0x10. netbt.ko loaded, unprivileged. Module fix: break.
No comments yet.