diff --git a/src/syscall/casefold-walk.c b/src/syscall/casefold-walk.c index bfee3a2e..a7c84198 100644 --- a/src/syscall/casefold-walk.c +++ b/src/syscall/casefold-walk.c @@ -155,7 +155,8 @@ const char *casefold_attr_stored_name(const void *reply, static probe_result_t probe_exact(host_fd_t base_fd, const char *path, const char *leaf, - bool *is_link) + bool *is_link, + bool *type_known) { /* ATTR_CMN_OBJTYPE rides along on a request already being made, so knowing * whether the entry is a symlink costs nothing beyond the byte comparison @@ -183,8 +184,8 @@ static probe_result_t probe_exact(host_fd_t base_fd, char name[CASEFOLD_STORED_NAME_MAX]; } __attribute__((aligned(4), packed)) attr_buf; - if (is_link) - *is_link = false; + *is_link = false; + *type_known = false; if (getattrlistat(base_fd, path, &al, &attr_buf, sizeof(attr_buf), FSOPT_NOFOLLOW) == 0) { @@ -208,9 +209,11 @@ static probe_result_t probe_exact(host_fd_t base_fd, size_t obj_end = (size_t) ((const char *) &attr_buf.obj_type - (const char *) &attr_buf) + sizeof(attr_buf.obj_type); - if (is_link && (attr_buf.returned.commonattr & ATTR_CMN_OBJTYPE) && - obj_end <= usable) + if ((attr_buf.returned.commonattr & ATTR_CMN_OBJTYPE) && + obj_end <= usable) { *is_link = attr_buf.obj_type == VLNK; + *type_known = true; + } if (!strcmp(stored, leaf)) return PROBE_EXACT; /* A mismatch is not yet a fold. For a second hard link to a @@ -220,7 +223,13 @@ static probe_result_t probe_exact(host_fd_t base_fd, * asked can still come back under another name. Only the listing * tells an aliased name from a genuinely folded one, and only a * mismatch pays for the scan. + * + * The listing carries no type, and this call typed the entry the + * volume named, not the one the listing finds, so withdraw it: + * all_types_known below is what lets a caller skip the canonical + * containment recheck. */ + *type_known = false; return probe_by_readdir(base_fd, path, leaf); } /* The call succeeded but the volume withheld the name or handed back @@ -323,40 +332,55 @@ static int name_by_rule(const char *guest, char *out, size_t outsz) return casefold_escape(guest, out, outsz); } -/* Spell one component, given the parent already spelled in @out. Reports - * through @present whether the entry is there, and writes the host spelling - * into @host. +/* Probe @cand appended to the parent in @out[0..len), restoring @out to that + * prefix on every exit. append_component writes the separator before the + * length check, so an unrestored buffer would keep a trailing '/'. + */ +static probe_result_t probe_candidate(host_fd_t base_fd, + char *out, + size_t outsz, + size_t len, + const char *cand, + bool *is_link, + bool *type_known) +{ + size_t probe_len = len; + probe_result_t verdict; + + if (append_component(out, outsz, &probe_len, cand) < 0) { + out[len] = '\0'; + return PROBE_ERROR; + } + verdict = probe_exact(base_fd, out, cand, is_link, type_known); + out[len] = '\0'; + return verdict; +} + +/* Spell one component, given the parent already spelled in @out. The entry is + * there exactly when the verdict is PROBE_EXACT; the host spelling goes to + * @host either way. @out doubles as the probe buffer. A candidate that does + * not fit reports ENAMETOOLONG exactly as the final spelling would, an escape + * never being shorter than the literal it stands for. */ static probe_result_t resolve_component(host_fd_t base_fd, - const char *out, + char *out, + size_t outsz, size_t len, const char *guest, char *host, size_t hostsz, - bool *present, - bool *is_link) + bool *is_link, + bool *type_known) { - char probe_path[LINUX_PATH_MAX]; - size_t probe_len = len; probe_result_t verdict; - *present = false; - /* An escape-shaped guest name is stored escaped unconditionally, so it can * never be mistaken for the encoding of a different name. Probing its * literal spelling would find some unrelated file. */ if (!casefold_is_escaped(guest)) { - if (str_copy_trunc(probe_path, out, sizeof(probe_path)) >= - sizeof(probe_path)) { - errno = ENAMETOOLONG; - return PROBE_ERROR; - } - if (append_component(probe_path, sizeof(probe_path), &probe_len, - guest) < 0) - return PROBE_ERROR; - - verdict = probe_exact(base_fd, probe_path, guest, is_link); + verdict = probe_candidate(base_fd, out, outsz, len, guest, is_link, + type_known); if (verdict == PROBE_ERROR) return PROBE_ERROR; if (verdict == PROBE_EXACT) { @@ -364,7 +388,6 @@ static probe_result_t resolve_component(host_fd_t base_fd, errno = ENAMETOOLONG; return PROBE_ERROR; } - *present = true; return PROBE_EXACT; } } else { @@ -374,34 +397,25 @@ static probe_result_t resolve_component(host_fd_t base_fd, * below says otherwise. */ verdict = PROBE_ABSENT; + *type_known = false; } /* The literal spelling is not what is stored. Whatever the reason (a * differently-spelled sibling in the slot, a name the volume refuses, or * simply nothing there), the escape is the only other place the name can * live, so ask whether it does. + * + * The escape cannot fail: casefold.h sizes @host by + * CASEFOLD_HOST_NAME_MAX for any name path_component_copy delivers, so a + * failure is a broken precondition and fails closed. */ - if (casefold_escape(guest, host, hostsz) < 0) { - if (errno != ENAMETOOLONG && errno != EINVAL) - return PROBE_ERROR; - /* Cannot be escaped, so the literal spelling is the only candidate and - * the probe already answered for it. - */ - return name_by_rule(guest, host, hostsz) < 0 ? PROBE_ERROR : verdict; - } - - probe_len = len; - if (str_copy_trunc(probe_path, out, sizeof(probe_path)) >= - sizeof(probe_path)) { - errno = ENAMETOOLONG; - return PROBE_ERROR; - } - if (append_component(probe_path, sizeof(probe_path), &probe_len, host) < 0) + if (casefold_escape(guest, host, hostsz) < 0) return PROBE_ERROR; - switch (probe_exact(base_fd, probe_path, host, is_link)) { + probe_result_t escape_verdict = + probe_candidate(base_fd, out, outsz, len, host, is_link, type_known); + switch (escape_verdict) { case PROBE_EXACT: - *present = true; return PROBE_EXACT; case PROBE_ERROR: return PROBE_ERROR; @@ -443,7 +457,10 @@ casefold_verdict_t casefold_resolve_at(host_fd_t base_fd, size_t comp_len; size_t len; bool absent = false; + casefold_walk_t local; + if (!walk) + walk = &local; walk->parent_found = true; walk->parent_offset = 0; walk->link_rest_offset = 0; @@ -451,6 +468,9 @@ casefold_verdict_t casefold_resolve_at(host_fd_t base_fd, walk->leaf_offset = 0; walk->folded = false; walk->notdir = false; + walk->leaf_type_known = false; + walk->leaf_is_link = false; + walk->all_types_known = true; len = str_copy_trunc(out, base_host_prefix ? base_host_prefix : "", outsz); if (len >= outsz) { @@ -461,7 +481,6 @@ casefold_verdict_t casefold_resolve_at(host_fd_t base_fd, while (path_next_component(&scan, &comp, &comp_len)) { char guest[CASEFOLD_GUEST_NAME_MAX + 1]; char host[CASEFOLD_HOST_NAME_MAX + 1]; - bool present = false; if (path_component_copy(guest, sizeof(guest), comp, comp_len) < 0) return CASEFOLD_ERROR; @@ -473,6 +492,8 @@ casefold_verdict_t casefold_resolve_at(host_fd_t base_fd, * the host kernel resolves those against the real descriptor. */ if (!strcmp(guest, ".") || !strcmp(guest, "..")) { + walk->leaf_type_known = false; + walk->leaf_is_link = false; if (append_leaf(out, outsz, &len, guest, walk) < 0) return CASEFOLD_ERROR; continue; @@ -483,16 +504,23 @@ casefold_verdict_t casefold_resolve_at(host_fd_t base_fd, * nothing needs to be: the spelling follows from the name. */ walk->parent_found = false; + walk->leaf_type_known = false; + walk->leaf_is_link = false; if (name_by_rule(guest, host, sizeof(host)) < 0) return CASEFOLD_ERROR; } else { bool is_link = false; + bool type_known = false; probe_result_t verdict = - resolve_component(base_fd, out, len, guest, host, sizeof(host), - &present, &is_link); + resolve_component(base_fd, out, outsz, len, guest, host, + sizeof(host), &is_link, &type_known); if (verdict == PROBE_ERROR) return CASEFOLD_ERROR; + walk->leaf_type_known = verdict == PROBE_EXACT && type_known; + walk->leaf_is_link = walk->leaf_type_known && is_link; + if (verdict == PROBE_EXACT && !type_known) + walk->all_types_known = false; /* A link the walk has to pass through stops it. That is every * intermediate component, and the final one only when the caller @@ -505,7 +533,7 @@ casefold_verdict_t casefold_resolve_at(host_fd_t base_fd, * sysroot rather than at the host root. Handing them to the kernel * looks somewhere else entirely. */ - if (present && is_link) { + if (verdict == PROBE_EXACT && is_link) { const char *rest = scan; while (*rest == '/') @@ -533,7 +561,7 @@ casefold_verdict_t casefold_resolve_at(host_fd_t base_fd, */ if (verdict == PROBE_NOTDIR) walk->notdir = true; - absent = !present; + absent = verdict != PROBE_EXACT; } if (append_leaf(out, outsz, &len, host, walk) < 0) @@ -544,10 +572,13 @@ casefold_verdict_t casefold_resolve_at(host_fd_t base_fd, return CASEFOLD_ABSENT; /* The probe deliberately stops at a symlink rather than following it, so a - * caller that asked about the target has to say so. A link pointing nowhere - * is absent for that caller, which is what an access(2) probe would report. + * caller that asked about the target has to say so. A link pointing + * nowhere is absent for that caller, which is what an access(2) probe + * would report. A typed leaf is a known non-link (a known link returned + * CASEFOLD_SYMLINK above), so the probe adds nothing and is skipped. */ - if (follow_final && faccessat(base_fd, out, F_OK, 0) < 0) + if (follow_final && !walk->leaf_type_known && + faccessat(base_fd, out, F_OK, 0) < 0) return CASEFOLD_ABSENT; return CASEFOLD_FOUND; } diff --git a/src/syscall/casefold-walk.h b/src/syscall/casefold-walk.h index 2115ff95..337a9c5f 100644 --- a/src/syscall/casefold-walk.h +++ b/src/syscall/casefold-walk.h @@ -85,6 +85,20 @@ typedef struct { * rebuild the directory a relative target is measured from. */ size_t link_guest_offset; + /* Object type the leaf's probe answered. False when the readdir fallback + * answered, whose listing carries no type, and for dot components, which + * are never probed. + */ + bool leaf_type_known; + bool leaf_is_link; + /* True when every component that resolved was typed by its probe, so no + * symlink can hide in the resolved path; only the readdir fallback, + * whose listing carries no type, can pass one unseen. Dot components + * navigate rather than name an entry, and components below an absent one + * name nothing, so neither withholds a type. A FOUND path with this set + * cannot resolve outside the prefix it was built under. + */ + bool all_types_known; } casefold_walk_t; /* Resolve @guest_path, interpreted relative to @base_fd, into its host spelling @@ -113,6 +127,12 @@ typedef struct { * * @follow_final rechecks the resolved object through symlinks, so a dangling * link reports absent, matching what an access(2) existence probe would say. + * + * @walk may be NULL when the caller needs only the verdict and @out. + * + * @out doubles as the walk's probe scratch, so it must not overlap + * @guest_path or @base_host_prefix, and it holds nothing meaningful once + * CASEFOLD_ERROR is returned. */ casefold_verdict_t casefold_resolve_at(host_fd_t base_fd, const char *base_host_prefix, diff --git a/src/syscall/casefold.c b/src/syscall/casefold.c index 93be0874..f2ada138 100644 --- a/src/syscall/casefold.c +++ b/src/syscall/casefold.c @@ -45,92 +45,6 @@ static unsigned hex_value(char c) return c <= '9' ? (unsigned) (c - '0') : (unsigned) (c - 'a' + 10); } -/* True when @s is well-formed UTF-8: no overlong forms, no surrogates, nothing - * above U+10FFFF. APFS refuses to create a name that is not, so an ill-formed - * name has to be escaped rather than stored. Not exported: the judgment is - * observable through casefold_utf16_units, which reports no unit cost for a - * name the volume will not hold literally. - */ -static bool casefold_utf8_valid(const char *s) -{ - const unsigned char *p = (const unsigned char *) s; - - while (*p) { - unsigned char c = *p; - unsigned extra; - uint32_t cp; - - if (c < 0x80) { - p++; - continue; - } - if (c >= 0xC2 && c <= 0xDF) { - extra = 1; - cp = c & 0x1Fu; - } else if (c >= 0xE0 && c <= 0xEF) { - extra = 2; - cp = c & 0x0Fu; - } else if (c >= 0xF0 && c <= 0xF4) { - extra = 3; - cp = c & 0x07u; - } else { - /* 0x80-0xC1 is a stray continuation or an overlong two-byte lead; - * 0xF5-0xFF encodes above U+10FFFF. - */ - return false; - } - - for (unsigned i = 0; i < extra; i++) { - unsigned char cc = p[1 + i]; - if ((cc & 0xC0u) != 0x80u) - return false; - cp = (cp << 6) | (cc & 0x3Fu); - } - - /* Reject the forms that encode a code point in more bytes than needed, - * the UTF-16 surrogate range, and anything past the Unicode maximum. - * Each has more than one byte sequence otherwise, which would break the - * one-spelling-per-name property. - */ - if (extra == 2 && cp < 0x800u) - return false; - if (extra == 3 && cp < 0x10000u) - return false; - if (cp >= 0xD800u && cp <= 0xDFFFu) - return false; - if (cp > 0x10FFFFu) - return false; - - p += 1 + extra; - } - return true; -} - -size_t casefold_utf16_units(const char *s) -{ - const unsigned char *p = (const unsigned char *) s; - size_t units = 0; - - if (!casefold_utf8_valid(s)) - return 0; - - while (*p) { - if (*p < 0x80) - p += 1; - else if (*p < 0xE0) - p += 2; - else if (*p < 0xF0) - p += 3; - else { - /* Above the BMP: encoded as a surrogate pair, so two units. */ - p += 4; - units++; - } - units++; - } - return units; -} - bool casefold_needs_escape(const char *name) { if (!name || name[0] == '\0') diff --git a/src/syscall/casefold.h b/src/syscall/casefold.h index 37311c68..a52184ba 100644 --- a/src/syscall/casefold.h +++ b/src/syscall/casefold.h @@ -84,12 +84,6 @@ _Static_assert(CASEFOLD_PREFIX_LEN + CASEFOLD_UNIT_MAX, "long-tier escape exceeds the per-name UTF-16 unit limit"); -/* UTF-16 code units @s occupies, which is what the host counts against its - * per-name limit: one per code point below U+10000, two above. Returns 0 when - * @s is not valid UTF-8, which callers treat as "cannot be stored literally". - */ -size_t casefold_utf16_units(const char *s); - /* True when @name cannot be stored under its own spelling. That is any name * carrying an uppercase ASCII letter or a byte >= 0x80, plus any name that is * itself escape-shaped so it cannot be confused with the escape of another diff --git a/src/syscall/path.c b/src/syscall/path.c index 4567e8f0..3348ff4e 100644 --- a/src/syscall/path.c +++ b/src/syscall/path.c @@ -195,8 +195,6 @@ static int path_check_relative_sysroot_containment(guest_fd_t dirfd, const char *path, unsigned int flags, bool *in_sysroot, - char *abs_out, - size_t abs_outsz, char *host_out, size_t host_outsz); @@ -297,13 +295,11 @@ int path_translate_at(guest_fd_t dirfd, */ bool climbed_root = false; bool relative_in_sysroot = false; - char relative_abs[LINUX_PATH_MAX]; 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, lookup_flags, &relative_in_sysroot, - relative_abs, sizeof(relative_abs), relative_host, - sizeof(relative_host)); + relative_host, sizeof(relative_host)); if (recheck < 0) { tx->host_path = NULL; if (errno == 0) @@ -350,16 +346,14 @@ int path_translate_at(guest_fd_t dirfd, bool follow_final = !(lookup_flags & (PATH_TR_NOFOLLOW | PATH_TR_CREATE)); host_fd_ref_t ref; - casefold_walk_t walk; casefold_verdict_t verdict; if (host_dirfd_ref_open(dirfd, &ref) < 0) { errno = EBADF; return -1; } - verdict = - casefold_resolve_at(ref.fd, "", tx->guest_path, follow_final, - tx->host_buf, sizeof(tx->host_buf), &walk); + verdict = casefold_resolve_at(ref.fd, "", tx->guest_path, follow_final, + tx->host_buf, sizeof(tx->host_buf), NULL); host_fd_ref_close(&ref); if (verdict == CASEFOLD_ERROR) return -1; @@ -564,10 +558,9 @@ int sys_path_has_symlink(guest_fd_t dirfd, const char *path) char sr[LINUX_PATH_MAX]; if (proc_sysroot_snapshot(sr, sizeof(sr))) { - casefold_walk_t walk; casefold_verdict_t verdict = casefold_resolve_at(AT_FDCWD, sr, path, false, sysroot_buf, - sizeof(sysroot_buf), &walk); + sizeof(sysroot_buf), NULL); if (verdict == CASEFOLD_ERROR) return -1; @@ -641,7 +634,7 @@ int sys_path_has_symlink(guest_fd_t dirfd, const char *path) bool in_sysroot = false; int recheck = path_check_relative_sysroot_containment( - dirfd, path, PATH_TR_NOFOLLOW, &in_sysroot, NULL, 0, NULL, 0); + dirfd, path, PATH_TR_NOFOLLOW, &in_sysroot, NULL, 0); if (recheck < 0) { rc = -1; goto out; @@ -653,10 +646,9 @@ int sys_path_has_symlink(guest_fd_t dirfd, const char *path) * through its !climbed_root gate. */ if (in_sysroot && recheck == 0) { - casefold_walk_t walk; casefold_verdict_t verdict = casefold_resolve_at(current_fd, "", path, false, sysroot_buf, - sizeof(sysroot_buf), &walk); + sizeof(sysroot_buf), NULL); if (verdict == CASEFOLD_ERROR) { rc = -1; @@ -1427,20 +1419,15 @@ static int dirfd_symlink_chain_reaches_absolute_target(guest_fd_t dirfd, 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 - * sysroot claims the reconstructed path. @abs_out (the reconstructed absolute - * guest path) and @host_out (the resolved host spelling, filled for a climbed - * or in-sysroot path) hand the work back to callers that need it; a NULL - * pointer opts out of either. +/* Returns 1 when the reconstruction climbed the guest root, so the caller + * opens @host_out instead of walking from dirfd; 0 when it stays beneath; -1 + * with errno set. @in_sysroot reports whether the sysroot claims the path; + * @host_out is filled for a climbed or in-sysroot path, NULL to opt out. */ static int path_check_relative_sysroot_containment(guest_fd_t dirfd, const char *path, unsigned int flags, bool *in_sysroot, - char *abs_out, - size_t abs_outsz, char *host_out, size_t host_outsz) { @@ -1504,22 +1491,10 @@ static int path_check_relative_sysroot_containment(guest_fd_t dirfd, * prefix of its own to make that call from. */ *in_sysroot = checked != abs_path; - - /* The reconstruction is not free, and a caller that has to follow a symlink - * needs the same absolute path to do it, so hand it back rather than make - * it build one of its own that could differ. - */ - if (abs_out && str_copy_trunc(abs_out, abs_path, abs_outsz) >= abs_outsz) { - errno = ENAMETOOLONG; - return -1; - } - - /* The resolution itself is not free either. A caller whose own walk stops - * at a link needs exactly this host path (resolved with the same flag - * mapping), and re-deriving it invites the two mappings to drift. A path - * that clamped at the guest root needs it whichever way it resolved: both - * spellings are absolute, so either lands where the guest's own resolution - * would, and the descriptor drops out of the walk. + /* Handing the resolution back rather than letting a caller re-derive it + * keeps the two flag mappings from drifting. A path that clamped at the + * guest root needs it either way: both spellings are absolute, so the + * descriptor drops out of the walk. */ if (host_out && (*in_sysroot || climbed) && str_copy_trunc(host_out, checked, host_outsz) >= host_outsz) { @@ -1666,14 +1641,20 @@ static int reset_walk_fd(host_fd_t *current_fd, host_fd_t root_fd) * the walker would then report the absence rather than what is actually there. * Outside a sysroot, and for any name needing no escape, this is the name * itself. + * + * @is_link: -1 unknown (the caller must stat), 0 not a symlink or not there, + * 1 a symlink. Only the readdir fallback, whose listing carries no type, + * leaves it unknown. */ static int host_component_spelling(host_fd_t dirfd, const char *guest, char *out, - size_t outsz) + size_t outsz, + int *is_link) { casefold_walk_t walk; + *is_link = -1; if (!casefold_active()) { if (str_copy_trunc(out, guest, outsz) >= outsz) { errno = ENAMETOOLONG; @@ -1681,10 +1662,19 @@ static int host_component_spelling(host_fd_t dirfd, } return 0; } - return casefold_resolve_at(dirfd, "", guest, false, out, outsz, &walk) == - CASEFOLD_ERROR - ? -1 - : 0; + casefold_verdict_t verdict = + casefold_resolve_at(dirfd, "", guest, false, out, outsz, &walk); + if (verdict == CASEFOLD_ERROR) + return -1; + /* Any non-FOUND verdict here means not there: @guest is one component + * with follow_final false, so CASEFOLD_SYMLINK, which the walk returns + * only for a link it must pass through, cannot come back. + */ + if (verdict != CASEFOLD_FOUND) + *is_link = 0; + else if (walk.leaf_type_known) + *is_link = walk.leaf_is_link; + return 0; } int path_openat2_crosses_mount(guest_fd_t dirfd, @@ -1797,68 +1787,76 @@ int path_openat2_crosses_mount(guest_fd_t dirfd, goto out; } + int leaf_link = -1; if (host_walk && host_component_spelling(current_fd, name, host_name, - sizeof(host_name)) < 0) + sizeof(host_name), &leaf_link) < 0) goto out; - struct stat st; - if (host_walk && - fstatat(current_fd, host_name, &st, AT_SYMLINK_NOFOLLOW) == 0) { - if (S_ISLNK(st.st_mode)) { - if (guest_path_append(current, sizeof(current), comp, len) < - 0) - goto out; + /* ENOENT still yields a verdict: nothing there can be a link. + * Any other errno leaves link-ness undecided, and an undecided + * component must not be walked through as a directory. + */ + if (host_walk && leaf_link < 0) { + struct stat st; - int cls = classify_guest_path_mount(current); - if (cls < 0) { - errno = EINVAL; - goto out; - } - if (cls != start_class) { - rc = 1; - goto out; - } - str_copy_trunc(current, parent, sizeof(current)); + if (fstatat(current_fd, host_name, &st, AT_SYMLINK_NOFOLLOW) == + 0) + leaf_link = S_ISLNK(st.st_mode) ? 1 : 0; + else if (errno != ENOENT) + goto out; + else + leaf_link = 0; + } + if (host_walk && leaf_link == 1) { + if (guest_path_append(current, sizeof(current), comp, len) < 0) + goto out; - char target[LINUX_PATH_MAX]; - ssize_t target_len = readlinkat(current_fd, host_name, - target, sizeof(target) - 1); - if (target_len < 0) - goto out; - if (++symlink_count > MAXSYMLINKS) { - errno = ELOOP; - goto out; - } - target[target_len] = '\0'; - - /* No prefix: an absolute target re-anchors the walk fd - * below, and a relative one continues from current_fd, - * which already names the link's directory. - */ - if (path_splice_link_target(NULL, 0, target, walk, pending, - sizeof(pending)) < 0) - goto out; - walk = pending; + int cls = classify_guest_path_mount(current); + if (cls < 0) { + errno = EINVAL; + goto out; + } + if (cls != start_class) { + rc = 1; + goto out; + } + str_copy_trunc(current, parent, sizeof(current)); - if (target[0] == '/') { - host_fd_t reset_fd = - in_root ? root_fd : absolute_root_fd; - if (reset_walk_fd(¤t_fd, reset_fd) < 0) + char target[LINUX_PATH_MAX]; + ssize_t target_len = readlinkat(current_fd, host_name, target, + sizeof(target) - 1); + if (target_len < 0) + goto out; + if (++symlink_count > MAXSYMLINKS) { + errno = ELOOP; + goto out; + } + target[target_len] = '\0'; + + /* No prefix: an absolute target re-anchors the walk fd + * below, and a relative one continues from current_fd, + * which already names the link's directory. + */ + if (path_splice_link_target(NULL, 0, target, walk, pending, + sizeof(pending)) < 0) + goto out; + walk = pending; + + if (target[0] == '/') { + host_fd_t reset_fd = in_root ? root_fd : absolute_root_fd; + if (reset_walk_fd(¤t_fd, reset_fd) < 0) + goto out; + if (in_root) { + if (dirfd_guest_base_path(dirfd, current, + sizeof(current)) < 0) goto out; - if (in_root) { - if (dirfd_guest_base_path(dirfd, current, - sizeof(current)) < 0) - goto out; - } else { - current[0] = '/'; - current[1] = '\0'; - } + } else { + current[0] = '/'; + current[1] = '\0'; } - continue; } - } else if (host_walk && errno != ENOENT) { - goto out; + continue; } if (guest_path_append(current, sizeof(current), comp, len) < 0) diff --git a/src/syscall/proc-state.c b/src/syscall/proc-state.c index 4024730b..9ce1b758 100644 --- a/src/syscall/proc-state.c +++ b/src/syscall/proc-state.c @@ -5,6 +5,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include #include #include #include @@ -55,7 +56,16 @@ static char sysroot_path[LINUX_PATH_MAX] = {0}; * the snprintf input buffer underneath that thread. */ static pthread_mutex_t sysroot_lock = PTHREAD_MUTEX_INITIALIZER; -static bool sysroot_casefold = false; +/* Atomic, not guarded by sysroot_lock: published once at startup and to a + * forked child before any vCPU thread issues a syscall, and no other state is + * kept consistent with it. + */ +static _Atomic bool sysroot_casefold = false; +/* True when proc_set_sysroot's realpath succeeded, so containment checks can + * copy sysroot_path instead of re-deriving it; false leaves them the per-call + * realpath. Atomic for the same reason as the fold flag above. + */ +static _Atomic bool sysroot_canonical = false; /* Cached current working directory for getcwd() and /proc/self/cwd. */ static pthread_mutex_t cwd_lock = PTHREAD_MUTEX_INITIALIZER; @@ -74,6 +84,7 @@ void proc_state_init(void) auxv_len = 0; sysroot_path[0] = '\0'; sysroot_casefold = false; + sysroot_canonical = false; pthread_mutex_lock(&cwd_lock); cwd_path[0] = '\0'; @@ -382,14 +393,17 @@ const void *proc_get_auxv(size_t *len_out) void proc_set_sysroot(const char *path) { pthread_mutex_lock(&sysroot_lock); + sysroot_canonical = false; if (path && path[0]) { str_copy_trunc(sysroot_path, path, sizeof(sysroot_path)); size_t len = strlen(sysroot_path); while (len > 1 && sysroot_path[len - 1] == '/') sysroot_path[--len] = '\0'; char resolved[LINUX_PATH_MAX]; - if (realpath(sysroot_path, resolved)) + if (realpath(sysroot_path, resolved)) { str_copy_trunc(sysroot_path, resolved, sizeof(sysroot_path)); + sysroot_canonical = true; + } } else { sysroot_path[0] = '\0'; } @@ -427,18 +441,12 @@ bool proc_sysroot_snapshot(char *out, size_t outsz) void proc_set_sysroot_casefold(bool enabled) { - pthread_mutex_lock(&sysroot_lock); - sysroot_casefold = enabled; - pthread_mutex_unlock(&sysroot_lock); + atomic_store(&sysroot_casefold, enabled); } bool proc_sysroot_casefold_enabled(void) { - bool enabled; - pthread_mutex_lock(&sysroot_lock); - enabled = sysroot_casefold; - pthread_mutex_unlock(&sysroot_lock); - return enabled; + return atomic_load(&sysroot_casefold); } /* True when realpath(3) failed because the path stopped resolving rather than @@ -476,7 +484,12 @@ static bool sysroot_path_is_contained(const char *resolved_path, { char real_sysroot[LINUX_PATH_MAX], real_path[LINUX_PATH_MAX]; - if (!realpath(sysroot, real_sysroot)) + /* proc_set_sysroot canonicalized @sysroot at configuration time, so + * re-deriving it here would realpath() a process constant per check. + */ + if (sysroot_canonical) + str_copy_trunc(real_sysroot, sysroot, sizeof(real_sysroot)); + else if (!realpath(sysroot, real_sysroot)) return realpath_vanished(); if (follow_final) { @@ -682,6 +695,8 @@ static bool clamp_dotdot_at_guest_root(char *dest, /* Snapshot the sysroot, clamp @path, and spell the host path it names. * Returns 1 with @buf, @sr, and @clamped filled, 0 when sysroot resolution * does not apply and the caller owes its input back, or -1 with errno set. + * On 0 the outputs are indeterminate: a relative path opts out before the + * snapshot runs, so @sr is not even the empty string. */ static int sysroot_seed_host_path(const char *path, char *buf, @@ -690,7 +705,8 @@ static int sysroot_seed_host_path(const char *path, char clamped[LINUX_PATH_MAX], bool follow_final) { - if (!proc_sysroot_snapshot(sr, LINUX_PATH_MAX) || !path || path[0] != '/') + /* Ordered so a relative path opts out before the snapshot locks. */ + if (!path || path[0] != '/' || !proc_sysroot_snapshot(sr, LINUX_PATH_MAX)) return 0; char real_path[LINUX_PATH_MAX]; @@ -1048,6 +1064,7 @@ static const char *proc_resolve_sysroot_path_flags(const char *path, bool folded = false; bool followed_link = false; bool followed_relative_link = false; + bool walk_typed_all = false; char followed[LINUX_PATH_MAX]; if (casefold_active()) { casefold_walk_t walk; @@ -1059,6 +1076,7 @@ static const char *proc_resolve_sysroot_path_flags(const char *path, return NULL; present = verdict == CASEFOLD_FOUND; folded = walk.folded; + walk_typed_all = present && walk.all_types_known; /* The walk stopped at a component that is not a directory, which is * the answer the byte-exact branch below reads off ENOTDIR: resolution * fails there (path_resolution(7)) and the host fallback must not run, @@ -1106,22 +1124,21 @@ static const char *proc_resolve_sysroot_path_flags(const char *path, if (!present && errno == ENOTDIR) return buf; } else { - int n = snprintf(buf, bufsz, "%s%s", sr, lookup); - if (n < 0) { - if (errno == 0) - errno = EINVAL; - return NULL; - } - if ((size_t) n >= bufsz) { - errno = ENAMETOOLONG; - return NULL; - } + /* @buf already holds sr + the clamped lookup, spelled by the seed. */ present = sysroot_path_exists(buf, follow_final); if (!present && errno == ENOTDIR) return buf; } if (present) { + /* A walk that typed every component has proven containment: '..' was + * clamped before it ran and no unseen symlink can hide in a fully + * typed path, so realpath() would re-derive the same bytes. The + * recheck remains for a readdir-answered component and for the + * byte-exact arm, which runs no walk. + */ + if (walk_typed_all) + return buf; if (!sysroot_path_is_contained(buf, sr, follow_final)) { errno = ELOOP; return NULL; @@ -1282,16 +1299,7 @@ const char *proc_resolve_sysroot_create_path(const char *path, return NULL; } } else { - int n = snprintf(buf, bufsz, "%s%s", sr, lookup); - if (n < 0) { - if (errno == 0) - errno = EINVAL; - return NULL; - } - if ((size_t) n >= bufsz) { - errno = ENAMETOOLONG; - return NULL; - } + /* @buf already holds sr + the clamped lookup, spelled by the seed. */ /* An all-slash guest path ("/", "///") names the root, which always * exists and has no parent to check; trimming it would walk strrchr diff --git a/tests/test-casefold-host.c b/tests/test-casefold-host.c index c2709cac..033ddaed 100644 --- a/tests/test-casefold-host.c +++ b/tests/test-casefold-host.c @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,95 @@ #include "host-test-util.h" #include "syscall/casefold.h" +/* True when @s is well-formed UTF-8: no overlong forms, no surrogates, + * nothing above U+10FFFF. APFS refuses to create a name that is not, so an + * ill-formed name has to be escaped rather than stored. + */ +static bool casefold_utf8_valid(const char *s) +{ + const unsigned char *p = (const unsigned char *) s; + + while (*p) { + unsigned char c = *p; + unsigned extra; + uint32_t cp; + + if (c < 0x80) { + p++; + continue; + } + if (c >= 0xC2 && c <= 0xDF) { + extra = 1; + cp = c & 0x1Fu; + } else if (c >= 0xE0 && c <= 0xEF) { + extra = 2; + cp = c & 0x0Fu; + } else if (c >= 0xF0 && c <= 0xF4) { + extra = 3; + cp = c & 0x07u; + } else { + /* 0x80-0xC1 is a stray continuation or an overlong two-byte lead; + * 0xF5-0xFF encodes above U+10FFFF. + */ + return false; + } + + for (unsigned i = 0; i < extra; i++) { + unsigned char cc = p[1 + i]; + if ((cc & 0xC0u) != 0x80u) + return false; + cp = (cp << 6) | (cc & 0x3Fu); + } + + /* Reject the forms that encode a code point in more bytes than + * needed, the UTF-16 surrogate range, and anything past the Unicode + * maximum. Each has more than one byte sequence otherwise, which + * would break the one-spelling-per-name property. + */ + if (extra == 2 && cp < 0x800u) + return false; + if (extra == 3 && cp < 0x10000u) + return false; + if (cp >= 0xD800u && cp <= 0xDFFFu) + return false; + if (cp > 0x10FFFFu) + return false; + + p += 1 + extra; + } + return true; +} + +/* The UTF-16 code units @s occupies, which is what the volume counts against + * its per-name limit. A second implementation of the codec's arithmetic, so a + * change breaking the unit budget shows up as a disagreement here rather than + * in both sides at once. 0 means @s is not well-formed UTF-8. + */ +static size_t casefold_utf16_units(const char *s) +{ + const unsigned char *p = (const unsigned char *) s; + size_t units = 0; + + if (!casefold_utf8_valid(s)) + return 0; + + while (*p) { + if (*p < 0x80) + p += 1; + else if (*p < 0xE0) + p += 2; + else if (*p < 0xF0) + p += 3; + else { + /* Above the BMP: encoded as a surrogate pair, so two units. */ + p += 4; + units++; + } + units++; + } + return units; +} + /* Print a name so a failure is diagnosable when the bytes are not printable. */ static void dump(const char *label, const char *s) { diff --git a/tests/test-casefold-walk-host.c b/tests/test-casefold-walk-host.c index d8e478f9..7534f39f 100644 --- a/tests/test-casefold-walk-host.c +++ b/tests/test-casefold-walk-host.c @@ -298,6 +298,38 @@ static void section_symlink(void) CASEFOLD_FOUND, "second-link"); check("escaped second link to a symlink", "/Hard.Link", CASEFOLD_FOUND, esc("Hard.Link")); + + /* Same entry, asked about the report rather than the spelling. A directory + * listing carries no object type, so a walk the fallback answered cannot + * claim to know the leaf's, and the NO_XDEV walker skips its own fstatat + * on the strength of that flag. + */ + if (casefold_resolve_at(AT_FDCWD, root, "/second-link", false, out, + sizeof(out), &walk) == CASEFOLD_FOUND && + !walk.leaf_type_known) + host_ok(); + else + host_fail("a listing-answered walk reports no leaf type", + "expected leaf_type_known false when readdir answered"); + + /* The walk-wide form of the same withdrawal. all_types_known is what lets + * the resolver skip its canonical containment recheck, so a component the + * listing answered has to clear it. + */ + if (casefold_resolve_at(AT_FDCWD, root, "/lowdir/inner.txt", false, out, + sizeof(out), &walk) == CASEFOLD_FOUND && + walk.all_types_known) + host_ok(); + else + host_fail("a fully probed walk sets all_types_known", + "expected all_types_known true"); + if (casefold_resolve_at(AT_FDCWD, root, "/second-link", false, out, + sizeof(out), &walk) == CASEFOLD_FOUND && + !walk.all_types_known) + host_ok(); + else + host_fail("a listing-answered walk clears all_types_known", + "expected all_types_known false when readdir answered"); } static void section_limits(void)