Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Emscripten jmp support in indirect function calls (invoke_xxx) #1611

Merged
merged 15 commits into from
Aug 9, 2023

Conversation

jerbob92
Copy link
Contributor

@jerbob92 jerbob92 commented Aug 4, 2023

This PR implements Emscripten jmp support from indirect function calls, which we have missed before. We missed this mainly due to me lazily patching out Emscripten methods in standalone builds, in this case _emscripten_throw_longjmp, which is a pretty vital part of doing setjmp/longjmp from indirect function calls.

How that seems to work in the JS version:

I have tried to implement it in a very similar way in Wazero, but I have to say that I'm not aware of any edge cases where this does not work.

Sample C code:

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

void jmpfunction(jmp_buf env_buf) {
   longjmp(env_buf, 2);
}

int main() {
   int val;
   jmp_buf env_buffer;

   /* save calling environment for longjmp */
   val = setjmp( env_buffer );
   
   if( val != 0 ) {
      printf("Returned from a longjmp() with value = %d\n", val);
      exit(0);
   }
   
   printf("Jump function call\n");
   jmpfunction( env_buffer );

   return(0);
}

@jerbob92
Copy link
Contributor Author

jerbob92 commented Aug 4, 2023

I think the tests currently fail because the invoke.wat/invoke.wasm do not contain the needed methods (stackSave/stackRestore/setThrew) right now, however, they are always included when you have invoke_ methods, so we can assume that they are always there.

Copy link
Collaborator

@ncruces ncruces left a comment

Choose a reason for hiding this comment

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

Looks good. I assume the same from the failing tests.

stackSave seems easy to stub out (do nothing), stackRestore I have no idea if it's safe to just ignore.

@ncruces
Copy link
Collaborator

ncruces commented Aug 4, 2023

Can you attach a wasm from built from your sample so can avoid figuring out how to build it? 😳

@jerbob92
Copy link
Contributor Author

jerbob92 commented Aug 4, 2023

Looks good. I assume the same from the failing tests.

stackSave seems easy to stub out (do nothing), stackRestore I have no idea if it's safe to just ignore.

I think in the current tests both are safe to stub out, as the tests won't end up in the error branch. If we would make a separate test for the jumps we would have to implement them.

Can you attach a wasm from built from your sample so can avoid figuring out how to build it? flushed

Here you go, compiled with latest emscripten using emcc -g sample.c -o sample.wasm
sample.zip

@ncruces
Copy link
Collaborator

ncruces commented Aug 4, 2023

I think a better implementation would be as such:

	// Lookup the table index we will call.
	t := m.Tables[0] // Note: Emscripten doesn't use multiple tables
	f, err := m.Engine.LookupFunction(t, typeID, tableOffset)
	if err != nil {
		panic(err)
	}

	// Call: savedStack = stackSave()
	var savedStack [2]uint64
	err = mod.ExportedFunction("stackSave").CallWithStack(ctx, savedStack[:])
	if err != nil {
		panic(err) // stackSave failed
	}

	err = f.CallWithStack(ctx, stack)
	if err != nil {
		// Module closed: any calls will just fail with the same error
		if _, ok := err.(*sys.ExitError); ok {
			panic(err)
		}

		// Call: stackRestore(savedStack)
		if err := mod.ExportedFunction("stackRestore").CallWithStack(ctx, savedStack[:]); err != nil {
			panic(err) // stackRestore failed
		}
		if !errors.Is(err, ThrowLongjmpError) {
			panic(err) // f failed
		}

		// Call: setThrew(1, 0)
		savedStack[0] = 1
		savedStack[1] = 0
		err = mod.ExportedFunction("setThrew").CallWithStack(ctx, savedStack[:])
		if err != nil {
			panic(err) // setThrew failed
		}
	}

This uses CallWithStack to avoid a few allocs (to the cost of some obfuscation). We can further keep the [2]uint64 in InvokeFunc to avoid one alloc per call.

More importantly: stackRestore and setThrew here (hopefully that's the correct source) are in the catch block, so their exceptions interrupt flow. We should do the same.

And it's pointless to call stackRestore with a sys.ExitError, because it'll just do the throw out the sys.ExitError again.

@ncruces
Copy link
Collaborator

ncruces commented Aug 4, 2023

My changes, including updating tests so make test passes:
jerbob92/wazero@feature/implement-emscripten-jmp...ncruces:wazero:ncruces-emscripten-jmp

@ncruces
Copy link
Collaborator

ncruces commented Aug 4, 2023

OK, so, sorry for the noise, but here's another idea: what if we just skip the entire dance if mod.ExportedFunction("stackSave") returns nil?

@jerbob92
Copy link
Contributor Author

jerbob92 commented Aug 4, 2023

OK, so, sorry for the noise, but here's another idea: what if we just skip the entire dance if mod.ExportedFunction("stackSave") returns nil?

Fine with me, but it should never be nil if it comes into the invoke call (except for not real emscripten like the Wazero tests).

I think a better implementation would be as such:

	// Lookup the table index we will call.
	t := m.Tables[0] // Note: Emscripten doesn't use multiple tables
	f, err := m.Engine.LookupFunction(t, typeID, tableOffset)
	if err != nil {
		panic(err)
	}

	// Call: savedStack = stackSave()
	var savedStack [2]uint64
	err = mod.ExportedFunction("stackSave").CallWithStack(ctx, savedStack[:])
	if err != nil {
		panic(err) // stackSave failed
	}

	err = f.CallWithStack(ctx, stack)
	if err != nil {
		// Module closed: any calls will just fail with the same error
		if _, ok := err.(*sys.ExitError); ok {
			panic(err)
		}

		// Call: stackRestore(savedStack)
		if err := mod.ExportedFunction("stackRestore").CallWithStack(ctx, savedStack[:]); err != nil {
			panic(err) // stackRestore failed
		}
		if !errors.Is(err, ThrowLongjmpError) {
			panic(err) // f failed
		}

		// Call: setThrew(1, 0)
		savedStack[0] = 1
		savedStack[1] = 0
		err = mod.ExportedFunction("setThrew").CallWithStack(ctx, savedStack[:])
		if err != nil {
			panic(err) // setThrew failed
		}
	}

This uses CallWithStack to avoid a few allocs (to the cost of some obfuscation). We can further keep the [2]uint64 in InvokeFunc to avoid one alloc per call.

More importantly: stackRestore and setThrew here (hopefully that's the correct source) are in the catch block, so their exceptions interrupt flow. We should do the same.

And it's pointless to call stackRestore with a sys.ExitError, because it'll just do the throw out the sys.ExitError again.

These changes sound good to me!

@ncruces
Copy link
Collaborator

ncruces commented Aug 4, 2023

Want to integrate them in your branch? Not sure I can.

@jerbob92 jerbob92 force-pushed the feature/implement-emscripten-jmp branch 2 times, most recently from cbf94a1 to 4195138 Compare August 6, 2023 13:39
internal/emscripten/emscripten.go Outdated Show resolved Hide resolved
@mathetake
Copy link
Member

it seems the failing tests legit but not directly relevant to this change (I guess it's something to do with unsafe.pointer/uintptr cast thingy)

@mathetake
Copy link
Member

no luck so far. The only thing I can say is this is not compiler specific (happens to interpreter as well), but I hanve't been able to reproduce

@jerbob92 jerbob92 force-pushed the feature/implement-emscripten-jmp branch from d7cfbcd to 5d12187 Compare August 9, 2023 12:32
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
…s about Emscripten implementation details

Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
@jerbob92 jerbob92 force-pushed the feature/implement-emscripten-jmp branch 2 times, most recently from 8f39227 to f95ece8 Compare August 9, 2023 12:35
jerbob92 and others added 6 commits August 9, 2023 14:35
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
Otherwise, the table address might change and dangerous

Signed-off-by: Takeshi Yoneda <t.y.mathetake@gmail.com>
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
Signed-off-by: Takeshi Yoneda <t.y.mathetake@gmail.com>
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
This reverts commit 67b901a.

Signed-off-by: Takeshi Yoneda <t.y.mathetake@gmail.com>
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
Signed-off-by: Jeroen Bobbeldijk <jeroen@klippa.com>
@mathetake mathetake merged commit a2b6510 into tetratelabs:main Aug 9, 2023
54 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants