β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0018

Duplicate DELETE for same DMSG msgid triggers KKASSERT panic (DoS)

Field Value
ID DF-0018
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE CWE-617 Reachable Assertion
File sys/kern/kern_dmsg.c
Lines 1076
Area kern
Confidence likely
Discovered 2026-06-29
Reported pending

Summary

kdmsg_state_msgrx() unconditionally asserts KKASSERT((state->rxcmd & DMSGF_DELETE) == 0) at kern_dmsg.c:1076 when processing a received DELETE. A peer can send two DELETE messages for the same transaction msgid back-to-back. The first DELETE sets state->rxcmd |= DMSGF_DELETE but does not remove the state from the RB tree (removal requires txcmd to also carry DELETE, which depends on the writer thread transmitting the reply). The second DELETE finds the same state still in the tree and reaches :1076, where the assertion fires. Note: in DragonFlyBSD both KASSERT and KKASSERT are #ifdef INVARIANTS (sys/sys/systm.h:94-118) β€” the assertion is INVARIANTS-only, so the panic occurs on debug/INVARIANTS kernels; on a production (non-INVARIANTS) kernel the KKASSERT compiles to a no-op and the second DELETE is absorbed benignly (rxcmd |= DMSGF_DELETE is idempotent and the txcmd & DMSGF_DELETE RB_REMOVE branch is not taken because the writer has not yet transmitted the reply). This is the same INVARIANTS-only class as DF-0001; recorded as an INVARIANTS-kernel remote DoS (Low).

Root cause

sys/kern/kern_dmsg.c:1075-1093:

} else if (msg->any.head.cmd & DMSGF_DELETE) {
    KKASSERT((state->rxcmd & DMSGF_DELETE) == 0);   /* :1076 */
    state->rxcmd |= DMSGF_DELETE;
    if (state->txcmd & DMSGF_DELETE) {
        ...
        RB_REMOVE(kdmsg_state_tree, &iocom->staterd_tree, state);  /* :1088 */
        ...
    }
}

The state is found by RB_FIND and remains in the tree after the first DELETE because RB_REMOVE only runs when both rxcmd and txcmd carry DELETE (:1078). The DELETE switch-case guard (:943) checks for msgid reuse ((state->rxcmd & DMSGF_CREATE) == 0), not for a pre-existing DELETE flag. The window between the reader finishing the first DELETE and the writer transmitting the reply is the race window; two back-to-back DELETEs win it reliably.

Threat model & preconditions

  • Attacker position: a DMSG peer (same reachability as DF-0017: the userland hammer2 relay daemon over the cluster network, or locally via DIOCRECLUSTER on a disk device node). CRC not verified on receive.
  • Privileges gained or impact: kernel panic (full-system DoS). No integrity/ confidentiality impact.
  • Required config or capabilities: a reachable DMSG link (HAMMER2 clustering in use for the network vector).
  • Reachability: CREATE a msgid, then two rapid DELETEs for it.

Proof of concept

PoC source: findings/poc/DF-0018/kdmsg_dupdelete.c

Sends a CREATE for msgid 42, then two back-to-back DELETEs for msgid 42.

Build & run

cc -o kdmsg_dupdelete findings/poc/DF-0018/kdmsg_dupdelete.c
./kdmsg_dupdelete <connected-dmsg-fd>     # see DF-0017 for obtaining the fd

Expected output

panic: (state->rxcmd & DMSGF_DELETE) == 0

Impact

Reliable (race-favored, easily won by sending both DELETEs back-to-back) INVARIANTS-kernel panic from any DMSG peer; on a production (non-INVARIANTS) kernel the second DELETE is absorbed benignly (the KKASSERT is a no-op and txcmd lacks DELETE so no double-RB_REMOVE). Low (INVARIANTS-only DoS, same class as DF-0001).

Discard a duplicate DELETE benignly rather than asserting, mirroring the existing EALREADY handling for the ABORT+DELETE races (:926-930, :944-947):

--- a/sys/kern/kern_dmsg.c
+++ b/sys/kern/kern_dmsg.c
@@ -942,6 +942,13 @@
                break;
            }
        }
+
+       /*
+        * A duplicate DELETE can race our reply transmission.
+        * Discard it silently rather than tripping the KKASSERT below.
+        */
+       if (state->rxcmd & DMSGF_DELETE) {
+           error = EALREADY;
+           break;
+       }
        error = 0;
        break;

References

Timeline

  • 2026-06-29 Discovered during automated file-by-file audit of sys/kern/kern_dmsg.c.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0018 Β· 17 files
FileTypeDescriptionSize
kdmsg_dupdelete.c trigger-source original reviewer PoC -- writes raw dmsg bytes to a connected DMSG fd (unchanged) 2.7 KB view raw
trigger.c trigger-source userland trigger via socketpair + DIOCRECLUSTER (hits environmental hang on this guest; documented) 3.9 KB view raw
harness.c trigger-source deterministic in-kernel harness used to validate the finding -- creates a private kdmsg_iocom over kern_pipe and writes CREATE+2xDELETE 4.4 KB view raw
Makefile build-script kld build for harness.c against /usr/src/sys 150 B ↓ download
build.sh build-script guest-side build wrapper -> df18_harness.ko 264 B view raw
run.sh run-script guest-side kldload wrapper 519 B view raw
fix.diff suggested-fix git-apply-able: insert EALREADY-discard guard before kern_dmsg.c:955 to skip duplicate DELETE 647 B view raw
panic.txt panic-signature KKASSERT panic at kern_dmsg.c:1076 from boot.log 540 B view raw
build.log build-log successful kld build, full output 1.1 KB view raw
run.log run-log decisive unpatched-kernel run, full output incl panic 2.1 KB view raw
fix_build.log build-log single-fix kernel build/install/boot log (#0 -> #1) 1.9 KB view raw
fix_run.log run-log SAME harness on patched #1 kernel -- no panic, EALREADY handling 1.8 KB view raw
env.txt environment uname, cc version, INVARIANTS config, reachability notes 911 B view raw
README.md readme human-facing build/run/expected + mechanism 5.8 KB ↓ raw
VERDICT.md verdict full narrative: reproduced, mechanism, fix validation 7.1 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing build/run/expected + mechanism
↓ download raw

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:

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).

VERDICT.md verdict full narrative: reproduced, mechanism, fix validation
↓ download raw

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_INSERTs 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:

/*
 * 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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. The kld harness that panicked the unpatched #0 baseline (KKASSERT at kern_dmsg.c:1076) is handled gracefully by the single-fix #1 kernel: prints 'duplicate DELETE' + 'error 37 (EALREADY)', then 'harness completed without panic'. Clean before/after.

baseline #0: panic: assertion rxcmd&DMSGF_DELETE==0 at kern_dmsg.c:1076 / db>. patched #1: kdmsg: duplicate DELETE / error 37 / harness completed without panic.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 14:16:09 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (sha256 f25163731649235e8032546312974e93ef8fa3eab1b25031749ad511b4e30d29)

Confirmed kernel references

Detail

Exploit chain

none (non-corruption class: pure INVARIANTS-only DoS via KKASSERT panic; no memory primitive, no escalation derivable).

Evidence (decisive lines)

UNPATCHED #0: panic: assertion "(state->rxcmd & DMSGF_DELETE) == 0" failed in kdmsg_state_msgrx at kern_dmsg.c:1076 / Stopped at Debugger+0x7c / db>. PATCHED #1: kdmsg: msgrx: duplicate DELETE / error 37 (EALREADY) / harness completed without panic.

PoC changes

Added harness.c + Makefile (deterministic in-kernel kld over kern_pipe writing CREATE+2xDELETE for msgid=42). Added trigger.c (userland socketpair+DIOCRECLUSTER trigger). Added fix.diff (EALREADY-discard guard before kern_dmsg.c:955). Added build.sh, run.sh, VERDICT.md, manifest.json, panic.txt, full logs.

Verified recommended fix

Insert a benign-discard guard before the DELETE case fall-through in kdmsg_state_msgrx (sys/kern/kern_dmsg.c:955): if (state->rxcmd & DMSGF_DELETE) { kdio_printf(...); error = EALREADY; break; }. kdmsg_msg_receive_handling already treats EALREADY as a benign discard (:689-690). Matches finding's proposal. Full git-apply-able diff in findings/poc/DF-0018/fix.diff.

Verdict

REPRODUCED + FIX VALIDATED. The KKASSERT at sys/kern/kern_dmsg.c:1076 fires on a duplicate DELETE for the same DMSG msgid. DELETE-1 sets rxcmd|=DMSGF_DELETE but RB_REMOVE is not taken (txcmd&DMSGF_DELETE is FALSE); DELETE-2 re-finds the same state, reaches :1076 where rxcmd now HAS DMSGF_DELETE -> assertion fires -> kernel panic. Reproduced deterministically with an in-kernel kld harness over kern_pipe writing CREATE+2xDELETE for msgid=42.