{"record":{"id":"4ad5c491ee6a81d1","repo":"gastownhall/beads","slug":"procid-open-process-d-w","errorCode":null,"errorMessage":"procid: open process %d: %w","messagePattern":"procid: open process (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/procid/procid_windows.go","lineNumber":33,"sourceCode":"type Handle struct {\n\tprocess windows.Handle\n\ttoken   Token\n}\n\n// errProcessExited marks a process which is terminated but whose PID is still\n// resolvable because some handle (ours or a third party's, such as Task\n// Manager or an antivirus scanner) keeps the process object alive. Treating\n// it as gone keeps the invariant \"Verify == true implies running\" on Windows.\nvar errProcessExited = errors.New(\"procid: process has exited\")\n\n// stillActive is the GetExitCodeProcess sentinel for a running process\n// (STILL_ACTIVE, 259).\nconst stillActive = 259\n\nfunc Capture(pid int) (Token, error) {\n\tprocess, err := openProcess(pid, windows.PROCESS_QUERY_LIMITED_INFORMATION)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"procid: open process %d: %w\", pid, err)\n\t}\n\tdefer func() { _ = windows.CloseHandle(process) }()\n\treturn tokenForProcess(process)\n}\n\nfunc Verify(pid int, tok Token) (bool, error) {\n\tprocess, err := openProcess(pid, windows.PROCESS_QUERY_LIMITED_INFORMATION)\n\tif err != nil {\n\t\tif errors.Is(err, windows.ERROR_INVALID_PARAMETER) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, fmt.Errorf(\"procid: open process %d: %w\", pid, err)\n\t}\n\tdefer func() { _ = windows.CloseHandle(process) }()\n\tcurrent, err := tokenForProcess(process)\n\tif err != nil {\n\t\tif errors.Is(err, errProcessExited) {\n\t\t\treturn false, nil","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/procid/procid_windows.go#L15-L51","documentation":"Capture on Windows opens the target process with PROCESS_QUERY_LIMITED_INFORMATION via OpenProcess and wraps any failure with this message. It means the OS refused to open the PID — most commonly the process does not exist (stale PID), or an access-control issue, and the wrapped win32 error (e.g. ERROR_INVALID_PARAMETER 87, ERROR_ACCESS_DENIED 5) distinguishes which.","triggerScenarios":"Calling procid.Capture(pid) on Windows with a PID that has already exited (OpenProcess returns ERROR_INVALID_PARAMETER for nonexistent PIDs); targeting a process owned by another user/service without permission (ERROR_ACCESS_DENIED); targeting a system-protected process (e.g. anti-virus, protected-light processes) that denies even limited query; PID reuse where the PID now points to a different, inaccessible process.","commonSituations":"Health checks probing stale PIDs after a crash; capture racing process exit; running beads as a non-admin user while the target runs elevated or as LocalSystem; PID reuse after fast process churn on long-running Windows hosts.","solutions":["Check errors.Is(err, windows.ERROR_INVALID_PARAMETER) / os.ErrNotExist semantics — treat it as 'process gone' rather than retrying","If ERROR_ACCESS_DENIED, run the caller with sufficient privileges (elevate, or run as the same user/service account as the target)","Re-capture a fresh PID immediately before Capture to narrow the exit race","If PID reuse is a concern, capture the token as early as possible after spawning the child and verify with Verify(pid, tok) before signaling"],"exampleFix":"// before\npid := getStalePIDFromConfig()\ntok, err := procid.Capture(pid)\nif err != nil {\n\treturn err\n}\n// after\ntok, err := procid.Capture(pid)\nif err != nil {\n\tif errors.Is(err, windows.ERROR_INVALID_PARAMETER) {\n\t\treturn nil // stale PID: process already exited\n\t}\n\treturn fmt.Errorf(\"capture pid %d: %w\", pid, err)\n}","handlingStrategy":"try-catch","validationCode":"// pre-check before Capture on Windows\nfunc canQuery(pid uint32) bool {\n\th, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)\n\tif err != nil {\n\t\treturn false\n\t}\n\twindows.CloseHandle(h)\n\treturn true\n}","typeGuard":"func isStalePID(err error) bool {\n\treturn errors.Is(err, windows.ERROR_INVALID_PARAMETER) || // process does not exist\n\t\terrors.Is(err, os.ErrProcessDone)\n}\n\nfunc isAccessDenied(err error) bool {\n\treturn errors.Is(err, windows.ERROR_ACCESS_DENIED)\n}","tryCatchPattern":"tok, err := procid.Capture(pid)\nswitch {\ncase err == nil:\n\t// proceed\ncase isStalePID(err):\n\treturn handleExitedProcess(pid)\ncase isAccessDenied(err):\n\treturn fmt.Errorf(\"need privileges to query pid %d (run elevated or same user): %w\", pid, err)\ndefault:\n\treturn fmt.Errorf(\"capture pid %d: %w\", pid, err)\n}","preventionTips":["Capture the token immediately after spawning the child to shrink the exit/PID-reuse race window","Run the caller under an account with at least PROCESS_QUERY_LIMITED_INFORMATION access to targets (same user or elevated)","Re-validate the PID right before Capture (windows.OpenProcess or tasklist) when working with externally supplied PIDs","On long-lived hosts with high process churn, re-capture tokens rather than caching PIDs across restarts"],"tags":["windows","process-management","openprocess","access-denied"],"backgroundTag":"process-open-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}