Split the single directory fd that chaseat() used to take into two separate fds: a root_fd that sets the chroot boundary (symlinks may not escape it, absolute symlinks resolve relative to it), and a dir_fd that path resolution starts from. This makes the chroot semantics of chaseat() explicit at every call site instead of encoding them in the CHASE_AT_RESOLVE_IN_ROOT flag, which is removed. It also decouples the starting directory from the root boundary, so callers can descend from any inode inside the tree without having to reopen the root separately. XAT_FDROOT passed as root_fd means "no containment" (host root); as dir_fd it means "start at root_fd". For a smoother transition, AT_FDCWD is also accepted as root_fd and treated as XAT_FDROOT. When root_fd points to a directory that is actually the host root, it is normalized to XAT_FDROOT up front so the existing shortcut path can kick in. Absolute paths returned by chaseat() are now relative to root_fd, and relative paths are relative to dir_fd. The result is absolute only when there is no chroot boundary (root_fd is XAT_FDROOT), or when an absolute symlink made resolution jump out of the dir_fd subtree; otherwise callers get a relative path they can feed straight back into an openat()-style call against dir_fd. Specifically, when dir_fd == root_fd and we're not operating on the host's root directory, we return a relative path even if we received an absolute path or resolved an absolute symlink to allow passing the path directly to openat() style functions. We do this to not have to go modify every caller of chaseat() to make sure they deal properly with any absolute paths they might receive. Only when root_fd != dir_fd do we have to return an absolute path to indicate that the path is relative to root_fd and not dir_fd. The shortcut that skips the per-component walk is reworked around a new chase_xopenat() helper that funnels CHASE_NOFOLLOW, CHASE_MUST_BE_* and CHASE_TRIGGER_AUTOFS through xopenat_full()'s O_NOFOLLOW, O_DIRECTORY, XO_REGULAR, XO_SOCKET and XO_TRIGGER_AUTOMOUNT flags. As a result these flags no longer force us off the shortcut and can be dropped from CHASE_NO_SHORTCUT_MASK, and the old openat_opath_with_automount() helper goes away. A CHASE_MUST_BE_ANY alias is introduced for shortcut callers (stat/access paths) that don't go through xopenat_full() and still need to bail on those flags locally. All *_and_* helpers built on top of chaseat() (chase_and_openat, chase_and_opendirat, chase_and_statat, chase_and_accessat, chase_and_fopenat_unlocked, chase_and_unlinkat, chase_and_open_parent_at) gain the same root_fd parameter, and every call site in the tree is ported to the new signature. chase_and_open() is also fixed to correctly handle CHASE_EXTRACT_FILENAME without CHASE_PARENT.
274 lines
9.6 KiB
C
274 lines
9.6 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
|
|
#include <unistd.h>
|
|
|
|
#include "alloc-util.h"
|
|
#include "btrfs.h"
|
|
#include "chase.h"
|
|
#include "errno-util.h"
|
|
#include "fd-util.h"
|
|
#include "format-util.h"
|
|
#include "fs-util.h"
|
|
#include "hashmap.h"
|
|
#include "log.h"
|
|
#include "mkdir.h"
|
|
#include "path-util.h"
|
|
#include "stat-util.h"
|
|
#include "string-util.h"
|
|
#include "time-util.h"
|
|
#include "user-util.h"
|
|
|
|
int mkdirat_safe_internal(
|
|
int dir_fd,
|
|
const char *path,
|
|
mode_t mode,
|
|
uid_t uid,
|
|
gid_t gid,
|
|
MkdirFlags flags,
|
|
mkdirat_func_t _mkdirat) {
|
|
|
|
struct stat st;
|
|
int r;
|
|
|
|
assert(path);
|
|
assert(mode != MODE_INVALID);
|
|
assert(_mkdirat && _mkdirat != mkdirat);
|
|
|
|
r = _mkdirat(dir_fd, path, mode);
|
|
if (r >= 0)
|
|
return chmod_and_chown_at(dir_fd, path, mode, uid, gid);
|
|
if (r != -EEXIST)
|
|
return r;
|
|
|
|
if (fstatat(dir_fd, path, &st, AT_SYMLINK_NOFOLLOW) < 0)
|
|
return -errno;
|
|
|
|
if ((flags & MKDIR_FOLLOW_SYMLINK) && S_ISLNK(st.st_mode)) {
|
|
_cleanup_free_ char *p = NULL;
|
|
|
|
r = chaseat(XAT_FDROOT, dir_fd, path, CHASE_NONEXISTENT, &p, NULL);
|
|
if (r < 0)
|
|
return r;
|
|
if (r == 0)
|
|
return mkdirat_safe_internal(dir_fd, p, mode, uid, gid,
|
|
flags & ~MKDIR_FOLLOW_SYMLINK,
|
|
_mkdirat);
|
|
|
|
if (fstatat(dir_fd, p, &st, AT_SYMLINK_NOFOLLOW) < 0)
|
|
return -errno;
|
|
}
|
|
|
|
if (flags & MKDIR_IGNORE_EXISTING)
|
|
return 0;
|
|
|
|
if (!S_ISDIR(st.st_mode))
|
|
return log_full_errno(flags & MKDIR_WARN_MODE ? LOG_WARNING : LOG_DEBUG, SYNTHETIC_ERRNO(ENOTDIR),
|
|
"Path \"%s\" already exists and is not a directory, refusing.", path);
|
|
|
|
if ((st.st_mode & ~mode & 0777) != 0)
|
|
return log_full_errno(flags & MKDIR_WARN_MODE ? LOG_WARNING : LOG_DEBUG, SYNTHETIC_ERRNO(EEXIST),
|
|
"Directory \"%s\" already exists, but has mode %04o that is too permissive (%04o was requested), refusing.",
|
|
path, st.st_mode & 0777, mode);
|
|
|
|
if ((uid != UID_INVALID && st.st_uid != uid) ||
|
|
(gid != GID_INVALID && st.st_gid != gid))
|
|
return log_full_errno(flags & MKDIR_WARN_MODE ? LOG_WARNING : LOG_DEBUG, SYNTHETIC_ERRNO(EEXIST),
|
|
"Directory \"%s\" already exists, but is owned by "UID_FMT":"GID_FMT" (%s:%s was requested), refusing.",
|
|
path, st.st_uid, st.st_gid, uid != UID_INVALID ? FORMAT_UID(uid) : "-",
|
|
gid != UID_INVALID ? FORMAT_GID(gid) : "-");
|
|
|
|
return 0;
|
|
}
|
|
|
|
int mkdirat_errno_wrapper(int dirfd, const char *pathname, mode_t mode) {
|
|
return RET_NERRNO(mkdirat(dirfd, pathname, mode));
|
|
}
|
|
|
|
int mkdirat_safe(int dir_fd, const char *path, mode_t mode, uid_t uid, gid_t gid, MkdirFlags flags) {
|
|
return mkdirat_safe_internal(dir_fd, path, mode, uid, gid, flags, mkdirat_errno_wrapper);
|
|
}
|
|
|
|
int mkdirat_parents_internal(int dir_fd, const char *path, mode_t mode, uid_t uid, gid_t gid, MkdirFlags flags, mkdirat_func_t _mkdirat) {
|
|
const char *e = NULL;
|
|
int r;
|
|
|
|
assert(path);
|
|
assert(_mkdirat != mkdirat);
|
|
|
|
if (isempty(path))
|
|
return 0;
|
|
|
|
if (!path_is_safe(path))
|
|
return -ENOTDIR;
|
|
|
|
/* return immediately if directory exists */
|
|
r = path_find_last_component(path, /* accept_dot_dot= */ false, &e, NULL);
|
|
if (r <= 0) /* r == 0 means path is equivalent to prefix. */
|
|
return r;
|
|
if (e == path)
|
|
return 0;
|
|
|
|
assert(e > path);
|
|
assert(*e == '/');
|
|
|
|
/* drop the last component */
|
|
path = strndupa_safe(path, e - path);
|
|
r = is_dir_at(dir_fd, path, /* follow= */ true);
|
|
if (r > 0)
|
|
return 0;
|
|
if (r == 0)
|
|
return -ENOTDIR;
|
|
|
|
/* create every parent directory in the path, except the last component */
|
|
for (const char *p = path;;) {
|
|
char *s;
|
|
int n;
|
|
|
|
n = path_find_first_component(&p, /* accept_dot_dot= */ false, (const char **) &s);
|
|
if (n <= 0)
|
|
return n;
|
|
|
|
assert(p);
|
|
assert(s >= path);
|
|
assert(IN_SET(s[n], '/', '\0'));
|
|
|
|
s[n] = '\0';
|
|
|
|
r = mkdirat_safe_internal(dir_fd, path, mode, uid, gid, flags | MKDIR_IGNORE_EXISTING, _mkdirat);
|
|
if (r < 0 && r != -EEXIST)
|
|
return r;
|
|
|
|
s[n] = *p == '\0' ? '\0' : '/';
|
|
}
|
|
}
|
|
|
|
int mkdir_parents_internal(const char *prefix, const char *path, mode_t mode, uid_t uid, gid_t gid, MkdirFlags flags, mkdirat_func_t _mkdirat) {
|
|
_cleanup_close_ int fd = AT_FDCWD;
|
|
const char *p;
|
|
|
|
assert(path);
|
|
assert(_mkdirat != mkdirat);
|
|
|
|
if (prefix) {
|
|
p = path_startswith_full(path, prefix, PATH_STARTSWITH_REFUSE_DOT_DOT);
|
|
if (!p)
|
|
return -EINVAL;
|
|
|
|
fd = open(prefix, O_PATH|O_DIRECTORY|O_CLOEXEC);
|
|
if (fd < 0)
|
|
return -errno;
|
|
} else
|
|
p = path;
|
|
|
|
return mkdirat_parents_internal(fd, p, mode, uid, gid, flags, _mkdirat);
|
|
}
|
|
|
|
int mkdirat_parents(int dir_fd, const char *path, mode_t mode) {
|
|
return mkdirat_parents_internal(dir_fd, path, mode, UID_INVALID, UID_INVALID, 0, mkdirat_errno_wrapper);
|
|
}
|
|
|
|
int mkdir_parents_safe(const char *prefix, const char *path, mode_t mode, uid_t uid, gid_t gid, MkdirFlags flags) {
|
|
return mkdir_parents_internal(prefix, path, mode, uid, gid, flags, mkdirat_errno_wrapper);
|
|
}
|
|
|
|
int mkdir_p_internal(const char *prefix, const char *path, mode_t mode, uid_t uid, gid_t gid, MkdirFlags flags, mkdirat_func_t _mkdirat) {
|
|
int r;
|
|
|
|
/* Like mkdir -p */
|
|
|
|
assert(_mkdirat != mkdirat);
|
|
|
|
r = mkdir_parents_internal(prefix, path, mode, uid, gid, flags | MKDIR_FOLLOW_SYMLINK, _mkdirat);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
if (!uid_is_valid(uid) && !gid_is_valid(gid) && flags == 0) {
|
|
r = _mkdirat(AT_FDCWD, path, mode);
|
|
if (r < 0 && (r != -EEXIST || is_dir(path, true) <= 0))
|
|
return r;
|
|
} else {
|
|
r = mkdir_safe_internal(path, mode, uid, gid, flags, _mkdirat);
|
|
if (r < 0 && r != -EEXIST)
|
|
return r;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int mkdir_p(const char *path, mode_t mode) {
|
|
return mkdir_p_internal(NULL, path, mode, UID_INVALID, UID_INVALID, 0, mkdirat_errno_wrapper);
|
|
}
|
|
|
|
int mkdir_p_safe(const char *prefix, const char *path, mode_t mode, uid_t uid, gid_t gid, MkdirFlags flags) {
|
|
return mkdir_p_internal(prefix, path, mode, uid, gid, flags, mkdirat_errno_wrapper);
|
|
}
|
|
|
|
int mkdir_p_root_full(const char *root, const char *p, uid_t uid, gid_t gid, mode_t m, usec_t ts, Hashmap *subvolumes) {
|
|
_cleanup_free_ char *pp = NULL, *bn = NULL;
|
|
_cleanup_close_ int dfd = -EBADF;
|
|
int r;
|
|
|
|
assert(p);
|
|
|
|
r = path_extract_directory(p, &pp);
|
|
if (r == -EDESTADDRREQ) {
|
|
/* only fname is passed, no prefix to operate on */
|
|
dfd = open(".", O_RDONLY|O_CLOEXEC|O_DIRECTORY);
|
|
if (dfd < 0)
|
|
return -errno;
|
|
} else if (r == -EADDRNOTAVAIL)
|
|
/* only root dir or "." was passed, i.e. there is no parent to extract, in that case there's nothing to do. */
|
|
return 0;
|
|
else if (r < 0)
|
|
return r;
|
|
else {
|
|
/* Extracting the parent dir worked, hence we aren't top-level? Recurse up first. */
|
|
r = mkdir_p_root_full(root, pp, uid, gid, m, ts, subvolumes);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
dfd = chase_and_open(pp, root, CHASE_PREFIX_ROOT, O_CLOEXEC|O_DIRECTORY, NULL);
|
|
if (dfd < 0)
|
|
return dfd;
|
|
}
|
|
|
|
r = path_extract_filename(p, &bn);
|
|
if (r == -EADDRNOTAVAIL) /* Already top-level */
|
|
return 0;
|
|
if (r < 0)
|
|
return r;
|
|
|
|
XOpenFlags flags = 0;
|
|
if (hashmap_contains(subvolumes, p)) {
|
|
flags = XO_SUBVOLUME;
|
|
if ((PTR_TO_INT(hashmap_get(subvolumes, p)) & BTRFS_SUBVOL_NODATACOW))
|
|
flags |= XO_NOCOW;
|
|
}
|
|
|
|
_cleanup_close_ int nfd = xopenat_full(
|
|
dfd, bn,
|
|
O_DIRECTORY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,
|
|
flags,
|
|
m);
|
|
if (nfd == -EEXIST)
|
|
return 0;
|
|
if (nfd < 0)
|
|
return nfd;
|
|
|
|
if ((uid_is_valid(uid) || gid_is_valid(gid)) && fchown(nfd, uid, gid) < 0)
|
|
return -errno;
|
|
|
|
if (ts != USEC_INFINITY) {
|
|
struct timespec tspec;
|
|
timespec_store(&tspec, ts);
|
|
|
|
if (futimens(dfd, (const struct timespec[2]) { TIMESPEC_OMIT, tspec }) < 0)
|
|
return -errno;
|
|
|
|
if (futimens(nfd, (const struct timespec[2]) { tspec, tspec }) < 0)
|
|
return -errno;
|
|
}
|
|
|
|
return 1;
|
|
}
|