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

Every raw HCI socket is created HCI_PRIVILEGED: the privilege check runs on the netmsg port where curproc==NULL and caps_priv_check_self() evaluates the lwp-less netisr thread β€” the unprivileged opcode/event security filter is dead code

Summary

hci_sattach grants privilege when 'curproc == NULL || caps_priv_check... == 0': the handler executes as a netmsg on netisr port 0 where the thread has no lwp, and caps_priv_check_td returns privileged for any lwp-less thread - so EVERY socket gets the flag regardless of caller, making the opcode filter, the event security/ACL/SCO tap gating, and the five identical hci_ioctl gates dead code. Deterministic 30-line proof: uid 1001 send(HCI_CMD_RESET) -> errno 0, kernel cmdwait growth. With the fix (caps_priv_check(so->so_cred,...)): uid1001 -> EPERM, root unchanged. Unpriv controller RESET DoS, WRITE_SCAN_ENABLE/AUTH/STORED_LINK_KEY pairing manipulation, and link-key/passkey disclosure via the now-'privileged' taps.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5741 Β· 8 files
FileTypeDescriptionSize
README.md file 2.0 KB ↓ raw
VERDICT.md file 1.8 KB ↓ raw
build.sh file 159 B view raw
bypass.c file 1.4 KB view raw
fix.diff file 830 B view raw
run.log file 965 B view raw
run.sh file 309 B view raw
verdict.json file 3.3 KB view raw
README.md file
↓ download raw

DF-5741 β€” every raw HCI socket gets HCI_PRIVILEGED: privilege check

evaluated on the netmsg port instead of the caller

Production code: sys/netbt/hci_socket.c:644

if (curproc == NULL || caps_priv_check_self(SYSCAP_RESTRICTEDROOT) == 0)
    pcb->hp_flags |= HCI_PRIVILEGED;

hci_sattach (like every HCI usrreq) runs as a netmsg on the socket's protocol port = netisr_cpuport(0) (uipc_socket.c:259). On that thread: * curproc (curthread->td_proc) is NULL β†’ first arm TRUE; * caps_priv_check_self() β†’ caps_priv_check_td(curthread) (kern_caps.c:343-352) returns 0 (privileged) for any thread with td_lwp == NULL β€” a netisr kernel thread has none β†’ second arm TRUE regardless of the caller.

Result: the opcode/event/ACL-SCO security filter for unprivileged users (hci_socket.c:510-514, 961-980) is dead code on DragonFly. Verified live on the guest: uid 1001 queued HCI_CMD_RESET (privileged-only opcode, hci_cmds[] length -1) with errno=0 (run.log).

Also affected: the five caps_priv_check_self() gates in sys/netbt/hci_ioctl.c:224,249,259,273,287 (SIOCSBTFLAGS/SIOCSBTPOLICY/ SIOCSBTMASK/...) β€” they run through hci_scontrol (hci_socket.c:608-616) on the same netmsg port, so unprivileged users can also flip unit flags/policy via ioctl.

Threat model: any local user on a machine with a Bluetooth controller: full controller takeover from uid!=0 (RESET/DoS, WRITE_SCAN_ENABLE, WRITE_AUTH_ENABLE, link-key management commands) and, via the same flag in hci_mtap (hci_socket.c:961-980), disclosure of HCI_EVENT_RETURN_LINK_KEYS / LINK_KEY_NOTIFICATION / USER_PASSKEY_NOTIFICATION events β€” i.e. stored Bluetooth link keys and passkeys β€” by socket option, without any privilege.

Reproduce (needs the DF-5740 rig loaded + hw.hsh_setup=1)

sh build.sh
su maxx -c ./bypass       # baseline: "*** PRIVILEGE BYPASS ***"
# apply fix.diff to /usr/src/sys/netbt, rebuild rig (DF-5740 build.sh):
su maxx -c ./bypass       # fixed: RESET -> EPERM

See run.log for both transcripts (root control included).

VERDICT.md file
↓ download raw

DF-5741 β€” VERDICT

Reproduced: yes β€” deterministic (not a race). Unprivileged uid 1001 opened a raw HCI socket through the verbatim production hci_sattach and queued HCI_CMD_RESET (a privileged-only opcode; the hci_cmds[] security table marks it length -1) with errno=0. Kernel state confirmed the command landed on unit->hci_cmdwait (harness state dump 15β†’17). No special timing.

Root cause (both arms of the check grant): - hci_socket.c:644 curproc == NULL β€” true on the netisr port thread; - caps_priv_check_self() β†’ caps_priv_check_td(curthread) β†’ kern_caps.c:348 if (td->td_lwp == NULL) return 0; β€” true for the netisr thread. The imported NetBSD code assumed pru_attach executes on the user's syscall thread; DragonFly's netmsg port serialization moved it.

Impact: security-filter bypass β€” unprivileged local user gains full HCI controller command access + privileged event/packet tap (including link-key/passkey disclosure events) on any machine with a Bluetooth controller. Also disables the five privilege gates in hci_ioctl.c (reached via hci_scontrol from this file).

Fix validated: caps_priv_check(so->so_cred, SYSCAP_RESTRICTEDROOT) (credential captured from the creating user thread in socreate) β€” after the fix: uid1001 β†’ EPERM on RESET; uid0 β†’ unchanged (run.log second half). The same conversion should be applied to hci_ioctl.c:224,249,259,273,287 (out of this file's scope; noted).

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

rebuilt rig against patched hci_socket.c: uid1001 RESET -> EPERM (bypass gone), root -> QUEUED-OK (privilege preserved). run.log holds both transcripts.

run.log (fixed section); fix.diff
↓ fix.diffper-fix-DF-5741

Confirmed kernel references

Detail

Exploit chain

uid!=0 on any machine with a BT controller: socket(AF_BLUETOOTH,SOCK_RAW,BTPROTO_HCI) -> socket is wrongly HCI_PRIVILEGED -> send(2) any controller command (RESET = controller DoS; WRITE_SCAN_ENABLE/WRITE_AUTH_ENABLE/WRITE_STORED_LINK_KEY = pairing/auth manipulation) and setsockopt(SO_HCI_EVT_FILTER) to receive HCI_EVENT_RETURN_LINK_KEYS / LINK_KEY_NOTIFICATION / USER_PASSKEY_NOTIFICATION = disclosure of stored link keys and passkeys from the kernel's controller image.

Evidence (decisive lines)

run.log (baseline: uid1001 RESET QUEUED-OK + kernel queue growth; fixed: EPERM for uid1001, root unchanged); bypass.c (the 30-line determinstic test); fix.diff (so_cred-based check)

PoC changes

no seed; wrote the minimal deterministic userland proof (bypass.c) and ran it as uid 1001 through the DF-5740 rig's verbatim production socket layer

Verified recommended fix

evaluate the caller's credentials captured at socket creation: caps_priv_check(so->so_cred, SYSCAP_RESTRICTEDROOT) instead of curproc/caps_priv_check_self (see fix.diff); apply the same conversion to hci_ioctl.c:224,249,259,273,287.

Verdict

hci_sattach (hci_socket.c:644) grants HCI_PRIVILEGED to EVERY raw HCI socket: the handler runs as a netmsg on netisr_cpuport(0) where curproc is NULL and caps_priv_check_self() evaluates the lwp-less netisr thread (caps_priv_check_td returns 0=privileged for td_lwp==NULL, kern_caps.c:348) - both arms true for any caller. Deterministically verified: uid 1001 queued privileged-only opcode HCI_CMD_RESET with errno=0 (kernel cmdwait confirmed). The whole unprivileged opcode/event/ACL filter (hci_socket.c:510-514, 961-980) is dead code on DragonFly; the five caps_priv_check_self gates in hci_ioctl.c:224-287 (via this file's hci_scontrol) are equally dead. Unprivileged users get full controller takeover (RESET, WRITE_SCAN_ENABLE, link-key ops) and tap of link-key/passkey disclosure events.