# DF-0018 — VERDICT

**REPRODUCED.** Duplicate DELETE for the same DMSG msgid panics the default
`X86_64_GENERIC` (INVARIANTS-on) DragonFly kernel via the `KKASSERT` at
`sys/kern/kern_dmsg.c:1076`.  Fix validated: a single-fix kernel that adds
a benign-discard guard before the assertion handles the same input
gracefully.

## Verdict
**REPRODUCED + FIX VALIDATED.**

## Mechanism (line-by-line)

The bug is in `kdmsg_state_msgrx()` (`sys/kern/kern_dmsg.c`), the
receive-side state machine driven by `kdmsg_iocom_thread_rd`.  The reader
is single-threaded per iocom; the writer (`kdmsg_iocom_thread_wr`)
transmits replies asynchronously.

1. **CREATE for msgid=N** -- case `DMSGF_CREATE` at `:851` allocates a new
   state (`:898`), sets `state->rxcmd = msg->any.head.cmd & ~DMSGF_DELETE`
   (`:912`, so rxcmd has `DMSGF_CREATE` but not `DMSGF_DELETE`),
   `RB_INSERT`s into `iocom->staterd_tree` (`:916`), and returns
   `error=0` (`:918`).
2. **DELETE-1 for msgid=N** -- case `DMSGF_DELETE` at `:920`.  RB_FIND
   finds the state.  The two early-out checks both fail:
   - `:925` `state == &iocom->state0` -- false (state is real);
   - `:943` `(state->rxcmd & DMSGF_CREATE) == 0` -- false (CREATE still set).
   So `error=0` (`:955`).  Post-callback, the DELETE post-block at `:1075`
   runs: `:1077` `state->rxcmd |= DMSGF_DELETE`; `:1078`
   `if (state->txcmd & DMSGF_DELETE)` is **FALSE** (the writer hasn't
   transmitted our REPLY|DELETE yet), so the `RB_REMOVE` at `:1088` is
   **NOT taken**.  The state stays in `staterd_tree` with rxcmd now
   carrying both `DMSGF_CREATE` and `DMSGF_DELETE`.
3. **DELETE-2 for msgid=N** -- RB_FIND still finds the state (it was never
   removed).  Same two early-out checks; both still fail; `error=0`.
   Reaches `:1075`.  `:1076`
   `KKASSERT((state->rxcmd & DMSGF_DELETE) == 0)` -- rxcmd now HAS
   `DMSGF_DELETE` -- **assertion fires -> kernel panic**.

The race window is between the reader finishing DELETE-1's cleanuprx and
the writer transmitting the REPLY|DELETE; sending both DELETEs
back-to-back wins it trivially (the reader processes them in sequence,
the writer has to dequeue + `fp_write` its reply).

`KKASSERT` is `#ifdef INVARIANTS` (`sys/sys/systm.h:94-122`); the default
`X86_64_GENERIC` ships `options INVARIANTS`, so the panic occurs on the
default kernel.  On a non-INVARIANTS kernel the assertion compiles to a
no-op and the duplicate DELETE is absorbed benignly -- so this is an
INVARIANTS-only DoS, same class as DF-0001.

## Reproduction

The harness (`harness.c` + `Makefile`) builds a kernel module
(`df18_harness.ko`) that creates a private `kdmsg_iocom` over an
in-kernel `kern_pipe(2)` (no `AUTO*` flags, so no autoinitiate
complications) and writes three raw dmsg headers into the pipe's write
end:

  1. CREATE msgid=42 (`LNK_PAD | DMSGF_CREATE = 0x80000001`)
  2. DELETE msgid=42 (`LNK_PAD | DMSGF_DELETE = 0x40000001`)
  3. DELETE msgid=42 (`LNK_PAD | DMSGF_DELETE = 0x40000001`)

The kernel's `kdmsg_iocom_thread_rd` drains them in order and panics on
the third.  `kldload`-ing the module reproduces deterministically (no
timing race needed -- the writer thread never gets to run between the two
DELETEs because the module writes them all before yielding).

### Panic signature (unpatched `#0`)

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

Call chain: reader thread -> `kdmsg_msg_receive_handling` ->
`kdmsg_state_msgrx` -> `KKASSERT` at `:1076`.  Exactly the path the
finding cites.

## Why a harness (vs. a userland-only trigger)

The threat model is "a DMSG peer" -- reachable via the HAMMER2 cluster
relay daemon (network vector, matches CVSS `AV:N`), via `DIOCRECLUSTER`
on a disk device node (requires operator/wheel privilege on the device),
or via the xdisk virtual block device.  None of these is reachable from
an unprivileged user on this guest (maxx uid 1001 is not in `operator`).
The realistic peer position is therefore a privileged one, which the
`kldload`-ed harness models directly: it exercises the exact receive
path that any peer-driven bytes would, without the DIOCRECLUSTER
autoinitiate-thread / socketpair-buffer deadlock that the userland
`socketpair + DIOCRECLUSTER` trigger hits on this particular guest
(environmental, unrelated to this bug).

This is a **code-level harness** in the sense the procedure allows for
too-narrow-to-reach-from-unprivileged findings; the bug it triggers is
the same KKASSERT any peer-driven bytes would trip.

## Impact / ceiling

**Low.** INVARIANTS-only kernel DoS (panic).  No memory corruption, no
info leak, no integrity impact, no escalation primitive -- the assertion
aborts the kernel before any further state manipulation.  Same class as
DF-0001.  CVSS `AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H` matches (the
network vector is unauthenticated from a packet standpoint; AC:H reflects
the "must be an established cluster peer" precondition).

No escalation chain applicable -- this is a pure DoS.

## Exploit chain

None (non-corruption class: pure DoS via INVARIANTS assertion).

## Fix

`fix.diff` inserts a benign-discard guard immediately before the
`error = 0;` fall-through of the DELETE case (`kern_dmsg.c:955`), after
the existing "msgid reused" check at `:943-954`:

```c
/*
 * A duplicate DELETE can race our reply transmission ...
 */
if (state->rxcmd & DMSGF_DELETE) {
    kdio_printf(iocom, 1, "%s\n", "msgrx: duplicate DELETE");
    error = EALREADY;
    break;
}
```

This mirrors the existing `EALREADY` handling for the ABORT+DELETE races
at `:926-930` and `:944-947`.  The duplicate DELETE is logged, error is
set to `EALREADY`, and `kdmsg_msg_receive_handling` (`:689-690`) treats
`EALREADY` as a benign discard.  **Functionally identical to the
finding's `## Recommended fix` proposal.**

## Fix validation (Phase 8)

- **Baseline** (`#0`, unpatched): harness panics with the assertion
  signature above.  (`run.log`, `panic.txt`.)
- **Patched** (`#1`, single-fix via `make installkernel`):
  `kern.version = DragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 14:16:09 UTC 2026`
  `sha256(/boot/kernel/kernel) = f25163731649235e8032546312974e93ef8fa3eab1b25031749ad511b4e30d29`
  Same harness input -> NO panic; prints
  `kdmsg: msgrx: duplicate DELETE` and `kdmsg: msgrx: state=0xffff... error 37`,
  then `df18: harness completed without panic`.  (`fix_run.log`.)
- **Before/after contrast**: panic-vs-EALREADY on identical input; the
  fix closes the bug.

## PoC changes

- Added `harness.c` + `Makefile` -- the deterministic in-kernel harness
  used to reproduce the panic.  The original reviewer PoC
  (`kdmsg_dupdelete.c`) is unchanged.
- Added `trigger.c` -- a self-contained userland trigger via `socketpair`
  + `DIOCRECLUSTER`; documented its environmental hang on this guest
  (unrelated to the bug).
- Added `fix.diff`, `build.sh`, `run.sh`, `panic.txt`, `build.log`,
  `run.log`, `fix_build.log`, `fix_run.log`, `env.txt`, `VERDICT.md`,
  `manifest.json`.
- Updated `README.md` to describe the harness path and the fix.
