import (
"internal/bytealg"
"runtime"
+ "slices"
"sync"
"unicode/utf16"
"unsafe"
return string(b)
}
+func envSorted(envv []string) []string {
+ if len(envv) < 2 {
+ return envv
+ }
+
+ lowerKeyCache := map[string][]byte{} // lowercased keys to avoid recomputing them in sort
+ lowerKey := func(kv string) []byte {
+ eq := bytealg.IndexByteString(kv, '=')
+ if eq < 0 {
+ return nil
+ }
+ k := kv[:eq]
+ v, ok := lowerKeyCache[k]
+ if !ok {
+ v = []byte(k)
+ for i, b := range v {
+ // We only normalize ASCII for now.
+ // In practice, all environment variables are ASCII, and the
+ // syscall package can't import "unicode" anyway.
+ // Also, per https://nullprogram.com/blog/2023/08/23/ the
+ // sorting of environment variables doesn't really matter.
+ // TODO(bradfitz): use RtlCompareUnicodeString instead,
+ // per that blog post? For now, ASCII is good enough.
+ if 'a' <= b && b <= 'z' {
+ v[i] -= 'a' - 'A'
+ }
+ }
+ lowerKeyCache[k] = v
+ }
+ return v
+ }
+
+ cmpEnv := func(a, b string) int {
+ return bytealg.Compare(lowerKey(a), lowerKey(b))
+ }
+
+ if !slices.IsSortedFunc(envv, cmpEnv) {
+ envv = slices.Clone(envv)
+ slices.SortFunc(envv, cmpEnv)
+ }
+ return envv
+}
+
// createEnvBlock converts an array of environment strings into
// the representation required by CreateProcess: a sequence of NUL
// terminated strings followed by a nil.
if len(envv) == 0 {
return utf16.Encode([]rune("\x00\x00")), nil
}
+
+ // https://learn.microsoft.com/en-us/windows/win32/procthread/changing-environment-variables
+ // says that: "All strings in the environment block must be sorted
+ // alphabetically by name."
+ envv = envSorted(envv)
+
var length int
for _, s := range envv {
if bytealg.IndexByteString(s, 0) != -1 {
"os"
"os/exec"
"path/filepath"
+ "slices"
"syscall"
"testing"
"time"
}
}
+func TestEnvBlockSorted(t *testing.T) {
+ tests := []struct {
+ env []string
+ want []string
+ }{
+ {},
+ {
+ env: []string{"A=1"},
+ want: []string{"A=1"},
+ },
+ {
+ env: []string{"A=1", "B=2", "C=3"},
+ want: []string{"A=1", "B=2", "C=3"},
+ },
+ {
+ env: []string{"C=3", "B=2", "A=1"},
+ want: []string{"A=1", "B=2", "C=3"},
+ },
+ {
+ env: []string{"c=3", "B=2", "a=1"},
+ want: []string{"a=1", "B=2", "c=3"},
+ },
+ }
+ for _, tt := range tests {
+ got := syscall.EnvSorted(tt.env)
+ if !slices.Equal(got, tt.want) {
+ t.Errorf("EnvSorted(%q) = %q, want %q", tt.env, got, tt.want)
+ }
+ }
+}
+
func TestChangingProcessParent(t *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") == "parent" {
// in parent process