tailscale/tailscale · error
WaitForSingleObject: %w
Error message
WaitForSingleObject: %w
What it means
RunProcessInSession creates a child in the target session then blocks in WaitForSingleObject(pi.Process, timeoutMillis). This is the raw syscall failure branch (err != nil), distinct from wait results like WAIT_TIMEOUT which are handled separately. With a freshly created, still-open process handle this is nearly impossible and signals handle misuse.
Source
Thrown at util/winutil/restartmgr_windows.go:758
// cmdLineInfo and will reside inside the session identified by sessID, with the
// security token whose logon is associated with sessID. The child process's
// environment will be inherited from the session token's environment.
func RunProcessInSession(sessID SessionID, cmdLineInfo CommandLineInfo, timeout time.Duration) (uint32, error) {
timeoutMillis, err := wingoes.DurationToTimeoutMilliseconds(timeout)
if err != nil {
return 1, err
}
pi, err := startProcessInSessionInternal(sessID, cmdLineInfo, 0)
if err != nil {
return 1, err
}
windows.CloseHandle(pi.Thread)
defer windows.CloseHandle(pi.Process)
waitCode, err := windows.WaitForSingleObject(pi.Process, timeoutMillis)
if err != nil {
return 1, fmt.Errorf("WaitForSingleObject: %w", err)
}
if e := windows.Errno(waitCode); e == windows.WAIT_TIMEOUT {
return 1, e
}
if waitCode != windows.WAIT_OBJECT_0 {
// This should not be possible; log
return 1, fmt.Errorf("unexpected state from WaitForSingleObject: %d", waitCode)
}
var exitCode uint32
if err := windows.GetExitCodeProcess(pi.Process, &exitCode); err != nil {
return 1, err
}
return exitCode, nil
}
func startProcessInSessionInternal(sessID SessionID, cmdLineInfo CommandLineInfo, extraFlags uint32) (*windows.ProcessInformation, error) {
if err := cmdLineInfo.Validate(); err != nil {View on GitHub (pinned to 6e0912f979)
Solutions
- Ensure nothing closes the process handle before RunProcessInSession returns
- Validate the timeout: negative or overflow-large durations are converted by wingoes.DurationToTimeoutMilliseconds - log the millis value
- Treat recurrence as an integrity bug: audit all CloseHandle paths for the child process
Example fix
// before
pi, _ := startProcessInSession(sessID, cli)
go func() { pi.Process.Close() }() // races with internal wait
// after
// let RunProcessInSession own the full lifecycle; do not close pi externally
exitCode, err := winutil.RunProcessInSession(sessID, cli, 30*time.Second) Defensive patterns
Strategy: try-catch
Try / catch
exitCode, err := winutil.RunProcessInSession(sessID, cli, timeout)
if err != nil {
if errors.Is(err, windows.ERROR_INVALID_HANDLE) {
// handle lifecycle bug in caller; do not retry blindly
return fmt.Errorf("process handle invalidated during wait: %w", err)
}
return err
} Prevention
- Let RunProcessInSession own the child handle lifecycle end to end
- Never close pi.Process/pi.Thread externally around the wait
- Validate timeout values before passing them in (avoid negative durations)
When it happens
Trigger: pi.Process closed concurrently before the wait (the deferred CloseHandle firing early via a wrapper); an invalid timeout value after conversion; memory corruption of the handle variable.
Common situations: Wrapping RunProcessInSession with custom context/timeout logic that closes handles; copying ProcessInformation structs after partial close.
Related errors
- GetProcessTimes: %w
- waiting on terminated process handles: %w
- failed to create a user policy lock: %w
- OpenProcess(%d[%#X]): %w
- OpenProcessToken: %w
AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18).
Data as JSON: /api/errors/ccdabf7b017aee80.
Report an issue: GitHub.