tailscale/tailscale · error

unexpected state from WaitForSingleObject: %d

Error message

unexpected state from WaitForSingleObject: %d

What it means

After WaitForSingleObject returns, RunProcessInSession expects exactly WAIT_OBJECT_0 (signaled) or WAIT_TIMEOUT; anything else lands here. The source comments say 'this should not be possible': process handles do not produce WAIT_ABANDONED, and WAIT_FAILED is normally surfaced as err. Encountering it means undocumented behavior or interference with the wait.

Source

Thrown at util/winutil/restartmgr_windows.go:765

	}

	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 {
		return nil, err
	}

	token, err := sessID.OpenToken()
	if err != nil {
		return nil, fmt.Errorf("(*SessionID).OpenToken: %w", err)
	}

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Log the waitCode value and retry the wait once with the remaining timeout
  2. Reproduce under Process Monitor / with app-verifier disabled to identify the hook
  3. If it recurs on specific machines, collect the waitCode and fall back to GetExitCodeProcess-based STILL_ACTIVE polling
Defensive patterns

Strategy: try-catch

Try / catch

exitCode, err := winutil.RunProcessInSession(sessID, cli, timeout)
if err != nil && strings.Contains(err.Error(), "unexpected state from WaitForSingleObject") {
    // defensive branch: log waitCode and retry once
    logf("wait anomaly, retrying once: %v", err)
    exitCode, err = winutil.RunProcessInSession(sessID, cli, timeout)
}

Prevention

When it happens

Trigger: Security software or shims hooking kernel32 WaitForSingleObject; WAIT_FAILED returned as a code instead of err by a hooked implementation; debugger-attached oddities altering wait semantics.

Common situations: Machines with AV/EDR products that intercept process APIs; application-verifier or compatibility shim layers installed; rarely, driver bugs.

Related errors


AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18). Data as JSON: /api/errors/5d71d7c03881338a. Report an issue: GitHub.