A zero, unaligned, overflowing or out-of-range length reached the physical pool, where a sub-page length silently freed a whole page and fragmented the block map, and an out-of-range length walked the page table past the backing store. Validate alignment and bounds up front and return EINVAL instead.
Importing guest memory collected the write watch over the whole import,
which write-protected every page of it, so pages nothing had collected yet
took a write fault on their first CPU write. The import now collects only
the parts a collect already protects (GuestWriteWatchCollectArmed). The
rest stays unprotected until something collects it, which reports it as
written; a GPU write recorded into such memory (MarkWritten) first collects
the never-collected parts of its 64 KiB blocks
(GuestWriteWatchCollectFresh), so that report is older than the GPU's
results and does not read as a CPU write over them.
Those pages are stamped as written and their CPU pages are noted, but the
block's CPU stamp is left alone, so a GPU label record or a pending storage
image elsewhere in the block does not see a CPU write. StoredOver and
UnchangedSinceCollected read the noted pages whenever a block has them, so
a store to such a page is still seen by a check over that page. The extra
generation step happens only when something was collected; a block whose
write protection fails is unwatched and the write returns no generation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An anonymous private mapping with MAP_ALIGNED_SUPER (FreeBSD MAP_ALIGNED(1))
is placed on a 2 MiB superpage boundary. Yamagi Quake II requests it for its
hunk allocations of 2 MiB and more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every direct memory allocation had a memfd of its own, so each one the
GPU used became its own udmabuf and its own imported buffer object. An
imported buffer object cannot be always valid in the GPU VM: amdgpu
validates every one of them at every command submission. PPSA14632
(LEGO Horizon Adventures) imports about 7000 allocations, 5800 of them
64 KiB, and each vkQueueSubmit took about 2.3 ms (5 s per 10 s, held
under the GPU mutex). A standalone test shows the cost follows the
count of imported dma-buf buffer objects, not their size or the count
of ordinary allocations.
- libkernel: on Linux all direct memory lives in one sparse memfd of
DIRECT_MEMORY_SIZE, at file offset = physical address
(PhysicalBacking keeps the file and its offset; SharedBacking returns
that offset). A range allocated again is zeroed through a temporary
mapping, as the old per-allocation memfd was.
- Driver: an import of shared direct memory that lies inside one
64 MiB chunk of the file binds its own VkBuffer into that chunk's
memory, one udmabuf import per chunk (HostImport::chunk holds it, so
it lives until the last buffer bound to it retires). Ranges that
cross a chunk or do not meet the buffer alignment are imported alone
as before.
On PPSA14632 vkQueueSubmit drops from about 2400 us to 46 us on
average, the GPU idles 36 ms instead of 172 ms per present, and
presents go from about 40 to 80-97 per 10 s.
Released direct memory stays committed in the file (see
TechnicalDebt.md): a live chunk import pins its pages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use the current registered range after protection splits and partial unmaps, including FIND_NEXT lookups. Add a regression covering extents, protection and surviving contents.
AI-assisted: yes
WindowsMappings::Collect resets each part of the range as it walks it:
it marks shared views seen and resets private pages' write-watch bits.
When it then met a NOACCESS view or memory that is not committed
private memory, it returned false with the earlier parts already reset,
so their writes were lost to the caller's fallback. With reset, the
whole range is now checked first and nothing is reset when it fails.
AI-assisted (Claude Code).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Releasing direct memory left the virtual ranges that mapped it registered,
so sceKernelVirtualQuery reported them as flexible memory at offset 0 and
titles walking a range with FIND_NEXT released physical page 0.
Collect every view of the released pages before freeing them and unmap
them through UnmapRegistered, as shadPS4's MemoryManager::Free does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clang's PS4 and PS5 targets set the default new alignment and the suitable
alignment to 32 bytes, so PS5 code may store to a fresh malloc or operator
new result with 32-byte-aligned AVX instructions. The guest heap aligned
blocks to 16 bytes, and the arena's 32-byte classes put every small block
16 bytes past a 32-byte boundary after its header, so such a store faulted.
The minimum alignment is now 32 bytes; the padding still assumes only the
16-byte alignment the raw blocks guarantee.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both return -1/EINVAL: FreeBSD kern_mprotect (sys/vm/vm_mmap.c) maps only
PROTECTION_FAILURE, RESOURCE_SHORTAGE and OUT_OF_BOUNDS to their own errno, so
KERN_INVALID_ADDRESS falls to EINVAL.
Co-Authored-By: Claude <noreply@anthropic.com>
mprotect changes the protection of guest memory through the same path
as sceKernelMprotect and reports failures through errno: EINVAL for a
null or overflowing range or a range the allocation tracker rejects,
ENOMEM when allocation fails. A zero length succeeds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fixed mapping without no-overwrite unmaps the registered pieces it overlaps before mapping, as it already did when the range lay inside registered allocations.
On Linux, direct memory is a memfd mapped MAP_SHARED. amdgpu accepts only
private anonymous memory for a host pointer import (libdrm sets
AMDGPU_GEM_USERPTR_ANONONLY), so every import of direct memory failed with
VK_ERROR_INVALID_EXTERNAL_HANDLE and its ranges were copied.
When a host pointer import fails, the range's memfd slice is turned into a
dma-buf through /dev/udmabuf and imported with VK_EXT_external_memory_dma_buf.
The direct memory backing is sealed against shrinking and growing, as
udmabuf requires. libkernel reports the backing of a guest range through
GuestArenaSharedBacking. Without /dev/udmabuf, or past its size limit, the
range is copied as before. APS5_TRACE_HOST_IMPORT now prints the mappings
behind a failed import on Linux too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also unpin the previous context when a fiber object is initialised again
without being finalised.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A title maps direct memory with protection 0x3f2, which adds bits
0x100 and 0x200 to the CPU, GPU and AMPR bits, and the mapping threw.
They are the audio coprocessor (ACP) read and write bits, as named in
the KytyPS5 and etaHEN headers. The coprocessor work runs on the host
CPU, so they map to host read and read/write access like the GPU bits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MAP_FIXED | MAP_NO_OVERWRITE over a range that already holds an
allocation fails with ENOMEM and leaves the mapping in place, as in
fpPS4's TVirtualManager.check_fixed. UFC 6 (PPSA23566) relies on it to
probe for a free address and aborted on the throw.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(libkernel): implement sceKernelReleaseFlexibleMemory
Unmaps the range through sceKernelMunmap, which returns its pages to
the flexible memory budget, as fpPS4 and prosper do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(libkernel): keep the flexible memory release check apart from the batch map checks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The SDK 9 libc.prx heap maps its arenas with flag 0x8000, which threw as
an unsupported flag. The flag is ignored for this function, as sharpemu
does; other unknown flags still throw.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release direct memory only when the whole range is allocated: a misaligned
start or length returns EINVAL, a zero length succeeds, and a range that
leaves direct memory or covers an unallocated page returns ENOENT without
releasing anything, as in shadPS4. The check and the release run under
the pool lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A range reserved with sceKernelReserveVirtualRange is registered as not
readable, and sceKernelVirtualQuery only searched readable registrations,
so a query inside a reservation fell back to the host and returned EACCES.
Reserved ranges are now tracked until a direct or flexible mapping or an
unmap covers them, and a query inside one succeeds with the reserved piece
as its extent, protection 0 and is_committed clear. PPSA12544's allocator
queries a reservation before committing it and treated the error as a
failed commit, then wrote to the inaccessible range.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows the guest arena ended at 0x72_0000_0000: #231 kept its
448 GiB size after #153 had moved its start down to 0x2_0000_0000.
Titles map fixed addresses up to the application map area end,
0xFC_0000_0000 (SCE_KERNEL_APP_MAP_AREA_END_ADDR on the PS4), and a
direct memory mapping above the old end threw "shared mapping outside
the guest arena".
The arena is now reserved up to 0xFC_0000_0000. The out-of-arena
errors name the address and length, and a fixed mapping over a used
arena range says whether it hit a host region. guest_memory maps
direct memory at the last two pages below the end, reads it through
an alias and checks that a mapping at the end is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ForgetDirectMemory erased the physical pages but left g_directMappings entries pointing at the dead backing, so sceKernelVirtualQuery kept reporting is_direct with a stale physical offset. Erase (and split, on the phys axis) the affected mapping tracking on release; mapped views are untouched, so no new faults are introduced.
Lock the guest pages of the range with VirtualLock or mlock, following FreeBSD mlock(2): the range is widened to guest pages, a zero length succeeds, a wrapping range is EINVAL and a range that is not mapped and accessible is ENOMEM. The Windows working set limits and the Linux RLIMIT_MEMLOCK soft limit are raised when the lock needs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>