mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-11-07 13:39:55 +00:00
feat(libkernel_sys): create symlink targets with parent-directory components
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
6cd36e6517
commit
69a7bd2c19
3 files changed
+65
-8
No files matched your search
@@ -243,6 +243,33 @@ bool HasLongComponent(std::string_view path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HostFollowsGuestTarget(const char* created, const char* target, const std::filesystem::path& native) {
|
||||
auto directory = native.parent_path();
|
||||
for (const auto& part : std::filesystem::path(target)) {
|
||||
if (part.empty() || part == ".") continue;
|
||||
if (part != "..") {
|
||||
directory /= part;
|
||||
continue;
|
||||
}
|
||||
std::error_code error;
|
||||
if (!std::filesystem::is_directory(directory, error) || IsSymbolicLink(directory)) return false;
|
||||
directory = directory.parent_path();
|
||||
}
|
||||
const std::string_view link(created);
|
||||
const auto separator = link.find_last_of("/\\");
|
||||
std::string guest = separator == std::string_view::npos ? std::string(".") : std::string(link.substr(0, separator == 0 ? 1 : separator));
|
||||
guest += '/';
|
||||
guest += target;
|
||||
std::filesystem::path resolved;
|
||||
if (ResolvePathError_nid_no_patch(guest.c_str(), resolved)) return false;
|
||||
const auto trimmed = [](std::filesystem::path path) {
|
||||
path = path.lexically_normal();
|
||||
if (!path.has_filename() && path.has_relative_path()) path = path.parent_path();
|
||||
return path;
|
||||
};
|
||||
return trimmed(resolved) == trimmed(directory);
|
||||
}
|
||||
|
||||
bool IsDotOrDotDot(std::string_view path) {
|
||||
while (!path.empty() && (path.back() == '/' || path.back() == '\\')) path.remove_suffix(1);
|
||||
const auto separator = path.find_last_of("/\\");
|
||||
@@ -518,12 +545,6 @@ int APS5_VABI symlink_nid_postfix(const char* target, const char* created) {
|
||||
NotImplemented_nid_no_patch("symlink: absolute targets");
|
||||
return -1;
|
||||
}
|
||||
for (const auto& part : contents) {
|
||||
if (part == "..") {
|
||||
NotImplemented_nid_no_patch("symlink: parent-directory targets");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
std::filesystem::path native;
|
||||
if (const int error = ResolvePathError_nid_no_patch(created, native)) return static_cast<int>(Fail(error));
|
||||
std::error_code error;
|
||||
@@ -533,6 +554,11 @@ int APS5_VABI symlink_nid_postfix(const char* target, const char* created) {
|
||||
if (std::filesystem::exists(std::filesystem::symlink_status(native, error))) return static_cast<int>(Fail(GuestEexist));
|
||||
#endif
|
||||
if (!std::filesystem::is_directory(native.parent_path(), error)) return static_cast<int>(Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent));
|
||||
if (std::any_of(contents.begin(), contents.end(), [](const std::filesystem::path& part) { return part == ".."; }) &&
|
||||
!HostFollowsGuestTarget(created, target, native)) {
|
||||
NotImplemented_nid_no_patch("symlink: a parent-directory target the host would not follow to the guest path it names");
|
||||
return -1;
|
||||
}
|
||||
const bool directory = std::filesystem::is_directory(native.parent_path() / contents, error);
|
||||
#ifdef _WIN32
|
||||
DWORD flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE;
|
||||
|
||||
@@ -61,6 +61,8 @@ int APS5_VABI dup2_nid_postfix(int, int);
|
||||
int APS5_VABI setsid_nid_postfix(void);
|
||||
int APS5_VABI link_nid_postfix(const char*, const char*);
|
||||
int APS5_VABI symlink_nid_postfix(const char*, const char*);
|
||||
void AddPathAlias_nid_no_patch(const char*, const char*);
|
||||
void RemovePathAlias_nid_no_patch(const char*);
|
||||
int APS5_VABI openat_nid_postfix(int, const char*, int, ...);
|
||||
int APS5_VABI _openat_nid_postfix(int, const char*, int, ...);
|
||||
int APS5_VABI unlinkat_nid_postfix(int, const char*, int);
|
||||
@@ -213,14 +215,14 @@ bool RemoveSymlink(const std::string& link) {
|
||||
|
||||
void CheckSymlinkTargetRestrictions(const std::filesystem::path& root) {
|
||||
const auto created = (root / "rejected").string();
|
||||
for (const auto& target : {std::string(".."), std::string("../x"), std::string("a/../b"), std::string("a/../../x"), std::string("/abs")})
|
||||
for (const auto& target : {std::string("a/../b"), std::string("a/../../x"), std::string("/abs")})
|
||||
Require(SymlinkTargetThrows(target.c_str(), created));
|
||||
#ifdef _WIN32
|
||||
for (const auto& target : {std::string("C:/"), std::string("\\\\server\\share")})
|
||||
Require(SymlinkTargetThrows(target.c_str(), created));
|
||||
#endif
|
||||
const auto links = [](const std::string& created, const char* target) {
|
||||
char buffer[16];
|
||||
char buffer[64];
|
||||
const auto length = std::strlen(target);
|
||||
return readlink_nid_postfix(created.c_str(), buffer, sizeof(buffer)) == static_cast<std::int64_t>(length) &&
|
||||
std::memcmp(buffer, target, length) == 0;
|
||||
@@ -234,6 +236,34 @@ void CheckSymlinkTargetRestrictions(const std::filesystem::path& root) {
|
||||
const auto ellipsis = (root / "accepted-ellipsis").string();
|
||||
Require(symlink_nid_postfix("...", ellipsis.c_str()) == 0 && links(ellipsis, "..."));
|
||||
Require(RemoveSymlink(nested) && RemoveSymlink(dots) && RemoveSymlink(ellipsis));
|
||||
const auto climb = root / "climb";
|
||||
Require(std::filesystem::create_directories(climb / "deeper"));
|
||||
const auto readsFile = [](const std::string& link) {
|
||||
const int fd = sceKernelOpen(link.c_str(), SCE_KERNEL_O_RDONLY, 0);
|
||||
char byte = 0;
|
||||
const bool read = fd >= 0 && sceKernelRead(fd, &byte, 1) == 1 && byte == 'x';
|
||||
return fd >= 0 && sceKernelClose(fd) == 0 && read;
|
||||
};
|
||||
const auto up = (climb / "up").string();
|
||||
Require(symlink_nid_postfix("../file.txt", up.c_str()) == 0 && links(up, "../file.txt") && readsFile(up));
|
||||
const auto inner = (climb / "inner").string();
|
||||
Require(symlink_nid_postfix("deeper/../../file.txt", inner.c_str()) == 0 && links(inner, "deeper/../../file.txt") && readsFile(inner));
|
||||
const auto parent = (climb / "parent").string();
|
||||
Require(symlink_nid_postfix("..", parent.c_str()) == 0 && links(parent, "..") && readsFile((climb / "parent" / "file.txt").string()));
|
||||
const auto parentDangling = (climb / "dangling").string();
|
||||
Require(symlink_nid_postfix("../missing/x", parentDangling.c_str()) == 0 && links(parentDangling, "../missing/x"));
|
||||
const auto through = (root / "through").string();
|
||||
Require(symlink_nid_postfix("climb", through.c_str()) == 0);
|
||||
Require(SymlinkTargetThrows("../file.txt", (root / "through" / "via").string()));
|
||||
Require(!std::filesystem::exists(std::filesystem::symlink_status(climb / "via")));
|
||||
const auto mount = "/anyps5-symlink-climb-mount";
|
||||
AddPathAlias_nid_no_patch(mount, std::filesystem::absolute(climb).string().c_str());
|
||||
Require(SymlinkTargetThrows("../file.txt", std::string(mount) + "/escape"));
|
||||
Require(symlink_nid_postfix("deeper/../up", (std::string(mount) + "/kept").c_str()) == 0 && readsFile((climb / "kept").string()));
|
||||
RemovePathAlias_nid_no_patch(mount);
|
||||
Require(RemoveSymlink(up) && RemoveSymlink(inner) && RemoveSymlink(parent) && RemoveSymlink(parentDangling));
|
||||
Require(RemoveSymlink((climb / "kept").string()) && RemoveSymlink(through));
|
||||
Require(std::filesystem::remove(climb / "deeper") && std::filesystem::remove(climb));
|
||||
} else {
|
||||
Require(link == -1 && Error() == 1);
|
||||
std::fprintf(stderr, "symbolic links unavailable: the symlink target success cases not tested\n");
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. 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. On Windows, `mode` is ignored. Console behaviour is unverified
|
||||
- [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 returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume (`\??\Volume{GUID}\`), any other name surrogate and every reparse point without the name surrogate bit (WOF compression, deduplication, cloud files) return `EINVAL`, as FreeBSD does for a file that is not a link and as CPython's `os.readlink`, which reads only symbolic links and junctions, does
|
||||
- [aio_read, aio_write, aio_fsync, aio_error, aio_return, aio_suspend, aio_waitcomplete, aio_cancel](../../../core/libs/prx/libkernel_sys/Aio.cpp) (libkernel_sys) - a request with `SIGEV_SIGNAL`, `SIGEV_THREAD_ID` or `SIGEV_KEVENT` completion, or on a socket, throws, because delivering a guest signal or kevent and socket aio are not modelled. One worker thread runs the process's requests one at a time in submission order, so a request is cancellable only until that thread takes it. A queued request keeps the descriptor number, not the open file: closing or reusing the descriptor before it runs changes what it reaches. Pipes fail with `EOPNOTSUPP`, as FreeBSD 11 does with `vfs.aio.enable_unsafe` 0; the console's setting is unverified. Waits are not interrupted by signals (`EINTR`), and timeouts under 1 ms round up to 1 ms (hz 1000). Otherwise the rules follow FreeBSD 11 `vfs_aio.c`
|
||||
- [symlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) stores the target unchanged in a host symbolic link, which the host follows later, not the guest path resolver. It throws for an absolute target, which would need a host path in the link and a translation back in `readlink`. A target with a `..` component is created only when the host and the guest would reach the same place: every directory a `..` leaves must exist and not be a symbolic link, so the host's physical `..` matches the guest resolver's lexical one, and the target joined to the link's host directory must equal the guest path it names, resolved through the guest mounts. A `..` that climbs out of a mount such as `/savedata0`, or past a directory reached through a symbolic link, throws, where FreeBSD 11.4's `namei` follows it from the link's directory. Console behaviour is unverified
|
||||
- [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
|
||||
- [link](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows resolves a symbolic link source through the host, which follows longer chains than FreeBSD: a chain of 40 links is linked, where FreeBSD 11.4 returns `ELOOP` after 32 (`MAXSYMLINKS`). A loop returns `ELOOP` as on FreeBSD
|
||||
- [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). 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, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified
|
||||
|
||||
Reference in new issue
Block a user