confine the remaining cross-tree operator-path syscalls

The backup-dir fix pinned chmod/chown for an operator leaf; extend the same
confinement to every other sink that re-resolves a cross-tree operator path
(an absolute --temp-dir/--partial-dir/--*-dest), each confirmed by a cross-uid
race PoC that is RED on stock 3.2.7 and GREEN here:

- set_file_attrs (rsync.c): route times (do_futimens), and -- by aliasing
  held_fd to the pinned op_leaf_fd -- the xattr/ACL ops through the same
  O_NOFOLLOW leaf fd, not a re-resolvable path.  A raced/refused pin skips them
  (op_refuse) rather than redirecting.  finish_transfer now wraps the pre-rename
  set_file_attrs in operator_path_resolve so an absolute --temp-dir temp file's
  metadata is pinned too (in-tree temps keep their held dirfd, so op_pin is off).

- do_rename_at / do_link_at (syscall.c): an ABSOLUTE side was left at AT_FDCWD
  and followed a flipped parent symlink, letting a name-disclosed --temp-dir /
  predictable --partial-dir rename pull an attacker file into the destination
  (content injection).  Resolve an absolute (operator) side via the ownership
  walk -- with module-exclude enforced -- while a relative (transfer) side stays
  on secure_relative_open.  --insecure-links keeps the legacy path.

- secure_basis_open (receiver.c): an alt-dest basis read
  (--copy-dest/--compare-dest/--link-dest) on a non-daemon receiver used a bare
  do_open; route it through the ownership walk (refuses a foreign-owned basis
  symlink, still allows the "../sibling" basis of #915).  Daemons keep their
  existing confinement branch.

- configure.ac: probe futimens (do_futimens is gated on HAVE_FUTIMENS).

New fd wrappers: do_fchmod, do_fchown, do_futimens.  Documented residuals left
as-is: copy_altdest_file's basis copy (routing it re-opens copy-xattrs-symlink-
race; basis_link_stat already refuses the foreign symlink), crtimes
(do_setattrlist_crtime/do_SetFileTime, path-based on macOS/Cygwin), and
device/socket leaf metadata (pinning a device via open has side effects).
This commit is contained in:
Andrew Tridgell committed 2026-06-27 18:24:31 +10:00
1 parent ea9e707b46
commit 433ed0e1db
4 files changed
+132 -26

No files matched your search

+1 -1
View File
@@ -897,7 +897,7 @@ AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo fdopendir \
setlocale setmode open64 lseek64 mkstemp64 mtrace va_copy __va_copy \
seteuid strerror putenv iconv_open locale_charset nl_langinfo getxattr \
extattr_get_link sigaction sigprocmask setattrlist getgrouplist \
initgroups utimensat posix_fallocate attropen setvbuf nanosleep usleep \
initgroups utimensat futimens posix_fallocate attropen setvbuf nanosleep usleep \
setenv unsetenv)
dnl dirfd() is a macro or static inline on several systems (the BSDs), so the
+14 -5
View File
@@ -25,6 +25,7 @@
extern int dry_run;
extern int do_xfers;
extern int am_root;
extern int am_daemon;
extern int am_server;
extern int inc_recurse;
extern int log_before_transfer;
@@ -960,11 +961,16 @@ int recv_files(int f_in, int f_out, char *local_name)
const char *slash = strrchr(fnamecmp, '/');
fd1 = do_open_atfd(bdfd, slash ? slash + 1 : fnamecmp, O_RDONLY, 0);
} else {
/* A --partial-dir basis is an operator/peer path: resolve it with
* the exclude-aware ownership walk so a symlinked partial-dir
* can't read (and feed back as delta) a file in an excluded
* subtree. */
if (fnamecmp_type == FNAMECMP_PARTIAL_DIR)
/* An operator-supplied basis -- a --partial-dir, or an
* alt-dest basedir (--copy-dest/--compare-dest/--link-dest) --
* is a peer/operator path: resolve it with the exclude-aware
* ownership walk so a flipped foreign-owned parent symlink can't
* read (and feed back as delta) an out-of-tree / excluded file.
* The walk still allows the legitimate "../sibling" basis (#915)
* and the operator's own uid0/euid symlinks. A daemon keeps its
* stronger confinement branch in secure_basis_open(), so only
* route the alt-dest basedir read through the walk off-daemon. */
if ((basedir && !am_daemon) || fnamecmp_type == FNAMECMP_PARTIAL_DIR)
operator_path_resolve = 1;
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
operator_path_resolve = 0;
@@ -983,7 +989,10 @@ int recv_files(int f_in, int f_out, char *local_name)
basedir = basis_dir[0];
fnamecmp = fname;
fnamecmp_type = FNAMECMP_BASIS_DIR_LOW;
if (!am_daemon)
operator_path_resolve = 1;
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
operator_path_resolve = 0;
}
}
+29 -9
View File
@@ -583,6 +583,10 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
* legacy path op rather than spuriously failing a real file. */
if (op_leaf_fd < 0 && (am_root > 0 || errno == ELOOP))
op_refuse = 1;
#if defined SUPPORT_XATTRS || defined SUPPORT_ACLS
if (op_leaf_fd >= 0)
held_fd = op_leaf_fd; /* xattr/ACL ops below pin to this leaf fd too */
#endif
}
if (inherit && S_ISDIR(new_mode) && sxp->st.st_mode & S_ISGID) {
@@ -595,7 +599,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
new_mode = tweak_mode(new_mode, daemon_chmod_modes);
#ifdef SUPPORT_ACLS
if (preserve_acls && !S_ISLNK(file->mode) && !ACL_READY(*sxp))
if (preserve_acls && !S_ISLNK(file->mode) && !ACL_READY(*sxp) && !op_refuse)
get_acl_fdat(held_fd, dfd, leaf, fname, sxp);
#endif
@@ -657,9 +661,9 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
}
#ifdef SUPPORT_XATTRS
if (am_root < 0)
if (am_root < 0 && !op_refuse)
set_stat_xattr(fname, file, new_mode, held_fd);
if (preserve_xattrs && fnamecmp)
if (preserve_xattrs && fnamecmp && !op_refuse)
set_xattr(fname, file, fnamecmp, sxp, held_fd);
#endif
@@ -714,9 +718,19 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
}
#endif
if (updated & (UPDATED_MTIME|UPDATED_ATIME)) {
int ret = dfd >= 0 ? set_times_at(dfd, leaf, &sx2.st) : -2;
if (ret == -2)
ret = set_times(fname, &sx2.st);
int ret;
#ifdef HAVE_FUTIMENS
if (op_leaf_fd >= 0)
ret = do_futimens(op_leaf_fd, &sx2.st);
else
#endif
if (op_refuse)
ret = (errno = ELOOP, -1);
else {
ret = dfd >= 0 ? set_times_at(dfd, leaf, &sx2.st) : -2;
if (ret == -2)
ret = set_times(fname, &sx2.st);
}
if (ret < 0) {
rsyserr(FERROR_XFER, errno, "failed to set times on %s", full_fname(fname));
goto cleanup;
@@ -734,7 +748,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
* If set_acl() changes permission bits in the process of setting
* an access ACL, it changes sxp->st.st_mode so we know whether we
* need to chmod(). */
if (preserve_acls && !S_ISLNK(new_mode)) {
if (preserve_acls && !S_ISLNK(new_mode) && !op_refuse) {
if (set_acl_fdat(held_fd, dfd, leaf, fname, file, sxp, new_mode) > 0)
updated |= UPDATED_ACLS;
}
@@ -766,7 +780,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
}
cleanup:
#if defined SUPPORT_XATTRS || defined SUPPORT_ACLS
if (held_fd >= 0)
if (held_fd >= 0 && held_fd != op_leaf_fd) /* may alias op_leaf_fd (cross-tree) */
close(held_fd);
#endif
if (op_leaf_fd >= 0)
@@ -839,9 +853,15 @@ int finish_transfer(const char *fname, const char *fnametmp,
fnamecmp = get_backup_name(fname);
}
/* Change permissions before putting the file into place. */
/* Change permissions before putting the file into place. An absolute
* --temp-dir/--partial-dir leaves fnametmp on an operator path with no held
* dirfd, so resolve its metadata through the ownership walk (op_pin); a
* flipped temp-dir parent then can't redirect the chmod/chown/times/etc.
* (in-tree temps keep their held dirfd, so op_pin stays off there). */
operator_path_resolve = 1;
set_file_attrs(fnametmp, file, NULL, fnamecmp,
ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME);
operator_path_resolve = 0;
/* move tmp file over real file */
if (DEBUG_GTE(RECV, 1))
+88 -11
View File
@@ -837,9 +837,21 @@ int do_link_at(const char *old_path, const char *new_path)
* "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. */
* confinement of a relative destination. An absolute side is an operator
* path resolved via the ownership walk (foreign-owned parent symlink refused;
* --insecure-links keeps the legacy AT_FDCWD path). */
if (*old_path == '/') {
old_bname = old_path;
#if defined O_NOFOLLOW && defined O_DIRECTORY
if (!symlink_optout_allowed()) {
operator_path_resolve = 1; /* operator side: enforce module-exclude */
old_dfd = owner_walk_parent(old_path, &old_bname);
operator_path_resolve = 0;
if (old_dfd < 0)
return -1;
old_owns = True;
} else
#endif
old_bname = old_path;
} else if (old_slash) {
old_dlen = old_slash - old_path;
if (old_dlen >= sizeof old_dirpath) { errno = ENAMETOOLONG; return -1; }
@@ -855,7 +867,21 @@ int do_link_at(const char *old_path, const char *new_path)
}
if (*new_path == '/') {
new_bname = new_path;
#if defined O_NOFOLLOW && defined O_DIRECTORY
if (!symlink_optout_allowed()) {
operator_path_resolve = 1; /* operator side: enforce module-exclude */
new_dfd = owner_walk_parent(new_path, &new_bname);
operator_path_resolve = 0;
if (new_dfd < 0) {
e = errno;
if (old_owns) close(old_dfd);
errno = e;
return -1;
}
new_owns = True;
} else
#endif
new_bname = new_path;
} else if (new_slash) {
new_dlen = new_slash - new_path;
if (new_dlen >= sizeof new_dirpath) {
@@ -1577,14 +1603,26 @@ int do_rename_at(const char *old_path, const char *new_path)
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). */
/* Confine each side independently. A *relative* side is a transfer path,
* confined beneath the tree via secure_relative_open(). An *absolute* side is
* an operator path (an absolute --temp-dir/--partial-dir temp file): resolve
* its parent via the ownership walk so a flipped foreign-owned parent symlink
* can't redirect the rename out of tree, while still allowing the operator's
* own dirs/".."/uid0-or-euid symlinks. (--insecure-links keeps the legacy
* unconfined AT_FDCWD path.) Doing each side independently means an absolute
* source never disables confinement of a relative destination. */
if (*old_path == '/') {
old_bname = old_path;
#if defined O_NOFOLLOW && defined O_DIRECTORY
if (!symlink_optout_allowed()) {
operator_path_resolve = 1; /* operator side: enforce module-exclude */
old_dfd = owner_walk_parent(old_path, &old_bname);
operator_path_resolve = 0;
if (old_dfd < 0)
return -1;
old_owns = True;
} else
#endif
old_bname = old_path;
} else if (old_slash) {
old_dlen = old_slash - old_path;
if (old_dlen >= sizeof old_dirpath) {
@@ -1603,7 +1641,21 @@ int do_rename_at(const char *old_path, const char *new_path)
}
if (*new_path == '/') {
new_bname = new_path;
#if defined O_NOFOLLOW && defined O_DIRECTORY
if (!symlink_optout_allowed()) {
operator_path_resolve = 1; /* operator side: enforce module-exclude */
new_dfd = owner_walk_parent(new_path, &new_bname);
operator_path_resolve = 0;
if (new_dfd < 0) {
e = errno;
if (old_owns) close(old_dfd);
errno = e;
return -1;
}
new_owns = True;
} else
#endif
new_bname = new_path;
} else if (new_slash) {
new_dlen = new_slash - new_path;
if (new_dlen >= sizeof new_dirpath) {
@@ -3432,6 +3484,31 @@ int do_fchmod(int fd, mode_t mode)
}
#endif
#ifdef HAVE_FUTIMENS
/* Set times on an already-open fd (the race-free counterpart for a pinned
* cross-tree operator leaf -- see set_file_attrs()). */
int do_futimens(int fd, STRUCT_STAT *stp)
{
struct timespec t[2];
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
t[0].tv_sec = stp->st_atime;
#ifdef ST_ATIME_NSEC
t[0].tv_nsec = stp->ST_ATIME_NSEC;
#else
t[0].tv_nsec = 0;
#endif
t[1].tv_sec = stp->st_mtime;
#ifdef ST_MTIME_NSEC
t[1].tv_nsec = stp->ST_MTIME_NSEC;
#else
t[1].tv_nsec = 0;
#endif
return futimens(fd, t);
}
#endif
#ifdef HAVE_UTIMENSAT
int do_utimensat_atfd(int dfd, const char *name, STRUCT_STAT *stp)
{