From 5543c73b370ed6045c38eff8eb34ba99f85e1efd Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 11 Oct 2026 13:56:11 +1300 Subject: [PATCH] fix(libkernel): reserve the AMM virtual address ranges at the top of the guest arena The AMM ranges were reserved address-less on first use, so they took the lowest free address of the arena, directly after the title's last mapping, where the title's next hinted maps go. They are now reserved at a fixed place, the last 64 GiB of the guest arena, which maps without MAP_FIXED and the hints derived from them do not reach; a reservation that cannot be made there throws. Fixes #3146 --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 9 +++++-- core/libs/tests/GuestAmpr.cpp | 34 +++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 1417f6fd4..279debe50 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -245,6 +245,7 @@ TCommand _read(const Apr::CommandBufferObject& buffer, std::uint32_t cursor) { constexpr std::uint64_t AmmRangeBytes = 32ull << 30; constexpr int GuestMapFixed = 0x10; +constexpr int GuestMapNoOverwrite = 0x80; struct AmmPage { std::uint64_t physical; @@ -270,8 +271,12 @@ AmmState& _amm() { std::uintptr_t _ammBase(AmmState& state) { if (state.base == 0) { - void* address = nullptr; - if (DoReserveVirtual(&address, 2 * AmmRangeBytes, 0, 0x200000) != 0) throw std::runtime_error("AMM: cannot reserve the virtual address range"); + std::uintptr_t arenaBase = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); + if (arenaBase == 0 || arenaBytes < 2 * AmmRangeBytes) throw std::runtime_error("AMM: the guest address space arena cannot hold the virtual address range"); + void* address = reinterpret_cast(arenaBase + arenaBytes - 2 * AmmRangeBytes); + if (DoReserveVirtual(&address, 2 * AmmRangeBytes, GuestMapFixed | GuestMapNoOverwrite, 0) != 0) throw std::runtime_error("AMM: cannot reserve the virtual address range"); state.base = reinterpret_cast(address); } return state.base; diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 7f45f4ff3..23bdcee6b 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" #include #include @@ -93,6 +94,9 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeMapBegin(std::uint64_t, std::ui std::uint64_t APS5_VABI sceAmprMeasureCommandSizeMapDirectBegin(std::uint64_t, std::uint64_t, std::uint64_t, std::uint32_t, std::uint32_t); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeMapEnd(); int APS5_VABI sceKernelQueryMemoryProtection(void*, void**, void**, int*); +int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelMunmap(void*, std::size_t); int APS5_VABI sceAmprAmmCommandBufferMapAsPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t); int APS5_VABI sceAmprAmmCommandBufferAllocatePaForPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t, std::int32_t, std::int32_t); int APS5_VABI sceAmprAmmCommandBufferRemapIntoPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t, std::uint64_t, std::int32_t, std::uint32_t); @@ -637,6 +641,35 @@ volatile std::uint64_t& At(std::uint64_t address) { return *reinterpret_cast(address); } +void TestAmmRangesAboveHintedMaps() { + constexpr std::uint64_t step = 0x400000; + void* span = nullptr; + Require(sceKernelReserveVirtualRange(&span, 4 * step, 0, 0) == 0); + Require(sceKernelMunmap(span, 4 * step) == 0); + const auto heap = reinterpret_cast(span); + void* placed = span; + Require(sceKernelMapFlexibleMemory(&placed, page, 3, 0) == 0 && placed == span); + + std::uint64_t start = 0; + std::uint64_t end = 0; + std::uint64_t multimapStart = 0; + std::uint64_t multimapEnd = 0; + Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0); + std::uintptr_t arenaBase = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); + Require(multimapEnd == arenaBase + arenaBytes); + + for (std::uint64_t hint = heap + step; hint < heap + 4 * step; hint += step) { + void* mapped = reinterpret_cast(hint); + Require(sceKernelMapFlexibleMemory(&mapped, page, 3, 0) == 0); + Require(reinterpret_cast(mapped) == hint); + Require(hint + page <= start || hint >= multimapEnd); + Require(sceKernelMunmap(mapped, page) == 0); + } + Require(sceKernelMunmap(span, page) == 0); +} + void TestAmm() { std::uint64_t start = 0; std::uint64_t end = 0; @@ -989,6 +1022,7 @@ int main() { TestConstructed(); TestZeroFilledBuffer(); TestGatherScatter(); + TestAmmRangesAboveHintedMaps(); TestAmm(); TestAmmRemapAndProtect(); TestAmmPrt(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 74a13e50a..fac18a731 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -94,7 +94,7 @@ Throughout the project, every function at every stage either **does exactly what - [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [AMPR counter access](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and checks of `WaitOnCounter_04_00`, `WriteCounter_04_00`, `ConstructNop` and `ConstructMarker` taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands counter commands to the console and does not show their effect. The counters are taken as 32 bits each: the 8-byte access of counter n spans n (low) and n + 1 (high), the 2- and 1-byte accesses select a half or a byte of the counter by its offset, waits compare at the access width (signed and wrapped compares included), the AND mask applies to both sides and the atomic writes wrap within the access. The `ConstructNop` type is not recorded - [APR gather and scatter reads](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, argument checks and the read cursor taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: each read leaves the file, the next file offset and the next destination; a gather reads that file at a new offset into the next destination, a scatter continues in the file into a new destination, a gather-scatter takes both. Recording one needs a read file since the last `ResetGatherScatterState`; like ampr_emu, `sceAmprCommandBufferReset` keeps that state, and a gather or scatter with no read before it in the submitted buffer throws instead of returning `SCE_AMPR_ERROR_APR_INVALIDGATHERSCATTERSTATE` -- [AMM map, map direct and unmap](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection mask (CPU, GPU, AMPR and ACP read and write) taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands the remaining checks and the mapping to the console kernel; the mapper model taken from [PS5PCEM](https://github.com/iStark/PS5PCEM): `sceAmprAmmGiveDirectMemory` usage 1 gives the allocated direct memory to the pool plain maps take pages from, usage 0 only allocates it. Not confirmed on a title. The AMM virtual address ranges are a 32 GiB range and a 32 GiB multimap range reserved in the guest arena on first use, not the console's; maps outside them throw. Addresses, sizes and direct offsets must be 16 KiB aligned. The memory type and the GPU mask are ignored, AMPR and ACP access maps as CPU access, unmapped pages go back to the pool and stay reserved. Submission runs the buffer before returning, the submit result is always 0 and a failing command throws; the AMM measures return errors sign-extended, as ampr_emu does. `sceAmprAmmGetVirtualAddressRanges` throws for a null output +- [AMM map, map direct and unmap](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection mask (CPU, GPU, AMPR and ACP read and write) taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands the remaining checks and the mapping to the console kernel; the mapper model taken from [PS5PCEM](https://github.com/iStark/PS5PCEM): `sceAmprAmmGiveDirectMemory` usage 1 gives the allocated direct memory to the pool plain maps take pages from, usage 0 only allocates it. Not confirmed on a title. The AMM virtual address ranges are a 32 GiB range and a 32 GiB multimap range reserved on first use at the top of the guest arena, above the addresses that maps without `MAP_FIXED` are given lowest first and that titles derive their next hints from; the console's addresses are not known (ampr_emu takes them from `sceKernelMapperGetParam`), and a reservation that cannot be made there throws. Maps outside them throw. Addresses, sizes and direct offsets must be 16 KiB aligned. The memory type and the GPU mask are ignored, AMPR and ACP access maps as CPU access, unmapped pages go back to the pool and stay reserved. Submission runs the buffer before returning, the submit result is always 0 and a failing command throws; the AMM measures return errors sign-extended, as ampr_emu does. `sceAmprAmmGetVirtualAddressRanges` throws for a null output - [AMM remap, multimap and protection changes](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection and mask checks taken from ampr_emu; the effects are inferred from the names, not confirmed on a title. A remap moves every page of a fully mapped range to the new address with the given protection and leaves the old range reserved; a multimap maps the same pages at a second address, anywhere in the AMM ranges, and a pool page goes back to the pool once its last mapping is unmapped; a multimap onto its own source range throws. A protection change sets the masked bits on every page of a fully mapped range; the memory type is recorded and ignored. A command on a range that is not fully mapped throws - [sceVideodec2GetAvcPictureInfo](../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - runs the `sceVideodec2GetPictureInfo` body (KytyPS5 binds both NIDs to one function; SharpProspero gives both the same `outputInfo, first, second` signature); the second (bottom field) picture info is never written, since only progressive H.264 output is modelled. prosper calls the AVC layout unestablished (no title observed calling it) - [AMM PRT ranges](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, checks and their order taken from ampr_emu, where `MapAsPrt` is a map with the PRT flag and protection 0 and `AllocatePaForPrt` a memory type and protection change with mask 1019, both checking the buffer memory after its size; the effects are inferred from the names, not confirmed on a title. Unbacked PRT pages are zero-filled pages with GPU read access, so CPU reads see zeros instead of faulting and GPU writes fault instead of being dropped. `AllocatePaForPrt` backs each unbacked page with a pool page and sets the type and protection of backed ones; `RemapIntoPrt` moves a fully mapped range into a PRT range, its opcode argument (1011 when 0, as in ampr_emu) is recorded and ignored; `UnmapToPrt` returns the pages to the pool and zero-fills them again. A plain map, unmap or remap over a PRT range ends it; `AllocatePaForPrt`, `RemapIntoPrt` or `UnmapToPrt` outside a PRT range throws