Skip to content

fix(libkernel): validate and roll back DirectMemory pool operations - #3469

Closed
yash27git wants to merge 37 commits into
boykopovar:mainfrom
yash27git:Direct-Memory-Exception
Closed

yash27git wants to merge 37 commits into
boykopovar:mainfrom
yash27git:Direct-Memory-Exception

Conversation

@yash27git

@yash27git yash27git commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

What

Hardens PhysicalMemoryPool in core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp:

  • Validates every guest-controlled argument (null pointers, zero or overflowing lengths, negative or unaligned starts, non power-of-two alignment) with overflow-safe arithmetic, so a bad start/len can no longer reach _used[].
  • Rounds lengths up to whole pages and raises alignment to at least a page. Search ends past the pool are clamped; a negative end means unbounded, as Alloc effectively did before.
  • Makes Alloc, Free/CheckedFree and Retype roll back the pool's own metadata (_blocks and _used[]) when node allocation or backing creation throws. Blocks are split in place (_splitAt/_unsplitAt plus a range erase) and split first, so every step that can throw runs before any state is committed.
  • Restores DirectMemoryLargestFree, now validated.
  • Keeps partial and multi-allocation Free; CheckedFree still requires every page in range to be allocated; Retype changes allocated pages and skips gaps.
  • Error rule: only DirectMemoryAlloc catches std::bad_alloc (to SCE_KERNEL_ERROR_ENOMEM). Every other entry point lets exceptions propagate. A mismatch between _blocks and _used[] throws std::logic_error instead of returning a plausible error code.
  • Adds MemoryPoolTest, registered in ctest, and updates the TechnicalDebt.md entry for MemoryPool.

Why

  • Bounds and overflow: Free, Retype and the page-marking helpers did no range checks, and cur + len, start + align - 1 and start + len could overflow, so guest input could write past _used[] or create overlapping allocations.
  • Consistency: Alloc, Free and Retype could throw part-way (bad_alloc, backing cleanup) and leave _used[] and _blocks out of sync.
  • Visibility: several entry points turned unexpected exceptions into false, 0 or EFAULT, which hides emulator invariant failures.

Behaviour changes

Area Before After
Invalid args (zero length, bad alignment, negative start, null pointer) UB or silent success EINVAL / EFAULT
Unaligned start in Free / CheckedFree / Retype truncating page math rejected
Free on a range with nothing allocated still called ForgetDirectMemory no-op
Exceptions Free/Retype propagated, others swallowed all propagate except bad_alloc in Alloc (to ENOMEM)
LargestFree with negative searchEnd returned false treated as unbounded

Known limitation

The rollback covers the pool's own metadata only. ForgetDirectMemory erases g_physPages before ErasePhysMappings, and ErasePhysMappings can throw while inserting the surviving pieces of a partially overlapped mapping. In that case the pool restores its splits, but the already-erased physical pages and mapping nodes are not restored, so a retry may fail ValidatePhysicalRange. Making those updates transactional is tracked in TechnicalDebt.md and is not part of this PR.

Tested

New tests (MemoryPoolTest): 14 cases covering argument validation, rounding/alignment/clamping, partial and spanning Free, Retype splitting, and rollback of _blocks/_used[] after injected bad_alloc and backing/forget failures. Run per case against the previous MemoryPool.cpp, 9 fail or crash; all 14 pass after the change (ASan + UBSan, C++11 and C++17, mock ForgetDirectMemory and CreateDirectMemoryBacking).

Environment: Windows 11 Home (26100), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release.

Full suite: 550 of 554 tests passed, 1 skipped.

  • agc_driver_prepared_shaders_debug_failure fails on clean main on this machine.
  • guest_module_identity failed once but passed when rerun alone.
  • Run via generated CTestTestfile.cmake commands because local antivirus blocks ctest.exe.

tools/check_conventions.py: Passed with 0 warnings.

Checklist

  • Based on current main (rebased, no merge commits); 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
  • 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
  • All guest memory / direct memory unit tests compile and pass cleanly
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

Yash Singh added 18 commits October 11, 2026 15:24
Registers the retained explanatory comments documenting manual rollback blocks, arithmetic overflow pre-validation, and the _allocations ledger in MemoryPool.cpp to satisfy check_conventions.py requirements.
Guest-controlled offsets and lengths reached the page bitmap and block
map without validation, and several mutating paths could leave the two
structures inconsistent if an allocation or backing cleanup failed.

Validation
- Reject null out-pointers, zero lengths, negative starts, unaligned
  Free/Retype starts, and non power-of-two alignments.
- Round lengths up to whole pages and raise alignment to at least a page.
- Make all range arithmetic overflow-safe.
- Clamp search ends to the pool size; a negative end means unbounded,
  matching previous Alloc behaviour.
- Share one set of normalisation helpers across every entry point.

Consistency
- Alloc: insert block, create backing, then mark pages; undo on failure.
- Free/Retype: split blocks first, roll splits back if a later step
  throws, then apply non-throwing updates. Pool is unchanged on throw.
- Free keeps partial and multi-allocation release semantics;
  CheckedFree still requires every page in range to be allocated.
- Retype retypes allocated pages and skips gaps.

Other
- Restore DirectMemoryLargestFree, now validated.
- Alloc skips past the last used page on collision instead of stepping
  one alignment unit at a time.
- Move the pool into an anonymous namespace; add static_asserts for
  page and pool size.
- Document the exception policy: Free and Retype propagate (void API),
  all other exports report failure via return value.
DirectMemoryFree and DirectMemoryRetype still propagate exceptions on
purpose, but the pool is now left unchanged when they throw. Record the
strong guarantee and the remaining gap (uncaught exceptions at the
C/syscall boundary).
Guest-controlled offsets and lengths reached the page bitmap and block
map without validation, and several mutating paths could leave the two
structures inconsistent if an allocation or backing cleanup failed.

Validation
- Reject null out-pointers, zero lengths, negative starts, unaligned
  Free/Retype starts, and non power-of-two alignments.
- Round lengths up to whole pages and raise alignment to at least a page.
- Make all range arithmetic overflow-safe.
- Clamp search ends to the pool size; a negative end means unbounded,
  matching previous Alloc behaviour.
- Share one set of normalisation helpers across every entry point.

Consistency
- Alloc: insert block, create backing, then mark pages; undo on failure.
- Free/Retype: split blocks first, roll splits back if a later step
  throws, then apply non-throwing updates. Pool is unchanged on throw.
- Free keeps partial and multi-allocation release semantics;
  CheckedFree still requires every page in range to be allocated.
- Retype retypes allocated pages and skips gaps.

Other
- Restore DirectMemoryLargestFree, now validated.
- Alloc skips past the last used page on collision instead of stepping
  one alignment unit at a time.
- Move the pool into an anonymous namespace; add static_asserts for
  page and pool size.
- Document the exception policy: Free and Retype propagate (void API),
  all other exports report failure via return value.
Guest-controlled offsets and lengths reached the page bitmap and block
map unchecked, and Alloc/Free/Retype could leave the two out of sync
when node allocation or backing cleanup failed.

- Validate pointers, lengths, alignment and ranges; make all range
  arithmetic overflow-safe. Search ends past the pool are clamped, and a
  negative end means unbounded, as before.
- Alloc: insert block, create backing, then mark pages; undo on failure.
- Free/Retype: split blocks first and roll the splits back if a later
  step throws, so the pool is unchanged on throw.
- Keep partial and multi-allocation Free; CheckedFree still requires
  every page in range to be allocated.
- Retype changes allocated pages and skips gaps.
- Restore DirectMemoryLargestFree, now validated.
- Only DirectMemoryAlloc catches std::bad_alloc (-> ENOMEM); all other
  exceptions propagate. A block-map/page-flag mismatch throws
  std::logic_error.
Add MemoryPoolTest with cases for argument validation, rounding,
alignment and clamping, partial and spanning Free, Retype splitting,
and rollback after injected bad_alloc and backing/forget failures.
Register each case with ctest.
docs(techdebt): update MemoryPool exception propagation entry

Only DirectMemoryAlloc translates bad_alloc; all other entry points
propagate, and mutating paths roll back before throwing.
Only DirectMemoryAlloc translates std::bad_alloc (to ENOMEM); all other
entry points, including the void DirectMemoryFree and DirectMemoryRetype,
let exceptions propagate. Mutating paths roll back before throwing, so
the pool is unchanged. Record that nothing catches at the guest boundary
yet.

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the work. Blocking points:

  • core/libs/tests/MemoryPoolTest.cpp is not registered anywhere: no add_test_executable/add_test in core/libs/CMakeLists.txt or core/libs/prx/libkernel/CMakeLists.txt mentions it, so it never runs locally or in CI (the PR body says "registered in ctest"). Please register it with an explicit add_test and make sure it fails against the old MemoryPool.cpp through ctest. Note it defines CreateDirectMemoryBacking, ForgetDirectMemory and the global operator new, so it must compile MemoryPool.cpp directly rather than link libkernel.
  • The ### Tested section still has template placeholders (<passed / total, skipped; ...>, <result> for tools/check_conventions.py). Fill them with the real results on the final head.
  • docs/dev/TechnicalDebt.md:591: the new line is the last of the "Functional" section, where every PR appends and conflicts. Put it a few lines away from the end.
  • Behaviour changes need a source (FreeBSD sys/kern/man pages, KytyPS5, shadPS4): DirectMemoryFree/DirectMemoryRetype now silently ignore invalid ranges (MemoryPool.cpp:306,321) instead of failing visibly, and a negative searchEnd now means unbounded in LargestFree. Say which reference each follows, or record it under "Unknown function info" in TechnicalDebt.

Happy to re-review once these are addressed.

Yash Singh added 2 commits October 11, 2026 17:13
Moves the existing functional inline comment registration to prevent merge conflicts at the end of the block. Documents the silent invalid range handling in DirectMemoryFree/Retype and the unbounded negative searchEnd in LargestFree under the unknown function info section.

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the update. The points from the previous review are still open on the current head (the new commits are merges from main plus a docs change):

  • core/libs/tests/MemoryPoolTest.cpp is still not registered: no add_test_executable/add_test mentions it in core/libs/CMakeLists.txt or core/libs/prx/libkernel/CMakeLists.txt, so it never runs locally or in CI. It defines CreateDirectMemoryBacking, ForgetDirectMemory and the global operator new, so compile MemoryPool.cpp directly into its own target instead of linking libkernel, and check through ctest that it fails against the old MemoryPool.cpp.
  • ### Tested still contains the template placeholders (<passed / total, skipped; ...>, <list any other failures...>, <result> for tools/check_conventions.py). Fill them with the real results on the final head.
  • docs/dev/TechnicalDebt.md:597-598: the entry is still the last line of "Functional" (everyone appends there, so it conflicts), and the last commit added a second line starting with - - [MemoryPool] that is malformed and repeats the first. Keep one line, a few lines away from the end of the section.
  • Behaviour changes need a source: DirectMemoryFree/DirectMemoryRetype now silently ignore invalid ranges, and a negative searchEnd in LargestFree means unbounded. Say which reference each follows (FreeBSD sys/vm, KytyPS5, shadPS4) or record it under "Unknown function info" in docs/dev/TechnicalDebt.md.

Happy to re-review once these are addressed.

Yash Singh added 3 commits October 11, 2026 17:23
Compiles the test directly against MemoryPool.cpp to ensure custom mocked functions are utilized, and registers it to run locally and in CI via ctest.

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The current head breaks CMake configure for the whole tree, so nothing can be built or tested:

  • core/libs/CMakeLists.txt:332-339: the block you added ends with an extra endif(), and cmake stops with Flow control statements are not properly nested (line 339). Drop the extra endif() and put the block inside the existing if(BUILD_TESTING), e.g. next to the other add_test_executable calls (indented like them, no blank line before endif()). Then check with ctest -N that MemoryPoolTest is listed and with ctest -R MemoryPoolTest that it fails against the old MemoryPool.cpp.
  • ### Tested still has the template placeholders (<passed / total, skipped; ...>, <list any other failures ...>, <result>). Fill them with the real results on the final head.
  • docs/dev/TechnicalDebt.md:600-601: there are two lines, the second starting with - - [MemoryPool] (malformed). Keep one, and not as the last line of "Functional" (everyone appends there and it conflicts).
  • Behaviour changes need a source: DirectMemoryFree/DirectMemoryRetype now ignore invalid ranges, and a negative searchEnd in LargestFree means unbounded. Say which reference each follows (FreeBSD sys/vm, KytyPS5, shadPS4) or record it under "Unknown function info".
  • The conventions job is red; run python3 tools/check_conventions.py and fix what belongs to your commits.

Happy to re-review once the build configures again.

@yash27git
yash27git requested a review from adriadam10 October 11, 2026 12:02
Removes a stray markdown list character and updates the unknown function info to accurately reflect that invalid ranges are now rejected, leaving only the unbounded negative searchEnd documentation.
@yash27git
yash27git requested a review from adriadam10 October 11, 2026 13:00
@github-actions

github-actions Bot commented Oct 11, 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
#3496 docs/dev/TechnicalDebt.md L599
#3650 docs/dev/TechnicalDebt.md L599
#3689 docs/dev/TechnicalDebt.md L599
#3694 docs/dev/TechnicalDebt.md L599
#3700 docs/dev/TechnicalDebt.md L599
#3718 docs/dev/TechnicalDebt.md L599
#3720 docs/dev/TechnicalDebt.md L599
#3730 docs/dev/TechnicalDebt.md L599
#3737 docs/dev/TechnicalDebt.md L599
#3740 core/libs/CMakeLists.txt L331
#3773 docs/dev/TechnicalDebt.md L593
#3781 docs/dev/TechnicalDebt.md L599
#3789 docs/dev/TechnicalDebt.md L599
#3791 docs/dev/TechnicalDebt.md L599
#3799 docs/dev/TechnicalDebt.md L593
#3800 docs/dev/TechnicalDebt.md L599
#3814 docs/dev/TechnicalDebt.md L593
#3819 docs/dev/TechnicalDebt.md L593
#3823 docs/dev/TechnicalDebt.md L599
#3829 docs/dev/TechnicalDebt.md L599

@yash27git yash27git closed this Oct 11, 2026
@yash27git
yash27git deleted the Direct-Memory-Exception branch October 12, 2026 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants