Missing privilege check on sys_kldstat()/sys_kldsym() leaks kernel symbol and module addresses
| Field | Value |
|---|---|
| ID | DF-0025 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-862 Missing Authorization |
| File | sys/kern/kern_linker.c |
| Lines | 940 (sys_kldstat), 1024 (sys_kldsym) |
| Area | kern |
| Confidence | likely |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
Only sys_kldload (:794) and sys_kldunload (:841) are gated by
caps_priv_check_self(SYSCAP_NOKLD). The query syscalls sys_kldstat
(:940) and sys_kldsym (:1024) have no privilege gate. sys_kldsym
resolves an arbitrary kernel symbol name to its absolute runtime address
(symval.value = ef->address + es->st_value, per link_elf.c/link_elf_obj.c),
and sys_kldstat returns each loaded module's base address (lf->address) and
size. Any local user can iterate fileids via kldnext and dump a complete
symbol→address map of the running kernel plus every loaded KLD — defeating
KASLR and handing an attacker developing another kernel exploit a ready symbol
map.
Root cause
/* sys_kldload :794 / sys_kldunload :841 -- gated */
if ((error = caps_priv_check_self(SYSCAP_NOKLD)) != 0)
return error;
/* sys_kldstat :940 / sys_kldsym :1024 -- NO gate */
The grep confirms caps_priv_check_self(SYSCAP_NOKLD) appears only at :794
and :841. Symbol resolution in sys/kern/link_elf.c /
sys/kern/link_elf_obj.c returns the absolute runtime address unmasked.
Threat model & preconditions
- Attacker position: any local unprivileged user.
- Privileges gained or impact: information disclosure — a complete kernel symbol→address map and the base address + size of every loaded module. A KASLR-defeat / symbol-map primitive that materially lowers the bar for exploiting any other kernel memory-safety bug (e.g. DF-0013). Standalone impact is info-leak only.
- Required config or capabilities: none; default kernel.
- Reachability:
kldsym(2)/kldstat(2)/kldnext(2)as any user.
Proof of concept
PoC source: findings/poc/DF-0025/kld_leak.c
Build & run (unprivileged)
cc -o kld_leak findings/poc/DF-0025/kld_leak.c ./kld_leak
Expected output
[+] kldsym("proc0") = 0xffffffff81xxxxxx (unprivileged KASLR/symbol leak)
[+] kldstat: loaded modules (base + size):
kernel id=1 base=0xffffffff80200000 size=1234567 refs=1
...
Impact
Unprivileged disclosure of the kernel's symbol map and module layout — a
KASLR-defeat / exploit-enabling primitive. Rated Low (info-leak only; matches
FreeBSD's historic ungated convention, but Linux restricts the analogous
/proc/kallsyms to root readers for exactly this reason).
Recommended fix
Gate sys_kldstat and sys_kldsym behind SYSCAP_NOKLD, matching
load/unload:
--- a/sys/kern/kern_linker.c
+++ b/sys/kern/kern_linker.c
@@ -940
+ if ((error = caps_priv_check_self(SYSCAP_NOKLD)) != 0)
+ return error;
@@ -1024
+ error = caps_priv_check_self(SYSCAP_NOKLD);
+ if (error)
+ return error;
(Confirm against any legitimate unprivileged consumer — e.g. the dynamic linker resolving symbols for a loaded module's userland glue — before restricting; if such a consumer exists, gate the address fields rather than the whole call, so name/existence remain queryable but addresses are redacted for unprivileged callers.)
References
sys/kern/kern_linker.c:940—sys_kldstat(no gate).sys/kern/kern_linker.c:1024—sys_kldsym(no gate).sys/kern/kern_linker.c:794,841— the gating pattern used by load/unload.- CWE-862 Missing Authorization.
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-0025 · 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| kld_leak.c | trigger-source | minimal kldsym/kldstat leak trigger (unmodified reviewer draft) | 1.7 KB | view raw |
| build.sh | build-script | cc -o kld_leak kld_leak.c | 159 B | view raw |
| run.sh | run-script | ./kld_leak | 112 B | view raw |
| build.log | build-log | final unprivileged build output | 13 B | view raw |
| run.baseline.log | run-log | decisive unpatched #0 run — symbol + module addresses leaked to maxx | 341 B | view raw |
| run.log | run-log | decisive patched #1 run — EPERM, 0 modules | 86 B | view raw |
| fix_run.log | run-log | three patched-kernel runs (stable EPERM) | 317 B | view raw |
| fix_build.log | build-log | full nativekernel + installkernel output | 5.7 MB | ↓ download |
| leak_sample.txt | leak-sample | three leak runs + nm cross-check confirming leaked proc0 address | 1.1 KB | view raw |
| env.txt | environment | uname, cc version, sysctl state | 1.0 KB | view raw |
| fix.diff | suggested-fix | add caps_priv_check_self(SYSCAP_NOKLD) gate to sys_kldstat and sys_kldsym | 1.1 KB | view raw |
| VERDICT.md | verdict | full narrative: mechanism, trace, before/after | 5.3 KB | ↓ raw |
| README.md | readme | PoC summary and instructions | 995 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-0025 — PoC
kld_leak.c — unprivileged leak of kernel symbol addresses (kldsym) and
loaded-module base addresses (kldstat).
The bug
sys_kldstat (sys/kern/kern_linker.c:940) and sys_kldsym (:1024) have no
caps_priv_check_self(SYSCAP_NOKLD) gate, unlike sys_kldload (:794) and
sys_kldunload (:841). kldsym resolves an arbitrary kernel symbol name to
its absolute runtime address; kldstat returns each loaded module's base
address (lf->address) and size. Any local user can dump a full
symbol→address map of the running kernel + every KLD.
Build & run (unprivileged)
cc -o kld_leak findings/poc/DF-0025/kld_leak.c ./kld_leak
Expected output (bug present)
[+] kldsym("proc0") = 0xffffffff81xxxxxx (unprivileged KASLR/symbol leak)
[+] kldstat: loaded modules (base + size):
kernel id=1 base=0xffffffff80200000 size=1234567 refs=1
...
A complete kernel symbol map + module layout handed to any local user.
DF-0025 — VERDICT
Status: REPRODUCED (info leak) — and the proposed fix is VALIDATED on a single-fix kernel.
The claim
sys_kldstat (sys/kern/kern_linker.c:940) and sys_kldsym
(sys/kern/kern_linker.c:1024) lack the caps_priv_check_self(SYSCAP_NOKLD)
privilege gate that sys_kldload (:794) and sys_kldunload (:841) use.
As a result any local user can dump:
- the absolute runtime address of any kernel symbol (
kldsym), vialink_elf_symbol_values(sys/kern/link_elf.c:872,880) which returnsef->address + es->st_valueand is copied out aslookup.symvalue = (uintptr_t)symval.value(kern_linker.c:1053,1062); - the base address (
lf->address) and size of every loaded KLD (kldstat, copyout atkern_linker.c:983,985).
Reproduction (unpatched #0 kernel, default GENERIC)
$ ssh dfbsd-maxx # uid=1001, no wheel, no operator
$ cd poc/DF-0025 && cc -o kld_leak kld_leak.c && ./kld_leak
[+] kldsym("proc0") = 0xffffffff81176920 (unprivileged KASLR/symbol leak)
[+] kldstat: loaded modules (base + size):
kernel id=1 base=0xffffffff80200000 size=28149048 refs=5
ehci.ko id=2 base=0xffffffff81cd9000 size= 508552 refs=1
xhci.ko id=3 base=0xffffffff81d56000 size= 575352 refs=1
Cross-check (root, post-leak):
$ nm /boot/kernel/kernel.debug | grep -w proc0 ffffffff81176920 B proc0 <-- matches the unprivileged leak exactly
Three back-to-back runs produced byte-identical output (KASLR is OFF on this audit guest, so addresses are stable across boots; on a KASLR-on kernel the same calls would leak the actual randomized addresses and defeat KASLR).
Path trace (every hop confirmed in sys/)
| Step | Where | What |
|---|---|---|
| syscall entry | sys/kern/kern_linker.c:1024 sys_kldsym |
NO caps_priv_check_self(SYSCAP_NOKLD) gate (vs :794 load / :841 unload which DO have it) |
| symbol lookup | sys/kern/kern_linker.c:1051,1060 lf->ops->lookup_symbol |
resolves user-supplied name to a kernel symbol |
| absolute address | sys/kern/link_elf.c:872,880 link_elf_symbol_values |
symval->value = ef->address + es->st_value (raw runtime address) |
| copyout to user | sys/kern/kern_linker.c:1053,1062 |
lookup.symvalue = (uintptr_t)symval.value |
| kldstat entry | sys/kern/kern_linker.c:940 |
NO gate |
| module base/size copyout | sys/kern/kern_linker.c:983,985 |
copyout(&lf->address, &stat->address, ...) and copyout(&lf->size, &stat->size, ...) |
This is a genuine CWE-862 (Missing Authorization). Severity: Low — info-leak only; no corruption, no escalation primitive derivable from these syscalls alone. The realistic impact ceiling is a KASLR-defeat / kernel-symbol-map primitive that materially lowers the bar for any other kernel memory-safety bug (e.g. DF-0013).
Why no escalation chain (per Phase 6)
This finding is a pure info-leak — kldstat/kldsym are read-only query
syscalls. The primitive is "learn a kernel address"; there is no write, no
corruption, no UAF, no refcount manipulation. The escalation-chain step is
therefore not applicable (the valid hard blocker: read-only primitive).
Documented impact ceiling: full kernel symbol map + module layout handed to any
local user.
Fix.diff (verified)
findings/poc/DF-0025/fix.diff adds the missing caps_priv_check_self(SYSCAP_NOKLD)
gate to both sys_kldstat and sys_kldsym, matching the gating that
sys_kldload and sys_kldunload already use. The fix supersedes the finding
markdown's sketch (which only sketched the call sites without unified-diff
headers) and is git apply-able against sys/kern/kern_linker.c.
Before/after (single-fix kernel)
| Run | Kernel | maxx output |
|---|---|---|
| baseline | #0 unpatched (5dc83dac…) |
kldsym("proc0") = 0xffffffff81176920 + 3 modules with addresses/sizes |
| patched | #1 single-fix (ae4e6e83…) |
kldsym: Operation not permitted + 0 modules |
| root (patched) | #1 single-fix |
full symbol+module data (root still allowed) |
make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel rebuilt
and installed cleanly (rc=0). The guest booted into the #1 kernel and the
SAME PoC now returns EPERM to maxx for both syscalls while root retains full
visibility — the gate is privilege-correct, not blanket-blocking.
PoC changes
None to kld_leak.c — the supplied trigger compiled and ran unmodified on the
first attempt (cc -o kld_leak kld_leak.c → BUILD_EXIT=0). The PoC source
correctly uses the public kldsym(2) / kldstat(2) / kldnext(2) libc
wrappers. Added: build.sh, run.sh, full logs, leak_sample.txt, env.txt,
VERDICT.md, manifest.json, and fix.diff.
Files
kld_leak.c— minimal trigger (unchanged from reviewer draft)build.sh/run.sh— exact repro commandsbuild.log— final unprivileged build outputrun.baseline.log— decisive unpatched-#0run (leak)run.log— decisive patched-#1run (EPERM)fix_run.log— three patched-kernel runs (stable EPERM)fix_build.log— full nativekernel + installkernel outputleak_sample.txt— three runs of the unprivileged leak + nm cross-checkenv.txt— guest environment (uname, cc, sysctls, user)fix.diff— git-apply-able fix forsys/kern/kern_linker.cmanifest.json— machine-readable catalog
Fix verification
fixedVALIDATED: baseline maxx leaks proc0 addr + module layout; patched maxx gets EPERM; root retains access. Clean before/after.
BEFORE #0 maxx: kldsym(proc0)=0xffffffff81176920 + 3 modules. AFTER #1 maxx: EPERM + 0 modules. AFTER #1 root: full access retained.
Confirmed kernel references
Detail
Exploit chain
none (pure read-only info leak; no write primitive). Impact ceiling: full kernel symbol->address map to any local user.
Evidence (decisive lines)
baseline #0 maxx: kldsym(proc0)=0xffffffff81176920; kernel base=0xffffffff80200000 size=28149048; ehci.ko/xhci.ko base+size. patched #1 maxx: EPERM. root on #1: retains access.
PoC changes
kld_leak.c unchanged. Added build.sh, run.sh, VERDICT.md, manifest.json, fix.diff, full logs.
Verified recommended fix
Add caps_priv_check_self(SYSCAP_NOKLD) to sys_kldstat (:940) and sys_kldsym (:1024), matching sys_kldload/unload. Matches finding markdown. Full git-apply-able diff in findings/poc/DF-0025/fix.diff.
Verdict
REPRODUCED. sys_kldstat (sys/kern/kern_linker.c:940) and sys_kldsym (:1024) lack the caps_priv_check_self(SYSCAP_NOKLD) gate that sys_kldload/unload use. Confirmed: maxx (uid 1001) calls kldsym('proc0') = 0xffffffff81176920 + base+size of every loaded KLD. KASLR-defeat / symbol-map info leak.
No comments yet.