Merge pull request #12955 from JJenkx/windows/libkernel-sys-linkat-symlink-itself

fix(libkernel_sys): hard-link a symbolic link itself in linkat on Windows
This commit is contained in:
Adrià authored and GitHub committed 2026-10-30 13:45:28 +01:00
commit 563d839a7a
3 files changed
+40 -15

No files matched your search

+10 -7
View File
@@ -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<int>(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<int>(Fail(GuestExdev));
throw std::system_error(static_cast<int>(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<int>(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<int>(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<int>(Fail(error));
if (const int error = CopyPath(path2, copy2)) return static_cast<int>(Fail(error));
if (const int error = ResolveAtPath(fd2, path2, destination)) return static_cast<int>(Fail(error));
#ifndef _WIN32
if (linkItself) return LinkSymbolicLinkItself(source, destination);
#endif
return LinkNative(source, destination);
}
+29 -7
View File
@@ -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<std::size_t>(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);
+1 -1
View File
@@ -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`