Skip to content

fix(threads): run Windows pthread and small fiber stacks in guest VA range - #7149

Open
Pouare514 wants to merge 2 commits into
boykopovar:mainfrom
Pouare514:fix/guest-thread-stacks
Open

Pouare514 wants to merge 2 commits into
boykopovar:mainfrom
Pouare514:fix/guest-thread-stacks

Conversation

@Pouare514

Copy link
Copy Markdown
Contributor

What

  • Windows scePthreadCreate threads now run their guest entry on a GuestArena stack: AllocateGuestStack via DoMapAnon + Mprotect guard, FreeGuestStack on failure/exit, and CallOnGuestStack asm trampoline that swaps TEB StackBase/StackLimit/0x1478 to the guest range for the entry (catch-and-rethrow) then restores host bounds.
  • libSceFiber: small contexts (<64 KiB) now allocate from GuestArena instead of host VirtualAlloc, so rsp-masking idioms resolve to mapped guest memory; large contexts unchanged.
  • Regression test: GuestPthreadStack asserts 9-digit (>= 0x100000000) guest address (GuestArena starts 0x200000000, host <4 GiB).
  • Debt docs updated for both libraries; adopted (scePthreadSelf) and main threads still run on host stacks — main entry is covered by fix(windows): enter guest startup on an arena-backed main stack #3965.

Tested

  • Unit: guest_pthread_stack fail-without (stash-abort: worker RSP 0x2b0fbc0 host) / pass-with (4/4 RunThread entries in guest range); neighbors pass: fiber, guest_memory, guest_pthread_start (4/4 on rebased tip, Release Ninja).
  • Game (in combo with fix(windows): enter guest startup on an arena-backed main stack #3965 main-thread entry stack, files disjoint): TLOU NdJob pool init fixed (no 0x1a54d81 fault), 13 NdJobWorkerThreads on guest stacks (0x1404xxbc80), Vulkan ready + swapchain 1472x828. Next wall is an unrelated VideoOut bad_alloc.
  • git diff --check clean; python tools/check_conventions.py --base origin/main --head HEAD passes.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none — complementary with fix(windows): enter guest startup on an arena-backed main stack #3965, validated in combination, files disjoint
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for this, and for the TLOU coordination evidence on #3965. Review at 992ab94: one blocker before this can go further.

scePthreadExit and cancellation never reach the stack cleanup on Windows. StartNativeThread's restore-and-free only runs when RunThread returns. On Windows, scePthreadExit (Thread.cpp, the ReleaseThread(self); _endthreadex(0); branch) is called from inside the guest frames, and cancellation goes through the same path. So on every exit or cancel:

  1. The guest stack is never freed. FreeGuestStack isn't reached, and ReleaseThread may delete self, which drops the only pointer to it. The whole committed stack and its arena range stay mapped.
  2. The thread exits with the TEB StackBase/StackLimit/DeallocationStack still describing the guest mapping. Windows frees a thread's own stack at exit through DeallocationStack, so the real host stack likely leaks, and the OS may release the guest mapping behind GuestArena's back (the arena maps through placeholders, so that release could succeed and the arena would later try to replace a placeholder that's gone). This part we couldn't verify here, since our bench is Linux.

Exit and cancel are common: guest_pthread_cancel, guest_once and guest_pthread_destructor_lifetime all use them.

Ask:

  • On Windows, have scePthreadExit leave through the same restore path: keep the host bounds where it can reach them (thread_local or PthreadPrivate) and restore them before _endthreadex.
  • Since it's still running on the guest stack at that point, retire the stack for later unmapping instead of freeing it inline, the way Linux's RetireGuestStack/ReapRetiredStacks does (keyed on the thread handle being signalled).
  • Add a Windows test that runs N threads that call scePthreadExit and N that are cancelled, then checks after join that each old guest stack is no longer committed (VirtualQuery), so process commit doesn't grow per thread.
  • Please also run guest_pthread_cancel, guest_once and guest_pthread_destructor_lifetime on Windows; the PR lists fiber, guest_memory, guest_pthread_start and guest_pthread_stack.

Non-blocking notes:

  • The trampoline has no SEH unwind info. C++ throws are caught above it so normal unwinding is fine, but full stack walks (crash dumps, RtlCaptureStackBackTrace, debuggers) will stop or misread at that frame. .seh_proc/.seh_pushreg/.seh_setframe would be cleaner.
  • Each Windows guest thread now commits its whole stack up front, on top of the native _beginthreadex reservation. Fine for tens of threads; worth noting for titles with hundreds.

Merge order with #6540, #7135 and #3965 is fine; the text merges cleanly and there's no semantic overlap.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

One addition to the exit/cancel ask above, from reviewing #3965 alongside this.

With #3965 the main thread also runs on a guest arena stack. If the main thread calls scePthreadExit while workers are still running (the adopted main thread case), Windows goes _endthreadex -> ExitThread with the TEB still on the guest allocation, the same class as the worker case here. Today main exits from the host stack, so this is new once #3965 lands.

Since this PR owns Thread.cpp, we'd suggest a single fix in scePthreadExit: restore the host bounds before _endthreadex whenever the current stack is a registered guest stack (GuestStacks already tracks Registered/HostBounds), so it covers worker threads from this PR and the main thread from #3965. Please add a main-thread-exit case to the Windows test as well.

Planned order: #6540 -> #7135 -> #7149 (with this fix) -> #3965.

@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 3965 core/libs/prx/libc/include/GuestStacks.hpp
core/libs/prx/libc/src/GuestStacks.cpp
PR 3986 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 4449 core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
PR 4563 core/libs/prx/libkernel/Pthread/include/Pthread.hpp
core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 4630 docs/dev/debt/libkernel.md
PR 5021 core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
core/libs/prx/libkernel/Pthread/include/Pthread.hpp
core/libs/prx/libkernel/Pthread/src/Thread.cpp
core/libs/tests/GuestPthreadStack.cpp
docs/dev/debt/libkernel.md
PR 6044 core/libs/tests/GuestPthreadStack.cpp
PR 6149 docs/dev/debt/libkernel.md
PR 6469 docs/dev/debt/libkernel.md
PR 6586 docs/dev/debt/libkernel.md
PR 6740 core/libs/prx/libkernel/Pthread/include/Pthread.hpp
docs/dev/debt/libkernel.md
PR 6830 docs/dev/debt/libkernel.md
PR 6879 core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 6936 docs/dev/debt/libkernel.md
PR 7049 docs/dev/debt/libkernel.md
PR 7088 docs/dev/debt/libkernel.md
PR 7090 docs/dev/debt/libkernel.md
PR 7121 docs/dev/debt/libkernel.md
PR 7135 docs/dev/debt/libkernel.md
PR 7192 docs/dev/debt/libkernel.md
PR 7201 core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
core/libs/prx/libkernel/Pthread/src/Thread.cpp
core/libs/tests/GuestPthreadStack.cpp
PR 7299 docs/dev/debt/libkernel.md
PR 7447 docs/dev/debt/libkernel.md
PR 7464 docs/dev/debt/libkernel.md
PR 7511 docs/dev/debt/libkernel.md
PR 7530 docs/dev/debt/libkernel.md
PR 7532 core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 7565 core/libs/prx/libkernel/Pthread/include/Pthread.hpp
core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 7605 docs/dev/debt/libkernel.md
PR 7631 core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
PR 7653 docs/dev/debt/libkernel.md
PR 7804 core/libs/prx/libkernel/Pthread/include/Pthread.hpp
core/libs/prx/libkernel/Pthread/src/Thread.cpp
core/libs/tests/GuestPthreadStack.cpp
docs/dev/debt/libkernel.md
PR 7838 core/libs/prx/libkernel/Pthread/src/Thread.cpp
core/libs/tests/GuestPthreadStack.cpp
PR 7853 core/libs/prx/libkernel/Pthread/include/Pthread.hpp
core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 7937 core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
docs/dev/debt/libkernel.md
PR 8010 docs/dev/debt/libkernel.md
PR 8041 core/libs/prx/libkernel/Pthread/include/Pthread.hpp
core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 8344 docs/dev/debt/libkernel.md
PR 8359 docs/dev/debt/libkernel.md
PR 8373 docs/dev/debt/libkernel.md
PR 8408 docs/dev/debt/libkernel.md
PR 8448 docs/dev/debt/libkernel.md
PR 8477 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 8520 docs/dev/debt/libkernel.md
PR 8533 docs/dev/debt/libkernel.md
PR 8537 docs/dev/debt/libkernel.md
PR 8544 docs/dev/debt/libkernel.md
PR 8547 docs/dev/debt/libkernel.md
PR 8553 docs/dev/debt/libkernel.md
PR 8592 core/libs/prx/libkernel/Pthread/include/Pthread.hpp
core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 8605 docs/dev/debt/libkernel.md
PR 8641 docs/dev/debt/libkernel.md
PR 8658 core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
core/libs/prx/libkernel/Pthread/include/Pthread.hpp
core/libs/prx/libkernel/Pthread/src/Thread.cpp
core/libs/tests/GuestPthreadStack.cpp
docs/dev/debt/libkernel.md
PR 8700 docs/dev/debt/libkernel.md
PR 8725 docs/dev/debt/libkernel.md
PR 8726 docs/dev/debt/libkernel.md
PR 8728 docs/dev/debt/libkernel.md
PR 8742 docs/dev/debt/libkernel.md
PR 8872 docs/dev/debt/libkernel.md
PR 8890 core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 8910 docs/dev/debt/libkernel.md
PR 8916 docs/dev/debt/libkernel.md
PR 8921 docs/dev/debt/libkernel.md
PR 9001 docs/dev/debt/libkernel.md
PR 9005 docs/dev/debt/libkernel.md
PR 9059 docs/dev/debt/libkernel.md
PR 9060 docs/dev/debt/libkernel.md
PR 9072 core/libs/prx/libSceFiber/Export.cpp
docs/dev/debt/libSceFiber.md
PR 9074 docs/dev/debt/libkernel.md
PR 9095 docs/dev/debt/libkernel.md
PR 9099 docs/dev/debt/libkernel.md
PR 9104 docs/dev/debt/libkernel.md
PR 9105 docs/dev/debt/libkernel.md
PR 9108 docs/dev/debt/libkernel.md
PR 9109 docs/dev/debt/libkernel.md
PR 9123 docs/dev/debt/libkernel.md
PR 9127 docs/dev/debt/libkernel.md
PR 9133 docs/dev/debt/libkernel.md
PR 9134 docs/dev/debt/libkernel.md
PR 9153 docs/dev/debt/libkernel.md
PR 9156 docs/dev/debt/libkernel.md
PR 9160 core/libs/prx/libkernel/Pthread/include/Pthread.hpp
core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 9164 docs/dev/debt/libkernel.md
PR 9165 docs/dev/debt/libkernel.md
PR 9167 docs/dev/debt/libkernel.md
PR 9187 docs/dev/debt/libkernel.md
PR 9199 docs/dev/debt/libkernel.md
PR 9239 docs/dev/debt/libkernel.md
PR 9242 docs/dev/debt/libkernel.md
PR 9256 docs/dev/debt/libkernel.md
PR 9263 docs/dev/debt/libkernel.md
PR 9265 docs/dev/debt/libkernel.md
PR 9273 docs/dev/debt/libkernel.md
PR 9284 docs/dev/debt/libkernel.md
PR 9293 docs/dev/debt/libkernel.md
PR 9297 docs/dev/debt/libkernel.md
PR 9298 docs/dev/debt/libkernel.md
PR 9302 docs/dev/debt/libkernel.md
PR 9314 docs/dev/debt/libkernel.md
PR 9326 core/libs/prx/libkernel/Pthread/include/Pthread.hpp
docs/dev/debt/libkernel.md
PR 9359 docs/dev/debt/libkernel.md
PR 9363 docs/dev/debt/libkernel.md
PR 9365 docs/dev/debt/libkernel.md
PR 9378 docs/dev/debt/libkernel.md
PR 9389 docs/dev/debt/libkernel.md
PR 9461 docs/dev/debt/libkernel.md
PR 9477 docs/dev/debt/libkernel.md
PR 9482 docs/dev/debt/libkernel.md
PR 9491 docs/dev/debt/libkernel.md
PR 9499 docs/dev/debt/libkernel.md
PR 9509 docs/dev/debt/libkernel.md
PR 9512 docs/dev/debt/libkernel.md
PR 9515 docs/dev/debt/libkernel.md
PR 9557 docs/dev/debt/libkernel.md
PR 9561 docs/dev/debt/libkernel.md
PR 9566 docs/dev/debt/libkernel.md
PR 9573 core/libs/prx/libkernel/Pthread/include/Pthread.hpp
core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 9575 docs/dev/debt/libkernel.md
PR 9578 docs/dev/debt/libkernel.md
PR 9582 core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 9632 docs/dev/debt/libkernel.md
PR 9669 docs/dev/debt/libkernel.md
PR 9682 docs/dev/debt/libkernel.md
PR 9684 core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 9693 docs/dev/debt/libkernel.md
PR 9732 docs/dev/debt/libkernel.md
PR 9736 docs/dev/debt/libkernel.md
PR 9794 docs/dev/debt/libkernel.md
PR 9840 docs/dev/debt/libkernel.md
PR 9843 docs/dev/debt/libkernel.md
PR 9876 docs/dev/debt/libkernel.md
PR 9877 docs/dev/debt/libkernel.md
PR 9882 core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp
docs/dev/debt/libkernel.md
PR 9938 core/libs/prx/libc/include/GuestStacks.hpp
core/libs/prx/libc/src/GuestStacks.cpp
core/libs/prx/libkernel/Pthread/include/Pthread.hpp
core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 9962 core/libs/prx/libkernel/Pthread/include/Pthread.hpp
docs/dev/debt/libkernel.md

@oneandonlydean oneandonlydean self-assigned this Oct 18, 2026
@Pouare514
Pouare514 force-pushed the fix/guest-thread-stacks branch from 992ab94 to 9c1ec7f Compare October 18, 2026 00:51
@Pouare514

Copy link
Copy Markdown
Contributor Author

Fixed the blocker: Windows scePthreadExit now restores host TEB StackBase/Limit/DeallocationStack before _endthreadex and retires the guest mapping instead of freeing inline. Host bounds live in thread-local GuestThreadState (FLS), not PthreadPrivate, so they survive ReleaseThread(self) deleting self. Retire duplicates the thread HANDLE and unmaps once signalled, mirroring Linux Retire/Reap; cancellation goes through the same path. Adopted-main is covered by one check: if the frame is in a GuestStacks registered range, restore MainHostBounds (main restores only, no retire, process-lifetime; #3965 EnterGuestMainStack should call GuestStackNoteMainHostBounds before switching).

Test: GuestPthreadStack on Windows runs 4 scePthreadExit + 4 cancelled (64 KiB) + 1 adopted exit; after join VirtualQuery asserts each old guest low is no longer MEM_COMMIT.

Suites (Release Ninja, winlibs gcc15): fiber, guest_memory, guest_pthread_start (30s), guest_pthread_stack, guest_pthread_cancel, guest_once, guest_pthread_destructor_lifetime — 7/7 pass. Start first segfaulted in the trampoline (Detach retired while the worker had not read guestStack); Detach no longer retires.

SEH left out: the rsp-switch cannot be expressed, wrong directives would mislead more. Commit-upfront noted in debt. Merge order ack #6540 -> #7135 -> #7149 -> #3965; Depends on stays none (clean rebase onto f1af6c5).

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks, that resolves the blocker. scePthreadExit and cancellation now retire the guest stack, restore the host TEB bounds and then call _endthreadex, and the stack is unmapped once the thread handle is signalled. The new exit and cancel checks in GuestPthreadStack cover exactly the path that leaked before.

We're running full CI on 9c1ec7f now. The adopted-main check can wait for #3965, since the hook does nothing until #3965 calls GuestStackNoteMainHostBounds.

@github-actions github-actions Bot added the libc label Oct 18, 2026
@Bonjourcestmatt

Copy link
Copy Markdown
Contributor

Windows run of 9c1ec7f53 merged onto main f663e565f (alone, without #6540, #7135 or #3965). Two existing tests regress; both pass on main.

Setup: Windows 11, MinGW-w64 GCC 15.2.0 UCRT, full configure with submodules, Release, cmake --build build -- -k 0, ctest -E "agc|Agc|vulkan|gpu|shader" -j6 --timeout 120.

Test main f663e565f (3 runs) 9c1ec7f53 on main
guest_kernel_stack_protect passes, 0.03 s aborts (0xc0000409) in 3 of 3 runs (alone) and in the full run
guest_pthread_start passes, 1.5 s passes in 56 s twice and times out at 60 s once; 1.5 s on main
guest_pthread_stack (new) n/a passes

The rest of the non-GPU suite passes (744 of 746; the two failures are the ones above).

guest_kernel_stack_protect: the test prints GuestKernelStackProtect failed at line 101, which is Require(memory.Protect == PAGE_NOACCESS && memory.RegionSize >= GuardBytes) after mprotect_nid_postfix(guard, GuardBytes, 0) on a worker's stack. The worker now runs on a GuestArena mapping, so what VirtualQuery reports for that range is not what the test expects from a host stack. I did not look into whether the test or the stack protection should change.

guest_pthread_start: about 37 times slower (56 s against 1.5 s), which is also well past the 30 s per test the CI warns about. I did not find where the time goes.

Not checked: Linux and macOS (the change is Windows-only), a title, and the combination with #6540, #7135 and #3965 that the plan above mentions. If the two tests only fail without those, that would explain it.

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

@Pouare514

Copy link
Copy Markdown
Contributor Author

Thanks for confirming. Standing by for the full CI results on 9c1ec7f - happy to address anything it turns up. And agreed on the adopted-main check waiting for #3965's GuestStackNoteMainHostBounds hook-up.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks @Bonjourcestmatt for the Windows run. Full CI on 9c1ec7f shows the same thing: Windows guest_kernel_stack_protect aborts ("failed at line 101", 0xc0000409) and passes on current main. guest_pthread_start passed in 28.3 s against 1.6 to 2.3 s on main. I traced both to the stacks now coming from the guest arena. There are three things to fix before this can merge.

1. The guard check: the arena commits in 16 KiB pieces. AllocateGuestStack maps the stack with DoMapAnon. On Windows that ends in CommitArenaRange, which calls GuestArenaCommit_nid_postfix(..., PS5_PAGE_SIZE). In WindowsMappings, each 16 KiB granule replaces its own placeholder (MEM_REPLACE_PLACEHOLDER), so a 2 MiB stack is about 130 separate allocations. VirtualQuery never reports a region across an allocation boundary. So after the test's mprotect(guard, 0x10000, PROT_NONE), every page really is PAGE_NOACCESS, but RegionSize is 0x4000, and RegionSize >= GuardBytes fails. The protection works. The failure comes from the stack's layout. Either fix is fine:

  • (a) commit the thread stack as one allocation (granule = the whole stack); or
  • (b) keep the granules and make the test walk the regions over GuardBytes, requiring every region to be PAGE_NOACCESS.

Please don't just drop the RegionSize condition. The test still needs to show that the whole 64 KiB is inaccessible.

2. The pages below the reported stack can now be changed by the guest. This is the real regression, and the next asserts in the same test check it. With (1) fixed, I expect the two mprotect(..., 3) == -1 checks just below sceKernelIsStack's start to fail next. On main, those pages belong to the host thread stack. DoMprotect finds no registered allocation there and returns EINVAL. On this head, they are the 1 MiB host margin and the guard page of a DoMapAnon mapping, which is registered with GuestAllocations, so the guest's mprotect succeeds. A title can then make the guard page writable, or change the margin the host uses below the guest frames. Linux handles this case: AllocateGuestStack records the mapping in guestStackMappings, and GuestThreadStackHostRangeOverlaps makes DoMprotect return EACCES for anything outside [stackStart, stackEnd). On Windows, that function is compiled to return false. Please record the Windows stacks in the same table, and enable the overlap check on Windows too, so both platforms refuse the guard and margin.

3. guest_pthread_start is 15 to 37 times slower. That test creates and joins 10,000 threads with 1 to 2 MiB stacks. On main, each thread is one _beginthreadex reservation that commits lazily. On this head, each thread maps stackSize + 1 MiB + 16 KiB through the arena. That's roughly 130 to 195 placeholder splits and replacements on map, the same again on unmap through GuestArenaReset, and a VirtualQuery walk over each 16 KiB region in StartNativeThread, all under the GuestAllocations mutation lock. I've counted the calls but not timed them, so please profile it. Committing the stack as one allocation (option 1a) should remove most of the work. Reusing retired stacks of the same size would remove the map/unmap churn entirely. _beginthreadex still reserves stackSize + 1 MiB of native stack that the guest no longer runs on. Shrinking that native stack would also help with large thread counts. The test should come back to roughly main's time. Its 60 s Windows timeout is not the bar.

Not verified here: these are from reading the code (our bench is Linux, so I can't run the Windows path). The arena granule and the missing Windows overlap check are both visible in the current head. When you push, please run guest_kernel_stack_protect and guest_pthread_start on Windows, plus the tests from before (guest_pthread_stack, guest_pthread_cancel, guest_once, guest_pthread_destructor_lifetime, fiber, guest_memory).

… margin guard

Commit each Windows guest thread stack with granule = whole stack so a 64 KiB guard mprotect spans one VirtualQuery region, record stacks in guestStackMappings and enable GuestThreadStackHostRangeOverlaps on Windows so mprotect of the host-only margin fails, cache freed stacks for same-size reuse, release one host allocation per unmap, and shrink the native reservation to 256 KiB.
@Pouare514
Pouare514 force-pushed the fix/guest-thread-stacks branch from 9c1ec7f to 2456bc0 Compare October 18, 2026 09:54
@Pouare514

Copy link
Copy Markdown
Contributor Author

Addressed all three findings (rebased on current main, force-pushed).

(1) Single-allocation commit: DoMapAnon down through CommitArenaRange takes a commit granule (0 keeps the old 16 KiB behavior for other callers); AllocateGuestStack passes bytes, so each stack is one MEM_REPLACE_PLACEHOLDER and the 64 KiB guard mprotect spans one region — the RegionSize check is untouched and passes. Required follow-up: UnmapRegistered now releases one host allocation at a time (registry pieces split by the guard protection are accumulated per allocation); per-piece reset truncated the single allocation (guest release truncates a host allocation).

(2) Windows stacks now use the Linux mechanism exactly: RegisterGuestStackMapping/UnregisterGuestStackMapping plus guestStackMappings compiled on Windows, Register in StartNativeThread, Unregister in FreeGuestStack/ReapRetiredWinStacks, GuestThreadStackHostRangeOverlaps enabled — mprotect below the reported start fails with EACCES.

(3) Reuse plus shrink: FreeGuestStack caches up to 4 committed stacks (exit-retired stacks still unmap on signal, keeping the RequireUncommitted asserts); Allocate reuses same-size entries (verified engaged: 6765 reuses / 10100 creates). Native _beginthreadex reserve cut from stackSize+1 MiB to 256 KiB (entry/exit and SEH only; no test reads the host reserve). guest_pthread_start: 14.5s before, 1.5-1.8s after (3 runs), vs main's 1.6-2.3s.

Linux unverified here: Windows Release Ninja (winlibs gcc15) only — guest_kernel_stack_protect, guest_pthread_start, guest_pthread_stack, guest_pthread_cancel, guest_once, guest_pthread_destructor_lifetime, fiber, guest_memory all green; git diff --check and check_conventions.py clean.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks, the single allocation, the Windows overlap check and the reuse cache all read right. Two things before CI:

  1. Linux doesn't compile. DirectMemory.cpp:740 passes commitGranule to the system mmap on Linux ("too many arguments to function mmap(void*, size_t, int, int, int, __off_t)"), and the new UnmapRegistered lambda calls KernelArena::Get().Contains(...) (:901), but KernelArena only exists under _WIN32. Main's file compiles with the same command.

  2. A partial unmap of a Windows mapping outside the arena now frees the whole allocation. On main, UnmapRegistered calls munmap_release(allocation) only when last is set (nothing of the allocation remains), and otherwise decommits just the piece. The new flushPending calls munmap_release for every non-arena run, including the flush after the loop. So unmapping part of a MAP_FIXED mapping outside the arena releases the rest while it's still registered, and unmapping the rest later throws "VirtualFree release failed". Keeping last with the pending run (release only when the run ends the allocation, otherwise munmap the run), or not merging non-arena pieces, would keep main's behaviour.

Minor: when ReuseWinStack's commit check fails, the cache entry is dropped without unmapping it, so that mapping leaks.

@MarcZX10

Copy link
Copy Markdown
Contributor

I checked the current head 2456bc05a2 against the earlier Linux and partial-unmap findings. They still appear present:

  • In DirectMemory.cpp, MapPlaced calls mmap(..., commitGranule) at line 740 on Linux, where POSIX mmap takes six arguments. A local g++ -fsyntax-only run with the Linux libkernel flags fails there with “too many arguments”.
  • UnmapRegistered calls KernelArena::Get() at line 901 without a platform guard, but KernelArena is declared only in the Windows section. The same Linux compile fails with “KernelArena has not been declared”.
  • On Windows, flushPending calls munmap_release(pendingAllocation) for every non-arena run. The final flush runs even when the callback’s last flag is false, so a partial unmap of a host allocation still releases the full allocation while its remainder is registered. The pending run needs to retain whether the allocation is fully unmapped and decommit only the partial range.

These findings are on the current head; the PR’s latest Linux/Windows app jobs are skipped, so they were not caught by hosted checks.

@milescoviello

Copy link
Copy Markdown
Contributor

@Pouare514 a finding for your rebase, from @frankea's Windows runs on #7700 (comment). RTX 4090, Windows 11, on current main: guest_kernel_stack_protect fails at line 109 with main + this PR alone, 3 of 3. That's the errno check for when a fixed mmap over a worker's stack is refused. It passes on main f29546a. This is separate from the Linux build break MarcZX10 listed above.

For context, #7700 (NHL 26's Heartbeat walking an ended thread's stack) now has its Windows fix planned on top of this PR. frankea has a local follow-up that moves #7838's spare-stack list out of the __linux__ block, so this PR's Windows FreeGuestStack and its retired-stack reap collect into it instead of unmapping. They'll open it once this PR lands.

AI-assisted: yes (Claude Code)

@MarcZX10

Copy link
Copy Markdown
Contributor

A separate Windows regression surfaced in the RTX 4090 run documented in #7700: guest_kernel_stack_protect fails at its fixed-mmap-over-worker-stack errno assertion (3/3) with current main + this PR, while current main passes. The report identifies this independently of the Linux build issue already noted here. Could you check the Windows fixed-map rejection path and keep this test green on the rebased PR? This is useful because the mapping must be refused without changing the live stack, and callers also rely on the expected errno.

@Queuse

Queuse commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Rebased branch, 5fc50bdbe9, main a6dcea9279; original authorship preserved.

Corrects Linux mmap/platform guards, split arena unmaps, partial host-allocation release and fixed-map rejection across host-only stack margins. Baseline and candidate pass all 8 focused Windows cases (Windows 11 26200, GCC 15.2.0-r7). Earlier independent unmap controls fail when the defects are restored. Original-head Linux syntax fails; integrated syntax passes with current headers under WSL GCC 15.2.

#4563 and #9272 address separate stack-lifetime concerns. No full Linux build or title run. AI-assisted: yes.

@MarcZX10

Copy link
Copy Markdown
Contributor

I checked the exact open heads for #7149 and dependent follow-up #9582 with git merge-tree; they have a content conflict in libkernel/Pthread/src/Thread.cpp. #9582 carries the #8890 timed-join helper change, while this PR has its Windows guest-stack adjustments. Please coordinate a merge/rebase order with the #9582 author and preserve both behaviors in the integration. I am recording the conflict so the test/fix can be carried through whichever order is easiest; no maintainer action is being requested.

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

docs libc libkernel needs rebase Conflicts with main, rebase needed prx

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants