# DF-0018 — PoC

**Duplicate DELETE for the same DMSG msgid trips the `KKASSERT` at
`kern_dmsg.c:1076` and panics an INVARIANTS (default GENERIC) DragonFly
kernel.**

## The bug

`kdmsg_state_msgrx()` (in `sys/kern/kern_dmsg.c`) unconditionally asserts
on a received DELETE:

```c
KKASSERT((state->rxcmd & DMSGF_DELETE) == 0);   /* kern_dmsg.c:1076 */
```

A peer sends CREATE for msgid N, then two DELETEs for msgid N back-to-back.
The reader thread processes them sequentially:

1. **CREATE** (case `DMSGF_CREATE`, `:851`): allocates a new state, sets
   `state->rxcmd = msg->any.head.cmd & ~DMSGF_DELETE` (so rxcmd has
   `DMSGF_CREATE`, no `DMSGF_DELETE`), RB_INSERTs into `staterd_tree`
   (`:916`).
2. **DELETE-1** (case `DMSGF_DELETE`, `:920`): state found via RB_FIND;
   the early-out checks at `:925` (state==state0) and `:943`
   (`(rxcmd & DMSGF_CREATE) == 0`) do NOT match (CREATE still set), so
   `error=0`.  At `:1075`, the DELETE post-block runs: `:1077`
   `state->rxcmd |= DMSGF_DELETE`; `:1078` `if (state->txcmd & DMSGF_DELETE)`
   is FALSE (the writer has not yet transmitted our REPLY|DELETE), so
   **`RB_REMOVE` is NOT taken** -- the state stays in the tree.
3. **DELETE-2**: RB_FIND finds the same state still in `staterd_tree`;
   same early-out checks; `error=0`; reaches `:1075`; `:1076`
   `KKASSERT((state->rxcmd & DMSGF_DELETE) == 0)` -- rxcmd now HAS
   `DMSGF_DELETE` (set in step 2) -- **assertion fires -> kernel panic**.

The race window (between the reader finishing DELETE-1's cleanuprx and the
writer transmitting the REPLY|DELETE that would set `txcmd |= DMSGF_DELETE`
and remove the state) is won trivially by sending both DELETEs back-to-back,
because the reader is single-threaded per iocom and the writer must dequeue
+ `fp_write` the reply.

`KKASSERT` is `#ifdef INVARIANTS` (`sys/sys/systm.h:94-122`); the default
`X86_64_GENERIC` ships `options INVARIANTS`, so the panic occurs on default
kernels.  On a non-INVARIANTS kernel the assertion compiles to a no-op and
the duplicate DELETE is absorbed benignly (rxcmd `|= DMSGF_DELETE` is
idempotent; the `RB_REMOVE` branch is not taken because txcmd lacks DELETE).

## Severity / impact

Low.  INVARIANTS-kernel DoS only; no integrity/confidentiality impact, no
escalation path (panic aborts the kernel before any further primitive can
be derived).  Same class as DF-0001.

## Threat model & reachability

The bug is in the kdmsg protocol state machine, exercised by every
received DELETE.  Reachable by any DMSG peer:

- the **HAMMER2 cluster relay daemon** over the cluster network
  (the network vector -- matches the finding's CVSS `AV:N`),
- a **disk cluster peer** via `DIOCRECLUSTER` on a disk device node
  (requires operator/wheel privilege on the device node), or
- the **xdisk** virtual block device peer.

The PoC needs an "iocom to talk to".  On this guest there is no
cluster-link to inject bytes into from an unprivileged user, so this
folder ships two equivalent demonstrations:

- `kdmsg_dupdelete.c` (original reviewer PoC) -- writes raw dmsg bytes to a
  fd passed as `argv[1]`.  Use when you already have a connected DMSG
  socket end (e.g. via a HAMMER2 relay).  Untouched.
- `trigger.c` -- a self-contained userland trigger that gets a kernel
  iocom via `socketpair(2)` + `DIOCRECLUSTER` on a disk device node.
  Requires operator/wheel privilege (matches the disk-cluster threat
  model).  NOTE: on this guest the `DIOCRECLUSTER` path exhibited an
  environmental hang unrelated to this bug (the disk-iocom autoinitiate
  path deadlocks against the socketpair); use `harness.c` for a
  deterministic reproduction.
- `harness.c` + `Makefile` -- **the deterministic in-kernel harness that
  was used to validate this finding.**  It creates a private
  `kdmsg_iocom` over an in-kernel `kern_pipe(2)`, with no `AUTO*` flags
  (so no autoinitiate complications), and writes the three header bytes
  (CREATE, DELETE, DELETE for msgid=42) into the pipe's write end.  The
  kernel's `kdmsg_iocom_thread_rd` reader drains them in order and
  panics at `kern_dmsg.c:1076`.  Loading it requires root (`kldload`),
  which models the cluster-peer privilege position the bug requires.

## Build & run

Inside the guest, as root:

```
cd /tmp/df18_harness     # or wherever you copied harness.c + Makefile
./build.sh               # -> df18_harness.ko
./run.sh                 # kldload ./df18_harness.ko
```

### Expected on the **unpatched** kernel (INVARIANTS on, default GENERIC)

```
panic: assertion "(state->rxcmd & DMSGF_DELETE) == 0" failed in kdmsg_state_msgrx at /usr/src/sys/kern/kern_dmsg.c:1076
cpuid = 5
Trace beginning at frame 0xfffff80117f5b940
kdmsg_msg_receive_handling() at kdmsg_msg_receive_handling+0xb39 0xffffffff80636d39
kdmsg_iocom_thread_rd() at kdmsg_iocom_thread_rd+0x98 0xffffffff806380e8
Debugger("panic")
Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)
db>
```

(Full panic signature in `panic.txt`; full boot-log excerpt in `run.log`.)

### Expected on the **fixed** kernel

```
df18: iocom up; writing CREATE+2xDELETE for msgid=42
kdmsg: msgrx: duplicate DELETE
kdmsg: msgrx: state=0xfffff80117554a40 error 37
df18: harness completed without panic ...
```

No panic, no wedge; the duplicate DELETE is discarded as `EALREADY` (errno
37) via the new guard added by `fix.diff`.  (Full excerpt in `fix_run.log`.)

## Fix

`fix.diff` -- inserts a benign-discard guard before the `error = 0;` fall-
through of the DELETE case, mirroring the existing `EALREADY` handling for
ABORT+DELETE races (`:926-930`, `:944-947`).  When `state->rxcmd` already
has `DMSGF_DELETE`, the duplicate DELETE is logged, error is set to
`EALREADY`, and the switch breaks -- never reaching the KKASSERT.  This
matches (and is functionally identical to) the `## Recommended fix` block
in `findings/DF-0018-*.md`.

Validated: the patched `#1` kernel handles the same input that panicked
`#0` without incident (`fix_run.log`).
