Skip to content

Commit 0806384

Browse files
authored
Put stack first when not optimizing (#18154)
Also, when not using santizers (which currently rely on using the start of memory). This allows `STACK_OVERFLOW_CHECK=1` to detect stack overflow conditions better without relying on the `STACK_OVERFLOW_CHECK=2` binaryen pass. This works because when stack is placed first in memory stack overflow results in SP dropping below zero which is a runtime error. We can then distinguish such runtime errors by looking at the SP value at the time of the crash. This change is part of a sequence of the effort to reduce the default stack size. The hope here is that we will be able to accurately catch overflows in debug builds even without the `STACK_OVERFLOW_CHECK=2` binaryen pass, thus minimizing the impact of reducing the stack size. See #14177
1 parent b92f824 commit 0806384

25 files changed

+92
-32
lines changed

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,7 @@ jobs:
551551
core2.test_em_js
552552
core2.test_em_js_pthreads
553553
core2.test_unicode_js_library
554+
other.test_stack_overflow
554555
other.test_dlmalloc_modes
555556
other.test_c_preprocessor
556557
other.test_prejs_unicode

ChangeLog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ See docs/process.md for more on how version tagging works.
2626
that will allow the command to be replicated. This can be useful for sharing
2727
reproduction cases with others (inspired by the lld option of the same name).
2828
(#18160)
29+
- In non-optimizing builds emscripten will now place the stack first in memory,
30+
before global data. This is to get more accurate stack overflow errors (since
31+
overflow will trap rather corrupting global data first). This should not
32+
be a user-visible change (unless your program does something very odd such
33+
depending on the specific location of stack data in memory). (#18154)
2934

3035
3.1.25 - 11/08/22
3136
-----------------

emcc.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1883,6 +1883,14 @@ def phase_linker_setup(options, state, newargs):
18831883
if not settings.AUTO_JS_LIBRARIES:
18841884
default_setting('USE_SDL', 0)
18851885

1886+
if 'GLOBAL_BASE' not in user_settings and not settings.SHRINK_LEVEL and not settings.OPT_LEVEL:
1887+
# When optimizing for size it helps to put static data first before
1888+
# the stack (sincs this makes instructions for accessing this data
1889+
# use a smaller LEB encoding).
1890+
# However, for debugability is better to have the stack come first
1891+
# (becuase stack overflows will trap rather than corrupting data).
1892+
settings.STACK_FIRST = True
1893+
18861894
# Default to TEXTDECODER=2 (always use TextDecoder to decode UTF-8 strings)
18871895
# in -Oz builds, since custom decoder for UTF-8 takes up space.
18881896
# In pthreads enabled builds, TEXTDECODER==2 may not work, see
@@ -2074,7 +2082,8 @@ def phase_linker_setup(options, state, newargs):
20742082
settings.REQUIRED_EXPORTS += [
20752083
'emscripten_stack_get_end',
20762084
'emscripten_stack_get_free',
2077-
'emscripten_stack_get_base'
2085+
'emscripten_stack_get_base',
2086+
'emscripten_stack_get_current',
20782087
]
20792088

20802089
# We call one of these two functions during startup which caches the stack limits
@@ -2219,7 +2228,7 @@ def phase_linker_setup(options, state, newargs):
22192228
exit_with_error('cannot have both DYNAMIC_EXECUTION=0 and RELOCATABLE enabled at the same time, since RELOCATABLE needs to eval()')
22202229

22212230
if settings.SIDE_MODULE and 'GLOBAL_BASE' in user_settings:
2222-
exit_with_error('Cannot set GLOBAL_BASE when building SIDE_MODULE')
2231+
exit_with_error('GLOBAL_BASE is not compatible with SIDE_MODULE')
22232232

22242233
if options.use_preload_plugins or len(options.preload_files) or len(options.embed_files):
22252234
if settings.NODERAWFS:
@@ -2567,6 +2576,7 @@ def check_memory_setting(setting):
25672576
# We start our global data after the shadow memory.
25682577
# We don't need to worry about alignment here. wasm-ld will take care of that.
25692578
settings.GLOBAL_BASE = shadow_size
2579+
settings.STACK_FIRST = False
25702580

25712581
if not settings.ALLOW_MEMORY_GROWTH:
25722582
settings.INITIAL_MEMORY = total_mem

src/library.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3442,6 +3442,14 @@ mergeInto(LibraryManager.library, {
34423442
if (e instanceof ExitStatus || e == 'unwind') {
34433443
return EXITSTATUS;
34443444
}
3445+
#if STACK_OVERFLOW_CHECK
3446+
checkStackCookie();
3447+
if (e instanceof WebAssembly.RuntimeError) {
3448+
if (_emscripten_stack_get_current() <= 0) {
3449+
err('Stack overflow detected. You can try increasing -sSTACK_SIZE (currently set to ' + STACK_SIZE + ')');
3450+
}
3451+
}
3452+
#endif
34453453
#if MINIMAL_RUNTIME
34463454
throw e;
34473455
#else

src/runtime_stack_check.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ function writeStackCookie() {
1414
#if ASSERTIONS
1515
assert((max & 3) == 0);
1616
#endif
17+
// If the stack ends at address zero we write our cookies 4 bytes into the
18+
// stack. This prevents interference with the (separate) address-zero check
19+
// below.
20+
if (max == 0) {
21+
max += 4;
22+
}
1723
// The stack grow downwards towards _emscripten_stack_get_end.
1824
// We write cookies to the final two words in the stack and detect if they are
1925
// ever overwritten.
@@ -33,6 +39,10 @@ function checkStackCookie() {
3339
#if RUNTIME_DEBUG
3440
dbg('checkStackCookie: ' + max.toString(16));
3541
#endif
42+
// See writeStackCookie().
43+
if (max == 0) {
44+
max += 4;
45+
}
3646
var cookie1 = {{{ makeGetValue('max', 0, 'u32') }}};
3747
var cookie2 = {{{ makeGetValue('max', 4, 'u32') }}};
3848
if (cookie1 != 0x2135467 || cookie2 != 0x89BACDFE) {

src/settings_internal.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,5 @@ var ALL_INCOMING_MODULE_JS_API = [];
244244
// when weak symbols are undefined. Only applies in the case of dyanmic linking
245245
// (MAIN_MODULE).
246246
var WEAK_IMPORTS = [];
247+
248+
var STACK_FIRST = false;
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
#include <stdio.h>
77
#include <stdlib.h>
88
#include <stdint.h>
9-
#include <emscripten.h>
9+
#include <emscripten/emscripten.h>
10+
#include <emscripten/stack.h>
1011

1112
void recurse(unsigned long x);
1213

@@ -18,7 +19,7 @@ void act(volatile unsigned long *a) {
1819
}
1920

2021
void recurse(volatile unsigned long x) {
21-
printf("recurse %ld\n", x);
22+
printf("recurse %ld sp=%#lx\n", x, emscripten_stack_get_current());
2223
volatile unsigned long a = x;
2324
volatile char buffer[1000*1000];
2425
buffer[x/2] = 0;
@@ -36,4 +37,3 @@ void recurse(volatile unsigned long x) {
3637
int main() {
3738
recurse(1000*1000);
3839
}
39-

test/core/test_emmalloc_trim.out

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
11
heap size: 134217728
22
dynamic heap 0: 4096
33
free dynamic memory 0: 4096
4-
unclaimed heap memory 0: 2142171136
5-
sbrk 0: 0x502000
4+
unclaimed heap memory 0: 2142175232
5+
sbrk 0: 0x501000
66
1
77
dynamic heap 1: 37752832
88
free dynamic memory 1: 4096
9-
unclaimed heap memory 1: 2104422400
10-
sbrk 1: 0x2902000
9+
unclaimed heap memory 1: 2104426496
10+
sbrk 1: 0x2901000
1111
1st trim: 1
1212
dynamic heap 1: 37752832
1313
free dynamic memory 1: 0
14-
unclaimed heap memory 1: 2104422400
15-
sbrk 1: 0x2902000
14+
unclaimed heap memory 1: 2104426496
15+
sbrk 1: 0x2901000
1616
2nd trim: 0
1717
dynamic heap 2: 37752832
1818
free dynamic memory 2: 0
19-
unclaimed heap memory 2: 2104422400
20-
sbrk 2: 0x2902000
19+
unclaimed heap memory 2: 2104426496
20+
sbrk 2: 0x2901000
2121
3rd trim: 1
2222
dynamic heap 3: 33656832
2323
free dynamic memory 3: 102400
24-
unclaimed heap memory 3: 2104422400
25-
sbrk 3: 0x2902000
24+
unclaimed heap memory 3: 2104426496
25+
sbrk 3: 0x2901000
2626
4th trim: 0
2727
dynamic heap 4: 33656832
2828
free dynamic memory 4: 102400
29-
unclaimed heap memory 4: 2104422400
30-
sbrk 4: 0x2902000
29+
unclaimed heap memory 4: 2104426496
30+
sbrk 4: 0x2901000
3131
5th trim: 1
3232
dynamic heap 5: 33558528
3333
free dynamic memory 5: 0
34-
unclaimed heap memory 5: 2104422400
35-
sbrk 5: 0x2902000
34+
unclaimed heap memory 5: 2104426496
35+
sbrk 5: 0x2901000

test/core/test_stack_placement.c

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include <stdio.h>
99
#include <assert.h>
10+
#include <emscripten/stack.h>
1011

1112
/* We had a regression where the stack position was not taking into account
1213
* the data and bss. This test runs with 1024 byte stack which given that bug
@@ -22,7 +23,14 @@ int main(int argc, char* argv[]) {
2223
int* bss_address = &static_bss[BSS_BYTES-1];
2324
int* stack_address = &stack_var;
2425
printf("data: %p bss: %p stack: %p\n", data_address, bss_address, stack_address);
25-
assert(stack_address > bss_address);
26+
// Stack can either come after BSS or before data (In debug builds we link
27+
// with --stack-first).
28+
int stack_first = emscripten_stack_get_end() == 0;
29+
if (stack_first) {
30+
assert(stack_address < data_address);
31+
} else {
32+
assert(stack_address > bss_address);
33+
}
2634
assert(bss_address > data_address);
2735
printf("success.\n");
2836
return 0;

test/other/metadce/test_metadce_hello_O0.exports

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ __indirect_function_table
33
__wasm_call_ctors
44
dynCall_jiji
55
emscripten_stack_get_base
6+
emscripten_stack_get_current
67
emscripten_stack_get_end
78
emscripten_stack_get_free
89
emscripten_stack_init

0 commit comments

Comments
 (0)