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-'.
Recommended fix
--- 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)
PoC verification
Evidence pack
findings/poc/DF-2674 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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
fixedVALIDATED: 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)
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.
No comments yet.