Skip to content
Open
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
2 changes: 1 addition & 1 deletion core/libs/prx/libkernel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2062,7 +2062,7 @@ if(BUILD_TESTING)
add_test(NAME guest_mmap_fixed_anonymous COMMAND guest_memory_tests fixed-anonymous)
if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_test(NAME guest_mmap_executable COMMAND guest_memory_tests executable-mappings)
set_tests_properties(guest_mmap_executable PROPERTIES TIMEOUT 20)
set_tests_properties(guest_mmap_executable PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77)
endif()
if(WIN32)
set_tests_properties(guest_memory guest_mmap_fixed_anonymous guest_mmap_executable PROPERTIES ENVIRONMENT_MODIFICATION
Expand Down
32 changes: 21 additions & 11 deletions core/libs/tests/GuestMemory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include <fstream>
#include <string>
#include <sys/mman.h>
#include <sys/statvfs.h>
#include <time.h>
#include <unistd.h>
#elif defined(__APPLE__)
Expand Down Expand Up @@ -3542,7 +3543,7 @@ static void CheckRegularFileMapping() {
Require(std::filesystem::remove(path));
}

static void CheckExecutableSharedMappings() {
static bool CheckExecutableSharedMappings() {
constexpr std::size_t page = 0x4000;
const auto failed = reinterpret_cast<void*>(static_cast<std::uintptr_t>(-1));
const auto code = [](std::uint32_t value) {
Expand All @@ -3556,8 +3557,9 @@ static void CheckExecutableSharedMappings() {
std::memcpy(&function, &address, sizeof(function));
return static_cast<std::uint32_t>(function());
};
const auto check = [&](int writer, int fd, int access) {
const auto check = [&](int writer, int fd, int access, bool executable) {
for (int protection = 0; protection <= 7; ++protection) {
if (!executable && (protection & 4) != 0) continue;
const auto stored = code(0x100 + protection);
Require(sceKernelPwrite(writer, stored.data(), stored.size(), page) == static_cast<std::int64_t>(stored.size()));
int refusal = 0;
Expand Down Expand Up @@ -3597,27 +3599,36 @@ static void CheckExecutableSharedMappings() {
const int fileReader = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0);
const int fileWriter = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_WRONLY, 0);
Require(file >= 0 && fileReader >= 0 && fileWriter >= 0);
check(file, file, 2);
check(file, fileReader, 0);
check(file, fileWriter, 1);
check(file, file, 2, true);
check(file, fileReader, 0, true);
check(file, fileWriter, 1, true);
Require(sceKernelClose(fileWriter) == 0 && sceKernelClose(fileReader) == 0 && sceKernelClose(file) == 0);
Require(std::filesystem::remove(path));
const int anonymous = shm_open_nid_postfix(reinterpret_cast<const char*>(1), SCE_KERNEL_O_RDWR, 0600);
Require(anonymous >= 0 && ftruncate_nid_postfix(anonymous, page * 2) == 0);
check(anonymous, anonymous, 2);
check(anonymous, anonymous, 2, true);
Require(close_nid_postfix(anonymous) == 0);
#ifdef _WIN32
const bool namedExecutable = true;
#else
struct statvfs sharedMemoryMount{};
Require(statvfs("/dev/shm", &sharedMemoryMount) == 0);
const bool namedExecutable = (sharedMemoryMount.f_flag & ST_NOEXEC) == 0;
#endif
const auto name = "/anyps5-executable-mapping-" + stamp;
const int named = shm_open_nid_postfix(name.c_str(), SCE_KERNEL_O_RDWR | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0600);
Require(named >= 0 && ftruncate_nid_postfix(named, page * 2) == 0);
const int namedReader = shm_open_nid_postfix(name.c_str(), SCE_KERNEL_O_RDONLY, 0);
Require(namedReader >= 0);
check(named, named, 2);
check(named, namedReader, 0);
check(named, named, 2, namedExecutable);
check(named, namedReader, 0, namedExecutable);
Require(close_nid_postfix(namedReader) == 0 && close_nid_postfix(named) == 0 && shm_unlink_nid_postfix(name.c_str()) == 0);
int jit = -1;
Require(sceKernelJitCreateSharedMemory(nullptr, page * 2, 7, &jit) == 0 && jit >= 0);
check(jit, jit, 2);
check(jit, jit, 2, true);
Require(close_nid_postfix(jit) == 0);
if (!namedExecutable) std::puts("skipped, the host mounts /dev/shm noexec, so named shared memory is not mapped executable");
return namedExecutable;
}
#endif

Expand Down Expand Up @@ -4154,8 +4165,7 @@ int main(int argc, char** argv) {
}
#if defined(__linux__) || defined(_WIN32)
if (argc == 2 && std::strcmp(argv[1], "executable-mappings") == 0) {
CheckExecutableSharedMappings();
return 0;
return CheckExecutableSharedMappings() ? 0 : 77;
}
#endif
Require(argc == 1);
Expand Down
1 change: 1 addition & 0 deletions docs/dev/debt/libkernel.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@
- [Guest thread x87 control word](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - not measured on a console, and the initial thread keeps the control word the host starts it with (`0x27f` on Windows): a new guest thread starts with `0x037f` (64-bit precision, every exception masked) whatever its creator uses. That is FreeBSD 11's start state, whose [`cpu_copy_thread`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/vm_machdep.c#L428-L446) leaves a new thread's FPU state uninitialized so that its first FPU instruction loads `fpu_initialstate` (`__INITIAL_FPUCW__`), and the control word of the libSceFiber option that starts fibers with the default FPU state
- [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.
- [Named shared memory mapped executable on Linux](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) - named objects are files under `/dev/shm`, so on a host that mounts it `noexec`, as Docker does by default, `mmap` with `PROT_EXEC` of a named object fails (the host refuses it with `EPERM`), where FreeBSD 11.4 maps POSIX shared memory executable. Anonymous objects are a `memfd` and are not affected. `guest_mmap_executable` skips the executable named mappings on such a host.
- [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).
- [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
Expand Down
Loading