From ae3e4c62eb87e03429aeebb7870c1b5dc3d8f79e 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 43ba485586..84e46da2b7 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 e3c85d854c..09ff0ab274 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 f5c73611e46ffa2dbf9feadd26658e541c05b3fc 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 9ea5762810..4fd893d3a6 100644 --- a/builder/build.go +++ b/builder/build.go @@ -264,6 +264,9 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (*BuildR if err != nil { return nil, 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 aa1af47fa1..fbcdeba37a 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 84e46da2b7..838a8f7e45 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 8672601feb713ed7d28a2d9fba0c9740a9d7e32a 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 838a8f7e45..2590696d95 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 2134227b372306cad27cdf327c4946826f39166a 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 4fd893d3a6..4d9423ce8a 100644 --- a/builder/build.go +++ b/builder/build.go @@ -1392,6 +1392,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 c228b30d80..8cf8e6b02e 100644 --- a/builder/testdata/binary-size.txt +++ b/builder/testdata/binary-size.txt @@ -19,8 +19,8 @@ pico2 ./testdata/size-timer z 13791 pico2 ./testdata/size-timer 2 33399 2809 136 5672 cortex-m-qemu ./testdata/size-corpus z 8775 1297 80 4188 cortex-m-qemu ./testdata/size-corpus 2 17034 862 72 4188 -riscv-qemu ./testdata/size-corpus z 16052 1028 88 16660 -riscv-qemu ./testdata/size-corpus 2 24808 744 80 16644 +riscv-qemu ./testdata/size-corpus z 16100 1028 88 16660 +riscv-qemu ./testdata/size-corpus 2 22873 935 80 16644 cortex-m-qemu ./testdata/size-init-recover z 2559 761 4 4168 cortex-m-qemu ./testdata/size-init-recover 2 6499 309 4 4168 pico2 ./testdata/size-init-regexp z 16047 11809 940 5736 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 2590696d95..9c1305a7a9 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 09ff0ab274..e3c85d854c 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) {