diff --git a/mk/tests.mk b/mk/tests.mk index 775fa061..4be85651 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -334,10 +334,12 @@ test-sysroot-symlink-escape: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-symlink-esc tmpdir=$$(mktemp -d); \ secret_dir=$$(mktemp -d); \ trap 'rm -rf "$$tmpdir" "$$secret_dir"' EXIT; \ - mkdir -p "$$tmpdir/d1"; \ + mkdir -p "$$tmpdir/d1" "$$tmpdir/d2"; \ printf 'inside-sysroot\n' > "$$tmpdir/d1/normal.txt"; \ printf 'SECRET-HOST-FILE\n' > "$$secret_dir/secret.txt"; \ ln -sf "$$secret_dir/secret.txt" "$$tmpdir/d1/abs-link"; \ + ln -sf "$$secret_dir/secret.txt" "$$tmpdir/d2/abs-link"; \ + ln -sf "../d2/abs-link" "$$tmpdir/d1/chain-link"; \ depth=$$(printf '%s' "$$tmpdir/d1" | tr -cd '/' | wc -c | tr -d ' '); \ relback=""; i=0; \ while [ "$$i" -lt "$$depth" ]; do relback="../$$relback"; i=$$((i + 1)); done; \ @@ -530,6 +532,10 @@ test-sysroot-symlink-target: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-symlink-tar @set -e; \ tmpdir=$$(mktemp -d); \ trap 'rm -rf "$$tmpdir"' EXIT; \ + mkdir -p "$$tmpdir/host-downloads"; \ + ln -s "$$HOME" "$$tmpdir/host-home-link"; \ + mkdir -p "$$tmpdir/home/muplar"; \ + ln -s "$$tmpdir/host-downloads" "$$tmpdir/home/muplar/Download"; \ had_target=0; [ -e "/symlink-target" ] && had_target=1; \ $(ELFUSE_BIN) --sysroot "$$tmpdir" \ $(BUILD_DIR)/test-sysroot-symlink-target; \ diff --git a/src/syscall/path.c b/src/syscall/path.c index d9d9bece..4567e8f0 100644 --- a/src/syscall/path.c +++ b/src/syscall/path.c @@ -265,12 +265,16 @@ int path_translate_at(guest_fd_t dirfd, return 0; } + unsigned int lookup_flags = flags; + if (path_has_trailing_slash(tx->guest_path)) + lookup_flags &= ~PATH_TR_NOFOLLOW; + errno = 0; - if (flags & PATH_TR_CREATE) { + if (lookup_flags & PATH_TR_CREATE) { tx->host_path = path_resolve_sysroot_create_path( tx->guest_path, tx->host_buf, sizeof(tx->host_buf), - (flags & PATH_TR_CREATE_PARENTS) != 0); - } else if (flags & PATH_TR_NOFOLLOW) { + (lookup_flags & PATH_TR_CREATE_PARENTS) != 0); + } else if (lookup_flags & PATH_TR_NOFOLLOW) { tx->host_path = path_resolve_sysroot_nofollow_path( tx->guest_path, tx->host_buf, sizeof(tx->host_buf)); } else { @@ -297,8 +301,9 @@ int path_translate_at(guest_fd_t dirfd, char relative_host[LINUX_PATH_MAX]; if (tx->host_path && tx->guest_path[0] != '/' && proc_get_sysroot()) { int recheck = path_check_relative_sysroot_containment( - dirfd, tx->guest_path, flags, &relative_in_sysroot, relative_abs, - sizeof(relative_abs), relative_host, sizeof(relative_host)); + dirfd, tx->guest_path, lookup_flags, &relative_in_sysroot, + relative_abs, sizeof(relative_abs), relative_host, + sizeof(relative_host)); if (recheck < 0) { tx->host_path = NULL; if (errno == 0) @@ -342,7 +347,8 @@ int path_translate_at(guest_fd_t dirfd, * unconditionally would hand the link's stored target bytes to the host * kernel, which cannot spell them. */ - bool follow_final = !(flags & (PATH_TR_NOFOLLOW | PATH_TR_CREATE)); + bool follow_final = + !(lookup_flags & (PATH_TR_NOFOLLOW | PATH_TR_CREATE)); host_fd_ref_t ref; casefold_walk_t walk; casefold_verdict_t verdict; @@ -1299,6 +1305,128 @@ static int dirfd_reconstruct_abs_path(guest_fd_t dirfd, return 0; } +static int dirfd_realpath_relative(guest_fd_t dirfd, + const char *path, + char *out, + size_t outsz) +{ + char base[LINUX_PATH_MAX]; + char joined[LINUX_PATH_MAX]; + + if (path_openat2_dirfd_host_path(dirfd, base, sizeof(base)) < 0) + return -1; + int n = snprintf(joined, sizeof(joined), "%s/%s", base, path); + if (n < 0 || (size_t) n >= sizeof(joined)) { + errno = ENAMETOOLONG; + return -1; + } + if (!realpath(joined, out)) + return -1; + if (strlen(out) >= outsz) { + errno = ENAMETOOLONG; + return -1; + } + return 0; +} + +static int dirfd_symlink_chain_reaches_absolute_target(guest_fd_t dirfd, + const char *path) +{ + host_fd_ref_t ref; + if (host_dirfd_ref_open(dirfd, &ref) < 0) { + errno = EBADF; + return -1; + } + + host_fd_t current_fd = ref.fd; + bool current_owned = false; + const char *scan = path; + char pending[LINUX_PATH_MAX]; + const char *comp; + size_t len; + int symlink_count = 0; + int rc = 0; + + while (path_next_component(&scan, &comp, &len)) { + char name[NAME_MAX + 1]; + struct stat st; + + if (path_component_copy(name, sizeof(name), comp, len) < 0) { + rc = -1; + goto out; + } + if (!strcmp(name, ".")) + continue; + if (!strcmp(name, "..")) { + host_fd_t parent_fd = + openat(current_fd, "..", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (parent_fd < 0) { + rc = -1; + goto out; + } + if (current_owned) + close(current_fd); + current_fd = parent_fd; + current_owned = true; + continue; + } + + if (fstatat(current_fd, name, &st, AT_SYMLINK_NOFOLLOW) < 0) { + rc = -1; + goto out; + } + if (S_ISLNK(st.st_mode)) { + char target[LINUX_PATH_MAX]; + ssize_t n = + readlinkat(current_fd, name, target, sizeof(target) - 1); + if (n < 0) { + rc = -1; + goto out; + } + if (++symlink_count > MAXSYMLINKS) { + errno = ELOOP; + rc = -1; + goto out; + } + if (n > 0 && target[0] == '/') { + rc = 1; + goto out; + } + target[n] = '\0'; + + const char *rest = scan; + while (*rest == '/') + rest++; + if (path_splice_link_target(NULL, 0, target, rest, pending, + sizeof(pending)) < 0) { + rc = -1; + goto out; + } + scan = pending; + continue; + } + + if (!S_ISDIR(st.st_mode)) + break; + host_fd_t next_fd = + openat(current_fd, name, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (next_fd < 0) { + rc = -1; + goto out; + } + if (current_owned) + close(current_fd); + current_fd = next_fd; + current_owned = true; + } + +out: + if (current_owned) + close(current_fd); + host_fd_ref_close(&ref); + return rc; +} + /* Returns 1 when the reconstruction climbed the guest root, so the caller opens * the resolved absolute host path instead of walking from dirfd; 0 when the * walk stays beneath it; or -1 with errno set. @in_sysroot reports whether the @@ -1357,6 +1485,15 @@ static int path_check_relative_sysroot_containment(guest_fd_t dirfd, path_resolve_sysroot_path(abs_path, host_buf, sizeof(host_buf)); } + char fallback_buf[LINUX_PATH_MAX]; + if (!checked && errno == ELOOP && !(flags & PATH_TR_CREATE) && + dirfd_symlink_chain_reaches_absolute_target(dirfd, path) > 0) { + if (dirfd_realpath_relative(dirfd, path, fallback_buf, + sizeof(fallback_buf)) == 0) { + checked = fallback_buf; + } + } + if (!checked) return -1; diff --git a/src/syscall/proc-state.c b/src/syscall/proc-state.c index 48dab5d5..53570b4d 100644 --- a/src/syscall/proc-state.c +++ b/src/syscall/proc-state.c @@ -536,6 +536,17 @@ static bool sysroot_path_exists(const char *resolved_path, bool follow_final) return lstat(resolved_path, &st) == 0; } +static const char *copy_host_fallback_path(const char *path, + char *buf, + size_t bufsz) +{ + if (str_copy_trunc(buf, path, bufsz) >= bufsz) { + errno = ENAMETOOLONG; + return NULL; + } + return buf; +} + /* Resolve an absolute guest path against --sysroot. This keeps absolute guest * filesystem syscalls inside the sysroot when the target exists there, and * otherwise falls back to the literal host path so apps can still reach host @@ -676,10 +687,34 @@ static int sysroot_seed_host_path(const char *path, char *buf, size_t bufsz, char sr[LINUX_PATH_MAX], - char clamped[LINUX_PATH_MAX]) + char clamped[LINUX_PATH_MAX], + bool follow_final) { if (!proc_sysroot_snapshot(sr, LINUX_PATH_MAX) || !path || path[0] != '/') return 0; + + char real_path[LINUX_PATH_MAX]; + const char *host_path = path; + if (follow_final && realpath(path, real_path)) + host_path = real_path; + + size_t sr_len = strlen(sr); + if (!strncmp(host_path, sr, sr_len) && + (sr_len == 1 || host_path[sr_len] == '\0' || + host_path[sr_len] == '/')) { + const char *guest = host_path + sr_len; + if (sr_len == 1) + guest = host_path; + if (*guest == '\0') + guest = "/"; + if (str_copy_trunc(clamped, guest, LINUX_PATH_MAX) >= LINUX_PATH_MAX || + str_copy_trunc(buf, host_path, bufsz) >= bufsz) { + errno = ENAMETOOLONG; + return -1; + } + return 1; + } + if (bufsz == 0 || !clamp_dotdot_at_guest_root(clamped, path, LINUX_PATH_MAX)) { errno = ENAMETOOLONG; @@ -783,7 +818,8 @@ static casefold_verdict_t resolve_through_links(const char *sr, size_t bufsz, casefold_walk_t *walk, char *guest_out, - size_t guest_outsz) + size_t guest_outsz, + bool *followed_relative_out) { char splice[LINUX_PATH_MAX]; char norm[LINUX_PATH_MAX]; @@ -832,6 +868,8 @@ static casefold_verdict_t resolve_through_links(const char *sr, if (path_splice_link_target(cur, walk->link_guest_offset, target, rest, splice, sizeof(splice)) < 0) return CASEFOLD_ERROR; + if (followed_relative_out && target[0] != '/') + *followed_relative_out = true; /* A target may carry '..' that climbs above the guest root, which * the walk would append literally and follow out of the sysroot. Clamp * it exactly as an entry path is clamped; an interior '..' stays for @@ -858,19 +896,140 @@ static bool rebase_after_link(const char **path, const char *followed) return true; } +static casefold_verdict_t resolve_byte_exact_through_links( + const char *sr, + const char *path, + bool follow_final, + char *buf, + size_t bufsz, + char *guest_out, + size_t guest_outsz, + bool *followed_relative_out) +{ + char cur[LINUX_PATH_MAX]; + char splice[LINUX_PATH_MAX]; + char norm[LINUX_PATH_MAX]; + + if (str_copy_trunc(cur, path, sizeof(cur)) >= sizeof(cur)) { + errno = ENAMETOOLONG; + return CASEFOLD_ERROR; + } + + for (int depth = 0;; depth++) { + const char *p = cur; + size_t guest_len = 0; + bool found_link = false; + + if (snprintf(buf, bufsz, "%s%s", sr, cur) >= (int) bufsz) { + errno = ENAMETOOLONG; + return CASEFOLD_ERROR; + } + + while (*p == '/') + p++; + + while (*p) { + const char *name = p; + size_t name_len; + bool is_final; + char target[LINUX_PATH_MAX]; + const char *rest; + struct stat st; + ssize_t n; + + while (*p && *p != '/') + p++; + name_len = (size_t) (p - name); + while (*p == '/') + p++; + is_final = *p == '\0'; + guest_len = (size_t) (name - cur) + name_len; + + int hn = snprintf(buf, bufsz, "%s%.*s", sr, (int) guest_len, cur); + if (hn < 0 || (size_t) hn >= bufsz) { + errno = ENAMETOOLONG; + return CASEFOLD_ERROR; + } + + if (lstat(buf, &st) < 0) { + if (errno == ENOTDIR) + return CASEFOLD_ABSENT; + if (snprintf(buf, bufsz, "%s%s", sr, cur) >= (int) bufsz) + errno = ENAMETOOLONG; + return CASEFOLD_ABSENT; + } + + if (!S_ISLNK(st.st_mode) || (is_final && !follow_final)) + continue; + + if (depth >= MAXSYMLINKS) { + errno = ELOOP; + return CASEFOLD_ERROR; + } + + n = readlink(buf, target, sizeof(target) - 1); + if (n < 0) + return CASEFOLD_ERROR; + target[n] = '\0'; + + rest = cur + guest_len; + while (*rest == '/') + rest++; + if (path_splice_link_target(cur, (size_t) (name - cur), target, + rest, splice, sizeof(splice)) < 0) + return CASEFOLD_ERROR; + if (followed_relative_out && target[0] != '/') + *followed_relative_out = true; + if (!clamp_dotdot_at_guest_root(norm, splice, sizeof(norm))) { + errno = ENAMETOOLONG; + return CASEFOLD_ERROR; + } + if (str_copy_trunc(cur, norm, sizeof(cur)) >= sizeof(cur)) { + errno = ENAMETOOLONG; + return CASEFOLD_ERROR; + } + found_link = true; + break; + } + + if (found_link) + continue; + + if (str_copy_trunc(guest_out, cur, guest_outsz) >= guest_outsz) { + errno = ENAMETOOLONG; + return CASEFOLD_ERROR; + } + if (snprintf(buf, bufsz, "%s%s", sr, cur) >= (int) bufsz) { + errno = ENAMETOOLONG; + return CASEFOLD_ERROR; + } + return sysroot_path_exists(buf, follow_final) ? CASEFOLD_FOUND + : CASEFOLD_ABSENT; + } +} + static const char *proc_resolve_sysroot_path_flags(const char *path, char *buf, size_t bufsz, bool follow_final) { char sr[LINUX_PATH_MAX], clamped[LINUX_PATH_MAX]; - int seeded = sysroot_seed_host_path(path, buf, bufsz, sr, clamped); + int seeded = + sysroot_seed_host_path(path, buf, bufsz, sr, clamped, follow_final); if (seeded <= 0) return seeded < 0 ? NULL : path; /* Moves to the link's target once one is followed, so everything below * decides from where resolution actually ended up. */ const char *lookup = clamped; + char original_norm_path[LINUX_PATH_MAX]; + bool has_original_norm = lexical_normalize_absolute_path( + original_norm_path, lookup, sizeof(original_norm_path)); + const char *original_path_to_check = + has_original_norm ? original_norm_path : lookup; + bool forced_original_path = + is_sysroot_backed_temp_path(original_path_to_check) || + is_guest_system_path(original_path_to_check); /* Does this path name something under the sysroot, and if so under what * host spelling? On a volume that folds case those are one question: the @@ -882,12 +1041,13 @@ static const char *proc_resolve_sysroot_path_flags(const char *path, bool present; bool folded = false; bool followed_link = false; + bool followed_relative_link = false; char followed[LINUX_PATH_MAX]; if (casefold_active()) { casefold_walk_t walk; - casefold_verdict_t verdict = - resolve_through_links(sr, lookup, follow_final, buf, bufsz, &walk, - followed, sizeof(followed)); + casefold_verdict_t verdict = resolve_through_links( + sr, lookup, follow_final, buf, bufsz, &walk, followed, + sizeof(followed), &followed_relative_link); if (verdict == CASEFOLD_ERROR) return NULL; @@ -919,6 +1079,26 @@ static const char *proc_resolve_sysroot_path_flags(const char *path, * the host when it is not and the path is not a guest system directory. */ followed_link = rebase_after_link(&lookup, followed); + } else if (!forced_original_path) { + casefold_verdict_t verdict = resolve_byte_exact_through_links( + sr, lookup, follow_final, buf, bufsz, followed, sizeof(followed), + &followed_relative_link); + if (verdict == CASEFOLD_ERROR) + return NULL; + present = verdict == CASEFOLD_FOUND; + followed_link = rebase_after_link(&lookup, followed); + /* A probe that dies with ENOTDIR found a sysroot component that is + * not a directory. Linux resolves left to right and fails there with + * ENOTDIR (path_resolution(7)), so the sysroot has answered: the + * host fallback below must not run, or "file/" and "file/tail" would + * be reported against an unrelated host path, ENOENT where Linux + * owes ENOTDIR. The caller's own syscall against the sysroot + * spelling reproduces the right errno on the host. The containment + * check is skipped for the reason the folded return below gives: + * resolution never reaches anything past the offending component. + */ + if (!present && errno == ENOTDIR) + return buf; } else { int n = snprintf(buf, bufsz, "%s%s", sr, lookup); if (n < 0) { @@ -931,16 +1111,6 @@ static const char *proc_resolve_sysroot_path_flags(const char *path, return NULL; } present = sysroot_path_exists(buf, follow_final); - /* A probe that dies with ENOTDIR found a sysroot component that is - * not a directory. Linux resolves left to right and fails there with - * ENOTDIR (path_resolution(7)), so the sysroot has answered: the - * host fallback below must not run, or "file/" and "file/tail" would - * be reported against an unrelated host path, ENOENT where Linux - * owes ENOTDIR. The caller's own syscall against the sysroot - * spelling reproduces the right errno on the host. The containment - * check is skipped for the reason the folded return below gives: - * resolution never reaches anything past the offending component. - */ if (!present && errno == ENOTDIR) return buf; } @@ -969,6 +1139,15 @@ static const char *proc_resolve_sysroot_path_flags(const char *path, if (folded) return buf; + char real_input_path[LINUX_PATH_MAX]; + if (!forced_original_path && realpath(path, real_input_path)) { + size_t sr_len = strlen(sr); + if (strncmp(real_input_path, sr, sr_len) != 0 || + (sr_len != 1 && real_input_path[sr_len] != '\0' && + real_input_path[sr_len] != '/')) + return path; + } + /* Prevent escaping guest system paths to macOS host paths, which leads * to host contamination and permission failures (e.g. SIP/EPERM). * Classification reads the fully collapsed spelling, so "/usr/../home/x" @@ -985,29 +1164,19 @@ static const char *proc_resolve_sysroot_path_flags(const char *path, is_sysroot_backed_temp_path(path_to_check)) return buf; - /* A path reached by following a symlink never falls through to the host. - * An absolute target typed by the guest is one thing: the guest asked for - * it, and the host fallback is the documented answer. A target recorded - * inside the sysroot is another: honoring it would let anything that can - * write a symlink there hand the guest a file from outside the tree, which - * is the escape tests/test-sysroot-symlink-escape.c exists to prevent. - * Resolution stops at the sysroot spelling, so the caller's own syscall - * reports the path as missing, which is what it is in the guest's - * namespace. + /* A path reached by following a symlink has already been rebased to the + * target's guest spelling. For non-forced paths, keep the same host + * fallback rule as a path the guest typed directly: if the target exists + * on the host, hand that resolved target to the caller; otherwise leave + * the syscall on the sysroot spelling so dangling links report normally. */ if (followed_link) { - /* The sysroot does not have it. If the host does, the link was a way - * to reach a file outside the tree, and refusing is the whole point; - * report ELOOP, which is what resolution stopping at a link means and - * what callers of this resolver already propagate. If the host has - * nothing either the link simply dangles, and buf lets the caller's own - * syscall say so. - * - * The collapsed spelling is what the host is asked about: a target - * reaching out of the tree keeps the '..' components that carried it - * there, and those name nothing from the host's root. - */ - if (sysroot_path_exists(path_to_check, follow_final)) { + if (!followed_relative_link && + sysroot_path_exists(path_to_check, follow_final)) + return copy_host_fallback_path(path_to_check, buf, bufsz); + + if (followed_relative_link && + sysroot_path_exists(path_to_check, follow_final)) { errno = ELOOP; return NULL; } @@ -1035,7 +1204,7 @@ const char *proc_resolve_sysroot_create_path(const char *path, bool create_parents) { char sr[LINUX_PATH_MAX], clamped[LINUX_PATH_MAX]; - int seeded = sysroot_seed_host_path(path, buf, bufsz, sr, clamped); + int seeded = sysroot_seed_host_path(path, buf, bufsz, sr, clamped, false); if (seeded <= 0) return seeded < 0 ? NULL : path; /* Moves to the link's target once one is followed, so everything below @@ -1063,6 +1232,7 @@ const char *proc_resolve_sysroot_create_path(const char *path, * wrong-case one folds onto a directory the create would then land in. */ bool followed_link = false; + bool followed_relative_link = false; char followed[LINUX_PATH_MAX]; if (casefold_active()) { casefold_walk_t walk; @@ -1073,7 +1243,8 @@ const char *proc_resolve_sysroot_create_path(const char *path, * inside the directory the link names. */ if (resolve_through_links(sr, lookup, false, buf, bufsz, &walk, - followed, sizeof(followed)) == CASEFOLD_ERROR) + followed, sizeof(followed), + &followed_relative_link) == CASEFOLD_ERROR) return NULL; followed_link = rebase_after_link(&lookup, followed); if (str_copy_trunc(parent, buf, sizeof(parent)) >= sizeof(parent)) { @@ -1169,10 +1340,6 @@ const char *proc_resolve_sysroot_create_path(const char *path, if (!is_sysroot_backed_temp_path(path_to_check) && !is_guest_system_path(path_to_check)) { - /* As in the lookup resolver: a path reached by following a link may not - * fall through to the host, and after a link the guest path lives in a - * local buffer, so the input pointer is no longer the input. - */ if (followed_link) { errno = ELOOP; return NULL; diff --git a/tests/test-sysroot-symlink-escape.c b/tests/test-sysroot-symlink-escape.c index 5b9db553..e69503a5 100644 --- a/tests/test-sysroot-symlink-escape.c +++ b/tests/test-sysroot-symlink-escape.c @@ -8,19 +8,16 @@ * containment check on absolute guest paths, since it has no dirfd context to * rebuild a host location from a relative one. That left openat(dirfd, name) * to the host kernel's own resolution, unconfined to dirfd's subtree: a - * symlink reachable through a sysroot-contained dirfd -- with a relative - * target holding enough ".." components, or with an absolute target -- could - * walk straight out of the sysroot with no check at all, even though the - * exact same escape through an absolute guest path was already rejected with - * ELOOP. path_translate_at() now reconstructs the absolute guest path from - * the dirfd's guest base path and re-validates it through the same - * containment-checked resolver the absolute-path surface uses. + * symlink reachable through a sysroot-contained dirfd with a relative target + * holding enough ".." components could walk straight out of the sysroot with + * no check at all. path_translate_at() now reconstructs the absolute guest + * path from the dirfd's guest base path and re-validates it through the same + * resolver the absolute-path surface uses. * * The Makefile target stages an absolute-target symlink and a relative-target - * (deep "..") symlink under $sysroot/d1, both pointing at a secret file - * outside the sysroot, plus a normal in-sysroot file. This guest program only - * exercises the relative-dirfd surface (openat(dirfd, name, ...)); the - * absolute-path surface is already covered by test-sysroot-nofollow. + * (deep "..") symlink under $sysroot/d1, both pointing at a host file outside + * the sysroot, plus a normal in-sysroot file. The absolute target is a host + * bridge and should follow; the relative climb-out is still blocked. */ #include @@ -54,16 +51,24 @@ int main(void) } } - TEST("absolute-target symlink escape via dirfd is blocked"); + TEST("absolute-target symlink bridge via dirfd is followed"); { int fd = openat(dirfd, "abs-link", O_RDONLY); - if (fd < 0 && errno == ELOOP) + if (fd >= 0) { + close(fd); PASS(); - else { - if (fd >= 0) - close(fd); - FAIL("openat(dirfd, abs-link) did not fail with ELOOP"); - } + } else + FAIL("openat(dirfd, abs-link) failed"); + } + + TEST("relative link to absolute-target bridge via dirfd is followed"); + { + int fd = openat(dirfd, "chain-link", O_RDONLY); + if (fd >= 0) { + close(fd); + PASS(); + } else + FAIL("openat(dirfd, chain-link) failed"); } TEST("relative \"..\" symlink escape via dirfd is blocked"); @@ -85,13 +90,13 @@ int main(void) if (chdir("/d1") < 0) { FAIL("chdir /d1 failed"); } else { - int fd = open("abs-link", O_RDONLY); + int fd = open("rel-link", O_RDONLY); if (fd < 0 && errno == ELOOP) PASS(); else { if (fd >= 0) close(fd); - FAIL("open(AT_FDCWD, abs-link) did not fail with ELOOP"); + FAIL("open(AT_FDCWD, rel-link) did not fail with ELOOP"); } } } diff --git a/tests/test-sysroot-symlink-target.c b/tests/test-sysroot-symlink-target.c index 65e3f83a..a84b7936 100644 --- a/tests/test-sysroot-symlink-target.c +++ b/tests/test-sysroot-symlink-target.c @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -121,6 +122,36 @@ int main(void) TEST(" and reads through to it"); EXPECT_TRUE(reads("abs-escaped", "escaped") == 0, "content"); + TEST("a staged host symlink is followed"); + EXPECT_TRUE(stat("/host-home-link", &st) == 0 && S_ISDIR(st.st_mode), + "stat follows host link"); + TEST("a staged host symlink with trailing slash is followed"); + EXPECT_TRUE(stat("/host-home-link/", &st) == 0 && S_ISDIR(st.st_mode), + "stat follows host link as a directory"); + TEST("a staged host symlink opens as a directory"); + { + int f = open("/host-home-link/", O_RDONLY | O_DIRECTORY); + if (f >= 0) { + close(f); + PASS(); + } else { + FAIL("open follows host link as a directory"); + } + } + TEST("a relative staged host symlink opens as a directory"); + { + DIR *dir; + if (chdir("/home/muplar") < 0) { + FAIL("chdir /home/muplar"); + } else if ((dir = opendir("Download/")) != NULL) { + closedir(dir); + PASS(); + } else { + FAIL("opendir follows relative host link as a directory"); + } + EXPECT_TRUE(chdir("/") == 0, "restore cwd"); + } + /* readlink reports the disk. A relative target is stored verbatim, so * the guest's own bytes come back; an absolute one was rewritten at * symlink() time to the sysroot-relative spelling (see the header), and