From 84fb1b8253b54a5809b2e84bd38d9f1408c46766 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 24 Jul 2025 15:38:35 +0200 Subject: [PATCH] [release-branch.go1.25] os/user: user random name for the test user account TestImpersonated and TestGroupIdsTestUser are flaky due to sporadic failures when creating the test user account when running the tests from different processes at the same time. This flakiness can be fixed by using a random name for the test user account. Fixes #73523 Fixes #74727 Fixes #74728 Fixes #74729 Fixes #74745 Fixes #74751 Cq-Include-Trybots: luci.golang.try:go1.25-windows-amd64-longtest Change-Id: Ib2283a888437420502b1c11d876c975f5af4bc03 Reviewed-on: https://go-review.googlesource.com/c/go/+/690175 Auto-Submit: Quim Muntal Reviewed-by: Michael Pratt Reviewed-by: Dmitri Shuralyov TryBot-Bypass: Dmitri Shuralyov (cherry picked from commit 374e3be2eb9b546ef1340f750e343c15a8f87dde) Reviewed-on: https://go-review.googlesource.com/c/go/+/690555 Auto-Submit: Dmitri Shuralyov LUCI-TryBot-Result: Go LUCI Reviewed-by: Quim Muntal Reviewed-by: Mark Freeman --- src/os/user/user_windows_test.go | 124 ++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 37 deletions(-) diff --git a/src/os/user/user_windows_test.go b/src/os/user/user_windows_test.go index 7dca2fc5f9..d9f2fe7c74 100644 --- a/src/os/user/user_windows_test.go +++ b/src/os/user/user_windows_test.go @@ -7,6 +7,7 @@ package user import ( "crypto/rand" "encoding/base64" + "encoding/binary" "errors" "fmt" "internal/syscall/windows" @@ -16,11 +17,92 @@ import ( "runtime" "slices" "strconv" + "strings" "syscall" "testing" + "unicode" + "unicode/utf8" "unsafe" ) +// addUserAccount creates a local user account. +// It returns the name and password of the new account. +// Multiple programs or goroutines calling addUserAccount simultaneously will not choose the same directory. +func addUserAccount(t *testing.T) (name, password string) { + t.TempDir() + pattern := t.Name() + // Windows limits the user name to 20 characters, + // leave space for a 4 digits random suffix. + const maxNameLen, suffixLen = 20, 4 + pattern = pattern[:min(len(pattern), maxNameLen-suffixLen)] + // Drop unusual characters from the account name. + mapper := func(r rune) rune { + if r < utf8.RuneSelf { + if '0' <= r && r <= '9' || + 'a' <= r && r <= 'z' || + 'A' <= r && r <= 'Z' { + return r + } + } else if unicode.IsLetter(r) || unicode.IsNumber(r) { + return r + } + return -1 + } + pattern = strings.Map(mapper, pattern) + + // Generate a long random password. + var pwd [33]byte + rand.Read(pwd[:]) + // Add special chars to ensure it satisfies password requirements. + password = base64.StdEncoding.EncodeToString(pwd[:]) + "_-As@!%*(1)4#2" + password16, err := syscall.UTF16PtrFromString(password) + if err != nil { + t.Fatal(err) + } + + try := 0 + for { + // Calculate a random suffix to append to the user name. + var suffix [2]byte + rand.Read(suffix[:]) + suffixStr := strconv.FormatUint(uint64(binary.LittleEndian.Uint16(suffix[:])), 10) + name := pattern + suffixStr[:min(len(suffixStr), suffixLen)] + name16, err := syscall.UTF16PtrFromString(name) + if err != nil { + t.Fatal(err) + } + // Create user. + userInfo := windows.UserInfo1{ + Name: name16, + Password: password16, + Priv: windows.USER_PRIV_USER, + } + err = windows.NetUserAdd(nil, 1, (*byte)(unsafe.Pointer(&userInfo)), nil) + if errors.Is(err, syscall.ERROR_ACCESS_DENIED) { + t.Skip("skipping test; don't have permission to create user") + } + // If the user already exists, try again with a different name. + if errors.Is(err, windows.NERR_UserExists) { + if try++; try < 1000 { + t.Log("user already exists, trying again with a different name") + continue + } + } + if err != nil { + t.Fatalf("NetUserAdd failed: %v", err) + } + // Delete the user when the test is done. + t.Cleanup(func() { + if err := windows.NetUserDel(nil, name16); err != nil { + if !errors.Is(err, windows.NERR_UserNotFound) { + t.Fatal(err) + } + } + }) + return name, password + } +} + // windowsTestAccount creates a test user and returns a token for that user. // If the user already exists, it will be deleted and recreated. // The caller is responsible for closing the token. @@ -32,47 +114,15 @@ func windowsTestAccount(t *testing.T) (syscall.Token, *User) { // See https://dev.go/issue/70396. t.Skip("skipping non-hermetic test outside of Go builders") } - const testUserName = "GoStdTestUser01" - var password [33]byte - rand.Read(password[:]) - // Add special chars to ensure it satisfies password requirements. - pwd := base64.StdEncoding.EncodeToString(password[:]) + "_-As@!%*(1)4#2" - name, err := syscall.UTF16PtrFromString(testUserName) + name, password := addUserAccount(t) + name16, err := syscall.UTF16PtrFromString(name) if err != nil { t.Fatal(err) } - pwd16, err := syscall.UTF16PtrFromString(pwd) + pwd16, err := syscall.UTF16PtrFromString(password) if err != nil { t.Fatal(err) } - userInfo := windows.UserInfo1{ - Name: name, - Password: pwd16, - Priv: windows.USER_PRIV_USER, - } - // Create user. - err = windows.NetUserAdd(nil, 1, (*byte)(unsafe.Pointer(&userInfo)), nil) - if errors.Is(err, syscall.ERROR_ACCESS_DENIED) { - t.Skip("skipping test; don't have permission to create user") - } - if errors.Is(err, windows.NERR_UserExists) { - // User already exists, delete and recreate. - if err = windows.NetUserDel(nil, name); err != nil { - t.Fatal(err) - } - if err = windows.NetUserAdd(nil, 1, (*byte)(unsafe.Pointer(&userInfo)), nil); err != nil { - t.Fatal(err) - } - } else if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err = windows.NetUserDel(nil, name); err != nil { - if !errors.Is(err, windows.NERR_UserNotFound) { - t.Fatal(err) - } - } - }) domain, err := syscall.UTF16PtrFromString(".") if err != nil { t.Fatal(err) @@ -80,13 +130,13 @@ func windowsTestAccount(t *testing.T) (syscall.Token, *User) { const LOGON32_PROVIDER_DEFAULT = 0 const LOGON32_LOGON_INTERACTIVE = 2 var token syscall.Token - if err = windows.LogonUser(name, domain, pwd16, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token); err != nil { + if err = windows.LogonUser(name16, domain, pwd16, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token); err != nil { t.Fatal(err) } t.Cleanup(func() { token.Close() }) - usr, err := Lookup(testUserName) + usr, err := Lookup(name) if err != nil { t.Fatal(err) } -- 2.50.0