diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 11002b4f93c..84b32480ad3 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -31,10 +31,28 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif #endif namespace { +#ifndef _WIN32 +std::filesystem::path HostDescriptorTarget(int fd, std::error_code& error) { +#ifdef __APPLE__ + char buffer[MAXPATHLEN]; + if (::fcntl(fd, F_GETPATH, buffer) != 0) { + error = std::error_code(errno, std::generic_category()); + return {}; + } + return std::filesystem::path(buffer); +#else + return std::filesystem::read_symlink(std::filesystem::path("/proc/self/fd") / std::to_string(fd), error); +#endif +} +#endif + constexpr std::uint8_t GuestUnknownType = 0; constexpr std::uint8_t GuestDirectoryType = 4; constexpr std::uint8_t GuestRegularType = 8; @@ -301,8 +319,7 @@ std::optional DirectoryDescriptorPath(int fd) { #else if (fd < 0) return std::nullopt; std::error_code error; - const auto link = std::filesystem::path("/proc/self/fd") / std::to_string(fd); - const auto target = std::filesystem::read_symlink(link, error); + const auto target = HostDescriptorTarget(fd, error); if (error) return std::nullopt; return target; #endif @@ -312,8 +329,7 @@ std::optional DirectoryDescriptorPath(int fd) { #else if (fd < 0) return std::nullopt; std::error_code error; - const auto link = std::filesystem::path("/proc/self/fd") / std::to_string(fd); - const auto target = std::filesystem::read_symlink(link, error); + const auto target = HostDescriptorTarget(fd, error); if (error) return std::nullopt; if (!std::filesystem::is_directory(target, error)) return std::nullopt; return target; diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 100bf5a9a3e..a7bfb34c90d 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -73,6 +73,49 @@ static int DoLstat(const std::filesystem::path& p, NativeStat* st) { st->isSymbolicLink = true; return 0; } +#elif defined(__APPLE__) +#include +#include +#include +#include +using NativeStat = struct stat; +static int StatAt(const std::filesystem::path& p, NativeStat* st, int flags) { + std::string rest = p.native(); + int directory = AT_FDCWD; + while (rest.size() >= PATH_MAX) { + const std::size_t split = rest.rfind('/', PATH_MAX - 1); + if (split == std::string::npos) { + if (directory != AT_FDCWD) ::close(directory); + errno = ENAMETOOLONG; + return -1; + } + const std::string prefix = split == 0 ? std::string("/") : rest.substr(0, split); + const int next = ::openat(directory, prefix.c_str(), O_SEARCH | O_CLOEXEC); + const int error = errno; + if (directory != AT_FDCWD) ::close(directory); + if (next < 0) { + errno = error; + return -1; + } + directory = next; + rest.erase(0, split + 1); + rest.erase(0, rest.find_first_not_of('/')); + } + const int result = ::fstatat(directory, rest.empty() ? "." : rest.c_str(), st, flags); + const int error = errno; + if (directory != AT_FDCWD) ::close(directory); + errno = error; + return result; +} +static int DoStat(const std::filesystem::path& p, NativeStat* st) { + return StatAt(p, st, 0); +} +static int DoFstat(int fd, NativeStat* st) { + return ::fstat(fd, st); +} +static int DoLstat(const std::filesystem::path& p, NativeStat* st) { + return StatAt(p, st, AT_SYMLINK_NOFOLLOW); +} #else #include using NativeStat = struct stat; @@ -129,12 +172,21 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { sb->st_rdev = static_cast(st.st_rdev); sb->st_blksize = static_cast(st.st_blksize); sb->st_blocks = static_cast(st.st_blocks); +#if defined(__APPLE__) + sb->st_atim.tv_sec = static_cast(st.st_atimespec.tv_sec); + sb->st_atim.tv_nsec = static_cast(st.st_atimespec.tv_nsec); + sb->st_mtim.tv_sec = static_cast(st.st_mtimespec.tv_sec); + sb->st_mtim.tv_nsec = static_cast(st.st_mtimespec.tv_nsec); + sb->st_ctim.tv_sec = static_cast(st.st_ctimespec.tv_sec); + sb->st_ctim.tv_nsec = static_cast(st.st_ctimespec.tv_nsec); +#else sb->st_atim.tv_sec = static_cast(st.st_atim.tv_sec); sb->st_atim.tv_nsec = static_cast(st.st_atim.tv_nsec); sb->st_mtim.tv_sec = static_cast(st.st_mtim.tv_sec); sb->st_mtim.tv_nsec = static_cast(st.st_mtim.tv_nsec); sb->st_ctim.tv_sec = static_cast(st.st_ctim.tv_sec); sb->st_ctim.tv_nsec = static_cast(st.st_ctim.tv_nsec); +#endif #if defined(__APPLE__) sb->st_birthtim.tv_sec = static_cast(st.st_birthtimespec.tv_sec); sb->st_birthtim.tv_nsec = static_cast(st.st_birthtimespec.tv_nsec); diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index 394c1387a84..e2c2a05e24e 100644 --- a/core/libs/prx/libkernel/File/src/SharedMemory.cpp +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -40,6 +40,10 @@ extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handle #include #include #include +#ifdef __APPLE__ +#include +#include +#endif #endif namespace { @@ -118,6 +122,20 @@ bool IsHostSharedMemoryDescriptor(int descriptor) { return std::all_of(value.begin(), value.end(), [](wchar_t character) { return character >= L'0' && character <= L'9'; }); }; return decimal(suffix.substr(0, separator)) && decimal(suffix.substr(separator + 1)); +#elif defined(__APPLE__) + struct stat status{}; + if (::fstat(descriptor, &status) != 0 || !S_ISREG(status.st_mode) || status.st_nlink != 0) return false; + char objectPath[MAXPATHLEN]; + if (::fcntl(descriptor, F_GETPATH, objectPath) != 0) return false; + const std::string_view path(objectPath); + const auto slash = path.rfind('/'); + if (slash == std::string_view::npos || !path.substr(slash + 1).starts_with("anyps5-shm-")) return false; + static const std::string directory = [] { + std::error_code error; + auto canonical = std::filesystem::canonical(std::filesystem::temp_directory_path(error), error); + return error ? std::string{} : canonical.string(); + }(); + return !directory.empty() && std::string_view(objectPath, slash) == directory; #else (void)descriptor; return false; @@ -601,6 +619,9 @@ int OpenNamedFile(const char* path, int flags, int mode) { int validationError = 0; const auto hostName = NamedHostName(path, validationError); if (validationError != 0) return Fail(NamedGuestError(validationError) ? NamedGuestError(validationError) : GuestInvalid); +#ifdef __APPLE__ + NotImplemented_nid_no_patch("shm_open"); +#endif HostShmLock namespaceLock; if (namespaceLock.Error() != 0) { errno = namespaceLock.Error(); @@ -677,6 +698,11 @@ int CreateAnonymousFile() { } errno = EEXIST; return -1; +#elif defined(__APPLE__) + auto path = (std::filesystem::temp_directory_path() / "anyps5-shm-XXXXXX").string(); + const int descriptor = mkostemp(path.data(), O_CLOEXEC); + if (descriptor >= 0) ::unlink(path.c_str()); + return descriptor; #else const int descriptor = memfd_create("anyps5 shm_open", MFD_CLOEXEC); return descriptor; @@ -1040,6 +1066,9 @@ extern "C" int APS5_VABI shm_unlink_nid_postfix(const char* path) { int validationError = 0; const auto hostName = NamedHostName(path, validationError); if (validationError != 0) return Fail(NamedGuestError(validationError) ? NamedGuestError(validationError) : GuestInvalid); +#ifdef __APPLE__ + NotImplemented_nid_no_patch("shm_unlink"); +#endif HostShmLock namespaceLock; if (namespaceLock.Error() != 0) { errno = namespaceLock.Error(); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index a0d6f805509..9b3b49635d9 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -373,6 +373,9 @@ static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nb #include #include #include +#ifdef __APPLE__ +extern "C" ssize_t __getdirentries64(int fd, void* buffer, size_t bytes, off_t* base); +#endif static int NativeRmdir(const std::filesystem::path& path) { return ::rmdir(path.c_str()); } @@ -596,26 +599,46 @@ static bool NativeIsPipe(int descriptor) { } #else #include +#ifdef __APPLE__ +#include +#else #include +#endif static bool NativeIsPipe(int descriptor) { +#ifdef __APPLE__ + struct stat file{}; + struct statfs kind{}; + return ::fstat(descriptor, &file) == 0 && S_ISFIFO(file.st_mode) && ::fstatfs(descriptor, &kind) != 0; +#else constexpr long PipeFilesystem = 0x50495045; struct stat file{}; struct statfs kind{}; return ::fstat(descriptor, &file) == 0 && S_ISFIFO(file.st_mode) && ::fstatfs(descriptor, &kind) == 0 && static_cast(kind.f_type) == PipeFilesystem; +#endif } static int NativeFstatfs(int descriptor, KernelStatfs* fs) { struct stat file{}; if (::fstat(descriptor, &file) != 0) return SceErrorFromErrno(errno) & 0xffff; +#ifdef __APPLE__ + if (S_ISSOCK(file.st_mode)) return GUEST_EINVAL; + if (NativeIsPipe(descriptor)) return GUEST_EINVAL; +#else if (S_ISSOCK(file.st_mode) && (::fcntl(descriptor, F_GETFL) & O_PATH) == 0) return GUEST_EINVAL; if (NativeIsPipe(descriptor)) return GUEST_EINVAL; +#endif if (S_ISCHR(file.st_mode)) return DeviceFilesystem(fs); struct statvfs host{}; if (::fstatvfs(descriptor, &host) != 0) return SceErrorFromErrno(errno) & 0xffff; if (host.f_flag & ST_RDONLY) fs->f_flags |= KERNEL_MNT_RDONLY; if (host.f_flag & ST_NOSUID) fs->f_flags |= KERNEL_MNT_NOSUID; +#ifdef __APPLE__ + struct statfs mount{}; + if (::fstatfs(descriptor, &mount) == 0 && (mount.f_flags & MNT_NOEXEC)) fs->f_flags |= KERNEL_MNT_NOEXEC; +#else if (host.f_flag & ST_NOEXEC) fs->f_flags |= KERNEL_MNT_NOEXEC; +#endif fs->f_bsize = host.f_frsize != 0 ? host.f_frsize : host.f_bsize; fs->f_iosize = host.f_bsize; fs->f_blocks = host.f_blocks; @@ -1205,6 +1228,8 @@ int APS5_VABI sceKernelFdatasync(int fd) { File::RefuseArchivedFile(fd, "fdatasync"); #ifdef _WIN32 if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); +#elif defined(__APPLE__) + if (::fsync(fd) != 0) return SceErrorFromErrno(errno); #else if (::fdatasync(fd) != 0) return SceErrorFromErrno(errno); #endif @@ -1255,6 +1280,85 @@ static bool InsideApp0(int fd) { return inside; } +#ifdef __APPLE__ +static unsigned char GuestEntryType(int fd, const struct dirent& entry, bool followLinks) { + if (entry.d_type != DT_LNK && entry.d_type != DT_UNKNOWN) return entry.d_type; + if (entry.d_type == DT_LNK && !followLinks) return DT_LNK; + const int savedErrno = errno; + struct stat target{}; + const bool found = ::fstatat(fd, entry.d_name, &target, followLinks ? 0 : AT_SYMLINK_NOFOLLOW) == 0; + errno = savedErrno; + if (!found) return entry.d_type; + if (S_ISDIR(target.st_mode)) return DT_DIR; + if (S_ISREG(target.st_mode)) return DT_REG; + if (S_ISLNK(target.st_mode)) return DT_LNK; + return entry.d_type; +} + +int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { + if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(fd, "getdirentries"); + constexpr std::size_t GuestHeaderBytes = 8; + constexpr std::size_t GuestMaxName = 255; + if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); + if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL); + const GuestArena::HostWrite destination(buf, static_cast(nbytes)); + const GuestArena::HostWrite baseDestination(basep, basep == nullptr ? 0 : sizeof(*basep)); + if (!destination.Open() || !baseDestination.Open()) return SceErrorFromErrno(GUEST_EFAULT); + if (File::IsDirectoryDescriptor(fd)) return File::ReadDirectoryDescriptor(fd, buf, nbytes, basep); + struct stat status{}; + if (::fstat(fd, &status) != 0) return SceErrorFromErrno(errno); + if (!S_ISDIR(status.st_mode)) return SceErrorFromErrno(GUEST_EINVAL); + const off_t base = ::lseek(fd, 0, SEEK_CUR); + if (base < 0) return SceErrorFromErrno(errno); + if (basep != nullptr) *basep = static_cast(base); + if (::lseek(fd, 0, SEEK_SET) < 0) return SceErrorFromErrno(errno); + std::vector listing; + for (;;) { + constexpr std::size_t Chunk = 64 * 1024; + const auto start = listing.size(); + listing.resize(start + Chunk); + off_t position = 0; + const auto read = ::__getdirentries64(fd, listing.data() + start, Chunk, &position); + if (read < 0) { + const int error = errno; + ::lseek(fd, base, SEEK_SET); + return SceErrorFromErrno(error); + } + listing.resize(start + static_cast(read)); + if (read == 0) break; + } + const bool followLinks = InsideApp0(fd); + std::size_t written = 0; + off_t index = 0; + for (std::size_t offset = 0; offset < listing.size(); ++index) { + const auto* entry = reinterpret_cast(listing.data() + offset); + if (entry->d_reclen == 0) break; + offset += entry->d_reclen; + if (index < base) continue; + const auto nameLength = std::strlen(entry->d_name); + const auto record = (GuestHeaderBytes + nameLength + 1 + 3) & ~std::size_t{3}; + if (nameLength > GuestMaxName || written + record > static_cast(nbytes)) { + if (::lseek(fd, index, SEEK_SET) < 0) return SceErrorFromErrno(errno); + if (written != 0) return static_cast(written); + return nameLength > GuestMaxName ? static_cast(0x80020000u | GUEST_ENAMETOOLONG) : SceErrorFromErrno(GUEST_EINVAL); + } + char* out = buf + written; + std::memset(out, 0, record); + const auto fileNumber = static_cast(entry->d_ino); + const auto recordLength = static_cast(record); + const auto nameBytes = static_cast(nameLength); + std::memcpy(out, &fileNumber, sizeof(fileNumber)); + std::memcpy(out + 4, &recordLength, sizeof(recordLength)); + out[6] = static_cast(GuestEntryType(fd, *entry, followLinks)); + out[7] = static_cast(nameBytes); + std::memcpy(out + GuestHeaderBytes, entry->d_name, nameLength); + written += record; + } + if (::lseek(fd, index, SEEK_SET) < 0) return SceErrorFromErrno(errno); + return static_cast(written); +} +#else static unsigned char GuestEntryType(int fd, const struct dirent64& entry, bool followLinks) { if (entry.d_type != DT_LNK && entry.d_type != DT_UNKNOWN) return entry.d_type; if (entry.d_type == DT_LNK && !followLinks) return DT_LNK; @@ -1324,6 +1428,7 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas if (::lseek(fd, resume, SEEK_SET) < 0) return SceErrorFromErrno(errno); return static_cast(written); } +#endif int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { return sceKernelGetdirentries(fd, buf, nbytes, nullptr); @@ -1556,6 +1661,16 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t return result < 0 ? SceErrorFromErrno(errno) : result; } +#ifdef __APPLE__ +static std::int64_t EmptyIovecsResult(int d, bool write, bool positioned) { + const int flags = ::fcntl(d, F_GETFL); + if (flags < 0) return SceErrorFromErrno(errno); + if ((flags & O_ACCMODE) == (write ? O_RDONLY : O_WRONLY)) return SceErrorFromErrno(EBADF); + if (positioned && ::lseek(d, 0, SEEK_CUR) < 0) return SceErrorFromErrno(errno); + return 0; +} +#endif + int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* guestIov, int iovcnt) { if (File::IsDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); std::vector vectors; @@ -1569,6 +1684,9 @@ int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* guestIov, int iovcnt) } if (File::IsRandomDevice(d)) return ReadRandomIovecs(d, iov, iovcnt); if (const auto archived = File::ReadArchivedIovecs(d, iov, iovcnt, nullptr)) return *archived; +#ifdef __APPLE__ + if (iovcnt == 0) return EmptyIovecsResult(d, false, false); +#endif const auto result = static_cast(::readv(d, NativeIovecs(iov), iovcnt)); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1586,6 +1704,9 @@ int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* guestIov, int iovcnt PrepareWriteIovecs(iov, iovcnt); const auto sharedResult = File::WritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt); if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult; +#ifdef __APPLE__ + if (iovcnt == 0) return EmptyIovecsResult(d, true, false); +#endif const auto result = static_cast(::writev(d, NativeIovecs(iov), iovcnt)); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1601,6 +1722,9 @@ int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* guestIov, int iovcnt if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); if (File::IsRandomDevice(d)) return ReadRandomIovecs(d, iov, iovcnt); if (const auto archived = File::ReadArchivedIovecs(d, iov, iovcnt, &offset)) return *archived; +#ifdef __APPLE__ + if (iovcnt == 0) return EmptyIovecsResult(d, false, true); +#endif const auto result = static_cast(::preadv(d, NativeIovecs(iov), iovcnt, static_cast(offset))); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1617,6 +1741,7 @@ int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* guestIov, int iovcn const auto sharedResult = File::PwritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt, offset); if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult; #ifdef __APPLE__ + if (iovcnt == 0) return EmptyIovecsResult(d, true, true); if (const auto appended = File::WritevAtEnd(d, NativeIovecs(iov), iovcnt); appended != -2) return appended < 0 ? SceErrorFromErrno(errno) : appended; #endif const auto result = static_cast(::pwritev(d, NativeIovecs(iov), iovcnt, static_cast(offset))); diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index a0f46af6d26..250680c0cd0 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -23,11 +24,17 @@ #include #else #include +#ifdef __APPLE__ +#include +#include +#include +#else #include #ifdef __linux__ #include #endif #endif +#endif #include "prx/libc/include/HostThreadLocal.hpp" #ifdef _WIN32 @@ -73,6 +80,10 @@ struct Module { bool global = false; #ifdef _WIN32 std::uintptr_t globalOrder = 0; +#endif +#ifdef __APPLE__ + const void* image = nullptr; + const void* loadBias = nullptr; #endif ~Module() { if (owned && native) { @@ -96,7 +107,59 @@ void* Symbol(Module& module, const char* name) { return ::dlsym(module.native, name); #endif } -#ifndef _WIN32 +#ifdef __APPLE__ +bool FromGuestModule(void* address) { + Dl_info info{}; + if (address == nullptr || !::dladdr(address, &info) || info.dli_fname == nullptr || info.dli_fbase == nullptr) return false; + if (static_cast(info.dli_fbase)->filetype == MH_EXECUTE) return true; + const std::string path(info.dli_fname); + return path.ends_with(".prx"); +} + +void FindImage(Module& module, bool mainProgram) { + for (std::uint32_t index = 0; index < _dyld_image_count() && module.image == nullptr; ++index) { + const auto* header = reinterpret_cast(_dyld_get_image_header(index)); + if (header == nullptr) continue; + if (mainProgram) { + if (header->filetype == MH_EXECUTE) module.image = header; + continue; + } + void* loaded = ::dlopen(_dyld_get_image_name(index), RTLD_LAZY | RTLD_NOLOAD); + if (loaded == nullptr) continue; + if (loaded == module.native) module.image = header; + ::dlclose(loaded); + } + module.loadBias = module.image; + unsigned long size = 0; + const auto* meta = module.image ? getsectiondata(static_cast(module.image), "__APS5DATA", "__meta", &size) : nullptr; + std::uint64_t version = 0; + if (meta != nullptr && size >= 0x60 && (std::memcpy(&version, meta, sizeof(version)), version == 2)) { + std::uint64_t loadBias = 0; + std::memcpy(&loadBias, meta + 0x08, sizeof(loadBias)); + module.loadBias = reinterpret_cast(static_cast(loadBias)); + } +} + +bool HiddenImage(const void* image) { + if (image == nullptr || static_cast(image)->filetype == MH_EXECUTE) return false; + bool local = false; + for (const auto& [key, module] : modules) { + if (module->image != image) continue; + if (module->global) return false; + local = true; + } + return local; +} + +void* DefaultScopeSymbol(const char* symbolName) { + auto* result = ::dlsym(RTLD_DEFAULT, symbolName); + if (!FromGuestModule(result)) return nullptr; + Dl_info info{}; + if (!::dladdr(result, &info) || info.dli_fbase == nullptr) return result; + std::lock_guard lock(modulesMutex); + return HiddenImage(info.dli_fbase) ? nullptr : result; +} +#elif !defined(_WIN32) const link_map* MainProgramMap() { static const link_map* const map = [] { void* handle = ::dlopen(nullptr, RTLD_LAZY); @@ -278,8 +341,12 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { #else const auto source = path ? ResolvePath_nid_no_patch(path) : std::filesystem::path{}; const auto resolved = path ? RelinkedModulePath_nid_no_patch(source) : std::filesystem::path{}; +#ifdef __APPLE__ + const int nativeFlags = ((flags & 3) == 1 ? RTLD_LAZY : RTLD_NOW) | RTLD_GLOBAL; +#else const int nativeFlags = ((flags & 3) == 1 ? RTLD_LAZY : RTLD_NOW) | ((flags & 0x100) ? RTLD_GLOBAL : RTLD_LOCAL); +#endif module->native = ::dlopen(path ? resolved.c_str() : nullptr, nativeFlags); if (!module->native) { if (path) LoadFailure(source, resolved, ::dlerror()); else Error(::dlerror()); @@ -288,6 +355,9 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { #ifdef __linux__ if (path) InitializeLinuxGuestModule(module->native); #endif +#ifdef __APPLE__ + FindImage(*module, path == nullptr); +#endif #endif std::lock_guard lock(modulesMutex); const auto handle = nextHandle++; @@ -313,9 +383,17 @@ void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name) { if (!name || !*name) { Error("dlsym: empty symbol name"); return nullptr; } try { std::vector> search; + bool defaultScope = handle == nullptr || handle == reinterpret_cast(static_cast(-2)); { std::lock_guard lock(modulesMutex); - if (handle == nullptr || handle == reinterpret_cast(static_cast(-2))) { +#ifdef __APPLE__ + if (!defaultScope) { + auto found = modules.find(reinterpret_cast(handle)); + defaultScope = found != modules.end() && found->second->image != nullptr && + static_cast(found->second->image)->filetype == MH_EXECUTE; + } +#endif + if (defaultScope) { for (const auto& [key, module] : modules) if (module->global) search.push_back(module); #ifdef _WIN32 std::sort(search.begin(), search.end(), [](const auto& left, const auto& right) { return left->globalOrder < right->globalOrder; }); @@ -329,7 +407,7 @@ void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name) { } for (const auto& module : search) if (auto* result = FindSymbol(*module, name)) return result; #ifndef _WIN32 - if (handle == nullptr || handle == reinterpret_cast(static_cast(-2))) { + if (defaultScope) { if (auto* result = DefaultScopeSymbol(name)) return result; const auto nid = Nid::ComputeNid(name, ""); if (auto* result = DefaultScopeSymbol(nid.c_str())) return result; @@ -358,6 +436,8 @@ int APS5_VABI dlclose_nid_postfix(void* handle) { GuestAllocations::Mutation mutation; #ifdef _WIN32 mutation.UnregisterImage(module->native); +#elif defined(__APPLE__) + mutation.UnregisterImage(module->loadBias); #else mutation.UnregisterImage(reinterpret_cast(static_cast(module->native)->l_addr)); #endif diff --git a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp index 37115f46f1d..b9b95408ff6 100644 --- a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp +++ b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp @@ -23,7 +23,14 @@ #include #else #include +#ifdef __APPLE__ +#include +#include +#include +#include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#else #include +#endif #include #include #include @@ -72,7 +79,159 @@ std::uint64_t PageSpan(std::uint64_t address, std::uint64_t size) { return ((address + size + pageMask) & ~pageMask) - (address & ~pageMask); } -#ifndef _WIN32 +#if defined(__APPLE__) +struct MachImage { + const mach_header_64* header; + std::uintptr_t slide; + const char* path; +}; + +std::vector MachImages() { + std::vector images; + const auto count = _dyld_image_count(); + for (std::uint32_t index = 0; index < count; ++index) { + const auto* header = reinterpret_cast(_dyld_get_image_header(index)); + if (header == nullptr || header->magic != MH_MAGIC_64) continue; + const MachImage image{header, static_cast(_dyld_get_image_vmaddr_slide(index)), _dyld_get_image_name(index)}; + if (header->filetype == MH_EXECUTE) images.insert(images.begin(), image); + else images.push_back(image); + } + return images; +} + +template +void ForEachSegment(const MachImage& image, TVisit&& visit) { + const auto* command = reinterpret_cast(image.header + 1); + for (std::uint32_t i = 0; i < image.header->ncmds; ++i) { + if (command->cmd == LC_SEGMENT_64) visit(*reinterpret_cast(command)); + command = reinterpret_cast(reinterpret_cast(command) + command->cmdsize); + } +} + +bool GuestSegment(const segment_command_64& segment) { + return std::strncmp(segment.segname, "__ELF", 5) == 0; +} + +bool Relinked(const MachImage& image) { + bool relinked = false; + ForEachSegment(image, [&](const segment_command_64& segment) { relinked = relinked || GuestSegment(segment); }); + return relinked; +} + +bool Reported(const segment_command_64& segment, bool relinked) { + if (segment.vmsize == 0 || (segment.initprot & (VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE)) == 0) return false; + return relinked ? GuestSegment(segment) : std::strncmp(segment.segname, SEG_LINKEDIT, sizeof(segment.segname)) != 0; +} + +std::int32_t Protection(const segment_command_64& segment) { + return ((segment.initprot & VM_PROT_READ) ? ProtRead : 0) | ((segment.initprot & VM_PROT_WRITE) ? ProtWrite : 0) | + ((segment.initprot & VM_PROT_EXECUTE) ? ProtExecute : 0); +} + +std::string_view ImagePath(const MachImage& image) { + return image.path ? image.path : ""; +} + +std::string ImageName(const MachImage& image) { + const std::string path(ImagePath(image)); + return ModuleName(path.substr(path.find_last_of('/') + 1)); +} + +void* ImageHandle(const MachImage& image) { + void* handle = ::dlopen(image.header->filetype == MH_EXECUTE ? nullptr : image.path, RTLD_LAZY | RTLD_NOLOAD); + if (!handle) throw std::runtime_error("module info: cannot open a loaded image"); + ::dlclose(handle); + return handle; +} + +const MachImage* ImageContaining(const std::vector& images, std::uintptr_t address) { + for (const auto& image : images) { + bool found = false; + ForEachSegment(image, [&](const segment_command_64& segment) { + const auto start = image.slide + segment.vmaddr; + if (segment.vmsize != 0 && std::strncmp(segment.segname, SEG_PAGEZERO, sizeof(segment.segname)) != 0 && + address >= start && address - start < segment.vmsize) + found = true; + }); + if (found) return ℑ + } + return nullptr; +} + +template +bool Fill(std::uintptr_t address, TInfo& info) { + const auto images = MachImages(); + const auto* image = ImageContaining(images, address); + if (!image) return false; + SetName(info, ImageName(*image)); + const bool relinked = Relinked(*image); + ForEachSegment(*image, [&](const segment_command_64& command) { + if (!Reported(command, relinked) || info.segment_count >= std::size(info.segments)) return; + auto& segment = info.segments[info.segment_count++]; + segment.address = image->slide + command.vmaddr; + segment.size = ToU32(command.vmsize, "segment size"); + segment.prot = Protection(command); + }); + if constexpr (Extended) { + info.id = ModuleIdForImage_nid_no_patch(ImageHandle(*image)); + info.ref_count = 1; + unsigned long size = 0; + if (const auto* frames = getsectiondata(image->header, SEG_TEXT, "__eh_frame", &size); frames && size != 0) { + info.eh_frame_addr = reinterpret_cast(frames); + info.eh_frame_size = ToU32(size, "eh_frame size"); + } + if (const auto* meta = getsectiondata(image->header, "__APS5DATA", "__meta", &size); meta && size >= 0x60) { + std::uint64_t version = 0, loadBias = 0, count = 0; + std::memcpy(&version, meta, sizeof(version)); + std::memcpy(&loadBias, meta + 0x08, sizeof(loadBias)); + std::memcpy(&count, meta + 0x58, sizeof(count)); + if (version == 2 && count <= (size - 0x60) / sizeof(Elf64_Phdr)) { + for (std::uint64_t index = 0; index < count; ++index) { + Elf64_Phdr header; + std::memcpy(&header, meta + 0x60 + index * sizeof(header), sizeof(header)); + if (header.p_type != PT_GNU_EH_FRAME) continue; + info.eh_frame_hdr_addr = loadBias + header.p_vaddr; + info.eh_frame_hdr_size = ToU32(header.p_memsz, "eh_frame_hdr size"); + break; + } + } + } + } + return true; +} + +std::vector GuestImages() { + std::vector images; + for (const auto& image : MachImages()) { + if (image.header->filetype != MH_EXECUTE && !ImagePath(image).ends_with(".prx")) continue; + images.push_back({ImageHandle(image), reinterpret_cast(image.header)}); + } + return images; +} + +std::uint64_t GuestSegmentBytes() { + constexpr auto allAccess = VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE; + std::uint64_t bytes = 0; + for (const auto& image : MachImages()) { + if (image.header->filetype != MH_EXECUTE && !ImagePath(image).ends_with(GuestModuleSuffix)) continue; + const bool relinked = Relinked(image); + ForEachSegment(image, [&](const segment_command_64& segment) { + const bool header = std::strncmp(segment.segname, SEG_TEXT, sizeof(segment.segname)) == 0; + if (Reported(segment, relinked) && (relinked || !header) && (segment.initprot & allAccess) != allAccess) + bytes += PageSpan(segment.vmaddr, segment.vmsize); + }); + } + return bytes; +} + +bool SystemImage(const GuestImage& image) { + const auto* header = reinterpret_cast(image.address); + if (header->filetype == MH_EXECUTE) return false; + Dl_info symbol{}; + if (!::dladdr(header, &symbol) || symbol.dli_fname == nullptr) return true; + return !std::string_view(symbol.dli_fname).ends_with(GuestModuleSuffix); +} +#elif !defined(_WIN32) template struct ImageSearch { std::uintptr_t address; @@ -366,7 +525,7 @@ int FillModuleInfo(KernelModule handle, TInfo* info, bool titleOnly) { if (titleOnly && SystemImage(image)) return SCE_KERNEL_ERROR_EPERM; TInfo result{}; result.st_size = sizeof(TInfo); -#ifdef _WIN32 +#if defined(_WIN32) || defined(__APPLE__) if (!Fill(image.address, result)) return SCE_KERNEL_ERROR_ESRCH; #else ImageSearch search{image.address, &result, false}; @@ -408,6 +567,11 @@ int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, M const bool found = Fill(static_cast(address), result) && VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) == sizeof(memory); const GuestImage image{nullptr, found ? reinterpret_cast(memory.AllocationBase) : 0}; +#elif defined(__APPLE__) + Dl_info symbol{}; + const bool found = Fill(static_cast(address), result) && + ::dladdr(reinterpret_cast(address), &symbol) != 0 && symbol.dli_fbase != nullptr; + const GuestImage image{nullptr, found ? reinterpret_cast(symbol.dli_fbase) : 0}; #else Dl_info symbol{}; link_map* native = nullptr; @@ -457,7 +621,7 @@ std::int32_t APS5_VABI sceKernelGetLibkernelTextLocation_nid_postfix(void** addr const auto images = GuestImages(); if (images.empty()) throw std::runtime_error("sceKernelGetLibkernelTextLocation: the main program is not loaded"); ModuleInfo info{}; -#ifdef _WIN32 +#if defined(_WIN32) || defined(__APPLE__) const bool found = Fill(images.front().address, info); #else ImageSearch search{images.front().address, &info, false}; diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index cbce3c4e096..9e83e595067 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -17,8 +17,12 @@ #include #else #include +#ifdef __APPLE__ +#include +#else #include #endif +#endif #ifdef _WIN32 extern "C" void ForgetUnloadedUnwindImages_nid_no_patch(); @@ -39,6 +43,15 @@ const void* imageContaining(const void* address, bool reference, void*& handle, if (!GetModuleHandleExW(flags, reinterpret_cast(address), &module)) return nullptr; if (reference) handle = module; return module; +#elif defined(__APPLE__) + Dl_info info{}; + if (!dladdr(address, &info) || !info.dli_fbase) return nullptr; + const auto* header = static_cast(info.dli_fbase); + if (reference && header->filetype != MH_EXECUTE && info.dli_fname && *info.dli_fname) { + handle = dlopen(info.dli_fname, RTLD_LAZY | RTLD_NOLOAD); + if (!handle) throw std::runtime_error(std::string(caller) + ": cannot reference " + info.dli_fname); + } + return info.dli_fbase; #else Dl_info info{}; link_map* image = nullptr; diff --git a/core/libs/tests/GuestModuleInfo.cpp b/core/libs/tests/GuestModuleInfo.cpp index fd112c8f695..531b9050c8b 100644 --- a/core/libs/tests/GuestModuleInfo.cpp +++ b/core/libs/tests/GuestModuleInfo.cpp @@ -12,6 +12,10 @@ #include #include #include +#ifdef __APPLE__ +#include +#include +#endif static void Require(bool value, const std::source_location& location = std::source_location::current()) { if (!value) { @@ -54,10 +58,15 @@ int main(int argc, char** argv) { Require(executable); #if defined(_WIN32) && defined(__SEH__) Require(info.eh_frame_addr == 0 && info.eh_frame_size == 0); +#elif defined(__APPLE__) + unsigned long frameSize = 0; + const auto* frames = getsectiondata(reinterpret_cast(info.segments[0].address), SEG_TEXT, "__eh_frame", &frameSize); + Require(info.eh_frame_addr == reinterpret_cast(frames) && info.eh_frame_size == frameSize); + Require(info.eh_frame_hdr_addr == 0 && info.eh_frame_hdr_size == 0); #else Require(info.eh_frame_addr != 0 && info.eh_frame_size != 0); #endif -#ifndef _WIN32 +#if !defined(_WIN32) && !defined(__APPLE__) Require(info.eh_frame_hdr_addr != 0 && info.eh_frame_hdr_size != 0); #endif Require(info.init_proc_addr == 0 || info.init_proc_addr >= info.segments[0].address); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index f686a0d3985..471e35352be 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -63,6 +63,7 @@ - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, and on macOS from the relinked image's program headers; both report an `eh_frame_size` that includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped after a loader free (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release) when the module is no longer loaded. A table changed in place, or an image freed by anything other than the loader, is not noticed. An address inside an image of the Windows loader's module list whose tables are cached is resolved with `RtlPcToFileHeader` and no `VirtualQuery`; other addresses, and images without tables, still use `VirtualQuery`. The `.eh_frame` walk that finds its end is done once per image instead of once per call - [sceKernelGetModuleInfoFromAddr](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - as in a payload process on a PS5 (firmware 13.60), flags 1 finds any module and flags 2 refuses the system ones (`ESRCH`), here the host libraries; other flags clear the 0x1A8 bytes and return `EINVAL`, any failure clears them, `st_size` is left 0, and a null info returns `EFAULT` (with other flags the console faulted; here it throws). Flags 2 finding a title module was not measured. The layout follows [shadPS4](https://github.com/shadps4-emu/shadPS4). The id is the loader handle or a stable id from that range, and the name is the host file name without `.guest.prx`, where the console keeps `.sprx`. Only the first 4 load segments are reported. `tls_offset` is 0 and `ref_count` 1 (the console gave 5 for system modules). On Windows a relinked image reports its `.elf` sections and the `.ehmeta` eh_frame_hdr; the TLS fields and init/fini are 0 +- [sceKernelGetModuleInfoFromAddr](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - on macOS the TLS fields, `init_proc_addr` and `fini_proc_addr` are 0, and `ref_count` is 1. `eh_frame_hdr_addr` and `eh_frame_hdr_size` come from the `PT_GNU_EH_FRAME` header in a relinked image's `__APS5DATA,__meta` section, as for `sceKernelGetModuleInfoForUnwind`, and are 0 for other images. `eh_frame_addr` and `eh_frame_size` come from the image's `__TEXT,__eh_frame` section and are 0 for an image without one; the Apple linker emits none for most images, and the `guest_module_info` fixture and test executable have none, so `GuestModuleInfo.cpp` compares the reported values with `getsectiondata` but does not cover a non-empty Mach-O `__eh_frame` - [sceKernelGetModuleList](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - lists the main program and every loaded `.prx` image in load order with the ids `sceKernelGetModuleInfoFromAddr` reports, where a payload process on PS5 firmware 13.60 got only its main program, as handle 0, from both `sceKernelGetModuleList` and `sceKernelGetModuleList2`, though libkernel (handle 1) and libSceLibcInternal (handle 2) were loaded; what a title gets was not measured. Otherwise it follows that console: a null handle array returns `EFAULT` without writing `actual`; with more images than `count` the first `count` handles are written and `ENOMEM` returned without writing `actual` (nothing is written for a count of 0); a null `actual` returns `EFAULT` after the handles are written. The `(int32_t* handles, size_t count, size_t* actual)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/kernel/process.cpp#L244-L264) - [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it - [sceKernelGetModuleList2 and sceKernelGetModuleInfo2](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the title-module variants follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp): `sceKernelGetModuleList2` lists what `sceKernelGetModuleList` lists without system libraries, and `sceKernelGetModuleInfo2` answers `EPERM` for a system library after the `EFAULT`, `EINVAL` and `ESRCH` checks of `sceKernelGetModuleInfo`, as a payload process on PS5 firmware 13.60 got for libkernel's handle, a size of 0, a null info and a bad handle. A system library here is a loaded `.prx` image that is not a relinked title module (`.guest.prx`); the main program and the title's own modules are listed. Which modules the console lists is in the entry above. PPSA34063 imports both and fails to load without them @@ -154,6 +155,7 @@ - [sched_getparam, sched_getscheduler, sched_setparam](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the own pid (`getpid()`) throws from any thread but the one `pthread_main_np` reports, and on macOS also from the guest's main thread, which runs on a second host thread. In a payload process on a PS5 (firmware 12.70), `sched_getparam(getpid())` from another thread read the priority of the thread that ran the payload's main, which may not be the process's first thread; FreeBSD's [`p1003_1b.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/p1003_1b.c#L110-L289) selects the first thread. `sched_setparam` throws for a policy other than `SCHED_FIFO` or `SCHED_RR`; `EPERM` outside 256-767 was measured only on `SCHED_FIFO`. Unwritable or unreadable parameter stores return `EFAULT` per FreeBSD. `sched_setscheduler` stays unimplemented - [sceKernelUuidCreate, sceKernelCreateUuidV4](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) return EFAULT (0x8002000e) for an unwritable destination buffer as a host-safety fallback; console behaviour for unwritable memory has not been measured. - [Linux runtime module initialization](../../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) (libkernel) - modules in explicit runtime directories can be mapped for bindings before an explicit guest load runs their entry and init array once with the caller arguments. Finalizers run only after initialization completed. Console dependency-start and finalization ordering are unmeasured. Windows and macOS use their existing lifecycle paths. +- [macOS module scope](../../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) (libkernel) - every module is opened with `RTLD_GLOBAL`, whatever the guest flags ask. A module loaded without flag 0x100 is kept out of the default scope by `DefaultScopeSymbol`, which drops a `dlsym(RTLD_DEFAULT, ...)` result from such a module. A lookup on the main program's handle (`sceKernelDlsym` with handle 0) takes the same path, because macOS `dlopen(NULL)` returns a handle equivalent to `RTLD_DEFAULT`. Whether `RTLD_LOCAL` works for relinked modules on macOS has not been tested - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, in the POSIX `mprotect` as well (FreeBSD 11.4 masks the argument, `sys/vm/vm_mmap.c:613`; the console was measured only through `sceKernelMprotect`), as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`); here the five flexible and direct map calls do too, before mapping, but `sceKernelMtypeprotect` and the batch map calls throw @@ -183,6 +185,7 @@ - [ftruncate, truncate, sceKernelFtruncate](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) extend a file on Windows with `SetFileInformationByHandle` (`FileEndOfFileInfo`), which reserves the clusters but writes no zero bytes, and report `EFBIG` for a length the volume cannot hold (`ERROR_INVALID_PARAMETER` on a disk file; seen with a length of `INT64_MAX`, the size from which NTFS refuses it was not measured) and `ENOSPC` when the free space is smaller. FreeBSD 11.4 makes the extension a hole on a file system that has them ([`ffs_truncate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/ufs/ffs/ffs_inode.c) writes only the last block), so a length above the free space succeeds there. A write behind the end of the file still makes NTFS write zero bytes over the gap, as the file is not sparse. None of this was measured on a console. One title (PPSA04354) calls it at 0xe9d4f2 and tests only the sign of the result; the lengths it passes are not examined. - [Windows directory entry types](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) - types other than regular files, directories and symbolic links, and reparse points unsupported by `lstat`, throw. If the host cannot stat an entry, its type is `DT_UNKNOWN`. Console directory-entry types have not been measured; the host `lstat` modes map to `DT_REG`, `DT_DIR` and `DT_LNK` following [FreeBSD 11.4 dirent.h](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/sys/dirent.h). A host link inside canonical app0 is reported as its target's type (directory or regular file); a link elsewhere, or a dangling one, stays a link. A directory opened through an app0 link resolves to its target's host path, so links nested inside a target outside app0 keep the link type. - [readv, writev, preadv, pwritev and their sceKernel forms](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - unreadable iovec-table errors follow FreeBSD 11.4's [`copyinuio`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/subr_uio.c#L401-L434), not console measurements. The table is copied before inspecting its entries; payload-buffer validation is unchanged. A total length above `SSIZE_MAX` fails with `EINVAL` before the descriptor is looked up, and the positioned forms, `pread` and `pwrite` included, report `EBADF` for a descriptor not open in that direction, then `ESPIPE` for a pipe, socket or kqueue, before `EINVAL` or `EFAULT`, as [`kern_preadv`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c) does. FreeBSD passes a negative offset on a character device to the driver; here it still returns `EINVAL`. A Linux `process_vm_readv` failure other than `EFAULT` throws, including when the host denies that API. +- [macOS directory entries](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - `sceKernelGetdirentries` seeks to 0 and reads the whole directory again through the undocumented `__getdirentries64` on every call, then skips the entries already returned, so paging through a directory of n entries reads it O(n²) times. The descriptor offset and `*basep` are entry indexes, not host directory cookies, so a host `readdir` or `lseek` on the same descriptor lands on the wrong entry Because the offset is an entry index into a fresh listing, an entry removed between two calls shifts later entries down and one of them is skipped, and an entry created between two calls can be returned twice; FreeBSD uses the file system's directory cookie, which does not move. A title that reads a batch, deletes it and reads again can leave entries behind - [/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.