bluetoothd, pairing, HID and audio Research · epic planThe plan for EPIC E16 (nextbsd#473): 38 tickets in six stages, from five parallel deep dives covering the kernel and hardware, the engine and Classic-vs-BLE decision, the daemon and CLI, HID, and audio. Seed ticket and decision record: nextbsd#412. Background: Apple stack research. Template: WLAN plan. Every ticket in full: E16 ticket drafts.
bluetoothd (job org.nextbsd.bluetoothd, service org.nextbsd.bluetooth) plus a bluetooth CLI giving it a Mach face.vkbd and cuse, compiled into config/NEXTBSD (K1). There is no .ko tree, so nothing can autoload; this is the wlan_xauth trap again.bluetoothd replaces hcsecd. hcsecd handles three events, has no SSP, and only stores keys for devices already in its config file. bluetoothd supervises stock sdpd, bthidd and virtual_oss, and builds the netgraph graph itself, which is the job devd and rc.d do on stock FreeBSD.virtual_oss moved into FreeBSD 15. HFP (the headset microphone) is the real gap, it starts in the kernel, and it’s parked behind a decision (A6).bluetoothd over the kernel’s LE sockets, so it coexists with Classic on one controller. Not NimBLE (it would need the ACL hook and would kill Classic), and not BlueZ.| ID | Issue | Title | Size | Track | Depends on |
|---|---|---|---|---|---|
| Stage 0 — kernel & image | |||||
| K1 | nextbsd-kernel#231 | config: bake netgraph + the Bluetooth stack, vkbd and cuse into NEXTBSD — no .ko tree, so nothing can autoload (#412 phase 0) | S | Ship | — |
| K2 | nextbsd#475 | boot-test: assert the Bluetooth stack is present on both arches (ngctl list/types, net.bluetooth, hccontrol exits 7) and guard the kernel options | S | Ship | K1 |
| K3 | nextbsd#476 | Ship Intel (ibt-*) and Realtek (rtl_bt) Bluetooth firmware in the image, with licences — fetched at assembly like the Pi DTBs | S | Ship | K1 |
| A2 | nextbsd#485 | Ship virtual_oss's Bluetooth backend (`/usr/local/lib/virtual_oss/voss_bt.so`) on the image | S | Ship | K1 |
| A3 | nextbsd-kernel#236 | kldload(2) of a statically linked module returns ENOENT on NextBSD — virtual_oss dies at `kldload("cuse.ko")` even with cuse baked | S | Ship | K1 |
| Stage 1 — hand verification | |||||
| K6 | nextbsd#477 | Phase 1: hand-verify the baked stack on real USB dongles (amd64 + Pi 500+ via RP1 xhci) and start the Bluetooth hardware matrix | S | Ship | K1 |
| B4 | nextbsd#478 | Spike: hand-verify FreeBSD 15.1's LE path on NextBSD hardware with FreeBSD-BLE (scan, connect, SMP pair, HOGP mouse) and record the controller matrix | S | Needs answer | K1 |
| Stage 2 — bluetoothd core | |||||
| D1 | nextbsd-userland#206 | bluetoothd v0 skeleton: launchd job, Mach service, no-stack/no-adapter idle, `bluetooth status`, CI markers | M | Ship | — |
| B2 | nextbsd-userland#204 | bluetoothd: engine interface (engine.h) that Classic and LE both implement - typed addresses, one pairing-prompt shape, engine-neutral bond record | M | Ship | — |
| D2 | nextbsd-userland#207 | bluetoothd: adapter lifecycle — detect ubtN, build the netgraph graph, initialise the controller, publish State:/Bluetooth/Adapter, power on/off | L | Ship | D1, K1, B2 |
| D3 | nextbsd-userland#208 | bluetoothd: Classic inquiry scan with cached results and remote names; `bluetooth scan` | M | Ship | D2 |
| D5 | nextbsd-userland#210 | bluetoothd: SCPreferences persistence — PairedDevices metadata plus a separate 0600 link-key file; `bluetooth list` | M | Ship | D1 |
| D4 | nextbsd-userland#209 | bluetoothd: security manager replacing hcsecd — link-key/PIN replies, SSP (numeric comparison, passkey), pair/unpair RPCs, pull-model user confirmation | L | Ship | D2, D5 |
| D7 | nextbsd-userland#212 | bluetoothd: supervise stock sdpd (and the child-supervision hook bthidd/btpand will use) | S | Ship | D2 |
| D8 | nextbsd-userland#213 | bluetoothd: connect/disconnect, per-device state publishing (State:/Bluetooth/Device), auto-reconnect of paired devices at boot | M | Ship | D4, D5 |
| D6 | nextbsd-userland#211 | bluetooth(8): full CLI — status|list|scan|pair|confirm|unpair|connect|disconnect|power|discoverable, `--no-prompt` exit codes, man pages | M | Ship | D1, D3, D4, D5, D8 |
| D9 | nextbsd#480 | CI: Bluetooth boot-test gates (BT-RPC-OK, BT-CLI-OK, BT-PREFS-OK, BT-NOSTACK→BT-NOADAPTER flip when the kernel bakes netgraph) | S | Ship | D1 |
| K4 | nextbsd-userland#202 | Contract: bluetoothd runs iwmbtfw/rtlbtfw on USB arrival for controllers that boot in bootloader/ROM mode (devd's iwmbtfw.conf/rtlbtfw.conf, without devd) | M | Sequence | K3, D2 |
| D10 | nextbsd#481 | Bluetooth hardware bring-up run: first execution of bluetoothd scan/pair/reconnect on a real USB dongle (test matrix + findings) | S | Sequence | D2, D3, D4, D8 |
| Stage 3 — keyboards, mice & trackpads (HID) | |||||
| H2 | nextbsd-userland#214 | bluetoothd: HID profile driver — generate bthidd.conf on pair, supervise bthidd -u, keyboards usable pre-login | M | Ship | K1, D4, D5, D7 |
| H3 | nextbsd-userland#215 | bthidd: reload config on SIGHUP without dropping sessions; rescan all devices per tick (NextBSD patch, upstream candidate) | S | Harden | H2 |
| H5 | nextbsd#483 | Bluetooth mice and trackpads in X via per-device uinput nodes (blocked on input hotplug) | M | Sequence | H2, H4 |
| Stage 4 — audio | |||||
| A4 | nextbsd-userland#218 | bluetoothd: A2DP source — on connect of an AudioSink device, run/retarget virtual_oss at `/dev/bluetooth/<addr>`, publish the node, fall back on disconnect | L | Ship | K1, A2, A3, D1, D8 |
| A5 | nextbsd-userland#219 | bluetoothd: A2DP sink — supervise virtual_bt_speaker so a phone can play through the machine's speakers | M | Sequence | K1, A3, D1 |
| A7 | nextbsd-userland#220 | bluetoothd: AVRCP target — accept the headset's play/pause/next/volume keys and deliver them to the session | L | Sequence | A4, D1 |
| Stage 5 — Bluetooth LE | |||||
| B5 | nextbsd-kernel#234 | ng_hci/ng_l2cap LE hardening: honour LE_Read_Buffer_Size for LE ACL flow control, forward LE connection-parameter updates to the controller, implement the stub LE meta handlers | M | Harden | B4, K1 |
| B6 | nextbsd-userland#205 | bluetoothd LE engine: GAP scan/connect, ATT/GATT client, SMP pairing agent and LTK responder over the kernel's ATT/SMP sockets | XL | Sequence | B2, D4, B4, B5 |
| H7 | nextbsd-userland#217 | Scope HOGP (HID over GATT) for LE-only keyboards/mice — blocked on the Bluetooth engine decision | XL | Parked | B6 |
| Needs an answer | |||||
| H4 | nextbsd#482 | Decide how Bluetooth HID input reaches X: kern.evdev.rcpt_mask policy and the input-hotplug interface | S | Needs answer | — |
| A6 | nextbsd-kernel#237 | Decide: HFP/HSP headset microphone — needs eSCO in ng_hci, alt-setting fix in ng_ubt, sco hook wiring, and a from-scratch HFP daemon (XL) | XL | Needs answer | K1, A4 |
| K7 | nextbsd-kernel#232 | rpi5: measure what the firmware leaves for the onboard CYW43455 Bluetooth (BT_ON gio 29, uarta pinmux, 96 MHz clock, bdaddr) before any driver work | S | Needs answer | — |
| Harden — CI without a radio | |||||
| K5 | nextbsd-userland#203 | btemu: a userland HCI controller on ng_hci's drv hook so CI can exercise hci/l2cap/btsocket and bluetoothd without a radio | M | Harden | K1, K2 |
| H8 | nextbsd#484 | CI: synthetic input-path test in qemu (vkbd → kbdmux/evdev, uinput → /dev/input → X) without a radio | M | Harden | K1 |
| A8 | nextbsd#486 | Boot-test: prove the Bluetooth-audio plumbing without a radio (cuse, virtual_oss under launchd, backend load, sndstat/sndctl visibility) | S | Harden | K1, A2, A3 |
| Docs | |||||
| B8 | nextbsd#479 | Docs: correct freebsd-bluetooth-management.html - FreeBSD's kernel already has LE HCI/L2CAP fixed channels and SMP sockets; the gap is userland | XS | Ship | — |
| Parked | |||||
| K8 | nextbsd-kernel#233 | rpi5: onboard Bluetooth over uarta — bcm7271 UART compat, brcmstb-gpio, bcm2712-pinctrl, an H4 transport on ttyhook, BCM4345C0.hcd patchram (parked) | XL | Parked | K1, K7 |
| B7 | nextbsd-kernel#235 | Spike: price a netgraph LE tee node (route LE connection-handle ACL to a userland ng_socket) that would let a full host like NimBLE run beside kernel Classic | M | Parked | B4 |
| H6 | nextbsd-userland#216 | bthidd: Apple Magic Mouse 2 and Magic Trackpad 1/2 vendor reports (scroll, multitouch via uinput) | L | Parked | H2, H5 |
Deep-dive notes, lightly edited. Draft ids map to issues in the index.
Scoping notes for nextbsd/nextbsd#473 (epic) and #412 (seed). Read-only research; nothing filed.
Evidence paths: nextbsd-kernel/…, nextbsd/…, ul/… are the local clones; sys/…, usr.sbin/…
are FreeBSD releng/15.1 unless marked 13.5.
options lines bake the whole USB Bluetooth stack. Every token is stock and
machine-independent (sys/conf/files:4299-4320, sys/conf/options:516-526, NOTES:813-818);
the only bus dependency is usb, which GENERIC already has on amd64 and arm64
(sys/arm64/conf/std.dev:101). One change to config/NEXTBSD covers both arches and
NEXTBSD-RPI5 (which includes it, config/NEXTBSD-RPI5:39).sys/netgraph/ng_socket.c:281-303
— an NGM_MKPEER for an unregistered type does kern_kldload("ng_<type>"). That is how stock
FreeBSD pulls in ng_hci/ng_l2cap from rc.d/bluetooth. On NextBSD it fails (no .ko tree,
nextbsd-kernel/ci/assemble-image.sh:168). Baking the types makes ng_findtype() succeed before
the fallback is reached, so the path is never taken. The BT directories themselves contain no
kldload at all.ng_ubt attaches a bare ubt0 node; on stock FreeBSD
sbin/devd/bluetooth.conf runs service bluetooth quietstart ubt0, and rc.d/bluetooth's
bluetooth_setup_stack() does the mkpeer/connect/hccontrol initialize dance. NextBSD strips
devd and ships no rc.d, so bluetoothd (area 2) owns graph assembly — the interface is spelled
out in §3.ng_ubt_intel.c / ng_ubt_rtl.c refuse
to attach until iwmbtfw(8) / rtlbtfw(8) have pushed firmware over ugen. The blobs come from
Debian firmware-nonfree (ports comms/iwmbt-firmware, comms/rtlbt-firmware; licences allow
redistribution). The trigger on stock FreeBSD is a devd rule; NextBSD has no devd, launchd's
HardwareMatch is iter-1 (parse only, ul/src/launchd/src/core.c:262-270), so the trigger must be
bluetoothd's job (interface in §4.5). Broadcom BCM20702/Apple parts work in ROM firmware; the
only Broadcom firmware tool in tree (bcmfw/ubtbcmfw) is for the 2003-era BCM2033 and ships no
firmware — skip it.uarta (brcm,bcm7271-uart, an 8250 variant FreeBSD has no compat
entry for), needs brcm,brcmstb-gpio (BT_ON = gio 29) and brcm,bcm2712-pinctrl (neither in tree),
a UART HCI transport (ng_h4 removed in 2021, commit 79a100e28e; hcseriald removed 66d6299848),
and a patchram download of BCM4345C0.hcd that FreeBSD has never had. USB dongles over RP1's xhci
are the E16 path on the Pi.compatible strings only
(src-overlay/sys/sys/iocatalogue.h:24-31, iokit_catalogue.c:270-279); there is no USB provider,
the personality generators are PCI-only, and the protocol nodes are not devices at all (nothing fires
device_nomatch for them). ng_ubt also fails the delivery plan's own kext rule
(kext ⇐ has-firmware(9) ∨ multi-version). ~10.6k LOC total, order of half a megabyte of kernel.ngctl list shows btsock_hci_raw/btsock_l2c_raw/
btsock_l2c, sysctl net.bluetooth.version, and hccontrol exiting 7 with Could not find HCI
nodes (hccontrol.c:110-112) instead of EAFNOSUPPORT. A userland HCI controller emulator on the
hci node's drv hook (ng_hci.h:62) can exercise the whole protocol stack without a radio — a
worthwhile hardening ticket. Real dongles: qemu -device usb-host,vendorid=…,productid=… on a
Linux host, or real hardware.### config/NEXTBSD (proposed)
options NETGRAPH # netgraph core: ng_base.c, ng_parse.c
options NETGRAPH_SOCKET # ng_socket: PF_NETGRAPH, what ngctl/libnetgraph/hccontrol use to find nodes
options NETGRAPH_BLUETOOTH # ng_bluetooth: net.bluetooth sysctls, shared version/timeouts
options NETGRAPH_BLUETOOTH_HCI # ng_hci
options NETGRAPH_BLUETOOTH_L2CAP # ng_l2cap
options NETGRAPH_BLUETOOTH_SOCKET # ng_btsocket: PF_BLUETOOTH (HCI raw, L2CAP, RFCOMM, SCO)
options NETGRAPH_BLUETOOTH_UBT # ng_ubt + ng_ubt_intel + ng_ubt_rtl (USB transport)
Why each is required, with the evidence:
| Token | Files it gates (sys/conf/files) |
Why it cannot be left out |
|---|---|---|
NETGRAPH |
netgraph/ng_base.c (:4326), ng_parse.c (:4353) |
Every node type MODULE_DEPEND(…, netgraph, …) (e.g. ng_ubt.c:2055, ng_btsocket.c:218). sys/conf/options:516 |
NETGRAPH_SOCKET |
netgraph/ng_socket.c (:4361) |
hccontrol calls find_hci_nodes() → NgMkSockNode() + NGM_LISTNAMES before it opens the BT socket (usr.sbin/bluetooth/hccontrol/hccontrol.c:110); ngctl(8), l2ping, sdpcontrol, and bluetoothd's graph assembly all need PF_NETGRAPH. options:561 |
NETGRAPH_BLUETOOTH |
common/ng_bluetooth.c (:4299) |
MODULE_DEPEND of ng_hci (ng_hci_main.c:95), ng_l2cap (:95), ng_btsocket (:216), ng_ubt (:2057). Provides net.bluetooth.version and the HCI timeouts (ng_bluetooth.c:59-113) |
NETGRAPH_BLUETOOTH_HCI |
hci/ng_hci_*.c (:4304-4308) |
MODULE_DEPEND(ng_ubt, ng_hci, …) (ng_ubt.c:2056); the hci type must be registered before bluetoothd mkpeers it |
NETGRAPH_BLUETOOTH_L2CAP |
l2cap/ng_l2cap_*.c (:4309-4314) |
Same: mkpeer ubt0hci: l2cap acl hci (rc.d/bluetooth bluetooth_setup_stack) |
NETGRAPH_BLUETOOTH_SOCKET |
socket/ng_btsocket*.c (:4315-4320) |
DOMAIN_SET(ng_btsocket_) (ng_btsocket.c:248) registers PF_BLUETOOTH. Domains are never autoloaded; without it socket(PF_BLUETOOTH, …) is EAFNOSUPPORT and hccontrol dies with Could not create socket (hccontrol.c:122-124) |
NETGRAPH_BLUETOOTH_UBT |
drivers/ubt/ng_ubt.c, ng_ubt_intel.c, ng_ubt_rtl.c — all optional netgraph_bluetooth_ubt usb (:4300-4302) |
The USB HCI transport. DRIVER_MODULE(ng_ubt, uhub, …) + USB_PNP_HOST_INFO(ubt_devs) (ng_ubt.c:2053,2059). One token brings all three files, so the Intel/Realtek "refuse to attach in bootloader mode" guards come for free |
Deliberately not baked:
NETGRAPH_BLUETOOTH_UBTBCMFW — ubtbcmfw(4) is the 2003 BCM2033 firmware-download cdev
(ubtbcmfw.c:32,66); bcmfw(8) says "Due to copyright issues I will no longer provide mini-driver
and firmware files" (bcmfw.8:54-56). Dead weight.NETGRAPH_BLUETOOTH_H4 / _BT3C — the option tokens still exist (sys/conf/options:520-521) but
gate no files: ng_h4 and ng_bt3c are gone in 14+ (§5).NETGRAPH_* node types (options:517-570). None is a BT dependency.All MODULE_DEPENDs in the BT tree resolve inside the set above plus usb:
ng_ubt→{netgraph, ng_hci, ng_bluetooth, usb} (ng_ubt.c:2055-2058), ng_ubt_intel/ng_ubt_rtl→{netgraph, ng_hci, usb}
(ng_ubt_intel.c:288-290, ng_ubt_rtl.c:270-272), ng_hci/ng_l2cap→ng_bluetooth, ng_btsocket→{ng_bluetooth, netgraph}.
device usb is in amd64 GENERIC and in arm64 std.dev:101 (device usb # USB Bus (required)), which arm64 GENERIC includes (sys/arm64/conf/GENERIC:24).
The Pi 5's USB is behind RP1 (patches/0018-…, 0020-…) and the Pi 500+ already runs a USB keyboard through it, so ng_ubt on a dongle has a working uhub parent there.
amd64 vs arm64: no difference. All tokens are in the MI sys/conf/files/options, not files.amd64/files.arm64. Neither GENERIC carries any of them (grep -i netgraph sys/amd64/conf/GENERIC and sys/arm64/conf/GENERIC are empty), so this is purely additive on both. NEXTBSD-RPI5 inherits via include NEXTBSD.
Source: 10,578 lines for ng_base.c + ng_socket.c + every file under netgraph/bluetooth/ (measured, wc -l).
Binary: not measured here — no build in this session. From memory of stock 14.x .ko sizes the resident text+data is on the order of 0.4–0.5 MB (netgraph ≈ 115K, ng_btsocket ≈ 105K, ng_hci/ng_l2cap ≈ 80K each, ng_ubt ≈ 40K, ng_socket ≈ 20K, ng_bluetooth ≈ 10K). K1's acceptance asks for the real size(1) delta on the CI kernel. Against the ~120 MB wired baseline the delivery plan measured, it is noise. On a machine with no radio the code is never entered beyond the SYSINIT that registers types and the btsock_* nodes.
Grep of sys/netgraph/** for kern_kldload|kldload|linker_load|modfind:
sys/netgraph/ng_socket.c:281-303 — the only hit. Inside ngc_send(): if a message is NGM_MKPEER and ng_findtype(mkp->type) == NULL, it does kern_kldload(curthread, "ng_<type>", &fileid); on failure the mkpeer errors out (ENOENT/ENXIO). This is how stock FreeBSD's rc.d/bluetooth (required_modules="ng_bluetooth ng_hci ng_l2cap ng_btsocket", line 8, plus the mkpeers) works without anything in the kernel config. On NextBSD the kldload can never succeed; with the types baked, ng_findtype() succeeds and the branch is skipped. This should be added to the kernel#52 audit table.sys/netgraph/bluetooth/. ng_ubt attach only creates the ubt node; it never loads or mkpeers hci.rc.d/bluetooth is itself a kldload consumer (required_modules), but NextBSD runs no rc.d, so it is irrelevant except as the reference for what bluetoothd must do.After K1, plugging in a class-E0/01/01 dongle yields:
ubt0 on uhub0
ubt0: <vendor product> on usbus0
and a netgraph node ubt0: of type ubt with one hook, hook. Nothing else. No ubt0hci, no L2CAP, no socket wiring, no HCI reset. On stock FreeBSD that is done by:
sbin/devd/bluetooth.conf: attach 100 { device-name "ubt[0-9]+"; action "service bluetooth quietstart $device-name"; } (and quietstop on detach).libexec/rc/rc.d/bluetooth bluetooth_setup_stack(): ngctl mkpeer ${dev}: hci ${hook} drv → ngctl name ${dev}:${hook} ${dev}hci → mkpeer ${dev}hci: l2cap acl hci → name … ${dev}l2cap → connect ${dev}hci: btsock_hci_raw: raw ${dev}raw → connect ${dev}l2cap: btsock_l2c_raw: ctl ${dev}ctl → connect ${dev}l2cap: btsock_l2c: l2c ${dev}l2c → hccontrol -n ${dev}hci reset (3 tries) → read_bd_addr, read_local_supported_features, read_buffer_size, write_scan_enable, write_class_of_device, write_authentication_enable, write_encryption_mode, write_node_role_switch, change_local_name, initialize.NextBSD strips /sbin/devd and /sbin/devmatch from the base (nextbsd-freebsd-compat README, "Superseded base tools": replaced by kextd + libIOKit) and never installs /etc/rc.d. So this is an interface to area 2: bluetoothd must subscribe to device arrival (libIOKit IOKitNotify / the kernel registry, the same path diskarbitrationd uses in ul/src/DiskArbitration/da_iokit_subscribe.c), and on ubt%d arrival run the equivalent of bluetooth_setup_stack() through libnetgraph (NgMkSockNode + NgSendMsg), then the HCI init sequence. Until it exists, §8's hand recipe does it with ngctl.
Good news on the userland tools: the compat base is a full buildworld with only WITHOUT_TOOLCHAIN/TESTS/LIB32/DEBUG_FILES (nextbsd-freebsd-compat .github/workflows/build.yml:30-31, README "What it builds"), and MK_BLUETOOTH/MK_NETGRAPH default to yes (share/mk/src.opts.mk:70,150; MK_BLUETOOTH is only forced off when MK_NETGRAPH=no, :444-445). So hccontrol, l2ping, l2control, sdpcontrol, sdpd, hcsecd, bthidd, bthidcontrol, iwmbtfw, rtlbtfw, ngctl, libbluetooth, libsdp, libnetgraph should already be in nextbsd-base-<arch>.tar.gz. Not verified on an image — the site's srclist plan (site/freebsd-srclist-build-plan.html) marks usr.sbin/ngctl as DEFER, but that plan is not what the compat repo builds today. K1's acceptance includes ls /usr/sbin/hccontrol /usr/sbin/ngctl on the CI image so this is settled by evidence.
ng_ubt claims (sys/netgraph/bluetooth/drivers/ubt/ng_ubt.c)UDCLASS_WIRELESS (0xE0) / subclass UDSUBCLASS_RF (0x01) / protocol UDPROTO_BLUETOOTH (0x01) — ubt_devs[]:452-455. This is every standards-conforming dongle (CSR, Cambridge, Qualcomm/CSR8510, modern Intel/Realtek/MediaTek once firmware is up).USB_VENDOR_BROADCOM + UICLASS_VENDOR/UDSUBCLASS_RF, :460-463), Apple (:466-469, i.e. every Mac's BCM20702/BCM4360-family combo card), Foxconn (:473-475), plus explicit BCM20702A0 ids (ASUS/Lite-On/Foxconn/Dell/Belkin, :520-526) and legacy TDK/ALPS/Ericsson/Canyon.ubt_ignore_devs[]:392-446: AVM BlueFritz v1, Atheros AR3011/AR3012/AR9285/AR5BBU12 with sflash firmware (need ath3kfw), all twelve Intel ids (handed to ng_ubt_intel.c), AzureWave 3602/3604. ubt_rtl_devs are likewise refused by the base probe (:625-627) and handled by ng_ubt_rtl.c.BUS_PROBE_GENERIC (:638,641), so a more specific driver can outbid it.Broadcom patchram: ng_ubt contains no patchram/.hcd download (grep broadcom|BCM in the file shows only id tables). Linux's btbcm would push a .hcd into BCM20702/BCM4350-class parts; FreeBSD runs them on ROM firmware. They work (Macs have run FreeBSD BT this way for years) but with whatever bugs the ROM has and no vendor updates. Not an E16 blocker; noted as a known gap.
ng_ubt_intel.c + iwmbtfw(8)07dc,0a2a,0aa7 (7260/7265), 0a2b,0aaa,0025,0026,0029 (8260/8265/9260-era), 0032,0033,0035,0036 (9260/9560/AX-class) — ng_ubt_intel.c:89-105.Intel_Read_Version, TLV image type) and returns ENXIO while the part is in bootloader mode (:193-270), because attaching the stack then "locks the adapter hardly so it requires power on/off cycle to restore" (:33-37). Once iwmbtfw has loaded operational firmware the device re-enumerates and ng_ubt attaches normally.iwmbtfw -d <ugenN.M> -f <dir> (iwmbtfw.8:59-61: "available in the comms/iwmbt-firmware port"). The port pulls firmware-iwlwifi_<date>_all.deb from Debian non-free-firmware and installs usr/lib/firmware/intel/ibt-* (.sfi + .ddc) to /usr/local/share/iwmbt-firmware (comms/iwmbt-firmware/Makefile; LICENSE=INTEL, LICENSE_PERMS= dist-mirror dist-sell pkg-mirror pkg-sell auto-accept). Redistribution is fine; the set is large (many ibt-* variants — size not measured, K3 asks for it).usr.sbin/bluetooth/iwmbtfw/iwmbtfw.conf — devd notify on system USB / subsystem DEVICE / type ATTACH / vendor 0x8087 / product (…12 ids…) → /usr/sbin/iwmbtfw -d $cdev -f /usr/local/share/iwmbt-firmware.ng_ubt_rtl.c + rtlbtfw(8)USB_VENDOR_REALTEK class E0/01/01 plus ~40 explicit VID/PIDs (ng_ubt_rtl.c:69-169); the probe reads HCI_Read_Local_Version and refuses (ENXIO) when lmp_subversion is in the "ROM firmware incompatible with ng_ubt" list (:172-181, 248-250).rtlbtfw -d <ugen> -f <dir>; port comms/rtlbt-firmware (Debian firmware-realtek_<date>_all.deb → usr/lib/firmware/rtl_bt/*, LICENSE=REALTEK, same permissive LICENSE_PERMS). Small (a few MB).usr.sbin/bluetooth/rtlbtfw/rtlbtfw.conf — 365 lines of devd notify rules: generic vendor 0x0bda + interface 0 + intclass 0xe0/intsubclass 0x01/intprotocol 0x01, then per-chip ids for 8821CE/8851BE/8852AE/8852BE/8852CE/8922AE/8723AE/BE/BU/DE/8761BUV/8821AE/8822BE/8822CE (VIDs 0x13d3, 0x0489, 0x04ca, 0x04c5, 0x0cb8, 0x0b05, 0x2357, 0x7392, …).ath3kfw(8)): firmware "available in the linux-firmware RPM" (ath3kfw.8:57), no port, devd rule is a commented-out example in sbin/devd/devd.conf:71-76. Rare today; park.ng_ubt claims it, but those parts need an mt7921-style firmware push FreeBSD has no tool for; expect a non-functional attach. Flagged, unverified.ubtbcmfw/bcmfw): skip (§1.1)./boot/firmware and the .ko tree (nextbsd-kernel/ci/assemble-image.sh:168-169); nextbsd#392 is about a stray /boot/firmware/iwm* that crept back.Contents/Resources/firmware/) and is found by firmware(9) via patch 0004-NextBSD-search-firmware_path-for-raw-firmware-images.patch (kernel#12/#13; IntelWiFi.kext carries 267 MB this way per the WLAN plan).firmware(9). ng_ubt_intel.c deliberately does not download; it just refuses to attach. So the kext firmware path does not apply and there is no kext to hang it on (§6).nextbsd/build.sh:444-482 fetches the Pi DTBs and LICENCE.broadcom from raspberrypi/firmware at a pinned tag, and ships the licence next to them. K3 does the same for ibt-* and rtl_bt/* from Debian's firmware-nonfree pool (the ports' own MASTER_SITES), into /usr/share/firmware/bluetooth/{intel,rtl_bt} plus the two copyright files. Alternative considered: a data-only org.nextbsd.firmware.bluetooth bundle under /System/Library/Extensions — consistent with the naming plan, but OSKext authentication (assemble-image.sh:199-202) and kextd would have to tolerate an executable-less bundle for no functional gain. Recommend the plain directory.On stock FreeBSD devd runs the loader on USB attach. On NextBSD:
devd is stripped (compat README).HardwareMatch is iter 1: parse and store only; the criteria are name/class/driver, and "iter 2 registers them against hwregd's watch RPC and dispatches" (ul/src/launchd/src/core.c:262-270, 6014-6047). There is no hwregd daemon in ul/src (only libIOKit consumers reference the name). Even at iter 2, the criteria cannot express VID/PID.device_nomatch for PCI and FDT only (§6). An Intel part in bootloader mode is a ugen with no driver, which is exactly a nomatch — but there is no USB provider to hang a personality on.So the trigger belongs to bluetoothd: subscribe to USB device arrival via libIOKit, match the union of iwmbtfw.conf/rtlbtfw.conf ids (or simply "VID 0x8087 or 0x0bda, or class E0/01/01 with no ubt sibling after N ms"), run /usr/sbin/iwmbtfw|rtlbtfw -d ugenX.Y -f /usr/share/firmware/bluetooth/<vendor>, and expect the device to detach/re-attach as ubtN. K4 records this contract for the daemon ticket set. An in-kernel port of Linux btintel/btrtl into ng_ubt_* (which would let firmware(9) + a kext bundle work) is the long-term "right" shape but is thousands of lines and not needed for E16.
raspberrypi/linux rpi-6.6.y (the tree whose bcm2712.dtsi still carries the SoC UART nodes; the rpi-6.12.y copy has moved them into includes):
bcm2712-rpi-5-b.dts:365-380: /* uarta communicates with the BT module */ &uarta { uart-has-rtscts; auto-flow-control; clock-frequency = <96000000>; pinctrl-0 = <&uarta_24_pins &bt_shutdown_pins>; bluetooth { compatible = "brcm,bcm43438-bt"; max-speed = <3000000>; shutdown-gpios = <&gio 29 GPIO_ACTIVE_HIGH>; local-bd-address = [00 00 00 00 00 00]; }; }. The Pi 500 dts #includes the 5-B dts (bcm2712-rpi-500.dts:2), so same wiring. krnbt (bcm2712-rpi.dtsi:220) and bdaddr (:177) are the config.txt overrides on that node.bcm2712.dtsi:510-519 (6.6.y): uarta: serial@7d50c000 { compatible = "brcm,bcm7271-uart"; reg-shift = <2>; reg-io-width = <4>; interrupts = <GIC_SPI 276>; skip-init; } — physical 0x107d50c000. Not the PL011: the debug console NextBSD uses is uart10: serial@7d001000 (arm,pl011, bcm2712.dtsi:241-249, config/rpi5.env).bcm2712.dtsi:497-508: gio: gpio@7d508500 { compatible = "brcm,brcmstb-gpio"; brcm,gpio-bank-widths = <32 22>; } — BT_ON is line 29 here (not on RP1's GPIO, which NextBSD already drives with patch 0025/0048).bcm2712.dtsi:402-…: pinctrl: pinctrl@7d504100 { compatible = "brcm,bcm2712-pinctrl"; uarta_24_pins { pin_rts { function="uart0"; pins="gpio24" }; pin_cts { pins="gpio25" } … } and bt_shutdown_pins { function="gpio"; pins="gpio29" } (bcm2712-rpi-5-b.dts:354-357).| Piece | Status in 15.1 | Evidence |
|---|---|---|
UART driver for brcm,bcm7271-uart |
None. uart_dev_ns8250.c's FDT table is ns16550/ns16550a only (:527-528); the DT node carries no such fallback. sys/dev/uart/ has no uart_dev_bcm7271.c. reg-shift/reg-io-width are honoured generically (uart_bus_fdt.c:87,96), so an added compat entry with a 96 MHz rclk may be enough for basic TX/RX — the bcm7271 auto-flow-control and >115200 behaviour are unverified |
sys/dev/uart listing |
GPIO for BT_ON (brcm,brcmstb-gpio) |
None. sys/dev/gpio/ has no brcmstb driver; files.arm64:570-595 has only the 2837/2838 bcm2835_gpio/raspberrypi_gpio, and NEXTBSD-RPI5 switches those SoC options off (config/NEXTBSD-RPI5:63-64) |
sys/conf/files.arm64 |
Pinctrl (brcm,bcm2712-pinctrl) |
None (the RP1 pinmux is patched in, 0048-rp1_gpio-implement-fdt-pinctrl…; the SoC-side one is not) |
|
| UART HCI transport (H4) | Removed. ng_h4 deleted 2021-09-05, commit 79a100e28e ("disconnected 13 years ago … completely fails to compile … Bluetooth has largely (completely?) moved on from bluetooth over UART transport"); hcseriald removed 66d6299848. Present in releng/13.5 (sys/netgraph/bluetooth/drivers/h4/ng_h4.c, 1019 lines, old linesw line discipline — ng_h4.c:98-107), absent in 14.0+. A rewrite would follow ng_tty(4)'s ttyhook pattern (sys/netgraph/ng_tty.c:47,116-121), which is alive in 15.1 |
gh api search/commits |
| Patchram (firmware download) | Never existed on FreeBSD. Linux btbcm sends BCM4345C0.hcd (63,806 bytes, RPi-Distro/bluez-firmware debian/firmware/broadcom/) over HCI vendor commands (0xFC2E download-minidriver, then the .hcd records, then 0xFC18 baud update) before the stack starts. Licence: "CYPRESS WIRELESS CONNECTIVITY DEVICES DRIVER END USER LICENSE AGREEMENT", Debian non-free-firmware; Raspberry Pi confirms it is released "on the expectation that we will release them to our users" (debian/copyright:19-131). Redistributable with the licence text, same class as LICENCE.broadcom the image already ships |
|
| BD address | local-bd-address is all-zeros in the DT; on Linux the Pi firmware patches it (bdaddr override) and btbcm applies it with vendor cmd 0xFC01. Without that the ROM default (43:45:C0:00:1F:AC) is shared by every board |
|
| 3 Mbaud + RTS/CTS | Needs the bcm7271 clock (96 MHz from the board dts) and hardware flow control; whether uart(4)'s ns8250 path drives auto-flow on this block is unverified |
|
| Power/mux state left by the Pi firmware | Unknown. Whether start4.elf/EEPROM leaves gio 29 high and gpio24-27 muxed to uart0 (Linux hci_bcm toggles the shutdown GPIO itself). K7 measures this before anyone writes a driver |
needs-hardware |
Five independent missing pieces (UART compat/clock, brcmstb-gpio, bcm2712-pinctrl, an H4 transport rebuilt on ttyhook, a patchram tool + firmware packaging), plus a BD-address fix-up, plus an hcseriald equivalent that has to be owned by bluetoothd. Every one is a first for FreeBSD. That is XL and off E16's critical path. kernel#99 (BCM43455 WiFi over SDIO) shares the chip but not a single line of this — the BT interface is a separate UART.
Recommendation: park (K8) behind a measurement spike (K7). For E16 the supported Pi path is a USB dongle in one of RP1's xhci ports, which exercises exactly the baked stack from §1. Say so in the epic so nobody expects the onboard radio to light up.
The driver-delivery plan's rule (site/nextbsd-driver-delivery-plan.html §3): three tiers, and "kext ⇐ has-firmware ∨ needs-multi-version"; otherwise compiled-in or plain kld. Applying it:
ng_hci, ng_l2cap, ng_btsocket, ng_bluetooth, netgraph itself) are not device drivers. They are created by NGM_MKPEER from userland; no device_nomatch ever fires for them, so neither the IOCatalogue nor kextd would ever be asked to load them. The only "autoload" mechanism is ng_socket.c's kern_kldload hack, and turning that into a kextd Mach request is a new kernel feature for no benefit. They must be baked.ng_ubt cannot be matched by the catalogue today. iocatalogue.h:24-31 defines exactly IOCAT_PROVIDER_IOPCIDEVICE and IOCAT_PROVIDER_IOPLATFORMDEVICE (FDT compatible); the nomatch work item carries a PCI match_word "0 if FDT" and a compat[] string (iokit_catalogue.c:270-279). The six gen-*-personalities.sh in nextbsd-kernel-extensions all emit IOProviderClass IOPCIDevice. Matching on USB class E0/01/01 (IOUSBInterface + bInterfaceClass/bInterfaceSubClass/bInterfaceProtocol, or idVendor/idProduct) is a new provider kind, a new capture path on the USB nomatch, and a new generator — real work that E16 does not need.ng_ubt fails the kext bar anyway. It carries no firmware(9) firmware (Intel/Realtek firmware goes through userland tools, §3), and there is one version of it. ~2,600 LOC across the three files.Where a kext would fit later: an IOUSBDevice provider in the catalogue is worth having for the general USB-driver population (audio, serial, WLAN dongles), and ng_ubt could move out then. That is a K-series feature, not an E16 one — listed as an open question, not a ticket. For the Pi 5 UART transport, the BT node is a child of a UART node with no bus driver enumerating it, so it too would be baked into NEXTBSD-RPI5 whenever it exists.
usb-bt-dongle device were removed in November 2019 (qemu commits 1d4ffe8dc7 "Remove the core bluetooth code", 43d68d0a94 "hw/usb: Remove the USB bluetooth dongle device"; shipped in 4.2). There is nothing to emulate against.-device usb-host,vendorid=0x0a12,productid=0x0001 (qemu docs/system/devices/usb.rst:160-165, :318-330), given /dev/bus/usb permissions and an xhci/ehci controller in the guest (-device qemu-xhci is already how the boot tests get USB). On macOS/HVF (nextbsd#408 runs the harness there) libusb passthrough requires detaching the host driver and is unreliable — treat it as a developer convenience, not CI. GitHub-hosted runners have no dongle either way.nextbsd-wlan-plan.html:45).After K1, on the existing qemu boot test (nextbsd/tests/boot-test.sh, both arches):
ngctl list # must show btsock_hci_raw, btsock_l2c_raw, btsock_l2c (and a
# `ngctl(pid)` socket node) — the ng_btsocket nodes exist at boot
# (ng_btsocket_hci_raw.c:197-207 creates and names them at init)
ngctl types # must list types hci, l2cap, ubt, socket (registered by NETGRAPH_INIT)
sysctl -n net.bluetooth.version # prints NG_BLUETOOTH_VERSION (ng_bluetooth.c:59-62); ENOENT before K1
hccontrol read_bd_addr; echo $? # exit 7, stderr "Could not find HCI nodes" (hccontrol.c:110-112)
# NOT "Could not create socket: Address family not supported…"
l2ping -a 00:00:00:00:00:01 # must fail on the connect, not on socket(2)
ls /usr/sbin/hccontrol /usr/sbin/ngctl /usr/sbin/iwmbtfw /usr/sbin/rtlbtfw /usr/sbin/sdpd /usr/sbin/bthidd
Plus the guard that the WLAN plan asked for in kernel#52: fail the kernel build if config -x of the built kernel lacks any of the seven options.
ng_hci does not care what is on its drv hook (ng_hci.h:62: "drv" /* Driver <-> HCI */). A userland process can NgMkSockNode(), mkpeer . hci <hook> drv, name it emu0hci, wire l2cap and the btsock_* nodes exactly as rc.d/bluetooth does, and then answer HCI commands arriving on its data socket with Command Complete events (Reset, Read_BD_ADDR, Read_Local_Supported_Features, Read_Buffer_Size, Write_Scan_Enable, …), and optionally fake an inquiry result and an ACL connection. That runs the whole ng_hci/ng_l2cap/ng_btsocket path plus hccontrol/l2ping/sdpd in qemu with no hardware — the Bluetooth analogue of a loopback interface, and the only way CI will ever exercise bluetoothd's state machine before real dongles do. ~400 LOC of C over libnetgraph. Linux has the same thing (vhci/btvirt); FreeBSD never grew one.
Class-E0 CSR/Cambridge dongles (0a12:0001) are the zero-firmware baseline; test Intel/Realtek only after K3/K4.
### 0. enumeration
usbconfig # ugenX.Y: <…> … cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON
dmesg | grep ubt # ubt0 on uhubN
ngctl list # ubt0 (type ubt), btsock_* nodes
### 1. build the graph by hand (what bluetoothd will do; verbatim from rc.d/bluetooth)
ngctl mkpeer ubt0: hci hook drv
ngctl name ubt0:hook ubt0hci
ngctl mkpeer ubt0hci: l2cap acl hci
ngctl name ubt0hci:acl ubt0l2cap
ngctl connect ubt0hci: btsock_hci_raw: raw ubt0raw
ngctl connect ubt0l2cap: btsock_l2c_raw: ctl ubt0ctl
ngctl connect ubt0l2cap: btsock_l2c: l2c ubt0l2c
### 2. bring the controller up
hccontrol -n ubt0hci reset
hccontrol -n ubt0hci read_bd_addr # BD_ADDR: xx:xx:xx:xx:xx:xx <- the real proof
hccontrol -n ubt0hci read_local_version_information
hccontrol -n ubt0hci read_local_supported_features
hccontrol -n ubt0hci read_buffer_size
hccontrol -n ubt0hci write_scan_enable 3 # discoverable + connectable
hccontrol -n ubt0hci write_class_of_device 0x1c0104
hccontrol -n ubt0hci change_local_name nextbsd
hccontrol -n ubt0hci initialize
hccontrol -n ubt0hci read_node_state # must be "up"
### 3. see the world
hccontrol -n ubt0hci inquiry # lists BD_ADDR + class of nearby discoverable devices
hccontrol -n ubt0hci remote_name_request <addr> 0 0 0
### 4. L2CAP + SDP against a phone/headset in pairing mode
l2ping -a <addr> # 44 bytes from … seq_no=0 time=… ms result=0
sdpcontrol -a <addr> browse # service records (Handsfree, A2DP sink, HID, …)
### 5. teardown (what bluetoothd does on detach)
ngctl shutdown ubt0hci:
ngctl shutdown ubt0l2cap:
Intel/Realtek addendum (after K3): usbconfig shows the part with no ubt sibling; run
iwmbtfw -d ugenX.Y -f /usr/share/firmware/bluetooth/intel (or rtlbtfw … /rtl_bt), watch the device
detach and come back as ubt0, then continue from step 1. Pairing (hcsecd), HID (bthidd) and
audio are areas 2–4.
Record the result in the hardware matrix (K6): dongle ids, arch, board (a Pi 500+ via RP1 xhci is on the list), what worked, kernel/userland SHAs.
size /boot/kernel/kernel before/after) rather than trusting §1.3's estimate.ibt-* — measure in K3; if it is tens of MB, consider trimming to the ids ng_ubt_intel.c lists (the tool picks by hw_variant/hw_revision, so the mapping is derivable from iwmbtfw's source).bluetoothd; if area 2 prefers a separate org.nextbsd.btfw one-shot, K4's contract still holds..hcd loader over ugen (~300 LOC, same shape as iwmbtfw) would be a Harden item if Mac users hit ROM bugs.hccontrol has an le_commands table, hccontrol.c:190) but no LE L2CAP/ATT/SMP; that is the seed ticket's "classic first" question, not a kernel-config one.hccontrol: Could not create socket or ngctl: send msg: No such file or directory, not a kernel message. K2's boot-test assertions are the guard.ng_ubt_intel.c:33-37). ng_ubt_intel prevents this, and it is inside the same NETGRAPH_BLUETOOTH_UBT token, so it cannot be forgotten — but a future "generic class E0" kext or a hand mkpeer on the wrong node could still do it.copyright/EULA text; the DTB precedent (build.sh:480-482) shows how.1-kernel-hardware-tickets.json)| ID | Repo | Title | Track | Size |
|---|---|---|---|---|
| K1 | nextbsd-kernel | config: bake netgraph + the Bluetooth stack (ng_bluetooth/hci/l2cap/btsocket/ubt) into NEXTBSD — no .ko tree, so nothing can autoload (#412 phase 0) | Ship | S |
| K2 | nextbsd | boot-test: assert the Bluetooth stack is present on both arches (ngctl list/types, net.bluetooth, hccontrol exits 7) and guard the kernel options | Ship | S |
| K3 | nextbsd | Ship Intel (ibt-*) and Realtek (rtl_bt) Bluetooth firmware in the image, with licences — fetched at assembly like the Pi DTBs | Ship | S |
| K4 | nextbsd-userland | Contract: bluetoothd runs iwmbtfw/rtlbtfw on USB arrival for controllers that boot in bootloader/ROM mode (devd's iwmbtfw.conf/rtlbtfw.conf without devd) | Sequence | M |
| K5 | nextbsd-userland | btemu: a userland HCI controller on ng_hci's drv hook so CI can exercise hci/l2cap/btsocket and bluetoothd without a radio | Harden | M |
| K6 | nextbsd | Phase 1: hand-verify the baked stack on real USB dongles (amd64 + Pi 500+ via RP1 xhci) and start the Bluetooth hardware matrix | Ship | S |
| K7 | nextbsd-kernel | rpi5: measure what the firmware leaves for the onboard CYW43455 Bluetooth (BT_ON gio 29, uarta pinmux, 96 MHz clock, bdaddr) before any driver work | Needs answer | S |
| K8 | nextbsd-kernel | rpi5: onboard Bluetooth over uarta — bcm7271 UART compat, brcmstb-gpio, bcm2712-pinctrl, an H4 transport on ttyhook, BCM4345C0.hcd patchram (parked) | Parked | XL |
Deep-dive notes, lightly edited. Draft ids map to issues in the index.
Research for nextbsd/nextbsd#473 (E16 Bluetooth), seed nextbsd/nextbsd#412. Read-only; nothing filed.
All FreeBSD file:line cites are against releng/15.1 at 88e7371d9dc26f85dfc1b008cbe59ebc7e4a33da (2026-08-24, "Add UPDATING entries and bump version"), sparse-cloned to freebsd-src.
site/freebsd-bluetooth-management.html §4–5 says FreeBSD's stack "supports Classic Bluetooth only — no BLE, no GATT, no SMP" and that "the gap is in the kernel". It is not. Since Takanori Watanabe's 2015 commits (fbc48c2bfb "Initial Bluetooth LE support", 3a601a2381 "Bluetooth LE Security Management channel support") the kernel does LE HCI, LE ACL connections, and exposes the ATT (CID 4) and SMP (CID 6) fixed channels as ordinary AF_BLUETOOTH L2CAP sockets with an LE address type in sockaddr_l2cap. What is missing is entirely userland: there is no GATT client, no SMP pairing agent, no LTK store, no HOGP driver in base. Those exist as an out-of-tree prototype (takawata/FreeBSD-BLE, HOGP mouse working; rhaberkorn/FreeBSD-BLE 2025 fork adds a one-shot HID pairing script) with no license file.bluetoothd over the raw HCI socket (absorbing hcsecd's three-event loop and adding SSP, which the kernel now passes through since 4ae0fa8a2f, 2025-01). LE engine = a small userland ATT/GATT/SMP host over the kernel's ATT/SMP sockets — the FreeBSD-BLE shape, clean-room or relicensed — not a full third-party host. NimBLE/BTstack-style full hosts need the raw ACL stream, which on netgraph means taking ng_hci's single acl hook away from ng_l2cap and killing Classic (BTstack's own FreeBSD port documents exactly this). BlueZ stays out. A NimBLE spike is worth one ticket only to price the "tee node" alternative; the default is the thin host.nextbsd-kernel: LE buffer accounting ignores LE_Read_Buffer_Size; LE_Connection_Update_Complete, LE_Read_Remote_Features, LE_Enhanced_Connection_Complete are stubs; LE signalling accepts parameter-update requests but never forwards them to the controller; LE CoC (dynamic LE channels) unsupported. None block HOGP.| Layer | Present in 15.1 | Evidence |
|---|---|---|
| HCI LE command group (OGF 0x08) | All Core 4.0 LE commands + data-length + buffer-size v2 defined; kernel post-processes Command_Complete / Command_Status for them | sys/netgraph/bluetooth/include/ng_hci.h:1587-1848 (OCFs 0x01–0x1f, 0x23, 0x24, 0x60); hci/ng_hci_cmds.c:817-876 process_le_params, :877-916 process_le_status |
| LE Meta event (0x3e) | Dispatched. Handled: LE_Connection_Complete (0x01), LE_Advertising_Report (0x02), LE_Connection_Update_Complete (0x03, body is /*TBD*/). Not handled (freed): LE_Read_Remote_Features_Complete (0x04), LE_Long_Term_Key_Request (0x05), everything else. Every event is still mirrored to the raw hook before the switch (ng_hci_mtap, hci/ng_hci_main.c:719, hci/ng_hci_misc.c:63-78), so userland sees LTK requests etc. |
hci/ng_hci_evnt.c:129 (case NG_HCI_EVENT_LE), :376-620 (le_event, le_advertizing_report, le_connection_complete, le_connection_update) |
| LE connections (central) | LP_CON_REQ with link type NG_HCI_LINK_LE_PUBLIC/_RANDOM emits LE_Create_Connection; connection descriptor created on LE_Connection_Complete; peripheral-role (incoming) connection also creates a descriptor and sends LP_CON_IND up |
hci/ng_hci_ulpi.c:107-109, :486-623 (ng_hci_lp_le_con_req); hci/ng_hci_evnt.c:471-569 |
| LE address types | NG_HCI_LINK_LE_PUBLIC 0x02 / NG_HCI_LINK_LE_RANDOM 0x03; userland BDADDR_BREDR 0 / BDADDR_LE_PUBLIC 1 / BDADDR_LE_RANDOM |
include/ng_hci.h:123-124; include/ng_bluetooth.h:228-229; hci/ng_hci_main.c:102-105 |
| LE advertising cache | Advertising reports populate the neighbor cache with addrtype, AD payload (extinq_data, up to 240 B) and RSSI (stored in page_scan_mode); exported to userland via the neighbor-cache ioctl (4aa92fe2f3, 2017) |
hci/ng_hci_evnt.c:379-469; include/ng_hci.h:632-634 |
| L2CAP LE fixed channels | CIDs defined: ATT 0x04, LE-signal 0x05, SMP 0x06; ATT/SMP channels are auto-created per LE connection and opened/closed with it; NG_L2CAP_MTU_LE_MINIMAM 23 |
include/ng_l2cap.h:75-87, :359-361 (IDTYPE_ATT/LE/SMP); l2cap/ng_l2cap_misc.c:343-345; l2cap/ng_l2cap_ulpi.c:131-136,196-199,245-246,872-875,1006-1020 |
| L2CAP LE signalling (CID 5) | Parses Connection_Parameter_Update_Request/Response; always replies ACCEPT but never issues LE_Connection_Update to the controller (/*TBD.*/) |
l2cap/ng_l2cap_evnt.c:134-136,278-372 (send_l2cap_param_urs(..., ACCEPT) at :363); l2cap/ng_l2cap_cmds.c:224-226 |
| L2CAP LE CoC (dynamic LE channels, PSM-based) | Not supported: connect() with LE addr type and l2cap_cid == 0 returns EINVAL (// Not supported yet) |
socket/ng_btsocket_l2cap.c:2183-2192 |
| Socket API | struct sockaddr_l2cap gained l2cap_cid and l2cap_bdaddr_type (with a compat struct); connect()/bind() to {bdaddr, type=LE_*, cid=ATT_CID or SMP_CID} gives a SEQPACKET socket on the fixed channel; SO_L2CAP_ENCRYPTED makes the connect wait for encryption; encryption-change events update the socket; LE socket is torn down when the HCI connection drops (b32073c458, 2016) |
include/ng_btsocket.h:225-247; socket/ng_btsocket_l2cap.c:228-231,465-483,690-720,2366-2372,2533-2537 |
| Raw HCI socket | Root sockets bypass the security filter and may send any opcode incl. all of OGF LE; per-socket packet/event filter; commands only (ACL data cannot be injected: ng_hci_raw_rcvdata drops anything but NG_HCI_CMD_PKT) but outbound and inbound ACL are mirrored to raw for sniffing |
socket/ng_btsocket_hci_raw.c:676-724,878,937; hci/ng_hci_main.c:1025-1102; hci/ng_hci_evnt.c:319, hci/ng_hci_main.c:719 |
| Classic SSP plumbing | IO_Capability_Request (0x31), Simple_Pairing_Complete (0x36) events and the IO-cap / user-confirmation reply OCFs (0x2b–0x2d, 0x34) defined; kernel passes the events through untouched to raw (commit 4ae0fa8a2f, 2025-01-27, PR 265066, FreeBSD Foundation). hcsecd does not use them — it filters only PIN_CODE_REQ, LINK_KEY_REQ, LINK_KEY_NOTIFICATION |
include/ng_hci.h:865-898,2089-2094; hci/ng_hci_evnt.c:123-124; usr.sbin/bluetooth/hcsecd/hcsecd.c:123-126 |
| hccontrol LE | 20 le_* subcommands: le_enable, scan/advertise params & data, whitelist, le_connect -a addr -t public|random, le_read_buffer_size, channel map, remote features, le_rand; generic AD-data builder adv_data.c (2c6feab253, 2026-03) |
usr.sbin/bluetooth/hccontrol/le.c:1240-1365, adv_data.c |
| Controllers | ng_ubt + Intel (iwmbtfw, AX200/AX210/AX411/BE200 as of d82bcf5681 2026-03) and Realtek (rtlbtfw, 8852CE 2e93669827 2026-03) firmware loaders; all dual-mode |
sys/netgraph/bluetooth/drivers/ubt/, usr.sbin/bluetooth/{iwmbtfw,rtlbtfw} |
Commit trail (all git log via gh api repos/freebsd/freebsd-src/commits?path=…&sha=releng/15.1): bcff2d9100 2015-04-18 hccontrol LE commands; fbc48c2bfb 2015-04-07 kernel initial LE; 3a601a2381 2015-10-27 SMP channel; 483ed39512 2015-11-07 encryption error handling; b32073c458 2016-06-07 LE socket teardown; 4aa92fe2f3 2017-04-27 AD cache to userland; 2020-04..07 hselasky/takawata hccontrol le_scan, le_connect, whitelist, advertising; 4ae0fa8a2f 2025-01-27 SSP events; 2c6feab253 2026-03-18 generic AdvData. Nothing LE-related landed on main between 2025-06 and 2026-09 beyond that (checked sys/netgraph/bluetooth and usr.sbin/bluetooth paths).
Kernel (nextbsd-kernel, all small):
LE_Read_Buffer_Size is in the "no post processing" list (ng_hci_cmds.c:824); the unit's ACL packet budget comes only from the BR/EDR Read_Buffer_Size reply (ng_hci_main.c:412 area, NG_HCI_BUFF_ACL_TOTAL). Controllers with a separate, smaller LE pool can be over-driven; controllers reporting LE buffer size 0 (shared pool) are fine. Not observed as a bug in FreeBSD-BLE use (HID traffic is tiny) — unverified risk, worth a targeted fix.LE_Connection_Update_Complete (ng_hci_evnt.c:571-578 TBD), LE_Read_Remote_Features_Complete, LE_Long_Term_Key_Request (:602-606, both //TBD fallthrough to free). Peripheral-role LTK reply therefore must come from userland via raw HCI (the event is mirrored there). LE_Enhanced_Connection_Complete (0x0a) is defined but not handled — so userland must not enable it in the LE event mask, or connections never register in the kernel.LE_Connection_Update (ng_l2cap_cmds.c:224-226 TBD). HOGP mice that ask for a faster interval get an ACK and no change — functional, higher latency. (rhaberkorn fork commit 6b146e2aae "commented-out connection update for optimized latency" is userland working around this via raw HCI.)ng_btsocket_l2cap.c:2188-2190) — not needed for HID; needed for some modern GATT-over-CoC/EATT devices only.ng_hci.h (0x20+, 0x36+ absent). Root raw sockets can still send them (the security filter only gates unprivileged sockets, ng_btsocket_hci_raw.c:684-715), but process_le_params returns EINVAL for unknown OCFs (:857-866), which just logs — unverified whether the error path drops the command-complete from the raw mirror; needs a test.Userland (base has none of this):
ng_hci.h).LE_Long_Term_Key_Request → LE_Long_Term_Key_Request_Reply (defined, ng_hci.h:1778).uhid/evdev bridge (bthidd is Classic only: bthidd/{client,server}.c hardcode BDADDR_BREDR, l2cap_cid = 0).All of that is what takawata/FreeBSD-BLE implements: le_enable (scan, connect ATT, dump attributes, ~23 KB C), lepair (SMP legacy pairing, prints EDIV/Rand/LTK, ~12 KB), lesecd (hcsecd-shaped LTK responder daemon, ~12 KB + yacc/lex config), lehid (GATT client with HOGP mouse via uinput, battery, HRP, MIDI… ~100 KB of small service drivers). README: "Kernel support was already committed to main trunk." Last upstream push 2023-05-01; rhaberkorn fork (improvements branch, 2025-01) adds HOGP heuristics, mouse events via uevent, random addresses, and hid-pair.sh. License: none in either repo (GitHub reports no license; all rights reserved by default). The FreeBSD forum thread "The current state of BT-LE?" (2024-11 → 2025-02) shows it being used against an MX Master 3S with ATT timeouts on that device — i.e. a prototype, not a product.
Complete for HID/A2DP-transport/RFCOMM/SDP: ng_l2cap, ng_btsocket_{l2cap,rfcomm,sco,hci_raw}, sdpd, bthidd (kbd + mouse → uinput/vkbd), btpand, rfcomm_pppd, hcsecd. Pairing is legacy PIN only in hcsecd; a device that insists on SSP/Secure Connections Only will not pair until a daemon answers IO_Capability_Request and User_Confirmation_Request over the raw socket — which the kernel now permits. No A2DP/AVRCP/HFP codec or audio path in base (area A).
Evaluated for: running in NextBSD userland, over what transport, coexisting with kernel Classic on the same controller.
| Stack | License | Language / size | Classic | OS abstraction needed | Transport it wants | Fit on netgraph | Verdict |
|---|---|---|---|---|---|---|---|
| Kernel + thin userland host (FreeBSD-BLE shape) | Kernel BSD-2; userland ours (or relicensed FreeBSD-BLE) | C; ~30–40 KB for ATT/GATT/SMP core + HOGP | Coexists (kernel L2CAP stays) | None — plain sockets + raw HCI | AF_BLUETOOTH L2CAP sockets on CID 4/6 + raw HCI for GAP |
Native | Recommended LE engine |
| Apache NimBLE host | Apache-2.0 | C; nimble/host/src 46 files, ~1.2 MB source; Bluetooth 5.4, LE-only ("Host runs on any board and architecture") |
No BR/EDR | NPL (porting/npl/linux: pthread mutex/sem/eventq/callout/task, ~20 KB) — trivially compiles on FreeBSD |
Raw H4 stream: HCI cmd/evt and ACL data. Linux port uses AF_BLUETOOTH HCI_CHANNEL_USER/RAW (nimble/transport/socket/src/ble_hci_socket.c:54-62,672-747) |
Bad by default: netgraph raw socket cannot inject ACL (ng_hci_raw_rcvdata rejects non-CMD). Needs a custom ng_socket on ng_hci's acl hook, which is a single hook (ng_hci_main.c:205-206) — displacing ng_l2cap and all Classic. Fix would be a new tee node routing LE conn-handles to userland (see B7). |
Spike only |
| Zephyr Bluetooth host | Apache-2.0 | C, large; LE + partial Classic (BR/EDR is experimental) | Partial | Deeply tied to Zephyr kernel (k_work, net_buf, k_fifo, Kconfig, devicetree). No standalone port; native_sim runs Zephyr-the-RTOS on POSIX, not the host as a library |
H4/H5/USB via Zephyr drivers | Would need the Zephyr kernel emulation layer — far more OS glue than NimBLE, for no gain | No |
| BTstack (BlueKitchen) | BSD-3 + clause 4 "solely for personal benefit and not for any commercial purpose"; commercial licence by quote | C; dual-mode, mature, qualified | Yes (A2DP, HFP, HID, plus LE GATT/SMP) | Own run loop; POSIX port exists | Has a FreeBSD netgraph port (port/freebsd-netgraph/, active 2025-26): creates an ng_socket on ng_hci's acl and raw hooks — README: "The OS Bluetooth functionality will be interrupted" |
Proves the full-host-on-netgraph pattern and its cost | Licence excludes it from an OS base; useful as a reference for the ng_socket transport |
| Google Bumble | Apache-2.0 | Python; LE + Classic incl. A2DP, HFP, HID, SDP, RFCOMM | Yes | asyncio; HCI over USB (pyusb), UART, VHCI | Raw H4 (owns the controller) | Same acl-hook problem; Python in a system daemon path is a non-starter | Excellent test/dev tool (virtual peers, protocol fuzzing), not an engine |
| Android Fluoride / GD | Apache-2.0 | C++/Rust, very large (AOSP packages/modules/Bluetooth), Android-only build, binder |
Yes | Enormous | Raw H4 / vendor HAL | No | No |
| NetBSD netbt | BSD-2 | C kernel; Classic only. LE: sys/netbt/hci_le.h (368 lines of defines, commit ace5c075a2 2024-03-13 "Bluetooth low energy - A beginning at least… I have not as yet completed") — no LE code in hci_event.c |
Yes | n/a | n/a | Behind FreeBSD; nothing to take | No |
| OpenBSD | — | Removed Bluetooth from the tree in 2014 | — | — | — | — | No |
| Direct-BT (jausoft) | MIT | C++; LE-centric, over Linux HCI + L2CAP sockets + SMP interception | Little | Linux-socket coupled; FreeBSD forum thread (2025-09 → 2026-01) discussed porting using FreeBSD-BLE as reference; no port exists | Kernel L2CAP sockets (same shape as ours!) | Conceptually compatible but C++/GPL-ish history (MIT now), heavy | No, but its design validates the thin-host-over-kernel-L2CAP approach |
| BlueZ | GPL-2 / LGPL | — | — | Linux kernel BT subsystem, D-Bus | — | Out (agreed) | No |
Coexistence rule, from the code: anything that needs to send LE ACL data must go through ng_l2cap, because ng_hci has exactly one acl hook and the raw hook is command-only. ng_l2cap already provides exactly the two LE fixed channels a GATT/SMP host needs, as sockets, with per-socket encryption state. So the thin host is not a compromise — it is the design the kernel was built for, and it coexists with Classic by construction: the same raw HCI socket, with different event filters (SO_HCI_RAW_FILTER), serves inquiry + SSP for Classic and scan + LTK for LE, and one daemon owns both.
| Device class | Radio | Evidence |
|---|---|---|
| Logitech MX Master 3S (Bluetooth Edition), MX Keys, MX Anywhere 3 | BLE only ("Connection Type: Bluetooth Low Energy"; Logitech's own "Check for Bluetooth Low Energy Support" article; Logi Bolt otherwise) | support.logi.com spec page 31605553413783; hub.sync.logitech.com "Check for Bluetooth Low Energy Support" |
| Microsoft Surface Keyboard / Designer line | BLE 4.0/4.1 (nRF51822) | microsoft.com Surface Keyboard page; teardown notes |
| Keychron K-series | Bluetooth 5.1 (BLE; nRF-class SoC) | keychron.com K2 spec |
| Apple Magic Keyboard / Magic Mouse 2 / Magic Trackpad 2 | Classic BR/EDR HID (pairs via inquiry; Linux hid-magicmouse/hid-apple over BlueZ HIDP; the recurring Linux fix is UserspaceHID=false) |
Linux Mint forum t=471463 / t=387452; kernel hid-magicmouse |
| Headsets, speakers, AirPods | Classic A2DP/HFP/AVRCP; LE Audio (LC3/Auracast) is shipping on some Android-first earbuds but "Apple does not support LE Audio as of early 2026", and every LE Audio headset still carries Classic for fallback | avantree LE Audio list; bluetooth.com LE Audio; alphonsolabs 2026 checklist |
| Game controllers (DualShock 4, Xbox pre-2021) | Classic | (general knowledge, unverified here) |
| Watches, trackers, sensors, phones' Continuity | BLE / GATT | plans-site §7 |
Reading: Classic covers audio and Apple input; LE covers nearly every third-party keyboard/mouse sold since ~2018. A desktop user with an MX Master and a Keychron gets nothing from a Classic-only stack; a user with AirPods and a Magic Keyboard gets everything. Neither half is optional for "a desktop", but Classic first is correct because (a) its kernel path is complete and battle-tested, (b) bthidd exists, (c) it is where pairing UX (PIN/SSP) and the bond store get designed once, and (d) LE's userland is the same daemon plus one more engine module, not a new architecture.
Classic-first on netgraph, LE second on the same daemon via a thin userland ATT/GATT/SMP host over the kernel's LE fixed-channel sockets. Concretely:
bluetoothd owns the raw HCI socket per adapter (root; privileged filter bypass). It absorbs hcsecd (three events → link-key file) and adds SSP (IO-cap reply "DisplayYesNo"/"KeyboardOnly"/"NoInputNoOutput" per UI availability, numeric comparison, passkey entry) — the kernel now forwards those events. This is engine work, not a wrapper: hcsecd is 500 lines and has no SSP; supervising it buys nothing (unlike wpa_supplicant, which is 300 k lines).sdpd and bthidd are supervised as launchd jobs, wpa_supplicant-style, not absorbed; bthidd.conf is generated by bluetoothd from the bond store (area H).bluetoothd (or a libbtle it links): GAP over raw HCI (scan/adv-report parsing, LE_Create_Connection — or let the kernel do it via socket connect() with an LE address type, which is simpler), ATT/GATT client over the CID-4 socket, SMP over the CID-6 socket, LTK responder for LE_Long_Term_Key_Request on raw HCI, LE_Start_Encryption for central role. HOGP → uhid/evdev bridge lives in area H but consumes this engine's GATT API.ng_btle_tee node that would let NimBLE take LE ACL — only if the thin host stalls on GATT breadth (EATT, CoC, LE Audio).{addr, addr_type, kind: BREDR|LE, link_key | {ltk, ediv, rand, irk, csrk, key_size, authenticated, sc}, name, class/appearance, hid: bool} in SCPreferences (WLAN precedent; keychain later).bluetoothd should code againstModelled on ul/src/WLAN/supplicant.h (opaque engine object, fd for the event loop, pump-events returning "state changed", caller-owned caches, no callbacks across threads). One header, two implementations sharing the adapter/raw-HCI plumbing:
/* engine.h — what bluetoothd's policy layer sees. Transport-agnostic. */
enum bt_addr_type { BT_ADDR_BREDR, BT_ADDR_LE_PUBLIC, BT_ADDR_LE_RANDOM };
struct bt_addr { uint8_t b[6]; enum bt_addr_type type; }; /* identity, not connection */
struct bt_found { struct bt_addr addr; char name[249]; int rssi;
uint32_t cod; /* Classic class-of-device */
uint16_t appearance; /* LE GAP appearance */
uint8_t ad[240]; uint8_t ad_len; /* raw AD/EIR */
bool bredr, le; }; /* which engines saw it */
enum bt_pair_io { BT_IO_NONE, BT_IO_DISPLAY_YESNO, BT_IO_KEYBOARD, BT_IO_DISPLAY_ONLY };
enum bt_pair_ask { BT_ASK_PIN, BT_ASK_PASSKEY, BT_ASK_CONFIRM, BT_ASK_DISPLAY_PASSKEY };
struct bt_pair_prompt { enum bt_pair_ask kind; uint32_t passkey; }; /* surfaced to CLI/UI */
struct bt_bond { struct bt_addr addr; enum { BOND_BREDR, BOND_LE } kind;
uint8_t link_key[16]; /* Classic */
uint8_t ltk[16], irk[16], csrk[16]; uint16_t ediv; uint64_t rand;
uint8_t key_size; bool authenticated, secure_conn; /* LE */ };
struct bt_engine_ops {
const char *name; /* "classic" | "le" */
int (*attach)(struct bt_adapter *, struct bt_engine **);
void (*detach)(struct bt_engine *);
int (*event_fd)(const struct bt_engine *); /* raw HCI / L2CAP fds for kqueue */
bool (*pump)(struct bt_engine *); /* drain events; true if state changed */
int (*discover)(struct bt_engine *, bool on); /* inquiry+name / LE scan */
int (*found_count)(const struct bt_engine *);
bool (*found_get)(const struct bt_engine *, int i, struct bt_found *);
int (*pair)(struct bt_engine *, const struct bt_addr *, enum bt_pair_io);
int (*pair_reply)(struct bt_engine *, const struct bt_addr *, const char *pin_or_passkey, bool yes);
int (*connect)(struct bt_engine *, const struct bt_addr *);
int (*disconnect)(struct bt_engine *, const struct bt_addr *);
int (*bond_export)(struct bt_engine *, const struct bt_addr *, struct bt_bond *);
int (*bond_import)(struct bt_engine *, const struct bt_bond *); /* at boot, from SCPreferences */
int (*profiles)(struct bt_engine *, const struct bt_addr *, uint16_t *uuids, int max); /* SDP / GATT primary services */
};
/* Events the policy layer consumes (queue drained by pump()): */
enum bt_ev { BT_EV_FOUND, BT_EV_PAIR_PROMPT, BT_EV_PAIRED, BT_EV_PAIR_FAILED,
BT_EV_CONNECTED, BT_EV_DISCONNECTED, BT_EV_BOND_UPDATED };
Rules that keep the LE engine a drop-in: device identity is bt_addr with type everywhere (the CLI shows AA:BB:..:FF/le-random); the bond store is keyed on it; bt_found carries raw AD/EIR so the UI can grow icons/appearance without an interface change; pairing prompts are one shape for PIN, SSP numeric comparison and LE passkey; the dynamic-store publication State:/Bluetooth/Device/<addr> uses the same dictionary for both. The Classic engine ships first with profiles() = SDP; the LE engine adds gatt_* calls in a sub-interface (struct bt_le_ops obtained via bt_engine_le(engine)) that area H's HOGP bridge uses — the policy layer never sees GATT.
SO_HCI_RAW_FILTER mask is not even needed — one socket with a union filter and a dispatcher keyed on event code).Write_LE_Host_Supported(1,0), LE_Set_Event_Mask (without bit 0x0a), Set_Event_Mask | LE_META (what hccontrol le_enable does, le.c:334-361).bt_found list; Classic inquiry and LE scan can run back-to-back on a dual-mode controller.bond_import at boot means "keyboard works at the login window" for both.bthidd (Classic HID) and the HOGP bridge both read bonds from bluetoothd; area H decides whether HOGP lands inside bthidd (it has the uinput glue) or a sibling.takawata/FreeBSD-BLE nor rhaberkorn/FreeBSD-BLE has a licence file. Ask Takanori Watanabe (FreeBSD committer takawata@) for BSD-2 — the kernel half is already BSD in base, and Hans Petter Selasky contributed PRs to the repo. Otherwise clean-room from the spec; the ATT/GATT/SMP core is ~2–3 kLOC.hcsecd SSP patch that never landed? bugs.freebsd.org is behind an Anubis wall for automated fetch — unverified. If a userland SSP patch exists it shortcuts B4.hccontrol le_read_buffer_size on real hardware; if the LE pool is separate and smaller than the ACL pool, B5's fix is mandatory before HOGP notifications at 125 Hz.iwmbtfw at boot — NextBSD has no devd; how is the firmware loader triggered? area K), Realtek 8852, and a CSR 8510 dongle as the reference; Apple Silicon/Broadcom is out.LE_Set_Random_Address for our own privacy; MVP uses identity addresses only (rhaberkorn -r shows random works at the socket level).CBCentralManager face is a later epic on the same engine.freebsd-bluetooth-management.html frames LE as a kernel project ("extend Netgraph… large kernel+userland effort"). If tickets are cut from that page, weeks go into re-implementing what ng_l2cap already does. Fix the page (B8).hcsecd-style PIN pairing. SSP in bluetoothd is Ship-track, not Harden.Read_By_Group_Type responses, notifications before CCCD write). Budget a hardware-in-the-loop soak.le_connection_update TBD, error paths). NextBSD bakes it into the kernel, so a panic in ng_l2cap's LE paths (note the panic() default in ng_l2cap_con_wakeup, ng_l2cap_cmds.c:228-232) is a boot-visible failure. Fuzz the LE signalling path with Bumble before enabling by default.gh api repos/freebsd/freebsd-src/commits.improvements branch (commits 2025-01-09..18).porting/npl/linux; nimble/transport/socket/src/ble_hci_socket.c).ace5c075a2, 2024-03-13).ul/src/WLAN/{supplicant.h,wland.h,wlan.defs} (engine-interface template); nextbsd-kernel/config/NEXTBSD (0 netgraph lines, confirming #412).Deep-dive notes, lightly edited. Draft ids map to issues in the index.
Scoping notes for nextbsd/nextbsd#473 (E16 Bluetooth), seed nextbsd/nextbsd#412. Read-only research; no issues filed. Written 2026-09-20 against local clones of the NextBSD repos and FreeBSD releng/15.1.
Agreed and not relitigated: /usr/sbin/bluetoothd, job org.nextbsd.bluetoothd, Mach service org.nextbsd.bluetooth, CLI /usr/sbin/bluetooth; C + CoreFoundation below the Mach line; MIG; pure-C MIG client CLI; SCPreferences persistence; SCDynamicStore publishing; supervise stock daemons rather than modify them.
ul/src/WLAN/, 4119 lines total) with one CF translation unit, one Mach-service thread, one flat MIG .defs, one plist, one build block, and a hand-linked CLI. Copying its file layout one-for-one gives bluetoothd its skeleton for free (§1).hcsecd; do not supervise it. Evidence: hcsecd handles exactly three HCI events and logs everything else as an error (hcsecd.c:124-127, :190-193); it only stores a new link key for a device that is already declared in /etc/bluetooth/hcsecd.conf (hcsecd.c:392-396 calls get_key(.., exact_match=1)), so a device paired at runtime never gets its key saved unless something rewrites the config and SIGHUPs it first; it writes keys to disk only on SIGHUP or exit (hcsecd.c:202, :423); it answers PIN requests from a static file and has no way to ask a user; and it has no Secure Simple Pairing at all. The whole daemon is 448 lines and its reply code (~120 lines) is trivially reimplemented over the same raw HCI socket. Two processes answering Link_Key_Request on the same adapter would race, so "supervise hcsecd" is not even a safe option (§3).ng_hci_main.c:719 → ng_hci_misc.c:63-78), the per-socket event filter is a 64-bit bitmap (ng_btsocket.h:71-74) so the SSP events 0x31/0x33/0x36 pass, and ng_hci.h already defines IO_Capability_Request, User_Confirmation_Request, Simple_Pairing_Complete, the reply opcodes (0x2b/0x2c/0x2d/0x34) and Write_Simple_Pairing_Mode (0x0056). But hccontrol has no SSP command, the kernel never sets the controller event mask, and User_Passkey_Request (0x34) / IO_Capability_Response (0x32) are not defined. bluetoothd must send Set_Event_Mask and Write_Simple_Pairing_Mode itself. First real exercise needs hardware (§3.3).ubt0 → ng_hci → ng_l2cap → btsock_*) is built by /etc/rc.d/bluetooth (libexec_rc_rc.d_bluetooth, bluetooth_setup_stack), triggered by devd. NextBSD has neither rc.d nor devd (nextbsd/README.md:31). So bt_devenum() returns nothing until bluetoothd builds the graph itself with libnetgraph (§4).CFUserNotification; the only user-facing mechanism is notify_post (hostnamed) and the CLI. So pairing confirmation is pull, not push: bluetoothd parks the pairing in State:/Bluetooth/Pairing/<addr> + notify_post, and the CLI (bluetooth pair, bluetooth confirm) is the agent. Gershwin's pane drives the same two verbs via NSTask (§5.3).0600, behind one function (bt_keys_save()) so the keychain migration later touches one file and one function. This is WLAN's Q2 decision (airport.h:40-48) with a one-file improvement (§6).wedge-check.sh:153,156) and has no boot-test gate at all. bluetoothd ships with markers (BT-RPC-OK, BT-NOSTACK/BT-NOADAPTER, BT-CLI-OK), a bluetooth status probe in wedge-check.sh, and is automatically covered by daemon-state.sh (§8).| File | Lines | Role | Bluetooth sibling |
|---|---|---|---|
ul/src/WLAN/wland.c |
655 | main loop, radio table, spawns/reaps wpa_supplicant, adopt-on-timer | bluetoothd.c |
ul/src/WLAN/vap.c/.h |
249/88 | PHY enumeration (net.wlan.devices sysctl), VAP clone/adopt |
adapter.c/.h (ubt detection, netgraph graph, HCI init) |
ul/src/WLAN/wpa_ctrl.c/.h, supplicant.c/.h |
263/82, 477/106 | engine driver + scan cache | hci.c/.h (raw HCI socket, event loop, inquiry cache), security.c/.h (keys, pairing) |
ul/src/WLAN/airport.c/.h |
471/110 | the only CF TU: SCDynamicStore publish + SCPreferences known networks | btstore.c/.h |
ul/src/WLAN/mach_service.c/.h |
136/13 | bootstrap_check_in + raw mach_msg receive loop, 500 ms timeout |
copy verbatim, rename |
ul/src/WLAN/wlan.defs, wlan_mig_types.h |
144/86 | MIG IDL, subsystem 30000, all fixed-size | bluetooth.defs, bluetooth_mig_types.h, subsystem 31000 |
ul/src/WLAN/wland_mig.c |
260 | routine bodies, every one takes wland_lock() |
bluetoothd_mig.c |
ul/src/WLAN/wlan.c |
385 | the CLI, pure C, bootstrap_look_up then RPCs |
bluetooth.c |
ul/src/WLAN/wland.8, wlan.8 |
214/237 | NextBSD-original man pages | bluetoothd.8, bluetooth.8 |
ul/src/WLAN/Makefile |
91 | bsd.prog.mk, MIGOUT=, link line |
copy |
ul/overlay/System/Library/LaunchDaemons/org.nextbsd.wland.plist |
RunAtLoad+KeepAlive, MachServices, stderr to /var/log/wland.stderr |
org.nextbsd.bluetoothd.plist |
|
ul/build-userland.sh:740-778 |
host mig over wlan.defs, make ... MIGOUT=, then hand-link the CLI with $CROSS_CC |
copy block after the WLAN one |
wlan.defs:10-16, ipconfig.defs:19-22. Every routine argument is a scalar or fixed-size array; lists are count() + entry(i) (wland_mig.c:110-119).CONFIG_DATA_MAX is 8192 bytes (ul/src/configd/config_types.h:74); airport.h:27-30. Scan results go over MIG only.org.nextbsd.wland.plist comment, wland.c:507-512). configd is opened lazily and NULL is tolerated (wland.c:376-384, airport.c:91-101).wland.c:41-55; wpa_supplicant is spawned by wland (not launchd) because its interface does not exist until wland creates it, with an empty config so wland is the sole author (wland.c:208-247).wland.c:499-505).adopt_phys() is re-run every 2 s (wland.c:93-99, :421-470), and stops only when the subsystem is provably absent from the kernel (VAP_NO_NET80211, vap.h:35-43, wland.c:526, :620-625)./var/run may not exist yet at boot (wland.c:284-296, :308-317; userland#49).WLAN-IF-OK, WLAN-AIRPORT-OK, WLAN-RPC-OK: wland.c:366, airport.c:207, mach_service.c:76), because CI reads /var/log/<daemon>.stderr (run.sh:869-873).airport.c:1-13), the Makefile carries WARNS=0, -fblocks, and the exact link line -llaunch -lsystem_kernel -lSystemConfiguration -lCoreFoundation -lsystem_dispatch -l:libsystem_blocks.so (Makefile:57-78).-llaunch -lsystem_kernel (build-userland.sh:763-776) — no CF, no SC — and that is the bridge for Gershwin (wlan.c:24-40).airport.c:319-324).wedge-check.sh:153 lists org.nextbsd.wland no client tool and :156 puts it in UNPROBED; nextbsd/tests/boot-test.sh contains no WLAN expectation at all (grep). The wlan CLI exists (build-userland.sh:763) but nothing runs it. bluetoothd's plan must not repeat this.notify_post. wland relies on SCDynamicStore change notifications only. hostnamed is the one daemon that posts a notify name (hostnamed.c:20, hostnamed/Makefile:82).Sources under a local releng/15.1 fetch.
| Daemon | What it owns | How it is driven | Verdict for bluetoothd |
|---|---|---|---|
hcsecd(8) — usr.sbin_bluetooth_hcsecd_hcsecd.c (448 lines) |
Answers PIN_Code_Request, Link_Key_Request; stores Link_Key_Notification. Config /etc/bluetooth/hcsecd.conf (bdaddr → name/pin/key); keys cached to /var/db/hcsecd.keys (hcsecd.h, parser.y:300,360-390). |
Raw HCI socket, filter on 3 events (hcsecd.c:120-131). Static PIN from file; no SSP; keys saved only for pre-declared devices (:392-396), and flushed only on SIGHUP/exit (:202, :423). |
Replace. bluetoothd opens the same raw socket and answers the same events, plus SSP. See §3. |
sdpd(8) — sdpd_main.c |
SDP server (what services we advertise: HID host, PAN, …). Control socket /var/run/sdp (libsdp/sdp.h:550), -d no-detach, -u/-g drop privs. |
Stateless; clients (bthidd, btpand) register records via the control socket. |
Supervise (spawn, reap, restart) exactly as wpa_supplicant. Needed before any profile daemon registers. |
bthidd(8) — bthidd.c |
HID host: connects to paired HID devices listed in /etc/bluetooth/bthidd.conf, caches descriptors in /var/db/bthidd.hids (bthid_config.h:38-39). Options -a -c -d -H -p -t -u (bthidd.c:79). |
Config-file driven; bthidcontrol writes the config from SDP queries. |
Supervise; H area owns its insides. bluetoothd's boundary: it generates bthidd's config from PairedDevices and restarts bthidd. Whether bthidd reloads on SIGHUP is unverified. |
btpand(8) |
PAN/NAP (Bluetooth networking). | Per-connection process. | Out of scope for E16. |
/etc/rc.d/bluetooth — libexec_rc_rc.d_bluetooth |
Builds the netgraph graph and initialises the controller: ngctl mkpeer ubtN: hci hook drv, name, mkpeer hci: l2cap acl hci, connect to btsock_hci_raw/btsock_l2c_raw/btsock_l2c, then hccontrol reset (×3), read_bd_addr, read_local_supported_features, read_buffer_size, write_scan_enable, write_class_of_device, write_authentication_enable, write_encryption_mode, write_node_role_switch, change_local_name, initialize (bluetooth_setup_stack). KEYWORD: nostart — devd starts it per device. |
rc.conf + devd. | Absorb into bluetoothd (it is the VAP-clone analogue). |
The devd hook that starts rc.d/bluetooth was not found in sbin/devd/devd.conf on releng/15.1 (grep for ubt, rc.d, quietstart returned nothing) — location unverified; irrelevant on NextBSD, which has no devd at all (nextbsd/README.md:31: device events are the in-kernel IORegistry + libIOKit).
ng_ubt.c does not create the hci/l2cap nodes itself (no mkpeer/NG_HCI_NODE_TYPE use; ng_ubt.c:2056 only MODULE_DEPENDs on ng_hci). Without the rc script, a plugged-in dongle is a bare ubt0: netgraph node and bt_devenum() (lib_libbluetooth_hci.c:691-720, ioctl SIOC_HCI_RAW_NODE_LIST_NAMES) lists nothing.
Whether NextBSD's base image actually ships libbluetooth, libsdp, libnetgraph, hccontrol, sdpd, bthidd (FreeBSD's MK_BLUETOOTH/MK_NETGRAPH default to yes; no WITHOUT_ knob was found in nextbsd/build.sh or ul/build-userland.sh) is unverified — nextbsd-freebsd-compat's src.conf is not in the scratch clones. Acceptance for D1 includes checking it.
Gershwin (ObjC, no Mach)
Bluetooth.prefPane ── NSTask ──▶ /usr/sbin/bluetooth
═══════════════════════════════ Mach line ══════════════════════════════
/usr/sbin/bluetooth ── MIG org.nextbsd.bluetooth ──▶
│
/usr/sbin/bluetoothd (C + CF, job org.nextbsd.bluetoothd)
┌─────────────┬──────────────┬──────────────┬─────────────┐
│ adapter.c │ hci.c │ security.c │ btstore.c │
│ ubt detect │ raw HCI sock │ Link_Key_Req │ SCPrefs │
│ libnetgraph │ inquiry │ PIN / SSP │ SCDynStore │
│ graph+init │ conn/disconn │ pairing FSM │ notify_post │
└──────┬──────┴──────┬───────┴──────┬───────┴──────┬──────┘
│ │ │ │
/dev/ioregistry (libIOKit watch) PF_BLUETOOTH/SOCK_RAW/HCI /Local/Library/Preferences/
or 2 s poll of bt_devenum() (sockaddr_hci "ubt0hci") SystemConfiguration/
│ │ org.nextbsd.bluetoothd.plist
ngctl-equivalent: ubt0: ─drv─ ubt0hci ─acl─ ubt0l2cap ─┬─ btsock_l2c org.nextbsd.bluetoothd.keys.plist (0600)
└─raw─ btsock_hci_raw └─ btsock_l2c_raw
│
children (spawned by bluetoothd, stock, unpatched):
/usr/sbin/sdpd -d (after first adapter is up)
/usr/sbin/bthidd -d -c <generated> (H area: when a paired HID device exists)
| Requirement | hcsecd today | Citation |
|---|---|---|
| Store the key of a device paired for the first time at runtime | Only if the bdaddr is already in hcsecd.conf; otherwise "Could not find entry" and the key is dropped |
hcsecd.c:381-396 (get_key(&ep->bdaddr, 1), exact match) |
| Survive a crash without losing keys | Keys are written only in sighup() and at exit |
hcsecd.c:197-202, :418-426 |
| Ask the user for a PIN | Static pin per entry or a default entry; negative reply otherwise |
hcsecd.c:210-235 |
| SSP: IO capability, numeric comparison, passkey | Not handled; any other event is LOG_ERR "Received unexpected HCI event" |
hcsecd.c:124-127, :190-193 |
| Coexist with another security manager | Two raw sockets both receive Link_Key_Request (kernel duplicates every event to every matching socket, ng_btsocket_hci_raw.c:500-517); both would reply |
design fact |
| Size of what would be reimplemented | send_pin_code_reply + send_link_key_reply + the event switch ≈ 120 lines |
hcsecd.c:154-379 |
Conclusion: bluetoothd's security.c is hcsecd rewritten with a key store behind SCPreferences and a pairing state machine in front. /etc/bluetooth/hcsecd.conf is never read (NextBSD never shipped one; no migration needed). hcsecd must not be started; if the base image ships it, bluetoothd's acceptance includes "no hcsecd process exists".
Same argument as wpa_supplicant (wland.c:41-55, userland#46 "wpa_supplicant stays stock"): they are base FreeBSD, they speak sockets not Mach, and a CVE is a base rebuild. They are spawned by bluetoothd rather than launchd because (a) sdpd needs the L2CAP socket layer connected, which does not exist until bluetoothd has built the graph, and (b) there is no launchd ordering to express that. Spawn with -d (no detach; sdpd_main.c:75 "c:dg:hu:", bthidd.c:79 "a:c:dH:hp:t:u") so waitpid reaps them and the main loop notices death (wland.c:584-601 pattern).
| Piece | Status | Citation |
|---|---|---|
| Raw HCI socket sees every event before kernel processing | Yes — ng_hci_mtap() dups the mbuf to the raw hook at the top of ng_hci_drv_rcvdata |
ng_hci_main.c:706-719, ng_hci_misc.c:63-78 |
| Kernel tolerates SSP events it does not model | Yes — 0x31 and 0x36 are in the "no post-processing" list; unknown events return EINVAL after the raw copy was already delivered | ng_hci_evnt.c:107-126, :204-208 |
| Per-socket event filter wide enough for 0x31..0x3b | Yes — bit_decl(event_mask, NG_HCI_EVENT_MASK_SIZE*8) = 64 bits |
ng_btsocket.h:71-74; filter test ng_btsocket_hci_raw.c:704-716 |
| Event/reply definitions | NG_HCI_EVENT_IO_CAPABILITY_REQUEST 0x31, NG_HCI_EVENT_USER_CONFIRMATION_REQUEST 0x33 (with numeric_value), NG_HCI_EVENT_SIMPLE_PAIRING_COMPLETE 0x36; replies 0x2b, 0x2c, 0x2d, 0x34; NG_HCI_OCF_WRITE_SIMPLE_PAIRING 0x0056; NG_HCI_OCF_SET_EVENT_MASK 0x0001 and the NG_HCI_EVMSK_* SSP bits |
ng_hci.h:2089-2103, :865-907, :1423-1428, :1015-1022, :275-283 |
| Missing definitions | IO_Capability_Response 0x32, User_Passkey_Request 0x34, User_Passkey_Notification 0x3b, Write_Inquiry_Mode (0x0045), Inquiry_Result_with_RSSI 0x22, Extended_Inquiry_Result 0x2f — grep of ng_hci.h finds none |
bluetoothd carries its own hci_compat.h defines; a K-area ticket should upstream them |
| Controller event mask | The kernel never sends Set_Event_Mask (no hit in ng_hci_*.c, ng_ubt.c); the spec's default mask (NG_HCI_EVENT_MASK_DEFAULT 0x1fffffffffff, ng_hci.h:1021) excludes bits 48+ i.e. all SSP events |
bluetoothd sends Set_Event_Mask with default ∪ SSP bits during adapter init |
| Root privilege | Non-root sockets are command-filtered (ng_btsocket_hci_raw.c:684-690, :841-880); bluetoothd is root, so any opcode is allowed |
|
| Any base tool that has ever done SSP | None — hccontrol has no write_simple_pairing_mode, set_event_mask, or user_confirmation command (grep of hccontrol/*.c) |
Needs hardware. The FreeBSD Handbook's pairing path is legacy PIN. |
Legacy PIN pairing (PIN_Code_Request → PIN_Code_Request_Reply) is what hcsecd proves works; bluetoothd keeps it for old devices.
adapter.c)Mirrors vap.c/adopt_phys():
socket(PF_BLUETOOTH, SOCK_RAW, BLUETOOTH_PROTO_HCI) fails with EPROTONOSUPPORT when ng_btsocket is not compiled in. That is the VAP_NO_NET80211 sentinel (vap.h:35-43): log BT-NOSTACK: ng_btsocket absent — idling, stop polling, keep serving RPC. This is exactly the state the CI kernel is in until the K ticket lands (nextbsd-kernel/config/NEXTBSD has no netgraph/ng_* line; grep).ubt via libnetgraph (NgMkSockNode, NgSendMsg(NGM_GENERIC_COOKIE, NGM_LISTNODES); netgraph.h:48-50). Simple, matches PHY_RESCAN_INTERVAL_SEC (wland.c:99). Recommended for v0.
- Subscribe to /dev/ioregistry with criteria name="ubt" or classname="ubt" (ioregistry.h:54-63; nodes carry device_get_name/devclass_get_name, iokit_registry.c:223-227) via IOServiceAddMatchingNotification, as diskarbitrationd does (da_iokit_subscribe.c:4-20, diskarbitrationd.c:107-114). Event-driven; adds -lIOKit. Do it in a follow-up once D2's poll is proven; DA's file is the template.bluetooth_setup_stack in C): mkpeer ubtN: hci hook drv, name ubtN:hook ubtNhci, mkpeer ubtNhci: l2cap acl hci, name … ubtNl2cap, connect ubtNhci: btsock_hci_raw: raw ubtNraw, connect ubtNl2cap: btsock_l2c_raw: ctl ubtNctl, connect ubtNl2cap: btsock_l2c: l2c ubtNl2c. Idempotent: on restart, adopt an existing ubtNhci (the vap_find_existing() rule, vap.h:79-86).ubtNhci (bt_devopen(), lib_libbluetooth_hci.c:54-76; bt_devreq() for command/complete pairs): Reset (retry ×3 like rc.d), Read_BD_ADDR, Read_Local_Supported_Features, Read_Buffer_Size, Write_Class_Of_Device (desktop 0x000104), Change_Local_Name (ComputerName from Setup:/System if present, else hostname), Write_Scan_Enable (page scan on, inquiry scan off unless discoverable), Set_Event_Mask (default ∪ SSP), Write_Simple_Pairing_Mode 1 if Read_Local_Supported_Features reports NG_HCI_LMP_SECURE_SIMPLE_PAIRING (ng_hci.h:118), then NGM_HCI_NODE_INIT (what hccontrol initialize sends).State:/Bluetooth/Adapter/ubtNhci (§7) and log BT-ADAPTER-OK: ubt0hci 00:11:22:33:44:55.bluetooth power off = disconnect all, Write_Scan_Enable 0, NGM_HCI_NODE_SHUTDOWN? No: keep the graph; just scan-disable and mark Powered:false in the store, refuse scan/connect RPCs with bt_status_powered_off_e. power on reverses it. Persist the choice in prefs (Adapters/<bdaddr>/Powered).ubtN: and its peers; the raw socket read returns an error. Mark the slot inactive, remove the store key, reap. (wland deliberately does not handle removal, wland.c:433-435; bluetoothd should, since USB dongles are the common case.)bluetooth.defs, subsystem 31000)Flat-only, forced by the OOL limitation (§1.2). Sketch:
subsystem bluetooth 31000;
serverprefix _;
import "bluetooth_mig_types.h";
type bt_adapter = array[32] of char ctype: BTAdapterName; /* ng node name, NG_NODESIZ */
type bt_addr = array[18] of char ctype: BTAddr; /* "aa:bb:cc:dd:ee:ff" */
type bt_name = array[249] of char ctype: BTDeviceName; /* BT names are <= 248 octets */
type bt_pin = array[17] of char ctype: BTPin; /* <= 16 ASCII digits */
type bt_status = MACH_MSG_TYPE_INTEGER_32 ctype: bt_status_t;
type bt_devstate= MACH_MSG_TYPE_INTEGER_32 ctype: bt_devstate_t;
type bt_pairmth = MACH_MSG_TYPE_INTEGER_32 ctype: bt_pair_method_t;
type bt_pairph = MACH_MSG_TYPE_INTEGER_32 ctype: bt_pair_phase_t;
/* adapters */
routine bt_adapter_count(server: mach_port_t; out count: int);
routine bt_adapter_info(server: mach_port_t; index: int;
out name: bt_adapter; out addr: bt_addr; out powered: int;
out discoverable: int; out status: bt_status);
routine bt_adapter_power(server: mach_port_t; name: bt_adapter; on: int;
out status: bt_status);
routine bt_adapter_discoverable(server: mach_port_t; name: bt_adapter; on: int;
seconds: int; out status: bt_status);
/* scan: returns when the inquiry is REQUESTED (wlan_scan rule) */
routine bt_scan(server: mach_port_t; name: bt_adapter; seconds: int;
out status: bt_status);
routine bt_scan_count(server: mach_port_t; name: bt_adapter; out count: int;
out scanning: int; out status: bt_status);
routine bt_scan_entry(server: mach_port_t; name: bt_adapter; index: int;
out addr: bt_addr; out devname: bt_name; out cod: int; out rssi: int;
out paired: int; out status: bt_status);
/* pairing */
routine bt_pair(server: mach_port_t; addr: bt_addr; pin: bt_pin;
out status: bt_status);
routine bt_pair_status(server: mach_port_t; addr: bt_addr;
out phase: bt_pairph; out method: bt_pairmth; out passkey: int;
out status: bt_status);
routine bt_pair_confirm(server: mach_port_t; addr: bt_addr; accept: int;
passkey: int; out status: bt_status);
routine bt_unpair(server: mach_port_t; addr: bt_addr; out status: bt_status);
/* connections + paired list */
routine bt_connect(server: mach_port_t; addr: bt_addr; out status: bt_status);
routine bt_disconnect(server: mach_port_t; addr: bt_addr; out status: bt_status);
routine bt_paired_count(server: mach_port_t; out count: int);
routine bt_paired_entry(server: mach_port_t; index: int; out addr: bt_addr;
out devname: bt_name; out cod: int; out state: bt_devstate;
out autoconnect: int; out status: bt_status);
routine bt_device_status(server: mach_port_t; addr: bt_addr;
out devname: bt_name; out cod: int; out state: bt_devstate;
out rssi: int; out status: bt_status);
Enums in bluetooth_mig_types.h (shape of wlan_mig_types.h:47-85):
bt_status_t: success, invalid_parameter, no_stack, no_adapter, adapter_does_not_exist, powered_off, device_unknown, not_paired, busy, index_out_of_range, pairing_rejected, pairing_timeout, internal_error, operation_not_supported.bt_devstate_t: unknown, disconnected, connecting, connected, pairing.bt_pair_method_t: none, just_works, pin, numeric_comparison, passkey_entry, passkey_display.bt_pair_phase_t: idle, connecting, awaiting_user, in_progress, done, failed.Notes:
- No callbacks. A reverse Mach port to a "UI agent" would need a Mach-speaking client; there is none above the Mach line (§5.3). Pairing is a state the client polls (bt_pair_status), with notify_post as the wake-up.
- The 8 KiB receive buffer (mach_service.c:42) is ample; the biggest reply is bt_scan_entry (18+249+scalars).
- bt_scan takes seconds because a Classic inquiry length is chosen up front (Inquiry cmd, ng_hci.h:697); the CLI passes 8 (≈10 s) and polls bt_scan_count's scanning flag instead of sleep(3) (the wlan.c:197-202 wart, called out in wlan.8 BUGS).
hci.c)Inquiry (LAP GIAC, length, unlimited responses) on the raw socket; collect Inquiry_Result (0x02) events (ng_hci.h:1871), then Inquiry_Complete. RSSI needs Write_Inquiry_Mode 1 and event 0x22, neither defined in ng_hci.h (§3.3) — carry local defines; fall back to RSSI 0 if the controller rejects the mode. Names: Remote_Name_Request per result (ng_hci.h:838; bt_devremote_name()), sequentially, cached by bdaddr so repeat scans are fast. Cache shape = struct scan_entry (supplicant.h:33-39) with bdaddr, name, cod, rssi, seen.
security.c)State machine per bdaddr:
idle ─bt_pair─▶ connecting (Create_Connection) ─▶ Authentication_Requested
├─ PIN_Code_Request → pin given? reply : phase=awaiting_user(method=pin)
├─ IO_Capability_Request → reply DisplayYesNo (0x01), no OOB, MITM required
├─ User_Confirmation_Request(numeric) → phase=awaiting_user(method=numeric_comparison, passkey)
│ bt_pair_confirm(accept) → User_Confirmation_Request_Reply / _Negative_Reply
├─ User_Passkey_Request → awaiting_user(method=passkey_entry); bt_pair_confirm(passkey)
├─ Link_Key_Notification → bt_keys_save(addr, key, type) (BEFORE Simple_Pairing_Complete)
├─ Simple_Pairing_Complete / Authentication_Complete(status) → done|failed
└─ timeout (no user answer in N s) → negative reply, failed(pairing_timeout)
Link_Key_Request at any time → reply from the key store or negative. bt_unpair = delete from store, Delete_Stored_Link_Key on the controller (ng_hci.h:1090, harmless if none), disconnect if connected, remove State:/Bluetooth/Device/<addr>, regenerate bthidd config (H).
Policy defaults (v0): pairing is only ever initiated by us; an incoming IO_Capability_Request for a device nobody asked to pair is answered with IO_Capability_Request_Negative_Reply (0x34). Just-works (peer NoInputNoOutput) still surfaces a User_Confirmation_Request; auto-accept it only for a pairing we initiated. The HCI/LMP authentication timeout for an unanswered confirmation is controller-defined (spec default ≈ 30 s) — unverified on real hardware; bluetoothd's own awaiting_user timeout is 60 s and it sends the negative reply itself.
Searched for any user-prompt path: no LaunchAgents directory in ul/overlay or nextbsd-overlays; no CFUserNotification; the only "per-user agent" mention is DA's future-iteration note (diskarbitrationd.c:26); the only notify use is hostnamed's notify_post("com.apple.system.hostname") (hostnamed.c:20), and its vendored code skipped notify_register_dispatch because it killed the daemon during bring-up (set-hostname.c:706-716) — so registration-side notify is not battle-tested; posting is.
Therefore: bluetoothd never pushes to a UI. It (1) sets State:/Bluetooth/Pairing/<addr> = {Method, Passkey, Expires}, (2) notify_post("org.nextbsd.bluetooth.pairing"), and (3) waits for bt_pair_confirm. The agent is whoever called bt_pair: the CLI prints Confirm that 123456 is shown on <name> [y/N]; Gershwin's pane calls bluetooth pair --no-prompt <addr> (exit 3 = user answer needed, prints method=numeric_comparison passkey=123456), shows its own dialog, then bluetooth confirm <addr> yes|no. That two-verb split keeps every NSTask invocation short-lived, which is the BSDBackend.m pattern the WLAN plan relies on (wlan.c:32-37).
btstore.c)Two SCPreferences application IDs, so that the secret file can move to a keychain later without touching the metadata file:
org.nextbsd.bluetoothd → /Local/Library/Preferences/SystemConfiguration/org.nextbsd.bluetoothd.plist (0644, like wland's):
PairedDevices = {
"aa:bb:cc:dd:ee:ff" = {
Name = "Magic Keyboard"; # last Remote_Name
Class = 9536; # Class_of_Device, int
Adapter = "00:11:22:33:44:55"; # local bdaddr that holds the bond
Paired = 1789000000; # unix time
LastSeen = 1789000000;
AutoConnect = true; # reconnect at boot / when it pages us
Profiles = ( "HID" ); # from SDP at pair time; H/A areas append
Trusted = true; # accept incoming ACL from it without asking
};
};
Adapters = {
"00:11:22:33:44:55" = { Powered = true; Discoverable = false; Name = "..." };
};
org.nextbsd.bluetoothd.keys → ...SystemConfiguration/org.nextbsd.bluetoothd.keys.plist:
LinkKeys = {
"aa:bb:cc:dd:ee:ff" = { Key = <16 bytes, CFData>; Type = 4; }; # Type = HCI key_type
};
Rules:
- Written immediately on Link_Key_Notification (the hcsecd failure mode, §3.1), via SCPreferencesSetValue + SCPreferencesCommitChanges (atomic temp+rename, airport.c:276-293).
- umask(077) in bluetoothd before the first commit and chmod(0600) after every commit of the keys file (a rename produces a new inode, so a one-time chmod does not survive; whether SCPreferences honours the process umask is unverified — the chmod-after-commit covers both cases).
- The plaintext decision is WLAN's Q2 repeated (airport.h:40-48; keychain plan §4.G "interim", Q1 "first consumer"). E16 becomes the second consumer arguing for the System keychain; bluetoothd's bt_keys_save/find/forget are the three functions that change, and only the .keys file migrates.
- Link keys are 16 bytes; store as CFData, never as a printable string in a log line (hcsecd logs "exists/doesn't exist" only, hcsecd.c:249-253 — keep that habit).
- BLE LTKs (B area) get a sibling dictionary LongTermKeys in the same keys file when they exist; the schema reserves the name.
Keys (all invented, like AirPort; airport.h:21-25):
| Key | Dict | When |
|---|---|---|
State:/Bluetooth/Adapter/<node> e.g. ubt0hci |
{Address, Name, Powered, Discoverable, Connectable, Scanning} |
adapter up/down, power, scan start/stop |
State:/Bluetooth/Device/<addr> |
{Name, Class, Adapter, Paired, Connected, Profiles, RSSI?} |
pair/unpair/connect/disconnect; one key per paired or connected device only (scan results never) |
State:/Bluetooth/Pairing/<addr> |
{Method, Passkey, Expires} |
created at awaiting_user, removed on done/failed |
Every dict stays far below CONFIG_DATA_MAX (config_types.h:74). No Setup:/Bluetooth in v0 — SCPreferences under the app IDs above is the durable side; a Setup: mirror can come with the pref-pane work.
Notify names (posted with notify_post, -lnotify, as hostnamed/Makefile:82):
org.nextbsd.bluetooth.adapter — adapter list/power changedorg.nextbsd.bluetooth.device — a Device/ key changedorg.nextbsd.bluetooth.pairing — user action required or pairing finishedorg.nextbsd.bluetooth.scan — scan cache changed (the CLI/pane may re-pull bt_scan_entry)org.nextbsd.bluetoothd.plist (ul/overlay/System/Library/LaunchDaemons/): Label org.nextbsd.bluetoothd, ProgramArguments /usr/sbin/bluetoothd, RunAtLoad true, KeepAlive true, MachServices {org.nextbsd.bluetooth = true}, StandardOutPath/StandardErrorPath /var/log/bluetoothd.stderr. "Start only when a controller exists" is not expressible: every plist in the overlay uses plain KeepAlive true (no PathState/OtherJobEnabled variants exist in the tree; grep), there is no device-triggered launch (no devd; IOKit notifications are a userland API, not a launch trigger), and wland's precedent is "always run, idle at 2 s polls, stop polling when the subsystem is provably absent" (wland.c:526-536). Do the same. daemon-state.sh then classifies it as resident and asserts a live PID automatically (its RunAtLoad/KeepAlive rule, daemon-state.sh:29-36).
There is no Bluetooth in qemu (#412), and today no netgraph in the kernel either. Both states must pass CI:
| Marker | Emitted by | Meaning | Gate |
|---|---|---|---|
BT-RPC-OK |
mach_service.c after bootstrap_check_in |
service registered (WLAN-RPC-OK twin, mach_service.c:76) |
boot-test.sh expects it (FAIL if absent) |
BT-NOSTACK |
adapter.c |
socket(PF_BLUETOOTH…) → EPROTONOSUPPORT; polling stopped |
informational until K lands, then must not appear |
BT-NOADAPTER |
adapter.c |
stack present, no ubt node; polling |
expected in qemu after K lands |
BT-CLI-OK |
run.sh |
timeout 15 bluetooth status exits 0 and prints no Bluetooth adapters (or stack: absent) |
FAIL otherwise |
| wedge probe | wedge-check.sh |
probe "org.nextbsd.bluetooth" "bluetooth status" bluetooth status; remove from UNPROBED |
existing WEDGE-CHECK-FAIL logic |
| daemon state | daemon-state.sh |
plist-driven, automatic | existing DAEMON-STATE gate |
Pattern to copy: the ipconfig block in run.sh:1512-1520 and its expect in boot-test.sh:986-997. Marker naming follows the <AREA>-<THING>-OK/FAIL convention (run.sh:1105-1298).
While here: wedge-check.sh:153,156 should get a wlan list probe too — the CLI exists and the reason recorded ("no client tool") is stale. Out of area; note for whoever owns E-network.
bluetooth CLI/usr/sbin/bluetooth, pure C, bootstrap_look_up("org.nextbsd.bluetooth"), links -llaunch -lsystem_kernel only (build-userland.sh:763-776 shape). Verbs and output mirror wlan(8) (wlan.c:4-8, wlan.8 COMMANDS/EXAMPLES):
bluetooth status [adapter] adapter: ubt0hci address: 00:11:.. powered: on discoverable: off
paired devices: 2 (1 connected) [or: no Bluetooth adapters / stack: absent]
bluetooth list ADDRESS NAME CLASS STATE AUTO
aa:bb:cc:dd:ee:ff Magic Keyboard keyboard connected yes
bluetooth scan [-t seconds] [adapter]
ADDRESS NAME CLASS SIGNAL PAIRED
aa:bb:.. Magic Keyboard keyboard *** -52 yes
bluetooth pair <addr> [pin] [--no-prompt]
interactive: prints passkey prompt, reads y/N or the passkey, calls confirm
--no-prompt: exit 0 done | 3 user answer needed (prints method=… passkey=…) | 1 failed
bluetooth confirm <addr> yes|no|<passkey>
bluetooth unpair <addr>
bluetooth connect <addr> returns when REQUESTED; watch with `bluetooth list`
bluetooth disconnect <addr>
bluetooth power on|off [adapter]
bluetooth discoverable on|off [-t seconds] [adapter]
Exit codes: 0 ok, 1 error/RPC failure, 2 usage, 3 user answer needed (--no-prompt only). cannot reach org.nextbsd.bluetooth — is bluetoothd running? on lookup failure (wlan.c:360-366). Signal bars as wlan.c:110-122. Class-of-device rendered from the major/minor class bits (keyboard, mouse, audio, phone, computer, …). Man page bluetooth.8 with FILES (/var/log/bluetoothd.stderr, both prefs paths, "link keys are stored in plaintext"), DIAGNOSTICS (no Bluetooth adapters → check ngctl list for ubt; stack: absent → kernel lacks ng_btsocket), and BUGS.
Sibling of nextbsd#396. The contract the pane builds on, nothing more:
bluetooth CLI verbs above, invoked via NSTask (the BSDBackend.m:262-268 path cited by wlan.c:32-37), including the --no-prompt/confirm pair for the passkey dialog;State:/Bluetooth/... schema (§7) for anything that later gains an SC reader;Everything ObjC — a BluetoothBackend protocol, the pane, an IOBluetooth-shaped API — lives above the Mach line in Gershwin and is not scoped here.
libbluetooth/libnetgraph/sdpd/bthidd/hccontrol? Default FreeBSD MK_BLUETOOTH/MK_NETGRAPH = yes and no WITHOUT_ knob was found, but nextbsd-freebsd-compat was not inspected. D1 checks. If absent, bluetoothd vendors libbluetooth's ~700-line hci.c (BSD-2) rather than adding a build knob.bthidd.c signal handling). H area's call; D7 exposes a generic "restart child" hook either way.ubt arrival (§4.2). Poll for v0; watch as a follow-up.Read_Local_Supported_Features report on the dongles we will test with? Determines whether SSP or legacy PIN is the default path in practice. Needs hardware..keys file split (§6) the accepted debt for now?Set_Event_Mask bits for LE (B area) are set from the same init sequence; agree who owns adapter_init() extensions.BT-NOADAPTER is untestable in CI. Same as WLAN (userland#46 "Testing"). Mitigation: D10 hardware checklist ticket with needs-hardware, and the hcsecd-proven legacy PIN path as a fallback if SSP misbehaves.ng_hci.hcsecd, both answer Link_Key_Request. bluetoothd logs BT-SEC-CONFLICT if a hcsecd process exists at start.recvfrom error as "adapter gone" and rebuild from scratch.bluetoothd.8.device netgraph, ng_bluetooth, ng_hci, ng_l2cap, ng_btsocket, ng_ubt compiled in (#412 phase 0); add the missing ng_hci.h defines (§3.3) so bluetoothd's hci_compat.h can be deleted.PairedDevices[*].Profiles ∋ "HID", SDP query at pair time (bthidcontrol's sdp.c logic), SIGHUP/restart semantics.Pairing/ state and .keys file (LongTermKeys).sdpd; nothing else from D.Deep-dive notes, lightly edited. Draft ids map to issues in the index.
Scope: how a paired Bluetooth keyboard/mouse/trackpad turns into keystrokes and pointer motion on a NextBSD desktop (Gershwin on XLibre), what the kernel and image need, how bluetoothd drives FreeBSD's bthidd, pre-login behaviour, Apple/Logitech specifics, HOGP scope, and what CI can test without a radio.
Sources are FreeBSD releng/15.1 (usr.sbin/bluetooth/bthidd, sys/dev/evdev, sys/dev/vkbd, sys/dev/kbdmux, sys/dev/vt), the NextBSD clones (nextbsd, nextbsd-kernel, nextbsd-userland), gershwin-desktop/gershwin-on-nextbsd, gershwin-desktop/gershwin-system, FreeBSD ports (x11-servers/xlibre-server, x11/xlibre-minimal) and FreeBSDDesktop/libudev-devd. Paths below are relative to those trees; file:line numbers are from the copies fetched on 2026-09-20. Anything not verified against source is marked (unverified).
vkbd(4) (bthidd/session.c:108-114), mice always go to /dev/consolectl → sysmouse (bthidd/hid.c:541-566), and with -u additionally to /dev/uinput per-device evdev nodes (bthidd/btuinput.c:339-413). The uinput writes are gated by kern.evdev.rcpt_mask bits (btuinput.c:474-484, 511-519).EVDEV_SUPPORT, evdev, uinput, kbdmux are in GENERIC on amd64 and arm64 (sys/amd64/conf/GENERIC:199,381-383, sys/arm64/conf/std.dev:49,72-74) and NEXTBSD includes GENERIC (config/NEXTBSD:1). vkbd is a module only (sys/conf/files:3501 optional vkbd; not in GENERIC) and NextBSD ships no .ko tree (ci/assemble-image.sh:168) → device vkbd must be baked or no BT keyboard can ever attach. Ticket H1.libudev-devd, whose monitor connects to devd's /var/run/devd.pipe (libudev-devd/udev-monitor.c:50,188-213). NextBSD has no devd (nextbsd/README.md:31, PORTING.md:487-489), so X sees only what exists when it starts. Gershwin's own LoginWindow.sh:91-93 says it: "X enumerates input exactly once at startup and XLibre has no hotplug backend on FreeBSD". A uinput node created when a BT mouse connects is therefore invisible to a running X.kern.evdev.rcpt_mask bit 1 (EVDEV_RCPT_KBDMUX) makes kbdmux's evdev node "System keyboard multiplexer" (kbdmux.c:455-473, gated kbdmux.c:707) carry every keyboard, including every vkbd bthidd creates (auto-attached by kbd.c:197-231). That node exists from boot, so libinput opens it at X start and BT keyboards work whenever they connect, pre-login included. XLibre's pkg-message.in recommends kern.evdev.rcpt_mask=6 for exactly this class of problem.consolectl → sysmouse → "System mouse" evdev node, vt_consolectl.c:55-59, vt_sysmouse.c:97-118,122-125, needs bit 0) with duplicate-event risk on psm(4) laptops; the clean per-device uinput route needs the input-hotplug interface that belongs to the other epic. Ticket H4 (decision), H5 (sequenced).bthidd cannot reload: SIGHUP is a shutdown (bthidd.c:138-140,249-253, loop at bthidd.c:167). Adding a paired device means restarting bthidd, which drops every connected BT HID session. Devices not in bthidd.conf are rejected on connect (server.c:244-250). Tickets H2 (bluetoothd drives it), H3 (make it reload).hid.c:73,434-512); Magic Mouse 2 and Magic Trackpad 1/2 are not (no digitizer/multitouch handling anywhere in bthidd). Logitech's current line is BLE-only → HOGP, blocked on the engine decision. Tickets H6, H7./dev/input/eventN → X) is testable in qemu with synthetic devices, no radio needed; only the L2CAP side is not. Ticket H8.usr.sbin/bluetooth/bthidd/Makefile:8-9: SRCS= bthidd.c btuinput.c client.c hid.c kbd.c lexer.l parser.y server.c session.c, LIBADD= bluetooth usbhid. uinput support is compiled in unconditionally in 15.1 (it is a runtime option, not a build option).bthidd.c:114-115: -u sets uinput = 1; usage text bthidd.c:272 "enable evdev protocol support". bthidd.8: "-u Enable support for input event device protocol. Requires evdev and uinput drivers to be loaded ... or compiled into the kernel."libexec/rc/rc.d/bthidd:18-38: stock rc script kldloads uinput (when check_kern_features evdev_support), kbdmux, vkbd, ng_btsocket and passes -u when evdev is present. On NextBSD none of those load_klds can work (no .ko tree), so every one of them must be static.session.c:106-121: when the device's descriptor marks it a keyboard, session_run() opens /dev/vkbdctl unconditionally and fails the session if it cannot ("Could not open /dev/vkbdctl"). This is independent of -u.vkbd.c:674: each open of /dev/vkbdctl clones a vkbd unit and calls kbd_register().sys/dev/kbd/kbd.c:197-231: kbd_register() looks up kbdmux and, if present, issues KBADDKBD so the new keyboard is multiplexed automatically. That is why bthidd.8 CAVEATS say kbdmux "must be used".kbd.c:358-372,401-408 (bthidd): every key change is written to the vkbd fd and to the uinput fd (uinput_kbd_write), the latter being a no-op unless -u.btuinput.c:378-413: uinput keyboard "Bluetooth Keyboard": EV_KEY/EV_LED/EV_SYN/EV_REP, LEDs, all HID usages via a keymap plus consumer keys; btuinput.c:300-333 sets UI_SET_PHYS (local bdaddr) and UI_SET_BSDUNIQ (remote bdaddr) so libinput sees a stable identity.btuinput.c:511-519: uinput_rep_key() returns early unless kern.evdev.rcpt_mask & EVDEV_RCPT_HW_KBD; the mask is re-read every 5 s (btuinput.c:422-457). If the sysctl is absent (kernel without EVDEV_SUPPORT) it assumes HW bits set.kbd.c:535-613 reads vkbd status changes and sends the HID output report; with -u also uinput_rep_leds.bthidd.8 KNOWN LIMITATIONS: no key auto-repeat and no double-click handling in the text console; both "work under X just fine". Comment at hid.c:515-521 confirms.server.c:76-81: server_init() opens /dev/consolectl and fails bthidd startup if it cannot.hid.c:541-566: mouse motion/buttons are sent as CONS_MOUSECTL / MOUSE_ACTION ioctls on consolectl; T-axis (tilt) becomes buttons 6/7. Then, hid.c:568-573, if -u and the device is a mouse, uinput_rep_mouse() is also called.sys/dev/vt/vt_consolectl.c:47-68: CONS_MOUSECTL → sysmouse_process_event(mi). (The EINVAL for MOUSE_ACTION at vt_core.c:2856-2884 is the per-tty ioctl, not the consolectl cdev, so it does not affect bthidd.)vt_sysmouse.c:209-262: sysmouse_process_event() accumulates into the /dev/sysmouse MouseSystems FIFO and, #ifdef EVDEV_SUPPORT, sysmouse_evdev_store() pushes to an evdev device named "System mouse" (vt_sysmouse.c:97-118) only if evdev_rcpt_mask & EVDEV_RCPT_SYSMOUSE (vt_sysmouse.c:122-125).btuinput.c:339-370: uinput mouse "Bluetooth Mouse": REL_X/Y, REL_WHEEL/HWHEEL when the descriptor has them, 8 buttons, INPUT_PROP_POINTER. btuinput.c:474-484: writes gated on EVDEV_RCPT_HW_MOUSE. Comment btuinput.c:336: "TODO: bring in more feature detection code from ums".parser.y:442-463: a device is a keyboard if its descriptor has a HUP_KEYBOARD page, mouse if it has X/Y on generic desktop, has_cons for consumer keys, has_wheel/has_hwheel.hid.c:227-430: handled usage pages are GENERIC_DESKTOP, KEYBOARD, BUTTON, CONSUMER and MICROSOFT (battery, 0xfe01). No HUP_DIGITIZER, no absolute axes, no multitouch. Anything trackpad-shaped is either ignored or (Apple) handled by vendor-specific code (see §5).sys/dev/evdev/evdev.h:48-56: bit0 SYSMOUSE, bit1 KBDMUX, bit2 HW_MOUSE, bit3 HW_KBD. evdev.c:73-82: default HW_MOUSE|HW_KBD (= 0xC) except 32-bit powerpc; sysctl kern.evdev.rcpt_mask is RWTUN (settable from loader tunables or at runtime).kbdmux.c:455-473: kbdmux creates an evdev "System keyboard multiplexer" (BUS_VIRTUAL, all known keys, LEDs, repeat); kbdmux.c:705-716: pushes keys only when bit1 is set; kbdmux.c:1136-1141,1218-1222,1350-1358: LED/repeat mirroring likewise.hms, hmt, hpen, the ones NEXTBSD bakes at config/NEXTBSD:165-174) honour HW_MOUSE at all — they are evdev-only and never feed sysmouse (touch spike, line 102: "No /dev/sysmouse from hms"). If they ignore the mask, clearing bit2 does not silence USB mice; if they honour it, clearing bit2 kills them. Must be checked before any mask change.| Needed by bthidd | Where declared | In GENERIC (amd64 / arm64)? | In config/NEXTBSD? |
|---|---|---|---|
options EVDEV_SUPPORT, device evdev |
sys/conf/files:1692-1695 |
yes / yes (GENERIC:381-382, std.dev:72-73) |
inherited via include GENERIC (NEXTBSD:1) (inferred, not seen in config -x output) |
device uinput |
files:1696 optional evdev uinput |
yes / yes (GENERIC:383, std.dev:74) |
inherited (same caveat) |
device kbdmux |
files:2332 |
yes / yes (GENERIC:199, std.dev:49) |
inherited |
device vkbd |
files:3501 optional vkbd |
no / no (module sys/modules/vkbd) |
no → H1 |
device hkbd,ukbd,usbhid,hidbus |
— | yes | yes; touch spike confirmed via config -x |
ng_btsocket (+ ng_bluetooth, ng_hci, ng_l2cap, ng_ubt) |
files:4299-4316, NOTES:813-817 |
no | no (area K) |
/dev/consolectl |
vt_consolectl.c:76-77 (vt) / syscons.c:668 (sc) |
yes (vt) | yes |
The comments in config/NEXTBSD:153-159 and the touch spike describe the exact failure class: "a driver that is a module on stock FreeBSD is absent on NextBSD unless it is compiled into the kernel". vkbd is that for BT keyboards: session_run() returns -1 at session.c:110-115 and the keyboard never attaches, with only a syslog line to show for it.
Recommendation (H1): add device vkbd next to the HID block in config/NEXTBSD with the same style of comment, and have the ticket's acceptance criteria include config -x /boot/kernel/kernel | grep -E 'vkbd|uinput|kbdmux|EVDEV_SUPPORT' on the built kernel so the inherited options are verified rather than inferred. ng_btsocket etc. are area K's ticket; H1 depends on it only for end-to-end testing, not for the build.
nextbsd/pkglist.txt contains only pkg (the XLibre set proposed in nextbsd#328 was later moved out).gershwin-desktop/gershwin-on-nextbsd: pkglist.txt = xlibre-minimal, xlibre-xf86-input-{evdev,joystick,keyboard,mouse,synaptics,vmmouse}, xlibre-xf86-video-{scfb,vesa}, mesa-dri, installed in the chroot (build.sh:193-209). build.sh:196-197: "Gershwin's LoginWindow execs /usr/local/bin/X at boot".x11/xlibre-minimal/Makefile: RUN_DEPENDS on x11-servers/xlibre-server and x11-drivers/xlibre-xf86-input-libinput → libinput is present and is the default driver.x11-servers/xlibre-server/Makefile:16-21: OPTIONS_DEFAULT=UDEV ..., UDEV_DESC= Enable udev. (For libinput & autoconfiguration/hotplug), UDEV_LIB_DEPENDS= libudev.so:devel/libudev-devd. Packages from pkg.FreeBSD.org are built with defaults, so the shipped server uses the udev config backend via libudev-devd. files/patch-config_udev.c: on kernels with the evdev_support feature, keyboards/pointers get no forced driver (so the fallback, libinput, is used); the kbdmux path is special-cased; files/20-evdev-kbd.conf sets XkbRules evdev for /dev/input/event* keyboards. x11/libinput/Makefile:16 also depends on libudev-devd.udev-utils.c:100-137 scans /dev/input/event[0-9]*, /dev/ukbd*, /dev/atkbd*, /dev/kbdmux*, /dev/ums*, /dev/psm*, /dev/sysmouse, /dev/dri/card*, ...; udev-enumerate.c:191-203 does a recursive scandir. This works on NextBSD (no devd involved).udev-monitor.c:50-51 DEVD_SOCK_PATH "/var/run/devd.pipe", reconnect every 1 s; udev-monitor.c:188-213 connects and otherwise arms a timer forever; udev-monitor.c:154-175 consumes devd notices with system=DEVFS subsystem=CDEV type=CREATE|DESTROY cdev=<name> (and system=DRM). On NextBSD nothing listens on that socket, so the monitor loops on the retry timer and X never learns about new nodes.sys/kern/kern_conf.c:530-554 notify() → devctl_notify("DEVFS", "CDEV", "CREATE", "cdev=<name>") from every make_dev (kern_conf.c:832,993), and evdev nodes are make_dev_s(... "input/event%d") (evdev/cdev.c:781). /dev/devctl currently has no NextBSD reader (ul/src/kext_tools/kextd/kextd.c:23 "Not yet ... listening on /dev/devctl"; nextbsd#225 tracks whether it is retired for IOKit notifications).gershwin-system/Library/Scripts/LoginWindow.sh:79-104 waits (on virtio) until devinfo shows (hkbd|ukbd|atkbd)[0-9] and (hms|ums)[0-9] before starting X because "X enumerates input exactly once at startup and XLibre has no hotplug backend on FreeBSD, so that keyboard is lost for the whole session (nextbsd#390/#391)". The same statement is repeated for NVIDIA at LoginWindow.sh:150-156. nextbsd-kernel#175 (USB mouse hotplug does not work) lists "the consumer: a new /dev/input/* node appearing but Gershwin/X not picking it up" as a candidate cause. Note that the LoginWindow wait would never be satisfied by a vkbd-only (BT-only keyboard) machine — it only greps for USB/AT keyboard drivers — and on bare metal (non-virtio, non-NVIDIA) there is no wait at all.Three routes, per class:
| Route | Node seen by X | Exists at X start? | Needs hotplug? | Requires rcpt_mask |
Device identity in libinput |
|---|---|---|---|---|---|
| K-A vkbd → kbdmux → "System keyboard multiplexer" evdev | one aggregate keyboard | yes (kbdmux attaches at boot) | no | bit1 set, bit3 clear (XLibre's recommended 6) |
lost (all keyboards are one device) |
K-B bthidd -u uinput "Bluetooth Keyboard" |
per device, named, UNIQ=bdaddr |
only if the keyboard connected before X | yes | bit3 set (default) | full |
K-C xf86-input-keyboard (kbd driver) on the console tty |
console keyboard | yes | no | n/a (not evdev) | lost; legacy driver, not what XLibre picks on an evdev kernel (patch-config_udev.c) |
| M-A consolectl → sysmouse → "System mouse" evdev | one aggregate pointer | yes | no | bit0 set | lost; psm(4) also feeds sysmouse → duplicates unless bit2 is cleared, which may silence hms (unverified, §1) |
M-B bthidd -u uinput "Bluetooth Mouse" |
per device | only if connected before X | yes | bit2 set (default) | full; also the only route that can ever carry trackpad MT |
M-C xf86-input-mouse on /dev/sysmouse |
legacy | yes | no | n/a | XLibre gives sysmouse no driver on evdev kernels (patch-config_udev.c) unless an InputClass forces it |
So: keyboards: yes, without restart, via K-A — and that is the configuration XLibre's own pkg-message.in tells FreeBSD users to set (kern.evdev.rcpt_mask=6). Mice/trackpads: no unless we either accept M-A's aggregate/duplicate trade-offs or the input-hotplug interface exists. Because bthidd always writes to both the aggregate sink and (with -u) the uinput sink, and the mask gates only the uinput side (btuinput.c:474-484,511-519) while also gating the aggregate's evdev emission (kbdmux.c:707, vt_sysmouse.c:125), there is no configuration in which bthidd double-delivers to X: bluetoothd can always start bthidd with -u and let the mask decide.
Whatever that epic builds, BT HID needs exactly one of:
/var/run/devd.pipe (line-oriented !system=DEVFS subsystem=CDEV type=CREATE cdev=input/event5, as udev-monitor.c:154-175 parses; exact wire format (unverified) — check sys/kern/subr_bus.c/subr_devctl.c devctl_notify formatting), fed from /dev/devctl or from hwregd/IORegistry notifications; orlibudev-devd whose monitor subscribes to IOKit-style notifications instead of devd; orrcpt_mask aggregate policy (K-A/M-A) accepted as the design, with per-device identity given up.Option 1 is a small shim and would also fix nextbsd-kernel#175's userland half and the LoginWindow.sh waits. This document does not choose; H4 asks the question.
bthid_config.h:38-39: BTHIDD_CONFFILE /etc/bluetooth/bthidd.conf, BTHIDD_HIDSFILE /var/db/bthidd.hids.bthidd.conf.sample: one device { bdaddr; name; vendor_id; product_id; version; control_psm 0x11; interrupt_psm 0x13; reconnect_initiate; normally_connectable; hid_descriptor { ... }; } stanza per device. bthid_config.h:41-63 is the parsed struct (new_device, reconnect_initiate, battery_power, normally_connectable, keyboard, mouse, has_wheel, has_hwheel, has_cons).bthidcontrol(8): Query prints a ready-to-append stanza from the device's SDP record (bthidcontrol/sdp.c:69-80 fetches ProtocolDescriptorList, AdditionalProtocolDescriptorLists, 0x0205 HIDReconnectInitiate, 0x0206 HIDDescriptorList, 0x0209 HIDBatteryPower, 0x020d HIDNormallyConnectable, plus DeviceID vendor/product/version sdp.c:58-66); Dump pretty-prints the descriptor; Known/Forget edit /var/db/bthidd.hids.bthidd -c <conf> -H <hids> -p <pid> [-u] [-t secs] [-a local] (bthidd.8).server.c:244-250 "Rejecting ... connection ... Device not configured". The stanza must exist before the device tries to connect.done (bthidd.c:249-253) and the main loop for (done = 0; !done; ) (bthidd.c:167) exits. Reload = restart = every connected BT HID device drops for the restart window. There is one bthidd per adapter (it binds L2CAP PSM 0x11/0x13 with listen, server.c:96-138), so you cannot run one per device.client.c:63-118): every -t seconds (default 10) client_rescan() looks at one device (get_next_hid_device(d) walks the list one entry per call, client.c:73), skips it if a session exists, and skips it if it is known (!new_device) and reconnect_initiate is set. Otherwise it opens an outbound control channel. new_device is cleared and the hids file rewritten when a session is first established (client.c:179, server.c:255-256). So: first connection after pairing is host-initiated (which is what makes the newly written stanza "take"); afterwards, devices that advertise HIDReconnectInitiate (all keyboards/mice in practice) are expected to connect to the host themselves, and bthidd only listens. With N configured devices the first outbound attempt for the last one happens up to N×10 s after start.Link_Key_Request that hcsecd(8) must answer from /var/db/hcsecd.keys (hcsecd.8 DESCRIPTION, hcsecd.h:39). Area D owns hcsecd/key persistence; for HID the constraint is ordering: hcsecd and bthidd must both be listening before the user presses a key.bthidcontrol -a <bdaddr> Query (either exec it or link the same sdp.c logic), append the stanza to /etc/bluetooth/bthidd.conf (or a bluetoothd-owned file passed with -c), and make sure the device is not in bthidd.hids so the first connect is host-initiated.-c -H -p -u, -a = adapter bdaddr for multi-adapter safety). Because that drops other sessions, batch config changes and log the restart; H3 removes this cost.Forget the hids entry, restart bthidd; also remove the hcsecd key (area D).bthidd.conf from it at boot, so the conf file is derived state, not a second source of truth.State:/Bluetooth/Device/<addr> with Connected/Profile=HID by watching bthidd's syslog lines or, better, by polling sessions — bthidd has no control socket. (gap) The only observable connection state is /var/db/bthidd.hids writes and syslog. H3 should add a status/dump facility if we are patching bthidd anyway.org.nextbsd.bluetoothd RunAtLoad, no dependency on the login window; bthidd/hcsecd are children. The Gershwin loginwindow job has no ordering keys (gershwin-on-nextbsd/overlays/.../io.github.gershwin-desktop.loginwindow.plist: RunAtLoad+KeepAlive only), so pre-login availability is purely "bluetoothd is up early enough and does not need the session".LoginWindow.sh starts X once card0 (and on virtio, a USB keyboard+pointer) appear; nothing waits for Bluetooth and nothing should — the BT device is not connected yet at that point.rc.d/bluetooth start ubt0 on attach (libexec/rc/rc.d/bluetooth:28-30 is nostart, driven by devd on FreeBSD); area D/K must trigger stack setup from IORegistry/hwregd notification or by polling ngctl. For HID this only sets the earliest time a keyboard can reconnect; with KeepAlive bthidd/hcsecd can be started immediately after the ubt0hci node is up.bthidd.8). A BT-only-keyboard machine can therefore log in on a text getty once the stack is up.btuinput.c:401-410).hid.c:73 MAGIC_MOUSE() and hid.c:434-512 parse the undeclared report 0x29 (finger blocks, two-finger scroll converted to wheel, centre-press to middle click), battery reports 0x30/0x47. Magic Mouse 2 (05ac:0269) is not matched by the macro, so it degrades to a buttons-only mouse with no scrolling. Extending the match and the report id is a small ticket (H6, first item). Linux's hid-magicmouse is the reference (report-id details unverified here).hmt(4) (USB/I2C HID, Precision-Touchpad TLCs) nor wsp(4)/atp(4) (USB internal trackpads) apply; bthidd has no digitizer handling (§1). Supporting them means a magicmouse-style parser in bthidd emitting evdev MT type-B events through uinput (UI_SET_ABSBIT, ABS_MT_*; FreeBSD uinput supports MT via evdev_mt.c (uinput MT support unverified)), and then libinput handles gestures. Size L, parked (H6).hkbd/hms).hccontrol already has le_scan, le_connect, white-list commands (hccontrol/le.c:76-1136) and ng_l2cap.h:78-80,359-361 defines ATT_CID 0x0004, SMP_CID 0x0006 and L2CA_IDTYPE_ATT/SMP, with ng_btsocket_l2cap.c referencing those idtypes — so raw ATT/SMP PDUs over L2CAP fixed channels are at least addressable from userland (depth of that support unverified).
2. SMP pairing in userland (LTK generation/exchange, then LE_Start_Encryption; FreeBSD has no SMP anywhere — bluetooth-management page, line 79: "no BLE, no GATT, no SMP") and LTK persistence (area D's secret-store problem again).
3. A GATT client: discover HID Service 0x1812, read Report Map 0x2A4B, Report 0x2A4D characteristics with Report Reference descriptors, HID Information, Protocol Mode; enable notifications (CCCD); Battery Service 0x180F.
4. Feed notified input reports into the same sinks — hid.c/kbd.c/btuinput.c are reusable almost verbatim if the HOGP client lives beside bthidd or inside bluetoothd.
5. Reconnect: LE devices reconnect by advertising; host must keep a white-list/auto-connect loop (no bthidd equivalent).
Size XL; H7 is a scoping ticket only.qemu has no Bluetooth controller and FreeBSD has no virtual HCI, so L2CAP/SDP/pairing cannot run in CI (same as WLAN). Everything from the sink down can:
/dev/vkbdctl, writes scancodes the way kbd.c:kbd_write does, and a reader verifies (a) the key arrives on the console tty and (b) it appears on kbdmux's evdev node when kern.evdev.rcpt_mask has bit1 — this proves H1 (vkbd baked), kbdmux auto-attach, and route K-A.btuinput.c (it is self-contained) to create "Bluetooth Keyboard"/"Bluetooth Mouse" uinput devices, assert /dev/input/eventN appears with the expected name/uniq, and inject events; read them back with a small evdev reader. This proves the kernel side of route K-B/M-B.gershwin-on-nextbsd/tests/loginwindow-test.sh exists), start X (XLibre with xlibre-xf86-video-dummy or the existing scfb), create the uinput device after X is up, and check xinput list. Today this is expected to fail (documents the gap); it becomes the acceptance test for the hotplug interface once the other epic delivers it. Create the device before X and check it is listed — expected to pass today.server_init() binds L2CAP sockets; whether bind() on NG_HCI_BDADDR_ANY succeeds with no HCI node present is (unverified). If it does, bthidd can be started in qemu against a config file to test config parsing and the restart-on-change logic of H2; if not, H2's config generation is unit-testable in isolation.vkbd; H2 bluetoothd HID driver around a supervised bthidd -u; ship with keyboards on route K-A, which is XLibre's recommended FreeBSD configuration anyway, so that a paired keyboard works pre-login and after any reconnect without X hotplug. Make the rcpt_mask value a deliberate, documented decision (H4) since it is desktop-wide.hms/hmt/hpen) honour kern.evdev.rcpt_mask HW bits? Decides whether mask 3/6/7 is safe on NextBSD hardware. (Check sys/dev/hid/hidmap.c / hms.c for evdev_rcpt_mask.)rcpt_mask does the Gershwin image actually run with today — nothing sets it in nextbsd, nextbsd-overlays, ul/overlay or gershwin-on-nextbsd (grep found no rcpt_mask/sysctl.conf), so it is the kernel default 0xC, and per-device nodes are in use. Changing it for BT touches every keyboard on the box.evdev/uinput/kbdmux really present in the shipping NEXTBSD kernel (inferred from include GENERIC)? One config -x line settles it; put it in H1's acceptance criteria.hid.c/kbd.c/btuinput.c into bluetoothd later; H3 is the cheaper first step and keeps the agreed "supervise" shape.vkbd (H1) or missing hcsecd produce only syslog lines; bluetoothd should surface bthidd's "Could not open /dev/vkbdctl" and "Rejecting ... Device not configured" as status.ANY by default; with two adapters, pass -a.bthidd.8); X is fine.bluetooth pair should say "LE-only HID not supported yet" when the inquiry result has no BR/EDR HID service) rather than failing silently.Deep-dive notes, lightly edited. Draft ids map to issues in the index.
Research for nextbsd/nextbsd#473 (epic) and #412 (seed). Read-only; nothing filed.
Local sources: freebsd-src (releng/15.1 sparse checkout, 88e7371d), nextbsd-kernel,
nextbsd-userland, nextbsd, plus raw fetches from freebsd/freebsd-src@releng/15.1,
freebsd/freebsd-ports@main, freebsd/virtual_oss@main, hselasky/virtual_oss@main (copies in e16/src/).
virtual_oss into base (usr.sbin/virtual_oss, lib/virtual_oss, lib/libcuse, private
contrib/libsamplerate), and the ports tree marks audio/virtual_oss IGNORE_FreeBSD_15= included in
base system. NextBSD's base is a full buildworld of releng/15.1 with MK_BLUETOOTH at its default
(yes), so /usr/sbin/virtual_oss, /usr/sbin/virtual_bt_speaker, sdpd, libbluetooth, libsdp,
mixer, sndctl already ship. What does not ship: the Bluetooth backend voss_bt.so (base only
builds null + oss), the cuse kernel module (not in GENERIC, and NextBSD has no .ko tree), and a
working kldload("cuse.ko") call inside virtual_oss (it errors out on NextBSD even with cuse baked).
Three small tickets (A1-A3) close that; A2DP playback then works with zero new protocol code.HAVE_LIBAV). No aptX/LDAC anywhere. No AVRCP anywhere.virtual_bt_speaker(8) (also base), registers an AudioSink SDP
record via sdpd and pipes decoded SBC to an OSS device. Cheap to supervise (A5).ng_ubt) and a PF_BLUETOOTH/BLUETOOTH_PROTO_SCO socket (ng_btsocket_sco), but:
ng_hci only issues legacy Add_SCO_Connection (HV1/HV2/HV3, CVSD) — no Setup_Synchronous_Connection,
no eSCO, no mSBC/wideband; the HCI event switch has no Synchronous_Connection_Complete case; ng_ubt
picks the largest isoc alt-setting rather than the one matching the SCO bandwidth; and stock
rc.d/bluetooth never connects the sco hook, so SCO sockets are dead until someone runs
ngctl connect ubt0hci: btsock_sco: sco ubt0sco. Nobody in FreeBSD base or ports implements the HFP
AT-command state machine or an SCO<->OSS audio bridge; sdpd has no Handsfree/Headset profile records.
NetBSD's btsco(4) is the nearest BSD-licensed prior art. Estimate: XL, and it starts with a
kernel patch series. Park it behind a decision (A6).virtual_oss_cmd one-liner once virtual_oss owns /dev/dsp. virtual_oss can
re-target its playback/record backend at runtime via its control device
(virtual_oss_cmd /dev/vdsp.ctl -P /dev/bluetooth/<bdaddr>), so the clean interface with the future
audio epic is: audio epic runs the persistent virtual_oss mixer that owns /dev/dsp; bluetoothd only
publishes "device /dev/bluetooth/<addr>" and asks for the
switch. If the audio epic has not landed, bluetoothd's interim fallback is to spawn its own
virtual_oss ... -P /dev/bluetooth/<addr> -d dsp.bt0 child, exactly the way wland forks
wpa_supplicant (ul/src/WLAN/wland.c:262-275)./dev/cuse exists),
virtual_oss starts under launchd against the emulated HDA (snd_hda is baked, config/NEXTBSD:240),
sndctl shows a from_user device, the backend .so resolves, and bluetoothd's audio state machine
runs against a fake /dev/bluetooth/ open failure path. A2DP/SCO over real links needs a dongle.usr.sbin/virtual_oss/Makefile (releng/15.1): SUBDIR+= virtual_oss_cmd virtual_oss;
SUBDIR.${MK_BLUETOOTH}+= virtual_bt_speaker. Makefile.inc: PACKAGE?= sound.usr.sbin/virtual_oss/virtual_oss/Makefile: LIBADD= cuse m nv pthread samplerate, with
-Wl,--export-dynamic-symbol=virtual_oss_wait etc. "because some backends make use of those symbols"
— backends are dlopen'd plugins.lib/virtual_oss/Makefile: SUBDIR+= null oss only. lib/virtual_oss/bt/ (bt.c, avdtp.c,
sbc_encode.c, SHLIB_NAME= voss_bt.so, LIBADD= bluetooth sdp, optional -DHAVE_LIBAV for AAC) and
lib/virtual_oss/sndio/ exist in the tree but are not built by buildworld.lib/libcuse/ (cuse.3, cuse_lib.c), sys/fs/cuse/cuse.c, sys/modules/cuse/ present.
sys/conf/files:3603: fs/cuse/cuse.c optional cuse. sys/amd64/conf/GENERIC and
sys/arm64/conf/std.dev have no cuse line — it is module-only on stock FreeBSD.audio/virtual_oss/Makefile (v1.3.3, christos@FreeBSD.org, LICENSE= BSD2CLAUSE,
WWW= https://github.com/freebsd/virtual_oss): IGNORE_FreeBSD_15= included in base system.
audio/virtual_oss_bluetooth/Makefile: DISTVERSION=${OSVERSION}, WRKSRC=${SRC_BASE}/lib/virtual_oss/bt,
PLIST_FILES= lib/virtual_oss/voss_bt.so, IGNORE= requires FreeBSD source files in ${SRC_BASE} if
absent; option LIBAV (AAC) pulls multimedia/gstreamer1-libav. pkg-descr: "The backend gets loaded
dynamically only when a virtual_oss(8) invocation uses a bluetooth device." Sibling
audio/virtual_oss_sndio same pattern.github.com/freebsd/virtual_oss (last push 2026-06-02, "Fix buffer overflow in
voss_compressor() call"; tags v1.3.0-v1.3.3); hselasky/virtual_oss is stale (last push 2023-05-27).
Copyright headers: Hans Petter Selasky 2012-2023, backend_bt also Nathanial Sloss 2015 and Itronix 2006
(BSD-2-Clause throughout).usr.sbin/virtual_oss/virtual_oss/main.c (releng/15.1):
1650 snprintf(lpath, sizeof(lpath), "%s/lib/virtual_oss/voss_%s.so", prefix, name);
1671 } else if (strstr(voss_dsp_rx_device, "/dev/bluetooth/") == voss_dsp_rx_device) {
1672 voss_rx_backend = voss_load_backend("/usr/local", "bt", "rec");
1686 } else if (strstr(voss_dsp_tx_device, "/dev/bluetooth/") == voss_dsp_tx_device) {
1687 voss_tx_backend = voss_load_backend("/usr/local", "bt", "play");
...
2554 if (kldload("cuse.ko") < 0 && errno != EEXIST)
2555 err(1, "Failed to load cuse kernel module");
2557 if (cuse_init() != 0)
2558 errx(EX_USAGE, "Could not connect to cuse module");
/usr/local/lib/virtual_oss/voss_bt.so (the port's install
location). dlopen failure is fatal (errx(1, ...), main.c:1654-1655). So NextBSD must either install
the FreeBSD virtual_oss_bluetooth package or build voss_bt.so itself into that path (A2).kldload("cuse.ko") is fatal on NextBSD even with cuse compiled in.
sys/kern/kern_linker.c:1218-1224: a name containing . is treated as a file (kldname = file;
modname = NULL); linker_load_module() at 2264-2282 then does linker_search_kld(kldname) and returns
ENOENT when no file is found — it never consults modlist_lookup2() for a statically-linked
module (that branch, which returns EEXIST, is only taken for modname != NULL). NextBSD deletes the
entire .ko tree at image assembly (nextbsd-kernel/ci/assemble-image.sh:168 rm -f
"$ROOTFS"/boot/kernel/*.ko) and strips /sbin/kldload from base
(nextbsd-freebsd-compat/scripts/superseded), so errno is ENOENT, not EEXIST, and virtual_oss dies
at line 2555. Same class as wlan_xauth (#51), but this time the userland trips. Fix options (A3):
kernel patch in nextbsd-kernel/patches/ so linker_load_module() returns EEXIST when the basename
(minus .ko) is already a linked module — fixes every tool of this shape at once (webcamd does the
same) — and/or an upstream PR to freebsd/virtual_oss guarding the call with modfind("cuse").
Non-root callers also die here with EPERM (priv_check(PRIV_KLD_LOAD), kern_linker.c:1210) — so
virtual_oss must run as root or the check must be skipped when /dev/cuse already exists.required_modules="cuse" and LD_PRELOADs libsamplerate/libfftw3 when
starting early (audio/virtual_oss/files/virtual_oss.in); base virtual_oss links the private
libsamplerate, so only the module problem carries over.lib/virtual_oss/bt/bt.c = upstream backend_bt/backend_bt.c)/dev/bluetooth/<bdaddr-or-bthost-name> (bt_open, backend_bt.c:288-300; uses
bt_aton / bt_gethostbyname from libbluetooth). Man page examples: playback only
virtual_oss -C 2 -c 2 -r 48000 -b 16 -s 4ms -R /dev/null -P /dev/bluetooth/xx:xx:xx:xx:xx:xx -d dsp;
play+rec -f /dev/bluetooth/... (virtual_oss.8:278-290).SDP_SERVICE_CLASS_AUDIO_SINK (play) / AUDIO_SOURCE (rec) to find the
AVDTP PSM (bt_query, backend_bt.c:227-269, via sdp_open/sdp_search, SDP_UUID_PROTOCOL_AVDTP
protover >= 0x0100), then two PF_BLUETOOTH/SOCK_SEQPACKET/BLUETOOTH_PROTO_L2CAP sockets to that
PSM (signalling cfg->hc at :410, transport cfg->fd at :438), MTU via SO_L2CAP_OMTU/IMTU (:460,
:475). AVDTP signalling in avdtp.c (discover, get capabilities, set configuration, open, start,
suspend, close, abort — INT role for source at :258-370; ACP-side handlers
avdtpSendCapabilitiesResponseSBCForACP, avdtpACPHandlePacket at :191/:589 for sink use).CODEC_SBC always (own encoder sbc_encode.c, decoder sbc_decode()/sbc_decode_frame()
at sbc_encode.c:417/629 — so record from an A2DP source also works, SBC only); CODEC_AAC only
under #ifdef HAVE_LIBAV (backend_bt.c:568-620 uses libavcodec "aac" in LATM, 128 kbit/s). No aptX,
no LDAC, no AVRCP (grep -ril avrcp over the whole tree: nothing).hcsecd
(or bluetoothd's equivalent) answering link-key requests — that is the D/B tracks' problem, not audio's.ubtN device with ng_hci/ng_l2cap/ng_btsocket attached and btsock_l2c
connected; sdpd only for the sink role (virtual_bt_speaker calls sdp_open_local +
sdp_register_service(SDP_SERVICE_CLASS_AUDIO_SINK) at bt_speaker.c:80-91 and listens on the AVDTP
PSM :113-127). Source role talks to the remote SDP server, so no local sdpd needed.DEFAULT_MAXBPOOL (backend_bt.c:384-397), no runtime codec
renegotiation, no AVRCP absolute volume so the headset's volume keys do nothing.What exists in sys/netgraph/bluetooth (releng/15.1):
| Layer | Status | Evidence |
|---|---|---|
| USB isoc SCO transport | present | drivers/ubt/ng_ubt.c:281-282, 336-378 (4 isoc xfers, UBT_ISOC_NFRAMES 32), ubt_isoc_read_one_frame :1290-1366 reassembles NG_HCI_SCO_DATA_PKT; sysctl net.bluetooth.usb_isoc_enable=1 :154-157 |
| Isoc alt-setting choice | wrong for SCO | ng_ubt.c:738-780 picks the alt setting with the largest wMaxPacketSize; BT-USB spec alt settings encode SCO bandwidth (alt 1 = 1 link CVSD ... alt 5), so the controller and host disagree on frame sizes on many dongles |
| HCI SCO connection | legacy only | hci/ng_hci_ulpi.c:322-450 builds NG_HCI_OCF_ADD_SCO_CON with HV1/HV2/HV3 pkt types; no Setup_Synchronous_Connection (eSCO), so no mSBC / wideband speech, no transparent air mode |
| HCI sync-conn events | missing | hci/ng_hci_evnt.c:107-200 event switch has CON_COMPL (0x03) but no SYNC_CON_COMPL (0x2C) / SYNC_CON_CHANGED (0x2D); the mask bits exist in ng_hci.h:270-271 but nothing consumes them, so a headset that initiates eSCO produces an unhandled event |
| SCO socket | present | socket/ng_btsocket_sco.c (SOL_SCO, SO_SCO_MTU, SO_SCO_CONNINFO :1444-1483; listen/accept/connect), node btsock_sco (ng_btsocket.h:363); buffers NG_HCI_BUFF_SCO_SET(unit->buffer, 1, 10, 1) (ng_hci_main.c:147) until read_buffer_size |
| Stack wiring | not wired | libexec/rc/rc.d/bluetooth:119-127 connects raw, ctl, l2c hooks only; nothing connects ubt0hci: sco to btsock_sco:; ng_btsocket_sco only learns routes from NGM_HCI_NODE_UP on a connected hook (ng_btsocket_sco.c:821-856). bluetoothd's stack bring-up (K/D tracks) must add ngctl connect ${dev}hci: btsock_sco: sco ${dev}sco |
| RFCOMM (HFP signalling) | present | socket/ng_btsocket_rfcomm.c; usr.bin/bluetooth/rfcomm_sppd |
| SDP records for HS/HF/AG | absent | usr.sbin/bluetooth/sdpd/profile.c:51-76 lists audio_sink, audio_source, dun, ftrn, irmc, lan, opush, sp, nap, gn, panu only; the UUIDs exist in lib/libsdp/sdp.h:136-159 |
| HFP AG/HF AT state machine | absent | gh code search: 0 hits for handsfree/HFP in freebsd-src and freebsd-ports (only wireshark's dissector list mentions avrcp) |
| SCO <-> OSS audio bridge | absent | nothing consumes BLUETOOTH_PROTO_SCO in base or ports |
Prior art: NetBSD sys/dev/bluetooth/btsco.c (Itronix/Iain Hibbert, BSD-2-Clause, "Bluetooth SCO Audio
device" attaching as a kernel audio device on top of a SCO PCB) — the only BSD-licensed SCO audio
implementation; it depends on NetBSD's own bt stack API, so it is a design reference, not a port.
Cost: kernel patch series (eSCO command + events + alt-setting selection in ng_ubt, all under
nextbsd-kernel/patches/ since the nodes are baked) + a userland HFP daemon (RFCOMM AT parser for the AG
role: AT+BRSF, +CIND, +CMER, AT+VGS/VGM, AT+BCC, ring/answer/hangup, codec negotiation) + an
SCO<->cuse bridge (8 kHz mono CVSD, 48-byte SCO frames every 3.75 ms) that virtual_oss can consume as
/dev/dsp.hfp. Reasonably an XL on its own, and CVSD-only narrowband unless eSCO lands. Recommendation:
park behind a decision whether narrowband headset mic is worth it in E16 at all (A6, "Needs answer").
Everything users notice first — music/video output to a headset — does not need it.
Nothing anywhere in FreeBSD implements AVRCP (AVCTP over L2CAP PSM 0x0017, AVC passthrough commands).
Consequence today with virtual_oss: play/pause/next keys on a headset are ignored and the headset cannot
report/absorb volume (no absolute-volume). Implementing the target role subset (accept the headset's
AVCTP connection on PSM 23, answer UNIT INFO/SUBUNIT INFO, decode PASSTHROUGH PLAY/PAUSE/STOP/
FORWARD/BACKWARD/VOLUME_UP/DOWN) is a userland-only L2CAP listener, no kernel work; it needs a local SDP
AV_REMOTE_CONTROL_TARGET (0x110C) record, which sdpd cannot register today (profile.c list above), so
either extend sdpd with a profile or let bluetoothd own the SDP server for the records it needs. The
key events have no consumer on NextBSD yet (there is no media-key path from a daemon to Gershwin/X); the
natural delivery is a notify(3) name (org.nextbsd.bluetooth.avrcp.<key>) plus SCDynamicStore state, and
absolute-volume writes routed to the audio epic's volume API. Size L, Sequence after A4 (A7).
nextbsd-kernel/config/NEXTBSD, include GENERIC at :1): device sound from GENERIC
(amd64) / std.dev (arm64), plus baked snd_uaudio (:206) and snd_hda (:240) with long rationale
comments about the unbaked-leaf trap. No cuse, no netgraph, no ng_* anywhere in the config.nextbsd-freebsd-compat = full buildworld of releng/15.1 with WITHOUT_TOOLCHAIN/TESTS/
LIB32/DEBUG_FILES (README, build.yml:30-32); Bluetooth and sound tools are not disabled, so
/usr/sbin/{mixer,sndctl,virtual_oss,virtual_oss_cmd,virtual_bt_speaker,sdpd,hccontrol,...},
/usr/lib/virtual_oss/voss_{oss,null}.so, libcuse, libbluetooth, libsdp ship. (Unverified on a
live image: I inferred from the build knobs, not from a file listing.) No /etc from base, so no
rc.d/bluetooth, no devd (stripped; kextd/IOKit replaces it).nextbsd/pkglist.txt contains only pkg; nothing pulls pulseaudio/pipewire/sndio.
Apps open OSS /dev/dsp directly./dev/dsp is a devfs clone resolved on every open to hw.snd.default_unit
(sys/dev/sound/pcm/dsp.c:2062-2079, gated by hw.snd.basename_clone, :62-64); hw.snd.default_unit
(sound.c:81-111) only indexes pcm units, and hw.snd.default_auto (:55) moves it to newly
attached cards. mixer -d unit / sndctl change it (mixer.8:51). A virtual_oss/cuse device is not a
pcm unit, so hw.snd.default_unit can never select a Bluetooth headset. The way virtual_oss makes
itself the default is by creating a real /dev/dsp node itself (-d dsp), which devfs finds before
the clone handler fires (the ports rc script defaults to exactly -f /dev/dsp0 ... -d dsp -t dsp.ctl,
files/virtual_oss.in). It registers with /dev/sndstat as provider "virtual_oss" (main.c:1787-1830,
SNDST_DSPS_PROVIDER), which is why sndctl(8) documents "Userland devices (e.g those registered by
virtual_oss) can also be selected" (sndctl.8:60-62). Unverified: devfs lookup ordering (real node vs
clone) on 15.1 with basename_clone=1 — test on hardware; fallback is hw.snd.basename_clone=0.voss_tx_backend_refresh()
(main.c:2205-2216), and virtual_oss_cmd.8:73-76 documents virtual_oss_cmd /dev/vdsp.ctl -P /dev/dsp4
"To change the playback device" — so a persistent virtual_oss can be pointed at
/dev/bluetooth/<addr> and back without restarting, and clients holding /dev/dsp open keep playing.Model A — preferred, once the audio epic exists:
1. Audio epic runs org.nextbsd.virtual_oss (persistent, -f /dev/dspN -d dsp -t dsp.ctl, owns /dev/dsp).
2. bluetoothd, on ACL connect + SDP shows AUDIO_SINK: publishes
State:/Bluetooth/Device/<addr> {Profiles:[A2DP-sink], AudioDevice:"/dev/bluetooth/<addr>"} and
sends the switch request (Mach call into the audio daemon, or interim: exec
virtual_oss_cmd /dev/dsp.ctl -P /dev/bluetooth/<addr> and, if the device advertises AUDIO_SOURCE,
-R). On disconnect: -P /dev/dspN (the value it recorded before switching). Policy (auto-switch on
connect? user preference in SCPreferences) is the audio epic's; bluetoothd only offers.
Model B — interim, no audio epic yet (what A4 ships):
- bluetoothd forks virtual_oss -S -C 2 -c 2 -r 48000 -b 16 -s 8ms -R /dev/null -P /dev/bluetooth/<addr>
-d dsp.bt0 -t dsp.bt0.ctl as a supervised child (same fork/exec/waitpid shape as wland ->
wpa_supplicant, ul/src/WLAN/wland.c:262-275, :356, :586), waits for /dev/dsp.bt0, publishes the
node in SCDynamicStore, and kills it on disconnect/unpair. Users (or Gershwin) pick /dev/dsp.bt0
explicitly; the "make it default" step is a mixer/sndctl-shaped hole the audio epic fills. Not a
launchd job per device: the child's lifetime is the ACL's, bluetoothd already owns that state, and
launchd has no service ordering to express "after the link is up" (wland plist comments).
- Sink role (A5): virtual_bt_speaker -B -d /dev/dsp supervised the same way, started when the
adapter is discoverable; needs bluetoothd's sdpd (or bluetoothd's own SDP server) running first.
/dev/dsp selection. Either the epic runs virtual_oss as the system mixer (then
Bluetooth is just a backend string) or it defines a "default output" switch bluetoothd can call. Today
neither hw.snd.default_unit nor sndctl can name a cuse device.ListOutputs() (pcm units + userland devices from /dev/sndstat),
SetDefaultOutput(devnode), SetVolume(devnode, level), and an "output list changed" notification.
bluetoothd will publish State:/Bluetooth/Device/<addr>/Audio = {Node, Profiles, Codec} and consume
SetDefaultOutput on connect (policy-gated) and on disconnect.SetVolume on the virtual device, and a volume-changed
callback so bluetoothd can push the level to the headset.audio/virtual_oss_bluetooth? It is a
DISTVERSION=${OSVERSION} src-based port (IGNORE without /usr/src); official builders have src, but
I did not confirm the package exists on pkg.FreeBSD.org. If not, A2 falls back to building voss_bt.so
in the NextBSD base build (compat repo) or nextbsd-userland.nextbsd-freebsd-compat has no patches dir; only
nextbsd-kernel/patches/. A3's kernel-side fix avoids the question; the modfind() guard should go
upstream to freebsd/virtual_oss.-d dsp on 15.1 (see 4). Needs a hardware run.net.bluetooth.usb_isoc_enable and alt-setting selection: does any tested dongle work with
ng_ubt's largest-alt policy? (Only matters for A6.)virtual_oss must run as root (cuse device creation + kldload privilege check) — one more root
daemon per epic; the audio epic should decide on a _virtual_oss user + devfs rules.hcsecd.