# DF-2532 — xdisk xa_start no-spans fail path KKASSERT panic

## Verdict: REPRODUCED (panic)

## Summary

The `xa_start()` failure path for "no spans available, bio is allowed to
fail" (xdisk.c:976–984, the `else` branch) calls `xa_done(tag, 1)` **without
clearing `tag->bio`**.  `xa_done()` (xdisk.c:1006–1017) opens with
`KKASSERT(tag->bio == NULL)`, which is compiled in by `options INVARIANTS`
(enabled in the default `X86_64_GENERIC` kernel).  The result is a
**guaranteed kernel panic** whenever a BIO with `B_FAILONDIS` set is issued
against an xa device that has no valid spans.

## Mechanism (line-by-line trace)

1. **Trigger**: A BIO with `B_FAILONDIS` set is issued against an xa device
   whose `spanq` is empty (all spans deleted / target disconnected).
   This happens during the disk framework's **automatic label probe**
   (`disk_setdiskinfo` → `DISK_DISK_PROBE` → `disk_probe` → `mbrinit` →
   reads with `B_FAILONDIS`, `subr_diskmbr.c:134`).

2. **`xa_strategy()`** (xdisk.c:799) dispatches the BIO:
   - `xa_setup_cmd()` (xdisk.c:846) allocates a tag, sets `tag->bio = bio`
     (xdisk.c:856).
   - Calls `xa_start(tag, NULL, 1)` (xdisk.c:816).

3. **`xa_start()`** (xdisk.c:873): `msg == NULL`, enters the bio-dispatch
   block (xdisk.c:882).  Checks for a valid span:
   ```c
   if (sc->opencnt == 0 || sc->open_tag == NULL) {
       TAILQ_FOREACH(trans, &sc->spanq, user_entry) {
           if ((trans->rxcmd & DMSGF_DELETE) == 0) break;
       }
   }
   if (trans == NULL) goto skip;   // ← spanq empty
   ```
   No valid span → `trans == NULL` → `goto skip`.

4. **`skip:` label** (xdisk.c:956): `msg` is NULL.  Checks `B_FAILONDIS`:
   ```c
   } else if (tag->bio &&
              (tag->bio->bio_buf->b_flags & B_FAILONDIS) == 0) {
       // requeue path (B_FAILONDIS NOT set) — correctly clears tag->bio
       tag->bio = NULL;
       xa_done(tag, 1);
   } else {
       // FAIL path (B_FAILONDIS set) — THE BUG
       tag->status.head.error = DMSG_ERR_IO;
       xa_done(tag, 1);   // ← tag->bio STILL SET (xdisk.c:982)
   }
   ```

5. **`xa_done()`** (xdisk.c:1006):
   ```c
   KKASSERT(tag->bio == NULL);   // ← PANIC: tag->bio is non-NULL (xdisk.c:1009)
   ```

6. **Panic**:
   ```
   panic: assertion "tag->bio == NULL" failed in xa_done at xdisk.c:1009
   ```

## Exploit chain / escalation

**Not a memory-corruption bug** — this is a logic/state assertion bug (DoS).
The KKASSERT panics the kernel before any corruption can occur.  There is no
write primitive, UAF, or type confusion.  Impact is **denial of service
(kernel panic)**.

The only victim of `xa_release()` (which `xa_done` would call if the
KKASSERT didn't fire) handles `tag->bio != NULL` correctly — it completes
the bio with `EIO` and clears `tag->bio`.  So on a non-INVARIANTS kernel
the code path is actually correct; the bug is purely the **incorrect
assertion** at xdisk.c:1009.

## Reproduction

### Deterministic harness (primary PoC)

A kernel module (`df2532_harness.ko`) that `#include`s the real `xdisk.c`
(shipping code, static functions and all) and adds a sysctl trigger that
constructs the exact impossible state and calls `xa_start(tag, NULL, 0)`:

```sh
kldload ./df2532_harness.ko   # xdisk.ko must NOT be loaded
sysctl -w debug.df2532_trigger=1
# → panic: assertion "tag->bio == NULL" failed in xa_done at xdisk.c:1009
```

**Privilege**: root (`kldload` + sysctl write).  Realistic precondition:
admin loads xdisk driver + configures a remote block target.  When the
target disconnects (span table empties), the next disk-label probe BIO
triggers the panic.  Any user-level action that triggers a disk reprobe
(e.g. `camcontrol`, `disklabel`, or the async probe at device creation)
can trip it; no special privilege beyond access to the xa device node.

### DMSG userland PoC (supplementary)

`df2532.c` creates a DMSG peer via `socketpair` + `XDISKIOCATTACH`, sends
`LNK_SPAN CREATE` then `DELETE`, and races span removal against the
kernel's async disk label probe.  The race is timing-dependent (the async
probe is fast); the harness module provides the deterministic demonstration.

## Fix

**Remove the incorrect `KKASSERT(tag->bio == NULL)`** at xdisk.c:1009.
`xa_release()` (called from `xa_done` when `tag->async`) already handles
`tag->bio != NULL` correctly — it completes the bio with `EIO` and clears
`tag->bio` (xdisk.c:1031–1040).  The assertion was wrong: `xa_done` CAN
legitimately be called with `tag->bio` set, and the existing code handles
it correctly once the assertion is removed.

```diff
--- a/sys/dev/disk/xdisk/xdisk.c
+++ b/sys/dev/disk/xdisk/xdisk.c
@@ -1006,7 +1006,7 @@
 static void
 xa_done(xa_tag_t *tag, int wasbio)
 {
-	KKASSERT(tag->bio == NULL);
+	/* tag->bio may be non-NULL here; xa_release handles it (DF-2532) */
 
 	tag->state = NULL;
 	tag->done = 1;
```

### Fix validation

- **Baseline (unpatched)**: harness trigger → `panic: assertion "tag->bio
  == NULL" failed in xa_done at xdisk.c:1009`, guest crashes (DDB prompt).
- **Patched (fix.diff applied)**: harness trigger → no panic; `xa_done`
  runs cleanly; "survived" printed; guest stays up.

## PoC changes

Authored from scratch (no existing PoC):
- `df2532_harness.c` — kernel module including real `xdisk.c` with sysctl trigger
- `df2532.c` — userland DMSG peer (supplementary, race-based)
- `Makefile` — kld module build for the harness
- `fix.diff` — one-line fix removing the incorrect KKASSERT
