Skip to content
Closed
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
41 changes: 40 additions & 1 deletion .github/workflows/llgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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=$!
Expand All @@ -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")
Expand All @@ -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" \
Expand Down
12 changes: 12 additions & 0 deletions internal/build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)
}
})
}
}
Expand Down
26 changes: 26 additions & 0 deletions internal/build/testdata/wasm-hardening/abi.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#include <stdint.h>

#if defined(__EMSCRIPTEN__) && defined(__EMSCRIPTEN_PTHREADS__)
#include <emscripten/emscripten.h>
#include <stdatomic.h>

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
}
11 changes: 11 additions & 0 deletions internal/build/testdata/wasm-hardening/abi.go
Original file line number Diff line number Diff line change
@@ -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
158 changes: 158 additions & 0 deletions internal/build/testdata/wasm-hardening/main.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
46 changes: 46 additions & 0 deletions internal/build/testdata/wasm-hardening/workers.go
Original file line number Diff line number Diff line change
@@ -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
}
}
5 changes: 5 additions & 0 deletions internal/build/testdata/wasm-hardening/workers_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//go:build !llgo.wasm_workers

package main

func testWorkerCBoundary() {}
40 changes: 40 additions & 0 deletions runtime/internal/lib/runtime/link_wasip1_llgo.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading