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

Use-after-free in DEVFS_RULE_RESET: devfs_gc() frees nodes under the topology walker's saved cursors

Summary

devfs_rule_reset_node() calls devfs_gc() from inside devfs_iterate_topology()'s TAILQ_FOREACH_MUTABLE walk; devfs_gc frees arbitrary nodes (all links targeting the node, empty dirs), so the walker advances onto freed memory and runs the reset callback on it - KKASSERT(node->link_target) fails on INVARIANTS kernels; on production kernels the next statement writes through the NULLed link_target (low-page fault) and the stale ORPHANED/list state drives orphan-list and child-list corruption routed through the freed chunk. VERIFIED: rule-built link chain L2->L1->null + RESET '*' -> 'assertion node->link_target failed in devfs_rule_reset_node at devfs_rules.c:223' with full stack through devfs_iterate_topology <- devfs_apply_reset_rules_caller. Privileged ioctl (root, or jailed root via DF-3197 -> host-kernel-corruption path). memcorrupt bucket. Fix validated (two-pass reset: collect then free after the walk): same PoC survives, links cleanly removed.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3198 Β· 11 files
FileTypeDescriptionSize
README.md β€” 204 B ↓ raw
VERDICT.md β€” 2.9 KB ↓ raw
build.sh β€” 93 B view raw
env.txt β€” 415 B view raw
fix.diff β€” 8.6 KB view raw
panic.txt β€” 621 B view raw
run.fixed.log β€” 476 B view raw
run.log β€” 376 B view raw
run.sh β€” 92 B view raw
uaf_reset.c β€” 3.0 KB view raw
verdict.json β€” 2.6 KB view raw

DF-3198 β€” UAF in DEVFS_RULE_RESET (devfs_gc frees under the walker). See VERDICT.md. Build: cc -static -O -o uaf_reset uaf_reset.c Run as root: ./uaf_reset -> expect panic (KKASSERT devfs_rules.c:223)

VERDICT.md
↓ download raw

DF-3198 β€” use-after-free in DEVFS_RULE_RESET: devfs_gc() frees nodes under the topology walker's feet

Root cause

devfs_apply_reset_rules_caller() (devfs_core.c:1912) walks the topology with devfs_iterate_topology(), whose TAILQ_FOREACH_MUTABLE saves the next sibling (node2) before descending. The callback devfs_rule_reset_node() (devfs_rules.c:213-234) calls devfs_gc() on every rule-created link, and devfs_gc() (devfs_core.c:721) frees ARBITRARY other nodes: - devfs_gc_links_callback frees every link whose link_target is the node, - devfs_gc_dirs_callback frees empty directories.

The walker then advances onto freed memory and runs the callback on it.

Reproduced (deterministic, INVARIANTS kernel)

Two rules build a link chain L2 -> L1 -> null (rule 2 matches the alias created by rule 1 β€” devfs_allocp applies rules to every new node, devfs_core.c:291-292). DEVFS_RULE_RESET "*":

panic: assertion "node->link_target" failed in devfs_rule_reset_node
        at /usr/src/sys/vfs/devfs/devfs_rules.c:223
devfs_rule_reset_node() at devfs_rule_reset_node+0xba
devfs_rule_reset_node() at devfs_rule_reset_node+0xba
devfs_iterate_topology() at devfs_iterate_topology+0x67
devfs_apply_reset_rules_caller() at devfs_apply_reset_rules_caller+0x8e
devfs_msg_exec() at devfs_msg_exec+0x3ac

Sequence: reset of L1 runs devfs_gc(L1) β†’ nlinks>0 (L2 points at L1) β†’ gc frees L2; the outer walk's saved cursor node2 == L2 (freed) β†’ reset_node(L2-freed) β†’ KKASSERT(node->link_target) fails because the first devfs_unlinkp already NULLed link_target (devfs_core.c:613).

On a production (non-INVARIANTS) kernel the very next statement is --node->link_target->nlinks β€” a write through the NULL low page β€” and the stale flags (DEVFS_ORPHANED, list cursors) drive double-unlink/orphan-list corruption. The corruption primitive (list tail routed through freed memory) is real, but the trigger is a privileged ioctl (root, or jailed root via DF-3197), so the practical impact here is a reliable privileged panic; combined with DF-3197 it is a jailed-root β†’ host-kernel-corruption path.

Fix

Two-pass reset (in fix.diff): the walk only unlinks rule-created links and collects them; all freeing happens after the walk in devfs_reset_mount(), with a DEVFS_DESTROYED guard for entries freed by an earlier links pass. Validated: fixed kernel resets the same chain without a panic (run.fixed.log).

Files

uaf_reset.c, build.sh, run.sh, run.log, panic.txt, fix.diff, run.fixed.log (post-fix), 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

same PoC on the fixed kernel: SURVIVED, /dev/L1 and /dev/L2 removed by the reset, tree consistent, guest stayed up

['DF-3198/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 (root, or jailed root via DF-3197) ioctl sequence: ADD LINK null->L1, ADD LINK L1->L2, APPLY '', RESET '' -> devfs core thread frees L2 during L1's gc -> reset_node runs on freed L2 -> KKASSERT/NULL-page write; production kernels get list corruption through the freed chunk (double-unlink of orphaned nodes)

Evidence (decisive lines)

['panic.txt: assertion + full backtrace', 'run.log: chain built then panic on RESET', 'run.fixed.log: SURVIVED, links cleaned, guest up']

PoC changes

none beyond compile fixes (limits.h include)

Verified recommended fix

two-pass reset: unlink+collect rule-created links during the walk, free (gc_links + freep + one gc_dirs sweep) only after it

Verdict

devfs_rule_reset_node() calls devfs_gc() while devfs_iterate_topology() holds saved TAILQ cursors; devfs_gc frees arbitrary nodes (links to the target, empty dirs), so the walker revisits freed memory. With a rule-built chain L2->L1->null, DEVFS_RULE_RESET panicked deterministically: 'assertion node->link_target failed in devfs_rule_reset_node at devfs_rules.c:223' with the full stack through devfs_iterate_topology <- devfs_apply_reset_rules_caller. On non-INVARIANTS kernels the next statement is a write through the NULL link_target (low-page fault), and the stale ORPHANED/list state drives orphan-list and child-list corruption routed through freed memory.