Skip to content

Commit 9316c51

Browse files
committed
[pthread] Honor guard size in pthread_create
Honor requested/default guard size in pthread_create by reserving guard_size bytes between thread control data/TSD/TLS and the usable stack. Although we cannot set memory protection on this region like a native platform this can still be useful for catching and reporting overflows that are smaller than the guard region itself. Without this change `-sSTACK_OVERFLOW_CHECK=1` is mostly useless for pthreads because any kind of overflow is likely to corrupt the stack before being reported. Add test cases covering guard size queries via pthread_getattr_np and stack overflow detection within guard region. Fixes: #27266
1 parent d154f06 commit 9316c51

9 files changed

Lines changed: 138 additions & 19 deletions

system/lib/pthread/pthread_create.c

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,22 +130,32 @@ int __pthread_create(pthread_t* restrict res,
130130
if (!attr._a_stacksize) {
131131
attr._a_stacksize = __default_stacksize;
132132
}
133+
if (!attrp || attrp == __ATTRP_C11_THREAD) {
134+
attr._a_guardsize = __default_guardsize;
135+
}
136+
137+
size_t guard_size = 0;
138+
if (!attr._a_stackaddr) {
139+
guard_size = ROUND_UP(attr._a_guardsize, STACK_ALIGN);
140+
}
133141

134142
// Allocate memory for new thread. The layout of the thread block is
135143
// as follows. From low to high address:
136144
//
137145
// 1. pthread struct (sizeof struct pthread)
138146
// 2. tls data (__builtin_wasm_tls_size())
139147
// 3. tsd pointers (__pthread_tsd_size)
140-
// 4. stack (__default_stacksize AKA -sDEFAULT_PTHREAD_STACK_SIZE)
148+
// 4. guard area (guard_size)
149+
// 5. stack (__default_stacksize AKA -sDEFAULT_PTHREAD_STACK_SIZE)
141150
size_t size = sizeof(struct pthread);
142151
if (__builtin_wasm_tls_size()) {
143152
size += __builtin_wasm_tls_size() + __builtin_wasm_tls_align() - 1;
144153
}
145154
size += __pthread_tsd_size + TSD_ALIGN - 1;
146155
size_t zero_size = size;
147156
if (!attr._a_stackaddr) {
148-
size += attr._a_stacksize + STACK_ALIGN - 1;
157+
size += guard_size + attr._a_stacksize + STACK_ALIGN - 1;
158+
zero_size += guard_size;
149159
}
150160

151161
// Allocate all the data for the new thread and zero-initialize all parts
@@ -176,6 +186,7 @@ int __pthread_create(pthread_t* restrict res,
176186
new->detach_state = DT_JOINABLE;
177187
}
178188
new->stack_size = attr._a_stacksize;
189+
new->guard_size = guard_size;
179190

180191
// 2. tls data
181192
if (__builtin_wasm_tls_size()) {
@@ -197,12 +208,13 @@ int __pthread_create(pthread_t* restrict res,
197208
if (attr._a_stackaddr) {
198209
new->stack = (void*)attr._a_stackaddr;
199210
} else {
211+
offset += guard_size;
200212
offset = ROUND_UP(offset + new->stack_size, STACK_ALIGN);
201213
new->stack = (void*)offset;
202214
}
203215

204216
// Check that we didn't use more data than we allocated.
205-
assert(offset < (uintptr_t)block + size);
217+
assert(offset <= (uintptr_t)block + size);
206218

207219
#ifndef NDEBUG
208220
_emscripten_thread_profiler_init(new);

test/codesize/test_codesize_cxx_wasmfs.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.out.js": 6725,
33
"a.out.js.gz": 3184,
4-
"a.out.nodebug.wasm": 172829,
5-
"a.out.nodebug.wasm.gz": 63329,
6-
"total": 179554,
7-
"total_gz": 66513,
4+
"a.out.nodebug.wasm": 172782,
5+
"a.out.nodebug.wasm.gz": 63311,
6+
"total": 179507,
7+
"total_gz": 66495,
88
"sent": [
99
"__cxa_throw",
1010
"_abort_js",

test/codesize/test_codesize_files_wasmfs.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.out.js": 5158,
33
"a.out.js.gz": 2458,
4-
"a.out.nodebug.wasm": 58606,
5-
"a.out.nodebug.wasm.gz": 18406,
6-
"total": 63764,
7-
"total_gz": 20864,
4+
"a.out.nodebug.wasm": 58562,
5+
"a.out.nodebug.wasm.gz": 18269,
6+
"total": 63720,
7+
"total_gz": 20727,
88
"sent": [
99
"a (emscripten_date_now)",
1010
"b (emscripten_err)",

test/codesize/test_codesize_minimal_pthreads.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.out.js": 6945,
33
"a.out.js.gz": 3424,
4-
"a.out.nodebug.wasm": 19063,
5-
"a.out.nodebug.wasm.gz": 8803,
6-
"total": 26008,
7-
"total_gz": 12227,
4+
"a.out.nodebug.wasm": 19130,
5+
"a.out.nodebug.wasm.gz": 8828,
6+
"total": 26075,
7+
"total_gz": 12252,
88
"sent": [
99
"a (memory)",
1010
"b (exit)",

test/codesize/test_codesize_minimal_pthreads_memgrowth.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.out.js": 7379,
33
"a.out.js.gz": 3639,
4-
"a.out.nodebug.wasm": 19064,
5-
"a.out.nodebug.wasm.gz": 8804,
6-
"total": 26443,
7-
"total_gz": 12443,
4+
"a.out.nodebug.wasm": 19131,
5+
"a.out.nodebug.wasm.gz": 8829,
6+
"total": 26510,
7+
"total_gz": 12468,
88
"sent": [
99
"a (memory)",
1010
"b (exit)",
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2026 The Emscripten Authors. All rights reserved.
2+
// Emscripten is available under two separate licenses, the MIT license and the
3+
// University of Illinois/NCSA Open Source License. Both these licenses can be
4+
// found in the LICENSE file.
5+
6+
#define _GNU_SOURCE
7+
8+
#include <assert.h>
9+
#include <pthread.h>
10+
#include <stdio.h>
11+
#include <stdlib.h>
12+
13+
void* thread_main_default(void* arg) {
14+
pthread_attr_t attr;
15+
size_t guard_size = 0;
16+
assert(pthread_getattr_np(pthread_self(), &attr) == 0);
17+
assert(pthread_attr_getguardsize(&attr, &guard_size) == 0);
18+
printf("default guard_size: %zu\n", guard_size);
19+
assert(guard_size == 8192);
20+
return NULL;
21+
}
22+
23+
void* thread_main_custom(void* arg) {
24+
pthread_attr_t attr;
25+
size_t guard_size = 0;
26+
assert(pthread_getattr_np(pthread_self(), &attr) == 0);
27+
assert(pthread_attr_getguardsize(&attr, &guard_size) == 0);
28+
printf("custom guard_size: %zu\n", guard_size);
29+
assert(guard_size == 16384);
30+
return NULL;
31+
}
32+
33+
void* thread_main_zero(void* arg) {
34+
pthread_attr_t attr;
35+
size_t guard_size = 0;
36+
assert(pthread_getattr_np(pthread_self(), &attr) == 0);
37+
assert(pthread_attr_getguardsize(&attr, &guard_size) == 0);
38+
printf("zero guard_size: %zu\n", guard_size);
39+
assert(guard_size == 0);
40+
return NULL;
41+
}
42+
43+
int main() {
44+
pthread_t t1, t2, t3;
45+
pthread_attr_t attr;
46+
47+
assert(pthread_create(&t1, NULL, thread_main_default, NULL) == 0);
48+
assert(pthread_join(t1, NULL) == 0);
49+
50+
assert(pthread_attr_init(&attr) == 0);
51+
assert(pthread_attr_setguardsize(&attr, 16384) == 0);
52+
assert(pthread_create(&t2, &attr, thread_main_custom, NULL) == 0);
53+
assert(pthread_join(t2, NULL) == 0);
54+
assert(pthread_attr_destroy(&attr) == 0);
55+
56+
assert(pthread_attr_init(&attr) == 0);
57+
assert(pthread_attr_setguardsize(&attr, 0) == 0);
58+
assert(pthread_create(&t3, &attr, thread_main_zero, NULL) == 0);
59+
assert(pthread_join(t3, NULL) == 0);
60+
assert(pthread_attr_destroy(&attr) == 0);
61+
62+
printf("done\n");
63+
return 0;
64+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
default guard_size: 8192
2+
custom guard_size: 16384
3+
zero guard_size: 0
4+
done
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2026 The Emscripten Authors. All rights reserved.
2+
// Emscripten is available under two separate licenses, the MIT license and the
3+
// University of Illinois/NCSA Open Source License. Both these licenses can be
4+
// found in the LICENSE file.
5+
6+
#include <assert.h>
7+
#include <pthread.h>
8+
#include <stdio.h>
9+
#include <emscripten/stack.h>
10+
11+
void* thread_main_overflow(void* arg) {
12+
uintptr_t stack_end = emscripten_stack_get_end();
13+
// Overwrite 500 bytes into the guard region below stack_end and the 8-byte stack cookie at stack_end.
14+
// Total 508 bytes (well under default guard_size = 8192).
15+
volatile char* ptr = (volatile char*)(stack_end - 500);
16+
for (int i = 0; i < 508; i++) {
17+
ptr[i] = 0xAA;
18+
}
19+
printf("thread_main_overflow done\n");
20+
return NULL;
21+
}
22+
23+
int main() {
24+
pthread_t t;
25+
assert(pthread_create(&t, NULL, thread_main_overflow, NULL) == 0);
26+
assert(pthread_join(t, NULL) == 0);
27+
printf("main done\n");
28+
return 0;
29+
}

test/test_core.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2655,6 +2655,16 @@ def test_pthread_attr_getstack(self):
26552655
self.skipTest('MINIMAL_RUNTIME + threads + asan does not work')
26562656
self.do_runf_out_file('pthread/test_pthread_attr_getstack.c')
26572657

2658+
@requires_pthreads
2659+
def test_pthread_guardsize(self):
2660+
self.do_runf_out_file('pthread/test_pthread_guardsize.c')
2661+
2662+
@requires_pthreads
2663+
def test_pthread_guardsize_overflow(self):
2664+
self.set_setting('STACK_OVERFLOW_CHECK', 1)
2665+
expected = r'Aborted\(Stack overflow! Stack cookie has been overwritten at 0x[0-9a-fA-F]+, expected hex dwords 0x89BACDFE and 0x2135467, but received 0xaaaaaaaa 0xaaaaaaaa\)'
2666+
self.do_runf('pthread/test_pthread_guardsize_overflow.c', expected, regex=True, assert_returncode=NON_ZERO)
2667+
26582668
@requires_pthreads
26592669
@no_bun('https://github.com/emscripten-core/emscripten/issues/26199')
26602670
@flaky('flaky specifically in esm_integration suite. https://github.com/emscripten-core/emscripten/issues/25151')

0 commit comments

Comments
 (0)