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).
This commit is contained in:
Andrew Tridgell committed 2026-08-13 10:20:13 +10:00
1 parent 893e88ade1
commit 1030929cba
8 files changed
+163 -37

No files matched your search

+23 -6
View File
@@ -1266,12 +1266,29 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx,
* hard_link_one() path above and basis_link_stat's !am_daemon gate).
* fname is the transfer destination (secure receiver resolve). */
if (vfs_link_at(cmpbuf, fname, !am_daemon ? VFS_OPERATOR_PATH : 0, 0) < 0) {
rsyserr(FERROR_XFER, errno,
"failed to hard-link %s with %s",
cmpbuf, fname);
return j;
}
if (preserve_hard_links && F_IS_HLINKED(file))
/* CAN_HARDLINK_SYMLINK/_SPECIAL answer for whatever
* filesystem the build tree sat on; the destination is
* free to disagree, and one host can hold both (macOS
* builds on APFS, backs up to HFS+). A refusal here is
* that same answer arriving late, so fall back to a copy
* as a build without the macro does -- the caller creates
* the entry either way, so failing the transfer only cost
* the exit status.
*
* Every errno, as the regular-file path next door already
* does (try_dests_reg -> hard_link_one -> try_a_copy).
* Picking out the "cannot" errnos is not possible anyway:
* link(2) documents EPERM both for a filesystem with no
* hard-link support and for an ordinary permission
* refusal, and FUSE reports ENOSYS for the same thing.
*
* The rest report themselves: ENOSPC/EDQUOT/EROFS fail the
* copy too, EMLINK and EXDEV mean it was never linkable.
* EIO alone goes unremarked, deliberately -- a diagnostic
* here lands in --link-dest's itemised output. */
cannot_hardlink = 1;
match_level = 2;
} else if (preserve_hard_links && F_IS_HLINKED(file))
finish_hard_link(file, fname, ndx, NULL, itemizing, code, -1);
} else
#endif
+1 -1
View File
@@ -60,7 +60,7 @@ int preserve_executability = 0;
int preserve_devices = 0;
int preserve_specials = 0;
int drop_devices = 0;
char *confine_root = NULL; /* --confine-root: see syscall.c */
char *confine_root = NULL; /* --confine-root: see vfs/dirstack.c */
unsigned int confine_rootlen = 0;
int preserve_uid = 0;
int preserve_gid = 0;
+1 -1
View File
@@ -122,7 +122,7 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags
* recv_files caller) may be absolute (module_dir-prefixed on a non-chroot
* daemon) and traverse a symlink the vfs_resolve_open path can't confine:
* resolve it with the ownership walk, which follows a uid0/euid-owned symlink
* but refuses a foreign one AND (via abspath_excluded_by_module) refuses a
* but refuses a foreign one AND (via abspath_outside_confinement) refuses a
* target the module's exclude hides -- closing the partial-dir exclude bypass. */
if (is_operator) {
char fullpath[MAXPATHLEN];
+2
View File
@@ -47,6 +47,8 @@ size_t max_alloc = (size_t)-1; /* test helpers are not memory-constrained;
char *partial_dir;
char *module_dir;
int module_dirfd = -1;
char *confine_root;
unsigned int confine_rootlen = 0;
/* vfs.curr_dir[]/vfs.curr_dir_len (read by vfs_resolve_open) are defined in
* syscall.c, which every helper links -- no stub needed here. */
filter_rule_list daemon_filter_list;
+94 -16
View File
@@ -60,24 +60,102 @@ static int path_within(const char *root, size_t rootlen, const char *path)
* 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. */
int abspath_excluded_by_module(const char *abspath, int is_operator)
/* 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: vfs.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 || !abspath || !vfs.module_dir)
if (am_daemon) {
*lenp = vfs.module_dirlen;
return vfs.module_dir;
}
*lenp = confine_rootlen;
return confine_root;
}
/* Split the "/proc/<self|pid>/fd" prefix off `p`, returning the tail -- "" for
* the pin directory itself, otherwise a string starting with '/'. NULL when `p`
* is not in the fd-pin namespace at all. */
const char *vfs_fd_pin_tail(const char *p)
{
const char *s;
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, "/proc/self/fd/7" -- the one spelling 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 = vfs_fd_pin_tail(p);
if (!tail || *tail != '/')
return 0;
if (vfs.module_dirlen <= 1) /* module root is "/": nothing is outside */
for (++tail; *tail >= '0' && *tail <= '9'; tail++) {}
return *tail == '\0' && tail[-1] != '/';
}
int abspath_outside_confinement(const char *abspath, int is_operator)
{
unsigned int rootlen;
const char *root = confinement_root(&rootlen);
char pinned[MAXPATHLEN];
if (!root || !abspath)
return 0;
if (path_within(vfs.module_dir, vfs.module_dirlen, abspath))
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 (is_operator); 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. */
if (path_within(abspath, strlen(abspath), vfs.module_dir))
return 0; /* ancestor of the module root: still descending */
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 = vfs_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 is_operator ? 1 : 0;
pinned[n] = '\0';
abspath = pinned;
}
}
if (path_within(root, rootlen, abspath))
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
* (is_operator); 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. */
if (!*abspath || path_within(abspath, strlen(abspath), root))
return 0; /* ancestor of the root: still descending */
return is_operator ? 1 : 0;
}
@@ -197,7 +275,7 @@ int ds_descend(struct dirstack *ds, const char *part, int *hops)
* symlink that redirected the walk into an excluded subtree). */
/* The strict resolver stays confined beneath the anchor (within the
* module), so this never actually refuses; pass is_operator=0. */
if (abspath_excluded_by_module(ds->abspath, 0)) {
if (abspath_outside_confinement(ds->abspath, 0)) {
errno = ELOOP;
return -1;
}
+36 -10
View File
@@ -24,7 +24,7 @@
/* Advance the tracked absolute path `abspath` by one resolved component,
* normalizing "." and ".." exactly as openat() does so the module-confinement
* check (abspath_excluded_by_module) sees the REAL resolved target. -1/
* 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)
{
@@ -88,14 +88,36 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz
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 "/". */
/* 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];
abspath[0] = '\0';
if (am_daemon && vfs.module_dir && vfs.module_dir[0] == '/')
strlcpy(abspath, vfs.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: vfs.curr_dir is the lexical name
* vfs_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. */
int pin_transit = !am_daemon && confine_root && vfs_fd_pin_tail(path) != NULL;
/* Path-walk state. `remaining` is the unconsumed tail; we splice
* symlink targets back into it as we go. Sized 2x MAXPATHLEN so a
@@ -149,7 +171,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz
saved_errno = errno;
goto out;
}
if (abspath_excluded_by_module(abspath, is_operator)) {
if (!pin_transit && abspath_outside_confinement(abspath, is_operator)) {
saved_errno = ELOOP;
goto out;
}
@@ -204,6 +226,10 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz
}
dfd_owns = 1;
abspath[0] = '\0'; /* followed an absolute target: restart from "/" */
/* "self" resolves to "<pid>", still inside the pin;
* the magic link itself lands elsewhere and ends the
* exemption. Never turns back on. */
pin_transit = pin_transit && vfs_fd_pin_tail(rebuilt) != NULL;
char *p = rebuilt;
while (*p == '/') p++;
strlcpy(remaining, p, sizeof remaining);
@@ -219,7 +245,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz
saved_errno = errno;
goto out;
}
if (abspath_excluded_by_module(abspath, is_operator)) {
if (!pin_transit && abspath_outside_confinement(abspath, is_operator)) {
saved_errno = ELOOP;
goto out;
}
@@ -243,7 +269,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz
saved_errno = errno;
goto out;
}
if (abspath_excluded_by_module(abspath, is_operator)) {
if (!pin_transit && abspath_outside_confinement(abspath, is_operator)) {
saved_errno = ELOOP;
goto out;
}
@@ -337,7 +363,7 @@ int vfs_owner_walk_parent(const char *path, const char **bname, int is_operator)
/* owner_walk only resolved the PARENT; check the resolved leaf too, so a
* symlinked operator path cannot act on a leaf that resolves OUTSIDE the
* module in an otherwise-served dir. (The module exclude/filter is name-
* based and not enforced here -- see abspath_excluded_by_module.) */
* based and not enforced here -- see abspath_outside_confinement.) */
if (pabs[0]) {
char leafabs[MAXPATHLEN];
if (snprintf(leafabs, sizeof leafabs, "%s/%s", pabs, *bname) >= (int)sizeof leafabs) {
@@ -345,7 +371,7 @@ int vfs_owner_walk_parent(const char *path, const char **bname, int is_operator)
errno = ENAMETOOLONG; /* fail closed, never skip the check */
return -1;
}
if (abspath_excluded_by_module(leafabs, is_operator)) {
if (abspath_outside_confinement(leafabs, is_operator)) {
close(dfd);
errno = ELOOP;
return -1;
+1 -1
View File
@@ -195,7 +195,7 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath,
char leafabs[MAXPATHLEN];
if (snprintf(leafabs, sizeof leafabs, "%s/%s", ds.abspath, part)
< (int)sizeof leafabs
&& abspath_excluded_by_module(leafabs, 0)) {
&& abspath_outside_confinement(leafabs, 0)) {
errno = ELOOP;
goto cleanup;
}
+5 -2
View File
@@ -33,6 +33,8 @@ extern int copy_links;
extern int copy_unsafe_links;
extern int insecure_links;
extern int module_id;
extern char *confine_root; /* --confine-root, or NULL; see confinement_root() */
extern unsigned int confine_rootlen;
/* Dry-run / read-only guard macros shared by the syscall wrappers. */
#define RETURN_ERROR_IF(x,e) \
@@ -52,7 +54,8 @@ extern int module_id;
/* Module-confinement helpers (pure logic, always compiled). */
int path_has_dotdot_component(const char *path);
int abspath_excluded_by_module(const char *abspath, int is_operator);
int abspath_outside_confinement(const char *abspath, int is_operator);
const char *vfs_fd_pin_tail(const char *p);
/* Per-operand parent resolver for the two-path ops (vfs_rename_at/vfs_link_at). */
int vfs_twopath_side(const char *path, int side_flags, const char **bname,
@@ -76,7 +79,7 @@ struct dirstack {
int fds[DS_MAXDEPTH]; /* fds[0] = anchor (borrowed); fds[top] = current dir */
int top;
/* Absolute path of fds[top], maintained as we descend/pop, for the
* exclude-aware refusal (abspath_excluded_by_module). Empty unless the
* exclude-aware refusal (abspath_outside_confinement). 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];