/* * Syscall wrappers to ensure that nothing gets done in dry_run mode * and to handle system peculiarities. * * Copyright (C) 1998 Andrew Tridgell * Copyright (C) 2002 Martin Pool * Copyright (C) 2003-2022 Wayne Davison * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, visit the http://fsf.org website. */ #include "rsync.h" /* Exercise the pre-*at() portability tier on modern build hosts. */#ifdef RSYNC_TEST_NO_AT_FDCWD#undef AT_FDCWD#undef AT_SYMLINK_NOFOLLOW#undef HAVE_LINKAT#undef HAVE_OPENAT2#undef HAVE_UTIMENSAT#undef O_RESOLVE_BENEATH#endif #if !defined MKNOD_CREATES_SOCKETS && defined HAVE_SYS_UN_H#include <sys/un.h> /* for the socket+bind() fallback in do_mknod() */#endif#ifdef HAVE_SYS_ATTR_H#include <sys/attr.h>#endif #if defined HAVE_SYS_FALLOCATE && !defined HAVE_FALLOCATE#include <sys/syscall.h>#endif #ifdef __linux__#include <sys/syscall.h> /* SYS_fchmodat2 / SYS_fallocate raw-syscall wrappers */#endif #include "ifuncs.h" extern int dry_run;extern int am_root;extern int am_sender;extern int read_only;extern int list_only;extern int inplace;extern int preallocate_files;extern int sparse_files;extern int preserve_perms;extern int preserve_executability;extern int open_noatime;extern int copy_links;extern int copy_unsafe_links;extern int am_daemon;extern int am_chrooted;extern int insecure_links;extern int module_id;extern unsigned int module_dirlen;extern char *module_dir;extern int module_dirfd; /* daemon: served module root pinned by identity, or -1 */extern char *confine_root; /* --confine-root, or NULL; see confinement_root() */extern unsigned int confine_rootlen;extern char curr_dir[MAXPATHLEN]; /* defined below; fwd-declared for the seed */extern int operator_path_resolve; /* defined below; fwd-declared for the exclude check */ /* A directory fd used only for pathname traversal, fchdir(), or as *at() * authority does not need read permission on Linux. Keep the portable * O_RDONLY fallback for systems without O_PATH. */static int directory_traverse_flags(void){#if defined O_PATH && defined O_DIRECTORY return O_PATH | O_DIRECTORY;#elif defined O_DIRECTORY return O_RDONLY | O_DIRECTORY;#else return O_RDONLY;#endif} #if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY/* Open a trusted absolute anchor directory as an owned dirfd. When the anchor is * the served module root and the daemon pinned it by identity (module_dirfd), dup * that fd rather than re-resolving the absolute path with openat(AT_FDCWD, ...) -- * which re-traverses the module's ancestors as the dropped-privilege module uid * and EACCESes when the module sits under a non-traversable parent (a 0700 home). * Functionally identical (same inode), just privilege-drop-safe. Gated like its * callers (the secure resolver and dpc_dir_fd both require these three). */static int open_anchor_dirfd(const char *path){ if (module_dirfd >= 0 && am_daemon && module_dir && strcmp(path, module_dir) == 0) return dup(module_dirfd); return openat(AT_FDCWD, path, directory_traverse_flags());}#endif /* Single gate for whether path resolution must be hardened against * parent-component symlink races (TOCTOU). Used by the do_*_at()/do_*_atfd() * wrappers and the receiver's secure-open/secure-mkstemp choices. Hardens * every non-chrooted receiver (a chroot is its own confinement); the sender is * excluded so it still follows -L/--copy-links symlinks. A daemon chroot with * an inner-module /./ boundary still needs these checks because the kernel * chroot confines the outer path, not the inner module. */int secure_relpath_active(void){ /* The "insecure links" / --insecure-links opt-out restores the legacy * follow-any-symlink behaviour uniformly, so it disables the secure * resolver on the RECEIVER side too (not just the sender enumeration that * already checks symlink_optout_allowed()). Without this an opted-out * module still confined receiver writes/stats through a pre-existing * in-module symlink -- failing to match the pre-3.4.3 behaviour the opt-out * promises (documented in rsyncd.conf(5) "munge symlinks"/"insecure links"). */ if (symlink_optout_allowed()) return 0; if (am_daemon && am_chrooted && module_dirlen) return 1; 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;} /* The root an operator/peer-supplied path must stay under, or NULL when nothing * is confined. A daemon has the served module; a server launched by a wrapper * with its own restricted directory (rrsync) gets one from --confine-root. * * A daemon never honours --confine-root: module_dir is the boundary there, and * the option arrives in a peer-supplied argv, so obeying it could only loosen * the module. */static const char *confinement_root(unsigned int *lenp){ if (am_daemon) { *lenp = module_dirlen; return module_dir; } *lenp = confine_rootlen; return confine_root;} /* Split a recognised fd-pin prefix off `p`, returning the tail -- "" for the * pin directory itself, otherwise a string starting with '/'. NULL when `p` * is not in an fd-pin namespace. */static const char *fd_pin_tail(const char *p){ const char *s; if (strncmp(p, "/dev/fd", 7) == 0) { s = p + 7; return (*s == '\0' || *s == '/') ? s : NULL; } if (strncmp(p, "/proc/", 6) != 0) return NULL; s = p + 6; if (strncmp(s, "self/", 5) == 0) /* "/proc/self/..." */ s += 4; else { /* "/proc/<pid>/..." */ const char *d = s; while (*s >= '0' && *s <= '9') s++; if (s == d || *s != '/') return NULL; } if (strncmp(s, "/fd", 3) != 0) return NULL; s += 3; return (*s == '\0' || *s == '/') ? s : NULL;} /* An EXACT pin entry, such as "/proc/self/fd/7" or "/dev/fd/7", whose target is * what confinement must judge. rrsync also writes a pinned parent as * ".../fd/7/<leaf>", but the walk resolves the magic link itself and checks the * components past it, so only the bare entry is resolved here. Requiring all * digits keeps a planted name like ".../fd/outside-secret" out. */static int is_exact_fd_pin(const char *p){ const char *tail = fd_pin_tail(p); if (!tail || *tail != '/') return 0; for (++tail; *tail >= '0' && *tail <= '9'; tail++) {} return *tail == '\0' && tail[-1] != '/';} /* Refuse (return 1) when the ABSOLUTE resolved path `abspath` lands OUTSIDE the * confinement root, for an operator/peer-supplied path that must stay inside it * (--partial-dir/--backup-dir/alt-basis/merge files: 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 root; this catches that escape. * * This is 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-tree subtree, exactly as in * stock rsync. The defense for a writable module is `munge symlinks` (see * rsyncd.conf(5)), not this walk. */static int abspath_outside_confinement(const char *abspath){ unsigned int rootlen; const char *root = confinement_root(&rootlen); char pinned[MAXPATHLEN]; if (!root || !abspath) return 0; if (rootlen <= 1) /* root is "/": nothing is outside */ return 0; /* An fd pin (rrsync rewrites a validated option path to /proc/self/fd/N so * no later symlink can redirect it) is spelled outside the root by * construction. Judge it by what it points AT rather than by its spelling, * so a pin is neither wrongly refused nor blindly trusted. A pin we cannot * resolve to an absolute path is refused, not waved through: an unreadable * pin is exactly the case where we cannot say where the open would land. */ if (!am_daemon) { const char *tail = fd_pin_tail(abspath); if (tail && !*tail) return 0; /* the pin directory: transit, opens nothing */ if (is_exact_fd_pin(abspath)) { ssize_t n = readlink(abspath, pinned, sizeof pinned - 1); if (n <= 0 || pinned[0] != '/') return operator_path_resolve ? 1 : 0; pinned[n] = '\0'; abspath = pinned; } } if (strncmp(abspath, root, rootlen) == 0 && (abspath[rootlen] == '\0' || abspath[rootlen] == '/')) return 0; /* inside: name-based exclude is not a boundary */ /* Not under the root. An ABSOLUTE walk passes through the 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 is * outside: refuse it for an operator/peer path that must stay in the tree * (operator_path_resolve); other 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, root, alen) == 0 && root[alen] == '/')) return 0; /* ancestor of the 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 module-confinement * check (abspath_outside_confinement) sees the REAL resolved target. -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 && defined O_DIRECTORY /* 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 const int dir_traverse_flags = directory_traverse_flags() | O_CLOEXEC; 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 path of the current dir, for the confinement refusal * (abspath_outside_confinement). 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] = {0}; if (am_daemon && module_dir && module_dir[0] == '/') strlcpy(abspath, module_dir, sizeof abspath); /* "/" for a path=/ module */ else if (confine_root) { /* Unlike a daemon's, this cwd is not pinned to the root -- the receiver * chdir's into the destination -- so it has to be read, not assumed. * It must be the PHYSICAL cwd: curr_dir is the lexical name change_dir() * was given, so after descending a trusted symlink the tracker sits at a * different depth than the kernel, and a ".." that really escapes looks * like it landed inside. * * Without it there is nothing to measure against, and an empty tracker * does NOT deny by itself -- a leading ".." pops nothing and an empty * path reads as an ancestor of the root -- so refuse the open instead. */ if (!getcwd(abspath, sizeof abspath)) return -1; } /* An fd pin (rrsync rewrites an option path to /proc/self/fd/N so no * later symlink can redirect it) is spelled outside the root by * construction, so the walk has to be allowed through /proc/self/fd to * reach the magic link. This only suspends the check for that prefix: * following the link restarts the walk at its absolute target, and every * component of THAT is checked, so a pin aimed outside is still refused. */ const char *ptail = fd_pin_tail(path); int pin_transit = !am_daemon && confine_root && ptail != NULL; /* 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("/", dir_traverse_flags); 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 (!pin_transit && abspath_outside_confinement(abspath)) { 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. In a user namespace the /proc/self and * /dev/fd symlinks may report the overflow uid, so * allow those exact components while traversing a recognised pin. */ int namespace_pin = pin_transit && ((strcmp(abspath, "/proc") == 0 && strcmp(comp, "self") == 0) || (strcmp(abspath, "/dev") == 0 && strcmp(comp, "fd") == 0)); if (!namespace_pin && lst.st_uid != 0 && lst.st_uid != trusted_uid) { rprintf(FERROR, "refusing to follow a symlink owned by an untrusted user; " "use --insecure-links locally or \"insecure links = yes\" in a " "daemon module ONLY if every path component is trusted\n"); 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'; /* Detect Linux kernel pseudo-paths (pipes, sockets, anon_inodes). * These are not real paths on disk and never contain slashes. */ const char *abstail = fd_pin_tail(abspath); int is_fd_dir = (abstail != NULL && *abstail == '\0' && ptail != NULL); if (is_fd_dir && (strncmp(target, "pipe:[", 6) == 0 || strncmp(target, "socket:[", 8) == 0 || strncmp(target, "anon_inode:", 11) == 0)) { if (!is_last) { saved_errno = ENOTDIR; goto out; } if (confine_root) { /* Anonymous objects cannot be proven to reside beneath * the confinement root. */ saved_errno = ENOENT; goto out; } /* Process substitution exposes /dev/fd/X as a symlink to a * kernel object. Reopen the validated leaf without O_NOFOLLOW * so the kernel applies the caller's requested open flags. */ retfd = openat(dfd, comp, (flags & ~O_NOFOLLOW) | O_CLOEXEC, mode); /* Refuse a descriptor that changed to a filesystem object * between validation and openat(). */ if (retfd >= 0) { STRUCT_STAT pst; if (fstat(retfd, &pst) < 0 || S_ISREG(pst.st_mode) || S_ISDIR(pst.st_mode)) { close(retfd); retfd = -1; errno = ELOOP; } } saved_errno = retfd < 0 ? errno : 0; goto out; } /* 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] == '/') {Lines 1–500 of 4062. Load more
/* * Syscall wrappers to ensure that nothing gets done in dry_run mode * and to handle system peculiarities. * * Copyright (C) 1998 Andrew Tridgell * Copyright (C) 2002 Martin Pool * Copyright (C) 2003-2022 Wayne Davison * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, visit the http://fsf.org website. */ #include "rsync.h" /* Exercise the pre-*at() portability tier on modern build hosts. */ #ifdef RSYNC_TEST_NO_AT_FDCWD #undef AT_FDCWD #undef AT_SYMLINK_NOFOLLOW #undef HAVE_LINKAT #undef HAVE_OPENAT2 #undef HAVE_UTIMENSAT #undef O_RESOLVE_BENEATH #endif #if !defined MKNOD_CREATES_SOCKETS && defined HAVE_SYS_UN_H #include <sys/un.h> /* for the socket+bind() fallback in do_mknod() */ #endif #ifdef HAVE_SYS_ATTR_H #include <sys/attr.h> #endif #if defined HAVE_SYS_FALLOCATE && !defined HAVE_FALLOCATE #include <sys/syscall.h> #endif #ifdef __linux__ #include <sys/syscall.h> /* SYS_fchmodat2 / SYS_fallocate raw-syscall wrappers */ #endif #include "ifuncs.h" extern int dry_run; extern int am_root; extern int am_sender; extern int read_only; extern int list_only; extern int inplace; extern int preallocate_files; extern int sparse_files; extern int preserve_perms; extern int preserve_executability; extern int open_noatime; extern int copy_links; extern int copy_unsafe_links; extern int am_daemon; extern int am_chrooted; extern int insecure_links; extern int module_id; extern unsigned int module_dirlen; extern char *module_dir; extern int module_dirfd; /* daemon: served module root pinned by identity, or -1 */ extern char *confine_root; /* --confine-root, or NULL; see confinement_root() */ extern unsigned int confine_rootlen; extern char curr_dir[MAXPATHLEN]; /* defined below; fwd-declared for the seed */ extern int operator_path_resolve; /* defined below; fwd-declared for the exclude check */ /* A directory fd used only for pathname traversal, fchdir(), or as *at() * authority does not need read permission on Linux. Keep the portable * O_RDONLY fallback for systems without O_PATH. */ static int directory_traverse_flags(void) { #if defined O_PATH && defined O_DIRECTORY return O_PATH | O_DIRECTORY; #elif defined O_DIRECTORY return O_RDONLY | O_DIRECTORY; #else return O_RDONLY; #endif } #if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY /* Open a trusted absolute anchor directory as an owned dirfd. When the anchor is * the served module root and the daemon pinned it by identity (module_dirfd), dup * that fd rather than re-resolving the absolute path with openat(AT_FDCWD, ...) -- * which re-traverses the module's ancestors as the dropped-privilege module uid * and EACCESes when the module sits under a non-traversable parent (a 0700 home). * Functionally identical (same inode), just privilege-drop-safe. Gated like its * callers (the secure resolver and dpc_dir_fd both require these three). */ static int open_anchor_dirfd(const char *path) { if (module_dirfd >= 0 && am_daemon && module_dir && strcmp(path, module_dir) == 0) return dup(module_dirfd); return openat(AT_FDCWD, path, directory_traverse_flags()); } #endif /* Single gate for whether path resolution must be hardened against * parent-component symlink races (TOCTOU). Used by the do_*_at()/do_*_atfd() * wrappers and the receiver's secure-open/secure-mkstemp choices. Hardens * every non-chrooted receiver (a chroot is its own confinement); the sender is * excluded so it still follows -L/--copy-links symlinks. A daemon chroot with * an inner-module /./ boundary still needs these checks because the kernel * chroot confines the outer path, not the inner module. */ int secure_relpath_active(void) { /* The "insecure links" / --insecure-links opt-out restores the legacy * follow-any-symlink behaviour uniformly, so it disables the secure * resolver on the RECEIVER side too (not just the sender enumeration that * already checks symlink_optout_allowed()). Without this an opted-out * module still confined receiver writes/stats through a pre-existing * in-module symlink -- failing to match the pre-3.4.3 behaviour the opt-out * promises (documented in rsyncd.conf(5) "munge symlinks"/"insecure links"). */ if (symlink_optout_allowed()) return 0; if (am_daemon && am_chrooted && module_dirlen) return 1; 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; } /* The root an operator/peer-supplied path must stay under, or NULL when nothing * is confined. A daemon has the served module; a server launched by a wrapper * with its own restricted directory (rrsync) gets one from --confine-root. * * A daemon never honours --confine-root: module_dir is the boundary there, and * the option arrives in a peer-supplied argv, so obeying it could only loosen * the module. */ static const char *confinement_root(unsigned int *lenp) { if (am_daemon) { *lenp = module_dirlen; return module_dir; } *lenp = confine_rootlen; return confine_root; } /* Split a recognised fd-pin prefix off `p`, returning the tail -- "" for the * pin directory itself, otherwise a string starting with '/'. NULL when `p` * is not in an fd-pin namespace. */ static const char *fd_pin_tail(const char *p) { const char *s; if (strncmp(p, "/dev/fd", 7) == 0) { s = p + 7; return (*s == '\0' || *s == '/') ? s : NULL; } if (strncmp(p, "/proc/", 6) != 0) return NULL; s = p + 6; if (strncmp(s, "self/", 5) == 0) /* "/proc/self/..." */ s += 4; else { /* "/proc/<pid>/..." */ const char *d = s; while (*s >= '0' && *s <= '9') s++; if (s == d || *s != '/') return NULL; } if (strncmp(s, "/fd", 3) != 0) return NULL; s += 3; return (*s == '\0' || *s == '/') ? s : NULL; } /* An EXACT pin entry, such as "/proc/self/fd/7" or "/dev/fd/7", whose target is * what confinement must judge. rrsync also writes a pinned parent as * ".../fd/7/<leaf>", but the walk resolves the magic link itself and checks the * components past it, so only the bare entry is resolved here. Requiring all * digits keeps a planted name like ".../fd/outside-secret" out. */ static int is_exact_fd_pin(const char *p) { const char *tail = fd_pin_tail(p); if (!tail || *tail != '/') return 0; for (++tail; *tail >= '0' && *tail <= '9'; tail++) {} return *tail == '\0' && tail[-1] != '/'; } /* Refuse (return 1) when the ABSOLUTE resolved path `abspath` lands OUTSIDE the * confinement root, for an operator/peer-supplied path that must stay inside it * (--partial-dir/--backup-dir/alt-basis/merge files: 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 root; this catches that escape. * * This is 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-tree subtree, exactly as in * stock rsync. The defense for a writable module is `munge symlinks` (see * rsyncd.conf(5)), not this walk. */ static int abspath_outside_confinement(const char *abspath) { unsigned int rootlen; const char *root = confinement_root(&rootlen); char pinned[MAXPATHLEN]; if (!root || !abspath) return 0; if (rootlen <= 1) /* root is "/": nothing is outside */ return 0; /* An fd pin (rrsync rewrites a validated option path to /proc/self/fd/N so * no later symlink can redirect it) is spelled outside the root by * construction. Judge it by what it points AT rather than by its spelling, * so a pin is neither wrongly refused nor blindly trusted. A pin we cannot * resolve to an absolute path is refused, not waved through: an unreadable * pin is exactly the case where we cannot say where the open would land. */ if (!am_daemon) { const char *tail = fd_pin_tail(abspath); if (tail && !*tail) return 0; /* the pin directory: transit, opens nothing */ if (is_exact_fd_pin(abspath)) { ssize_t n = readlink(abspath, pinned, sizeof pinned - 1); if (n <= 0 || pinned[0] != '/') return operator_path_resolve ? 1 : 0; pinned[n] = '\0'; abspath = pinned; } } if (strncmp(abspath, root, rootlen) == 0 && (abspath[rootlen] == '\0' || abspath[rootlen] == '/')) return 0; /* inside: name-based exclude is not a boundary */ /* Not under the root. An ABSOLUTE walk passes through the 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 is * outside: refuse it for an operator/peer path that must stay in the tree * (operator_path_resolve); other 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, root, alen) == 0 && root[alen] == '/')) return 0; /* ancestor of the 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 module-confinement * check (abspath_outside_confinement) sees the REAL resolved target. -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 && defined O_DIRECTORY /* 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 const int dir_traverse_flags = directory_traverse_flags() | O_CLOEXEC; 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 path of the current dir, for the confinement refusal * (abspath_outside_confinement). 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] = {0}; if (am_daemon && module_dir && module_dir[0] == '/') strlcpy(abspath, module_dir, sizeof abspath); /* "/" for a path=/ module */ else if (confine_root) { /* Unlike a daemon's, this cwd is not pinned to the root -- the receiver * chdir's into the destination -- so it has to be read, not assumed. * It must be the PHYSICAL cwd: curr_dir is the lexical name change_dir() * was given, so after descending a trusted symlink the tracker sits at a * different depth than the kernel, and a ".." that really escapes looks * like it landed inside. * * Without it there is nothing to measure against, and an empty tracker * does NOT deny by itself -- a leading ".." pops nothing and an empty * path reads as an ancestor of the root -- so refuse the open instead. */ if (!getcwd(abspath, sizeof abspath)) return -1; } /* An fd pin (rrsync rewrites an option path to /proc/self/fd/N so no * later symlink can redirect it) is spelled outside the root by * construction, so the walk has to be allowed through /proc/self/fd to * reach the magic link. This only suspends the check for that prefix: * following the link restarts the walk at its absolute target, and every * component of THAT is checked, so a pin aimed outside is still refused. */ const char *ptail = fd_pin_tail(path); int pin_transit = !am_daemon && confine_root && ptail != NULL; /* 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("/", dir_traverse_flags); 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 (!pin_transit && abspath_outside_confinement(abspath)) { 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. In a user namespace the /proc/self and * /dev/fd symlinks may report the overflow uid, so * allow those exact components while traversing a recognised pin. */ int namespace_pin = pin_transit && ((strcmp(abspath, "/proc") == 0 && strcmp(comp, "self") == 0) || (strcmp(abspath, "/dev") == 0 && strcmp(comp, "fd") == 0)); if (!namespace_pin && lst.st_uid != 0 && lst.st_uid != trusted_uid) { rprintf(FERROR, "refusing to follow a symlink owned by an untrusted user; " "use --insecure-links locally or \"insecure links = yes\" in a " "daemon module ONLY if every path component is trusted\n"); 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'; /* Detect Linux kernel pseudo-paths (pipes, sockets, anon_inodes). * These are not real paths on disk and never contain slashes. */ const char *abstail = fd_pin_tail(abspath); int is_fd_dir = (abstail != NULL && *abstail == '\0' && ptail != NULL); if (is_fd_dir && (strncmp(target, "pipe:[", 6) == 0 || strncmp(target, "socket:[", 8) == 0 || strncmp(target, "anon_inode:", 11) == 0)) { if (!is_last) { saved_errno = ENOTDIR; goto out; } if (confine_root) { /* Anonymous objects cannot be proven to reside beneath * the confinement root. */ saved_errno = ENOENT; goto out; } /* Process substitution exposes /dev/fd/X as a symlink to a * kernel object. Reopen the validated leaf without O_NOFOLLOW * so the kernel applies the caller's requested open flags. */ retfd = openat(dfd, comp, (flags & ~O_NOFOLLOW) | O_CLOEXEC, mode); /* Refuse a descriptor that changed to a filesystem object * between validation and openat(). */ if (retfd >= 0) { STRUCT_STAT pst; if (fstat(retfd, &pst) < 0 || S_ISREG(pst.st_mode) || S_ISDIR(pst.st_mode)) { close(retfd); retfd = -1; errno = ELOOP; } } saved_errno = retfd < 0 ? errno : 0; goto out; } /* 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] == '/') {