# DF-0746 — Verdict

## Verdict: REPRODUCED (code-level harness) + FIX VALIDATED — runtime impact NONE on default kernel

The source-level bug is **real and confirmed**: in `l2cap_rtx` (`sys/netbt/l2cap_misc.c:183-197`), the body calls `l2cap_request_free(req)` at line 190 (which `zfree()`s `req` via line 173), and then at line 192 the `DPRINTF(... req->lr_id)` reads `req->lr_id` from freed memory — a use-after-free READ. The deterministic userspace harness `harness.c` transcribes the exact body of `l2cap_rtx` together with a faithful model of `vm_zone` `zalloc/zfree` and prints `UAF READ CONFIRMED` for both the immediate-stale case and the same-CPU-slab-reuse case.

**However the runtime impact on the default `X86_64_GENERIC` kernel is `NONE`:**

1. `netbt/l2cap_misc.c` is `optional bluetooth` (`sys/conf/files:1614`) and `X86_64_GENERIC` has no `device bluetooth` — so the file is **not compiled into the running kernel at all**. It exists only in the loadable module `/boot/kernel/netbt.ko`, which is **not loaded** on this guest (`kldstat` shows only ehci/xhci).
2. Even inside `netbt.ko`, `BLUETOOTH_DEBUG` is **not** defined (`sys/netbt/Makefile` has no `-DBLUETOOTH_DEBUG`), so `DPRINTF` expands to `((void)0)` (`sys/netbt/bluetooth.h:145`) and the expression `req->lr_id` is **never evaluated at runtime**. The shipped module's `l2cap_rtx` disassembly (collected in `disasm_unfixed_l2cap_rtx.txt`) contains **zero** references to `req` after the `callq l2cap_request_free` — only the `chan->lc_state` check at offset 8 of the (separately-captured) `chan` pointer, which is safe.
3. Decisively: the unfixed and fixed `netbt.ko` modules are **byte-for-byte identical** at `l2cap_rtx` (`0x5710`, same 14 instructions — see `disasm_unfixed_l2cap_rtx.txt` vs `disasm_fixed_l2cap_rtx.txt`). The bug exists only as source-level latent text that becomes live exclusively under `-DBLUETOOTH_DEBUG`.
4. The live trigger also requires a Bluetooth radio + an L2CAP signaling exchange whose RTX timer (30 s default) fires — absent on this KVM guest.

So the bug is correctly classified **Low / latent hardening**: it would manifest (as a benign read of the original `lr_id` value, or as a read of attacker-reshaped bytes after same-CPU slab reuse) only in a `BLUETOOTH_DEBUG` build with live BT hardware. The fix is one line and is validated by building the actual `netbt.ko` with the patch and by the harness transcription.

## Mechanism (every hop cited)

`l2cap_request_free` (`sys/netbt/l2cap_misc.c:163-174`):

```c
163  void
164  l2cap_request_free(struct l2cap_req *req)
165  {
166      struct hci_link *link = req->lr_link;
...
172      TAILQ_REMOVE(&link->hl_reqs, req, lr_next);
173      zfree(l2cap_req_pool, req);          /* <--- req is freed HERE */
174  }
```

`l2cap_rtx` — the RTX (Response Timeout eXpired) callout callback (`sys/netbt/l2cap_misc.c:183-197`):

```c
183  void
184  l2cap_rtx(void *arg)
185  {
186      struct l2cap_req *req = arg;
187      struct l2cap_channel *chan;
188
189      chan = req->lr_chan;                   /* safe: read BEFORE free */
190      l2cap_request_free(req);               /* frees req via line 173 */
191
192      DPRINTF("cid %d, ident %d\n",
193              (chan ? chan->lc_lcid : 0),
194              req->lr_id);                   /* <--- UAF READ (offset 17) */
195
196      if (chan && chan->lc_state != L2CAP_CLOSED)
197          l2cap_close(chan, ETIMEDOUT);
198  }
```

After line 190, `req` points at freed memory. Line 194 reads `req->lr_id`.

**Why the stale read returns the original id in practice (not attacker-controlled in the single-threaded path):**

`struct l2cap_req` layout (`sys/netbt/l2cap.h:423-430`, amd64):
- offset 0  `lr_link`  (8 bytes)
- offset 8  `lr_chan`  (8 bytes)
- offset 16 `lr_code`  (1 byte)
- offset 17 `lr_id`    (1 byte)  ← the field read after free
- offset 18 pad
- offset 24 `lr_rtx`   (`struct callout`)
- ...        `lr_next`  (`TAILQ_ENTRY`)

`zfree` (`sys/vm/vm_zone.c:211-244`) writes **only**:
- offset 0  = freelist link (`((void **)item)[0] = zpcpu->zitems;`, vm_zone.c:233)
- offset 8  = `ZENTRY_FREE` magic, **only under `INVARIANTS`** (`((void **)item)[1] = (void *)ZENTRY_FREE;`, vm_zone.c:237)

It does **not** touch offset 16/17. So immediately after `zfree`, `req->lr_id` still holds the **original** id set in `l2cap_request_alloc` (`l2cap_misc.c:129`). The harness `Test 1` confirms this (`g_uaf_read_value == 0x42`).

The latent risk — the reason this is a real bug worth fixing — is that the dereference is of freed memory: any intervening same-CPU `zalloc` from `l2cap_req_pool` hands the slot to a different consumer, who can shape offset 17 to an arbitrary byte. `Test 2` of the harness models that and shows the read returns the attacker-shaped value (`0xDD`).

In the **actual `l2cap_rtx` callout path** there is no allocator call between the `zfree` and the `DPRINTF`, so the *immediate* read returns the benign original id; but the bug is still a genuine UAF — the compiler is free to reorder, the slab is shared, and any future code edit that inserts an allocation in between turns this into an attacker-shaped read. Plus, under `BLUETOOTH_DEBUG` the read is live today.

## Why it is not exploitable to uid=0

This is a **single-byte read** of freed memory whose value:
- in the immediate path is the original id (no information disclosed — the caller set it),
- goes only to `kprintf` (dmesg),
- has no write primitive, no control flow hijack, no influence over a victim object's lifetime or pointers.

There is no path from this read to privilege escalation. The "valid hard blocker — read-only primitive" applies: the bug gives the attacker **no write capability at all**, so no escalation chain exists to develop. (See AGENT.md Phase 6: "The primitive is genuinely read-only — no write, no corruption. Then there is no chain.") The honest classification is a low-impact latent hardening bug.

## Reachability on this guest

| Layer | Status | Evidence |
|---|---|---|
| Compiled into default kernel | **NO** | `sys/conf/files:1614` `netbt/l2cap_misc.c optional bluetooth`; `sys/config/X86_64_GENERIC` has no `device bluetooth` (`grep bluetooth` ⇒ none) |
| Loadable module present | yes (unloaded) | `/boot/kernel/netbt.ko` exists; `kldstat` shows only ehci/xhci |
| `BLUETOOTH_DEBUG` enabled in module | **NO** | `sys/netbt/Makefile` has no `-DBLUETOOTH_DEBUG` ⇒ `DPRINTF`=`((void)0)` (`bluetooth.h:145`) |
| UAF read emitted in shipped binary | **NO** | disasm of `l2cap_rtx` in `netbt.ko` references `req` only at `0x8(%rdi)` (the pre-free `chan = req->lr_chan`); no post-free `req` deref (see `disasm_unfixed_l2cap_rtx.txt`) |
| Live trigger reachable | **NO** | requires BT radio + L2CAP RTX timer firing; KVM guest has neither |
| **Net runtime impact on default kernel** | **NONE** | bug is dead source text on production builds |

This is the same harness-precedent cluster as DF-0745 (sibling double-free on the same function) and the DF-0393/0594/0616/0732/0733 wifi/netgraph/bt-unreachable set.

## PoC changes (what was authored — the folder started empty)

The PoC folder was **empty** at start (no `README.md`, no sources — only the DB row and the rendered `www/findings/DF-0746.html` summary). Authored from scratch:

- `harness.c` — deterministic userspace transcription of `l2cap_rtx` + `l2cap_request_free` + a faithful `vm_zone` `zalloc/zfree` model. Two tests: (1) immediate stale read returns original id; (2) same-CPU slab reuse makes the freed read return attacker-shaped bytes. Prints `UAF READ CONFIRMED`.
- `harness_fixed.c` — same body with `lr_id` captured into a local before `l2cap_request_free`. Proves no byte of `*req` is read after the free.
- `build.sh`, `run.sh` — exact, runnable.
- `fix.diff` — minimal one-hunk unified diff against `sys/netbt/l2cap_misc.c` (capture `id = req->lr_id` before the free, use `id` in the `DPRINTF`).

## Fix validation (Phase 8)

1. **Applies cleanly** to `/usr/src` (`patch -p1 --forward < fix.diff` ⇒ `Hunk #1 succeeded at 185`).
2. **Builds the actual kernel module** with the fix: `cd /usr/src/sys/netbt && make` (warm obj, `-Werror`) ⇒ **0 errors, 0 warnings**, produces `netbt.ko` (`/usr/obj/usr/src/sys/netbt/netbt.ko`, 105024 bytes). See `fix_build.log`.
3. **Disassembly proof**: the fixed module's `l2cap_rtx` (`disasm_fixed_l2cap_rtx.txt`) is identical to the unfixed module's (`disasm_unfixed_l2cap_rtx.txt`) — both at `0x5710`, 14 instructions, zero `req` references after the call to `l2cap_request_free`. This simultaneously proves (a) the fix doesn't perturb production codegen and (b) the production module never emitted the UAF read in the first place.
4. **Logic transcription**: `harness_fixed.c` reproduces the fix at the source-logic level and prints `FIXED: no read of *req after zfree; DPRINTF used the saved local` (`fix_run.log`).

`fix_status: fixed` — the source-level UAF is eliminated; the production module was already free of the runtime read because `DPRINTF` is a no-op.

## Recommended fix

`fix.diff` (matches the finding's proposal): in `l2cap_rtx` (`sys/netbt/l2cap_misc.c`), introduce `uint8_t id;`, set `id = req->lr_id;` before `l2cap_request_free(req)`, and use `id` in the `DPRINTF`. One-line logical change.
