diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 77f36e3089..a26fd1347f 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -659,17 +659,25 @@ static int LinkNative(const std::filesystem::path& source, const std::filesystem return 0; } -#ifndef _WIN32 static int LinkSymbolicLinkItself(const std::filesystem::path& source, const std::filesystem::path& destination) { if (const int error = LinkDestinationError(destination)) return static_cast(Fail(error)); +#ifdef _WIN32 + if ((GetFileAttributesW(source.c_str()) & FILE_ATTRIBUTE_DIRECTORY) != 0) + NotImplemented_nid_no_patch("linkat: a hard link to a directory symbolic link itself on Windows"); + if (!CreateHardLinkW(destination.c_str(), source.c_str(), nullptr)) { + const DWORD error = GetLastError(); + if (error == ERROR_NOT_SAME_DEVICE) return static_cast(Fail(GuestExdev)); + throw std::system_error(static_cast(error), std::system_category(), "linkat"); + } +#else if (::linkat(AT_FDCWD, source.c_str(), AT_FDCWD, destination.c_str(), 0) != 0) { if (errno == EXDEV) return static_cast(Fail(GuestExdev)); throw std::system_error(errno, std::generic_category(), "linkat"); } +#endif RecordWrittenPath_nid_no_patch(destination); return 0; } -#endif int APS5_VABI link_nid_postfix(const char* existing, const char* created) { std::string existingCopy, createdCopy; @@ -699,16 +707,11 @@ int APS5_VABI linkat_nid_postfix(int fd1, const char* path1, int fd2, const char std::filesystem::path source, destination; if (const int error = ResolveAtPath(fd1, path1, source)) return static_cast(Fail(error)); const bool linkItself = flag == 0 && IsSymbolicLink(source); -#ifdef _WIN32 - if (linkItself) NotImplemented_nid_no_patch("linkat: a hard link to a symbolic link itself on Windows"); -#endif if (!linkItself) if (const int error = LinkSourceError(source)) return static_cast(Fail(error)); if (const int error = CopyPath(path2, copy2)) return static_cast(Fail(error)); if (const int error = ResolveAtPath(fd2, path2, destination)) return static_cast(Fail(error)); -#ifndef _WIN32 if (linkItself) return LinkSymbolicLinkItself(source, destination); -#endif return LinkNative(source, destination); } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index f3d70f70e4..1c0fe74f80 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -76,7 +76,18 @@ bool ThrowsNaming(Call call, const char* text) { return false; } -#ifndef _WIN32 +#ifdef _WIN32 +unsigned long LinkCount(const std::string& path) { + const HANDLE link = CreateFileW(std::filesystem::path(path).c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(link != INVALID_HANDLE_VALUE); + BY_HANDLE_FILE_INFORMATION information{}; + const bool read = GetFileInformationByHandle(link, &information) != 0; + CloseHandle(link); + Require(read); + return information.nNumberOfLinks; +} +#else nlink_t LinkCount(const std::string& path) { struct stat status {}; Require(::lstat(path.c_str(), &status) == 0); @@ -84,6 +95,13 @@ nlink_t LinkCount(const std::string& path) { } #endif +std::string LinkTarget(const std::string& path) { + char buffer[256]{}; + const std::int64_t length = readlink_nid_postfix(path.c_str(), buffer, sizeof(buffer)); + Require(length > 0); + return std::string(buffer, static_cast(length)); +} + void OwnershipAndLinkTests(const std::filesystem::path& root) { constexpr std::uint32_t Unchanged = 0xFFFFFFFFu; const auto file = (root / "owned.txt").string(); @@ -152,27 +170,31 @@ void OwnershipAndLinkTests(const std::filesystem::path& root) { Require(lchown_nid_postfix(dangling.c_str(), 0, 0) == 0); Require(fchownat_nid_postfix(-100, dangling.c_str(), 0, 0, 0) == -1 && Error() == 2); Require(fchownat_nid_postfix(-100, dangling.c_str(), 0, 0, 0x200) == 0); -#ifndef _WIN32 const auto symbolicCopy = (root / "copy.link").string(); const auto danglingCopy = (root / "dangling.copy").string(); Require(linkat_nid_postfix(-100, symbolic.c_str(), -100, symbolicCopy.c_str(), 0) == 0); - Require(std::filesystem::read_symlink(symbolicCopy) == "owned.txt" && LinkCount(symbolic) == 2); + Require(LinkTarget(symbolicCopy) == "owned.txt" && LinkCount(symbolic) == 2); Require(std::filesystem::hard_link_count(file) == 2); Require(linkat_nid_postfix(directory, "dangling.link", directory, "dangling.copy", 0) == 0); - Require(std::filesystem::read_symlink(danglingCopy) == "missing.txt" && LinkCount(dangling) == 2); + Require(LinkTarget(danglingCopy) == "missing.txt" && LinkCount(dangling) == 2); Require(linkat_nid_postfix(-100, dangling.c_str(), -100, other.c_str(), 0x400) == -1 && Error() == 2); Require(linkat_nid_postfix(-100, symbolic.c_str(), -100, linked.c_str(), 0) == -1 && Error() == 17); Require(linkat_nid_postfix(-100, symbolic.c_str(), -100, (missing + "/inner").c_str(), 0) == -1 && Error() == 2); Require(linkat_nid_postfix(-100, symbolic.c_str(), -100, (throughFile + "/inner").c_str(), 0) == -1 && Error() == 20); Require(LinkCount(symbolic) == 2 && !std::filesystem::exists(std::filesystem::symlink_status(other))); + Require(unlink_nid_postfix(symbolicCopy.c_str()) == 0 && unlink_nid_postfix(danglingCopy.c_str()) == 0); + Require(LinkCount(symbolic) == 1 && LinkCount(dangling) == 1 && std::filesystem::hard_link_count(file) == 2); const auto folder = root / "folder"; const auto folderLink = (root / "folder.link").string(); - const auto folderCopy = (root / "folder.copy").string(); Require(std::filesystem::create_directory(folder) && symlink_nid_postfix("folder", folderLink.c_str()) == 0); Require(linkat_nid_postfix(-100, folderLink.c_str(), -100, other.c_str(), 0x400) == -1 && Error() == 1); +#ifdef _WIN32 + Require(RemoveDirectoryW(std::filesystem::path(folderLink).c_str()) != 0 && std::filesystem::remove(folder)); +#else + const auto folderCopy = (root / "folder.copy").string(); Require(linkat_nid_postfix(-100, folderLink.c_str(), -100, folderCopy.c_str(), 0) == 0); - Require(std::filesystem::read_symlink(folderCopy) == "folder" && LinkCount(folderLink) == 2); - Require(std::filesystem::remove(symbolicCopy) && std::filesystem::remove(danglingCopy) && std::filesystem::remove(folderCopy)); + Require(LinkTarget(folderCopy) == "folder" && LinkCount(folderLink) == 2); + Require(std::filesystem::remove(folderCopy)); Require(std::filesystem::remove(folderLink) && std::filesystem::remove(folder)); #endif Require(linkat_nid_postfix(-100, symbolic.c_str(), -100, other.c_str(), 0x400) == 0); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 2ca3a95026..d67f40b91d 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -375,8 +375,8 @@ - [kldstat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) never succeeds: no linker file is listed here, as for `kldnext`, so the kernel's own file (id 1 on FreeBSD) and the modules a console preloads are missing, every file id gets `ENOENT` and the structure is never written. The checks before the lookup follow FreeBSD 11.4's [`sys_kldstat`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/kern_linker.c#L1228-L1250): only the `version` field is read (`EFAULT` when it cannot be), and a version other than the size of `struct kld_file_stat_1` (1056 bytes on amd64) or `struct kld_file_stat` (2080) gets `EINVAL`. Console behaviour is unverified - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it - [chown, lchown and fchownat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled, as for `fchown`: owner and group 0 and the unchanged value (-1) are accepted, and any other owner or group throws, naming it. The path is looked up first, as in FreeBSD 11's `kern_fchownat`, with `stat`, with `lstat` for `lchown`, and with `fstatat` for `fchownat`, so a lookup error is returned before the owner is checked and nothing is changed on the host. `fchownat` flags other than `AT_SYMLINK_NOFOLLOW` (0x200) return `EINVAL`, as in FreeBSD 11. The console's ownership rules are unverified -- [linkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) resolves `AT_FDCWD` (-100) and absolute paths as `link` does, and flags other than `AT_SYMLINK_FOLLOW` (0x400) return `EINVAL`, as in FreeBSD 11. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. A symbolic link as the source without `AT_SYMLINK_FOLLOW` throws on Windows, where FreeBSD links the symbolic link itself; on Linux and macOS the host's `linkat` with flag 0 links it. Console behaviour is unverified - [sceKernelIsNeoMode](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - not measured on a console, and the PS5 module's body is not read. A title with `param.json` throws: what a PS5 application gets is unknown. For a title with `param.sfo` it returns bit 23 (0x00800000) of `ATTRIBUTE`, the title's Neo-mode support, and 0 without the key, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3eecea94ddaca360b19f4b800523b609a8b2ffcc/src/core/libraries/kernel/process.cpp#L29-L35) answers on a Neo console ([bit 23](https://github.com/shadps4-emu/shadPS4/blob/3eecea94ddaca360b19f4b800523b609a8b2ffcc/src/common/elf_info.h#L50-L51)), which `sceKernelHasNeoMode` and `sceKernelIsAuthenticNeo` already report. A base PS4 (firmware 10.71) answers 0 ([discussions/7941](https://github.com/boykopovar/AnyPS5/discussions/7941)) +- [linkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) resolves `AT_FDCWD` (-100) and absolute paths as `link` does, and flags other than `AT_SYMLINK_FOLLOW` (0x400) return `EINVAL`, as in FreeBSD 11. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Without `AT_SYMLINK_FOLLOW` a symbolic link as the source is linked itself, as in FreeBSD: on Linux and macOS through the host's `linkat` with flag 0, and on Windows through `CreateHardLinkW`, which links the link's reparse point rather than its target, dangling or not. Windows cannot hard-link a directory, so there a symbolic link or junction to a directory as the source throws. Console behaviour is unverified - [pthread_set_defaultstacksize_np and scePthreadSetDefaultstacksize](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) set the stack size that `scePthreadAttrInit` and a thread created without attributes use, as [fpPS4's `pthread_set_defaultstacksize_np`](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_pthread_attr.pas#L89-L102) does: a size of 16384 bytes or less fails with `EINVAL` (`0x80020016` from the sce form) and there is no upper limit, so a default the host cannot allocate fails only when a thread is created. Attributes initialised earlier keep their size. Not checked on a PS5 - [sceKernelTitleWorkaroundIsEnabled](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a console; the `{version, align, ids[2]}` layout, the bit test and the limit of 0x3A bits follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/kernel.cpp), which notes that the limit changes between firmwares. A negative bit is `EINVAL` here; shadPS4 compares it signed and reads outside `ids`. Titles usually pass the `title_workaround` block of `sceKernelGetAppInfo`, which is written as zeros here; where `title_workaround` sits in the 0x58-byte structure is unmeasured, and with the shadPS4 offsets (0x30 to 0x48) it falls in the zeroed bytes, so its bits read 0; the console's values in a game are unmeasured - [sceKernelGetProcessType](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a console; as in shadPS4, the title's own pid, -1 and 0 are type 0 (big application) and any other pid is `ESRCH`