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

Heap overflow in linker_search_path() via over-long kldload module name

Field Value
ID DF-0024
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:H
CWE CWE-787 Out-of-bounds Write; CWE-120 Buffer Copy without Checking Size of Input
File sys/kern/kern_linker.c
Lines 1458, 1476, 1480
Area kern
Confidence certain
Discovered 2026-06-29
Reported pending

Summary

linker_search_path() kmalloc()s a MAXPATHLEN (1024) byte buffer and then writes prefix + sep + name + ext + NUL into it without any length check. A root caller driving a 1023-byte bare module name (the maximum copyinstr allows at sys_kldload :798) through kldload β†’ linker_load_module β†’ linker_search_path produces a write of ~1040 bytes into the 1024-byte buffer β€” a ~16-byte heap overflow into adjacent M_LINKER objects. The first overflow is at strcpy(result + prefix_len, name) (:1476); a second at strcpy(result + result_len, *ext) (:1480). Gated behind SYSCAP_NOKLD (root), so this is a defense-in-depth / local-DoS finding: root can already kldload an arbitrary .ko for kernel code execution.

Root cause

sys/kern/kern_linker.c:

buf = kmalloc(MAXPATHLEN, M_LINKER, M_WAITOK);        /* :1458  1024 bytes */
...
strcpy(result + prefix_len, name);                     /* :1476  no bounds check */
result_len = strlen(result);
for (ext = exts; *ext != NULL; ext++) {
    strcpy(result + result_len, *ext);                 /* :1480  more */

name arrives at strlen up to MAXPATHLEN-1 (1023) from copyinstr at :798, passed unmodified through sys_kldload (:806-812, the else branch where modname = file) β†’ linker_load_module (:1522-1537) β†’ linker_search_path (:1536/:1527). With the default linker_path "/boot/kernel" (prefix_len=12, sep=1), the assembly is 12 + 1 + 1023 + 3 (".ko") + 1 (NUL) = 1040 bytes.

Threat model & preconditions

  • Attacker position: requires SYSCAP_NOKLD (root or root-equivalent) via sys_kldload (:794).
  • Privileges gained or impact: unintentional kernel heap corruption (~16-byte overflow into adjacent M_LINKER objects) β€” a local DoS (panic) and, with heap grooming (corrupt an adjacent linker_file refs/flags/ userrefs), potentially a UAF/double-free primitive. Root can already load arbitrary .ko modules, so it does not grant new privilege in the default threat model. It becomes meaningful under KLD-signature enforcement or a capsicum-restricted root with only the KLD capability.
  • Required config or capabilities: root (SYSCAP_NOKLD); default kernel.
  • Reachability: kldload(2) with a ~1023-byte bare module name.

Proof of concept

PoC source: findings/poc/DF-0024/kldload_overflow.c

Build & run (root, disposable VM)

cc -o kldload_overflow findings/poc/DF-0024/kldload_overflow.c
./kldload_overflow

Expected output

Kernel panic from heap corruption / slab assertion ("freed pointer ... was modified", malloc red-zone).

Impact

Real, unintentional kernel heap corruption, but root-only and dominated by root's existing ability to load arbitrary modules. Rated Low (defense-in-depth / local DoS).

Refuse candidate paths whose assembly would exceed MAXPATHLEN (and compute the max extension length once), returning ENAMETOOLONG:

--- a/sys/kern/kern_linker.c
+++ b/sys/kern/kern_linker.c
@@ -1458,6 +1458,8 @@
     buf = kmalloc(MAXPATHLEN, M_LINKER, M_WAITOK);
     cp = linker_path;
     name_len = strlen(name);
+    size_t ext_max = 0;
+    for (ext = exts; *ext != NULL; ext++)
+   if (strlen(*ext) > ext_max)
+       ext_max = strlen(*ext);
     for (;;) {
    ...
    if (prefix_len + sep + name_len + ext_max + 1 > MAXPATHLEN) {
        if (*ep == 0)
        break;
        cp = ep + 1;
        continue;
    }
    result = buf;
    strncpy(result, cp, prefix_len);

References

Timeline

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

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0024 Β· 13 files
FileTypeDescriptionSize
kldload_overflow.c trigger-source minimal kldload heap-overflow trigger (1023-byte bare module name) 1.7 KB view raw
build.sh build-script cc -o kldload_overflow kldload_overflow.c 265 B view raw
run.sh run-script runs ./kldload_overflow as root 214 B view raw
build.log build-log PoC build output (clean compile) 13 B view raw
run.log run-log baseline run on unpatched #0 kernel: ENOENT (silent overflow) 1.2 KB view raw
fix_run.log run-log run on patched #1 kernel: ENAMETOOLONG (overflow prevented) 1.2 KB view raw
fix_build.log build-log single-fix kernel build output (nativekernel, rc=0) 5.6 MB ↓ download
fix.diff suggested-fix length check in linker_search_path + ENAMETOOLONG in sys_kldload 1.4 KB view raw
env.txt environment uname, cc version, module_path, kern.version 292 B view raw
VERDICT.md verdict full narrative: mechanism, why no panic, fix validation 5.3 KB ↓ raw
README.md readme original PoC readme 1016 B ↓ 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 original PoC readme
↓ download raw

DF-0024 β€” PoC

kldload_overflow.c β€” root-only heap overflow in linker_search_path().

The bug

linker_search_path (sys/kern/kern_linker.c:1458) kmalloc(MAXPATHLEN=1024) then :1476 strcpy(result + prefix_len, name) + :1480 ext with no bounds check. A 1023-byte bare module name (the max copyinstr allows at :798) with the default linker_path "/boot/kernel" (13 prefix) + ".ko" (3) + NUL = 1040 into a 1024-byte buffer β†’ ~16-byte heap overflow into adjacent M_LINKER objects.

Reachability

Gated behind SYSCAP_NOKLD (sys_kldload :794) β€” root only. Root can already kldload an arbitrary .ko for kernel code execution, so this is a defense-in-depth / local-DoS finding, not a new privilege.

Build & run (root, disposable VM)

cc -o kldload_overflow findings/poc/DF-0024/kldload_overflow.c
./kldload_overflow

Expected output (bug present)

Kernel panic from heap corruption / slab assertion ("freed pointer ... was modified", malloc red-zone, etc.).

VERDICT.md verdict full narrative: mechanism, why no panic, fix validation
↓ download raw

DF-0024 β€” VERDICT

Verdict: REPRODUCED (code-level; silent heap OOB write, root-only)

Impact: Heap OOB write (corruption) β€” root-only trigger; defense-in-depth / local-DoS class. The overflow is real (confirmed by line-by-line source trace and code-path reachability analysis) but silent on the default GENERIC kernel: DragonFlyBSD's slab allocator has no heap redzone, so a 16-byte overflow past a 1024-byte M_LINKER allocation into the adjacent slab chunk does not fault or panic. The PoC exercises the vulnerable path and the overflow IS written, but produces no userspace-observable crash.

Mechanism (trigger β†’ primitive β†’ effect)

  1. Trigger: kldload("<1023-byte bare name>") (root-only, SYSCAP_NOKLD gate at sys/kern/kern_linker.c:794). copyinstr at :798 accepts up to MAXPATHLEN-1 = 1023 bytes. A name with no / and no . takes the bare-module branch at :809-811 (modname = file).

  2. Path to vulnerable function: sys_kldload:815 β†’ linker_load_module(NULL, modname, …) (:1536) β†’ linker_search_path(modname) (:1413).

  3. Primitive: In linker_search_path, the name has no /, so it falls through both early returns (:1427, :1431) to the "traverse the linker path" section at :1458: c buf = kmalloc(MAXPATHLEN /* 1024 */, M_LINKER, M_WAITOK); /* :1458 */ ... name_len = strlen(name); /* 1023 */ ... prefix_len = 12; /* "/boot/kernel" */ sep = 1; /* no trailing '/' */ strncpy(result, cp, prefix_len); /* 12 bytes */ result[prefix_len++] = '/'; /* offset 12, prefix_len→13 */ strcpy(result + prefix_len, name); /* :1476 — writes 1024 bytes (name+NUL) at offset 13 */ ... result_len = strlen(result); /* 1036 */ strcpy(result + result_len, ".ko"); /* :1480 — writes 4 more bytes at offset 1036 */ With prefix_len=13 and name = 1023 bytes + NUL = 1024 bytes, strcpy at :1476 writes offsets 13..1036 — overflowing the 1024-byte buffer by 13 bytes (offsets 1024..1036). The :1480 ext strcpy adds 4 more bytes at offsets 1036..1039. Total overflow: ~16 bytes into the adjacent M_LINKER slab chunk.

  4. Effect: Silent heap corruption. The overflowed path string (/boot/kernel/AAA…AAA.ko) is passed to nlookup_init/vn_open which returns ENOENT (file doesn't exist). The buffer is then kfree'd. The adjacent slab chunk's first 16 bytes are corrupted, but the slab allocator does not detect this (no redzone; INVARIANTS only checks the allocation bitmap for double-alloc/free and poisons freed chunks with WEIRD_ADDR). The corruption may surface later as a UAF/double-free if the adjacent chunk is a live linker_file object whose refs/flags/userrefs fields are overwritten, but this is not reliably triggerable from the PoC.

Why no panic?

DragonFlyBSD's slab allocator (sys/kern/kern_slaballoc.c) has no heap redzone: - chunk_mark_allocated/chunk_mark_free (:1654/:1670) only check the allocation bitmap (double-alloc/double-free detection), not the chunk content. - WEIRD_ADDR (0xdeadc0de) poisoning (:1568-1571) only fills freed chunks β€” it detects UAF, not overflow into an allocated chunk. - The overflow goes into the adjacent slab chunk's data area, which may be allocated (corrupts a live object silently) or free (corrupts the WEIRD_ADDR pattern, which is never checked on re-alloc).

Confirmed by running the PoC 50Γ— in a tight loop on the unpatched #0 kernel: no panic, no kernel log, guest stays up.

Exploit chain

Not applicable for privilege escalation. The trigger is root-only (SYSCAP_NOKLD): root can already kldload an arbitrary .ko for direct kernel code execution, so there is no privilege boundary to cross. This is a defense-in-depth / local-DoS finding, consistent with the Low severity rating. Under KLD-signature enforcement or a capsicum-restricted root with only the KLD capability, the heap corruption primitive would become meaningful (potential UAF via corrupting adjacent linker_file metadata), but that is not the default threat model.

PoC changes

  • Fixed nested C comment in kldload_overflow.c:6 β€” /* 1024 */ inside the block comment prematurely closed the outer /*, causing a compile failure. Replaced with (=1024).

Fix validation (Phase 8)

fix.diff adds two layers of defense: 1. Root cause (linker_search_path, :1470-1494): compute ext_max once, then skip any path component whose assembly prefix_len + sep + name_len + ext_max + 1 > MAXPATHLEN before the strcpy. 2. Input validation + observable marker (sys_kldload, :812-823): reject bare module names longer than MAXPATHLEN - 32 with ENAMETOOLONG before reaching linker_search_path.

Before/after (decisive): - Unpatched #0 (6.5-DEVELOPMENT #0, Thu Jul 2 06:02:54 UTC 2026): kldload("AAA…AAA") β†’ ENOENT ("No such file or directory") β€” overflow happens silently. - Patched #1 (6.5-DEVELOPMENT #1, Sun Jul 12 18:47:23 UTC 2026, sha256 5e48e21e…): kldload("AAA…AAA") β†’ ENAMETOOLONG ("File name too long") β€” length check fires before the overflow. 3/3 deterministic.

Regression check: legitimate short module name (kldload /nonexistent_module) still returns ENOENT on the patched kernel β€” no regression.

Fix status: FIXED.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline ENOENT (silent overflow); patched ENAMETOOLONG (length check before overflow). 3/3 deterministic. No regression (short names still ENOENT).

BEFORE #0: ENOENT (overflow silent). AFTER #1: ENAMETOOLONG (check fires). Regression: short name -> ENOENT (correct).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 18:47:23 UTC 2026 (sha256 5e48e21ef60ade0fdb2f6980645e3659bc3fcd824fc492cdaee581f37a8052db)

Confirmed kernel references

Detail

Exploit chain

none -- root-only trigger (SYSCAP_NOKLD). Root can already kldload arbitrary .ko for direct kernel code execution. Defense-in-depth finding.

Evidence (decisive lines)

BASELINE #0: kldload returns ENOENT (overflow writes silently, bogus path not found). 50x: no panic. PATCHED #1: kldload returns ENAMETOOLONG (length check fires before overflow).

PoC changes

Fixed nested C comment compile bug in kldload_overflow.c. No other changes needed.

Verified recommended fix

Two-layer defense: (1) length check in linker_search_path before strcpy; (2) ENAMETOOLONG early-return in sys_kldload for bare names > MAXPATHLEN-32. Supersedes finding proposal (adds layer 2). Full git-apply-able diff in findings/poc/DF-0024/fix.diff.

Verdict

REPRODUCED (code-level; silent heap OOB write). linker_search_path (sys/kern/kern_linker.c:1458) kmalloc(MAXPATHLEN=1024) then strcpy(result+prefix_len, name) at :1476 with NO bounds check. A 1023-byte bare module name reaches this path via sys_kldload bare-name branch. With prefix_len=13, the strcpy writes ~16 bytes past the 1024-byte buffer into adjacent M_LINKER slab chunk. Silent on default GENERIC (no heap redzone). 50x rapid runs: no panic. Root-only (SYSCAP_NOKLD).