diff --git a/.github/workflows/asan-build.yml b/.github/workflows/asan-build.yml index 4f4b6b5ce..8e5b19e91 100644 --- a/.github/workflows/asan-build.yml +++ b/.github/workflows/asan-build.yml @@ -57,7 +57,7 @@ jobs: CC=clang \ CFLAGS="-fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer -g -O1 -DNDEBUG" \ LDFLAGS="-fsanitize=address,undefined" \ - ./configure --with-rrsync --disable-md2man + ./configure --with-rrsync --disable-md2man --enable-strict-confinement - name: make # check-progs builds rsync plus the test helper programs (tls, trimslash, # t_unsafe, ...) that runtests.py requires; plain "make" builds only rsync diff --git a/.gitignore b/.gitignore index f73d65f27..d7116bfe0 100644 --- a/.gitignore +++ b/.gitignore @@ -43,19 +43,19 @@ aclocal.m4 /testrun /trimslash /t_unsafe -/wildtest -/getfsdev -/simdtest /t_acl /t_chmod_secure -/t_clean_fname +/t_rename_secure +/t_secure_relpath +/t_symlink_secure /t_hashtable_overflow /t_iwildmatch -/t_rename_secure +/t_clean_fname /t_safe_arg +/simdtest +/wildtest +/getfsdev /t_safe_arg_main -/t_secure_relpath -/t_symlink_secure /rounding.h /doc/rsync.pdf /doc/rsync.ps diff --git a/Makefile.in b/Makefile.in index 801e8643e..1d50e2fdb 100644 --- a/Makefile.in +++ b/Makefile.in @@ -18,6 +18,9 @@ CXXFLAGS=@CXXFLAGS@ EXEEXT=@EXEEXT@ LDFLAGS=@LDFLAGS@ LIBOBJDIR=lib/ +AR=@AR@ +ARFLAGS=cr +RANLIB=@RANLIB@ INSTALLCMD=@INSTALL@ INSTALLMAN=@INSTALL@ @@ -38,13 +41,13 @@ GENFILES=configure.sh aclocal.m4 config.h.in rsync.1 rsync.1.html \ rsync-ssl.1 rsync-ssl.1.html rsyncd.conf.5 rsyncd.conf.5.html \ @GEN_RRSYNC@ HEADERS=byteorder.h config.h errcode.h proto.h rsync.h ifuncs.h itypes.h inums.h \ - lib/pool_alloc.h lib/mdigest.h lib/md-defines.h + lib/pool_alloc.h lib/mdigest.h lib/md-defines.h vfs/vfs.h LIBOBJ=lib/wildmatch.o lib/compat.o lib/snprintf.o lib/mdfour.o lib/md5.o \ lib/permstring.o lib/pool_alloc.o lib/sysacls.o lib/sysxattrs.o lib/acl.o @LIBOBJS@ zlib_OBJS=zlib/deflate.o zlib/inffast.o zlib/inflate.o zlib/inftrees.o \ zlib/trees.o zlib/zutil.o zlib/adler32.o zlib/compress.o zlib/crc32.o OBJS1_NO_MAIN=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \ - util1.o util2.o checksum.o match.o syscall.o log.o backup.o delete.o + util1.o util2.o checksum.o match.o log.o backup.o delete.o OBJS1=$(OBJS1_NO_MAIN) main.o OBJS2=options.o io.o compat.o hlink.o token.o uidlist.o socket.o hashtable.o \ usage.o fileio.o batch.o clientname.o chmod.o acls.o xattrs.o @@ -52,9 +55,10 @@ OBJS3=progress.o pipe.o @MD5_ASM@ @ROLL_SIMD@ @ROLL_ASM@ DAEMON_OBJ = params.o loadparm.o clientserver.o access.o connection.o authenticate.o popt_OBJS= popt/popt.o popt/poptconfig.o \ popt/popthelp.o popt/poptparse.o popt/poptint.o -OBJS=$(OBJS1) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@ +VFS_OBJ=vfs/vfs.o vfs/dirstack.o vfs/secure_open.o vfs/owner_walk.o vfs/dircache.o vfs/stat.o vfs/rename.o vfs/unlink.o vfs/open.o vfs/chmod.o vfs/symlink.o vfs/link.o vfs/mkdir.o vfs/chown.o vfs/mknod.o vfs/times.o vfs/fileio.o vfs/make_path.o vfs/copy_file.o vfs/robust.o +OBJS=$(OBJS1) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@ libvfs.a -TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@ +TLS_OBJ = tls.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@ libvfs.a # Programs we must have to run the test cases CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \ @@ -67,7 +71,7 @@ CHECK_SYMLINKS = testsuite/chown-fake_test.py testsuite/devices-fake_test.py \ # Objects for CHECK_PROGS to clean CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o t_chmod_secure.o t_rename_secure.o t_symlink_secure.o t_secure_relpath.o t_acl.o t_hashtable_overflow.o t_iwildmatch.o t_clean_fname.o t_safe_arg.o trimslash.o wildtest.o # Compile-only feature-shape checks. -CHECK_COMPILE_OBJS=syscall-no-at-fdcwd.o +CHECK_COMPILE_OBJS=vfs-no-at-fdcwd.o # note that the -I. is needed to handle config.h when using VPATH .c.o: @@ -80,9 +84,20 @@ CHECK_COMPILE_OBJS=syscall-no-at-fdcwd.o all: Makefile rsync$(EXEEXT) stunnel-rsyncd.conf @MAKE_RRSYNC@ @MAKE_MAN@ .PHONY: all -syscall-no-at-fdcwd.o: syscall.c $(HEADERS) - $(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \ - -DRSYNC_TEST_NO_AT_FDCWD -c $(srcdir)/syscall.c -o $@ +# Compile-check the pre-*at() portability tier. syscall.c's *at wrappers were +# split into vfs/, so compile every vfs source with the AT_FDCWD primitives +# undefined (via vfs/vfs_internal.h's RSYNC_TEST_NO_AT_FDCWD block) and confirm +# the fallback arms still build. A shell loop keeps this portable (BSD/Solaris +# make have no pattern rules); the last object compiled is left as the target. +# $(VFS_OBJ:.o=.c) is POSIX suffix substitution, portable across makes. +vfs-no-at-fdcwd.o: $(VFS_OBJ:.o=.c) $(HEADERS) vfs/vfs.h vfs/vfs_internal.h + @rm -f $@ $@.tmp + @for f in $(VFS_OBJ:.o=.c); do \ + echo " no-AT_FDCWD compile-check: $$f"; \ + $(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \ + -DRSYNC_TEST_NO_AT_FDCWD -c $(srcdir)/$$f -o $@.tmp || exit 1; \ + done + @mv $@.tmp $@ .PHONY: install install: all @@ -141,7 +156,17 @@ rrsync: support/rrsync $(OBJS): $(HEADERS) $(CHECK_OBJS): $(HEADERS) +$(VFS_OBJ): $(HEADERS) +$(VFS_OBJ): vfs/vfs_internal.h tls.o xattrs.o: lib/sysxattrs.h + +# The VFS layer is bundled into a static archive linked last on every target so +# that moving a filesystem family between files never breaks a test harness link +# (the linker pulls only the members each program references). +libvfs.a: $(VFS_OBJ) + rm -f $@ + $(AR) $(ARFLAGS) $@ $(VFS_OBJ) + $(RANLIB) $@ usage.o: version.h latest-year.h help-rsync.h help-rsyncd.h git-version.h default-cvsignore.h loadparm.o: default-dont-compress.h daemon-parm.h @@ -202,15 +227,15 @@ getgroups$(EXEEXT): getgroups.o getfsdev$(EXEEXT): getfsdev.o $(CC) $(CFLAGS) $(LDFLAGS) -o $@ getfsdev.o $(LIBS) -TRIMSLASH_OBJ = trimslash.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o +TRIMSLASH_OBJ = trimslash.o util2.o t_stub.o lib/compat.o lib/snprintf.o libvfs.a trimslash$(EXEEXT): $(TRIMSLASH_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(TRIMSLASH_OBJ) $(LIBS) -T_UNSAFE_OBJ = t_unsafe.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o +T_UNSAFE_OBJ = t_unsafe.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o libvfs.a t_unsafe$(EXEEXT): $(T_UNSAFE_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_UNSAFE_OBJ) $(LIBS) -T_HASHTABLE_OVERFLOW_OBJ = t_hashtable_overflow.o hashtable.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o +T_HASHTABLE_OVERFLOW_OBJ = t_hashtable_overflow.o hashtable.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o libvfs.a t_hashtable_overflow$(EXEEXT): $(T_HASHTABLE_OVERFLOW_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_HASHTABLE_OVERFLOW_OBJ) $(LIBS) @@ -218,7 +243,7 @@ T_IWILDMATCH_OBJ = t_iwildmatch.o lib/wildmatch.o t_iwildmatch$(EXEEXT): $(T_IWILDMATCH_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_IWILDMATCH_OBJ) $(LIBS) -T_CLEAN_FNAME_OBJ = t_clean_fname.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o +T_CLEAN_FNAME_OBJ = t_clean_fname.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o libvfs.a t_clean_fname$(EXEEXT): $(T_CLEAN_FNAME_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CLEAN_FNAME_OBJ) $(LIBS) @@ -231,23 +256,23 @@ t_clean_fname$(EXEEXT): $(T_CLEAN_FNAME_OBJ) # GNU-make-only; BSD and Solaris make expand it to nothing. t_safe_arg_main.o: main.c $(HEADERS) $(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) -Dmain=rsync_unused_main -c $(srcdir)/main.c -o t_safe_arg_main.o -T_SAFE_ARG_OBJ = t_safe_arg.o t_safe_arg_main.o $(OBJS1_NO_MAIN) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@ +T_SAFE_ARG_OBJ = t_safe_arg.o t_safe_arg_main.o $(OBJS1_NO_MAIN) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@ libvfs.a t_safe_arg$(EXEEXT): $(T_SAFE_ARG_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SAFE_ARG_OBJ) $(LIBS) -T_CHMOD_SECURE_OBJ = t_chmod_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o +T_CHMOD_SECURE_OBJ = t_chmod_secure.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o libvfs.a t_chmod_secure$(EXEEXT): $(T_CHMOD_SECURE_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CHMOD_SECURE_OBJ) $(LIBS) -T_RENAME_SECURE_OBJ = t_rename_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o +T_RENAME_SECURE_OBJ = t_rename_secure.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o libvfs.a t_rename_secure$(EXEEXT): $(T_RENAME_SECURE_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_RENAME_SECURE_OBJ) $(LIBS) -T_SYMLINK_SECURE_OBJ = t_symlink_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o +T_SYMLINK_SECURE_OBJ = t_symlink_secure.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o libvfs.a t_symlink_secure$(EXEEXT): $(T_SYMLINK_SECURE_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SYMLINK_SECURE_OBJ) $(LIBS) -T_SECURE_RELPATH_OBJ = t_secure_relpath.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o +T_SECURE_RELPATH_OBJ = t_secure_relpath.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o libvfs.a t_secure_relpath$(EXEEXT): $(T_SECURE_RELPATH_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SECURE_RELPATH_OBJ) $(LIBS) @@ -348,10 +373,10 @@ rrsync.1: support/rrsync.1.md md-convert Makefile .PHONY: clean clean: cleantests - rm -f *~ $(OBJS) $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS) @MAKE_RRSYNC@ \ + rm -f *~ $(OBJS) $(VFS_OBJ) libvfs.a $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_COMPILE_OBJS) $(CHECK_COMPILE_OBJS:.o=.o.tmp) $(CHECK_SYMLINKS) @MAKE_RRSYNC@ \ git-version.h rounding rounding.h *.old rsync*.1 rsync*.5 @MAKE_RRSYNC_1@ \ *.html daemon-parm.h help-*.h default-*.h proto.h proto.h-tstamp - rm -f *.gcno *.gcda lib/*.gcno lib/*.gcda zlib/*.gcno zlib/*.gcda popt/*.gcno popt/*.gcda + rm -f *.gcno *.gcda lib/*.gcno lib/*.gcda zlib/*.gcno zlib/*.gcda popt/*.gcno popt/*.gcda vfs/*.gcno vfs/*.gcda rm -rf coverage coverage-tcp coverage-all coverage-fallback .PHONY: cleantests diff --git a/acls.c b/acls.c index d51cbe9fa..52f1d3ed5 100644 --- a/acls.c +++ b/acls.c @@ -1213,7 +1213,7 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname, * the legacy path fallback (op_pin am_root != 0 rule). */ if (fd >= 0) rc = sys_acl_delete_def_fd(fd); - else if (secure_relpath_active() && am_root) { + else if (vfs_relpath_active() && am_root) { errno = ELOOP; rc = -1; } else @@ -1334,7 +1334,7 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname, sxp->st.st_mode = cur_mode; return 0; } - if (secure_relpath_active() && am_root) { + if (vfs_relpath_active() && am_root) { /* Real root always can open its own freshly-staged reg/dir/fifo leaf, * so a missing held fd on a confined receiver means the leaf was raced * to a symlink; sys_acl_set_file() follows the leaf, so refuse rather diff --git a/authenticate.c b/authenticate.c index 3376bb1ed..3e489d6c4 100644 --- a/authenticate.c +++ b/authenticate.c @@ -156,7 +156,7 @@ static const char *check_secret(int module, const char *user, const char *group, if (!fname || !*fname) return "no secrets file"; { - int fd = open_no_attacker_symlinks(fname, O_RDONLY, 0); + int fd = vfs_open_owner_walk(fname, O_RDONLY, 0, 0); if (fd < 0) return "no secrets file"; fh = fdopen(fd, "r"); @@ -166,7 +166,7 @@ static const char *check_secret(int module, const char *user, const char *group, } } - if (do_fstat(fileno(fh), &st) == -1) { + if (vfs_fstat(fileno(fh), &st) == -1) { rsyserr(FLOG, errno, "fstat(%s)", fname); ok = 0; } else if (lp_strict_modes(module)) { @@ -239,10 +239,10 @@ static const char *getpassf(const char *filename) /* --password-file=PATH client open. Its first line is sent as the * auth response, so a planted symlink leaks the target's content - * (e.g. shadow hashes) to a malicious daemon; the do_stat() + * (e.g. shadow hashes) to a malicious daemon; the vfs_stat() * other-access check runs on the target mode and passes 0640 * root:shadow. Refuse symlinks not owned by uid 0 or our euid. */ - if ((fd = open_no_attacker_symlinks(filename, O_RDONLY, 0)) < 0) { + if ((fd = vfs_open_owner_walk(filename, O_RDONLY, 0, 0)) < 0) { rsyserr(FERROR, errno, "could not open password file %s", filename); exit_cleanup(RERR_SYNTAX); } @@ -252,7 +252,7 @@ static const char *getpassf(const char *filename) * path between open and check can't make the owner/mode test * validate a different inode than the one we read the password * from. */ - if (do_fstat(fd, &st) == -1) { + if (vfs_fstat(fd, &st) == -1) { rsyserr(FERROR, errno, "fstat(%s)", filename); exit_cleanup(RERR_SYNTAX); } diff --git a/backup.c b/backup.c index df9dac987..16dc33c8c 100644 --- a/backup.c +++ b/backup.c @@ -30,7 +30,6 @@ extern int preserve_links; extern int safe_symlinks; extern int backup_dir_len; extern unsigned int backup_dir_remainder; -extern int operator_path_resolve; extern char backup_dir_buf[MAXPATHLEN]; extern char *backup_suffix; extern char *backup_dir; @@ -44,19 +43,14 @@ extern char *backup_dir; * backup_metadata_hardened() to tell the two -1 cases apart). */ int backup_metadata_hardened(void) { - return secure_relpath_active() && !symlink_optout_allowed(); + return vfs_relpath_active() && !vfs_symlink_optout_allowed(); } int backup_source_fd(const char *path) { #if defined AT_FDCWD && defined O_NOFOLLOW - if (backup_metadata_hardened() && path && *path) { - int save = operator_path_resolve, fd; - operator_path_resolve = 1; - fd = do_open_at(path, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0); - operator_path_resolve = save; - return fd; - } + if (backup_metadata_hardened() && path && *path) + return vfs_open_at(path, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0, VFS_OPERATOR_PATH); #endif return -1; } @@ -66,7 +60,7 @@ static int validate_backup_dir(void) { STRUCT_STAT st; - if (do_lstat_at(backup_dir_buf, &st) < 0) { + if (vfs_lstat(VFS_AT_FDCWD, backup_dir_buf, &st, VFS_OPERATOR_PATH) < 0) { if (errno == ENOENT) return 0; rsyserr(FERROR, errno, "backup lstat %s failed", backup_dir_buf); @@ -125,7 +119,7 @@ static BOOL copy_valid_path(const char *fname) for ( ; b; name = b + 1, b = strchr(name, '/')) { *b = '\0'; - while (do_mkdir_at(backup_dir_buf, ACCESSPERMS) < 0) { + while (vfs_mkdir(VFS_AT_FDCWD, backup_dir_buf, ACCESSPERMS, VFS_OPERATOR_PATH) < 0) { if (errno == EEXIST) { val = validate_backup_dir(); if (val > 0) @@ -141,7 +135,7 @@ static BOOL copy_valid_path(const char *fname) /* Try to transfer the directory settings of the actual dir * that the files are coming from. */ - if (x_stat(rel, &sx.st, NULL) < 0) + if (x_stat(rel, &sx.st, NULL, VFS_OPERATOR_PATH) < 0) rsyserr(FERROR, errno, "backup stat %s failed", full_fname(rel)); else { struct file_struct *file; @@ -170,7 +164,7 @@ static BOOL copy_valid_path(const char *fname) close(bfd); } #endif - set_file_attrs(backup_dir_buf, file, NULL, NULL, 0); + set_file_attrs(backup_dir_buf, file, NULL, NULL, ATTRS_OPERATOR_PATH); unmake_file(file); } @@ -203,7 +197,7 @@ char *get_backup_name(const char *fname) } if (backup_dir_len > 1) dirbuf[backup_dir_len-1] = '\0'; - ret = make_path(dirbuf, 0); + ret = vfs_make_path(dirbuf, 0, VFS_OPERATOR_PATH); if (ret < 0) return NULL; initialized = 1; @@ -236,7 +230,11 @@ static inline int link_or_rename(const char *from, const char *to, if (IS_SPECIAL(stp->st_mode) || IS_DEVICE(stp->st_mode)) return 0; /* Use copy code. */ #endif - if (do_link_at(from, to) == 0) { + /* from = the live dest file being backed up (a transfer path); to = the + * --backup-dir path (operator). Per-operand policy keeps the transfer + * source under the secure receiver resolve and only owner-walks the + * operator backup parent. */ + if (vfs_link_at(from, to, 0, VFS_OPERATOR_PATH) == 0) { if (DEBUG_GTE(BACKUP, 1)) rprintf(FINFO, "make_backup: HLINK %s successful.\n", from); return 2; @@ -246,11 +244,12 @@ static inline int link_or_rename(const char *from, const char *to, return 0; } #endif - if (do_rename_at(from, to) == 0) { + if (vfs_rename_at(from, to, 0, VFS_OPERATOR_PATH) == 0) { if (stp->st_nlink > 1 && !S_ISDIR(stp->st_mode)) { /* If someone has hard-linked the file into the backup - * dir, rename() might return success but do nothing! */ - robust_unlink(from); /* Just in case... */ + * dir, rename() might return success but do nothing! from is the + * transfer-side source, so unlink it under the secure resolve (0). */ + robust_unlink(from, 0); /* Just in case... */ } if (DEBUG_GTE(BACKUP, 1)) rprintf(FINFO, "make_backup: RENAME %s successful.\n", from); @@ -272,7 +271,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename) init_stat_x(&sx); /* Return success if no file to keep. */ - if (x_lstat(fname, &sx.st, NULL) < 0) + if (x_lstat(fname, &sx.st, NULL, VFS_OPERATOR_PATH) < 0) return 3; if (!(buf = get_backup_name(fname))) @@ -288,7 +287,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename) * unsafe symlink. */ if (preserve_links && S_ISLNK(sx.st.st_mode) && safe_symlinks) { char lnkbuf[MAXPATHLEN]; - int llen = do_readlink(fname, lnkbuf, MAXPATHLEN - 1); + int llen = vfs_readlink(fname, lnkbuf, MAXPATHLEN - 1); /* A failed readlink means we can't verify the target, so fail * closed: skip the backup rather than let the hard-link fast path * preserve a possibly-unsafe symlink unchecked. */ @@ -317,7 +316,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename) goto success; if (errno == EEXIST || errno == EISDIR) { STRUCT_STAT bakst; - if (do_lstat_at(buf, &bakst) == 0) { + if (vfs_lstat(VFS_AT_FDCWD, buf, &bakst, VFS_OPERATOR_PATH) == 0) { int flags = get_del_for_flag(bakst.st_mode) | DEL_FOR_BACKUP | DEL_RECURSE; if (delete_item(buf, bakst.st_mode, flags) != 0) return 0; @@ -357,7 +356,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename) /* Check to see if this is a device file, or link */ if ((am_root && preserve_devices && IS_DEVICE(file->mode)) || (preserve_specials && IS_SPECIAL(file->mode))) { - if (do_mknod_at(buf, file->mode, sx.st.st_rdev) < 0) + if (vfs_mknod(VFS_AT_FDCWD, buf, file->mode, sx.st.st_rdev, VFS_OPERATOR_PATH) < 0) rsyserr(FERROR, errno, "mknod %s failed", full_fname(buf)); else if (DEBUG_GTE(BACKUP, 1)) rprintf(FINFO, "make_backup: DEVICE %s successful.\n", fname); @@ -374,7 +373,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename) } ret = 2; } else { - if (do_symlink_at(sl, buf) < 0) + if (vfs_symlink(sl, VFS_AT_FDCWD, buf, VFS_OPERATOR_PATH) < 0) rsyserr(FERROR, errno, "link %s -> \"%s\"", full_fname(buf), sl); else if (DEBUG_GTE(BACKUP, 1)) rprintf(FINFO, "make_backup: SYMLINK %s successful.\n", fname); @@ -398,7 +397,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename) /* Copy to backup tree if a file. */ if (!ret) { - if (copy_file(fname, buf, -1, file->mode) < 0) { + if (copy_file(fname, buf, -1, file->mode, VFS_OPERATOR_PATH) < 0) { rsyserr(FERROR, errno, "keep_backup failed: %s -> \"%s\"", full_fname(fname), buf); unmake_file(file); @@ -417,7 +416,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename) save_preserve_xattrs = preserve_xattrs; preserve_xattrs = 0; - set_file_attrs(buf, file, NULL, fname, ATTRS_ACCURATE_TIME); + set_file_attrs(buf, file, NULL, fname, ATTRS_OPERATOR_PATH | ATTRS_ACCURATE_TIME); preserve_xattrs = save_preserve_xattrs; unmake_file(file); @@ -442,8 +441,6 @@ int make_backup(const char *fname, BOOL prefer_rename) * symlink component is refused while the operator's own is followed -- * absolute and relative alike. --insecure-links / "insecure links =" * restores legacy following. */ - operator_path_resolve = 1; ret = make_backup_inner(fname, prefer_rename); - operator_path_resolve = 0; return ret; } diff --git a/batch.c b/batch.c index a4d19c058..e47f9b813 100644 --- a/batch.c +++ b/batch.c @@ -251,7 +251,7 @@ void open_batch_files(void) stringjoin(filename, sizeof filename, batch_name, ".sh", NULL); - batch_sh_fd = open_no_attacker_symlinks(filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR | S_IXUSR); + batch_sh_fd = vfs_open_owner_walk(filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR | S_IXUSR, 0); if (batch_sh_fd < 0) { rsyserr(FERROR, errno, "Batch file %s open error", full_fname(filename)); exit_cleanup(RERR_FILESELECT); @@ -259,12 +259,12 @@ void open_batch_files(void) /* O_BINARY: the batch stream is binary protocol data; without it * Cygwin et al apply CRLF translation and corrupt it. Unlike - * do_open(), open_no_attacker_symlinks passes flags verbatim. */ - batch_fd = open_no_attacker_symlinks(batch_name, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR); + * vfs_open(), vfs_open_owner_walk passes flags verbatim. */ + batch_fd = vfs_open_owner_walk(batch_name, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR, 0); } else if (strcmp(batch_name, "-") == 0) batch_fd = STDIN_FILENO; else - batch_fd = open_no_attacker_symlinks(batch_name, O_RDONLY | O_BINARY, S_IRUSR | S_IWUSR); + batch_fd = vfs_open_owner_walk(batch_name, O_RDONLY | O_BINARY, S_IRUSR | S_IWUSR, 0); if (batch_fd < 0) { rsyserr(FERROR, errno, "Batch file %s open error", full_fname(batch_name)); @@ -275,7 +275,7 @@ void open_batch_files(void) * non-regular files (FIFO, device, socket) at the batch path. */ if (!write_batch && batch_fd != STDIN_FILENO) { STRUCT_STAT st; - if (do_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode)) { + if (vfs_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode)) { rprintf(FERROR, "Batch file %s is not a regular file\n", full_fname(batch_name)); exit_cleanup(RERR_FILEIO); diff --git a/checksum.c b/checksum.c index 4c91c2b2b..def427ad1 100644 --- a/checksum.c +++ b/checksum.c @@ -423,7 +423,7 @@ void file_checksum(const char *fname, const STRUCT_STAT *st_p, char *sum) int32 remainder; int fd; - fd = do_open_checklinks(fname); + fd = vfs_open_checklinks(fname); if (fd == -1) { memset(sum, 0, file_sum_len); return; diff --git a/cleanup.c b/cleanup.c index 7f1864ccb..c97b43579 100644 --- a/cleanup.c +++ b/cleanup.c @@ -58,7 +58,7 @@ void close_all(void) max_fd = sysconf(_SC_OPEN_MAX) - 1; for (fd = max_fd; fd >= 0; fd--) { - if ((ret = do_fstat(fd, &st)) == 0) { + if ((ret = vfs_fstat(fd, &st)) == 0) { if (is_a_socket(fd)) ret = shutdown(fd, 2); ret = close(fd); @@ -198,7 +198,7 @@ NORETURN void _exit_cleanup(int code, const char *file, int line) switch_step++; if (cleanup_fname) - do_unlink_at(cleanup_fname); + vfs_unlink(VFS_AT_FDCWD, cleanup_fname, 0); if (exit_code) kill_all(SIGUSR1); if (cleanup_pid && cleanup_pid == getpid()) { diff --git a/clientserver.c b/clientserver.c index 0e9aca36a..03aa5aee9 100644 --- a/clientserver.c +++ b/clientserver.c @@ -185,7 +185,7 @@ static int exchange_protocols(int f_in, int f_out, char *buf, size_t bufsiz, int /* 'motd file = PATH': motd content is sent to every client, so * a planted symlink would leak the target's bytes. Refuse * symlinks not owned by uid 0 or our euid. */ - int motd_fd = open_no_attacker_symlinks(motd, O_RDONLY, 0); + int motd_fd = vfs_open_owner_walk(motd, O_RDONLY, 0, 0); FILE *f = motd_fd >= 0 ? fdopen(motd_fd, "r") : NULL; if (!f && motd_fd >= 0) close(motd_fd); while (f && !feof(f)) { @@ -300,10 +300,10 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char STRUCT_STAT st; /* --early-input-file=PATH: refuse symlinks not owned by uid 0 or * our euid anywhere in the path. */ - int ei_fd = open_no_attacker_symlinks(early_input_file, O_RDONLY, 0); + int ei_fd = vfs_open_owner_walk(early_input_file, O_RDONLY, 0, 0); FILE *f = ei_fd >= 0 ? fdopen(ei_fd, "rb") : NULL; if (!f && ei_fd >= 0) close(ei_fd); - if (!f || do_fstat(fileno(f), &st) < 0) { + if (!f || vfs_fstat(fileno(f), &st) < 0) { rsyserr(FERROR, errno, "failed to open %s", early_input_file); if (f) fclose(f); @@ -925,6 +925,12 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char } else set_filter_dir(module_dir, module_dirlen); + /* Snapshot the module root for the VFS confinement checks now that the + * path is final. The root dirfd is pinned later (below); this first call + * must precede any VFS open of an operator-supplied path -- the filter/ + * include files just below, and the log file -- so they see the boundary. */ + vfs_set_module_root(module_dir, module_dirlen, -1); + /* Everything loaded from here to the end of the exclude block is the * operator's own configuration, so it keeps the ownership walk without the * module-confinement parse_filter_file() applies to peer-driven merges. */ @@ -1064,6 +1070,8 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char #if defined HAVE_FDOPENDIR && defined O_DIRECTORY module_dirfd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC); #endif + /* Update the VFS snapshot with the now-pinned root dirfd. */ + vfs_set_module_root(module_dir, module_dirlen, module_dirfd); if (module_dirlen) sanitize_paths = 1; @@ -1073,7 +1081,7 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char STRUCT_STAT st; char prefix[SYMLINK_PREFIX_LEN]; /* NOT +1 ! */ strlcpy(prefix, SYMLINK_PREFIX, sizeof prefix); /* trim the trailing slash */ - if (do_stat(prefix, &st) == 0 && S_ISDIR(st.st_mode)) { + if (vfs_stat(VFS_AT_FDCWD, prefix, &st, VFS_ALLOW_SYMLINK) == 0 && S_ISDIR(st.st_mode)) { rprintf(FLOG, "Symlink munging is unsafe when a %s directory exists.\n", prefix); io_printf(f_out, "@ERROR: daemon security issue -- contact admin\n", name); @@ -1087,11 +1095,11 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char * the receiver finish/rename path must still resolve beneath the module * root. This prevents TOCTOU race attacks where an attacker could switch a * directory to a symlink between path validation and file open. Match the - * gate in secure_relpath_active() (syscall.c) -- the protection has nothing + * gate in vfs_relpath_active() (syscall.c) -- the protection has nothing * to do with symlink munging, so a module configured with "munge symlinks = * false" must still get the secure-open path. */ use_secure_symlinks = am_daemon && (!am_chrooted || module_dirlen) - && !symlink_optout_allowed(); + && !vfs_symlink_optout_allowed(); if (gid_list.count) { gid_t *gid_array = gid_list.items; @@ -1477,7 +1485,7 @@ int start_daemon(int f_in, int f_out) } /* Deliberately do NOT set am_chrooted here. am_chrooted * gates the per-module symlink-race defenses - * (secure_relative_open() and the do_*_at() wrappers in + * (vfs_resolve_open() and the do_*_at() wrappers in * syscall.c) and means "the kernel is enforcing path * confinement at the module boundary". The daemon chroot * confines path resolution to the daemon-chroot directory, @@ -1486,7 +1494,7 @@ int start_daemon(int f_in, int f_out) * subtrees and a sender-controlled symlink in module A * could redirect a syscall to module B (or to other files * inside the daemon chroot) without the per-module - * defenses. Leave am_chrooted=0 here so secure_relative_open() + * defenses. Leave am_chrooted=0 here so vfs_resolve_open() * still fires for "use chroot = no" modules. */ if (chdir("/") < 0) { rsyserr(FLOG, errno, "daemon chdir(\"/\") failed"); @@ -1618,18 +1626,18 @@ static void create_pid_file(void) dir = dirbuf; base = slash + 1; } - if ((pdfd = do_open(dir, O_RDONLY|O_DIRECTORY, 0)) < 0) { + if ((pdfd = vfs_open(dir, O_RDONLY|O_DIRECTORY, 0)) < 0) { rsyserr(FLOG, errno, "failed to open pid-file directory \"%s\"", dir); exit_cleanup(RERR_FILEIO); } } -#define PID_LSTAT(stp) do_lstat_atfd(pdfd, base, stp) -#define PID_UNLINK() do_unlink_atfd(pdfd, base, 0) -#define PID_OPEN() do_open_atfd(pdfd, base, O_RDWR|O_CREAT, 0664) +#define PID_LSTAT(stp) vfs_lstat(pdfd, base, stp, 0) +#define PID_UNLINK() vfs_unlink(pdfd, base, 0) +#define PID_OPEN() vfs_open_atfd(pdfd, base, O_RDWR|O_CREAT, 0664) #else -#define PID_LSTAT(stp) do_lstat(base, stp) +#define PID_LSTAT(stp) vfs_lstat(VFS_AT_FDCWD, base, stp, VFS_ALLOW_SYMLINK) #define PID_UNLINK() unlink(base) -#define PID_OPEN() do_open(base, O_RDWR|O_CREAT|SAFE_NOFOLLOW, 0664) +#define PID_OPEN() vfs_open(base, O_RDWR|O_CREAT|SAFE_NOFOLLOW, 0664) #endif /* These tests make sure that a temp-style lock dir is handled safely. */ @@ -1640,7 +1648,7 @@ static void create_pid_file(void) fail = S_ISREG(st1.st_mode) ? "open" : "create"; else if (!lock_range(pid_file_fd, 0, 4)) fail = "lock"; - else if (do_fstat(pid_file_fd, &st1) < 0) + else if (vfs_fstat(pid_file_fd, &st1) < 0) fail = "fstat opened"; else if (st1.st_size > (int)sizeof pidbuf) fail = "find small"; @@ -1651,7 +1659,7 @@ static void create_pid_file(void) else if (st1.st_dev != st2.st_dev || st1.st_ino != st2.st_ino) fail = "verify stat info for"; #ifdef HAVE_FTRUNCATE - else if (do_ftruncate(pid_file_fd, 0) < 0) + else if (vfs_ftruncate(pid_file_fd, 0) < 0) fail = "truncate"; #endif else { diff --git a/configure.ac b/configure.ac index 57cf2828d..afa0346b9 100644 --- a/configure.ac +++ b/configure.ac @@ -60,6 +60,8 @@ AC_PROG_AWK AC_PROG_EGREP AC_PROG_INSTALL AC_PROG_MKDIR_P +AC_CHECK_TOOL([AR], [ar], [ar]) +AC_PROG_RANLIB AC_SUBST(SHELL) AC_PATH_PROG([PERL], [perl]) AC_PATH_PROG([PYTHON3], [python3]) @@ -104,6 +106,12 @@ dnl by default (the knob only REMOVES a tier when explicitly disabled). AC_ARG_ENABLE(openat2, AS_HELP_STRING([--disable-openat2],[do not use Linux openat2(RESOLVE_BENEATH); force the portable resolver (for exercising the fallback tier)])) +AC_ARG_ENABLE(strict-confinement, + AS_HELP_STRING([--enable-strict-confinement],[abort if a confined receiver ever does a raw path-based metadata op (a CI/dev hardening check; no effect on a normal build)])) +if test x"$enable_strict_confinement" = x"yes"; then + AC_DEFINE(STRICT_CONFINEMENT, 1, [Define to abort on a confined-regime raw path-based metadata op (CI hardening check)]) +fi + AC_MSG_CHECKING([if md2man can create manpages]) if test x"$ac_cv_path_PYTHON3" = x; then AC_MSG_RESULT(no - python3 not found) @@ -371,7 +379,7 @@ return SYS_openat2 + (int)how.resolve; if test x"$enable_openat2" != x"no"; then if test x"$rsync_cv_HAVE_OPENAT2" = x"yes"; then AC_DEFINE([HAVE_OPENAT2], 1, - [Define to use Linux openat2(RESOLVE_BENEATH) in secure_relative_open where available.]) + [Define to use Linux openat2(RESOLVE_BENEATH) in vfs_resolve_open where available.]) fi fi @@ -1542,7 +1550,7 @@ case "$CC" in ;; esac -AC_CONFIG_FILES([Makefile lib/dummy zlib/dummy popt/dummy shconfig]) +AC_CONFIG_FILES([Makefile lib/dummy zlib/dummy popt/dummy vfs/dummy shconfig]) AC_OUTPUT AC_MSG_RESULT() diff --git a/connection.c b/connection.c index a5ca19f3d..85320c5af 100644 --- a/connection.c +++ b/connection.c @@ -32,7 +32,7 @@ int claim_connection(char *fname, int max_connections) /* 'lock file = PATH': refuse symlinks not owned by uid 0 or our euid so * a planted parent can't redirect the root daemon's O_CREAT open. */ - if ((fd = open_no_attacker_symlinks(fname, O_RDWR|O_CREAT, 0600)) < 0) + if ((fd = vfs_open_owner_walk(fname, O_RDWR|O_CREAT, 0600, 0)) < 0) return 0; /* Find a free spot. */ diff --git a/delete.c b/delete.c index 83ebb9e27..19ba0a3ea 100644 --- a/delete.c +++ b/delete.c @@ -63,18 +63,23 @@ static void del_chmod(const char *fbuf, mode_t mode) const char *leaf; int dfd = del_held_dfd(fbuf, &leaf); if (dfd >= 0) - do_chmod_atfd(dfd, leaf, mode); + vfs_chmod(dfd, leaf, mode, 0); else - do_chmod_at(fbuf, mode); + vfs_chmod(VFS_AT_FDCWD, fbuf, mode, 0); } -static int del_unlink(const char *fbuf) +/* vfs_flags carries VFS_OPERATOR_PATH for a backup-tree delete (DEL_FOR_BACKUP): + * the path-based fallback then resolves the leaf's parent via the ownership walk, + * matching the confinement the base gives this unlink under make_backup() (where + * the held dirfd is absent for a cross-tree --backup-dir leaf). A held-dirfd + * delete is already confined, so it ignores the flag. */ +static int del_unlink(const char *fbuf, int vfs_flags) { const char *leaf; int dfd = del_held_dfd(fbuf, &leaf); - if (dfd >= 0 && do_unlink_atfd(dfd, leaf, 0) == 0) + if (dfd >= 0 && vfs_unlink(dfd, leaf, 0) == 0) return 0; - return robust_unlink(fbuf); /* fall back (ETXTBSY retry, or not held) */ + return robust_unlink(fbuf, vfs_flags); /* fall back (ETXTBSY retry, or not held) */ } static inline int is_backup_file(char *fn) @@ -133,7 +138,7 @@ static enum delret delete_dir_contents(char *fname, uint16 flags) const char *save_del_prefix = del_dir_prefix; int save_del_prefix_len = del_dir_prefix_len; fname[dlen] = '\0'; - del_dirfd = open_dir_secure(fname); + del_dirfd = vfs_opendir(fname); fname[dlen] = '/'; del_dir_prefix = fname; del_dir_prefix_len = dlen; @@ -223,18 +228,20 @@ enum delret delete_item(char *fbuf, uint16 mode, uint16 flags) const char *leaf; int dfd = del_held_dfd(fbuf, &leaf); what = "rmdir"; - ok = (dfd >= 0 ? do_unlink_atfd(dfd, leaf, AT_REMOVEDIR) : do_rmdir_at(fbuf)) == 0; + ok = (dfd >= 0 ? vfs_unlink(dfd, leaf, VFS_REMOVEDIR) + : vfs_unlink(VFS_AT_FDCWD, fbuf, + VFS_REMOVEDIR | ((flags & DEL_FOR_BACKUP) ? VFS_OPERATOR_PATH : 0))) == 0; } else { if (make_backups > 0 && !(flags & DEL_FOR_BACKUP) && (backup_dir || !is_backup_file(fbuf))) { what = "make_backup"; ok = make_backup(fbuf, True); if (ok == 2) { what = "unlink"; - ok = del_unlink(fbuf) == 0; + ok = del_unlink(fbuf, (flags & DEL_FOR_BACKUP) ? VFS_OPERATOR_PATH : 0) == 0; } } else { what = "unlink"; - ok = del_unlink(fbuf) == 0; + ok = del_unlink(fbuf, (flags & DEL_FOR_BACKUP) ? VFS_OPERATOR_PATH : 0) == 0; } } diff --git a/exclude.c b/exclude.c index 7476f6a87..950ea7e1a 100644 --- a/exclude.c +++ b/exclude.c @@ -41,7 +41,6 @@ extern int sanitize_paths; extern int protocol_version; extern int trust_sender_args; extern int module_id; -extern int operator_path_resolve; /* Set while the daemon loads its own filter parameters; see parse_filter_file(). */ int daemon_config_filter_file = 0; @@ -136,8 +135,6 @@ static void filter_rule_err(const char *msg, const char *rulestr) exit_cleanup(RERR_SYNTAX); } -extern char curr_dir[MAXPATHLEN]; -extern unsigned int curr_dir_len; extern unsigned int module_dirlen; filter_rule_list filter_list = { .debug_type = "" }; @@ -155,7 +152,7 @@ int trust_sender_filter = 0; #define SLASH_WILD3_SUFFIX "/***" /* The dirbuf is set by push_local_filters() to the current subdirectory - * relative to curr_dir that is being processed. The path always has a + * relative to vfs.curr_dir that is being processed. The path always has a * trailing slash appended, and the variable dirbuf_len contains the length * of this path prefix. The path is always absolute. */ static char dirbuf[MAXPATHLEN+1]; @@ -757,9 +754,9 @@ void set_filter_dir(const char *dir, unsigned int dirlen) { unsigned int len; if (*dir != '/') { - memcpy(dirbuf, curr_dir, curr_dir_len); - dirbuf[curr_dir_len] = '/'; - len = curr_dir_len + 1; + memcpy(dirbuf, vfs.curr_dir, vfs.curr_dir_len); + dirbuf[vfs.curr_dir_len] = '/'; + len = vfs.curr_dir_len + 1; if (len + dirlen >= MAXPATHLEN) dirlen = 0; } else @@ -853,7 +850,7 @@ struct local_filter_state { /* Each time rsync changes to a new directory it call this function to * handle all the per-dir merge-files. The "dir" value is the current path - * relative to curr_dir (which might not be null-terminated). We copy it + * relative to vfs.curr_dir (which might not be null-terminated). We copy it * into dirbuf so that we can easily append a file name on the end. */ void *push_local_filters(const char *dir, unsigned int dirlen) { @@ -1020,10 +1017,10 @@ static int rule_matches(const char *fname, filter_rule *ex, int name_flags) if ((p = strrchr(name,'/')) != NULL) name = p+1; } else if (ex->rflags & FILTRULE_ABS_PATH && *fname != '/' - && curr_dir_len > module_dirlen + 1) { + && vfs.curr_dir_len > module_dirlen + 1) { /* If we're matching against an absolute-path pattern, * we need to prepend our full path info. */ - strings[str_cnt++] = curr_dir + module_dirlen + 1; + strings[str_cnt++] = vfs.curr_dir + module_dirlen + 1; strings[str_cnt++] = "/"; } else if (ex->rflags & FILTRULE_WILD2_PREFIX && *fname != '/') { /* Allow "**"+"/" to match at the start of the string. */ @@ -1665,24 +1662,18 @@ void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_ open_path = line; } else open_path = fname; - /* Confine the open to the module root. The ownership walk on its own * is not enough for a peer-driven merge file: a non-chrooted daemon * writes --backup-dir entries as root, so a raced backup symlink is - * ROOT-owned -- exactly what open_no_attacker_symlinks() treats as - * trusted -- and naming it in a dir-merge rule would read an - * out-of-module file in as filter rules (their text comes back to the - * peer in "Unknown filter rule" errors). + * ROOT-owned -- exactly what the ownership walk treats as trusted -- + * and naming it in a dir-merge rule would read an out-of-module file + * in as filter rules (their text comes back to the peer in "Unknown + * filter rule" errors). * * The daemon's own "filter"/"include from"/"exclude from" parameters * are exempt: those are operator-configured and legitimately live * outside the module (/etc/rsync/excludes and the like). */ - int save_opr = operator_path_resolve; - if (!daemon_config_filter_file) - operator_path_resolve = 1; - fd = open_no_attacker_symlinks(open_path, O_RDONLY, 0); - operator_path_resolve = save_opr; - + fd = vfs_open_owner_walk(open_path, O_RDONLY, 0, !daemon_config_filter_file); if (fd < 0) fp = NULL; else if (!(fp = fdopen(fd, "rb"))) diff --git a/fileio.c b/fileio.c index c4cf57829..3e43a5ceb 100644 --- a/fileio.c +++ b/fileio.c @@ -45,17 +45,17 @@ int sparse_end(int f, OFF_T size, int updating_basis_or_equiv) int ret = 0; if (updating_basis_or_equiv) { - if (sparse_seek && do_punch_hole(f, sparse_past_write, sparse_seek) < 0) + if (sparse_seek && vfs_punch_hole(f, sparse_past_write, sparse_seek) < 0) ret = -1; #ifdef HAVE_FTRUNCATE /* A compilation formality -- in-place requires ftruncate() */ else /* Just in case the original file was longer */ - ret = do_ftruncate(f, size); + ret = vfs_ftruncate(f, size); #endif } else if (sparse_seek) { #ifdef HAVE_FTRUNCATE - ret = do_ftruncate(f, size); + ret = vfs_ftruncate(f, size); #else - if (do_lseek(f, sparse_seek-1, SEEK_CUR) != size-1) + if (vfs_lseek(f, sparse_seek-1, SEEK_CUR) != size-1) ret = -1; else { do { @@ -76,17 +76,17 @@ int sparse_end(int f, OFF_T size, int updating_basis_or_equiv) * the current file position is in the file. The use_seek arg tells * us that we should seek over matching data instead of writing it. */ /* Flush any deferred run of zero bytes as a hole, advancing the file - * position past it (both do_lseek() and do_punch_hole() move the offset). */ + * position past it (both vfs_lseek() and vfs_punch_hole() move the offset). */ static int flush_sparse_hole(int f) { if (!sparse_seek) return 0; if (sparse_past_write >= preallocated_len) { - if (do_lseek(f, sparse_seek, SEEK_CUR) < 0) { + if (vfs_lseek(f, sparse_seek, SEEK_CUR) < 0) { sparse_seek = 0; return -1; } - } else if (do_punch_hole(f, sparse_past_write, sparse_seek) < 0) { + } else if (vfs_punch_hole(f, sparse_past_write, sparse_seek) < 0) { sparse_seek = 0; return -1; } @@ -119,7 +119,7 @@ static int emit_sparse_span(int f, int use_seek, const char *buf, int len) if (flush_sparse_hole(f) < 0) return -1; if (use_seek) - return do_lseek(f, len, SEEK_CUR) < 0 ? -1 : 0; + return vfs_lseek(f, len, SEEK_CUR) < 0 ? -1 : 0; return full_sparse_write(f, buf, len); } @@ -262,7 +262,7 @@ int skip_matched(int fd, OFF_T offset, const char *buf, int len) if (flush_write_file(fd) < 0) return -1; - if ((pos = do_lseek(fd, len, SEEK_CUR)) != offset + len) { + if ((pos = vfs_lseek(fd, len, SEEK_CUR)) != offset + len) { rsyserr(FERROR_XFER, errno, "lseek returned %s, not %s", big_num(pos), big_num(offset)); return -1; @@ -345,7 +345,7 @@ char *map_ptr(struct map_struct *map, OFF_T offset, int32 len) } if (map->p_fd_offset != read_start) { - OFF_T ret = do_lseek(map->fd, read_start, SEEK_SET); + OFF_T ret = vfs_lseek(map->fd, read_start, SEEK_SET); if (ret != read_start) { rsyserr(FERROR, errno, "lseek returned %s, not %s", big_num(ret), big_num(read_start)); diff --git a/flist.c b/flist.c index 9276c65fc..763d33600 100644 --- a/flist.c +++ b/flist.c @@ -33,7 +33,6 @@ extern int am_chrooted; extern char *module_dir; extern unsigned int module_dirlen; extern int module_dirfd; -extern unsigned int curr_dir_len; extern int am_sender; extern int am_generator; extern int inc_recurse; @@ -87,7 +86,6 @@ extern char *usermap, *groupmap; extern struct name_num_item *file_sum_nni; -extern char curr_dir[MAXPATHLEN]; extern struct chmod_mode_struct *chmod_modes; @@ -250,8 +248,8 @@ static int scan_readlink(const char *path, char *linkbuf, size_t bufsiz) && strncmp(path, scan_dir_prefix, scan_dir_prefix_len) == 0 && path[scan_dir_prefix_len] == '/' && strchr(path + scan_dir_prefix_len + 1, '/') == NULL) - return do_readlink_atfd(scan_dirfd, path + scan_dir_prefix_len + 1, linkbuf, bufsiz); - return do_readlink(path, linkbuf, bufsiz); + return vfs_readlink_atfd(scan_dirfd, path + scan_dir_prefix_len + 1, linkbuf, bufsiz); + return vfs_readlink(path, linkbuf, bufsiz); } static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf) @@ -269,7 +267,7 @@ static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf) rprintf(FINFO,"copying unsafe symlink \"%s\" -> \"%s\"\n", path, linkbuf); } - return x_stat(path, stp, NULL); + return x_stat(path, stp, NULL, 0); } if (munge_symlinks && am_sender && llen > SYMLINK_PREFIX_LEN && strncmp(linkbuf, SYMLINK_PREFIX, SYMLINK_PREFIX_LEN) == 0) { @@ -279,7 +277,7 @@ static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf) } return 0; #else - return x_stat(path, stp, NULL); + return x_stat(path, stp, NULL, 0); #endif } @@ -287,17 +285,17 @@ int link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks) { #ifdef SUPPORT_LINKS if (copy_links) - return x_stat(path, stp, NULL); - if (x_lstat(path, stp, NULL) < 0) + return x_stat(path, stp, NULL, 0); + if (x_lstat(path, stp, NULL, 0) < 0) return -1; if (follow_dirlinks && S_ISLNK(stp->st_mode)) { STRUCT_STAT st; - if (x_stat(path, &st, NULL) == 0 && S_ISDIR(st.st_mode)) + if (x_stat(path, &st, NULL, 0) == 0 && S_ISDIR(st.st_mode)) *stp = st; } return 0; #else - return x_stat(path, stp, NULL); + return x_stat(path, stp, NULL, 0); #endif } @@ -311,17 +309,17 @@ int link_stat_at(int dfd, const char *name, STRUCT_STAT *stp, int follow_dirlink { #ifdef SUPPORT_LINKS if (copy_links) - return do_stat_atfd(dfd, name, stp); - if (do_lstat_atfd(dfd, name, stp) < 0) + return vfs_stat(dfd, name, stp, 0); + if (vfs_lstat(dfd, name, stp, 0) < 0) return -1; if (follow_dirlinks && S_ISLNK(stp->st_mode)) { STRUCT_STAT st; - if (do_stat_atfd(dfd, name, &st) == 0 && S_ISDIR(st.st_mode)) + if (vfs_stat(dfd, name, &st, 0) == 0 && S_ISDIR(st.st_mode)) *stp = st; } return 0; #else - return do_stat_atfd(dfd, name, stp); + return vfs_stat(dfd, name, stp, 0); #endif } @@ -1449,7 +1447,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist, * options was specified, so there's no need for the * extra lstat() if one of these options isn't on. */ if ((copy_links || copy_unsafe_links || copy_dirlinks) - && x_lstat(thisname, &st, NULL) == 0 + && x_lstat(thisname, &st, NULL, 0) == 0 && S_ISLNK(st.st_mode)) { io_error |= IOERR_GENERAL; rprintf(FERROR_XFER, "symlink has no referent: %s\n", @@ -1564,7 +1562,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist, if (copy_devices && am_sender && IS_DEVICE(st.st_mode)) { if (st.st_size == 0) { - int fd = do_open_checklinks(fname); + int fd = vfs_open_checklinks(fname); if (fd >= 0) { st.st_size = get_device_size(fd, fname); close(fd); @@ -1677,7 +1675,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist, F_ATIME(file) = st.st_atime; #ifdef SUPPORT_CRTIMES if (crtimes_ndx) - F_CRTIME(file) = get_create_time(fname, &st); + F_CRTIME(file) = vfs_get_create_time(fname, &st); #endif if (basename != thisname) @@ -2015,14 +2013,14 @@ static void interpret_stat_error(const char *fname, int is_dir) #if defined HAVE_FDOPENDIR && defined HAVE_DIRFD /* Open a source directory for scanning confined beneath the transfer root. - * secure_relative_open() does a per-component O_NOFOLLOW walk that refuses a + * vfs_resolve_open() does a per-component O_NOFOLLOW walk that refuses a * parent component raced into a symlink pointing out of the tree; fdopendir() * then turns the held fd into the DIR* the scan reads. This mirrors the * sender's confined content open (sender.c): the directory enumeration must be * confined the same way, or a parent-symlink race (or, for a daemon following * mode, an in-module symlink to outside) lets the scan enumerate an out-of-tree * directory and leak its names/metadata/symlink targets. O_DIRECTORY without - * O_NOFOLLOW makes secure_relative_open() follow in-tree directory symlinks + * O_NOFOLLOW makes vfs_resolve_open() follow in-tree directory symlinks * beneath the anchor and refuse escapes, so this serves both the default * no-follow scan and a daemon's symlink-following scan (see the caller). * Returns NULL with errno set on failure, like opendir(). */ @@ -2033,9 +2031,9 @@ static DIR *secure_opendir(const char *fbuf) if (am_daemon && (!am_chrooted || module_dirlen) && module_dir && module_dir[0] == '/' && *fbuf != '/' && module_dirfd >= 0 - && curr_dir_len >= module_dirlen - && strncmp(curr_dir, module_dir, module_dirlen) == 0 - && (curr_dir[module_dirlen] == '\0' || curr_dir[module_dirlen] == '/')) { + && vfs.curr_dir_len >= module_dirlen + && strncmp(vfs.curr_dir, module_dir, module_dirlen) == 0 + && (vfs.curr_dir[module_dirlen] == '\0' || vfs.curr_dir[module_dirlen] == '/')) { /* Daemon: anchor the confined scan at the module root pinned by identity * at module setup (module_dirfd, opened while the daemon was positioned * there and still privileged), and walk the module-relative path of the @@ -2043,11 +2041,11 @@ static DIR *secure_opendir(const char *fbuf) * legitimate in-module ".." climb (sub/climb -> ../sibling) or an in-module * directory symlink is followed, and an escape refused -- without * re-walking the absolute module path as the dropped uid (the privilege- - * drop EACCES), and without assuming the lexical curr_dir depth matches the + * drop EACCES), and without assuming the lexical vfs.curr_dir depth matches the * real cwd (a followed in-module symlink can desync them; anchoring at the * pinned module root and walking down the logical path is correct either * way). */ - const char *p = curr_dir + module_dirlen; + const char *p = vfs.curr_dir + module_dirlen; char modrel[MAXPATHLEN]; while (*p == '/') p++; @@ -2056,7 +2054,7 @@ static DIR *secure_opendir(const char *fbuf) errno = ENAMETOOLONG; return NULL; } - dfd = secure_relative_open_at(module_dirfd, *modrel ? modrel : ".", + dfd = vfs_resolve_open_at(module_dirfd, *modrel ? modrel : ".", O_RDONLY | O_DIRECTORY, 0); } else if (*fbuf == '/') { /* An absolute scan path (an absolute --relative / --files-from name, or a @@ -2064,11 +2062,11 @@ static DIR *secure_opendir(const char *fbuf) const char *relp = fbuf; while (*relp == '/') relp++; - dfd = secure_relative_open("/", relp, O_RDONLY | O_DIRECTORY, 0); + dfd = vfs_resolve_open("/", relp, O_RDONLY | O_DIRECTORY, 0); } else { /* Non-daemon (or chrooted) sender: confine beneath the cwd the sender * chdir'd into (the transfer root). */ - dfd = secure_relative_open(NULL, fbuf, O_RDONLY | O_DIRECTORY, 0); + dfd = vfs_resolve_open(NULL, fbuf, O_RDONLY | O_DIRECTORY, 0); } if (dfd < 0) @@ -2106,7 +2104,7 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len, /* Confine the enumeration beneath the transfer root. secure_opendir() * follows in-tree directory symlinks (RESOLVE_BENEATH) and refuses one that * escapes, so it serves both modes: - * - a daemon/hardened sender (secure_relpath_active()) is confined to the + * - a daemon/hardened sender (vfs_relpath_active()) is confined to the * module in EVERY mode -- including -L/--copy-dirlinks/--copy-unsafe- * links, matching the content open (sender_open_copylinks_confined) -- * so a following mode cannot be lured to enumerate outside the module; @@ -2118,7 +2116,7 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len, * yes", admin-only) -- or a non-daemon --insecure-links -- uses the legacy * opendir() too, restoring the pre-hardening enumeration (re-opening the * escape; documented). */ - if (f >= 0 && !symlink_optout_allowed() && (secure_relpath_active() + if (f >= 0 && !vfs_symlink_optout_allowed() && (vfs_relpath_active() || !(copy_links || copy_unsafe_links || copy_dirlinks || insecure_links))) d = secure_opendir(fbuf); else @@ -2560,7 +2558,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[]) } if (!orig_dir) - orig_dir = strdup(curr_dir); + orig_dir = strdup(vfs.curr_dir); while (1) { char fbuf[MAXPATHLEN], *fn, name_type; diff --git a/generator.c b/generator.c index 7e5ad60a3..8b25d3707 100644 --- a/generator.c +++ b/generator.c @@ -29,7 +29,6 @@ extern int do_xfers; extern int stdout_format_has_i; extern int logfile_format_has_i; extern int am_root; -extern int operator_path_resolve; extern int am_server; extern int am_daemon; extern int inc_recurse; @@ -133,7 +132,7 @@ static int start_delete_delay_temp(void) dry_run = 0; if (!get_tmpname(fnametmp, "deldelay", False) - || (deldelay_fd = do_mkstemp(fnametmp, 0600)) < 0) { + || (deldelay_fd = vfs_mkstemp(fnametmp, 0600)) < 0) { rprintf(FINFO, "NOTE: Unable to create delete-delay temp file%s.\n", inc_recurse ? "" : " -- switching to --delete-after"); delete_during = 0; @@ -414,7 +413,7 @@ static inline int any_time_differs(stat_x *sxp, struct file_struct *file, UNUSED #ifdef SUPPORT_CRTIMES if (!differs && crtimes_ndx) { if (sxp->crtime == 0) - sxp->crtime = get_create_time(fname, &sxp->st); + sxp->crtime = vfs_get_create_time(fname, &sxp->st); differs = !same_time(sxp->crtime, 0, F_CRTIME(file), 0); } #endif @@ -540,7 +539,7 @@ void itemize(const char *fnamecmp, struct file_struct *file, int ndx, int statre #ifdef SUPPORT_CRTIMES if (crtimes_ndx) { if (sxp->crtime == 0) - sxp->crtime = get_create_time(fnamecmp, &sxp->st); + sxp->crtime = vfs_get_create_time(fnamecmp, &sxp->st); if (!same_time(sxp->crtime, 0, F_CRTIME(file), 0)) iflags |= ITEM_REPORT_CRTIME; } @@ -656,7 +655,7 @@ int quick_check_ok(enum filetype ftype, const char *fn, struct file_struct *file case FT_SYMLINK: { #ifdef SUPPORT_LINKS char lnk[MAXPATHLEN]; - int len = do_readlink(fn, lnk, MAXPATHLEN-1); + int len = vfs_readlink(fn, lnk, MAXPATHLEN-1); if (len <= 0) return 0; lnk[len] = '\0'; @@ -932,15 +931,15 @@ static int copy_altdest_file(const char *src, const char *dest, struct file_stru copy_to = buf; } cleanup_set(copy_to, NULL, NULL, -1, -1); - if (copy_file(src, copy_to, fd_w, file->mode) < 0) { + if (copy_file(src, copy_to, fd_w, file->mode, 0) < 0) { if (INFO_GTE(COPY, 1)) { rsyserr(FINFO, errno, "copy_file %s => %s", full_fname(src), copy_to); } /* Try to clean up. copy_to's parent components are peer-named * and can be raced to a symlink, so resolve each with O_NOFOLLOW - * via do_unlink_at() like the other generator-side unlinks. */ - do_unlink_at(copy_to); + * via vfs_unlink_at() like the other generator-side unlinks. */ + vfs_unlink(VFS_AT_FDCWD, copy_to, 0); cleanup_disable(); return -1; } @@ -955,14 +954,13 @@ static int copy_altdest_file(const char *src, const char *dest, struct file_stru /* Stat an alternate-basis candidate (basis_dir[j]/fname) for a daemon /./ * inner-module chroot through the secure resolver, so a --compare/copy/link-dest * basis can't reach outside the inner module via a symlinked parent (the kernel - * chroot confines only the outer path). secure_relative_open() refuses a parent + * chroot confines only the outer path). vfs_resolve_open() refuses a parent * that escapes beneath the module root. Plain link_stat() everywhere else -- * the non-chroot daemon sanitizes basis paths already, and a local receiver must * still follow an operator's --link-dest=../backup. */ static int basis_link_stat(const char *path, STRUCT_STAT *stp) { extern int am_chrooted; - extern int operator_path_resolve; extern unsigned int module_dirlen; #if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY /* The basis dir (--link-dest/--compare-dest/--copy-dest) is an operator- @@ -976,9 +974,11 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp) * resolver) below. Only when am_root >= 0: link_stat_at() omits the * fake-super %stat xattr that link_stat() folds in, so --fake-super keeps * the plain path (a lower-severity, non-root basis lookup). */ - if (!am_daemon && am_root >= 0 && !symlink_optout_allowed()) { + if (!am_daemon && am_root >= 0 && !vfs_symlink_optout_allowed()) { const char *leaf; - int dfd = owner_walk_parent(path, &leaf); + /* non-daemon path: is_operator only gates the daemon module-confinement + * (a no-op here), so the ownership walk is identical either way. */ + int dfd = vfs_owner_walk_parent(path, &leaf, 0); int r, e; if (dfd < 0) return -1; @@ -989,7 +989,7 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp) return r; } /* A non-chroot daemon serving an operator/peer alt-dest basis: resolve through - * the ownership walk with module-ROOT confinement (operator_path_resolve) so an + * the ownership walk with module-ROOT confinement (is_operator=1) so an * in-module symlink whose target lands OUTSIDE the module is refused -- the * basis then looks absent and the file transfers normally instead of being * stat'd/read/linked through the link (closes the --compare-dest=/E read @@ -1001,16 +1001,14 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp) * and must keep the plain link_stat below (#915/#930). The leaf is taken * under the confined parent with O_NOFOLLOW/AT_SYMLINK_NOFOLLOW, so * --copy-links can't follow a leaf symlink out of the module. */ - if (am_daemon && !am_chrooted && path[0] == '/' && !symlink_optout_allowed()) { + if (am_daemon && !am_chrooted && path[0] == '/' && !vfs_symlink_optout_allowed()) { const char *leaf; - int dfd, e, save = operator_path_resolve; - operator_path_resolve = 1; - dfd = owner_walk_parent(path, &leaf); - operator_path_resolve = save; + int dfd, e; + dfd = vfs_owner_walk_parent(path, &leaf, 1); if (dfd < 0) return -1; if (am_root >= 0) { - int r = do_lstat_atfd(dfd, leaf, stp); + int r = vfs_lstat(dfd, leaf, stp, 0); e = errno; close(dfd); errno = e; @@ -1021,12 +1019,12 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp) /* --fake-super: O_NOFOLLOW-open the held leaf (the daemon owns its * fake-super files) so the %stat xattr link_stat() would fold is * preserved while a leaf symlink is still refused. */ - int lfd = do_open_atfd(dfd, leaf, O_RDONLY | O_NOFOLLOW | O_NONBLOCK, 0); + int lfd = vfs_open_atfd(dfd, leaf, O_RDONLY | O_NOFOLLOW | O_NONBLOCK, 0); STRUCT_STAT xst; e = errno; close(dfd); if (lfd < 0) { errno = e; return -1; } - if (do_fstat(lfd, stp) < 0) { e = errno; close(lfd); errno = e; return -1; } + if (vfs_fstat(lfd, stp) < 0) { e = errno; close(lfd); errno = e; return -1; } if (get_stat_xattr(NULL, lfd, stp, &xst) == 0) *stp = xst; close(lfd); @@ -1034,7 +1032,7 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp) } #else { - int r = do_lstat_atfd(dfd, leaf, stp); + int r = vfs_lstat(dfd, leaf, stp, 0); e = errno; close(dfd); errno = e; @@ -1043,7 +1041,7 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp) #endif } #endif - if (am_daemon && am_chrooted && module_dirlen && path[0] != '/' && !symlink_optout_allowed()) { + if (am_daemon && am_chrooted && module_dirlen && path[0] != '/' && !vfs_symlink_optout_allowed()) { const char *slash = strrchr(path, '/'); if (slash) { char dir[MAXPATHLEN]; @@ -1052,7 +1050,7 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp) if (dlen >= sizeof dir) { errno = ENAMETOOLONG; return -1; } memcpy(dir, path, dlen); dir[dlen] = '\0'; - if ((dfd = secure_relative_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0)) < 0) + if ((dfd = vfs_resolve_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0)) < 0) return -1; r = link_stat_at(dfd, slash + 1, stp, 0); e = errno; @@ -1115,7 +1113,7 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx, if (find_exact_for_existing) { if (alt_dest_type == LINK_DEST && real_st.st_dev == sxp->st.st_dev && real_st.st_ino == sxp->st.st_ino) return -1; - if (do_unlink_at(fname) < 0 && errno != ENOENT) + if (vfs_unlink(VFS_AT_FDCWD, fname, 0) < 0 && errno != ENOENT) goto got_nothing_for_ya; } #ifdef SUPPORT_HARD_LINKS @@ -1124,15 +1122,11 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx, * resolve the link source via the ownership walk so a foreign-owned * symlink raced in after the basis_link_stat() check is still * refused (matching basis_link_stat's !am_daemon gate). A daemon - * keeps its stronger module-anchored confinement (do_link_at's - * secure_relpath_active path) -- the ownership walk would follow an + * keeps its stronger module-anchored confinement (vfs_link_at's + * vfs_relpath_active path) -- the ownership walk would follow an * operator-owned symlink out of the module. */ - int hlok, op = !am_daemon; - if (op) - operator_path_resolve = 1; - hlok = hard_link_one(file, fname, cmpbuf, 1); - if (op) - operator_path_resolve = 0; + int hlok = hard_link_one(file, fname, cmpbuf, 1, + !am_daemon ? VFS_OPERATOR_PATH : 0); if (!hlok) goto try_a_copy; if (atimes_ndx) @@ -1164,9 +1158,10 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx, #endif /* NB: the copy-dest basis read is deliberately NOT routed through the * ownership walk: copy_altdest_file()->copy_file() also opens the dest - * and copies xattrs through a held O_NOFOLLOW fd, and forcing - * operator_path_resolve across that re-opens the copy_xattrs parent- - * symlink race (copy-xattrs-symlink-race). basis_link_stat() already + * and copies xattrs through a held O_NOFOLLOW fd, and passing + * VFS_OPERATOR_PATH across that re-opens the copy_xattrs parent- + * symlink race (copy-xattrs-symlink-race) -- so copy_file gets flags 0. + * basis_link_stat() already * refuses a foreign-owned basis symlink, closing the static escape; the * post-stat race on an absolute copy-dest basis is a documented residual. */ if (!dry_run && copy_altdest_file(cmpbuf, fname, file) < 0) { @@ -1266,7 +1261,11 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx, && !IS_SPECIAL(file->mode) && !IS_DEVICE(file->mode) #endif && !S_ISDIR(file->mode)) { - if (do_link_at(cmpbuf, fname) < 0) { + /* cmpbuf is the alt-dest (--link-dest) basis: for a non-daemon + * receiver it is an operator path (owner walk; matches the + * 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) { /* 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 @@ -1382,8 +1381,8 @@ static BOOL is_below(struct file_struct *file, struct file_struct *subtree) /* Held-dirfd helpers for the per-entry ops below: when the secure resolver is * active they act on the entry's basename relative to its cached directory fd - * (held_dfd_for, keyed on file->dirname), else fall back to the full-path - * do_*_at wrappers (behaviour-identical). held_dfd_for() declines when fname + * (vfs_cached_dirfd, keyed on file->dirname), else fall back to the full-path + * do_*_at wrappers (behaviour-identical). vfs_cached_dirfd() declines when fname * isn't in file->dirname (e.g. the single-file local_name dest), and the leaf * is derived from fname, not file->basename. */ static int gen_entry_stat(const char *fname, struct file_struct *file, @@ -1392,7 +1391,7 @@ static int gen_entry_stat(const char *fname, struct file_struct *file, int dfd; /* link_stat_at folds in no fake-super xattr, so only use it when * am_root >= 0 (where link_stat's get_stat_xattr is a no-op anyway). */ - if (am_root >= 0 && (dfd = held_dfd_for(fname, file)) >= 0) { + if (am_root >= 0 && (dfd = vfs_cached_dirfd(fname, file)) >= 0) { const char *slash = strrchr(fname, '/'); return link_stat_at(dfd, slash ? slash + 1 : fname, stp, follow_dirlinks); } @@ -1401,27 +1400,27 @@ static int gen_entry_stat(const char *fname, struct file_struct *file, static int gen_entry_mkdir(char *fname, struct file_struct *file, mode_t mode) { - int dfd = held_dfd_for(fname, file); + int dfd = vfs_cached_dirfd(fname, file); if (dfd >= 0) { - const char *slash = strrchr(fname, '/'); - return do_mkdir_atfd(dfd, slash ? slash + 1 : fname, mode); + char *slash = strrchr(fname, '/'); + return vfs_mkdir(dfd, slash ? slash + 1 : fname, mode, 0); } - return do_mkdir_at(fname, mode); + return vfs_mkdir(VFS_AT_FDCWD, fname, mode, 0); } static int gen_entry_chmod(const char *fname, struct file_struct *file, mode_t mode) { - int dfd = held_dfd_for(fname, file); + int dfd = vfs_cached_dirfd(fname, file); if (dfd >= 0) { const char *slash = strrchr(fname, '/'); - return do_chmod_atfd(dfd, slash ? slash + 1 : fname, mode); + return vfs_chmod(dfd, slash ? slash + 1 : fname, mode, 0); } - return do_chmod_at(fname, mode); + return vfs_chmod(VFS_AT_FDCWD, fname, mode, 0); } static void gen_entry_set_times(const char *fname, struct file_struct *file, STRUCT_STAT *stp) { - int dfd = held_dfd_for(fname, file); + int dfd = vfs_cached_dirfd(fname, file); if (dfd >= 0) { const char *slash = strrchr(fname, '/'); if (set_times_at(dfd, slash ? slash + 1 : fname, stp) != -2) @@ -1432,12 +1431,12 @@ static void gen_entry_set_times(const char *fname, struct file_struct *file, STR static int gen_entry_symlink(const char *slnk, const char *path, struct file_struct *file) { - int dfd = held_dfd_for(path, file); + int dfd = vfs_cached_dirfd(path, file); if (dfd >= 0) { const char *slash = strrchr(path, '/'); - return do_symlink_atfd(slnk, dfd, slash ? slash + 1 : path); + return vfs_symlink(slnk, dfd, slash ? slash + 1 : path, 0); } - return do_symlink_at(slnk, path); + return vfs_symlink(slnk, VFS_AT_FDCWD, path, 0); } /* True when this build compiled no fd-relative primitive able to create this @@ -1471,10 +1470,10 @@ static int no_atfd_mknod_primitive(mode_t mode) static int gen_entry_mknod(const char *path, struct file_struct *file, mode_t mode, dev_t rdev) { int dfd; - /* do_mknod_atfd can't create a socket (no portable bindat); fall back. */ - if (!S_ISSOCK(mode) && (dfd = held_dfd_for(path, file)) >= 0) { + /* vfs_mknod_atfd can't create a socket (no portable bindat); fall back. */ + if (!S_ISSOCK(mode) && (dfd = vfs_cached_dirfd(path, file)) >= 0) { const char *slash = strrchr(path, '/'); - int ret = do_mknod_atfd(dfd, slash ? slash + 1 : path, mode, rdev); + int ret = vfs_mknod(dfd, slash ? slash + 1 : path, mode, rdev, 0); /* Fall through to the unconfined path-based create only where this * build compiled no fd-relative primitive for this kind of node -- * SECURITY.md's rule for a platform that cannot be secure at all. @@ -1485,17 +1484,17 @@ static int gen_entry_mknod(const char *path, struct file_struct *file, mode_t mo if (ret == 0 || !no_atfd_mknod_primitive(mode)) return ret; } - return do_mknod_at(path, mode, rdev); + return vfs_mknod(VFS_AT_FDCWD, path, mode, rdev, 0); } static int gen_entry_unlink(const char *path, struct file_struct *file) { - int dfd = held_dfd_for(path, file); + int dfd = vfs_cached_dirfd(path, file); if (dfd >= 0) { const char *slash = strrchr(path, '/'); - return do_unlink_atfd(dfd, slash ? slash + 1 : path, 0); + return vfs_unlink(dfd, slash ? slash + 1 : path, 0); } - return do_unlink_at(path); + return vfs_unlink(VFS_AT_FDCWD, path, 0); } /* opath and npath are both expected to live in the entry's directory (the @@ -1503,14 +1502,14 @@ static int gen_entry_unlink(const char *path, struct file_struct *file) * single renameat() within it, else fall back to the full-path wrapper. */ static int gen_entry_rename(const char *opath, const char *npath, struct file_struct *file) { - int odfd = held_dfd_for(opath, file); - int ndfd = held_dfd_for(npath, file); + int odfd = vfs_cached_dirfd(opath, file); + int ndfd = vfs_cached_dirfd(npath, file); if (odfd >= 0 && ndfd >= 0) { const char *os = strrchr(opath, '/'); const char *ns = strrchr(npath, '/'); - return do_rename_atfd(odfd, os ? os + 1 : opath, ndfd, ns ? ns + 1 : npath); + return vfs_rename_atfd(odfd, os ? os + 1 : opath, ndfd, ns ? ns + 1 : npath); } - return do_rename_at(opath, npath); + return vfs_rename_at(opath, npath, 0, 0); /* both live in the entry's dir (transfer) */ } #ifdef SUPPORT_XATTRS @@ -1522,7 +1521,7 @@ static int gen_entry_rename(const char *opath, const char *npath, struct file_st * set_file_attrs' held-fd handling). */ static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file_struct *file) { - int dfd = held_dfd_for(fname, file); + int dfd = vfs_cached_dirfd(fname, file); int xfd = -1, sfd = -1, ret; if (dfd >= 0) { const char *slash = strrchr(fname, '/'); @@ -1540,18 +1539,18 @@ static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file } } #if defined AT_FDCWD && defined O_NOFOLLOW - else if (secure_relpath_active()) { - /* No cached parent dirfd (a path deeper than the dirfd cache, or a raced - * parent) but we must confine: re-pin the dest leaf through the secure - * resolver so copy_xattrs uses fsetxattr, not a path-based lsetxattr a - * flipped parent could redirect out of tree. A raced parent/leaf makes - * this fail -> refuse rather than path-write. */ + else if (vfs_relpath_active()) { + /* No cached parent dirfd (e.g. a path deeper than the dirfd cache, or a + * raced parent) but we must confine: re-pin the dest leaf through the + * secure resolver so copy_xattrs uses fsetxattr, not a path-based + * lsetxattr a flipped parent could redirect out of tree. A raced + * parent/leaf makes this fail -> refuse rather than path-write. */ int odir = 0; # ifdef O_DIRECTORY if (S_ISDIR(file->mode)) odir = O_DIRECTORY; # endif - xfd = secure_relative_open(NULL, fname, + xfd = vfs_resolve_open(NULL, fname, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC | odir, 0); if (xfd < 0) { rsyserr(FERROR_XFER, errno, @@ -1567,20 +1566,18 @@ static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file * through the operator ownership walk. Refuse (don't path-read) when we are * meant to confine but can't pin; a non-hardened receiver path-reads (sfd<0). */ #if defined AT_FDCWD && defined O_NOFOLLOW - if (secure_relpath_active() && src && *src && !symlink_optout_allowed()) { + if (vfs_relpath_active() && src && *src && !vfs_symlink_optout_allowed()) { int odir = 0; #ifdef O_DIRECTORY - if (S_ISDIR(file->mode)) /* secure_relative_open rejects a dir leaf without this */ + if (S_ISDIR(file->mode)) /* vfs_resolve_open rejects a dir leaf without this */ odir = O_DIRECTORY; #endif if (src[0] != '/') - sfd = secure_relative_open(NULL, src, O_RDONLY | O_NOFOLLOW | odir, 0); + sfd = vfs_resolve_open(NULL, src, O_RDONLY | O_NOFOLLOW | odir, 0); else { - int save = operator_path_resolve, sdfd, e; + int sdfd, e; const char *leaf; - operator_path_resolve = 1; - sdfd = owner_walk_parent(src, &leaf); - operator_path_resolve = save; + sdfd = vfs_owner_walk_parent(src, &leaf, 1); if (sdfd >= 0) { sfd = openat(sdfd, leaf, O_RDONLY | O_NOFOLLOW | odir | O_NONBLOCK | O_NOCTTY | O_CLOEXEC); e = errno; close(sdfd); errno = e; @@ -1595,6 +1592,13 @@ static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file return -1; } } +#endif +#ifdef STRICT_CONFINEMENT + /* In the confined regime the dfd/re-pin paths above yield xfd >= 0 or already + * returned -1; reaching copy_xattrs with xfd < 0 while confined would let it + * path-write the dest xattrs (the copy-xattrs fallback class) -- abort. */ + if (xfd < 0 && vfs_must_be_confined(fname, 0)) + vfs_strict_confine_fail(fname, "gen_entry_copy_xattrs dest"); #endif ret = copy_xattrs(src, sfd, fname, xfd); if (sfd >= 0) @@ -1716,10 +1720,10 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, } } if (relative_paths && !implied_dirs && file->mode != 0 - && do_stat_at(dn, &sx.st) < 0) { + && vfs_stat(VFS_AT_FDCWD, dn, &sx.st, 0) < 0) { if (dry_run) goto parent_is_dry_missing; - if (make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0) { + if (vfs_make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH, 0) < 0) { rsyserr(FERROR_XFER, errno, "recv_generator: mkdir %s failed", full_fname(dn)); @@ -1873,9 +1877,9 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, if (real_ret != 0 && gen_entry_mkdir(fname, file, file->mode|added_perms) < 0 && errno != EEXIST) { /* The parent may have just been created by make_path(), so * drop any cached (failed) dir fd before the retry. */ - reset_dir_fd_cache(); + vfs_dircache_reset(); if (!relative_paths || errno != ENOENT - || make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0 + || vfs_make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH, 0) < 0 || (gen_entry_mkdir(fname, file, file->mode|added_perms) < 0 && errno != EEXIST)) { rsyserr(FERROR_XFER, errno, "recv_generator: mkdir %s failed", @@ -2225,7 +2229,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, if (write_devices && IS_DEVICE(sx.st.st_mode) && sx.st.st_size == 0) { /* This early open into fd skips the regular open below. */ - if ((fd = do_open_nofollow(fnamecmp, O_RDONLY)) >= 0) + if ((fd = vfs_open_nofollow(fnamecmp, O_RDONLY)) >= 0) real_sx.st.st_size = sx.st.st_size = get_device_size(fd, fnamecmp); } @@ -2238,9 +2242,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, /* The --partial-dir basis is an operator/peer path: unlink it * through the exclude-aware ownership walk so a symlinked * partial-dir can't delete a file in an excluded subtree. */ - operator_path_resolve = 1; - do_unlink_at(partialptr); - operator_path_resolve = 0; + vfs_unlink(VFS_AT_FDCWD, partialptr, VFS_OPERATOR_PATH); handle_partial_dir(partialptr, PDIR_DELETE); } set_file_attrs(fname, file, &sx, NULL, maybe_ATTRS_REPORT | maybe_ATTRS_ACCURATE_TIME); @@ -2280,25 +2282,20 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, if (read_batch || whole_file) { if (inplace && make_backups > 0 && fnamecmp_type == FNAMECMP_FNAME) { /* The --backup-dir (backupptr) is an operator path; this in-place - * backup bypasses make_backup(), so set operator_path_resolve here - * too -- get_backup_name() (make_path) and copy_file() then resolve - * it with the ownership walk instead of following any symlink. */ - operator_path_resolve = 1; + * backup bypasses make_backup(), so get_backup_name() (make_path) and + * copy_file() below are passed VFS_OPERATOR_PATH to resolve it with the + * ownership walk instead of following any symlink. */ if (!(backupptr = get_backup_name(fname))) { - operator_path_resolve = 0; goto cleanup; } if (!(back_file = make_file(fname, NULL, NULL, 0, NO_FILTERS))) { - operator_path_resolve = 0; goto pretend_missing; } - if (copy_file(fname, backupptr, -1, back_file->mode) < 0) { - operator_path_resolve = 0; + if (copy_file(fname, backupptr, -1, back_file->mode, VFS_OPERATOR_PATH) < 0) { unmake_file(back_file); back_file = NULL; goto cleanup; } - operator_path_resolve = 0; } goto notify_others; } @@ -2310,7 +2307,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, } /* open the file */ - if (fd < 0 && (fd = do_open_checklinks(fnamecmp)) < 0) { + if (fd < 0 && (fd = vfs_open_checklinks(fnamecmp)) < 0) { rsyserr(FERROR, errno, "failed to open %s, continuing", full_fname(fnamecmp)); pretend_missing: @@ -2328,31 +2325,25 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, if (inplace && make_backups > 0 && fnamecmp_type == FNAMECMP_FNAME) { /* Operator --backup-dir, bypassing make_backup(): resolve get_backup_name() * (make_path), the unlink and the create with the ownership walk. */ - operator_path_resolve = 1; if (!(backupptr = get_backup_name(fname))) { - operator_path_resolve = 0; goto cleanup; } if (!(back_file = make_file(fname, NULL, NULL, 0, NO_FILTERS))) { - operator_path_resolve = 0; goto pretend_missing; } - if (robust_unlink(backupptr) && errno != ENOENT) { - operator_path_resolve = 0; + if (robust_unlink(backupptr, VFS_OPERATOR_PATH) && errno != ENOENT) { rsyserr(FERROR_XFER, errno, "unlink %s", full_fname(backupptr)); unmake_file(back_file); back_file = NULL; goto cleanup; } - if ((f_copy = do_open_at(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600)) < 0) { - operator_path_resolve = 0; + if ((f_copy = vfs_open_at(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600, VFS_OPERATOR_PATH)) < 0) { rsyserr(FERROR_XFER, errno, "open %s", full_fname(backupptr)); unmake_file(back_file); back_file = NULL; goto cleanup; } - operator_path_resolve = 0; fnamecmp_type = FNAMECMP_BACKUP; } @@ -2436,7 +2427,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, if (f_copy >= 0) close(f_copy); /* backupptr's data/xattrs were written safely (confined create under - * operator_path_resolve, held-fd xattr copy above). This metadata set + * VFS_OPERATOR_PATH, held-fd xattr copy above). This metadata set * re-resolves backupptr by path and is NOT wrapped in operator mode: * set_file_attrs() also drives the path-based xattr set whose held-fd * race-fix operator mode would defeat (cf. the copy-dest note in @@ -2498,7 +2489,7 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const #endif } else if (hlnk) { #ifdef SUPPORT_HARD_LINKS - if (!hard_link_one(file, create_name, hlnk, 0)) + if (!hard_link_one(file, create_name, hlnk, 0, 0)) return 0; #else return 0; diff --git a/hlink.c b/hlink.c index 2e84b49f4..114f3cc1d 100644 --- a/hlink.c +++ b/hlink.c @@ -473,9 +473,12 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname, } int hard_link_one(struct file_struct *file, const char *fname, - const char *oldname, int terse) + const char *oldname, int terse, int vfs_flags) { - if (do_link_at(oldname, fname) < 0) { + /* oldname is the link source (vfs_flags carries its policy -- VFS_OPERATOR_PATH + * for an alt-dest basis on a non-daemon receiver, else 0); fname is the + * transfer destination, always under the secure receiver resolve. */ + if (vfs_link_at(oldname, fname, vfs_flags, 0) < 0) { enum logcode code; if (terse) { if (!INFO_GTE(NAME, 1)) diff --git a/log.c b/log.c index 889b57bba..cec44ebd1 100644 --- a/log.c +++ b/log.c @@ -55,7 +55,6 @@ extern iconv_t ic_chck; #ifdef ICONV_OPTION extern iconv_t ic_recv; #endif -extern char curr_dir[MAXPATHLEN]; extern char *full_module_path; extern unsigned int module_dirlen; extern char sender_file_sum[MAX_DIGEST_LEN]; @@ -166,8 +165,8 @@ static void logfile_open(void) * attacker-writable dirs; a planted symlink could redirect root's log * into e.g. /root/.ssh/authorized_keys. Refuse symlinks not owned by * uid 0 or our euid. */ - int fd = open_no_attacker_symlinks(logfile_name, - O_WRONLY | O_APPEND | O_CREAT, 0644); + int fd = vfs_open_owner_walk(logfile_name, + O_WRONLY | O_APPEND | O_CREAT, 0644, 0); logfile_fp = fd >= 0 ? fdopen(fd, "a") : NULL; if (!logfile_fp && fd >= 0) close(fd); @@ -648,7 +647,7 @@ static void log_formatted(enum logcode code, const char *format, const char *op, n = buf2; } else if (am_daemon && *c != '/') { pathjoin(buf2, sizeof buf2, - curr_dir + module_dirlen, c); + vfs.curr_dir + module_dirlen, c); clean_fname(buf2, 0); if (fmt[1]) { strlcpy(c, buf2, MAXPATHLEN); diff --git a/main.c b/main.c index 6050d0e5b..475d9f969 100644 --- a/main.c +++ b/main.c @@ -70,7 +70,6 @@ extern int protect_args; extern int relative_paths; extern int sanitize_paths; extern int curr_dir_depth; -extern unsigned int curr_dir_len; extern int module_id; extern int rsync_port; extern int whole_file; @@ -106,7 +105,6 @@ extern char *password_file; extern char *backup_dir; extern char *copy_as; extern char *tmpdir; -extern char curr_dir[MAXPATHLEN]; extern char backup_dir_buf[MAXPATHLEN]; extern char *basis_dir[MAX_BASIS_DIRS+1]; extern struct file_list *first_flist; @@ -737,13 +735,13 @@ static char *get_local_name(struct file_list *flist, char *dest_path) } /* See what currently exists at the destination. */ - statret = do_stat(dest_path, &st); + statret = vfs_stat(VFS_AT_FDCWD, dest_path, &st, VFS_ALLOW_SYMLINK); cp = strrchr(dest_path, '/'); trailing_slash = cp && !cp[1]; if (mkpath_dest_arg && statret < 0 && (cp || file_total > 1)) { int save_errno = errno; - int ret = make_path(dest_path, file_total > 1 && !trailing_slash ? 0 : MKP_DROP_NAME); + int ret = vfs_make_path(dest_path, file_total > 1 && !trailing_slash ? 0 : MKP_DROP_NAME, 0); if (ret < 0) goto mkdir_error; if (ret && (INFO_GTE(NAME, 1) || stdout_format_has_i)) { @@ -754,7 +752,7 @@ static char *get_local_name(struct file_list *flist, char *dest_path) *cp = '/'; } if (ret) - statret = do_stat(dest_path, &st); + statret = vfs_stat(VFS_AT_FDCWD, dest_path, &st, VFS_ALLOW_SYMLINK); else errno = save_errno; } @@ -801,7 +799,7 @@ static char *get_local_name(struct file_list *flist, char *dest_path) exit_cleanup(RERR_SYNTAX); } - if (do_mkdir(dest_path, ACCESSPERMS) != 0) { + if (vfs_mkdir(VFS_AT_FDCWD, dest_path, ACCESSPERMS, VFS_ALLOW_SYMLINK) != 0) { mkdir_error: rsyserr(FERROR, errno, "mkdir %s failed", full_fname(dest_path)); @@ -840,7 +838,7 @@ static char *get_local_name(struct file_list *flist, char *dest_path) dest_path = "/"; *cp = '\0'; - if (dry_run && mkpath_dest_arg && do_stat(dest_path, &st) < 0) { + if (dry_run && mkpath_dest_arg && vfs_stat(VFS_AT_FDCWD, dest_path, &st, VFS_ALLOW_SYMLINK) < 0) { /* --mkpath would have created this parent dir, but a dry run did * not, so don't chdir into it; flag the destination as not yet * present (as the dir-creation path above does) so the generator @@ -862,12 +860,12 @@ static char *get_local_name(struct file_list *flist, char *dest_path) /* This function checks on our alternate-basis directories. If we're in * dry-run mode and the destination dir does not yet exist, we'll try to * tweak any dest-relative paths to make them work for a dry-run (the - * destination dir must be in curr_dir[] when this function is called). + * destination dir must be in vfs.curr_dir[] when this function is called). * We also warn about any arg that is non-existent or not a directory. */ static void check_alt_basis_dirs(void) { STRUCT_STAT st; - char *slash = strrchr(curr_dir, '/'); + char *slash = strrchr(vfs.curr_dir, '/'); int j; for (j = 0; j < basis_dir_cnt; j++) { @@ -877,13 +875,13 @@ static void check_alt_basis_dirs(void) if (bd_len > 1 && bdir[bd_len-1] == '/') bdir[--bd_len] = '\0'; /* Make a relative --link-dest/--copy-dest/--compare-dest absolute - * (vs the destination curr_dir). These are operator-trusted roots, so + * (vs the destination vfs.curr_dir). These are operator-trusted roots, so * an absolute path makes the do_*_at() wrappers use plain resolution * rather than reject an operator '..' outside the dest tree (e.g. * --copy-dest=../to). Skipped when sanitize_paths already confined * them; the dry_run>1 case keeps its leading-"../"-strip. */ if (*bdir != '/' && (dry_run > 1 || !sanitize_paths)) { - int len = curr_dir_len + 1 + bd_len + 1; + int len = vfs.curr_dir_len + 1 + bd_len + 1; char *new = new_array(char, len); if (dry_run > 1 && slash && strncmp(bdir, "../", 3) == 0) { /* We want to remove only one leading "../" prefix for @@ -891,13 +889,13 @@ static void check_alt_basis_dirs(void) * this ensures that any other ".." references get * evaluated the same as they would for a live copy. */ *slash = '\0'; - pathjoin(new, len, curr_dir, bdir + 3); + pathjoin(new, len, vfs.curr_dir, bdir + 3); *slash = '/'; } else - pathjoin(new, len, curr_dir, bdir); + pathjoin(new, len, vfs.curr_dir, bdir); basis_dir[j] = bdir = new; } - if (do_stat(bdir, &st) < 0) + if (vfs_stat(VFS_AT_FDCWD, bdir, &st, VFS_ALLOW_SYMLINK) < 0) rprintf(FWARNING, "%s arg does not exist: %s\n", alt_dest_opt(0), bdir); else if (!S_ISDIR(st.st_mode)) rprintf(FWARNING, "%s arg is not a dir: %s\n", alt_dest_opt(0), bdir); @@ -1025,7 +1023,7 @@ static int do_recv(int f_in, int f_out, char *local_name) int ret; if (backup_dir_len > 1) backup_dir_buf[backup_dir_len-1] = '\0'; - ret = do_stat(backup_dir_buf, &st); + ret = vfs_stat(VFS_AT_FDCWD, backup_dir_buf, &st, VFS_ALLOW_SYMLINK); if (ret != 0 || !S_ISDIR(st.st_mode)) { if (ret == 0) { rprintf(FERROR, "The backup-dir is not a directory: %s\n", backup_dir_buf); @@ -1045,7 +1043,7 @@ static int do_recv(int f_in, int f_out, char *local_name) if (tmpdir) { STRUCT_STAT st; - int ret = do_stat(tmpdir, &st); + int ret = vfs_stat(VFS_AT_FDCWD, tmpdir, &st, VFS_ALLOW_SYMLINK); if (ret < 0 || !S_ISDIR(st.st_mode)) { if (ret == 0) { rprintf(FERROR, "The temp-dir is not a directory: %s\n", tmpdir); @@ -1782,7 +1780,7 @@ static void unset_env_var(const char *var) } -/* The symlink-race-safe path resolver (secure_relative_open) holds one open +/* The symlink-race-safe path resolver (vfs_resolve_open) holds one open * dirfd per path component while it walks a path, plus an ancestor-dirfd cache * -- far more descriptors than legacy rsync's single open(). On a host with a * low default soft limit (e.g. OpenBSD's 128) a deep tree can hit EMFILE. @@ -1814,6 +1812,8 @@ int main(int argc,char *argv[]) raw_argc = argc; raw_argv = argv; + vfs_init(); + raise_fd_limit(); #ifdef HAVE_SIGACTION diff --git a/options.c b/options.c index 0efd02eb0..f4458b528 100644 --- a/options.c +++ b/options.c @@ -27,7 +27,6 @@ extern int module_id; extern int local_server; extern int sanitize_paths; -extern int operator_path_resolve; extern int trust_sender_args; extern int trust_sender_filter; extern unsigned int module_dirlen; @@ -61,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; @@ -123,7 +122,7 @@ int am_daemon = 0; * clientserver.c. NOT set for the daemon-level "daemon chroot = /X" * chroot: that confines path resolution to /X, but module paths * /X/modA, /X/modB, etc. are not chroot boundaries, so the per-module - * symlink-race defenses (secure_relative_open() / do_*_at() in + * symlink-race defenses (vfs_resolve_open() / do_*_at() in * syscall.c, gated by `am_daemon && !am_chrooted`) must still fire * even when the daemon is inside a daemon chroot. */ int am_chrooted = 0; @@ -331,7 +330,7 @@ static struct output_struct debug_words[COUNT_DEBUG+1] = { }; static int verbose = 0; -static int do_stats = 0; +static int vfs_stats = 0; static int do_progress = 0; static int daemon_opt; /* sets am_daemon after option error-reporting */ static int F_option_cnt = 0; @@ -621,7 +620,7 @@ static struct poptOption long_options[] = { {"quiet", 'q', POPT_ARG_NONE, 0, 'q', 0, 0 }, {"motd", 0, POPT_ARG_VAL, &output_motd, 1, 0, 0 }, {"no-motd", 0, POPT_ARG_VAL, &output_motd, 0, 0, 0 }, - {"stats", 0, POPT_ARG_NONE, &do_stats, 0, 0, 0 }, + {"stats", 0, POPT_ARG_NONE, &vfs_stats, 0, 0, 0 }, {"human-readable", 'h', POPT_ARG_NONE, 0, 'h', 0, 0}, {"no-human-readable",0, POPT_ARG_VAL, &human_readable, 0, 0, 0}, {"no-h", 0, POPT_ARG_VAL, &human_readable, 0, 0, 0}, @@ -2178,7 +2177,7 @@ int parse_arguments(int *argc_p, const char ***argv_p) set_output_verbosity(verbose, DEFAULT_PRIORITY); - if (do_stats) { + if (vfs_stats) { parse_output_words(info_words, info_levels, verbose > 1 ? "stats3" : "stats2", DEFAULT_PRIORITY); } @@ -2372,7 +2371,7 @@ int parse_arguments(int *argc_p, const char ***argv_p) STRUCT_STAT st; char prefix[SYMLINK_PREFIX_LEN]; /* NOT +1 ! */ strlcpy(prefix, SYMLINK_PREFIX, sizeof prefix); /* trim the trailing slash */ - if (do_stat(prefix, &st) == 0 && S_ISDIR(st.st_mode)) { + if (vfs_stat(VFS_AT_FDCWD, prefix, &st, VFS_ALLOW_SYMLINK) == 0 && S_ISDIR(st.st_mode)) { rprintf(FERROR, "Symlink munging is unsafe when a %s directory exists.\n", prefix); exit_cleanup(RERR_UNSUPPORTED); @@ -2645,14 +2644,11 @@ int parse_arguments(int *argc_p, const char ***argv_p) * as for --exclude-from/--include-from/--filter in exclude.c. * A daemon reads this list from a CLIENT-requested path * (--files-from=:LIST) and it must stay inside the module: - * operator_path_resolve makes the ownership walk also refuse a - * (trusted-owned) symlink that redirects the list outside the - * module root -- e.g. a root-owned backup symlink. No-op off a - * daemon (the module-root check only fires when am_daemon). */ - int save_opr = operator_path_resolve; - operator_path_resolve = 1; - filesfrom_fd = open_no_attacker_symlinks(files_from, O_RDONLY|O_BINARY, 0); - operator_path_resolve = save_opr; + * the is_operator walk also refuses a (trusted-owned) symlink + * that redirects the list outside the module root -- e.g. a + * root-owned backup symlink. No-op off a daemon (the module-root + * check only fires when am_daemon). */ + filesfrom_fd = vfs_open_owner_walk(files_from, O_RDONLY|O_BINARY, 0, 1); if (filesfrom_fd < 0) { snprintf(err_buf, sizeof err_buf, "failed to open files-from file %s: %s\n", @@ -3019,7 +3015,7 @@ void server_options(char **args, int *argc_p) args[ac++] = "--super"; if (size_only) args[ac++] = "--size-only"; - if (do_stats) + if (vfs_stats) args[ac++] = "--stats"; } else { if (skip_compress) diff --git a/params.c b/params.c index 933db7cb1..c3950d994 100644 --- a/params.c +++ b/params.c @@ -416,7 +416,7 @@ static int include_config(char *include, int manage_globals) char *match = manage_globals ? "*.conf" : "*.inc"; int ret; - if (do_stat(include, &sb) < 0) { + if (vfs_stat(VFS_AT_FDCWD, include, &sb, VFS_ALLOW_SYMLINK) < 0) { rsyserr(FLOG, errno, "unable to stat config file \"%s\"", include); return 0; } @@ -583,7 +583,7 @@ static FILE *OpenConfFile( char *FileName ) /* rsyncd.conf path (--config or default): a planted symlink could redirect * the daemon's config read. Refuse symlinks not owned by uid 0 or euid. */ { - int cfg_fd = open_no_attacker_symlinks( FileName, O_RDONLY, 0 ); + int cfg_fd = vfs_open_owner_walk( FileName, O_RDONLY, 0 , 0); OpenedFile = cfg_fd >= 0 ? fdopen( cfg_fd, "r" ) : NULL; if( !OpenedFile && cfg_fd >= 0 ) close( cfg_fd ); diff --git a/receiver.c b/receiver.c index 28d663acc..df95f95c2 100644 --- a/receiver.c +++ b/receiver.c @@ -72,7 +72,6 @@ extern int fuzzy_basis; extern struct name_num_item *xfer_sum_nni; extern int xfer_sum_len; extern int use_secure_symlinks; -extern int operator_path_resolve; static struct bitbag *delayed_bits = NULL; static int phase = 0, redoing = 0; @@ -87,7 +86,7 @@ static int updating_basis_or_equiv; /* Open a basis/output path that may legitimately be an operator-trusted * ABSOLUTE path -- e.g. an absolute --partial-dir ("a directory reserved for - * partial-dir work") or --backup-dir. secure_relative_open() deliberately + * partial-dir work") or --backup-dir. vfs_resolve_open() deliberately * rejects an absolute relpath, so feeding it the whole absolute partialptr * (with a NULL basedir) returns EINVAL: the basis fd is then -1, no basis is * mapped, and receive_data() omits every matched block from the whole-file @@ -99,7 +98,7 @@ static int updating_basis_or_equiv; * (trusted) and leaf and confine just the leaf -- exactly how secure_relative_ * open already trusts an absolute basedir while O_NOFOLLOW-confining the leaf. * Anything else is a straight pass-through that preserves the strict contract. */ -static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode) +static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode, int is_operator) { extern int am_daemon, am_chrooted; extern unsigned int module_dirlen; @@ -107,25 +106,25 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags /* "insecure links = yes": restore the 3.2.7 plain open so an operator/peer * alt-dest basis follows symlinks like legacy rsync, the same opt-out the * other daemon symlink sites honour. */ - if (symlink_optout_allowed()) { + if (vfs_symlink_optout_allowed()) { if (basedir) { char fullpath[MAXPATHLEN]; if (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) { errno = ENAMETOOLONG; return -1; } - return do_open(fullpath, flags, mode); + return vfs_open(fullpath, flags, mode); } - return do_open(relpath, flags, mode); + return vfs_open(relpath, flags, mode); } - /* A peer-supplied --partial-dir basis/staging path (operator_path_resolve set - * by recv_files) may be absolute (module_dir-prefixed on a non-chroot daemon) - * and traverse a symlink the secure_relative_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 target - * the module's exclude hides -- closing the partial-dir exclude bypass. */ - if (operator_path_resolve) { + /* A peer-supplied --partial-dir basis/staging path (is_operator, set by the + * 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_outside_confinement) refuses a + * target the module's exclude hides -- closing the partial-dir exclude bypass. */ + if (is_operator) { char fullpath[MAXPATHLEN]; const char *p = relpath; if (basedir) { @@ -135,7 +134,7 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags } p = fullpath; } - return open_no_attacker_symlinks(p, flags, mode); + return vfs_open_owner_walk(p, flags, mode, is_operator); } /* The confined resolver is needed for the sanitizing daemon @@ -147,7 +146,7 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags * "use chroot = yes" makes the kernel root the boundary, so there an alt-dest * basis like --link-dest=../01 must resolve against the cwd as a bare open did * before the hardening (confining it would reject the legitimate sibling - * "..", #915). The re-anchoring in secure_relative_open() covers the + * "..", #915). The re-anchoring in vfs_resolve_open() covers the * in-module ".." climb for the inner-module case too. */ if (!am_daemon || (am_chrooted && !module_dirlen)) { if (basedir) { @@ -156,9 +155,9 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags errno = ENAMETOOLONG; return -1; } - return do_open(fullpath, flags, mode); + return vfs_open(fullpath, flags, mode); } - return do_open(relpath, flags, mode); + return vfs_open(relpath, flags, mode); } if (!basedir && relpath && *relpath == '/') { @@ -178,9 +177,9 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags dirbuf[dlen] = '\0'; dir = dirbuf; } - return secure_relative_open(dir, leaf, flags, mode); + return vfs_resolve_open(dir, leaf, flags, mode); } - return secure_relative_open(basedir, relpath, flags, mode); + return vfs_resolve_open(basedir, relpath, flags, mode); } /* Keep the ownership policy for every attempt to open a one-inplace partial @@ -188,13 +187,8 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags * must not downgrade an operator-path open to the ordinary path resolver. */ static int secure_recv_open(const char *path, int flags, mode_t mode, int owner_walk) { - int fd, save = operator_path_resolve; - - if (owner_walk) - operator_path_resolve = 1; - fd = secure_basis_open(NULL, path, flags, mode); - operator_path_resolve = save; - return fd; + return secure_basis_open(NULL, path, flags, mode, + owner_walk ? VFS_OPERATOR_PATH : 0); } /* Open a read-only regular file for an in-place update without leaving its @@ -216,7 +210,7 @@ static int open_readonly_inplace(const char *fname, int one_inplace) cfd = secure_recv_open(fname, O_RDONLY|O_NOFOLLOW, 0, one_inplace); if (cfd < 0) goto failed; - if (do_fstat(cfd, &cst) < 0 || !S_ISREG(cst.st_mode)) { + if (vfs_fstat(cfd, &cst) < 0 || !S_ISREG(cst.st_mode)) { errno = EACCES; /* refused: not the read-only regular file we recover */ goto failed; } @@ -252,10 +246,10 @@ static int open_readonly_inplace(const char *fname, int one_inplace) /* Local and chrooted transfers retain the existing pathname semantics. * Note the S_ISREG test here is a type check on a stable path, NOT race - * protection: do_stat() follows a leaf symlink and each call below + * protection: vfs_stat() follows a leaf symlink and each call below * re-resolves the name. The fd-based branch above is the one that * pins an inode; a chroot is what confines this one. */ - if (do_stat(fname, &cst) < 0) { + if (vfs_stat(VFS_AT_FDCWD, fname, &cst, VFS_ALLOW_SYMLINK) < 0) { errno = EACCES; return -1; } @@ -264,11 +258,11 @@ static int open_readonly_inplace(const char *fname, int one_inplace) return -1; } prior_mode = cst.st_mode & CHMOD_BITS; - if (do_chmod_at(fname, prior_mode | S_IWUSR) < 0) + if (vfs_chmod(VFS_AT_FDCWD, fname, prior_mode | S_IWUSR, 0) < 0) return -1; - fd = do_open(fname, O_WRONLY, 0600); + fd = vfs_open(fname, O_WRONLY, 0600); open_errno = errno; - if (do_chmod_at(fname, prior_mode) < 0) { + if (vfs_chmod(VFS_AT_FDCWD, fname, prior_mode, 0) < 0) { restore_errno = errno; if (fd >= 0) close(fd); @@ -418,34 +412,34 @@ int open_tmpfile(char *fnametmp, const char *fname, struct file_struct *file) * access to ensure that there is no race condition. They will be * correctly updated after the right owner and group info is set. * (Thanks to snabb@epipe.fi for pointing this out.) */ - /* For any non-chrooted receiver (secure_relpath_active()), create the + /* For any non-chrooted receiver (vfs_relpath_active()), create the * temp file securely so a parent-symlink race can't redirect it. When * the temp lives in the entry's own dir (the common case, no --temp-dir) - * use the cached held dir fd; otherwise fall back to secure_mkstemp. An + * use the cached held dir fd; otherwise fall back to vfs_secure_mkstemp. An * operator-supplied --temp-dir (tmpdir) gets the ownership-walk resolver * (it may legitimately point outside the tree); the deep-entry-dir fallback, * when the held-dirfd cache declines, gets the strict transfer-path one. */ - if (secure_relpath_active()) { - int dfd = held_dfd_for(fnametmp, file); + if (vfs_relpath_active()) { + int dfd = vfs_cached_dirfd(fnametmp, file); if (dfd >= 0) { char *slash = strrchr(fnametmp, '/'); - fd = do_mkstemp_atfd(dfd, slash ? slash + 1 : fnametmp, + fd = vfs_mkstemp_atfd(dfd, slash ? slash + 1 : fnametmp, (file->mode|added_perms) & INITACCESSPERMS); } else - fd = secure_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS, + fd = vfs_secure_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS, tmpdir != NULL); } else - fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS); + fd = vfs_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS); #if 0 /* In most cases parent directories will already exist because their * information should have been previously transferred, but that may * not be the case with -R */ if (fd == -1 && relative_paths && errno == ENOENT - && make_path(fnametmp, MKP_SKIP_SLASH | MKP_DROP_NAME) == 0) { + && vfs_make_path(fnametmp, MKP_SKIP_SLASH | MKP_DROP_NAME, 0) == 0) { /* Get back to name with XXXXXX in it. */ get_tmpname(fnametmp, fname, False); - fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS); + fd = vfs_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS); } #endif @@ -476,14 +470,14 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r, if (preallocate_files && fd != -1 && total_size > 0 && (!inplace_sizing || total_size > size_r)) { /* Try to preallocate enough space for file's eventual length. Can * reduce fragmentation on filesystems like ext4, xfs, and NTFS. */ - if ((preallocated_len = do_fallocate(fd, 0, total_size)) < 0) - rsyserr(FWARNING, errno, "do_fallocate %s", full_fname(fname)); + if ((preallocated_len = vfs_fallocate(fd, 0, total_size)) < 0) + rsyserr(FWARNING, errno, "vfs_fallocate %s", full_fname(fname)); } else #endif if (inplace_sizing) { #ifdef HAVE_FTRUNCATE /* The most compatible way to create a sparse file is to start with no length. */ - if (sparse_files > 0 && whole_file && fd >= 0 && do_ftruncate(fd, 0) == 0) + if (sparse_files > 0 && whole_file && fd >= 0 && vfs_ftruncate(fd, 0) == 0) preallocated_len = 0; else #endif @@ -526,7 +520,7 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r, } } offset = sum.flength; - if (fd != -1 && (j = do_lseek(fd, offset, SEEK_SET)) != offset) { + if (fd != -1 && (j = vfs_lseek(fd, offset, SEEK_SET)) != offset) { rsyserr(FERROR_XFER, errno, "lseek of %s returned %s, not %s", full_fname(fname), big_num(j), big_num(offset)); exit_cleanup(RERR_FILEIO); @@ -650,7 +644,7 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r, * preallocate_files: total_size could have been an overestimate. * Cut off any extra preallocated zeros from dest file. */ if ((inplace_sizing || preallocated_len > offset) && fd != -1 && !IS_DEVICE(file->mode)) { - if (do_ftruncate(fd, offset) < 0) + if (vfs_ftruncate(fd, offset) < 0) rsyserr(FERROR_XFER, errno, "ftruncate failed on %s", full_fname(fname)); } #endif @@ -703,9 +697,9 @@ static void handle_delayed_updates(char *local_name) * walk so a symlinked partial-dir can't move a file out of * an excluded subtree. */ int rret; - operator_path_resolve = 1; - rret = do_rename_at(partialptr, fname); - operator_path_resolve = 0; + /* partialptr is the operator-supplied --partial-dir source (owner + * walk); fname is the transfer destination (secure receiver resolve). */ + rret = vfs_rename_at(partialptr, fname, VFS_OPERATOR_PATH, 0); if (rret < 0) { rsyserr(FERROR_XFER, errno, "rename failed for %s (from %s)", @@ -1068,12 +1062,12 @@ int recv_files(int f_in, int f_out, char *local_name) && fnamecmp && *fnamecmp != '/') { /* The relative partial path contains peer-derived directory * components. It is not an operator-trusted path as a whole. */ - fd1 = secure_relative_open(NULL, fnamecmp, O_RDONLY, 0); - } else if (!basedir && (bdfd = held_dfd_for(fnamecmp, file)) >= 0) { + fd1 = vfs_resolve_open(NULL, fnamecmp, O_RDONLY, 0); + } else if (!basedir && (bdfd = vfs_cached_dirfd(fnamecmp, file)) >= 0) { const char *slash; assert(fnamecmp != NULL); /* set on every path above */ slash = strrchr(fnamecmp, '/'); - fd1 = do_open_atfd(bdfd, slash ? slash + 1 : fnamecmp, O_RDONLY, 0); + fd1 = vfs_open_atfd(bdfd, slash ? slash + 1 : fnamecmp, O_RDONLY, 0); } else { /* An operator-supplied basis -- a --partial-dir, or an * alt-dest basedir (--copy-dest/--compare-dest/--link-dest) -- @@ -1084,10 +1078,9 @@ int recv_files(int f_in, int f_out, char *local_name) * 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; + fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0, + ((basedir && !am_daemon) || fnamecmp_type == FNAMECMP_PARTIAL_DIR) ? VFS_OPERATOR_PATH : 0); + } } if (fnamecmp_type == FNAMECMP_PARTIAL_DIR && fd1 == -1) { @@ -1110,7 +1103,7 @@ int recv_files(int f_in, int f_out, char *local_name) if (fnamecmp != fname) { fnamecmp = fname; fnamecmp_type = FNAMECMP_FNAME; - fd1 = do_open_nofollow(fnamecmp, O_RDONLY); + fd1 = vfs_open_nofollow(fnamecmp, O_RDONLY); } if (fd1 == -1 && basis_dir[0]) { @@ -1118,10 +1111,8 @@ 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; + fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0, + !am_daemon ? VFS_OPERATOR_PATH : 0); } } @@ -1142,7 +1133,7 @@ int recv_files(int f_in, int f_out, char *local_name) if (fd1 == -1) { st.st_mode = 0; st.st_size = 0; - } else if (do_fstat(fd1,&st) != 0) { + } else if (vfs_fstat(fd1,&st) != 0) { rsyserr(FERROR_XFER, errno, "fstat %s failed", full_fname(fnamecmp)); discard_receive_data(f_in, file); @@ -1194,18 +1185,18 @@ int recv_files(int f_in, int f_out, char *local_name) /* We now check to see if we are writing the file "inplace" */ if (inplace || one_inplace) { fnametmp = one_inplace ? partialptr : fname; - /* For any non-chrooted receiver (secure_relpath_active()), + /* For any non-chrooted receiver (vfs_relpath_active()), * use secure open to prevent symlink race attacks where an * attacker could switch a directory to a symlink between * path validation and file open. */ /* one_inplace stages into the operator/peer --partial-dir path: * resolve it with the ownership walk (exclude-aware) so it can't be * redirected through a symlink into an excluded subtree. */ - if (secure_relpath_active()) + if (vfs_relpath_active()) fd2 = secure_recv_open(fnametmp, O_WRONLY|O_CREAT, 0600, one_inplace); else - fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600); + fd2 = vfs_open(fnametmp, O_WRONLY|O_CREAT, 0600); #ifdef linux if (fd2 == -1 && errno == EACCES) { /* Maybe the error was due to protected_regular setting? */ @@ -1213,7 +1204,7 @@ int recv_files(int f_in, int f_out, char *local_name) fd2 = secure_recv_open(fnametmp, O_WRONLY, 0600, one_inplace); else - fd2 = do_open(fnametmp, O_WRONLY, 0600); + fd2 = vfs_open(fnametmp, O_WRONLY, 0600); } #endif if (fd2 == -1 && errno == EACCES) { @@ -1292,9 +1283,7 @@ int recv_files(int f_in, int f_out, char *local_name) /* Unlink the consumed --partial-dir basis through the * exclude-aware ownership walk (a symlinked partial-dir * must not delete a file in an excluded subtree). */ - operator_path_resolve = 1; - do_unlink_at(partialptr); - operator_path_resolve = 0; + vfs_unlink(VFS_AT_FDCWD, partialptr, VFS_OPERATOR_PATH); } handle_partial_dir(partialptr, PDIR_DELETE); } @@ -1304,7 +1293,7 @@ int recv_files(int f_in, int f_out, char *local_name) "Unable to create partial-dir for %s -- discarding %s.\n", local_name ? local_name : f_name(file, NULL), recv_ok ? "completed file" : "partial file"); - do_unlink_at(fnametmp); + vfs_unlink(VFS_AT_FDCWD, fnametmp, 0); recv_ok = -1; } else if (!finish_transfer(partialptr, fnametmp, fnamecmp, NULL, file, recv_ok, !partial_dir)) @@ -1315,7 +1304,7 @@ int recv_files(int f_in, int f_out, char *local_name) } else partialptr = NULL; } else if (!one_inplace) - do_unlink_at(fnametmp); + vfs_unlink(VFS_AT_FDCWD, fnametmp, 0); cleanup_disable(); diff --git a/rsync.c b/rsync.c index 19a371d97..1f0322598 100644 --- a/rsync.c +++ b/rsync.c @@ -37,7 +37,6 @@ extern int omit_dir_times; extern int omit_link_times; extern int am_root; extern int am_server; -extern int operator_path_resolve; extern int am_daemon; extern int am_sender; extern int am_receiver; @@ -514,6 +513,13 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp, int op_leaf_fd = -1; /* O_NOFOLLOW fd pinning a cross-tree operator leaf */ int op_pin = 0; /* drive chmod/chown off op_leaf_fd for a cross-tree leaf */ int op_refuse = 0; /* pin open hit the symlink-race signal: refuse, don't redirect */ + /* The ownership walk for the path-based chmod/chown fallbacks below. op_pin + * covers a reg/dir/fifo leaf with a pinned fd, but a symlink or device leaf + * never enters it, and a non-root operator can fail the pin open with a plain + * EACCES and fall through -- both must still resolve the operator path via the + * walk rather than a bare lchown()/chmod(). (vfs_chmod's operator branch skips + * S_ISLNK itself, so a symlink-as-object keeps the lchmod/setattrlist path.) */ + int op_vfs = (flags & ATTRS_OPERATOR_PATH) ? VFS_OPERATOR_PATH : 0; #if defined SUPPORT_XATTRS || defined SUPPORT_ACLS int held_fd = -1; /* held O_NOFOLLOW fd for fd-based xattr/ACL ops, or -1 */ int xattr_refuse = 0; /* no confined fd for a slashed path: skip path-based xattr/ACL */ @@ -526,7 +532,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp, /* Stat through the entry's held dir fd (like gen_entry_stat) so we * don't re-walk the full path here; link_stat_at folds in no * fake-super xattr, so only when am_root >= 0. */ - if (am_root >= 0 && (sdfd = held_dfd_for(fname, file)) >= 0) { + if (am_root >= 0 && (sdfd = vfs_cached_dirfd(fname, file)) >= 0) { const char *sl = strrchr(fname, '/'); sret = link_stat_at(sdfd, sl ? sl + 1 : fname, &sx2.st, 0); } else @@ -546,7 +552,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp, * issue single-component *at() calls against it instead of re-resolving * the full path each time. -1 => fall back to the full-path wrappers * (cross-tree path such as --temp-dir/--backup-dir, or gated off). */ - dfd = held_dfd_for(fname, file); + dfd = vfs_cached_dirfd(fname, file); if (dfd >= 0) { const char *slash = strrchr(fname, '/'); leaf = slash ? slash + 1 : fname; @@ -569,30 +575,26 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp, )) held_fd = openat(dfd, leaf, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC); - /* If the held-fd pin above missed (no cached dir fd -- a path deeper than the - * dirfd cache, or a raced leaf) but we are a confined receiver on a - * non-operator path, re-pin the leaf through the secure resolver so the - * xattr/ACL ops below drive fsetxattr off a confined fd -- NOT a raw path-based - * lsetxattr, which re-resolves the parent and lets a flipped dest/sub symlink - * land the xattr OUTSIDE the tree (copy-xattrs-symlink-race). If the secure - * re-pin also fails (a genuinely raced parent/leaf symlink), held_fd stays -1 - * and xattr_refuse skips the path-based ops rather than redirecting them. - * (chmod/chown/times stay safe via their secure path wrappers; operator paths - * use op_pin/op_refuse below.) */ - if (held_fd < 0 && !operator_path_resolve && secure_relpath_active() + /* If the held-fd pin above missed (no cached dir fd, or a raced leaf) but we + * are a confined receiver on a non-operator path, re-pin the leaf through the + * secure resolver so the xattr/ACL ops below drive fsetxattr off a confined fd + * -- NOT a raw path-based lsetxattr, which re-resolves the parent and lets a + * flipped dest/sub symlink land the xattr OUTSIDE the tree (copy-xattrs- + * symlink-race). A confined receiver normally always has the cached pin; a + * miss here is a raced parent/leaf. If the secure re-pin also fails (the + * parent/leaf is a symlink), held_fd stays -1 and xattr_refuse below skips the + * path-based ops rather than redirecting them. (chmod/chown/times stay safe + * via their secure path wrappers; operator paths use op_pin/op_refuse.) */ + if (held_fd < 0 && !(flags & ATTRS_OPERATOR_PATH) && vfs_relpath_active() && (S_ISREG(sxp->st.st_mode) || S_ISDIR(sxp->st.st_mode) || S_ISFIFO(sxp->st.st_mode)) && (preserve_xattrs || am_root < 0 # ifdef SUPPORT_ACLS || (preserve_acls && am_root >= 0) # endif )) { - int odir = 0; -# ifdef O_DIRECTORY - if (S_ISDIR(sxp->st.st_mode)) - odir = O_DIRECTORY; -# endif - held_fd = secure_relative_open(NULL, fname, - O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC | odir, 0); + held_fd = vfs_resolve_open(NULL, fname, + O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC + | (S_ISDIR(sxp->st.st_mode) ? O_DIRECTORY : 0), 0); if (held_fd < 0 && strchr(fname, '/')) xattr_refuse = 1; } @@ -607,16 +609,16 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp, * operator owner-walk resolver and drive fchmod/fchown off that fd. A raced * symlink leaf makes the open fail, leaving op_leaf_fd == -1: the metadata op * is then refused, never redirected. --insecure-links opts back out (the - * resolver in do_open_at honours it), and a genuine symlink leaf (a symlink + * resolver in vfs_open_at honours it), and a genuine symlink leaf (a symlink * backup) keeps the existing l-variant path. */ /* Gate on the INTENDED type (new_mode), not the on-disk type (sxp->st): the * attacker controls the latter via the flip, and a dir component that has * just been flipped to a symlink must still take the pinned path so the * O_NOFOLLOW open refuses it -- otherwise the lchown would launder it. */ - op_pin = operator_path_resolve && dfd < 0 && !symlink_optout_allowed() + op_pin = (flags & ATTRS_OPERATOR_PATH) && dfd < 0 && !vfs_symlink_optout_allowed() && (S_ISREG(new_mode) || S_ISDIR(new_mode) || S_ISFIFO(new_mode)); if (op_pin) { - op_leaf_fd = do_open_at(fname, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0); + op_leaf_fd = vfs_open_at(fname, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0, VFS_OPERATOR_PATH); /* When running as root (the uid-0 trust-laundering case) an O_RDONLY open * of a real owned reg/dir/fifo leaf never fails for permission reasons, so * ANY failure here means the leaf is being raced (a symlink refused by @@ -647,6 +649,29 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp, if (daemon_chmod_modes && !S_ISLNK(new_mode)) new_mode = tweak_mode(new_mode, daemon_chmod_modes); +#if (defined SUPPORT_XATTRS || defined SUPPORT_ACLS) && defined STRICT_CONFINEMENT + /* Enforce the pin/re-pin invariant: for a confined, pinnable, non-operator + * leaf with metadata work pending, the held-fd pin/re-pin above must have + * produced a confined fd (held_fd >= 0) or set xattr_refuse. Reaching here + * with neither means the xattr/ACL setters would take their raw path-based + * branch (the copy-xattrs fallback class) -- abort so the suite catches the + * regression. The clause mirrors the pin condition (excluding no-metadata- + * work, symlink/operator/opt-out, etc.); it is intentionally a touch broader + * than "the next setter definitely path-writes" (set_xattr is skipped when + * fnamecmp == NULL; a native ACL may still take a dirfd+leaf route), but a + * confined slashed path only reaches here once the invariant is already + * broken, so it cannot false-abort a legitimate transfer. */ + if (held_fd < 0 && !op_refuse && !xattr_refuse && !(flags & ATTRS_OPERATOR_PATH) + && (S_ISREG(sxp->st.st_mode) || S_ISDIR(sxp->st.st_mode) || S_ISFIFO(sxp->st.st_mode)) + && (preserve_xattrs || am_root < 0 +# ifdef SUPPORT_ACLS + || (preserve_acls && am_root >= 0) +# endif + ) + && vfs_must_be_confined(fname, 0)) + vfs_strict_confine_fail(fname, "xattr/ACL set"); +#endif + #ifdef SUPPORT_ACLS if (preserve_acls && !S_ISLNK(file->mode) && !ACL_READY(*sxp) && !op_refuse && !xattr_refuse) get_acl_fdat(held_fd, dfd, leaf, fname, sxp); @@ -676,10 +701,10 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp, if (am_root >= 0) { uid_t uid = change_uid ? (uid_t)F_OWNER(file) : sxp->st.st_uid; gid_t gid = change_gid ? (gid_t)F_GROUP(file) : sxp->st.st_gid; - if ((op_leaf_fd >= 0 ? do_fchown(op_leaf_fd, uid, gid) + if ((op_leaf_fd >= 0 ? vfs_fchown(op_leaf_fd, uid, gid) : op_refuse ? (errno = ELOOP, -1) - : dfd >= 0 ? do_lchown_atfd(dfd, leaf, uid, gid) - : do_lchown_at(fname, uid, gid)) != 0) { + : dfd >= 0 ? vfs_lchown(dfd, leaf, uid, gid, 0) + : vfs_lchown(VFS_AT_FDCWD, fname, uid, gid, op_vfs)) != 0) { /* We shouldn't have attempted to change uid * or gid unless have the privilege. */ rsyserr(FERROR_XFER, errno, "%s %s failed", @@ -751,13 +776,13 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp, if (crtimes_ndx && !(flags & ATTRS_SKIP_CRTIME)) { time_t file_crtime = F_CRTIME(file); if (sxp->crtime == 0) - sxp->crtime = get_create_time(fname, &sxp->st); + sxp->crtime = vfs_get_create_time(fname, &sxp->st); if (!same_time(sxp->crtime, 0L, file_crtime, 0L)) { if ( #ifdef HAVE_GETATTRLIST - do_setattrlist_crtime(fname, file_crtime) == 0 + vfs_setattrlist_crtime(fname, file_crtime) == 0 #elif defined __CYGWIN__ - do_SetFileTime(fname, file_crtime) == 0 + vfs_SetFileTime(fname, file_crtime) == 0 #else #error Unknown crtimes implementation #endif @@ -770,7 +795,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp, int ret; #ifdef HAVE_FUTIMENS if (op_leaf_fd >= 0) - ret = do_futimens(op_leaf_fd, &sx2.st); + ret = vfs_futimens(op_leaf_fd, &sx2.st); else #endif if (op_refuse) @@ -806,10 +831,10 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp, #ifdef HAVE_CHMOD if (!BITS_EQUAL(sxp->st.st_mode, new_mode, CHMOD_BITS)) { int ret = am_root < 0 ? 0 - : op_leaf_fd >= 0 ? do_fchmod(op_leaf_fd, new_mode) + : op_leaf_fd >= 0 ? vfs_fchmod(op_leaf_fd, new_mode) : op_refuse ? (errno = ELOOP, -1) - : dfd >= 0 && !S_ISLNK(new_mode) ? do_chmod_atfd(dfd, leaf, new_mode) - : do_chmod_at(fname, new_mode); + : dfd >= 0 && !S_ISLNK(new_mode) ? vfs_chmod(dfd, leaf, new_mode, 0) + : vfs_chmod(VFS_AT_FDCWD, fname, new_mode, op_vfs); if (ret < 0) { rsyserr(FERROR_XFER, errno, "failed to set permissions on %s", @@ -907,10 +932,8 @@ int finish_transfer(const char *fname, const char *fnametmp, * 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; + ATTRS_OPERATOR_PATH | (ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME)); /* move tmp file over real file */ if (DEBUG_GTE(RECV, 1)) @@ -922,7 +945,7 @@ int finish_transfer(const char *fname, const char *fnametmp, full_fname(fnametmp), fname); if (!partialptr || (ret == -2 && temp_copy_name) || robust_rename(fnametmp, partialptr, NULL, file->mode, file) < 0) - do_unlink_at(fnametmp); + vfs_unlink(VFS_AT_FDCWD, fnametmp, 0); return 0; } if (ret == 0) { @@ -938,7 +961,9 @@ int finish_transfer(const char *fname, const char *fnametmp, ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME); if (temp_copy_name) { - if (do_rename_at(fnametmp, fname) < 0) { + /* temp_copy_name and fname both live in the dest tree here; flag 0 lets + * vfs_twopath_side confine each side (absolute=owner-walk, relative=secure). */ + if (vfs_rename_at(fnametmp, fname, 0, 0) < 0) { rsyserr(FERROR_XFER, errno, "rename %s -> \"%s\"", full_fname(fnametmp), fname); return 0; diff --git a/rsync.h b/rsync.h index b15aa1af6..f5c286ed4 100644 --- a/rsync.h +++ b/rsync.h @@ -223,6 +223,7 @@ #define ATTRS_SKIP_MTIME (1<<1) #define ATTRS_ACCURATE_TIME (1<<2) #define ATTRS_SKIP_ATIME (1<<3) +#define ATTRS_OPERATOR_PATH (1<<4) /* fname is a cross-tree operator path: pin its leaf (op_pin) */ #define ATTRS_SKIP_CRTIME (1<<5) #define MSG_FLUSH 2 @@ -1237,12 +1238,13 @@ struct name_num_obj { #ifndef __cplusplus #include "proto.h" +#include "vfs/vfs.h" #endif #ifndef SUPPORT_XATTRS -#define x_stat(fn,fst,xst) do_stat(fn,fst) -#define x_lstat(fn,fst,xst) do_lstat(fn,fst) -#define x_fstat(fd,fst,xst) do_fstat(fd,fst) +#define x_stat(fn,fst,xst,vfsflags) vfs_stat(VFS_AT_FDCWD, fn, fst, vfsflags) +#define x_lstat(fn,fst,xst,vfsflags) vfs_lstat(VFS_AT_FDCWD, fn, fst, vfsflags) +#define x_fstat(fd,fst,xst) vfs_fstat(fd,fst) #endif /* We have replacement versions of these if they're missing. */ @@ -1281,7 +1283,7 @@ extern int errno; #ifdef HAVE_READLINK #define SUPPORT_LINKS 1 #if !defined NO_SYMLINK_XATTRS && !defined NO_SYMLINK_USER_XATTRS -#define do_readlink(path, buf, bufsiz) readlink(path, buf, bufsiz) +#define vfs_readlink(path, buf, bufsiz) readlink(path, buf, bufsiz) #endif #endif #ifdef HAVE_LINK diff --git a/sender.c b/sender.c index bb1b137d0..5b7c861cb 100644 --- a/sender.c +++ b/sender.c @@ -85,7 +85,7 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname, /* "insecure links = yes" / --insecure-links: restore the 3.2.7 plain re-stat * by declining the confined parent (errno=0 makes the caller use do_lstat). */ - if (symlink_optout_allowed()) { + if (vfs_symlink_optout_allowed()) { errno = 0; return -1; } @@ -127,21 +127,21 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname, #endif while (*rel == '/') rel++; - return secure_relative_open("/", rel, + return vfs_resolve_open("/", rel, O_RDONLY | O_DIRECTORY, 0); } - /* held_dir_path_fd returns a cache-OWNED fd; the caller closes + /* vfs_path_dirfd returns a cache-OWNED fd; the caller closes * what we return, so hand back an owned dup and leave the cache's * dirfd intact. An uncacheable (very deep) dir declines with * errno 0 -- fall back to the full confined walk (an owned fd, * matching the sender's content open) so deep paths stay confined * too; a real error propagates. */ - dfd = held_dir_path_fd(NULL, dir); + dfd = vfs_path_dirfd(NULL, dir); if (dfd >= 0) return dup(dfd); if (errno != 0) return -1; - return secure_relative_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0); + return vfs_resolve_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0); } errno = 0; /* top-level file: no parent component to confine */ return -1; @@ -176,9 +176,9 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname, } memcpy(dir, relp, dlen); dir[dlen] = '\0'; - dfd = secure_relative_open(module_dir, dir, O_RDONLY | O_DIRECTORY, 0); + dfd = vfs_resolve_open(module_dir, dir, O_RDONLY | O_DIRECTORY, 0); } else - dfd = secure_relative_open(module_dir, "", O_RDONLY | O_DIRECTORY, 0); + dfd = vfs_resolve_open(module_dir, "", O_RDONLY | O_DIRECTORY, 0); /* The leaf is the same last component either way; take it from the caller's * persistent fname buffer, not the local secure_path. */ @@ -195,20 +195,20 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname, #endif } -/* Go through the do_*() wrapper rather than a raw unlinkat(): it carries the - * dry_run no-op and the read-only/list-only refusal that do_unlink() applies - * on the non-fd path, plus the missing-AT_FDCWD fallback. */ +/* Go through the VFS wrapper rather than a raw unlinkat(): it carries the + * dry_run no-op and the read-only/list-only refusal that the plain unlink path + * applies, plus the missing-AT_FDCWD fallback. */ static int secure_remove_source_file(int dfd, const char *bname) { - return do_unlink_atfd(dfd, bname, 0); + return vfs_unlink(dfd, bname, 0); } /* Open `relpath` (relative to `anchor`: NULL=cwd, else an absolute trusted root) * with `flags`, opening the leaf via the shared held ancestor-dirfd stack - * (held_dir_path_fd) so a directory is walked once, not once per file. The leaf - * semantics are identical to secure_relative_open() -- it always O_NOFOLLOWs a + * (vfs_path_dirfd) so a directory is walked once, not once per file. The leaf + * semantics are identical to vfs_resolve_open() -- it always O_NOFOLLOWs a * file leaf and folds in O_NOATIME, both preserved here. An uncacheable path - * (held_dir_path_fd returns -1) falls back to the full confined walk. */ + * (vfs_path_dirfd returns -1) falls back to the full confined walk. */ static int sender_open_confined(const char *anchor, const char *relpath, int flags) { #ifdef AT_FDCWD @@ -237,14 +237,14 @@ static int sender_open_confined(const char *anchor, const char *relpath, int fla if (open_noatime) flags |= O_NOATIME; #endif - dfd = held_dir_path_fd(anchor, dir); + dfd = vfs_path_dirfd(anchor, dir); if (dfd < 0) - return secure_relative_open(anchor, relpath, flags | O_NOFOLLOW, 0); + return vfs_resolve_open(anchor, relpath, flags | O_NOFOLLOW, 0); return openat(dfd, bname, flags | O_NOFOLLOW, 0); #else - /* No *at() support: secure_relative_open is a plain open() here (no walk, + /* No *at() support: vfs_resolve_open is a plain open() here (no walk, * so nothing to amortise); use it directly to keep the anchor semantics. */ - return secure_relative_open(anchor, relpath, flags | O_NOFOLLOW, 0); + return vfs_resolve_open(anchor, relpath, flags | O_NOFOLLOW, 0); #endif } @@ -253,7 +253,7 @@ static int sender_open_confined(const char *anchor, const char *relpath, int fla * O_NOFOLLOW that sender_open_confined() applies refuses an in-tree symlink the * operator explicitly asked to follow, so resolve the link ourselves: read it, * refuse an absolute or "../"-escaping target (a module escape), and re-resolve - * the relative target through secure_relative_open() -- which follows in-tree + * the relative target through vfs_resolve_open() -- which follows in-tree * links and rejects an escape above the anchor -- looping for a symlink chain. * The final open is still O_NOFOLLOW, so a raced flip at the resolved leaf is * refused. This keeps the module boundary while honouring --copy-links. */ @@ -292,14 +292,14 @@ static int sender_open_copylinks_confined(const char *anchor, const char *relpat * only this branch would hand it to strcmp(). */ if (am_daemon && module_dirfd >= 0 && module_dir && anchor && strcmp(anchor, module_dir) == 0) - pdfd = secure_relative_open_at_beneath(module_dirfd, dir, + pdfd = vfs_resolve_open_at_beneath(module_dirfd, dir, O_RDONLY | O_DIRECTORY, 0); else - pdfd = secure_relative_open(anchor, dir, + pdfd = vfs_resolve_open(anchor, dir, O_RDONLY | O_DIRECTORY, 0); if (pdfd < 0) return -1; - n = do_readlink_atfd(pdfd, bname, tgt, sizeof tgt - 1); + n = vfs_readlink_atfd(pdfd, bname, tgt, sizeof tgt - 1); e = errno; if (n < 0) { /* EINVAL: not a symlink -> the resolved target file. Open it @@ -327,7 +327,7 @@ static int sender_open_copylinks_confined(const char *anchor, const char *relpat errno = ELOOP; return -1; #else - return secure_relative_open(anchor, relpath, O_RDONLY | O_NOFOLLOW, 0); + return vfs_resolve_open(anchor, relpath, O_RDONLY | O_NOFOLLOW, 0); #endif } @@ -424,8 +424,8 @@ void successful_send(int ndx) } if (dfd >= 0 - ? (copy_links ? do_stat_atfd(dfd, bname, &st) : do_lstat_atfd(dfd, bname, &st)) < 0 - : (copy_links ? do_stat(fname, &st) : do_lstat(fname, &st)) < 0) { + ? (copy_links ? vfs_stat(dfd, bname, &st, 0) : vfs_lstat(dfd, bname, &st, 0)) < 0 + : (copy_links ? vfs_stat(VFS_AT_FDCWD, fname, &st, VFS_ALLOW_SYMLINK) : vfs_lstat(VFS_AT_FDCWD, fname, &st, VFS_ALLOW_SYMLINK)) < 0) { failed_op = "re-lstat"; goto failed; } @@ -450,7 +450,7 @@ void successful_send(int ndx) return; } - if (dfd >= 0 ? secure_remove_source_file(dfd, bname) < 0 : do_unlink(fname) < 0) { + if (dfd >= 0 ? secure_remove_source_file(dfd, bname) < 0 : vfs_unlink(VFS_AT_FDCWD, fname, VFS_ALLOW_SYMLINK) < 0) { failed_op = "remove"; failed: if (errno == ENOENT) @@ -647,13 +647,13 @@ void send_files(int f_in, int f_out) exit_cleanup(RERR_PROTOCOL); } - if (symlink_optout_allowed()) { + if (vfs_symlink_optout_allowed()) { /* Module opted out of symlink confinement ("insecure links = * yes", admin-only) -- or a non-daemon --insecure-links: legacy * unconfined open, restoring the pre-hardening content read * (re-opening the escape for that module; documented). */ - fd = do_open_checklinks(fname); - } else if (secure_relpath_active()) { + fd = vfs_open_checklinks(fname); + } else if (vfs_relpath_active()) { /* Open from module root to prevent TOCTOU race where * change_pathname's chdir follows a directory symlink. * Reconstruct the full path relative to module_dir @@ -671,7 +671,7 @@ void send_files(int f_in, int f_out) } /* A module with `path = /` makes F_PATHNAME absolute, so the * joined path starts with '/'; strip leading slashes to a - * module-relative path that secure_relative_open accepts (#897). */ + * module-relative path that vfs_resolve_open accepts (#897). */ relp = secure_path; while (*relp == '/') relp++; @@ -704,7 +704,7 @@ void send_files(int f_in, int f_out) } else fd = sender_open_confined(NULL, fname, O_RDONLY); } else { - fd = do_open_checklinks(fname); + fd = vfs_open_checklinks(fname); } if (fd == -1) { if (errno == ENOENT) { @@ -725,7 +725,7 @@ void send_files(int f_in, int f_out) } /* map the local file */ - if (do_fstat(fd, &st) != 0) { + if (vfs_fstat(fd, &st) != 0) { io_error |= IOERR_GENERAL; rsyserr(FERROR_XFER, errno, "fstat failed"); free_sums(s); diff --git a/syscall.c b/syscall.c deleted file mode 100644 index 5e92edca0..000000000 --- a/syscall.c +++ /dev/null @@ -1,3975 +0,0 @@ -/* - * 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 /* for the socket+bind() fallback in do_mknod() */ -#endif -#ifdef HAVE_SYS_ATTR_H -#include -#endif - -#if defined HAVE_SYS_FALLOCATE && !defined HAVE_FALLOCATE -#include -#endif - -#ifdef __linux__ -#include /* 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 */ - -#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, O_RDONLY | O_DIRECTORY); -} -#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 the "/proc//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. */ -static const char *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//..." */ - 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/", 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 - /* 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 - 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]; - abspath[0] = '\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. */ - int pin_transit = !am_daemon && confine_root && 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 - * 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("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); - 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. */ - if (lst.st_uid != 0 && lst.st_uid != trusted_uid) { - 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'; - - /* Splice: new `remaining` = + . - * 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] == '/') { - if (dfd_owns) close(dfd); - dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); - if (dfd < 0) { - saved_errno = errno; - dfd_owns = 0; - goto out; - } - dfd_owns = 1; - abspath[0] = '\0'; /* followed an absolute target: restart from "/" */ - /* "self" resolves to "", still inside the pin; - * the magic link itself lands elsewhere and ends the - * exemption. Never turns back on. */ - pin_transit = pin_transit && fd_pin_tail(rebuilt) != NULL; - char *p = rebuilt; - while (*p == '/') p++; - strlcpy(remaining, p, sizeof remaining); - } else { - strlcpy(remaining, rebuilt, sizeof remaining); - } - continue; - } - - /* Non-symlink. */ - if (is_last) { - 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; - /* Resolved leaf dir (O_DIRECTORY): hand its path back so - * owner_walk_parent can filter-check the operation's leaf. */ - if (retfd >= 0 && out_abs && out_cap) - /* Root-resolved (".." popped abspath empty) tracked daemon walk: - * hand back "/" so owner_walk_parent still leaf-checks (path=/ bypass). */ - strlcpy(out_abs, (am_daemon && !abspath[0]) ? "/" : abspath, out_cap); - goto out; - } - - if (!S_ISDIR(lst.st_mode)) { - saved_errno = ENOTDIR; - goto out; - } - /* track the resolved path so a target outside the module is refused */ - 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; - } - int next = openat(dfd, comp, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); - if (next < 0) { - saved_errno = errno; - goto out; - } - if (dfd_owns) close(dfd); - dfd = next; - dfd_owns = 1; - - /* Advance `remaining` past this component (and the slash). */ - if (slash) { - char *p = slash; - while (*p == '/') p++; - memmove(remaining, p, strlen(p) + 1); - } else { - remaining[0] = '\0'; - } - } - - /* Path resolved entirely to a directory (no leaf component left). - * If the caller wanted O_DIRECTORY we already hold the dirfd we - * built up; otherwise it's an EISDIR. */ - if (flags & O_DIRECTORY) { - retfd = dfd; - dfd_owns = 0; /* caller now owns it */ - saved_errno = 0; - if (out_abs && out_cap) - /* Root-resolved (".." popped abspath empty) tracked daemon walk: - * hand back "/" so owner_walk_parent still leaf-checks (path=/ bypass). */ - strlcpy(out_abs, (am_daemon && !abspath[0]) ? "/" : abspath, out_cap); - } else { - saved_errno = EISDIR; - } - -out: - if (dfd_owns) close(dfd); - errno = saved_errno; - return retfd; -#else - /* Pre-AT_FDCWD / no O_NOFOLLOW systems: best-effort fallback. */ - (void)out_abs; (void)out_cap; - return open(path, flags, mode); -#endif -} - -int open_no_attacker_symlinks(const char *path, int flags, mode_t mode) -{ - return ona_open(path, flags, mode, NULL, 0); -} - -/* When set, the do_*_at() wrappers resolve their path as an OPERATOR-supplied - * directory path (an absolute or relative --backup-dir/--temp-dir/--*-dest) - * using the ownership walk -- follow a symlink owned by uid 0 or our euid, - * refuse any other-uid one, at every component -- instead of the stricter - * transfer-path resolver (which refuses all symlinks and is confined beneath the - * transfer root). An operator path may legitimately point outside the tree, so - * the trust signal is authority (ownership), not location. Set around the - * relevant ops by backup.c et al.; the opt-out (--insecure-links / "insecure - * links =") restores legacy following. Default 0 (transfer-path resolver). */ -int operator_path_resolve = 0; - -#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY -/* For an operator-supplied path: open its parent directory via the ownership - * walk (handles absolute and relative paths) and point *bname at the final - * component. Returns the dirfd (caller closes) or -1 with errno set. */ -int owner_walk_parent(const char *path, const char **bname) -{ - const char *slash = strrchr(path, '/'); - char dir[MAXPATHLEN], pabs[MAXPATHLEN]; - size_t dlen; - int dfd; - - *bname = slash ? slash + 1 : path; - pabs[0] = '\0'; - if (!slash) - dfd = ona_open(".", O_RDONLY | O_DIRECTORY, 0, pabs, sizeof pabs); - else { - dlen = slash == path ? 1 : (size_t)(slash - path); /* "/x" -> parent "/" */ - if (dlen >= sizeof dir) { - errno = ENAMETOOLONG; - return -1; - } - memcpy(dir, path, dlen); - dir[dlen] = '\0'; - dfd = ona_open(dir, O_RDONLY | O_DIRECTORY, 0, pabs, sizeof pabs); - } - if (dfd < 0) - return -1; - /* 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_outside_confinement.) */ - if (pabs[0]) { - char leafabs[MAXPATHLEN]; - if (snprintf(leafabs, sizeof leafabs, "%s/%s", pabs, *bname) >= (int)sizeof leafabs) { - close(dfd); - errno = ENAMETOOLONG; /* fail closed, never skip the check */ - return -1; - } - if (abspath_outside_confinement(leafabs)) { - close(dfd); - errno = ELOOP; - return -1; - } - } - return dfd; -} -#endif - -#ifndef S_BLKSIZE -# if defined hpux || defined __hpux__ || defined __hpux -# define S_BLKSIZE 1024 -# elif defined _AIX && defined _I386 -# define S_BLKSIZE 4096 -# else -# define S_BLKSIZE 512 -# endif -#endif - -#ifdef SUPPORT_CRTIMES -#ifdef HAVE_GETATTRLIST -#pragma pack(push, 4) -struct create_time { - uint32 length; - struct timespec crtime; -}; -#pragma pack(pop) -#elif defined __CYGWIN__ -#include -#endif -#endif - -#define RETURN_ERROR_IF(x,e) \ - do { \ - if (x) { \ - errno = (e); \ - return -1; \ - } \ - } while (0) - -#define RETURN_ERROR_IF_RO_OR_LO RETURN_ERROR_IF(read_only || list_only, EROFS) - -/* A NULL path reaching one of the path-forwarding wrappers below is always a - * caller bug; reject it rather than forwarding NULL to libc. Also quiets the - * static analyzer's interprocedural nonnull false positives. */ -#define RETURN_ERROR_IF_NULL(p) RETURN_ERROR_IF(!(p), EFAULT) - -int do_unlink(const char *path) -{ - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - return unlink(path); -} - -/* - Symlink-race-safe variant of do_unlink() for receiver-side use. See - the comment on do_chmod_at() for the threat model. unlink() resolves - parent components, so a parent-symlink swap can delete an outside - file under the daemon's authority. Defence: open the parent of path - under secure_relative_open() and use unlinkat() (flags=0) against - that dirfd. - - Falls through to do_unlink() for the same dry-run / non-daemon / - chrooted / no-parent / absolute-path cases as the other wrappers. -*/ -int do_unlink_at(const char *path) -{ -#ifdef AT_FDCWD - extern int am_daemon, am_chrooted; - 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; - RETURN_ERROR_IF_NULL(path); - -#if defined O_NOFOLLOW && defined O_DIRECTORY - if (operator_path_resolve) { - if (symlink_optout_allowed()) - return unlink(path); - dfd = owner_walk_parent(path, &bname); - if (dfd < 0) - return -1; - ret = unlinkat(dfd, bname, 0); - e = errno; - close(dfd); - errno = e; - return ret; - } -#endif - - if (!secure_relpath_active()) - return unlink(path); - - if (!path || !*path || *path == '/') - return unlink(path); - - slash = strrchr(path, '/'); - if (!slash) - return unlink(path); - - dlen = slash - path; - if (dlen >= sizeof dirpath) { - errno = ENAMETOOLONG; - return -1; - } - memcpy(dirpath, path, dlen); - dirpath[dlen] = '\0'; - bname = slash + 1; - - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); - if (dfd < 0) - return -1; - - ret = unlinkat(dfd, bname, 0); - e = errno; - close(dfd); - errno = e; - return ret; -#else - return do_unlink(path); -#endif -} - -#ifdef SUPPORT_LINKS -int do_symlink(const char *lnk, const char *path) -{ - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - RETURN_ERROR_IF_NULL(lnk); - RETURN_ERROR_IF_NULL(path); - -#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS - /* For --fake-super, we create a normal file with mode 0600 - * and write the lnk into it. */ - if (am_root < 0) { - int ok, len = strlen(lnk); - int fd = open(path, O_WRONLY|O_CREAT|O_TRUNC, S_IWUSR|S_IRUSR); - if (fd < 0) - return -1; - ok = write(fd, lnk, len) == len; - if (close(fd) < 0) - ok = 0; - return ok ? 0 : -1; - } -#endif - - return symlink(lnk, path); -} - -/* - Symlink-race-safe variant of do_symlink() for receiver-side use. See - the comment on do_chmod_at() for the threat model. For a real symlink - only the parent directory of `path` needs protection -- symlinkat() - does not resolve the final component (it creates it). Defence: open - the parent of `path` under secure_relative_open() and call symlinkat() - against that dirfd; a top-level (no-slash) path has no parent to - confine, so it uses AT_FDCWD directly. The link target string `lnk` is - stored verbatim and not resolved at creation time, so it doesn't need - scrutiny here. - - For --fake-super (am_root < 0) the "symlink" is written as a regular - file, so the final component IS resolved at creation: we create it - with openat(... O_NOFOLLOW) so a pre-planted symlink at the basename - cannot redirect the write outside the module. This protection applies - to top-level paths too -- the previous code fell through to the - bare-path do_symlink() there, whose plain open() followed such a - symlink. -*/ -int do_symlink_at(const char *lnk, const char *path) -{ -#ifdef AT_FDCWD - extern int am_daemon, am_chrooted; - char dirpath[MAXPATHLEN]; - const char *bname; - const char *slash; - int dfd = AT_FDCWD, ret, e; - BOOL owns = False; - size_t dlen; - - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - -#if defined O_NOFOLLOW && defined O_DIRECTORY - if (operator_path_resolve) { - /* Operator path (e.g. an absolute --backup-dir): confine the - * parent with the ownership walk, then fall through to the shared - * leaf-creation below so fake-super emulation is preserved. */ - if (symlink_optout_allowed()) - return do_symlink(lnk, path); - dfd = owner_walk_parent(path, &bname); - if (dfd < 0) - return -1; - owns = True; - } else -#endif - { - if (!secure_relpath_active()) - return do_symlink(lnk, path); - - if (!path || !*path || *path == '/') - return do_symlink(lnk, path); - - /* A path with a slash needs secure_relative_open to confine its - * parent; a top-level path is in CWD (AT_FDCWD), no parent to - * subvert. The leaf is protected below either way (symlinkat() - * won't follow it; the fake-super openat() uses O_NOFOLLOW). */ - slash = strrchr(path, '/'); - if (slash) { - dlen = slash - path; - if (dlen >= sizeof dirpath) { - errno = ENAMETOOLONG; - return -1; - } - memcpy(dirpath, path, dlen); - dirpath[dlen] = '\0'; - bname = slash + 1; - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); - if (dfd < 0) - return -1; - owns = True; - } else { - bname = path; - } - } - -#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS - /* For --fake-super, do_symlink writes the link target into a - * regular file rather than creating a real symlink. Do that here - * against the (secure or AT_FDCWD) dirfd, with O_NOFOLLOW so a pre- - * planted symlink at the basename can't redirect the file creation. */ - if (am_root < 0) { - int len = strlen(lnk); - int fd = openat(dfd, bname, - O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, - S_IWUSR | S_IRUSR); - if (fd < 0) { - e = errno; - if (owns) close(dfd); - errno = e; - return -1; - } - ret = (write(fd, lnk, len) == len) ? 0 : -1; - if (close(fd) < 0) - ret = -1; - e = errno; - if (owns) close(dfd); - errno = e; - return ret; - } -#endif - - ret = symlinkat(lnk, dfd, bname); - e = errno; - if (owns) close(dfd); - errno = e; - return ret; -#else - return do_symlink(lnk, path); -#endif -} - -/* NOFOLLOW_HIT_SYMLINK() lives in rsync.h (shared with util1.c's change_dir). */ - -#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS -ssize_t do_readlink(const char *path, char *buf, size_t bufsiz) -{ - /* For --fake-super, we read the link from the file. */ - if (am_root < 0) { - int fd = do_open_nofollow(path, O_RDONLY); - if (fd >= 0) { - int len = read(fd, buf, bufsiz); - close(fd); - return len; - } - if (!NOFOLLOW_HIT_SYMLINK(errno)) - return -1; - /* A real symlink needs to be turned into a fake one on the receiving - * side, so tell the generator that the link has no length. */ - if (!am_sender) - return 0; - /* Otherwise fall through and let the sender report the real length. */ - } - - return readlink(path, buf, bufsiz); -} -#endif - -ssize_t do_readlink_atfd(int dfd, const char *name, char *buf, size_t bufsiz) -{ -#ifdef AT_FDCWD -# if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS - if (am_root < 0) { - int fd = openat(dfd, name, O_RDONLY | O_NOFOLLOW); - if (fd >= 0) { - int len = read(fd, buf, bufsiz); - close(fd); - return len; - } - if (!NOFOLLOW_HIT_SYMLINK(errno)) - return -1; - if (!am_sender) - return 0; - } -# endif - return readlinkat(dfd, name, buf, bufsiz); -#else - (void)dfd; - return do_readlink(name, buf, bufsiz); -#endif -} -#endif - -#if defined HAVE_LINK || defined HAVE_LINKAT -int do_link(const char *old_path, const char *new_path) -{ - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - RETURN_ERROR_IF_NULL(old_path); - RETURN_ERROR_IF_NULL(new_path); -#ifdef HAVE_LINKAT - return linkat(AT_FDCWD, old_path, AT_FDCWD, new_path, 0); -#else - return link(old_path, new_path); -#endif -} - -/* - Symlink-race-safe variant of do_link() for receiver-side use. See - the comment on do_chmod_at() for the threat model. link() resolves - parent components of *both* old_path and new_path, so a parent- - symlink swap on either side can plant the new hard link outside - the module, or hard-link an outside file into the module (read - disclosure). - - Defence: open each parent under secure_relative_open() and use - linkat() between the two dirfds, reusing one when the parents - match. flags=0 matches the existing do_link() (don't follow a - symbolic-link old_path). Only available on systems with linkat(); - pre-AT_FDCWD systems fall through to do_link(). -*/ -int do_link_at(const char *old_path, const char *new_path) -{ -#if defined AT_FDCWD && defined HAVE_LINKAT - extern int am_daemon, am_chrooted; - char old_dirpath[MAXPATHLEN], new_dirpath[MAXPATHLEN]; - const char *old_bname, *new_bname; - const char *old_slash, *new_slash; - int old_dfd = AT_FDCWD, new_dfd = AT_FDCWD; - BOOL old_owns = False, new_owns = False; - int ret, e; - size_t old_dlen = 0, new_dlen = 0; - - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - - if (!secure_relpath_active()) - return do_link(old_path, new_path); - - if (!old_path || !*old_path || !new_path || !*new_path) - return do_link(old_path, new_path); - -#if defined O_NOFOLLOW && defined O_DIRECTORY - /* Operator-supplied path (a --backup-dir/--link-dest side): resolve each - * parent via the ownership walk (follow uid0/euid symlinks, refuse others). */ - if (operator_path_resolve) { - if (symlink_optout_allowed()) - return do_link(old_path, new_path); - old_dfd = owner_walk_parent(old_path, &old_bname); - if (old_dfd < 0) - return -1; - new_dfd = owner_walk_parent(new_path, &new_bname); - if (new_dfd < 0) { - e = errno; - close(old_dfd); - errno = e; - return -1; - } - ret = linkat(old_dfd, old_bname, new_dfd, new_bname, 0); - e = errno; - close(new_dfd); - close(old_dfd); - errno = e; - return ret; - } -#endif - - old_slash = strrchr(old_path, '/'); - new_slash = strrchr(new_path, '/'); - - /* Resolve each path's parent dir independently. A path without a - * slash lives in CWD (AT_FDCWD), no parent open required. A path - * with a slash needs secure_relative_open to confine its parent - * resolution -- otherwise a parent symlink (e.g. --link-dest=cd - * where cd -> /outside) lets the kernel-level linkat(AT_FDCWD, - * "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. 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 == '/') { -#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; } - memcpy(old_dirpath, old_path, old_dlen); - old_dirpath[old_dlen] = '\0'; - old_bname = old_slash + 1; - old_dfd = secure_relative_open(NULL, old_dirpath, O_RDONLY | O_DIRECTORY, 0); - if (old_dfd < 0) - return -1; - old_owns = True; - } else { - old_bname = old_path; - } - - if (*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) { - e = ENAMETOOLONG; - if (old_owns) close(old_dfd); - errno = e; - return -1; - } - memcpy(new_dirpath, new_path, new_dlen); - new_dirpath[new_dlen] = '\0'; - new_bname = new_slash + 1; - if (old_owns && old_dlen == new_dlen - && memcmp(old_dirpath, new_dirpath, old_dlen) == 0) { - new_dfd = old_dfd; - } else { - new_dfd = secure_relative_open(NULL, new_dirpath, O_RDONLY | O_DIRECTORY, 0); - if (new_dfd < 0) { - e = errno; - if (old_owns) close(old_dfd); - errno = e; - return -1; - } - new_owns = True; - } - } else { - new_bname = new_path; - } - - ret = linkat(old_dfd, old_bname, new_dfd, new_bname, 0); - e = errno; - if (new_owns) - close(new_dfd); - if (old_owns) - close(old_dfd); - errno = e; - return ret; -#else - return do_link(old_path, new_path); -#endif -} -#endif - -int do_lchown(const char *path, uid_t owner, gid_t group) -{ - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - RETURN_ERROR_IF_NULL(path); -#ifndef HAVE_LCHOWN -#define lchown chown -#endif - return lchown(path, owner, group); -} - -/* - Symlink-race-safe variant of do_lchown() for receiver-side use. See the - comment on do_chmod_at() for the threat model and design rationale. - - Resolves the parent directory under secure_relative_open() and invokes - fchownat(..., AT_SYMLINK_NOFOLLOW) against that dirfd, so that an - attacker who substitutes a symlink into one of the parent components - cannot redirect the chown outside the receiver's confinement. The - AT_SYMLINK_NOFOLLOW flag matches lchown()'s "do not follow a final- - component symlink" semantics. - - Falls through to do_lchown() in the dry-run / non-daemon / chrooted / - absolute-path / no-parent cases, identical to do_chmod_at(). -*/ -int do_lchown_at(const char *fname, uid_t owner, gid_t group) -{ -#if defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW - extern int am_daemon, am_chrooted; - 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 do_*_at() wrappers do. Without this the caller's - * operator_path_resolve has no effect here, and an absolute name would - * fall straight through to the unconfined full-path do_lchown(). */ - if (operator_path_resolve && fname && *fname) { - if (symlink_optout_allowed()) - return do_lchown(fname, owner, group); - dfd = owner_walk_parent(fname, &bname); - if (dfd < 0) - return -1; - ret = fchownat(dfd, bname, owner, group, AT_SYMLINK_NOFOLLOW); - e = errno; - close(dfd); - errno = e; - return ret; - } -#endif - - if (!secure_relpath_active()) - return do_lchown(fname, owner, group); - - if (!fname || !*fname || *fname == '/') - return do_lchown(fname, owner, group); - - slash = strrchr(fname, '/'); - if (!slash) - return do_lchown(fname, owner, group); - - dlen = slash - fname; - if (dlen >= sizeof dirpath) { - errno = ENAMETOOLONG; - return -1; - } - memcpy(dirpath, fname, dlen); - dirpath[dlen] = '\0'; - bname = slash + 1; - - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); - if (dfd < 0) - return -1; - - ret = fchownat(dfd, bname, owner, group, AT_SYMLINK_NOFOLLOW); - e = errno; - close(dfd); - errno = e; - return ret; -#else - return do_lchown(fname, owner, group); -#endif -} - -int do_mknod(const char *pathname, mode_t mode, dev_t dev) -{ - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - RETURN_ERROR_IF_NULL(pathname); - - /* For --fake-super, we create a normal file with mode 0600. */ - if (am_root < 0) { - int fd = open(pathname, O_WRONLY|O_CREAT|O_TRUNC, S_IWUSR|S_IRUSR); - if (fd < 0 || close(fd) < 0) - return -1; - return 0; - } - - /* Try mknod first: it handles every node type on Linux. Only if it - * can't make this type on this filesystem (sockets on the BSDs/macOS/ - * Solaris, or an old system lacking FIFO support) do we retry with the - * type-specific primitive. That capability is filesystem-dependent, so - * it is decided per call -- not cached, not probed at build time. */ -#ifdef HAVE_MKNOD - if (mknod(pathname, mode, dev) == 0) - return 0; -#endif -#ifdef HAVE_MKFIFO - if (S_ISFIFO(mode)) - return mkfifo(pathname, mode); -#endif -#ifdef HAVE_SYS_UN_H - if (S_ISSOCK(mode)) { - int sock; - struct sockaddr_un saddr; - unsigned int len = strlcpy(saddr.sun_path, pathname, sizeof saddr.sun_path); - if (len >= sizeof saddr.sun_path) { - errno = ENAMETOOLONG; - return -1; - } -#ifdef HAVE_SOCKADDR_UN_LEN - saddr.sun_len = len + 1; -#endif - saddr.sun_family = AF_UNIX; - - if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0 - || (unlink(pathname) < 0 && errno != ENOENT) - || (bind(sock, (struct sockaddr*)&saddr, sizeof saddr)) < 0) - return -1; - close(sock); -#ifdef HAVE_CHMOD - return do_chmod(pathname, mode); -#else - return 0; -#endif - } -#endif -#ifdef HAVE_MKNOD - return -1; /* mknod() failed for a regular/device node; errno is set */ -#else - errno = ENOSYS; - return -1; -#endif -} - -/* - Symlink-race-safe variant of do_mknod() for receiver-side use. See - the comment on do_chmod_at() for the threat model. Defence: open - the parent of pathname under secure_relative_open() and use - mknodat() against that dirfd. mknodat() covers both regular-file - (S_IFREG with dev=0) and FIFO (S_IFIFO) and device-node creation. - - A top-level (no-slash) pathname has no parent to confine, so it uses - AT_FDCWD; the final component is still protected (mknodat/mkfifoat do - not follow it, and the fake-super openat() uses O_NOFOLLOW). - - Fake-super (am_root < 0) is handled inline against the (secure or - AT_FDCWD) dirfd: it creates a regular empty file (the same file-as- - metadata-placeholder pattern do_mknod uses) via openat() with - O_NOFOLLOW so a pre-planted symlink at the basename can't redirect - the file creation -- top-level paths included (the previous code fell - through to the bare-path do_mknod() there, whose plain open() followed - such a symlink). On Linux, sockets are recreated with mknodat() like any - other special file; on systems where mknod() can't create sockets the - at-variant fails instead of re-resolving an unsafe parent. -*/ -int do_mknod_at(const char *pathname, mode_t mode, dev_t dev) -{ - /* HAVE_MKNODAT: older Darwin declares AT_FDCWD but not mknodat(), so - * the at-variant won't build there; fall back to do_mknod() (#896). */ -#if defined(AT_FDCWD) && defined(HAVE_MKNODAT) - extern int am_daemon, am_chrooted; - char dirpath[MAXPATHLEN]; - const char *bname; - const char *slash; - int dfd = AT_FDCWD, ret, e; - BOOL owns = False; - size_t dlen; - - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - -#if defined O_NOFOLLOW && defined O_DIRECTORY - if (operator_path_resolve) { - if (symlink_optout_allowed()) - return do_mknod(pathname, mode, dev); - dfd = owner_walk_parent(pathname, &bname); - if (dfd < 0) - return -1; - if (am_root < 0) { - /* Fake-super represents a special file with an inert regular - * placeholder. Keep that representation when the destination - * is an operator path, but create it relative to the verified - * parent so the confinement guarantee is unchanged. */ - int fd = openat(dfd, bname, - O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, - S_IWUSR | S_IRUSR); - ret = fd < 0 ? -1 : close(fd); - } else { - ret = mknodat(dfd, bname, mode, dev); - } - if (ret < 0 && am_root >= 0) { - /* mknodat() can't make a FIFO/socket on the BSDs/macOS/ - * Solaris (EINVAL); retry race-safely on the held dirfd, - * mirroring the secure-relpath path below. Without this a - * FIFO backup to an operator --backup-dir fails there. */ -#ifdef HAVE_MKFIFOAT - if (S_ISFIFO(mode)) - ret = mkfifoat(dfd, bname, mode); - else -#endif - if (S_ISSOCK(mode)) - errno = EOPNOTSUPP; /* no dirfd-relative socket bind */ - } - e = errno; - close(dfd); - errno = e; - return ret; - } -#endif - - if (!secure_relpath_active()) - return do_mknod(pathname, mode, dev); - - if (!pathname || !*pathname || *pathname == '/') - return do_mknod(pathname, mode, dev); - - /* A path with a slash needs secure_relative_open to confine its - * parent resolution; a top-level path lives in CWD (AT_FDCWD) with - * no parent to subvert. The final component is protected below - * regardless. */ - slash = strrchr(pathname, '/'); - if (slash) { - dlen = slash - pathname; - if (dlen >= sizeof dirpath) { - errno = ENAMETOOLONG; - return -1; - } - memcpy(dirpath, pathname, dlen); - dirpath[dlen] = '\0'; - bname = slash + 1; - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); - if (dfd < 0) - return -1; - owns = True; - } else { - bname = pathname; - } - - if (am_root < 0) { - /* For --fake-super, do_mknod creates a regular empty - * file as a placeholder for the special-file metadata - * (which is stored in xattrs elsewhere). Do that against - * the (secure or AT_FDCWD) dirfd, with O_NOFOLLOW so a - * pre-planted symlink at the basename can't redirect the - * file creation. */ - int fd = openat(dfd, bname, - O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, - S_IWUSR | S_IRUSR); - if (fd < 0) { - e = errno; - if (owns) close(dfd); - errno = e; - return -1; - } - ret = (close(fd) < 0) ? -1 : 0; - e = errno; - if (owns) close(dfd); - errno = e; - return ret; - } - - /* Try mknodat first (handles every type on Linux); on failure retry - * race-safely with the type-specific primitive. Decided per call -- - * the capability is filesystem-dependent (see do_mknod()). */ - ret = mknodat(dfd, bname, mode, dev); - if (ret < 0) { -#ifdef HAVE_MKFIFOAT - if (S_ISFIFO(mode)) - ret = mkfifoat(dfd, bname, mode); - else -#endif - if (S_ISSOCK(mode)) { - /* There is no dirfd-relative socket bind without - * /proc/self/fd: a top-level path can bind via - * do_mknod(), but a nested one fails safe rather than - * re-resolve a potentially unsafe parent. */ - if (dfd == AT_FDCWD) - ret = do_mknod(pathname, mode, dev); - else - errno = EOPNOTSUPP; - } - /* else: regular/device node -- keep mknodat()'s errno */ - } - e = errno; - if (owns) close(dfd); - errno = e; - return ret; -#else - return do_mknod(pathname, mode, dev); -#endif -} - -int do_rmdir(const char *pathname) -{ - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - return rmdir(pathname); -} - -/* - Symlink-race-safe variant of do_rmdir(). See do_unlink_at() above; - same shape but with AT_REMOVEDIR set to require the target be a - directory. -*/ -int do_rmdir_at(const char *pathname) -{ -#ifdef AT_FDCWD - extern int am_daemon, am_chrooted; - 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; - RETURN_ERROR_IF_NULL(pathname); - -#if defined O_NOFOLLOW && defined O_DIRECTORY - if (operator_path_resolve) { - if (symlink_optout_allowed()) - return do_rmdir(pathname); - dfd = owner_walk_parent(pathname, &bname); - if (dfd < 0) - return -1; - ret = unlinkat(dfd, bname, AT_REMOVEDIR); - e = errno; - close(dfd); - errno = e; - return ret; - } -#endif - - if (!secure_relpath_active()) - return rmdir(pathname); - - if (!pathname || !*pathname || *pathname == '/') - return rmdir(pathname); - - slash = strrchr(pathname, '/'); - if (!slash) - return rmdir(pathname); - - dlen = slash - pathname; - if (dlen >= sizeof dirpath) { - errno = ENAMETOOLONG; - return -1; - } - memcpy(dirpath, pathname, dlen); - dirpath[dlen] = '\0'; - bname = slash + 1; - - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); - if (dfd < 0) - return -1; - - ret = unlinkat(dfd, bname, AT_REMOVEDIR); - e = errno; - close(dfd); - errno = e; - return ret; -#else - return do_rmdir(pathname); -#endif -} - -int do_open(const char *pathname, int flags, mode_t mode) -{ - RETURN_ERROR_IF_NULL(pathname); - if (flags != O_RDONLY) { - RETURN_ERROR_IF(dry_run, 0); - RETURN_ERROR_IF_RO_OR_LO; - } - -#ifdef O_NOATIME - if (open_noatime) - flags |= O_NOATIME; -#endif - - return open(pathname, flags | O_BINARY, mode); -} - -/* - Symlink-race-safe variant of do_open() for receiver-side use. See - the comment on do_chmod_at() for the threat model. open() resolves - parent components, so a parent-symlink swap can redirect the open - to a file outside the module. This wrapper is defence-in-depth for - bare-path do_open() sites that callers know are otherwise - protected by secure parent-syscalls (e.g. generator.c's in-place - backup creation, where robust_unlink() rejects the symlinked - parent before this open is reached): if any of those upstream - protections is later removed or regresses, the open here still - refuses to escape the module. - - Defence: open the parent of pathname under secure_relative_open() - and call openat() against the resulting dirfd with O_NOFOLLOW - (so the basename itself isn't followed if it happens to be a - pre-planted symlink, which is what we want for O_CREAT|O_EXCL). -*/ -int do_open_at(const char *pathname, int flags, mode_t mode) -{ -#ifdef AT_FDCWD - extern int am_daemon, am_chrooted; - char dirpath[MAXPATHLEN]; - const char *bname; - const char *slash; - int dfd, ret, e; - size_t dlen; - - if (flags != O_RDONLY) { - RETURN_ERROR_IF(dry_run, 0); - RETURN_ERROR_IF_RO_OR_LO; - } - -#if defined O_NOFOLLOW && defined O_DIRECTORY - if (operator_path_resolve) { - if (symlink_optout_allowed()) - return do_open(pathname, flags, mode); - dfd = owner_walk_parent(pathname, &bname); - if (dfd < 0) - return -1; - ret = openat(dfd, bname, flags | O_NOFOLLOW, mode); - e = errno; - close(dfd); - errno = e; - return ret; - } -#endif - - if (!secure_relpath_active()) - return do_open(pathname, flags, mode); - - if (!pathname || !*pathname || *pathname == '/') - return do_open(pathname, flags, mode); - - slash = strrchr(pathname, '/'); - if (!slash) - return do_open(pathname, flags, mode); - - dlen = slash - pathname; - if (dlen >= sizeof dirpath) { - errno = ENAMETOOLONG; - return -1; - } - memcpy(dirpath, pathname, dlen); - dirpath[dlen] = '\0'; - bname = slash + 1; - - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); - if (dfd < 0) - return -1; - -#ifdef O_NOATIME - if (open_noatime) - flags |= O_NOATIME; -#endif - - ret = openat(dfd, bname, flags | O_NOFOLLOW | O_BINARY, mode); - e = errno; - close(dfd); - errno = e; - return ret; -#else - return do_open(pathname, flags, mode); -#endif -} - -#ifdef HAVE_CHMOD -int do_chmod(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 (do_lstat_atfd(dfd, name, &st) < 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, "do_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, "do_chmod: no symlink-safe chmod for \"%s\"; mode not set\n", name); - return 1; -#endif -} - -/* - Symlink-race-safe variant of do_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 secure_relative_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 do_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 do_chmod() which has portability code for - that case. - - Falls back to do_chmod() for absolute paths and for paths with no parent - component, where there is nothing to protect against. -*/ -int do_chmod_at(const char *fname, mode_t mode) -{ -#ifdef AT_FDCWD - extern int am_daemon, am_chrooted; - 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 do_*_at() wrappers do. Without this the caller's - * operator_path_resolve has no effect here, and an absolute name would - * fall straight through to the unconfined full-path do_chmod(). - * S_ISLNK(mode) still needs do_chmod()'s lchmod()/setattrlist() handling. */ - if (operator_path_resolve && fname && *fname && !S_ISLNK(mode)) { - if (symlink_optout_allowed()) - return do_chmod(fname, mode); - dfd = owner_walk_parent(fname, &bname); - 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 - * do_chmod() to avoid the dirfd-open overhead on every call. */ - if (!secure_relpath_active()) - return do_chmod(fname, mode); - - if (!fname || !*fname || *fname == '/' || S_ISLNK(mode)) - return do_chmod(fname, mode); - - slash = strrchr(fname, '/'); - if (!slash) - return do_chmod(fname, mode); - - dlen = slash - fname; - if (dlen >= sizeof dirpath) { - errno = ENAMETOOLONG; - return -1; - } - memcpy(dirpath, fname, dlen); - dirpath[dlen] = '\0'; - bname = slash + 1; - - dfd = secure_relative_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 - return do_chmod(fname, mode); -#endif -} -#endif - -int do_rename(const char *old_path, const char *new_path) -{ - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - return rename(old_path, new_path); -} - -/* - Symlink-race-safe variant of do_rename() for receiver-side use. See - the comment on do_chmod_at() for the threat model and design rationale. - - rename() is the central tmp -> final operation in rsync; if either the - source or the destination has an attacker-substituted symlink in one - of its parent components, the rename can publish or vanish files - outside the module. Defence: open the parent of *each* path under - secure_relative_open() and use renameat() against the resulting - dirfds. When old_path and new_path share the same parent (the common - case -- tmp file living next to its final name), we reuse the same - dirfd for both sides. - - Falls through to do_rename() in dry-run, non-daemon, chrooted and - absolute-path cases, identical to the other do_*_at() wrappers. -*/ -int do_rename_at(const char *old_path, const char *new_path) -{ -#ifdef AT_FDCWD - extern int am_daemon, am_chrooted; - char old_dirpath[MAXPATHLEN], new_dirpath[MAXPATHLEN]; - const char *old_bname, *new_bname; - const char *old_slash, *new_slash; - int old_dfd = AT_FDCWD, new_dfd = AT_FDCWD; - BOOL old_owns = False, new_owns = False; - int ret = -1, e; - size_t old_dlen = 0, new_dlen = 0; - - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - - if (!secure_relpath_active()) - return do_rename(old_path, new_path); - - if (!old_path || !*old_path || !new_path || !*new_path) - return do_rename(old_path, new_path); - -#if defined O_NOFOLLOW && defined O_DIRECTORY - /* Operator-supplied path (e.g. a --backup-dir destination or a --temp-dir - * source): resolve each side's parent via the ownership walk (follow - * uid0/euid symlinks, refuse others; absolute and relative alike). */ - if (operator_path_resolve) { - if (symlink_optout_allowed()) - return do_rename(old_path, new_path); - old_dfd = owner_walk_parent(old_path, &old_bname); - if (old_dfd < 0) - return -1; - new_dfd = owner_walk_parent(new_path, &new_bname); - if (new_dfd < 0) { - e = errno; - close(old_dfd); - errno = e; - return -1; - } - ret = renameat(old_dfd, old_bname, new_dfd, new_bname); - e = errno; - close(new_dfd); - close(old_dfd); - errno = e; - return ret; - } -#endif - - old_slash = strrchr(old_path, '/'); - new_slash = strrchr(new_path, '/'); - - /* 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 == '/') { -#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; - } - memcpy(old_dirpath, old_path, old_dlen); - old_dirpath[old_dlen] = '\0'; - old_bname = old_slash + 1; - old_dfd = secure_relative_open(NULL, old_dirpath, O_RDONLY | O_DIRECTORY, 0); - if (old_dfd < 0) - return -1; - old_owns = True; - } else { - old_bname = old_path; - } - - if (*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) { - e = ENAMETOOLONG; - if (old_owns) close(old_dfd); - errno = e; - return -1; - } - memcpy(new_dirpath, new_path, new_dlen); - new_dirpath[new_dlen] = '\0'; - new_bname = new_slash + 1; - if (old_owns && old_dlen == new_dlen - && memcmp(old_dirpath, new_dirpath, old_dlen) == 0) { - new_dfd = old_dfd; - } else { - new_dfd = secure_relative_open(NULL, new_dirpath, O_RDONLY | O_DIRECTORY, 0); - if (new_dfd < 0) { - e = errno; - if (old_owns) close(old_dfd); - errno = e; - return -1; - } - new_owns = True; - } - } else { - new_bname = new_path; - } - - ret = renameat(old_dfd, old_bname, new_dfd, new_bname); - e = errno; - if (new_owns) - close(new_dfd); - if (old_owns) - close(old_dfd); - errno = e; - return ret; -#else - return do_rename(old_path, new_path); -#endif -} - -#ifdef HAVE_FTRUNCATE -int do_ftruncate(int fd, OFF_T size) -{ - int ret; - - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - - do { - ret = ftruncate(fd, size); - } while (ret < 0 && errno == EINTR); - - return ret; -} -#endif - -void trim_trailing_slashes(char *name) -{ - int l; - /* Some BSD systems cannot make a directory if the name - * contains a trailing slash. - * */ - - /* Don't change empty string; and also we can't improve on - * "/" */ - - l = strlen(name); - while (l > 1) { - if (name[--l] != '/') - break; - name[l] = '\0'; - } -} - -int do_mkdir(char *path, mode_t mode) -{ - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - RETURN_ERROR_IF_NULL(path); - trim_trailing_slashes(path); - return mkdir(path, mode); -} - -/* - Symlink-race-safe variant of do_mkdir() for receiver-side use. See - the comment on do_chmod_at() for the threat model and design rationale. - - mkdir() resolves parent symlinks at every component, so a parent- - component swap can place an attacker-named directory outside the - module. Defence: open the parent of fname under secure_relative_open() - and call mkdirat() against that dirfd. - - Mutates path in place to trim trailing slashes (matches do_mkdir()). - Falls through to do_mkdir() in dry-run, non-daemon, chrooted, no- - parent and absolute-path cases. -*/ -int do_mkdir_at(char *path, mode_t mode) -{ -#ifdef AT_FDCWD - extern int am_daemon, am_chrooted; - 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; - RETURN_ERROR_IF_NULL(path); - trim_trailing_slashes(path); - -#if defined O_NOFOLLOW && defined O_DIRECTORY - if (operator_path_resolve) { - if (symlink_optout_allowed()) - return mkdir(path, mode); - dfd = owner_walk_parent(path, &bname); - if (dfd < 0) - return -1; - ret = mkdirat(dfd, bname, mode); - e = errno; - close(dfd); - errno = e; - return ret; - } -#endif - - if (!secure_relpath_active()) - return mkdir(path, mode); - - if (!path || !*path || *path == '/') - return mkdir(path, mode); - - slash = strrchr(path, '/'); - if (!slash) - return mkdir(path, mode); - - dlen = slash - path; - if (dlen >= sizeof dirpath) { - errno = ENAMETOOLONG; - return -1; - } - memcpy(dirpath, path, dlen); - dirpath[dlen] = '\0'; - bname = slash + 1; - - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); - if (dfd < 0) - return -1; - - ret = mkdirat(dfd, bname, mode); - e = errno; - close(dfd); - errno = e; - return ret; -#else - return do_mkdir(path, mode); -#endif -} - -/* like mkstemp but forces permissions */ -int do_mkstemp(char *template, mode_t perms) -{ - RETURN_ERROR_IF(dry_run, 0); - RETURN_ERROR_IF(read_only, EROFS); - perms |= S_IWUSR; - -#if defined HAVE_SECURE_MKSTEMP && defined HAVE_FCHMOD && (!defined HAVE_OPEN64 || defined HAVE_MKSTEMP64) - { - int fd = mkstemp(template); - if (fd == -1) - return -1; - if (fchmod(fd, perms) != 0 && preserve_perms) { - int errno_save = errno; - close(fd); - unlink(template); - errno = errno_save; - return -1; - } -#if defined HAVE_SETMODE && O_BINARY - setmode(fd, O_BINARY); -#endif - return fd; - } -#else - if (!mktemp(template)) - return -1; - return do_open(template, O_RDWR|O_EXCL|O_CREAT, perms); -#endif -} - -int do_stat(const char *path, STRUCT_STAT *st) -{ - RETURN_ERROR_IF_NULL(path); -#ifdef USE_STAT64_FUNCS - return stat64(path, st); -#else - return stat(path, st); -#endif -} - -int do_lstat(const char *path, STRUCT_STAT *st) -{ - RETURN_ERROR_IF_NULL(path); -#ifdef SUPPORT_LINKS -# ifdef USE_STAT64_FUNCS - return lstat64(path, st); -# else - return lstat(path, st); -# endif -#else - return do_stat(path, st); -#endif -} - -/* - Symlink-race-safe variants of do_stat() / do_lstat() for receiver- - side use. See the comment on do_chmod_at() for the threat model. - stat() and lstat() resolve parent components, so a parent-symlink - swap can make the receiver's stat see attributes of a victim file - outside the module -- which then drives later behaviour (e.g. - "this isn't a directory, delete it" -> attacker-controlled unlink - on something outside the module). - - Defence: open the parent under secure_relative_open() and use - fstatat() with AT_SYMLINK_NOFOLLOW (lstat) or 0 (stat) against - that dirfd. Same fall-through gating as the other wrappers. -*/ -static int do_xstat_at(const char *path, STRUCT_STAT *st, int at_flags, int (*fallback)(const char *, STRUCT_STAT *)) -{ -#ifdef AT_FDCWD - extern int am_daemon, am_chrooted; - char dirpath[MAXPATHLEN]; - const char *bname; - const char *slash; - int dfd, ret, e; - size_t dlen; - -#if defined O_NOFOLLOW && defined O_DIRECTORY - if (operator_path_resolve) { - if (symlink_optout_allowed()) - return fallback(path, st); - dfd = owner_walk_parent(path, &bname); - if (dfd < 0) - return -1; - ret = fstatat(dfd, bname, st, at_flags); - e = errno; - close(dfd); - errno = e; - return ret; - } -#endif - - if (!secure_relpath_active()) - return fallback(path, st); - - if (!path || !*path || *path == '/') - return fallback(path, st); - - slash = strrchr(path, '/'); - if (!slash) - return fallback(path, st); - - dlen = slash - path; - if (dlen >= sizeof dirpath) { - errno = ENAMETOOLONG; - return -1; - } - memcpy(dirpath, path, dlen); - dirpath[dlen] = '\0'; - bname = slash + 1; - - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); - if (dfd < 0) - return -1; - - ret = fstatat(dfd, bname, st, at_flags); - e = errno; - close(dfd); - errno = e; - return ret; -#else - (void)at_flags; - return fallback(path, st); -#endif -} - -int do_stat_at(const char *path, STRUCT_STAT *st) -{ - return do_xstat_at(path, st, 0, do_stat); -} - -int do_lstat_at(const char *path, STRUCT_STAT *st) -{ -#if defined SUPPORT_LINKS && defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW - return do_xstat_at(path, st, AT_SYMLINK_NOFOLLOW, do_lstat); -#elif defined SUPPORT_LINKS - return do_lstat(path, st); -#else - return do_xstat_at(path, st, 0, do_stat); -#endif -} - -int do_fstat(int fd, STRUCT_STAT *st) -{ -#ifdef USE_STAT64_FUNCS - return fstat64(fd, st); -#else - return fstat(fd, st); -#endif -} - -OFF_T do_lseek(int fd, OFF_T offset, int whence) -{ -#ifdef HAVE_LSEEK64 - return lseek64(fd, offset, whence); -#else - return lseek(fd, offset, whence); -#endif -} - -#ifdef HAVE_SETATTRLIST -int do_setattrlist_times(const char *path, STRUCT_STAT *stp) -{ - struct attrlist attrList; - struct timespec ts[2]; - - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - - /* setattrlist() takes a raw path and follows parent symlinks - * (FSOPT_NOFOLLOW only blocks the final component). When hardened - * resolution is active -- secure_relpath_active(): any non-chroot - * daemon/receiver module, plus a /./ inner-module chroot -- return - * ENOSYS so set_times()' tier walk falls through to do_utimensat_at(), - * which routes the update through a secure parent dirfd. The attribute - * set this would have used (ATTR_CMN_MODTIME / ATTR_CMN_ACCTIME) is the - * same set utimensat() handles, so no functionality is lost. */ - if (secure_relpath_active()) { - errno = ENOSYS; - return -1; - } - - /* Yes, this is in the opposite order of utime and similar. */ - ts[0].tv_sec = stp->st_mtime; - ts[0].tv_nsec = stp->ST_MTIME_NSEC; - - ts[1].tv_sec = stp->st_atime; - ts[1].tv_nsec = stp->ST_ATIME_NSEC; - - memset(&attrList, 0, sizeof attrList); - attrList.bitmapcount = ATTR_BIT_MAP_COUNT; - attrList.commonattr = ATTR_CMN_MODTIME | ATTR_CMN_ACCTIME; - return setattrlist(path, &attrList, ts, sizeof ts, FSOPT_NOFOLLOW); -} - -#ifdef SUPPORT_CRTIMES -int do_setattrlist_crtime(const char *path, time_t crtime) -{ - struct attrlist attrList; - struct timespec ts; - - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - - /* setattrlist() is path-based and follows parent symlinks - * (FSOPT_NOFOLLOW only blocks the final component), and macOS has no - * at-aware variant targeting ATTR_CMN_CRTIME. As with POSIX ACLs where - * the OS offers no race-safe primitive, we keep --crtimes functional - * (daemon and non-daemon) and accept the parent-component symlink race - * as a documented residual rather than dropping crtime. A daemon - * operator who does not want that residual can disable the feature with - * "refuse options = crtimes" in rsyncd.conf. */ - ts.tv_sec = crtime; - ts.tv_nsec = 0; - - memset(&attrList, 0, sizeof attrList); - attrList.bitmapcount = ATTR_BIT_MAP_COUNT; - attrList.commonattr = ATTR_CMN_CRTIME; - return setattrlist(path, &attrList, &ts, sizeof ts, FSOPT_NOFOLLOW); -} -#endif -#endif /* HAVE_SETATTRLIST */ - -#ifdef SUPPORT_CRTIMES -time_t get_create_time(const char *path, STRUCT_STAT *stp) -{ -#ifdef HAVE_GETATTRLIST - static struct create_time attrBuf; - struct attrlist attrList; - - (void)stp; - /* getattrlist() is path-based and follows parent symlinks; like - * do_setattrlist_crtime() there is no race-safe variant, so reading the - * source crtime stays functional and the parent-component symlink race - * is an accepted residual (refusable via "refuse options = crtimes"). */ - memset(&attrList, 0, sizeof attrList); - attrList.bitmapcount = ATTR_BIT_MAP_COUNT; - attrList.commonattr = ATTR_CMN_CRTIME; - if (getattrlist(path, &attrList, &attrBuf, sizeof attrBuf, FSOPT_NOFOLLOW) < 0) - return 0; - return attrBuf.crtime.tv_sec; -#elif defined __CYGWIN__ - (void)path; - return stp->st_birthtime; -#else -#error Unknown crtimes implementation -#endif -} - -#if defined __CYGWIN__ -int do_SetFileTime(const char *path, time_t crtime) -{ - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - - int cnt = MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0); - if (cnt == 0) - return -1; - WCHAR *pathw = new_array(WCHAR, cnt); - if (!pathw) - return -1; - MultiByteToWideChar(CP_UTF8, 0, path, -1, pathw, cnt); - HANDLE handle = CreateFileW(pathw, FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); - free(pathw); - if (handle == INVALID_HANDLE_VALUE) - return -1; - int64 temp_time = (crtime * 10000000LL) + 116444736000000000LL; - FILETIME birth_time; - birth_time.dwLowDateTime = (DWORD)temp_time; - birth_time.dwHighDateTime = (DWORD)(temp_time >> 32); - int ok = SetFileTime(handle, &birth_time, NULL, NULL); - CloseHandle(handle); - return ok ? 0 : -1; -} -#endif -#endif /* SUPPORT_CRTIMES */ - -#ifdef HAVE_UTIMENSAT -int do_utimensat(const char *path, STRUCT_STAT *stp) -{ - struct timespec t[2]; - - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - RETURN_ERROR_IF_NULL(path); - - 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 utimensat(AT_FDCWD, path, t, AT_SYMLINK_NOFOLLOW); -} - -/* - Symlink-race-safe variant of do_utimensat() for receiver-side use. - See the comment on do_chmod_at() for the threat model. utimes() - resolves parent components and follows a final-component symlink; - lutimes() doesn't follow the final component but still resolves - parents. Either way, a parent-symlink swap can redirect the - timestamp update outside the module. Defence: open the parent of - path under secure_relative_open() and call utimensat() with - AT_SYMLINK_NOFOLLOW against that dirfd. - - Falls through to do_utimensat() in the same dry-run / non-daemon / - chrooted / no-parent / absolute-path cases as the other wrappers. - Returns -1 with errno=ENOSYS on systems without utimensat() - (caller is expected to fall back to the legacy tier walk). -*/ -int do_utimensat_at(const char *path, STRUCT_STAT *stp) -{ -#ifdef AT_FDCWD - extern int am_daemon, am_chrooted; - struct timespec t[2]; - 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 (!secure_relpath_active()) - return do_utimensat(path, stp); - - if (!path || !*path || *path == '/') - return do_utimensat(path, stp); - - slash = strrchr(path, '/'); - if (!slash) - return do_utimensat(path, stp); - - dlen = slash - path; - if (dlen >= sizeof dirpath) { - errno = ENAMETOOLONG; - return -1; - } - memcpy(dirpath, path, dlen); - dirpath[dlen] = '\0'; - bname = slash + 1; - - 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 - - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); - if (dfd < 0) - return -1; - - ret = utimensat(dfd, bname, t, AT_SYMLINK_NOFOLLOW); - e = errno; - close(dfd); - errno = e; - return ret; -#else - return do_utimensat(path, stp); -#endif -} -#endif - -#ifdef HAVE_LUTIMES -int do_lutimes(const char *path, STRUCT_STAT *stp) -{ - struct timeval 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_usec = stp->ST_ATIME_NSEC / 1000; -#else - t[0].tv_usec = 0; -#endif - t[1].tv_sec = stp->st_mtime; -#ifdef ST_MTIME_NSEC - t[1].tv_usec = stp->ST_MTIME_NSEC / 1000; -#else - t[1].tv_usec = 0; -#endif - return lutimes(path, t); -} -#endif - -#ifdef HAVE_UTIMES -int do_utimes(const char *path, STRUCT_STAT *stp) -{ - struct timeval 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_usec = stp->ST_ATIME_NSEC / 1000; -#else - t[0].tv_usec = 0; -#endif - t[1].tv_sec = stp->st_mtime; -#ifdef ST_MTIME_NSEC - t[1].tv_usec = stp->ST_MTIME_NSEC / 1000; -#else - t[1].tv_usec = 0; -#endif - return utimes(path, t); -} - -#elif defined HAVE_UTIME -int do_utime(const char *path, STRUCT_STAT *stp) -{ -#ifdef HAVE_STRUCT_UTIMBUF - struct utimbuf tbuf; -#else - time_t t[2]; -#endif - - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - -# ifdef HAVE_STRUCT_UTIMBUF - tbuf.actime = stp->st_atime; - tbuf.modtime = stp->st_mtime; - return utime(path, &tbuf); -# else - t[0] = stp->st_atime; - t[1] = stp->st_mtime; - return utime(path, t); -# endif -} - -#else -#error Need utimes or utime function. -#endif - -#ifdef SUPPORT_PREALLOCATION -#ifdef FALLOC_FL_KEEP_SIZE -#define DO_FALLOC_OPTIONS FALLOC_FL_KEEP_SIZE -#else -#define DO_FALLOC_OPTIONS 0 -#endif - -OFF_T do_fallocate(int fd, OFF_T offset, OFF_T length) -{ - /* FALLOC_FL_KEEP_SIZE lets --preallocate/--inplace keep the file size at 0 - * until data is written, but a later hole-punch (for --sparse) can only - * deallocate blocks that lie within the file's size -- with KEEP_SIZE the - * reserved blocks sit beyond EOF and the punch silently does nothing, - * leaving the file fully allocated. So when holes will also be punched, - * preallocate at full size instead (write_sparse then punches the nulls). */ - int opts = (inplace || preallocate_files) && sparse_files <= 0 ? DO_FALLOC_OPTIONS : 0; - int ret; - RETURN_ERROR_IF(dry_run, 0); - RETURN_ERROR_IF_RO_OR_LO; - if (length & 1) /* make the length not match the desired length */ - length++; - else - length--; -#if defined HAVE_FALLOCATE - ret = fallocate(fd, opts, offset, length); -#elif defined HAVE_SYS_FALLOCATE - ret = syscall(SYS_fallocate, fd, opts, (loff_t)offset, (loff_t)length); -#elif defined HAVE_EFFICIENT_POSIX_FALLOCATE - ret = posix_fallocate(fd, offset, length); -#else -#error Coding error in SUPPORT_PREALLOCATION logic. -#endif - if (ret < 0) - return ret; - if (opts == 0) { - STRUCT_STAT st; - if (do_fstat(fd, &st) < 0) - return length; - return st.st_blocks * S_BLKSIZE; - } - /* With FALLOC_FL_KEEP_SIZE the blocks for [0, length) are reserved even - * though the file size stays put. Return that reserved length (not 0) so - * the caller's preallocated_len is meaningful: write_sparse() needs it to - * choose do_punch_hole() over a plain lseek() when turning a null run into - * a hole, and the receiver uses it to trim any over-preallocation. (A - * stray 0 here, from 2019's switch to KEEP_SIZE, is why --preallocate - * --sparse stopped producing sparse files.) */ - return length; -} -#endif - -/* Punch a hole at pos for len bytes. The current file position must be at pos and will be - * changed to be at pos + len. */ -int do_punch_hole(int fd, OFF_T pos, OFF_T len) -{ -#ifdef HAVE_FALLOCATE -# ifdef HAVE_FALLOC_FL_PUNCH_HOLE - if (fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, pos, len) == 0) { - if (do_lseek(fd, len, SEEK_CUR) != pos + len) - return -1; - return 0; - } -# endif -# ifdef HAVE_FALLOC_FL_ZERO_RANGE - if (fallocate(fd, FALLOC_FL_ZERO_RANGE, pos, len) == 0) { - if (do_lseek(fd, len, SEEK_CUR) != pos + len) - return -1; - return 0; - } -# endif -#else - (void)pos; -#endif - { - char zeros[4096]; - memset(zeros, 0, sizeof zeros); - while (len > 0) { - int chunk = len > (int)sizeof zeros ? (int)sizeof zeros : len; - int wrote = write(fd, zeros, chunk); - if (wrote <= 0) { - if (wrote < 0 && errno == EINTR) - continue; - return -1; - } - len -= wrote; - } - } - return 0; -} - -int do_open_nofollow(const char *pathname, int flags) -{ -#ifndef O_NOFOLLOW - STRUCT_STAT f_st, l_st; -#endif - int fd; - - if (flags != O_RDONLY) { - RETURN_ERROR_IF(dry_run, 0); - RETURN_ERROR_IF_RO_OR_LO; -#ifndef O_NOFOLLOW - /* This function doesn't support write attempts w/o O_NOFOLLOW. */ - errno = EINVAL; - return -1; -#endif - } - -#ifdef O_NOATIME - if (open_noatime) - flags |= O_NOATIME; -#endif - -#ifdef O_NOFOLLOW - fd = open(pathname, flags|O_NOFOLLOW); -#else - if (do_lstat(pathname, &l_st) < 0) - return -1; - if (S_ISLNK(l_st.st_mode)) { - errno = ELOOP; - return -1; - } - if ((fd = open(pathname, flags)) < 0) - return fd; - if (do_fstat(fd, &f_st) < 0) { - close_and_return_error: - { - int save_errno = errno; - close(fd); - errno = save_errno; - } - return -1; - } - if (l_st.st_dev != f_st.st_dev || l_st.st_ino != f_st.st_ino) { - errno = EINVAL; - goto close_and_return_error; - } -#endif - - return fd; -} - -/* - 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). -*/ - -/* Returns 1 if path has any "/"-separated component that is exactly - * "..", 0 otherwise. Used by secure_relative_open's front-door - * validation to reject ".." inputs (bare "..", "foo/..", "subdir/..") - * for non-re-anchored paths; the walk itself resolves an in-tree ".." - * safely (ds_descend pops to the held parent) for a re-anchored path. */ -static int path_has_dotdot_component(const char *path) -{ - const char *p = path; - - while (*p) { - const char *q; - if (*p == '/') { p++; continue; } - q = p; - while (*q && *q != '/') - q++; - if (q - p == 2 && p[0] == '.' && p[1] == '.') - return 1; - p = q; - } - return 0; -} - -/* The logical current directory (maintained by change_dir() in util1.c). - * Defined here -- rather than in util1.c -- so the test helpers that link - * syscall.o but not util1.o (tls, trimslash) get the definition without a - * weak-symbol fallback, which is not portable to PE/COFF targets (Cygwin). */ -char curr_dir[MAXPATHLEN]; -unsigned int curr_dir_len; - -#if defined(O_NOFOLLOW) && defined(O_DIRECTORY) && defined(AT_FDCWD) -/* In-tree symlink following for secure_relative_open()'s directory walk (below). - * An early version refused every symlink with O_NOFOLLOW on each component, which - * broke legitimate within-tree directory symlinks (--keep-dirlinks #715, and -aR - * through a symlinked parent); the walk now follows them safely. - * - * A directory walk keeps a stack of the open dirfds from the anchor (index 0, - * borrowed -- not closed here) down to the current directory. Descending into - * a real subdirectory pushes its fd; a ".." in a followed symlink target pops - * back to the already-pinned parent fd rather than re-resolving ".." with - * openat(), so an ancestor renamed mid-walk cannot redirect the climb, and the - * climb can never rise above the anchor (a pop at the anchor returns ELOOP). - * This matches RESOLVE_BENEATH, which allows in-tree ".." that stays beneath the - * root. Absolute symlink targets are refused; symlink hops are bounded. */ -#ifndef SECURE_OPEN_MAXSYMLINKS -#define SECURE_OPEN_MAXSYMLINKS 40 -#endif - -/* Max directory levels held open at once during a single resolve. The walk - * holds one fd per component, so depth is bounded by RLIMIT_NOFILE anyway; a - * fixed array (no malloc/realloc) keeps the stack simple and the static - * analyzer happy. Mirrors DPC_MAXDEPTH's fixed-cap approach. */ -#define DS_MAXDEPTH 1024 - -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_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]; -}; - -/* Append "/comp" to ds->abspath (no-op if it's unseeded/empty so non-daemon - * callers pay nothing). Returns -1 (ENAMETOOLONG) on overflow. */ -static int ds_path_push(struct dirstack *ds, const char *comp) -{ - size_t al = strlen(ds->abspath); - if (al == 0) - return 0; /* unseeded: tracking disabled for this walk */ - size_t cl = strlen(comp); - if (al + 1 + cl >= sizeof ds->abspath) { - errno = ENAMETOOLONG; - return -1; - } - ds->abspath[al] = '/'; - memcpy(ds->abspath + al + 1, comp, cl + 1); - return 0; -} - -/* Drop the last component of ds->abspath (mirrors a ".." pop). */ -static void ds_path_pop(struct dirstack *ds) -{ - char *slash; - if (!ds->abspath[0]) - return; - slash = strrchr(ds->abspath, '/'); - if (slash && slash != ds->abspath) - *slash = '\0'; -} - -/* Initialise with `anchor` (which may be AT_FDCWD) as the un-owned base. - * Returns int for caller symmetry, but cannot fail (the fd array is inline). */ -static int ds_init(struct dirstack *ds, int anchor) -{ - ds->abspath[0] = '\0'; - ds->fds[0] = anchor; - ds->top = 0; - return 0; -} - -/* Close every pushed fd (but not the borrowed anchor at index 0). */ -static void ds_free(struct dirstack *ds) -{ - while (ds->top > 0) - close(ds->fds[ds->top--]); -} - -static int ds_cur(struct dirstack *ds) -{ - return ds->fds[ds->top]; -} - -static int ds_push(struct dirstack *ds, int fd) -{ - if (ds->top + 1 >= DS_MAXDEPTH) { /* deeper than we'll hold open */ - close(fd); - errno = ENOMEM; - return -1; - } - ds->fds[++ds->top] = fd; - return 0; -} - -/* Detach the current dir as an owned fd the caller must close. At the anchor - * (top 0) the anchor is borrowed, so return a fresh dup of it instead. */ -static int ds_take(struct dirstack *ds) -{ - if (ds->top > 0) - return ds->fds[ds->top--]; - return openat(ds->fds[0], ".", O_RDONLY | O_DIRECTORY); -} - -static int ds_walk_path(struct dirstack *ds, char *path, int *hops); - -/* Descend one path component on the stack: "." stays, ".." pops to the pinned - * parent (ELOOP at the anchor), a real subdirectory is pushed, and an in-tree - * directory symlink is followed by walking its (relative, possibly - * ..-containing) target on the same stack. Returns 0, or -1 with errno set: - * ELOOP for a refused/escaping symlink or a hop overrun, otherwise the - * underlying openat()/readlinkat() errno (ENOENT, a real ENOTDIR, EACCES). */ -static int ds_descend(struct dirstack *ds, const char *part, int *hops) -{ - if (part[0] == '.' && part[1] == '\0') - return 0; /* "." -- no movement */ - if (part[0] == '.' && part[1] == '.' && part[2] == '\0') { - if (ds->top == 0) { /* would rise above the anchor */ - errno = ELOOP; - return -1; - } - close(ds->fds[ds->top--]); /* pop to the held parent fd */ - ds_path_pop(ds); - return 0; - } - - int fd = openat(ds_cur(ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); - if (fd != -1) { /* a real subdirectory */ - if (ds_push(ds, fd) < 0) - return -1; - if (ds_path_push(ds, part) < 0) - return -1; - /* exclude-aware: refuse descending into a module-hidden dir (catches a - * symlink that redirected the walk into an excluded subtree). */ - if (abspath_outside_confinement(ds->abspath)) { - errno = ELOOP; - return -1; - } - return 0; - } - /* O_NOFOLLOW refused a symlink (NOFOLLOW_HIT_SYMLINK: ELOOP on Linux, EMLINK - * on FreeBSD, EFTYPE on NetBSD/OpenBSD), or O_DIRECTORY hit a non-directory - * (ENOTDIR). Either may be a symlink, so fall through to the readlink probe; - * anything else is a hard error. */ - if (errno != ENOTDIR && !NOFOLLOW_HIT_SYMLINK(errno)) { - if (errno == EMFILE || errno == ENFILE) { - /* The resolver holds one dirfd per path component, so a deep path - * can exhaust descriptors where plain open() would not. Hint at - * the fix once -- otherwise "Too many open files" is opaque. */ - static int warned = 0; - if (!warned) { - int e = errno; - warned = 1; - rprintf(FWARNING, "out of file descriptors resolving a deep path;" - " raise the open-file limit (e.g. `ulimit -n`)\n"); - errno = e; - } - } - return -1; - } - int open_errno = errno; - - char buf[MAXPATHLEN]; - ssize_t n = readlinkat(ds_cur(ds), part, buf, sizeof buf - 1); - if (n < 0) { - if (errno == EINVAL) /* not a symlink: a real non-dir */ - errno = open_errno; - return -1; - } - if (n == 0 || (size_t)n >= sizeof buf - 1) { - errno = ELOOP; /* empty or truncated target */ - return -1; - } - buf[n] = '\0'; - if (buf[0] == '/') { /* absolute target: refuse */ - errno = ELOOP; - return -1; - } - if (--(*hops) < 0) { - errno = ELOOP; - return -1; - } - return ds_walk_path(ds, buf, hops); -} - -/* Walk every component of a relative path on the stack (used for the basedir, - * and for a followed symlink's target -- which may contain ".."). */ -static int ds_walk_path(struct dirstack *ds, char *path, int *hops) -{ - char *save = NULL; - for (char *c = strtok_r(path, "/", &save); c; c = strtok_r(NULL, "/", &save)) { - if (ds_descend(ds, c, hops) < 0) - return -1; - } - return 0; -} - -/* 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 secure_relative_open() (which first resolves a - * basedir to the anchor) and secure_relative_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)) { - 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 secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode) -{ - extern int am_daemon, am_chrooted; - extern char *module_dir; - extern unsigned int module_dirlen; - 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 && 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 curr_dir[], a guaranteed lexical prefix of - * module_dir, unlike getcwd()) and resolving beneath module_dir; RESOLVE_ - * BENEATH then allows in-module climbs and still rejects escapes. Only for - * paths that contain "..". module_dirlen is 0 for a `path = /` module - * (clientserver.c), so the non-chroot arm gates on module_dir, not its - * length, to cover that case too -- the prefix check below treats - * module_dirlen 0 as "module root is /". */ - if (am_daemon && (!am_chrooted || module_dirlen) - && module_dir && module_dir[0] == '/' - && (basedir == NULL || basedir[0] != '/') - && (path_has_dotdot_component(relpath) - || (basedir && path_has_dotdot_component(basedir)))) { - const char *p; - int n; - if (curr_dir_len >= module_dirlen - && strncmp(curr_dir, module_dir, module_dirlen) == 0 - && (curr_dir[module_dirlen] == '\0' || curr_dir[module_dirlen] == '/')) { - for (p = curr_dir + 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 = 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 ? 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 - * secure_relative_open(). */ -static int secure_relative_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 secure_relative_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 secure_relative_open_at(int anchor_fd, const char *relpath, int flags, mode_t mode) -{ - return secure_relative_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 secure_relative_open_at_beneath(int anchor_fd, const char *relpath, - int flags, mode_t mode) -{ - return secure_relative_open_at_internal(anchor_fd, relpath, flags, mode, 1); -} - -#if defined O_NOFOLLOW && defined O_DIRECTORY && defined AT_FDCWD -/* Fill buf with len random bytes. Prefers /dev/urandom for cryptographic - * quality; falls back to rand() if /dev/urandom cannot be opened or read - * (e.g. inside a chroot or container without /dev populated). */ -static void rand_bytes(unsigned char *buf, size_t len) -{ -#ifndef O_CLOEXEC -#define O_CLOEXEC 0 -#endif - int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC); - if (fd >= 0) { - ssize_t n = read(fd, buf, len); - close(fd); - if (n == (ssize_t)len) { - return; - } - } - for (size_t i = 0; i < len; i++) { - buf[i] = (unsigned char)rand(); - } -} -#endif - -/* Create a unique temp file directly in directory `dfd` for the held-dirfd - * traversal: `filename` is the basename ending in "XXXXXX", rewritten in place - * to the chosen name. O_EXCL|O_NOFOLLOW so a planted name can't be followed or - * clobbered. Does NOT close dfd (the caller owns it). Returns the fd, or -1. - * This is the create loop shared with secure_mkstemp(). */ -int do_mkstemp_atfd(int dfd, char *filename, mode_t perms) -{ -#ifdef AT_FDCWD - static const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - size_t filename_len = strlen(filename); - char *suffix; - int fd = -1; - - if (filename_len < 6) { - errno = EINVAL; - return -1; - } - suffix = filename + filename_len - 6; /* Points to XXXXXX */ - if (strcmp(suffix, "XXXXXX") != 0) { - errno = EINVAL; - return -1; - } - - perms |= S_IWUSR; - for (int tries = 0; tries < 100; tries++) { - unsigned char rbytes[6]; - rand_bytes(rbytes, sizeof(rbytes)); - for (int i = 0; i < 6; i++) - suffix[i] = letters[rbytes[i] % (sizeof(letters) - 1)]; - - fd = openat(dfd, filename, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, perms); - if (fd >= 0) - break; - if (errno != EEXIST) - return -1; - } - - if (fd >= 0) { - if (fchmod(fd, perms) != 0 && preserve_perms) { - int errno_save = errno; - close(fd); - unlinkat(dfd, filename, 0); - errno = errno_save; - return -1; - } -#if defined HAVE_SETMODE && O_BINARY - setmode(fd, O_BINARY); -#endif - } - return fd; -#else - (void)dfd; (void)filename; (void)perms; - errno = ENOSYS; - return -1; -#endif -} - -/* - Secure version of mkstemp that prevents symlink attacks on parent directories. - Like secure_relative_open(), this walks the path checking each component - with O_NOFOLLOW to prevent TOCTOU race conditions. - - The template may be relative or absolute, but must not contain ../ components. - Returns fd on success, -1 on error. -*/ -int secure_mkstemp(char *template, mode_t perms, int operator_path) -{ -#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD) - /* Fall back to regular mkstemp on old systems */ - return do_mkstemp(template, perms); -#else - char *lastslash; - int dirfd = AT_FDCWD; - int fd = -1; - - if (!template) { - errno = EINVAL; - return -1; - } - if (strncmp(template, "../", 3) == 0 || strstr(template, "/../")) { - errno = EINVAL; - return -1; - } - - /* An operator-supplied --temp-dir may point outside the tree; --insecure-links - * (or a daemon module's "insecure links =") restores legacy following. */ - if (operator_path && symlink_optout_allowed()) - return do_mkstemp(template, perms); - - /* Open the temp file's directory. For an operator --temp-dir use the - * ownership walk (follow a uid0/euid-owned symlink, refuse a foreign one, - * absolute and relative alike); otherwise -- the deep-entry-dir fallback when - * the held-dirfd cache declines -- use the strict transfer-path resolver - * (refuse all symlinks, confine beneath the transfer root). The temp file - * itself is created below with O_EXCL|O_NOFOLLOW, so a planted name can't be - * followed either way. */ - lastslash = strrchr(template, '/'); - if (lastslash) { - char dirbuf[MAXPATHLEN]; - size_t dlen = lastslash - template; - const char *dir; - if (dlen == 0) - dir = "/"; - else { - if (dlen >= sizeof dirbuf) { - errno = ENAMETOOLONG; - return -1; - } - memcpy(dirbuf, template, dlen); - dirbuf[dlen] = '\0'; - dir = dirbuf; - } - dirfd = operator_path - ? open_no_attacker_symlinks(dir, O_RDONLY | O_DIRECTORY, 0) - : secure_relative_open(dir, ".", O_RDONLY | O_DIRECTORY, 0); - if (dirfd < 0) - return -1; - } - - /* Create the temp file in the securely-opened directory. */ - { - char *filename = lastslash ? lastslash + 1 : template; - int e; - fd = do_mkstemp_atfd(dirfd, filename, perms); - e = errno; - if (dirfd != AT_FDCWD) close(dirfd); - errno = e; - } - return fd; -#endif -} - -/* - varient of do_open/do_open_nofollow which does do_open() if the - copy_links or copy_unsafe_links options are set and does - do_open_nofollow() otherwise - - This is used to prevent a race condition where an attacker could be - switching a file between being a symlink and being a normal file - - The open is always done with O_RDONLY flags - */ -int do_open_checklinks(const char *pathname) -{ - if (copy_links || copy_unsafe_links) { - return do_open(pathname, O_RDONLY, 0); - } - return do_open_nofollow(pathname, O_RDONLY); -} - -/* Held-directory-fd traversal. - * - * Rather than re-resolve a full path on every syscall (do_*_at() re-opens the - * parent via secure_relative_open() each call), the generator and receiver - * open each directory ONCE via open_dir_secure() and issue single-component - * *at() ops against that held dirfd with the do_*_atfd() wrappers below. The - * parent is a pinned fd, not re-resolved, so the per-entry symlink-race window - * is closed and the re-resolution overhead is gone. - * - * open_dir_secure() owns both the authority gate and the resolver choice: it - * returns a held dirfd only when hardened resolution is in effect, else -1 - * with errno==0 so the caller falls back to the do_*_at() wrappers - * (behaviour-neutral). The do_*_atfd() wrappers are thin shims with the same - * leaf semantics as do_*_at() (dry-run/read-only guards, AT_SYMLINK_NOFOLLOW, - * fake-super placeholder files); they never re-check the gate or re-resolve a - * parent. */ - -int open_dir_secure(const char *dirname) -{ -#ifdef AT_FDCWD - extern int am_daemon, am_chrooted; - int dfd; - - /* Authority gate, identical to the do_*_at() wrappers. When hardened - * resolution isn't in effect, return -1 with errno cleared so the caller - * uses the full-path wrappers. */ - if (!secure_relpath_active()) { - errno = 0; - return -1; - } - - if (!dirname || !*dirname) { - /* The transfer root itself (file->dirname == NULL): the cwd. */ - dfd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY); - } else if (dirname[0] == '/') { - /* An absolute dirname is not expected for an in-transfer entry; - * leave it to the legacy path. */ - errno = 0; - return -1; - } else { - dfd = secure_relative_open(NULL, dirname, O_RDONLY | O_DIRECTORY, 0); - } - - if (dfd >= 0) { - /* O_CLOEXEC on every tier (the per-component walk fallback - * doesn't thread our flags onto the returned dirfd). */ - int fl = fcntl(dfd, F_GETFD); - if (fl >= 0) - fcntl(dfd, F_SETFD, fl | FD_CLOEXEC); - } - return dfd; -#else - (void)dirname; - errno = 0; - return -1; -#endif -} - -/* Persistent ancestor-dirfd stack for held-directory traversal. - * - * The transfer's file list is path-sorted, so iterating it walks the tree in - * DFS order and consecutive directory resolutions share a long leading prefix. - * Rather than re-resolve a full path from the anchor each time (re-opening - * every ancestor dir per file), we keep the whole current ancestor chain open - * as pinned, race-safe dirfds and, on the next resolution, reuse the longest - * common component prefix -- popping only the divergent tail and descending the - * new tail. Each directory is then opened once while we are inside its subtree. - * - * The chain is relative to the process cwd (for a NULL anchor), so change_dir() - * drops it on any real chdir; it otherwise persists across flist chunks (the - * pinned fds stay valid, and a raced/replaced ancestor resolves to the original - * inode the fd holds -- the held-dirfd race-safety property, not a hazard). - * Each component is resolved with ds_descend(), which follows in-tree directory - * symlinks exactly as secure_relative_open() does; only the resolved dir fd is - * kept (intermediate symlink-target fds are closed -- sound, since an open - * dirfd needs no live parent). */ -#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY -#define DPC_MAXDEPTH 64 -static const char *dpc_anchor = (const char *)-2; -static int dpc_base = -1; /* opened anchor dir (owned), or -1 */ -static int dpc_fd[DPC_MAXDEPTH]; /* dpc_fd[i] = dir after components 0..i */ -static char dpc_name[DPC_MAXDEPTH][256]; /* textual component names */ -static int dpc_depth = 0; - -void reset_dir_fd_cache(void) -{ - while (dpc_depth > 0) - close(dpc_fd[--dpc_depth]); - if (dpc_base >= 0) - close(dpc_base); - dpc_base = -1; - dpc_anchor = (const char *)-2; -} - -/* Resolve directory `dirpath` beneath `anchor` (NULL = cwd, else an absolute - * trusted root), reusing the held ancestor stack. Returns a BORROWED dirfd - * owned by the cache (do NOT close), or -1 (errno preserved for a real open - * error, errno==0 for an uncacheable path -- "..", too deep/long, or a relative - * non-cwd anchor) so the caller can fall back to secure_relative_open(). */ -static int dpc_dir_fd(const char *anchor, const char *dirpath) -{ - char copy[MAXPATHLEN]; - char *comps[DPC_MAXDEPTH]; - char *sv = NULL; - int nc = 0, p, i; - - if (anchor && anchor[0] != '/') { errno = 0; return -1; } - if (!dirpath) - dirpath = ""; - if (dirpath[0] == '/') { errno = 0; return -1; } - - if (anchor != dpc_anchor || dpc_base < 0) { - int fl; - reset_dir_fd_cache(); - dpc_base = open_anchor_dirfd(anchor ? anchor : "."); - if (dpc_base < 0) - return -1; - if ((fl = fcntl(dpc_base, F_GETFD)) >= 0) - fcntl(dpc_base, F_SETFD, fl | FD_CLOEXEC); - dpc_anchor = anchor; - } - - if (strlcpy(copy, dirpath, sizeof copy) >= sizeof copy) { errno = ENAMETOOLONG; return -1; } - for (char *c = strtok_r(copy, "/", &sv); c; c = strtok_r(NULL, "/", &sv)) { - if (c[0] == '.' && c[1] == '\0') - continue; /* "." */ - if (c[0] == '.' && c[1] == '.' && c[2] == '\0') { errno = 0; return -1; } - if (nc >= DPC_MAXDEPTH || strlen(c) >= sizeof dpc_name[0]) { - /* Too deep / a too-long component to cache. Release the held - * ancestor fds first so the caller's full-path fallback walk does - * not stack on top of them: a deep tree plus a low RLIMIT_NOFILE - * (e.g. OpenBSD's default 128) would otherwise exhaust descriptors - * (cache depth + walk depth). */ - reset_dir_fd_cache(); - errno = 0; - return -1; - } - comps[nc++] = c; - } - - /* Reuse the longest common prefix; drop the divergent tail. */ - for (p = 0; p < dpc_depth && p < nc && strcmp(dpc_name[p], comps[p]) == 0; p++) - ; - while (dpc_depth > p) - close(dpc_fd[--dpc_depth]); - - /* Descend the new tail, holding each resolved component. */ - for (i = p; i < nc; i++) { - int afd = dpc_depth > 0 ? dpc_fd[dpc_depth-1] : dpc_base; - struct dirstack ds; - int hops = SECURE_OPEN_MAXSYMLINKS; - int fd, fl; - if (ds_init(&ds, afd) < 0) - return -1; - if (ds_descend(&ds, comps[i], &hops) < 0) { - int e = errno; - ds_free(&ds); - errno = e; - return -1; - } - fd = ds_take(&ds); - ds_free(&ds); /* closes intermediate symlink fds, not afd */ - if (fd < 0) - return -1; - if ((fl = fcntl(fd, F_GETFD)) >= 0) - fcntl(fd, F_SETFD, fl | FD_CLOEXEC); - strlcpy(dpc_name[dpc_depth], comps[i], sizeof dpc_name[0]); - dpc_fd[dpc_depth++] = fd; - } - - return nc > 0 ? dpc_fd[dpc_depth-1] : dpc_base; -} - -/* Public entry for the sender (no secure_relpath_active gate: its send paths - * confine unconditionally). Borrowed fd; -1 => caller uses the full walk. */ -int held_dir_path_fd(const char *anchor, const char *dirpath) -{ - return dpc_dir_fd(anchor, dirpath); -} - -int get_dir_fd(const char *dirname) -{ - if (!secure_relpath_active()) { errno = 0; return -1; } - return dpc_dir_fd(NULL, dirname); -} -#else -void reset_dir_fd_cache(void) -{ -} -int held_dir_path_fd(const char *anchor, const char *dirpath) -{ - (void)anchor; - (void)dirpath; - errno = 0; - return -1; -} -int get_dir_fd(const char *dirname) -{ - (void)dirname; - errno = 0; - return -1; -} -#endif - -/* Return the cached current-directory fd iff `path` lives directly in the - * entry's own directory (file->dirname) -- the common case for held-dirfd - * traversal. Returns -1 (caller falls back to the do_*_at() wrappers) for - * anything elsewhere: --temp-dir/--partial-dir/--backup-dir, an absolute path, - * a differently-nested dir, or when open_dir_secure() is gated off. The dirfd - * is opened once and cached. - * - * file->basename is NOT assumed to equal `path`'s leaf (a temp file has a - * different basename), so the caller derives the leaf from `path`. */ -int held_dfd_for(const char *path, const struct file_struct *file) -{ - const char *slash, *dn; - size_t plen; - - if (!path || *path == '/') - return -1; - dn = file && file->dirname ? file->dirname : ""; - slash = strrchr(path, '/'); - plen = slash ? (size_t)(slash - path) : 0; - if (strlen(dn) != plen || memcmp(path, dn, plen) != 0) - return -1; - return get_dir_fd(file ? file->dirname : NULL); -} - -int do_unlink_atfd(int dfd, const char *name, int flags) -{ -#ifdef AT_FDCWD - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - return unlinkat(dfd, name, flags); -#else - (void)dfd; (void)name; (void)flags; - errno = ENOSYS; - return -1; -#endif -} - -int do_mkdir_atfd(int dfd, const char *name, mode_t mode) -{ -#ifdef AT_FDCWD - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - return mkdirat(dfd, name, mode); -#else - (void)dfd; (void)name; (void)mode; - errno = ENOSYS; - return -1; -#endif -} - -#ifdef HAVE_CHMOD -int do_chmod_atfd(int dfd, const char *name, mode_t mode) -{ -#ifdef AT_FDCWD - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - /* Do not follow a final-component symlink (closes the leaf race; the - * held parent dfd already confines the ancestors). A symlink-as-object - * (S_ISLNK(mode)) is still handled by the caller via the full-path - * do_chmod() lchmod/setattrlist code, exactly as do_chmod_at() does. */ - return do_fchmodat_nofollow(dfd, name, mode); -#else - (void)dfd; (void)name; (void)mode; - errno = ENOSYS; - return -1; -#endif -} -#endif - -int do_lchown_atfd(int dfd, const char *name, uid_t owner, gid_t group) -{ -#ifdef AT_FDCWD - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - return fchownat(dfd, name, owner, group, AT_SYMLINK_NOFOLLOW); -#else - (void)dfd; (void)name; (void)owner; (void)group; - errno = ENOSYS; - return -1; -#endif -} - -/* Mode/owner on an already-open fd (no path, no symlink to follow): the - * race-free way to set metadata on a cross-tree operator-path leaf that was - * pinned with O_NOFOLLOW. See set_file_attrs(). */ -int do_fchown(int fd, uid_t owner, gid_t group) -{ - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - return fchown(fd, owner, group); -} - -#ifdef HAVE_CHMOD -int do_fchmod(int fd, mode_t mode) -{ - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - return fchmod(fd, 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) -{ -#ifdef AT_FDCWD - 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 utimensat(dfd, name, t, AT_SYMLINK_NOFOLLOW); -#else - (void)dfd; (void)name; (void)stp; - errno = ENOSYS; - return -1; -#endif -} -#endif - -int do_open_atfd(int dfd, const char *name, int flags, mode_t mode) -{ -#ifdef AT_FDCWD - if (flags != O_RDONLY) { - RETURN_ERROR_IF(dry_run, 0); - RETURN_ERROR_IF_RO_OR_LO; - } -#ifdef O_NOATIME - if (open_noatime) - flags |= O_NOATIME; -#endif - return openat(dfd, name, flags | O_NOFOLLOW | O_BINARY, mode); -#else - (void)dfd; (void)name; (void)flags; (void)mode; - errno = ENOSYS; - return -1; -#endif -} - -int do_symlink_atfd(const char *lnk, int dfd, const char *name) -{ -#ifdef AT_FDCWD - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - -#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS - /* --fake-super: store the link target in a regular placeholder file, - * created with O_NOFOLLOW so a planted basename symlink can't redirect - * the write (mirrors do_symlink_at()). */ - if (am_root < 0) { - int len = strlen(lnk); - int ok; - int fd = openat(dfd, name, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, - S_IWUSR | S_IRUSR); - if (fd < 0) - return -1; - ok = write(fd, lnk, len) == len; - if (close(fd) < 0) - ok = 0; - return ok ? 0 : -1; - } -#endif - return symlinkat(lnk, dfd, name); -#else - (void)lnk; (void)dfd; (void)name; - errno = ENOSYS; - return -1; -#endif -} - -int do_mknod_atfd(int dfd, const char *name, mode_t mode, dev_t dev) -{ -#ifdef AT_FDCWD - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - - if (am_root < 0) { - /* --fake-super: regular empty placeholder file (O_NOFOLLOW). */ - int fd = openat(dfd, name, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, - S_IWUSR | S_IRUSR); - if (fd < 0) - return -1; - return (close(fd) < 0) ? -1 : 0; - } - - /* Try mknodat first; on failure retry race-safely with the type- - * specific primitive (see do_mknod()). HAVE_MKNODAT, not HAVE_MKNOD: - * older Darwin has mknod() but not mknodat(), so keying off the former - * compiles a call that then fails to link (#161). */ -#ifdef HAVE_MKNODAT - if (mknodat(dfd, name, mode, dev) == 0) - return 0; -#endif -#ifdef HAVE_MKFIFOAT - if (S_ISFIFO(mode)) - return mkfifoat(dfd, name, mode); -#endif - if (S_ISSOCK(mode)) { - /* No dirfd-relative socket bind without /proc/self/fd; fail safe. - * (The generator routes sockets to do_mknod_at(), not here.) */ - errno = EOPNOTSUPP; - return -1; - } -#ifdef HAVE_MKNODAT - return -1; /* mknodat()'s errno (regular/device node) */ -#else - /* Must match the guard above: reporting "mknodat()'s errno" where the - * call was never compiled would return a stale errno. */ - (void)dev; - errno = ENOSYS; - return -1; -#endif -#else - (void)dfd; (void)name; (void)mode; (void)dev; - errno = ENOSYS; - return -1; -#endif -} - -int do_rename_atfd(int old_dfd, const char *old_name, int new_dfd, const char *new_name) -{ -#ifdef AT_FDCWD - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - return renameat(old_dfd, old_name, new_dfd, new_name); -#else - (void)old_dfd; (void)old_name; (void)new_dfd; (void)new_name; - errno = ENOSYS; - return -1; -#endif -} - -#if defined HAVE_LINK || defined HAVE_LINKAT -int do_link_atfd(int old_dfd, const char *old_name, int new_dfd, const char *new_name, int flags) -{ -#if defined AT_FDCWD && defined HAVE_LINKAT - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - return linkat(old_dfd, old_name, new_dfd, new_name, flags); -#else - (void)old_dfd; (void)old_name; (void)new_dfd; (void)new_name; (void)flags; - errno = ENOSYS; - return -1; -#endif -} -#endif - -int do_lstat_atfd(int dfd, const char *name, STRUCT_STAT *st) -{ -#ifdef AT_FDCWD -# ifdef SUPPORT_LINKS - return fstatat(dfd, name, st, AT_SYMLINK_NOFOLLOW); -# else - return fstatat(dfd, name, st, 0); -# endif -#else - (void)dfd; (void)name; (void)st; - errno = ENOSYS; - return -1; -#endif -} - -int do_stat_atfd(int dfd, const char *name, STRUCT_STAT *st) -{ -#ifdef AT_FDCWD - return fstatat(dfd, name, st, 0); -#else - (void)dfd; (void)name; (void)st; - errno = ENOSYS; - return -1; -#endif -} diff --git a/t_chmod_secure.c b/t_chmod_secure.c index bb6d2dd24..607c995a3 100644 --- a/t_chmod_secure.c +++ b/t_chmod_secure.c @@ -1,7 +1,7 @@ /* - * Test harness for do_chmod_at(). Confirms the symlink-TOCTOU + * Test harness for vfs_chmod(). Confirms the symlink-TOCTOU * primitive used by CVE-2026-29518 (and its incomplete-fix follow-up - * for chmod) is closed by do_chmod_at(): a parent directory component + * for chmod) is closed by vfs_chmod(): a parent directory component * being a symlink that escapes the receiver's confinement must be * rejected, while a parent symlink that resolves *within* the tree * must still work (so legitimate dir-symlinks are not regressed). @@ -31,7 +31,7 @@ short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG]; static int errs = 0; -/* Does do_chmod_at()'s leaf handling refuse to follow a symlink at the final +/* Does vfs_chmod()'s leaf handling refuse to follow a symlink at the final * component? Yes wherever AT_SYMLINK_NOFOLLOW exists; otherwise the wrapper * falls back to a following fchmodat() (documented limitation). Mirrors the * #ifdef ladder in do_fchmodat_nofollow. */ @@ -85,9 +85,9 @@ int main(int argc, char **argv) return 2; } - /* Simulate the daemon-without-chroot deployment that do_chmod_at() + /* Simulate the daemon-without-chroot deployment that vfs_chmod() * defends. With am_daemon=0 or am_chrooted=1 the wrapper falls - * through to plain do_chmod() and the symlink-race test would be + * through to plain vfs_chmod() and the symlink-race test would be * meaningless. */ am_daemon = 1; am_chrooted = 0; @@ -112,26 +112,26 @@ int main(int argc, char **argv) * Solaris, older Cygwin, HPE NonStop, pre-5.6 Linux) -- which now follows * an in-tree directory symlink whose target is relative and ".."-free. * Escapes are still rejected on both paths (Scenario B). */ - int rc = do_chmod_at("inside_link/sentinel", 0640); + int rc = vfs_chmod(VFS_AT_FDCWD, "inside_link/sentinel", 0640, 0); check("A: legit dir-symlink within tree (followed)", rc, 1, "realdir/sentinel", 0640); /* Scenario B: parent symlink escapes the tree -- chmod must be * rejected and the outside file's mode must be unchanged. */ - rc = do_chmod_at("escape_link/sentinel", 0666); + rc = vfs_chmod(VFS_AT_FDCWD, "escape_link/sentinel", 0666, 0); check("B: parent symlink escapes tree (the attack)", rc, 0, "../trap/sentinel", 0600); /* Scenario C: plain relative path with no symlink components, * regression check that the safe wrapper doesn't break the * normal case. */ - rc = do_chmod_at("realdir/sentinel", 0644); + rc = vfs_chmod(VFS_AT_FDCWD, "realdir/sentinel", 0644, 0); check("C: plain relative path (regression check)", rc, 1, "realdir/sentinel", 0644); /* Scenario D: top-level file, no parent directory component. - * Falls back to do_chmod(); should succeed. */ - rc = do_chmod_at("topfile", 0640); + * Falls back to vfs_chmod(); should succeed. */ + rc = vfs_chmod(VFS_AT_FDCWD, "topfile", 0640, 0); check("D: top-level file, no parent component", rc, 1, "topfile", 0640); @@ -141,12 +141,12 @@ int main(int argc, char **argv) * (refused on Linux, lchmod-the-symlink on *BSD/macOS), so assert only that * the outside target's mode is unchanged. */ if (leaf_chmod_nofollow_supported()) { - rc = do_chmod_at("realdir/leaflink", 0666); + rc = vfs_chmod(VFS_AT_FDCWD, "realdir/leaflink", 0666, 0); check("E: leaf component is an escaping symlink (must not be followed)", rc, -1, "../trap/sentinel", 0600); } else { fprintf(stderr, "INFO: leaf-nofollow chmod unsupported here; " - "do_chmod_at follows a leaf symlink (documented limitation), " + "vfs_chmod follows a leaf symlink (documented limitation), " "skipping scenario E\n"); } diff --git a/t_rename_secure.c b/t_rename_secure.c index 79684a004..3de047fbb 100644 --- a/t_rename_secure.c +++ b/t_rename_secure.c @@ -1,6 +1,6 @@ /* - * Test harness for do_rename_at(): a mixed top-level/slashed rename must still - * resolve the slashed side's parent under secure_relative_open() rather than + * Test harness for vfs_rename_at(): a mixed top-level/slashed rename must still + * resolve the slashed side's parent under vfs_resolve_open() rather than * fall back to plain rename(). Not linked into rsync. GPL version 2. */ @@ -23,21 +23,21 @@ static int errs = 0; #ifdef AT_FDCWD /* The 3.4.3 bug: if either side has no slash the whole op fell back to plain - * rename(), leaving the slashed side's parent outside secure_relative_open(). */ + * rename(), leaving the slashed side's parent outside vfs_resolve_open(). */ static int vulnerable_mixed_rename_at(const char *old_path, const char *new_path) { const char *old_slash, *new_slash; if (!old_path || !*old_path || *old_path == '/' || !new_path || !*new_path || *new_path == '/') - return do_rename(old_path, new_path); + return vfs_rename(old_path, new_path); old_slash = strrchr(old_path, '/'); new_slash = strrchr(new_path, '/'); if (!old_slash || !new_slash) - return do_rename(old_path, new_path); + return vfs_rename(old_path, new_path); - return do_rename_at(old_path, new_path); + return vfs_rename_at(old_path, new_path, 0, 0); } #endif @@ -64,7 +64,7 @@ static void check_rename(const char *label, const char *old_path, int saved_errno; errno = 0; - rc = do_rename_at(old_path, new_path); + rc = vfs_rename_at(old_path, new_path, 0, 0); saved_errno = errno; got_ok = rc == 0; @@ -79,6 +79,30 @@ static void check_rename(const char *label, const char *old_path, label, old_path, new_path, expect_ok ? "succeeded" : "rejected"); } +/* Like check_rename() but with explicit per-operand policy flags, for the + * two-path per-side split (PR #30). */ +static void check_rename_flags(const char *label, const char *old_path, + const char *new_path, int old_flags, int new_flags, + int expect_ok) +{ + int rc, got_ok, saved_errno; + + errno = 0; + rc = vfs_rename_at(old_path, new_path, old_flags, new_flags); + saved_errno = errno; + got_ok = rc == 0; + + if (got_ok != expect_ok) { + fprintf(stderr, "FAIL [%s]: rename %s -> %s (of=%d nf=%d) rc=%d errno=%d (%s), expected %s\n", + label, old_path, new_path, old_flags, new_flags, rc, saved_errno, + strerror(saved_errno), expect_ok ? "success" : "rejection"); + errs++; + return; + } + fprintf(stderr, "OK [%s]: rename %s -> %s %s\n", + label, old_path, new_path, expect_ok ? "succeeded" : "rejected"); +} + static void check_vulnerable_rename(const char *label, const char *old_path, const char *new_path) { @@ -199,6 +223,38 @@ int main(int argc, char **argv) check_exists("F source consumed", "top-old", 0); check_exists("F destination created", "top-new", 1); + /* Per-operand policy split (PR #30): the NEW side's policy must be independent + * of the OLD side's. oplink is a caller-owned (uid0/euid) symlink that ESCAPES + * the tree (-> ../trap): the ownership walk (operator policy) follows the + * operator's own symlink, but the secure receiver resolve (transfer, flag 0) + * refuses it because it leaves the cwd anchor. PS-refuse and PS-follow rename + * to the SAME oplink/ path with the SAME operator old side, differing ONLY in + * the new-side flag -- so the per-side flag, not a whole-call flag, decides. + * The old single-flag API applied VFS_OPERATOR_PATH to both operands, so it + * would have followed oplink in PS-refuse too (PS-refuse is RED on that code). + * Run non-daemon (the regime where an operator basis/backup path legitimately + * carries the operator's own uid0/euid symlinks). */ + { + struct stat lst; + if (lstat("oplink", &lst) == 0 && S_ISLNK(lst.st_mode)) { + int save_daemon = am_daemon; + am_daemon = 0; + + check_rename_flags("PS-refuse: new side transfer refuses an escaping owned symlink", + "realdir/perside-src2", "oplink/tr-out", + VFS_OPERATOR_PATH, 0, 0); + check_exists("PS-refuse out-of-tree dest absent", "../trap/tr-out", 0); + check_exists("PS-refuse source preserved", "realdir/perside-src2", 1); + + check_rename_flags("PS-follow: new side operator follows the same owned symlink", + "realdir/perside-src3", "oplink/op-out", + VFS_OPERATOR_PATH, VFS_OPERATOR_PATH, 1); + check_exists("PS-follow out-of-tree dest created (operator's own symlink)", "../trap/op-out", 1); + + am_daemon = save_daemon; + } + } + if (errs) fprintf(stderr, "%d failure(s)\n", errs); return errs ? 1 : 0; diff --git a/t_secure_relpath.c b/t_secure_relpath.c index d20570d61..c8083e6a9 100644 --- a/t_secure_relpath.c +++ b/t_secure_relpath.c @@ -1,5 +1,5 @@ /* - * Test harness for secure_relative_open()'s front-door input + * Test harness for vfs_resolve_open()'s front-door input * validation. Codex audit Finding 5 noted that the existing check * * if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../")) @@ -14,7 +14,7 @@ * pre-5.6 Linux does not, so the validation must happen at the * front door. * - * This helper invokes secure_relative_open() with each suspect + * This helper invokes vfs_resolve_open() with each suspect * input and checks both the failure (rc < 0) and the errno * (EINVAL means "rejected at the front door"). Pre-fix, the kernel * may reject with a different errno (EXDEV from RESOLVE_BENEATH); @@ -47,7 +47,7 @@ static void check_relpath(const char *relpath) int saved_errno; errno = 0; - fd = secure_relative_open(NULL, relpath, O_RDONLY | O_DIRECTORY, 0); + fd = vfs_resolve_open(NULL, relpath, O_RDONLY | O_DIRECTORY, 0); saved_errno = errno; if (fd >= 0) { @@ -76,7 +76,7 @@ static void check_basedir(const char *basedir) int saved_errno; errno = 0; - fd = secure_relative_open(basedir, "ok", O_RDONLY | O_DIRECTORY, 0); + fd = vfs_resolve_open(basedir, "ok", O_RDONLY | O_DIRECTORY, 0); saved_errno = errno; if (fd >= 0) { @@ -111,7 +111,7 @@ static void check_beneath_dotdot(void) return; } - fd = secure_relative_open_at_beneath(anchor, "alias/../subdir", + fd = vfs_resolve_open_at_beneath(anchor, "alias/../subdir", O_RDONLY | O_DIRECTORY, 0); if (fd < 0 || fstat(fd, &fst) < 0 || fst.st_dev != ast.st_dev || fst.st_ino == ast.st_ino) { @@ -138,7 +138,7 @@ static void check_beneath_dotdot(void) for (ci = 0; ci < sizeof dotdot_cases / sizeof *dotdot_cases; ci++) { int dfd; errno = 0; - dfd = secure_relative_open_at_beneath(anchor, "..", + dfd = vfs_resolve_open_at_beneath(anchor, "..", dotdot_cases[ci].flags, 0); if (dfd >= 0) { STRUCT_STAT dst; @@ -159,7 +159,7 @@ static void check_beneath_dotdot(void) } errno = 0; - fd = secure_relative_open_at_beneath(anchor, "../outside", + fd = vfs_resolve_open_at_beneath(anchor, "../outside", O_RDONLY | O_DIRECTORY, 0); if (fd >= 0 || errno != ELOOP) { fprintf(stderr, "FAIL [beneath escape]: rc=%d errno=%d, expected -1/ELOOP\n", @@ -184,7 +184,7 @@ int main(int argc, char **argv) return 2; } - /* secure_relative_open's daemon-only confinement protections only + /* vfs_resolve_open's daemon-only confinement protections only * fire when am_daemon && !am_chrooted (the threat model is the * daemon-no-chroot deployment), but the front-door input * validation runs unconditionally. We set am_daemon anyway so the @@ -196,7 +196,7 @@ int main(int argc, char **argv) symlink("subdir", "alias"); /* Each of these relpaths must be rejected with EINVAL at the - * secure_relative_open() front door. ".." is the actual one-level + * vfs_resolve_open() front door. ".." is the actual one-level * escape; the others ("subdir/..", "subdir/../subdir") resolve * back to the start dir on systems that allow them, but we still * reject them as defence-in-depth: a path containing a ".." token diff --git a/t_stub.c b/t_stub.c index 1518c7932..8e1aa1e25 100644 --- a/t_stub.c +++ b/t_stub.c @@ -42,14 +42,14 @@ size_t max_alloc = (size_t)-1; /* test helpers are not memory-constrained; * 0 here makes every my_alloc()/my_strdup() in * util2.c trip the "exceeded --max-alloc=0" * check, which any helper exercising the - * per-component fallback of secure_relative_open() + * per-component fallback of vfs_resolve_open() * hits at its first my_strdup() call. */ char *partial_dir; char *module_dir; int module_dirfd = -1; char *confine_root; unsigned int confine_rootlen = 0; -/* curr_dir[]/curr_dir_len (read by secure_relative_open) are defined in +/* 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; diff --git a/t_symlink_secure.c b/t_symlink_secure.c index 64eae888a..6a501baf3 100644 --- a/t_symlink_secure.c +++ b/t_symlink_secure.c @@ -1,8 +1,8 @@ /* - * Test harness for the fake-super branches of do_symlink_at()/do_mknod_at(). + * Test harness for the fake-super branches of vfs_symlink_at()/vfs_mknod_at(). * Fake-super stores a symlink/device as a placeholder file, so the create * resolves the final component; the no-slash branch used to fall back to - * do_symlink()/do_mknod(), whose plain open() followed a planted basename + * vfs_symlink()/vfs_mknod(), whose plain open() followed a planted basename * symlink and escaped the module. Checks the fixed wrappers refuse it; * --poc shows the old fallback escaping. Not linked into rsync. GPL version 2. */ @@ -12,7 +12,7 @@ #include /* The symlink placeholder (and thus this escape) exists only where symlink - * xattrs are unavailable -- the same guard do_symlink() uses. Elsewhere + * xattrs are unavailable -- the same guard vfs_symlink() uses. Elsewhere * symlink() fails EEXIST on a planted link, so only the device path applies. */ #if defined SUPPORT_LINKS && (defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS) #define TEST_SYMLINK_PLACEHOLDER 1 @@ -89,8 +89,8 @@ int main(int argc, char **argv) const char *moddir; # if !defined(HAVE_MKNODAT) && !defined(TEST_SYMLINK_PLACEHOLDER) - /* Nothing left to assert: the do_mknod_at() checks need mknodat(), and - * the do_symlink_at() ones are not compiled here. Skip rather than + /* Nothing left to assert: the vfs_mknod() checks need mknodat(), and + * the vfs_symlink() ones are not compiled here. Skip rather than * pass vacuously. */ (void)argc; (void)argv; fprintf(stderr, "SKIP: no mknodat() and no symlink placeholders -- " @@ -118,39 +118,39 @@ int main(int argc, char **argv) am_root = -1; /* fake-super: symlinks/devices stored as files */ if (poc) { - /* Pre-fix fallback: a no-slash path went to do_symlink()/do_mknod(), + /* Pre-fix fallback: a no-slash path went to vfs_symlink()/vfs_mknod(), * which open() the basename without O_NOFOLLOW. */ #ifdef TEST_SYMLINK_PLACEHOLDER - do_symlink("VULN_SYM_PAYLOAD", "sympath"); - check_clobbered("poc do_symlink bare", "../outside/secret_sym", + vfs_symlink("VULN_SYM_PAYLOAD", VFS_AT_FDCWD, "sympath", VFS_ALLOW_SYMLINK); + check_clobbered("poc vfs_symlink bare", "../outside/secret_sym", "VULN_SYM_PAYLOAD"); #endif - do_mknod("nodpath", S_IFCHR | 0600, 0); - check_clobbered("poc do_mknod bare", "../outside/secret_nod", ""); + vfs_mknod(VFS_AT_FDCWD, "nodpath", S_IFCHR | 0600, 0, VFS_ALLOW_SYMLINK); + check_clobbered("poc vfs_mknod bare", "../outside/secret_nod", ""); return errs ? 1 : 0; } /* Fixed wrappers: a bare-path basename symlink must not be followed; * the victim outside the module stays untouched. */ #ifdef TEST_SYMLINK_PLACEHOLDER - do_symlink_at("FIXED_SYM_PAYLOAD", "sympath"); - check_preserved("do_symlink_at bare", "../outside/secret_sym", "VICTIM_SYM"); + vfs_symlink("FIXED_SYM_PAYLOAD", VFS_AT_FDCWD, "sympath", 0); + check_preserved("vfs_symlink bare", "../outside/secret_sym", "VICTIM_SYM"); /* Slashed path for parity (already protected before the fix). */ - do_symlink_at("FIXED_SYM_PAYLOAD", "sub/sympath2"); - check_preserved("do_symlink_at slashed", "../outside/secret_sym2", "VICTIM_SYM2"); + vfs_symlink("FIXED_SYM_PAYLOAD", VFS_AT_FDCWD, "sub/sympath2", 0); + check_preserved("vfs_symlink slashed", "../outside/secret_sym2", "VICTIM_SYM2"); #endif # ifdef HAVE_MKNODAT - /* Without mknodat() do_mknod_at() IS do_mknod(): the confinement is - * compiled out by design (SECURITY.md), so these would assert a - * property the build deliberately does not have. The do_symlink_at() - * checks above do not depend on it and still run. */ - do_mknod_at("nodpath", S_IFCHR | 0600, 0); - check_preserved("do_mknod_at bare", "../outside/secret_nod", "VICTIM_NOD"); - - do_mknod_at("sub/nodpath2", S_IFCHR | 0600, 0); - check_preserved("do_mknod_at slashed", "../outside/secret_nod2", "VICTIM_NOD2"); + /* Without mknodat() the secure vfs_mknod() IS the plain mknod(): the + * confinement is compiled out by design (SECURITY.md), so these would + * assert a property the build deliberately does not have. The + * vfs_symlink() checks above do not depend on it and still run. */ + vfs_mknod(VFS_AT_FDCWD, "nodpath", S_IFCHR | 0600, 0, 0); + check_preserved("vfs_mknod bare", "../outside/secret_nod", "VICTIM_NOD"); + + vfs_mknod(VFS_AT_FDCWD, "sub/nodpath2", S_IFCHR | 0600, 0, 0); + check_preserved("vfs_mknod slashed", "../outside/secret_nod2", "VICTIM_NOD2"); # endif if (errs) diff --git a/testsuite/daemon-exclude-namebased_test.py b/testsuite/daemon-exclude-namebased_test.py index 84275d551..269cac063 100644 --- a/testsuite/daemon-exclude-namebased_test.py +++ b/testsuite/daemon-exclude-namebased_test.py @@ -19,7 +19,9 @@ SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf, ) -DAEMON_PORT = 13010 +# Not 13000-13060: ASUS Armoury Crate on the Cygwin CI host parks localhost +# listeners there (13010 among them). +DAEMON_PORT = 12931 # (module name, exclude pattern, expect the pushed file to land) CASES = [ diff --git a/testsuite/fleettest.json.example b/testsuite/fleettest.json.example index 32c0da09a..def1b057e 100644 --- a/testsuite/fleettest.json.example +++ b/testsuite/fleettest.json.example @@ -227,9 +227,10 @@ "scratchbase": "/Volumes/RsyncHFS", "expect_skip_omit": [ "backup-crossdev-copy", - "chmod-temp-dir" + "chmod-temp-dir", + "operator-path-backup-chown" ], - "_skip_comment": "The two omitted entries are macOS-wide expected-skips that this target RUNS: the separate volume supplies the cross-device conditions mac2 lacks. (itemize used to XFAIL here, before --link-dest learned to fall back when the filesystem cannot hard-link a symlink.)" + "_skip_comment": "The three omitted entries are macOS-wide expected-skips that this target RUNS: the separate volume supplies the cross-device conditions mac2 lacks. (itemize used to XFAIL here, before --link-dest learned to fall back when the filesystem cannot hard-link a symlink.)" }, { "_comment": "The x86-64 Mac (macOS 10.13). The ONLY target that can build the x86-64 md5 assembly -- mac2 is arm64, where configure refuses --enable-md5-asm outright. MacPorts supplies autotools, python3 and the crypto/hash libs the stock 10.13 image lacks, and is not on the non-interactive ssh PATH, so put it there for the whole run. This target keeps the STOCK Apple compiler (clang 10, the 10.13 ceiling), which is what caught #161; it cannot build --enable-roll-simd, because a clang that old rejects configure's target(\"default\") multiversioning probe. That is a compiler-VERSION limit, not a Mach-O one -- see mac-x86-asm below, which builds the same source with MacPorts clang 19 and all three optimizations on.", diff --git a/testsuite/operator-path-backup-chown_test.py b/testsuite/operator-path-backup-chown_test.py new file mode 100644 index 000000000..1ff1d937a --- /dev/null +++ b/testsuite/operator-path-backup-chown_test.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +# --backup-dir parent-component symlink-race confinement for the OWNERSHIP set, +# not the create. operator-path-backup-symlink covers the create side (a backup +# symlink must not be written outside the backup tree); this covers what +# set_file_attrs() does to the item afterwards. +# +# make_backup() recreates the item at the backup name and then calls +# set_file_attrs(buf, ..., ATTRS_OPERATOR_PATH). A regular/dir/fifo leaf is +# pinned by op_pin and its metadata driven off that fd, but a SYMLINK leaf never +# enters op_pin (there is no O_NOFOLLOW open of a symlink), so the chown falls +# through to the path-based wrapper on the full operator path. Unless that +# wrapper resolves through the ownership walk, a parent component flipped to an +# attacker-owned symlink redirects the lchown out of the backup tree and retags +# a victim inode as the attacker's -- an ownership-transfer primitive, and the +# trust laundering that then defeats the walk on any later pass. +# +# Reaching the recreate path at all needs the backup dir on ANOTHER filesystem: +# on one filesystem make_backup() hard-links or renames the item across and +# never calls set_file_attrs(). So the whole fixture lives on tmpfs. +# +# A statically planted symlink is not enough either -- rsync's own backup-dir +# validation deletes a non-directory component before using it -- so the plant +# has to be a live flip, as in the sibling test. + +import os +import subprocess +import time + +from rsyncfns import ( + SCRATCHDIR, race_budget, find_attacker_uid, rmtree, makepath, + start_c_flipper, stop_flipper, test_fail, test_skipped, +) + +if os.geteuid() != 0: + test_skipped("requires root to own a symlink by a foreign uid and to chown backups") + +ATT_UID = find_attacker_uid() +if ATT_UID is None: + test_skipped("no untrusted-uid user available for cross-uid plant") + +# The backup dir must be on a different st_dev from the destination, or +# make_backup() renames into it and the set_file_attrs() path never runs. +dest_dev = os.stat(SCRATCHDIR).st_dev +TMPFS = None +for cand in ('/dev/shm', '/run/shm', os.environ.get('TMPDIR', '/tmp')): + try: + if os.stat(cand).st_dev != dest_dev and os.access(cand, os.W_OK): + TMPFS = cand + break + except OSError: + continue +if TMPFS is None: + test_skipped("no writable cross-device dir (tmpfs) for the --backup-dir EXDEV path") + +# Many files widen the per-file backup window so the flipper has more chances to +# land the swap between the recreate and the chown. +NFILES = 95 + +base = SCRATCHDIR / 'bdir-chown-race' +src = base / 'src' +dest = base / 'dest' + +bakroot = os.path.join(TMPFS, 'rsync-bakchown-race') +backup = os.path.join(bakroot, 'backup') +outside = os.path.join(bakroot, 'outside') +sub = os.path.join(backup, 'sub') +sublink = os.path.join(backup, '.sublink') + + +def build(): + """Reset the workspace. Call only while the flipper is stopped.""" + rmtree(base) + subprocess.run(['rm', '-rf', bakroot], check=False) + makepath(src / 'sub', dest / 'sub') + os.makedirs(outside, exist_ok=True) + os.makedirs(backup, exist_ok=True) + + # Distinct source and destination symlink values so each transfer replaces + # the destination symlink and thus backs the old one up. The destination + # symlinks are attacker-owned, so restoring their ownership onto the backup + # copy REQUIRES an lchown -- without that there is no chown to redirect and + # the test would pass vacuously. + for i in range(NFILES): + (src / 'sub' / f'f{i}').symlink_to('test') + d = dest / 'sub' / f'f{i}' + d.symlink_to('test2') + os.lchown(d, ATT_UID, ATT_UID) + + # Victims: root-owned regular files carrying the names the backup would use + # if a flipped `sub` redirected the operator path into outside/. + for i in range(NFILES): + v = os.path.join(outside, f'f{i}') + with open(v, 'w') as fh: + fh.write('victim\n') + os.chown(v, 0, 0) + + # The attacker-owned parent-swap target. + os.symlink(outside, sublink) + os.lchown(sublink, ATT_UID, ATT_UID) + os.makedirs(sub, exist_ok=True) + + +def push(): + """Blocking local rsync push that backs up the old destination symlinks.""" + return subprocess.run( + ['./rsync', '-a', '-b', f'--backup-dir={backup}', f'{src}/', f'{dest}/'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + ) + + +def retagged(): + """Name of a victim in outside/ that stopped being root-owned, or ''. + + A swap killed mid-rename can leave outside/ momentarily odd; anything we + cannot stat is simply not evidence of a win.""" + try: + with os.scandir(outside) as it: + for e in it: + try: + st = e.stat(follow_symlinks=False) + except OSError: + continue + if st.st_uid != 0 or st.st_gid != 0: + return e.name + except (FileNotFoundError, NotADirectoryError): + return '' + return '' + + +# ---- POSITIVE CONTROL ------------------------------------------------------ +# A clean run must (a) back the old destination symlink into the backup tree via +# the cross-device recreate path and (b) carry the attacker ownership onto that +# backup copy -- which is the lchown this test is about. Without both, the race +# below would be asserting on a code path that never executes. +build() +proc = push() +if proc.returncode != 0: + test_fail(f"positive control: clean --backup-dir run failed (rc={proc.returncode}):\n{proc.stdout or ''}") + +bak0 = os.path.join(sub, 'f0') +if not os.path.islink(bak0) or os.readlink(bak0) != 'test2': + test_fail(f"positive control: the old destination symlink was not backed up into {sub}; " + "the cross-device recreate path was not exercised") +st = os.lstat(bak0) +if st.st_uid != ATT_UID: + test_fail(f"positive control: backup copy {bak0} is uid {st.st_uid}, expected the " + f"attacker uid {ATT_UID}; set_file_attrs() did not lchown the backup, so " + "this test would pass vacuously") +if retagged(): + test_fail("positive control: a victim in outside/ changed ownership during a no-flipper run") + + +# ---- THE LIVE RACE --------------------------------------------------------- +# Flip backup/sub between the real backup directory and the attacker-owned +# symlink to outside/ under a live transfer. The ownership walk must refuse the +# foreign-owned component, so no victim in outside/ is ever retagged. + +deadline = time.monotonic() + race_budget(10.0) +flip = None +try: + while time.monotonic() < deadline: + # Reset only while the flipper is quiet, so build()'s rmtree/mkdir + # cannot race the swapper and drop artifacts in outside/. + if flip is not None: + stop_flipper(flip) + flip = None + build() + flip = start_c_flipper(sub, sublink) + push() + + victim = retagged() + if victim: + test_fail( + "--backup-dir parent symlink race: victim " + f"{os.path.join(outside, victim)} was retagged away from root; rsync " + "chowned through the flipped attacker-owned backup/sub component " + "instead of refusing it." + ) +finally: + if flip is not None: + stop_flipper(flip) + subprocess.run(['rm', '-rf', bakroot], check=False) + +print("operator-path-backup-chown: backup ownership confined under parent-swap race") diff --git a/testsuite/rename-mixed-parent-symlink-race_test.py b/testsuite/rename-mixed-parent-symlink-race_test.py index f5c25dd34..f7fa5ffe6 100644 --- a/testsuite/rename-mixed-parent-symlink-race_test.py +++ b/testsuite/rename-mixed-parent-symlink-race_test.py @@ -31,6 +31,14 @@ (mod / 'top-old').write_text("top-old\n") os.symlink('../trap', mod / 'escape_link') +# Per-operand policy split (PR #30): a caller-owned symlink that ESCAPES the tree +# (-> ../trap). The ownership walk (operator) follows the operator's own symlink; +# the secure receiver resolve (transfer, flag 0) refuses it. The harness +# PS-refuse/PS-follow checks rename to oplink/ under each new-side policy. +os.symlink('../trap', mod / 'oplink') +for n in ('perside-src2', 'perside-src3'): + (mod / 'realdir' / n).write_text(n + "\n") + proc = subprocess.run([str(TOOLDIR / 't_rename_secure'), str(mod)]) if proc.returncode == 77: test_skipped("t_rename_secure skipped") diff --git a/testsuite/rsyncfns.py b/testsuite/rsyncfns.py index 15f294ef4..12d21c648 100644 --- a/testsuite/rsyncfns.py +++ b/testsuite/rsyncfns.py @@ -35,6 +35,7 @@ import sys import tempfile import time +import zlib from pathlib import Path from exitcodes import Exit # re-exported: tests may `from rsyncfns import Exit` @@ -2193,7 +2194,12 @@ def setup_chroot_inner(name): ('mod', {'path': str(outer) + '/./inner', 'read only': 'no', 'use chroot': 'yes', 'munge symlinks': 'no'}), ], name=f'{name}.conf') - url = start_test_daemon(conf, 12940 + (abs(hash(name)) % 200)) + # crc32, not hash(): str hash is per-process randomized (PYTHONHASHSEED), + # so the port would wander run to run -- and the old 12940+200 span reached + # into 13000+, where desktop bloatware (e.g. ASUS Armoury Crate on the + # Cygwin CI host) parks localhost listeners. 12800-12859 is otherwise + # unused by the suite. + url = start_test_daemon(conf, 12800 + (zlib.crc32(name.encode()) % 60)) return base, inner, outside, src, url diff --git a/testsuite/skiplist/cygwin.txt b/testsuite/skiplist/cygwin.txt index ba499cc54..47f9992a2 100644 --- a/testsuite/skiplist/cygwin.txt +++ b/testsuite/skiplist/cygwin.txt @@ -45,6 +45,7 @@ msg-io-timeout-overflow nondaemon-symlink-race nonroot-restrictive-perms open-noatime +operator-path-backup-chown operator-path-backup-rmdir operator-path-backup-symlink operator-path-insecure-links-daemon diff --git a/testsuite/skiplist/macos.txt b/testsuite/skiplist/macos.txt index 3e7653ce8..90eb4e598 100644 --- a/testsuite/skiplist/macos.txt +++ b/testsuite/skiplist/macos.txt @@ -19,6 +19,7 @@ dir-sgid fake-super-acl-xattr link-dest-symlink-enotsup # the ENOTSUP hard-link hook is an LD_PRELOAD, Linux-only open-noatime +operator-path-backup-chown partial-protected-regular-retry-linux preallocate protected-regular diff --git a/tls.c b/tls.c index e05f7ec23..dd8ccf2d8 100644 --- a/tls.c +++ b/tls.c @@ -160,11 +160,11 @@ static void list_file(const char *fname) char linkbuf[4096]; int nsecs; - if (do_lstat(fname, &buf) < 0) + if (vfs_lstat(VFS_AT_FDCWD, fname, &buf, VFS_ALLOW_SYMLINK) < 0) failed("stat", fname); #ifdef SUPPORT_CRTIMES - if (display_crtimes && (crtime = get_create_time(fname, &buf)) == 0) - failed("get_create_time", fname); + if (display_crtimes && (crtime = vfs_get_create_time(fname, &buf)) == 0) + failed("vfs_get_create_time", fname); #endif #ifdef SUPPORT_XATTRS if (am_root < 0) @@ -188,9 +188,9 @@ static void list_file(const char *fname) buf.st_uid = buf.st_gid = 0; strlcpy(linkbuf, " -> ", sizeof linkbuf); /* const-cast required for silly UNICOS headers */ - len = do_readlink((char*)fname, linkbuf+4, sizeof linkbuf - 4); + len = vfs_readlink((char*)fname, linkbuf+4, sizeof linkbuf - 4); if (len == -1) - failed("do_readlink", fname); + failed("vfs_readlink", fname); else /* it's not nul-terminated */ linkbuf[4+len] = 0; diff --git a/util1.c b/util1.c index 87ff22595..515a7a2f7 100644 --- a/util1.c +++ b/util1.c @@ -34,7 +34,6 @@ extern int relative_paths; extern int preserve_xattrs; extern int omit_link_times; extern int preallocate_files; -extern int operator_path_resolve; extern char *module_dir; extern unsigned int module_dirlen; extern char *partial_dir; @@ -42,8 +41,6 @@ extern filter_rule_list daemon_filter_list; int sanitize_paths = 0; -extern char curr_dir[MAXPATHLEN]; /* defined in syscall.c */ -extern unsigned int curr_dir_len; int curr_dir_depth; /* This is only set for a sanitizing daemon. */ /* Set a fd into nonblocking mode. */ @@ -133,7 +130,7 @@ int set_times(const char *fname, STRUCT_STAT *stp) switch (switch_step) { #ifdef HAVE_SETATTRLIST #include "case_N.h" - if (do_setattrlist_times(fname, stp) == 0) + if (vfs_setattrlist_times(fname, stp) == 0) break; if (errno != ENOSYS) return -1; @@ -142,7 +139,7 @@ int set_times(const char *fname, STRUCT_STAT *stp) #ifdef HAVE_UTIMENSAT #include "case_N.h" - if (do_utimensat_at(fname, stp) == 0) + if (vfs_utimensat_at(fname, stp) == 0) break; if (errno != ENOSYS) return -1; @@ -151,7 +148,7 @@ int set_times(const char *fname, STRUCT_STAT *stp) #ifdef HAVE_LUTIMES #include "case_N.h" - if (do_lutimes(fname, stp) == 0) + if (vfs_lutimes(fname, stp) == 0) break; if (errno != ENOSYS) return -1; @@ -168,10 +165,10 @@ int set_times(const char *fname, STRUCT_STAT *stp) #include "case_N.h" #ifdef HAVE_UTIMES - if (do_utimes(fname, stp) == 0) + if (vfs_utimes(fname, stp) == 0) break; #else - if (do_utime(fname, stp) == 0) + if (vfs_utime(fname, stp) == 0) break; #endif @@ -189,7 +186,7 @@ int set_times(const char *fname, STRUCT_STAT *stp) int set_times_at(int dfd, const char *name, STRUCT_STAT *stp) { #if defined HAVE_UTIMENSAT && !defined HAVE_SETATTRLIST - int r = do_utimensat_atfd(dfd, name, stp); + int r = vfs_utimensat_atfd(dfd, name, stp); if (r == 0) return 0; if (errno == ENOSYS) @@ -201,91 +198,6 @@ int set_times_at(int dfd, const char *name, STRUCT_STAT *stp) #endif } -/* Create any necessary directories in fname. Any missing directories are - * created with default permissions. Returns < 0 on error, or the number - * of directories created. */ -int make_path(char *fname, int flags) -{ - char *end, *p; - int ret = 0; - - if (flags & MKP_SKIP_SLASH) { - while (*fname == '/') - fname++; - } - - while (*fname == '.' && fname[1] == '/') - fname += 2; - - if (flags & MKP_DROP_NAME) { - end = strrchr(fname, '/'); - if (!end || end == fname) - return 0; - *end = '\0'; - } else - end = fname + strlen(fname); - - /* Try to find an existing dir, starting from the deepest dir. */ - for (p = end; ; ) { - if (dry_run) { - STRUCT_STAT st; - if (do_stat(fname, &st) == 0) { - if (S_ISDIR(st.st_mode)) - errno = EEXIST; - else - errno = ENOTDIR; - } - } else if (do_mkdir_at(fname, ACCESSPERMS) == 0) { - ret++; - break; - } - - if (errno != ENOENT) { - STRUCT_STAT st; - if (errno != EEXIST || (do_stat(fname, &st) == 0 && !S_ISDIR(st.st_mode))) - ret = -ret - 1; - break; - } - while (1) { - if (p == fname) { - /* We got a relative path that doesn't exist, so assume that '.' - * is there and just break out and create the whole thing. */ - p = NULL; - goto double_break; - } - if (*--p == '/') { - if (p == fname) { - /* We reached the "/" dir, which we assume is there. */ - goto double_break; - } - *p = '\0'; - break; - } - } - } - double_break: - - /* Make all the dirs that we didn't find on the way here. */ - while (p != end) { - if (p) - *p = '/'; - else - p = fname; - p += strlen(p); - if (ret < 0) /* Skip mkdir on error, but keep restoring the path. */ - continue; - if (do_mkdir_at(fname, ACCESSPERMS) < 0) - ret = -ret - 1; - else - ret++; - } - - if (flags & MKP_DROP_NAME) - *end = '/'; - - return ret; -} - /** * Write @p len bytes at @p ptr to descriptor @p desc, retrying if * interrupted. @@ -315,353 +227,6 @@ int full_write(int desc, const char *ptr, size_t len) return total_written; } -/** - * Read @p len bytes at @p ptr from descriptor @p desc, retrying if - * interrupted. - * - * @retval >0 the actual number of bytes read - * - * @retval 0 for EOF - * - * @retval <0 for an error. - * - * Derived from GNU C's cccp.c. */ -static int safe_read(int desc, char *ptr, size_t len) -{ - int n_chars; - - if (len == 0) - return len; - - do { - n_chars = read(desc, ptr, len); - } while (n_chars < 0 && errno == EINTR); - - return n_chars; -} - -/* Remove existing file @dest and reopen, creating a new file with @mode */ -static int unlink_and_reopen(const char *dest, mode_t mode) -{ - int ofd; - - if (robust_unlink(dest) && errno != ENOENT) { - int save_errno = errno; - rsyserr(FERROR_XFER, errno, "unlink %s", full_fname(dest)); - errno = save_errno; - return -1; - } - -#ifdef SUPPORT_XATTRS - if (preserve_xattrs) - mode |= S_IWUSR; -#endif - mode &= INITACCESSPERMS; - /* Use do_open_at so the create/truncate goes through a secure - * parent dirfd in the daemon-no-chroot deployment. Otherwise - * an attacker could swap a parent component with a symlink in - * the window between robust_unlink (which uses do_unlink_at, - * already secure) and the create here, and redirect the new - * file outside the module. */ - if ((ofd = do_open_at(dest, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, mode)) < 0) { - int save_errno = errno; - rsyserr(FERROR_XFER, save_errno, "open %s", full_fname(dest)); - errno = save_errno; - return -1; - } - return ofd; -} - -/* Copy contents of file @source to file @dest with mode @mode. - * - * If @tmpfilefd is < 0, copy_file unlinks @dest and then opens a new - * file with name @dest. - * - * Otherwise, copy_file writes to and closes the provided file - * descriptor. - * - * In either case, if --xattrs are being preserved, the dest file will - * have its xattrs set from the source file. - * - * This is used in conjunction with the --temp-dir, --backup, and - * --copy-dest options. */ -int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode) -{ - int ifd, ofd; - char buf[1024 * 8]; - int len; /* Number of bytes read into `buf'. */ - OFF_T prealloc_len = 0, offset = 0; - - /* For any hardened (non-chrooted) receiver, route the source open through - * secure_relative_open so a parent-symlink on the source path (e.g. - * --copy-dest=cd where cd is a symlink to an outside directory) cannot - * redirect the read to a file the attacker should not see. Plain - * do_open_nofollow only refuses a final-component symlink; parents are - * still followed. An ABSOLUTE source is an operator basis (e.g. an absolute - * --copy-dest): confine its parents via the ownership walk -- a foreign-owned - * parent symlink is refused, the operator's own dirs/uid0/euid symlinks - * followed -- so a flipped parent can't redirect the basis read out of tree. - * operator_path_resolve is set only across the walk (so module-exclude is - * enforced) and restored, leaving the caller's value for the dest side -- this - * is why confining the source here does not re-open the copy_xattrs dest - * race the way wrapping the whole copy_altdest_file would. */ - if (secure_relpath_active() && source && *source && source[0] != '/') - ifd = secure_relative_open(NULL, source, O_RDONLY | O_NOFOLLOW, 0); -#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY - else if (secure_relpath_active() && source && source[0] == '/' - && !symlink_optout_allowed()) { - int save = operator_path_resolve, dfd, e; - const char *leaf; - operator_path_resolve = 1; - dfd = owner_walk_parent(source, &leaf); - operator_path_resolve = save; - if (dfd < 0) - ifd = -1; - else { - ifd = openat(dfd, leaf, O_RDONLY | O_NOFOLLOW); - e = errno; - close(dfd); - errno = e; - } - } -#endif - else - ifd = do_open_nofollow(source, O_RDONLY); - if (ifd < 0) { - int save_errno = errno; - rsyserr(FERROR_XFER, errno, "open %s", full_fname(source)); - errno = save_errno; - return -1; - } - - if (tmpfilefd >= 0) { - ofd = tmpfilefd; - } else { - ofd = unlink_and_reopen(dest, mode); - if (ofd < 0) { - int save_errno = errno; - close(ifd); - errno = save_errno; - return -1; - } - } - -#ifdef SUPPORT_PREALLOCATION - if (preallocate_files) { - STRUCT_STAT srcst; - - /* Try to preallocate enough space for file's eventual length. Can - * reduce fragmentation on filesystems like ext4, xfs, and NTFS. */ - if (do_fstat(ifd, &srcst) < 0) - rsyserr(FWARNING, errno, "fstat %s", full_fname(source)); - else if (srcst.st_size > 0) { - prealloc_len = do_fallocate(ofd, 0, srcst.st_size); - if (prealloc_len < 0) - rsyserr(FWARNING, errno, "do_fallocate %s", full_fname(dest)); - } - } -#endif - - while ((len = safe_read(ifd, buf, sizeof buf)) > 0) { - if (full_write(ofd, buf, len) < 0) { - int save_errno = errno; - rsyserr(FERROR_XFER, errno, "write %s", full_fname(dest)); - close(ifd); - close(ofd); - errno = save_errno; - return -1; - } - offset += len; - } - - if (len < 0) { - int save_errno = errno; - rsyserr(FERROR_XFER, errno, "read %s", full_fname(source)); - close(ifd); - close(ofd); - errno = save_errno; - return -1; - } - - /* Source file might have shrunk since we fstatted it. - * Cut off any extra preallocated zeros from dest file. */ - if (offset < prealloc_len) { -#ifdef HAVE_FTRUNCATE - /* If we fail to truncate, the dest file may be wrong, so we - * must trigger the "partial transfer" error. */ - if (do_ftruncate(ofd, offset) < 0) - rsyserr(FERROR_XFER, errno, "ftruncate %s", full_fname(dest)); -#else - rprintf(FERROR_XFER, "no ftruncate for over-long pre-alloc: %s", full_fname(dest)); -#endif - } - - if (do_fsync && fsync(ofd) < 0) { - int save_errno = errno; - rsyserr(FERROR, errno, "fsync failed on %s", full_fname(dest)); - close(ofd); - close(ifd); /* ifd is held open until after the xattr copy below */ - errno = save_errno; - return -1; - } - -#ifdef SUPPORT_XATTRS - /* Read the source xattrs through the held source fd (ifd) and set them - * through ofd while both are still held, so a parent-symlink race can't - * redirect the read out of tree or the write onto a file outside it. */ - if (preserve_xattrs) - copy_xattrs(source, ifd, dest, ofd); -#endif - - if (close(ifd) < 0) { - rsyserr(FWARNING, errno, "close failed on %s", - full_fname(source)); - } - - if (close(ofd) < 0) { - int save_errno = errno; - rsyserr(FERROR_XFER, errno, "close failed on %s", full_fname(dest)); - errno = save_errno; - return -1; - } - - return 0; -} - -/* MAX_RENAMES should be 10**MAX_RENAMES_DIGITS */ -#define MAX_RENAMES_DIGITS 3 -#define MAX_RENAMES 1000 - -/** - * Robust unlink: some OS'es (HPUX) refuse to unlink busy files, so - * rename to /.rsyncNNN instead. - * - * Note that successive rsync runs will shuffle the filenames around a - * bit as long as the file is still busy; this is because this function - * does not know if the unlink call is due to a new file coming in, or - * --delete trying to remove old .rsyncNNN files, hence it renames it - * each time. - **/ -int robust_unlink(const char *fname) -{ -#ifndef ETXTBSY - return do_unlink_at(fname); -#else - static int counter = 1; - int rc, pos, start; - char path[MAXPATHLEN]; - - rc = do_unlink_at(fname); - if (rc == 0 || errno != ETXTBSY) - return rc; - - if ((pos = strlcpy(path, fname, MAXPATHLEN)) >= MAXPATHLEN) - pos = MAXPATHLEN - 1; - - while (pos > 0 && path[pos-1] != '/') - pos--; - pos += strlcpy(path+pos, ".rsync", MAXPATHLEN-pos); - - if (pos > (MAXPATHLEN-MAX_RENAMES_DIGITS-1)) { - errno = ETXTBSY; - return -1; - } - - /* start where the last one left off to reduce chance of clashes */ - start = counter; - do { - snprintf(&path[pos], MAX_RENAMES_DIGITS+1, "%03d", counter); - if (++counter >= MAX_RENAMES) - counter = 1; - } while (access(path, 0) == 0 && counter != start); - - if (INFO_GTE(MISC, 1)) { - rprintf(FWARNING, "renaming %s to %s because of text busy\n", - fname, path); - } - - /* maybe we should return rename()'s exit status? Nah. */ - if (do_rename_at(fname, path) != 0) { - errno = ETXTBSY; - return -1; - } - return 0; -#endif -} - -/* Returns 0 on successful rename, 1 if we successfully copied the file - * across filesystems, -2 if copy_file() failed, and -1 on other errors. - * If partialptr is not NULL and we need to do a copy, copy the file into - * the active partial-dir instead of over the destination file. */ -int robust_rename(const char *from, const char *to, const char *partialptr, - int mode, struct file_struct *file) -{ - int tries = 4; - - /* A resumed in-place partial-dir transfer might call us with from and - * to pointing to the same buf if the transfer failed yet again. */ - if (from == to) - return 0; - - while (tries--) { - /* tmp -> final usually live in the entry's own dir: rename via the - * held dir fd when both do, else the full-path wrapper. */ - int ofd = held_dfd_for(from, file); - int nfd = held_dfd_for(to, file); - int rr; - if (ofd >= 0 && nfd >= 0) { - const char *os = strrchr(from, '/'); - const char *ns = strrchr(to, '/'); - rr = do_rename_atfd(ofd, os ? os + 1 : from, nfd, ns ? ns + 1 : to); - } else - rr = do_rename_at(from, to); - if (rr == 0) - return 0; - - switch (errno) { -#ifdef ETXTBSY - case ETXTBSY: - if (robust_unlink(to) != 0) { - errno = ETXTBSY; - return -1; - } - errno = ETXTBSY; - break; -#endif - case EXDEV: { - int save = operator_path_resolve, rc; - if (partialptr) { - if (!handle_partial_dir(partialptr,PDIR_CREATE)) - return -2; - to = partialptr; - } - /* Cross-fs fallback: copy then unlink. An absolute --temp-dir - * source / --partial-dir dest is an operator path whose parents - * do_open_at()/do_unlink_at() would otherwise follow via plain libc - * -- confine them through the ownership walk so a raced parent - * symlink can't redirect the dest-write or the source-unlink out of - * the module. copy_file already confines the source READ; a - * relative in-module path stays on the secure_relative_open arm, so - * only flip the flag for an absolute (operator) path. */ - if (*to == '/') - operator_path_resolve = 1; - rc = copy_file(from, to, -1, mode); - operator_path_resolve = save; - if (rc != 0) - return -2; - if (*from == '/') - operator_path_resolve = 1; - do_unlink_at(from); - operator_path_resolve = save; - return 1; - } - default: - return -1; - } - } - return -1; -} - static pid_t all_pids[10]; static int num_pids; @@ -801,7 +366,7 @@ static inline void call_glob_match(const char *name, int len, int from_glob, STRUCT_STAT st; int is_dir; - if (do_stat(glob.arg_buf, &st) != 0) + if (vfs_stat(VFS_AT_FDCWD, glob.arg_buf, &st, VFS_ALLOW_SYMLINK) != 0) return; is_dir = S_ISDIR(st.st_mode) != 0; if (arg && !is_dir) @@ -1211,7 +776,7 @@ char *sanitize_path(char *dest, const char *p, const char *rootdir, int depth, i } /* Like chdir(), but it keeps track of the current directory (in the - * global "curr_dir"), and ensures that the path size doesn't overflow. + * global "vfs.curr_dir"), and ensures that the path size doesn't overflow. * Also cleans the path using the clean_fname() function. */ int change_dir(const char *dir, int set_path_only) { @@ -1221,11 +786,11 @@ int change_dir(const char *dir, int set_path_only) if (!initialised) { initialised = 1; - if (getcwd(curr_dir, sizeof curr_dir - 1) == NULL) { + if (getcwd(vfs.curr_dir, sizeof vfs.curr_dir - 1) == NULL) { rsyserr(FERROR, errno, "getcwd()"); exit_cleanup(RERR_FILESELECT); } - curr_dir_len = strlen(curr_dir); + vfs.curr_dir_len = strlen(vfs.curr_dir); } if (!dir) /* this call was probably just to initialize */ @@ -1236,13 +801,13 @@ int change_dir(const char *dir, int set_path_only) return 1; if (*dir == '/') { - if (len >= sizeof curr_dir) { + if (len >= sizeof vfs.curr_dir) { errno = ENAMETOOLONG; return 0; } if (!set_path_only) { /* The destination is operator-supplied (like --log-file et al.), so - * resolve it with open_no_attacker_symlinks: walk each component + * resolve it with vfs_open_owner_walk: walk each component * refusing a symlink not owned by uid 0 or our euid, then fchdir to * the result. This still follows the operator's/root's own symlinked * dest -- the `/backup -> /mnt/disk` / `/var/www -> /srv/www` admin @@ -1252,7 +817,7 @@ int change_dir(const char *dir, int set_path_only) * non-daemon receiver can opt back into the legacy plain chdir with * --insecure-links. */ if (am_daemon && !am_chrooted) { - int dfd = open_no_attacker_symlinks(dir, O_RDONLY | O_DIRECTORY, 0); + int dfd = vfs_open_owner_walk(dir, O_RDONLY | O_DIRECTORY, 0, 0); if (dfd < 0) return 0; if (fchdir(dfd) != 0) { @@ -1283,7 +848,7 @@ int change_dir(const char *dir, int set_path_only) * another uid. A real dir is opened directly. This closes the * destination chdir TOCTOU; --insecure-links keeps the plain * chdir for an operator whose dest is a foreign-owned symlink. */ - dfd = open_no_attacker_symlinks(nf, O_RDONLY | O_DIRECTORY, 0); + dfd = vfs_open_owner_walk(nf, O_RDONLY | O_DIRECTORY, 0, 0); if (dfd < 0) return 0; if (fchdir(dfd) != 0) { @@ -1299,16 +864,16 @@ int change_dir(const char *dir, int set_path_only) } } skipped_chdir = set_path_only; - memcpy(curr_dir, dir, len + 1); + memcpy(vfs.curr_dir, dir, len + 1); } else { - unsigned int save_dir_len = curr_dir_len; - if (curr_dir_len + 1 + len >= sizeof curr_dir) { + unsigned int save_dir_len = vfs.curr_dir_len; + if (vfs.curr_dir_len + 1 + len >= sizeof vfs.curr_dir) { errno = ENAMETOOLONG; return 0; } - if (!(curr_dir_len && curr_dir[curr_dir_len-1] == '/')) - curr_dir[curr_dir_len++] = '/'; - memcpy(curr_dir + curr_dir_len, dir, len + 1); + if (!(vfs.curr_dir_len && vfs.curr_dir[vfs.curr_dir_len-1] == '/')) + vfs.curr_dir[vfs.curr_dir_len++] = '/'; + memcpy(vfs.curr_dir + vfs.curr_dir_len, dir, len + 1); if (!set_path_only) { int chdir_failed; @@ -1317,20 +882,20 @@ int change_dir(const char *dir, int set_path_only) * target -- otherwise CWD escapes the module and * every subsequent path-relative syscall (open, * chmod, lchown, ...) inherits the escape, which - * defeats secure_relative_open's RESOLVE_BENEATH + * defeats vfs_resolve_open's RESOLVE_BENEATH * anchor and re-opens the CVE-2026-29518 class of * symlink TOCTOU attacks. Use the secure resolver * to get a confined dirfd, then fchdir() to it. * * If skipped_chdir is set, a previous CD_SKIP_CHDIR - * call buffered an absolute prefix in curr_dir + * call buffered an absolute prefix in vfs.curr_dir * (e.g. change_pathname's CD_SKIP_CHDIR to orig_dir) * without syncing the kernel's CWD. Resolve `dir` * relative to that prefix as basedir so the secure * branch still anchors at the operator-trusted * directory rather than wherever the kernel CWD * happens to be. */ - if (am_daemon && (!am_chrooted || module_dirlen) && !symlink_optout_allowed()) { + if (am_daemon && (!am_chrooted || module_dirlen) && !vfs_symlink_optout_allowed()) { const char *basedir = NULL; char prefix[MAXPATHLEN]; int dfd; @@ -1340,11 +905,11 @@ int change_dir(const char *dir, int set_path_only) chdir_failed = 1; goto chdir_cleanup; } - memcpy(prefix, curr_dir, save_dir_len); + memcpy(prefix, vfs.curr_dir, save_dir_len); prefix[save_dir_len] = '\0'; basedir = prefix; } - dfd = secure_relative_open(basedir, dir, + dfd = vfs_resolve_open(basedir, dir, O_RDONLY | O_DIRECTORY, 0); if (dfd < 0) { chdir_failed = 1; @@ -1352,19 +917,19 @@ int change_dir(const char *dir, int set_path_only) chdir_failed = fchdir(dfd) != 0; close(dfd); } - } else if (am_daemon && symlink_optout_allowed()) { + } else if (am_daemon && vfs_symlink_optout_allowed()) { /* "insecure links = yes": restore the 3.2.7 follow-any-symlink * traversal with a plain chdir to the accumulated path, the same * legacy behaviour the per-operation sites grant under the opt-out. */ - chdir_failed = chdir(curr_dir) != 0; + chdir_failed = chdir(vfs.curr_dir) != 0; } else if (!am_chrooted && !am_sender && !insecure_links) { /* Non-daemon receiver: confine the operator-named relative * destination like the absolute case above -- refuse a component * symlink not owned by uid 0 or our euid, closing the * relative-dest chdir TOCTOU while still following the operator's * own symlinks. --insecure-links keeps the plain chdir. */ - int dfd = open_no_attacker_symlinks(curr_dir, - O_RDONLY | O_DIRECTORY, 0); + int dfd = vfs_open_owner_walk(vfs.curr_dir, + O_RDONLY | O_DIRECTORY, 0, 0); if (dfd < 0) chdir_failed = 1; else { @@ -1372,30 +937,30 @@ int change_dir(const char *dir, int set_path_only) close(dfd); } } else { - chdir_failed = chdir(curr_dir) != 0; + chdir_failed = chdir(vfs.curr_dir) != 0; } chdir_cleanup: if (chdir_failed) { - curr_dir_len = save_dir_len; - curr_dir[curr_dir_len] = '\0'; + vfs.curr_dir_len = save_dir_len; + vfs.curr_dir[vfs.curr_dir_len] = '\0'; return 0; } } skipped_chdir = set_path_only; } - curr_dir_len = clean_fname(curr_dir, CFN_COLLAPSE_DOT_DOT_DIRS | CFN_DROP_TRAILING_DOT_DIR); + vfs.curr_dir_len = clean_fname(vfs.curr_dir, CFN_COLLAPSE_DOT_DOT_DIRS | CFN_DROP_TRAILING_DOT_DIR); if (sanitize_paths) { - if (module_dirlen > curr_dir_len) - module_dirlen = curr_dir_len; - curr_dir_depth = count_dir_elements(curr_dir + module_dirlen); + if (module_dirlen > vfs.curr_dir_len) + module_dirlen = vfs.curr_dir_len; + curr_dir_depth = count_dir_elements(vfs.curr_dir + module_dirlen); } if (!set_path_only) /* a real chdir invalidates the cwd-relative dir-fd stack */ - reset_dir_fd_cache(); + vfs_dircache_reset(); if (DEBUG_GTE(CHDIR, 1) && !set_path_only) - rprintf(FINFO, "[%s] change_dir(%s)\n", who_am_i(), curr_dir); + rprintf(FINFO, "[%s] change_dir(%s)\n", who_am_i(), vfs.curr_dir); return 1; } @@ -1408,12 +973,12 @@ char *normalize_path(char *path, BOOL force_newbuf, unsigned int *len_ptr) if (*path != '/') { /* Make path absolute. */ int len = strlen(path); - if (curr_dir_len + 1 + len >= sizeof curr_dir) + if (vfs.curr_dir_len + 1 + len >= sizeof vfs.curr_dir) return NULL; - curr_dir[curr_dir_len] = '/'; - memcpy(curr_dir + curr_dir_len + 1, path, len + 1); - path = strdup(curr_dir); - curr_dir[curr_dir_len] = '\0'; + vfs.curr_dir[vfs.curr_dir_len] = '/'; + memcpy(vfs.curr_dir + vfs.curr_dir_len + 1, path, len + 1); + path = strdup(vfs.curr_dir); + vfs.curr_dir[vfs.curr_dir_len] = '\0'; } else if (force_newbuf) path = strdup(path); @@ -1445,7 +1010,7 @@ char *full_fname(const char *fn) if (*fn == '/') p1 = p2 = ""; else { - p1 = curr_dir + module_dirlen; + p1 = vfs.curr_dir + module_dirlen; for (p2 = p1; *p2 == '/'; p2++) {} if (*p2) p2 = "/"; @@ -1515,26 +1080,22 @@ int handle_partial_dir(const char *fname, int create) * outside the tree): resolve it with the ownership walk -- follow a * uid0/euid-owned symlink, refuse a foreign one, absolute and relative alike. * --insecure-links (or a daemon module's "insecure links =") opts out. */ - operator_path_resolve = 1; if (create) { STRUCT_STAT st; - int statret = do_lstat_at(dir, &st); + int statret = vfs_lstat(VFS_AT_FDCWD, dir, &st, VFS_OPERATOR_PATH); if (statret == 0 && !S_ISDIR(st.st_mode)) { - if (do_unlink_at(dir) < 0) { - operator_path_resolve = 0; + if (vfs_unlink(VFS_AT_FDCWD, dir, VFS_OPERATOR_PATH) < 0) { *fn = '/'; return 0; } statret = -1; } - if (statret < 0 && do_mkdir_at(dir, 0700) < 0) { - operator_path_resolve = 0; + if (statret < 0 && vfs_mkdir(VFS_AT_FDCWD, dir, 0700, VFS_OPERATOR_PATH) < 0) { *fn = '/'; return 0; } } else - do_rmdir_at(dir); - operator_path_resolve = 0; + vfs_unlink(VFS_AT_FDCWD, dir, VFS_REMOVEDIR | VFS_OPERATOR_PATH); *fn = '/'; return 1; diff --git a/vfs/chmod.c b/vfs/chmod.c new file mode 100644 index 000000000..d7c717cc4 --- /dev/null +++ b/vfs/chmod.c @@ -0,0 +1,347 @@ +/* + * 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 /* for the macOS setattrlist() chmod path */ +#endif +#ifdef __linux__ +#include /* 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 diff --git a/vfs/chown.c b/vfs/chown.c new file mode 100644 index 000000000..34a3c4ae4 --- /dev/null +++ b/vfs/chown.c @@ -0,0 +1,126 @@ +/* + * vfs/chown.c - lchown wrappers (path, parent-resolved, held-dirfd). + * + * 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" + +#ifndef HAVE_LCHOWN +#define lchown chown +#endif + +static int vfs__lchown_plain(const char *path, uid_t owner, gid_t group) +{ + return lchown(path, owner, group); +} + +/* Secure receiver-side resolve: open the parent under vfs_resolve_open() and + * fchownat(..., AT_SYMLINK_NOFOLLOW) so a parent-component symlink swap can't + * redirect the chown outside the module. VFS_OPERATOR_PATH takes the ownership + * walk instead, as the other VFS wrappers do. Falls through to the plain + * lchown in non-daemon/sender, chrooted, no-parent and absolute-path cases. */ +static int vfs__lchown_secure(const char *path, uid_t owner, gid_t group, int flags) +{ +#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY && defined AT_SYMLINK_NOFOLLOW + char dirpath[MAXPATHLEN]; + const char *bname, *slash; + int dfd, ret, e; + size_t dlen; + + /* Operator-supplied path: without this branch the caller's + * VFS_OPERATOR_PATH has no effect here and an absolute name would fall + * straight through to the unconfined full-path lchown. */ + if ((flags & VFS_OPERATOR_PATH) && path && *path) { + if (vfs_symlink_optout_allowed()) + return vfs__lchown_plain(path, owner, group); + dfd = vfs_owner_walk_parent(path, &bname, 1); + if (dfd < 0) + return -1; + ret = fchownat(dfd, bname, owner, group, AT_SYMLINK_NOFOLLOW); + e = errno; + close(dfd); + errno = e; + return ret; + } + + if (!vfs_relpath_active() || !*path || *path == '/') + return vfs__lchown_plain(path, owner, group); + slash = strrchr(path, '/'); + if (!slash) + return vfs__lchown_plain(path, owner, group); + dlen = slash - path; + if (dlen >= sizeof dirpath) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dirpath, path, dlen); + dirpath[dlen] = '\0'; + bname = slash + 1; + + dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + if (dfd < 0) + return -1; + ret = fchownat(dfd, bname, owner, group, AT_SYMLINK_NOFOLLOW); + e = errno; + close(dfd); + errno = e; + return ret; +#else + (void)flags; + return vfs__lchown_plain(path, owner, group); +#endif +} + +/* Unified lchown. dirfd == VFS_AT_FDCWD resolves `path`; a real held dirfd + * makes `path` a single component chowned (no-follow) under it. flags: + * VFS_ALLOW_SYMLINK (trusted, plain lchown), default 0 (secure receiver + * resolve). */ +int vfs_lchown(int dirfd, const char *path, uid_t owner, gid_t group, int flags) +{ + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + RETURN_ERROR_IF_NULL(path); + + if (dirfd != VFS_AT_FDCWD) { +#if defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW + /* Held-fd: reject empty, multi-component and ".." (writing the + * parent of the pinned dir); "." (chown 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 fchownat(dirfd, path, owner, group, AT_SYMLINK_NOFOLLOW); +#else + (void)dirfd; (void)owner; (void)group; + errno = ENOSYS; + return -1; +#endif + } + + if (flags & VFS_ALLOW_SYMLINK) + return vfs__lchown_plain(path, owner, group); + return vfs__lchown_secure(path, owner, group, flags); +} + +/* Mode/owner on an already-open fd (no path, no symlink to follow): the + * race-free way to set metadata on a cross-tree operator-path leaf that was + * pinned with O_NOFOLLOW. See set_file_attrs(). */ +int vfs_fchown(int fd, uid_t owner, gid_t group) +{ + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + return fchown(fd, owner, group); +} diff --git a/vfs/copy_file.c b/vfs/copy_file.c new file mode 100644 index 000000000..dca5db023 --- /dev/null +++ b/vfs/copy_file.c @@ -0,0 +1,228 @@ +/* + * vfs/copy_file.c - compound VFS op: copy a file's contents (and, for + * --xattrs, its xattrs) to a new destination. Layered on the vfs_* open/ + * read/write primitives; calls out to the metadata layer (copy_xattrs) for + * the held-fd xattr copy. + * + * Moved out of util1.c as part of the VFS compound layer. + * + * Copyright (C) 1996-2022 Andrew Tridgell, Paul Mackerras, 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 do_fsync; +extern int preallocate_files; +extern int preserve_xattrs; + +/* Read @p len bytes at @p ptr from descriptor @p desc, retrying if interrupted. + * Returns the number of bytes read (0 = EOF), or <0 on error. */ +static int safe_read(int desc, char *ptr, size_t len) +{ + int n_chars; + + if (len == 0) + return len; + + do { + n_chars = read(desc, ptr, len); + } while (n_chars < 0 && errno == EINTR); + + return n_chars; +} + +/* Remove existing file @dest and reopen, creating a new file with @mode. + * vfs_flags carries the resolution policy (VFS_OPERATOR_PATH for an operator + * dest) to both the robust_unlink and the create. */ +static int unlink_and_reopen(const char *dest, mode_t mode, int vfs_flags) +{ + int ofd; + + if (robust_unlink(dest, vfs_flags) && errno != ENOENT) { + int save_errno = errno; + rsyserr(FERROR_XFER, errno, "unlink %s", full_fname(dest)); + errno = save_errno; + return -1; + } + +#ifdef SUPPORT_XATTRS + if (preserve_xattrs) + mode |= S_IWUSR; +#endif + mode &= INITACCESSPERMS; + /* Use vfs_open_at so the create/truncate goes through a secure + * parent dirfd in the daemon-no-chroot deployment. Otherwise + * an attacker could swap a parent component with a symlink in + * the window between robust_unlink (which uses vfs_unlink, + * already secure) and the create here, and redirect the new + * file outside the module. */ + if ((ofd = vfs_open_at(dest, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, mode, vfs_flags)) < 0) { + int save_errno = errno; + rsyserr(FERROR_XFER, save_errno, "open %s", full_fname(dest)); + errno = save_errno; + return -1; + } + return ofd; +} + +/* Copy contents of file @source to file @dest with mode @mode. + * + * If @tmpfilefd is < 0, copy_file unlinks @dest and then opens a new + * file with name @dest. + * + * Otherwise, copy_file writes to and closes the provided file + * descriptor. + * + * In either case, if --xattrs are being preserved, the dest file will + * have its xattrs set from the source file. + * + * This is used in conjunction with the --temp-dir, --backup, and + * --copy-dest options. */ +int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode, int vfs_flags) +{ + int ifd, ofd; + char buf[1024 * 8]; + int len; /* Number of bytes read into `buf'. */ + OFF_T prealloc_len = 0, offset = 0; + + /* For any hardened (non-chrooted) receiver, route the source open through + * vfs_resolve_open so a parent-symlink on the source path (e.g. + * --copy-dest=cd where cd is a symlink to an outside directory) cannot + * redirect the read to a file the attacker should not see. Plain + * vfs_open_nofollow only refuses a final-component symlink; parents are + * still followed. An ABSOLUTE source is an operator basis (e.g. an absolute + * --copy-dest): confine its parents via the ownership walk -- a foreign-owned + * parent symlink is refused, the operator's own dirs/uid0/euid symlinks + * followed -- so a flipped parent can't redirect the basis read out of tree. + * The walk runs with is_operator=1 (module-exclude enforced) and pins only + * the source side, leaving the dest open untouched -- this is why confining + * the source here does not re-open the copy_xattrs dest race the way wrapping + * the whole copy_altdest_file would. */ + if (vfs_relpath_active() && source && *source && source[0] != '/') + ifd = vfs_resolve_open(NULL, source, O_RDONLY | O_NOFOLLOW, 0); +#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY + else if (vfs_relpath_active() && source && source[0] == '/' + && !vfs_symlink_optout_allowed()) { + int dfd, e; + const char *leaf; + dfd = vfs_owner_walk_parent(source, &leaf, 1); + if (dfd < 0) + ifd = -1; + else { + ifd = openat(dfd, leaf, O_RDONLY | O_NOFOLLOW); + e = errno; + close(dfd); + errno = e; + } + } +#endif + else + ifd = vfs_open_nofollow(source, O_RDONLY); + if (ifd < 0) { + int save_errno = errno; + rsyserr(FERROR_XFER, errno, "open %s", full_fname(source)); + errno = save_errno; + return -1; + } + + if (tmpfilefd >= 0) { + ofd = tmpfilefd; + } else { + ofd = unlink_and_reopen(dest, mode, vfs_flags); + if (ofd < 0) { + int save_errno = errno; + close(ifd); + errno = save_errno; + return -1; + } + } + +#ifdef SUPPORT_PREALLOCATION + if (preallocate_files) { + STRUCT_STAT srcst; + + /* Try to preallocate enough space for file's eventual length. Can + * reduce fragmentation on filesystems like ext4, xfs, and NTFS. */ + if (vfs_fstat(ifd, &srcst) < 0) + rsyserr(FWARNING, errno, "fstat %s", full_fname(source)); + else if (srcst.st_size > 0) { + prealloc_len = vfs_fallocate(ofd, 0, srcst.st_size); + if (prealloc_len < 0) + rsyserr(FWARNING, errno, "vfs_fallocate %s", full_fname(dest)); + } + } +#endif + + while ((len = safe_read(ifd, buf, sizeof buf)) > 0) { + if (full_write(ofd, buf, len) < 0) { + int save_errno = errno; + rsyserr(FERROR_XFER, errno, "write %s", full_fname(dest)); + close(ifd); + close(ofd); + errno = save_errno; + return -1; + } + offset += len; + } + + if (len < 0) { + int save_errno = errno; + rsyserr(FERROR_XFER, errno, "read %s", full_fname(source)); + close(ifd); + close(ofd); + errno = save_errno; + return -1; + } + + /* Source file might have shrunk since we fstatted it. + * Cut off any extra preallocated zeros from dest file. */ + if (offset < prealloc_len) { +#ifdef HAVE_FTRUNCATE + /* If we fail to truncate, the dest file may be wrong, so we + * must trigger the "partial transfer" error. */ + if (vfs_ftruncate(ofd, offset) < 0) + rsyserr(FERROR_XFER, errno, "ftruncate %s", full_fname(dest)); +#else + rprintf(FERROR_XFER, "no ftruncate for over-long pre-alloc: %s", full_fname(dest)); +#endif + } + + if (do_fsync && fsync(ofd) < 0) { + int save_errno = errno; + rsyserr(FERROR, errno, "fsync failed on %s", full_fname(dest)); + close(ofd); + close(ifd); /* ifd is held open until after the xattr copy below */ + errno = save_errno; + return -1; + } + +#ifdef SUPPORT_XATTRS + /* Read the source xattrs through the held source fd (ifd) and set them + * through ofd while both are still held, so a parent-symlink race can't + * redirect the read out of tree or the write onto a file outside it. */ + if (preserve_xattrs) + copy_xattrs(source, ifd, dest, ofd); +#endif + + if (close(ifd) < 0) { + rsyserr(FWARNING, errno, "close failed on %s", + full_fname(source)); + } + + if (close(ofd) < 0) { + int save_errno = errno; + rsyserr(FERROR_XFER, errno, "close failed on %s", full_fname(dest)); + errno = save_errno; + return -1; + } + + return 0; +} diff --git a/vfs/dircache.c b/vfs/dircache.c new file mode 100644 index 000000000..125388898 --- /dev/null +++ b/vfs/dircache.c @@ -0,0 +1,244 @@ +/* + * vfs/dircache.c - persistent ancestor-dirfd cache for held-directory traversal. + * + * The file list is path-sorted, so consecutive directory resolutions share a + * long leading prefix. Rather than re-resolve a full path from the anchor per + * file, we keep the whole current ancestor chain open as pinned, race-safe + * dirfds and reuse the longest common component prefix on the next resolution. + * vfs_opendir() hands out a held dirfd (or -1 to fall back); vfs_dircache_reset() + * drops the chain (called by change_dir() on a real chdir). 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" + +/* Held-directory-fd traversal. + * + * Rather than re-resolve a full path on every syscall (do_*_at() re-opens the + * parent via vfs_resolve_open() each call), the generator and receiver + * open each directory ONCE via vfs_opendir() and issue single-component + * *at() ops against that held dirfd with the do_*_atfd() wrappers below. The + * parent is a pinned fd, not re-resolved, so the per-entry symlink-race window + * is closed and the re-resolution overhead is gone. + * + * vfs_opendir() owns both the authority gate and the resolver choice: it + * returns a held dirfd only when hardened resolution is in effect, else -1 + * with errno==0 so the caller falls back to the do_*_at() wrappers + * (behaviour-neutral). The do_*_atfd() wrappers are thin shims with the same + * leaf semantics as do_*_at() (dry-run/read-only guards, AT_SYMLINK_NOFOLLOW, + * fake-super placeholder files); they never re-check the gate or re-resolve a + * parent. */ + +int vfs_opendir(const char *dirname) +{ +#ifdef AT_FDCWD + int dfd; + + /* Authority gate, identical to the do_*_at() wrappers. When hardened + * resolution isn't in effect, return -1 with errno cleared so the caller + * uses the full-path wrappers. */ + if (!vfs_relpath_active()) { + errno = 0; + return -1; + } + + if (!dirname || !*dirname) { + /* The transfer root itself (file->dirname == NULL): the cwd. */ + dfd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY); + } else if (dirname[0] == '/') { + /* An absolute dirname is not expected for an in-transfer entry; + * leave it to the legacy path. */ + errno = 0; + return -1; + } else { + dfd = vfs_resolve_open(NULL, dirname, O_RDONLY | O_DIRECTORY, 0); + } + + if (dfd >= 0) { + /* O_CLOEXEC on every tier (the per-component walk fallback + * doesn't thread our flags onto the returned dirfd). */ + int fl = fcntl(dfd, F_GETFD); + if (fl >= 0) + fcntl(dfd, F_SETFD, fl | FD_CLOEXEC); + } + return dfd; +#else + (void)dirname; + errno = 0; + return -1; +#endif +} + +/* Persistent ancestor-dirfd stack for held-directory traversal. + * + * The transfer's file list is path-sorted, so iterating it walks the tree in + * DFS order and consecutive directory resolutions share a long leading prefix. + * Rather than re-resolve a full path from the anchor each time (re-opening + * every ancestor dir per file), we keep the whole current ancestor chain open + * as pinned, race-safe dirfds and, on the next resolution, reuse the longest + * common component prefix -- popping only the divergent tail and descending the + * new tail. Each directory is then opened once while we are inside its subtree. + * + * The chain is relative to the process cwd (for a NULL anchor), so change_dir() + * drops it on any real chdir; it otherwise persists across flist chunks (the + * pinned fds stay valid, and a raced/replaced ancestor resolves to the original + * inode the fd holds -- the held-dirfd race-safety property, not a hazard). + * Each component is resolved with ds_descend(), which follows in-tree directory + * symlinks exactly as vfs_resolve_open() does; only the resolved dir fd is + * kept (intermediate symlink-target fds are closed -- sound, since an open + * dirfd needs no live parent). */ +#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY + +void vfs_dircache_reset(void) +{ + while (vfs.dpc.depth > 0) + close(vfs.dpc.fd[--vfs.dpc.depth]); + if (vfs.dpc.base >= 0) + close(vfs.dpc.base); + vfs.dpc.base = -1; + vfs.dpc.anchor = VFS_DPC_ANCHOR_NONE; +} + +/* Resolve directory `dirpath` beneath `anchor` (NULL = cwd, else an absolute + * trusted root), reusing the held ancestor stack. Returns a BORROWED dirfd + * owned by the cache (do NOT close), or -1 (errno preserved for a real open + * error, errno==0 for an uncacheable path -- "..", too deep/long, or a relative + * non-cwd anchor) so the caller can fall back to vfs_resolve_open(). */ +static int dpc_dir_fd(const char *anchor, const char *dirpath) +{ + char copy[MAXPATHLEN]; + char *comps[VFS_DPC_MAXDEPTH]; + char *sv = NULL; + int nc = 0, p, i; + + if (anchor && anchor[0] != '/') { errno = 0; return -1; } + if (!dirpath) + dirpath = ""; + if (dirpath[0] == '/') { errno = 0; return -1; } + + if (anchor != vfs.dpc.anchor || vfs.dpc.base < 0) { + int fl; + vfs_dircache_reset(); + vfs.dpc.base = open_anchor_dirfd(anchor ? anchor : "."); + if (vfs.dpc.base < 0) + return -1; + if ((fl = fcntl(vfs.dpc.base, F_GETFD)) >= 0) + fcntl(vfs.dpc.base, F_SETFD, fl | FD_CLOEXEC); + vfs.dpc.anchor = anchor; + } + + if (strlcpy(copy, dirpath, sizeof copy) >= sizeof copy) { errno = ENAMETOOLONG; return -1; } + for (char *c = strtok_r(copy, "/", &sv); c; c = strtok_r(NULL, "/", &sv)) { + if (c[0] == '.' && c[1] == '\0') + continue; /* "." */ + if (c[0] == '.' && c[1] == '.' && c[2] == '\0') { errno = 0; return -1; } + if (nc >= VFS_DPC_MAXDEPTH || strlen(c) >= sizeof vfs.dpc.name[0]) { + /* Too deep / a too-long component to cache. Release the held + * ancestor fds first so the caller's full-path fallback walk does + * not stack on top of them: a deep tree plus a low RLIMIT_NOFILE + * (e.g. OpenBSD's default 128) would otherwise exhaust descriptors + * (cache depth + walk depth). */ + vfs_dircache_reset(); + errno = 0; + return -1; + } + comps[nc++] = c; + } + + /* Reuse the longest common prefix; drop the divergent tail. */ + for (p = 0; p < vfs.dpc.depth && p < nc && strcmp(vfs.dpc.name[p], comps[p]) == 0; p++) + ; + while (vfs.dpc.depth > p) + close(vfs.dpc.fd[--vfs.dpc.depth]); + + /* Descend the new tail, holding each resolved component. */ + for (i = p; i < nc; i++) { + int afd = vfs.dpc.depth > 0 ? vfs.dpc.fd[vfs.dpc.depth-1] : vfs.dpc.base; + struct dirstack ds; + int hops = SECURE_OPEN_MAXSYMLINKS; + int fd, fl; + if (ds_init(&ds, afd) < 0) + return -1; + if (ds_descend(&ds, comps[i], &hops) < 0) { + int e = errno; + ds_free(&ds); + errno = e; + return -1; + } + fd = ds_take(&ds); + ds_free(&ds); /* closes intermediate symlink fds, not afd */ + if (fd < 0) + return -1; + if ((fl = fcntl(fd, F_GETFD)) >= 0) + fcntl(fd, F_SETFD, fl | FD_CLOEXEC); + strlcpy(vfs.dpc.name[vfs.dpc.depth], comps[i], sizeof vfs.dpc.name[0]); + vfs.dpc.fd[vfs.dpc.depth++] = fd; + } + + return nc > 0 ? vfs.dpc.fd[vfs.dpc.depth-1] : vfs.dpc.base; +} + +/* Public entry for the sender (no vfs_relpath_active gate: its send paths + * confine unconditionally). Borrowed fd; -1 => caller uses the full walk. */ +int vfs_path_dirfd(const char *anchor, const char *dirpath) +{ + return dpc_dir_fd(anchor, dirpath); +} + +int vfs_get_dirfd(const char *dirname) +{ + if (!vfs_relpath_active()) { errno = 0; return -1; } + return dpc_dir_fd(NULL, dirname); +} +#else +void vfs_dircache_reset(void) +{ +} +int vfs_path_dirfd(const char *anchor, const char *dirpath) +{ + (void)anchor; + (void)dirpath; + errno = 0; + return -1; +} +int vfs_get_dirfd(const char *dirname) +{ + (void)dirname; + errno = 0; + return -1; +} +#endif + +/* Return the cached current-directory fd iff `path` lives directly in the + * entry's own directory (file->dirname) -- the common case for held-dirfd + * traversal. Returns -1 (caller falls back to the do_*_at() wrappers) for + * anything elsewhere: --temp-dir/--partial-dir/--backup-dir, an absolute path, + * a differently-nested dir, or when vfs_opendir() is gated off. The dirfd + * is opened once and cached. + * + * file->basename is NOT assumed to equal `path`'s leaf (a temp file has a + * different basename), so the caller derives the leaf from `path`. */ +int vfs_cached_dirfd(const char *path, const struct file_struct *file) +{ + const char *slash, *dn; + size_t plen; + + if (!path || *path == '/') + return -1; + dn = file && file->dirname ? file->dirname : ""; + slash = strrchr(path, '/'); + plen = slash ? (size_t)(slash - path) : 0; + if (strlen(dn) != plen || memcmp(path, dn, plen) != 0) + return -1; + return vfs_get_dirfd(file ? file->dirname : NULL); +} diff --git a/vfs/dirstack.c b/vfs/dirstack.c new file mode 100644 index 000000000..bd0765c75 --- /dev/null +++ b/vfs/dirstack.c @@ -0,0 +1,341 @@ +/* + * vfs/dirstack.c - race-safe component-walk primitives for rsync's VFS. + * + * The dirstack walks a relative path one component at a time, keeping an open + * dirfd for every ancestor from the anchor down, so a parent renamed mid-walk + * cannot redirect the climb (TOCTOU). Plus the module-confinement helpers that + * decide whether a resolved absolute path has escaped the served module root. + * 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 "vfs/vfs_internal.h" + +/* Returns 1 if path has any "/"-separated component that is exactly + * "..", 0 otherwise. Used by vfs_resolve_open's front-door + * validation to reject ".." inputs (bare "..", "foo/..", "subdir/..") + * for non-re-anchored paths; the walk itself resolves an in-tree ".." + * safely (ds_descend pops to the held parent) for a re-anchored path. */ +int path_has_dotdot_component(const char *path) +{ + const char *p = path; + + while (*p) { + const char *q; + if (*p == '/') { p++; continue; } + q = p; + while (*q && *q != '/') + q++; + if (q - p == 2 && p[0] == '.' && p[1] == '.') + return 1; + p = q; + } + return 0; +} + +/* True if `path` lies within directory `root` (path == root, or path begins + * with root followed by '/'). `rootlen` is strlen(root). */ +static int path_within(const char *root, size_t rootlen, const char *path) +{ + return strncmp(path, root, rootlen) == 0 + && (path[rootlen] == '\0' || path[rootlen] == '/'); +} + +/* Refuse (return 1) when the ABSOLUTE resolved path `abspath` lands OUTSIDE the + * serving module's root, for an operator/peer-supplied path that must stay in the + * module (--partial-dir/--backup-dir/alt-basis: is_operator). An + * in-tree symlink owned by uid 0 / the euid is followed by design, so it can + * redirect the resolved target outside the module; this catches that escape. + * + * This is module-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-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. */ +/* 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) { + *lenp = vfs.module_dirlen; + return vfs.module_dir; + } + *lenp = confine_rootlen; + return confine_root; +} + +/* Split the "/proc//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//..." */ + 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/", 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; + 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 (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; +} + +#if defined(O_NOFOLLOW) && defined(O_DIRECTORY) && defined(AT_FDCWD) + +/* 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 (vfs.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). */ +int open_anchor_dirfd(const char *path) +{ + if (vfs.module_dirfd >= 0 && am_daemon && vfs.module_dir && strcmp(path, vfs.module_dir) == 0) + return dup(vfs.module_dirfd); + return openat(AT_FDCWD, path, O_RDONLY | O_DIRECTORY); +} + +/* Append "/comp" to ds->abspath (no-op if it's unseeded/empty so non-daemon + * callers pay nothing). Returns -1 (ENAMETOOLONG) on overflow. */ +static int ds_path_push(struct dirstack *ds, const char *comp) +{ + size_t al = strlen(ds->abspath); + if (al == 0) + return 0; /* unseeded: tracking disabled for this walk */ + size_t cl = strlen(comp); + if (al + 1 + cl >= sizeof ds->abspath) { + errno = ENAMETOOLONG; + return -1; + } + ds->abspath[al] = '/'; + memcpy(ds->abspath + al + 1, comp, cl + 1); + return 0; +} + +/* Drop the last component of ds->abspath (mirrors a ".." pop). */ +static void ds_path_pop(struct dirstack *ds) +{ + char *slash; + if (!ds->abspath[0]) + return; + slash = strrchr(ds->abspath, '/'); + if (slash && slash != ds->abspath) + *slash = '\0'; +} + +/* Initialise with `anchor` (which may be AT_FDCWD) as the un-owned base. + * Returns int for caller symmetry, but cannot fail (the fd array is inline). */ +int ds_init(struct dirstack *ds, int anchor) +{ + ds->abspath[0] = '\0'; + ds->fds[0] = anchor; + ds->top = 0; + return 0; +} + +/* Close every pushed fd (but not the borrowed anchor at index 0). */ +void ds_free(struct dirstack *ds) +{ + while (ds->top > 0) + close(ds->fds[ds->top--]); +} + +int ds_cur(struct dirstack *ds) +{ + return ds->fds[ds->top]; +} + +static int ds_push(struct dirstack *ds, int fd) +{ + if (ds->top + 1 >= DS_MAXDEPTH) { /* deeper than we'll hold open */ + close(fd); + errno = ENOMEM; + return -1; + } + ds->fds[++ds->top] = fd; + return 0; +} + +/* Detach the current dir as an owned fd the caller must close. At the anchor + * (top 0) the anchor is borrowed, so return a fresh dup of it instead. */ +int ds_take(struct dirstack *ds) +{ + if (ds->top > 0) + return ds->fds[ds->top--]; + return openat(ds->fds[0], ".", O_RDONLY | O_DIRECTORY); +} + +/* Descend one path component on the stack: "." stays, ".." pops to the pinned + * parent (ELOOP at the anchor), a real subdirectory is pushed, and an in-tree + * directory symlink is followed by walking its (relative, possibly + * ..-containing) target on the same stack. Returns 0, or -1 with errno set: + * ELOOP for a refused/escaping symlink or a hop overrun, otherwise the + * underlying openat()/readlinkat() errno (ENOENT, a real ENOTDIR, EACCES). */ +int ds_descend(struct dirstack *ds, const char *part, int *hops) +{ + if (part[0] == '.' && part[1] == '\0') + return 0; /* "." -- no movement */ + if (part[0] == '.' && part[1] == '.' && part[2] == '\0') { + if (ds->top == 0) { /* would rise above the anchor */ + errno = ELOOP; + return -1; + } + close(ds->fds[ds->top--]); /* pop to the held parent fd */ + ds_path_pop(ds); + return 0; + } + + int fd = openat(ds_cur(ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); + if (fd != -1) { /* a real subdirectory */ + if (ds_push(ds, fd) < 0) + return -1; + if (ds_path_push(ds, part) < 0) + return -1; + /* exclude-aware: refuse descending into a module-hidden dir (catches a + * 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_outside_confinement(ds->abspath, 0)) { + errno = ELOOP; + return -1; + } + return 0; + } + /* O_NOFOLLOW refused a symlink (NOFOLLOW_HIT_SYMLINK: ELOOP on Linux, EMLINK + * on FreeBSD, EFTYPE on NetBSD/OpenBSD), or O_DIRECTORY hit a non-directory + * (ENOTDIR). Either may be a symlink, so fall through to the readlink probe; + * anything else is a hard error. */ + if (errno != ENOTDIR && !NOFOLLOW_HIT_SYMLINK(errno)) { + if (errno == EMFILE || errno == ENFILE) { + /* The resolver holds one dirfd per path component, so a deep path + * can exhaust descriptors where plain open() would not. Hint at + * the fix once -- otherwise "Too many open files" is opaque. */ + static int warned = 0; + if (!warned) { + int e = errno; + warned = 1; + rprintf(FWARNING, "out of file descriptors resolving a deep path;" + " raise the open-file limit (e.g. `ulimit -n`)\n"); + errno = e; + } + } + return -1; + } + int open_errno = errno; + + char buf[MAXPATHLEN]; + ssize_t n = readlinkat(ds_cur(ds), part, buf, sizeof buf - 1); + if (n < 0) { + if (errno == EINVAL) /* not a symlink: a real non-dir */ + errno = open_errno; + return -1; + } + if (n == 0 || (size_t)n >= sizeof buf - 1) { + errno = ELOOP; /* empty or truncated target */ + return -1; + } + buf[n] = '\0'; + if (buf[0] == '/') { /* absolute target: refuse */ + errno = ELOOP; + return -1; + } + if (--(*hops) < 0) { + errno = ELOOP; + return -1; + } + return ds_walk_path(ds, buf, hops); +} + +/* Walk every component of a relative path on the stack (used for the basedir, + * and for a followed symlink's target -- which may contain ".."). */ +int ds_walk_path(struct dirstack *ds, char *path, int *hops) +{ + char *save = NULL; + for (char *c = strtok_r(path, "/", &save); c; c = strtok_r(NULL, "/", &save)) { + if (ds_descend(ds, c, hops) < 0) + return -1; + } + return 0; +} + +#endif /* O_NOFOLLOW && O_DIRECTORY && AT_FDCWD */ diff --git a/vfs/dummy.in b/vfs/dummy.in new file mode 100644 index 000000000..f39c56ef8 --- /dev/null +++ b/vfs/dummy.in @@ -0,0 +1,2 @@ +This is a dummy file to ensure that the vfs directory gets created +by configure when a VPATH is used. diff --git a/vfs/fileio.c b/vfs/fileio.c new file mode 100644 index 000000000..0561234d3 --- /dev/null +++ b/vfs/fileio.c @@ -0,0 +1,160 @@ +/* + * vfs/fileio.c - fd-based file-data ops: ftruncate, lseek, fallocate, + * hole-punching. + * + * 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" +#if defined HAVE_SYS_FALLOCATE && !defined HAVE_FALLOCATE +#include +#endif + +#ifndef S_BLKSIZE +# if defined hpux || defined __hpux__ || defined __hpux +# define S_BLKSIZE 1024 +# elif defined _AIX && defined _I386 +# define S_BLKSIZE 4096 +# else +# define S_BLKSIZE 512 +# endif +#endif + +#ifdef HAVE_FTRUNCATE +int vfs_ftruncate(int fd, OFF_T size) +{ + int ret; + + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + + do { + ret = ftruncate(fd, size); + } while (ret < 0 && errno == EINTR); + + return ret; +} +#endif + + + +OFF_T vfs_lseek(int fd, OFF_T offset, int whence) +{ +#ifdef HAVE_LSEEK64 + return lseek64(fd, offset, whence); +#else + return lseek(fd, offset, whence); +#endif +} + + +#ifdef SUPPORT_PREALLOCATION +#ifdef FALLOC_FL_KEEP_SIZE +#define DO_FALLOC_OPTIONS FALLOC_FL_KEEP_SIZE +#else +#define DO_FALLOC_OPTIONS 0 +#endif + +OFF_T vfs_fallocate(int fd, OFF_T offset, OFF_T length) +{ + /* FALLOC_FL_KEEP_SIZE lets --preallocate/--inplace keep the file size at 0 + * until data is written, but a later hole-punch (for --sparse) can only + * deallocate blocks that lie within the file's size -- with KEEP_SIZE the + * reserved blocks sit beyond EOF and the punch silently does nothing, + * leaving the file fully allocated. So when holes will also be punched, + * preallocate at full size instead (write_sparse then punches the nulls). */ + int opts = (inplace || preallocate_files) && sparse_files <= 0 ? DO_FALLOC_OPTIONS : 0; + int ret; + RETURN_ERROR_IF(dry_run, 0); + RETURN_ERROR_IF_RO_OR_LO; + if (length & 1) /* make the length not match the desired length */ + length++; + else + length--; +#if defined HAVE_FALLOCATE + ret = fallocate(fd, opts, offset, length); +#elif defined HAVE_SYS_FALLOCATE + ret = syscall(SYS_fallocate, fd, opts, (loff_t)offset, (loff_t)length); +#elif defined HAVE_EFFICIENT_POSIX_FALLOCATE + ret = posix_fallocate(fd, offset, length); +#else +#error Coding error in SUPPORT_PREALLOCATION logic. +#endif + if (ret < 0) + return ret; + if (opts == 0) { + STRUCT_STAT st; + if (vfs_fstat(fd, &st) < 0) + return length; + return st.st_blocks * S_BLKSIZE; + } + /* With FALLOC_FL_KEEP_SIZE the blocks for [0, length) are reserved even + * though the file size stays put. Return that reserved length (not 0) so + * the caller's preallocated_len is meaningful: write_sparse() needs it to + * choose vfs_punch_hole() over a plain lseek() when turning a null run into + * a hole, and the receiver uses it to trim any over-preallocation. (A + * stray 0 here, from 2019's switch to KEEP_SIZE, is why --preallocate + * --sparse stopped producing sparse files.) */ + return length; +} +#endif + +/* Write all @len bytes from @ptr to @fd, retrying short writes and EINTR. + * Returns 0 on success, -1 on error. */ +static int safe_write(int fd, const char *ptr, size_t len) +{ + while (len > 0) { + int wrote = write(fd, ptr, len); + if (wrote <= 0) { + if (wrote < 0 && errno == EINTR) + continue; + return -1; + } + ptr += wrote; + len -= wrote; + } + return 0; +} + +/* Punch a hole at pos for len bytes. The current file position must be at pos and will be + * changed to be at pos + len. */ +int vfs_punch_hole(int fd, OFF_T pos, OFF_T len) +{ +#ifdef HAVE_FALLOCATE +# ifdef HAVE_FALLOC_FL_PUNCH_HOLE + if (fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, pos, len) == 0) { + if (vfs_lseek(fd, len, SEEK_CUR) != pos + len) + return -1; + return 0; + } +# endif +# ifdef HAVE_FALLOC_FL_ZERO_RANGE + if (fallocate(fd, FALLOC_FL_ZERO_RANGE, pos, len) == 0) { + if (vfs_lseek(fd, len, SEEK_CUR) != pos + len) + return -1; + return 0; + } +# endif +#else + (void)pos; +#endif + { + char zeros[4096]; + memset(zeros, 0, sizeof zeros); + while (len > 0) { + int chunk = len > (int)sizeof zeros ? (int)sizeof zeros : len; + if (safe_write(fd, zeros, chunk) < 0) + return -1; + len -= chunk; + } + } + return 0; +} diff --git a/vfs/link.c b/vfs/link.c new file mode 100644 index 000000000..14e691e5d --- /dev/null +++ b/vfs/link.c @@ -0,0 +1,107 @@ +/* + * vfs/link.c - hard-link wrappers (path, parent-resolved, held-dirfd). + * + * 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" + +#if defined HAVE_LINK || defined HAVE_LINKAT +int vfs_link(const char *old_path, const char *new_path) +{ + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + RETURN_ERROR_IF_NULL(old_path); + RETURN_ERROR_IF_NULL(new_path); +#ifdef HAVE_LINKAT + return linkat(AT_FDCWD, old_path, AT_FDCWD, new_path, 0); +#else + return link(old_path, new_path); +#endif +} + +/* + Symlink-race-safe variant of vfs_link() for receiver-side use. See + the comment on vfs__chmod_secure() for the threat model. link() resolves + parent components of *both* old_path and new_path, so a parent- + symlink swap on either side can plant the new hard link outside + the module, or hard-link an outside file into the module (read + disclosure). + + Defence: resolve each path's parent under its OWN policy and linkat() + between the two dirfds. old_flags / new_flags are the per-operand policy + (VFS_OPERATOR_PATH for an operator operand -- e.g. a --link-dest/--backup-dir + basis; 0 for a transfer path), so an operator basis on one side can't relax the + transfer-path confinement of the other -- see vfs_twopath_side(). flags=0 to + linkat() matches the existing vfs_link() (don't follow a symlink old_path). + Only available on systems with linkat(); pre-AT_FDCWD systems fall through. +*/ +int vfs_link_at(const char *old_path, const char *new_path, int old_flags, int new_flags) +{ +#if defined AT_FDCWD && defined HAVE_LINKAT + char old_dirpath[MAXPATHLEN], new_dirpath[MAXPATHLEN]; + const char *old_bname, *new_bname; + int old_dfd = AT_FDCWD, new_dfd = AT_FDCWD; + BOOL old_owns = False, new_owns = False; + int ret, e; + + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + + if (!vfs_relpath_active()) + return vfs_link(old_path, new_path); + + if (!old_path || !*old_path || !new_path || !*new_path) + return vfs_link(old_path, new_path); + + if (vfs_twopath_side(old_path, old_flags, &old_bname, &old_dfd, &old_owns, + old_dirpath, sizeof old_dirpath) < 0) + return -1; + if (vfs_twopath_side(new_path, new_flags, &new_bname, &new_dfd, &new_owns, + new_dirpath, sizeof new_dirpath) < 0) { + e = errno; + if (old_owns) close(old_dfd); + errno = e; + return -1; + } + + ret = linkat(old_dfd, old_bname, new_dfd, new_bname, 0); + e = errno; + if (new_owns) + close(new_dfd); + if (old_owns) + close(old_dfd); + errno = e; + return ret; +#else + (void)old_flags; (void)new_flags; + return vfs_link(old_path, new_path); +#endif +} +#endif + + +#if defined HAVE_LINK || defined HAVE_LINKAT +int vfs_link_atfd(int old_dfd, const char *old_name, int new_dfd, const char *new_name, int flags) +{ +#if defined AT_FDCWD && defined HAVE_LINKAT + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + return linkat(old_dfd, old_name, new_dfd, new_name, flags); +#else + (void)old_dfd; (void)old_name; (void)new_dfd; (void)new_name; (void)flags; + errno = ENOSYS; + return -1; +#endif +} +#endif diff --git a/vfs/make_path.c b/vfs/make_path.c new file mode 100644 index 000000000..192d3680d --- /dev/null +++ b/vfs/make_path.c @@ -0,0 +1,106 @@ +/* + * vfs/make_path.c - compound VFS op: create a directory path, making any + * missing parent components. Layered on the vfs_mkdir/vfs_stat primitives. + * + * Moved out of util1.c as part of the VFS compound layer: filesystem mechanics + * live in vfs/, so the operator-path resolution policy travels as an explicit + * vfs_flags argument rather than ambient state. + * + * 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 "vfs/vfs_internal.h" + +/* Creates a directory path, making missing components as needed. mkp_flags is + * MKP_DROP_NAME / MKP_SKIP_SLASH (path handling); vfs_flags is the resolution + * policy passed straight to vfs_mkdir (VFS_OPERATOR_PATH for operator-supplied + * --backup-dir trees, else 0). Returns the number of dirs created, or a + * negative value on error. */ +int vfs_make_path(char *fname, int mkp_flags, int vfs_flags) +{ + char *end, *p; + int ret = 0; + + if (mkp_flags & MKP_SKIP_SLASH) { + while (*fname == '/') + fname++; + } + + while (*fname == '.' && fname[1] == '/') + fname += 2; + + if (mkp_flags & MKP_DROP_NAME) { + end = strrchr(fname, '/'); + if (!end || end == fname) + return 0; + *end = '\0'; + } else + end = fname + strlen(fname); + + /* Try to find an existing dir, starting from the deepest dir. */ + for (p = end; ; ) { + if (dry_run) { + STRUCT_STAT st; + if (vfs_stat(VFS_AT_FDCWD, fname, &st, VFS_ALLOW_SYMLINK) == 0) { + if (S_ISDIR(st.st_mode)) + errno = EEXIST; + else + errno = ENOTDIR; + } + } else if (vfs_mkdir(VFS_AT_FDCWD, fname, ACCESSPERMS, vfs_flags) == 0) { + ret++; + break; + } + + if (errno != ENOENT) { + STRUCT_STAT st; + if (errno != EEXIST || (vfs_stat(VFS_AT_FDCWD, fname, &st, VFS_ALLOW_SYMLINK) == 0 && !S_ISDIR(st.st_mode))) + ret = -ret - 1; + break; + } + while (1) { + if (p == fname) { + /* We got a relative path that doesn't exist, so assume that '.' + * is there and just break out and create the whole thing. */ + p = NULL; + goto double_break; + } + if (*--p == '/') { + if (p == fname) { + /* We reached the "/" dir, which we assume is there. */ + goto double_break; + } + *p = '\0'; + break; + } + } + } + double_break: + + /* Make all the dirs that we didn't find on the way here. */ + while (p != end) { + if (p) + *p = '/'; + else + p = fname; + p += strlen(p); + if (ret < 0) /* Skip mkdir on error, but keep restoring the path. */ + continue; + if (vfs_mkdir(VFS_AT_FDCWD, fname, ACCESSPERMS, vfs_flags) < 0) + ret = -ret - 1; + else + ret++; + } + + if (mkp_flags & MKP_DROP_NAME) + *end = '/'; + + return ret; +} diff --git a/vfs/mkdir.c b/vfs/mkdir.c new file mode 100644 index 000000000..ab1c11344 --- /dev/null +++ b/vfs/mkdir.c @@ -0,0 +1,323 @@ +/* + * vfs/mkdir.c - mkdir and mkstemp wrappers, plus the trim_trailing_slashes + * path helper and the race-safe vfs_secure_mkstemp / vfs_mkstemp_atfd create loop. + * + * 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" + +/* Fill buf with len random bytes for the mkstemp-style temp-name suffix. Only + * collision avoidance is needed here -- the O_EXCL|O_NOFOLLOW create is the real + * guard against a guessed/pre-planted name -- so an rand() fallback is fine when + * /dev/urandom can't be opened or read (e.g. a chroot/container without /dev). + * We read /dev/urandom directly rather than probing getrandom()/arc4random_buf() + * to match authenticate.c and avoid new configure checks. */ +#ifdef AT_FDCWD /* only vfs_mkstemp_atfd's held-dirfd create loop uses this */ +static void rand_bytes(unsigned char *buf, size_t len) +{ +#ifndef O_CLOEXEC +#define O_CLOEXEC 0 +#endif + int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC); + if (fd >= 0) { + ssize_t n = read(fd, buf, len); + close(fd); + if (n == (ssize_t)len) { + return; + } + } + for (size_t i = 0; i < len; i++) { + buf[i] = (unsigned char)rand(); + } +} +#endif + +void trim_trailing_slashes(char *name) +{ + int l; + /* Some BSD systems cannot make a directory if the name + * contains a trailing slash. + * */ + + /* Don't change empty string; and also we can't improve on + * "/" */ + + l = strlen(name); + while (l > 1) { + if (name[--l] != '/') + break; + name[l] = '\0'; + } +} + +/* Secure receiver-side resolve for a path mkdir. An operator path + * (--backup-dir/--temp-dir, may live outside the tree) uses the ownership walk; + * otherwise the strict transfer-path resolver (refuse all symlinks, confine + * beneath the transfer root). mkdir() resolves parent symlinks at every + * component, so a parent-component swap can place an attacker-named directory + * outside the module -- defence is to resolve the parent securely and mkdirat() + * the leaf. Falls through to a plain mkdir() in non-daemon/sender, chrooted, + * no-parent and absolute-path cases. */ +static int vfs__mkdir_secure(char *path, mode_t mode, int flags) +{ +#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY + char dirpath[MAXPATHLEN]; + const char *bname, *slash; + int dfd, ret, e; + size_t dlen; + + if (flags & VFS_OPERATOR_PATH) { + if (vfs_symlink_optout_allowed()) + return mkdir(path, mode); + dfd = vfs_owner_walk_parent(path, &bname, 1); + if (dfd < 0) + return -1; + ret = mkdirat(dfd, bname, mode); + e = errno; + close(dfd); + errno = e; + return ret; + } + + if (!vfs_relpath_active()) + return mkdir(path, mode); + if (!*path || *path == '/') + return mkdir(path, mode); + slash = strrchr(path, '/'); + if (!slash) + return mkdir(path, mode); + dlen = slash - path; + if (dlen >= sizeof dirpath) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dirpath, path, dlen); + dirpath[dlen] = '\0'; + bname = slash + 1; + + dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + if (dfd < 0) + return -1; + ret = mkdirat(dfd, bname, mode); + e = errno; + close(dfd); + errno = e; + return ret; +#else + (void)flags; + return mkdir(path, mode); +#endif +} + +/* Unified mkdir. dirfd == VFS_AT_FDCWD resolves `path`; a real held dirfd makes + * `path` a single component created directly under it. flags: VFS_ALLOW_SYMLINK + * (trusted, follow symlinks), VFS_OPERATOR_PATH (operator-supplied path), + * default 0 (secure receiver resolve). */ +int vfs_mkdir(int dirfd, 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: `path` must be a single harmless component -- reject + * anything that could reintroduce path resolution under the pinned + * dir (empty, any '/', "." or ".."). */ + if (!*path || strchr(path, '/') + || (path[0] == '.' && (path[1] == '\0' + || (path[1] == '.' && path[2] == '\0')))) { + errno = EINVAL; + return -1; + } + return mkdirat(dirfd, path, mode); +#else + (void)dirfd; (void)mode; + errno = ENOSYS; + return -1; +#endif + } + + trim_trailing_slashes(path); + + /* VFS_ALLOW_SYMLINK takes precedence: the call site asserts the path is + * trusted, so follow symlinks (the legacy plain mkdir). */ + if (flags & VFS_ALLOW_SYMLINK) + return mkdir(path, mode); + + return vfs__mkdir_secure(path, mode, flags); +} + +/* like mkstemp but forces permissions */ +int vfs_mkstemp(char *template, mode_t perms) +{ + RETURN_ERROR_IF(dry_run, 0); + RETURN_ERROR_IF(read_only, EROFS); + perms |= S_IWUSR; + +#if defined HAVE_SECURE_MKSTEMP && defined HAVE_FCHMOD && (!defined HAVE_OPEN64 || defined HAVE_MKSTEMP64) + { + int fd = mkstemp(template); + if (fd == -1) + return -1; + if (fchmod(fd, perms) != 0 && preserve_perms) { + int errno_save = errno; + close(fd); + unlink(template); + errno = errno_save; + return -1; + } +#if defined HAVE_SETMODE && O_BINARY + setmode(fd, O_BINARY); +#endif + return fd; + } +#else + if (!mktemp(template)) + return -1; + return vfs_open(template, O_RDWR|O_EXCL|O_CREAT, perms); +#endif +} + +/* Create a unique temp file directly in directory `dfd` for the held-dirfd + * traversal: `filename` is the basename ending in "XXXXXX", rewritten in place + * to the chosen name. O_EXCL|O_NOFOLLOW so a planted name can't be followed or + * clobbered. Does NOT close dfd (the caller owns it). Returns the fd, or -1. + * This is the create loop shared with vfs_secure_mkstemp(). */ +int vfs_mkstemp_atfd(int dfd, char *filename, mode_t perms) +{ +#ifdef AT_FDCWD + static const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + size_t filename_len = strlen(filename); + char *suffix; + int fd = -1; + + if (filename_len < 6) { + errno = EINVAL; + return -1; + } + suffix = filename + filename_len - 6; /* Points to XXXXXX */ + if (strcmp(suffix, "XXXXXX") != 0) { + errno = EINVAL; + return -1; + } + + perms |= S_IWUSR; + for (int tries = 0; tries < 100; tries++) { + unsigned char rbytes[6]; + rand_bytes(rbytes, sizeof(rbytes)); + for (int i = 0; i < 6; i++) + suffix[i] = letters[rbytes[i] % (sizeof(letters) - 1)]; + + fd = openat(dfd, filename, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, perms); + if (fd >= 0) + break; + if (errno != EEXIST) + return -1; + } + + if (fd >= 0) { + if (fchmod(fd, perms) != 0 && preserve_perms) { + int errno_save = errno; + close(fd); + unlinkat(dfd, filename, 0); + errno = errno_save; + return -1; + } +#if defined HAVE_SETMODE && O_BINARY + setmode(fd, O_BINARY); +#endif + } + return fd; +#else + (void)dfd; (void)filename; (void)perms; + errno = ENOSYS; + return -1; +#endif +} + +/* + Secure version of mkstemp that prevents symlink attacks on parent directories. + Like vfs_resolve_open(), this walks the path checking each component + with O_NOFOLLOW to prevent TOCTOU race conditions. + + The template may be relative or absolute, but must not contain ../ components. + Returns fd on success, -1 on error. +*/ +int vfs_secure_mkstemp(char *template, mode_t perms, int operator_path) +{ +#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD) + /* Fall back to regular mkstemp on old systems */ + return vfs_mkstemp(template, perms); +#else + char *lastslash; + int dirfd = AT_FDCWD; + int fd = -1; + + if (!template) { + errno = EINVAL; + return -1; + } + if (strncmp(template, "../", 3) == 0 || strstr(template, "/../")) { + errno = EINVAL; + return -1; + } + + /* An operator-supplied --temp-dir may point outside the tree; --insecure-links + * (or a daemon module's "insecure links =") restores legacy following. */ + if (operator_path && vfs_symlink_optout_allowed()) + return vfs_mkstemp(template, perms); + + /* Open the temp file's directory. For an operator --temp-dir use the + * ownership walk (follow a uid0/euid-owned symlink, refuse a foreign one, + * absolute and relative alike); otherwise -- the deep-entry-dir fallback when + * the held-dirfd cache declines -- use the strict transfer-path resolver + * (refuse all symlinks, confine beneath the transfer root). The temp file + * itself is created below with O_EXCL|O_NOFOLLOW, so a planted name can't be + * followed either way. */ + lastslash = strrchr(template, '/'); + if (lastslash) { + char dirbuf[MAXPATHLEN]; + size_t dlen = lastslash - template; + const char *dir; + if (dlen == 0) + dir = "/"; + else { + if (dlen >= sizeof dirbuf) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dirbuf, template, dlen); + dirbuf[dlen] = '\0'; + dir = dirbuf; + } + dirfd = operator_path + ? vfs_open_owner_walk(dir, O_RDONLY | O_DIRECTORY, 0, 0) + : vfs_resolve_open(dir, ".", O_RDONLY | O_DIRECTORY, 0); + if (dirfd < 0) + return -1; + } + + /* Create the temp file in the securely-opened directory. */ + { + char *filename = lastslash ? lastslash + 1 : template; + int e; + fd = vfs_mkstemp_atfd(dirfd, filename, perms); + e = errno; + if (dirfd != AT_FDCWD) close(dirfd); + errno = e; + } + return fd; +#endif +} diff --git a/vfs/mknod.c b/vfs/mknod.c new file mode 100644 index 000000000..8d973d738 --- /dev/null +++ b/vfs/mknod.c @@ -0,0 +1,310 @@ +/* + * vfs/mknod.c - device/fifo/socket node creation wrappers. + * + * Includes the HAVE_MKNOD/HAVE_MKNODAT/HAVE_MKFIFO and AF_UNIX socket-bind + * fallbacks and the fake-super placeholder handling. 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" +#ifdef HAVE_SYS_UN_H +#include /* for the socket+bind() fallback in vfs_mknod() */ +#endif + +static int vfs__mknod_plain(const char *pathname, mode_t mode, dev_t dev) +{ + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + RETURN_ERROR_IF_NULL(pathname); + + /* For --fake-super, we create a normal file with mode 0600. */ + if (am_root < 0) { + int fd = open(pathname, O_WRONLY|O_CREAT|O_TRUNC, S_IWUSR|S_IRUSR); + if (fd < 0 || close(fd) < 0) + return -1; + return 0; + } + + /* Try mknod first: it handles every node type on Linux. Only if it + * can't make this type on this filesystem (sockets on the BSDs/macOS/ + * Solaris, or an old system lacking FIFO support) do we retry with the + * type-specific primitive. That capability is filesystem-dependent, so + * it is decided per call -- not cached, not probed at build time. */ +#ifdef HAVE_MKNOD + if (mknod(pathname, mode, dev) == 0) + return 0; +#endif +#ifdef HAVE_MKFIFO + if (S_ISFIFO(mode)) + return mkfifo(pathname, mode); +#endif +#ifdef HAVE_SYS_UN_H + if (S_ISSOCK(mode)) { + int sock; + struct sockaddr_un saddr; + unsigned int len = strlcpy(saddr.sun_path, pathname, sizeof saddr.sun_path); + if (len >= sizeof saddr.sun_path) { + errno = ENAMETOOLONG; + return -1; + } +#ifdef HAVE_SOCKADDR_UN_LEN + saddr.sun_len = len + 1; +#endif + saddr.sun_family = AF_UNIX; + + if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0 + || (unlink(pathname) < 0 && errno != ENOENT) + || (bind(sock, (struct sockaddr*)&saddr, sizeof saddr)) < 0) + return -1; + close(sock); +#ifdef HAVE_CHMOD + return vfs_chmod(VFS_AT_FDCWD, pathname, mode, VFS_ALLOW_SYMLINK); +#else + return 0; +#endif + } +#endif +#ifdef HAVE_MKNOD + return -1; /* mknod() failed for a regular/device node; errno is set */ +#else + errno = ENOSYS; + return -1; +#endif +} + +/* + Symlink-race-safe variant of vfs_mknod() for receiver-side use. See + the comment on vfs__chmod_secure() for the threat model. Defence: open + the parent of pathname under vfs_resolve_open() and use + mknodat() against that dirfd. mknodat() covers both regular-file + (S_IFREG with dev=0) and FIFO (S_IFIFO) and device-node creation. + + A top-level (no-slash) pathname has no parent to confine, so it uses + AT_FDCWD; the final component is still protected (mknodat/mkfifoat do + not follow it, and the fake-super openat() uses O_NOFOLLOW). + + Fake-super (am_root < 0) is handled inline against the (secure or + AT_FDCWD) dirfd: it creates a regular empty file (the same file-as- + metadata-placeholder pattern vfs_mknod uses) via openat() with + O_NOFOLLOW so a pre-planted symlink at the basename can't redirect + the file creation -- top-level paths included (the previous code fell + through to the bare-path vfs_mknod() there, whose plain open() followed + such a symlink). On Linux, sockets are recreated with mknodat() like any + other special file; on systems where mknod() can't create sockets the + at-variant fails instead of re-resolving an unsafe parent. +*/ +static int vfs__mknod_secure(const char *pathname, mode_t mode, dev_t dev, int flags) +{ + /* HAVE_MKNODAT: older Darwin declares AT_FDCWD but not mknodat(), so + * the at-variant won't build there; fall back to the plain mknod (#896). */ +#if defined AT_FDCWD && defined HAVE_MKNODAT + char dirpath[MAXPATHLEN]; + const char *bname; + const char *slash; + int dfd = AT_FDCWD, ret, e; + BOOL owns = False; + size_t dlen; + + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + +#if defined O_NOFOLLOW && defined O_DIRECTORY + if (flags & VFS_OPERATOR_PATH) { + if (vfs_symlink_optout_allowed()) + return vfs__mknod_plain(pathname, mode, dev); + dfd = vfs_owner_walk_parent(pathname, &bname, 1); + if (dfd < 0) + return -1; + if (am_root < 0) { + /* Fake-super represents a special file with an inert regular + * placeholder. Keep that representation when the destination + * is an operator path, but create it relative to the verified + * parent so the confinement guarantee is unchanged. */ + int fd = openat(dfd, bname, + O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, + S_IWUSR | S_IRUSR); + ret = fd < 0 ? -1 : close(fd); + } else { + ret = mknodat(dfd, bname, mode, dev); + } + if (ret < 0 && am_root >= 0) { + /* mknodat() can't make a FIFO/socket on the BSDs/macOS/ + * Solaris (EINVAL); retry race-safely on the held dirfd, + * mirroring the secure-relpath path below. Without this a + * FIFO backup to an operator --backup-dir fails there. */ +#ifdef HAVE_MKFIFOAT + if (S_ISFIFO(mode)) + ret = mkfifoat(dfd, bname, mode); + else +#endif + if (S_ISSOCK(mode)) + errno = EOPNOTSUPP; /* no dirfd-relative socket bind */ + } + e = errno; + close(dfd); + errno = e; + return ret; + } +#endif + + if (!vfs_relpath_active()) + return vfs__mknod_plain(pathname, mode, dev); + + if (!pathname || !*pathname || *pathname == '/') + return vfs__mknod_plain(pathname, mode, dev); + + /* A path with a slash needs vfs_resolve_open to confine its + * parent resolution; a top-level path lives in CWD (AT_FDCWD) with + * no parent to subvert. The final component is protected below + * regardless. */ + slash = strrchr(pathname, '/'); + if (slash) { + dlen = slash - pathname; + if (dlen >= sizeof dirpath) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dirpath, pathname, dlen); + dirpath[dlen] = '\0'; + bname = slash + 1; + dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + if (dfd < 0) + return -1; + owns = True; + } else { + bname = pathname; + } + + if (am_root < 0) { + /* For --fake-super, vfs_mknod creates a regular empty + * file as a placeholder for the special-file metadata + * (which is stored in xattrs elsewhere). Do that against + * the (secure or AT_FDCWD) dirfd, with O_NOFOLLOW so a + * pre-planted symlink at the basename can't redirect the + * file creation. */ + int fd = openat(dfd, bname, + O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, + S_IWUSR | S_IRUSR); + if (fd < 0) { + e = errno; + if (owns) close(dfd); + errno = e; + return -1; + } + ret = (close(fd) < 0) ? -1 : 0; + e = errno; + if (owns) close(dfd); + errno = e; + return ret; + } + + /* Try mknodat first (handles every type on Linux); on failure retry + * race-safely with the type-specific primitive. Decided per call -- + * the capability is filesystem-dependent (see vfs_mknod()). */ + ret = mknodat(dfd, bname, mode, dev); + if (ret < 0) { +#ifdef HAVE_MKFIFOAT + if (S_ISFIFO(mode)) + ret = mkfifoat(dfd, bname, mode); + else +#endif + if (S_ISSOCK(mode)) { + /* There is no dirfd-relative socket bind without + * /proc/self/fd: a top-level path can bind via + * vfs_mknod(), but a nested one fails safe rather than + * re-resolve a potentially unsafe parent. */ + if (dfd == AT_FDCWD) + ret = vfs__mknod_plain(pathname, mode, dev); + else + errno = EOPNOTSUPP; + } + /* else: regular/device node -- keep mknodat()'s errno */ + } + e = errno; + if (owns) close(dfd); + errno = e; + return ret; +#else + return vfs__mknod_plain(pathname, mode, dev); +#endif +} + +static int vfs__mknod_atfd(int dfd, const char *name, mode_t mode, dev_t dev) +{ +#ifdef AT_FDCWD + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + + if (am_root < 0) { + /* --fake-super: regular empty placeholder file (O_NOFOLLOW). */ + int fd = openat(dfd, name, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, + S_IWUSR | S_IRUSR); + if (fd < 0) + return -1; + return (close(fd) < 0) ? -1 : 0; + } + + /* Try mknodat first; on failure retry race-safely with the type- + * specific primitive (see vfs_mknod()). HAVE_MKNODAT, not HAVE_MKNOD: + * older Darwin has mknod() but not mknodat(), so keying off the former + * compiles a call that then fails to link (#161). */ +#ifdef HAVE_MKNODAT + if (mknodat(dfd, name, mode, dev) == 0) + return 0; +#endif +#ifdef HAVE_MKFIFOAT + if (S_ISFIFO(mode)) + return mkfifoat(dfd, name, mode); +#endif + if (S_ISSOCK(mode)) { + /* No dirfd-relative socket bind without /proc/self/fd; fail safe. + * (The generator routes sockets to vfs_mknod(), not here.) */ + errno = EOPNOTSUPP; + return -1; + } +#ifdef HAVE_MKNODAT + return -1; /* mknodat()'s errno (regular/device node) */ +#else + /* Must match the guard above: reporting "mknodat()'s errno" where the + * call was never compiled would return a stale errno. */ + (void)dev; + errno = ENOSYS; + return -1; +#endif +#else + (void)dfd; (void)name; (void)mode; (void)dev; + errno = ENOSYS; + return -1; +#endif +} + +/* Unified node creation. dirfd == VFS_AT_FDCWD resolves `path`; a real held + * dirfd makes `path` a single component created directly under it. flags: + * VFS_ALLOW_SYMLINK (trusted, plain mknod), VFS_OPERATOR_PATH (operator path), + * default 0 (secure receiver resolve). */ +int vfs_mknod(int dirfd, const char *path, mode_t mode, dev_t dev, int flags) +{ + if (dirfd != VFS_AT_FDCWD) { + /* Held-fd: `path` must be a single harmless component. */ + if (!path || !*path || strchr(path, '/') + || (path[0] == '.' && (path[1] == '\0' + || (path[1] == '.' && path[2] == '\0')))) { + errno = EINVAL; + return -1; + } + return vfs__mknod_atfd(dirfd, path, mode, dev); + } + if (flags & VFS_ALLOW_SYMLINK) + return vfs__mknod_plain(path, mode, dev); + return vfs__mknod_secure(path, mode, dev, flags); +} diff --git a/vfs/open.c b/vfs/open.c new file mode 100644 index 000000000..57d08d82f --- /dev/null +++ b/vfs/open.c @@ -0,0 +1,205 @@ +/* + * vfs/open.c - open wrappers (path, parent-resolved, held-dirfd, nofollow). + * + * 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" + +int vfs_open(const char *pathname, int flags, mode_t mode) +{ + RETURN_ERROR_IF_NULL(pathname); + if (flags != O_RDONLY) { + RETURN_ERROR_IF(dry_run, 0); + RETURN_ERROR_IF_RO_OR_LO; + } + +#ifdef O_NOATIME + if (open_noatime) + flags |= O_NOATIME; +#endif + + return open(pathname, flags | O_BINARY, mode); +} + +/* + Symlink-race-safe variant of vfs_open() for receiver-side use. See + the comment on vfs__chmod_secure() for the threat model. open() resolves + parent components, so a parent-symlink swap can redirect the open + to a file outside the module. This wrapper is defence-in-depth for + bare-path vfs_open() sites that callers know are otherwise + protected by secure parent-syscalls (e.g. generator.c's in-place + backup creation, where robust_unlink() rejects the symlinked + parent before this open is reached): if any of those upstream + protections is later removed or regresses, the open here still + refuses to escape the module. + + Defence: open the parent of pathname under vfs_resolve_open() + and call openat() against the resulting dirfd with O_NOFOLLOW + (so the basename itself isn't followed if it happens to be a + pre-planted symlink, which is what we want for O_CREAT|O_EXCL). +*/ +int vfs_open_at(const char *pathname, int flags, mode_t mode, int vfs_flags) +{ +#ifdef AT_FDCWD + char dirpath[MAXPATHLEN]; + const char *bname; + const char *slash; + int dfd, ret, e; + size_t dlen; + + if (flags != O_RDONLY) { + RETURN_ERROR_IF(dry_run, 0); + RETURN_ERROR_IF_RO_OR_LO; + } + +#if defined O_NOFOLLOW && defined O_DIRECTORY + if (vfs_flags & VFS_OPERATOR_PATH) { + if (vfs_symlink_optout_allowed()) + return vfs_open(pathname, flags, mode); + dfd = vfs_owner_walk_parent(pathname, &bname, 1); + if (dfd < 0) + return -1; + ret = openat(dfd, bname, flags | O_NOFOLLOW, mode); + e = errno; + close(dfd); + errno = e; + return ret; + } +#endif + + if (!vfs_relpath_active()) + return vfs_open(pathname, flags, mode); + + if (!pathname || !*pathname || *pathname == '/') + return vfs_open(pathname, flags, mode); + + slash = strrchr(pathname, '/'); + if (!slash) + return vfs_open(pathname, flags, mode); + + dlen = slash - pathname; + if (dlen >= sizeof dirpath) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dirpath, pathname, dlen); + dirpath[dlen] = '\0'; + bname = slash + 1; + + dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + if (dfd < 0) + return -1; + +#ifdef O_NOATIME + if (open_noatime) + flags |= O_NOATIME; +#endif + + ret = openat(dfd, bname, flags | O_NOFOLLOW | O_BINARY, mode); + e = errno; + close(dfd); + errno = e; + return ret; +#else + return vfs_open(pathname, flags, mode); +#endif +} + +int vfs_open_nofollow(const char *pathname, int flags) +{ +#ifndef O_NOFOLLOW + STRUCT_STAT f_st, l_st; +#endif + int fd; + + if (flags != O_RDONLY) { + RETURN_ERROR_IF(dry_run, 0); + RETURN_ERROR_IF_RO_OR_LO; +#ifndef O_NOFOLLOW + /* This function doesn't support write attempts w/o O_NOFOLLOW. */ + errno = EINVAL; + return -1; +#endif + } + +#ifdef O_NOATIME + if (open_noatime) + flags |= O_NOATIME; +#endif + +#ifdef O_NOFOLLOW + fd = open(pathname, flags|O_NOFOLLOW); +#else + if (vfs_lstat(VFS_AT_FDCWD, pathname, &l_st, VFS_ALLOW_SYMLINK) < 0) + return -1; + if (S_ISLNK(l_st.st_mode)) { + errno = ELOOP; + return -1; + } + if ((fd = open(pathname, flags)) < 0) + return fd; + if (vfs_fstat(fd, &f_st) < 0) { + close_and_return_error: + { + int save_errno = errno; + close(fd); + errno = save_errno; + } + return -1; + } + if (l_st.st_dev != f_st.st_dev || l_st.st_ino != f_st.st_ino) { + errno = EINVAL; + goto close_and_return_error; + } +#endif + + return fd; +} + +/* + varient of vfs_open/vfs_open_nofollow which does vfs_open() if the + copy_links or copy_unsafe_links options are set and does + vfs_open_nofollow() otherwise + + This is used to prevent a race condition where an attacker could be + switching a file between being a symlink and being a normal file + + The open is always done with O_RDONLY flags + */ +int vfs_open_checklinks(const char *pathname) +{ + if (copy_links || copy_unsafe_links) { + return vfs_open(pathname, O_RDONLY, 0); + } + return vfs_open_nofollow(pathname, O_RDONLY); +} + +int vfs_open_atfd(int dfd, const char *name, int flags, mode_t mode) +{ +#ifdef AT_FDCWD + if (flags != O_RDONLY) { + RETURN_ERROR_IF(dry_run, 0); + RETURN_ERROR_IF_RO_OR_LO; + } +#ifdef O_NOATIME + if (open_noatime) + flags |= O_NOATIME; +#endif + return openat(dfd, name, flags | O_NOFOLLOW | O_BINARY, mode); +#else + (void)dfd; (void)name; (void)flags; (void)mode; + errno = ENOSYS; + return -1; +#endif +} diff --git a/vfs/owner_walk.c b/vfs/owner_walk.c new file mode 100644 index 000000000..8be7feaf3 --- /dev/null +++ b/vfs/owner_walk.c @@ -0,0 +1,382 @@ +/* + * vfs/owner_walk.c - operator-supplied-path resolution by ownership. + * + * For operator paths (--backup-dir/--temp-dir/--*-dest, daemon + * --log-file/motd/lock/config, etc.) that may legitimately point outside the + * transfer tree, the trust signal is authority not location: follow a symlink + * owned by uid 0 or our euid at every component, refuse any other-uid one. + * vfs_open_owner_walk() opens such a path; vfs_vfs_owner_walk_parent() opens its + * parent. 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" + + +/* 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 -- vfs_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, int is_operator) +{ +#if defined AT_FDCWD && defined O_NOFOLLOW + /* 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 + 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 (vfs_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]; + 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 + * 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("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + 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, is_operator)) { + 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. */ + if (lst.st_uid != 0 && lst.st_uid != trusted_uid) { + 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'; + + /* Splice: new `remaining` = + . + * 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] == '/') { + if (dfd_owns) close(dfd); + dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (dfd < 0) { + saved_errno = errno; + dfd_owns = 0; + goto out; + } + dfd_owns = 1; + abspath[0] = '\0'; /* followed an absolute target: restart from "/" */ + /* "self" resolves to "", 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); + } else { + strlcpy(remaining, rebuilt, sizeof remaining); + } + continue; + } + + /* Non-symlink. */ + if (is_last) { + if (abspath_step(abspath, sizeof abspath, comp, comp_len) < 0) { + saved_errno = errno; + goto out; + } + if (!pin_transit && abspath_outside_confinement(abspath, is_operator)) { + saved_errno = ELOOP; + goto out; + } + retfd = openat(dfd, comp, flags | O_NOFOLLOW, mode); + saved_errno = errno; + /* Resolved leaf dir (O_DIRECTORY): hand its path back so + * vfs_owner_walk_parent can filter-check the operation's leaf. */ + if (retfd >= 0 && out_abs && out_cap) + /* Root-resolved (".." popped abspath empty) tracked daemon walk: + * hand back "/" so vfs_owner_walk_parent still leaf-checks (path=/ bypass). */ + strlcpy(out_abs, (am_daemon && !abspath[0]) ? "/" : abspath, out_cap); + goto out; + } + + if (!S_ISDIR(lst.st_mode)) { + saved_errno = ENOTDIR; + goto out; + } + /* track the resolved path so a target outside the module is refused */ + if (abspath_step(abspath, sizeof abspath, comp, comp_len) < 0) { + saved_errno = errno; + goto out; + } + if (!pin_transit && abspath_outside_confinement(abspath, is_operator)) { + saved_errno = ELOOP; + goto out; + } + int next = openat(dfd, comp, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (next < 0) { + saved_errno = errno; + goto out; + } + if (dfd_owns) close(dfd); + dfd = next; + dfd_owns = 1; + + /* Advance `remaining` past this component (and the slash). */ + if (slash) { + char *p = slash; + while (*p == '/') p++; + memmove(remaining, p, strlen(p) + 1); + } else { + remaining[0] = '\0'; + } + } + + /* Path resolved entirely to a directory (no leaf component left). + * If the caller wanted O_DIRECTORY we already hold the dirfd we + * built up; otherwise it's an EISDIR. */ + if (flags & O_DIRECTORY) { + retfd = dfd; + dfd_owns = 0; /* caller now owns it */ + saved_errno = 0; + if (out_abs && out_cap) + /* Root-resolved (".." popped abspath empty) tracked daemon walk: + * hand back "/" so vfs_owner_walk_parent still leaf-checks (path=/ bypass). */ + strlcpy(out_abs, (am_daemon && !abspath[0]) ? "/" : abspath, out_cap); + } else { + saved_errno = EISDIR; + } + +out: + if (dfd_owns) close(dfd); + errno = saved_errno; + return retfd; +#else + /* Pre-AT_FDCWD / no O_NOFOLLOW systems: best-effort fallback. */ + (void)out_abs; (void)out_cap; + return open(path, flags, mode); +#endif +} + +int vfs_open_owner_walk(const char *path, int flags, mode_t mode, int is_operator) +{ + return ona_open(path, flags, mode, NULL, 0, is_operator); +} + +/* When set, the do_*_at() wrappers resolve their path as an OPERATOR-supplied + * directory path (an absolute or relative --backup-dir/--temp-dir/--*-dest) + * using the ownership walk -- follow a symlink owned by uid 0 or our euid, + * refuse any other-uid one, at every component -- instead of the stricter + * transfer-path resolver (which refuses all symlinks and is confined beneath the + * transfer root). An operator path may legitimately point outside the tree, so + * the trust signal is authority (ownership), not location. Set around the + * relevant ops by backup.c et al.; the opt-out (--insecure-links / "insecure + * links =") restores legacy following. Default 0 (transfer-path resolver). */ + +#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY +/* For an operator-supplied path: open its parent directory via the ownership + * walk (handles absolute and relative paths) and point *bname at the final + * component. Returns the dirfd (caller closes) or -1 with errno set. */ +int vfs_owner_walk_parent(const char *path, const char **bname, int is_operator) +{ + const char *slash = strrchr(path, '/'); + char dir[MAXPATHLEN], pabs[MAXPATHLEN]; + size_t dlen; + int dfd; + + *bname = slash ? slash + 1 : path; + pabs[0] = '\0'; + if (!slash) + dfd = ona_open(".", O_RDONLY | O_DIRECTORY, 0, pabs, sizeof pabs, is_operator); + else { + dlen = slash == path ? 1 : (size_t)(slash - path); /* "/x" -> parent "/" */ + if (dlen >= sizeof dir) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dir, path, dlen); + dir[dlen] = '\0'; + dfd = ona_open(dir, O_RDONLY | O_DIRECTORY, 0, pabs, sizeof pabs, is_operator); + } + if (dfd < 0) + return -1; + /* 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_outside_confinement.) */ + if (pabs[0]) { + char leafabs[MAXPATHLEN]; + if (snprintf(leafabs, sizeof leafabs, "%s/%s", pabs, *bname) >= (int)sizeof leafabs) { + close(dfd); + errno = ENAMETOOLONG; /* fail closed, never skip the check */ + return -1; + } + if (abspath_outside_confinement(leafabs, is_operator)) { + close(dfd); + errno = ELOOP; + return -1; + } + } + return dfd; +} +#endif diff --git a/vfs/rename.c b/vfs/rename.c new file mode 100644 index 000000000..daec7f2b4 --- /dev/null +++ b/vfs/rename.c @@ -0,0 +1,99 @@ +/* + * vfs/rename.c - rename wrappers (path, parent-resolved, held-dirfd). + * + * 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" + +int vfs_rename(const char *old_path, const char *new_path) +{ + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + return rename(old_path, new_path); +} + +/* + Symlink-race-safe variant of vfs_rename() for receiver-side use. See + the comment on vfs__chmod_secure() for the threat model and design rationale. + + rename() is the central tmp -> final operation in rsync; if either the + source or the destination has an attacker-substituted symlink in one + of its parent components, the rename can publish or vanish files + outside the module. Defence: resolve the parent of *each* path under its + OWN policy and renameat() between the resulting dirfds. + + old_flags / new_flags are the per-operand policy (VFS_OPERATOR_PATH for an + operator-supplied operand -- a --backup-dir/--partial-dir/--temp-dir path; 0 + for a transfer path). Passing them separately means an operator operand on one + side cannot relax (owner-walk) the transfer-path confinement of the other side + -- see vfs_twopath_side() for the per-side resolution. + + Falls through to vfs_rename() in dry-run, non-daemon, chrooted and + --insecure-links cases, identical to the other *_at() wrappers. +*/ +int vfs_rename_at(const char *old_path, const char *new_path, int old_flags, int new_flags) +{ +#ifdef AT_FDCWD + char old_dirpath[MAXPATHLEN], new_dirpath[MAXPATHLEN]; + const char *old_bname, *new_bname; + int old_dfd = AT_FDCWD, new_dfd = AT_FDCWD; + BOOL old_owns = False, new_owns = False; + int ret, e; + + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + + if (!vfs_relpath_active()) + return vfs_rename(old_path, new_path); + + if (!old_path || !*old_path || !new_path || !*new_path) + return vfs_rename(old_path, new_path); + + if (vfs_twopath_side(old_path, old_flags, &old_bname, &old_dfd, &old_owns, + old_dirpath, sizeof old_dirpath) < 0) + return -1; + if (vfs_twopath_side(new_path, new_flags, &new_bname, &new_dfd, &new_owns, + new_dirpath, sizeof new_dirpath) < 0) { + e = errno; + if (old_owns) close(old_dfd); + errno = e; + return -1; + } + + ret = renameat(old_dfd, old_bname, new_dfd, new_bname); + e = errno; + if (new_owns) + close(new_dfd); + if (old_owns) + close(old_dfd); + errno = e; + return ret; +#else + (void)old_flags; (void)new_flags; + return vfs_rename(old_path, new_path); +#endif +} + +int vfs_rename_atfd(int old_dfd, const char *old_name, int new_dfd, const char *new_name) +{ +#ifdef AT_FDCWD + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + return renameat(old_dfd, old_name, new_dfd, new_name); +#else + (void)old_dfd; (void)old_name; (void)new_dfd; (void)new_name; + errno = ENOSYS; + return -1; +#endif +} diff --git a/vfs/robust.c b/vfs/robust.c new file mode 100644 index 000000000..c12447171 --- /dev/null +++ b/vfs/robust.c @@ -0,0 +1,146 @@ +/* + * vfs/robust.c - compound VFS ops: robust unlink/rename that retry around + * busy files (ETXTBSY) and fall back to a cross-filesystem copy. Layered on + * the vfs_* unlink/rename primitives and vfs_copy_file; calls out to the + * partial-dir handler for the EXDEV copy path. + * + * Moved out of util1.c as part of the VFS compound layer. + * + * Copyright (C) 1996-2022 Andrew Tridgell, Paul Mackerras, 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 "vfs/vfs_internal.h" + +/* MAX_RENAMES should be 10**MAX_RENAMES_DIGITS */ +#define MAX_RENAMES_DIGITS 3 +#define MAX_RENAMES 1000 + +/** + * Robust unlink: some OS'es (HPUX) refuse to unlink busy files, so + * rename to /.rsyncNNN instead. + * + * Note that successive rsync runs will shuffle the filenames around a + * bit as long as the file is still busy; this is because this function + * does not know if the unlink call is due to a new file coming in, or + * --delete trying to remove old .rsyncNNN files, hence it renames it + * each time. + **/ +int robust_unlink(const char *fname, int vfs_flags) +{ +#ifndef ETXTBSY + return vfs_unlink(VFS_AT_FDCWD, fname, vfs_flags); +#else + static int counter = 1; + int rc, pos, start; + char path[MAXPATHLEN]; + + rc = vfs_unlink(VFS_AT_FDCWD, fname, vfs_flags); + if (rc == 0 || errno != ETXTBSY) + return rc; + + if ((pos = strlcpy(path, fname, MAXPATHLEN)) >= MAXPATHLEN) + pos = MAXPATHLEN - 1; + + while (pos > 0 && path[pos-1] != '/') + pos--; + pos += strlcpy(path+pos, ".rsync", MAXPATHLEN-pos); + + if (pos > (MAXPATHLEN-MAX_RENAMES_DIGITS-1)) { + errno = ETXTBSY; + return -1; + } + + /* start where the last one left off to reduce chance of clashes */ + start = counter; + do { + snprintf(&path[pos], MAX_RENAMES_DIGITS+1, "%03d", counter); + if (++counter >= MAX_RENAMES) + counter = 1; + } while (access(path, 0) == 0 && counter != start); + + if (INFO_GTE(MISC, 1)) { + rprintf(FWARNING, "renaming %s to %s because of text busy\n", + fname, path); + } + + /* maybe we should return rename()'s exit status? Nah. path is a sibling of + * fname in the same parent, so both sides share fname's policy (vfs_flags). */ + if (vfs_rename_at(fname, path, vfs_flags, vfs_flags) != 0) { + errno = ETXTBSY; + return -1; + } + return 0; +#endif +} + +/* Returns 0 on successful rename, 1 if we successfully copied the file + * across filesystems, -2 if copy_file() failed, and -1 on other errors. + * If partialptr is not NULL and we need to do a copy, copy the file into + * the active partial-dir instead of over the destination file. */ +int robust_rename(const char *from, const char *to, const char *partialptr, + int mode, struct file_struct *file) +{ + int tries = 4; + + /* A resumed in-place partial-dir transfer might call us with from and + * to pointing to the same buf if the transfer failed yet again. */ + if (from == to) + return 0; + + while (tries--) { + /* tmp -> final usually live in the entry's own dir: rename via the + * held dir fd when both do, else the full-path wrapper. */ + int ofd = vfs_cached_dirfd(from, file); + int nfd = vfs_cached_dirfd(to, file); + int rr; + if (ofd >= 0 && nfd >= 0) { + const char *os = strrchr(from, '/'); + const char *ns = strrchr(to, '/'); + rr = vfs_rename_atfd(ofd, os ? os + 1 : from, nfd, ns ? ns + 1 : to); + } else + rr = vfs_rename_at(from, to, 0, 0); + if (rr == 0) + return 0; + + switch (errno) { +#ifdef ETXTBSY + case ETXTBSY: + if (robust_unlink(to, 0) != 0) { + errno = ETXTBSY; + return -1; + } + errno = ETXTBSY; + break; +#endif + case EXDEV: + if (partialptr) { + if (!handle_partial_dir(partialptr,PDIR_CREATE)) + return -2; + to = partialptr; + } + /* Cross-fs fallback: copy then unlink. An absolute --temp-dir + * source / --partial-dir dest is an operator path whose parents + * the plain-libc arm would otherwise follow -- confine them + * through the ownership walk (VFS_OPERATOR_PATH) so a raced + * parent symlink can't redirect the dest-write or the + * source-unlink out of the module. copy_file already confines + * the source READ; a relative in-module path stays on the + * secure-relative arm, so only flag an absolute (operator) + * path. */ + if (copy_file(from, to, -1, mode, *to == '/' ? VFS_OPERATOR_PATH : 0) != 0) + return -2; + vfs_unlink(VFS_AT_FDCWD, from, *from == '/' ? VFS_OPERATOR_PATH : 0); + return 1; + default: + return -1; + } + } + return -1; +} diff --git a/vfs/secure_open.c b/vfs/secure_open.c new file mode 100644 index 000000000..7d18cf300 --- /dev/null +++ b/vfs/secure_open.c @@ -0,0 +1,500 @@ +/* + * 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 +} diff --git a/vfs/stat.c b/vfs/stat.c new file mode 100644 index 000000000..f7f4e3409 --- /dev/null +++ b/vfs/stat.c @@ -0,0 +1,189 @@ +/* + * vfs/stat.c - stat/lstat/fstat wrappers (path, parent-resolved, held-dirfd). + * + * 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" + +static int vfs__stat_plain(const char *path, STRUCT_STAT *st) +{ + RETURN_ERROR_IF_NULL(path); +#ifdef USE_STAT64_FUNCS + return stat64(path, st); +#else + return stat(path, st); +#endif +} + +static int vfs__lstat_plain(const char *path, STRUCT_STAT *st) +{ + RETURN_ERROR_IF_NULL(path); +#ifdef SUPPORT_LINKS +# ifdef USE_STAT64_FUNCS + return lstat64(path, st); +# else + return lstat(path, st); +# endif +#else + return vfs__stat_plain(path, st); +#endif +} + +/* + Symlink-race-safe variants of vfs_stat() / vfs_lstat() for receiver- + side use. See the comment on vfs__chmod_secure() for the threat model. + stat() and lstat() resolve parent components, so a parent-symlink + swap can make the receiver's stat see attributes of a victim file + outside the module -- which then drives later behaviour (e.g. + "this isn't a directory, delete it" -> attacker-controlled unlink + on something outside the module). + + Defence: open the parent under vfs_resolve_open() and use + fstatat() with AT_SYMLINK_NOFOLLOW (lstat) or 0 (stat) against + that dirfd. Same fall-through gating as the other wrappers. +*/ +static int do_xstat_at(const char *path, STRUCT_STAT *st, int at_flags, int (*fallback)(const char *, STRUCT_STAT *), int vfs_flags) +{ +#ifdef AT_FDCWD + char dirpath[MAXPATHLEN]; + const char *bname; + const char *slash; + int dfd, ret, e; + size_t dlen; + +#if defined O_NOFOLLOW && defined O_DIRECTORY + if (vfs_flags & VFS_OPERATOR_PATH) { + if (vfs_symlink_optout_allowed()) + return fallback(path, st); + dfd = vfs_owner_walk_parent(path, &bname, 1); + if (dfd < 0) + return -1; + ret = fstatat(dfd, bname, st, at_flags); + e = errno; + close(dfd); + errno = e; + return ret; + } +#endif + + if (!vfs_relpath_active()) + return fallback(path, st); + + if (!path || !*path || *path == '/') + return fallback(path, st); + + slash = strrchr(path, '/'); + if (!slash) + return fallback(path, st); + + dlen = slash - path; + if (dlen >= sizeof dirpath) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dirpath, path, dlen); + dirpath[dlen] = '\0'; + bname = slash + 1; + + dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + if (dfd < 0) + return -1; + + ret = fstatat(dfd, bname, st, at_flags); + e = errno; + close(dfd); + errno = e; + return ret; +#else + (void)at_flags; (void)vfs_flags; + return fallback(path, st); +#endif +} + +/* Unified stat/lstat. dirfd == VFS_AT_FDCWD resolves `path` (VFS_ALLOW_SYMLINK = + * plain libc stat/lstat, default 0 = secure receiver resolve, VFS_OPERATOR_PATH = + * ownership walk); a real held dirfd fstatat()s a single validated component + * under it. vfs_stat follows the leaf, vfs_lstat does not. */ +int vfs_stat(int dirfd, const char *path, STRUCT_STAT *st, int flags) +{ + RETURN_ERROR_IF_NULL(path); + if (dirfd != VFS_AT_FDCWD) { +#ifdef AT_FDCWD + /* Held-fd: reject empty, multi-component (a '/' would resolve a path + * under the pinned dir) and ".." (escapes to the parent). "." is + * allowed: a read-only fstatat of the dir itself is legitimate + * (link_stat_at). */ + if (!*path || strchr(path, '/') + || (path[0] == '.' && path[1] == '.' && path[2] == '\0')) { + errno = EINVAL; + return -1; + } + return fstatat(dirfd, path, st, 0); +#else + (void)dirfd; errno = ENOSYS; return -1; +#endif + } + if (flags & VFS_ALLOW_SYMLINK) + return vfs__stat_plain(path, st); + return do_xstat_at(path, st, 0, vfs__stat_plain, flags); +} + +int vfs_lstat(int dirfd, const char *path, STRUCT_STAT *st, int flags) +{ + RETURN_ERROR_IF_NULL(path); + if (dirfd != VFS_AT_FDCWD) { +#ifdef AT_FDCWD + /* Held-fd: reject empty, multi-component and ".."; "." is allowed + * (read-only fstatat of the dir itself -- link_stat_at). */ + if (!*path || strchr(path, '/') + || (path[0] == '.' && path[1] == '.' && path[2] == '\0')) { + errno = EINVAL; + return -1; + } +# if defined SUPPORT_LINKS && defined AT_SYMLINK_NOFOLLOW + return fstatat(dirfd, path, st, AT_SYMLINK_NOFOLLOW); +# elif defined SUPPORT_LINKS + /* No AT_SYMLINK_NOFOLLOW: an fstatat via a held dirfd cannot honour + * lstat's no-follow contract, so fail loud rather than silently + * follow the leaf (mirrors the held-fd vfs_lchown ENOSYS arm); a + * symlink-sensitive caller must use the path-based form. */ + (void)dirfd; errno = ENOSYS; return -1; +# else + /* No link support: nothing to follow, fstatat == lstat. */ + return fstatat(dirfd, path, st, 0); +# endif +#else + (void)dirfd; errno = ENOSYS; return -1; +#endif + } + if (flags & VFS_ALLOW_SYMLINK) + return vfs__lstat_plain(path, st); +#if defined SUPPORT_LINKS && defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW + return do_xstat_at(path, st, AT_SYMLINK_NOFOLLOW, vfs__lstat_plain, flags); +#elif defined SUPPORT_LINKS + return vfs__lstat_plain(path, st); +#else + return do_xstat_at(path, st, 0, vfs__stat_plain, flags); +#endif +} + +int vfs_fstat(int fd, STRUCT_STAT *st) +{ +#ifdef USE_STAT64_FUNCS + return fstat64(fd, st); +#else + return fstat(fd, st); +#endif +} + diff --git a/vfs/symlink.c b/vfs/symlink.c new file mode 100644 index 000000000..60b209156 --- /dev/null +++ b/vfs/symlink.c @@ -0,0 +1,259 @@ +/* + * vfs/symlink.c - symlink and readlink wrappers. + * + * Includes the fake-super (NO_SYMLINK_*XATTRS) placeholder-file handling that + * stands in for real symlinks when the receiver can't create them. 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" + +#ifdef SUPPORT_LINKS +static int vfs__symlink_plain(const char *lnk, const char *path) +{ + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + RETURN_ERROR_IF_NULL(lnk); + RETURN_ERROR_IF_NULL(path); + +#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS + /* For --fake-super, we create a normal file with mode 0600 + * and write the lnk into it. */ + if (am_root < 0) { + int ok, len = strlen(lnk); + int fd = open(path, O_WRONLY|O_CREAT|O_TRUNC, S_IWUSR|S_IRUSR); + if (fd < 0) + return -1; + ok = write(fd, lnk, len) == len; + if (close(fd) < 0) + ok = 0; + return ok ? 0 : -1; + } +#endif + + return symlink(lnk, path); +} + +/* + Symlink-race-safe variant of vfs_symlink() for receiver-side use. See + the comment on vfs__chmod_secure() for the threat model. For a real symlink + only the parent directory of `path` needs protection -- symlinkat() + does not resolve the final component (it creates it). Defence: open + the parent of `path` under vfs_resolve_open() and call symlinkat() + against that dirfd; a top-level (no-slash) path has no parent to + confine, so it uses AT_FDCWD directly. The link target string `lnk` is + stored verbatim and not resolved at creation time, so it doesn't need + scrutiny here. + + For --fake-super (am_root < 0) the "symlink" is written as a regular + file, so the final component IS resolved at creation: we create it + with openat(... O_NOFOLLOW) so a pre-planted symlink at the basename + cannot redirect the write outside the module. This protection applies + to top-level paths too -- the previous code fell through to the + bare-path vfs_symlink() there, whose plain open() followed such a + symlink. +*/ +static int vfs__symlink_secure(const char *lnk, const char *path, int flags) +{ +#ifdef AT_FDCWD + char dirpath[MAXPATHLEN]; + const char *bname; + const char *slash; + int dfd = AT_FDCWD, ret, e; + BOOL owns = False; + size_t dlen; + + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + +#if defined O_NOFOLLOW && defined O_DIRECTORY + if (flags & VFS_OPERATOR_PATH) { + /* Operator path (e.g. an absolute --backup-dir): confine the + * parent with the ownership walk, then fall through to the shared + * leaf-creation below so fake-super emulation is preserved. */ + if (vfs_symlink_optout_allowed()) + return vfs__symlink_plain(lnk, path); + dfd = vfs_owner_walk_parent(path, &bname, 1); + if (dfd < 0) + return -1; + owns = True; + } else +#endif + { + (void)flags; + if (!vfs_relpath_active()) + return vfs__symlink_plain(lnk, path); + + if (!path || !*path || *path == '/') + return vfs__symlink_plain(lnk, path); + + /* A path with a slash needs vfs_resolve_open to confine its + * parent; a top-level path is in CWD (AT_FDCWD), no parent to + * subvert. The leaf is protected below either way (symlinkat() + * won't follow it; the fake-super openat() uses O_NOFOLLOW). */ + slash = strrchr(path, '/'); + if (slash) { + dlen = slash - path; + if (dlen >= sizeof dirpath) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dirpath, path, dlen); + dirpath[dlen] = '\0'; + bname = slash + 1; + dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + if (dfd < 0) + return -1; + owns = True; + } else { + bname = path; + } + } + +#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS + /* For --fake-super, vfs_symlink writes the link target into a + * regular file rather than creating a real symlink. Do that here + * against the (secure or AT_FDCWD) dirfd, with O_NOFOLLOW so a pre- + * planted symlink at the basename can't redirect the file creation. */ + if (am_root < 0) { + int len = strlen(lnk); + int fd = openat(dfd, bname, + O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, + S_IWUSR | S_IRUSR); + if (fd < 0) { + e = errno; + if (owns) close(dfd); + errno = e; + return -1; + } + ret = (write(fd, lnk, len) == len) ? 0 : -1; + if (close(fd) < 0) + ret = -1; + e = errno; + if (owns) close(dfd); + errno = e; + return ret; + } +#endif + + ret = symlinkat(lnk, dfd, bname); + e = errno; + if (owns) close(dfd); + errno = e; + return ret; +#else + return vfs__symlink_plain(lnk, path); +#endif +} + +/* NOFOLLOW_HIT_SYMLINK() lives in rsync.h (shared with util1.c's change_dir). */ + +#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS +ssize_t vfs_readlink(const char *path, char *buf, size_t bufsiz) +{ + /* For --fake-super, we read the link from the file. */ + if (am_root < 0) { + int fd = vfs_open_nofollow(path, O_RDONLY); + if (fd >= 0) { + int len = read(fd, buf, bufsiz); + close(fd); + return len; + } + if (!NOFOLLOW_HIT_SYMLINK(errno)) + return -1; + /* A real symlink needs to be turned into a fake one on the receiving + * side, so tell the generator that the link has no length. */ + if (!am_sender) + return 0; + /* Otherwise fall through and let the sender report the real length. */ + } + + return readlink(path, buf, bufsiz); +} +#endif + +ssize_t vfs_readlink_atfd(int dfd, const char *name, char *buf, size_t bufsiz) +{ +#ifdef AT_FDCWD +# if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS + if (am_root < 0) { + int fd = openat(dfd, name, O_RDONLY | O_NOFOLLOW); + if (fd >= 0) { + int len = read(fd, buf, bufsiz); + close(fd); + return len; + } + if (!NOFOLLOW_HIT_SYMLINK(errno)) + return -1; + if (!am_sender) + return 0; + } +# endif + return readlinkat(dfd, name, buf, bufsiz); +#else + (void)dfd; + return vfs_readlink(name, buf, bufsiz); +#endif +} +#endif + +static int vfs__symlink_atfd(const char *lnk, int dfd, const char *name) +{ +#ifdef AT_FDCWD + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + +#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS + /* --fake-super: store the link target in a regular placeholder file, + * created with O_NOFOLLOW so a planted basename symlink can't redirect + * the write (mirrors vfs__symlink_secure()). */ + if (am_root < 0) { + int len = strlen(lnk); + int ok; + int fd = openat(dfd, name, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, + S_IWUSR | S_IRUSR); + if (fd < 0) + return -1; + ok = write(fd, lnk, len) == len; + if (close(fd) < 0) + ok = 0; + return ok ? 0 : -1; + } +#endif + return symlinkat(lnk, dfd, name); +#else + (void)lnk; (void)dfd; (void)name; + errno = ENOSYS; + return -1; +#endif +} + +/* Unified symlink creation. dirfd == VFS_AT_FDCWD resolves `path`; a real held + * dirfd makes `path` a single validated component under it. flags: + * VFS_ALLOW_SYMLINK (trusted, plain symlink), VFS_OPERATOR_PATH (operator + * path: ownership walk), default 0 (secure receiver resolve). */ +int vfs_symlink(const char *lnk, int dirfd, const char *path, int flags) +{ + if (dirfd != VFS_AT_FDCWD) { + if (!path || !*path || strchr(path, '/') + || (path[0] == '.' && (path[1] == '\0' + || (path[1] == '.' && path[2] == '\0')))) { + errno = EINVAL; + return -1; + } + return vfs__symlink_atfd(lnk, dirfd, path); + } + if (flags & VFS_ALLOW_SYMLINK) + return vfs__symlink_plain(lnk, path); + return vfs__symlink_secure(lnk, path, flags); +} diff --git a/vfs/times.c b/vfs/times.c new file mode 100644 index 000000000..d88981bbb --- /dev/null +++ b/vfs/times.c @@ -0,0 +1,380 @@ +/* + * vfs/times.c - timestamp-setting wrappers (utimensat/lutimes/utimes/utime) + * plus the macOS setattrlist crtime path and the Cygwin SetFileTime path. + * + * 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" +#ifdef HAVE_SYS_ATTR_H +#include +#endif + +#ifdef SUPPORT_CRTIMES +#ifdef HAVE_GETATTRLIST +#pragma pack(push, 4) +struct create_time { + uint32 length; + struct timespec crtime; +}; +#pragma pack(pop) +#elif defined __CYGWIN__ +#include +#endif +#endif + +#ifdef HAVE_SETATTRLIST +int vfs_setattrlist_times(const char *path, STRUCT_STAT *stp) +{ + struct attrlist attrList; + struct timespec ts[2]; + + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + + /* setattrlist() takes a raw path and follows parent symlinks + * (FSOPT_NOFOLLOW only blocks the final component). When hardened + * resolution is active -- vfs_relpath_active(): any non-chroot + * daemon/receiver module, plus a /./ inner-module chroot -- return + * ENOSYS so set_times()' tier walk falls through to vfs_utimensat_at(), + * which routes the update through a secure parent dirfd. The attribute + * set this would have used (ATTR_CMN_MODTIME / ATTR_CMN_ACCTIME) is the + * same set utimensat() handles, so no functionality is lost. */ + if (vfs_relpath_active()) { + errno = ENOSYS; + return -1; + } + + /* Yes, this is in the opposite order of utime and similar. */ + ts[0].tv_sec = stp->st_mtime; + ts[0].tv_nsec = stp->ST_MTIME_NSEC; + + ts[1].tv_sec = stp->st_atime; + ts[1].tv_nsec = stp->ST_ATIME_NSEC; + + memset(&attrList, 0, sizeof attrList); + attrList.bitmapcount = ATTR_BIT_MAP_COUNT; + attrList.commonattr = ATTR_CMN_MODTIME | ATTR_CMN_ACCTIME; + return setattrlist(path, &attrList, ts, sizeof ts, FSOPT_NOFOLLOW); +} + +#ifdef SUPPORT_CRTIMES +int vfs_setattrlist_crtime(const char *path, time_t crtime) +{ + struct attrlist attrList; + struct timespec ts; + + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + + /* setattrlist() is path-based and follows parent symlinks + * (FSOPT_NOFOLLOW only blocks the final component), and macOS has no + * at-aware variant targeting ATTR_CMN_CRTIME. As with POSIX ACLs where + * the OS offers no race-safe primitive, we keep --crtimes functional + * (daemon and non-daemon) and accept the parent-component symlink race + * as a documented residual rather than dropping crtime. A daemon + * operator who does not want that residual can disable the feature with + * "refuse options = crtimes" in rsyncd.conf. */ + ts.tv_sec = crtime; + ts.tv_nsec = 0; + + memset(&attrList, 0, sizeof attrList); + attrList.bitmapcount = ATTR_BIT_MAP_COUNT; + attrList.commonattr = ATTR_CMN_CRTIME; + return setattrlist(path, &attrList, &ts, sizeof ts, FSOPT_NOFOLLOW); +} +#endif +#endif /* HAVE_SETATTRLIST */ + +#ifdef SUPPORT_CRTIMES +time_t vfs_get_create_time(const char *path, STRUCT_STAT *stp) +{ +#ifdef HAVE_GETATTRLIST + static struct create_time attrBuf; + struct attrlist attrList; + + (void)stp; + /* getattrlist() is path-based and follows parent symlinks; like + * vfs_setattrlist_crtime() there is no race-safe variant, so reading the + * source crtime stays functional and the parent-component symlink race + * is an accepted residual (refusable via "refuse options = crtimes"). */ + memset(&attrList, 0, sizeof attrList); + attrList.bitmapcount = ATTR_BIT_MAP_COUNT; + attrList.commonattr = ATTR_CMN_CRTIME; + if (getattrlist(path, &attrList, &attrBuf, sizeof attrBuf, FSOPT_NOFOLLOW) < 0) + return 0; + return attrBuf.crtime.tv_sec; +#elif defined __CYGWIN__ + (void)path; + return stp->st_birthtime; +#else +#error Unknown crtimes implementation +#endif +} + +#if defined __CYGWIN__ +int vfs_SetFileTime(const char *path, time_t crtime) +{ + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + + int cnt = MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0); + if (cnt == 0) + return -1; + WCHAR *pathw = new_array(WCHAR, cnt); + if (!pathw) + return -1; + MultiByteToWideChar(CP_UTF8, 0, path, -1, pathw, cnt); + HANDLE handle = CreateFileW(pathw, FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + free(pathw); + if (handle == INVALID_HANDLE_VALUE) + return -1; + int64 temp_time = (crtime * 10000000LL) + 116444736000000000LL; + FILETIME birth_time; + birth_time.dwLowDateTime = (DWORD)temp_time; + birth_time.dwHighDateTime = (DWORD)(temp_time >> 32); + int ok = SetFileTime(handle, &birth_time, NULL, NULL); + CloseHandle(handle); + return ok ? 0 : -1; +} +#endif +#endif /* SUPPORT_CRTIMES */ + +#ifdef HAVE_UTIMENSAT +int vfs_utimensat(const char *path, STRUCT_STAT *stp) +{ + struct timespec t[2]; + + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + RETURN_ERROR_IF_NULL(path); + + 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 utimensat(AT_FDCWD, path, t, AT_SYMLINK_NOFOLLOW); +} + +/* + Symlink-race-safe variant of vfs_utimensat() for receiver-side use. + See the comment on vfs__chmod_secure() for the threat model. utimes() + resolves parent components and follows a final-component symlink; + lutimes() doesn't follow the final component but still resolves + parents. Either way, a parent-symlink swap can redirect the + timestamp update outside the module. Defence: open the parent of + path under vfs_resolve_open() and call utimensat() with + AT_SYMLINK_NOFOLLOW against that dirfd. + + Falls through to vfs_utimensat() in the same dry-run / non-daemon / + chrooted / no-parent / absolute-path cases as the other wrappers. + Returns -1 with errno=ENOSYS on systems without utimensat() + (caller is expected to fall back to the legacy tier walk). +*/ +int vfs_utimensat_at(const char *path, STRUCT_STAT *stp) +{ +#ifdef AT_FDCWD + struct timespec t[2]; + 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 (!vfs_relpath_active()) + return vfs_utimensat(path, stp); + + if (!path || !*path || *path == '/') + return vfs_utimensat(path, stp); + + slash = strrchr(path, '/'); + if (!slash) + return vfs_utimensat(path, stp); + + dlen = slash - path; + if (dlen >= sizeof dirpath) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dirpath, path, dlen); + dirpath[dlen] = '\0'; + bname = slash + 1; + + 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 + + dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + if (dfd < 0) + return -1; + + ret = utimensat(dfd, bname, t, AT_SYMLINK_NOFOLLOW); + e = errno; + close(dfd); + errno = e; + return ret; +#else + return vfs_utimensat(path, stp); +#endif +} +#endif + +#ifdef HAVE_LUTIMES +int vfs_lutimes(const char *path, STRUCT_STAT *stp) +{ + struct timeval 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_usec = stp->ST_ATIME_NSEC / 1000; +#else + t[0].tv_usec = 0; +#endif + t[1].tv_sec = stp->st_mtime; +#ifdef ST_MTIME_NSEC + t[1].tv_usec = stp->ST_MTIME_NSEC / 1000; +#else + t[1].tv_usec = 0; +#endif + return lutimes(path, t); +} +#endif + +#ifdef HAVE_UTIMES +int vfs_utimes(const char *path, STRUCT_STAT *stp) +{ + struct timeval 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_usec = stp->ST_ATIME_NSEC / 1000; +#else + t[0].tv_usec = 0; +#endif + t[1].tv_sec = stp->st_mtime; +#ifdef ST_MTIME_NSEC + t[1].tv_usec = stp->ST_MTIME_NSEC / 1000; +#else + t[1].tv_usec = 0; +#endif + return utimes(path, t); +} + +#elif defined HAVE_UTIME +int vfs_utime(const char *path, STRUCT_STAT *stp) +{ +#ifdef HAVE_STRUCT_UTIMBUF + struct utimbuf tbuf; +#else + time_t t[2]; +#endif + + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + +# ifdef HAVE_STRUCT_UTIMBUF + tbuf.actime = stp->st_atime; + tbuf.modtime = stp->st_mtime; + return utime(path, &tbuf); +# else + t[0] = stp->st_atime; + t[1] = stp->st_mtime; + return utime(path, t); +# endif +} + +#else +#error Need utimes or utime function. +#endif + +#ifdef HAVE_UTIMENSAT +int vfs_utimensat_atfd(int dfd, const char *name, STRUCT_STAT *stp) +{ +#ifdef AT_FDCWD + 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 utimensat(dfd, name, t, AT_SYMLINK_NOFOLLOW); +#else + (void)dfd; (void)name; (void)stp; + errno = ENOSYS; + return -1; +#endif +} +#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 vfs_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 diff --git a/vfs/unlink.c b/vfs/unlink.c new file mode 100644 index 000000000..e6c732945 --- /dev/null +++ b/vfs/unlink.c @@ -0,0 +1,107 @@ +/* + * vfs/unlink.c - unlink and rmdir wrappers (path, parent-resolved, held-dirfd). + * + * 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" + +/* Secure receiver-side resolve for an unlink/rmdir. unlink() resolves parent + * components, so a parent-symlink swap can delete an outside file under the + * daemon's authority -- defence is to resolve the parent securely and unlinkat() + * the leaf. Both unlink and rmdir honour the operator ownership walk + * (VFS_OPERATOR_PATH): a foreign-owned parent component is refused while the + * operator's own is followed, absolute and relative alike. + * Falls through to a plain unlink()/rmdir() in non-daemon/sender, chrooted, + * no-parent and absolute-path cases. */ +static int vfs__unlink_secure(const char *path, int flags) +{ + int rmdir_op = flags & VFS_REMOVEDIR; +#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY + char dirpath[MAXPATHLEN]; + const char *bname, *slash; + int dfd, ret, e, atflag = rmdir_op ? AT_REMOVEDIR : 0; + size_t dlen; + + if (flags & VFS_OPERATOR_PATH) { + if (vfs_symlink_optout_allowed()) + return rmdir_op ? rmdir(path) : unlink(path); + dfd = vfs_owner_walk_parent(path, &bname, 1); + if (dfd < 0) + return -1; + ret = unlinkat(dfd, bname, atflag); + e = errno; + close(dfd); + errno = e; + return ret; + } + + if (!vfs_relpath_active() || !*path || *path == '/') + return rmdir_op ? rmdir(path) : unlink(path); + slash = strrchr(path, '/'); + if (!slash) + return rmdir_op ? rmdir(path) : unlink(path); + dlen = slash - path; + if (dlen >= sizeof dirpath) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dirpath, path, dlen); + dirpath[dlen] = '\0'; + bname = slash + 1; + + dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + if (dfd < 0) + return -1; + ret = unlinkat(dfd, bname, atflag); + e = errno; + close(dfd); + errno = e; + return ret; +#else + return rmdir_op ? rmdir(path) : unlink(path); +#endif +} + +/* Unified unlink/rmdir. dirfd == VFS_AT_FDCWD resolves `path`; a real held + * dirfd makes `path` a single component removed directly under it. flags: + * VFS_REMOVEDIR (rmdir/AT_REMOVEDIR instead of unlink), VFS_ALLOW_SYMLINK + * (trusted, plain), VFS_OPERATOR_PATH (operator path: ownership walk), default 0 + * (secure receiver resolve). */ +int vfs_unlink(int dirfd, const char *path, 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 + if (!*path || strchr(path, '/') + || (path[0] == '.' && (path[1] == '\0' + || (path[1] == '.' && path[2] == '\0')))) { + errno = EINVAL; + return -1; + } + return unlinkat(dirfd, path, (flags & VFS_REMOVEDIR) ? AT_REMOVEDIR : 0); +#else + (void)dirfd; + errno = ENOSYS; + return -1; +#endif + } + + if (flags & VFS_ALLOW_SYMLINK) + return (flags & VFS_REMOVEDIR) ? rmdir(path) : unlink(path); + + return vfs__unlink_secure(path, flags); +} diff --git a/vfs/vfs.c b/vfs/vfs.c new file mode 100644 index 000000000..3e91f4b92 --- /dev/null +++ b/vfs/vfs.c @@ -0,0 +1,46 @@ +/* + * vfs/vfs.c - core state for rsync's virtual filesystem layer. + * + * 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" + +/* The single global VFS state. The designated initializer makes the cache and + * module snapshot safe by construction (a plain definition would zero base/ + * module_dirfd, making fd 0 look like a valid cached dir) -- this matters for + * the t_*_secure test harnesses, which never run main() and so never call + * vfs_init(). */ +struct vfs vfs = { + .dpc = { .base = -1, .anchor = VFS_DPC_ANCHOR_NONE }, + .module_dirfd = -1, +}; + +/* Reset the VFS to a safe state. Idempotent; re-establishes the same safe + * sentinels as the static initializer, and clears the module-root snapshot so + * confinement state can never be stale (no module is attached yet). */ +void vfs_init(void) +{ + vfs.dpc.base = -1; + vfs.dpc.anchor = VFS_DPC_ANCHOR_NONE; + vfs.dpc.depth = 0; + vfs.module_dir = NULL; + vfs.module_dirlen = 0; + vfs.module_dirfd = -1; +} + +/* Snapshot the served daemon module root for the confinement checks. Called + * by clientserver.c once the module path is final (with dirfd == -1), and again + * once the root dirfd is pinned. The dirfd is BORROWED -- open_anchor_dirfd() + * dup()s it; the VFS never closes it. */ +void vfs_set_module_root(const char *dir, unsigned int len, int dirfd) +{ + vfs.module_dir = dir; + vfs.module_dirlen = len; + vfs.module_dirfd = dirfd; +} diff --git a/vfs/vfs.h b/vfs/vfs.h new file mode 100644 index 000000000..b4bdd516e --- /dev/null +++ b/vfs/vfs.h @@ -0,0 +1,241 @@ +/* + * vfs/vfs.h - public interface to rsync's virtual filesystem layer. + * + * The VFS owns the messy, security-critical filesystem details (the vfs_* + * syscall wrappers, the race-safe path resolver, the held-dirfd cache, the + * operator-path ownership walk and the daemon module confinement) so the + * mainline protocol/transfer code can stay clean. Implementations live in the + * per-concern sources under vfs/ in two layers: PRIMITIVES (one file per + * operation family, plus the resolver/dirstack/dircache/owner-walk cores) and + * COMPOUNDS built on them (vfs_make_path, copy_file, robust_unlink/rename). The + * remaining VFS-internal state lives in the single "struct vfs vfs" below. + * + * Each single-path operation is one call taking a dirfd and a flags word: + * + * vfs_(int dirfd, path, ..., int flags) + * + * dirfd == VFS_AT_FDCWD -- resolve `path`, per flags: + * 0 secure receiver resolve (race-safe O_NOFOLLOW parent + * walk + *at() leaf) when vfs_relpath_active(), else plain + * VFS_ALLOW_SYMLINK plain libc op -- the call site asserts the path is + * trusted to follow symlinks (was the bare vfs_()) + * VFS_OPERATOR_PATH operator-supplied path (--backup-dir/--temp-dir/ + * --partial-dir/--link-dest): ownership walk (follow a + * uid0/euid-owned symlink, refuse a foreign one) + + * daemon module-root confinement + * a real held dirfd -- `path` is a single component acted on directly under + * it (from vfs_opendir/vfs_get_dirfd); no resolution. + * + * The operator-path policy is this explicit per-call flag, never ambient state. + * VFS_REMOVEDIR turns vfs_unlink into rmdir. chmod/lchown have no + * ownership-walk branch (VFS_OPERATOR_PATH resolves as the default secure walk). + * Two-path ops (vfs_rename_at, vfs_link_at) and open (distinct nofollow/ + * checklinks variants) keep explicit forms + a vfs_flags arg; vfs_fstat and the + * fileio ops are fd-based. + * + * This header is included by rsync.h (just after proto.h) so every translation + * unit sees the vfs_* API. It must not include rsync.h itself. + * + * 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. + */ + +#ifndef RSYNC_VFS_H +#define RSYNC_VFS_H + +/* Exercise the pre-*at() portability tier on modern build hosts. rsync.h pulls + * this header in after the system headers that define AT_FDCWD, so stripping the + * *at primitives here strips them for the whole translation unit -- including + * the VFS_AT_FDCWD sentinel below, which then takes its no-AT_FDCWD value. The + * CHECK_COMPILE_OBJS target compiles every vfs/ source with this flag to keep + * the fallback arms building. (Was syscall.c's top before the split into vfs/; + * it must precede the VFS_AT_FDCWD definition.) */ +#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 + +/* Max held ancestor-dirfd cache depth (was DPC_MAXDEPTH in syscall.c). */ +#define VFS_DPC_MAXDEPTH 64 + +/* dirfd sentinel for the unified vfs_* ops: "no held directory -- resolve the + * path argument". Always defined (even on platforms without AT_FDCWD, where the + * held-fd forms are unavailable and report ENOSYS), so mainline code never has + * to mention AT_FDCWD directly. */ +#ifdef AT_FDCWD +#define VFS_AT_FDCWD AT_FDCWD +#else +#define VFS_AT_FDCWD (-100) +#endif + +/* Per-call flags for the unified vfs_* operations. The default (0) is the + * secure, no-follow parent resolve used by the receiver; the flags are explicit + * opt-ins that each carry a security meaning the call site is asserting. + * VFS_ALLOW_SYMLINK and VFS_OPERATOR_PATH are mutually exclusive policies; if + * both are passed VFS_ALLOW_SYMLINK wins (it is checked first). No call site + * needs both. */ +#define VFS_ALLOW_SYMLINK (1<<0) /* call site is known-safe to follow symlinks */ +#define VFS_OPERATOR_PATH (1<<1) /* operator-supplied path: ownership walk + module confinement */ +#define VFS_REMOVEDIR (1<<2) /* unlink op targets a directory (AT_REMOVEDIR) */ + +/* Sentinel for dpc.anchor meaning "no anchor cached" -- a non-NULL, non-pointer + * value so it can never alias a real anchor path (NULL is a valid anchor: cwd). */ +#define VFS_DPC_ANCHOR_NONE ((const char *)-2) + +/* The single global VFS state instance (defined in vfs/vfs.c). + * + * Only curr_dir/curr_dir_len are read by mainline code; the dpc cache and the + * module_* snapshot are VFS-internal (touched only by the vfs/ sources) and are + * documented as such. The operator-supplied path resolution policy is no + * longer ambient state -- it travels as an explicit VFS_OPERATOR_PATH flag. */ +struct vfs { + char curr_dir[MAXPATHLEN]; /* logical cwd (tracked by change_dir) */ + unsigned int curr_dir_len; + + /* VFS-INTERNAL: held ancestor-dirfd cache. */ + struct { + const char *anchor; /* anchor path, or VFS_DPC_ANCHOR_NONE */ + int base; /* owned anchor dir fd, or -1 */ + int fd[VFS_DPC_MAXDEPTH]; /* fd after components 0..i */ + char name[VFS_DPC_MAXDEPTH][256]; /* component names */ + int depth; + } dpc; + + /* VFS-INTERNAL: daemon served-module-root snapshot. */ + const char *module_dir; + unsigned int module_dirlen; + int module_dirfd; /* identity-pinned module root fd, or -1 */ +}; + +extern struct vfs vfs; + +/* Reset the VFS to a safe between-transfers state. Safety at startup comes + * from the static initializer in vfs/vfs.c, not from this call. */ +void vfs_init(void); + +/* Snapshot the served daemon module root (clientserver.c calls this); the + * dirfd is borrowed, never closed by the VFS. */ +void vfs_set_module_root(const char *dir, unsigned int len, int dirfd); + +/* Race-safe path resolution (vfs/secure_open.c). */ +int vfs_relpath_active(void); +int vfs_symlink_optout_allowed(void); +int vfs_resolve_open(const char *basedir, const char *relpath, int flags, mode_t mode); +int vfs_resolve_open_at(int anchor_fd, const char *relpath, int flags, mode_t mode); +int vfs_resolve_open_at_beneath(int anchor_fd, const char *relpath, int flags, mode_t mode); + +/* STRICT_CONFINEMENT enforcement (vfs/secure_open.c): a build-time-gated CI + * harness that turns a confined-regime raw path metadata op into a hard failure. + * vfs_must_be_confined() is the predicate; vfs_strict_confine_fail() the hard + * stop, only compiled/called under STRICT_CONFINEMENT. */ +int vfs_must_be_confined(const char *path, int is_operator); +#ifdef STRICT_CONFINEMENT +void vfs_strict_confine_fail(const char *path, const char *what); +#endif + +/* Operator-supplied-path resolution by ownership (vfs/owner_walk.c). */ +int vfs_open_owner_walk(const char *path, int flags, mode_t mode, int is_operator); +int vfs_owner_walk_parent(const char *path, const char **bname, int is_operator); + +/* Held ancestor-dirfd cache for directory traversal (vfs/dircache.c). */ +int vfs_opendir(const char *dirname); +int vfs_get_dirfd(const char *dirname); +int vfs_path_dirfd(const char *anchor, const char *dirpath); +int vfs_cached_dirfd(const char *path, const struct file_struct *file); +void vfs_dircache_reset(void); + +/* stat/lstat/fstat (vfs/stat.c). */ +int vfs_stat(int dirfd, const char *path, STRUCT_STAT *st, int flags); +int vfs_lstat(int dirfd, const char *path, STRUCT_STAT *st, int flags); +int vfs_fstat(int fd, STRUCT_STAT *st); + +/* rename (vfs/rename.c). */ +int vfs_rename(const char *old_path, const char *new_path); +int vfs_rename_at(const char *old_path, const char *new_path, int old_flags, int new_flags); +int vfs_rename_atfd(int old_dfd, const char *old_name, int new_dfd, const char *new_name); + +/* unlink and rmdir (vfs/unlink.c). */ +int vfs_unlink(int dirfd, const char *path, int flags); + +/* open (vfs/open.c). */ +int vfs_open(const char *pathname, int flags, mode_t mode); +int vfs_open_at(const char *pathname, int flags, mode_t mode, int vfs_flags); +int vfs_open_atfd(int dfd, const char *name, int flags, mode_t mode); +int vfs_open_nofollow(const char *pathname, int flags); +int vfs_open_checklinks(const char *pathname); + +/* chmod (vfs/chmod.c). */ +int vfs_chmod(int dirfd, const char *path, mode_t mode, int flags); +#ifdef HAVE_CHMOD +int vfs_fchmod(int fd, mode_t mode); +#endif + +/* symlink/readlink (vfs/symlink.c). vfs_readlink is a function only in + * fake-super builds; otherwise it is a macro -> readlink() (see rsync.h). */ +int vfs_symlink(const char *lnk, int dirfd, const char *path, int flags); +ssize_t vfs_readlink_atfd(int dfd, const char *name, char *buf, size_t bufsiz); +#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS +ssize_t vfs_readlink(const char *path, char *buf, size_t bufsiz); +#endif + +/* hard links (vfs/link.c). */ +int vfs_link(const char *old_path, const char *new_path); +int vfs_link_at(const char *old_path, const char *new_path, int old_flags, int new_flags); +int vfs_link_atfd(int old_dfd, const char *old_name, int new_dfd, const char *new_name, int flags); + +/* mkdir / mkstemp and the trim_trailing_slashes path helper (vfs/mkdir.c). */ +void trim_trailing_slashes(char *name); +/* dirfd == VFS_AT_FDCWD: resolve `path` (secure parent walk unless + * VFS_ALLOW_SYMLINK / VFS_OPERATOR_PATH); a real held dirfd: `path` is a single + * component created directly under it. */ +int vfs_mkdir(int dirfd, char *path, mode_t mode, int flags); +int vfs_mkstemp(char *template, mode_t perms); +int vfs_mkstemp_atfd(int dfd, char *filename, mode_t perms); +int vfs_secure_mkstemp(char *template, mode_t perms, int operator_path); + +/* lchown (vfs/chown.c). */ +int vfs_lchown(int dirfd, const char *path, uid_t owner, gid_t group, int flags); +int vfs_fchown(int fd, uid_t owner, gid_t group); + +/* device/fifo/socket node creation (vfs/mknod.c). */ +int vfs_mknod(int dirfd, const char *path, mode_t mode, dev_t dev, int flags); + +/* timestamp setting + crtimes (vfs/times.c). */ +int vfs_setattrlist_times(const char *path, STRUCT_STAT *stp); +int vfs_setattrlist_crtime(const char *path, time_t crtime); +time_t vfs_get_create_time(const char *path, STRUCT_STAT *stp); +int vfs_SetFileTime(const char *path, time_t crtime); + +/* Compound operations (vfs/.c): filesystem mechanics layered on the + * primitives above. The resolution policy travels as an explicit vfs_flags + * argument (VFS_OPERATOR_PATH for operator-supplied paths, else 0). */ +int vfs_make_path(char *fname, int mkp_flags, int vfs_flags); +int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode, int vfs_flags); +int robust_unlink(const char *fname, int vfs_flags); +int robust_rename(const char *from, const char *to, const char *partialptr, + int mode, struct file_struct *file); +int vfs_utimensat(const char *path, STRUCT_STAT *stp); +int vfs_utimensat_at(const char *path, STRUCT_STAT *stp); +int vfs_utimensat_atfd(int dfd, const char *name, STRUCT_STAT *stp); +#ifdef HAVE_FUTIMENS +int vfs_futimens(int fd, STRUCT_STAT *stp); +#endif +int vfs_lutimes(const char *path, STRUCT_STAT *stp); +int vfs_utimes(const char *path, STRUCT_STAT *stp); +int vfs_utime(const char *path, STRUCT_STAT *stp); + +/* fd-based file-data ops (vfs/fileio.c). */ +int vfs_ftruncate(int fd, OFF_T size); +OFF_T vfs_lseek(int fd, OFF_T offset, int whence); +OFF_T vfs_fallocate(int fd, OFF_T offset, OFF_T length); +int vfs_punch_hole(int fd, OFF_T pos, OFF_T len); + +#endif /* RSYNC_VFS_H */ diff --git a/vfs/vfs_internal.h b/vfs/vfs_internal.h new file mode 100644 index 000000000..e320c0ef2 --- /dev/null +++ b/vfs/vfs_internal.h @@ -0,0 +1,98 @@ +/* + * vfs/vfs_internal.h - private interface shared among the vfs/ sources (and, + * during the syscall.c -> vfs/ migration, by syscall.c itself). NOT for use by + * mainline rsync code; the public surface is vfs/vfs.h. + * + * 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. + */ + +#ifndef RSYNC_VFS_INTERNAL_H +#define RSYNC_VFS_INTERNAL_H + +/* Option/daemon globals the VFS internals read (defined in options.c / + * clientserver.c / syscall.c). Centralized here so each vfs/ source picks them + * up from one place rather than re-declaring them. */ +extern int dry_run; +extern int am_root; +extern int am_sender; +extern int am_daemon; +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 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) \ + do { \ + if (x) { \ + errno = (e); \ + return -1; \ + } \ + } while (0) + +#define RETURN_ERROR_IF_RO_OR_LO RETURN_ERROR_IF(read_only || list_only, EROFS) + +/* A NULL path reaching one of the path-forwarding wrappers is always a caller + * bug; reject it rather than forwarding NULL to libc. Also quiets the static + * analyzer's interprocedural nonnull false positives. */ +#define RETURN_ERROR_IF_NULL(p) RETURN_ERROR_IF(!(p), EFAULT) + +/* Module-confinement helpers (pure logic, always compiled). */ +int path_has_dotdot_component(const char *path); +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, + int *dfd_out, BOOL *owns, char *dirbuf, size_t dirbufsz); + +#if defined(O_NOFOLLOW) && defined(O_DIRECTORY) && defined(AT_FDCWD) + +#ifndef SECURE_OPEN_MAXSYMLINKS +#define SECURE_OPEN_MAXSYMLINKS 40 +#endif + +/* Max directory levels held open at once during a single resolve. The walk + * holds one fd per component, so depth is bounded by RLIMIT_NOFILE anyway; a + * fixed array (no malloc/realloc) keeps the stack simple and the static + * analyzer happy. Mirrors DPC_MAXDEPTH's fixed-cap approach. */ +#define DS_MAXDEPTH 1024 + +/* The component-walk dirfd stack used by the secure resolver: a stack of the + * open dirfds from the anchor (index 0, borrowed) down to the current dir. */ +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_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]; +}; + +int open_anchor_dirfd(const char *path); +int ds_init(struct dirstack *ds, int anchor); +void ds_free(struct dirstack *ds); +int ds_cur(struct dirstack *ds); +int ds_take(struct dirstack *ds); +int ds_descend(struct dirstack *ds, const char *part, int *hops); +int ds_walk_path(struct dirstack *ds, char *path, int *hops); + +#endif /* O_NOFOLLOW && O_DIRECTORY && AT_FDCWD */ + +#endif /* RSYNC_VFS_INTERNAL_H */ diff --git a/xattrs.c b/xattrs.c index ab7120be1..b167eac20 100644 --- a/xattrs.c +++ b/xattrs.c @@ -1154,7 +1154,7 @@ int set_xattr(const char *fname, const struct file_struct *file, const char *fna #endif && access(fname, W_OK) < 0 && (fd >= 0 ? fchmod(fd, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR) - : do_chmod_at(fname, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR)) == 0) + : vfs_chmod(VFS_AT_FDCWD, fname, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR, 0)) == 0) added_write_perm = 1; ndx = F_XATTR(file); @@ -1166,7 +1166,7 @@ int set_xattr(const char *fname, const struct file_struct *file, const char *fna if (fd >= 0) fchmod(fd, sxp->st.st_mode); else - do_chmod_at(fname, sxp->st.st_mode); + vfs_chmod(VFS_AT_FDCWD, fname, sxp->st.st_mode, 0); } return 0; } @@ -1177,7 +1177,7 @@ int set_xattr(const char *fname, const struct file_struct *file, const char *fna if (fd >= 0) fchmod(fd, sxp->st.st_mode); else - do_chmod_at(fname, sxp->st.st_mode); + vfs_chmod(VFS_AT_FDCWD, fname, sxp->st.st_mode, 0); } return return_value; } @@ -1286,7 +1286,7 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode, } if (fd >= 0) { - if (do_fstat(fd, &fst) < 0) { + if (vfs_fstat(fd, &fst) < 0) { rsyserr(FERROR_XFER, errno, "failed to re-stat %s", full_fname(fname)); return -1; @@ -1295,7 +1295,7 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode, xst = fst; /* keep xst fully defined; st_mode=0 means "no stat xattr" */ xst.st_mode = 0; } - } else if (x_lstat(fname, &fst, &xst) < 0) { + } else if (x_lstat(fname, &fst, &xst, 0) < 0) { rsyserr(FERROR_XFER, errno, "failed to re-stat %s", full_fname(fname)); return -1; @@ -1317,7 +1317,7 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode, if (fd >= 0) fchmod(fd, mode); else - do_chmod_at(fname, mode); + vfs_chmod(VFS_AT_FDCWD, fname, mode, 0); } if (!IS_DEVICE(fst.st_mode)) fst.st_rdev = 0; /* just in case */ @@ -1356,22 +1356,23 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode, return 0; } -int x_stat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst) +int x_stat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst, int vfs_flags) { /* Use the *_at variants so that on a daemon-no-chroot deployment * the metadata read goes through a secure parent dirfd instead * of bare path resolution. The *_at wrappers fall through to - * plain do_stat outside the daemon-no-chroot context, so this - * change is transparent for non-daemon use. */ - int ret = do_stat_at(fname, fst); + * plain vfs_stat outside the daemon-no-chroot context, so this + * change is transparent for non-daemon use. vfs_flags carries the + * operator-path policy (VFS_OPERATOR_PATH for a backup-dir stat). */ + int ret = vfs_stat(VFS_AT_FDCWD, fname, fst, vfs_flags); if ((ret < 0 || get_stat_xattr(fname, -1, fst, xst) < 0) && xst) xst->st_mode = 0; return ret; } -int x_lstat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst) +int x_lstat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst, int vfs_flags) { - int ret = do_lstat_at(fname, fst); + int ret = vfs_lstat(VFS_AT_FDCWD, fname, fst, vfs_flags); if ((ret < 0 || get_stat_xattr(fname, -1, fst, xst) < 0) && xst) xst->st_mode = 0; return ret; @@ -1379,7 +1380,7 @@ int x_lstat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst) int x_fstat(int fd, STRUCT_STAT *fst, STRUCT_STAT *xst) { - int ret = do_fstat(fd, fst); + int ret = vfs_fstat(fd, fst); if ((ret < 0 || get_stat_xattr(NULL, fd, fst, xst) < 0) && xst) xst->st_mode = 0; return ret;