Files
Andrew Tridgell fca1d10ff5 vfs: adapt the merged-in base changes to the VFS layer
The base gained 99 commits since the last rebase.  Nine files needed a
hand-port because the change lands on code the VFS split moved or
renamed, and syscall.c no longer exists here:

- backup.c: make_path() now runs on a private copy of backup_dir_buf
  (c933f622), so vfs_make_path() takes dirbuf and drops the restore.
- clientserver.c: keep both the module-root snapshot and the new
  daemon_config_filter_file window.
- exclude.c: the peer-driven merge-file confinement is the
  vfs_open_owner_walk() is_operator argument, not a global.
- fileio.c: the coalesced --sparse writer's new helpers use
  vfs_lseek()/vfs_punch_hole().
- generator.c/vfs/mknod.c: gen_entry_mknod() falls back through
  vfs_mknod(); the atfd path keys its mknodat() off HAVE_MKNODAT.
- receiver.c: secure_recv_open() passes VFS_OPERATOR_PATH instead of
  toggling operator_path_resolve; open_readonly_inplace() uses the VFS
  stat/chmod/open wrappers.
- sender.c: absolute --relative cleanup anchors at "/" via
  vfs_resolve_open(), the copy-links walk uses
  vfs_resolve_open_at_beneath(), and the source removal goes through
  vfs_unlink().
- vfs/chmod.c, vfs/chown.c: VFS_OPERATOR_PATH now takes the ownership
  walk, and the no-follow chmod grows the non-Linux fd path.
- vfs/secure_open.c: the fd-anchored resolver splits into a shared
  internal with an allow-dotdot entry point, and secure_walk_at() routes
  a literal "."/".." through ds_descend() before the leaf fast paths.

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

348 lines
12 KiB
C

/*
* vfs/chmod.c - chmod wrappers (path, parent-resolved, held-dirfd).
*
* Includes the platform-specific lchmod/setattrlist/SYS_fchmodat2 handling and
* the leaf-safe do_fchmodat_nofollow helper.
*
* 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"
#ifdef HAVE_SYS_ATTR_H
#include <sys/attr.h> /* for the macOS setattrlist() chmod path */
#endif
#ifdef __linux__
#include <sys/syscall.h> /* SYS_fchmodat2 raw-syscall wrapper */
#endif
#ifdef HAVE_CHMOD
static int vfs__chmod_plain(const char *path, mode_t mode)
{
static int switch_step = 0;
int code;
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
RETURN_ERROR_IF_NULL(path);
switch (switch_step) {
#ifdef HAVE_LCHMOD
case 0:
if ((code = lchmod(path, mode & CHMOD_BITS)) == 0)
break;
if (errno == ENOSYS)
switch_step++;
else if (errno != ENOTSUP)
break;
#endif
/* FALLTHROUGH */
default:
if (S_ISLNK(mode)) {
# if defined HAVE_SETATTRLIST
struct attrlist attrList;
uint32_t m = mode & CHMOD_BITS; /* manpage is wrong: not mode_t! */
memset(&attrList, 0, sizeof attrList);
attrList.bitmapcount = ATTR_BIT_MAP_COUNT;
attrList.commonattr = ATTR_CMN_ACCESSMASK;
if ((code = setattrlist(path, &attrList, &m, sizeof m, FSOPT_NOFOLLOW)) == 0)
break;
if (errno == ENOTSUP)
code = 1;
# else
code = 1;
# endif
} else
code = chmod(path, mode & CHMOD_BITS); /* DISCOURAGED FUNCTION */
break;
}
if (code != 0 && (preserve_perms || preserve_executability))
return code;
return 0;
}
/* chmod `name` relative to dfd without following a final-component symlink.
* The held parent fd confines the ancestors; this closes the leaf race (an
* attacker swapping the leaf to a symlink that fchmodat(...,0) would follow out
* of the tree).
*
* Never follows the leaf: a regular file or dir is pinned via
* openat(O_NOFOLLOW) and chmod'd with fchmod() (leaf-safe, every kernel, and
* fakeroot-wrappable unlike the raw fchmodat2() syscall); a symlink leaf is
* refused (ELOOP, or EMLINK/EFTYPE on the BSDs). Other types or an open
* failure fall to fchmodat(AT_SYMLINK_NOFOLLOW) (a real no-follow chmod on
* glibc>=2.32 / Linux>=6.6), then the raw fchmodat2() syscall. If no
* no-follow primitive exists we skip with a warning rather than follow the
* leaf.
*
* A FIFO takes the fd path on Linux and the pathname path elsewhere -- see the
* S_ISFIFO arm below for why, and for what that costs. Note the type used to
* choose between them comes from the lstat above, so a leaf swapped between
* that and the open is classified by what it WAS: an observed regular file or
* dir that becomes a FIFO is still opened. O_NOFOLLOW rejects symlinks, not
* type changes. Constraining the open to the observed type would close that;
* it is not done here. */
static int do_fchmodat_nofollow(int dfd, const char *name, mode_t mode)
{
#if defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW
mode &= CHMOD_BITS;
# ifdef O_NOFOLLOW
{
STRUCT_STAT st;
int oflags = O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY;
if (vfs_lstat(dfd, name, &st, 0) < 0)
return -1;
if (S_ISLNK(st.st_mode)) {
errno = ELOOP; /* refuse to chmod through a symlink leaf */
return -1;
}
if (S_ISREG(st.st_mode) || S_ISDIR(st.st_mode) || S_ISFIFO(st.st_mode)) {
int fd;
# ifndef __linux__
/* Never open a FIFO here. Opening one -- even O_NONBLOCK --
* makes this process a reader for as long as the descriptor
* lives, which wakes a writer blocked in open(O_WRONLY) and
* can cost it a SIGPIPE or the bytes it writes before we
* close. The pathname call reaches the same end state
* without that: it succeeds outright when the mode is
* grantable, and when macOS refuses an ungrantable setgid
* with EPERM (having applied nothing), asking again without
* that bit gives exactly what fchmod() would have -- it drops
* the bit it cannot grant and applies the ordinary ones.
* Measured on macOS: fchmodat(2750) EPERM leaving 0600,
* fchmodat(0750) ok giving 0750, for a FIFO and a directory
* alike.
*
* This is a pathname call, so unlike the descriptor path it
* does not pin the inode; a leaf swapped for another object
* of the same name is chmod'd instead. AT_SYMLINK_NOFOLLOW
* still keeps it off a symlink's target. That trade buys
* away the reader hazard, and only for FIFOs.
*
* Only S_ISGID is retried. An ungrantable S_ISUID would
* still fail where fchmod() would have cleared it, but
* setuid is meaningless on a FIFO and the behaviour is
* undemonstrated, so it is not coded for.
*
* Linux keeps the fd-first order it has always had. */
if (S_ISFIFO(st.st_mode)) {
if (fchmodat(dfd, name, mode, AT_SYMLINK_NOFOLLOW) == 0)
return 0;
if (errno == EPERM && (mode & S_ISGID)
&& fchmodat(dfd, name, mode & ~S_ISGID,
AT_SYMLINK_NOFOLLOW) == 0)
return 0;
return -1;
}
# endif
# ifdef O_CLOEXEC
oflags |= O_CLOEXEC;
# endif
fd = openat(dfd, name, oflags);
if (fd >= 0) {
int r = fchmod(fd, mode), e = errno;
close(fd);
errno = e;
return r;
}
/* A leaf swapped for a symlink between the lstat above and
* this open: refuse rather than fall through. The errno is
* not the same everywhere -- Linux/Solaris ELOOP, FreeBSD
* EMLINK, NetBSD EFTYPE. */
if (errno == ELOOP
# ifdef EMLINK
|| errno == EMLINK
# endif
# ifdef EFTYPE
|| errno == EFTYPE
# endif
)
return -1; /* raced to a symlink: refuse */
/* otherwise (e.g. EACCES on an unreadable file) fall through */
}
}
# endif
# if defined __linux__
{
int r = fchmodat(dfd, name, mode, AT_SYMLINK_NOFOLLOW);
if (r == 0)
return 0;
if (errno != ENOTSUP && errno != EOPNOTSUPP && errno != ENOSYS)
return r; /* a real error (EPERM, ENOENT, ...) */
}
# ifdef SYS_fchmodat2
{
int r = syscall(SYS_fchmodat2, dfd, name, (unsigned int)mode, AT_SYMLINK_NOFOLLOW);
if (r == 0)
return 0;
if (errno != ENOSYS && errno != EPERM && errno != EOPNOTSUPP)
return r;
}
# endif
/* No symlink-safe chmod primitive here: skip rather than follow the leaf. */
rprintf(FWARNING, "vfs_chmod: no symlink-safe chmod for \"%s\"; mode not set\n", name);
return 1;
# else
return fchmodat(dfd, name, mode, AT_SYMLINK_NOFOLLOW);
# endif
#else
(void)dfd;
(void)mode;
/* No symlink-safe chmod primitive here: skip rather than follow the leaf. */
rprintf(FWARNING, "vfs_chmod: no symlink-safe chmod for \"%s\"; mode not set\n", name);
return 1;
#endif
}
/*
Symlink-race-safe variant of vfs_chmod() for receiver-side use.
Threat model: on a daemon running with "use chroot = no" (the prerequisite
for CVE-2026-29518), a local attacker can race a symlink swap of one of
the parent directory components of a path the receiver is about to chmod.
Because chmod() resolves symlinks at every component, the swap redirects
the chmod outside the receiver's confinement.
Defence: open the *parent* directory of fname under vfs_resolve_open()
(a portable per-component O_NOFOLLOW walk on held parent dirfds) and do
fchmodat() against that dirfd. A symlink substituted into one of the parent
components is then either followed within the tree (legitimate dir-symlinks
still work) or rejected (escape attempts fail).
Final-component handling matches vfs_chmod(): fchmodat() with flag 0
follows a symlink at the final component, which is the same behaviour as
chmod() and matches every current call site (the file being chmod'd is
one the receiver itself just created or transferred). For the rare case
where the caller wants to chmod a symlink-as-an-object (S_ISLNK in the
mode bits), we fall through to vfs_chmod() which has portability code for
that case.
Falls back to vfs_chmod() for absolute paths and for paths with no parent
component, where there is nothing to protect against.
*/
static int vfs__chmod_secure(const char *fname, mode_t mode, int flags)
{
#ifdef AT_FDCWD
char dirpath[MAXPATHLEN];
const char *bname;
const char *slash;
int dfd, ret, e;
size_t dlen;
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
#if defined O_NOFOLLOW && defined O_DIRECTORY
/* Operator-supplied path: resolve the parent via the ownership walk, as
* the other VFS wrappers do. Without this the caller's VFS_OPERATOR_PATH
* has no effect here, and an absolute name would fall straight through to
* the unconfined full-path chmod. S_ISLNK(mode) still needs the plain
* lchmod()/setattrlist() handling. */
if ((flags & VFS_OPERATOR_PATH) && fname && *fname && !S_ISLNK(mode)) {
if (vfs_symlink_optout_allowed())
return vfs__chmod_plain(fname, mode);
dfd = vfs_owner_walk_parent(fname, &bname, 1);
if (dfd < 0)
return -1;
ret = do_fchmodat_nofollow(dfd, bname, mode);
e = errno;
close(dfd);
errno = e;
return ret;
}
#endif
/* Only the daemon-without-chroot case is exposed to the symlink-
* race attack: a chroot already confines the receiver, and a
* non-daemon rsync runs with the user's own authority so a
* symlink they planted can only redirect to files they could
* already access. Everywhere else, fall through to plain
* vfs_chmod() to avoid the dirfd-open overhead on every call. */
if (!vfs_relpath_active())
return vfs__chmod_plain(fname, mode);
if (!fname || !*fname || *fname == '/' || S_ISLNK(mode))
return vfs__chmod_plain(fname, mode);
slash = strrchr(fname, '/');
if (!slash)
return vfs__chmod_plain(fname, mode);
dlen = slash - fname;
if (dlen >= sizeof dirpath) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(dirpath, fname, dlen);
dirpath[dlen] = '\0';
bname = slash + 1;
dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0);
if (dfd < 0)
return -1;
ret = do_fchmodat_nofollow(dfd, bname, mode);
e = errno;
close(dfd);
errno = e;
return ret;
#else
(void)flags;
return vfs__chmod_plain(fname, mode);
#endif
}
#endif
/* Unified chmod. dirfd == VFS_AT_FDCWD resolves `path`; a real held dirfd makes
* `path` a single component chmod'd (no-follow leaf, via do_fchmodat_nofollow)
* under it. flags: VFS_ALLOW_SYMLINK (trusted, plain chmod), default 0 (secure
* receiver resolve). A symlink-as-object (S_ISLNK(mode)) goes through the plain
* lchmod/setattrlist path. */
#ifdef HAVE_CHMOD
int vfs_chmod(int dirfd, const char *path, mode_t mode, int flags)
{
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
RETURN_ERROR_IF_NULL(path);
if (dirfd != VFS_AT_FDCWD) {
#ifdef AT_FDCWD
/* Held-fd: reject empty, multi-component and ".." (writing the
* parent of the pinned dir); "." (chmod the dir itself) is a
* legitimate single-component op. */
if (!*path || strchr(path, '/')
|| (path[0] == '.' && path[1] == '.' && path[2] == '\0')) {
errno = EINVAL;
return -1;
}
return do_fchmodat_nofollow(dirfd, path, mode);
#else
(void)dirfd; (void)mode;
errno = ENOSYS;
return -1;
#endif
}
if (flags & VFS_ALLOW_SYMLINK)
return vfs__chmod_plain(path, mode);
return vfs__chmod_secure(path, mode, flags);
}
/* Mode on an already-open fd (no path, no symlink to follow): the race-free
* counterpart for a pinned cross-tree operator leaf -- see set_file_attrs(). */
int vfs_fchmod(int fd, mode_t mode)
{
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
return fchmod(fd, mode);
}
#endif