mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-09-16 07:10:39 -04:00
syscall/util1: race-safe path resolution via a held dirfd-stack resolver
Resolve every operator/peer-reachable filesystem path one component at a time through a stack of O_NOFOLLOW-held directory fds, so a symlink swapped in mid-walk cannot redirect the operation (TOCTOU). Adds the do_*_atfd() wrappers, the held dirfd cache (held_dfd_for) and do_mkstemp_atfd() for race-safe temp-file creation, plus secure change_dir()/robust_rename() in util1.c. t_stub.c gains the matching test stubs.
This commit is contained in:
1 parent
ff544c6920
commit
49bb61b8eb
3 files changed
+764
-249
No files matched your search
@@ -45,6 +45,8 @@ extern int am_root;
|
||||
extern int am_sender;
|
||||
extern int am_daemon;
|
||||
extern int am_chrooted;
|
||||
extern int insecure_links;
|
||||
extern int module_id;
|
||||
extern int read_only;
|
||||
extern int list_only;
|
||||
extern int inplace;
|
||||
@@ -54,6 +56,10 @@ extern int preserve_executability;
|
||||
extern int open_noatime;
|
||||
extern int copy_links;
|
||||
extern int copy_unsafe_links;
|
||||
extern int operator_path_resolve; /* defined below; fwd-declared for the exclude check */
|
||||
extern unsigned int module_dirlen;
|
||||
extern char *module_dir;
|
||||
extern int module_dirfd; /* daemon: served module root pinned by identity, or -1 */
|
||||
|
||||
int secure_relpath_active(void)
|
||||
{
|
||||
@@ -63,6 +69,400 @@ int secure_relpath_active(void)
|
||||
return !am_chrooted && (am_daemon || !am_sender);
|
||||
}
|
||||
|
||||
/* Whether the operator-supplied-path symlink confinement is opted out. For a
|
||||
* non-daemon transfer this is the local --insecure-links flag. For a daemon it
|
||||
* is governed ONLY by the module's "insecure links" config (lp_insecure_links)
|
||||
* -- never by a peer-supplied --insecure-links (a client cannot disable a
|
||||
* daemon's confinement; the daemon also drops a connection that sends it). So a
|
||||
* forwarded flag is structurally inert here. */
|
||||
int symlink_optout_allowed(void)
|
||||
{
|
||||
if (am_daemon)
|
||||
return module_id >= 0 && lp_insecure_links(module_id);
|
||||
return insecure_links;
|
||||
}
|
||||
|
||||
/* Refuse (return 1) when the ABSOLUTE resolved path `abspath` lands OUTSIDE the
|
||||
* serving module's root, for an operator/peer-supplied path that must stay in the
|
||||
* module (--partial-dir/--backup-dir/alt-basis: operator_path_resolve). An
|
||||
* in-tree symlink owned by uid 0 / the euid is followed by design, so it can
|
||||
* redirect the resolved target outside the module; this catches that escape.
|
||||
*
|
||||
* This is module-ROOT confinement only. The daemon exclude/filter list is a
|
||||
* name-based visibility filter, NOT a physical-path boundary: a symlink whose own
|
||||
* name is not excluded may still resolve into an excluded IN-module subtree,
|
||||
* exactly as in stock rsync. The defense for a writable module is `munge
|
||||
* symlinks` (see rsyncd.conf(5)), not this walk. No-op unless we're a daemon. */
|
||||
static int abspath_excluded_by_module(const char *abspath, int name_is_dir)
|
||||
{
|
||||
(void)name_is_dir;
|
||||
if (!am_daemon || !abspath || !module_dir)
|
||||
return 0;
|
||||
if (module_dirlen <= 1) /* module root is "/": nothing is outside */
|
||||
return 0;
|
||||
if (strncmp(abspath, module_dir, module_dirlen) == 0
|
||||
&& (abspath[module_dirlen] == '\0' || abspath[module_dirlen] == '/'))
|
||||
return 0; /* inside the module: name-based exclude is not a boundary */
|
||||
/* Not under the module root. An ABSOLUTE walk passes through the module
|
||||
* root's ancestors ("/", "/home", ...) on the way down -- those are not
|
||||
* "outside", just not-yet-arrived, so allow them. A path that has truly
|
||||
* DIVERGED from the module tree is outside: refuse it for an operator/peer
|
||||
* path that must stay in the module (operator_path_resolve); other daemon
|
||||
* opens (--log-file, --*-from, lock/motd) may legitimately live elsewhere.
|
||||
* The --insecure-links / "insecure links = yes" opt-out short-circuits
|
||||
* before we get here. */
|
||||
size_t alen = strlen(abspath);
|
||||
if (alen == 0
|
||||
|| (strncmp(abspath, module_dir, alen) == 0 && module_dir[alen] == '/'))
|
||||
return 0; /* ancestor of the module root: still descending */
|
||||
return operator_path_resolve ? 1 : 0;
|
||||
}
|
||||
|
||||
/* Advance the tracked absolute path `abspath` by one resolved component,
|
||||
* normalizing "." and ".." exactly as openat() does so the exclude check sees
|
||||
* the REAL target (a symlink target like "tmp/../secret" must not slip past a
|
||||
* "/secret/" rule because the literal string differs). -1/ENAMETOOLONG on
|
||||
* overflow. */
|
||||
static int abspath_step(char *abspath, size_t cap, const char *comp, size_t comp_len)
|
||||
{
|
||||
if (comp_len == 1 && comp[0] == '.')
|
||||
return 0; /* "." -- no movement */
|
||||
if (comp_len == 2 && comp[0] == '.' && comp[1] == '.') {
|
||||
char *s = strrchr(abspath, '/'); /* ".." -- pop a component */
|
||||
if (s)
|
||||
*s = '\0';
|
||||
else
|
||||
abspath[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
size_t al = strlen(abspath);
|
||||
size_t off = (al > 0 && abspath[al-1] == '/') ? al : al + 1; /* no "//" */
|
||||
if (off + comp_len >= cap) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
if (off != al)
|
||||
abspath[al] = '/';
|
||||
memcpy(abspath + off, comp, comp_len + 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Open an operator-supplied path, refusing to traverse any symlink (parent or
|
||||
* leaf) not owned by uid 0 or our euid. A trusted-owned symlink (e.g. root's
|
||||
* /var/log -> /data/log) is still followed; an untrusted one fails ELOOP.
|
||||
* Unlike plain O_NOFOLLOW this also defends a planted parent component
|
||||
* (--log-file=$plant/log), not just a planted leaf. Used for opens that may
|
||||
* transit attacker-writable parents: --log-file, --password-file, --*-from,
|
||||
* --read/write-batch, daemon motd/lock/early-input/--config.
|
||||
*
|
||||
* Walks component-by-component with fstatat(AT_SYMLINK_NOFOLLOW) +
|
||||
* openat(O_NOFOLLOW), splicing a trusted symlink's target back into the path.
|
||||
* Returns the fd, or -1 (errno ELOOP on the security refusal so callers can
|
||||
* tell it apart). Falls back to plain open() where openat/O_NOFOLLOW are
|
||||
* unavailable. */
|
||||
/* Core walk. When out_abs is non-NULL and the path resolves to a directory
|
||||
* (O_DIRECTORY), the resolved absolute path is copied there -- owner_walk_parent
|
||||
* uses it to filter-check the (otherwise unchecked) leaf basename. */
|
||||
static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, size_t out_cap)
|
||||
{
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW
|
||||
/* O_CLOEXEC predates some still-supported targets; mirror rand_bytes()'s
|
||||
* fallback in syscall.c so a build without it still compiles. */
|
||||
#ifndef O_CLOEXEC
|
||||
#define O_CLOEXEC 0
|
||||
#endif
|
||||
if (!path || !*path) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Opted out (local --insecure-links, or a daemon module with "insecure
|
||||
* links = yes"): restore the legacy symlink-following open. */
|
||||
if (symlink_optout_allowed())
|
||||
return open(path, flags, mode);
|
||||
|
||||
const uid_t trusted_uid = geteuid();
|
||||
int dfd = AT_FDCWD;
|
||||
int dfd_owns = 0;
|
||||
|
||||
/* Absolute module-relative path of the current dir, for the exclude-aware
|
||||
* refusal (abspath_excluded_by_module). A relative operator path starts at
|
||||
* the daemon's cwd == the module root; an absolute one (or a followed
|
||||
* absolute symlink target) restarts at "/". */
|
||||
char abspath[MAXPATHLEN];
|
||||
abspath[0] = '\0';
|
||||
if (am_daemon && module_dir && module_dir[0] == '/')
|
||||
strlcpy(abspath, module_dir, sizeof abspath); /* "/" for a path=/ module */
|
||||
|
||||
/* Path-walk state. `remaining` is the unconsumed tail; we splice
|
||||
* symlink targets back into it as we go. Sized 2x MAXPATHLEN so a
|
||||
* one-level expansion can't immediately overflow; deeper chains
|
||||
* fail with ENAMETOOLONG below. */
|
||||
char remaining[MAXPATHLEN * 2];
|
||||
if (strlcpy(remaining, path, sizeof remaining) >= sizeof remaining) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Absolute path: pin "/" as the starting dfd. */
|
||||
if (remaining[0] == '/') {
|
||||
dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
dfd_owns = 1;
|
||||
abspath[0] = '\0'; /* now resolving from "/" */
|
||||
char *p = remaining;
|
||||
while (*p == '/') p++;
|
||||
memmove(remaining, p, strlen(p) + 1);
|
||||
}
|
||||
|
||||
int loops = 40; /* SYMLOOP_MAX-ish; breaks symlink cycles. Counts symlink
|
||||
* expansions only (below), NOT path depth -- a deep but
|
||||
* symlink-free path must resolve, not ELOOP. */
|
||||
int retfd = -1;
|
||||
int saved_errno = 0;
|
||||
|
||||
while (*remaining) {
|
||||
/* Peel one component off the front of `remaining`. */
|
||||
char *slash = strchr(remaining, '/');
|
||||
size_t comp_len = slash ? (size_t)(slash - remaining) : strlen(remaining);
|
||||
char comp[MAXPATHLEN];
|
||||
if (comp_len == 0 || comp_len >= sizeof comp) {
|
||||
saved_errno = comp_len == 0 ? EINVAL : ENAMETOOLONG;
|
||||
goto out;
|
||||
}
|
||||
memcpy(comp, remaining, comp_len);
|
||||
comp[comp_len] = '\0';
|
||||
int is_last = (slash == NULL);
|
||||
|
||||
/* Inspect this component without following symlinks. */
|
||||
STRUCT_STAT lst;
|
||||
if (fstatat(dfd, comp, &lst, AT_SYMLINK_NOFOLLOW) < 0) {
|
||||
/* The leaf may not exist yet (O_CREAT case). Allow it
|
||||
* and openat with O_NOFOLLOW so a race-planted leaf
|
||||
* symlink at this instant is still refused. */
|
||||
if (is_last && errno == ENOENT && (flags & O_CREAT)) {
|
||||
if (abspath_step(abspath, sizeof abspath, comp, comp_len) < 0) {
|
||||
saved_errno = errno;
|
||||
goto out;
|
||||
}
|
||||
if (abspath_excluded_by_module(abspath, 0)) {
|
||||
saved_errno = ELOOP;
|
||||
goto out;
|
||||
}
|
||||
retfd = openat(dfd, comp, flags | O_NOFOLLOW, mode);
|
||||
saved_errno = errno;
|
||||
goto out;
|
||||
}
|
||||
saved_errno = errno;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (S_ISLNK(lst.st_mode)) {
|
||||
/* Symlink: untrusted owner is refused; trusted owner
|
||||
* is followed via readlinkat + splice. */
|
||||
if (lst.st_uid != 0 && lst.st_uid != trusted_uid) {
|
||||
saved_errno = ELOOP;
|
||||
goto out;
|
||||
}
|
||||
if (--loops < 0) { /* cap symlink-follow chains */
|
||||
saved_errno = ELOOP;
|
||||
goto out;
|
||||
}
|
||||
char target[MAXPATHLEN];
|
||||
ssize_t n = readlinkat(dfd, comp, target, sizeof target - 1);
|
||||
if (n < 0) {
|
||||
saved_errno = errno;
|
||||
goto out;
|
||||
}
|
||||
target[n] = '\0';
|
||||
|
||||
/* Splice: new `remaining` = <target> + <tail-after-comp>.
|
||||
* Absolute target restarts the walk from "/". */
|
||||
char tail[MAXPATHLEN];
|
||||
tail[0] = '\0';
|
||||
if (slash)
|
||||
strlcpy(tail, slash, sizeof tail);
|
||||
|
||||
char rebuilt[MAXPATHLEN * 2];
|
||||
if (snprintf(rebuilt, sizeof rebuilt, "%s%s",
|
||||
target, tail) >= (int)sizeof rebuilt) {
|
||||
saved_errno = ENAMETOOLONG;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (target[0] == '/') {
|
||||
if (dfd_owns) close(dfd);
|
||||
dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (dfd < 0) {
|
||||
saved_errno = errno;
|
||||
dfd_owns = 0;
|
||||
goto out;
|
||||
}
|
||||
dfd_owns = 1;
|
||||
abspath[0] = '\0'; /* followed an absolute target: restart from "/" */
|
||||
char *p = rebuilt;
|
||||
while (*p == '/') p++;
|
||||
strlcpy(remaining, p, sizeof remaining);
|
||||
} else {
|
||||
strlcpy(remaining, rebuilt, sizeof remaining);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Non-symlink. */
|
||||
if (is_last) {
|
||||
if (abspath_step(abspath, sizeof abspath, comp, comp_len) < 0) {
|
||||
saved_errno = errno;
|
||||
goto out;
|
||||
}
|
||||
if (abspath_excluded_by_module(abspath, S_ISDIR(lst.st_mode))) {
|
||||
saved_errno = ELOOP;
|
||||
goto out;
|
||||
}
|
||||
retfd = openat(dfd, comp, flags | O_NOFOLLOW, mode);
|
||||
saved_errno = errno;
|
||||
/* Resolved leaf dir (O_DIRECTORY): hand its path back so
|
||||
* owner_walk_parent can filter-check the operation's leaf. */
|
||||
if (retfd >= 0 && out_abs && out_cap)
|
||||
/* Root-resolved (".." popped abspath empty) tracked daemon walk:
|
||||
* hand back "/" so owner_walk_parent still leaf-checks (path=/ bypass). */
|
||||
strlcpy(out_abs, (am_daemon && !abspath[0]) ? "/" : abspath, out_cap);
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (!S_ISDIR(lst.st_mode)) {
|
||||
saved_errno = ENOTDIR;
|
||||
goto out;
|
||||
}
|
||||
/* exclude-aware: refuse descending into a module-hidden dir */
|
||||
if (abspath_step(abspath, sizeof abspath, comp, comp_len) < 0) {
|
||||
saved_errno = errno;
|
||||
goto out;
|
||||
}
|
||||
if (abspath_excluded_by_module(abspath, 1)) {
|
||||
saved_errno = ELOOP;
|
||||
goto out;
|
||||
}
|
||||
int next = openat(dfd, comp, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
if (next < 0) {
|
||||
saved_errno = errno;
|
||||
goto out;
|
||||
}
|
||||
if (dfd_owns) close(dfd);
|
||||
dfd = next;
|
||||
dfd_owns = 1;
|
||||
|
||||
/* Advance `remaining` past this component (and the slash). */
|
||||
if (slash) {
|
||||
char *p = slash;
|
||||
while (*p == '/') p++;
|
||||
memmove(remaining, p, strlen(p) + 1);
|
||||
} else {
|
||||
remaining[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/* Path resolved entirely to a directory (no leaf component left).
|
||||
* If the caller wanted O_DIRECTORY we already hold the dirfd we
|
||||
* built up; otherwise it's an EISDIR. */
|
||||
if (flags & O_DIRECTORY) {
|
||||
retfd = dfd;
|
||||
dfd_owns = 0; /* caller now owns it */
|
||||
saved_errno = 0;
|
||||
if (out_abs && out_cap)
|
||||
/* Root-resolved (".." popped abspath empty) tracked daemon walk:
|
||||
* hand back "/" so owner_walk_parent still leaf-checks (path=/ bypass). */
|
||||
strlcpy(out_abs, (am_daemon && !abspath[0]) ? "/" : abspath, out_cap);
|
||||
} else {
|
||||
saved_errno = EISDIR;
|
||||
}
|
||||
|
||||
out:
|
||||
if (dfd_owns) close(dfd);
|
||||
errno = saved_errno;
|
||||
return retfd;
|
||||
#else
|
||||
/* Pre-AT_FDCWD / no O_NOFOLLOW systems: best-effort fallback. */
|
||||
(void)out_abs; (void)out_cap;
|
||||
return open(path, flags, mode);
|
||||
#endif
|
||||
}
|
||||
|
||||
int open_no_attacker_symlinks(const char *path, int flags, mode_t mode)
|
||||
{
|
||||
return ona_open(path, flags, mode, NULL, 0);
|
||||
}
|
||||
|
||||
/* When set, the do_*_at() wrappers resolve their path as an OPERATOR-supplied
|
||||
* directory path (an absolute or relative --backup-dir/--temp-dir/--*-dest)
|
||||
* using the ownership walk -- follow a symlink owned by uid 0 or our euid,
|
||||
* refuse any other-uid one, at every component -- instead of the stricter
|
||||
* transfer-path resolver (which refuses all symlinks and is confined beneath the
|
||||
* transfer root). An operator path may legitimately point outside the tree, so
|
||||
* the trust signal is authority (ownership), not location. Set around the
|
||||
* relevant ops by backup.c et al.; the opt-out (--insecure-links / "insecure
|
||||
* links =") restores legacy following. Default 0 (transfer-path resolver). */
|
||||
int operator_path_resolve = 0;
|
||||
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
/* For an operator-supplied path: open its parent directory via the ownership
|
||||
* walk (handles absolute and relative paths) and point *bname at the final
|
||||
* component. Returns the dirfd (caller closes) or -1 with errno set. */
|
||||
int owner_walk_parent(const char *path, const char **bname)
|
||||
{
|
||||
const char *slash = strrchr(path, '/');
|
||||
char dir[MAXPATHLEN], pabs[MAXPATHLEN];
|
||||
size_t dlen;
|
||||
int dfd;
|
||||
|
||||
*bname = slash ? slash + 1 : path;
|
||||
pabs[0] = '\0';
|
||||
if (!slash)
|
||||
dfd = ona_open(".", O_RDONLY | O_DIRECTORY, 0, pabs, sizeof pabs);
|
||||
else {
|
||||
dlen = slash == path ? 1 : (size_t)(slash - path); /* "/x" -> parent "/" */
|
||||
if (dlen >= sizeof dir) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
memcpy(dir, path, dlen);
|
||||
dir[dlen] = '\0';
|
||||
dfd = ona_open(dir, O_RDONLY | O_DIRECTORY, 0, pabs, sizeof pabs);
|
||||
}
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
/* owner_walk only resolved the PARENT; the leaf basename has not been
|
||||
* filter-checked, so a symlinked operator path can act on a module-excluded
|
||||
* (or out-of-module) leaf in an otherwise-served dir. Filter-check the
|
||||
* resolved leaf and refuse it. */
|
||||
if (pabs[0]) {
|
||||
char leafabs[MAXPATHLEN];
|
||||
STRUCT_STAT lst;
|
||||
int isdir = 0, absent = 0, refuse;
|
||||
if (fstatat(dfd, *bname, &lst, AT_SYMLINK_NOFOLLOW) == 0)
|
||||
isdir = S_ISDIR(lst.st_mode);
|
||||
else
|
||||
absent = 1; /* mkdir/rename target: type unknown yet */
|
||||
if (snprintf(leafabs, sizeof leafabs, "%s/%s", pabs, *bname) >= (int)sizeof leafabs) {
|
||||
close(dfd);
|
||||
errno = ENAMETOOLONG; /* fail closed, never skip the check */
|
||||
return -1;
|
||||
}
|
||||
/* For an absent leaf the op may create a dir, so also test dir-only
|
||||
* filter rules (a "/foo/" rule never matches a file). */
|
||||
refuse = abspath_excluded_by_module(leafabs, isdir)
|
||||
|| (absent && abspath_excluded_by_module(leafabs, 1));
|
||||
if (refuse) {
|
||||
close(dfd);
|
||||
errno = ELOOP;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return dfd;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#ifndef S_BLKSIZE
|
||||
# if defined hpux || defined __hpux__ || defined __hpux
|
||||
@@ -128,6 +528,21 @@ int do_unlink_at(const char *path)
|
||||
if (dry_run) return 0;
|
||||
RETURN_ERROR_IF_RO_OR_LO;
|
||||
|
||||
#if defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
if (operator_path_resolve) {
|
||||
if (symlink_optout_allowed())
|
||||
return unlink(path);
|
||||
dfd = owner_walk_parent(path, &bname);
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
ret = unlinkat(dfd, bname, 0);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!secure_relpath_active())
|
||||
return unlink(path);
|
||||
|
||||
@@ -378,6 +793,31 @@ int do_link_at(const char *old_path, const char *new_path)
|
||||
if (!old_path || !*old_path || !new_path || !*new_path)
|
||||
return do_link(old_path, new_path);
|
||||
|
||||
#if defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
/* Operator-supplied path (a --backup-dir/--link-dest side): resolve each
|
||||
* parent via the ownership walk (follow uid0/euid symlinks, refuse others). */
|
||||
if (operator_path_resolve) {
|
||||
if (symlink_optout_allowed())
|
||||
return do_link(old_path, new_path);
|
||||
old_dfd = owner_walk_parent(old_path, &old_bname);
|
||||
if (old_dfd < 0)
|
||||
return -1;
|
||||
new_dfd = owner_walk_parent(new_path, &new_bname);
|
||||
if (new_dfd < 0) {
|
||||
e = errno;
|
||||
close(old_dfd);
|
||||
errno = e;
|
||||
return -1;
|
||||
}
|
||||
ret = linkat(old_dfd, old_bname, new_dfd, new_bname, 0);
|
||||
e = errno;
|
||||
close(new_dfd);
|
||||
close(old_dfd);
|
||||
errno = e;
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
old_slash = strrchr(old_path, '/');
|
||||
new_slash = strrchr(new_path, '/');
|
||||
|
||||
@@ -386,7 +826,10 @@ int do_link_at(const char *old_path, const char *new_path)
|
||||
* with a slash needs secure_relative_open to confine its parent
|
||||
* resolution -- otherwise a parent symlink (e.g. --link-dest=cd
|
||||
* where cd -> /outside) lets the kernel-level linkat(AT_FDCWD,
|
||||
* "cd/target.txt", ...) escape the module. */
|
||||
* "cd/target.txt", ...) escape the module. An absolute path uses
|
||||
* AT_FDCWD + the full path; each side is confined independently, so an
|
||||
* absolute source (e.g. an absolute --link-dest) cannot disable
|
||||
* confinement of a relative destination. */
|
||||
if (*old_path == '/') {
|
||||
old_bname = old_path;
|
||||
} else if (old_slash) {
|
||||
@@ -604,6 +1047,21 @@ int do_mknod_at(const char *pathname, mode_t mode, dev_t dev)
|
||||
if (dry_run) return 0;
|
||||
RETURN_ERROR_IF_RO_OR_LO;
|
||||
|
||||
#if defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
if (operator_path_resolve) {
|
||||
if (symlink_optout_allowed())
|
||||
return do_mknod(pathname, mode, dev);
|
||||
dfd = owner_walk_parent(pathname, &bname);
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
ret = mknodat(dfd, bname, mode, dev);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!secure_relpath_active())
|
||||
return do_mknod(pathname, mode, dev);
|
||||
|
||||
@@ -780,6 +1238,21 @@ int do_open_at(const char *pathname, int flags, mode_t mode)
|
||||
RETURN_ERROR_IF_RO_OR_LO;
|
||||
}
|
||||
|
||||
#if defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
if (operator_path_resolve) {
|
||||
if (symlink_optout_allowed())
|
||||
return do_open(pathname, flags, mode);
|
||||
dfd = owner_walk_parent(pathname, &bname);
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
ret = openat(dfd, bname, flags | O_NOFOLLOW, mode);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!secure_relpath_active())
|
||||
return do_open(pathname, flags, mode);
|
||||
|
||||
@@ -924,7 +1397,9 @@ static int do_fchmodat_nofollow(int dfd, const char *name, mode_t mode)
|
||||
return fchmodat(dfd, name, mode, AT_SYMLINK_NOFOLLOW);
|
||||
# endif
|
||||
#else
|
||||
return fchmodat(dfd, name, mode, 0);
|
||||
/* No symlink-safe chmod primitive here: skip rather than follow the leaf. */
|
||||
rprintf(FWARNING, "do_chmod: no symlink-safe chmod for \"%s\"; mode not set\n", name);
|
||||
return 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1052,9 +1527,41 @@ int do_rename_at(const char *old_path, const char *new_path)
|
||||
if (!old_path || !*old_path || !new_path || !*new_path)
|
||||
return do_rename(old_path, new_path);
|
||||
|
||||
#if defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
/* Operator-supplied path (e.g. a --backup-dir destination or a --temp-dir
|
||||
* source): resolve each side's parent via the ownership walk (follow
|
||||
* uid0/euid symlinks, refuse others; absolute and relative alike). */
|
||||
if (operator_path_resolve) {
|
||||
if (symlink_optout_allowed())
|
||||
return do_rename(old_path, new_path);
|
||||
old_dfd = owner_walk_parent(old_path, &old_bname);
|
||||
if (old_dfd < 0)
|
||||
return -1;
|
||||
new_dfd = owner_walk_parent(new_path, &new_bname);
|
||||
if (new_dfd < 0) {
|
||||
e = errno;
|
||||
close(old_dfd);
|
||||
errno = e;
|
||||
return -1;
|
||||
}
|
||||
ret = renameat(old_dfd, old_bname, new_dfd, new_bname);
|
||||
e = errno;
|
||||
close(new_dfd);
|
||||
close(old_dfd);
|
||||
errno = e;
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
old_slash = strrchr(old_path, '/');
|
||||
new_slash = strrchr(new_path, '/');
|
||||
|
||||
/* An absolute path uses AT_FDCWD with the full path; only a *relative* side
|
||||
* is confined under the secure resolver. Confine each side independently:
|
||||
* an absolute source (e.g. an absolute --temp-dir temp file) must NOT
|
||||
* disable confinement of a relative destination, or finish_transfer's
|
||||
* tmp->final rename re-resolves the dest from the path and a flipped parent
|
||||
* symlink writes the file outside the tree (a symlink-race write escape). */
|
||||
if (*old_path == '/') {
|
||||
old_bname = old_path;
|
||||
} else if (old_slash) {
|
||||
@@ -1186,6 +1693,21 @@ int do_mkdir_at(char *path, mode_t mode)
|
||||
RETURN_ERROR_IF_RO_OR_LO;
|
||||
trim_trailing_slashes(path);
|
||||
|
||||
#if defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
if (operator_path_resolve) {
|
||||
if (symlink_optout_allowed())
|
||||
return mkdir(path, mode);
|
||||
dfd = owner_walk_parent(path, &bname);
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
ret = mkdirat(dfd, bname, mode);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!secure_relpath_active())
|
||||
return mkdir(path, mode);
|
||||
|
||||
@@ -1295,6 +1817,21 @@ static int do_xstat_at(const char *path, STRUCT_STAT *st, int at_flags, int (*fa
|
||||
int dfd, ret, e;
|
||||
size_t dlen;
|
||||
|
||||
#if defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
if (operator_path_resolve) {
|
||||
if (symlink_optout_allowed())
|
||||
return fallback(path, st);
|
||||
dfd = owner_walk_parent(path, &bname);
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
ret = fstatat(dfd, bname, st, at_flags);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!secure_relpath_active())
|
||||
return fallback(path, st);
|
||||
|
||||
@@ -1883,31 +2420,65 @@ unsigned int curr_dir_len;
|
||||
#define SECURE_OPEN_MAXSYMLINKS 40
|
||||
#endif
|
||||
|
||||
/* Max directory levels held open at once during a single resolve. The walk
|
||||
* holds one fd per component, so depth is bounded by RLIMIT_NOFILE anyway; a
|
||||
* fixed array (no malloc/realloc) keeps the stack simple and the static
|
||||
* analyzer happy. Mirrors DPC_MAXDEPTH's fixed-cap approach. */
|
||||
#define DS_MAXDEPTH 1024
|
||||
|
||||
struct dirstack {
|
||||
int *fds; /* fds[0] = anchor (borrowed); fds[top] = current dir */
|
||||
int fds[DS_MAXDEPTH]; /* fds[0] = anchor (borrowed); fds[top] = current dir */
|
||||
int top;
|
||||
int cap;
|
||||
/* Absolute path of fds[top], maintained as we descend/pop, for the
|
||||
* exclude-aware refusal (abspath_excluded_by_module). Empty unless the
|
||||
* caller seeds it with the anchor's absolute path; then a followed symlink
|
||||
* that redirects the walk into a module-excluded dir is refused. */
|
||||
char abspath[MAXPATHLEN];
|
||||
};
|
||||
|
||||
/* Initialise with `anchor` (which may be AT_FDCWD) as the un-owned base. */
|
||||
/* Append "/comp" to ds->abspath (no-op if it's unseeded/empty so non-daemon
|
||||
* callers pay nothing). Returns -1 (ENAMETOOLONG) on overflow. */
|
||||
static int ds_path_push(struct dirstack *ds, const char *comp)
|
||||
{
|
||||
size_t al = strlen(ds->abspath);
|
||||
if (al == 0)
|
||||
return 0; /* unseeded: tracking disabled for this walk */
|
||||
size_t cl = strlen(comp);
|
||||
if (al + 1 + cl >= sizeof ds->abspath) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
ds->abspath[al] = '/';
|
||||
memcpy(ds->abspath + al + 1, comp, cl + 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Drop the last component of ds->abspath (mirrors a ".." pop). */
|
||||
static void ds_path_pop(struct dirstack *ds)
|
||||
{
|
||||
char *slash;
|
||||
if (!ds->abspath[0])
|
||||
return;
|
||||
slash = strrchr(ds->abspath, '/');
|
||||
if (slash && slash != ds->abspath)
|
||||
*slash = '\0';
|
||||
}
|
||||
|
||||
/* Initialise with `anchor` (which may be AT_FDCWD) as the un-owned base.
|
||||
* Returns int for caller symmetry, but cannot fail (the fd array is inline). */
|
||||
static int ds_init(struct dirstack *ds, int anchor)
|
||||
{
|
||||
ds->cap = 16;
|
||||
ds->fds = (int*)malloc(ds->cap * sizeof(int));
|
||||
if (!ds->fds)
|
||||
return -1;
|
||||
ds->abspath[0] = '\0';
|
||||
ds->fds[0] = anchor;
|
||||
ds->top = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Close every pushed fd (but not the borrowed anchor at index 0) and free. */
|
||||
/* Close every pushed fd (but not the borrowed anchor at index 0). */
|
||||
static void ds_free(struct dirstack *ds)
|
||||
{
|
||||
while (ds->top > 0)
|
||||
close(ds->fds[ds->top--]);
|
||||
free(ds->fds);
|
||||
ds->fds = NULL;
|
||||
}
|
||||
|
||||
static int ds_cur(struct dirstack *ds)
|
||||
@@ -1917,16 +2488,10 @@ static int ds_cur(struct dirstack *ds)
|
||||
|
||||
static int ds_push(struct dirstack *ds, int fd)
|
||||
{
|
||||
if (ds->top + 1 >= ds->cap) {
|
||||
int ncap = ds->cap * 2;
|
||||
int *n = (int*)realloc(ds->fds, ncap * sizeof(int));
|
||||
if (!n) {
|
||||
close(fd);
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
ds->fds = n;
|
||||
ds->cap = ncap;
|
||||
if (ds->top + 1 >= DS_MAXDEPTH) { /* deeper than we'll hold open */
|
||||
close(fd);
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
ds->fds[++ds->top] = fd;
|
||||
return 0;
|
||||
@@ -1959,12 +2524,24 @@ static int ds_descend(struct dirstack *ds, const char *part, int *hops)
|
||||
return -1;
|
||||
}
|
||||
close(ds->fds[ds->top--]); /* pop to the held parent fd */
|
||||
ds_path_pop(ds);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fd = openat(ds_cur(ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
|
||||
if (fd != -1)
|
||||
return ds_push(ds, fd); /* a real subdirectory */
|
||||
if (fd != -1) { /* a real subdirectory */
|
||||
if (ds_push(ds, fd) < 0)
|
||||
return -1;
|
||||
if (ds_path_push(ds, part) < 0)
|
||||
return -1;
|
||||
/* exclude-aware: refuse descending into a module-hidden dir (catches a
|
||||
* symlink that redirected the walk into an excluded subtree). */
|
||||
if (abspath_excluded_by_module(ds->abspath, 1)) {
|
||||
errno = ELOOP;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/* O_NOFOLLOW refused a symlink (NOFOLLOW_HIT_SYMLINK: ELOOP on Linux, EMLINK
|
||||
* on FreeBSD, EFTYPE on NetBSD/OpenBSD), or O_DIRECTORY hit a non-directory
|
||||
* (ENOTDIR). Either may be a symlink, so fall through to the readlink probe;
|
||||
@@ -2027,7 +2604,8 @@ static int ds_walk_path(struct dirstack *ds, char *path, int *hops)
|
||||
* the caller owns it. Shared by secure_relative_open() (which first resolves a
|
||||
* basedir to the anchor) and secure_relative_open_at() (handed an already-open
|
||||
* anchor, e.g. a held module-root fd). `hops` is the shared symlink-hop budget. */
|
||||
static int secure_walk_at(int anchor_fd, const char *relpath, int flags, mode_t mode, int *hops)
|
||||
static int secure_walk_at(int anchor_fd, const char *anchor_abspath,
|
||||
const char *relpath, int flags, mode_t mode, int *hops)
|
||||
{
|
||||
struct dirstack ds;
|
||||
int retfd = -1;
|
||||
@@ -2035,6 +2613,10 @@ static int secure_walk_at(int anchor_fd, const char *relpath, int flags, mode_t
|
||||
|
||||
if (ds_init(&ds, anchor_fd) < 0)
|
||||
return -1;
|
||||
/* Seed the abspath tracker so the exclude-aware refusal can map a resolved
|
||||
* path back to module-relative. Only an absolute anchor enables it. */
|
||||
if (anchor_abspath && anchor_abspath[0] == '/')
|
||||
strlcpy(ds.abspath, anchor_abspath, sizeof ds.abspath);
|
||||
path_copy = my_strdup(relpath, __FILE__, __LINE__);
|
||||
if (!path_copy) {
|
||||
ds_free(&ds);
|
||||
@@ -2061,6 +2643,15 @@ static int secure_walk_at(int anchor_fd, const char *relpath, int flags, mode_t
|
||||
/* File leaf (final component, caller did not ask for O_DIRECTORY):
|
||||
* never follow a symlink leaf. */
|
||||
if (is_last && !(flags & O_DIRECTORY)) {
|
||||
if (ds.abspath[0]) {
|
||||
char leafabs[MAXPATHLEN];
|
||||
if (snprintf(leafabs, sizeof leafabs, "%s/%s", ds.abspath, part)
|
||||
< (int)sizeof leafabs
|
||||
&& abspath_excluded_by_module(leafabs, 0)) {
|
||||
errno = ELOOP;
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
int next_fd = openat(ds_cur(&ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
|
||||
if (next_fd == -1 && (errno == ENOTDIR || errno == ENOENT)) {
|
||||
retfd = openat(ds_cur(&ds), part, flags | O_NOFOLLOW, mode);
|
||||
@@ -2237,7 +2828,13 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo
|
||||
}
|
||||
}
|
||||
|
||||
int retfd = secure_walk_at(dirfd, relpath, flags, mode, &hops);
|
||||
/* Absolute path of the anchor, for the exclude-aware refusal: the cwd (==
|
||||
* module root for a daemon) when AT_FDCWD, or an operator-trusted absolute
|
||||
* basedir. A relative basedir's resolved abspath isn't tracked, so leave it
|
||||
* unseeded (the refusal is then a no-op for that uncommon case). */
|
||||
const char *anchor_abspath = !basedir ? curr_dir
|
||||
: (basedir[0] == '/' ? basedir : NULL);
|
||||
int retfd = secure_walk_at(dirfd, anchor_abspath, relpath, flags, mode, &hops);
|
||||
if (dirfd != AT_FDCWD)
|
||||
close(dirfd);
|
||||
return retfd;
|
||||
@@ -2272,7 +2869,10 @@ int secure_relative_open_at(int anchor_fd, const char *relpath, int flags, mode_
|
||||
if (open_noatime)
|
||||
flags |= O_NOATIME;
|
||||
#endif
|
||||
return secure_walk_at(anchor_fd, relpath, flags, mode, &hops);
|
||||
/* The anchor fd's absolute path isn't known here (it may be a held module
|
||||
* root or a climbed-to dir), so leave the abspath tracker unseeded; the
|
||||
* exclude-aware refusal is a no-op for this entry point. */
|
||||
return secure_walk_at(anchor_fd, NULL, relpath, flags, mode, &hops);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2297,6 +2897,62 @@ static void rand_bytes(unsigned char *buf, size_t len)
|
||||
}
|
||||
}
|
||||
|
||||
/* mkstemp against an already-resolved held dir fd: `filename` is a single leaf
|
||||
ending in "XXXXXX". The dirfd has already confined (and followed in-tree
|
||||
symlinks for) the parent, so we just randomize the leaf and O_NOFOLLOW|O_EXCL
|
||||
create it. Returns fd on success, -1 on error. */
|
||||
int do_mkstemp_atfd(int dfd, char *filename, mode_t perms)
|
||||
{
|
||||
#ifdef AT_FDCWD
|
||||
static const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
size_t filename_len = strlen(filename);
|
||||
char *suffix;
|
||||
int fd = -1;
|
||||
|
||||
if (filename_len < 6) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
suffix = filename + filename_len - 6; /* Points to XXXXXX */
|
||||
if (strcmp(suffix, "XXXXXX") != 0) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
perms |= S_IWUSR;
|
||||
for (int tries = 0; tries < 100; tries++) {
|
||||
unsigned char rbytes[6];
|
||||
rand_bytes(rbytes, sizeof(rbytes));
|
||||
for (int i = 0; i < 6; i++)
|
||||
suffix[i] = letters[rbytes[i] % (sizeof(letters) - 1)];
|
||||
|
||||
fd = openat(dfd, filename, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, perms);
|
||||
if (fd >= 0)
|
||||
break;
|
||||
if (errno != EEXIST)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fd >= 0) {
|
||||
if (fchmod(fd, perms) != 0 && preserve_perms) {
|
||||
int errno_save = errno;
|
||||
close(fd);
|
||||
unlinkat(dfd, filename, 0);
|
||||
errno = errno_save;
|
||||
return -1;
|
||||
}
|
||||
#if defined HAVE_SETMODE && O_BINARY
|
||||
setmode(fd, O_BINARY);
|
||||
#endif
|
||||
}
|
||||
return fd;
|
||||
#else
|
||||
(void)dfd; (void)filename; (void)perms;
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
Secure version of mkstemp that prevents symlink attacks on parent directories.
|
||||
Like secure_relative_open(), this walks the path checking each component
|
||||
@@ -2305,10 +2961,11 @@ static void rand_bytes(unsigned char *buf, size_t len)
|
||||
The template may be relative or absolute, but must not contain ../ components.
|
||||
Returns fd on success, -1 on error.
|
||||
*/
|
||||
int secure_mkstemp(char *template, mode_t perms)
|
||||
int secure_mkstemp(char *template, mode_t perms, int operator_path)
|
||||
{
|
||||
#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)
|
||||
/* Fall back to regular mkstemp on old systems */
|
||||
(void)operator_path;
|
||||
return do_mkstemp(template, perms);
|
||||
#else
|
||||
char *lastslash;
|
||||
@@ -2324,40 +2981,69 @@ int secure_mkstemp(char *template, mode_t perms)
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* For absolute paths, start the secure walk from "/" rather than CWD. */
|
||||
if (template[0] == '/') {
|
||||
dirfd = open("/", O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
|
||||
if (dirfd < 0)
|
||||
return -1;
|
||||
}
|
||||
/* An operator-supplied --temp-dir may point outside the tree; --insecure-links
|
||||
* (or a daemon module's "insecure links =") restores legacy following. */
|
||||
if (operator_path && symlink_optout_allowed())
|
||||
return do_mkstemp(template, perms);
|
||||
|
||||
/* Find the last slash to separate directory from filename */
|
||||
lastslash = strrchr(template, '/');
|
||||
if (lastslash) {
|
||||
char *path_copy = my_strdup(template, __FILE__, __LINE__);
|
||||
if (!path_copy)
|
||||
return -1;
|
||||
|
||||
/* Null-terminate at the last slash to get directory part */
|
||||
path_copy[lastslash - template] = '\0';
|
||||
|
||||
/* Walk the directory path securely */
|
||||
for (const char *part = strtok(path_copy, "/");
|
||||
part != NULL;
|
||||
part = strtok(NULL, "/"))
|
||||
{
|
||||
int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
|
||||
if (next_fd == -1) {
|
||||
int save_errno = errno;
|
||||
free(path_copy);
|
||||
if (dirfd != AT_FDCWD) close(dirfd);
|
||||
errno = (save_errno == ELOOP) ? ELOOP : save_errno;
|
||||
return -1;
|
||||
if (operator_path) {
|
||||
/* An operator --temp-dir is resolved with the ownership walk: a symlink
|
||||
* owned by uid 0 or the euid (the operator's own temp dir) is followed,
|
||||
* a foreign-owned one refused, absolute and relative alike. */
|
||||
if (lastslash) {
|
||||
char dirbuf[MAXPATHLEN];
|
||||
size_t dlen = lastslash - template;
|
||||
const char *dir;
|
||||
if (dlen == 0)
|
||||
dir = "/";
|
||||
else {
|
||||
if (dlen >= sizeof dirbuf) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
memcpy(dirbuf, template, dlen);
|
||||
dirbuf[dlen] = '\0';
|
||||
dir = dirbuf;
|
||||
}
|
||||
if (dirfd != AT_FDCWD) close(dirfd);
|
||||
dirfd = next_fd;
|
||||
dirfd = open_no_attacker_symlinks(dir, O_RDONLY | O_DIRECTORY, 0);
|
||||
if (dirfd < 0)
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
/* For absolute paths, start the secure walk from "/" rather than CWD. */
|
||||
if (template[0] == '/') {
|
||||
dirfd = open("/", O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
|
||||
if (dirfd < 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Walk the directory path securely (refuse every symlink). */
|
||||
if (lastslash) {
|
||||
char *path_copy = my_strdup(template, __FILE__, __LINE__);
|
||||
if (!path_copy)
|
||||
return -1;
|
||||
|
||||
/* Null-terminate at the last slash to get directory part */
|
||||
path_copy[lastslash - template] = '\0';
|
||||
|
||||
for (const char *part = strtok(path_copy, "/");
|
||||
part != NULL;
|
||||
part = strtok(NULL, "/"))
|
||||
{
|
||||
int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
|
||||
if (next_fd == -1) {
|
||||
int save_errno = errno;
|
||||
free(path_copy);
|
||||
if (dirfd != AT_FDCWD) close(dirfd);
|
||||
errno = (save_errno == ELOOP) ? ELOOP : save_errno;
|
||||
return -1;
|
||||
}
|
||||
if (dirfd != AT_FDCWD) close(dirfd);
|
||||
dirfd = next_fd;
|
||||
}
|
||||
free(path_copy);
|
||||
}
|
||||
free(path_copy);
|
||||
}
|
||||
|
||||
/* Now create the temp file in the securely-opened directory */
|
||||
|
||||
@@ -25,6 +25,7 @@ int do_fsync = 0;
|
||||
int inplace = 0;
|
||||
int am_daemon = 0;
|
||||
int am_chrooted = 0;
|
||||
int insecure_links = 0;
|
||||
int modify_window = 0;
|
||||
int preallocate_files = 0;
|
||||
int protect_args = 0;
|
||||
@@ -100,7 +101,12 @@ filter_rule_list daemon_filter_list;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
BOOL lp_use_chroot(UNUSED(int mod))
|
||||
BOOL lp_insecure_links(UNUSED(int mod))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
BOOL lp_use_chroot(UNUSED(int mod))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ extern int relative_paths;
|
||||
extern int preserve_xattrs;
|
||||
extern int omit_link_times;
|
||||
extern int preallocate_files;
|
||||
extern int operator_path_resolve;
|
||||
extern char *module_dir;
|
||||
extern unsigned int module_dirlen;
|
||||
extern char *partial_dir;
|
||||
@@ -284,192 +285,6 @@ int make_path(char *fname, int flags)
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Open an operator-supplied path, refusing to traverse any symlink (parent or
|
||||
* leaf) not owned by uid 0 or our euid. A trusted-owned symlink (e.g. root's
|
||||
* /var/log -> /data/log) is still followed; an untrusted one fails ELOOP.
|
||||
* Unlike plain O_NOFOLLOW this also defends a planted parent component
|
||||
* (--log-file=$plant/log), not just a planted leaf. Used for opens that may
|
||||
* transit attacker-writable parents: --log-file, --password-file, --*-from,
|
||||
* --read/write-batch, daemon motd/lock/early-input/--config.
|
||||
*
|
||||
* Walks component-by-component with fstatat(AT_SYMLINK_NOFOLLOW) +
|
||||
* openat(O_NOFOLLOW), splicing a trusted symlink's target back into the path.
|
||||
* Returns the fd, or -1 (errno ELOOP on the security refusal so callers can
|
||||
* tell it apart). Falls back to plain open() where openat/O_NOFOLLOW are
|
||||
* unavailable. */
|
||||
int safe_open_no_attacker_symlinks(const char *path, int flags, mode_t mode)
|
||||
{
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW
|
||||
/* O_CLOEXEC predates some still-supported targets; mirror rand_bytes()'s
|
||||
* fallback in syscall.c so a build without it still compiles. */
|
||||
#ifndef O_CLOEXEC
|
||||
#define O_CLOEXEC 0
|
||||
#endif
|
||||
if (!path || !*path) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
const uid_t trusted_uid = geteuid();
|
||||
int dfd = AT_FDCWD;
|
||||
int dfd_owns = 0;
|
||||
|
||||
/* Path-walk state. `remaining` is the unconsumed tail; we splice
|
||||
* symlink targets back into it as we go. Sized 2x MAXPATHLEN so a
|
||||
* one-level expansion can't immediately overflow; deeper chains
|
||||
* fail with ENAMETOOLONG below. */
|
||||
char remaining[MAXPATHLEN * 2];
|
||||
if (strlcpy(remaining, path, sizeof remaining) >= sizeof remaining) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Absolute path: pin "/" as the starting dfd. */
|
||||
if (remaining[0] == '/') {
|
||||
dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
dfd_owns = 1;
|
||||
char *p = remaining;
|
||||
while (*p == '/') p++;
|
||||
memmove(remaining, p, strlen(p) + 1);
|
||||
}
|
||||
|
||||
int loops = 40; /* SYMLOOP_MAX-ish; breaks symlink cycles. */
|
||||
int retfd = -1;
|
||||
int saved_errno = 0;
|
||||
|
||||
while (*remaining) {
|
||||
if (--loops < 0) {
|
||||
saved_errno = ELOOP;
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* Peel one component off the front of `remaining`. */
|
||||
char *slash = strchr(remaining, '/');
|
||||
size_t comp_len = slash ? (size_t)(slash - remaining) : strlen(remaining);
|
||||
char comp[MAXPATHLEN];
|
||||
if (comp_len == 0 || comp_len >= sizeof comp) {
|
||||
saved_errno = comp_len == 0 ? EINVAL : ENAMETOOLONG;
|
||||
goto out;
|
||||
}
|
||||
memcpy(comp, remaining, comp_len);
|
||||
comp[comp_len] = '\0';
|
||||
int is_last = (slash == NULL);
|
||||
|
||||
/* Inspect this component without following symlinks. */
|
||||
STRUCT_STAT lst;
|
||||
if (fstatat(dfd, comp, &lst, AT_SYMLINK_NOFOLLOW) < 0) {
|
||||
/* The leaf may not exist yet (O_CREAT case). Allow it
|
||||
* and openat with O_NOFOLLOW so a race-planted leaf
|
||||
* symlink at this instant is still refused. */
|
||||
if (is_last && errno == ENOENT && (flags & O_CREAT)) {
|
||||
retfd = openat(dfd, comp, flags | O_NOFOLLOW, mode);
|
||||
saved_errno = errno;
|
||||
goto out;
|
||||
}
|
||||
saved_errno = errno;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (S_ISLNK(lst.st_mode)) {
|
||||
/* Symlink: untrusted owner is refused; trusted owner
|
||||
* is followed via readlinkat + splice. */
|
||||
if (lst.st_uid != 0 && lst.st_uid != trusted_uid) {
|
||||
saved_errno = ELOOP;
|
||||
goto out;
|
||||
}
|
||||
char target[MAXPATHLEN];
|
||||
ssize_t n = readlinkat(dfd, comp, target, sizeof target - 1);
|
||||
if (n < 0) {
|
||||
saved_errno = errno;
|
||||
goto out;
|
||||
}
|
||||
target[n] = '\0';
|
||||
|
||||
/* Splice: new `remaining` = <target> + <tail-after-comp>.
|
||||
* Absolute target restarts the walk from "/". */
|
||||
char tail[MAXPATHLEN];
|
||||
tail[0] = '\0';
|
||||
if (slash)
|
||||
strlcpy(tail, slash, sizeof tail);
|
||||
|
||||
char rebuilt[MAXPATHLEN * 2];
|
||||
if (snprintf(rebuilt, sizeof rebuilt, "%s%s",
|
||||
target, tail) >= (int)sizeof rebuilt) {
|
||||
saved_errno = ENAMETOOLONG;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (target[0] == '/') {
|
||||
if (dfd_owns) close(dfd);
|
||||
dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (dfd < 0) {
|
||||
saved_errno = errno;
|
||||
dfd_owns = 0;
|
||||
goto out;
|
||||
}
|
||||
dfd_owns = 1;
|
||||
char *p = rebuilt;
|
||||
while (*p == '/') p++;
|
||||
strlcpy(remaining, p, sizeof remaining);
|
||||
} else {
|
||||
strlcpy(remaining, rebuilt, sizeof remaining);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Non-symlink. */
|
||||
if (is_last) {
|
||||
retfd = openat(dfd, comp, flags | O_NOFOLLOW, mode);
|
||||
saved_errno = errno;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (!S_ISDIR(lst.st_mode)) {
|
||||
saved_errno = ENOTDIR;
|
||||
goto out;
|
||||
}
|
||||
int next = openat(dfd, comp, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
if (next < 0) {
|
||||
saved_errno = errno;
|
||||
goto out;
|
||||
}
|
||||
if (dfd_owns) close(dfd);
|
||||
dfd = next;
|
||||
dfd_owns = 1;
|
||||
|
||||
/* Advance `remaining` past this component (and the slash). */
|
||||
if (slash) {
|
||||
char *p = slash;
|
||||
while (*p == '/') p++;
|
||||
memmove(remaining, p, strlen(p) + 1);
|
||||
} else {
|
||||
remaining[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/* Path resolved entirely to a directory (no leaf component left).
|
||||
* If the caller wanted O_DIRECTORY we already hold the dirfd we
|
||||
* built up; otherwise it's an EISDIR. */
|
||||
if (flags & O_DIRECTORY) {
|
||||
retfd = dfd;
|
||||
dfd_owns = 0; /* caller now owns it */
|
||||
saved_errno = 0;
|
||||
} else {
|
||||
saved_errno = EISDIR;
|
||||
}
|
||||
|
||||
out:
|
||||
if (dfd_owns) close(dfd);
|
||||
errno = saved_errno;
|
||||
return retfd;
|
||||
#else
|
||||
/* Pre-AT_FDCWD / no O_NOFOLLOW systems: best-effort fallback. */
|
||||
return open(path, flags, mode);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write @p len bytes at @p ptr to descriptor @p desc, retrying if
|
||||
@@ -1371,7 +1186,7 @@ int change_dir(const char *dir, int set_path_only)
|
||||
* transfer begins. For daemon receivers, refuse symlinks not
|
||||
* owned by uid 0 or our euid in the path walk. */
|
||||
if (am_daemon && (!am_chrooted || module_dirlen)) {
|
||||
int dfd = safe_open_no_attacker_symlinks(dir, O_RDONLY | O_DIRECTORY, 0);
|
||||
int dfd = open_no_attacker_symlinks(dir, O_RDONLY | O_DIRECTORY, 0);
|
||||
if (dfd < 0)
|
||||
return 0;
|
||||
if (fchdir(dfd) != 0) {
|
||||
@@ -1384,7 +1199,7 @@ int change_dir(const char *dir, int set_path_only)
|
||||
#if defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
} else if (!am_chrooted && !am_sender) {
|
||||
/* The destination is operator-supplied: resolve it with
|
||||
* safe_open_no_attacker_symlinks -- follow the operator's/root's
|
||||
* open_no_attacker_symlinks -- follow the operator's/root's
|
||||
* own symlinked dest (the /backup -> /mnt/disk admin pattern) but
|
||||
* refuse one an attacker raced in from another uid, closing the
|
||||
* dest chdir TOCTOU. Strip the trailing slash so the final
|
||||
@@ -1399,7 +1214,7 @@ int change_dir(const char *dir, int set_path_only)
|
||||
memcpy(nf, dir, nl + 1);
|
||||
while (nl > 1 && nf[nl-1] == '/')
|
||||
nf[--nl] = '\0';
|
||||
dfd = safe_open_no_attacker_symlinks(nf, O_RDONLY | O_DIRECTORY, 0);
|
||||
dfd = open_no_attacker_symlinks(nf, O_RDONLY | O_DIRECTORY, 0);
|
||||
if (dfd < 0)
|
||||
return 0;
|
||||
if (fchdir(dfd) != 0) {
|
||||
@@ -1473,7 +1288,7 @@ int change_dir(const char *dir, int set_path_only)
|
||||
* destination like the absolute case -- refuse a component
|
||||
* symlink not owned by uid 0 or our euid, while still following
|
||||
* the operator's own symlinks. */
|
||||
int dfd = safe_open_no_attacker_symlinks(curr_dir,
|
||||
int dfd = open_no_attacker_symlinks(curr_dir,
|
||||
O_RDONLY | O_DIRECTORY, 0);
|
||||
if (dfd < 0)
|
||||
chdir_failed = 1;
|
||||
@@ -1621,22 +1436,30 @@ int handle_partial_dir(const char *fname, int create)
|
||||
|
||||
*fn = '\0';
|
||||
dir = partial_fname;
|
||||
/* The --partial-dir is an operator-supplied path (an absolute one may point
|
||||
* outside the tree): resolve it with the ownership walk -- follow a
|
||||
* uid0/euid-owned symlink, refuse a foreign one, absolute and relative alike.
|
||||
* --insecure-links (or a daemon module's "insecure links =") opts out. */
|
||||
operator_path_resolve = 1;
|
||||
if (create) {
|
||||
STRUCT_STAT st;
|
||||
int statret = do_lstat_at(dir, &st);
|
||||
if (statret == 0 && !S_ISDIR(st.st_mode)) {
|
||||
if (do_unlink_at(dir) < 0) {
|
||||
operator_path_resolve = 0;
|
||||
*fn = '/';
|
||||
return 0;
|
||||
}
|
||||
statret = -1;
|
||||
}
|
||||
if (statret < 0 && do_mkdir_at(dir, 0700) < 0) {
|
||||
operator_path_resolve = 0;
|
||||
*fn = '/';
|
||||
return 0;
|
||||
}
|
||||
} else
|
||||
do_rmdir_at(dir);
|
||||
operator_path_resolve = 0;
|
||||
*fn = '/';
|
||||
|
||||
return 1;
|
||||
|
||||
Reference in new issue
Block a user