fix(libs): take guest entropy from the host OS instead of std::random_device

std::random_device in libstdc++ reads RDSEED on x86 Linux. On an AMD
EPYC 9V45 (Zen 5) CI runner its 32-bit results repeat about 130 times
in 65536 draws, so guest_posix_random fails. libc now exports
HostEntropy_nid_no_patch, which uses getentropy (rand_s on Windows) and
throws when the OS cannot provide entropy. arc4random, arc4random_buf,
sceRandomGetRandomNumber, /dev/random and /dev/urandom reads,
std::_Random_device and sceKernelUuidCreate use it, and the arc4random
and random device TechnicalDebt entries say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
winstrex 2026-10-11 21:29:29 -03:00
parent 7a84b22267
commit 8f9efc25f2
11 changed files with 71 additions and 38 deletions

View File

@ -1,10 +1,9 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <algorithm>
#include "prx/libc/include/HostEntropy.hpp"
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <random>
#include <stdexcept>
namespace {
@ -172,12 +171,7 @@ char* APS5_VABI strcasestr_nid_postfix(const char* text, const char* needle) {
}
void APS5_VABI arc4random_buf_nid_postfix(void* buffer, std::size_t size) {
static thread_local std::random_device device;
auto* bytes = static_cast<unsigned char*>(buffer);
for (std::size_t offset = 0; offset < size; offset += sizeof(unsigned int)) {
const unsigned int value = device();
std::memcpy(bytes + offset, &value, std::min(sizeof(value), size - offset));
}
HostEntropy_nid_no_patch(buffer, size);
}
std::uint32_t APS5_VABI arc4random_nid_postfix() {

View File

@ -1,10 +1,8 @@
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <algorithm>
#include <random>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/HostEntropy.hpp"
namespace {
@ -17,12 +15,7 @@ extern "C" {
int APS5_VABI sceRandomGetRandomNumber(void* buf, size_t size) {
if (!buf || size > SCE_RANDOM_MAX_SIZE) return SCE_RANDOM_ERROR_INVALID;
static thread_local std::random_device device;
auto* bytes = static_cast<unsigned char*>(buf);
for (std::size_t offset = 0; offset < size; offset += sizeof(unsigned int)) {
const unsigned int value = device();
std::memcpy(bytes + offset, &value, std::min(sizeof(value), size - offset));
}
HostEntropy_nid_no_patch(buf, size);
return 0;
}

View File

@ -14,6 +14,7 @@ target_sources(libc PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/StringExtensions.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Math.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Random.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/HostEntropy.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/StdioExtras.cpp

View File

@ -0,0 +1,8 @@
#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTENTROPY_HPP
#define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTENTROPY_HPP
#include <cstddef>
extern "C" void HostEntropy_nid_no_patch(void* buffer, std::size_t size);
#endif

View File

@ -0,0 +1,33 @@
#ifdef _WIN32
#define _CRT_RAND_S
#endif
#include "prx/libc/include/HostEntropy.hpp"
#include <algorithm>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <system_error>
#ifdef __APPLE__
#include <sys/random.h>
#elif !defined(_WIN32)
#include <unistd.h>
#endif
void HostEntropy_nid_no_patch(void* buffer, std::size_t size) {
auto* bytes = static_cast<unsigned char*>(buffer);
#ifdef _WIN32
for (std::size_t offset = 0; offset < size; offset += sizeof(unsigned int)) {
unsigned int value = 0;
if (const errno_t error = rand_s(&value); error != 0)
throw std::system_error(error, std::generic_category(), "HostEntropy: rand_s failed");
std::memcpy(bytes + offset, &value, std::min(sizeof(value), size - offset));
}
#else
constexpr std::size_t MaxChunk = 256;
for (std::size_t offset = 0; offset < size; offset += MaxChunk) {
if (getentropy(bytes + offset, std::min(MaxChunk, size - offset)) != 0)
throw std::system_error(errno, std::generic_category(), "HostEntropy: getentropy failed");
}
#endif
}

View File

@ -9,7 +9,6 @@
#include <mutex>
#include <vector>
#include <utility>
#include <random>
#include <string>
#include <cinttypes>
#ifdef _WIN32
@ -20,6 +19,7 @@
#include "prx/libc/include/SonyCrt.hpp"
#include "prx/libc/include/specifics/gcc/AtomicOps.hpp"
#include "prx/libc/include/FileStream.hpp"
#include "prx/libc/include/HostEntropy.hpp"
namespace {
@ -123,10 +123,9 @@ std::uintmax_t APS5_VABI strtoumax_nid_postfix(const char* str, char** endptr, i
}
unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix() {
static std::random_device device;
static std::mutex mutex;
std::lock_guard lock(mutex);
return device();
unsigned int value = 0;
HostEntropy_nid_no_patch(&value, sizeof(value));
return value;
}
[[noreturn]] void APS5_VABI _ZSt14_Throw_C_errori_nid_postfix(int code) {

View File

@ -1,9 +1,7 @@
#include "prx/libkernel/File/include/RandomDevice.hpp"
#include "prx/libc/include/HostEntropy.hpp"
#include <cstdint>
#include <cstring>
#include <mutex>
#include <random>
#include <set>
#ifdef _WIN32
@ -47,12 +45,7 @@ bool IsRandomDevice(int fd) {
bool ReadRandomDevice(int fd, void* buf, std::size_t nbytes) {
std::lock_guard lock(g_mutex);
if (!g_descriptors.contains(fd)) return false;
static std::random_device device;
auto* bytes = static_cast<std::uint8_t*>(buf);
for (std::size_t offset = 0; offset < nbytes; offset += sizeof(std::uint32_t)) {
const std::uint32_t value = device();
std::memcpy(bytes + offset, &value, nbytes - offset < sizeof(value) ? nbytes - offset : sizeof(value));
}
HostEntropy_nid_no_patch(buf, nbytes);
return true;
}

View File

@ -13,6 +13,7 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/ApplicationHeap.hpp"
#include "prx/libc/include/HostEntropy.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp"
#include "prx/libkernel/DirectMemory/DirectMemory.hpp"
@ -29,7 +30,6 @@
#include <limits>
#include <memory>
#include <mutex>
#include <random>
#include <stdexcept>
#include <string>
#include <system_error>
@ -342,11 +342,8 @@ void* APS5_VABI sceKernelGetProcParam(void) {
int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid) {
if (!uuid) return sceInvalidArgument;
static thread_local std::random_device device;
std::uniform_int_distribution<std::uint32_t> distribution;
std::array<std::uint32_t, 4> value;
for (auto& word : value)
word = distribution(device);
HostEntropy_nid_no_patch(value.data(), sizeof(value));
value[1] = (value[1] & 0x0fffffffu) | 0x40000000u;
value[2] = (value[2] & 0xffffff3fu) | 0x80u;
std::memcpy(uuid, value.data(), sizeof(value));

View File

@ -10,9 +10,11 @@
extern "C" {
int APS5_VABI sceKernelGetAppInfo(int pid, void* info);
int APS5_VABI getpid_nid_postfix(void);
int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid);
}
static constexpr int SCE_KERNEL_ERROR_EPERM = static_cast<int>(0x80020001);
static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast<int>(0x80020016);
static void Require(bool value) { if (!value) std::abort(); }
@ -65,4 +67,11 @@ int main(int argc, char** argv) {
#endif
Require(std::system(command.c_str()) != 0);
std::filesystem::remove_all(empty);
Require(sceKernelUuidCreate(nullptr) == SCE_KERNEL_ERROR_EINVAL);
std::uint32_t first[4]{};
std::uint32_t second[4]{};
Require(sceKernelUuidCreate(first) == 0 && sceKernelUuidCreate(second) == 0);
for (const std::uint32_t* uuid : {first, second}) Require((uuid[1] >> 28) == 4u && (uuid[2] & 0xc0u) == 0x80u);
Require(std::memcmp(first, second, sizeof(first)) != 0);
}

View File

@ -33,6 +33,7 @@ int APS5_VABI __fpclassifyd_nid_postfix(double value);
unsigned __int128 APS5_VABI __udivti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor);
SonyLconv* APS5_VABI localeconv_nid_postfix(void);
int APS5_VABI sceAmprFpclassifyd(double value);
unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix();
}
#define Require(condition) do { if (!(condition)) { std::fprintf(stderr, "guest_crt: line %d: %s\n", __LINE__, #condition); std::abort(); } } while (0)
@ -145,4 +146,9 @@ int main() {
Require(sceAmprMbstowcs(nullptr, "ABC", 8) == 3);
Require(sceAmprMbstowcs(wide, "ABC", 2) == 2);
Require(mbstowcs_nid_postfix(wide, "ABC", 8) == 3);
const unsigned int firstRandom = _ZSt14_Random_devicev_nid_postfix();
bool randomChanged = false;
for (int draw = 0; draw < 8; ++draw) randomChanged |= _ZSt14_Random_devicev_nid_postfix() != firstRandom;
Require(randomChanged);
}

View File

@ -381,7 +381,7 @@ Throughout the project, every function at every stage either **does exactly what
- [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown.
- [mkstemp and isatty](../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`.
- [arc4random](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes from the same thread-local host `std::random_device` as `arc4random_buf`, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent
- [arc4random](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent
- [fnmatch](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws
- [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`, `xtime.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_sleep` reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning
- [_Cnd_signal, _Cnd_init_with_name](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known
@ -405,7 +405,7 @@ Throughout the project, every function at every stage either **does exactly what
- [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified
- [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown
- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies
- [/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 from `std::random_device`; both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws
- [/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, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws
- [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported
- [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target.
- [nan and nanf](../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep.