# DF-1043 — ufoma sysctl handlers access freed sc_modetable after detach (UAF)

## Verdict

**NOT REPRODUCED (latent — source-confirmed real bug, not triggerable on this guest due to no USB hardware).** Fix VALIDATED as compile+boot+source-trace correct; dynamic before/after not testable (no USB HW on either kernel).

## Mechanism (source-level confirmation — the bug IS real)

The UAF race window described in the finding is genuine and confirmed by source:

1. **OID creation** — `ufoma_attach` (`sys/bus/u4b/serial/ufoma.c:453-463`) creates three sysctl OIDs (`supportmode`, `currentmode`, `openmode`) via `SYSCTL_ADD_PROC(sctx, …)` where `sctx = device_get_sysctl_ctx(dev)` (the device's sysctl context). The handlers `ufoma_sysctl_support` (`ufoma.c:1169`) and `ufoma_sysctl_open` (`ufoma.c:1208`) dereference `sc->sc_modetable` at lines 1177, 1184, 1232 **without holding `sc->sc_lock`**.

2. **Free before OID removal** — `ufoma_detach` (`ufoma.c:475-491`) calls `kfree(sc->sc_modetable, M_USBDEV)` at line 485 but never removes the three OIDs. The OIDs are torn down later by newbus in `device_sysctl_fini` → `sysctl_ctx_free` (`sys/kern/subr_bus.c:2151`), which runs **after** `DEVICE_DETACH` returns (`subr_bus.c:2139`). So between `kfree` (line 485) and `device_sysctl_fini` (subr_bus.c:2151), the OIDs are still live and their handlers can fire on freed memory.

3. **Unprivileged read** — `supportmode` and `currentmode` are `CTLFLAG_RD`, readable by any local user. `openmode` is `CTLFLAG_RW`.

### Why the bug is real (rwlock analysis)

The finding cites `oid_running` drain (`kern_sysctl.c:391-398`) as the handler-drain mechanism. In fact, `grep -rn oid_running sys/` shows `oid_running` is **never incremented anywhere in the kernel** — that drain loop is dead code. However, the real protection is the `SYSCTL_SLOCK()` held across the entire handler invocation in `userland_sysctl` (`kern_sysctl.c:1572-1574`):

```c
SYSCTL_SLOCK();
error = sysctl_root(0, name, namelen, &req);   /* handler runs here */
SYSCTL_SUNLOCK();
```

`sysctl_remove_oid` / `sysctl_unregister_oid` need `SYSCTL_XLOCK()` (exclusive), so they block until all shared-lock holders (in-flight handlers) release. This means `sysctl_remove_oid` does drain — via the rwlock, not `oid_running`. But since `ufoma_detach` never calls `sysctl_remove_oid` before `kfree`, the drain never happens in the vulnerable window. A handler that wins the rwlock shared-lock before `kfree` can read `sc->sc_modetable[0]` after the buffer is freed.

## Why NOT reproducible on this guest

The audit QEMU guest has **no USB host controller** at all (`pciconf -l` shows 0 USB devices — only virtio-net, virtio-blk, IDE, ISA bridge, ACPI, VGA). The `ufoma` driver's `ufoma_attach` is called only when `ufoma_probe` matches a connected USB CDC-ACM mobile device. With no USB hardware:

- `ufoma_attach` never runs → `SYSCTL_ADD_PROC` never executes → no `dev.ufoma.*` OIDs exist.
- `sysctl dev.ufoma.0.supportmode` → "unknown oid" (ENOENT).
- The PoC reader loop calls `sysctlbyname` in a tight loop — every call fails ENOENT, producing 0 output.
- The `ufoma.ko` module can be `kldload`ed as root, but it only registers the probe/attach methods — no device nodes or sysctl OIDs are created without a matching USB device.

This is a valid hard blocker (Phase 6): the vulnerable code path is unreachable at runtime on this guest, and no harness can exercise the **actual bug path from userspace** without either adding USB hardware to the VM (outside scope) or building a kernel module that bypasses `ufoma_probe` (which violates the bright-line rule — it would not be driving the real bug path).

The finding's threat model is honest about this: it requires "a `ufoma`-attached USB CDC-ACM mobile-broadband device" plus a detach trigger.

## Finding's proposed fix is BROKEN — corrected fix authored

The finding's recommended fix saves the OID pointers and calls `sysctl_remove_oid(oid, 1, 0)` in `ufoma_detach` before `kfree`. **This fix is itself broken**: `sysctl_remove_oid(del=1)` frees the OID struct but does NOT remove the corresponding entry from the device's sysctl context list. The later `device_sysctl_fini` → `sysctl_ctx_free` (`kern_sysctl.c:226`) iterates the context list and calls `sysctl_remove_oid_locked(e->entry, …)` on each entry — `e->entry` would be a pointer to already-freed memory → **a new UAF in `sysctl_ctx_free`**.

The correct pattern (used in `sys/kern/kern_sensors.c:444-445`) is to call `sysctl_ctx_entry_del(sctx, oid)` BEFORE `sysctl_remove_oid(oid, 1, 0)`. The fix.diff in this evidence pack uses this corrected pattern.

### Corrected fix (fix.diff)

1. Add `struct sysctl_oid *sc_oid_support, *sc_oid_current, *sc_oid_open` to `struct ufoma_softc`.
2. Save the return values of `SYSCTL_ADD_PROC` in `ufoma_attach`.
3. At the top of `ufoma_detach`, before `ucom_detach` and before `kfree(sc->sc_modetable)`:
   ```c
   sctx = device_get_sysctl_ctx(dev);
   if (sc->sc_oid_support != NULL) {
       sysctl_ctx_entry_del(sctx, sc->sc_oid_support);
       sysctl_remove_oid(sc->sc_oid_support, 1, 0);
       sc->sc_oid_support = NULL;
   }
   /* … same for current and open … */
   ```

`sysctl_remove_oid(oid, 1, 0)` blocks on the sysctl rwlock until all in-flight handlers drain, so after it returns no handler can touch `sc_modetable`. `sysctl_ctx_entry_del` removes the stale context entry so `device_sysctl_fini`'s later `sysctl_ctx_free` won't dereference a freed OID.

## Fix validation (Phase 8)

| Step | Result |
|------|--------|
| `patch -p1 --dry-run` | All 3 hunks apply cleanly |
| `make -j6 nativekernel` | **rc=0** (0 errors, 35757 lines of log) |
| Kernel install (`kernel.stripped` → `/boot/kernel/kernel`) | sha256 `a61e073f…` |
| Boot | `#1` build, today's timestamp — boots and is stable |
| `kldload ufoma` | rc=0 — module loads cleanly |
| PoC reader on patched kernel | Same ENOENT (no USB HW) — no regression |

**fix_status = `not_testable`**: The PoC cannot trigger the UAF on this guest (no USB hardware on either the unpatched or patched kernel), so a dynamic before/after comparison is impossible. The fix is validated as: (a) applies cleanly, (b) compiles with `-Werror` (rc=0), (c) boots and is stable, (d) source trace confirms it closes the code path (`sysctl_remove_oid` drains via rwlock; `sysctl_ctx_entry_del` prevents `sysctl_ctx_free` UAF).

## Exploit chain

Not applicable. This is a read-freed/UAF class bug that is (a) not dynamically triggerable on this guest (no USB HW), and (b) classified Low severity by the finding. No escalation chain developed — the valid hard blocker (code path unreachable at runtime on this guest, no harness can exercise the actual bug path without USB hardware) applies.

## PoC changes

- **`build.sh`** (new): exact build command for the reader.
- **`run.sh`** (new): exact run command with documentation of the USB HW precondition.
- **`fix.diff`** (new): **corrected** fix — pairs `sysctl_ctx_entry_del` + `sysctl_remove_oid` (supersedes the finding's proposed fix which would introduce a `sysctl_ctx_free` UAF).
- Original `ufoma_uaf.c` and `run.sh` unchanged in logic.

## References

- `sys/bus/u4b/serial/ufoma.c:453-463` — `SYSCTL_ADD_PROC` for the three OIDs
- `sys/bus/u4b/serial/ufoma.c:484-486` — `kfree(sc->sc_modetable)` with no prior OID removal
- `sys/bus/u4b/serial/ufoma.c:1169-1193` — `ufoma_sysctl_support` derefs `sc_modetable` unlocked
- `sys/bus/u4b/serial/ufoma.c:1208-1240` — `ufoma_sysctl_open` derefs `sc_modetable` unlocked
- `sys/kern/subr_bus.c:2129-2153` — newbus order: `DEVICE_DETACH` (2139) then `device_sysctl_fini` (2151) — the gap
- `sys/kern/kern_sysctl.c:1519-1578` — `userland_sysctl`: `SYSCTL_SLOCK()` held across `sysctl_root` (the real drain mechanism)
- `sys/kern/kern_sysctl.c:213-261` — `sysctl_ctx_free`: iterates context entries (would UAF on stale entries)
- `sys/kern/kern_sysctl.c:300-317` — `sysctl_ctx_entry_del`: the missing call in the finding's proposed fix
- `sys/kern/kern_sensors.c:444-445` — correct pattern: `sysctl_ctx_entry_del` + `sysctl_remove_oid`
