Merge pull request #2538 from husseinkad/fix/windows-stat-file-identity

fix(libkernel): report a stable file identity from stat and fstat on Windows
This commit is contained in:
Adrià 2026-10-10 11:26:32 +02:00 committed by GitHub
commit cb6870f965
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 39 additions and 3 deletions

View File

@ -2,6 +2,7 @@
#include "prx/libkernel/File/include/DirectoryDescriptor.hpp"
#include "prx/libc/include/General.hpp"
#include <algorithm>
#include <stdexcept>
#include <string>
@ -12,28 +13,35 @@
#include <sys/types.h>
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;
st->hasIdentity = GetFileInformationByHandle(handle, &st->identity) != 0;
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<HANDLE>(::_get_osfhandle(fd)), st);
return ReadHandleInfo(reinterpret_cast<HANDLE>(::_get_osfhandle(fd)), st);
}
static int DoLstat(const std::filesystem::path& p, NativeStat* st) {
std::error_code error;
@ -81,6 +89,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::uint16_t>(std::min<DWORD>(st.identity.nNumberOfLinks, 0xffff));
}
#else
sb->st_dev = static_cast<std::uint32_t>(st.st_dev);
sb->st_ino = static_cast<std::uint32_t>(st.st_ino);

View File

@ -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);

View File

@ -269,6 +269,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.
- [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware.