Repository navigation
terminate_process_tree() kills unrelated processes when a PID has been reused (no creation-time check) #6435
Description
Existing issues matching what you're seeing
- I was not able to find an open or closed issue matching what I'm seeing
Git for Windows version
git version 2.54.0.windows.1
cpu: x86_64
built from commit: 2b8a3ab140826ac423c2845ef81d4c6ac4f7bf3c
sizeof-long: 4
sizeof-size_t: 8
shell-path: D:/git-sdk-64-build-installers/usr/bin/sh
rust: disabled
feature: fsmonitor--daemon
gettext: enabled
libcurl: 8.19.0
OpenSSL: OpenSSL 3.5.6 7 Apr 2026
zlib: 1.3.2
SHA-1: SHA1_DC
SHA-256: SHA256_BLK
default-ref-format: files
default-hash: sha1Windows version
Windows 11
Windows CPU architecture
x86_64 (64-bit)
Additional Windows version information
Microsoft Windows [Version 10.0.26200.9457]Options set during installation
Editor Option: Notepad++
Custom Editor Path:
Default Branch Option: main
Path Option: Cmd
SSH Option: OpenSSH
Tortoise Option: false
CURL Option: WinSSL
CRLF Option: CRLFAlways
Bash Terminal Option: MinTTY
Git Pull Behavior Option: Merge
Use Credential Manager: Enabled
Performance Tweaks FSCache: Enabled
Enable Symlinks: Disabled
Enable FSMonitor: DisabledOther interesting things
Only seen during a git-heavy parallel test suite (pytest with 32 xdist workers, each running many short git commands against temporary repositories), which recycles PIDs very quickly. The victim was a long-running process whose parent had exited long before (started via cmd /c start ... from a batch file). Nothing else on the machine kills processes; Sysmon (ProcessCreate + ProcessAccess) attributed the PROCESS_TERMINATE opens on both victims to C:\Utilities\Git\mingw64\bin\git.exe running ls-remote, with the call stack entirely inside git.exe. Both victims exited with code 143 and produced no crash event. Git config has nothing unusual: credential.helper=manager, no fsmonitor, no pager, no hooksPath.
Terminal/shell
None interactive: git was invoked from Python's subprocess module (CPython 3.12 on Windows, from a pytest suite), which runs C:\Utilities\Git\cmd\git.exe directly. Git then spawned C:/Utilities/Git/usr/bin/sh.exe for the upload-pack step.
Commands that trigger the issue
The triggering command, run in a repository that has no `origin` remote configured:
git ls-remote origin main
Git treats `origin` as a local path, spawns `sh.exe -c "git-upload-pack 'origin'"`, the upload-pack fails, and on exit git terminates that sh.exe via mingw_kill(SIGTERM) -> terminate_process_tree(). The tree walk then also kills any unrelated process whose (dead) parent PID equals the PID Windows assigned to that sh.exe.
The PID collision cannot be forced, so there is no deterministic reproducer. The conditions that made it happen several times a day:
1. Start a long-running process through a parent that exits immediately, e.g. from cmd: `start "" pythonw.exe -m some_app`, so the long-running process keeps a dead parent PID.
2. In parallel, run many short git commands to churn PIDs, including repeated `git ls-remote origin main` in a remote-less repository (in our case a 32-worker pytest suite doing this among hundreds of other git calls per minute).
3. Eventually the sh.exe spawned for upload-pack receives the long-running process's dead parent PID; the long-running process and its descendants then exit with code 143.
A deterministic check of the underlying flaw: in terminate_process_tree(), any process whose th32ParentProcessID equals the target's PID is killed, even if it was created before the target, which a real child cannot be.Expected behaviour
git ls-remote origin main in a repository without an origin remote fails with an error (e.g. "'origin' does not appear to be a git repository") and exits non-zero. When git cleans up the sh.exe it spawned for upload-pack, only that sh.exe and its genuine descendants are terminated. No process that git did not start, directly or indirectly, is affected.
Actual behaviour
The command fails as expected, but git's cleanup also terminated two unrelated processes that were never its descendants: a long-running pythonw.exe launcher (the venv redirector C:....venv\Scripts\pythonw.exe) and its child, the real interpreter (C:...\Python312\pythonw.exe). Both had been running for several minutes before this git command started.
Sysmon timeline (local time, 2026-09-23):
- 19:38:48.377: ProcessCreate:
git.exe ls-remote origin main(C:\Utilities\Git\mingw64\bin\git.exe), pid 61596 - 19:38:48.398: ProcessCreate:
sh.exe -c "git-upload-pack 'origin'", pid 68884, parent 61596. PID 68884 was also the recorded (long dead) parent PID of the pythonw launcher. - 19:38:48.485 / .486: ProcessAccess: git.exe pid 61596 opens the pythonw child and the pythonw launcher, GrantedAccess 0x1 (PROCESS_TERMINATE); the call stack is git.exe frames only, down to KERNELBASE
- 19:38:48.492 / .494: ProcessTerminate: both pythonw processes exit with code 143 (128 + SIGTERM)
There is no crash event, no log line from the victims, and nothing else on the system touched them. We saw the same exit code 143 with the same silent disappearance several times the same day, each time during a parallel git-heavy test run.
Repository
No response
Activity
ammerritt-gh commentedon Sep 23, 2026
Previous discussion
This builds on #2221, which is about Git for Windows' background detachment. In #2221 (comment), @mfriedrich74 noted: "PIDs can be reused by windows once a process is terminated and all process handles closed. I think to guarantee a PID belongs to the the desired process you must hold a valid process handle to it." In #2221 (comment), @dscho agreed about "all those PID problems" but was wary of the complexity of a Job Object approach.
This report is an observed instance of that concern: unrelated, long-running processes terminated by the tree walk in
terminate_process_tree()(compat/win32/exit-process.h), reached frommingw_kill()(compat/mingw.c) with exit code 128 + SIGTERM = 143. Notably, the git-heavy suite where we hit it also rangit maintenance run --auto --quiet --detachafter every commit, so PID churn from the detach path is part of the environment.Workaround on our side
We now start the long-running service so that every process in its ancestry stays alive for its lifetime (the chain is rooted at
explorer.exe), so no ancestor's PID can be reused. Any other long-lived program whose parent has exited remains exposed whenever PID reuse coincides with git reaching this fallback with permission to terminate it.junkerderprovinz commentedon Sep 27, 2026
Same thing here, with a different trigger and a victim that most Firefox users run.
Git for Windows 2.54.0.windows.1, Windows 11 (build 26200). Firefox kept vanishing about every 20 minutes with exit code 143 and no crash report. Sysmon showed
C:\Program Files\Git\mingw64\libexec\git-core\git.exeopening all 14 Firefox processes with PROCESS_TERMINATE (access 0x1) within 30 ms, followed by a ProcessTerminate for each of them.Firefox is exposed by default:
firefox.exefirst runs as a launcher process that starts the browser and exits, so the main browser process normally has a dead parent PID.On the git side there was a partial clone (
--filter=blob:none) whose main pack had been truncated to 0 bytes while its .idx was intact. VS Code's git extension (autofetch on) kept runninggit for-each-ref --sort -committerdate ...in that repository, which triggered a lazy fetch (git -c fetch.negotiationAlgorithm=noop fetch origin ... --filter=blob:none --stdin) for the missing commit more than once a second. Each fetch died with "You are attempting to fetch ..., which is in the commit graph file but not in the object database" and on exit cleaned up itsgit remote-httpschild. The call stack of the ProcessAccess events, checked against the 2.54.0 binary, is exactly that:die()in fetch-pack.c, exit,cleanup_children(),mingw_kill(), thenexit_process()falling back toterminate_process_tree(). With that many short-lived helpers, one of them sooner or later got Firefox's dead parent PID, and Firefox went down with all its content processes.This path exists because 2.54.0 marks the remote helper (transport-helper.c) and the connect.c child with clean_on_exit, from dd3693e ("transport-helper, connect: use clean_on_exit to reap children on abnormal exit"). Your ls-remote case goes through the connect.c half of the same change. Upstream reverted it in 8b44dee, and 2.55.0 no longer has either line, so these two triggers should be gone there.
terminate_process_tree()itself hasn't changed, and git.c still uses clean_on_exit for dashed externals and aliases.So on 2.54.0 this doesn't need a 32-worker test suite. One broken partial clone in a folder that an editor autofetches is enough, and the victim can be any program that was started through a launcher which has since exited.
Repairing the clone stopped it. The creation-time check your report points to (skip entries created before the process being terminated) would have prevented it.
mingw: avoid terminating unrelated processes during cleanup
mingw: avoid terminating unrelated processes during cleanup
mingw: avoid terminating unrelated processes during cleanup