diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 1d71d62516..4132216df1 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -400,6 +400,16 @@ jobs: # Run the wasm binary using llgo_wasm iwasm --stack-size=819200000 --heap-size=800000000 hello.wasm + ( + cd "$GITHUB_WORKSPACE" + LLGO_WASI_THREADS=1 GOOS=wasip1 GOARCH=wasm llgo build \ + -o "$RUNNER_TEMP/wasm-blocking-threads.wasm" \ + ./internal/build/testdata/wasm-blocking + ) + output=$(iwasm --stack-size=819200000 --heap-size=800000000 \ + "$RUNNER_TEMP/wasm-blocking-threads.wasm" 2>&1) + test "$output" = "wasm blocking primitives ok" + wasm-runtime: timeout-minutes: 30 strategy: @@ -491,6 +501,14 @@ jobs: grep -Fxq "$expected" <<<"$output" } + run_wasm_hardening() { + local module="$1" + local argument="$2" + local output + output=$(node -e "import('$module').then(async imported => { const config = { arguments: ['$argument'], preRun: [() => { config.ENV.LLGO_WASM_EXPECT_ARG = '$argument'; config.ENV.LLGO_WASM_BLOCKED_G = '1000'; }] }; await imported.default(config); }).catch(error => { console.error(error); process.exit(1); });" 2>&1) + grep -Fxq "wasm hardening ok" <<<"$output" + } + run_wasi_timers() { local output wasm-tools validate --features all "$1" @@ -527,6 +545,18 @@ jobs: LLGO_WASM_WORKERS=2 llgo build -target wasm \ -o "$RUNNER_TEMP/wasm-gc-workers.mjs" ./internal/build/testdata/wasm-gc run_wasm_workers "$RUNNER_TEMP/wasm-gc-workers.mjs" "wasm gc ok" + GOOS=js GOARCH=wasm llgo build \ + -o "$RUNNER_TEMP/wasm-hardening-go.mjs" ./internal/build/testdata/wasm-hardening + run_wasm_hardening "$RUNNER_TEMP/wasm-hardening-go.mjs" "hardening-j64" + llgo build -target wasm \ + -o "$RUNNER_TEMP/wasm-hardening.mjs" ./internal/build/testdata/wasm-hardening + run_wasm_hardening "$RUNNER_TEMP/wasm-hardening.mjs" "hardening-j32" + LLGO_WASM_WORKERS=2 GOOS=js GOARCH=wasm llgo build \ + -o "$RUNNER_TEMP/wasm-hardening-workers-go.mjs" ./internal/build/testdata/wasm-hardening + run_wasm_hardening "$RUNNER_TEMP/wasm-hardening-workers-go.mjs" "hardening-workers-j64" + LLGO_WASM_WORKERS=2 llgo build -target wasm \ + -o "$RUNNER_TEMP/wasm-hardening-workers.mjs" ./internal/build/testdata/wasm-hardening + run_wasm_hardening "$RUNNER_TEMP/wasm-hardening-workers.mjs" "hardening-workers-j32" cp ./internal/build/testdata/wasm-workers/browser.html "$RUNNER_TEMP/browser.html" node ./internal/build/testdata/wasm-workers/server.mjs "$RUNNER_TEMP" 8123 & browser_server=$! @@ -541,7 +571,8 @@ jobs: browser="$(command -v google-chrome || command -v google-chrome-stable || command -v chromium || true)" test -n "$browser" for module in wasm-timers.mjs wasm-workers.mjs wasm-workers-go.mjs \ - wasm-gc-workers.mjs wasm-gc-workers-go.mjs; do + wasm-gc-workers.mjs wasm-gc-workers-go.mjs wasm-hardening-workers.mjs \ + wasm-hardening-workers-go.mjs; do html=$("$browser" --headless=new --no-sandbox --disable-gpu \ --disable-dev-shm-usage --virtual-time-budget=15000 --dump-dom \ "http://127.0.0.1:8123/browser.html?module=$module") @@ -557,6 +588,14 @@ jobs: GOOS=wasip1 GOARCH=wasm LLGO_WASI_THREADS=0 llgo build -o "$RUNNER_TEMP/wasm-gc-wasip1.wasm" ./internal/build/testdata/wasm-gc wasm-tools validate --features all "$RUNNER_TEMP/wasm-gc-wasip1.wasm" test "$(wasmtime run -W exceptions=y "$RUNNER_TEMP/wasm-gc-wasip1.wasm" 2>&1)" = "wasm gc ok" + GOOS=wasip1 GOARCH=wasm LLGO_WASI_THREADS=0 llgo build \ + -o "$RUNNER_TEMP/wasm-hardening-wasip1.wasm" ./internal/build/testdata/wasm-hardening + wasm-tools validate --features all "$RUNNER_TEMP/wasm-hardening-wasip1.wasm" + output=$(wasmtime run -W exceptions=y \ + --env LLGO_WASM_EXPECT_ARG=hardening-p1 \ + --env LLGO_WASM_BLOCKED_G=1000 \ + "$RUNNER_TEMP/wasm-hardening-wasip1.wasm" hardening-p1 2>&1) + grep -Fxq "wasm hardening ok" <<<"$output" file "$RUNNER_TEMP/runtime-js.wasm" \ "$RUNNER_TEMP/runtime-wasip1.wasm" \ "$RUNNER_TEMP/runtime-wasip1-threads.wasm" \ diff --git a/internal/build/build_test.go b/internal/build/build_test.go index af81a3ec48..f0163041e9 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -490,6 +490,7 @@ func TestWasmRuntimeAvoidsNativeHostDependencies(t *testing.T) { for _, name := range []string{ "mfinal_wasm.go", + "poll_stub_llgo.go", "runtime_baremetal.go", "runtime_gc_nonmoving.go", "signal_baremetal_llgo.go", @@ -500,6 +501,17 @@ func TestWasmRuntimeAvoidsNativeHostDependencies(t *testing.T) { t.Errorf("wasm runtime did not select %s", name) } } + argsFile := "link_wasm_js_llgo.go" + otherArgsFile := "link_wasip1_llgo.go" + if goos == "wasip1" { + argsFile, otherArgsFile = otherArgsFile, argsFile + } + if !selected[argsFile] { + t.Errorf("%s runtime did not select %s", goos, argsFile) + } + if selected[otherArgsFile] { + t.Errorf("%s runtime selected host-specific %s", goos, otherArgsFile) + } }) } } diff --git a/internal/build/testdata/wasm-hardening/abi.c b/internal/build/testdata/wasm-hardening/abi.c new file mode 100644 index 0000000000..96089bc8b3 --- /dev/null +++ b/internal/build/testdata/wasm-hardening/abi.c @@ -0,0 +1,26 @@ +#include + +#if defined(__EMSCRIPTEN__) && defined(__EMSCRIPTEN_PTHREADS__) +#include +#include + +static _Atomic int llgo_hardening_entered; +#endif + +uint64_t llgo_hardening_hold_payload(const uint64_t *value) { +#if defined(__EMSCRIPTEN__) && defined(__EMSCRIPTEN_PTHREADS__) + atomic_store_explicit(&llgo_hardening_entered, 1, memory_order_release); + double deadline = emscripten_get_now() + 25.0; + while (emscripten_get_now() < deadline) { + } +#endif + return *value; +} + +int llgo_hardening_hold_entered(void) { +#if defined(__EMSCRIPTEN__) && defined(__EMSCRIPTEN_PTHREADS__) + return atomic_load_explicit(&llgo_hardening_entered, memory_order_acquire); +#else + return 0; +#endif +} diff --git a/internal/build/testdata/wasm-hardening/abi.go b/internal/build/testdata/wasm-hardening/abi.go new file mode 100644 index 0000000000..e97cf7d02c --- /dev/null +++ b/internal/build/testdata/wasm-hardening/abi.go @@ -0,0 +1,11 @@ +package main + +import _ "unsafe" + +const LLGoFiles = "abi.c" + +//go:linkname holdPayloadInC C.llgo_hardening_hold_payload +func holdPayloadInC(*payload) uint64 + +//go:linkname cHoldEntered C.llgo_hardening_hold_entered +func cHoldEntered() int32 diff --git a/internal/build/testdata/wasm-hardening/main.go b/internal/build/testdata/wasm-hardening/main.go new file mode 100644 index 0000000000..eef3d77602 --- /dev/null +++ b/internal/build/testdata/wasm-hardening/main.go @@ -0,0 +1,158 @@ +package main + +import ( + "os" + "runtime" + "time" +) + +type payload struct { + value uint64 +} + +type indirectRunner interface { + run(chan<- struct{}, <-chan struct{}) *payload +} + +type indirectState struct { + value *payload +} + +func (state *indirectState) run(ready chan<- struct{}, resume <-chan struct{}) (result *payload) { + defer func() { + if recovered := recover(); recovered != "indirect panic" { + panic("unexpected indirect recover value") + } + result = state.value + }() + ready <- struct{}{} + <-resume + panic("indirect panic") +} + +func main() { + testProcessArgs() + testIndirectSuspension() + testSchedulerHandoffs() + testBlockedGRoots(blockedGCount()) + testWorkerCBoundary() + testCanceledExitTimer() + println("wasm hardening ok") +} + +func testProcessArgs() { + expected := os.Getenv("LLGO_WASM_EXPECT_ARG") + if expected == "" { + return + } + if len(os.Args) < 2 || os.Args[len(os.Args)-1] != expected { + panic("process arguments were not preserved") + } +} + +func testIndirectSuspension() { + state := &indirectState{value: &payload{value: 0x12345678}} + var runner indirectRunner = state + call := runner.run + ready := make(chan struct{}) + resume := make(chan struct{}) + done := make(chan *payload) + go func() { + done <- call(ready, resume) + }() + <-ready + runtime.GC() + close(resume) + if got := <-done; got == nil || got.value != 0x12345678 { + panic("indirect suspended root was not retained") + } +} + +func testSchedulerHandoffs() { + const count = 10_000 + ping := make(chan int) + pong := make(chan int) + done := make(chan struct{}) + go func() { + for i := range count { + if value := <-ping; value != i { + panic("scheduler ping lost ordering") + } + pong <- i + } + close(done) + }() + start := time.Now() + for i := range count { + ping <- i + if value := <-pong; value != i { + panic("scheduler pong lost ordering") + } + } + <-done + println("wasm channel roundtrip ns/op", time.Since(start).Nanoseconds()/count) +} + +func blockedGCount() int { + const defaultCount = 100 + value := os.Getenv("LLGO_WASM_BLOCKED_G") + if value == "" { + return defaultCount + } + count := 0 + for i := range len(value) { + digit := value[i] + if digit < '0' || digit > '9' { + panic("invalid LLGO_WASM_BLOCKED_G") + } + count = count*10 + int(digit-'0') + if count > 10_000 { + panic("invalid LLGO_WASM_BLOCKED_G") + } + } + if count < 1 { + panic("invalid LLGO_WASM_BLOCKED_G") + } + return count +} + +func testBlockedGRoots(count int) { + ready := make(chan struct{}, count) + release := make(chan struct{}) + done := make(chan uint64, count) + var before runtime.MemStats + runtime.ReadMemStats(&before) + for i := range count { + go func() { + live := &payload{value: uint64(i + 1)} + ready <- struct{}{} + <-release + done <- live.value + }() + } + for range count { + <-ready + } + runtime.GC() + close(release) + var sum uint64 + for range count { + sum += <-done + } + want := uint64(count) * uint64(count+1) / 2 + if sum != want { + panic("blocked goroutine roots were not retained") + } + var after runtime.MemStats + runtime.ReadMemStats(&after) + println("wasm blocked goroutines", count, "heap_sys_delta", int64(after.HeapSys)-int64(before.HeapSys)) +} + +func testCanceledExitTimer() { + timer := time.AfterFunc(time.Hour, func() { + panic("canceled exit timer fired") + }) + if !timer.Stop() { + panic("failed to cancel exit timer") + } +} diff --git a/internal/build/testdata/wasm-hardening/workers.go b/internal/build/testdata/wasm-hardening/workers.go new file mode 100644 index 0000000000..484e416d6a --- /dev/null +++ b/internal/build/testdata/wasm-hardening/workers.go @@ -0,0 +1,46 @@ +//go:build llgo.wasm_workers + +package main + +import ( + "runtime" + _ "unsafe" +) + +//go:linkname schedulerProcID github.com/goplus/llgo/runtime/internal/runtime.SchedulerProcID +func schedulerProcID() int + +const workerPayloadValue = 0xabcdef01 + +func testWorkerCBoundary() { + for { + proc := make(chan int) + start := make(chan bool) + done := make(chan uint64) + go func() { + proc <- schedulerProcID() + if !<-start { + done <- 0 + return + } + live := &payload{value: workerPayloadValue} + value := holdPayloadInC(live) + runtime.Gosched() + done <- value + }() + if <-proc == 0 { + start <- false + <-done + continue + } + start <- true + for cHoldEntered() == 0 { + runtime.Gosched() + } + runtime.GC() + if value := <-done; value != workerPayloadValue { + panic("C boundary lost a live Go pointer") + } + return + } +} diff --git a/internal/build/testdata/wasm-hardening/workers_stub.go b/internal/build/testdata/wasm-hardening/workers_stub.go new file mode 100644 index 0000000000..be01c3e046 --- /dev/null +++ b/internal/build/testdata/wasm-hardening/workers_stub.go @@ -0,0 +1,5 @@ +//go:build !llgo.wasm_workers + +package main + +func testWorkerCBoundary() {} diff --git a/runtime/internal/lib/runtime/link_wasip1_llgo.go b/runtime/internal/lib/runtime/link_wasip1_llgo.go new file mode 100644 index 0000000000..8b7df23ce8 --- /dev/null +++ b/runtime/internal/lib/runtime/link_wasip1_llgo.go @@ -0,0 +1,40 @@ +//go:build wasip1 && wasm + +package runtime + +import ( + "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" +) + +//go:wasmimport wasi_snapshot_preview1 args_sizes_get +func wasiArgsSizesGet(argc, argvBufLen *uint32) uint32 + +//go:wasmimport wasi_snapshot_preview1 args_get +func wasiArgsGet(argv, argvBuf *byte) uint32 + +//go:linkname os_runtime_args os.runtime_args +func os_runtime_args() []string { + var argc, argvBufLen uint32 + if wasiArgsSizesGet(&argc, &argvBufLen) != 0 || argc == 0 { + return nil + } + argv := make([]*byte, int(argc)) + if argvBufLen == 0 { + argvBufLen = 1 + } + argvBuf := make([]byte, int(argvBufLen)) + if wasiArgsGet((*byte)(unsafe.Pointer(&argv[0])), &argvBuf[0]) != 0 { + return nil + } + args := make([]string, 0, len(argv)) + for _, value := range argv { + if value == nil { + break + } + args = append(args, c.GoString((*c.Char)(unsafe.Pointer(value)))) + } + KeepAlive(argvBuf) + return args +} diff --git a/runtime/internal/lib/runtime/link_wasm_js_llgo.go b/runtime/internal/lib/runtime/link_wasm_js_llgo.go new file mode 100644 index 0000000000..ff11bb30d1 --- /dev/null +++ b/runtime/internal/lib/runtime/link_wasm_js_llgo.go @@ -0,0 +1,26 @@ +//go:build js && wasm + +package runtime + +import ( + _ "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" +) + +//go:linkname os_runtime_args os.runtime_args +func os_runtime_args() []string { + argc := int(c.Argc) + if argc <= 0 || c.Argv == nil { + return nil + } + args := make([]string, 0, argc) + for i := 0; i < argc; i++ { + p := c.Index(c.Argv, i) + if p == nil { + break + } + args = append(args, c.GoString(p)) + } + return args +} diff --git a/runtime/internal/lib/runtime/poll_baremetal_llgo.go b/runtime/internal/lib/runtime/poll_stub_llgo.go similarity index 90% rename from runtime/internal/lib/runtime/poll_baremetal_llgo.go rename to runtime/internal/lib/runtime/poll_stub_llgo.go index 7dd477ca21..d3a68f0f68 100644 --- a/runtime/internal/lib/runtime/poll_baremetal_llgo.go +++ b/runtime/internal/lib/runtime/poll_stub_llgo.go @@ -1,11 +1,10 @@ -//go:build baremetal +//go:build baremetal || wasm package runtime import _ "unsafe" -// Minimal internal/poll hooks for baremetal target smoke builds. -// Embedded targets in this path do not provide a runtime poller. +// Minimal internal/poll hooks for targets without a runtime fd poller. const ( pollNoError = 0