From 2e0adb25b3fb81109b933d05fba6f7f47bea0d48 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:55:48 -0700 Subject: [PATCH 1/4] runtime: use GOROOT from the startup environment Go defines runtime.GOROOT from the process environment captured at startup, with the build-time root as a fallback. TinyGo only used the build-time value, so binaries could not select a relocated toolchain root. Capture GOROOT before user package initialization and retain that value after later environment changes. --- runtime_test.go | 34 +++++++++++++++++++++++++++++++++ src/runtime/env_startup_libc.go | 25 ++++++++++++++++++++++++ src/runtime/nonhosted.go | 13 +++++++++++++ src/runtime/runtime.go | 1 - src/runtime/runtime_wasip2.go | 13 +++++++++++++ testdata/runtimegoroot.go | 13 +++++++++++++ webassembly_test.go | 2 +- 7 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/runtime/env_startup_libc.go create mode 100644 testdata/runtimegoroot.go diff --git a/runtime_test.go b/runtime_test.go index 0fba3f2a26..0b101e8bb3 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -13,6 +13,7 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" "github.com/tinygo-org/tinygo/builder" + "github.com/tinygo-org/tinygo/goenv" ) // TestTimerStopResetRace checks that stopping or resetting a timer while its @@ -166,3 +167,36 @@ func TestStdin(t *testing.T) { } checkOutput(t, TESTDATA+"/stdin.txt", output.Bytes()) } + +func TestRuntimeGOROOT(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + environment []string + want string + }{ + {"startup environment", []string{"GOROOT=/startup"}, "/startup"}, + {"empty startup environment", []string{"GOROOT="}, goenv.Get("GOROOT")}, + } { + t.Run(test.name, func(t *testing.T) { + options := optionsFromTarget(*testTarget, sema) + config, err := builder.NewConfig(&options) + if err != nil { + t.Fatal(err) + } + + output := &bytes.Buffer{} + _, err = buildAndRun("testdata/runtimegoroot.go", config, output, nil, test.environment, time.Minute, func(cmd *exec.Cmd, _ builder.BuildResult) error { + return cmd.Run() + }) + if err != nil { + t.Fatal(err) + } + want := test.want + "\n" + test.want + "\n" + if output.String() != want { + t.Fatalf("unexpected output:\n%s\nwant:\n%s", output.String(), want) + } + }) + } +} diff --git a/src/runtime/env_startup_libc.go b/src/runtime/env_startup_libc.go new file mode 100644 index 0000000000..49db0220d5 --- /dev/null +++ b/src/runtime/env_startup_libc.go @@ -0,0 +1,25 @@ +//go:build darwin || windows || wasip1 || (linux && !baremetal && !wasm_unknown && !wasip2 && !nintendoswitch) + +package runtime + +import "unsafe" + +//export getenv +func libc_getenv(name *byte) *byte + +func startupEnv(key string) string { + name := make([]byte, len(key)+1) + copy(name, key) + value := libc_getenv(&name[0]) + if value == nil { + return "" + } + length := strlen(unsafe.Pointer(value)) + return string(unsafe.Slice(value, length)) +} + +func init() { + if value := startupEnv("GOROOT"); value != "" { + goroot = value + } +} diff --git a/src/runtime/nonhosted.go b/src/runtime/nonhosted.go index 9f01a7621a..889dfc5788 100644 --- a/src/runtime/nonhosted.go +++ b/src/runtime/nonhosted.go @@ -53,4 +53,17 @@ func init() { } env = append(env, s[start:]) } + + if value := startupEnv("GOROOT"); value != "" { + goroot = value + } +} + +func startupEnv(key string) string { + for _, entry := range env { + if len(entry) > len(key) && entry[:len(key)] == key && entry[len(key)] == '=' { + return entry[len(key)+1:] + } + } + return "" } diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index 85684dd039..63c0cb3f46 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -34,7 +34,6 @@ func GOMAXPROCS(n int) int { var goroot string // set by the builder func GOROOT() string { - // TODO: Use GOROOT from the startup environment when it is set. return goroot } diff --git a/src/runtime/runtime_wasip2.go b/src/runtime/runtime_wasip2.go index 5dfe4de62d..122931d4af 100644 --- a/src/runtime/runtime_wasip2.go +++ b/src/runtime/runtime_wasip2.go @@ -13,12 +13,25 @@ import ( ) func init() { + if value := startupEnv("GOROOT"); value != "" { + goroot = value + } + wasiclirun.Exports.Run = func() cm.BoolResult { callMain() return false } } +func startupEnv(key string) string { + for _, entry := range environment.GetEnvironment().Slice() { + if entry[0] == key { + return entry[1] + } + } + return "" +} + var args []string //go:linkname os_runtime_args os.runtime_args diff --git a/testdata/runtimegoroot.go b/testdata/runtimegoroot.go new file mode 100644 index 0000000000..d4dad9a4e9 --- /dev/null +++ b/testdata/runtimegoroot.go @@ -0,0 +1,13 @@ +package main + +import ( + "fmt" + "os" + "runtime" +) + +func main() { + fmt.Println(runtime.GOROOT()) + os.Setenv("GOROOT", "/changed") + fmt.Println(runtime.GOROOT()) +} diff --git a/webassembly_test.go b/webassembly_test.go index 4dcdd748d5..309ffd97de 100644 --- a/webassembly_test.go +++ b/webassembly_test.go @@ -30,7 +30,7 @@ func TestWebAssembly(t *testing.T) { for _, tc := range []testCase{ // Test whether there really are no imports when using -panic=trap. This // tests the bugfix for https://github.com/tinygo-org/tinygo/issues/4161. - {name: "panic-default", target: "wasip1", imports: []string{"wasi_snapshot_preview1.fd_write", "wasi_snapshot_preview1.proc_exit", "wasi_snapshot_preview1.random_get"}}, + {name: "panic-default", target: "wasip1", imports: []string{"wasi_snapshot_preview1.fd_write", "wasi_snapshot_preview1.proc_exit", "wasi_snapshot_preview1.environ_get", "wasi_snapshot_preview1.environ_sizes_get", "wasi_snapshot_preview1.random_get"}}, {name: "panic-trap", target: "wasm-unknown", panicStrategy: "trap", imports: []string{}}, } { t.Run(tc.name, func(t *testing.T) { From 024c544f616b30ff90da711e0cfb9c32aa44be2c Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 20 Sep 2026 22:25:23 -0700 Subject: [PATCH 2/4] runtime: initialize GODEBUG settings internal/godebug needs both the compiled defaults and the current environment value when it registers with the runtime. Without that initial state, package behavior can differ from the selected Go version and startup environment until the process changes GODEBUG. Pass the main package defaults from go list to the runtime and retain GODEBUG changes that happen before internal/godebug registers. --- builder/build.go | 3 +++ loader/loader.go | 17 +++++++------- runtime_test.go | 32 +++++++++++++++++++++++++ src/runtime/env.go | 4 ++-- src/runtime/env_nonhosted.go | 12 +++++----- src/runtime/env_wasip2.go | 8 +++---- src/runtime/godebug_startup_libc.go | 7 ++++++ src/runtime/nonhosted.go | 1 + src/runtime/runtime.go | 36 +++++++++++++++++++++++++++-- src/runtime/runtime_wasip1.go | 1 + src/runtime/runtime_wasip2.go | 1 + testdata/runtimegodebug.go | 34 +++++++++++++++++++++++++++ 12 files changed, 134 insertions(+), 22 deletions(-) create mode 100644 src/runtime/godebug_startup_libc.go create mode 100644 testdata/runtimegodebug.go diff --git a/builder/build.go b/builder/build.go index 266179baaa..ac0846ae9d 100644 --- a/builder/build.go +++ b/builder/build.go @@ -248,6 +248,9 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe if err != nil { return BuildResult{}, err } + if _, ok := globalValues["runtime"]["godebugDefault"]; !ok { + globalValues["runtime"]["godebugDefault"] = lprogram.MainPkg().DefaultGODEBUG + } result := BuildResult{ ModuleRoot: lprogram.MainPkg().Module.Dir, MainDir: lprogram.MainPkg().Dir, diff --git a/loader/loader.go b/loader/loader.go index 7996154bf3..bcbb68a8f3 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -54,14 +54,15 @@ type Program struct { // PackageJSON is a subset of the JSON struct returned from `go list`. type PackageJSON struct { - Dir string - ImportPath string - Name string - ForTest string - Root string - Standard bool - DepOnly bool - Module struct { + Dir string + ImportPath string + Name string + ForTest string + Root string + Standard bool + DepOnly bool + DefaultGODEBUG string + Module struct { Path string Main bool Dir string diff --git a/runtime_test.go b/runtime_test.go index 0b101e8bb3..732c5b0104 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -200,3 +200,35 @@ func TestRuntimeGOROOT(t *testing.T) { }) } } + +func TestRuntimeGODEBUG(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + environment []string + want string + }{ + {"compiled default", nil, "true \"\"\ntrue \"\"\ntrue \"tarinsecurepath=1\"\n"}, + {"environment override", []string{"GODEBUG=tarinsecurepath=1"}, "true \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"tarinsecurepath=1\"\n"}, + } { + t.Run(test.name, func(t *testing.T) { + options := optionsFromTarget(*testTarget, sema) + config, err := builder.NewConfig(&options) + if err != nil { + t.Fatal(err) + } + + output := &bytes.Buffer{} + _, err = buildAndRun("testdata/runtimegodebug.go", config, output, nil, test.environment, time.Minute, func(cmd *exec.Cmd, _ builder.BuildResult) error { + return cmd.Run() + }) + if err != nil { + t.Fatal(err) + } + if output.String() != test.want { + t.Fatalf("unexpected output:\n%s\nwant:\n%s", output.String(), test.want) + } + }) + } +} diff --git a/src/runtime/env.go b/src/runtime/env.go index e6c96f2417..6d17d4e4d1 100644 --- a/src/runtime/env.go +++ b/src/runtime/env.go @@ -10,11 +10,11 @@ func syscallSetenv(key, value string) { keydata := cstring(key) valdata := cstring(value) setenv(&keydata[0], &valdata[0]) - if key == "GODEBUG" && godebugUpdate != nil { + if key == "GODEBUG" { // Starting with Go 1.20, we need to call a callback (set by // internal/godebug) to notify the GODEBUG environment variable has // changed. This is necessary to get archive/zip to pass tests. - godebugUpdate(key, value) + godebugSetEnv(value) } } diff --git a/src/runtime/env_nonhosted.go b/src/runtime/env_nonhosted.go index e3bef9dbcc..2f4110d290 100644 --- a/src/runtime/env_nonhosted.go +++ b/src/runtime/env_nonhosted.go @@ -8,15 +8,15 @@ func syscallSetenv(key, value string) { for i, e := range env { if envKey(e) == key { env[i] = entry - if key == "GODEBUG" && godebugUpdate != nil { - godebugUpdate(key, value) + if key == "GODEBUG" { + godebugSetEnv(value) } return } } env = append(env, entry) - if key == "GODEBUG" && godebugUpdate != nil { - godebugUpdate(key, value) + if key == "GODEBUG" { + godebugSetEnv(value) } } @@ -25,8 +25,8 @@ func syscallUnsetenv(key string) { for i, e := range env { if envKey(e) == key { env = append(env[:i], env[i+1:]...) - if key == "GODEBUG" && godebugUpdate != nil { - godebugUpdate(key, "") + if key == "GODEBUG" { + godebugUnsetEnv() } return } diff --git a/src/runtime/env_wasip2.go b/src/runtime/env_wasip2.go index ffc18d9253..5dfc7f57dd 100644 --- a/src/runtime/env_wasip2.go +++ b/src/runtime/env_wasip2.go @@ -8,14 +8,14 @@ package runtime //go:linkname syscallSetenv syscall.runtimeSetenv func syscallSetenv(key, value string) { - if key == "GODEBUG" && godebugUpdate != nil { - godebugUpdate(key, value) + if key == "GODEBUG" { + godebugSetEnv(value) } } //go:linkname syscallUnsetenv syscall.runtimeUnsetenv func syscallUnsetenv(key string) { - if key == "GODEBUG" && godebugUpdate != nil { - godebugUpdate(key, "") + if key == "GODEBUG" { + godebugUnsetEnv() } } diff --git a/src/runtime/godebug_startup_libc.go b/src/runtime/godebug_startup_libc.go new file mode 100644 index 0000000000..7b216e6e81 --- /dev/null +++ b/src/runtime/godebug_startup_libc.go @@ -0,0 +1,7 @@ +//go:build (linux && !baremetal && !wasm_unknown && !wasip2 && !nintendoswitch) || darwin || windows + +package runtime + +func init() { + godebugStartup = startupEnv("GODEBUG") +} diff --git a/src/runtime/nonhosted.go b/src/runtime/nonhosted.go index 889dfc5788..b4dcb09cbc 100644 --- a/src/runtime/nonhosted.go +++ b/src/runtime/nonhosted.go @@ -57,6 +57,7 @@ func init() { if value := startupEnv("GOROOT"); value != "" { goroot = value } + godebugStartup = startupEnv("GODEBUG") } func startupEnv(key string) string { diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index 63c0cb3f46..75481cb7fe 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -1,6 +1,7 @@ package runtime import ( + "sync/atomic" "unsafe" ) @@ -145,13 +146,44 @@ func makeStrongFromWeak(ptr unsafe.Pointer) unsafe.Pointer { return ptr } -var godebugUpdate func(string, string) +var godebugDefault string // set by the builder +var godebugStartup string +var godebugEmpty string +var godebugEnv atomic.Pointer[string] +var godebugUpdate atomic.Pointer[func(string, string)] //go:linkname godebug_setUpdate internal/godebug.setUpdate func godebug_setUpdate(update func(string, string)) { // The 'update' function needs to be called whenever the GODEBUG environment // variable changes (for example, via os.Setenv). - godebugUpdate = update + p := new(func(string, string)) + *p = update + godebugUpdate.Store(p) + godebugNotify() +} + +func godebugSetEnv(value string) { + p := new(string) + *p = value + godebugEnv.Store(p) + godebugNotify() +} + +func godebugUnsetEnv() { + godebugEnv.Store(&godebugEmpty) + godebugNotify() +} + +func godebugNotify() { + update := godebugUpdate.Load() + if update == nil { + return + } + env := godebugStartup + if p := godebugEnv.Load(); p != nil { + env = *p + } + (*update)(godebugDefault, env) } //go:linkname godebug_setNewIncNonDefault internal/godebug.setNewIncNonDefault diff --git a/src/runtime/runtime_wasip1.go b/src/runtime/runtime_wasip1.go index 275599dcde..5509d12e12 100644 --- a/src/runtime/runtime_wasip1.go +++ b/src/runtime/runtime_wasip1.go @@ -17,6 +17,7 @@ func __wasm_call_ctors() // wasmtime run ./program.wasm arg1 arg2 func init() { __wasm_call_ctors() + godebugStartup = startupEnv("GODEBUG") } var args []string diff --git a/src/runtime/runtime_wasip2.go b/src/runtime/runtime_wasip2.go index 122931d4af..78f34b7e0c 100644 --- a/src/runtime/runtime_wasip2.go +++ b/src/runtime/runtime_wasip2.go @@ -16,6 +16,7 @@ func init() { if value := startupEnv("GOROOT"); value != "" { goroot = value } + godebugStartup = startupEnv("GODEBUG") wasiclirun.Exports.Run = func() cm.BoolResult { callMain() diff --git a/testdata/runtimegodebug.go b/testdata/runtimegodebug.go new file mode 100644 index 0000000000..6a76226f2c --- /dev/null +++ b/testdata/runtimegodebug.go @@ -0,0 +1,34 @@ +//go:debug tarinsecurepath=0 + +package main + +import ( + "fmt" + "strings" + "syscall" + _ "unsafe" +) + +var defaults, environment string + +//go:linkname setUpdate internal/godebug.setUpdate +func setUpdate(update func(defaults, environment string)) + +func init() { + setUpdate(func(newDefaults, newEnvironment string) { + defaults = newDefaults + environment = newEnvironment + }) +} + +func main() { + printState() + syscall.Setenv("GODEBUG", "") + printState() + syscall.Setenv("GODEBUG", "tarinsecurepath=1") + printState() +} + +func printState() { + fmt.Printf("%t %q\n", strings.Contains(defaults, "tarinsecurepath=0"), environment) +} From 539910f97837bdab57e517e77e3f784be1ca14a5 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:14:18 -0700 Subject: [PATCH 3/4] runtime: notify GODEBUG when the environment is removed Removing GODEBUG must immediately expose the compiled defaults again. TinyGo did not notify internal/godebug for every unset path, which let an old environment override remain cached after cleanup or Clearenv. --- runtime_test.go | 4 ++-- src/runtime/env.go | 3 +++ src/runtime/env_nonhosted.go | 3 +++ src/syscall/env_wasip2.go | 4 ++++ testdata/runtimegodebug.go | 7 +++++++ 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/runtime_test.go b/runtime_test.go index 732c5b0104..ef0b92024d 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -209,8 +209,8 @@ func TestRuntimeGODEBUG(t *testing.T) { environment []string want string }{ - {"compiled default", nil, "true \"\"\ntrue \"\"\ntrue \"tarinsecurepath=1\"\n"}, - {"environment override", []string{"GODEBUG=tarinsecurepath=1"}, "true \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"tarinsecurepath=1\"\n"}, + {"compiled default", nil, "true \"\"\ntrue \"\"\ntrue \"\"\ntrue \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"\"\n"}, + {"environment override", []string{"GODEBUG=tarinsecurepath=1"}, "true \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"\"\ntrue \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"\"\n"}, } { t.Run(test.name, func(t *testing.T) { options := optionsFromTarget(*testTarget, sema) diff --git a/src/runtime/env.go b/src/runtime/env.go index 6d17d4e4d1..6e03944e7c 100644 --- a/src/runtime/env.go +++ b/src/runtime/env.go @@ -25,6 +25,9 @@ func syscallSetenv(key, value string) { func syscallUnsetenv(key string) { keydata := cstring(key) unsetenv(&keydata[0]) + if key == "GODEBUG" { + godebugUnsetEnv() + } } // Clear the environment. diff --git a/src/runtime/env_nonhosted.go b/src/runtime/env_nonhosted.go index 2f4110d290..33eeb0163d 100644 --- a/src/runtime/env_nonhosted.go +++ b/src/runtime/env_nonhosted.go @@ -31,6 +31,9 @@ func syscallUnsetenv(key string) { return } } + if key == "GODEBUG" { + godebugUnsetEnv() + } } func envKey(entry string) string { diff --git a/src/syscall/env_wasip2.go b/src/syscall/env_wasip2.go index f9468936bb..1d9105290d 100644 --- a/src/syscall/env_wasip2.go +++ b/src/syscall/env_wasip2.go @@ -54,5 +54,9 @@ func Unsetenv(key string) (err error) { } func Clearenv() { + _, hadGODEBUG := libc_envs["GODEBUG"] clear(libc_envs) + if hadGODEBUG { + runtimeUnsetenv("GODEBUG") + } } diff --git a/testdata/runtimegodebug.go b/testdata/runtimegodebug.go index 6a76226f2c..52d86dc367 100644 --- a/testdata/runtimegodebug.go +++ b/testdata/runtimegodebug.go @@ -22,11 +22,18 @@ func init() { } func main() { + printState() + syscall.Unsetenv("GODEBUG") printState() syscall.Setenv("GODEBUG", "") printState() syscall.Setenv("GODEBUG", "tarinsecurepath=1") printState() + syscall.Unsetenv("GODEBUG") + printState() + syscall.Setenv("GODEBUG", "tarinsecurepath=1") + syscall.Clearenv() + printState() } func printState() { From 0f1fcdda6a508074f4e5716ac1332ede516fe4d4 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:05:25 -0700 Subject: [PATCH 4/4] runtime: prune unused startup environment handling GOROOT and GODEBUG startup support retains environment access even when programs do not use either feature. Go 1.25 also registers the GODEBUG updater without any setting reads, retaining its parser and atomic operations. This adds imports and code to small WASI and bare-metal programs and prevents AVR builds with LLVM 15. Unused startup support should not impose those costs on programs that never read the settings. Startup settings must also remain available to compile-time initialization so disabled FIPS self-tests do not survive in multicore builds. --- builder/build.go | 3 + builder/testdata/binary-size.txt | 4 +- compiler/compiler.go | 2 + compiler/defer.go | 2 + compiler/goroutine.go | 2 + compiler/runtimefeature.go | 34 +++++++ runtime_test.go | 9 +- src/runtime/env.go | 4 +- src/runtime/env_nonhosted.go | 6 +- src/runtime/env_startup_libc.go | 6 +- src/runtime/env_wasip2.go | 4 +- src/runtime/godebug_startup_libc.go | 4 +- src/runtime/nonhosted.go | 12 ++- src/runtime/runtime.go | 7 ++ src/runtime/runtime_wasip1.go | 4 +- src/runtime/runtime_wasip2.go | 10 +- testdata/runtimegodebugsetting.go | 43 +++++++++ transform/optimizer.go | 91 ++++++++++++++++++- transform/optimizer_internal_test.go | 57 ++++++++++++ .../testdata/runtime-startup-referenced.ll | 19 ++++ transform/testdata/runtime-startup-unused.ll | 35 +++++++ transform/testdata/runtime-startup-used.ll | 18 ++++ webassembly_test.go | 2 +- 23 files changed, 354 insertions(+), 24 deletions(-) create mode 100644 compiler/runtimefeature.go create mode 100644 testdata/runtimegodebugsetting.go create mode 100644 transform/testdata/runtime-startup-referenced.ll create mode 100644 transform/testdata/runtime-startup-unused.ll create mode 100644 transform/testdata/runtime-startup-used.ll diff --git a/builder/build.go b/builder/build.go index ac0846ae9d..eb23adc05f 100644 --- a/builder/build.go +++ b/builder/build.go @@ -1368,6 +1368,9 @@ func createEmbedObjectFile(data, hexSum, sourceFile, sourceDir, tmpdir string, c // needed to convert a program to its final form. Some transformations are not // optional and must be run as the compiler expects them to run. func optimizeProgram(mod llvm.Module, config *compileopts.Config) error { + if err := transform.PrepareRuntimeStartup(mod); err != nil { + return err + } err := interp.Run(mod, config.Options.InterpTimeout, config.Options.InterpMaxLoopIterations, config.DumpSSA()) if err != nil { return err diff --git a/builder/testdata/binary-size.txt b/builder/testdata/binary-size.txt index cfdd88aaea..de501b2758 100644 --- a/builder/testdata/binary-size.txt +++ b/builder/testdata/binary-size.txt @@ -17,5 +17,5 @@ pico2 ./testdata/size-os z 6199 1209 128 5592 pico2 ./testdata/size-os 2 15290 1014 128 5592 cortex-m-qemu ./testdata/size-corpus z 9891 1301 80 4188 cortex-m-qemu ./testdata/size-corpus 2 17466 862 72 4188 -riscv-qemu ./testdata/size-corpus z 17508 1028 88 16660 -riscv-qemu ./testdata/size-corpus 2 25160 744 80 16644 +riscv-qemu ./testdata/size-corpus z 17556 1028 88 16660 +riscv-qemu ./testdata/size-corpus 2 23225 935 80 16644 diff --git a/compiler/compiler.go b/compiler/compiler.go index 421a9b7676..d28e36c095 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -2355,6 +2355,8 @@ func (b *builder) createBuiltin(argTypes []types.Type, argValues []llvm.Value, c // // This is also where compiler intrinsics are implemented. func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error) { + b.markRuntimeFeatureUse(instr) + // See if this is an intrinsic function that is handled specially. if fn := instr.StaticCallee(); fn != nil { // Direct function call, either to a named or anonymous (directly diff --git a/compiler/defer.go b/compiler/defer.go index 8340a3d83d..f1ec306c67 100644 --- a/compiler/defer.go +++ b/compiler/defer.go @@ -420,6 +420,8 @@ func (b *builder) loadDeferredCallParams(structType llvm.Type, ptr llvm.Value) [ // createDefer emits a single defer instruction, to be run when this function // returns. func (b *builder) createDefer(instr *ssa.Defer) { + b.markRuntimeFeatureUse(&instr.Call) + // The pointer to the previous defer struct, which we will replace to // make a linked list. next := b.CreateLoad(b.dataPtrType, b.deferPtr, "defer.next") diff --git a/compiler/goroutine.go b/compiler/goroutine.go index 72a0c7706b..5b399d17e9 100644 --- a/compiler/goroutine.go +++ b/compiler/goroutine.go @@ -14,6 +14,8 @@ import ( // createGo emits code to start a new goroutine. func (b *builder) createGo(instr *ssa.Go) { + b.markRuntimeFeatureUse(&instr.Call) + if builtin, ok := instr.Call.Value.(*ssa.Builtin); ok { // We cheat. None of the builtins do any long or blocking operation, so // we might as well run these builtins right away without the program diff --git a/compiler/runtimefeature.go b/compiler/runtimefeature.go new file mode 100644 index 0000000000..040460bbf5 --- /dev/null +++ b/compiler/runtimefeature.go @@ -0,0 +1,34 @@ +package compiler + +import ( + "golang.org/x/tools/go/ssa" + "tinygo.org/x/go-llvm" +) + +func (b *builder) markRuntimeFeatureUse(call *ssa.CallCommon) { + callee := call.StaticCallee() + if callee == nil { + return + } + + var markerName string + switch b.getFunctionInfo(callee).linkName { + case "runtime.GOROOT": + markerName = "tinygo.runtime.feature.goroot" + case "(*internal/godebug.Setting).Value": + markerName = "tinygo.runtime.feature.godebug" + case "internal/godebug.setUpdate": + if b.fn.Pkg != nil && b.fn.Pkg.Pkg.Path() == "internal/godebug" { + return + } + markerName = "tinygo.runtime.feature.godebug" + default: + return + } + + marker := b.mod.NamedFunction(markerName) + if marker.IsNil() { + marker = llvm.AddFunction(b.mod, markerName, llvm.FunctionType(b.ctx.VoidType(), nil, false)) + } + b.CreateCall(marker.GlobalValueType(), marker, nil, "") +} diff --git a/runtime_test.go b/runtime_test.go index ef0b92024d..bde803e82a 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -206,11 +206,14 @@ func TestRuntimeGODEBUG(t *testing.T) { for _, test := range []struct { name string + program string environment []string want string }{ - {"compiled default", nil, "true \"\"\ntrue \"\"\ntrue \"\"\ntrue \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"\"\n"}, - {"environment override", []string{"GODEBUG=tarinsecurepath=1"}, "true \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"\"\ntrue \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"\"\n"}, + {"compiled default", "testdata/runtimegodebug.go", nil, "true \"\"\ntrue \"\"\ntrue \"\"\ntrue \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"\"\n"}, + {"environment override", "testdata/runtimegodebug.go", []string{"GODEBUG=tarinsecurepath=1"}, "true \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"\"\ntrue \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"\"\n"}, + {"setting compiled default", "testdata/runtimegodebugsetting.go", nil, "true\nfalse\ntrue\ntrue\n"}, + {"setting environment override", "testdata/runtimegodebugsetting.go", []string{"GODEBUG=tarinsecurepath=1"}, "false\nfalse\ntrue\ntrue\n"}, } { t.Run(test.name, func(t *testing.T) { options := optionsFromTarget(*testTarget, sema) @@ -220,7 +223,7 @@ func TestRuntimeGODEBUG(t *testing.T) { } output := &bytes.Buffer{} - _, err = buildAndRun("testdata/runtimegodebug.go", config, output, nil, test.environment, time.Minute, func(cmd *exec.Cmd, _ builder.BuildResult) error { + _, err = buildAndRun(test.program, config, output, nil, test.environment, time.Minute, func(cmd *exec.Cmd, _ builder.BuildResult) error { return cmd.Run() }) if err != nil { diff --git a/src/runtime/env.go b/src/runtime/env.go index 6e03944e7c..e2b81010ed 100644 --- a/src/runtime/env.go +++ b/src/runtime/env.go @@ -10,7 +10,7 @@ func syscallSetenv(key, value string) { keydata := cstring(key) valdata := cstring(value) setenv(&keydata[0], &valdata[0]) - if key == "GODEBUG" { + if key == "GODEBUG" && godebugEnvEnabled() { // Starting with Go 1.20, we need to call a callback (set by // internal/godebug) to notify the GODEBUG environment variable has // changed. This is necessary to get archive/zip to pass tests. @@ -25,7 +25,7 @@ func syscallSetenv(key, value string) { func syscallUnsetenv(key string) { keydata := cstring(key) unsetenv(&keydata[0]) - if key == "GODEBUG" { + if key == "GODEBUG" && godebugEnvEnabled() { godebugUnsetEnv() } } diff --git a/src/runtime/env_nonhosted.go b/src/runtime/env_nonhosted.go index 33eeb0163d..e183d2331d 100644 --- a/src/runtime/env_nonhosted.go +++ b/src/runtime/env_nonhosted.go @@ -8,14 +8,14 @@ func syscallSetenv(key, value string) { for i, e := range env { if envKey(e) == key { env[i] = entry - if key == "GODEBUG" { + if key == "GODEBUG" && godebugEnvEnabled() { godebugSetEnv(value) } return } } env = append(env, entry) - if key == "GODEBUG" { + if key == "GODEBUG" && godebugEnvEnabled() { godebugSetEnv(value) } } @@ -25,7 +25,7 @@ func syscallUnsetenv(key string) { for i, e := range env { if envKey(e) == key { env = append(env[:i], env[i+1:]...) - if key == "GODEBUG" { + if key == "GODEBUG" && godebugEnvEnabled() { godebugUnsetEnv() } return diff --git a/src/runtime/env_startup_libc.go b/src/runtime/env_startup_libc.go index 49db0220d5..dad2c686c7 100644 --- a/src/runtime/env_startup_libc.go +++ b/src/runtime/env_startup_libc.go @@ -19,7 +19,9 @@ func startupEnv(key string) string { } func init() { - if value := startupEnv("GOROOT"); value != "" { - goroot = value + if gorootEnvEnabled() { + if value := startupEnv("GOROOT"); value != "" { + goroot = value + } } } diff --git a/src/runtime/env_wasip2.go b/src/runtime/env_wasip2.go index 5dfc7f57dd..9460845312 100644 --- a/src/runtime/env_wasip2.go +++ b/src/runtime/env_wasip2.go @@ -8,14 +8,14 @@ package runtime //go:linkname syscallSetenv syscall.runtimeSetenv func syscallSetenv(key, value string) { - if key == "GODEBUG" { + if key == "GODEBUG" && godebugEnvEnabled() { godebugSetEnv(value) } } //go:linkname syscallUnsetenv syscall.runtimeUnsetenv func syscallUnsetenv(key string) { - if key == "GODEBUG" { + if key == "GODEBUG" && godebugEnvEnabled() { godebugUnsetEnv() } } diff --git a/src/runtime/godebug_startup_libc.go b/src/runtime/godebug_startup_libc.go index 7b216e6e81..f32eee26cd 100644 --- a/src/runtime/godebug_startup_libc.go +++ b/src/runtime/godebug_startup_libc.go @@ -3,5 +3,7 @@ package runtime func init() { - godebugStartup = startupEnv("GODEBUG") + if godebugEnvEnabled() { + godebugStartup = startupEnv("GODEBUG") + } } diff --git a/src/runtime/nonhosted.go b/src/runtime/nonhosted.go index b4dcb09cbc..640820cefb 100644 --- a/src/runtime/nonhosted.go +++ b/src/runtime/nonhosted.go @@ -53,11 +53,17 @@ func init() { } env = append(env, s[start:]) } +} - if value := startupEnv("GOROOT"); value != "" { - goroot = value +func init() { + if gorootEnvEnabled() { + if value := startupEnv("GOROOT"); value != "" { + goroot = value + } + } + if godebugEnvEnabled() { + godebugStartup = startupEnv("GODEBUG") } - godebugStartup = startupEnv("GODEBUG") } func startupEnv(key string) string { diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index 75481cb7fe..3f184919f9 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -34,6 +34,8 @@ func GOMAXPROCS(n int) int { var goroot string // set by the builder +func gorootEnvEnabled() bool + func GOROOT() string { return goroot } @@ -152,8 +154,13 @@ var godebugEmpty string var godebugEnv atomic.Pointer[string] var godebugUpdate atomic.Pointer[func(string, string)] +func godebugEnvEnabled() bool + //go:linkname godebug_setUpdate internal/godebug.setUpdate func godebug_setUpdate(update func(string, string)) { + if !godebugEnvEnabled() { + return + } // The 'update' function needs to be called whenever the GODEBUG environment // variable changes (for example, via os.Setenv). p := new(func(string, string)) diff --git a/src/runtime/runtime_wasip1.go b/src/runtime/runtime_wasip1.go index 5509d12e12..b2ea819ae7 100644 --- a/src/runtime/runtime_wasip1.go +++ b/src/runtime/runtime_wasip1.go @@ -17,7 +17,9 @@ func __wasm_call_ctors() // wasmtime run ./program.wasm arg1 arg2 func init() { __wasm_call_ctors() - godebugStartup = startupEnv("GODEBUG") + if godebugEnvEnabled() { + godebugStartup = startupEnv("GODEBUG") + } } var args []string diff --git a/src/runtime/runtime_wasip2.go b/src/runtime/runtime_wasip2.go index 78f34b7e0c..80b989c345 100644 --- a/src/runtime/runtime_wasip2.go +++ b/src/runtime/runtime_wasip2.go @@ -13,10 +13,14 @@ import ( ) func init() { - if value := startupEnv("GOROOT"); value != "" { - goroot = value + if gorootEnvEnabled() { + if value := startupEnv("GOROOT"); value != "" { + goroot = value + } + } + if godebugEnvEnabled() { + godebugStartup = startupEnv("GODEBUG") } - godebugStartup = startupEnv("GODEBUG") wasiclirun.Exports.Run = func() cm.BoolResult { callMain() diff --git a/testdata/runtimegodebugsetting.go b/testdata/runtimegodebugsetting.go new file mode 100644 index 0000000000..1768144b48 --- /dev/null +++ b/testdata/runtimegodebugsetting.go @@ -0,0 +1,43 @@ +//go:debug tarinsecurepath=0 + +package main + +import ( + "archive/tar" + "bytes" + "fmt" + "syscall" +) + +func main() { + var data bytes.Buffer + writer := tar.NewWriter(&data) + if err := writer.WriteHeader(&tar.Header{Name: "../test"}); err != nil { + panic(err) + } + if err := writer.Close(); err != nil { + panic(err) + } + printState := func() { + _, err := tar.NewReader(bytes.NewReader(data.Bytes())).Next() + if err != nil && err != tar.ErrInsecurePath { + panic(err) + } + fmt.Println(err == tar.ErrInsecurePath) + } + + printState() + if err := syscall.Setenv("GODEBUG", "tarinsecurepath=1"); err != nil { + panic(err) + } + printState() + if err := syscall.Unsetenv("GODEBUG"); err != nil { + panic(err) + } + printState() + if err := syscall.Setenv("GODEBUG", "tarinsecurepath=1"); err != nil { + panic(err) + } + syscall.Clearenv() + printState() +} diff --git a/transform/optimizer.go b/transform/optimizer.go index 282170cf2b..5dae94f908 100644 --- a/transform/optimizer.go +++ b/transform/optimizer.go @@ -68,7 +68,6 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error { if err != nil { return []error{fmt.Errorf("could not build pass pipeline: %w", err)} } - // Run TinyGo-specific optimization passes. OptimizeStringToBytes(mod) maxStackSize := config.MaxStackAlloc() @@ -210,6 +209,96 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error { return nil } +func pruneRuntimeStartup(mod llvm.Module) error { + options := llvm.NewPassBuilderOptions() + defer options.Dispose() + if pruneUnusedRuntimeStartup(mod) { + if err := mod.RunPasses("globaldce", llvm.TargetMachine{}, options); err != nil { + return fmt.Errorf("could not run post-startup-pruning globaldce pass: %w", err) + } + } + return nil +} + +// PrepareRuntimeStartup resolves startup checks before interpreting initializers. +func PrepareRuntimeStartup(mod llvm.Module) error { + linkages := make(map[llvm.Value]llvm.Linkage, len(functionsUsedInTransforms)) + for _, name := range functionsUsedInTransforms { + fn := mod.NamedFunction(name) + if !fn.IsNil() { + linkages[fn] = fn.Linkage() + fn.SetLinkage(llvm.ExternalLinkage) + } + } + defer func() { + for fn, linkage := range linkages { + fn.SetLinkage(linkage) + } + }() + options := llvm.NewPassBuilderOptions() + defer options.Dispose() + err := mod.RunPasses("globaldce,function-attrs,adce,globaldce", llvm.TargetMachine{}, options) + if err != nil { + return fmt.Errorf("could not prepare runtime startup: %w", err) + } + return pruneRuntimeStartup(mod) +} + +func pruneUnusedRuntimeStartup(mod llvm.Module) bool { + changed := lowerRuntimeStartupCheck(mod, + "runtime.gorootEnvEnabled", + "tinygo.runtime.feature.goroot", + "runtime.GOROOT") + return lowerRuntimeStartupCheck(mod, + "runtime.godebugEnvEnabled", + "tinygo.runtime.feature.godebug", + "(*internal/godebug.Setting).Value") || changed +} + +func lowerRuntimeStartupCheck(mod llvm.Module, checkName, markerName, featureName string) bool { + marker := mod.NamedFunction(markerName) + enabled := !marker.IsNil() && hasUses(marker) + feature := mod.NamedFunction(featureName) + enabled = enabled || (!feature.IsNil() && hasUses(feature)) + + check := mod.NamedFunction(checkName) + if check.IsNil() { + return false + } + + for _, use := range getUses(check) { + if use.IsACallInst().IsNil() || use.CalledValue() != check { + panic("unexpected use of runtime startup check") + } + use.ReplaceAllUsesWith(llvm.ConstInt(use.Type(), boolToUint64(enabled), false)) + use.EraseFromParentAsInstruction() + } + if hasUses(check) { + panic("runtime startup check still has uses") + } + check.EraseFromParentAsFunction() + if !marker.IsNil() { + for _, use := range getUses(marker) { + if use.IsACallInst().IsNil() || use.CalledValue() != marker { + panic("unexpected use of runtime feature marker") + } + use.EraseFromParentAsInstruction() + } + if hasUses(marker) { + panic("runtime feature marker still has uses") + } + marker.EraseFromParentAsFunction() + } + return true +} + +func boolToUint64(value bool) uint64 { + if value { + return 1 + } + return 0 +} + func blockGlobalAllocPromotion(mod llvm.Module) { ctx := mod.Context() ptrType := llvm.PointerType(ctx.Int8Type(), 0) diff --git a/transform/optimizer_internal_test.go b/transform/optimizer_internal_test.go index 3358706dd0..ae06ff935c 100644 --- a/transform/optimizer_internal_test.go +++ b/transform/optimizer_internal_test.go @@ -31,3 +31,60 @@ func TestBlockGlobalAllocPromotionUses(t *testing.T) { t.Fatalf("got %d marker uses, want 1", len(uses)) } } + +func TestPruneUnusedRuntimeStartup(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + wantEnabled bool + }{ + { + name: "unused", + path: "testdata/runtime-startup-unused.ll", + }, + { + name: "used", + path: "testdata/runtime-startup-used.ll", + wantEnabled: true, + }, + { + name: "referenced", + path: "testdata/runtime-startup-referenced.ll", + wantEnabled: true, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ctx := llvm.NewContext() + defer ctx.Dispose() + buf, err := llvm.NewMemoryBufferFromFile(tc.path) + if err != nil { + t.Fatal(err) + } + mod, err := ctx.ParseIR(buf) + if err != nil { + t.Fatal(err) + } + defer mod.Dispose() + + if err := PrepareRuntimeStartup(mod); err != nil { + t.Fatal(err) + } + for _, name := range []string{"runtime.gorootEnvEnabled", "runtime.godebugEnvEnabled"} { + if !mod.NamedFunction(name).IsNil() { + t.Errorf("%s was not removed", name) + } + } + for _, name := range []string{"test.goroot", "test.godebug"} { + ret := mod.NamedFunction(name).EntryBasicBlock().LastInstruction() + got := ret.Operand(0).ZExtValue() != 0 + if got != tc.wantEnabled { + t.Errorf("%s returned %v, want %v", name, got, tc.wantEnabled) + } + } + }) + } +} diff --git a/transform/testdata/runtime-startup-referenced.ll b/transform/testdata/runtime-startup-referenced.ll new file mode 100644 index 0000000000..1602a5eea2 --- /dev/null +++ b/transform/testdata/runtime-startup-referenced.ll @@ -0,0 +1,19 @@ +@goroot = global ptr @runtime.GOROOT +@godebug = global ptr @"(*internal/godebug.Setting).Value" + +declare i1 @runtime.gorootEnvEnabled(ptr) +declare i1 @runtime.godebugEnvEnabled(ptr) +declare ptr @runtime.GOROOT(ptr) +declare ptr @"(*internal/godebug.Setting).Value"(ptr, ptr) + +define i1 @test.goroot() { +entry: + %enabled = call i1 @runtime.gorootEnvEnabled(ptr undef) + ret i1 %enabled +} + +define i1 @test.godebug() { +entry: + %enabled = call i1 @runtime.godebugEnvEnabled(ptr undef) + ret i1 %enabled +} diff --git a/transform/testdata/runtime-startup-unused.ll b/transform/testdata/runtime-startup-unused.ll new file mode 100644 index 0000000000..0e3330e3a7 --- /dev/null +++ b/transform/testdata/runtime-startup-unused.ll @@ -0,0 +1,35 @@ +declare i1 @runtime.gorootEnvEnabled(ptr) +declare i1 @runtime.godebugEnvEnabled(ptr) +declare void @tinygo.runtime.feature.goroot() +declare void @tinygo.runtime.feature.godebug() +declare void @"internal/godebug.setUpdate"(ptr, ptr) +declare ptr @runtime.GOROOT(ptr) +declare ptr @"(*internal/godebug.Setting).Value"(ptr, ptr) + +define internal void @test.noop(ptr %callback) { +entry: + ret void +} + +define internal void @test.unusedCallback() { +entry: + call void @tinygo.runtime.feature.goroot() + %goroot = call ptr @runtime.GOROOT(ptr undef) + call void @tinygo.runtime.feature.godebug() + %godebug = call ptr @"(*internal/godebug.Setting).Value"(ptr null, ptr undef) + ret void +} + +define i1 @test.goroot() { +entry: + call void @test.noop(ptr @test.unusedCallback) + %enabled = call i1 @runtime.gorootEnvEnabled(ptr undef) + ret i1 %enabled +} + +define i1 @test.godebug() { +entry: + call void @"internal/godebug.setUpdate"(ptr null, ptr undef) + %enabled = call i1 @runtime.godebugEnvEnabled(ptr undef) + ret i1 %enabled +} diff --git a/transform/testdata/runtime-startup-used.ll b/transform/testdata/runtime-startup-used.ll new file mode 100644 index 0000000000..d605b9ab03 --- /dev/null +++ b/transform/testdata/runtime-startup-used.ll @@ -0,0 +1,18 @@ +declare i1 @runtime.gorootEnvEnabled(ptr) +declare i1 @runtime.godebugEnvEnabled(ptr) +declare void @tinygo.runtime.feature.goroot() +declare void @tinygo.runtime.feature.godebug() + +define i1 @test.goroot() { +entry: + call void @tinygo.runtime.feature.goroot() + %enabled = call i1 @runtime.gorootEnvEnabled(ptr undef) + ret i1 %enabled +} + +define i1 @test.godebug() { +entry: + call void @tinygo.runtime.feature.godebug() + %enabled = call i1 @runtime.godebugEnvEnabled(ptr undef) + ret i1 %enabled +} diff --git a/webassembly_test.go b/webassembly_test.go index 309ffd97de..4dcdd748d5 100644 --- a/webassembly_test.go +++ b/webassembly_test.go @@ -30,7 +30,7 @@ func TestWebAssembly(t *testing.T) { for _, tc := range []testCase{ // Test whether there really are no imports when using -panic=trap. This // tests the bugfix for https://github.com/tinygo-org/tinygo/issues/4161. - {name: "panic-default", target: "wasip1", imports: []string{"wasi_snapshot_preview1.fd_write", "wasi_snapshot_preview1.proc_exit", "wasi_snapshot_preview1.environ_get", "wasi_snapshot_preview1.environ_sizes_get", "wasi_snapshot_preview1.random_get"}}, + {name: "panic-default", target: "wasip1", imports: []string{"wasi_snapshot_preview1.fd_write", "wasi_snapshot_preview1.proc_exit", "wasi_snapshot_preview1.random_get"}}, {name: "panic-trap", target: "wasm-unknown", panicStrategy: "trap", imports: []string{}}, } { t.Run(tc.name, func(t *testing.T) {