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
DIOCRECLUSTERon 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).
Recommended fix
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
sys/kern/kern_dmsg.c:1076βKKASSERT((state->rxcmd & DMSGF_DELETE) == 0).sys/kern/kern_dmsg.c:1078-1093βRB_REMOVErequirestxcmdDELETE.- CWE-617 Reachable Assertion.
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)
PoC verification
Evidence pack
findings/poc/DF-0018 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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:
- CREATE (case
DMSGF_CREATE,:851): allocates a new state, setsstate->rxcmd = msg->any.head.cmd & ~DMSGF_DELETE(so rxcmd hasDMSGF_CREATE, noDMSGF_DELETE), RB_INSERTs intostaterd_tree(:916). - 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), soerror=0. At:1075, the DELETE post-block runs::1077state->rxcmd |= DMSGF_DELETE;:1078if (state->txcmd & DMSGF_DELETE)is FALSE (the writer has not yet transmitted our REPLY|DELETE), soRB_REMOVEis NOT taken -- the state stays in the tree. - DELETE-2: RB_FIND finds the same state still in
staterd_tree; same early-out checks;error=0; reaches:1075;:1076KKASSERT((state->rxcmd & DMSGF_DELETE) == 0)-- rxcmd now HASDMSGF_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
DIOCRECLUSTERon 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 asargv[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 viasocketpair(2)+DIOCRECLUSTERon a disk device node. Requires operator/wheel privilege (matches the disk-cluster threat model). NOTE: on this guest theDIOCRECLUSTERpath exhibited an environmental hang unrelated to this bug (the disk-iocom autoinitiate path deadlocks against the socketpair); useharness.cfor a deterministic reproduction.harness.c+Makefile-- the deterministic in-kernel harness that was used to validate this finding. It creates a privatekdmsg_iocomover an in-kernelkern_pipe(2), with noAUTO*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'skdmsg_iocom_thread_rdreader drains them in order and panics atkern_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).
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.
- CREATE for msgid=N -- case
DMSGF_CREATEat:851allocates a new state (:898), setsstate->rxcmd = msg->any.head.cmd & ~DMSGF_DELETE(:912, so rxcmd hasDMSGF_CREATEbut notDMSGF_DELETE),RB_INSERTs intoiocom->staterd_tree(:916), and returnserror=0(:918). - DELETE-1 for msgid=N -- case
DMSGF_DELETEat:920. RB_FIND finds the state. The two early-out checks both fail: -:925state == &iocom->state0-- false (state is real); -:943(state->rxcmd & DMSGF_CREATE) == 0-- false (CREATE still set). Soerror=0(:955). Post-callback, the DELETE post-block at:1075runs::1077state->rxcmd |= DMSGF_DELETE;:1078if (state->txcmd & DMSGF_DELETE)is FALSE (the writer hasn't transmitted our REPLY|DELETE yet), so theRB_REMOVEat:1088is NOT taken. The state stays instaterd_treewith rxcmd now carrying bothDMSGF_CREATEandDMSGF_DELETE. - 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.:1076KKASSERT((state->rxcmd & DMSGF_DELETE) == 0)-- rxcmd now HASDMSGF_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:
- CREATE msgid=42 (
LNK_PAD | DMSGF_CREATE = 0x80000001) - DELETE msgid=42 (
LNK_PAD | DMSGF_DELETE = 0x40000001) - 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 viamake installkernel):kern.version = DragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 14:16:09 UTC 2026sha256(/boot/kernel/kernel) = f25163731649235e8032546312974e93ef8fa3eab1b25031749ad511b4e30d29Same harness input -> NO panic; printskdmsg: msgrx: duplicate DELETEandkdmsg: msgrx: state=0xffff... error 37, thendf18: 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 viasocketpair 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.mdto describe the harness path and the fix.
Fix verification
fixedVALIDATED. 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.
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.
No comments yet.