DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0025

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/kern/kern_linker.c:

/* 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).

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

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-0025 · 15 files
FileTypeDescriptionSize
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
README.md readme PoC summary and instructions
↓ download 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.

VERDICT.md verdict full narrative: mechanism, trace, before/after
↓ download raw

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), via link_elf_symbol_values (sys/kern/link_elf.c:872,880) which returns ef->address + es->st_value and is copied out as lookup.symvalue = (uintptr_t)symval.value (kern_linker.c:1053,1062);
  • the base address (lf->address) and size of every loaded KLD (kldstat, copyout at kern_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-leakkldstat/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.cBUILD_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 commands
  • build.log — final unprivileged build output
  • run.baseline.log — decisive unpatched-#0 run (leak)
  • run.log — decisive patched-#1 run (EPERM)
  • fix_run.log — three patched-kernel runs (stable EPERM)
  • fix_build.log — full nativekernel + installkernel output
  • leak_sample.txt — three runs of the unprivileged leak + nm cross-check
  • env.txt — guest environment (uname, cc, sysctls, user)
  • fix.diff — git-apply-able fix for sys/kern/kern_linker.c
  • manifest.json — machine-readable catalog

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 19:25:12 UTC 2026 (sha256 ae4e6e83af79a097d8dfcb5b220d74094d7b51ed1396e0a06f5c58115d7eddd1)

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.