diff --git a/src/client/dfs/dfs_internal.h b/src/client/dfs/dfs_internal.h index ff96b5fc271..c4cc060e589 100644 --- a/src/client/dfs/dfs_internal.h +++ b/src/client/dfs/dfs_internal.h @@ -1,5 +1,6 @@ /** * (C) Copyright 2019-2024 Intel Corporation. + * (C) Copyright 2026 Hewlett Packard Enterprise Development LP * * SPDX-License-Identifier: BSD-2-Clause-Patent */ @@ -419,4 +420,7 @@ update_stbuf_times(struct dfs_entry entry, daos_epoch_t max_epoch, struct stat * int lookup_rel_path(dfs_t *dfs, dfs_obj_t *root, const char *path, int flags, dfs_obj_t **_obj, mode_t *mode, struct stat *stbuf, size_t depth); +int +follow_symlink(dfs_t *dfs, dfs_obj_t *parent, const char *value, int flags, dfs_obj_t **_obj, + mode_t *mode, struct stat *stbuf, size_t depth); #endif /* __DFS_INTERNAL_H__ */ diff --git a/src/client/dfs/lookup.c b/src/client/dfs/lookup.c index c57a602accd..efe6523eb9d 100644 --- a/src/client/dfs/lookup.c +++ b/src/client/dfs/lookup.c @@ -14,6 +14,22 @@ #include "dfs_internal.h" +/* A symlink value is resolved from the directory holding the link. dfs has no notion of the + * process root that POSIX resolves an absolute value from, so such a value is rejected wherever + * the link sits and left to a caller that can resolve it, as dfuse and pil4dfs do with the kernel. + */ +int +follow_symlink(dfs_t *dfs, dfs_obj_t *parent, const char *value, int flags, dfs_obj_t **_obj, + mode_t *mode, struct stat *stbuf, size_t depth) +{ + if (value[0] == '/') { + D_DEBUG(DB_TRACE, "Cannot follow absolute symlink value %s\n", value); + return EINVAL; + } + + return lookup_rel_path(dfs, parent, value, flags, _obj, mode, stbuf, depth); +} + int lookup_rel_path(dfs_t *dfs, dfs_obj_t *root, const char *path, int flags, dfs_obj_t **_obj, mode_t *mode, struct stat *stbuf, size_t depth) @@ -195,8 +211,8 @@ lookup_rel_path(dfs_t *dfs, dfs_obj_t *root, const char *path, int flags, dfs_ob D_GOTO(err_obj, rc = ENOTSUP); } - rc = lookup_rel_path(dfs, &parent, entry.value, flags, &sym, NULL, - NULL, depth + 1); + rc = follow_symlink(dfs, &parent, entry.value, flags, &sym, NULL, + NULL, depth + 1); if (rc) { D_DEBUG(DB_TRACE, "Failed to lookup symlink %s\n", entry.value); @@ -229,8 +245,8 @@ lookup_rel_path(dfs_t *dfs, dfs_obj_t *root, const char *path, int flags, dfs_ob D_GOTO(err_obj, rc = ENOTSUP); } - rc = lookup_rel_path(dfs, &parent, entry.value, flags, &sym, mode, - stbuf, depth + 1); + rc = follow_symlink(dfs, &parent, entry.value, flags, &sym, mode, + stbuf, depth + 1); if (rc) { D_DEBUG(DB_TRACE, "Failed to lookup symlink %s\n", entry.value); @@ -506,7 +522,7 @@ lookup_rel_int(dfs_t *dfs, dfs_obj_t *parent, const char *name, int flags, dfs_o if (entry.value == NULL) D_GOTO(err_obj, rc = EIO); /* dereference the symlink */ - rc = lookup_rel_path(dfs, parent, entry.value, flags, &sym, mode, stbuf, 0); + rc = follow_symlink(dfs, parent, entry.value, flags, &sym, mode, stbuf, 0); if (rc) { D_DEBUG(DB_TRACE, "Failed to lookup symlink %s\n", entry.value); D_FREE(entry.value); diff --git a/src/client/dfs/obj.c b/src/client/dfs/obj.c index 8e5ed768db6..7c0f94eaec4 100644 --- a/src/client/dfs/obj.c +++ b/src/client/dfs/obj.c @@ -1139,7 +1139,7 @@ dfs_access(dfs_t *dfs, dfs_obj_t *parent, const char *name, int mask) D_ASSERT(entry.value); - rc = lookup_rel_path(dfs, parent, entry.value, O_RDONLY, &sym, NULL, NULL, 0); + rc = follow_symlink(dfs, parent, entry.value, O_RDONLY, &sym, NULL, NULL, 0); if (rc) { D_DEBUG(DB_TRACE, "Failed to lookup symlink %s\n", entry.value); D_GOTO(out, rc); @@ -1214,7 +1214,7 @@ dfs_chmod(dfs_t *dfs, dfs_obj_t *parent, const char *name, mode_t mode) if (S_ISLNK(entry.mode)) { D_ASSERT(entry.value); - rc = lookup_rel_path(dfs, parent, entry.value, O_RDWR, &sym, NULL, NULL, 0); + rc = follow_symlink(dfs, parent, entry.value, O_RDWR, &sym, NULL, NULL, 0); if (rc) { D_ERROR("Failed to lookup symlink %s\n", entry.value); D_FREE(entry.value); @@ -1341,7 +1341,7 @@ dfs_chown(dfs_t *dfs, dfs_obj_t *parent, const char *name, uid_t uid, gid_t gid, /** resolve symlink */ if (!(flags & O_NOFOLLOW) && S_ISLNK(entry.mode)) { D_ASSERT(entry.value); - rc = lookup_rel_path(dfs, parent, entry.value, O_RDWR, &sym, NULL, NULL, 0); + rc = follow_symlink(dfs, parent, entry.value, O_RDWR, &sym, NULL, NULL, 0); if (rc) { D_DEBUG(DB_TRACE, "Failed to lookup symlink '%s': %d (%s)\n", entry.value, rc, strerror(rc)); diff --git a/src/client/dfuse/pil4dfs/dfs_dcache.c b/src/client/dfuse/pil4dfs/dfs_dcache.c index b0c138a4261..dc797954a4d 100644 --- a/src/client/dfuse/pil4dfs/dfs_dcache.c +++ b/src/client/dfuse/pil4dfs/dfs_dcache.c @@ -1,5 +1,6 @@ /** * (C) Copyright 2022-2024 Intel Corporation. + * (C) Copyright 2026 Hewlett Packard Enterprise Development LP * * SPDX-License-Identifier: BSD-2-Clause-Patent */ @@ -674,8 +675,17 @@ dcache_find_insert_act(dfs_dcache_t *dcache, char *path, size_t path_len, dcache name_len = 0; while (name + name_len < path + path_len && name[name_len] != '/') ++name_len; - if (name_len == 0) + if (name_len == 0) { + /* An empty component would silently return a partial parent. The path is + * normalized by the caller, so this is a bug rather than a user error. + */ + if (name < path + path_len) { + D_ERROR("empty component in path " DF_PATH "\n", DP_PATH(path)); + drec_decref(dcache, rec_tmp); + D_GOTO(out, rc = -DER_INVAL); + } break; + } key_prefix = &rec_tmp->dr_key_child_prefix[0]; parent = rec_tmp; diff --git a/src/client/dfuse/pil4dfs/int_dfs.c b/src/client/dfuse/pil4dfs/int_dfs.c index 492b7710bc1..f4f476c7817 100644 --- a/src/client/dfuse/pil4dfs/int_dfs.c +++ b/src/client/dfuse/pil4dfs/int_dfs.c @@ -94,6 +94,11 @@ static int fd_dummy = -1; /* Default dir cache garbage collector time-out in seconds */ #define DCACHE_GC_PERIOD 120 +/* Symlink hops followed while resolving ".." before giving up with ELOOP, as the kernel's + * MAXSYMLINKS + */ +#define MAX_SYMLINK_HOPS 40 + /* the number of low fd reserved */ static uint16_t low_fd_count; /* the list of low fd reserved */ @@ -225,6 +230,11 @@ struct statx { /* working dir of current process */ static char cur_dir[DFS_MAX_PATH] = ""; +/* pid cur_dir was last set for. CPython uses vfork() for subprocess with cwd=, and a vfork()ed + * child runs in the parent's address space until execve(), so a chdir() there would otherwise + * leave the parent resolving relative paths against the child's directory. + */ +static pid_t cur_dir_pid; static bool segv_handler_inited; /* Old segv handler */ struct sigaction old_segv; @@ -247,6 +257,8 @@ static void finalize_dfs(void); static void update_cwd(void); +static void +cur_dir_check_owner(void); static int get_eqh(daos_handle_t *eqh); static void @@ -482,9 +494,20 @@ static void *(*next_dlopen)(const char *filename, int flags); */ static int -remove_dot_dot(char path[], int *len); +normalize_path(char path[], int len); static int -remove_dot_and_cleanup(char szPath[], int len); +resolve_dot_dot(struct dfs_mt *dfs_mt, char path[], int *len, bool *use_kernel); + +/* dfs cannot follow a symlink whose value is absolute (EINVAL), or one that climbs more than one + * level with ".." (ENOTSUP): the first needs the process root, the second the ancestry of the + * link, and only the kernel has both. The lookup that hits such a link fails, so a path that fails + * this way is handed to the kernel, which resolves it through dfuse. + */ +static inline bool +lookup_needs_kernel(int rc) +{ + return rc == EINVAL || rc == ENOTSUP; +} /* reference count of fake fd duplicated by real fd with dup2() */ static int dup_ref_count[MAX_OPENED_FILE]; @@ -1095,6 +1118,7 @@ query_path(const char *szInput, int *is_target_path, struct dcache_rec **parent, { int pos, len; bool with_daos_prefix; + bool use_kernel; char pool[DAOS_PROP_MAX_LABEL_BUF_LEN + 1]; char cont[DAOS_PROP_MAX_LABEL_BUF_LEN + 1]; char *rel_path = NULL; @@ -1132,6 +1156,10 @@ query_path(const char *szInput, int *is_target_path, struct dcache_rec **parent, if (full_path_parse == NULL) goto out_oom; + /* Relative paths resolve against cur_dir, which may still belong to a different process. */ + if (szInput[0] != '/') + cur_dir_check_owner(); + if (strncmp(szInput, ".", 2) == 0) { /* special case for current work directory */ pt_end = stpncpy(full_path_parse, cur_dir, DFS_MAX_PATH + 1); @@ -1160,17 +1188,8 @@ query_path(const char *szInput, int *is_target_path, struct dcache_rec **parent, } } - /* Remove '/./'; Replace '//' with '/'; Remove '/' at the end of path. */ - len = remove_dot_and_cleanup(full_path_parse, len); - - /* standarlize and determine whether a path is a target path or not */ - - /* Assume full_path_parse[] = "/A/B/C/../D/E", it will be "/A/B/D/E" after - * remove_dot_dot. - */ - rc = remove_dot_dot(full_path_parse, &len); - if (rc) - D_GOTO(out_err, rc); + /* e.g. "/A/./B//C/../D/" becomes "/A/B/C/../D"; ".." is left to resolve_dot_dot() */ + len = normalize_path(full_path_parse, len); /* determine whether the path contains any known dfs mount point */ idx_dfs = query_dfs_mount(full_path_parse); @@ -1275,6 +1294,19 @@ query_path(const char *szInput, int *is_target_path, struct dcache_rec **parent, } D_MUTEX_UNLOCK(&lock_dfs); } + + rc = resolve_dot_dot(*dfs_mt, full_path_parse, &len, &use_kernel); + if (rc) + D_GOTO(out_err, rc); + if (use_kernel) { + /* the path leaves the container through "..", or dfs cannot follow a link + * in it + */ + strncpy(*full_path, full_path_parse, len + 1); + *is_target_path = 0; + item_name[0] = '\0'; + goto out_normal; + } *is_target_path = 1; /* root dir */ @@ -1318,9 +1350,18 @@ query_path(const char *szInput, int *is_target_path, struct dcache_rec **parent, /* look up the dfs object from hash table for the parent dir */ rc = dcache_find_insert((*dfs_mt)->dcache, *parent_dir, parent_dir_len, parent); - /* parent dir does not exist or something wrong */ - if (rc) - D_GOTO(out_err, rc = daos_der2errno(rc)); + if (rc) { + rc = daos_der2errno(rc); + if (!lookup_needs_kernel(rc)) + /* parent dir does not exist or something wrong */ + D_GOTO(out_err, rc); + /* a symlink in the parent path that only the kernel can follow */ + full_path_parse[pos] = '/'; + strncpy(*full_path, full_path_parse, len + 1); + *is_target_path = 0; + item_name[0] = '\0'; + goto out_normal; + } } } else { strncpy(*full_path, full_path_parse, len + 1); @@ -1347,113 +1388,200 @@ query_path(const char *szInput, int *is_target_path, struct dcache_rec **parent, return ENOMEM; } +/* Lexically clean an absolute path in place: drop empty and "." components and strip the trailing + * "/". Working on components rather than on substrings keeps a "//" that follows a "/./" from + * surviving the pass. ".." is kept: whether it can be collapsed depends on the component in front + * of it being a directory rather than a symlink, which resolve_dot_dot() decides with a lookup. + * Returns the new length, at least 1 for "/". + */ static int -remove_dot_dot(char path[], int *len) +normalize_path(char path[], int len) { - char *p_Offset_2Dots, *p_Back, *pTmp, *pMax, *new_str; - int i, nNonZero; - - /* the length of path[] is already checked in the caller of this function. */ + int r = 0; + int w = 0; - p_Offset_2Dots = strstr(path, "/../"); -again: - nNonZero = 0; - if (p_Offset_2Dots == path) { - D_DEBUG(DB_ANY, "wrong path %s: %d (%s)\n", path, EINVAL, strerror(EINVAL)); - return EINVAL; - } + D_ASSERT(len == 0 || path[0] == '/'); - while (p_Offset_2Dots != NULL) { - pMax = p_Offset_2Dots + 4; - for (p_Back = p_Offset_2Dots - 2; p_Back >= path; p_Back--) { - if (*p_Back == '/') { - for (pTmp = p_Back; pTmp < (pMax - 1); pTmp++) - *pTmp = 0; - break; - } - } - p_Offset_2Dots = strstr(p_Offset_2Dots + 3, "/../"); - if (p_Offset_2Dots == NULL) - break; - } + while (r < len) { + int start; + int comp_len; - new_str = path; - for (i = 0; i < *len; i++) { - if (path[i]) { - new_str[nNonZero] = path[i]; - nNonZero++; - } + while (r < len && path[r] == '/') + r++; + start = r; + while (r < len && path[r] != '/') + r++; + comp_len = r - start; + if (comp_len == 0 || (comp_len == 1 && path[start] == '.')) + continue; + /* w < start always holds, as each kept component consumed at least one more '/' */ + path[w++] = '/'; + memmove(path + w, path + start, comp_len); + w += comp_len; } - new_str[nNonZero] = 0; - *len = nNonZero; + if (w == 0) + path[w++] = '/'; + path[w] = '\0'; - p_Offset_2Dots = strstr(path, "/../"); - if (p_Offset_2Dots) - goto again; - p_Offset_2Dots = strstr(path, "/.."); - if (p_Offset_2Dots && p_Offset_2Dots[3] == '\0') - /* /.. at the very end of the path. */ - goto again; - - return 0; + return w; } -/* Remove '/./'. Replace '//' with '/'. Remove '/.'. Remove '/' at the end of path. */ +/* Index of the '/' in front of the first ".." component at or after 'from', or -1. The path has + * been through normalize_path(), so ".." is always delimited by '/' or the end of the string. + */ static int -remove_dot_and_cleanup(char path[], int len) +find_dot_dot(const char *path, int from) { - char *p_Offset_Dots, *p_Offset_Slash, *new_str; - int i, nNonZero = 0; - - /* the length of path[] is already checked in the caller of this function. */ + const char *p = path + from; - p_Offset_Dots = strstr(path, "/./"); - while ((p_Offset_Dots != NULL)) { - p_Offset_Dots[0] = 0; - p_Offset_Dots[1] = 0; - p_Offset_Dots = strstr(p_Offset_Dots + 2, "/./"); - if (p_Offset_Dots == NULL) - break; + while ((p = strstr(p, "/..")) != NULL) { + if (p[3] == '/' || p[3] == '\0') + return (int)(p - path); + p += 3; } + return -1; +} - /* replace "//" with "/" */ - p_Offset_Slash = strstr(path, "//"); - while (p_Offset_Slash != NULL) { - p_Offset_Slash[0] = 0; - p_Offset_Slash = strstr(p_Offset_Slash + 1, "//"); - if (p_Offset_Slash == NULL) - break; - } +/* Resolve the ".." components of a path inside a container the way the kernel does: ".." applies + * to the component in front of it after that component has been resolved, so "link/.." with + * "link -> c/d" leads to "c", not to the parent of "link". Each such component is looked up with + * O_NOFOLLOW: a directory lets "X/.." collapse, a relative symlink value is spliced in place of X + * and the scan restarts, anything else is an error as in the kernel. A ".." applied to the mount + * point itself collapses lexically, the mount path being taken as canonical as query_dfs_mount() + * does, and the path stays with pil4dfs only if it comes straight back into this mount. A symlink + * with an absolute value, or one in the path in front of ".." that dfs cannot follow, leaves the + * container and is handed to the kernel. Paths without ".." return at the first scan and pay + * nothing. + */ +static int +resolve_dot_dot(struct dfs_mt *dfs_mt, char path[], int *len, bool *use_kernel) +{ + int hops = 0; + + *use_kernel = false; + for (;;) { + struct dcache_rec *parent = NULL; + dfs_obj_t *obj = NULL; + mode_t mode = 0; + char *parent_path; + char *value = NULL; + daos_size_t value_len; + int pos, xs, end, parent_len, rc; + char saved; + + pos = find_dot_dot(path, dfs_mt->len_fs_root); + if (pos < 0) + return 0; + if (pos <= dfs_mt->len_fs_root) { + /* ".." right after the mount root: its parent is on the host, so only a + * path that re-enters this very mount can be served here + */ + if (dfs_mt->len_fs_root <= 1) { + *use_kernel = true; + return 0; + } + xs = dfs_mt->len_fs_root; + while (xs > 0 && path[xs - 1] != '/') + xs--; + end = pos + 3; + memmove(path + xs, path + end, *len - end + 1); + *len -= end - xs; + *len = normalize_path(path, *len); + if (strncmp(path, dfs_mt->fs_root, dfs_mt->len_fs_root) != 0 || + (path[dfs_mt->len_fs_root] != '/' && + path[dfs_mt->len_fs_root] != '\0')) { + *use_kernel = true; + return 0; + } + continue; + } - /* remove '/.' at the end */ - if (len > 2 && strncmp(path + len - 2, "/.", 3) == 0) { - p_Offset_Slash = path + len - 2; - p_Offset_Slash[0] = 0; - p_Offset_Slash[1] = 0; - } + /* X is the component in front of "..", its parent the container path before it */ + xs = pos - 1; + while (path[xs - 1] != '/') + xs--; + parent_len = (xs - 1) - dfs_mt->len_fs_root; + if (parent_len == 0) { + D_STRNDUP(parent_path, "/", 1); + parent_len = 1; + } else { + D_STRNDUP(parent_path, path + dfs_mt->len_fs_root, parent_len); + } + if (parent_path == NULL) + return ENOMEM; + rc = dcache_find_insert(dfs_mt->dcache, parent_path, parent_len, &parent); + D_FREE(parent_path); + if (rc) { + rc = daos_der2errno(rc); + if (!lookup_needs_kernel(rc)) + return rc; + *use_kernel = true; + return 0; + } - new_str = path; - for (i = 0; i < len; i++) { - if (path[i]) { - new_str[nNonZero] = path[i]; - nNonZero++; + saved = path[pos]; + path[pos] = '\0'; + rc = dfs_lookup_rel(dfs_mt->dfs, drec2obj(parent), path + xs, O_RDONLY | O_NOFOLLOW, + &obj, &mode, NULL); + path[pos] = saved; + drec_decref(dfs_mt->dcache, parent); + if (rc) + return rc; + + if (S_ISDIR(mode)) { + dfs_release(obj); + /* drop "X/.."; the cleanup pass removes the "/" left behind */ + end = pos + 3; + memmove(path + xs, path + end, *len - end + 1); + *len -= end - xs; + *len = normalize_path(path, *len); + continue; } - } - /* remove "/" at the end of path */ - new_str[nNonZero] = 0; - if (new_str[1] == 0 && new_str[0] == '/') - /* root dir */ - return 1; - for (i = nNonZero - 1; i >= 0; i--) { - if (new_str[i] == '/') { - new_str[i] = 0; - nNonZero--; - } else { - break; + if (!S_ISLNK(mode)) { + dfs_release(obj); + return ENOTDIR; } - } - return nNonZero; + value_len = 0; + rc = dfs_get_symlink_value(obj, NULL, &value_len); + if (rc == 0) { + D_ALLOC(value, value_len); + if (value == NULL) + rc = ENOMEM; + else + rc = dfs_get_symlink_value(obj, value, &value_len); + } + dfs_release(obj); + if (rc) { + D_FREE(value); + return rc; + } + /* value_len includes the terminating NUL */ + value_len--; + if (value_len == 0) { + D_FREE(value); + return ENOENT; + } + if (value[0] == '/') { + D_FREE(value); + *use_kernel = true; + return 0; + } + if (++hops > MAX_SYMLINK_HOPS) { + D_FREE(value); + return ELOOP; + } + /* splice the value in place of X, then clean up whatever it contained */ + if (*len - (pos - xs) + (int)value_len >= DFS_MAX_PATH) { + D_FREE(value); + return ENAMETOOLONG; + } + memmove(path + xs + value_len, path + pos, *len - pos + 1); + memcpy(path + xs, value, value_len); + *len += (int)value_len - (pos - xs); + D_FREE(value); + *len = normalize_path(path, *len); + } } static int @@ -1974,6 +2102,7 @@ check_path_with_dirfd(int dirfd, char **full_path_out, const char *rel_path, int else if (len_str < 0) goto out_oom; } else if (dirfd_directed == AT_FDCWD) { + cur_dir_check_owner(); len_str = asprintf(full_path_out, "%s/%s", cur_dir, rel_path); if (len_str >= DFS_MAX_PATH) goto out_toolong; @@ -2027,6 +2156,43 @@ check_path_with_dirfd(int dirfd, char **full_path_out, const char *rel_path, int return (-1); } +/* The leaf of the path is a symlink that dfs cannot follow, see lookup_needs_kernel(). A link in + * the container root whose relative value climbs out of the container reports ENOENT instead, as + * dfs resolves ".." of its root to the root itself. None of these errnos is specific to a symlink, + * so the entry is looked up again to confirm that it really is one. The value that dfs rejected + * may sit anywhere in a chain of links, as with the python3 -> python -> /usr/bin/python3.x layout + * of a venv, so the value of the entry itself is not inspected. + * + * A symlink of this kind in a non-leaf position fails inside query_path(), which hands the path to + * the kernel itself. + */ +static bool +need_kernel_to_resolve(int rc, struct dfs_mt *dfs_mt, struct dcache_rec *parent, + const char *item_name, const char *parent_dir) +{ + dfs_obj_t *obj = NULL; + mode_t mode = 0; + + if (parent == NULL || item_name[0] == '\0') + return false; + if (rc == ENOENT) { + /* A lookup miss is common, so only probe where dfs can report one for such a + * link, and keep it cheap everywhere else. + */ + if (parent_dir == NULL || strncmp(parent_dir, "/", 2) != 0) + return false; + } else if (!lookup_needs_kernel(rc)) { + return false; + } + + if (dfs_lookup_rel(dfs_mt->dfs, drec2obj(parent), item_name, O_RDONLY | O_NOFOLLOW, &obj, + &mode, NULL) != 0) + return false; + dfs_release(obj); + + return S_ISLNK(mode); +} + static int open_common(int (*real_open)(const char *pathname, int oflags, ...), const char *caller_name, const char *pathname, int oflags, ...) @@ -2059,10 +2225,12 @@ open_common(int (*real_open)(const char *pathname, int oflags, ...), const char if (!d_hook_enabled) goto org_func; + item_name[0] = '\0'; + rc = query_path(pathname, &is_target_path, &parent, item_name, &parent_dir, &full_path, &dfs_mt); - if (rc == ENOENT) - D_GOTO(out_error, rc = ENOENT); + if (rc) + D_GOTO(out_error, rc); parent_dfs = NULL; if (parent != NULL) parent_dfs = drec2obj(parent); @@ -2186,7 +2354,8 @@ open_common(int (*real_open)(const char *pathname, int oflags, ...), const char return fd_kernel; } - if (oflags & __O_TMPFILE) { + /* __O_TMPFILE includes O_DIRECTORY, so the whole mask has to match */ + if ((oflags & __O_TMPFILE) == __O_TMPFILE) { if (!parent && (strncmp(item_name, "/", 2) == 0)) rc = dfs_access(dfs_mt->dfs, NULL, NULL, X_OK | W_OK); else @@ -2229,9 +2398,26 @@ open_common(int (*real_open)(const char *pathname, int oflags, ...), const char &dfs_obj, &mode_query, NULL); } + if (need_kernel_to_resolve(rc, dfs_mt, parent, item_name, parent_dir)) + goto org_func; if (rc) D_GOTO(out_error, rc); + /* O_NOFOLLOW on a symlink: ELOOP, or ENOTDIR with O_DIRECTORY as the kernel checks that + * first, or with O_PATH an fd on the link that only the kernel can provide. A fake fd on + * the link object would only fail later, at read(). + */ + if (S_ISLNK(mode_query)) { + dfs_release(dfs_obj); + if (oflags & O_PATH) + goto org_func; + D_GOTO(out_error, rc = (oflags & O_DIRECTORY) ? ENOTDIR : ELOOP); + } + if ((oflags & O_DIRECTORY) && !S_ISDIR(mode_query)) { + dfs_release(dfs_obj); + D_GOTO(out_error, rc = ENOTDIR); + } + if (S_ISDIR(mode_query)) { rc = find_next_available_dirfd(NULL, &idx_dirfd); if (rc) @@ -3041,6 +3227,7 @@ static int new_xstat(int ver, const char *path, struct stat *stat_buf) { int is_target_path, rc; + bool use_kernel; dfs_obj_t *obj; mode_t mode; char item_name[DFS_MAX_NAME]; @@ -3052,6 +3239,8 @@ new_xstat(int ver, const char *path, struct stat *stat_buf) if (!d_hook_enabled) return next_xstat(ver, path, stat_buf); + item_name[0] = '\0'; + rc = query_path(path, &is_target_path, &parent, item_name, &parent_dir, &full_path, &dfs_mt); if (rc) @@ -3087,10 +3276,12 @@ new_xstat(int ver, const char *path, struct stat *stat_buf) return next_xstat(ver, path, stat_buf); out_err: + use_kernel = need_kernel_to_resolve(rc, dfs_mt, parent, item_name, parent_dir) || + ((rc == EIO || rc == EINVAL) && d_compatible_mode); if (parent != NULL) drec_decref(dfs_mt->dcache, parent); FREE(parent_dir); - if ((rc == EIO || rc == EINVAL) && d_compatible_mode) + if (use_kernel) return next_xstat(ver, path, stat_buf); errno = rc; return (-1); @@ -3100,6 +3291,7 @@ static int new_lxstat(int ver, const char *path, struct stat *stat_buf) { int is_target_path, rc; + bool use_kernel; char item_name[DFS_MAX_NAME]; struct dfs_mt *dfs_mt = NULL; struct dcache_rec *parent = NULL; @@ -3109,6 +3301,8 @@ new_lxstat(int ver, const char *path, struct stat *stat_buf) if (!d_hook_enabled) return libc_lxstat(ver, path, stat_buf); + item_name[0] = '\0'; + rc = query_path(path, &is_target_path, &parent, item_name, &parent_dir, &full_path, &dfs_mt); if (rc) @@ -3135,10 +3329,12 @@ new_lxstat(int ver, const char *path, struct stat *stat_buf) return libc_lxstat(ver, path, stat_buf); out_err: + use_kernel = need_kernel_to_resolve(rc, dfs_mt, parent, item_name, parent_dir) || + ((rc == EIO || rc == EINVAL) && d_compatible_mode); if (parent != NULL) drec_decref(dfs_mt->dcache, parent); FREE(parent_dir); - if ((rc == EIO || rc == EINVAL) && d_compatible_mode) + if (use_kernel) return libc_lxstat(ver, path, stat_buf); errno = rc; return (-1); @@ -3150,7 +3346,7 @@ new_fxstatat(int ver, int dirfd, const char *path, struct stat *stat_buf, int fl int idx_dfs, error = 0, rc; char *full_path = NULL; - if (!d_hook_enabled) + if (path == NULL || !d_hook_enabled) return libc_fxstatat(ver, dirfd, path, stat_buf, flags); if (path[0] == '/') { @@ -3203,7 +3399,7 @@ new_fstatat(int dirfd, const char *__restrict path, struct stat *__restrict stat int idx_dfs, error = 0, rc; char *full_path = NULL; - if (!d_hook_enabled) + if (path == NULL || !d_hook_enabled) return libc_fstatat(dirfd, path, stat_buf, flags); if (path[0] == '/') { @@ -3282,13 +3478,21 @@ statx(int dirfd, const char *path, int flags, unsigned int mask, struct statx *s int rc, idx_dfs, error = 0; struct stat stat_buf; char *full_path = NULL; + /* glibc declares path nonnull and gcc folds a direct NULL test away, so read it as + * volatile. Rust's std probes statx() with a NULL path and expects EFAULT from the kernel. + */ + const char *volatile path_probe = path; if (next_statx == NULL) { next_statx = dlsym(RTLD_NEXT, "statx"); D_ASSERT(next_statx != NULL); } - if (!d_hook_enabled) + if (path_probe == NULL || !d_hook_enabled) return next_statx(dirfd, path, flags, mask, statx_buf); + if (path[0] == 0 && ((flags & AT_EMPTY_PATH) == 0)) { + errno = ENOENT; + return (-1); + } /* absolute path, dirfd is ignored */ if (path[0] == '/') { @@ -4985,6 +5189,7 @@ getcwd(char *buf, size_t size) if (!d_hook_enabled) return next_getcwd(buf, size); + cur_dir_check_owner(); if (cur_dir[0] != '/') update_cwd(); @@ -5049,6 +5254,8 @@ access(const char *path, int mode) if (!d_hook_enabled) return next_access(path, mode); + item_name[0] = '\0'; + rc = query_path(path, &is_target_path, &parent, item_name, &parent_dir, &full_path, &dfs_mt); if (rc) @@ -5074,6 +5281,8 @@ access(const char *path, int mode) return next_access(path, mode); out_err: + if (need_kernel_to_resolve(rc, dfs_mt, parent, item_name, parent_dir)) + goto out_org; if (parent != NULL) drec_decref(dfs_mt->dcache, parent); FREE(parent_dir); @@ -5136,22 +5345,33 @@ chdir(const char *path) if (!d_hook_enabled) return next_chdir(path); - rc = query_path(path, &is_target_path, &parent, item_name, &parent_dir, - &full_path, &dfs_mt); - if (rc) - D_GOTO(out_err, rc); - rc = next_chdir(path); if (rc) - D_GOTO(out_err, rc = errno); + return (-1); + + if (cur_dir_pid != 0 && cur_dir_pid != getpid()) { + /* A vfork()ed child shares the parent's memory until execve(): updating cur_dir + * here would redirect the parent's relative paths to this child's directory. + */ + return 0; + } + + rc = + query_path(path, &is_target_path, &parent, item_name, &parent_dir, &full_path, &dfs_mt); + if (rc) { + /* The kernel already moved cwd, so chdir() has to report success. Only cur_dir + * cannot be derived from path here. query_path() above allocates and takes locks, + * so a chdir() between fork() and exec() is still not safe in general. + */ + update_cwd(); + return 0; + } if (!is_target_path) { - len_str = snprintf(cur_dir, DFS_MAX_PATH, "%s", full_path); - if (len_str >= DFS_MAX_PATH) { - D_DEBUG(DB_ANY, "path is too long: %d (%s)\n", ENAMETOOLONG, - strerror(ENAMETOOLONG)); - D_GOTO(out_err, rc = ENAMETOOLONG); - } + /* the path is not normalized for ".." outside a container, so take the kernel's + * view of the new cwd + */ + update_cwd(); D_GOTO(out, rc); } @@ -5191,8 +5411,13 @@ fchdir(int dirfd) return next_fchdir(dirfd); fd_directed = d_get_fd_redirected(dirfd); - if (fd_directed < FD_DIR_BASE) - return next_fchdir(dirfd); + if (fd_directed < FD_DIR_BASE) { + rc = next_fchdir(dirfd); + /* as in chdir(), a vfork()ed child must leave the parent's cur_dir alone */ + if (rc == 0 && (cur_dir_pid == 0 || cur_dir_pid == getpid())) + update_cwd(); + return rc; + } /* assume dfuse is running. call chdir() to update cwd. */ if (next_chdir == NULL) { @@ -5203,7 +5428,11 @@ fchdir(int dirfd) if (rc) return rc; + if (cur_dir_pid != 0 && cur_dir_pid != getpid()) + return 0; + pt_end = stpncpy(cur_dir, dir_list[fd_directed - FD_DIR_BASE]->path, DFS_MAX_PATH - 1); + cur_dir_pid = getpid(); if ((long int)(pt_end - cur_dir) >= DFS_MAX_PATH - 1) { D_DEBUG(DB_ANY, "path is too long: %d (%s)\n", ENAMETOOLONG, strerror(ENAMETOOLONG)); @@ -5777,6 +6006,26 @@ utimes(const char *path, const struct timeval times[2]) return (-1); } +/* DFS does not store atime, so only the mtime of a utimensat()/futimens() request is applied. */ +static int +utimens_get_mtime(const struct timespec times[2], struct timespec *mtime, bool *set_mtime) +{ + int i; + + for (i = 0; times != NULL && i < 2; i++) { + if (times[i].tv_nsec != UTIME_NOW && times[i].tv_nsec != UTIME_OMIT && + (times[i].tv_nsec < 0 || times[i].tv_nsec >= NSEC_PER_SEC)) + return EINVAL; + } + + *set_mtime = (times == NULL || times[1].tv_nsec != UTIME_OMIT); + if (times == NULL || times[1].tv_nsec == UTIME_NOW) + clock_gettime(CLOCK_REALTIME, mtime); + else + *mtime = times[1]; + return 0; +} + static int utimens_timespec(const char *path, const struct timespec times[2], int flags) { @@ -5784,8 +6033,8 @@ utimens_timespec(const char *path, const struct timespec times[2], int flags) dfs_obj_t *obj; mode_t mode; struct stat stbuf; - struct timespec times_loc; - struct timeval times_us[2]; + struct timespec mtime; + bool set_mtime; char item_name[DFS_MAX_NAME]; struct dfs_mt *dfs_mt = NULL; struct dcache_rec *parent = NULL; @@ -5797,18 +6046,14 @@ utimens_timespec(const char *path, const struct timespec times[2], int flags) if (rc) D_GOTO(out_err, rc); if (!is_target_path) { - if (next_utimes == NULL) { - next_utimes = dlsym(RTLD_NEXT, "utimes"); - D_ASSERT(next_utimes != NULL); - } - times_us[0].tv_sec = times[0].tv_sec; - times_us[0].tv_usec = times[0].tv_nsec / 1000; - times_us[1].tv_sec = times[1].tv_sec; - times_us[1].tv_usec = times[1].tv_nsec / 1000; FREE(parent_dir); - return next_utimes(path, times_us); + return next_utimensat(AT_FDCWD, path, times, flags); } + rc = utimens_get_mtime(times, &mtime, &set_mtime); + if (rc) + D_GOTO(out_err, rc); + flags |= O_RDWR; /* POSIX API uses AT_SYMLINK_NOFOLLOW; DFS dfs_lookup() uses O_NOFOLLOW. */ if (flags & AT_SYMLINK_NOFOLLOW) @@ -5821,23 +6066,13 @@ utimens_timespec(const char *path, const struct timespec times[2], int flags) if (rc) D_GOTO(out_err, rc); - if (times == NULL) { - clock_gettime(CLOCK_REALTIME, ×_loc); - stbuf.st_atim.tv_sec = times_loc.tv_sec; - stbuf.st_atim.tv_nsec = times_loc.tv_nsec; - stbuf.st_mtim.tv_sec = times_loc.tv_sec; - stbuf.st_mtim.tv_nsec = times_loc.tv_nsec; - } else { - stbuf.st_atim.tv_sec = times[0].tv_sec; - stbuf.st_atim.tv_nsec = times[0].tv_nsec; - stbuf.st_mtim.tv_sec = times[1].tv_sec; - stbuf.st_mtim.tv_nsec = times[1].tv_nsec; - } - - rc = dfs_osetattr(dfs_mt->dfs, obj, &stbuf, DFS_SET_ATTR_MTIME); - if (rc) { - dfs_release(obj); - D_GOTO(out_err, rc); + if (set_mtime) { + stbuf.st_mtim = mtime; + rc = dfs_osetattr(dfs_mt->dfs, obj, &stbuf, DFS_SET_ATTR_MTIME); + if (rc) { + dfs_release(obj); + D_GOTO(out_err, rc); + } } rc = dfs_release(obj); @@ -5859,10 +6094,8 @@ utimens_timespec(const char *path, const struct timespec times[2], int flags) int utimensat(int dirfd, const char *path, const struct timespec times[2], int flags) { - int idx_dfs, error = 0, rc; - char *full_path = NULL; - struct timespec times_loc[2]; - struct timespec *times_ptr; + int idx_dfs, error = 0, rc; + char *full_path = NULL; if (next_utimensat == NULL) { next_utimensat = dlsym(RTLD_NEXT, "utimensat"); @@ -5881,29 +6114,23 @@ utimensat(int dirfd, const char *path, const struct timespec times[2], int flags _Pragma("GCC diagnostic pop") /* clang-format off */ - - if (times == NULL) { - clock_gettime(CLOCK_REALTIME, ×_loc[0]); - times_loc[1].tv_sec = times_loc[0].tv_sec; - times_loc[1].tv_nsec = times_loc[0].tv_nsec; - times_ptr = times_loc; - } else { - times_ptr = (struct timespec *)times; - } + /* as the kernel does, nothing to change means the path is not even checked */ + if (times != NULL && times[0].tv_nsec == UTIME_OMIT && times[1].tv_nsec == UTIME_OMIT) + return 0; /* clang-format on */ /* absolute path, dirfd is ignored */ if (path[0] == '/') - return utimens_timespec(path, times_ptr, flags); + return utimens_timespec(path, times, flags); idx_dfs = check_path_with_dirfd(dirfd, &full_path, path, &error); if (error) goto out_err; if (idx_dfs >= 0) - rc = utimens_timespec(full_path, times_ptr, flags); + rc = utimens_timespec(full_path, times, flags); else - rc = next_utimensat(dirfd, path, times_ptr, flags); + rc = next_utimensat(dirfd, path, times, flags); error = errno; if (full_path) { @@ -5922,8 +6149,8 @@ futimens(int fd, const struct timespec times[2]) { int rc; int fd_directed; - struct timespec times_loc; struct stat stbuf; + bool set_mtime; if (next_futimens == NULL) { next_futimens = dlsym(RTLD_NEXT, "futimens"); @@ -5935,18 +6162,13 @@ futimens(int fd, const struct timespec times[2]) if (fd_directed < FD_FILE_BASE) return next_futimens(fd, times); - if (times == NULL) { - clock_gettime(CLOCK_REALTIME, ×_loc); - stbuf.st_atim.tv_sec = times_loc.tv_sec; - stbuf.st_atim.tv_nsec = times_loc.tv_nsec; - stbuf.st_mtim.tv_sec = times_loc.tv_sec; - stbuf.st_mtim.tv_nsec = times_loc.tv_nsec; - } else { - stbuf.st_atim.tv_sec = times[0].tv_sec; - stbuf.st_atim.tv_nsec = times[0].tv_nsec; - stbuf.st_mtim.tv_sec = times[1].tv_sec; - stbuf.st_mtim.tv_nsec = times[1].tv_nsec; + rc = utimens_get_mtime(times, &stbuf.st_mtim, &set_mtime); + if (rc) { + errno = rc; + return (-1); } + if (!set_mtime) + return 0; rc = dfs_osetattr(d_file_list[fd_directed - FD_FILE_BASE]->dfs_mt->dfs, d_file_list[fd_directed - FD_FILE_BASE]->file, &stbuf, @@ -6590,25 +6812,39 @@ new_exit(int rc) static void update_cwd(void) { - char *cwd = NULL; - char *pt_end = NULL; + int saved_errno = errno; - /* daos_init() may be not called yet. */ - cwd = get_current_dir_name(); + if (next_getcwd == NULL) { + next_getcwd = dlsym(RTLD_NEXT, "getcwd"); + D_ASSERT(next_getcwd != NULL); + } - if (cwd == NULL) { - D_FATAL("fatal error to get CWD with get_current_dir_name(): %d (%s)\n", errno, - strerror(errno)); - abort(); - } else { - pt_end = stpncpy(cur_dir, cwd, DFS_MAX_PATH - 1); - if ((long int)(pt_end - cur_dir) >= DFS_MAX_PATH - 1) { - D_FATAL("fatal error, cwd path is too long: %d (%s)\n", ENAMETOOLONG, - strerror(ENAMETOOLONG)); - abort(); - } - free(cwd); + /* get_current_dir_name() returns $PWD when stat($PWD) and stat(".") match, but both go + * through the intercepted stat() and so agree with the stale cur_dir to be refreshed. + * cur_dir is filled directly to stay allocation free between fork() and exec(). + */ + if (next_getcwd(cur_dir, DFS_MAX_PATH) == NULL) { + /* e.g. cwd was removed or is too long. Relative paths are left to libc until the + * cwd is known again. + */ + D_DEBUG(DB_ANY, "getcwd() failed: %d (%s)\n", errno, strerror(errno)); + cur_dir[0] = '\0'; } + cur_dir_pid = getpid(); + + errno = saved_errno; +} + +/* Adopt cur_dir for this process when it was last set by another one, e.g. after fork(). By the + * time a parent runs again after vfork() the child has already exec'd, so writing here is safe. + */ +static void +cur_dir_check_owner(void) +{ + if (cur_dir_pid == getpid()) + return; + + update_cwd(); } static int diff --git a/src/include/daos_fs.h b/src/include/daos_fs.h index 11c3053e498..d2fe22947a5 100644 --- a/src/include/daos_fs.h +++ b/src/include/daos_fs.h @@ -1,6 +1,6 @@ /* * (C) Copyright 2018-2024 Intel Corporation. - * (C) Copyright 2025 Hewlett Packard Enterprise Development LP + * (C) Copyright 2025-2026 Hewlett Packard Enterprise Development LP * * SPDX-License-Identifier: BSD-2-Clause-Patent */ @@ -409,6 +409,11 @@ dfs_obj2id(dfs_obj_t *obj, daos_obj_id_t *oid); * Lookup a path in the DFS and return the associated open object and mode. * The object must be released with dfs_release(). * + * Symlinks along the path, and the last one unless O_NOFOLLOW is passed, are + * followed within the container. DFS has no notion of the process root, so a + * symlink with an absolute value cannot be followed and the lookup fails with + * EINVAL; the caller has to resolve such a value itself. + * * \param[in] dfs Pointer to the mounted file system. * \param[in] path Path to lookup. * \param[in] flags Access flags to open with (O_RDONLY or O_RDWR). @@ -424,9 +429,11 @@ dfs_lookup(dfs_t *dfs, const char *path, int flags, dfs_obj_t **obj, /** * Lookup an entry in the parent object and return the associated open object - * and mode of that entry. If the entry is a symlink, the symlink value is not - * resolved and the user can decide what to do to further resolve the value of - * the symlink. The object must be released with dfs_release(). + * and mode of that entry. If the entry is a symlink, it is followed unless + * O_NOFOLLOW is passed, in which case the symlink object itself is returned and + * the user can decide how to resolve its value. A symlink with an absolute + * value cannot be followed, see dfs_lookup(). + * The object must be released with dfs_release(). * * \param[in] dfs Pointer to the mounted file system. * \param[in] parent Opened parent directory object. If NULL, use root obj. diff --git a/src/tests/ftest/daos_test/dfuse.py b/src/tests/ftest/daos_test/dfuse.py index 4ef4a88fd02..2aa082db46d 100644 --- a/src/tests/ftest/daos_test/dfuse.py +++ b/src/tests/ftest/daos_test/dfuse.py @@ -1,5 +1,6 @@ """ (C) Copyright 2021-2024 Intel Corporation. + (C) Copyright 2025-2026 Hewlett Packard Enterprise Development LP SPDX-License-Identifier: BSD-2-Clause-Patent """ @@ -112,7 +113,8 @@ def run_test(self, il_lib=None): '--mmap', '--exec', '--directory', - '--cache' + '--cache', + '--path' ] if use_dfuse: parameters.append('--lowfd') diff --git a/src/tests/suite/dfs_unit_test.c b/src/tests/suite/dfs_unit_test.c index 007b4b6af94..bf6f8812a41 100644 --- a/src/tests/suite/dfs_unit_test.c +++ b/src/tests/suite/dfs_unit_test.c @@ -612,6 +612,106 @@ dfs_test_syml_follow(void **state) assert_int_equal(rc, ELOOP); } +/* dfs has no notion of the process root, so a symlink with an absolute value cannot be followed + * and is rejected with EINVAL wherever the link sits, in the container root or below. The link + * itself is still returned with O_NOFOLLOW, and a relative link that walks the same way works. + */ +static void +dfs_test_syml_abs(void **state) +{ + test_arg_t *arg = *state; + dfs_obj_t *dir; + dfs_obj_t *obj; + mode_t mode; + char value[64]; + daos_size_t value_len; + int rc; + + if (arg->myrank != 0) + return; + + /** /abs_dir/target, /abs_root -> /abs_dir/target, /abs_dir/abs_deep -> /abs_dir/target */ + rc = dfs_open(dfs_mt, NULL, "abs_dir", S_IFDIR | S_IWUSR | S_IRUSR | S_IXUSR, + O_RDWR | O_CREAT | O_EXCL, 0, 0, NULL, &dir); + assert_int_equal(rc, 0); + rc = dfs_open(dfs_mt, dir, "target", S_IFREG | S_IWUSR | S_IRUSR, O_RDWR | O_CREAT | O_EXCL, + 0, 0, NULL, &obj); + assert_int_equal(rc, 0); + rc = dfs_release(obj); + assert_int_equal(rc, 0); + rc = dfs_open(dfs_mt, NULL, "abs_root", S_IFLNK, O_RDWR | O_CREAT | O_EXCL, 0, 0, + "/abs_dir/target", &obj); + assert_int_equal(rc, 0); + rc = dfs_release(obj); + assert_int_equal(rc, 0); + rc = dfs_open(dfs_mt, dir, "abs_deep", S_IFLNK, O_RDWR | O_CREAT | O_EXCL, 0, 0, + "/abs_dir/target", &obj); + assert_int_equal(rc, 0); + rc = dfs_release(obj); + assert_int_equal(rc, 0); + /** /abs_dir/rel_deep -> ../abs_dir/target, the same target reached relatively */ + rc = dfs_open(dfs_mt, dir, "rel_deep", S_IFLNK, O_RDWR | O_CREAT | O_EXCL, 0, 0, + "../abs_dir/target", &obj); + assert_int_equal(rc, 0); + rc = dfs_release(obj); + assert_int_equal(rc, 0); + + /** following an absolute value fails the same way at both depths */ + rc = dfs_lookup(dfs_mt, "/abs_root", O_RDONLY, &obj, &mode, NULL); + assert_int_equal(rc, EINVAL); + rc = dfs_lookup(dfs_mt, "/abs_dir/abs_deep", O_RDONLY, &obj, &mode, NULL); + assert_int_equal(rc, EINVAL); + rc = dfs_lookup_rel(dfs_mt, NULL, "abs_root", O_RDONLY, &obj, &mode, NULL); + assert_int_equal(rc, EINVAL); + rc = dfs_lookup_rel(dfs_mt, dir, "abs_deep", O_RDONLY, &obj, &mode, NULL); + assert_int_equal(rc, EINVAL); + rc = dfs_access(dfs_mt, NULL, "abs_root", R_OK); + assert_int_equal(rc, EINVAL); + rc = dfs_access(dfs_mt, dir, "abs_deep", R_OK); + assert_int_equal(rc, EINVAL); + /** and in the middle of a path */ + rc = dfs_lookup(dfs_mt, "/abs_root/x", O_RDONLY, &obj, &mode, NULL); + assert_int_equal(rc, EINVAL); + + /** the link itself is still accessible */ + rc = dfs_lookup(dfs_mt, "/abs_root", O_RDONLY | O_NOFOLLOW, &obj, &mode, NULL); + assert_int_equal(rc, 0); + assert_true(S_ISLNK(mode)); + value_len = sizeof(value); + rc = dfs_get_symlink_value(obj, value, &value_len); + assert_int_equal(rc, 0); + assert_string_equal(value, "/abs_dir/target"); + rc = dfs_release(obj); + assert_int_equal(rc, 0); + rc = dfs_lookup_rel(dfs_mt, dir, "abs_deep", O_RDONLY | O_NOFOLLOW, &obj, &mode, NULL); + assert_int_equal(rc, 0); + assert_true(S_ISLNK(mode)); + rc = dfs_release(obj); + assert_int_equal(rc, 0); + rc = dfs_chown(dfs_mt, dir, "abs_deep", geteuid(), getegid(), O_NOFOLLOW); + assert_int_equal(rc, 0); + + /** a relative value reaching the same target is followed */ + rc = dfs_lookup_rel(dfs_mt, dir, "rel_deep", O_RDONLY, &obj, &mode, NULL); + assert_int_equal(rc, 0); + assert_true(S_ISREG(mode)); + rc = dfs_release(obj); + assert_int_equal(rc, 0); + + rc = dfs_remove(dfs_mt, dir, "rel_deep", false, NULL); + assert_int_equal(rc, 0); + rc = dfs_remove(dfs_mt, dir, "abs_deep", false, NULL); + assert_int_equal(rc, 0); + rc = dfs_remove(dfs_mt, dir, "target", false, NULL); + assert_int_equal(rc, 0); + rc = dfs_release(dir); + assert_int_equal(rc, 0); + rc = dfs_remove(dfs_mt, NULL, "abs_root", false, NULL); + assert_int_equal(rc, 0); + rc = dfs_remove(dfs_mt, NULL, "abs_dir", false, NULL); + assert_int_equal(rc, 0); +} + static int dfs_test_file_gen(const char *name, daos_size_t chunk_size, daos_oclass_id_t cid, daos_size_t file_size) @@ -3574,62 +3674,47 @@ dfs_test_pipeline_find(void **state) } static const struct CMUnitTest dfs_unit_tests[] = { - { "DFS_UNIT_TEST1: DFS mount / umount", - dfs_test_mount, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST2: DFS container modes", - dfs_test_modes, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST3: DFS lookup / lookup_rel", - dfs_test_lookup, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST4: Simple Symlinks", - dfs_test_syml, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST5: Symlinks with / without O_NOFOLLOW", - dfs_test_syml_follow, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST6: multi-threads read shared file", - dfs_test_read_shared_file, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST7: DFS lookupx", - dfs_test_lookupx, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST8: DFS IO sync error code", - dfs_test_io_error_code, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST9: DFS IO async error code", - dfs_test_io_error_code, async_enable, test_case_teardown}, - { "DFS_UNIT_TEST10: multi-threads mkdir same dir", - dfs_test_mt_mkdir, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST11: Simple rename", - dfs_test_rename, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST12: DFS API compat", - dfs_test_compat, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST13: DFS l2g/g2l_all", - dfs_test_handles, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST14: multi-threads connect to same container", - dfs_test_mt_connect, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST15: DFS chown", - dfs_test_chown, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST16: DFS stat mtime", - dfs_test_mtime, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST17: multi-threads async IO", - dfs_test_async_io_th, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST18: async IO", - dfs_test_async_io, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST19: DFS readdir", - dfs_test_readdir, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST20: dfs oclass hints", - dfs_test_oclass_hints, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST21: dfs multiple pools", - dfs_test_multiple_pools, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST22: dfs extended attributes", - dfs_test_xattrs, test_case_teardown}, - { "DFS_UNIT_TEST23: dfs MWC container checker", - dfs_test_checker, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST24: dfs MWC SB fix", - dfs_test_fix_sb, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST25: dfs MWC root fix", - dfs_test_relink_root, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST26: dfs MWC chunk size fix", - dfs_test_fix_chunk_size, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST27: dfs pipeline find", - dfs_test_pipeline_find, async_disable, test_case_teardown}, - { "DFS_UNIT_TEST28: dfs open/lookup flags", - dfs_test_oflags, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST1: DFS mount / umount", dfs_test_mount, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST2: DFS container modes", dfs_test_modes, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST3: DFS lookup / lookup_rel", dfs_test_lookup, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST4: Simple Symlinks", dfs_test_syml, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST5: Symlinks with / without O_NOFOLLOW", dfs_test_syml_follow, async_disable, + test_case_teardown}, + {"DFS_UNIT_TEST6: multi-threads read shared file", dfs_test_read_shared_file, async_disable, + test_case_teardown}, + {"DFS_UNIT_TEST7: DFS lookupx", dfs_test_lookupx, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST8: DFS IO sync error code", dfs_test_io_error_code, async_disable, + test_case_teardown}, + {"DFS_UNIT_TEST9: DFS IO async error code", dfs_test_io_error_code, async_enable, + test_case_teardown}, + {"DFS_UNIT_TEST10: multi-threads mkdir same dir", dfs_test_mt_mkdir, async_disable, + test_case_teardown}, + {"DFS_UNIT_TEST11: Simple rename", dfs_test_rename, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST12: DFS API compat", dfs_test_compat, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST13: DFS l2g/g2l_all", dfs_test_handles, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST14: multi-threads connect to same container", dfs_test_mt_connect, async_disable, + test_case_teardown}, + {"DFS_UNIT_TEST15: DFS chown", dfs_test_chown, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST16: DFS stat mtime", dfs_test_mtime, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST17: multi-threads async IO", dfs_test_async_io_th, async_disable, + test_case_teardown}, + {"DFS_UNIT_TEST18: async IO", dfs_test_async_io, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST19: DFS readdir", dfs_test_readdir, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST20: dfs oclass hints", dfs_test_oclass_hints, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST21: dfs multiple pools", dfs_test_multiple_pools, async_disable, + test_case_teardown}, + {"DFS_UNIT_TEST22: dfs extended attributes", dfs_test_xattrs, test_case_teardown}, + {"DFS_UNIT_TEST23: dfs MWC container checker", dfs_test_checker, async_disable, + test_case_teardown}, + {"DFS_UNIT_TEST24: dfs MWC SB fix", dfs_test_fix_sb, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST25: dfs MWC root fix", dfs_test_relink_root, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST26: dfs MWC chunk size fix", dfs_test_fix_chunk_size, async_disable, + test_case_teardown}, + {"DFS_UNIT_TEST27: dfs pipeline find", dfs_test_pipeline_find, async_disable, + test_case_teardown}, + {"DFS_UNIT_TEST28: dfs open/lookup flags", dfs_test_oflags, async_disable, test_case_teardown}, + {"DFS_UNIT_TEST29: Symlinks with an absolute value", dfs_test_syml_abs, async_disable, + test_case_teardown}, }; static int diff --git a/src/tests/suite/dfuse_test.c b/src/tests/suite/dfuse_test.c index 632acf801dc..b380b468584 100644 --- a/src/tests/suite/dfuse_test.c +++ b/src/tests/suite/dfuse_test.c @@ -39,7 +39,7 @@ /* Tests can be run by specifying the appropriate argument for a test or all will be run if no test * is specified. */ -static const char *all_tests = "ismdlfec"; +static const char *all_tests = "ismdlfecp"; static void print_usage() @@ -58,6 +58,7 @@ print_usage() /* verifyenv is only run by exec test. Should not be executed directly */ /* print_message("dfuse_test --verifyenv\n"); */ print_message("dfuse_test -c|--cache\n"); + print_message("dfuse_test -p|--path\n"); print_message("Default runs all tests\n=============\n"); print_message("\n=============================\n"); } @@ -533,6 +534,186 @@ do_mtime(void **state) assert_return_code(rc, errno); } +static void +check_mtime(int dirfd, const char *path, int flags, const struct timespec *expected) +{ + struct stat stbuf; + int rc; + + rc = fstatat(dirfd, path, &stbuf, flags); + assert_return_code(rc, errno); + assert_int_equal(stbuf.st_mtim.tv_sec, expected->tv_sec); + assert_int_equal(stbuf.st_mtim.tv_nsec, expected->tv_nsec); +} + +/* mtime was updated to the current time, not to the UTIME_NOW marker */ +static void +check_mtime_now(int dirfd, const char *path, const struct timespec *old) +{ + struct stat stbuf; + int rc; + + rc = fstatat(dirfd, path, &stbuf, 0); + assert_return_code(rc, errno); + assert_true(stbuf.st_mtim.tv_nsec >= 0 && stbuf.st_mtim.tv_nsec < 1000000000L); + assert_true(timespec_gt(stbuf.st_mtim, *old)); +} + +/* utimensat()/futimens() on an absolute path, on a fd, and with UTIME_NOW/UTIME_OMIT. */ +static void +check_utimens(int root, const char *name, const char *abs_path) +{ + struct timespec old = {.tv_sec = time(NULL) - 1000, .tv_nsec = 500}; + struct timespec times[2]; + int fd; + int rc; + + /* explicit times */ + times[0] = old; + times[1] = old; + rc = utimensat(root, name, times, 0); + assert_return_code(rc, errno); + check_mtime(root, name, 0, &old); + + /* UTIME_OMIT on mtime leaves it alone */ + times[0].tv_nsec = UTIME_NOW; + times[1].tv_nsec = UTIME_OMIT; + rc = utimensat(root, name, times, 0); + assert_return_code(rc, errno); + check_mtime(root, name, 0, &old); + + /* both UTIME_OMIT is a no-op */ + times[0].tv_nsec = UTIME_OMIT; + rc = utimensat(AT_FDCWD, abs_path, times, 0); + assert_return_code(rc, errno); + check_mtime(root, name, 0, &old); + + /* UTIME_NOW on mtime sets the current time */ + times[1].tv_sec = 0; + times[1].tv_nsec = UTIME_NOW; + rc = utimensat(AT_FDCWD, abs_path, times, 0); + assert_return_code(rc, errno); + check_mtime_now(root, name, &old); + + /* out of range nsec */ + times[0] = old; + times[1].tv_sec = old.tv_sec; + times[1].tv_nsec = 1000000000L; + rc = utimensat(root, name, times, 0); + assert_int_equal(rc, -1); + assert_int_equal(errno, EINVAL); + + /* DAOS-19755: NULL times on an absolute path */ + rc = utimensat(AT_FDCWD, abs_path, (struct timespec[2]){old, old}, 0); + assert_return_code(rc, errno); + rc = utimensat(AT_FDCWD, abs_path, NULL, 0); + assert_return_code(rc, errno); + check_mtime_now(root, name, &old); + + /* the same through futimens() */ + fd = openat(root, name, O_RDWR); + assert_return_code(fd, errno); + + times[0] = old; + times[1] = old; + rc = futimens(fd, times); + assert_return_code(rc, errno); + check_mtime(root, name, 0, &old); + + times[0].tv_nsec = UTIME_NOW; + times[1].tv_nsec = UTIME_OMIT; + rc = futimens(fd, times); + assert_return_code(rc, errno); + check_mtime(root, name, 0, &old); + + times[1].tv_sec = 0; + times[1].tv_nsec = UTIME_NOW; + rc = futimens(fd, times); + assert_return_code(rc, errno); + check_mtime_now(root, name, &old); + + times[1].tv_nsec = -1; + rc = futimens(fd, times); + assert_int_equal(rc, -1); + assert_int_equal(errno, EINVAL); + + rc = close(fd); + assert_return_code(rc, errno); +} + +/* DAOS-19755/DAOS-19757: utimensat() and futimens() honor NULL times, UTIME_NOW, UTIME_OMIT and + * AT_SYMLINK_NOFOLLOW both in the container and on a path outside of it. + */ +void +do_utimens(void **state) +{ + struct timespec old = {.tv_sec = time(NULL) - 2000, .tv_nsec = 700}; + struct timespec target_mtime; + struct stat stbuf; + char path[512]; + char out_file[256]; + char out_link[256]; + size_t len; + int root; + int fd; + int rc; + + root = open(test_dir, O_PATH | O_DIRECTORY); + assert_return_code(root, errno); + + len = snprintf(path, sizeof(path), "%s/utimens_file", test_dir); + assert_true(len < sizeof(path)); + fd = openat(root, "utimens_file", O_RDWR | O_CREAT | O_TRUNC, S_IWUSR | S_IRUSR); + assert_return_code(fd, errno); + rc = close(fd); + assert_return_code(rc, errno); + + check_utimens(root, "utimens_file", path); + + rc = unlinkat(root, "utimens_file", 0); + assert_return_code(rc, errno); + rc = close(root); + assert_return_code(rc, errno); + + /* a path outside of the container is handed to libc */ + len = snprintf(out_file, sizeof(out_file), "/tmp/dfuse_test_utimens_%d", getpid()); + assert_true(len < sizeof(out_file)); + len = snprintf(out_link, sizeof(out_link), "/tmp/dfuse_test_utimens_link_%d", getpid()); + assert_true(len < sizeof(out_link)); + /* cmocka has no teardown here, so an aborted run can leave these behind */ + unlink(out_link); + unlink(out_file); + + fd = open(out_file, O_RDWR | O_CREAT | O_EXCL, S_IWUSR | S_IRUSR); + assert_return_code(fd, errno); + rc = close(fd); + assert_return_code(rc, errno); + + root = open("/tmp", O_PATH | O_DIRECTORY); + assert_return_code(root, errno); + + check_utimens(root, out_file + strlen("/tmp/"), out_file); + + /* AT_SYMLINK_NOFOLLOW changes the link, not its target */ + rc = symlink(out_file, out_link); + assert_return_code(rc, errno); + rc = stat(out_file, &stbuf); + assert_return_code(rc, errno); + target_mtime = stbuf.st_mtim; + + rc = utimensat(AT_FDCWD, out_link, (struct timespec[2]){old, old}, AT_SYMLINK_NOFOLLOW); + assert_return_code(rc, errno); + check_mtime(AT_FDCWD, out_link, AT_SYMLINK_NOFOLLOW, &old); + check_mtime(AT_FDCWD, out_file, 0, &target_mtime); + + rc = close(root); + assert_return_code(rc, errno); + rc = unlink(out_link); + assert_return_code(rc, errno); + rc = unlink(out_file); + assert_return_code(rc, errno); +} + static int get_dir_num_entry(DIR *dirp) { @@ -950,6 +1131,1113 @@ do_cachingcheck(void **state) assert_return_code(rc, errno); } +static void +check_symlink_outside(const char *dir, const char *target) +{ + struct stat stbuf; + char link_path[512]; + char chain_name[64]; + char chain_path[512]; + size_t len; + int fd; + int rc; + + len = snprintf(link_path, sizeof(link_path) - 1, "%s/symlink_outside_%d", dir, getpid()); + assert_true(len < (sizeof(link_path) - 1)); + + len = snprintf(chain_name, sizeof(chain_name) - 1, "symlink_chain_%d", getpid()); + assert_true(len < (sizeof(chain_name) - 1)); + + len = snprintf(chain_path, sizeof(chain_path) - 1, "%s/%s", dir, chain_name); + assert_true(len < (sizeof(chain_path) - 1)); + + /* cmocka has no teardown here, so an aborted run can leave the links behind */ + unlink(link_path); + unlink(chain_path); + + rc = symlink(target, link_path); + assert_return_code(rc, errno); + + rc = lstat(link_path, &stbuf); + assert_return_code(rc, errno); + assert_true(S_ISLNK(stbuf.st_mode)); + + rc = stat(link_path, &stbuf); + assert_return_code(rc, errno); + assert_true(S_ISREG(stbuf.st_mode)); + assert_int_equal(stbuf.st_size, 6); + + rc = access(link_path, R_OK | W_OK | X_OK); + assert_return_code(rc, errno); + + rc = faccessat(AT_FDCWD, link_path, R_OK, 0); + assert_return_code(rc, errno); + + fd = open(link_path, O_RDONLY); + assert_return_code(fd, errno); + rc = close(fd); + assert_return_code(rc, errno); + + /* a relative link to the absolute one, as python3 -> python -> /usr/bin/python3.x in a + * venv. Only the last hop has the absolute value, so the entry itself does not reveal it. + */ + len = strnlen(link_path, sizeof(link_path)); + while (len > 0 && link_path[len - 1] != '/') + len--; + rc = symlink(link_path + len, chain_path); + assert_return_code(rc, errno); + + rc = lstat(chain_path, &stbuf); + assert_return_code(rc, errno); + assert_true(S_ISLNK(stbuf.st_mode)); + + rc = stat(chain_path, &stbuf); + assert_return_code(rc, errno); + assert_true(S_ISREG(stbuf.st_mode)); + assert_int_equal(stbuf.st_size, 6); + + rc = access(chain_path, R_OK | W_OK | X_OK); + assert_return_code(rc, errno); + + fd = open(chain_path, O_RDONLY); + assert_return_code(fd, errno); + rc = close(fd); + assert_return_code(rc, errno); + + rc = unlink(chain_path); + assert_return_code(rc, errno); + + rc = unlink(link_path); + assert_return_code(rc, errno); +} + +/* A symlink stored in a container may point outside of it. dfs resolves a link value within the + * container, so an absolute value is only resolvable by the kernel. dfs reports EINVAL for a link + * below the container root but ENOENT for a link in the root itself, so cover both positions. + */ +void +do_symlink_outside(void **state) +{ + char target[256]; + char sub_dir[512]; + size_t len; + int fd; + int rc; + + len = snprintf(target, sizeof(target) - 1, "/tmp/dfuse_test_target_%d", getpid()); + assert_true(len < (sizeof(target) - 1)); + + len = snprintf(sub_dir, sizeof(sub_dir) - 1, "%s/symlink_dir_%d", test_dir, getpid()); + assert_true(len < (sizeof(sub_dir) - 1)); + + /* cmocka has no teardown here, so an aborted run can leave these behind */ + unlink(target); + rmdir(sub_dir); + + fd = open(target, O_RDWR | O_CREAT | O_EXCL, S_IRWXU); + assert_return_code(fd, errno); + rc = write(fd, "hello", 6); + assert_return_code(rc, errno); + rc = close(fd); + assert_return_code(rc, errno); + + check_symlink_outside(test_dir, target); + + /* one level down, as the python3 symlink of a venv is */ + rc = mkdir(sub_dir, S_IRWXU); + assert_return_code(rc, errno); + check_symlink_outside(sub_dir, target); + rc = rmdir(sub_dir); + assert_return_code(rc, errno); + + rc = unlink(target); + assert_return_code(rc, errno); +} + +static void +create_a_file(const char *file_name) +{ + int fd; + int rc; + + fd = open(file_name, O_RDWR | O_CREAT, S_IWUSR | S_IRUSR); + assert_return_code(fd, errno); + rc = close(fd); + assert_return_code(rc, errno); +} + +/* fchdir() to a directory outside of the container has to move the cwd that relative paths are + * resolved against. + */ +void +do_fchdir(void **state) +{ + struct stat stbuf; + char start_dir[512]; + char cwd[512]; + char name_old[64]; + char name_new[64]; + char path[512]; + size_t len; + int fd; + int rc; + + assert_non_null(getcwd(start_dir, sizeof(start_dir))); + + rc = chdir(test_dir); + assert_return_code(rc, errno); + + /* get_current_dir_name() returns $PWD when it looks valid, so a stale $PWD is what lets a + * wrong cwd survive into relative path resolution. + */ + assert_non_null(getcwd(cwd, sizeof(cwd))); + rc = setenv("PWD", cwd, 1); + assert_return_code(rc, errno); + + fd = open("/tmp", O_RDONLY | O_DIRECTORY); + assert_return_code(fd, errno); + rc = fchdir(fd); + assert_return_code(rc, errno); + rc = close(fd); + assert_return_code(rc, errno); + + assert_non_null(getcwd(cwd, sizeof(cwd))); + assert_string_equal(cwd, "/tmp"); + + len = snprintf(name_old, sizeof(name_old) - 1, "dfuse_test_fchdir_%d.tmp", getpid()); + assert_true(len < (sizeof(name_old) - 1)); + + len = snprintf(name_new, sizeof(name_new) - 1, "dfuse_test_fchdir_%d.new", getpid()); + assert_true(len < (sizeof(name_new) - 1)); + + /* cmocka has no teardown here, so an aborted run can leave these behind */ + unlink(name_old); + unlink(name_new); + + create_a_file(name_old); + + rc = rename(name_old, name_new); + assert_return_code(rc, errno); + + /* the entry has to exist under /tmp, not under the container */ + len = snprintf(path, sizeof(path) - 1, "/tmp/%s", name_new); + assert_true(len < (sizeof(path) - 1)); + + rc = stat(path, &stbuf); + assert_return_code(rc, errno); + + rc = unlink(path); + assert_return_code(rc, errno); + + rc = chdir(start_dir); + assert_return_code(rc, errno); + + rc = setenv("PWD", start_dir, 1); + assert_return_code(rc, errno); +} + +/* Passing a cwd to posix_spawn()/subprocess() makes chdir() run between fork() and exec(), where + * dfs is not usable. chdir() has to succeed there anyway. + */ +void +do_chdir_fork(void **state) +{ + char *argv[3] = {"cat", "marker", NULL}; + char start_dir[512]; + char sub_dir[64]; + char marker[128]; + char path[512]; + size_t len; + pid_t pid; + int status; + int rc; + + assert_non_null(getcwd(start_dir, sizeof(start_dir))); + + rc = chdir(test_dir); + assert_return_code(rc, errno); + + /* a name reused across runs can hit a stale dentry cached by dfuse */ + len = snprintf(sub_dir, sizeof(sub_dir) - 1, "chdir_fork_dir_%d", getpid()); + assert_true(len < (sizeof(sub_dir) - 1)); + + len = snprintf(marker, sizeof(marker) - 1, "%s/marker", sub_dir); + assert_true(len < (sizeof(marker) - 1)); + + /* cmocka has no teardown here, so an aborted run can leave these behind */ + unlink(marker); + rmdir(sub_dir); + + rc = mkdir(sub_dir, S_IRWXU); + assert_return_code(rc, errno); + + create_a_file(marker); + + pid = fork(); + assert_return_code(pid, errno); + if (pid == 0) { + /* cmocka macros are not usable in the child, report through the exit code */ + if (chdir(sub_dir) != 0) + _exit(10); + execv("/bin/cat", argv); + _exit(11); + } + + rc = waitpid(pid, &status, 0); + assert_return_code(rc, errno); + + len = snprintf(path, sizeof(path) - 1, "%s/%s", test_dir, marker); + assert_true(len < (sizeof(path) - 1)); + rc = unlink(path); + assert_return_code(rc, errno); + + len = snprintf(path, sizeof(path) - 1, "%s/%s", test_dir, sub_dir); + assert_true(len < (sizeof(path) - 1)); + rc = rmdir(path); + assert_return_code(rc, errno); + + rc = chdir(start_dir); + assert_return_code(rc, errno); + + assert_true(WIFEXITED(status)); + assert_int_equal(WEXITSTATUS(status), 0); +} + +/* POSIX resolves '..' at the root as the root itself. Such a path leaves the container, so it has + * to be handed to the kernel rather than rejected. + */ +void +do_dot_dot_above_root(void **state) +{ + struct stat stbuf; + char target[256]; + char path[512]; + char start_dir[512]; + size_t len; + int fd; + int rc; + + len = snprintf(target, sizeof(target) - 1, "/tmp/dfuse_test_dotdot_%d", getpid()); + assert_true(len < (sizeof(target) - 1)); + + /* cmocka has no teardown here, so an aborted run can leave this behind */ + unlink(target); + + fd = open(target, O_RDWR | O_CREAT | O_EXCL, S_IRWXU); + assert_return_code(fd, errno); + rc = close(fd); + assert_return_code(rc, errno); + + /* an absolute path starting above the root */ + len = snprintf(path, sizeof(path) - 1, "/..%s", target); + assert_true(len < (sizeof(path) - 1)); + + rc = stat(path, &stbuf); + assert_return_code(rc, errno); + rc = access(path, R_OK); + assert_return_code(rc, errno); + fd = open(path, O_RDONLY); + assert_return_code(fd, errno); + rc = close(fd); + assert_return_code(rc, errno); + + /* "/./" is erased in place, which hides the following "//" from the cleanup pass, so the + * ".." reaches remove_dot_dot() with no component before it. This used to loop forever. + */ + len = snprintf(path, sizeof(path) - 1, "/.//..%s", target); + assert_true(len < (sizeof(path) - 1)); + + rc = stat(path, &stbuf); + assert_return_code(rc, errno); + rc = access(path, R_OK); + assert_return_code(rc, errno); + fd = open(path, O_RDONLY); + assert_return_code(fd, errno); + rc = close(fd); + assert_return_code(rc, errno); + + /* a relative path climbing out of the container with more '..' than the cwd has depth */ + assert_non_null(getcwd(start_dir, sizeof(start_dir))); + + rc = chdir(test_dir); + assert_return_code(rc, errno); + + len = snprintf(path, sizeof(path) - 1, "../../../../../../../../../../../..%s", target); + assert_true(len < (sizeof(path) - 1)); + + rc = stat(path, &stbuf); + assert_return_code(rc, errno); + rc = access(path, R_OK); + assert_return_code(rc, errno); + fd = open(path, O_RDONLY); + assert_return_code(fd, errno); + rc = close(fd); + assert_return_code(rc, errno); + + rc = chdir(start_dir); + assert_return_code(rc, errno); + + rc = unlink(target); + assert_return_code(rc, errno); +} + +/* Rust's std checks once per process whether statx() is usable by calling it with a NULL path and + * a NULL buffer, expecting EFAULT from the kernel. uv does this at start up when its cache dir is + * missing, so the interception must pass a NULL path through rather than dereference it. + */ +void +do_statx_null_probe(void **state) +{ + /* glibc declares both nonnull, so the compiler would otherwise reject literal NULLs */ + const char *volatile null_path = NULL; + struct statx *volatile null_buf = NULL; + int rc; + + rc = statx(0, null_path, 0, STATX_BASIC_STATS, null_buf); + assert_int_equal(rc, -1); + assert_int_equal(errno, EFAULT); +} + +static void +write_file(const char *path, const char *content) +{ + int fd; + int rc; + + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU); + assert_return_code(fd, errno); + rc = write(fd, content, strlen(content)); + assert_int_equal(rc, strlen(content)); + rc = close(fd); + assert_return_code(rc, errno); +} + +static void +check_file_content(const char *path, const char *content) +{ + char buf[64] = {0}; + int fd; + int rc; + + fd = open(path, O_RDONLY); + assert_return_code(fd, errno); + rc = read(fd, buf, sizeof(buf) - 1); + assert_return_code(rc, errno); + assert_string_equal(buf, content); + rc = close(fd); + assert_return_code(rc, errno); +} + +/* Paths are normalized lexically before the leaf is split from its parent, so "//", "/./" and + * ".." in any combination have to resolve like the kernel does. Every probe here used to leave a + * component behind and then resolve the leaf in the wrong directory. + */ +void +do_path_normalize(void **state) +{ + struct stat stbuf; + struct stat stbuf_b; + char base[512]; + char path[1024]; + char start_dir[512]; + size_t len; + + len = snprintf(base, sizeof(base) - 1, "%s/norm_%d", test_dir, getpid()); + assert_true(len < (sizeof(base) - 1)); + + /* fixture: b/c/x, b/c/w and b/w with different contents, b/..c, no b/x */ + assert_return_code(mkdir(base, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/b", base); + assert_return_code(mkdir(path, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/b/c", base); + assert_return_code(mkdir(path, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/b/..c", base); + assert_return_code(mkdir(path, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/b/c/x", base); + write_file(path, "b/c/x"); + snprintf(path, sizeof(path), "%s/b/c/w", base); + write_file(path, "b/c/w"); + snprintf(path, sizeof(path), "%s/b/w", base); + write_file(path, "b/w"); + + /* "//" after a "/./" */ + snprintf(path, sizeof(path), "%s/./b//c/x", base); + assert_return_code(stat(path, &stbuf), errno); + snprintf(path, sizeof(path), "%s/./b//c/w", base); + check_file_content(path, "b/c/w"); + + /* the same through the implicit "/./" of a relative path */ + assert_non_null(getcwd(start_dir, sizeof(start_dir))); + assert_return_code(chdir(base), errno); + assert_return_code(stat("./b//c/x", &stbuf), errno); + check_file_content("./b//c/w", "b/c/w"); + check_file_content(".//b/./c//w", "b/c/w"); + assert_return_code(chdir(start_dir), errno); + + /* trailing ".." after a component that itself starts with ".." */ + snprintf(path, sizeof(path), "%s/b", base); + assert_return_code(stat(path, &stbuf_b), errno); + snprintf(path, sizeof(path), "%s/b/..c/..", base); + assert_return_code(stat(path, &stbuf), errno); + assert_true(S_ISDIR(stbuf.st_mode)); + assert_int_equal(stbuf.st_ino, stbuf_b.st_ino); + snprintf(path, sizeof(path), "%s/b/..c/../c/./x", base); + check_file_content(path, "b/c/x"); + + /* trailing "/" and "/." */ + snprintf(path, sizeof(path), "%s/b/c/", base); + assert_return_code(stat(path, &stbuf), errno); + assert_true(S_ISDIR(stbuf.st_mode)); + snprintf(path, sizeof(path), "%s/b/c/.", base); + assert_return_code(stat(path, &stbuf), errno); + assert_true(S_ISDIR(stbuf.st_mode)); + + snprintf(path, sizeof(path), "%s/b/c/x", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/b/c/w", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/b/w", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/b/..c", base); + assert_return_code(rmdir(path), errno); + snprintf(path, sizeof(path), "%s/b/c", base); + assert_return_code(rmdir(path), errno); + snprintf(path, sizeof(path), "%s/b", base); + assert_return_code(rmdir(path), errno); + assert_return_code(rmdir(base), errno); +} + +/* ".." applies to the resolved component in front of it: with "l2 -> c/d", "l2/../y2" is "c/y2" + * and not "y2". A component that is not a directory makes the path fail as it does in the kernel. + */ +void +do_dot_dot_symlink(void **state) +{ + struct stat stbuf; + struct stat stbuf_c; + char base[512]; + char host_dir[256]; + char path[1024]; + char start_dir[512]; + size_t len; + int rc; + + len = snprintf(base, sizeof(base) - 1, "%s/dotdot_%d", test_dir, getpid()); + assert_true(len < (sizeof(base) - 1)); + len = snprintf(host_dir, sizeof(host_dir) - 1, "/tmp/dfuse_test_dotdot_%d", getpid()); + assert_true(len < (sizeof(host_dir) - 1)); + + /* fixture: b/c/d, b/c/x, b/c/y2, b/y2, b/l2 -> c/d, b/l3 -> l2, b/labs -> */ + assert_return_code(mkdir(base, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/b", base); + assert_return_code(mkdir(path, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/b/c", base); + assert_return_code(mkdir(path, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/b/c/d", base); + assert_return_code(mkdir(path, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/b/c/x", base); + write_file(path, "b/c/x"); + snprintf(path, sizeof(path), "%s/b/c/y2", base); + write_file(path, "b/c/y2"); + snprintf(path, sizeof(path), "%s/b/y2", base); + write_file(path, "b/y2"); + snprintf(path, sizeof(path), "%s/b/l2", base); + assert_return_code(symlink("c/d", path), errno); + snprintf(path, sizeof(path), "%s/b/l3", base); + assert_return_code(symlink("l2", path), errno); + rc = mkdir(host_dir, S_IRWXU); + if (rc != 0) + assert_int_equal(errno, EEXIST); + snprintf(path, sizeof(path), "%s/y3", host_dir); + write_file(path, "host"); + snprintf(path, sizeof(path), "%s/b/labs", base); + assert_return_code(symlink(host_dir, path), errno); + + /* through one and two relative links */ + snprintf(path, sizeof(path), "%s/b/l2/../y2", base); + check_file_content(path, "b/c/y2"); + snprintf(path, sizeof(path), "%s/b/l3/../y2", base); + check_file_content(path, "b/c/y2"); + snprintf(path, sizeof(path), "%s/b/l2/../../y2", base); + check_file_content(path, "b/y2"); + + /* "l2/.." is "c" */ + snprintf(path, sizeof(path), "%s/b/c", base); + assert_return_code(stat(path, &stbuf_c), errno); + snprintf(path, sizeof(path), "%s/b/l2/..", base); + assert_return_code(stat(path, &stbuf), errno); + assert_true(S_ISDIR(stbuf.st_mode)); + assert_int_equal(stbuf.st_ino, stbuf_c.st_ino); + + /* the same relative to a cwd reached through the link */ + assert_non_null(getcwd(start_dir, sizeof(start_dir))); + snprintf(path, sizeof(path), "%s/b/l2", base); + assert_return_code(chdir(path), errno); + check_file_content("../y2", "b/c/y2"); + check_file_content("./../x", "b/c/x"); + assert_return_code(chdir(start_dir), errno); + + /* through a link with an absolute value, which only the kernel can resolve */ + snprintf(path, sizeof(path), "%s/b/labs/../dfuse_test_dotdot_%d/y3", base, getpid()); + check_file_content(path, "host"); + + /* not a directory in front of "..", and a missing one */ + snprintf(path, sizeof(path), "%s/b/c/x/../y2", base); + rc = stat(path, &stbuf); + assert_int_equal(rc, -1); + assert_int_equal(errno, ENOTDIR); + snprintf(path, sizeof(path), "%s/b/nonexist/../y2", base); + rc = stat(path, &stbuf); + assert_int_equal(rc, -1); + assert_int_equal(errno, ENOENT); + + snprintf(path, sizeof(path), "%s/y3", host_dir); + assert_return_code(unlink(path), errno); + assert_return_code(rmdir(host_dir), errno); + snprintf(path, sizeof(path), "%s/b/labs", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/b/l3", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/b/l2", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/b/y2", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/b/c/y2", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/b/c/x", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/b/c/d", base); + assert_return_code(rmdir(path), errno); + snprintf(path, sizeof(path), "%s/b/c", base); + assert_return_code(rmdir(path), errno); + snprintf(path, sizeof(path), "%s/b", base); + assert_return_code(rmdir(path), errno); + assert_return_code(rmdir(base), errno); +} + +/* dfs resolves an absolute symlink value in the container root from that root. When the same path + * also exists inside the container the lookup succeeds with the wrong file, so such a link has to + * be handed to the kernel before it is dereferenced. test_dir is the container root under ftest. + */ +void +do_root_symlink_shadow(void **state) +{ + struct stat stbuf; + char target[256]; + char shadow_dir[512]; + char shadow[512]; + char link_path[512]; + char chain_path[512]; + char chain_name[64]; + size_t len; + int rc; + + len = snprintf(target, sizeof(target) - 1, "/tmp/dfuse_test_shadow_%d", getpid()); + assert_true(len < (sizeof(target) - 1)); + len = snprintf(shadow_dir, sizeof(shadow_dir) - 1, "%s/tmp", test_dir); + assert_true(len < (sizeof(shadow_dir) - 1)); + len = snprintf(shadow, sizeof(shadow) - 1, "%s%s", test_dir, target); + assert_true(len < (sizeof(shadow) - 1)); + len = snprintf(link_path, sizeof(link_path) - 1, "%s/shadow_link_%d", test_dir, getpid()); + assert_true(len < (sizeof(link_path) - 1)); + len = snprintf(chain_name, sizeof(chain_name) - 1, "shadow_chain_%d", getpid()); + assert_true(len < (sizeof(chain_name) - 1)); + len = snprintf(chain_path, sizeof(chain_path) - 1, "%s/%s", test_dir, chain_name); + assert_true(len < (sizeof(chain_path) - 1)); + + /* cmocka has no teardown here, so an aborted run can leave these behind */ + unlink(target); + unlink(shadow); + unlink(link_path); + unlink(chain_path); + + write_file(target, "host"); + rc = mkdir(shadow_dir, S_IRWXU); + if (rc != 0) + assert_int_equal(errno, EEXIST); + write_file(shadow, "container"); + + rc = symlink(target, link_path); + assert_return_code(rc, errno); + /* a bare relative link to the absolute one, as in a venv */ + rc = symlink(link_path + strlen(test_dir) + 1, chain_path); + assert_return_code(rc, errno); + + check_file_content(link_path, "host"); + check_file_content(chain_path, "host"); + + rc = stat(link_path, &stbuf); + assert_return_code(rc, errno); + assert_int_equal(stbuf.st_size, strlen("host")); + rc = stat(chain_path, &stbuf); + assert_return_code(rc, errno); + assert_int_equal(stbuf.st_size, strlen("host")); + + /* the shadow is not writable through the link: the host file is */ + assert_return_code(chmod(shadow, S_IRUSR), errno); + assert_return_code(access(link_path, W_OK), errno); + assert_return_code(access(chain_path, W_OK), errno); + + assert_return_code(unlink(chain_path), errno); + assert_return_code(unlink(link_path), errno); + assert_return_code(unlink(shadow), errno); + rmdir(shadow_dir); + assert_return_code(unlink(target), errno); +} + +/* Split test_dir, the container root, into the host directory that holds it and its name */ +static void +split_test_dir(char *parent, size_t parent_size, char *name, size_t name_size) +{ + size_t len = strlen(test_dir); + char *sep; + + assert_true(len > 1 && len < parent_size); + strncpy(parent, test_dir, parent_size); + parent[parent_size - 1] = '\0'; + while (len > 1 && parent[len - 1] == '/') + parent[--len] = '\0'; + sep = strrchr(parent, '/'); + assert_non_null(sep); + assert_true(sep > parent); + *sep = '\0'; + assert_true(strlen(sep + 1) < name_size); + strncpy(name, sep + 1, name_size); + name[name_size - 1] = '\0'; +} + +/* A symlink in the container root whose relative value climbs out of the container, as in + * "foo -> ../bar" for a file beside the mount point. dfs resolves ".." of its root to the root + * itself and reports ENOENT, so the link has to be handed to the kernel. + */ +void +do_root_symlink_up(void **state) +{ + struct stat stbuf; + char parent[512]; + char name[256]; + char target[512]; + char value[256]; + char link_path[512]; + size_t len; + int rc; + + split_test_dir(parent, sizeof(parent), name, sizeof(name)); + len = snprintf(target, sizeof(target) - 1, "%s/dfuse_test_up_%d", parent, getpid()); + assert_true(len < (sizeof(target) - 1)); + len = snprintf(value, sizeof(value) - 1, "../dfuse_test_up_%d", getpid()); + assert_true(len < (sizeof(value) - 1)); + len = snprintf(link_path, sizeof(link_path) - 1, "%s/up_link_%d", test_dir, getpid()); + assert_true(len < (sizeof(link_path) - 1)); + + /* cmocka has no teardown here, so an aborted run can leave these behind */ + unlink(target); + unlink(link_path); + + write_file(target, "host"); + rc = symlink(value, link_path); + assert_return_code(rc, errno); + + check_file_content(link_path, "host"); + rc = stat(link_path, &stbuf); + assert_return_code(rc, errno); + assert_int_equal(stbuf.st_size, strlen("host")); + assert_return_code(access(link_path, R_OK), errno); + + /* the link itself is in the container */ + rc = lstat(link_path, &stbuf); + assert_return_code(rc, errno); + assert_true(S_ISLNK(stbuf.st_mode)); + + /* a dangling one still reports ENOENT */ + assert_return_code(unlink(target), errno); + rc = stat(link_path, &stbuf); + assert_int_equal(rc, -1); + assert_int_equal(errno, ENOENT); + + assert_return_code(unlink(link_path), errno); +} + +/* ".." applied to the mount point itself leads to the host directory that holds it. A path that + * comes straight back into the mount, as a relative path from a cwd at the container root does with + * "..//x", is served from the container; one that stays outside is left to the kernel. + */ +void +do_dot_dot_mount_root(void **state) +{ + struct stat stbuf; + struct stat stbuf_parent; + char parent[512]; + char name[256]; + char file_path[512]; + char path[1024]; + char start_dir[512]; + size_t len; + + split_test_dir(parent, sizeof(parent), name, sizeof(name)); + len = snprintf(file_path, sizeof(file_path) - 1, "%s/mount_up_%d", test_dir, getpid()); + assert_true(len < (sizeof(file_path) - 1)); + write_file(file_path, "container"); + + /* back into the mount, absolute and relative to a cwd at the container root */ + snprintf(path, sizeof(path), "%s/../%s/mount_up_%d", test_dir, name, getpid()); + check_file_content(path, "container"); + assert_non_null(getcwd(start_dir, sizeof(start_dir))); + assert_return_code(chdir(test_dir), errno); + snprintf(path, sizeof(path), "../%s/mount_up_%d", name, getpid()); + check_file_content(path, "container"); + snprintf(path, sizeof(path), "../%s/./mount_up_%d", name, getpid()); + assert_return_code(stat(path, &stbuf), errno); + assert_int_equal(stbuf.st_size, strlen("container")); + assert_return_code(chdir(start_dir), errno); + + /* out of the mount, with and without a return trip first */ + assert_return_code(stat(parent, &stbuf_parent), errno); + snprintf(path, sizeof(path), "%s/..", test_dir); + assert_return_code(stat(path, &stbuf), errno); + assert_true(S_ISDIR(stbuf.st_mode)); + assert_int_equal(stbuf.st_ino, stbuf_parent.st_ino); + snprintf(path, sizeof(path), "%s/../%s/..", test_dir, name); + assert_return_code(stat(path, &stbuf), errno); + assert_true(S_ISDIR(stbuf.st_mode)); + assert_int_equal(stbuf.st_ino, stbuf_parent.st_ino); + + assert_return_code(unlink(file_path), errno); +} + +/* A directory symlink with an absolute value in a non-leaf position, in a subdirectory and in the + * container root. dfs cannot follow it and the path has to be handed to the kernel; an in-container + * copy of the same path proves that the host file is the one reached. test_dir is the container + * root under ftest. + */ +void +do_symlink_abs_nonleaf(void **state) +{ + struct stat stbuf; + char base[512]; + char host_dir[256]; + char shadow_dir[512]; + char path[1024]; + char start_dir[512]; + size_t len; + int rc; + + len = snprintf(base, sizeof(base) - 1, "%s/absnl_%d", test_dir, getpid()); + assert_true(len < (sizeof(base) - 1)); + len = snprintf(host_dir, sizeof(host_dir) - 1, "/tmp/dfuse_test_absnl_%d", getpid()); + assert_true(len < (sizeof(host_dir) - 1)); + len = snprintf(shadow_dir, sizeof(shadow_dir) - 1, "%s%s", test_dir, host_dir); + assert_true(len < (sizeof(shadow_dir) - 1)); + + /* fixture: /f on the host, /f as its in-container shadow, + * sub/hlink and /absnl_root_ -> , sub/back -> /d, d/g + */ + rc = mkdir(host_dir, S_IRWXU); + if (rc != 0) + assert_int_equal(errno, EEXIST); + snprintf(path, sizeof(path), "%s/f", host_dir); + write_file(path, "host"); + snprintf(path, sizeof(path), "%s/tmp", test_dir); + rc = mkdir(path, S_IRWXU); + if (rc != 0) + assert_int_equal(errno, EEXIST); + rc = mkdir(shadow_dir, S_IRWXU); + if (rc != 0) + assert_int_equal(errno, EEXIST); + snprintf(path, sizeof(path), "%s/f", shadow_dir); + write_file(path, "container"); + assert_return_code(mkdir(base, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/sub", base); + assert_return_code(mkdir(path, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/d", base); + assert_return_code(mkdir(path, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/d/g", base); + write_file(path, "in-d"); + snprintf(path, sizeof(path), "%s/sub/hlink", base); + assert_return_code(symlink(host_dir, path), errno); + snprintf(path, sizeof(path), "%s/absnl_root_%d", test_dir, getpid()); + unlink(path); + assert_return_code(symlink(host_dir, path), errno); + snprintf(path, sizeof(path), "%s/sub/back", base); + len = snprintf(start_dir, sizeof(start_dir) - 1, "%s/d", base); + assert_true(len < (sizeof(start_dir) - 1)); + assert_return_code(symlink(start_dir, path), errno); + + /* through the link in a subdirectory, absolute and relative to a cwd */ + snprintf(path, sizeof(path), "%s/sub/hlink/f", base); + check_file_content(path, "host"); + assert_return_code(stat(path, &stbuf), errno); + assert_int_equal(stbuf.st_size, strlen("host")); + assert_return_code(access(path, R_OK), errno); + assert_non_null(getcwd(start_dir, sizeof(start_dir))); + snprintf(path, sizeof(path), "%s/sub", base); + assert_return_code(chdir(path), errno); + check_file_content("hlink/f", "host"); + check_file_content("./hlink//f", "host"); + assert_return_code(chdir(start_dir), errno); + + /* through the link in the container root */ + snprintf(path, sizeof(path), "%s/absnl_root_%d/f", test_dir, getpid()); + check_file_content(path, "host"); + assert_return_code(stat(path, &stbuf), errno); + assert_int_equal(stbuf.st_size, strlen("host")); + + /* an absolute value that points back into the container */ + snprintf(path, sizeof(path), "%s/sub/back/g", base); + check_file_content(path, "in-d"); + + /* a missing leaf behind the link is still ENOENT */ + snprintf(path, sizeof(path), "%s/sub/hlink/nonexist", base); + rc = stat(path, &stbuf); + assert_int_equal(rc, -1); + assert_int_equal(errno, ENOENT); + + snprintf(path, sizeof(path), "%s/sub/back", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/absnl_root_%d", test_dir, getpid()); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/sub/hlink", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/d/g", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/d", base); + assert_return_code(rmdir(path), errno); + snprintf(path, sizeof(path), "%s/sub", base); + assert_return_code(rmdir(path), errno); + assert_return_code(rmdir(base), errno); + snprintf(path, sizeof(path), "%s/f", shadow_dir); + assert_return_code(unlink(path), errno); + assert_return_code(rmdir(shadow_dir), errno); + snprintf(path, sizeof(path), "%s/tmp", test_dir); + rmdir(path); + snprintf(path, sizeof(path), "%s/f", host_dir); + assert_return_code(unlink(path), errno); + assert_return_code(rmdir(host_dir), errno); +} + +/* A symlink whose relative value climbs more than one level with "..", as a leaf and in front of + * other components, inside the container and out of it. dfs only knows the parent of the link, so + * it cannot follow such a value and the path has to be handed to the kernel. + */ +void +do_symlink_dot_dot_value(void **state) +{ + struct stat stbuf; + char base[512]; + char parent[512]; + char name[64]; + char target[512]; + char value[128]; + char path[1024]; + char start_dir[512]; + size_t len; + int rc; + + len = snprintf(base, sizeof(base) - 1, "%s/ddv_%d", test_dir, getpid()); + assert_true(len < (sizeof(base) - 1)); + split_test_dir(parent, sizeof(parent), name, sizeof(name)); + len = snprintf(target, sizeof(target) - 1, "%s/dfuse_test_ddv_%d", parent, getpid()); + assert_true(len < (sizeof(target) - 1)); + + /* fixture: a/b, d/f, a/one_up -> ../d/f, a/b/two_up -> ../../d/f, a/b/dotdot -> .., + * a/b/two_up_dir -> ../../d, a/b/esc -> ../../../../, + * a/b/dangling -> ../../nonexist + */ + unlink(target); + write_file(target, "host"); + assert_return_code(mkdir(base, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/a", base); + assert_return_code(mkdir(path, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/a/b", base); + assert_return_code(mkdir(path, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/d", base); + assert_return_code(mkdir(path, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/d/f", base); + write_file(path, "in-d"); + snprintf(path, sizeof(path), "%s/a/one_up", base); + assert_return_code(symlink("../d/f", path), errno); + snprintf(path, sizeof(path), "%s/a/b/two_up", base); + assert_return_code(symlink("../../d/f", path), errno); + snprintf(path, sizeof(path), "%s/a/b/dotdot", base); + assert_return_code(symlink("..", path), errno); + snprintf(path, sizeof(path), "%s/a/b/two_up_dir", base); + assert_return_code(symlink("../../d", path), errno); + snprintf(path, sizeof(path), "%s/a/b/esc", base); + len = snprintf(value, sizeof(value) - 1, "../../../../dfuse_test_ddv_%d", getpid()); + assert_true(len < (sizeof(value) - 1)); + assert_return_code(symlink(value, path), errno); + snprintf(path, sizeof(path), "%s/a/b/dangling", base); + assert_return_code(symlink("../../nonexist", path), errno); + + /* leaf links */ + snprintf(path, sizeof(path), "%s/a/one_up", base); + check_file_content(path, "in-d"); + snprintf(path, sizeof(path), "%s/a/b/two_up", base); + check_file_content(path, "in-d"); + assert_return_code(stat(path, &stbuf), errno); + assert_int_equal(stbuf.st_size, strlen("in-d")); + assert_return_code(access(path, R_OK), errno); + rc = lstat(path, &stbuf); + assert_return_code(rc, errno); + assert_true(S_ISLNK(stbuf.st_mode)); + /* no inode comparison: pil4dfs fakes st_ino, the kernel that serves this link does not */ + snprintf(path, sizeof(path), "%s/a/b/dotdot", base); + assert_return_code(stat(path, &stbuf), errno); + assert_true(S_ISDIR(stbuf.st_mode)); + + /* the same in front of other components */ + snprintf(path, sizeof(path), "%s/a/b/two_up_dir/f", base); + check_file_content(path, "in-d"); + snprintf(path, sizeof(path), "%s/a/b/dotdot/one_up", base); + check_file_content(path, "in-d"); + snprintf(path, sizeof(path), "%s/a/b/two_up_dir/../d/f", base); + check_file_content(path, "in-d"); + + /* relative to a cwd next to the links */ + assert_non_null(getcwd(start_dir, sizeof(start_dir))); + snprintf(path, sizeof(path), "%s/a/b", base); + assert_return_code(chdir(path), errno); + check_file_content("two_up", "in-d"); + check_file_content("two_up_dir/f", "in-d"); + check_file_content("./dotdot/one_up", "in-d"); + assert_return_code(chdir(start_dir), errno); + + /* out of the container */ + snprintf(path, sizeof(path), "%s/a/b/esc", base); + check_file_content(path, "host"); + assert_return_code(stat(path, &stbuf), errno); + assert_int_equal(stbuf.st_size, strlen("host")); + + /* a dangling one still reports ENOENT */ + snprintf(path, sizeof(path), "%s/a/b/dangling", base); + rc = stat(path, &stbuf); + assert_int_equal(rc, -1); + assert_int_equal(errno, ENOENT); + snprintf(path, sizeof(path), "%s/a/b/dangling/x", base); + rc = stat(path, &stbuf); + assert_int_equal(rc, -1); + assert_int_equal(errno, ENOENT); + + snprintf(path, sizeof(path), "%s/a/b/dangling", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/a/b/esc", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/a/b/two_up_dir", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/a/b/dotdot", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/a/b/two_up", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/a/one_up", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/d/f", base); + assert_return_code(unlink(path), errno); + snprintf(path, sizeof(path), "%s/d", base); + assert_return_code(rmdir(path), errno); + snprintf(path, sizeof(path), "%s/a/b", base); + assert_return_code(rmdir(path), errno); + snprintf(path, sizeof(path), "%s/a", base); + assert_return_code(rmdir(path), errno); + assert_return_code(rmdir(base), errno); + assert_return_code(unlink(target), errno); +} + +/* open() with O_NOFOLLOW on a symlink fails with ELOOP whatever the link points to and wherever it + * sits; with O_PATH it yields an fd on the link itself. Without O_NOFOLLOW the link is followed. + */ +void +do_open_nofollow(void **state) +{ + struct stat stbuf; + char base[512]; + char host_file[256]; + char value[64]; + char path[1024]; + const char *links[4]; + size_t len; + int fd; + int i; + + len = snprintf(base, sizeof(base) - 1, "%s/nofollow_%d", test_dir, getpid()); + assert_true(len < (sizeof(base) - 1)); + len = snprintf(host_file, sizeof(host_file) - 1, "/tmp/dfuse_test_nofollow_%d", getpid()); + assert_true(len < (sizeof(host_file) - 1)); + + /* fixture: links in the container root and in a subdirectory, each with an absolute value + * to a host file and a relative one to a container file + */ + unlink(host_file); + write_file(host_file, "host"); + assert_return_code(mkdir(base, S_IRWXU), errno); + snprintf(path, sizeof(path), "%s/t", base); + write_file(path, "container"); + snprintf(path, sizeof(path), "%s/nofollow_root_abs_%d", test_dir, getpid()); + unlink(path); + assert_return_code(symlink(host_file, path), errno); + snprintf(path, sizeof(path), "%s/nofollow_root_rel_%d", test_dir, getpid()); + unlink(path); + len = snprintf(value, sizeof(value) - 1, "nofollow_%d/t", getpid()); + assert_true(len < (sizeof(value) - 1)); + assert_return_code(symlink(value, path), errno); + snprintf(path, sizeof(path), "%s/sub_abs", base); + assert_return_code(symlink(host_file, path), errno); + snprintf(path, sizeof(path), "%s/sub_rel", base); + assert_return_code(symlink("t", path), errno); + + links[0] = "%s/nofollow_root_abs_%d"; + links[1] = "%s/nofollow_root_rel_%d"; + links[2] = "%s/nofollow_%d/sub_abs"; + links[3] = "%s/nofollow_%d/sub_rel"; + for (i = 0; i < 4; i++) { + snprintf(path, sizeof(path), links[i], test_dir, getpid()); + + fd = open(path, O_RDONLY | O_NOFOLLOW); + assert_int_equal(fd, -1); + assert_int_equal(errno, ELOOP); + + /* as fts() and nftw() open directories: the directory check comes first */ + fd = open(path, O_RDONLY | O_NOFOLLOW | O_DIRECTORY); + assert_int_equal(fd, -1); + assert_int_equal(errno, ENOTDIR); + fd = open(path, O_PATH | O_NOFOLLOW | O_DIRECTORY); + assert_int_equal(fd, -1); + assert_int_equal(errno, ENOTDIR); + + fd = open(path, O_PATH | O_NOFOLLOW); + assert_return_code(fd, errno); + assert_return_code(fstat(fd, &stbuf), errno); + assert_true(S_ISLNK(stbuf.st_mode)); + assert_return_code(close(fd), errno); + + check_file_content(path, (i % 2 == 0) ? "host" : "container"); + } + + /* a regular file is unaffected by the flag */ + snprintf(path, sizeof(path), "%s/t", base); + fd = open(path, O_RDONLY | O_NOFOLLOW); + assert_return_code(fd, errno); + assert_return_code(close(fd), errno); + + /* O_DIRECTORY: ENOTDIR for a file, and no write access needed on the directory */ + fd = open(path, O_RDONLY | O_DIRECTORY); + assert_int_equal(fd, -1); + assert_int_equal(errno, ENOTDIR); + assert_return_code(chmod(base, S_IRUSR | S_IXUSR), errno); + fd = open(base, O_RDONLY | O_DIRECTORY); + assert_return_code(fd, errno); + assert_return_code(close(fd), errno); + assert_return_code(chmod(base, S_IRWXU), errno); + + for (i = 0; i < 4; i++) { + snprintf(path, sizeof(path), links[i], test_dir, getpid()); + assert_return_code(unlink(path), errno); + } + snprintf(path, sizeof(path), "%s/t", base); + assert_return_code(unlink(path), errno); + assert_return_code(rmdir(base), errno); + assert_return_code(unlink(host_file), errno); +} + static int run_specified_tests(const char *tests, int *sub_tests, int sub_tests_size) { @@ -989,6 +2277,7 @@ run_specified_tests(const char *tests, int *sub_tests, int sub_tests_size) printf("=====================\n"); const struct CMUnitTest metadata_tests[] = { cmocka_unit_test(do_mtime), + cmocka_unit_test(do_utimens), }; nr_failed += cmocka_run_group_tests(metadata_tests, NULL, NULL); break; @@ -1044,6 +2333,29 @@ run_specified_tests(const char *tests, int *sub_tests, int sub_tests_size) nr_failed += cmocka_run_group_tests(cache_tests, NULL, NULL); break; + case 'p': { + const struct CMUnitTest path_tests[] = { + cmocka_unit_test(do_symlink_outside), + cmocka_unit_test(do_fchdir), + cmocka_unit_test(do_chdir_fork), + cmocka_unit_test(do_dot_dot_above_root), + cmocka_unit_test(do_statx_null_probe), + cmocka_unit_test(do_path_normalize), + cmocka_unit_test(do_dot_dot_symlink), + cmocka_unit_test(do_root_symlink_shadow), + cmocka_unit_test(do_root_symlink_up), + cmocka_unit_test(do_dot_dot_mount_root), + cmocka_unit_test(do_symlink_abs_nonleaf), + cmocka_unit_test(do_symlink_dot_dot_value), + cmocka_unit_test(do_open_nofollow), + }; + printf("\n\n================="); + printf("dfuse path resolution tests"); + printf("=====================\n"); + nr_failed += cmocka_run_group_tests(path_tests, NULL, NULL); + break; + } + default: assert_true(0); } @@ -1073,9 +2385,10 @@ main(int argc, char **argv) {"exec", no_argument, NULL, 'e'}, {"verifyenv", no_argument, NULL, 't'}, {"cache", no_argument, NULL, 'c'}, + {"path", no_argument, NULL, 'p'}, {NULL, 0, NULL, 0}}; - while ((opt = getopt_long(argc, argv, "aM:imsdlfetc", long_options, &index)) != -1) { + while ((opt = getopt_long(argc, argv, "aM:imsdlfetcp", long_options, &index)) != -1) { if (strchr(all_tests, opt) != NULL) { tests[ntests] = opt; ntests++;