Merge pull request #4090 from yunusshaikh7/fix/libc-fopen-delete-sharing-windows

fix(libc): open fopen streams with delete sharing on Windows
This commit is contained in:
Dean 2026-10-12 21:39:18 +13:00 committed by GitHub
commit 9b152c43d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 94 additions and 36 deletions

View File

@ -24,6 +24,9 @@ extern "C" void RecordWrittenPath_nid_no_patch(const std::filesystem::path& path
extern "C" std::vector<std::filesystem::path> WrittenPaths_nid_no_patch();
extern "C" void SyncWrittenPaths_nid_no_patch();
extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix);
#ifdef _WIN32
extern "C" int WindowsOpenError_nid_no_patch(unsigned long error);
#endif
#define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument")

View File

@ -11,6 +11,14 @@
#include <utility>
#include <cerrno>
#include <cstring>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <fcntl.h>
#include <io.h>
#include <windows.h>
#endif
#include "prx/libc/include/FileStream.hpp"
#include "prx/libc/include/ApplicationHeap.hpp"
@ -45,9 +53,29 @@ static std::optional<GuestFileMode> ParseFileMode(const char* mode) {
static std::FILE* OpenNative(const std::filesystem::path& path, const std::string& nativeMode) {
#ifdef _WIN32
std::wstring wideMode;
for (const char character : nativeMode) wideMode.push_back(static_cast<unsigned char>(character));
return ::_wfopen(path.c_str(), wideMode.c_str());
const char access = nativeMode[0];
const bool update = nativeMode.find('+') != std::string::npos;
const DWORD desired = update ? GENERIC_READ | GENERIC_WRITE : access == 'r' ? GENERIC_READ : GENERIC_WRITE;
const DWORD disposition = access == 'r' ? OPEN_EXISTING
: nativeMode.find('x') != std::string::npos ? CREATE_NEW : access == 'w' ? CREATE_ALWAYS : OPEN_ALWAYS;
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
const auto handle = CreateFileW(path.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security,
disposition, FILE_ATTRIBUTE_NORMAL, nullptr);
if (handle == INVALID_HANDLE_VALUE) {
errno = WindowsOpenError_nid_no_patch(GetLastError());
return nullptr;
}
const int descriptor = ::_open_osfhandle(reinterpret_cast<std::intptr_t>(handle), access == 'a' ? _O_APPEND : 0);
if (descriptor < 0) {
CloseHandle(handle);
return nullptr;
}
std::wstring streamMode;
for (const char character : nativeMode)
if (character != 'x') streamMode.push_back(static_cast<unsigned char>(character));
std::FILE* stream = ::_wfdopen(descriptor, streamMode.c_str());
if (!stream) ::_close(descriptor);
return stream;
#else
return std::fopen(path.c_str(), nativeMode.c_str());
#endif

View File

@ -401,6 +401,41 @@ extern "C" char* APS5_VABI getcwd_nid_postfix(char* buffer, std::size_t size) {
catch (const std::filesystem::filesystem_error& error) { errno = DirectoryFailure(error.code()); return nullptr; }
}
#ifdef _WIN32
extern "C" int WindowsOpenError_nid_no_patch(unsigned long error) {
switch (error) {
case ERROR_FILE_NOT_FOUND:
case ERROR_PATH_NOT_FOUND:
case ERROR_INVALID_DRIVE:
case ERROR_BAD_NETPATH:
case ERROR_BAD_NET_NAME:
case ERROR_BAD_PATHNAME:
case ERROR_FILENAME_EXCED_RANGE:
return ENOENT;
case ERROR_ACCESS_DENIED:
case ERROR_SHARING_VIOLATION:
case ERROR_LOCK_VIOLATION:
case ERROR_WRITE_PROTECT:
case ERROR_NETWORK_ACCESS_DENIED:
case ERROR_CANNOT_MAKE:
case ERROR_DRIVE_LOCKED:
return EACCES;
case ERROR_FILE_EXISTS:
case ERROR_ALREADY_EXISTS:
return EEXIST;
case ERROR_TOO_MANY_OPEN_FILES:
return EMFILE;
case ERROR_NOT_ENOUGH_MEMORY:
case ERROR_OUTOFMEMORY:
return ENOMEM;
case ERROR_DISK_FULL:
return ENOSPC;
default:
return EINVAL;
}
}
#endif
extern "C" void NotImplemented_nid_no_patch(const char* funcName) {
throw std::runtime_error(std::string(funcName) + " not implemented");
}

View File

@ -23,38 +23,6 @@
#include <io.h>
#include <sys/stat.h>
#include <windows.h>
static int OpenError(DWORD error) {
switch (error) {
case ERROR_FILE_NOT_FOUND:
case ERROR_PATH_NOT_FOUND:
case ERROR_INVALID_DRIVE:
case ERROR_BAD_NETPATH:
case ERROR_BAD_NET_NAME:
case ERROR_BAD_PATHNAME:
case ERROR_FILENAME_EXCED_RANGE:
return ENOENT;
case ERROR_ACCESS_DENIED:
case ERROR_SHARING_VIOLATION:
case ERROR_LOCK_VIOLATION:
case ERROR_WRITE_PROTECT:
case ERROR_NETWORK_ACCESS_DENIED:
case ERROR_CANNOT_MAKE:
case ERROR_DRIVE_LOCKED:
return EACCES;
case ERROR_FILE_EXISTS:
case ERROR_ALREADY_EXISTS:
return EEXIST;
case ERROR_TOO_MANY_OPEN_FILES:
return EMFILE;
case ERROR_NOT_ENOUGH_MEMORY:
case ERROR_OUTOFMEMORY:
return ENOMEM;
case ERROR_DISK_FULL:
return ENOSPC;
default:
return EINVAL;
}
}
static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint16_t mode) {
const int access = nativeFlags & (_O_RDONLY | _O_WRONLY | _O_RDWR);
const DWORD desired = access == _O_RDONLY ? GENERIC_READ : access == _O_WRONLY ? GENERIC_WRITE : GENERIC_READ | GENERIC_WRITE;
@ -65,7 +33,7 @@ static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
const auto handle = CreateFileW(p.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, disposition, attributes, nullptr);
if (handle == INVALID_HANDLE_VALUE) {
errno = OpenError(GetLastError());
errno = WindowsOpenError_nid_no_patch(GetLastError());
return -1;
}
const int descriptor = ::_open_osfhandle(reinterpret_cast<std::intptr_t>(handle), nativeFlags & (_O_APPEND | _O_RDONLY | _O_BINARY));

View File

@ -19,6 +19,8 @@ int APS5_VABI fseek_nid_postfix(FileStream* stream, std::int64_t offset, int ori
std::int64_t APS5_VABI ftell_nid_postfix(FileStream* stream);
int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream);
int APS5_VABI fflush_nid_postfix(FileStream* stream);
int APS5_VABI remove_nid_postfix(const char* path);
int APS5_VABI rename_nid_postfix(const char* from, const char* to);
}
static void Require(bool condition) {
@ -91,5 +93,26 @@ int main(int argc, char** argv) {
Require(fclose_nid_postfix(stream) == 0);
Require(std::filesystem::file_size(hostName) == 0);
std::filesystem::remove(hostName);
const std::string heldName = std::string(argv[1]) + ".held";
const std::string movedName = std::string(argv[1]) + ".moved";
stream = fopen_nid_postfix(heldName.c_str(), "w+b");
Require(stream != nullptr && fwrite_nid_postfix(payload, 1, sizeof(payload), stream) == sizeof(payload));
Require(fflush_nid_postfix(stream) == 0);
Require(rename_nid_postfix(heldName.c_str(), movedName.c_str()) == 0 && !std::filesystem::exists(heldName));
Require(remove_nid_postfix(movedName.c_str()) == 0 && !std::filesystem::exists(movedName));
Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0);
std::array<char, sizeof(payload)> heldBuffer{};
Require(fread_nid_postfix(heldBuffer.data(), 1, heldBuffer.size(), stream) == heldBuffer.size());
Require(std::strcmp(heldBuffer.data(), payload) == 0 && fclose_nid_postfix(stream) == 0);
stream = fopen_nid_postfix(heldName.c_str(), "ab");
Require(stream != nullptr && fputs_nid_postfix("ab", stream) >= 0 && fclose_nid_postfix(stream) == 0);
stream = fopen_nid_postfix(heldName.c_str(), "ab");
Require(stream != nullptr && fputs_nid_postfix("cd", stream) >= 0 && fclose_nid_postfix(stream) == 0);
errno = 0;
Require(fopen_nid_postfix(heldName.c_str(), "wbx") == nullptr && errno == EEXIST);
stream = fopen_nid_postfix(heldName.c_str(), "rb");
std::array<char, 5> appended{};
Require(stream != nullptr && fread_nid_postfix(appended.data(), 1, 4, stream) == 4 && std::strcmp(appended.data(), "abcd") == 0);
Require(fclose_nid_postfix(stream) == 0 && remove_nid_postfix(heldName.c_str()) == 0);
std::cout << "PASS: stream objects, file operations, EOF and error handling\n";
}

View File

@ -449,6 +449,7 @@ Throughout the project, every function at every stage either **does exactly what
- [sceKernelGetFsSandboxRandomWord](../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist
- [sceKernelSync](../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync`
- [sceKernelSync](../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way
- [`fopen` streams](../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access
- [getcontext and setcontext](../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`)
- [makecontext](../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) follows FreeBSD 11's `lib/libc/amd64/gen/makecontext.c`: at most six arguments, a stack of at least `MINSIGSTKSZ` (2048) bytes, and `exit(0)` when the function returns with no `uc_link`. The console's version is not verified
- [getcontext and setcontext](../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them