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

vm_map_growstack() maps grown stack pages VM_PROT_ALL (RWX), silently making the grown stack executable (W^X bypass)

Field Value
ID DF-2674
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N
CWE CWE-453 Insecure Default Initialization of Resource
File sys/vm/vm_map.c
Lines 4246-4251
Area vm
Confidence certain
Discovered 2026-08-29
Pass 2 (GLM 5.3 second pass)
Bucket privesc
Reported pending
Known CVE none
CVE match novel

Summary

The growth insert hardcodes prot=max=VM_PROT_ALL instead of inheriting stack_entry->protection/max_protection. exec maps the main stack with prot RW (non-exec current protection, kern_exec.c:991-995), but every region the stack grows into silently becomes RWX.

Threat model & preconditions

Any process whose stack grows (deep recursion / alloca / large locals) gets an executable mapping below the original stack with no mprotect: stack-overflow exploits gain an executable landing area, defeating non-executable-stack hardening. Unprivileged, silent, permanent for the process.

Proof of concept

VERIFIED unprivileged (findings/poc/DF-2674/stack_growx.c): read /proc/self/map, fault one page below the lowest stack region, re-read. Stock kernel: new region 0x7fffffdc0000-0x7fffffde0000 'rwx' while the original stack is 'rw-' (stable over 3 runs). Patched kernel: grown region 'rw-'.

--- a/sys/vm/vm_map.c
+++ b/sys/vm/vm_map.c
@@ -4246,7 +4246,9 @@
    rv = vm_map_insert(map, &count,
               NULL, NULL,
               0, NULL,
               addr, stack_entry->ba.start,
               VM_MAPTYPE_NORMAL,
-              VM_SUBSYS_STACK, VM_PROT_ALL, VM_PROT_ALL, 0);
+              VM_SUBSYS_STACK,
+              stack_entry->protection,
+              stack_entry->max_protection, 0);

Timeline

  • 2026-08-29 Discovered during pass-2 audit of vm_map.c (GLM 5.3); reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2674 Β· 10 files
FileTypeDescriptionSize
README.md β€” 1.3 KB ↓ raw
VERDICT.md β€” 1.6 KB ↓ raw
stack_growx.c β€” 2.2 KB view raw
build.sh β€” 90 B view raw
run.sh β€” 34 B view raw
run.log β€” 472 B view raw
run.2.log β€” 26 B view raw
fix_run.log β€” 426 B view raw
fix.diff β€” 388 B view raw
env.txt β€” 320 B view raw

DF-2674 β€” vm_map_growstack() maps grown stack pages RWX (W^X weakening)

What

vm_map_growstack() inserts grown stack pages with hardcoded VM_PROT_ALL, VM_PROT_ALL (sys/vm/vm_map.c:4246-4251), ignoring the protection of the stack entry being grown. The main stack is mapped by exec with prot = VM_PROT_READ|VM_PROT_WRITE (non-executable current protection; sys/kern/kern_exec.c:991-995) β€” but every region the stack grows into silently becomes readable/writable/executable.

Impact

Silent defeat of non-executable-stack hardening for grown stack regions: attackers exploiting a stack overflow gain an executable landing area below the original stack mapping without calling mprotect. (max_protection of the stack includes EXEC, so the boundary was always user-raisable; the defect is that growth raises current protection unprompted.)

Reproduce (unprivileged)

./build.sh && ./run.sh    # cc -O2 -o ~/poc/stack_growx stack_growx.c; run

Observed /proc/self/map (before β†’ after faulting 1 page below the stack):

before: 0x00007fffffde0000-0x00007fffffdfe000 rw-
after:  0x00007fffffdc0000-0x00007fffffde0000 rwx   <-- grown chunk
        0x00007fffffde0000-0x00007fffffdfe000 rw-

Fix

fix.diff β€” inherit stack_entry->protection / stack_entry->max_protection for the growth insert (matches FreeBSD semantics).

VERDICT.md
↓ download raw

DF-2674 VERDICT β€” REPRODUCED (W^X weakening), fix validated

Question

Are stack regions created by downward stack growth executable?

Root cause (path:line)

vm_map_growstack() inserts the growth with hardcoded protections (sys/vm/vm_map.c:4246-4251):

rv = vm_map_insert(map, &count, NULL, NULL, 0, NULL,
                   addr, stack_entry->ba.start,
                   VM_MAPTYPE_NORMAL,
                   VM_SUBSYS_STACK, VM_PROT_ALL, VM_PROT_ALL, 0);

VM_PROT_ALL = READ|WRITE|EXECUTE becomes the current protection of the grown chunk. The main stack is created by exec with prot = VM_PROT_READ|VM_PROT_WRITE (sys/kern/kern_exec.c:991-995), so growth silently makes the newly usable stack executable.

Reproduction (unprivileged, /proc/self/map)

run.log (stable across 3 runs):

---- before growth ----
0x00007fffffde0000-0x00007fffffdfe000 rw-      <- original stack
---- after growth (fault 1 page below) ----
0x00007fffffdc0000-0x00007fffffde0000 rwx      <- grown chunk: RWX
0x00007fffffde0000-0x00007fffffdfe000 rw-

The other r-x region in the log is the program text, unrelated.

Impact: silent executable-stack introduction on growth β€” weakens non-exec-stack hardening for stack-overflow exploitation. Rated Low (the stack's max_protection already includes EXEC, so a process could ask for it via mprotect; the defect is the kernel granting X unprompted).

Fix validation

fix.diff passes stack_entry->protection / stack_entry->max_protection to the insert. On the patched kernel the grown region is rw- (see fix run in run.log).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: patched kernel maps grown stack pages rw- (inherited stack_entry protections) while stock maps them rwx; original stack behavior unchanged.

fix_run.log (before/after /proc/self/map captures)
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1 (in-guest nativekernel + DF-2674 fix.diff)

Confirmed kernel references

Detail

Evidence (decisive lines)

['run.log: before 0x7fffffde0000-0x7fffffdfe000 rw- ; after growth 0x7fffffdc0000-0x7fffffde0000 rwx (new executable stack region), run2/run3 stable', "fix_run.log: patched kernel #2 - grown region is rw-, executable-region count unchanged (1 = text only), PoC reports 'not reproduced'"]

PoC changes

seed sketch assumed Linux /proc/self/maps format with [stack] annotations; DragonFly exposes /proc/self/map (procfs_map.c) with a different column layout - parser rewritten for the DFly format; growth triggered by faulting one page below the lowest stack-region address

Verified recommended fix

pass stack_entry->protection/max_protection to the vm_map_insert() growth call instead of VM_PROT_ALL (fix.diff, validated)

Verdict

vm_map_growstack() inserts grown stack pages with hardcoded VM_PROT_ALL/VM_PROT_ALL (sys/vm/vm_map.c:4246-4251) instead of the stack entry's protection, so although exec maps the main stack RW (prot) with exec-disabled current protection (sys/kern/kern_exec.c:991-995), every region the stack grows into silently becomes RWX. Verified unprivileged via /proc/self/map: grown chunk 0x7fffffdc0000-0x7fffffde0000 is 'rwx' while the original stack is 'rw-'; stable across 3 runs. Impact is a W^X hardening bypass (executable stack appears on growth without any mprotect); no direct memory-safety impact, hence impact=none.