diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 92cbd44e5e4..f1bccb7a91e 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1779,6 +1779,13 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() if(WIN32) + add_test_executable(guest_windows_directory_link_types_tests ${LIBS_INCLUDE_DIR}/tests/GuestWindowsDirectoryLinkTypes.cpp) + target_include_directories(guest_windows_directory_link_types_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_windows_directory_link_types_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_windows_directory_link_types_tests) + add_test(NAME guest_windows_directory_link_types COMMAND guest_windows_directory_link_types_tests) + set_tests_properties(guest_windows_directory_link_types PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") add_test_executable(guest_directory_buffer_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryBuffer.cpp) target_include_directories(guest_directory_buffer_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_directory_buffer_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index e140a6b0f48..58e97e8c806 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -10,6 +10,7 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/NativeStat.hpp" #include #include @@ -30,8 +31,10 @@ namespace { +constexpr std::uint8_t GuestUnknownType = 0; constexpr std::uint8_t GuestDirectoryType = 4; constexpr std::uint8_t GuestRegularType = 8; +constexpr std::uint8_t GuestSymbolicLinkType = 10; struct DirectoryState { std::mutex mutex; @@ -50,18 +53,66 @@ std::shared_ptr Find(int fd) { return found == g_directories.end() ? nullptr : found->second; } +bool InsideApp0(const std::filesystem::path& directory) { + const auto root = ResolveDiskPath_nid_no_patch("/app0"); + const auto handle = CreateFileW(root.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 false; + const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), _O_RDONLY | _O_BINARY); + if (descriptor < 0) { + CloseHandle(handle); + return false; + } + const auto app0 = File::WindowsDescriptorPath_nid_no_patch(descriptor); + ::_close(descriptor); + if (!app0) return false; + auto entry = directory.begin(); + for (const auto& component : *app0) { + if (entry == directory.end()) return false; + const auto& left = entry->native(); + const auto& right = component.native(); + if (CompareStringOrdinal(left.c_str(), static_cast(left.size()), + right.c_str(), static_cast(right.size()), TRUE) != CSTR_EQUAL) return false; + ++entry; + } + return true; +} + +std::uint8_t EntryType(const std::filesystem::path& path, bool followLinks) { + FileStat status{}; + if (!File::FillLinkStat(path, &status)) + return GuestUnknownType; + if (followLinks && (status.st_mode & 0170000) == 0120000) { + FileStat target{}; + const int savedErrno = errno; + const bool found = File::TryFillFileStat(path, &target); + errno = savedErrno; + if (found && ((target.st_mode & 0170000) == 0040000 || (target.st_mode & 0170000) == 0100000)) + status = target; + } + switch (status.st_mode & 0170000) { + case 0040000: return GuestDirectoryType; + case 0100000: return GuestRegularType; + case 0120000: return GuestSymbolicLinkType; + default: NotImplemented_nid_no_patch("getdents of a Windows file type other than a directory, regular file or symbolic link"); + } +} + void Load(DirectoryState& state, int fd) { if (state.loaded) return; const auto path = File::WindowsDescriptorPath_nid_no_patch(fd); if (!path) throw std::system_error(errno, std::generic_category(), "directory descriptor path"); - state.loaded = true; - state.entries.emplace_back(".", GuestDirectoryType); - state.entries.emplace_back("..", GuestDirectoryType); + const int savedErrno = errno; + const bool followLinks = InsideApp0(*path); + errno = savedErrno; + std::vector> entries{{".", GuestDirectoryType}, {"..", GuestDirectoryType}}; std::error_code error; for (const auto& entry : std::filesystem::directory_iterator(*path, error)) { - std::error_code typeError; - state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType); + entries.emplace_back(entry.path().filename().string(), EntryType(entry.path(), followLinks)); } + state.entries = std::move(entries); + state.loaded = true; } int OpenStreamDirectory(const wchar_t* path) { diff --git a/core/libs/tests/GuestWindowsDirectoryLinkTypes.cpp b/core/libs/tests/GuestWindowsDirectoryLinkTypes.cpp new file mode 100644 index 00000000000..1c5227b3f8a --- /dev/null +++ b/core/libs/tests/GuestWindowsDirectoryLinkTypes.cpp @@ -0,0 +1,197 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Directory link type check failed at line %d\n", line); + std::exit(1); + } +} + +#define Require(value) Check((value), __LINE__) + +static void Junction(const std::filesystem::path& link, const std::filesystem::path& target) { + Require(std::filesystem::create_directory(link)); + const auto printName = std::filesystem::absolute(target).make_preferred().wstring(); + const auto substituteName = L"\\??\\" + printName; + struct MountPoint { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteOffset; + USHORT substituteLength; + USHORT printOffset; + USHORT printLength; + wchar_t names[2048]; + } data{}; + Require(substituteName.size() + printName.size() + 2 <= std::size(data.names)); + data.tag = IO_REPARSE_TAG_MOUNT_POINT; + data.substituteLength = static_cast(substituteName.size() * sizeof(wchar_t)); + data.printOffset = static_cast((substituteName.size() + 1) * sizeof(wchar_t)); + data.printLength = static_cast(printName.size() * sizeof(wchar_t)); + data.dataLength = static_cast(8 + data.printOffset + data.printLength + sizeof(wchar_t)); + std::memcpy(data.names, substituteName.c_str(), data.substituteLength + sizeof(wchar_t)); + std::memcpy(reinterpret_cast(data.names) + data.printOffset, printName.c_str(), data.printLength + sizeof(wchar_t)); + const auto handle = CreateFileW(link.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE); + DWORD returned = 0; + const auto result = DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, &data, 8 + data.dataLength, nullptr, 0, &returned, nullptr); + Require(CloseHandle(handle) != 0); + Require(result != 0); +} + +static void OutsideApp0() { + const auto root = std::filesystem::absolute("anyps5-directory-links-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + Require(std::filesystem::create_directory(root / "directory")); + { std::ofstream file(root / "regular"); file << "data"; } + std::vector links{"junction", "dangling-junction", "loop-junction"}; + Junction(root / links[0], root / "directory"); + Junction(root / links[1], root / "missing"); + Junction(root / links[2], root / links[2]); + if (CreateSymbolicLinkW((root / "file-link").c_str(), L"regular", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)) { + links.push_back("file-link"); + Require(CreateSymbolicLinkW((root / "directory-link").c_str(), L"directory", + SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + links.push_back("directory-link"); + Require(CreateSymbolicLinkW((root / "dangling-link").c_str(), L"missing", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + links.push_back("dangling-link"); + } else { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "Symbolic-link cases require Windows developer mode or symbolic-link privilege; junction cases still run\n"); + } + const int descriptor = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(descriptor >= 0); + std::map entries; + std::array buffer{}; + for (;;) { + const int bytes = sceKernelGetdents(descriptor, buffer.data(), static_cast(buffer.size())); + Require(bytes >= 0 && bytes <= static_cast(buffer.size())); + if (bytes == 0) break; + for (int offset = 0; offset < bytes;) { + std::uint16_t length = 0; + std::memcpy(&length, buffer.data() + offset + 4, sizeof(length)); + const auto nameLength = static_cast(buffer[offset + 7]); + Require(length >= 8 + nameLength + 1 && offset + length <= bytes); + const std::string name(buffer.data() + offset + 8, nameLength); + Require(entries.emplace(name, static_cast(buffer[offset + 6])).second); + offset += length; + } + } + Require(sceKernelClose(descriptor) == 0); + Require(entries.size() == links.size() + 4); + Require(entries.at(".") == 4 && entries.at("..") == 4); + Require(entries.at("directory") == 4 && entries.at("regular") == 8); + for (const auto& name : links) { + if (entries.at(name) != 10) + std::fprintf(stderr, "%s: getdents type %u, expected DT_LNK (10)\n", name.c_str(), entries.at(name)); + Require(entries.at(name) == 10); + const auto path = root / name; + FileStat status{}; + Require(lstat_nid_postfix(path.string().c_str(), &status) == 0); + Require((status.st_mode & 0170000) == 0120000); + } + for (const auto& name : links) Require(RemoveDirectoryW((root / name).c_str()) != 0 || DeleteFileW((root / name).c_str()) != 0); + Require(std::filesystem::remove_all(root) == 3); +} + + +static std::map Types(const char* path) { + const int descriptor = sceKernelOpen(path, SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(descriptor >= 0); + std::map entries; + std::array buffer{}; + for (;;) { + const int bytes = sceKernelGetdents(descriptor, buffer.data(), static_cast(buffer.size())); + Require(bytes >= 0 && bytes <= static_cast(buffer.size())); + if (bytes == 0) break; + for (int offset = 0; offset < bytes;) { + std::uint16_t length = 0; + std::memcpy(&length, buffer.data() + offset + 4, sizeof(length)); + const auto nameLength = static_cast(buffer[offset + 7]); + Require(length >= 8 + nameLength + 1 && offset + length <= bytes); + Require(entries.emplace(std::string(buffer.data() + offset + 8, nameLength), + static_cast(buffer[offset + 6])).second); + offset += length; + } + } + Require(sceKernelClose(descriptor) == 0); + return entries; +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = previous / ("anyps5-app0-links-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root / "app0" / "real")); + Require(std::filesystem::create_directories(root / "external")); + Require(std::filesystem::create_directories(root / "data")); + Require(std::filesystem::create_directories(root / "app0-sibling")); + std::filesystem::current_path(root); + OutsideApp0(); + std::vector links; + const auto junction = [&](const std::filesystem::path& link, const std::filesystem::path& target) { + Junction(root / link, root / target); + links.push_back(link); + }; + junction("app0/packed", "external"); + junction("app0/chain", "app0/packed"); + junction("app0/dangling", "missing"); + junction("app0/loop", "app0/loop"); + junction("app0/real/nested", "external"); + junction("external/nested", "app0/real"); + junction("data/saved", "external"); + junction("app0-sibling/saved", "external"); + { std::ofstream file(root / "external/regular"); file << "data"; } + const bool symbolic = CreateSymbolicLinkW((root / "app0/file-link").c_str(), L"..\\external\\regular", + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0; + if (symbolic) { + links.push_back("app0/file-link"); + Require(CreateSymbolicLinkW((root / "app0/relative").c_str(), L"..\\external", + SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + links.push_back("app0/relative"); + } else { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "app0 symbolic-link subcases skipped: privilege unavailable\n"); + } + auto app0 = Types("/app0"); + std::fprintf(stderr, "app0 packed/chain/real types: %u/%u/%u (expected 4/4/4)\n", app0.at("packed"), app0.at("chain"), app0.at("real")); + Require(app0.at("real") == 4 && app0.at("packed") == 4 && app0.at("chain") == 4); + Require(app0.at("dangling") == 10 && app0.at("loop") == 10); + if (symbolic) Require(app0.at("file-link") == 8 && app0.at("relative") == 4); + Require(Types("/app0/real").at("nested") == 4); + Require(Types("/app0/packed").at("nested") == 10); + Require(Types("/data").at("saved") == 10); + Require(Types((root / "app0-sibling").string().c_str()).at("saved") == 10); + FileStat status{}; + Require(lstat_nid_postfix("/app0/packed", &status) == 0 && (status.st_mode & 0170000) == 0120000); + for (auto it = links.rbegin(); it != links.rend(); ++it) Require(RemoveDirectoryW((root / *it).c_str()) != 0 || DeleteFileW((root / *it).c_str()) != 0); + std::filesystem::rename(root / "app0", root / "application"); + Junction(root / "app0", root / "application"); + Junction(root / "application/packed", root / "external"); + Require(Types("/app0").at("packed") == 4); + Require(std::filesystem::remove(root / "application/packed")); + Require(std::filesystem::remove(root / "app0")); + std::filesystem::current_path(previous); + Require(root.parent_path() == previous && root.filename().string().starts_with("anyps5-app0-links-")); + std::filesystem::remove_all(root); + std::puts("Windows directory link type tests passed"); +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index ead756f295c..93435a19164 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -152,6 +152,7 @@ - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies. Descriptors retain the opened directory's native handle across renames. Enumeration reloads and `*at` operations resolve its current path, so a concurrent rename between resolution and the operation is not atomic. Enumeration of a removed directory and network-share path queries have not been verified - [Linux directory entry types](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - `sceKernelGetdirentries` reports a host symlink inside the app0 tree as the type of its target (directory or regular file), since a console app0 has no links and links there come from how the title was unpacked; a link elsewhere, or a dangling one, stays a link. A directory opened through an app0 link resolves to its target's host path, so links nested inside that target keep the link type. - [ftruncate, truncate, sceKernelFtruncate](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) extend a file on Windows with `SetFileInformationByHandle` (`FileEndOfFileInfo`), which reserves the clusters but writes no zero bytes, and report `EFBIG` for a length the volume cannot hold (`ERROR_INVALID_PARAMETER` on a disk file; seen with a length of `INT64_MAX`, the size from which NTFS refuses it was not measured) and `ENOSPC` when the free space is smaller. FreeBSD 11.4 makes the extension a hole on a file system that has them ([`ffs_truncate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/ufs/ffs/ffs_inode.c) writes only the last block), so a length above the free space succeeds there. A write behind the end of the file still makes NTFS write zero bytes over the gap, as the file is not sparse. None of this was measured on a console. +- [Windows directory entry types](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) - types other than regular files, directories and symbolic links, and reparse points unsupported by `lstat`, throw. If the host cannot stat an entry, its type is `DT_UNKNOWN`. Console directory-entry types have not been measured; the host `lstat` modes map to `DT_REG`, `DT_DIR` and `DT_LNK` following [FreeBSD 11.4 dirent.h](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/sys/dirent.h). A host link inside canonical app0 is reported as its target's type (directory or regular file); a link elsewhere, or a dangling one, stays a link. A directory opened through an app0 link resolves to its target's host path, so links nested inside a target outside app0 keep the link type. - [readv, writev, preadv, pwritev and their sceKernel forms](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - unreadable iovec-table errors follow FreeBSD 11.4's [`copyinuio`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/subr_uio.c#L401-L434), not console measurements. The table is copied before inspecting its entries; payload-buffer validation is unchanged. A Linux `process_vm_readv` failure other than `EFAULT` throws, including when the host denies that API. - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing