Merge pull request #3596 from Lorkydey/feat/libc-fstatvfs

feat(libc): implement guest fstatvfs volume statistics
This commit is contained in:
Adrià 2026-10-11 23:48:10 +02:00 committed by GitHub
commit d956de8425
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 272 additions and 30 deletions

View File

@ -2,8 +2,24 @@ add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp U
target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS})
configure_windows_unwind(libSceLibcInternal)
target_sources(libSceLibcInternal PRIVATE FilesystemStatistics.cpp)
if(BUILD_TESTING)
add_test_executable(guest_fstatvfs_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfsConversion.cpp FilesystemStatistics.cpp)
target_include_directories(guest_fstatvfs_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR})
configure_windows_unwind(guest_fstatvfs_conversion_tests)
add_test(NAME guest_fstatvfs_conversion COMMAND guest_fstatvfs_conversion_tests)
set_tests_properties(guest_fstatvfs_conversion PROPERTIES TIMEOUT 20)
add_test_executable(guest_fstatvfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfs.cpp)
target_include_directories(guest_fstatvfs_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_fstatvfs_tests PRIVATE libSceLibcInternal libkernel libc)
configure_windows_unwind(guest_fstatvfs_tests)
add_test(NAME guest_fstatvfs COMMAND guest_fstatvfs_tests)
set_tests_properties(guest_fstatvfs PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(guest_fstatvfs PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibcInternal>")
endif()
add_test_executable(guest_utime_forwarding_tests ${LIBS_INCLUDE_DIR}/tests/GuestUtimeForwarding.cpp FileTime.cpp)
target_include_directories(guest_utime_forwarding_tests PRIVATE ${LIBS_INCLUDE_DIR})
configure_windows_unwind(guest_utime_forwarding_tests)

View File

@ -0,0 +1,46 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libkernel/File/include/KernelStatfs.hpp"
#include <cstddef>
#include <cstdint>
#include <cstring>
struct LibcStatvfs {
std::uint64_t f_bavail;
std::uint64_t f_bfree;
std::uint64_t f_blocks;
std::uint64_t f_favail;
std::uint64_t f_ffree;
std::uint64_t f_files;
std::uint64_t f_bsize;
std::uint64_t f_flag;
std::uint64_t f_frsize;
std::uint64_t f_fsid;
std::uint64_t f_namemax;
};
static_assert(sizeof(LibcStatvfs) == 88);
static_assert(offsetof(LibcStatvfs, f_bsize) == 48);
static_assert(offsetof(LibcStatvfs, f_namemax) == 80);
extern "C" int APS5_VABI _fstatfs_nid_postfix(int, KernelStatfs*);
extern "C" int* APS5_VABI __error_nid_postfix();
extern "C" int APS5_VABI fstatvfs_nid_postfix(int descriptor, LibcStatvfs* output) {
int* const error = __error_nid_postfix();
const int saved = *error;
KernelStatfs volume{};
const int status = _fstatfs_nid_postfix(descriptor, &volume);
if (status != 0) return status;
if (output == nullptr) {
*error = 14;
return -1;
}
const LibcStatvfs result{
static_cast<std::uint64_t>(volume.f_bavail), volume.f_bfree, volume.f_blocks,
static_cast<std::uint64_t>(volume.f_ffree), static_cast<std::uint64_t>(volume.f_ffree), volume.f_files,
volume.f_iosize, (volume.f_flags & 1) | ((volume.f_flags & 8) >> 2), volume.f_bsize,
0, volume.f_namemax
};
std::memcpy(output, &result, sizeof(result));
*error = saved;
return 0;
}

View File

@ -0,0 +1,33 @@
#pragma once
#include <cstddef>
#include <cstdint>
struct KernelStatfs {
std::uint32_t f_version;
std::uint32_t f_type;
std::uint64_t f_flags;
std::uint64_t f_bsize;
std::uint64_t f_iosize;
std::uint64_t f_blocks;
std::uint64_t f_bfree;
std::int64_t f_bavail;
std::uint64_t f_files;
std::int64_t f_ffree;
std::uint64_t f_syncwrites;
std::uint64_t f_asyncwrites;
std::uint64_t f_syncreads;
std::uint64_t f_asyncreads;
std::uint64_t f_spare[10];
std::uint32_t f_namemax;
std::uint32_t f_owner;
std::int32_t f_fsid[2];
char f_charspare[80];
char f_fstypename[16];
char f_mntfromname[88];
char f_mntonname[88];
};
static_assert(sizeof(KernelStatfs) == 472);
static_assert(offsetof(KernelStatfs, f_bavail) == 48);
static_assert(offsetof(KernelStatfs, f_namemax) == 184);
static_assert(offsetof(KernelStatfs, f_fstypename) == 280);
static_assert(offsetof(KernelStatfs, f_mntonname) == 384);

View File

@ -9,6 +9,7 @@
#include "prx/libkernel/File/include/File.hpp"
#include "prx/libkernel/File/include/FileFlags.hpp"
#include "prx/libkernel/File/include/NativeStat.hpp"
#include "prx/libkernel/File/include/KernelStatfs.hpp"
#include "prx/libkernel/File/include/DirectoryDescriptor.hpp"
#include "prx/libkernel/File/include/FileLock.hpp"
#include "prx/libkernel/File/include/RandomDevice.hpp"
@ -32,36 +33,6 @@ static_assert(sizeof(KernelIovec) == sizeof(GuestSockets::Iovec));
static constexpr int KERNEL_IOV_MAX = 1024;
struct KernelStatfs {
std::uint32_t f_version;
std::uint32_t f_type;
std::uint64_t f_flags;
std::uint64_t f_bsize;
std::uint64_t f_iosize;
std::uint64_t f_blocks;
std::uint64_t f_bfree;
std::int64_t f_bavail;
std::uint64_t f_files;
std::int64_t f_ffree;
std::uint64_t f_syncwrites;
std::uint64_t f_asyncwrites;
std::uint64_t f_syncreads;
std::uint64_t f_asyncreads;
std::uint64_t f_spare[10];
std::uint32_t f_namemax;
std::uint32_t f_owner;
std::int32_t f_fsid[2];
char f_charspare[80];
char f_fstypename[16];
char f_mntfromname[88];
char f_mntonname[88];
};
static_assert(sizeof(KernelStatfs) == 472);
static_assert(offsetof(KernelStatfs, f_bavail) == 48);
static_assert(offsetof(KernelStatfs, f_namemax) == 184);
static_assert(offsetof(KernelStatfs, f_fstypename) == 280);
static_assert(offsetof(KernelStatfs, f_mntonname) == 384);
static constexpr std::uint32_t KERNEL_STATFS_VERSION = 0x20030518;
static constexpr std::uint64_t KERNEL_MNT_RDONLY = 0x1;
static constexpr std::uint64_t KERNEL_MNT_NOEXEC = 0x4;

View File

@ -0,0 +1,97 @@
#include "prx/libc/include/General.hpp"
#include "prx/libkernel/File/include/FileFlags.hpp"
#include <array>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
extern "C" {
int APS5_VABI fstatvfs_nid_postfix(int, void*);
int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t);
std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int);
int APS5_VABI close_nid_postfix(int);
int APS5_VABI pipe_nid_postfix(int*);
int APS5_VABI socketpair_nid_postfix(int, int, int, int*);
int* APS5_VABI __error_nid_postfix();
}
static void Check(bool value, int line) {
if (!value) {
std::fprintf(stderr, "fstatvfs check failed at line %d\n", line);
std::abort();
}
}
#define Require(value) Check((value), __LINE__)
static void RequireFailure(int descriptor, int error) {
std::array<std::uint64_t, 11> output;
output.fill(0x5A5A5A5A5A5A5A5Aull);
const auto saved = output;
*__error_nid_postfix() = 0;
Require(fstatvfs_nid_postfix(descriptor, output.data()) == -1);
Require(*__error_nid_postfix() == error);
Require(output == saved);
}
static void RequireVolume(int descriptor, const std::filesystem::path& path) {
std::array<std::uint64_t, 13> guarded;
guarded.fill(0xA5A5A5A5A5A5A5A5ull);
*__error_nid_postfix() = 12345;
Require(fstatvfs_nid_postfix(descriptor, guarded.data() + 1) == 0);
Require(*__error_nid_postfix() == 12345);
Require(guarded.front() == 0xA5A5A5A5A5A5A5A5ull && guarded.back() == guarded.front());
const auto* fields = guarded.data() + 1;
const auto capacity = std::filesystem::space(path).capacity;
Require(fields[6] > 0 && fields[8] > 0);
Require(fields[2] > 0 && fields[2] == capacity / fields[8]);
Require(fields[0] <= fields[1] && fields[1] <= fields[2]);
Require(fields[3] == fields[4]);
Require((fields[7] & ~std::uint64_t{3}) == 0);
Require(fields[9] == 0 && fields[10] >= 255);
}
int main() {
const auto root = std::filesystem::temp_directory_path() /
("anyps5-fstatvfs-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
Require(std::filesystem::create_directory(root));
const auto filePath = root / "file.txt";
{
std::ofstream file(filePath, std::ios::binary);
file << "fstatvfs";
Require(static_cast<bool>(file));
}
AddPathAlias_nid_no_patch("/anyps5-fstatvfs-test", root.string().c_str());
const int file = sceKernelOpen("/anyps5-fstatvfs-test/file.txt", SCE_KERNEL_O_RDONLY, 0);
Require(file >= 0);
Require(sceKernelLseek(file, 3, 0) == 3);
RequireVolume(file, filePath);
Require(sceKernelLseek(file, 0, 1) == 3);
Require(fstatvfs_nid_postfix(file, nullptr) == -1 && *__error_nid_postfix() == 14);
const int directory = sceKernelOpen("/anyps5-fstatvfs-test", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0);
Require(directory >= 0);
RequireVolume(directory, root);
Require(close_nid_postfix(directory) == 0);
int pipes[2];
Require(pipe_nid_postfix(pipes) == 0);
RequireFailure(pipes[0], 22);
RequireFailure(pipes[1], 22);
for (int fd : pipes) Require(close_nid_postfix(fd) == 0);
int sockets[2];
Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0);
RequireFailure(sockets[0], 22);
for (int fd : sockets) Require(close_nid_postfix(fd) == 0);
RequireFailure(sockets[0], 9);
RequireFailure(-1, 9);
Require(close_nid_postfix(file) == 0);
RemovePathAlias_nid_no_patch("/anyps5-fstatvfs-test");
Require(std::filesystem::remove(filePath));
Require(std::filesystem::remove(root));
}

View File

@ -0,0 +1,78 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libkernel/File/include/KernelStatfs.hpp"
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
extern "C" int APS5_VABI fstatvfs_nid_postfix(int, void*);
namespace {
KernelStatfs backend;
int backendError;
int guestError;
int calls;
void Check(bool value, int line) {
if (!value) {
std::fprintf(stderr, "fstatvfs conversion check failed at line %d\n", line);
std::abort();
}
}
}
#define Require(value) Check((value), __LINE__)
extern "C" int* APS5_VABI __error_nid_postfix() { return &guestError; }
extern "C" int APS5_VABI _fstatfs_nid_postfix(int descriptor, KernelStatfs* output) {
Require(descriptor == 42);
++calls;
*output = backend;
guestError = backendError ? backendError : 71;
return backendError ? -1 : 0;
}
int main() {
backend.f_blocks = 0x100000002ull;
backend.f_bfree = 0x100000001ull;
backend.f_bavail = 0x100000000ll;
backend.f_files = 0x200000002ull;
backend.f_ffree = 0x200000001ll;
backend.f_iosize = 65536;
backend.f_bsize = 4096;
backend.f_namemax = 255;
backend.f_fsid[0] = 123;
for (std::uint64_t flags = 0; flags != 16; ++flags) {
backend.f_flags = flags;
std::array<std::uint64_t, 13> output;
output.fill(0xA5A5A5A5A5A5A5A5ull);
const std::array<std::uint64_t, 11> expected{
0x100000000ull, 0x100000001ull, 0x100000002ull,
0x200000001ull, 0x200000001ull, 0x200000002ull,
65536, (flags & 1) | ((flags & 8) ? 2u : 0u), 4096, 0, 255
};
const int before = calls;
guestError = 12345;
Require(fstatvfs_nid_postfix(42, output.data() + 1) == 0);
Require(calls == before + 1 && guestError == 12345);
Require(output.front() == 0xA5A5A5A5A5A5A5A5ull && output.back() == output.front());
Require(std::memcmp(output.data() + 1, expected.data(), sizeof(expected)) == 0);
}
backend.f_bavail = -1;
backend.f_ffree = -2;
std::array<std::uint64_t, 11> output{};
Require(fstatvfs_nid_postfix(42, output.data()) == 0);
Require(output[0] == std::numeric_limits<std::uint64_t>::max());
Require(output[3] == std::numeric_limits<std::uint64_t>::max() - 1 && output[4] == output[3]);
Require(fstatvfs_nid_postfix(42, nullptr) == -1 && guestError == 14);
for (int failure : {5, 9, 22, 45}) {
backendError = failure;
output.fill(0x5A5A5A5A5A5A5A5Aull);
const auto saved = output;
Require(fstatvfs_nid_postfix(42, output.data()) == -1 && guestError == failure);
Require(output == saved);
Require(fstatvfs_nid_postfix(42, nullptr) == -1 && guestError == failure);
}
}

View File

@ -619,6 +619,7 @@ Throughout the project, every function at every stage either **does exactly what
- [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware.
- [Descriptor pool recycling](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured.
- [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields
- [fstatvfs](../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run)
- [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert
- [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not.
- [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well