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

Kernel stack-smashing protection is a no-op: canary is an unseeded compile-time constant (0x00000aff) from a dead ABI, __stack_chk_guard/__stack_chk_fail absent from the kernel, and -fno-stack-protector is forced for all kernel/KLD code

Summary

The file implements the 2002 propolike ABI (__guard, link-time constant 0x00000aff, never reseeded - karc4random is never even called here; identical across independent boots and matching the ELF .data cell) while the modern ABI every supported compiler emits (__stack_chk_guard/__stack_chk_fail) is defined NOWHERE and kern.mk:7 forces -fno-stack-protector on all kernel/KLD code. Net: zero canary protection - live-proven by a default-built KLD doing a 48->16 strcpy: saved RBP smashed to 0x4242424242424242, kernel dead at the ret with no canary in frame; the -fstack-protector-all variant cannot even kldload (undefined __stack_chk_guard). Terminator property also wrong on LE (first NUL at byte 2). The threat is systemic: every kernel stack-overflow bug anywhere lands on completely unprotected frames on a no-SMEP/SMAP/KASLR guest. seed-at-SI_SUB_DRIVERS + modern-ABI + flip-to-strong fix (git-apply-able, validated boot-ordering against arc4 init).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5713 Β· 17 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 6.4 KB ↓ raw
verdict.json β€” 5.0 KB view raw
guest/guard_read.c β€” 1.0 KB view raw
guest/ssp_smash/ β€” 4.0 KB ↓ download
guest/guard_probe/ β€” 4.0 KB ↓ download
guest/guard_panic/ β€” 4.0 KB ↓ download
build.log β€” 4.2 KB view raw
run.log β€” 289 B view raw
run.2.log β€” 970 B view raw
run.3.log β€” 0 B ↓ download
run.4.log β€” 53 B view raw
panic.txt β€” 921 B view raw
panic2.txt β€” 805 B view raw
console_full.log β€” 4.5 KB view raw
env.txt β€” 594 B view raw
fix.diff β€” 2.8 KB view raw

DF-5713 β€” stack_protector.c: kernel stack-smashing protection is a no-op

What this PoC proves (all demonstrated live on the QEMU guest)

  1. The canary is a compile-time constant, never seeded. sys/libkern/stack_protector.c:36 (int __guard = 0x00000aff;) is the only definition/assignment of __guard in the entire tree (tree-wide grep). Live proof: - /dev/kmem read of the running kernel's __guard (0xffffffff8113bae0): ff 0a 00 00 = 0x00000aff β€” run.2.log - KLD guard_probe.ko reads the exported symbol: __guard=0xaff β€” run.2.log / msgbuf - The shipped /boot/kernel/kernel ELF .data cell at the symbol address holds ff0a0000 β€” identical, i.e. nothing rewrote it at boot. - Two independent fresh boots (vm.sh reset with-src) read the same ff 0a 00 00 β€” no per-boot seeding exists (run.2.log, run.4.log).

  2. The modern stack-protector ABI does not exist in the kernel. sys/ contains no __stack_chk_guard/__stack_chk_fail definition (tree-wide grep) and nm /boot/kernel/kernel confirms absence. A KLD compiled with -fstack-protector-all (ssp_smash_ssp.ko, manually built because the build system strips the flag) fails to load: link_elf_obj_obj: symbol __stack_chk_guard undefined β€” panic.txt / console_full.log.

  3. Kernel code is force-built WITHOUT any stack protector. sys/platform/pc64/conf/kern.mk:7 β€” CFLAGS+= -fno-stack-protector ... (grep of the guest's /usr/src). The default-built ssp_smash.ko performs a 48-byte strcpy into a 16-byte kernel-stack buffer and the kernel dies instantly with the saved frame pointer smashed: frame pointer = 0x10:0x4242424242424242 ... Stopped at boom+0x2c: ret β€” panic.txt. No canary sits between the buffer and the saved frame.

  4. The legacy fail path works but is attached to nothing. guard_panic.ko calls __stack_smash_handler() directly β†’ panic: stack overflow in function guard_panic_smash_test β€” panic2.txt.

Build

# on the guest (sources pushed via the pack below):
cc -O2 -o /tmp/guest/guard_read /tmp/guest/guard_read.c
cd /tmp/guest/guard_probe && make -m /usr/share/mk
cd /tmp/guest/guard_panic && make -m /usr/share/mk
cd /tmp/guest/ssp_smash    && make -m /usr/share/mk        # default (no SSP)
# SSP variant (build system strips -fstack-protector-all, so compile by hand):
cc <kern flags> -fstack-protector-all -c ssp_smash.c -o ssp_smash_ssp.o
cc -nostdlib -r -o ssp_smash_ssp.ko ssp_smash_ssp.o        # exact flags in build.log

Run

/tmp/guest/guard_read 0xffffffff8113bae0 /dev/kmem   # -> ff 0a 00 00, rc=42
kldload /tmp/guest/ssp_smash/ssp_smash_ssp.ko        # -> Undefined symbol __stack_chk_guard
kldload /tmp/guest/guard_probe/guard_probe.ko        # -> __guard=0xaff
kldload /tmp/guest/ssp_smash/ssp_smash.ko            # -> kernel GPF, fp=0x4242...42 (PANIC)
kldload /tmp/guest/guard_panic/guard_panic.ko        # -> panic: stack overflow in function ... (PANIC)
# leave the guest clean:  vm.sh reset with-src

Expected results

See the four numbered points above; all captured in build.log, run.log, run.2.log, run.3.log, run.4.log, panic.txt, panic2.txt, console_full.log.

VERDICT.md
↓ download raw

DF-5713 VERDICT β€” reproduced (hardening failure; impact: none by itself)

Bottom line: every factual claim of the finding was demonstrated live on the DragonFly 6.5-DEVELOPMENT X86_64_GENERIC guest. The kernel's only stack-canary symbol, __guard, is a compile-time constant (sys/libkern/stack_protector.c:36, 0x00000aff) that is never seeded from any entropy source at any point in boot; the modern __stack_chk_guard/__stack_chk_fail ABI is absent from the kernel entirely; and sys/platform/pc64/conf/kern.mk:7 force-disables -fstack-protector for all kernel and KLD code. The "security-critical mitigation layer" therefore protects exactly nothing: it is 51 lines of dead code force-linked into every pc64 kernel (sys/platform/pc64/conf/files:119), whose only exported value is a publicly-known constant.

Mitigation-family checklist (as tasked)

(a) Canary source / seeding vs first use

  • Source: sys/libkern/stack_protector.c:36 (0x00000aff LE) / :38 (0xff0a0000 BE) β€” a link-time constant in .data, not karc4random.
  • Tree-wide grep for __guard writes: only lines 36/38. No SYSINIT, no reseed, nothing. The DF-5694/5695 (arc4 bias) and DF-2243 (zero-window) concerns are moot but in the worst way: the entropy path does not exist at all, so the canary is not "weakly random" β€” it is known to everyone who can read the public source.
  • Live: /dev/kmem reads ff 0a 00 00 on two independent fresh boots (run.2.log, run.4.log); the ELF .data cell at 0xffffffff8113bae0 holds the same bytes; KLD guard_probe.ko reads 0xaff through the exported symbol. Constant from link time through runtime, forever.

(b) Guard placement / infoleak surface

  • Single global int in .data at a fixed link-time address (ffffffff8113bae0 on this build), exported in the kernel symbol table (nm), readable by any loaded KLD and by root via /dev/kem///dev/mem. Not per-CPU, not per-boot. There is nothing to leak, though β€” the value is public in the source. (sys/libkern/stack_protector.c:36)
  • Notably the file is force-linked into every pc64 kernel via sys/platform/pc64/conf/files:119 (libkern/stack_protector.c standard) even though no in-tree code references __guard or __stack_smash_handler (tree-wide grep) β€” pure dead weight. sys/conf/files:2219 has the equivalent line commented out; pc32 (i386) doesn't list the file at all.

(c) Fail path

  • __stack_smash_handler() panics with stack overflow in function <name> (sys/libkern/stack_protector.c:49; i386 variant at :47 prints the caller PC from 4(%ebp) β€” cosmetic only, and that asm is dead for amd64). Demonstrated live: loading guard_panic.ko β†’ panic: stack overflow in function guard_panic_smash_test with a clean backtrace through module_register_init (panic2.txt). The handler works; it is simply connected to no instrumentation.

(d) Terminator-canary property

  • LE value bytes in memory: ff 0a 00 00 β€” first NUL at byte 2, not byte 0. A proper terminator canary places the NUL in the lowest byte so an ascending strcpy stops inside the canary before touching anything past it. Here a byte-string overflow first clobbers ff 0a (both publicly known, so an attacker with a controlled copy simply re-writes them) and terminates inside the canary. With a known constant canary the property is irrelevant anyway: any arbitrary-content overflow primitive (memcpy/bcopy/read) forges all 4 bytes. The BE variant (0xff0a0000 β†’ bytes 00 00 0a ff lowβ†’high) has the NUL low, ironically correct β€” but pc64 is LE.

The decisive live demonstration (why this is a real hardening failure)

The default-built KLD ssp_smash.ko (exactly what bsd.kmod.mk produces: -fno-stack-protector forced by sys/platform/pc64/conf/kern.mk:7) executes a textbook 48-byte strcpy into a 16-byte kernel-stack buffer:

Fatal trap 9: general protection fault while in kernel mode
frame pointer   = 0x10:0x4242424242424242
Stopped at      boom+0x2c:      ret

The saved RBP is 0x4242… β€” the attacker payload β€” and the kernel dies at boom's ret on the way to the smashed return address. There is no canary anywhere in that frame, because no kernel/KLD code is ever instrumented. The identical module recompiled with -fstack-protector-all (by hand; the build system silently strips the flag) cannot even load: link_elf_obj_obj: symbol __stack_chk_guard undefined β€” the kernel provides no modern ABI to link against.

Exploit chain

None developed β€” none exists through this file. The finding is a missing mitigation (CWE-693), not a corruption primitive: unprivileged attack surface of the file itself is zero (it defines two symbols and a panic). The impact is systemic: every kernel stack-overflow bug in DragonFly lands on unprotected frames, so any such bug elsewhere in sys/ is strictly more exploitable than on a stack-protected kernel; and if SSP were enabled without fixing the file, the only available canary (__guard, propolice ABI) is a publicly-known constant β€” also decorative.

Why not uid0 / why impact=none

The panics in the evidence were root-deliberate (kldload of a test module). No unprivileged-reachable path through this file exists. Severity Low (hardening/defense-in-depth), confidence certain.

Fix validation

Not performed (fix_status: not_testable): the fix (fix.diff β€” modern __stack_chk_guard/__stack_chk_fail, SYSINIT seeding from karc4random_buf() after arc4_init_pcpu() has run, kern.mk switched to -fstack-protector-strong) requires a full kernel rebuild/boot cycle whose payoff is a hardening A/B, not a behavior A/B of a reproduced corruption. git apply --check passes against the tree. Upstream should build/boot-test fix.diff (especially the kern.mk hunk) before commit.

Guest state

Guest panicked twice (both deliberate evidence runs), restored with vm.sh reset with-src after each; final state: up, clean snapshot, read-only /dev/kmem probe only after the last reset. guest_dirty: 0.

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

not_testable
baseline no→ patch + rebuild →patched clean

Fix not build-validated: this is a hardening finding (no memory-corruption reproduction to A/B), and the kern.mk hunk requires a full kernel rebuild+boot validation that belongs upstream. fix.diff passes git apply --check against the tree; SYSINIT ordering (SI_SUB_DRIVERS=0x2400000) is provably after arc4_init_pcpu (SI_BOOT2_FINISH_SMP=0x1c80000, sys/platform/pc64/x86_64/mp_machdep.c:571) so karc4random_buf() is safe at seed time.

findings/poc/DF-5713/fix.diff (git apply --check clean); ordering refs sys/platform/pc64/x86_64/mp_machdep.c:571, sys/sys/kernel.h:158,167,187
↓ fix.diffper-fix-DF-5713

Confirmed kernel references

Detail

Evidence (decisive lines)

run.2.log (live /dev/kmem read 'ff 0a 00 00', guard_probe KLD __guard=0xaff, objdump ELF cell ff0a0000); run.4.log (fresh boot #2 identical bytes β€” no per-boot seeding); panic.txt (link_elf_obj_obj: symbol __stack_chk_guard undefined + Fatal trap 9 with frame pointer 0x4242424242424242, Stopped at boom+0x2c: ret); panic2.txt (panic: stack overflow in function guard_panic_smash_test with backtrace); build.log (kmod CFLAGS show -fno-stack-protector from kern.mk; manual -fstack-protector-all build shows U __stack_chk_guard/__stack_chk_fail); console_full.log (full serial); guest/ (all sources); fix.diff (git apply --check clean)

PoC changes

Rewrote from scratch: original task sketch assumed karc4random-seeded __stack_chk_guard; the file actually implements the 2002 propolice ABI (constant int __guard + __stack_smash_handler). Built 4 artifacts: guard_read.c (/dev/kmem reader; fixed lseek error check for sign-extended kernel addresses), ssp_smash KLD (smash probe, two variants), guard_probe KLD (live symbol read), guard_panic KLD (fail-path trigger). bsd.kmod.mk (not kmod.mk) is the include name on DF; -fstack-protector-all must be compiled by hand because kern.mk strips it.

Verified recommended fix

Provide the modern ABI in this file (unsigned long __stack_chk_guard seeded once from karc4random_buf at SI_SUB_DRIVERS + __stack_chk_fail panic; also reseed legacy __guard) and flip kern.mk from -fno-stack-protector to -fstack-protector-strong; see fix.diff.

Verdict

Every claim reproduced live on the 6.5-DEVELOPMENT X86_64_GENERIC guest: the kernel's only canary symbol __guard is the compile-time constant 0x00000aff (sys/libkern/stack_protector.c:36) β€” /dev/kmem reads 'ff 0a 00 00' on two independent fresh boots and the ELF .data cell matches, so no seeding exists anywhere in boot (karc4random is never involved); the modern __stack_chk_guard/__stack_chk_fail ABI is absent from the kernel (a -fstack-protector-all KLD fails kldload with 'link_elf_obj_obj: symbol __stack_chk_guard undefined'); sys/platform/pc64/conf/kern.mk:7 force-disables -fstack-protector for all kernel/KLD code, so a default-built module's 48->16 kernel-stack strcpy smashes the saved frame pointer to 0x4242424242424242 and kills the kernel at boom+0x2c 'ret' with no canary in the frame; the legacy __stack_smash_handler panic path itself works (panic: stack overflow in function guard_panic_smash_test). The mitigation layer protects exactly nothing; it is dead code force-linked via sys/platform/pc64/conf/files:119 with a publicly-known canary value. Hardening failure (CWE-693/CWE-330): no unprivileged attack path through the file itself, hence impact none β€” but every kernel stack-overflow bug elsewhere in sys/ lands on unprotected frames, and any future SSP enablement would inherit the known-constant canary unless the file is fixed.