[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 <quimmuntal@gmail.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
TryBot-Bypass: Dmitri Shuralyov <dmitshur@golang.org>
(cherry picked from commit 374e3be2eb)
Reviewed-on: https://go-review.googlesource.com/c/go/+/690555
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
Reviewed-by: Mark Freeman <mark@golang.org>
This commit is contained in:
qmuntal 2025-07-24 15:38:35 +02:00 committed by Gopher Robot
parent c95d3093ca
commit 84fb1b8253

View file

@ -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)
}