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

Heap over-read in devfs_rule_create_link() disclosed as world-readable /dev link names

Summary

When the rule name ends in '*', create_link strips strlen(pattern)-1 bytes from the NODE name; leading '*'s let arbitrarily long patterns match short names, so the read starts up to ~200 bytes past the node's exact-size kmalloc'd d_name buffer and ksnprintf copies adjacent M_DEVFS heap contents into the new alias name, visible to every user via /dev. VERIFIED 3/3 deterministic: aliases LK17.urandom / LK20.ndom (same adjacent chunk, +3 shift) / LK33.kpmap disclosed stale kernel heap strings; unprivileged user listed them. Attacker-chosen offset (17..1022); world-readable output; self-matching variants additionally drive unbounded alias generation. Fix validated (clamp strip to d_namlen): post-fix aliases plain, no leaked bytes.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3199 Β· 10 files
FileTypeDescriptionSize
README.md β€” 218 B ↓ raw
VERDICT.md β€” 2.4 KB ↓ raw
build.sh β€” 101 B view raw
env.txt β€” 415 B view raw
fix.diff β€” 8.6 KB view raw
leak_linkname.c β€” 3.6 KB view raw
leak_sample.txt β€” 2.1 KB view raw
run.fixed.log β€” 362 B view raw
run.sh β€” 96 B view raw
verdict.json β€” 2.6 KB view raw

DF-3199 β€” heap over-read in devfs_rule_create_link disclosed via link names. See VERDICT.md. Build: cc -static -O -o leak_linkname leak_linkname.c Run as root: ./leak_linkname -> /dev/lk/LK.

VERDICT.md
↓ download raw

DF-3199 β€” heap over-read in devfs_rule_create_link() disclosed as world-readable link names

Root cause

devfs_rule_create_link() (devfs_rules.c:237-253) takes the wildcard branch whenever rule->name ends in '*', then strips

len = strlen(pattern_basename) - 1

bytes from the NODE's name:

ksnprintf(buf, sizeof(buf), "%s%s", rule->linkname, node->d_dir.d_name+len);

devfs_WildCaseCmp allows any number of leading '*'s, so a pattern of N stars + "null" + trailing '*' full-matches the node null (4-char name) while stripping N+4 bytes. node->d_dir.d_name is a separate kmalloc(namlen+1) allocation (devfs_core.c:253) β€” 5 bytes for "null" β€” so the read starts up to ~200 bytes PAST the chunk and copies adjacent M_DEVFS heap contents into the new alias's name, visible to every user via ls /dev/lk.

Reproduced (3/3 runs, deterministic; plus a live unprivileged read)

=== strip offset +17 ===  LK17.urandom     <- stale "urandom" name chunk
=== strip offset +20 ===  LK20.ndom        <- same chunk, shifted +3
=== strip offset +24 ===  LK24.            <- landed on NUL
=== strip offset +33 ===  LK33.kpmap       <- another stale heap string

Identical across three runs (stable zone layout). Live check: unprivileged user maxx listed /dev/lk/LIVE17urandom β€” kernel heap bytes (stale devfs name allocations) disclosed into the filesystem namespace.

The suffix content is bounded by the first NUL; what leaks is the slack/adjacent 16-byte-zone chunks (previous device-name strings). Content disclosure is modest, but the over-read itself is unbounded w.r.t. attacker-chosen offset (17..1022) and the output is world-readable. On this kernel, star-heavy variants (>~90 stars) instead crash via DF-3202 before creating the link.

Fix

Clamp the strip to the node's actual name length (fix.diff): if (len > node->d_dir.d_namlen) len = node->d_dir.d_namlen; Validated: post-fix aliases carry no leaked suffix (run.fixed.log).

Files

leak_linkname.c, build.sh, run.sh, run.log, leak_sample.txt, fix.diff, run.fixed.log, verdict.json, manifest.json

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

post-fix aliases carry no leaked suffix (LK17..LK33 plain); offsets with >32 stars are rejected at ADD by the DF-3202 cap

['DF-3199/run.fixed.log']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 8 10:25:07 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

privileged rule install -> every matching short-named node gets an alias whose name embeds heap bytes past d_name -> any user reads them via readdir/lookup on /dev

Evidence (decisive lines)

['run.log / leak_sample.txt: LK17.urandom, LK20.ndom, LK33.kpmap identical across 3 runs', 'unprivileged ls showing LIVE17urandom', 'run.fixed.log: aliases plain (strip clamped), no leaked bytes']

PoC changes

offsets 100/200 removed: their ~95-star patterns crash earlier via DF-3202; added path-scoped linkname (lk/) so aliases cannot self-match (DF-3201)

Verified recommended fix

clamp the strip to the node name length: if (len > node->d_dir.d_namlen) len = node->d_dir.d_namlen;

Verdict

devfs_rule_create_link() strips strlen(pattern)-1 bytes from the NODE name when the pattern ends in ''; leading ''s make patterns of any length match short names, so the read starts up to ~200 bytes past the node's exact-size kmalloc'd d_name and copies adjacent M_DEVFS heap contents into the alias name, world-readable in /dev. Reproduced 3/3 deterministic: LK17.urandom / LK20.ndom (same adjacent chunk, shifted) / LK33.kpmap - stale device-name strings from neighbouring 16-byte-zone chunks; unprivileged user maxx listed the leaked alias live.