Skip to content

fix: request SYNCHRONIZE access in the Windows PID liveness check#597

Open
SHudici wants to merge 1 commit into
tirth8205:mainfrom
SHudici:fix/windows-pid-liveness
Open

fix: request SYNCHRONIZE access in the Windows PID liveness check#597
SHudici wants to merge 1 commit into
tirth8205:mainfrom
SHudici:fix/windows-pid-liveness

Conversation

@SHudici

@SHudici SHudici commented Jul 3, 2026

Copy link
Copy Markdown

Problem

_pid_alive_windows opens the process with PROCESS_QUERY_LIMITED_INFORMATION only, then calls WaitForSingleObject on the handle. That access right does not include SYNCHRONIZE, so WaitForSingleObject always returns WAIT_FAILED (last error 5, access denied) — for live and dead processes alike. The code treated any non-WAIT_OBJECT_0 result as "alive", so dead PIDs read as alive: stale daemons were never detected, and crg daemon start refused to start after a crash left a PID file behind.

Verified with a direct Win32 probe: waiting on a handle opened without SYNCHRONIZE fails with error 5; adding SYNCHRONIZE makes the wait report dead processes correctly.

Fix

  • Open with PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE.
  • Treat WAIT_FAILED explicitly: log and presume alive (fail-safe — never kill or replace a daemon we cannot inspect), instead of silently conflating it with "still running".
  • Declare argtypes/restype (via ctypes.wintypes) for OpenProcess, WaitForSingleObject, and CloseHandle. Without them ctypes defaults every return to C int: 64-bit HANDLEs can truncate, and WaitForSingleObject's WAIT_FAILED (0xFFFFFFFF) comes back as -1, which never compares equal to the unsigned constant — so the explicit WAIT_FAILED branch above could never fire against the real API.
  • Name the Win32 constants instead of inlining magic numbers.

Testing

  • tests/test_daemon.py: the fake kernel32 now asserts the exact access mask, and a new test covers the WAIT_FAILED → presumed-alive path with handle cleanup. Prototypes are declared only where the real DLL is constructed, so the injected-fake seam the tests use is unchanged.
  • On Windows 11 the previously-failing test_pid_alive_for_dead_pid / test_is_daemon_running_alive pass against the real API. POSIX path untouched.

One of a small series of PRs making the project fully usable on Windows contributor machines.

🤖 Generated with Claude Code

_pid_alive_windows opened the process with
PROCESS_QUERY_LIMITED_INFORMATION only. That access right cannot be
waited on, so WaitForSingleObject always returned WAIT_FAILED
(ERROR_ACCESS_DENIED) — for live and dead processes alike — and the
code treated any non-signaled result as "alive". Dead PIDs therefore
read as alive: stale daemons were never detected and `daemon start`
refused to run after a crash left a PID file behind.

Three changes:

- Open with PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE so the
  handle is waitable.
- Treat WAIT_FAILED explicitly: log and presume alive (fail-safe,
  consistent with the ACCESS_DENIED branch) instead of conflating it
  with "still running".
- Declare argtypes/restype for OpenProcess, WaitForSingleObject, and
  CloseHandle. ctypes otherwise defaults everything to c_int, which
  truncates 64-bit HANDLEs and returns WAIT_FAILED as -1 — never equal
  to the unsigned 0xFFFFFFFF constant, so the explicit WAIT_FAILED
  handling could never fire against the real API.

The kernel32 interface stays injectable, so the tests drive the
handle/wait outcomes portably; the prototypes are applied where the
real DLL is constructed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant