From 0b1941b313332f55925ba65ae9879ae6eb6853ff Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 05:06:05 +0300 Subject: [PATCH 1/3] fix(libkernel): report a stable file identity from stat and fstat on Windows st_ino was always 0 and st_dev differed between stat (drive number) and fstat (0), so the usual a.st_dev == b.st_dev && a.st_ino == b.st_ino identity check gave wrong answers. Read the volume serial, the file index and the link count from the file handle, which works the same for path and descriptor lookups. Fixes #2296 --- .../prx/libkernel/File/src/NativeStat.cpp | 20 ++++++++++++++--- core/libs/tests/GuestFilesystem.cpp | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index a6fb3304a..a08c971fe 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libc/include/General.hpp" +#include #include #include @@ -12,28 +13,36 @@ #include struct NativeStat : __stat64 { FILE_BASIC_INFO times{}; + BY_HANDLE_FILE_INFORMATION identity{}; bool hasTimes = false; + bool hasIdentity = false; }; -static int ReadTimes(HANDLE handle, NativeStat* st) { +static int ReadHandleInfo(HANDLE handle, NativeStat* st) { if (GetFileType(handle) != FILE_TYPE_DISK) return 0; if (!GetFileInformationByHandleEx(handle, FileBasicInfo, &st->times, sizeof(st->times))) return File::WindowsFileTime::Failure(GetLastError()); st->hasTimes = true; + if (!GetFileInformationByHandle(handle, &st->identity)) return File::WindowsFileTime::Failure(GetLastError()); + st->hasIdentity = true; return 0; } +static std::uint32_t FoldFileIndex(const BY_HANDLE_FILE_INFORMATION& identity) { + const std::uint32_t folded = identity.nFileIndexLow ^ (identity.nFileIndexHigh * 0x9E3779B1u); + return folded == 0 ? 1 : folded; +} static int DoStat(const std::filesystem::path& p, NativeStat* st) { if (_wstat64(p.c_str(), st) != 0) return -1; const auto handle = CreateFileW(p.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (handle == INVALID_HANDLE_VALUE) return File::WindowsFileTime::Failure(GetLastError()); - const int result = ReadTimes(handle, st); + const int result = ReadHandleInfo(handle, st); CloseHandle(handle); return result; } static int DoFstat(int fd, NativeStat* st) { if (const auto directory = File::DirectoryDescriptorPath(fd)) return DoStat(*directory, st); if (_fstat64(fd, st) != 0) return -1; - return ReadTimes(reinterpret_cast(::_get_osfhandle(fd)), st); + return ReadHandleInfo(reinterpret_cast(::_get_osfhandle(fd)), st); } static int DoLstat(const std::filesystem::path& p, NativeStat* st) { std::error_code error; @@ -81,6 +90,11 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { sb->st_ctim = File::WindowsFileTime::Decode(st.times.ChangeTime.QuadPart); sb->st_birthtim = File::WindowsFileTime::Decode(st.times.CreationTime.QuadPart); } + if (st.hasIdentity) { + sb->st_dev = st.identity.dwVolumeSerialNumber; + sb->st_ino = FoldFileIndex(st.identity); + sb->st_nlink = static_cast(std::min(st.identity.nNumberOfLinks, 0xffff)); + } #else sb->st_dev = static_cast(st.st_dev); sb->st_ino = static_cast(st.st_ino); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 8def58d3f..bd0f598b7 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -37,6 +37,7 @@ int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); int APS5_VABI sceKernelOpen(const char*, int, unsigned short); int APS5_VABI sceKernelClose(int); int APS5_VABI sceKernelStat(const char*, FileStat*); +int APS5_VABI sceKernelFstat(int, FileStat*); int APS5_VABI sceKernelUnlink(const char*); int APS5_VABI sceKernelRmdir(const char*); int* APS5_VABI __error_nid_postfix(); @@ -211,6 +212,27 @@ int main() { Require(lstat_nid_postfix((root / "present.txt" / "child").string().c_str(), &linkStatus) == -1 && *__error_nid_postfix() == 20); Require(unlink_nid_postfix(link.string().c_str()) == 0 && unlink_nid_postfix(dangling.string().c_str()) == 0); #endif + FileStat identity{}; + FileStat byDescriptor{}; + const int identityDescriptor = open_nid_postfix(presentName.c_str(), 0, 0); + Require(identityDescriptor >= 0 && stat_nid_postfix(presentName.c_str(), &identity) == 0); + Require(sceKernelFstat(identityDescriptor, &byDescriptor) == 0 && close_nid_postfix(identityDescriptor) == 0); + Require(identity.st_ino != 0 && byDescriptor.st_dev == identity.st_dev && byDescriptor.st_ino == identity.st_ino); + const auto sibling = root / "sibling.txt"; + { std::ofstream stream(sibling); stream << "other"; } + FileStat siblingStatus{}; + Require(stat_nid_postfix(sibling.string().c_str(), &siblingStatus) == 0); + Require(siblingStatus.st_dev == identity.st_dev && siblingStatus.st_ino != identity.st_ino); + Require(unlink_nid_postfix(sibling.string().c_str()) == 0); + const auto hardLink = root / "hardlink.txt"; + std::error_code linkError; + std::filesystem::create_hard_link(present, hardLink, linkError); + if (!linkError) { + FileStat linked{}; + Require(stat_nid_postfix(hardLink.string().c_str(), &linked) == 0); + Require(linked.st_dev == identity.st_dev && linked.st_ino == identity.st_ino && linked.st_nlink == 2); + Require(unlink_nid_postfix(hardLink.string().c_str()) == 0); + } const int opened = open_nid_postfix(presentName.c_str(), 0, 0); Require(opened >= 0 && close_nid_postfix(opened) == 0); const int reopened = _open_nid_postfix(presentName.c_str(), 0); From 9b5be73a99c35545118f41d2f64ca90e60ae1556 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 05:10:45 +0300 Subject: [PATCH 2/3] fix(libkernel): keep stat working when a handle has no file identity Leave the Windows identity fields at their stat values when GetFileInformationByHandle fails on a handle whose basic information could be read, instead of failing the whole call. --- core/libs/prx/libkernel/File/src/NativeStat.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index a08c971fe..1c0ada2a6 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -22,8 +22,7 @@ static int ReadHandleInfo(HANDLE handle, NativeStat* st) { if (!GetFileInformationByHandleEx(handle, FileBasicInfo, &st->times, sizeof(st->times))) return File::WindowsFileTime::Failure(GetLastError()); st->hasTimes = true; - if (!GetFileInformationByHandle(handle, &st->identity)) return File::WindowsFileTime::Failure(GetLastError()); - st->hasIdentity = true; + st->hasIdentity = GetFileInformationByHandle(handle, &st->identity) != 0; return 0; } static std::uint32_t FoldFileIndex(const BY_HANDLE_FILE_INFORMATION& identity) { From 43cc101910057ca2b81ec3fbd7c4cf46d0ec8ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 10 Oct 2026 11:08:15 +0200 Subject: [PATCH 3/3] docs(technical-debt): record the lossy Windows file identity fold Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index a335f9e00..25a50b386 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -267,6 +267,7 @@ Throughout the project, every function at every stage either **does exactly what - [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`. - [libSceZlib](../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. - [Windows file timestamps](../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond +- [Windows file identity](../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails - On Linux, [direct memory](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [Per-face depth bias](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. - [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic