`gdi32` and `user32` read handle entries straight out of `Peb->GdiSharedHandleTable`. On iOS win32u's unix side is loaded once for the whole session, so `init_user()` — and with it `gdi_init()`, which writes that PEB field — runs behind a `pthread_once` in whichever pseudo-process loads user32 first. Every later 64-bit process keeps a NULL table and faults on its first GDI handle lookup (`NULL + index * 0x18`).
`win32u_gdi_check_publish()` already publishes a view per 32-bit pseudo-process; its guard requires a WoW64 TEB. This drops that requirement so a 64-bit process whose PEB has not been published yet is covered too. The check compares a thread-local pointer with the TEB's PEB, so the cost on the GDI hot path is one load and a compare.
Seen on an iPhone 15 Pro: a program that pulls in user32 (winhttp does) initialised the table; a GDI window program run next in the same session faulted in user32. Run first in a fresh session it showed its window. With this change a GDI window program passes after other GUI programs, repeatedly.
The other half (calling `win32u_gdi_check_publish()` after the `pthread_once` in `NtUserInitializeClientPfnArrays`) is in Madeira's `build/win32u-unix/class_ios.c`; that change is harmless without this one.
🤖 Generated with Claude Code