diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index c14e6a4e0..2b4024dde 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -42,6 +42,7 @@ constexpr int GuestEperm = 1; constexpr int GuestEio = 5; constexpr int GuestEfault = 14; constexpr int GuestEnotdir = 20; +constexpr int GuestEisdir = 21; constexpr int GuestEinval = 22; constexpr int GuestEexist = 17; constexpr int GuestExdev = 18; @@ -194,6 +195,64 @@ bool IsDotOrDotDot(std::string_view path) { const auto component = path.substr(separator == std::string_view::npos ? 0 : separator + 1); return component == "." || component == ".."; } + +int RenamePath(const std::filesystem::path& source, const std::filesystem::path& destination) { + namespace fs = std::filesystem; + std::error_code error; + const auto sourceStatus = fs::symlink_status(source, error); + if (error && error != std::errc::no_such_file_or_directory) return FilesystemError(error); + if (sourceStatus.type() == fs::file_type::not_found) + return HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent; + + const auto parent = destination.parent_path().empty() ? fs::path(".") : destination.parent_path(); + error.clear(); + const auto parentStatus = fs::status(parent, error); + if (error) return FilesystemError(error); + if (!fs::exists(parentStatus)) + return HasNonDirectoryPrefix(destination) ? GuestEnotdir : GuestEnoent; + if (!fs::is_directory(parentStatus)) return GuestEnotdir; + + error.clear(); + const auto destinationStatus = fs::symlink_status(destination, error); + if (error && error != std::errc::no_such_file_or_directory) return FilesystemError(error); + const bool destinationExists = destinationStatus.type() != fs::file_type::not_found; + const bool sourceIsDirectory = fs::is_directory(sourceStatus); + const bool destinationIsDirectory = destinationExists && fs::is_directory(destinationStatus); + bool sameFile = false; + if (destinationExists) { + error.clear(); + sameFile = fs::equivalent(source, destination, error); + if (error == std::errc::no_such_file_or_directory) error.clear(); + if (error) return FilesystemError(error); + if (!sameFile && sourceIsDirectory && !destinationIsDirectory) return GuestEnotdir; + if (!sameFile && !sourceIsDirectory && destinationIsDirectory) return GuestEisdir; + } + if (sourceIsDirectory && !sameFile) { + error.clear(); + const auto canonicalSource = fs::weakly_canonical(source, error); + if (error) return FilesystemError(error); + const auto canonicalDestination = fs::weakly_canonical(destination, error); + if (error) return FilesystemError(error); + const auto relative = canonicalDestination.lexically_relative(canonicalSource); + if (!relative.empty() && *relative.begin() != "..") return GuestEinval; + } + if (destinationExists && !sameFile && destinationIsDirectory) { + error.clear(); + if (!fs::is_empty(destination, error)) { + return error ? FilesystemError(error) : GuestEnotempty; + } + } +#ifdef _WIN32 + if (!sameFile && sourceIsDirectory && destinationIsDirectory) { + error.clear(); + if (!fs::remove(destination, error)) return error ? FilesystemError(error) : GuestEio; + } +#endif + + error.clear(); + fs::rename(source, destination, error); + return error ? FilesystemError(error) : 0; +} } extern "C" { @@ -485,4 +544,64 @@ int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { return 0; } +int APS5_VABI rename_nid_postfix(const char* from, const char* to); + +int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const char* to) { + if (from == nullptr || to == nullptr) return static_cast(Fail(GuestEfault)); + if (*from == '\0' || *to == '\0') return static_cast(Fail(GuestEnoent)); + if ((oldfd == GuestAtFdcwd || *from == '/') && (newfd == GuestAtFdcwd || *to == '/')) + return rename_nid_postfix(from, to); + if (std::strlen(from) >= GuestPathMax || HasLongComponent(from) || + std::strlen(to) >= GuestPathMax || HasLongComponent(to)) + return static_cast(Fail(GuestEnametoolong)); + if (IsDotOrDotDot(from) || IsDotOrDotDot(to)) return static_cast(Fail(GuestEinval)); + + std::filesystem::path source; + if (oldfd == GuestAtFdcwd || *from == '/') { + if (const int error = ResolvePathError_nid_no_patch(from, source)) return static_cast(Fail(error)); + } else { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(oldfd, &error); + if (base == nullptr) return static_cast(Fail(error)); + source = std::filesystem::path(reinterpret_cast(base)) / + std::filesystem::path(reinterpret_cast(from)); + } + + std::filesystem::path destination; + if (newfd == GuestAtFdcwd || *to == '/') { + if (const int error = ResolvePathError_nid_no_patch(to, destination)) return static_cast(Fail(error)); + } else { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(newfd, &error); + if (base == nullptr) return static_cast(Fail(error)); + destination = std::filesystem::path(reinterpret_cast(base)) / + std::filesystem::path(reinterpret_cast(to)); + } + + const std::string_view fromView(from); + const std::string_view toView(to); + const bool oldTrailingSlash = !fromView.empty() && fromView.back() == '/'; + const bool newTrailingSlash = !toView.empty() && toView.back() == '/'; + + while (!source.has_filename() && source.has_relative_path()) source = source.parent_path(); + while (!destination.has_filename() && destination.has_relative_path()) destination = destination.parent_path(); + + std::error_code statusError; + const auto sourceStatus = std::filesystem::symlink_status(source, statusError); + if (oldTrailingSlash) { + if (sourceStatus.type() == std::filesystem::file_type::not_found) + return static_cast(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); + if (!std::filesystem::is_directory(sourceStatus)) return static_cast(Fail(GuestEnotdir)); + } + if (newTrailingSlash && sourceStatus.type() != std::filesystem::file_type::not_found && + !std::filesystem::is_directory(sourceStatus)) + return static_cast(Fail(GuestEnotdir)); + + const int error = RenamePath(source, destination); + if (error) return static_cast(Fail(error)); + RecordWrittenPath_nid_no_patch(source); + RecordWrittenPath_nid_no_patch(destination); + return 0; +} + } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 38bbbaec6..1bbdff4b7 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -40,6 +40,7 @@ int APS5_VABI openat_nid_postfix(int, const char*, int, ...); int APS5_VABI unlinkat_nid_postfix(int, const char*, int); std::int64_t APS5_VABI readlinkat_nid_postfix(int, const char*, char*, std::size_t); int APS5_VABI mkdirat_nid_postfix(int, const char*, std::uint16_t); +int APS5_VABI renameat_nid_postfix(int, const char*, int, const char*); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); int* APS5_VABI __error_nid_postfix(); @@ -356,6 +357,53 @@ int main() { Require(openat_nid_postfix(-1, "file.txt", SCE_KERNEL_O_RDONLY) == -1 && Error() == 9); Require(openat_nid_postfix(base, nullptr, SCE_KERNEL_O_RDONLY) == -1 && Error() == 14); Require(openat_nid_postfix(base, "", SCE_KERNEL_O_RDONLY) == -1 && Error() == 2); + Require(renameat_nid_postfix(base, nullptr, base, "renamed.txt") == -1 && Error() == 14); + Require(renameat_nid_postfix(base, "made.bin", base, nullptr) == -1 && Error() == 14); + Require(renameat_nid_postfix(base, "", base, "renamed.txt") == -1 && Error() == 2); + Require(renameat_nid_postfix(base, "made.bin", base, "") == -1 && Error() == 2); + const int bad = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(bad >= 0); + Require(renameat_nid_postfix(-1, "made.bin", base, "renamed.txt") == -1 && Error() == 9); + Require(renameat_nid_postfix(base, "made.bin", -1, "renamed.txt") == -1 && Error() == 9); + Require(renameat_nid_postfix(bad, "made.bin", base, "renamed.txt") == -1 && Error() == 20); + Require(renameat_nid_postfix(base, "made.bin", bad, "renamed.txt") == -1 && Error() == 20); + Require(sceKernelClose(bad) == 0); + const auto longPart = std::string(256, 'y'); + Require(renameat_nid_postfix(base, longPart.c_str(), base, "renamed.txt") == -1 && Error() == 63); + Require(renameat_nid_postfix(base, "made.bin", base, longPart.c_str()) == -1 && Error() == 63); + Require(renameat_nid_postfix(base, ".", base, "renamed.txt") == -1 && Error() == 22); + Require(renameat_nid_postfix(base, "..", base, "renamed.txt") == -1 && Error() == 22); + Require(renameat_nid_postfix(base, "made.bin", base, ".") == -1 && Error() == 22); + Require(renameat_nid_postfix(base, "made.bin", base, "..") == -1 && Error() == 22); + Require(renameat_nid_postfix(base, "missing.bin", base, "renamed.txt") == -1 && Error() == 2); + Require(renameat_nid_postfix(base, "made.bin", base, "renamed.bin") == 0); + Require(!std::filesystem::exists(root / "made.bin")); + Require(std::filesystem::is_regular_file(root / "renamed.bin")); + Require(renameat_nid_postfix(base, "renamed.bin", base, "renamed.bin") == 0); + const auto subPath = root / "sub_dir"; + std::filesystem::create_directory(subPath); + Require(renameat_nid_postfix(base, "renamed.bin", base, "sub_dir") == -1 && Error() == 21); + Require(renameat_nid_postfix(base, "sub_dir", base, "renamed.bin") == -1 && Error() == 20); + Require(renameat_nid_postfix(base, "sub_dir", base, "sub_renamed") == 0); + Require(!std::filesystem::exists(subPath)); + Require(std::filesystem::is_directory(root / "sub_renamed")); + const int subFd = sceKernelOpen((root / "sub_renamed").string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(subFd >= 0); + Require(renameat_nid_postfix(base, "renamed.bin", subFd, "nested.bin") == 0); + Require(!std::filesystem::exists(root / "renamed.bin")); + Require(std::filesystem::is_regular_file(root / "sub_renamed" / "nested.bin")); + Require(renameat_nid_postfix(base, "sub_renamed", subFd, "child") == -1 && Error() == 22); + const auto otherDir = root / "other_dir"; + std::filesystem::create_directory(otherDir); + { std::ofstream stream(otherDir / "item.txt"); } + Require(renameat_nid_postfix(base, "sub_renamed", base, "other_dir") == -1 && Error() == 66); + Require(renameat_nid_postfix(subFd, "nested.bin/", base, "back.bin") == -1 && Error() == 20); + Require(renameat_nid_postfix(subFd, "nested.bin", base, "nonexistent/") == -1 && Error() == 20); + Require(renameat_nid_postfix(subFd, "nested.bin", base, "made.bin") == 0); + Require(std::filesystem::is_regular_file(root / "made.bin")); + Require(sceKernelClose(subFd) == 0); + Require(renameat_nid_postfix(-100, (root / "made.bin").string().c_str(), -100, (root / "final.bin").string().c_str()) == 0); + Require(std::filesystem::is_regular_file(root / "final.bin")); Require(sceKernelClose(base) == 0); std::filesystem::remove_all(root); return 0; diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 269141119..ea44a7b58 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -9,4 +9,5 @@ - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target and a junction throw, as a host path has no guest form. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. 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. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. 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. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified +- [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, 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. Console behaviour is unverified - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask