Files
rsync/vfs/secure_open.c
Andrew Tridgell 1030929cba vfs: adapt the merged-in base changes to the VFS layer
The base gained 13 commits.  Three needed hand-porting because the change
lands on code the VFS split moved or renamed:

- 31130112 (--confine-root) is almost entirely in syscall.c, which does
  not exist here.  abspath_excluded_by_module() becomes
  abspath_outside_confinement() in vfs/dirstack.c, taking its root from
  vfs.module_dir when we are a daemon and from confine_root otherwise,
  plus the fd-pin helpers; ona_open() in vfs/owner_walk.c gains the
  getcwd() seed and the pin_transit exemption.  The VFS passes is_operator
  as an argument where the base reads operator_path_resolve, so the
  refusal takes it from there rather than the deleted global.

- d09edb85 (--link-dest hard-link fallback) keeps the VFS call
  vfs_link_at(cmpbuf, fname, !am_daemon ? VFS_OPERATOR_PATH : 0, 0) and
  adopts the base's cannot_hardlink/match_level fallback around it.

- t_stub.c gains the confine_root/confine_rootlen stubs beside the VFS
  curr_dir note.

Tree is byte-identical to the merge oracle (tag merge-reference-11).
2026-08-13 10:20:13 +10:00

501 lines
19 KiB
C

/*
* vfs/secure_open.c - rsync's race-safe path resolver and its policy gates.
*
* vfs_resolve_open()/vfs_resolve_open_at() walk a relative path one component
* at a time beneath a trusted anchor (via the dirstack in vfs/dirstack.c), so a
* parent-component symlink swapped mid-walk cannot redirect resolution. The two
* gates decide when this hardening applies and whether the symlink confinement
* is opted out. Moved verbatim out of syscall.c.
*
* Copyright (C) 1998-2022 Andrew Tridgell, Martin Pool, Wayne Davison
* Copyright (C) 2026 Wayne Davison, Andrew Tridgell
*
* 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.
*/
#include "rsync.h"
#include "ifuncs.h"
#include "vfs/vfs_internal.h"
extern int am_chrooted;
extern int am_sender;
extern int module_id;
extern int insecure_links;
extern int open_noatime;
/* 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 vfs_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 vfs_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 (vfs_symlink_optout_allowed())
return 0;
if (am_daemon && am_chrooted && vfs.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 vfs_symlink_optout_allowed(void)
{
if (am_daemon)
return module_id >= 0 && lp_insecure_links(module_id);
return insecure_links;
}
/* STRICT_CONFINEMENT enforcement predicate (no effect on production behaviour).
* True when `path` is a relative, multi-component path that the receiver-side
* confinement is meant to protect -- i.e. a metadata op on it was expected to go
* through a confined fd, never a raw path-based syscall. False for: a build
* lacking the *at/O_NOFOLLOW primitives (the portability-fallback regime), an
* operator-supplied path (its own ownership walk governs it), an absolute path,
* and a top-level (no-slash) path with no parent component to flip. */
int vfs_must_be_confined(const char *path, int is_operator)
{
#if defined(O_NOFOLLOW) && defined(O_DIRECTORY) && defined(AT_FDCWD)
if (is_operator)
return 0;
if (!path || !*path || *path == '/')
return 0;
if (!strchr(path, '/'))
return 0;
return vfs_relpath_active();
#else
(void)path; (void)is_operator;
return 0;
#endif
}
#ifdef STRICT_CONFINEMENT
/* The strict build's hard stop: a confined-regime path-based metadata op was
* about to run where a confined fd was required. Loudly fail (the test suite
* then catches the reintroduced fallback) rather than silently doing an
* unconfined op a flipped parent could redirect outside the tree. */
void vfs_strict_confine_fail(const char *path, const char *what)
{
rprintf(FERROR,
"STRICT_CONFINEMENT violation: unconfined path-based %s on confined path \"%s\"\n",
what ? what : "metadata op", path ? path : "(null)");
abort();
}
#endif
/*
open a file relative to a base directory. The basedir can be NULL,
in which case the current working directory is used. The relpath
must be a relative path. Resolution cannot escape basedir (or the
cwd, when basedir is NULL): no ".." jumps above the start, no
symlinks pointing outside, no absolute paths.
Symlinks *within* basedir are followed normally — earlier rsync
versions rejected every symlink with O_NOFOLLOW on each component,
which broke legitimate directory symlinks on the receiver side
(https://github.com/RsyncProject/rsync/issues/715).
Escape prevention is handled by a single portable mechanism on every
platform: a per-component O_NOFOLLOW walk on a stack of held parent
dirfds (the dirstack helpers above). Each component is opened
relative to a pinned parent fd, so no rename or symlink-swap can
redirect resolution; ".." pops to the already-held parent (never
above the anchor); an in-tree directory symlink is followed by
reading its target and walking it on the same stack (absolute
targets refused, symlink hops bounded). Single-component + O_NOFOLLOW
+ a pinned parent fd is race-free by construction, with no kernel
"beneath" primitive required (see dev-notes/resolver-race-freeness).
The relpath must also not contain any ../ elements in the path
(except for a deliberately re-anchored module path; see below).
*/
#if defined(O_NOFOLLOW) && defined(O_DIRECTORY) && defined(AT_FDCWD)
/* Walk `relpath` confined beneath the borrowed anchor dirfd (which may be
* AT_FDCWD) and return the opened leaf fd, or -1. Does NOT close `anchor_fd` --
* the caller owns it. Shared by vfs_resolve_open() (which first resolves a
* basedir to the anchor) and vfs_resolve_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 *anchor_abspath,
const char *relpath, int flags, mode_t mode, int *hops)
{
struct dirstack ds;
int retfd = -1;
char *path_copy;
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);
return -1;
}
/* Trim trailing slashes so the last-component test below is exact, then
* note the offset of the final component. */
size_t pclen = strlen(path_copy);
while (pclen > 1 && path_copy[pclen-1] == '/')
path_copy[--pclen] = '\0';
char *last_slash = strrchr(path_copy, '/');
size_t last_off = last_slash ? (size_t)(last_slash + 1 - path_copy) : 0;
int saw_component = 0;
char *psave = NULL;
for (char *part = strtok_r(path_copy, "/", &psave);
part != NULL;
part = strtok_r(NULL, "/", &psave))
{
int is_last = (size_t)(part - path_copy) == last_off;
saw_component = 1;
/* A literal "." or ".." is a movement, not a name to open. It must go
* through ds_descend(), which refuses to pop above the anchor, BEFORE
* the leaf fast paths below -- those openat() the component directly,
* so a final ".." would otherwise hand back the anchor's own parent
* (with O_NOFOLLOW) or open it transiently (without O_DIRECTORY). */
if (part[0] == '.'
&& (part[1] == '\0' || (part[1] == '.' && part[2] == '\0'))) {
if (ds_descend(&ds, part, hops) < 0)
goto cleanup;
if (is_last) {
if (flags & O_DIRECTORY)
retfd = ds_take(&ds);
else
errno = EISDIR;
goto cleanup;
}
continue;
}
/* 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_outside_confinement(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);
goto cleanup;
}
if (next_fd == -1)
goto cleanup;
close(next_fd);
errno = EISDIR;
goto cleanup;
}
/* O_DIRECTORY|O_NOFOLLOW leaf: the caller's O_NOFOLLOW governs the leaf. */
if (is_last && (flags & O_NOFOLLOW)) {
retfd = openat(ds_cur(&ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
goto cleanup;
}
/* Directory component (intermediate, or an O_DIRECTORY leaf to follow):
* descend on the stack, following in-tree symlinks. */
if (ds_descend(&ds, part, hops) < 0) {
if (!is_last && errno == ENOTDIR)
errno = ELOOP;
goto cleanup;
}
if (is_last) {
retfd = ds_take(&ds);
goto cleanup;
}
}
/* Empty relpath: hand back a real anchor for an O_DIRECTORY caller (ds_take
* dups the borrowed anchor), else EISDIR. An AT_FDCWD anchor is not a
* resolvable target, so it fails rather than silently returning the cwd. */
if (!saw_component) {
if ((flags & O_DIRECTORY) && anchor_fd != AT_FDCWD)
retfd = ds_take(&ds);
else
errno = EISDIR;
}
cleanup:
free(path_copy);
ds_free(&ds);
return retfd;
}
#endif /* O_NOFOLLOW && O_DIRECTORY && AT_FDCWD */
int vfs_resolve_open(const char *basedir, const char *relpath, int flags, mode_t mode)
{
extern int am_daemon, am_chrooted;
char modrel_buf[MAXPATHLEN];
int reanchored = 0;
if (!relpath || relpath[0] == '/') {
// must be a relative path
errno = EINVAL;
return -1;
}
/* Sanitizing daemon (am_daemon && !am_chrooted) and the /./ inner-module
* chroot (am_daemon && am_chrooted && vfs.module_dirlen) -- both keep the module
* root, not the cwd, as the trust boundary. Here we have chdir'd into a
* sub-dir of the module (the transfer destination), so a relative alt-dest
* like "../01" may legitimately climb to a sibling that is still inside the
* module (#915). Confining beneath the cwd would reject that climb.
* Re-anchor at the module root by prefixing the cwd's module-relative path
* (from rsync's logical vfs.curr_dir[], a guaranteed lexical prefix of
* vfs.module_dir, unlike getcwd()) and resolving beneath vfs.module_dir; RESOLVE_
* BENEATH then allows in-module climbs and still rejects escapes. Only for
* paths that contain "..". vfs.module_dirlen is 0 for a `path = /` module
* (clientserver.c), so the non-chroot arm gates on vfs.module_dir, not its
* length, to cover that case too -- the prefix check below treats
* vfs.module_dirlen 0 as "module root is /". */
if (am_daemon && (!am_chrooted || vfs.module_dirlen)
&& vfs.module_dir && vfs.module_dir[0] == '/'
&& (basedir == NULL || basedir[0] != '/')
&& (path_has_dotdot_component(relpath)
|| (basedir && path_has_dotdot_component(basedir)))) {
const char *p;
int n;
if (vfs.curr_dir_len >= vfs.module_dirlen
&& strncmp(vfs.curr_dir, vfs.module_dir, vfs.module_dirlen) == 0
&& (vfs.curr_dir[vfs.module_dirlen] == '\0' || vfs.curr_dir[vfs.module_dirlen] == '/')) {
for (p = vfs.curr_dir + vfs.module_dirlen; *p == '/'; p++) {}
if (basedir)
n = snprintf(modrel_buf, sizeof modrel_buf, "%s%s%s/%s",
p, *p ? "/" : "", basedir, relpath);
else
n = snprintf(modrel_buf, sizeof modrel_buf, "%s%s%s",
p, *p ? "/" : "", relpath);
if (n < 0 || n >= (int)sizeof modrel_buf) {
errno = ENAMETOOLONG;
return -1;
}
basedir = vfs.module_dir; /* absolute, operator-trusted anchor */
relpath = modrel_buf;
reanchored = 1;
}
/* else: cwd not under module root as expected -- fall through to the
* front-door rejection below (fail safe). */
}
/* Reject any path with a literal ".." component (bare "..",
* "../foo", "foo/..", "foo/../bar", "subdir/..") at the front door,
* with EINVAL, so callers can rely on the validation regardless of
* platform. Skipped for a re-anchored path: its ".." is deliberate,
* stays within the module, and is adjudicated safely by the walk
* below (ds_descend pops a "../" to the held parent, never above the
* anchor). */
if (!reanchored) {
if (path_has_dotdot_component(relpath)) {
errno = EINVAL;
return -1;
}
if (basedir && basedir[0] != '/' && path_has_dotdot_component(basedir)) {
errno = EINVAL;
return -1;
}
}
#ifdef O_NOATIME
if (open_noatime)
flags |= O_NOATIME;
#endif
#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)
// really old system, all we can do is live with the risks
if (!basedir) {
return open(relpath, flags, mode);
}
char fullpath[MAXPATHLEN];
pathjoin(fullpath, sizeof fullpath, basedir, relpath);
return open(fullpath, flags, mode);
#else
int dirfd = AT_FDCWD; /* anchor for the relpath walk (owned unless AT_FDCWD) */
int hops = SECURE_OPEN_MAXSYMLINKS; /* shared symlink-hop budget */
if (basedir != NULL) {
if (basedir[0] == '/') {
/* Absolute basedir: operator-trusted. Prefer the identity-pinned
* module-root fd when this is the served module, so a dropped-
* privilege daemon need not re-traverse the absolute path. */
dirfd = open_anchor_dirfd(basedir);
if (dirfd == -1)
return -1;
} else {
/* Relative basedir: resolve it on a dirfd stack anchored at
* the CWD, following in-tree directory symlinks -- the
* portable RESOLVE_BENEATH equivalent. A symlink target's
* ".." may climb but not above the CWD anchor. */
struct dirstack bds;
char *bcopy;
if (ds_init(&bds, AT_FDCWD) < 0)
return -1;
bcopy = my_strdup(basedir, __FILE__, __LINE__);
if (!bcopy) {
ds_free(&bds);
return -1;
}
if (ds_walk_path(&bds, bcopy, &hops) < 0) {
int e = errno;
free(bcopy);
ds_free(&bds);
errno = e;
return -1;
}
free(bcopy);
dirfd = ds_take(&bds); /* owned dirfd for the basedir */
ds_free(&bds);
if (dirfd == -1)
return -1;
}
}
/* 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 ? vfs.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;
#endif // O_NOFOLLOW, O_DIRECTORY
}
/* Common fd-anchored resolver. A caller may explicitly allow literal ".."
* components when the fd itself is the confinement boundary: secure_walk_at()
* resolves each one by popping its held-dirfd stack and refuses a pop above the
* anchor. Other callers retain the front-door validation used by
* vfs_resolve_open(). */
static int vfs__resolve_open_at_internal(int anchor_fd, const char *relpath,
int flags, mode_t mode, int allow_dotdot)
{
#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)
(void)anchor_fd; (void)relpath; (void)flags; (void)mode; (void)allow_dotdot;
errno = ENOSYS;
return -1;
#else
int hops = SECURE_OPEN_MAXSYMLINKS;
if (!relpath || relpath[0] == '/') {
errno = EINVAL;
return -1;
}
if (!allow_dotdot && path_has_dotdot_component(relpath)) {
errno = EINVAL;
return -1;
}
#ifdef O_NOATIME
if (open_noatime)
flags |= O_NOATIME;
#endif
/* 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
}
/* Like vfs_resolve_open() but anchored at an already-open directory fd
* (borrowed -- the caller keeps ownership) rather than a basedir path. Lets a
* caller pin the trust root once -- e.g. a daemon's module root opened while
* still privileged -- and resolve a relative path beneath it without re-walking
* the absolute path as a dropped-privilege uid. The ordinary entry point keeps
* rejecting literal ".." components as suspicious caller input. */
int vfs_resolve_open_at(int anchor_fd, const char *relpath, int flags, mode_t mode)
{
return vfs__resolve_open_at_internal(anchor_fd, relpath, flags, mode, 0);
}
/* Resolve a path that may contain literal ".." beneath a trusted anchor fd.
* Used for a followed symlink target, where parent-relative components are
* normal pathname semantics. The held-fd stack still refuses every escape
* above anchor_fd. */
int vfs_resolve_open_at_beneath(int anchor_fd, const char *relpath,
int flags, mode_t mode)
{
return vfs__resolve_open_at_internal(anchor_fd, relpath, flags, mode, 1);
}
/* Resolve ONE operand of a two-path op (rename/link) to a parent dirfd + leaf,
* per that operand's OWN policy -- so a two-path op can confine each side
* independently (an operator basis/backup path on one side must not relax the
* transfer-path confinement of the other). Policy:
* - side_flags & VFS_OPERATOR_PATH, or an absolute path: ownership walk
* (follow uid0/euid symlinks, refuse foreign; module-exclude enforced).
* - a relative path with a slash: secure receiver resolve of the parent.
* - a bare name: AT_FDCWD + the name.
* Sets *bname and *dfd_out (a dirfd or AT_FDCWD), and *owns True when *dfd_out
* must be closed by the caller. dirbuf (>= MAXPATHLEN) is scratch for a parent
* path. Returns 0 on success, -1 (errno set) on error. The caller must already
* have confirmed vfs_relpath_active() (otherwise it does the plain libc op). */
int vfs_twopath_side(const char *path, int side_flags, const char **bname,
int *dfd_out, BOOL *owns, char *dirbuf, size_t dirbufsz)
{
*owns = False;
#ifdef AT_FDCWD
const char *slash = strrchr(path, '/');
size_t dlen;
#if defined O_NOFOLLOW && defined O_DIRECTORY
if (((side_flags & VFS_OPERATOR_PATH) || *path == '/')
&& !vfs_symlink_optout_allowed()) {
int dfd = vfs_owner_walk_parent(path, bname, 1);
if (dfd < 0)
return -1;
*dfd_out = dfd;
*owns = True;
return 0;
}
#endif
if (*path == '/' || !slash) {
/* absolute under --insecure-links, or a bare name: AT_FDCWD + path. */
*bname = path;
*dfd_out = AT_FDCWD;
return 0;
}
dlen = (size_t)(slash - path);
if (dlen >= dirbufsz) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(dirbuf, path, dlen);
dirbuf[dlen] = '\0';
*bname = slash + 1;
*dfd_out = vfs_resolve_open(NULL, dirbuf, O_RDONLY | O_DIRECTORY, 0);
if (*dfd_out < 0)
return -1;
*owns = True;
return 0;
#else
(void)side_flags; (void)dirbuf; (void)dirbufsz;
*bname = path;
*dfd_out = -1;
return 0;
#endif
}