Skip to content

compat/poll: load user32 only when poll() runs - #6512

Open
azchohfi wants to merge 1 commit into
git-for-windows:mainfrom
azchohfi:compat-poll-lazy-user32
Open

azchohfi wants to merge 1 commit into
git-for-windows:mainfrom
azchohfi:compat-poll-lazy-user32

Conversation

@azchohfi

Copy link
Copy Markdown

On Windows, poll() in compat/poll/poll.c waits with MsgWaitForMultipleObjects() and pumps
window messages with PeekMessage(), TranslateMessage() and DispatchMessage(). These are the
only functions git.exe imports from user32.dll, so every Git process loads user32, and with it
gdi32, gdi32full, win32u and imm32, even though most commands never call poll().

This loads the four functions with the existing lazyload.h helpers the first time poll() runs.
If user32 cannot be loaded, the process cannot have a message queue to pump, so it waits with
WaitForMultipleObjects().

git.exe then maps 17 images for git --version instead of 23. On a Windows 11 desktop, built
from v2.55.0.windows.5 with and without the change, 100 interleaved runs each:

before after saved
git --version 15.5 ms 13.2 ms 2.2 ms [2.0, 2.4]
git rev-parse --git-dir 17.5 ms 15.2 ms 2.3 ms [2.0, 2.4]
git status --short 48.5 ms 46.2 ms 2.0 ms [1.7, 2.2]
git log -1 --oneline 20.4 ms 18.0 ms 2.3 ms [2.0, 2.5]

t0061-run-command, t5400-send-pack and t5601-clone pass with the change, on the release and
on main.

The same code is in upstream Git, so I can send it to the mailing list too if you prefer it to go
upstream first.

I used GitHub Copilot to help investigate and prepare this change. I have reviewed and tested it.

@rimrul

rimrul commented Oct 23, 2026

Copy link
Copy Markdown
Member

If user32 cannot be loaded, the process cannot have a message queue to pump, so it waits with
WaitForMultipleObjects()

Could you elaborate on where we'd expect to need that fallback?

@azchohfi

Copy link
Copy Markdown
Author

Mostly sandboxes that start processes with Win32k system calls disabled (PROCESS_CREATION_MITIGATION_POLICY_WIN32K_SYSTEM_CALL_DISABLE_ALWAYS_ON). user32.dll can't initialize in those, so today's git.exe doesn't even start there (it fails with 0xC0000142 before main() runs). With this change, git --version and git rev-parse work in that setup.

If such a process gets to poll(), loading user32 fails and it falls back to a plain wait on the handles. There's no message queue to pump in that case anyway.

I haven't been able to hit that path, though, because anything that reaches poll() also starts sh.exe, which fails the same way. If you'd rather not keep code that isn't exercised, I can drop the fallback and have poll() return an error instead.

@azchohfi
azchohfi force-pushed the compat-poll-lazy-user32 branch from 391f401 to b3e463c Compare November 2, 2026 18:42
@rimrul

rimrul commented Nov 3, 2026

Copy link
Copy Markdown
Member

Mostly sandboxes that start processes with Win32k system calls disabled (PROCESS_CREATION_MITIGATION_POLICY_WIN32K_SYSTEM_CALL_DISABLE_ALWAYS_ON). user32.dll can't initialize in those, so today's git.exe doesn't even start there (it fails with 0xC0000142 before main() runs). With this change, git --version and git rev-parse work in that setup.

Ah, sandboxes are the puzzle piece I didn't think about hard enough. I even looked up if nano server had no user32.dll, but sandboxes make sense.

I haven't been able to hit that path, though, because anything that reaches poll() also starts sh.exe, which fails the same way. If you'd rather not keep code that isn't exercised, I can drop the fallback and have poll() return an error instead.

That fallback might become reasonably reachable in combination with msys2/MSYS2-packages#6785 and git-for-windows/MINGW-packages#230.

It might make sense to mention in the commit message that this fallback is intended for sandboxed environments with PROCESS_CREATION_MITIGATION_POLICY_WIN32K_SYSTEM_CALL_DISABLE_ALWAYS_ON.

On Windows, poll() waits with MsgWaitForMultipleObjects() and pumps
window messages with PeekMessage(), TranslateMessage() and
DispatchMessage(). These are the only functions git.exe imports from
user32.dll, so every Git process loads user32, and with it gdi32,
gdi32full, win32u and imm32, even though most commands never call
poll().

Load the four functions with the existing lazyload helpers the first
time poll() runs instead. If user32 cannot be loaded, the process
cannot have a message queue to pump, so wait with
WaitForMultipleObjects().

That fallback is for sandboxed processes started with Win32k system
calls disabled
(PROCESS_CREATION_MITIGATION_POLICY_WIN32K_SYSTEM_CALL_DISABLE_ALWAYS_ON).
user32.dll cannot initialize there, so today git.exe fails to start
in them with 0xC0000142 before main() runs.

Signed-off-by: Alexandre Zollinger Chohfi <alzollin@microsoft.com>
@azchohfi
azchohfi force-pushed the compat-poll-lazy-user32 branch from b3e463c to fc94eca Compare November 3, 2026 18:54
@azchohfi

azchohfi commented Nov 3, 2026 •

Copy link
Copy Markdown
Author

Thanks, I've added that to the commit message.

I also found a way to reach the fallback without a shell: parallel checkout waits for its checkout--worker children with poll(). In a process started with Win32k disabled, git -c checkout.workers=8 -c checkout.thresholdForParallelism=0 switch <branch> worked 10 times out of 10 with this change, and the checked-out files were correct. And you're right about msys2/MSYS2-packages#6785: with bash built against that readline, a post-checkout hook also runs through sh.exe in the same sandbox, while with the current bash it doesn't start.

This branch has not been deployed

No deployments
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.

2 participants