fix(libkernel): keep shared-memory identity and guest permissions consistent

Pin the descriptor before mmap validation so guest descriptor reuse cannot
redirect the eventual mapping. Apply the canonical guest creation mask when
creating named objects, distinguishing new objects before setting permissions
without changing the host process umask. Add regressions that reproduce both
failures and document the remaining host requirements.

Constraint: Guest umask must not mutate the process-wide host mask
Confidence: high
Scope-risk: narrow
Tested: 17 targeted Linux runtime tests passed with zero skips; conventions and diff checks; Clang analyzer on three changed production translation units
Not-tested: Full project build and suite, Windows runtime, console hardware and titles
This commit is contained in:
bilel 2026-10-11 14:28:21 +01:00 committed by Adrià Franch
parent 2aaeaa3f05
commit 72b3a2abdd
9 changed files with 312 additions and 14 deletions

View File

@ -766,6 +766,16 @@ if(BUILD_TESTING)
target_link_libraries(guest_named_shm_lifecycle_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS})
add_test(NAME guest_named_shm_lifecycle COMMAND guest_named_shm_lifecycle_tests)
set_tests_properties(guest_named_shm_lifecycle PROPERTIES TIMEOUT 20)
add_test_executable(guest_named_shm_descriptor_reuse_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmDescriptorReuse.cpp)
target_include_directories(guest_named_shm_descriptor_reuse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include)
target_link_libraries(guest_named_shm_descriptor_reuse_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS})
add_test(NAME guest_named_shm_descriptor_reuse COMMAND guest_named_shm_descriptor_reuse_tests)
set_tests_properties(guest_named_shm_descriptor_reuse PROPERTIES TIMEOUT 20)
add_test_executable(guest_named_shm_permissions_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmPermissions.cpp)
target_include_directories(guest_named_shm_permissions_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include)
target_link_libraries(guest_named_shm_permissions_tests PRIVATE libkernel_sys libkernel libc rt)
add_test(NAME guest_named_shm_permissions COMMAND guest_named_shm_permissions_tests)
set_tests_properties(guest_named_shm_permissions PROPERTIES TIMEOUT 20)
add_test_executable(guest_named_shm_concurrency_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmConcurrency.cpp)
target_include_directories(guest_named_shm_concurrency_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include)
target_link_libraries(guest_named_shm_concurrency_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS})

View File

@ -40,6 +40,25 @@ constexpr std::uintptr_t GuestUserAddressEnd = 0x800000000000;
constexpr int GuestAlignedSuper = 0x1000000;
constexpr std::size_t SuperpageSize = std::size_t{1} << 21;
#if defined(__linux__)
class ScopedDescriptor {
public:
explicit ScopedDescriptor(int value) noexcept : descriptor(fcntl(value, F_DUPFD_CLOEXEC, 0)), error(descriptor < 0 ? errno : 0) {}
~ScopedDescriptor() {
const int savedError = errno;
if (descriptor >= 0) close(descriptor);
errno = savedError;
}
ScopedDescriptor(const ScopedDescriptor&) = delete;
ScopedDescriptor& operator=(const ScopedDescriptor&) = delete;
int Get() const noexcept { return descriptor; }
int Error() const noexcept { return error; }
private:
int descriptor = -1;
int error = 0;
};
#endif
bool RoundLength(std::size_t length, std::size_t& rounded) {
constexpr auto mask = PS5_PAGE_SIZE - 1;
if (length == 0 || length > std::numeric_limits<std::size_t>::max() - mask)
@ -95,18 +114,24 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti
static_cast<std::uint64_t>(offset) > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()) - rounded)
return failed(GuestInvalid);
#if defined(__linux__)
const int descriptorFlags = fcntl(descriptor, F_GETFL);
const ScopedDescriptor pinnedDescriptor(descriptor);
if (pinnedDescriptor.Get() < 0) return failed(pinnedDescriptor.Error());
const int mappingDescriptor = pinnedDescriptor.Get();
const int descriptorFlags = fcntl(mappingDescriptor, F_GETFL);
if (descriptorFlags < 0) return failed(errno);
const int accessMode = descriptorFlags & O_ACCMODE;
if ((protection & 1) != 0 && accessMode == O_WRONLY) return failed(GuestAccess);
if (kind == GuestShared && (protection & 2) != 0 && accessMode == O_RDONLY) return failed(GuestAccess);
if (!File::IsSharedMemoryDescriptor_nid_no_patch(mappingDescriptor)) return failed(GuestNotSupported);
#else
const int mappingDescriptor = descriptor;
if (!File::IsSharedMemoryDescriptor_nid_no_patch(mappingDescriptor)) return failed(GuestNotSupported);
#endif
if (!File::IsSharedMemoryDescriptor_nid_no_patch(descriptor)) return failed(GuestNotSupported);
const std::size_t alignment = (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE;
void* mapped = nullptr;
try {
mapped = address;
const auto result = DoMapFileBacked_nid_no_patch(&mapped, rounded, protection, kind | placement, descriptor, static_cast<std::uint64_t>(offset), alignment);
const auto result = DoMapFileBacked_nid_no_patch(&mapped, rounded, protection, kind | placement, mappingDescriptor, static_cast<std::uint64_t>(offset), alignment);
if (result != 0) return failed(result == SCE_KERNEL_ERROR_EINVAL ? GuestInvalid : GuestNoMemory);
return mapped;
} catch (const std::invalid_argument&) {

View File

@ -9,6 +9,7 @@
namespace File {
std::uint16_t ExchangeCreationMask_nid_no_patch(std::uint16_t mask);
bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor);
std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size);
std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size, std::int64_t offset);

View File

@ -12,14 +12,13 @@
#include <cstdint>
#include <limits>
#include <optional>
#include <mutex>
#include <atomic>
#ifdef _WIN32
#include <fcntl.h>
#include <io.h>
#include <share.h>
#include <sys/stat.h>
#include <atomic>
#include <filesystem>
#include <random>
#else
@ -80,6 +79,8 @@ private:
int error = 0;
};
std::atomic<std::uint16_t> creationMask{022};
constexpr int GuestNoMemory = 12;
constexpr int GuestInvalid = 22;
constexpr int GuestTooManyFilesInSystem = 23;
@ -163,9 +164,25 @@ int OpenNamedFile(const char* path, int flags, int mode) {
if (flags & SCE_KERNEL_O_CREAT) hostFlags |= O_CREAT;
if (flags & SCE_KERNEL_O_EXCL) hostFlags |= O_EXCL;
if ((flags & SCE_KERNEL_O_TRUNC) && (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_RDWR) hostFlags |= O_TRUNC;
const int descriptor = ::shm_open(hostName.c_str(), hostFlags | O_CLOEXEC, mode & 0777);
const auto permissions = (mode & 0777) & ~creationMask.load(std::memory_order_relaxed);
bool created = false;
int descriptor;
if (flags & SCE_KERNEL_O_CREAT) {
descriptor = ::shm_open(hostName.c_str(), hostFlags | O_CLOEXEC | O_EXCL, permissions);
created = descriptor >= 0;
if (descriptor < 0 && errno == EEXIST && !(flags & SCE_KERNEL_O_EXCL)) {
descriptor = ::shm_open(hostName.c_str(), (hostFlags & ~(O_CREAT | O_EXCL)) | O_CLOEXEC, 0);
}
} else {
descriptor = ::shm_open(hostName.c_str(), hostFlags | O_CLOEXEC, 0);
}
if (descriptor >= 0) {
return descriptor;
if (!created || ::fchmod(descriptor, permissions) == 0) return descriptor;
const int error = errno;
::close(descriptor);
::shm_unlink(hostName.c_str());
if (const int guest = NamedGuestError(error)) return Fail(guest);
throw std::runtime_error("shm_open: setting guest creation permissions failed, errno=" + std::to_string(error));
}
const int error = errno;
if (const int guest = NamedGuestError(error)) return Fail(guest);
@ -202,6 +219,10 @@ int CreateAnonymousFile() {
namespace File {
std::uint16_t ExchangeCreationMask_nid_no_patch(std::uint16_t mask) {
return creationMask.exchange(static_cast<std::uint16_t>(mask & 07777), std::memory_order_relaxed);
}
bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor) {
return IsHostSharedMemoryDescriptor(descriptor);
}

View File

@ -1,4 +1,3 @@
#include <atomic>
#include <algorithm>
#include <cstdint>
#include <cstddef>
@ -17,6 +16,7 @@
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/FilesystemError.hpp"
#include "prx/libkernel/File/include/DirectoryDescriptor.hpp"
#include "prx/libkernel/File/include/SharedMemory.hpp"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
@ -38,8 +38,6 @@ constexpr int GuestEinval = 22;
constexpr int GuestEexist = 17;
constexpr int GuestExdev = 18;
std::atomic<std::uint16_t> creationMask{022};
std::int64_t Fail(int error) {
*__error_nid_postfix() = error;
return -1;
@ -63,7 +61,7 @@ int APS5_VABI setsid_nid_postfix(void) {
}
std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t mask) {
return creationMask.exchange(static_cast<std::uint16_t>(mask & 07777));
return File::ExchangeCreationMask_nid_no_patch(mask);
}
int APS5_VABI _fstatfs_nid_postfix(int d, void* buf);

View File

@ -0,0 +1,160 @@
#include "SceTypes.hpp"
#include "nid/Sha1.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <cstdint>
#include <cerrno>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <stdexcept>
#include <string>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <vector>
extern "C" {
int APS5_VABI shm_open_nid_postfix(const char*, int, int);
int APS5_VABI close_nid_postfix(int);
int APS5_VABI ftruncate_nid_postfix(int, std::int64_t);
std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::size_t, std::int64_t);
std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t);
void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept;
int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept;
int* APS5_VABI __error_nid_postfix();
}
namespace {
constexpr int ReadWrite = 2;
constexpr int Create = 0x200;
constexpr int Exclusive = 0x800;
constexpr int Shared = 0x1;
constexpr std::size_t Page = 0x4000;
int armedDescriptor = -1;
int replacementDescriptor = -1;
bool armed = false;
bool swapped = false;
void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")");
}
struct Object {
std::string name = "/anyps5-descriptor-reuse-" + std::to_string(getpid());
std::string hostName = "/anyps5-shm-";
Object() {
constexpr char hex[] = "0123456789abcdef";
const auto digest = Nid::Sha1(std::vector<std::uint8_t>(name.begin(), name.end()));
for (const auto byte : digest) {
hostName += hex[byte >> 4];
hostName += hex[byte & 15];
}
::shm_unlink(hostName.c_str());
}
~Object() { ::shm_unlink(hostName.c_str()); }
int Open(int flags) const { return shm_open_nid_postfix(name.c_str(), flags, 0600); }
};
}
extern "C" int fcntl(int descriptor, int command, ...) {
long argument = 0;
switch (command) {
case F_GETFD:
case F_GETFL:
case F_GETOWN:
#ifdef F_GETSIG
case F_GETSIG:
#endif
#ifdef F_GETLEASE
case F_GETLEASE:
#endif
#ifdef F_GETPIPE_SZ
case F_GETPIPE_SZ:
#endif
#ifdef F_GET_SEALS
case F_GET_SEALS:
#endif
break;
#ifdef F_GETLK
case F_GETLK:
case F_SETLK:
case F_SETLKW: {
va_list args;
va_start(args, command);
argument = reinterpret_cast<long>(va_arg(args, void*));
va_end(args);
break;
}
#endif
case F_DUPFD:
case F_DUPFD_CLOEXEC:
case F_SETFD:
case F_SETFL:
case F_SETOWN:
#ifdef F_SETSIG
case F_SETSIG:
#endif
#ifdef F_SETLEASE
case F_SETLEASE:
#endif
#ifdef F_NOTIFY
case F_NOTIFY:
#endif
#ifdef F_SETPIPE_SZ
case F_SETPIPE_SZ:
#endif
#ifdef F_ADD_SEALS
case F_ADD_SEALS:
#endif
{
va_list args;
va_start(args, command);
argument = va_arg(args, int);
va_end(args);
break;
}
default:
errno = EINVAL;
return -1;
}
const long result = ::syscall(SYS_fcntl, descriptor, command, argument);
if (command == F_GETFL && armed && !swapped) {
swapped = true;
if (::syscall(SYS_dup2, replacementDescriptor, armedDescriptor) != armedDescriptor) return -1;
}
return static_cast<int>(result);
}
int main() {
try {
Object object;
const int descriptor = object.Open(ReadWrite | Create | Exclusive);
Require(descriptor >= 0 && ftruncate_nid_postfix(descriptor, Page) == 0, "shared-memory setup failed");
Require(pwrite_nid_disambig1_nid_postfix(descriptor, "original", 8, 0) == 8, "shared-memory seed failed");
const int verifier = object.Open(ReadWrite);
Require(verifier >= 0, "shared-memory verifier open failed");
const int replacement = ::open(__FILE__, O_RDONLY);
Require(replacement >= 0, "ordinary-file setup failed");
armedDescriptor = descriptor;
replacementDescriptor = replacement;
armed = true;
auto* mapped = static_cast<char*>(mmap_nid_postfix(nullptr, Page, 3, Shared, descriptor, 0));
armed = false;
Require(swapped, "native F_GETFL interposer did not replace the guest descriptor");
Require(mapped != reinterpret_cast<char*>(static_cast<std::uintptr_t>(-1)), "guest mmap failed after descriptor replacement");
Require(std::memcmp(mapped, "original", 8) == 0, "mapping followed the replacement descriptor instead of the pinned shared-memory object");
mapped[0] = 0x70;
char byte = 0;
Require(pread_nid_postfix(verifier, &byte, 1, 0) == 1 && byte == 0x70, "mapping did not update the original shared-memory object");
Require(munmap_nid_postfix(mapped, Page) == 0, "guest mapping cleanup failed");
Require(close_nid_postfix(descriptor) == 0 && close_nid_postfix(verifier) == 0, "guest descriptor cleanup failed");
Require(::close(replacement) == 0, "replacement descriptor cleanup failed");
std::puts("named shared-memory descriptor reuse checks passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}

View File

@ -99,7 +99,7 @@ static void Run() {
const int longDescriptor = longName.Open(create | readWrite, 07777);
umask(previousMask);
Require(longDescriptor >= 0 && fstat(longDescriptor, &info) == 0, "long name creation failed");
Require((info.st_mode & 07777) == 0700, "mode or umask was not applied");
Require((info.st_mode & 07777) == 0755, "default guest umask was not applied independently of host umask");
Require(close(longDescriptor) == 0, "long name close failed");
const int longReader = longName.Open(0);
Require(longReader >= 0 && close(longReader) == 0, "maximum-length name reopen failed");

View File

@ -0,0 +1,83 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "nid/Sha1.hpp"
#include <cstdint>
#include <cstdio>
#include <stdexcept>
#include <string>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
extern "C" int APS5_VABI shm_open_nid_postfix(const char*, int, int);
extern "C" std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t);
static void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
class Masks {
public:
Masks(std::uint16_t guest, mode_t host) : guest(umask_nid_postfix(guest)), host(::umask(host)) {}
~Masks() { ::umask(host); umask_nid_postfix(guest); }
Masks(const Masks&) = delete;
Masks& operator=(const Masks&) = delete;
private:
std::uint16_t guest;
mode_t host;
};
class Object {
public:
explicit Object(const char* suffix) : name("/anyps5-permissions-" + std::to_string(getpid()) + suffix) {
const auto digest = Nid::Sha1(std::vector<std::uint8_t>(name.begin(), name.end()));
for (auto byte : digest) {
hostName += "0123456789abcdef"[byte >> 4];
hostName += "0123456789abcdef"[byte & 15];
}
::shm_unlink(hostName.c_str());
}
~Object() { if (descriptor >= 0) ::close(descriptor); ::shm_unlink(hostName.c_str()); }
Object(const Object&) = delete;
Object& operator=(const Object&) = delete;
std::string name;
int descriptor = -1;
private:
std::string hostName = "/anyps5-shm-";
};
static void CheckMode(const char* suffix, std::uint16_t guestMask, mode_t hostMask, int requested, mode_t expected) {
Masks masks(guestMask, hostMask);
Object object(suffix);
object.descriptor = shm_open_nid_postfix(object.name.c_str(), 2 | 0x200 | 0x800, requested);
Require(object.descriptor >= 0, "shared-memory creation failed");
struct stat info{};
Require(::fstat(object.descriptor, &info) == 0, "stat failed");
if ((info.st_mode & 07777) != expected) {
std::fprintf(stderr, "requested=%o guest-mask=%o host-mask=%o actual=%o expected=%o\n", requested, guestMask, hostMask, info.st_mode & 07777, expected);
throw std::runtime_error("shared-memory mode did not follow the guest umask");
}
const auto observedHostMask = ::umask(hostMask);
Require(observedHostMask == hostMask, "guest creation changed host umask");
if (expected == 0600) {
const int reopened = shm_open_nid_postfix(object.name.c_str(), 2 | 0x200, 0777);
Require(reopened >= 0, "existing-object reopen failed");
const int result = ::fstat(reopened, &info);
::close(reopened);
Require(result == 0 && (info.st_mode & 07777) == expected, "reopen changed existing permissions");
}
}
int main() {
try {
CheckMode("-guest-restricts", 0077, 0000, 0666, 0600);
CheckMode("-host-restricts", 0000, 0077, 0666, 0666);
CheckMode("-guest-denies", 0777, 0000, 0777, 0000);
CheckMode("-special-bits", 0022, 0077, 07777, 0755);
std::puts("guest shared-memory permissions passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}

View File

@ -396,7 +396,7 @@ Throughout the project, every function at every stage either **does exactly what
- [Pending storage-image results](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). `APS5_STORE_AT_FLIP=1` stores every pending result at each flip; storing them at the title's GPU waits (labels, fences) is unimplemented.
- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented.
- [Storage-image units under a Linux import window](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) that the window stamped as a possible CPU store (`BlockMaybeWritten`) are compared with the bytes the image was uploaded from or written back as, and are unchanged when those still match. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Images tracked by layer instead of by 64 KiB unit still read the window as a change and upload again.
- [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. 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. [Guest mappings](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified.
- [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. [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. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified.
- [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw
- [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported
- [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries
@ -431,7 +431,7 @@ Throughout the project, every function at every stage either **does exactly what
- [waitpid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`.
- [execv](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it.
- [execvp](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created.
- [fork](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, but files are still created with the host process's mask
- [fork](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask
- [MIMG BY2/BY4 loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_by2/by4`, opcodes 0x42, 0x43, 0x4a and 0x4b) were measured on RDNA2 (gfx1035) with the [hardware oracle](HW_ORACLE.md) for 2D images with identity swizzle. They read N texels (BY2: 2, BY4: 4) from `x & ~(N - 1)` on row y, texel by texel and component by component within each texel, with each texel converted like `image_load`. Every register is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when the N texels fit in one dword: R8 and R16 (DMASK 0x3) or RG8 (0xf) for BY2, and R8 (0xf) for BY4. With 32-bit texels (R32, RG32, RG16, RGBA8) the hardware returns zeros. Other formats, masks, dimensions, swizzles, D16/A16 and indirect descriptors throw. The stores (0x52, 0x53, 0x5a and 0x5b) decode but throw at translation. Measured on hardware, they write from the unaligned x up to the end of its dword, which is not implemented.
- [2D stores to 1D storage images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp): an `image_store` with DIM 2D through a 1D T# writes texel x and ignores y, as measured on RDNA2 (gfx1035) for y from 0 to 4 and 0xffffffff, single- and four-component formats and a 1x1 8_8_8_8_UNORM T#: the T# type decides the addressing, DIM only how many address VGPRs are read. Storage loads, atomics and `image_store_mip` through such a pair still throw
- [MIMG PCK2/PCK4 loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_pck2/pck4`, opcodes 0x70, 0x71, 0x73 and 0x74) were measured on RDNA2 (gfx1035) with the [hardware oracle](HW_ORACLE.md) for 2D images with identity swizzle. With data mask 0x1, they write one dword: the raw bits of the N texels (PCK2: 2, PCK4: 4) from `x & ~(N - 1)` on row y, concatenated (element k at bit `k * texel bits`) and zero-extended. The dword is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when N texels fit in 32 bits: R8, R16 or RG8 for PCK2, R8 for PCK4. Wider texels (R32, RG16, RGBA8) and masks other than 0x1 write zeros on hardware. Those masks throw at decode, and formats outside UNORM/UINT/SINT R8, R16 and RG8 throw. #1314's earlier measurement saw only zeros because it used masks 0x3 and 0xf. The stores (0x76, 0x77, 0x79 and 0x7a) decode but throw at translation. They are not measured, and opcode 0x76 on 32_32_32_32_FLOAT hung the GPU (#1314).