Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions scripts/test/lld.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@


def args_for_finalize(filename):
if 'shared' in filename:
return ['--side-module']
else:
return ['--global-base=568']
if 'safe_stack' in filename:
return ['--check-stack-overflow', '--global-base=568']
elif 'shared' in filename:
return ['--side-module']
else:
return ['--global-base=568']


def test_wasm_emscripten_finalize():
Expand Down
12 changes: 12 additions & 0 deletions src/tools/wasm-emscripten-finalize.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ int main(int argc, const char* argv[]) {
bool debugInfo = false;
bool isSideModule = false;
bool legalizeJavaScriptFFI = true;
bool checkStackOverflow = false;
uint64_t globalBase = INVALID_BASE;
ToolOptions options("wasm-emscripten-finalize",
"Performs Emscripten-specific transforms on .wasm files");
Expand Down Expand Up @@ -127,6 +128,13 @@ int main(int argc, const char* argv[]) {
[&dataSegmentFile](Options* o, const std::string& argument) {
dataSegmentFile = argument;
})
.add("--check-stack-overflow",
"",
"Check for stack overflows every time the stack is extended",
Options::Arguments::Zero,
[&checkStackOverflow](Options* o, const std::string&) {
checkStackOverflow = true;
})
.add_positional("INFILE",
Options::Arguments::One,
[&infile](Options* o, const std::string& argument) {
Expand Down Expand Up @@ -200,6 +208,10 @@ int main(int argc, const char* argv[]) {
}
wasm.updateMaps();

if (checkStackOverflow && !isSideModule) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of silently doing nothing for side modules, how about Fatal() << "stack overflow checks are not supported with side modules"; ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, we actually need to do nothing for side modules because all stack operations gets turned into stackSave and stackRestore, which are exported from the main module. In the main module, stackRestore performs the overflow check.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that right? I thought side modules manage their own stack in their own memory. At least that's how fastcomp does it - is it completely different in the wasm backend?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like it. This is the prologue of a function in the side module:

 (func $f (; 5 ;) (type $2) (result i32)
  (local $0 i32)
  (local $1 i32)
  (local $2 i32)
  ...
  (local.set $0
   (call $stackSave)
  )
  (local.set $1
   (i32.const 8192)
  )
  (local.set $2
   (i32.sub
    (local.get $0)
    (local.get $1)
   )
  )
  (call $stackRestore
   (local.get $2)
  )

and this is the epilogue:

  (local.set $7
   (i32.const 8192)
  )
  (local.set $8
   (i32.add
    (local.get $2)
    (local.get $7)
   )
  )
  (call $stackRestore
   (local.get $8)
  )

stackSave and stackRestore are imported:

 (import "env" "stackSave" (func $stackSave (result i32)))
 (import "env" "stackRestore" (func $stackRestore (param i32)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's very surprising. Let's see what @sbc100 says. But that doesn't need to block this PR landing (we may need a followup though).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this surprising? The sp with the wasm backend is mutable global but we don't have any way to import or export mutable globals (yet), so the side module has to manipulate the global via these helper functions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks @sbc100

I guess I'm used to the fastcomp model where not just threads but shared modules also got their own stack.

generator.enforceStackLimit();
}

if (isSideModule) {
generator.replaceStackPointerGlobal();
generator.generatePostInstantiateFunction();
Expand Down
6 changes: 5 additions & 1 deletion src/wasm-emscripten.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class EmscriptenGlueGenerator {

void fixInvokeFunctionNames();

void enforceStackLimit();

// Emits the data segments to a file. The file contains data from address base
// onwards (we must pass in base, as we can't tell it from the wasm - the
// first segment may start after a run of zeros, but we need those zeros in
Expand All @@ -71,11 +73,13 @@ class EmscriptenGlueGenerator {

Global* getStackPointerGlobal();
Expression* generateLoadStackPointer();
Expression* generateStoreStackPointer(Expression* value);
Expression* generateStoreStackPointer(Function* func, Expression* value);
void generateDynCallThunk(std::string sig);
void generateStackSaveFunction();
void generateStackAllocFunction();
void generateStackRestoreFunction();
void generateSetStackLimitFunction();
Name importStackOverflowHandler();
};

} // namespace wasm
Expand Down
121 changes: 118 additions & 3 deletions src/wasm/wasm-emscripten.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ static Name STACK_SAVE("stackSave");
static Name STACK_RESTORE("stackRestore");
static Name STACK_ALLOC("stackAlloc");
static Name STACK_INIT("stack$init");
static Name STACK_LIMIT("__stack_limit");
static Name SET_STACK_LIMIT("__set_stack_limit");
static Name POST_INSTANTIATE("__post_instantiate");
static Name ASSIGN_GOT_ENTIRES("__assign_got_enties");
static Name STACK_OVERFLOW_IMPORT("__handle_stack_overflow");

void addExportedFunction(Module& wasm, Function* function) {
wasm.addFunction(function);
Expand Down Expand Up @@ -92,8 +95,32 @@ Expression* EmscriptenGlueGenerator::generateLoadStackPointer() {
return builder.makeGlobalGet(stackPointer->name, i32);
}

inline Expression* stackBoundsCheck(Builder& builder,
Function* func,
Expression* value,
Global* stackPointer,
Global* stackLimit,
Name handler) {
// Add a local to store the value of the expression. We need the value twice:
// once to check if it has overflowed, and again to assign to store it.
auto newSP = Builder::addVar(func, stackPointer->type);
// (if (i32.lt_u (local.tee $newSP (...value...)) (global.get $__stack_limit))
// (call $handler))
auto check =
builder.makeIf(builder.makeBinary(
BinaryOp::LtUInt32,
builder.makeLocalTee(newSP, value),
builder.makeGlobalGet(stackLimit->name, stackLimit->type)),
builder.makeCall(handler, {}, none));
// (global.set $__stack_pointer (local.get $newSP))
auto newSet = builder.makeGlobalSet(
stackPointer->name, builder.makeLocalGet(newSP, stackPointer->type));
return builder.blockify(check, newSet);
}

Expression*
EmscriptenGlueGenerator::generateStoreStackPointer(Expression* value) {
EmscriptenGlueGenerator::generateStoreStackPointer(Function* func,
Expression* value) {
if (!useStackPointerGlobal) {
return builder.makeStore(
/* bytes =*/4,
Expand All @@ -107,6 +134,14 @@ EmscriptenGlueGenerator::generateStoreStackPointer(Expression* value) {
if (!stackPointer) {
Fatal() << "stack pointer global not found";
}
if (auto* stackLimit = wasm.getGlobalOrNull(STACK_LIMIT)) {
return stackBoundsCheck(builder,
func,
value,
stackPointer,
stackLimit,
importStackOverflowHandler());
}
return builder.makeGlobalSet(stackPointer->name, value);
}

Expand All @@ -132,7 +167,7 @@ void EmscriptenGlueGenerator::generateStackAllocFunction() {
Const* subConst = builder.makeConst(Literal(~bitMask));
Binary* maskedSub = builder.makeBinary(AndInt32, sub, subConst);
LocalSet* teeStackLocal = builder.makeLocalTee(1, maskedSub);
Expression* storeStack = generateStoreStackPointer(teeStackLocal);
Expression* storeStack = generateStoreStackPointer(function, teeStackLocal);

Block* block = builder.makeBlock();
block->list.push_back(storeStack);
Expand All @@ -149,7 +184,7 @@ void EmscriptenGlueGenerator::generateStackRestoreFunction() {
Function* function =
builder.makeFunction(STACK_RESTORE, std::move(params), none, {});
LocalGet* getArg = builder.makeLocalGet(0, i32);
Expression* store = generateStoreStackPointer(getArg);
Expression* store = generateStoreStackPointer(function, getArg);

function->body = store;

Expand Down Expand Up @@ -444,6 +479,86 @@ void EmscriptenGlueGenerator::replaceStackPointerGlobal() {
wasm.removeGlobal(stackPointer->name);
}

struct StackLimitEnforcer : public WalkerPass<PostWalker<StackLimitEnforcer>> {
StackLimitEnforcer(Global* stackPointer,
Global* stackLimit,
Builder& builder,
Name handler)
: stackPointer(stackPointer), stackLimit(stackLimit), builder(builder),
handler(handler) {}

bool isFunctionParallel() override { return true; }

Pass* create() override {
return new StackLimitEnforcer(stackPointer, stackLimit, builder, handler);
}

void visitGlobalSet(GlobalSet* curr) {
if (getModule()->getGlobalOrNull(curr->name) == stackPointer) {
replaceCurrent(stackBoundsCheck(builder,
getFunction(),
curr->value,
stackPointer,
stackLimit,
handler));
}
}

private:
Global* stackPointer;
Global* stackLimit;
Builder& builder;
Name handler;
};

void EmscriptenGlueGenerator::enforceStackLimit() {
Global* stackPointer = getStackPointerGlobal();
if (!stackPointer) {
return;
}

auto* stackLimit = builder.makeGlobal(STACK_LIMIT,
stackPointer->type,
builder.makeConst(Literal(0)),
Builder::Mutable);
wasm.addGlobal(stackLimit);

auto handler = importStackOverflowHandler();

StackLimitEnforcer walker(stackPointer, stackLimit, builder, handler);
PassRunner runner(&wasm);
walker.run(&runner, &wasm);

generateSetStackLimitFunction();
}

void EmscriptenGlueGenerator::generateSetStackLimitFunction() {
Function* function =
builder.makeFunction(SET_STACK_LIMIT, std::vector<Type>({i32}), none, {});
LocalGet* getArg = builder.makeLocalGet(0, i32);
Expression* store = builder.makeGlobalSet(STACK_LIMIT, getArg);
function->body = store;
addExportedFunction(wasm, function);
}

Name EmscriptenGlueGenerator::importStackOverflowHandler() {
ImportInfo info(wasm);

if (auto* existing = info.getImportedFunction(ENV, STACK_OVERFLOW_IMPORT)) {
return existing->name;
} else {
auto* import = new Function;
import->name = STACK_OVERFLOW_IMPORT;
import->module = ENV;
import->base = STACK_OVERFLOW_IMPORT;
auto* functionType = ensureFunctionType("v", &wasm);
import->type = functionType->name;
FunctionTypeUtils::fillFunction(import, functionType);
wasm.addFunction(import);
return STACK_OVERFLOW_IMPORT;
}
}

const Address UNKNOWN_OFFSET(uint32_t(-1));

std::vector<Address> getSegmentOffsets(Module& wasm) {
Expand Down
90 changes: 90 additions & 0 deletions test/lld/recursive_safe_stack.wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
(module
(type $0 (func (param i32 i32) (result i32)))
(type $1 (func))
(type $2 (func (result i32)))
(import "env" "printf" (func $printf (param i32 i32) (result i32)))
(memory $0 2)
(data (i32.const 568) "%d:%d\n\00Result: %d\n\00")
(table $0 1 1 funcref)
(global $global$0 (mut i32) (i32.const 66128))
(global $global$1 i32 (i32.const 66128))
(global $global$2 i32 (i32.const 587))
(export "memory" (memory $0))
(export "__wasm_call_ctors" (func $__wasm_call_ctors))
(export "__heap_base" (global $global$1))
(export "__data_end" (global $global$2))
(export "main" (func $main))
(func $__wasm_call_ctors (; 1 ;) (type $1)
)
(func $foo (; 2 ;) (type $0) (param $0 i32) (param $1 i32) (result i32)
(local $2 i32)
(global.set $global$0
(local.tee $2
(i32.sub
(global.get $global$0)
(i32.const 16)
)
)
)
(i32.store offset=4
(local.get $2)
(local.get $1)
)
(i32.store
(local.get $2)
(local.get $0)
)
(drop
(call $printf
(i32.const 568)
(local.get $2)
)
)
(global.set $global$0
(i32.add
(local.get $2)
(i32.const 16)
)
)
(i32.add
(local.get $1)
(local.get $0)
)
)
(func $__original_main (; 3 ;) (type $2) (result i32)
(local $0 i32)
(global.set $global$0
(local.tee $0
(i32.sub
(global.get $global$0)
(i32.const 16)
)
)
)
(i32.store
(local.get $0)
(call $foo
(i32.const 1)
(i32.const 2)
)
)
(drop
(call $printf
(i32.const 575)
(local.get $0)
)
)
(global.set $global$0
(i32.add
(local.get $0)
(i32.const 16)
)
)
(i32.const 0)
)
(func $main (; 4 ;) (type $0) (param $0 i32) (param $1 i32) (result i32)
(call $__original_main)
)
;; custom section "producers", size 111
)

Loading