diff --git a/src/runtime/cgo/clearenv.go b/src/runtime/cgo/clearenv.go new file mode 100644 index 00000000000000..6d605e5f1ad316 --- /dev/null +++ b/src/runtime/cgo/clearenv.go @@ -0,0 +1,15 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux + +package cgo + +import _ "unsafe" // for go:linkname + +//go:cgo_import_static x_cgo_clearenv +//go:linkname x_cgo_clearenv x_cgo_clearenv +//go:linkname _cgo_clearenv runtime._cgo_clearenv +var x_cgo_clearenv byte +var _cgo_clearenv = &x_cgo_clearenv diff --git a/src/runtime/cgo/gcc_clearenv.c b/src/runtime/cgo/gcc_clearenv.c new file mode 100644 index 00000000000000..7657e3562d439c --- /dev/null +++ b/src/runtime/cgo/gcc_clearenv.c @@ -0,0 +1,18 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux + +#include "libcgo.h" + +#include + +/* Stub for calling clearenv */ +void +x_cgo_clearenv(void **_unused) +{ + _cgo_tsan_acquire(); + clearenv(); + _cgo_tsan_release(); +} diff --git a/src/runtime/runtime_clearenv.go b/src/runtime/runtime_clearenv.go new file mode 100644 index 00000000000000..0cb14101c1de20 --- /dev/null +++ b/src/runtime/runtime_clearenv.go @@ -0,0 +1,29 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux + +package runtime + +import "unsafe" + +var _cgo_clearenv unsafe.Pointer // pointer to C function + +// Clear the C environment if cgo is loaded. +func clearenv_c() { + if _cgo_clearenv == nil { + return + } + asmcgocall(_cgo_clearenv, nil) +} + +//go:linkname syscall_runtimeClearenv syscall.runtimeClearenv +func syscall_runtimeClearenv(env map[string]int) { + clearenv_c() + // Did we just unset GODEBUG? + if _, ok := env["GODEBUG"]; ok { + godebugEnv.Store(nil) + godebugNotify(true) + } +} diff --git a/src/runtime/runtime_noclearenv.go b/src/runtime/runtime_noclearenv.go new file mode 100644 index 00000000000000..67e60cc9050377 --- /dev/null +++ b/src/runtime/runtime_noclearenv.go @@ -0,0 +1,18 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !linux + +package runtime + +import _ "unsafe" // for go:linkname + +//go:linkname syscall_runtimeClearenv syscall.runtimeClearenv +func syscall_runtimeClearenv(env map[string]int) { + // The system doesn't have clearenv(3) so emulate it by unsetting all of + // the variables manually. + for k := range env { + syscall_runtimeUnsetenv(k) + } +} diff --git a/src/syscall/env_unix.go b/src/syscall/env_unix.go index 256048f6ff1ccb..e1d7e213f7608e 100644 --- a/src/syscall/env_unix.go +++ b/src/syscall/env_unix.go @@ -126,9 +126,8 @@ func Clearenv() { envLock.Lock() defer envLock.Unlock() - for k := range env { - runtimeUnsetenv(k) - } + runtimeClearenv(env) + env = make(map[string]int) envs = []string{} } diff --git a/src/syscall/env_unix_test.go b/src/syscall/env_unix_test.go new file mode 100644 index 00000000000000..dfd637a690d4e0 --- /dev/null +++ b/src/syscall/env_unix_test.go @@ -0,0 +1,160 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix || (js && wasm) || plan9 || wasip1 + +package syscall_test + +import ( + "fmt" + "strconv" + "strings" + "syscall" + "testing" +) + +type env struct { + name, val string +} + +func genDummyEnv(tb testing.TB, size int) []env { + tb.Helper() + envList := make([]env, size) + for idx := range size { + envList[idx] = env{ + name: fmt.Sprintf("DUMMY_VAR_%d", idx), + val: fmt.Sprintf("val-%d", idx*100), + } + } + return envList +} + +func setDummyEnv(tb testing.TB, envList []env) { + tb.Helper() + for _, env := range envList { + if err := syscall.Setenv(env.name, env.val); err != nil { + tb.Fatalf("setenv %s=%q failed: %v", env.name, env.val, err) + } + } +} + +func setupEnvCleanup(tb testing.TB) { + tb.Helper() + originalEnv := map[string]string{} + for _, env := range syscall.Environ() { + fields := strings.SplitN(env, "=", 2) + name, val := fields[0], fields[1] + originalEnv[name] = val + } + tb.Cleanup(func() { + syscall.Clearenv() + for name, val := range originalEnv { + if err := syscall.Setenv(name, val); err != nil { + tb.Fatalf("could not reset env %s=%q: %v", name, val, err) + } + } + }) +} + +func TestClearenv(t *testing.T) { + setupEnvCleanup(t) + + t.Run("DummyVars-4096", func(t *testing.T) { + envList := genDummyEnv(t, 4096) + setDummyEnv(t, envList) + + if env := syscall.Environ(); len(env) < 4096 { + t.Fatalf("env is missing dummy variables: %v", env) + } + for idx := range 4096 { + name := fmt.Sprintf("DUMMY_VAR_%d", idx) + if _, ok := syscall.Getenv(name); !ok { + t.Fatalf("env is missing dummy variable %s", name) + } + } + + syscall.Clearenv() + + if env := syscall.Environ(); len(env) != 0 { + t.Fatalf("clearenv should've cleared all variables: %v still set", env) + } + for idx := range 4096 { + name := fmt.Sprintf("DUMMY_VAR_%d", idx) + if val, ok := syscall.Getenv(name); ok { + t.Fatalf("clearenv should've cleared all variables: %s=%q still set", name, val) + } + } + }) + + // Test that GODEBUG getting cleared by Clearenv also resets the behaviour. + t.Run("GODEBUG", func(t *testing.T) { + envList := genDummyEnv(t, 100) + setDummyEnv(t, envList) + + doNilPanic := func() (ret any) { + defer func() { + ret = recover() + }() + panic(nil) + return "should not return" + } + + // Allow panic(nil). + if err := syscall.Setenv("GODEBUG", "panicnil=1"); err != nil { + t.Fatalf("setenv GODEBUG=panicnil=1 failed: %v", err) + } + + got := doNilPanic() + if got != nil { + t.Fatalf("GODEBUG=panicnil=1 did not allow for nil panic: got %#v", got) + } + + // Disallow panic(nil). + syscall.Clearenv() + + if env := syscall.Environ(); len(env) != 0 { + t.Fatalf("clearenv should've cleared all variables: %v still set", env) + } + + got = doNilPanic() + if got == nil { + t.Fatalf("GODEBUG=panicnil=1 being unset didn't reset panicnil behaviour") + } + if godebug, ok := syscall.Getenv("GODEBUG"); ok { + t.Fatalf("GODEBUG still exists in environment despite being unset: GODEBUG=%q", godebug) + } + }) +} + +func BenchmarkClearenv(b *testing.B) { + setupEnvCleanup(b) + b.ResetTimer() + for _, size := range []int{100, 1000, 10000} { + b.Run(strconv.Itoa(size), func(b *testing.B) { + envList := genDummyEnv(b, size) + b.ResetTimer() + for b.Loop() { + // Ideally we would use StopTimer to hide the environment + // variable setup cost but that causes go test to loop + // seemingly infinitely.... + setDummyEnv(b, envList) + syscall.Clearenv() + } + }) + } +} + +func BenchmarkSetenv(b *testing.B) { + setupEnvCleanup(b) + b.ResetTimer() + for _, size := range []int{100, 1000, 10000} { + b.Run(strconv.Itoa(size), func(b *testing.B) { + envList := genDummyEnv(b, size) + b.ResetTimer() + for b.Loop() { + setDummyEnv(b, envList) + } + }) + } +} diff --git a/src/syscall/syscall.go b/src/syscall/syscall.go index a46f22ddb53c6a..d70f4fbd15a53d 100644 --- a/src/syscall/syscall.go +++ b/src/syscall/syscall.go @@ -104,3 +104,7 @@ func Exit(code int) // runtimeSetenv and runtimeUnsetenv are provided by the runtime. func runtimeSetenv(k, v string) func runtimeUnsetenv(k string) + +// runtimeClearenv is provided by the runtime (on platforms without +// clearenv(3), it is just a wrapper around runtimeUnsetenv). +func runtimeClearenv(env map[string]int)