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
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) viasys_kldload(:794). - Privileges gained or impact: unintentional kernel heap corruption
(~16-byte overflow into adjacent
M_LINKERobjects) β a local DoS (panic) and, with heap grooming (corrupt an adjacentlinker_filerefs/flags/userrefs), potentially a UAF/double-free primitive. Root can already load arbitrary.komodules, 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).
Recommended fix
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
sys/kern/kern_linker.c:1458βkmalloc(MAXPATHLEN)buffer.sys/kern/kern_linker.c:1476,1480β uncheckedstrcpyof name/ext.sys/kern/kern_linker.c:794βSYSCAP_NOKLDgate (root-only reachability).- CWE-787 Out-of-bounds Write; CWE-120 Buffer Copy without Checking Size.
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)
PoC verification
Evidence pack
findings/poc/DF-0024 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.).
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)
-
Trigger:
kldload("<1023-byte bare name>")(root-only,SYSCAP_NOKLDgate atsys/kern/kern_linker.c:794).copyinstrat:798accepts up toMAXPATHLEN-1 = 1023bytes. A name with no/and no.takes the bare-module branch at:809-811(modname = file). -
Path to vulnerable function:
sys_kldload:815βlinker_load_module(NULL, modname, β¦)(:1536) βlinker_search_path(modname)(:1413). -
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 */Withprefix_len=13andname= 1023 bytes + NUL = 1024 bytes,strcpyat:1476writes offsets 13..1036 β overflowing the 1024-byte buffer by 13 bytes (offsets 1024..1036). The:1480ext strcpy adds 4 more bytes at offsets 1036..1039. Total overflow: ~16 bytes into the adjacentM_LINKERslab chunk. -
Effect: Silent heap corruption. The overflowed path string (
/boot/kernel/AAAβ¦AAA.ko) is passed tonlookup_init/vn_openwhich returnsENOENT(file doesn't exist). The buffer is thenkfree'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 withWEIRD_ADDR). The corruption may surface later as a UAF/double-free if the adjacent chunk is a livelinker_fileobject whoserefs/flags/userrefsfields 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
fixedVALIDATED: 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).
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).
No comments yet.