From 0b1941b313332f55925ba65ae9879ae6eb6853ff Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 05:06:05 +0300 Subject: [PATCH] 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);