# DF-1045 — VERDICT

## Verdict

**NOT REPRODUCED on guest (latent source-level UAF, confirmed by line-by-line trace).**
The bug is **real** — a use-after-free in `targclose` where `kfree(softc)` runs
before `cam_periph_release(periph)` which synchronously invokes `targdtor` on the
freed softc — but it is **not triggerable on this guest** for two independent
reasons, each sufficient on its own:

1. **The targ driver is not compiled into the default kernel.**
   `sys/conf/files` line 45: `bus/cam/scsi/scsi_target.c  optional targ`.
   `X86_64_GENERIC` does **not** contain `device targ` (verified:
   `grep -c 'device targ' = 0`). No `/dev/targ*` device exists, no
   `targ.ko` module exists, and `nm /boot/kernel/kernel | grep -w targclose`
   returns empty. The vulnerable code is simply absent from the running kernel.

2. **Even with the driver compiled in, no target-capable SCSI HBA is present.**
   The QEMU guest only exposes ATA buses (`ata0`, `ata1` with a QEMU DVD-ROM).
   `targenable()` at `scsi_target.c:435` rejects paths where
   `(cpi->target_sprt & PIT_PROCESSOR) == 0`, and ATA SIMs never set
   `PIT_PROCESSOR`. So `TARGIOCENABLE` always fails before `softc->periph`
   is populated, and `targclose` always takes the safe "never enabled"
   early-exit branch.

A third, lesser gate: `targopen()` at `scsi_target.c:177` requires
`SYSCAP_RESTRICTEDROOT` (real root outside jail/chroot), so this is not a
local-unprivileged→kernel bug regardless.

## Mechanism (source-level UAF — confirmed by trace)

The UAF is in the **success path** of `targclose` (`scsi_target.c:207-248`):

1. `targclose:228` — `periph = softc->periph` (non-NULL after successful `TARGIOCENABLE`).
2. `targclose:229` — `cam_periph_acquire(periph)` bumps refcount `0→1`
   (periph starts at refcount=0 per `cam_periph_alloc` at `cam_periph.c:215`).
3. `targclose:231` — `targdisable(softc)` aborts pending CCBs back to
   `user_ccb_queue` / `abort_queue`, clears `TARG_STATE_LUN_ENABLED`.
4. `targclose:235` — `cam_periph_invalidate(softc->periph)` sets
   `CAM_PERIPH_INVALID`. Since `refcount==1`, `camperiphfree` is **NOT**
   called yet (`cam_periph.c:574`: only if `refcount==0`).
5. **`targclose:240` — `kfree(softc, M_TARG)` FREES THE SOFTC.**
   `periph->softc` (set at `targctor:521`) is now dangling.
6. `targclose:245` — `cam_periph_release(periph)` enters the slow path
   (`cam_periph.c:352-353`: `INVALID && refcount==1`), decrements to 0,
   and calls `camperiphfree` (`cam_periph.c:373`).
7. `camperiphfree:600-601` — calls `periph->periph_dtor(periph)` = **`targdtor`**.
8. **`targdtor:534` — `softc = (struct targ_softc *)periph->softc` → dangling.**
9. `targdtor:541` — `TAILQ_FIRST(&softc->user_ccb_queue)` reads freed memory.
10. `targdtor:542-543` — `TAILQ_REMOVE` + `targfreeccb` write/call on freed softc.
11. `targdtor:550` — `softc->periph = NULL` writes to freed memory.

This is a textbook UAF: free at step 5, dereference at steps 8-11.

A **second manifestation** (early-exit path) exists when the user does
`TARGIOCENABLE` then `TARGIOCDISABLE` then `close()`: `targclose:216-222`
kfrees softc while `periph->softc` stays dangling in the still-registered
periph. A subsequent `TARGIOCENABLE` on the same path via `targenable:442-448`
retrieves `del_softc = periph->softc` (freed) and dereferences it.

## Why it cannot be triggered on this guest

| Gate | Status on guest | Effect |
|------|----------------|--------|
| `device targ` in `X86_64_GENERIC` | **ABSENT** | scsi_target.c not compiled; no `/dev/targ` |
| SCSI HBA with `PIT_PROCESSOR` | **ABSENT** (only ATA `ata0/ata1`) | `TARGIOCENABLE` fails at `scsi_target.c:435` |
| `SYSCAP_RESTRICTEDROOT` | Present (root) | Not a blocker for root |

For the fix-validation boot, I added `device targ` to the config and rebuilt —
this brought the targ driver online (`/dev/targ` appeared, `targclose`/`targdtor`
symbols in `kernel.debug`). I then opened `/dev/targ` as root and issued
`TARGIOCENABLE` on the ATA bus: it returned `ENOTTY` (ATA does not support
target mode), so `softc->periph` stayed NULL and `targclose` took the safe
"never enabled" early-exit. No panic, guest healthy. The bug path remained
unreachable.

## Exploit chain

**Not applicable** — no chain developed because the bug is not triggerable
on this guest (valid Phase 6 blocker: the vulnerable code path is dead at
runtime on this guest AND no harness can exercise it without absent hardware).
The bug requires `device targ` in the kernel config AND a SCSI HBA advertising
`PIT_PROCESSOR` (target mode) — neither is present. The primitive IS
characterizable at the source level (free-of-attacker-influenced-address via
`targfreeccb` if slab grooming shapes the reused `M_TARG` chunk), but the
precondition (RESTRICTEDROOT + target HBA) means this is a root→kernel
hardening gap, not a local-unpriv→kernel escalation.

## Fix validation (Phase 8)

**fix_status: `not_testable`** — the bug path cannot be exercised on this guest.

What was validated:
1. **Fix applies cleanly** — `patch -p1 --forward < fix.diff` succeeds (3 hunks).
2. **Fix compiles** — rebuilt `X86_64_GENERIC` + `device targ` with `make nativekernel`;
   `scsi_target.c` compiled with `-Werror` (no warnings), kernel linked successfully.
   `targclose`/`targdtor` symbols present in `kernel.debug`.
3. **Patched kernel boots** — `#1` kernel (Jul 14 09:16:43) boots cleanly, `/dev/targ`
   active, guest healthy.
4. **Source-level verification** — patched `targclose` no longer has `kfree(softc)` in
   the success path or the disabled-via-ioctl early-exit; `targdtor` is now the sole
   place softc is freed (after queue draining, before clearing `periph->softc`).

What could NOT be validated: live reproduction of the UAF on the patched kernel
(there is no target-capable HBA to make `TARGIOCENABLE` succeed, so the bug path
that frees-then-dereferences softc is never entered).

## The fix

The fix makes `targdtor` the **sole owner of softc deallocation** (the canonical
periph-dtor-owns-softc pattern, matching `scsi_sa.c:sadtor/sacleanup`):

1. **`targclose` early-exit**: split into two branches.
   - `softc->periph == NULL` (never enabled): kfree directly (safe — no periph).
   - `softc->periph != NULL` AND `!(state & LUN_ENABLED)` (disabled via ioctl):
     now tears down the periph via `cam_periph_acquire` → `cam_periph_invalidate`
     → `cam_periph_release` which invokes `targdtor` → frees softc safely.

2. **`targclose` success path**: removed `kfree(softc)`; softc is freed by
   `targdtor` via the final `cam_periph_release`.

3. **`targdtor`**: added `kfree(softc, M_TARG)` after queue draining and before
   clearing `periph->softc = NULL`.

This ensures softc is freed exactly once and only after `targdtor` has finished
all queue operations. `periph->softc` is cleared immediately after the free.

## PoC changes

- `tgt_uaf.c` — unchanged (the reviewer-written trigger; compiles cleanly).
- `build.sh` / `run.sh` — added reproducible build/run scripts.
- `test_targ.c` — added a direct open(`/dev/targ`) + `TARGIOCENABLE` test for
  the fix-validation kernel (the original PoC opens `/dev/targN` which doesn't
  exist until the autoclone handler runs on `/dev/targ`).
- `fix.diff` — authored the git-apply-able fix (supersedes finding proposal;
  same approach, cleaner implementation of the early-exit split).

## Kernel references (confirmed)

- `sys/bus/cam/scsi/scsi_target.c:240` — `kfree(softc, M_TARG)` before release (BUG)
- `sys/bus/cam/scsi/scsi_target.c:245` — `cam_periph_release` → `targdtor` on freed softc
- `sys/bus/cam/scsi/scsi_target.c:216-222` — early-exit that kfrees softc with periph dangling
- `sys/bus/cam/scsi/scsi_target.c:534` — `targdtor` reads `softc = periph->softc` (dangling)
- `sys/bus/cam/scsi/scsi_target.c:541-550` — `targdtor` dereferences freed softc
- `sys/bus/cam/scsi/scsi_target.c:435` — `PIT_PROCESSOR` gate (why TARGIOCENABLE fails on ATA)
- `sys/bus/cam/cam_periph.c:215` — periph starts at refcount=0
- `sys/bus/cam/cam_periph.c:370-374` — slow-path release → `camperiphfree` → `targdtor`
- `sys/bus/cam/cam_periph.c:574` — invalidate only frees if refcount==0
- `sys/bus/cam/cam_periph.c:600-601` — `camperiphfree` calls `periph_dtor`
- `sys/conf/files:45` — `scsi_target.c optional targ`
- `sys/config/X86_64_GENERIC` — no `device targ` (verified)
