Skip to content

fix(libkernel): start thread stacks on a 16 KiB page boundary on Linux - #9684

Open
NevesHenry wants to merge 7 commits into
boykopovar:mainfrom
NevesHenry:fix/libkernel-page-aligned-thread-stacks
Open

NevesHenry wants to merge 7 commits into
boykopovar:mainfrom
NevesHenry:fix/libkernel-page-aligned-thread-stacks

Conversation

@NevesHenry

@NevesHenry NevesHenry commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What

On Linux, scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack reported a thread's stack starting at an address the host picked. A created thread's stack started wherever the host's start frames ended (for example 0x200107f90), and the main thread's started at glibc's 4 KiB boundary. On the console a stack starts on a 16 KiB page boundary.

HotSpot (OpenJDK 25) aligns its stack guard zone down to the page size it reads (16384) before protecting it. That protect started below the reported stack, mprotect refused it with EINVAL (main thread) or EACCES (created threads), and the VM stopped:

[0.459s][warning][os,thread] Attempt to protect stack guard pages failed (0x00007ffc66c4a000-0x00007ffc66c56000).
# Native memory allocation (mprotect) failed to protect 49152 bytes. Error detail: memory to guard stack pages
#  Internal Error (stackOverflow.cpp:113), pid=114037, tid=100001

The VM's error report then aborted the process with sysctl_nid_postfix: unsupported MIB 1.21 (kern.boottime, read by os::print_os_info for the report).

#7532, on main since 2026-11-03, runs a relinked executable's main thread on a stack at the console's address, which starts on a 16 KiB boundary. It doesn't change created threads, which is where ProsperoCraft stops with it (below).

Tested

Linux (Ubuntu 22.04, WSL2), Ryzen 9 7900, lavapipe, GCC 13, Release.

Titles, from mateo-cogeanu/ps5-java25-homebrew, unwrapped and relinked with --trap-sdk-syscall-stubs --registry, rows driven with tools/pad_input.py, run on main 504ea3b. All three report ALL PASS on a PS5 (firmware 13.60):

Title main This PR
Java 25 File I/O (PPSA99866, 01.000.004) stops at Calling real JNI_CreateJavaVM with the error above runtime init, JNI_CreateJavaVM returned 0, FileIO25 returned normally, DestroyJavaVM complete
Java 25 ZIP/JAR (PPSA99867, 01.000.000) same stop JNI_CreateJavaVM returned 0, ZipJar25 returned normally, DestroyJavaVM complete
Java 25 Dependencies (PPSA99873, 01.000.001) same stop all six rows: VM created, multi-JAR classes and services, System.loadLibrary of the embedded JNI fixture, Dependencies fixture returned normally, DestroyJavaVM complete

At DestroyJavaVM HotSpot still warns Attempt to deallocate stack guard pages failed for the main thread; it isn't fatal and the row passes.

milescoviello ran ProsperoCraft (PPSA99874, 01.000.117), the catalog's Minecraft Java launcher, on main 750cf6c (comment). With #12436 the JVM stops at start-up with the mprotect error above, in the primordial thread. With #7532 as well, the primordial thread gets past it and the same error moves to the Reference Handler thread, whose stack doesn't start on a page boundary. With #7532 and this PR it runs the full 120 s (#7532 is on main now): the Instances screen, the new-instance form and its on-screen keyboard, following pad input.

  • New guest_thread_stack_guard: the main thread's and created threads' (64 KiB, 1 MiB, 16 MiB) stacks start on a 16 KiB boundary, keep the requested size, contain the running frame, match sceKernelIsStack, and their lowest three pages can be protected read/write, none, read/write. It aborts on main and passes here.
  • Head 8989d4f merges main d63781e. The one conflict was in docs/dev/debt/libkernel.md, where feat(libkernel): deliver host faults to sceKernelInstallExceptionHandler handlers #7192 split the sceKernelInstallExceptionHandler entry: those entries are main's, the stack entry is this branch's. main's Thread.cpp changes since 5454cd2 (the guest main stack from fix(linux): enter the guest entry on a main stack at the console's address #7532, the Windows GS state, thread references in join and detach) don't move created threads' stacks, and on the guest main stack HostStackLimits returns bounds that already start on a 16 KiB boundary, so the rounding changes nothing there. On that head, GCC 11.4:
    • Release: the same ctest -R now selects 172 tests (main added 22) and all pass. guest_thread_stack_guard passes 50/50 repeats, and public_headers passes.
    • ASan + UBSan, Debug, same options as below: the 8 stack and thread tests listed there pass 20/20 with no sanitizer report under setarch -R. With address randomization on, 7 of them pass 20/20, and guest_kernel_stack_protect aborts in 25 of 40 runs at ASan start-up, before the test runs (Shadow memory range interleaves with an existing memory mapping, Workarounds for #837 (Shadow memory range interleaves with an existing memory mapping. ASan cannot proceed correctly. ABORTING.) google/sanitizers#856, with GCC 11's libasan here). It aborts the same way in 25 of 40 runs with Thread.cpp from main; the other 15 runs pass, as with this PR.
    • Control with Thread.cpp from main: guest_thread_stack_guard aborts, in Release and under ASan.
    • tools/check_conventions.py --base origin/main and git diff --check pass.
  • On 790fe65 (main 5454cd2): the 150 tests then selected by ctest -R '^guest_(.*pthread.*|.*thread.*|.*stack.*|cond_.*|.*sem.*|.*sleep_cancel.*|.*tls.*|rwlock_retire|timed_join.*|sigaltstack|kernel_sys.*)$', without ult_, libressl, net_thread, ampr, savedata and the relinker TLS tests, passed in Release. ASan + UBSan, Debug (-DANYPS5_ENABLE_ASAN=ON, -fsanitize=undefined -fno-sanitize-recover=undefined, UBSAN_OPTIONS=halt_on_error=1), default ASAN_OPTIONS: guest_thread_stack_guard (which sets detect_stack_use_after_return=0, like its neighbours), guest_kernel_is_stack, guest_kernel_stack_protect, guest_thread_self, guest_pthread_stack, guest_pthread_join_detach, guest_cond_timedwait and guest_pthread_join_self passed 20/20; of the 150, 146 passed, and guest_libc_threads, guest_posix_sem_post, guest_pthread_rwlock (LeakSanitizer leaks) and guest_force_tls_destructor (UBSan misaligned load at libSceLibcInternal/Export.cpp:87) failed with the same reports with Thread.cpp from main.
  • On the earlier bases (504ea3b, 737f172): ctest -R '^guest_' passed 737/738; guest_runtime_lifecycle timed out in its concurrent mode, as it did with Thread.cpp from main. guest_thread_stack_guard passed with ulimit -s 1024, 8192, 65536 and unlimited. With the thread start frame taken from a local's address, as before 9c1557e, guest_pthread_join_detach, guest_cond_timedwait and guest_pthread_join_self segfaulted under ASan, as reported in review. The test includes prx/libkernel/include/Api.hpp instead of declaring the libkernel exports itself (2d1d417), as the public header audit from refactor(libs): add full export headers for libs #10706 requires.

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
  • Fixes: none
  • Unblocks: PPSA99866 Java 25 File I/O (PPSA99866) #9683, PPSA99867, PPSA99873, PPSA99874
  • GPUs tested: n/a
  • AI-assisted: yes (Claude Code)

🤖 Generated with Claude Code

@github-actions github-actions Bot added the unblocks title Names a title ID on the Unblocks line label Oct 22, 2026

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the Linux stack-bound calculations and thread startup path, including the native-stack reserve and the downward alignment of the guest stack start. The new test covers adopted main and created threads at three stack sizes, verifies the reported bounds against the active frame and sceKernelIsStack, and exercises guard-page protection. I ran the current-main conventions check and git diff --check; both passed. Pairwise merge-tree checks with open #7532 and #7838 are clean, and #9683 is the Game bring-up discussion documenting all three Java 25 titles and this blocker. The PR author reports 547/547 guest tests passing on a supported Linux host plus before/after title runs; local runtime reproduction is not available on this Xeon E5-1620 v2 host because the project CPU guard requires features it lacks. The automatic product jobs are skipped by repository policy. I found no remaining issue requiring changes.

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 14190 docs/dev/debt/libkernel.md L223-224

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 3986 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 4563 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 4630 docs/dev/debt/libkernel.md
PR 5021 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 6740 docs/dev/debt/libkernel.md
PR 6936 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 7088 docs/dev/debt/libkernel.md
PR 7105 docs/dev/debt/libkernel.md
PR 7121 docs/dev/debt/libkernel.md
PR 7135 docs/dev/debt/libkernel.md
PR 7149 core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 7192 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 7447 docs/dev/debt/libkernel.md
PR 7464 docs/dev/debt/libkernel.md
PR 7804 core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 8592 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 9019 core/libs/prx/libkernel/CMakeLists.txt
PR 9156 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9199 docs/dev/debt/libkernel.md
PR 9293 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9297 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9302 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9383 core/libs/prx/libkernel/CMakeLists.txt
PR 9389 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9557 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9566 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9843 docs/dev/debt/libkernel.md
PR 10025 docs/dev/debt/libkernel.md
PR 10319 docs/dev/debt/libkernel.md
PR 10442 docs/dev/debt/libkernel.md
PR 11831 core/libs/prx/libkernel/CMakeLists.txt
PR 11896 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 11939 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 12104 docs/dev/debt/libkernel.md
PR 12130 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 12544 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 12560 docs/dev/debt/libkernel.md
PR 12642 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 12682 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 12688 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 12700 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 12965 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/debt/libkernel.md
PR 12970 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 13560 docs/dev/debt/libkernel.md
PR 14103 docs/dev/debt/libkernel.md
PR 14128 docs/dev/debt/libkernel.md
PR 14212 docs/dev/debt/libkernel.md
PR 14264 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 14299 core/libs/prx/libkernel/CMakeLists.txt
PR 14310 docs/dev/debt/libkernel.md
PR 14832 docs/dev/debt/libkernel.md
PR 14854 docs/dev/debt/libkernel.md
PR 14886 docs/dev/debt/libkernel.md
PR 14894 docs/dev/debt/libkernel.md

@jotitadev jotitadev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran on native Linux (the PR was tested on WSL2 / glibc 2.35): Ubuntu 26.04, kernel 7.0, glibc 2.43, x86_64 i7-8700, GCC 15.2.0, Release, head d821307 merged --no-commit onto origin/main 7e2ba73.

  • guest_thread_stack_guard + patched_libraries: 2/2 pass.
  • Control (Thread.cpp from main, test and CMake from the PR): guest_thread_stack_guard aborts, as described.
  • The PR build also passes with ulimit -s 1024, 8192, 16384, 65536 and unlimited (main thread bounds come from pthread_getattr_np, which depends on that limit).

One thing for the -DANYPS5_ENABLE_ASAN=ON build: guest_thread_stack_guard aborts there at

Require(reinterpret_cast<std::uintptr_t>(&local) > start + GUARD_SIZE && reinterpret_cast<std::uintptr_t>(&local) < start + size);

on the main thread, because ASan's use-after-return fake stack puts local on the heap (start=0x7ffdc63e0000, &local=0x7097fc722130). No ASan or UBSan report. It's the same reason guest_kernel_is_stack, guest_pthread_stack, guest_thread_self and guest_kernel_stack_protect have add_asan_test_options(... detect_stack_use_after_return=0). Adding the same line after the set_tests_properties call:

add_asan_test_options(guest_thread_stack_guard detect_stack_use_after_return=0)

makes it pass under ASan+UBSan with no reports, as do those three neighbours.

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

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new independent test report found one configuration gap in the added Linux-only guest_thread_stack_guard test. It passes in Release, but aborts in an ASan+UBSan build at the assertion that a local variable lies inside the native main-thread stack. With ASan use-after-return detection enabled, that local can be placed on ASan's fake heap stack instead. The neighboring guest_kernel_is_stack, guest_kernel_stack_protect, guest_thread_self and guest_pthread_stack tests all call add_asan_test_options with detect_stack_use_after_return=0 for this same stack-inspection reason. Please add the same option for guest_thread_stack_guard and rerun it under ASan+UBSan; the independent report says this makes the test pass without sanitizer findings. The exact PR head is unchanged, so the prior code review stands apart from this newly identified test-configuration blocker.

@valenciana-nc

Copy link
Copy Markdown
Contributor

Merge-order coordination with #9482: I rebased that pthread lifetime fix onto main b7d1d47a5f (head 6298429beb) and reran the overlap check. git merge-tree finds only one conflict between our branches: adjacent entries in docs/dev/debt/libkernel.md. Your stack-alignment update and my ended-thread update are separate; there is no code/test conflict or runtime dependency.

Proposing #9482 first, followed by rebasing this PR and keeping both updated debt entries. If this PR is ready first, I can instead rebase #9482 afterward and preserve your stack-alignment text. Please let me know which order you prefer; not treating this as an agreed dependency or copying your changes into my PR.

For #9482, Windows focused pthread tests pass 27/27, with 25 attribute-test repeats and a new-main negative control. No Linux/title validation of your stack change is implied. AI-assisted investigation/report.

@MarcZX10

Copy link
Copy Markdown
Contributor

The proposed order (#9482 first, then rebase #9684 while preserving both debt entries) looks reasonable from the overlap described here; there is no code or test dependency to add. Please coordinate the final order between the two PR authors. Separately, the ASan test-configuration request from my review remains outstanding.

@NevesHenry

Copy link
Copy Markdown
Contributor Author

@MarcZX10 @jotitadev thanks. Added add_asan_test_options(guest_thread_stack_guard detect_stack_use_after_return=0) in cbb1115, the same option the neighbouring stack tests use.

Rerun on Linux (Ubuntu 22.04, GCC 13, Release), with -DANYPS5_ENABLE_ASAN=ON, -fsanitize=undefined and UBSAN_OPTIONS=halt_on_error=1:

  • Without the option, guest_thread_stack_guard_tests aborts at the main-thread frame check with no sanitizer report. That matches what you saw.
  • With it, guest_thread_stack_guard, guest_kernel_is_stack, guest_kernel_stack_protect, guest_thread_self and guest_pthread_stack pass (5/5), with no ASan or UBSan report.
  • The Release build still passes guest_thread_stack_guard.

@valenciana-nc fine with #9482 going first. Once it lands I'll rebase this one and keep both debt entries; no Depends on needed.

AI-assisted (Claude Code); the runs above were made on my machine.

@adriadam10 adriadam10 self-assigned this Oct 23, 2026
@adriadam10
adriadam10 dismissed MarcZX10’s stale review October 24, 2026 10:20

Dismissing a stale review: the requested changes were addressed after it (new head and/or updated description with the asked-for runs) and the merge queue blocks on it. Please request changes again if anything is still open.

@ahmadmuj ahmadmuj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed head cbb1115af0b6 merged onto main 0119103fa9ed (merges clean). WSL2 Ubuntu 24.04, GCC 13.3, i7-13700K. Two builds: Release, and Debug with -DANYPS5_ENABLE_ASAN=ON plus -fsanitize=undefined -fno-sanitize-recover=undefined.

What I ran, on this PR and with Thread.cpp from main as the control:

  • Release: guest_thread_stack_guard, guest_kernel_is_stack, guest_kernel_stack_protect, guest_thread_self, guest_pthread_stack, guest_pthread_join_detach, guest_cond_timedwait, guest_pthread_join_self all pass. guest_thread_stack_guard passes 50/50 repeats, and with ulimit -s 1024, 8192, 65536 and unlimited. On the control it aborts and the other 7 pass.
  • I also checked the alignment against main's new mmap guest stack (AllocateGuestStack). nativeStack includes HOST_STACK_MARGIN, so the aligned-down start stays about 1 MiB above the guard page, whatever the stack size.

ASan: the alloca segfaults guest threads. StartNativeThread sizes the alloca from &stackEnd (Thread.cpp L812). With ASan's default options, use-after-return detection is on. A local's address can then point into ASan's fake stack, as jotitadev saw for local in the test, so frame - stackEnd is no longer a distance on the thread stack. Tests that start a guest thread without detect_stack_use_after_return=0 crash. The three I ran:

ASan build guest_pthread_join_detach guest_cond_timedwait guest_pthread_join_self
this PR SEGFAULT SEGFAULT SEGFAULT
this PR, ASAN_OPTIONS=detect_stack_use_after_return=0 pass pass pass
control (main's Thread.cpp) pass pass pass

Taking the frame from the compiler fixes it:

if (const auto frame = reinterpret_cast<std::uintptr_t>(__builtin_frame_address(0)); frame > stackEnd)

With only that line changed, all 8 tests above pass in Release and in ASan+UBSan, with no sanitizer report. The three tests above pass 20/20 under default ASan options, and so does guest_thread_stack_guard with its option.

Nit, debt entry. The rewritten line in docs/dev/debt/libkernel.md still ends with "on Linux it cannot change the unmapped lower pages". That is no longer true: since b5909a5, DoMprotect maps a host stack's unmapped pages before protecting them (MapUnmappedPages in DirectMemory.cpp, and the "Protecting a host thread's stack" entry says so). Your own test relies on this: it protects the bottom three pages of the main thread's 8 MiB range, which are unmapped, and it passes. I'd drop that clause.

Not covered: Windows (I didn't build it) and the Java titles (no dumps here).

AI-assisted (Claude Code); I ran the builds and tests above on my machine.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 25, 2026
NevesHenry and others added 4 commits October 25, 2026 23:08
scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack reported a
Linux thread's stack from an address the host chose: a created thread's
stack began wherever the host's start frames left it (0x200107f90), and
the main thread's at the 4 KiB boundary glibc reports. On the console a
stack starts on a page boundary. HotSpot aligns its stack guard zone down
to the 16 KiB page before protecting it, so the protect started below the
reported stack and failed, and the VM stopped with "memory to guard stack
pages".

A created thread's stack now starts at the 16 KiB boundary below the
requested size and keeps that size; the thread enters it below its top.
An adopted host thread's stack starts at the first 16 KiB boundary above
the bottom of its host stack.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With ASan's use-after-return detection on, the test's local variable can
live on ASan's fake stack in the heap instead of the native main-thread
stack, so the check that it lies inside the reported stack aborts the
test in a -DANYPS5_ENABLE_ASAN=ON build. Turn it off for this test, as
guest_kernel_is_stack, guest_kernel_stack_protect, guest_thread_self and
guest_pthread_stack already do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
StartNativeThread sized the alloca that moves a created thread below the
top of its reported stack from the address of a local. With ASan's
use-after-return detection on, that local can live on ASan's fake stack
in the heap, so the distance was not one on the thread stack and guest
threads crashed in a -DANYPS5_ENABLE_ASAN=ON build with default options.
Take the frame from __builtin_frame_address(0), as MeasureStackReserve
does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since b5909a5 DoMprotect maps a host stack's unmapped pages before
protecting them on Linux, so the entry no longer says it cannot change
them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@NevesHenry
NevesHenry force-pushed the fix/libkernel-page-aligned-thread-stacks branch from cbb1115 to 2295675 Compare October 26, 2026 02:30
@NevesHenry

Copy link
Copy Markdown
Contributor Author

@ahmadmuj thanks, both points were right.

  • StartNativeThread now takes the frame from __builtin_frame_address(0) (9c1557e), as MeasureStackReserve does. In an ASan + UBSan Debug build with default ASAN_OPTIONS, guest_pthread_join_detach, guest_cond_timedwait and guest_pthread_join_self segfault with &stackEnd and pass 20/20 with the change. guest_thread_stack_guard, guest_kernel_is_stack, guest_kernel_stack_protect, guest_thread_self and guest_pthread_stack pass too, with no sanitizer report.
  • The debt entry no longer has the Linux clause (2295675).

#9482 is in, so I rebased onto main 504ea3b. The only conflict was the adjacent debt entry, and both entries are kept. Release: ctest -R '^guest_' 737/738; guest_runtime_lifecycle times out in its concurrent mode on this machine with main's Thread.cpp too.

AI-assisted (Claude Code); the runs above were made on my machine.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 26, 2026
@NevesHenry

Copy link
Copy Markdown
Contributor Author

@MarcZX10 your review was set aside only for the ASan option on guest_thread_stack_guard, which is in (6586e25 after the rebase). Since then the PR is rebased onto main 504ea3b after #9482, and has ahmadmuj's two fixes (9c1557e, 2295675), with the runs in the description and in the reply above. Could you take another look at head 2295675 when you have time?

AI-assisted (Claude Code).

@jotitadev jotitadev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up to my earlier ASan note, on head 229567502: with the add_asan_test_options line in, guest_thread_stack_guard passes under ASan. The guest-thread tests from @ahmadmuj's table pass with ASan's default options.

Machine: Linux x86_64, i7-8700, Ubuntu 26.04, glibc 2.43, GCC 15.2. Head 229567502 merged --no-commit onto main abed03546 (clean).

  • Release, full build: guest_thread_stack_guard 3/3. The 64 non-GPU pthread|thread|stack tests pass, patched_libraries too.
  • Control, Thread.cpp from main with the rest of the PR kept: guest_thread_stack_guard aborts 3/3.
  • ASan + UBSan (-DANYPS5_ENABLE_ASAN=ON, -fsanitize=undefined -fno-sanitize-recover=undefined, RelWithDebInfo): guest_thread_stack_guard, guest_kernel_is_stack, guest_kernel_stack_protect, guest_thread_self, guest_pthread_stack, guest_pthread_join_detach, guest_cond_timedwait and guest_pthread_join_self pass 8/8, three times, with no sanitizer report.
    • ctest runs guest_thread_stack_guard with detect_stack_use_after_return=0. Run by hand with detect_stack_use_after_return=1, it still aborts, so the option is still needed.
    • The three join/condvar tests run with ASan's default options, with no detect_stack_use_after_return override. They pass, so the __builtin_frame_address(0) change holds on this toolchain.

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

@jotitadev jotitadev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public_headers fails on this head, both on its own and merged onto current main (b7105f728). All 10 functions declared at the top of the new core/libs/tests/GuestThreadStackGuard.cpp are also declared in core/libs/prx/libkernel/include/Api.hpp:

scePthreadCreate: expected exactly one header declaration; found 2: core/libs/prx/libkernel/include/Api.hpp:559, core/libs/tests/GuestThreadStackGuard.cpp:8
... same for scePthreadJoin, scePthreadAttrInit, scePthreadAttrDestroy, scePthreadAttrGet,
    scePthreadAttrSetstacksize, scePthreadAttrGetstack, scePthreadSelf, sceKernelIsStack, mprotect_nid_postfix

#10706 (merged 2026-10-24) added this audit: every export must be declared exactly once, in its library's public header. CI did not catch it because the linux/windows jobs were skipped on this head.

Fix: include the libkernel header in place of SceTypes.hpp and drop the extern "C" block. The header's signatures match the ones in the test:

-#include "SceTypes.hpp"
+#include "prx/libkernel/include/Api.hpp"
 #include <cstddef>
 #include <cstdint>
 #include <cstdlib>
 #include <initializer_list>
 
-extern "C" {
-int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name);
-...
-int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protection) noexcept;
-}
-

With that change the audit passes for this PR. The only remaining error is the sceKernelGetModuleInfoForUnwind one that is already on main from #9263 (#11718 fixes it). The test passes a GCC 15.2 -fsyntax-only check with the test build flags.

Checked on Linux (i7-8700, Ubuntu 26.04, GCC 15.2): core/libs/tests/test_public_headers.py --audit-only on head 229567502, alone and merged onto main b7105f728.

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

The test declared ten libkernel exports itself, which the public header
audit from boykopovar#10706 rejects: each export must be declared once, in its
library's public header. Include prx/libkernel/include/Api.hpp instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NevesHenry

Copy link
Copy Markdown
Contributor Author

@jotitadev thanks, you were right. 2d1d417 does what you suggested: the test includes prx/libkernel/include/Api.hpp and the extern "C" block is gone.

test_public_headers.py --audit-only passes on the new head, and also merged onto main 737f172 (#9263's error is fixed on main now). On that merge, guest_thread_stack_guard, guest_pthread_stack, guest_kernel_is_stack and guest_pthread_join_detach pass 3/3 (Linux, GCC 13, Release).

AI-assisted (Claude Code).

@milescoviello

Copy link
Copy Markdown
Contributor

@NevesHenry a title that needs this: ProsperoCraft (PPSA99874 01.000.117), the catalog's Minecraft Java launcher. Its Java 25 HotSpot runtime mprotects guard pages at the bottom of each thread's stack, at addresses computed from the stack bounds the thread reports. Linux, RTX 4070 Ti, release libraries, pad input:

Build (on main 750cf6cf48) ProsperoCraft, 120 s
#12436 (answers sysctl vm.loadavg, which the JVM reads first) JVM aborts at start-up: Native memory allocation (mprotect) failed to protect 49152 bytes. Error detail: memory to guard stack pages (stackOverflow.cpp:113), in the primordial thread, whose stack is the host process's (0x00007ffe107c1000-0x00007ffe10fbf000)
+ #7532 (0c55cfcc6a) the primordial thread gets past it; the same abort moves to the Reference Handler thread, stack 0x2737203d0-0x2738203d0, which doesn't start on a page boundary
+ #7532 + #9684 (2d1d41779e), merged clean as 27e6cd782a runs the full 120 s: the launcher's Instances screen, the new-instance form and its on-screen keyboard, all following the input

So with #7532 for the main thread, this PR is what lets the JVM guard the stacks of the threads it creates.

AI-assisted: yes (Claude Code)

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review — changes requested

The earlier ASan configuration issue is resolved: the current PR includes detect_stack_use_after_return=0 for the stack-inspection test, the thread-start frame now comes from __builtin_frame_address(0), and the declaration-only test change uses the public libkernel header. The title report is also a useful direct before/after: ProsperoCraft advances from the primordial-thread failure to the created-thread failure with #7532, then completes its 120-second flow with this PR.

I checked exact head 2d1d41779e22266275f103085a77b6a055e1c1a9 against current main f7e84c4cf1609cc10bac95a44cd431805e5ebc8a:

  • Current-main merge preview is clean.
  • tools/check_conventions.py and git diff --check pass.
  • tools/check_main_sync.py reports 3 changed paths on both sides since merge base 504ea3b53: Thread.cpp, libkernel CMakeLists.txt, and docs/dev/debt/libkernel.md. Main's Thread.cpp changes include thread creation/cancellation and scheduling behavior, so the earlier test runs on main 504ea3b53 or 737f17291a do not cover the current combined thread implementation.
  • The reported Linux title run, ASan/UBSan run, and 50-repeat test are author-reported; I did not run them locally. Hosted product Linux is skipped.

Please merge current main, preserve the 16 KiB stack-bound behavior alongside the newer thread lifecycle changes, and rerun guest_thread_stack_guard plus the focused stack/thread tests and ASan/UBSan check on that integrated head. The earlier code findings appear addressed; this request is for current-main integration and exact-head validation.

@NevesHenry

Copy link
Copy Markdown
Contributor Author

@milescoviello thanks for running ProsperoCraft. The before/after with #7532 is in the description now, linked to your comment, and PPSA99874 is under Unblocks.

@MarcZX10 I merged main 5454cd2 (790fe65, no conflicts; the tree is the same as a rebase onto it). main's Thread.cpp changes since 504ea3b don't touch the stack bounds, so the 16 KiB alignment is unchanged. On that head (Linux, GCC 13):

  • Release: guest_thread_stack_guard passes 50/50, and the 150 thread, stack, condition, semaphore, cancellation, TLS and kernel_sys tests pass.
  • ASan + UBSan, Debug, UBSAN_OPTIONS=halt_on_error=1: guest_thread_stack_guard, guest_kernel_is_stack, guest_kernel_stack_protect, guest_thread_self, guest_pthread_stack, guest_pthread_join_detach, guest_cond_timedwait and guest_pthread_join_self pass 20/20 with no sanitizer report. 4 of the 150 fail (three LeakSanitizer leaks and a UBSan misaligned load at libSceLibcInternal/Export.cpp:87), with the same reports when Thread.cpp comes from main.
  • With Thread.cpp from main, guest_thread_stack_guard aborts in both builds.
  • The public header audit, check_conventions.py and git diff --check pass. check_main_sync.py needs git 2.38, so I didn't run it here.

The description has the test filter and the rest of the details.

AI-assisted (Claude Code); the runs above were made on my machine.

@XairRrR XairRrR left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — approved

On the current head, merged onto current main, the 16 KiB stack start works with the newer thread lifecycle code: the focused stack and thread tests pass in Release and in ASan + UBSan, and guest_thread_stack_guard fails when Thread.cpp comes from main.

What I ran

  • Head: 790fe650fb37edeb3a89ef25adde463110e01050
  • main: 5454cd2e455994d992aca39335ce60507eeda1eb. This is also the merge base, so the merged tree is identical to the head tree.
  • Arch Linux 7.2.11-zen, GCC 16.2.1, Intel Core i9-14900KF, NVIDIA GeForce RTX 4070 Ti SUPER (driver 615.78.08). I worked in a clean worktree with network access disabled.
git fetch -q origin main pull/9684/head:refs/review/9684
git checkout -q --detach origin/main
git merge -q --no-ff --no-edit refs/review/9684
git diff --check origin/main...refs/review/9684
python3 tools/check_conventions.py --base origin/main

# Release (Ninja, BUILD_TESTING=ON)
nice -n 10 cmake --build build -j10 --target libs guest_thread_stack_guard_tests guest_kernel_is_stack_tests \
  guest_kernel_stack_protect_tests guest_thread_self_tests guest_pthread_stack_tests guest_pthread_join_detach_tests \
  guest_cond_timedwait_tests guest_pthread_join_self_tests guest_pthread_attr_stack_tests guest_pthread_start_tests \
  guest_pthread_frame_chain_tests
nice -n 10 ctest --test-dir build -j4 --output-on-failure -R '^(guest_thread_stack_guard|guest_kernel_is_stack|guest_kernel_stack_protect|guest_thread_self|guest_pthread_stack|guest_pthread_join_detach|guest_cond_timedwait|guest_pthread_join_self|guest_pthread_attr_stack|guest_pthread_start|guest_pthread_frame_chain)$'
nice -n 10 ctest --test-dir build --repeat until-fail:50 -R '^guest_thread_stack_guard$'
for s in 1024 8192 65536 unlimited; do (ulimit -s $s && build/tests/guest_thread_stack_guard_tests); done
ctest --test-dir build --output-on-failure -R '^public_headers$'

# ASan + UBSan, separate build directory
cmake -S . -B build-asan -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_TESTING=ON -DANYPS5_ENABLE_ASAN=ON \
  "-DCMAKE_CXX_FLAGS=-fsanitize=undefined -fno-sanitize-recover=undefined" \
  "-DCMAKE_C_FLAGS=-fsanitize=undefined -fno-sanitize-recover=undefined"
nice -n 10 cmake --build build-asan -j10 --target <the 8 test targets below>
UBSAN_OPTIONS=halt_on_error=1 nice -n 10 ctest --test-dir build-asan -j4 --output-on-failure \
  -R '^(guest_thread_stack_guard|guest_kernel_is_stack|guest_kernel_stack_protect|guest_thread_self|guest_pthread_stack|guest_pthread_join_detach|guest_cond_timedwait|guest_pthread_join_self)$'
for t in guest_pthread_join_detach guest_cond_timedwait guest_pthread_join_self; do
  ASAN_OPTIONS=detect_stack_use_after_return=1 UBSAN_OPTIONS=halt_on_error=1 build-asan/tests/${t}_tests
done

# Negative control
git checkout origin/main -- core/libs/prx/libkernel/Pthread/src/Thread.cpp
nice -n 10 cmake --build build -j10 --target libs guest_thread_stack_guard_tests
nice -n 10 ctest --test-dir build --output-on-failure -R '^guest_thread_stack_guard$'
git checkout HEAD -- core/libs/prx/libkernel/Pthread/src/Thread.cpp   # rebuilt, test passes again

Results

Check Result
Merge onto main Clean. The head already merges current main.
git diff --check Clean
tools/check_conventions.py --base origin/main All conventions checks passed.
Release build (libs + 11 test targets) OK, no warnings
Release: 11 focused stack/thread tests 11/11 pass, longest 0.40 s
guest_thread_stack_guard repeated 50/50 pass
guest_thread_stack_guard with ulimit -s 1024, 8192, 65536, unlimited Exit code 0 for each
public_headers Pass
ASan + UBSan: 8 focused tests 8/8 pass, no AddressSanitizer or runtime error output
ASan + UBSan: join/detach, cond_timedwait, join_self with detect_stack_use_after_return=1 All exit 0, no sanitizer report
Negative control (Thread.cpp from main) guest_thread_stack_guard aborts
Release focused tests
 1/11 Test #2466: guest_pthread_stack ..............   Passed    0.04 sec
 2/11 Test #2517: guest_pthread_join_self ..........   Passed    0.00 sec
 3/11 Test #2451: guest_thread_self ................   Passed    0.00 sec
 4/11 Test #2458: guest_pthread_attr_stack .........   Passed    0.00 sec
 5/11 Test #2459: guest_pthread_frame_chain ........   Passed    0.00 sec
 6/11 Test #2446: guest_kernel_is_stack ............   Passed    0.00 sec
 7/11 Test #2464: guest_pthread_join_detach ........   Passed    0.05 sec
 8/11 Test #2467: guest_thread_stack_guard .........   Passed    0.00 sec
 9/11 Test #2449: guest_kernel_stack_protect .......   Passed    0.01 sec
10/11 Test #2468: guest_cond_timedwait .............   Passed    0.07 sec
11/11 Test #2463: guest_pthread_start ..............   Passed    0.40 sec

100% tests passed out of 11
ASan + UBSan focused tests
1/8 Test #2446: guest_kernel_is_stack ............   Passed    0.02 sec
2/8 Test #2451: guest_thread_self ................   Passed    0.02 sec
3/8 Test #2467: guest_thread_stack_guard .........   Passed    0.02 sec
4/8 Test #2464: guest_pthread_join_detach ........   Passed    0.08 sec
5/8 Test #2449: guest_kernel_stack_protect .......   Passed    0.08 sec
6/8 Test #2466: guest_pthread_stack ..............   Passed    0.06 sec
7/8 Test #2517: guest_pthread_join_self ..........   Passed    0.03 sec
8/8 Test #2468: guest_cond_timedwait .............   Passed    0.75 sec
100% tests passed out of 8

ctest runs guest_thread_stack_guard with ASAN_OPTIONS=string_append::detect_stack_use_after_return=0. The three join/condvar tests have no such override, and they also pass when run directly with detect_stack_use_after_return=1.

Negative control
1/1 Test #2467: guest_thread_stack_guard .........Subprocess aborted***Exception:   0.05 sec
0% tests passed, 1 tests failed out of 1
	2467 - guest_thread_stack_guard (Subprocess aborted)

Negative control

I restored only core/libs/prx/libkernel/Pthread/src/Thread.cpp from main and kept the new test and the CMake entry. After a rebuild, guest_thread_stack_guard aborted. With the merged Thread.cpp restored and rebuilt, it passes again.

Findings

Blocking

None.

MarcZX10's last review asked for the stack/thread tests and ASan/UBSan to be run again on a head that merges current main. The results above are from such a head (790fe650fb, which merges main 5454cd2e45), and all of those tests pass. They also confirm ahmadmuj's __builtin_frame_address(0) fix and jotitadev's ASan option on this head.

FreeBSD's libthr supports the created-thread layout. _thr_stack_alloc places a page-aligned stack directly above its guard and keeps the requested stacksize_attr, which leaves "unused space above the beginning of the stack" when the size is not a page multiple (thr_stack.c, release/11.4.0). Thread.cpp:930 does the same: it aligns the start down and keeps stackSize. The alloca at Thread.cpp:952-954 then moves the guest frames below the reported end. nativeStack includes the 1 MiB HOST_STACK_MARGIN, so the lower start (at most 16 KiB − 1 lower) stays inside the mapping. This matches ahmadmuj's earlier reading.

Non-blocking

  1. Windows created threads can still start off a 16 KiB boundary, and the debt file doesn't say so. Thread.cpp:895 still sets stackAddress = high - stackSize. scePthreadAttrSetstacksize accepts any size of at least the minimum (Attr.cpp:72-80). So on Windows, a size that is not a multiple of 16 KiB gives the same unaligned start this PR fixes on Linux, and HotSpot's aligned-down guard protect would fail there too. The description says the Windows path is unchanged, which is fine for this PR's scope. CONTRIBUTING.md asks for known gaps to be recorded in technical debt, so consider adding a line for this to docs/dev/debt/libkernel.md, or a follow-up that aligns the Windows start the same way.
  2. The rounding is not limited to Linux, but the wording is. SetStackFromHost (Thread.cpp:331-339) has no platform guard, and the created-thread change sits in the #else (non-Windows) StartNativeThread, so macOS gets both changes too. The PR title and the debt entry at docs/dev/debt/libkernel.md:197 say "on Linux". Either say "outside Windows" or note that macOS was not tested. This is wording only. The code is correct on any host whose page size divides 16 KiB.

Not verified

  • Windows and macOS builds. I built and ran on Linux only.
  • The Java 25 titles (PPSA99866, PPSA99867, PPSA99873) and ProsperoCraft. Per the review rules I launched no titles, so the title results are the author's and milescoviello's reports.
  • The full build and full test suite. I built libs and the focused test targets and ran the 11 tests listed above plus public_headers. I did not run patched_libraries or the rest of the guest_ suite.

@jotitadev jotitadev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up on the public_headers failure from my last review: fixed on 790fe650f.

Linux, Ubuntu 26.04, i7-8700, GCC 15.2, Release, full build. Head merged --no-commit onto main f91137142e (clean).

  • public_headers and guest_thread_stack_guard pass, 3/3 each. The 102 non-GPU pthread|thread|stack tests pass.
  • Controls: the previous head's GuestThreadStackGuard.cpp (229567502) makes the audit fail again (scePthreadCreate, scePthreadJoin, scePthreadSelf, ... found 2). Thread.cpp from main with the rest of the PR kept makes guest_thread_stack_guard abort, 3/3.

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

docs/dev/debt/libkernel.md: main split the sceKernelInstallExceptionHandler
entry into the host-fault delivery entries (boykopovar#7192); kept those with this
branch's Linux 16 KiB boundary wording of the stack entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Nov 4, 2026
@NevesHenry

Copy link
Copy Markdown
Contributor Author

@MarcZX10 about your change request: after your review I merged main d63781e into the branch as 8989d4f. That main already had the thread creation/cancellation changes in Thread.cpp. The merge kept main's thread lifecycle code and the 16 KiB stack start. Since then main has not touched Thread.cpp.

I reran the tests today on 8989d4f merged onto current main e8b1bd3. The merge was clean, together with my other open PRs #11571, #15821, #16243 and #16270. Linux, GCC 13:

  • Release: the focused stack/thread set passes, 177 tests (guest thread, pthread, stack, cond, sem, sleep, tls). guest_thread_stack_guard passes 20/20.

  • ASan + UBSan (-DANYPS5_ENABLE_ASAN=ON, -fsanitize=undefined -fno-sanitize=vptr, halt_on_error=1): guest_thread_stack_guard passes 20/20 with no sanitizer report. 174 of the 177 focused tests pass. The other 3 fail the same way on plain main e8b1bd3 in the same configuration:

    • guest_cnd_at_thread_exit: alloc-dealloc-mismatch between _Mtx_init and _Mtx_destroy in libc/src/Threads.cpp.
    • guest_main_stack and guest_main_stack_adopted: fail at line 68.

    None of the three is in sanitizer-known-failures.txt.

Could you take another look?

AI-assisted (Claude Code); the runs above were made on my machine.

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

build docs libkernel unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants