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

NUL-termination write underflows when aux_size==0 (latent OOB write, dead code today; live twin at hammer2_iocom.c:149)

Field Value
ID DF-0936
Status new
Severity Info
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:N
CWE CWE-787 Out-of-bounds Write
File sys/vfs/hammer2/hammer2_msgops.c
Lines 72-74
Area vfs (hammer2)
Confidence certain
Discovered 2026-07-05
Reported pending
Known CVE none
CVE match dfly_specific

Summary

hammer2_msg_dbg_rcvmsg writes msg->aux_data[msg->aux_size - 1] = 0 with only an aux_data != NULL guard. aux_size is size_t (unsigned, sys/sys/dmsg.h:774), so if the precondition aux_data != NULL && aux_size == 0 ever arises, the index underflows to SIZE_MAX and the kernel writes a NUL byte at aux_data + 2^64-1 β€” a guaranteed page-fault panic or worse. The bug is currently unreachable because the kdmsg receive path (kern_dmsg.c:380-391) only allocates aux_data when aux_size > 0, and both functions in this file are dead code (no callers in sys/). The identical pattern is live at hammer2_iocom.c:149-152 with the same protection.

Root cause

At hammer2_msgops.c:72-74:

case DMSG_DBG_SHELL | DMSGF_REPLY:
    if (msg->aux_data) {
        msg->aux_data[msg->aux_size - 1] = 0;
        kprintf("DEBUGMSG: %s\n", msg->aux_data);
    }

The guard tests only aux_data != NULL. The subtraction aux_size - 1 is on a size_t (sys/sys/dmsg.h:774 declares size_t aux_size;), so when aux_size == 0 it evaluates to (size_t)-1 = 0xFFFFFFFFFFFFFFFF on amd64, and the assignment writes a NUL byte at that offset. The author assumed aux_data != NULL implies aux_size > 0, which is an undocumented invariant maintained only by kern_dmsg.c:380-391 (if (msg->aux_size) { ... kmalloc ... }). The invariant is not enforced locally and is easy to break.

Auxiliary reachability notes:

  • hammer2_msg_adhoc_input (hammer2_msgops.c:42-47) is harmless (prints only the 32-bit cmd).
  • hammer2_msg_dbg_rcvmsg is dead code: rg finds only the definitions (hammer2_msgops.c:43,50) and the prototypes (hammer2.h:1869-1870). No C code in the tree takes their address or calls them.
  • The live twin is hammer2_rcvdmsg at hammer2_iocom.c:149-152; same bug shape, same kdmsg receive-path protection.

Threat model & preconditions

  • Attacker position: No attacker-reachable path exists today. To trigger from an unauthenticated cluster peer one would first need a code change that violates the kdmsg receive invariant (aux_data allocated only when aux_size > 0), or that wires hammer2_msg_dbg_rcvmsg/hammer2_msg_adhoc_input up to a msg source that does not preserve it.
  • Privileges gained or impact: If such a change is ever made, an unauthenticated HAMMER2 cluster peer (plain TCP, no auth in the wire protocol) sending a DMSG_DBG_SHELL | DMSGF_REPLY message with aux_bytes == 0 in the header but with aux_data populated by some other path would convert this into an immediate kernel panic (write to unmapped address) or, if the address space maps that offset, an arbitrary kernel-memory NUL byte write.
  • Required config or capabilities: None today (dead code + invariant holds).
  • Reachability: Not reachable against an unmodified tree.

Proof of concept

No working exploit can be produced against an unmodified tree because (a) the function is never called and (b) the kdmsg receive path never produces aux_data != NULL with aux_size == 0. A demonstrative PoC must synthesize the precondition in-kernel (KLD), which only proves the bug exists in the code; it cannot be triggered by an unprivileged user or a network peer against a stock kernel.

Impact

Nil today (dead code + invariant holds). Reported as defense-in-depth / hardening: the bug is a landmine waiting for a future code change, and the live twin at hammer2_iocom.c:149-152 carries the same risk.

Guard the write with aux_size != 0, making the function self-defensive regardless of the caller's invariant:

--- a/sys/vfs/hammer2/hammer2_msgops.c
+++ b/sys/vfs/hammer2/hammer2_msgops.c
@@ -69,8 +69,11 @@ hammer2_msg_dbg_rcvmsg(kdmsg_msg_t *msg)
     * This is a one-way packet but if not (e.g. if part of
     * a streaming transaction), we will have already closed
     * our end.
+    *
+    * Be defensive: aux_data may be non-NULL with aux_size==0
+    * on some paths; bail out instead of underflowing.
     */
-   if (msg->aux_data) {
+   if (msg->aux_data != NULL && msg->aux_size != 0) {
        msg->aux_data[msg->aux_size - 1] = 0;
        kprintf("DEBUGMSG: %s\n", msg->aux_data);
    }

The identical fix should be applied to the live twin at sys/vfs/hammer2/hammer2_iocom.c:149 (the if (msg->aux_data) there should likewise become if (msg->aux_data != NULL && msg->aux_size != 0)).

Additionally, since both functions in this file are dead code, consider deleting them (and their prototypes at hammer2.h:1869-1870) to reduce attack surface and the chance of someone wiring them up later without re-auditing.

References

Timeline

  • 2026-07-05 Discovered during automated audit.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0936 Β· 1 files
FileTypeDescriptionSize
fix.diff suggested-fix NUL-termination write underflows when aux_size==0 (latent OOB write, dead code t 405 B view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied + combined nativekernel build rc=0 (-Werror)

fix.diff applied + combined nativekernel build rc=0 (-Werror)
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none (Info severity)

Evidence (decisive lines)

Source-confirmed at sys/vfs/hammer2/hammer2_msgops.c:72: NUL-termination write underflows when aux_size==0 (dead code; live twin at iocom.c)

Verified recommended fix

Source-confirmed at sys/vfs/hammer2/hammer2_msgops.c:72: NUL-termination write underflows when aux_size==0 (dead code; live twin at iocom.c)

Verdict

Source-confirmed at sys/vfs/hammer2/hammer2_msgops.c:72: NUL-termination write underflows when aux_size==0 (dead code; live twin at iocom.c)