Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions core/libs/prx/libkernel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1534,6 +1534,13 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
set_tests_properties(guest_shm_open PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
add_test_executable(guest_named_shm_windows_mapping_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmWindowsMapping.cpp)
target_include_directories(guest_named_shm_windows_mapping_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_named_shm_windows_mapping_tests PRIVATE libkernel libc)
configure_windows_unwind(guest_named_shm_windows_mapping_tests)
add_test(NAME guest_named_shm_windows_mapping COMMAND guest_named_shm_windows_mapping_tests)
set_tests_properties(guest_named_shm_windows_mapping PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_jit_shared_memory_tests ${LIBS_INCLUDE_DIR}/tests/GuestJitSharedMemory.cpp)
target_include_directories(guest_jit_shared_memory_tests PRIVATE ${LIBS_INCLUDE_DIR})
Expand Down
6 changes: 4 additions & 2 deletions core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,12 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti
if (!File::IsSharedMemoryDescriptor_nid_no_patch(mappingDescriptor) && !RegularFile(mappingDescriptor)) return failed(GuestNotSupported);
#elif defined(_WIN32)
const int mappingDescriptor = descriptor;
if (!File::IsSharedMemoryDescriptor_nid_no_patch(mappingDescriptor)) {
{
const HANDLE handle = WindowsDescriptorHandle_nid_no_patch(mappingDescriptor);
if (handle == INVALID_HANDLE_VALUE) return failed(GuestBadDescriptor);
if (File::DirectoryDescriptorPath(mappingDescriptor) || File::IsRandomDevice(mappingDescriptor) || GetFileType(handle) != FILE_TYPE_DISK) return failed(GuestNotSupported);
if (!File::IsSharedMemoryDescriptor_nid_no_patch(mappingDescriptor) &&
(File::DirectoryDescriptorPath(mappingDescriptor) || File::IsRandomDevice(mappingDescriptor) || GetFileType(handle) != FILE_TYPE_DISK))
return failed(GuestNotSupported);
const int accessMode = DescriptorAccess(handle, mappingDescriptor);
if (accessMode < 0) return failed(GuestBadDescriptor);
if ((protection & 1) != 0 && accessMode == 1) return failed(GuestAccess);
Expand Down
89 changes: 89 additions & 0 deletions core/libs/tests/GuestNamedShmWindowsMapping.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#include "prx/libkernel/include/Api.hpp"
#include "prx/libc/include/Api.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <windows.h>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>

namespace {
constexpr int ReadOnly = 0;
constexpr int ReadWrite = 2;
constexpr int Create = 0x200;
constexpr int Exclusive = 0x800;
constexpr int ProtRead = 1;
constexpr int ProtReadWrite = 3;
constexpr int MapShared = 1;
constexpr int MapPrivate = 2;
constexpr std::size_t Size = 0x8000;
void* const Failed = reinterpret_cast<void*>(static_cast<std::uintptr_t>(-1));

void Require(bool value, const char* message) {
if (value) return;
std::fprintf(stderr, "named shared-memory mapping: %s (guest errno %d)\n", message, *__error_nid_postfix());
std::abort();
}

char* Map(std::size_t length, int protection, int flags, int descriptor, std::int64_t offset) {
return static_cast<char*>(mmap_nid_postfix(nullptr, length, protection, flags, descriptor, offset));
}
}

int main() {
const std::string name = "/aps5-named-map-" + std::to_string(::GetCurrentProcessId());
shm_unlink_nid_postfix(name.c_str());
const int first = shm_open_nid_postfix(name.c_str(), Create | Exclusive | ReadWrite, 0600);
Require(first >= 0, "creating the named object failed");
Require(ftruncate_nid_postfix(first, Size) == 0, "sizing the named object failed");

auto* view = Map(Size, ProtReadWrite, MapShared, first, 0);
Require(view != Failed, "a shared read-write mapping of the named object failed");
view[0x10] = 'a';

const int second = shm_open_nid_postfix(name.c_str(), ReadWrite, 0);
Require(second >= 0 && second != first, "reopening the named object failed");
auto* other = Map(Size, ProtReadWrite, MapShared, second, 0);
Require(other != Failed && other != view, "a second shared mapping of the named object failed");
Require(other[0x10] == 'a', "a store through one mapping is missing from a mapping of another descriptor");
other[0x5000] = 'b';
Require(view[0x5000] == 'b', "a store through the second mapping is missing from the first");

char byte = 0;
Require(pread_nid_postfix(second, &byte, 1, 0x5000) == 1 && byte == 'b', "pread does not see a store made through a mapping");
const char written = 'c';
Require(pwrite_nid_disambig1_nid_postfix(first, &written, 1, 0x6000) == 1, "pwrite into the named object failed");
Require(view[0x6000] == 'c' && other[0x6000] == 'c', "a mapping does not see a pwrite into the named object");

auto* offset = Map(0x4000, ProtRead, MapShared, second, 0x4000);
Require(offset != Failed && offset[0x1000] == 'b' && offset[0x2000] == 'c', "a mapping at a nonzero offset shows the wrong bytes");
Require(munmap_nid_postfix(offset, 0x4000) == 0, "unmapping the offset mapping failed");

const int reader = shm_open_nid_postfix(name.c_str(), ReadOnly, 0);
Require(reader >= 0, "opening the named object read-only failed");
auto* readView = Map(Size, ProtRead, MapShared, reader, 0);
Require(readView != Failed && readView[0x5000] == 'b', "a read-only mapping of a read-only descriptor failed");
Require(munmap_nid_postfix(readView, Size) == 0, "unmapping the read-only mapping failed");
*__error_nid_postfix() = 0;
Require(Map(Size, ProtReadWrite, MapShared, reader, 0) == Failed && *__error_nid_postfix() == 13, "a writable shared mapping of a read-only descriptor was not refused with EACCES");
Require(close_nid_postfix(reader) == 0, "closing the read-only descriptor failed");

Require(shm_unlink_nid_postfix(name.c_str()) == 0, "unlinking the named object failed");
*__error_nid_postfix() = 0;
Require(shm_open_nid_postfix(name.c_str(), ReadWrite, 0) == -1 && *__error_nid_postfix() == 2, "the unlinked name still opens");
view[0x7000] = 'u';
Require(other[0x7000] == 'u', "mappings of an unlinked object stopped sharing stores");
Require(pread_nid_postfix(second, &byte, 1, 0x7000) == 1 && byte == 'u', "a descriptor of an unlinked object stopped seeing mapped stores");

auto* copy = Map(Size, ProtReadWrite, MapPrivate, second, 0);
Require(copy != Failed && copy[0x10] == 'a', "a private mapping of the named object failed");
copy[0x10] = 'p';
Require(view[0x10] == 'a' && pread_nid_postfix(first, &byte, 1, 0x10) == 1 && byte == 'a', "a store through a private mapping reached the named object");
Require(munmap_nid_postfix(copy, Size) == 0, "unmapping the private mapping failed");

Require(munmap_nid_postfix(view, Size) == 0 && munmap_nid_postfix(other, Size) == 0, "unmapping the shared mappings failed");
Require(close_nid_postfix(first) == 0 && close_nid_postfix(second) == 0, "closing the named object's descriptors failed");
std::puts("named shared-memory mapping tests passed");
return 0;
}
2 changes: 1 addition & 1 deletion docs/dev/debt/libkernel.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@
- [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws
- [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - every guest thread is a host thread at system scope, so `PTHREAD_SCOPE_PROCESS` is stored and reported (`pthread_attr_get_np`, `scePthreadAttrGet`) but changes nothing. Measured in a payload process on a PS5 (firmware 13.60), as FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c)) does: the scope defaults to 2, only 0 and 2 are accepted (`EINVAL` otherwise), an inherit value other than 4 or 0 returns `ENOTSUP`, `pthread_attr_getstackaddr` returns the configured pointer (null by default), and a null output, a null, uninitialized or destroyed attribute return `EINVAL`. A thread keeps the scope it was created with, taken from its creator under `PTHREAD_INHERIT_SCHED`
- [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. On Windows, anonymous objects are delete-on-close temporary files and `mmap` maps them as [regular files](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp); named objects are described in the next entry. Invalid non-null name pointers, console behaviour and title compatibility remain unverified.
- [shm_open, shm_unlink](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel, Windows) - named objects are regular files in the current user's temporary directory under `AnyPS5/shm`, identified by SHA-1 of the exact guest name. The namespace is per host user and shared by every AnyPS5 process using that temporary root, so two titles using the same name share the object. Namespace operations use a cross-session named mutex, while file-size changes and writes use a mutex keyed by the host file identity; unlink renames the visible object to a unique internal name so already-open handles remain usable while the guest name can be recreated. Orphaned files are reaped when the last tracked guest descriptor closes or opportunistically during a later open. Closing through host stdio wrappers does not update this bookkeeping or reap immediately. The namespace directory has a protected ACL for the owner, SYSTEM and Administrators; newly created objects map the guest owner read/write bits after applying the guest creation mask. `fchmod` updates those owner permissions on the descriptor's file object. Guest group/other permission classes are not represented independently on Windows. The owner's read permission also grants execute, so a read-only descriptor can be mapped `PROT_EXEC` as FreeBSD 11.4's `shm_mmap` allows; objects created by earlier builds lack it until `fchmod`. No behavior here has been measured on PS5. Lifecycle, cross-process resize/write, and mode behavior are exercised by Windows tests. See Microsoft's [file sharing modes](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea), [DeleteFile](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-deletefilew) and [mutex objects](https://learn.microsoft.com/en-us/windows/win32/sync/mutex-objects).
- [shm_open, shm_unlink](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel, Windows) - named objects are regular files in the current user's temporary directory under `AnyPS5/shm`, identified by SHA-1 of the exact guest name. The namespace is per host user and shared by every AnyPS5 process using that temporary root, so two titles using the same name share the object. Namespace operations use a cross-session named mutex, while file-size changes and writes use a mutex keyed by the host file identity; unlink renames the visible object to a unique internal name so already-open handles remain usable while the guest name can be recreated. Orphaned files are reaped when the last tracked guest descriptor closes or opportunistically during a later open. Closing through host stdio wrappers does not update this bookkeeping or reap immediately. The namespace directory has a protected ACL for the owner, SYSTEM and Administrators; newly created objects map the guest owner read/write bits after applying the guest creation mask. `fchmod` updates those owner permissions on the descriptor's file object. Guest group/other permission classes are not represented independently on Windows. The owner's read permission also grants execute, so a read-only descriptor can be mapped `PROT_EXEC` as FreeBSD 11.4's `shm_mmap` allows; objects created by earlier builds lack it until `fchmod`. Mappings take a regular file's checks and paths (`guest_named_shm_windows_mapping`). No behavior here has been measured on PS5. Lifecycle, cross-process resize/write, and mode behavior are exercised by Windows tests. See Microsoft's [file sharing modes](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea), [DeleteFile](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-deletefilew) and [mutex objects](https://learn.microsoft.com/en-us/windows/win32/sync/mutex-objects).

Check warning on line 178 in docs/dev/debt/libkernel.md

View workflow job for this annotation

GitHub Actions / conventions

debt-length

Technical debt entry over 1000 characters: state the gap first (what differs from the console, what is unverified, what throws) and keep only the behaviour it needs: 1928 characters

Check warning on line 178 in docs/dev/debt/libkernel.md

View workflow job for this annotation

GitHub Actions / conventions

debt-length

Technical debt entry over 1000 characters: state the gap first (what differs from the console, what is unverified, what throws) and keep only the behaviour it needs: 1928 characters
- [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a read event on a listening socket, here and in `kevent`, reports the number of completed connections waiting to be accepted in `data`, as FreeBSD 11.4's [`filt_solisten`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3384-L3392) reports `so_qlen`: on Linux a TCP listener's accept queue from `TCP_INFO` (`tcpi_unacked`, which Linux fills with the accept backlog for a listening socket), and elsewhere, or for a listener that is not TCP, 1 while a connection is waiting, as no host call reports that count. A write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw
- [sceKernelAddReadEvent on pipes](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) is an exception to the sockets-only rule above: it also takes the read end of a pipe, as `kevent` does through FreeBSD's `filt_piperead`, and ignores the size argument there. Host streams, other descriptors and a write event on a pipe throw
- [sceKernelAddReadEvent and sceKernelAddWriteEvent on regular files](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) are the other exception: a size argument above 1, directories and other vnode types throw, and whether the console's file systems answer as `vfs_kqfilter` does was not measured. A wait holding one of these events polls it every millisecond, as no host call reports every file change, so a write through another descriptor shows up to 1 ms late. Unlike FreeBSD, a seek can make a read event fire again: FreeBSD 11.4's [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) deactivates a level-triggered read knote once `filt_vfsread` reports `data` 0, and only a vnode `KNOTE` (a write, an extend or a truncate) activates it again; `lseek` posts none, so after a file read to its end is seeked back, the 1 ms poll here reports the new `data` (10 for a 10-byte file seeked to 0) where FreeBSD keeps waiting until the file changes. Otherwise they follow FreeBSD 11.4's [`filt_vfsread` and `filt_vfswrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_subr.c#L5122-L5168): a read event reports the size less the descriptor's offset in `data` and fires while that is not 0; a write event always fires with `data` 0
Expand Down
Loading