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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0688 Β· 15 files
FileTypeDescriptionSize
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
README.md readme human reproduce doc
↓ download 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.ko loaded 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.

VERDICT.md verdict this analysis
↓ download raw

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

fixed

validated

see evidence pack
↓ fix.diffn/a (module-level netbt.ko)

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.