Skip to content

fix(windows): spawn git without inheriting handles (UI hang)#846

Merged
DeusData merged 1 commit into
mainfrom
distill/799-win-handles
Jul 4, 2026
Merged

fix(windows): spawn git without inheriting handles (UI hang)#846
DeusData merged 1 commit into
mainfrom
distill/799-win-handles

Conversation

@DeusData

@DeusData DeusData commented Jul 4, 2026

Copy link
Copy Markdown
Owner

fix(windows): spawn git without inheriting handles (UI hang)

Closes #798.

Distilled from #799 (thanks @Flipper1994) with three review corrections.

Mechanism

On Windows, cbm_popen wrapped the CRT's _popen, which calls
CreateProcess(bInheritHandles=TRUE): every inheritable handle in the parent
— the UI listening socket, Winsock/AFD helper handles, the MCP stdio pipe —
leaks into the child. When the child is git-for-Windows, its MSYS2/Cygwin
runtime walks all inherited handles at startup and calls NtQueryObject on
each to classify it; on an inherited socket/AFD handle that call deadlocks.
The UI server handles requests on a single thread, so one wedged
git child (e.g. list_projects shelling out per project) hangs the whole
web UI.

The fix replaces the read-mode _popen path with an isolated spawn:

  • CreatePipe (inheritable SA) + SetHandleInformation de-inherits the
    parent read end;
  • STARTUPINFOEXW + PROC_THREAD_ATTRIBUTE_HANDLE_LIST restricted to
    exactly two handles: the stdout write end and a NUL handle for
    stdin/stderr (STARTF_USESTDHANDLES);
  • command runs through cmd.exe /c so quoting and 2>NUL redirections
    behave exactly as under _popen;
  • cbm_pclose resolves the stream via a small handle table and reaps the
    child with WaitForSingleObject + GetExitCodeProcess
    (INIT_ONCE-guarded critical section).

Nothing foreign crosses into git, so there is no handle to deadlock on.
POSIX popen already sets O_CLOEXEC; that path is untouched.

Corrections applied on top of #799

  1. No silent fallback to _popen — the original fell back to _popen
    whenever the isolated spawn failed, silently re-arming the deadlock. Now a
    failure logs a structured warning
    (compat.popen_isolated_failed stage=... gle=... errno=...) and returns
    NULL; all 17 cbm_popen call sites already handle NULL. The composed
    command line is heap-allocated (the original used a fixed 2048-byte buffer
    whose overflow forced the fallback) and widened via UTF-8 so non-ASCII
    repo paths survive.
  2. Explicit cmd.exe resolution — the shell is resolved from %COMSPEC%
    (GetEnvironmentVariableW), falling back to GetSystemDirectoryW +
    \cmd.exe, and passed as lpApplicationName so CreateProcessW never
    walks the search path (no cmd.exe planting from a hostile CWD).
  3. Exit-code and NUL-handle hardeningGetExitCodeProcess failure now
    returns -1 instead of reporting success; a failed NUL open fails the
    spawn instead of passing INVALID_HANDLE_VALUE into
    STARTF_USESTDHANDLES slots.

Tests

Two Windows-only regression tests in tests/test_security.c
(suite security, windows-latest CI):

  • popen_isolated_git_version_round_tripcbm_popen("git --version", "r") returns output through the isolated spawn and cbm_pclose returns 0.
    Asserts via the cbm_popen_last_was_isolated() test hook that the stream
    came from the isolated path — a revert to raw _popen fails the guard.
  • popen_isolated_propagates_exit_code — child exit code 7 surfaces through
    GetExitCodeProcess.

Verification

  • macOS: make -f Makefile.cbm cbm (-Werror) clean — POSIX path untouched;
    lint-ci clean; full C test suite green (Windows tests compiled out).
  • Only Windows CI can prove: compilation of the _WIN32 block, the two
    new regression tests against real git-for-Windows, and the absence of the
    hang in practice. The full UI-mode repro (listening socket + MSYS2 handle
    walk under a single-threaded server) is not covered by these tests and is
    noted as a follow-up harness.

Windows cbm_popen wrapped _popen, whose CreateProcess(bInheritHandles=TRUE)
leaks every inheritable handle - including the UI listening socket and
Winsock/AFD helpers - into git children. Git-for-Windows' MSYS2 runtime
walks inherited handles with NtQueryObject at startup and deadlocks on
socket/AFD handles, wedging the single-threaded UI server (#798).

Read-mode cbm_popen now spawns via CreateProcessW with STARTUPINFOEXW and
PROC_THREAD_ATTRIBUTE_HANDLE_LIST restricted to the stdout pipe write end
and a NUL handle for stdin/stderr; cbm_pclose reaps the child via a handle
table + WaitForSingleObject + GetExitCodeProcess.

Distilled from #799 with three corrections:
- no silent fallback to _popen: a failed isolated spawn logs a structured
  warning (compat.popen_isolated_failed stage/gle/errno) and returns NULL
  (all call sites handle NULL); the composed command line is heap-allocated
  instead of a fixed 2048-byte buffer whose overflow forced the fallback
- cmd.exe resolved explicitly from %COMSPEC% (GetSystemDirectoryW\cmd.exe
  fallback) and passed as lpApplicationName - no search-path lookup
- GetExitCodeProcess failure reports -1 instead of success; a failed NUL
  open fails the spawn instead of feeding INVALID_HANDLE_VALUE into
  STARTF_USESTDHANDLES

Adds two windows-only regression tests (test_security.c) driving the
cbm_popen/cbm_pclose round-trip against real git and asserting the isolated
path was taken via the cbm_popen_last_was_isolated() hook.

Closes #798

Co-authored-by: Flipper <jacobphilipp@ymail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData DeusData enabled auto-merge July 4, 2026 11:37
@DeusData DeusData merged commit d2a5975 into main Jul 4, 2026
15 checks passed
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.

Windows: web UI hangs — list_projects deadlocks the HTTP server (git via _popen handle inheritance)

1 participant