Version in base suite: 3.4.1+ds1-5+deb13u4 Base version: rsync_3.4.1+ds1-5+deb13u4 Target version: rsync_3.5.0+ds1-0+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/r/rsync/rsync_3.4.1+ds1-5+deb13u4.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/r/rsync/rsync_3.5.0+ds1-0+deb13u1.dsc .gitattributes | 16 .gitignore | 14 INSTALL.md | 28 Makefile.in | 275 NEWS.md | 889 ++ README.md | 11 SECURITY.md | 529 + TODO | 11 access.c | 30 aclocal.m4 | 4 acls.c | 401 + authenticate.c | 108 backup.c | 162 batch.c | 76 byteorder.h | 31 checksum.c | 25 chmod.c | 96 cleanup.c | 12 clientname.c | 2 clientserver.c | 274 compat.c | 38 config.h.in | 327 configure.ac | 199 configure.sh | 3647 ++++++---- connection.c | 4 daemon-parm.awk | 11 daemon-parm.txt | 3 debian/changelog | 108 debian/control | 2 debian/copyright | 5 debian/patches/2026-05-20/0001-bool-is-a-keyword-in-C23.patch | 26 debian/patches/2026-05-20/0002-syscall-fix-a-Y2038-bug-by-replacing-Int32x32To64-wi.patch | 29 debian/patches/2026-05-20/0003-options.c-Fix-segv-if-poptGetContext-returns-NULL.patch | 35 debian/patches/2026-05-20/0004-Using-a-correct-time-in-log-file.patch | 53 debian/patches/2026-05-20/0005-configure.ac-check-for-xattr-support-both-in-libc-an.patch | 54 debian/patches/2026-05-20/0006-util-fixed-issue-in-clean_fname.patch | 43 debian/patches/2026-05-20/0007-testsuite-added-clean-fname-underflow-test.patch | 85 debian/patches/2026-05-20/0008-fixed-an-invalid-access-to-files-array.patch | 30 debian/patches/2026-05-20/0009-fix-uninitialized-buf1-in-get_checksum2-MD4-path.patch | 33 debian/patches/2026-05-20/0010-reject-negative-token-values-in-compressed-stream-re.patch | 72 debian/patches/2026-05-20/0011-acl-fixed-ACL-ID-mapping-for-non-root.patch | 26 debian/patches/2026-05-20/0012-fix-uninitialized-mul_one-in-AVX2-checksum-and-add-S.patch | 205 debian/patches/2026-05-20/0013-Fix-glibc-2.43-constness-warnings.patch | 115 debian/patches/2026-05-20/0015-fix-signed-integer-overflow-in-proxy-protocol-v2-hea.patch | 38 debian/patches/2026-05-20/0016-zero-all-new-memory-from-allocations.patch | 48 debian/patches/2026-05-20/0017-xattrs-fixed-count-in-qsort.patch | 35 debian/patches/2026-05-20/0018-call-tzset-before-chroot-to-cache-timezone-data.patch | 39 debian/patches/2026-05-20/0019-testsuite-xattrs-ignore-SUNWattr_-in-the-Solaris-xls.patch | 45 debian/patches/2026-05-20/0020-syscall-use-openat2-RESOLVE_BENEATH-on-Linux-for-sec.patch | 389 - debian/patches/2026-05-20/0021-syscall-also-use-O_RESOLVE_BENEATH-on-FreeBSD-and-Ma.patch | 96 debian/patches/2026-05-20/0022-testsuite-skip-symlink-dirlink-basis-on-platforms-wi.patch | 49 debian/patches/2026-05-20/0023-syscall-clientserver-am_chrooted-and-use_secure_syml.patch | 325 debian/patches/2026-05-20/0024-sender-fix-read-path-TOCTOU-by-opening-from-module-r.patch | 68 debian/patches/2026-05-20/0025-syscall-receiver-secure-receiver-side-do_chmod-again.patch | 463 - debian/patches/2026-05-20/0026-util1-secure-change_dir-against-symlink-race-chdir-e.patch | 203 debian/patches/2026-05-20/0027-syscall-add-symlink-race-safe-do_-_at-wrappers-and-h.patch | 1774 ---- debian/patches/2026-05-20/0028-util1-syscall-secure-copy_file-source-dest-opens-bar.patch | 565 - debian/patches/2026-05-20/0029-testsuite-end-to-end-regression-test-for-chdir-symli.patch | 289 debian/patches/2026-05-20/0030-token-harden-compressed-token-decoding-against-integ.patch | 251 debian/patches/2026-05-20/0031-testsuite-cover-refuse-options-compress-for-the-daem.patch | 81 debian/patches/2026-05-20/0032-receiver-add-parent_ndx-0-guard-mirroring-797e17f.patch | 119 debian/patches/2026-05-20/0033-clientserver-fix-hostname-ACL-bypass-when-using-daem.patch | 192 debian/patches/2026-05-20/0034-defence-in-depth-bound-wire-supplied-counts-and-leng.patch | 261 debian/patches/2026-05-20/0035-defence-in-depth-guard-cumulative-snprintf-against-l.patch | 79 debian/patches/2026-05-20/0036-defence-in-depth-receiver-block-index-bounds-read_de.patch | 80 debian/patches/CVE-2026-45232.patch | 236 debian/patches/Fix_ENOENT_when_resolving_kernel_pseudo_paths_in_ona_open_patch | 312 debian/patches/Fix_handling_of_dev_std_in_out_err_pseudo-paths_and_namespace_overflow_UID_checks.patch | 673 + debian/patches/Honor_STRIP_in_install-strip_for_cross-compilation.patch | 70 debian/patches/batch_c_Allow_FIFO_pipes_in_batch_file_processing.patch | 169 debian/patches/disable_reconfigure_req.diff | 6 debian/patches/env_shebang.patch | 10 debian/patches/fix-flaky-hardlinks-test.patch | 60 debian/patches/fix_rrsync_man_generation.patch | 2 debian/patches/options_accept_max_alloc_0_again_resolved_to_the_parsers_own_ceiling.patch | 383 + debian/patches/options_c_Fix_files-from_confinement_for_local_and_SSH_transfers.patch | 134 debian/patches/receiver_c_Tighten_alt-dest_path_resolution_and_partial-dir_state_validation.patch | 343 debian/patches/rrsync_restore_restricted-root_paths.patch | 164 debian/patches/rrsync_support_fd_pins_in_user_namespaces.patch | 233 debian/patches/series | 62 debian/patches/skip_devices_test_non_linux.patch | 61 debian/patches/syscall_explain_untrusted_symlink_refusal.patch | 54 debian/patches/syscall_follow_trusted_sender_ancestor_links.patch | 1177 +++ debian/patches/syscall_use_O_PATH_for_directory_traversal.patch | 159 debian/patches/syscall_use_O_PATH_for_held_directory_traversal.patch | 790 ++ debian/patches/testsuite_bound_the_unshare_probe.patch | 36 debian/patches/testsuite_interpose_lfs_open_symbols.patch | 207 debian/patches/testsuite_make_basis_xname_oracle_deterministic.patch | 269 debian/patches/testsuite_punch_granularity_guard.patch | 137 debian/rsync.NEWS | 68 debian/rules-pre-dh | 131 debian/tests/control | 12 debian/tests/local-tests | 10 debian/tests/remote-tests | 4 debian/tests/upstream-tests | 8 debian/tests/upstream-tests-as-root | 8 delete.c | 73 doc/README-SGML | 20 doc/profile.txt | 42 doc/rsync.sgml | 351 exclude.c | 399 - fileio.c | 116 flist.c | 417 + generator.c | 617 + getgroups.c | 1 hashtable.c | 35 hlink.c | 29 io.c | 408 - latest-year.h | 2 lib/acl.c | 451 + lib/acl.h | 74 lib/md5-asm-x86_64.S | 4 lib/md5.c | 2 lib/mdfour.c | 4 lib/pool_alloc.c | 45 lib/sysacls.c | 158 lib/sysacls.h | 11 lib/sysxattrs.c | 179 lib/sysxattrs.h | 3 lib/wildmatch.c | 17 loadparm.c | 137 log.c | 85 main.c | 166 match.c | 41 maybe-make-man | 2 md-convert | 5 md2man | 5 mkgitver | 18 mkproto.awk | 1 old_versions/README.md | 87 old_versions/build_static.sh | 128 options.c | 239 packaging/auto-Makefile | 2 packaging/branch-from-patch | 174 packaging/ftp.filt | 2 packaging/lsb/rsync.spec | 6 packaging/patch-update | 244 packaging/pkglib.py | 11 packaging/release-rsync | 414 - packaging/release.py | 714 + packaging/samba-rsync | 124 packaging/send-news | 33 packaging/year-tweak | 56 params.c | 9 receiver.c | 423 + rrsync.1 | 28 rrsync.1.html | 19 rsync-ssl | 44 rsync-ssl.1 | 23 rsync-ssl.1.html | 27 rsync-ssl.1.md | 23 rsync.1 | 1909 ++--- rsync.1.html | 1187 +-- rsync.1.md | 1235 ++- rsync.c | 204 rsync.h | 59 rsync3.txt | 467 - rsyncd.conf.5 | 352 rsyncd.conf.5.html | 299 rsyncd.conf.5.md | 309 rsyncsh.txt | 26 runtests.py | 1024 ++ runtests.sh | 360 sender.c | 361 simd-checksum-avx2.S | 32 simd-checksum-x86_64.cpp | 206 socket.c | 323 support/git-set-file-times | 4 support/rrsh.sh | 21 support/rrsync | 642 + support/rrsync.1.md | 16 support/rsync-no-vanished | 2 syscall.c | 3263 ++++++++ t_acl.c | 532 + t_chmod_secure.c | 156 t_clean_fname.c | 46 t_hashtable_overflow.c | 50 t_iwildmatch.c | 44 t_rename_secure.c | 206 t_safe_arg.c | 56 t_secure_relpath.c | 231 t_stub.c | 23 t_symlink_secure.c | 160 testhelp/maketree.py | 133 testsuite/00-hello.test | 61 testsuite/00-hello_test.py | 104 testsuite/COVERAGE.md | 204 testsuite/README.md | 304 testsuite/README.testsuite | 28 testsuite/abdiff.py | 2824 +++++++ testsuite/acl-symlink-race_test.py | 169 testsuite/acls-default.test | 66 testsuite/acls-default_test.py | 98 testsuite/acls-depth_test.py | 63 testsuite/acls-unpinnable_test.py | 86 testsuite/acls.test | 62 testsuite/acls_test.py | 93 testsuite/alt-dest-deep_test.py | 85 testsuite/alt-dest-symlink-race_test.py | 98 testsuite/alt-dest.test | 68 testsuite/alt-dest_test.py | 79 testsuite/append-shortsum_test.py | 52 testsuite/append_test.py | 73 testsuite/atimes.test | 19 testsuite/atimes_test.py | 33 testsuite/authenticate-no-ocloexec-build-regression_test.py | 82 testsuite/backup-acl-xattr-cache_test.py | 77 testsuite/backup-crossdev-copy_test.py | 110 testsuite/backup-deep_test.py | 86 testsuite/backup-dir-relative_test.py | 47 testsuite/backup-dir-repeated-separator-delete_test.py | 49 testsuite/backup-dir-symlink-race_test.py | 127 testsuite/backup-incremental_test.py | 114 testsuite/backup.test | 63 testsuite/backup_test.py | 133 testsuite/bare-do-open-symlink-race_test.py | 156 testsuite/basis-xname-traversal_test.py | 222 testsuite/batch-file-symlink_test.py | 219 testsuite/batch-mode.test | 51 testsuite/batch-mode_test.py | 95 testsuite/batch-only-remove-source-regression_test.py | 73 testsuite/change-shrink_test.py | 51 testsuite/change-vanish_test.py | 36 testsuite/chdir-symlink-race_test.py | 143 testsuite/checksum-zero-blocklen_test.py | 82 testsuite/chgrp.test | 29 testsuite/chgrp_test.py | 38 testsuite/chmod-option.test | 71 testsuite/chmod-option_test.py | 126 testsuite/chmod-setid_test.py | 33 testsuite/chmod-symlink-race_test.py | 57 testsuite/chmod-temp-dir.test | 41 testsuite/chmod-temp-dir_test.py | 65 testsuite/chmod.test | 30 testsuite/chmod_test.py | 40 testsuite/chown-fake_test.py | 73 testsuite/chown.test | 86 testsuite/chown_test.py | 73 testsuite/chroot-alt-dest-inner-module_test.py | 14 testsuite/chroot-basis-forge-inner-module_test.py | 100 testsuite/chroot-copy-dest-inner-module_test.py | 37 testsuite/chroot-link-dest-inner-module_test.py | 18 testsuite/chroot-receiver-write-inner-module_test.py | 10 testsuite/chroot-special-inner-module_test.py | 12 testsuite/clean-fname-collapse_test.py | 21 testsuite/clean-fname-underflow_test.py | 38 testsuite/cmptree.py | 82 testsuite/compare_test.py | 90 testsuite/compress-options_test.py | 88 testsuite/compress-zlib-insert_test.py | 72 testsuite/connect-prog-host-quoting_test.py | 27 testsuite/connect-prog-nested-singlequote-host-injection_test.py | 105 testsuite/copy-dest-source-symlink_test.py | 95 testsuite/copy-dest-symlink-readleak_test.py | 110 testsuite/copy-xattrs-symlink-race_test.py | 160 testsuite/crtimes.test | 26 testsuite/crtimes_test.py | 38 testsuite/cvs-exclude_test.py | 46 testsuite/daemon-access-ip_test.py | 64 testsuite/daemon-access_test.py | 82 testsuite/daemon-argv-limit_test.py | 46 testsuite/daemon-auth-digest-floor_test.py | 139 testsuite/daemon-auth-group_test.py | 171 testsuite/daemon-auth-users-comma-only_test.py | 180 testsuite/daemon-auth_test.py | 92 testsuite/daemon-chroot-acl_test.py | 144 testsuite/daemon-chroot-munge-default_test.py | 107 testsuite/daemon-chroot_test.py | 119 testsuite/daemon-config-symlink_test.py | 126 testsuite/daemon-config_test.py | 52 testsuite/daemon-copy-links-symlink_test.py | 65 testsuite/daemon-copylinks-intree_test.py | 49 testsuite/daemon-copylinks-parent-escape_test.py | 87 testsuite/daemon-copylinks-parent-target-regression_test.py | 55 testsuite/daemon-delete-stats_test.py | 48 testsuite/daemon-deny-dns-failopen_test.py | 70 testsuite/daemon-dot-file-force-wipe_test.py | 60 testsuite/daemon-early-exec-nameconv_test.py | 146 testsuite/daemon-exclude-from-outside-module_test.py | 60 testsuite/daemon-exclude-namebased_test.py | 59 testsuite/daemon-exec-metachar-documented-limit_test.py | 128 testsuite/daemon-exec-metachar-refused_test.py | 115 testsuite/daemon-exec-rsync-env-shell-escape_test.py | 32 testsuite/daemon-exec-second-shell-argument-injection_test.py | 105 testsuite/daemon-exec-singlequote-injection_test.py | 84 testsuite/daemon-exec_test.py | 86 testsuite/daemon-filter-merge-bypass_test.py | 132 testsuite/daemon-filter_test.py | 88 testsuite/daemon-groupmap-wild_test.py | 87 testsuite/daemon-gzip-download.test | 37 testsuite/daemon-gzip-download_test.py | 29 testsuite/daemon-gzip-upload.test | 31 testsuite/daemon-gzip-upload_test.py | 29 testsuite/daemon-handshake-timeout_test.py | 374 + testsuite/daemon-http-proxy_test.py | 159 testsuite/daemon-include-maxconn_test.py | 118 testsuite/daemon-leaf-type-race-fchmod_test.py | 444 + testsuite/daemon-link-dest-escape_test.py | 73 testsuite/daemon-max-alloc-zero_test.py | 83 testsuite/daemon-module-chdir-symlink_test.py | 99 testsuite/daemon-module-options_test.py | 146 testsuite/daemon-module-private-parent_test.py | 103 testsuite/daemon-munge_test.py | 53 testsuite/daemon-namecvt-empty-response_test.py | 191 testsuite/daemon-namecvt-newline-token_test.py | 91 testsuite/daemon-path-root-read_test.py | 79 testsuite/daemon-path-rsync-var_test.py | 52 testsuite/daemon-proxy-protocol_test.py | 239 testsuite/daemon-refuse-compress-threads-alias_test.py | 163 testsuite/daemon-refuse-compress_test.py | 52 testsuite/daemon-refuse-delete-alias_test.py | 109 testsuite/daemon-refuse_test.py | 79 testsuite/daemon-scan-cwd-desync_test.py | 76 testsuite/daemon-scan-dir-escape_test.py | 99 testsuite/daemon-secrets-file-symlink_test.py | 157 testsuite/daemon-size-arg-overflow_test.py | 76 testsuite/daemon-standalone-detach_test.py | 137 testsuite/daemon-strict-modes-matrix_test.py | 141 testsuite/daemon-subdir-climb-symlink_test.py | 63 testsuite/daemon-symlink-escape-matrix_test.py | 286 testsuite/daemon-unix-socket-atfd_test.py | 36 testsuite/daemon-zstd-thread-exhaustion_test.py | 141 testsuite/daemon.test | 90 testsuite/daemon_test.py | 136 testsuite/deep-path_test.py | 59 testsuite/delay-updates-deep_test.py | 62 testsuite/delay-updates.test | 21 testsuite/delay-updates_test.py | 31 testsuite/delete-deep_test.py | 107 testsuite/delete-missing-args-files-from_test.py | 80 testsuite/delete.test | 57 testsuite/delete_test.py | 105 testsuite/dest-symlinked-dir_test.py | 40 testsuite/devices-fake_test.py | 163 testsuite/devices.test | 171 testsuite/devices_test.py | 163 testsuite/dir-sgid.test | 48 testsuite/dir-sgid_test.py | 98 testsuite/dirs_test.py | 33 testsuite/duplicates.test | 44 testsuite/duplicates_test.py | 49 testsuite/early-input-symlink_test.py | 148 testsuite/exclude-implied-trailing-backslash_test.py | 98 testsuite/exclude-lsh.test | 252 testsuite/exclude-lsh_test.py | 321 testsuite/exclude.test | 252 testsuite/exclude_test.py | 321 testsuite/excludefrom-symlink_test.py | 120 testsuite/executability.test | 47 testsuite/executability_test.py | 49 testsuite/exitcodes.py | 17 testsuite/expect/rsync_2.6.0.expect | 35 testsuite/expect/rsync_3.0.0.expect | 35 testsuite/expect/rsync_3.1.0.expect | 35 testsuite/expect/rsync_3.1.3.expect | 51 testsuite/expect/rsync_3.2.0.expect | 36 testsuite/expect/rsync_3.2.7.expect | 33 testsuite/expect/rsync_3.3.0.expect | 33 testsuite/expect/rsync_3.4.0.expect | 42 testsuite/expect/rsync_3.4.1.expect | 42 testsuite/fake-super-acl-xattr_test.py | 112 testsuite/fake-super-backup-fifo-regression_test.py | 207 testsuite/file-to-file-mkpath-dry-run_test.py | 62 testsuite/files-from-depth_test.py | 94 testsuite/files-from-leak_test.py | 181 testsuite/files-from-path-clamp_test.py | 65 testsuite/files-from.test | 45 testsuite/files-from_test.py | 51 testsuite/filter-depth_test.py | 71 testsuite/filter-leak_test.py | 183 testsuite/filter-merge-content-echo_test.py | 294 testsuite/filter-merge-recursion_test.py | 62 testsuite/filter-merge-symlink_test.py | 100 testsuite/fleettest.json.example | 298 testsuite/fleettest.py | 1403 +++ testsuite/fuzzy-basis_test.py | 45 testsuite/fuzzy.test | 24 testsuite/fuzzy_test.py | 34 testsuite/git-set-file-times-python-compat-regression_test.py | 30 testsuite/growing-file_test.py | 105 testsuite/hands.test | 38 testsuite/hands_test.py | 63 testsuite/hardlinks-deep_test.py | 36 testsuite/hardlinks.test | 87 testsuite/hardlinks_test.py | 113 testsuite/hashsearch-chain_test.py | 108 testsuite/hashtable-overflow_test.py | 37 testsuite/highfd-hang_test.py | 130 testsuite/inband-modname-leak_test.py | 67 testsuite/inplace_test.py | 71 testsuite/insecure-links-admin-optout_test.py | 90 testsuite/io-noop-flood-recursion_test.py | 90 testsuite/io-nosend-flood-recursion_test.py | 100 testsuite/io-readargs-argv-nullwrite_test.py | 90 testsuite/itemize.test | 246 testsuite/itemize_test.py | 253 testsuite/iwildmatch-fold_test.py | 22 testsuite/keep-dirlinks-rule_test.py | 82 testsuite/keep-dirlinks-symlinked-dest_test.py | 37 testsuite/ki58-log-format-percent_test.py | 131 testsuite/ki62-io-error-mask_test.py | 156 testsuite/ki72-safe-links-backup_test.py | 129 testsuite/ki73-cvs-clear-list_test.py | 40 testsuite/link-dest-module-escape_test.py | 67 testsuite/link-dest-pathroot_test.py | 62 testsuite/link-dest-relative-basis_test.py | 118 testsuite/link-dest-symlink-enotsup_test.py | 202 testsuite/links_test.py | 65 testsuite/log-control-chars_test.py | 40 testsuite/log-file-symlink_test.py | 129 testsuite/longdir.test | 26 testsuite/longdir_test.py | 34 testsuite/macos-setgid-ordinary-mode-regression_test.py | 116 testsuite/malicious-dot-dir-delete-scope_test.py | 98 testsuite/malicious-dot-file-delete-scope_test.py | 94 testsuite/malicious-sender-delete-scope_test.py | 180 testsuite/malicious-server-partial-basis-symlink-overwrite_test.py | 231 testsuite/match-append-empty-nullmap_test.py | 84 testsuite/match-want-i-nolen_test.py | 94 testsuite/max-alloc-zero-rejected_test.py | 9 testsuite/merge.test | 57 testsuite/merge_test.py | 89 testsuite/metadata-depth_test.py | 57 testsuite/misc-coverage_test.py | 125 testsuite/missing.test | 34 testsuite/missing_test.py | 58 testsuite/mkpath.test | 47 testsuite/mkpath_test.py | 76 testsuite/mkvariety.py | 126 testsuite/msg-io-timeout-overflow_test.py | 87 testsuite/msg-io-timeout-zero_test.py | 114 testsuite/mutatefns.py | 84 testsuite/nested-socket-specials_test.py | 55 testsuite/no-implied-dirs-symlink_test.py | 72 testsuite/nondaemon-symlink-race_test.py | 87 testsuite/nonroot-restrictive-perms_test.py | 107 testsuite/omit-times_test.py | 58 testsuite/open-noatime_test.py | 62 testsuite/operator-path-backup-dir-daemon_test.py | 47 testsuite/operator-path-backup-dir-exclude-daemon_test.py | 65 testsuite/operator-path-backup-dir_test.py | 33 testsuite/operator-path-backup-rmdir_test.py | 115 testsuite/operator-path-backup-symlink_test.py | 115 testsuite/operator-path-compare-dest_test.py | 36 testsuite/operator-path-copy-dest_test.py | 37 testsuite/operator-path-dir-daemon-inmodule_test.py | 39 testsuite/operator-path-dir-daemon-leaf_test.py | 59 testsuite/operator-path-dir-daemon-mkdir_test.py | 43 testsuite/operator-path-dir-daemon-outside_test.py | 56 testsuite/operator-path-files-from_test.py | 29 testsuite/operator-path-inplace-backup-dir_test.py | 37 testsuite/operator-path-insecure-links-daemon_test.py | 63 testsuite/operator-path-insecure-links-refused_test.py | 43 testsuite/operator-path-link-dest_test.py | 41 testsuite/operator-path-log-file_test.py | 28 testsuite/operator-path-partial-dir-daemon_test.py | 77 testsuite/operator-path-partial-dir-exclude-daemon_test.py | 63 testsuite/operator-path-partial-dir_test.py | 80 testsuite/operator-path-temp-dir_test.py | 43 testsuite/operator-path-traversal-backup-dir-daemon_test.py | 67 testsuite/operator-path-traversal-dest-dir-daemon_test.py | 62 testsuite/operator-path-traversal-partial-dir-daemon_test.py | 67 testsuite/operator-path-write-batch_test.py | 27 testsuite/output-options_test.py | 146 testsuite/ownership-depth_test.py | 121 testsuite/partial-dir-abs-delta_test.py | 76 testsuite/partial-protected-regular-retry-linux_test.py | 385 + testsuite/partial-protected-regular-retry-policy_test.py | 265 testsuite/partial_nowrite_test.py | 70 testsuite/partial_test.py | 139 testsuite/password-file-symlink_test.py | 155 testsuite/peer-legacy-implied-delete-scope_test.py | 122 testsuite/perftest.py | 506 + testsuite/preallocate_test.py | 156 testsuite/protected-regular.test | 31 testsuite/protected-regular_test.py | 80 testsuite/proto-cleared-dirflist_test.py | 85 testsuite/proto-cleared-ndx_test.py | 77 testsuite/proto-hlink-flag-oob_test.py | 88 testsuite/proto-hlink-gnum_test.py | 85 testsuite/proto-msg-info-assert_test.py | 80 testsuite/proto-parent-ndx-empty-dirflist_test.py | 117 testsuite/proto-sender-selftest_test.py | 97 testsuite/proto-subflist-freed_test.py | 82 testsuite/proxy-connect-request-too-long_test.py | 10 testsuite/proxy-host-crlf_test.py | 70 testsuite/proxy-protocol-trusted-peer_test.py | 46 testsuite/proxy-response-header-too-long_test.py | 10 testsuite/proxy-response-line-too-long_test.py | 102 testsuite/prune-empty-dirs_test.py | 45 testsuite/readonly-partial-abort-mode-regression_test.py | 171 testsuite/recv-discard-nullderef_test.py | 126 testsuite/recv-generator-acl-leak_test.py | 68 testsuite/relative-content_test.py | 42 testsuite/relative-implied-symlink_test.py | 49 testsuite/relative-implied_test.py | 54 testsuite/relative-mkpath-dir-symlink_test.py | 64 testsuite/relative-mkpath-symlink_test.py | 74 testsuite/relative-symlinked-parent-dotdot_test.py | 37 testsuite/relative-symlinked-parent_test.py | 35 testsuite/relative.test | 60 testsuite/relative_test.py | 111 testsuite/remote-shell-newline-escaping_test.py | 38 testsuite/rename-fullpath-symlink-race_test.py | 116 testsuite/rename-mixed-parent-escape-poc_test.py | 52 testsuite/rename-mixed-parent-symlink-race_test.py | 43 testsuite/rename-mixed-parent-transfer_test.py | 151 testsuite/reverse-daemon-delta_test.py | 127 testsuite/rrsync-alt-dest-inband-pivot_test.py | 144 testsuite/rrsync-archive-mode_test.py | 47 testsuite/rrsync-backup-dir-inband-pivot_test.py | 224 testsuite/rrsync-copy-unsafe-links-denied_test.py | 50 testsuite/rrsync-debug-denied_test.py | 78 testsuite/rrsync-files-from-stdin_test.py | 120 testsuite/rrsync-logfile-symlink_test.py | 26 testsuite/rrsync-merge-file-confine_test.py | 139 testsuite/rrsync-no-overwrite-backup-collision_test.py | 99 testsuite/rrsync-no-overwrite-delay-updates_test.py | 67 testsuite/rrsync-no-overwrite-logfile_test.py | 73 testsuite/rrsync-no-overwrite-partial-dir_test.py | 75 testsuite/rrsync-pull-arg-shapes_test.py | 220 testsuite/rrsync-pull-delivers-content_test.py | 105 testsuite/rrsync-sender-leaf-flip_test.py | 136 testsuite/rrsync-sender-parent-pin_test.py | 184 testsuite/rrsync-specials-denied_test.py | 225 testsuite/rrsync-symlink_test.py | 151 testsuite/rrsync-write-only-files-from_test.py | 68 testsuite/rsync-ssl-hostname-validation_test.py | 59 testsuite/rsync-ssl-openssl-hostname-check_test.py | 58 testsuite/rsync-ssl-stunnel-ca-required_test.py | 56 testsuite/rsync-ssl-stunnel-hostname-check_test.py | 73 testsuite/rsync.fns | 498 - testsuite/rsync_proto.py | 1073 ++ testsuite/rsyncfns.py | 2904 +++++++ testsuite/safe-arg-leak_test.py | 22 testsuite/safe-links-absolute-intree_test.py | 86 testsuite/safe-links-unsafe-def_test.py | 130 testsuite/safe-links.test | 55 testsuite/safe-links_test.py | 51 testsuite/scanner-argv-bounds_test.py | 50 testsuite/scanner-batch-flag-mismatch_test.py | 54 testsuite/scanner-daemon-log-checksum_test.py | 42 testsuite/scanner-delete-delay-overread_test.py | 34 testsuite/secure-relpath-validation_test.py | 30 testsuite/sender-flist-symlink-leak_test.py | 100 testsuite/sender-readlink-atfd_test.py | 54 testsuite/sender-remove-source-relative-anchor_test.py | 39 testsuite/sender-remove-source-root-anchor_test.py | 80 testsuite/sender-remove-source-secure_test.py | 70 testsuite/sender-scan-dir-escape_test.py | 110 testsuite/simd-checksum_test.py | 20 testsuite/size-filter_test.py | 54 testsuite/skiplist-spec_test.py | 174 testsuite/skiplist/README.md | 117 testsuite/skiplist/common.txt | 61 testsuite/skiplist/cygwin.txt | 66 testsuite/skiplist/linux.txt | 10 testsuite/skiplist/macos.txt | 33 testsuite/skiplist/proto29.txt | 16 testsuite/source-change-size-continues_test.py | 213 testsuite/sparse_test.py | 59 testsuite/ssh-basic.test | 34 testsuite/ssh-basic_test.py | 56 testsuite/stdio_daemon.py | 63 testsuite/stop-time_test.py | 45 testsuite/symlink-dest-backupdir_test.py | 73 testsuite/symlink-dirlink-basis_test.py | 201 testsuite/symlink-exclude-chdir-alias_test.py | 61 testsuite/symlink-exclude-component_test.py | 62 testsuite/symlink-exclude-deep_test.py | 64 testsuite/symlink-exclude-leaf_test.py | 51 testsuite/symlink-exclude-meta_test.py | 57 testsuite/symlink-exclude-xattr_test.py | 70 testsuite/symlink-exclude_test.py | 54 testsuite/symlink-ignore.test | 34 testsuite/symlink-ignore_test.py | 28 testsuite/symlink-mknod-fakesuper-symlink-race_test.py | 104 testsuite/symlink-race-dest_test.py | 76 testsuite/symlink-race-relative-dest_test.py | 85 testsuite/symlink-race-source_test.py | 121 testsuite/temp-dir-symlink-injection_test.py | 143 testsuite/temp-dir_test.py | 61 testsuite/trimslash.test | 26 testsuite/trimslash_test.py | 41 testsuite/uidlist-id0-name-leak_test.py | 56 testsuite/unsafe-byname.test | 58 testsuite/unsafe-byname_test.py | 77 testsuite/unsafe-links.test | 65 testsuite/unsafe-links_test.py | 82 testsuite/update_test.py | 78 testsuite/valgrind.supp | 48 testsuite/variety-symlink-traversal_test.py | 174 testsuite/variety_test.py | 237 testsuite/wildmatch.test | 23 testsuite/wildmatch_test.py | 49 testsuite/write-batch-filter-injection_test.py | 54 testsuite/write-batch-quoting_test.py | 54 testsuite/xattr-wire-cap_test.py | 97 testsuite/xattrs-depth_test.py | 52 testsuite/xattrs-hlink_test.py | 265 testsuite/xattrs.test | 239 testsuite/xattrs_test.py | 265 testsuite/xrsync.py | 164 testsuite/xrsync_test.py | 130 tls.c | 4 token.c | 157 uidlist.c | 8 usage.c | 18 util1.c | 329 version.h | 4 wildtest.c | 19 xattrs.c | 254 612 files changed, 70320 insertions(+), 17284 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp3s9ed5zx/rsync_3.4.1+ds1-5+deb13u4.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp3s9ed5zx/rsync_3.5.0+ds1-0+deb13u1.dsc: no acceptable signature found diff -Nru rsync-3.4.1+ds1/.gitattributes rsync-3.5.0+ds1/.gitattributes --- rsync-3.4.1+ds1/.gitattributes 2020-07-15 17:20:52.000000000 +0000 +++ rsync-3.5.0+ds1/.gitattributes 2026-06-01 09:21:35.000000000 +0000 @@ -1 +1,17 @@ * text=auto eol=lf + +# The rsync-web/ subdirectory holds the project website source content +# (mirrors what gets pushed to https://rsync.samba.org). Exclude it from +# `git archive` output so the release source tarball produced by +# packaging/release.py step_7_tarball does not bloat with HTML the +# tarball doesn't need. +/rsync-web/ export-ignore + +# old_versions/ holds static binaries of historical rsync releases, used by the +# version-mixing test suite (.github/workflows/ubuntu-version-mix.yml) to run +# the current code against a real old peer over the daemon / remote-shell. +# Mark the binaries as binary so the `text=auto eol=lf` rule above can't try to +# normalise line endings and corrupt them; export-ignore keeps them out of the +# release source tarball. +/old_versions/rsync_* binary +/old_versions/rsync_* export-ignore diff -Nru rsync-3.4.1+ds1/.gitignore rsync-3.5.0+ds1/.gitignore --- rsync-3.4.1+ds1/.gitignore 2022-03-13 17:45:09.000000000 +0000 +++ rsync-3.5.0+ds1/.gitignore 2026-08-02 06:05:24.000000000 +0000 @@ -45,6 +45,17 @@ /t_unsafe /wildtest /getfsdev +/simdtest +/t_acl +/t_chmod_secure +/t_clean_fname +/t_hashtable_overflow +/t_iwildmatch +/t_rename_secure +/t_safe_arg +/t_safe_arg_main +/t_secure_relpath +/t_symlink_secure /rounding.h /doc/rsync.pdf /doc/rsync.ps @@ -52,9 +63,12 @@ /testsuite/chown-fake.test /testsuite/devices-fake.test /testsuite/xattrs-hlink.test +/testsuite/fleettest.json +/fleettest-logs /patches /patches.gen /build /auto-build-save .deps /*.exe +*.dSYM/ diff -Nru rsync-3.4.1+ds1/INSTALL.md rsync-3.5.0+ds1/INSTALL.md --- rsync-3.4.1+ds1/INSTALL.md 2024-05-29 01:19:19.000000000 +0000 +++ rsync-3.5.0+ds1/INSTALL.md 2026-06-01 05:37:10.000000000 +0000 @@ -7,6 +7,34 @@ support libraries that you may want to install to build rsync with the maximum features (the impatient can skip down to the package summary): +## Ubuntu users: skip the build, use the PPA + +If you are on a currently supported Ubuntu series (jammy 22.04 LTS, noble +24.04 LTS, questing 25.10, resolute 26.04 LTS) and just want the latest +upstream rsync, the rsync project maintains a Launchpad PPA that tracks +stable releases: + +> sudo add-apt-repository ppa:rsyncproject/rsync +> sudo apt update && sudo apt install rsync + +See [the PPA page][ppa] for current build status across architectures. + +[ppa]: https://launchpad.net/~rsyncproject/+archive/ubuntu/rsync + +To test the upcoming release instead, there is also a [`rsync-latest` +PPA][ppa-latest] that is rebuilt from the tip of the git master branch. These +are development snapshots whose version numbers (such as +`3.5.0~git20260601...`) deliberately sort below the matching stable release, so +the stable PPA above will never silently move you from a release onto a +snapshot. Use it for testing only -- it may contain unreleased changes: + +> sudo add-apt-repository ppa:rsyncproject/rsync-latest +> sudo apt update && sudo apt install rsync + +[ppa-latest]: https://launchpad.net/~rsyncproject/+archive/ubuntu/rsync-latest + +The rest of this document covers building from source. + ## The basic setup You need to have a C compiler installed and optionally a C++ compiler in order diff -Nru rsync-3.4.1+ds1/Makefile.in rsync-3.5.0+ds1/Makefile.in --- rsync-3.4.1+ds1/Makefile.in 2024-11-05 20:42:42.000000000 +0000 +++ rsync-3.5.0+ds1/Makefile.in 2026-08-01 22:13:25.000000000 +0000 @@ -40,16 +40,17 @@ 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 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 @LIBOBJS@ + 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=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \ - util1.o util2.o main.o checksum.o match.o syscall.o log.o backup.o delete.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 +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 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/findme.o popt/popt.o popt/poptconfig.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@ @@ -57,12 +58,16 @@ # Programs we must have to run the test cases CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \ - testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) wildtest$(EXEEXT) + testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) t_chmod_secure$(EXEEXT) \ + t_rename_secure$(EXEEXT) t_symlink_secure$(EXEEXT) t_secure_relpath$(EXEEXT) t_acl$(EXEEXT) t_hashtable_overflow$(EXEEXT) t_iwildmatch$(EXEEXT) t_clean_fname$(EXEEXT) t_safe_arg$(EXEEXT) wildtest$(EXEEXT) simdtest$(EXEEXT) -CHECK_SYMLINKS = testsuite/chown-fake.test testsuite/devices-fake.test testsuite/xattrs-hlink.test +CHECK_SYMLINKS = testsuite/chown-fake_test.py testsuite/devices-fake_test.py \ + testsuite/xattrs-hlink_test.py testsuite/exclude-lsh_test.py # Objects for CHECK_PROGS to clean -CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o trimslash.o wildtest.o +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 # note that the -I. is needed to handle config.h when using VPATH .c.o: @@ -75,6 +80,10 @@ 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 $@ + .PHONY: install install: all -$(MKDIR_P) $(DESTDIR)$(bindir) @@ -82,12 +91,19 @@ $(INSTALLCMD) -m 755 $(srcdir)/rsync-ssl $(DESTDIR)$(bindir) -$(MKDIR_P) $(DESTDIR)$(mandir)/man1 -$(MKDIR_P) $(DESTDIR)$(mandir)/man5 - if test -f rsync.1; then $(INSTALLMAN) -m 644 rsync.1 $(DESTDIR)$(mandir)/man1; fi - if test -f rsync-ssl.1; then $(INSTALLMAN) -m 644 rsync-ssl.1 $(DESTDIR)$(mandir)/man1; fi - if test -f rsyncd.conf.5; then $(INSTALLMAN) -m 644 rsyncd.conf.5 $(DESTDIR)$(mandir)/man5; fi + for fn in rsync.1 rsync-ssl.1; do \ + if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man1; \ + elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man1; fi; \ + done + for fn in rsyncd.conf.5; do \ + if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man5; \ + elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man5; fi; \ + done if test "$(with_rrsync)" = yes; then \ $(INSTALLCMD) -m 755 rrsync $(DESTDIR)$(bindir); \ - if test -f rrsync.1; then $(INSTALLMAN) -m 644 rrsync.1 $(DESTDIR)$(mandir)/man1; fi; \ + fn=rrsync.1; \ + if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man1; \ + elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man1; fi; \ fi install-ssl-daemon: stunnel-rsyncd.conf @@ -102,6 +118,21 @@ install-strip: $(MAKE) INSTALL_STRIP='-s' install +.PHONY: uninstall +uninstall: + rm -f $(DESTDIR)$(bindir)/rsync$(EXEEXT) $(DESTDIR)$(bindir)/rsync-ssl + rm -f $(DESTDIR)$(bindir)/rrsync + rm -f $(DESTDIR)$(mandir)/man1/rsync.1 $(DESTDIR)$(mandir)/man1/rsync-ssl.1 + rm -f $(DESTDIR)$(mandir)/man1/rrsync.1 + rm -f $(DESTDIR)$(mandir)/man5/rsyncd.conf.5 + +.PHONY: uninstall-ssl-daemon +uninstall-ssl-daemon: + rm -f $(DESTDIR)/etc/stunnel/rsyncd.conf + +.PHONY: uninstall-all +uninstall-all: uninstall uninstall-ssl-daemon + rsync$(EXEEXT): $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(LIBS) @@ -115,6 +146,7 @@ loadparm.o: default-dont-compress.h daemon-parm.h flist.o: rounding.h +log.o: rounding.h default-cvsignore.h default-dont-compress.h: rsync.1.md define-from-md.awk $(AWK) -f $(srcdir)/define-from-md.awk -v hfile=$@ $(srcdir)/rsync.1.md @@ -178,6 +210,55 @@ 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$(EXEEXT): $(T_HASHTABLE_OVERFLOW_OBJ) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_HASHTABLE_OVERFLOW_OBJ) $(LIBS) + +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$(EXEEXT): $(T_CLEAN_FNAME_OBJ) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CLEAN_FNAME_OBJ) $(LIBS) + +# safe_arg lives in options.c alongside the whole option parser. Rather than +# rely on a non-portable linker --gc-sections to drop the parser (GNU ld only; +# macOS ld64 and the cygwin PE linker do not), link the real rsync objects so +# every dep resolves. t_safe_arg_main.o is main.c with main() renamed out, to +# supply main.c's globals while letting t_safe_arg.o provide the test's main(). +# OBJS minus main.o is spelled out via OBJS1_NO_MAIN because $(filter-out) is +# 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$(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$(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$(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$(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$(EXEEXT): $(T_SECURE_RELPATH_OBJ) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SECURE_RELPATH_OBJ) $(LIBS) + +# Unit test for lib/acl.c: compares our fd/at ACL ops against the system libacl +# (linked via $(LIBS), which carries -lacl). lib/acl.o references no rsync +# globals, so this links with no stubs. Self-skips (exit 77) when built +# without SUPPORT_ACL_FD. +T_ACL_OBJ = t_acl.o lib/acl.o +t_acl$(EXEEXT): $(T_ACL_OBJ) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_ACL_OBJ) $(LIBS) + .PHONY: conf conf: configure.sh config.h.in @@ -267,9 +348,11 @@ .PHONY: clean clean: cleantests - rm -f *~ $(OBJS) $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_SYMLINKS) @MAKE_RRSYNC@ \ + rm -f *~ $(OBJS) $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_COMPILE_OBJS) $(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 -rf coverage coverage-tcp coverage-all coverage-fallback .PHONY: cleantests cleantests: @@ -310,30 +393,172 @@ # catch Bash-isms earlier even if we're running on GNU. Of course, we # might lose in the future where POSIX diverges from old sh. +# `make check` runs tests in parallel by default. Override with +# `make check CHECK_J=1` (serial) or any other value. +CHECK_J = 8 + +# Parallelism for `make coverage`. Defaults to the same as CHECK_J: the +# coverage build sets -fprofile-update=atomic (atomic in-memory counters) and +# gcc's libgcov serializes the per-source .gcda read-modify-write merge with a +# file lock, so concurrent rsync processes (incl. the forked sender/generator/ +# receiver) accumulate exactly -- verified by a count-linearity check (a hot +# line accumulates identically at -j1 and -P16). Override with +# `make coverage COVERAGE_J=1` if your libgcov does not lock .gcda merges. +COVERAGE_J = $(CHECK_J) + +# Output directory and extra runtests.py flags for `make coverage`. The +# `coverage-tcp` target reuses the coverage recipe with --use-tcp (real +# loopback rsyncd, which exercises the TCP accept/auth path and the +# require_tcp-only tests) and a separate output directory. +COVERAGE_DIR = coverage +COVERAGE_RUNFLAGS = + +# Excluded from the coverage report so the percentages reflect rsync's own +# runtime source. Three buckets: +# (1) Bundled third-party code rsync ships but does not own: zlib/, popt/, and +# the named lib/ imports (PostgreSQL getaddrinfo, ISC inet_ntop/inet_pton, +# standalone getpass). The other lib/*.c are rsync's own and stay in. +# (2) Test-helper / build-time programs that link against rsync objects but are +# not the rsync runtime: t_*.c, tls.c, wildtest.c, testrun.c, getgroups.c, +# getfsdev.c, trimslash.c, rounding.c. These have their own main() and are +# either driven directly by a test (counted there) or are configure-time +# probes; counting them as "rsync uncovered" is noise. +# (3) Compile-time-dead fallbacks under this build's config.h: lib/md5.c (the +# reference md5 -- openssl's EVP path is used when HAVE_OPENSSL) and +# lib/snprintf.c (only the #include line survives under +# HAVE_C99_VSNPRINTF). Covering these would mean a separate non-openssl / +# non-C99 build, which is out of scope for this report. +COVERAGE_EXCLUDE = -e '(^|/)zlib/' -e '(^|/)popt/' \ + -e '(^|/)lib/(getaddrinfo|getpass|inet_ntop|inet_pton)\.' \ + -e '(^|/)(t_[a-z_]+|tls|wildtest|testrun|getgroups|getfsdev|trimslash|rounding)\.c$$' \ + -e '(^|/)lib/(md5|snprintf)\.c$$' + +# Build everything the test suite needs (rsync + helper programs + symlinks) +# WITHOUT running it. Used by CI jobs that invoke runtests.py directly with +# custom options (e.g. the version-mix workflow's --rsync-bin2/--expect-result). +.PHONY: check-progs +check-progs: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS) + .PHONY: check -check: all $(CHECK_PROGS) $(CHECK_SYMLINKS) - rsync_bin=`pwd`/rsync$(EXEEXT) $(srcdir)/runtests.sh +check: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS) + "$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J) .PHONY: check29 -check29: all $(CHECK_PROGS) $(CHECK_SYMLINKS) - rsync_bin=`pwd`/rsync$(EXEEXT) $(srcdir)/runtests.sh --protocol=29 +check29: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS) + "$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J) --protocol=29 .PHONY: check30 -check30: all $(CHECK_PROGS) $(CHECK_SYMLINKS) - rsync_bin=`pwd`/rsync$(EXEEXT) $(srcdir)/runtests.sh --protocol=30 +check30: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS) + "$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J) --protocol=30 + +# Whole-suite gcov coverage report (HTML, with branch + decision coverage). +# Requires a build configured with --enable-coverage and the `gcovr` tool +# (pip install gcovr). Runs the suite in parallel (COVERAGE_J, default CHECK_J): +# this is safe because the coverage build uses -fprofile-update=atomic and +# libgcov locks the per-source .gcda during its merge, so concurrent rsync +# processes accumulate exactly (see COVERAGE_J above). Use COVERAGE_J=1 if your +# toolchain's libgcov does not lock .gcda merges. +.PHONY: coverage +coverage: all $(CHECK_PROGS) $(CHECK_SYMLINKS) + @case '$(CFLAGS)' in *--coverage*) ;; \ + *) echo "*** not a coverage build; reconfigure with --enable-coverage"; exit 1 ;; esac + @command -v gcovr >/dev/null 2>&1 || { echo "*** gcovr not found (pip install gcovr)"; exit 1; } + find . -name '*.gcda' -delete + @# Daemon modules with `uid = ` setuid the per-connection child + @# (and so the forked generator/receiver), which then cannot create or + @# merge .gcda files in a root-owned build dir -- silently dropping ALL + @# coverage from those processes. Make every .gcno's directory + @# world-writable so any uid can create the sibling .gcda, and set a + @# default ACL of o::rw so the .gcda are world-mergeable regardless of + @# the creator's umask (every test process resets umask to 022 via + @# rsyncfns.py, so a Makefile-level `umask 0` would not survive). + @find . -name '*.gcno' -printf '%h\n' 2>/dev/null | sort -u | \ + while read d; do \ + chmod a+rwx "$$d"; \ + setfacl -m 'd:u::rwx,d:g::rwx,d:o::rwx' "$$d" 2>/dev/null || true; \ + done + @rc=0; "$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(COVERAGE_J) $(COVERAGE_RUNFLAGS) || rc=$$?; \ + rm -rf $(COVERAGE_DIR) && mkdir -p $(COVERAGE_DIR); \ + gcovr --root $(srcdir) $(COVERAGE_EXCLUDE) --decisions --print-summary \ + --gcov-ignore-parse-errors=negative_hits.warn_once_per_file \ + --html-details -o $(COVERAGE_DIR)/index.html . || exit $$?; \ + echo "Coverage report written to $(COVERAGE_DIR)/index.html"; \ + if test $$rc != 0; then \ + echo "*** test suite FAILED (status $$rc) -- coverage report still written above"; \ + fi; \ + exit $$rc + +# Same as `make coverage` but with the daemon tests run over a real loopback +# rsyncd (--use-tcp), into a separate report directory. +.PHONY: coverage-tcp +coverage-tcp: + $(MAKE) coverage COVERAGE_RUNFLAGS=--use-tcp COVERAGE_DIR=coverage-tcp + +# Comprehensive single report: run the suite under several configurations, +# accumulating into the shared .gcda counters (NOT cleared between runs), then +# emit one merged, rsync-scoped report. Covers the default (pipe) transport, the +# protocol-29/30 compat branches, and the real-TCP daemon path (which also runs +# the require_tcp-only tests). Run under sudo to additionally cover root-only +# paths (devices, chown, use-chroot, protected-regular). Local target -- CI uses +# the plain `coverage`/`coverage-tcp` targets. +.PHONY: coverage-all +coverage-all: all $(CHECK_PROGS) $(CHECK_SYMLINKS) + @case '$(CFLAGS)' in *--coverage*) ;; \ + *) echo "*** not a coverage build; reconfigure with --enable-coverage"; exit 1 ;; esac + @command -v gcovr >/dev/null 2>&1 || { echo "*** gcovr not found (pip install gcovr)"; exit 1; } + find . -name '*.gcda' -delete + @# See the `coverage` target above for why: setuid'd daemon children must + @# be able to create/merge .gcda owned by a different uid. + @find . -name '*.gcno' -printf '%h\n' 2>/dev/null | sort -u | \ + while read d; do \ + chmod a+rwx "$$d"; \ + setfacl -m 'd:u::rwx,d:g::rwx,d:o::rwx' "$$d" 2>/dev/null || true; \ + done + @rc=0; \ + for cfg in '' '--protocol=30' '--protocol=29' '--use-tcp'; do \ + echo "===== coverage-all: runtests.py $$cfg ====="; \ + "$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(COVERAGE_J) $$cfg || rc=$$?; \ + done; \ + rm -rf coverage-all && mkdir -p coverage-all; \ + gcovr --root $(srcdir) $(COVERAGE_EXCLUDE) --decisions --print-summary \ + --gcov-ignore-parse-errors=negative_hits.warn_once_per_file \ + --html-details -o coverage-all/index.html . || exit $$?; \ + echo "Merged coverage report written to coverage-all/index.html"; \ + if test $$rc != 0; then \ + echo "*** some suite runs FAILED (status $$rc) -- report still written above"; \ + fi; \ + exit $$rc + +# Coverage for the portable (non-openat2) resolver tier. Requires a SEPARATE +# build configured with --enable-coverage --disable-openat2: its .gcno differ +# from the openat2 build, so this report cannot be merged with the others. +.PHONY: coverage-fallback +coverage-fallback: + $(MAKE) coverage COVERAGE_DIR=coverage-fallback wildtest.o: wildtest.c t_stub.o lib/wildmatch.c rsync.h config.h wildtest$(EXEEXT): wildtest.o lib/compat.o lib/snprintf.o @BUILD_POPT@ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ wildtest.o lib/compat.o lib/snprintf.o @BUILD_POPT@ $(LIBS) -testsuite/chown-fake.test: - ln -s chown.test $(srcdir)/testsuite/chown-fake.test +simdtest$(EXEEXT): simd-checksum-x86_64.cpp $(HEADERS) + @if test x"@ROLL_SIMD@" != x; then \ + $(CXX) -I. $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) -DTEST_SIMD_CHECKSUM1 \ + -o $@ $(srcdir)/simd-checksum-x86_64.cpp @ROLL_ASM@ $(LIBS); \ + else \ + touch $@; \ + fi + +testsuite/chown-fake_test.py: + ln -s chown_test.py $(srcdir)/testsuite/chown-fake_test.py + +testsuite/devices-fake_test.py: + ln -s devices_test.py $(srcdir)/testsuite/devices-fake_test.py -testsuite/devices-fake.test: - ln -s devices.test $(srcdir)/testsuite/devices-fake.test +testsuite/xattrs-hlink_test.py: + ln -s xattrs_test.py $(srcdir)/testsuite/xattrs-hlink_test.py -testsuite/xattrs-hlink.test: - ln -s xattrs.test $(srcdir)/testsuite/xattrs-hlink.test +testsuite/exclude-lsh_test.py: + ln -s exclude_test.py $(srcdir)/testsuite/exclude-lsh_test.py # This does *not* depend on building or installing: you can use it to # check a version installed from a binary or some other source tree, @@ -341,7 +566,7 @@ .PHONY: installcheck installcheck: $(CHECK_PROGS) $(CHECK_SYMLINKS) - POSIXLY_CORRECT=1 TOOLDIR=`pwd` rsync_bin="$(bindir)/rsync$(EXEEXT)" srcdir="$(srcdir)" $(srcdir)/runtests.sh + "$(srcdir)/runtests.py" --rsync-bin="$(bindir)/rsync$(EXEEXT)" --srcdir="$(srcdir)" --tooldir="`pwd`" -j $(CHECK_J) # TODO: Add 'dist' target; need to know which files will be included diff -Nru rsync-3.4.1+ds1/NEWS.md rsync-3.5.0+ds1/NEWS.md --- rsync-3.4.1+ds1/NEWS.md 2025-01-15 20:47:07.000000000 +0000 +++ rsync-3.5.0+ds1/NEWS.md 2026-08-12 23:50:33.000000000 +0000 @@ -1,3 +1,884 @@ +# NEWS for rsync 3.5.0 (13 Aug 2026) + +## Changes in this version: + +### Thanks! + +This has been an extraordinary release developed over several months +and I'd like to thank everyone who has helped make it possible. The +volume of security issues we had to deal with would have been quite +overwhelming without the help that I've received. + +I'm particularly grateful to Zen Dodd (Tao), Omar Elsayed (seks99x), +Will Sargeant, Paul Mackerras, Aleksa Sarai and Leonid Bugaev (buger) +who joined the rsync admins group helping to triage all the issues, +develop new tests, review PRs and helped develop the guidelines we used +for where to draw the line between a security issue and expected +behaviour (a surprisingly difficult thing to do in some cases). You've +all been a huge help and rsync is much better off for your assistance. + +A big thank you also to Filipe Casal from Trail of Bits who worked with +us on the "Patch the Planet" program. Filipe provided a huge trove of +valuable tests and security reports. + +Also a big thank you to Greg Kroah-Hartman for invaluable advice and +security reports and to Stuart Inglis for particularly high quality +bug reports and testing. + +Many thanks to everyone who submitted bug reports, credits are listed +below against individual items. + +Finally, thank you to everyone who joined in the discussion and +testing on the rsync-security mailing list, and to the rsync user +community for your patience in waiting for this release. + +### SECURITY FIXES: + +This release fixes 33 security issues found during a focused audit of rsync's +path handling and daemon protocol, a companion daemon-protocol fuzzing pass, and +reports from external researchers -- plus several robustness hardenings. CVE +IDs were assigned by VulnCheck (CNA); the precise "introduced in" version ranges +accompany each advisory, and many are much narrower than "everything before +3.5.0". Every fix ships with a regression test in the test suite that fails on +the unfixed tree. Many thanks to the external researchers credited below. + +Link following (CWE-59/61) -- a local user who controls a path component plants +a symlink that a privileged rsync then follows: + +- CVE-2026-53802 (HIGH): Arbitrary file read / transfer-shaping via symlinked + operator-supplied input files. rsync followed attacker-planted symlinks in + `--filter` merge files (including per-directory merges and `-C` `.cvsignore`), + `--files-from` / `--include-from` / `--exclude-from`, and the client + `--password-file` / daemon secrets file -- reading an arbitrary file as filter + rules, or sending a victim file's contents as the daemon authentication + response. Operator-supplied paths are now resolved component-by-component with + `openat(O_PATH|O_NOFOLLOW)`, allowing a symlink component only when it is owned + by uid 0 or the effective uid. + +- CVE-2026-53803 (HIGH): Arbitrary file write / privilege escalation via + symlinked operator-supplied output paths -- `--log-file`, + `--write-batch`/`--read-batch`, and the daemon's motd / lock / early-input / + `--config` opens. A planted symlink (or parent component) could redirect the + write, e.g. append the log to `authorized_keys`; `--read-batch` could also feed + chosen bytes to the protocol parser. Same trusted-owner path walk, plus an + `S_ISREG` check on the `--read-batch` file. + +- CVE-2026-53785 (HIGH): Under `--relative`, the receiver's implied-parent + creation (`make_path()`) built the parent chain with a plain `mkdir()` on the + full path, so a planted parent symlink placed the new directories and file + outside the destination tree. `make_path()` now creates each component through + the held-directory-fd primitive. Reported by Omar Elsayed (seks99x). + +- CVE-2026-53784 (HIGH): Daemon module-root chdir escape under `use chroot = + no`: a plain `chdir()` followed a planted parent-component symlink, serving + files from outside the module. The module-root chdir now goes through the + secure resolver. + +- CVE-2026-53793 (HIGH): Chroot `/./` inner-module escape -- a symlinked + parent component inside the inner module reached a sibling outside it (the + generator basis stat, the receiver write/finish path, the module chdir, and the + receiver's delta-basis open). The secure resolver is now engaged for all of + those paths. + +- CVE-2026-53795 (HIGH): An absolute `--temp-dir` or `--link-dest` disabled + the receiver's rename/link confinement. `do_rename_at()`/`do_link_at()` bailed + to the unconfined path-based call whenever *either* path was absolute, so an + absolute source (the temp file, or the link-dest basis) let `finish_transfer()`'s + tmp->final rename -- or a hard-link create -- follow a destination parent + component an attacker flipped to a symlink mid-transfer, writing the file outside + the tree. Each side is now confined independently. Reported by Omar Elsayed + (seks99x). + +- CVE-2026-53796 (MEDIUM): A non-daemon receiver's one-time `chdir()` into the + operator-named destination was not fully confined (a relative destination took a + plain `chdir()`), so an attacker who raced the named destination from a directory + to a symlink moved the receiver's CWD -- and every file it then created -- + outside the tree. The destination chdir now uses the same ownership-checked + `O_NOFOLLOW` walk as the daemon module chdir (see BEHAVIOR CHANGES). Reported by + Omar Elsayed (seks99x). + +- CVE-2026-53797 (MEDIUM): A non-daemon sender opened each transferred file's + content by path (leaf `O_NOFOLLOW` only), so a source parent component an + unprivileged user raced to a symlink after the file-list scan was followed -- + reading a file from outside the source tree into an attacker-readable + destination. The content open is now anchored at the transfer root with + `secure_relative_open()`; `-L` / `--copy-unsafe-links` / `-k` still follow, and + `--insecure-links` restores the legacy open. + +- CVE-2026-53799 (MEDIUM): Receiver ACL/xattr metadata application followed a + symlink race -> arbitrary ACL set (local privilege escalation). When preserving + metadata (`-A`/`--acls`, `-X`/`--xattrs`, or fake-super ACL-as-xattr), the + receiver applied each entry's ACL/xattrs by path via `acl_set_file()` / + `setxattr()`. A local user who raced a just-received entry (or a parent) into a + symlink before the apply could redirect an attacker-chosen ACL -- the bytes are + carried in the source entry -- onto a victim inode outside the destination tree, + granting rwx on a root-owned file. The apply now pins each entry's inode with an + `O_RDONLY|O_NOFOLLOW` fd and sets all metadata on the held inode (Linux 6.13+ + `*xattrat` syscalls, or a patched libacl's `*_at` bindings, else the + `/proc/self/fd` compat path). Where neither primitive exists (the BSDs, Solaris, + macOS, or a `/proc`-less Linux container) it falls back to the path-based apply to + keep `--acls` functional -- a documented residual, refusable via `refuse options = + acls`. + +- CVE-2026-53800 (MEDIUM): Sender `--remove-source-files` unlink followed a + parent-component symlink race -> arbitrary file deletion outside the source tree. + The post-send unlink and its same-file safety re-stat resolved by path relative to + the process CWD, so an unprivileged user who raced a source parent into a symlink + after the file was sent could make a higher-authority sender (a root + `--remove-source-files` run, or a daemon module not refusing the option) delete a + file outside the served tree. The removal is now resolved through the secure + held-dirfd walk anchored at the served module root (daemon) or transfer-root CWD + (local sender), the safety re-stat is confined likewise, and the per-file dev/ino + is only computed when `--remove-source-files` is in effect. + +- CVE-2026-53801 (MEDIUM): Sender/daemon directory-scan enumeration escaped the + transfer root / module -> out-of-tree disclosure. The sender enumerated each + source directory with a plain `opendir()` on the accumulated path, not through the + secure resolver (the enumeration sibling of the previous item, which confined only + the content open). A parent component raced to a symlink between the file-list + scan and the recursive `opendir()` -- or, in daemon following mode + (`-L`/`--copy-dirlinks`/`--copy-unsafe-links`), an in-module symlinked directory + pointing outside the module -- let a higher-authority sender enumerate an + out-of-tree directory and copy its entry names, metadata and symlink targets. The + directory scan is now confined through a held `opendir` fd anchored at the transfer + root / module. + +`support/rrsync` (the restricted SSH wrapper): + +- CVE-2026-53783 (HIGH): rrsync restricted-directory escape. It validated each + argument with `realpath()` and then exec'd rsync against the same name (a + TOCTOU window), and left dangerous options enabled in a restricted subdir. + rrsync now inode-pins the validated path and roots the argument it hands rsync + at that pinned fd, denies `--copy-unsafe-links`, forces `--no-D`, and refuses a + symlinked `--log-file`. The pin relies on Linux's `/proc/self/fd` magic links + being bound to the open inode, so it is Linux-only; on the BSDs, macOS, Solaris + and Cygwin rrsync passes the `realpath()`-validated name as it always did. + Two limits are worth stating: under `--relative` only the anchor the + transmitted name starts from is pinned, so a component below it can still be + raced, and the final component of an ordinary sender argument is not pinned + either (rsync does not follow a symlink there, and the options that would + change that are refused in a restricted dir). + +- A filter rule that failed to parse was echoed back verbatim, including when + the rule came from a merge file's contents. A per-directory merge rule names + a file the peer chooses and travels over the protocol rather than in an + argument, so this let a peer read back any line of any file the server process + could open that is not valid filter syntax -- through an `rrsync` restricted + account as well as a daemon module, since neither confined a merge open that + the wrapper never sees. A syntax error in a rule read from a file now reports + the file and line rather than the text; a rule given as an argument is still + shown. The `--debug=FILTER` traces print the same file-derived text, so + `rrsync` now refuses a peer-selected `--debug` (a stock client never sends + one). An operator who turns debugging on for their own server still sees the + rule text. + +- Redacting those diagnostics did not close the merge route on its own, because + the worst shape produces no diagnostic at all: an exclude-only merge (the `-` + modifier) makes every line of the file a pattern, so nothing fails to parse + and the peer reads the contents off which of its own names went missing from + the file list. Through an `rrsync` restricted account that needs no + `--delete` and no verbosity on a pull. The open is now confined rather than + the disclosure suppressed: rsync gained `--confine-root=DIR`, which refuses an + operator- or peer-supplied path that resolves outside DIR, and `rrsync` passes + its restricted directory. A merge file inside that directory keeps working. + A daemon already had this through its module root and is unaffected. + +Daemon protocol / identity: + +- CVE-2026-53786 (MEDIUM): A client-supplied `--filter` merge file bypassed + the module filter list (it was checked against the module-prefixed path, which + never matched a module rule). The module-dir prefix is now stripped before the + check. Reported by Mitchell Benjamin (Revamp Studio). + +- CVE-2026-53798 (MEDIUM): The daemon name converter mapped an unknown name to + uid/gid 0 (an empty response was read as `atol("") == 0`); with `fake super = + yes` the stored metadata became root-owned. An empty/non-numeric response is + now treated as a lookup failure. Reported by Mitchell Benjamin (Revamp + Studio). + +- CVE-2026-53788 (MEDIUM): A peer-controlled name containing a newline/CR was + written verbatim into the name-converter line protocol, allowing request + injection. Converter tokens containing control characters are now rejected. + Reported by Mitchell Benjamin (Revamp Studio). + +- CVE-2026-53789 (MEDIUM): A malicious daemon-sender could widen `--delete` + scope by omitting the "no content dir" flag on an implied parent, making the + receiver run `delete_in_dir()` on it. Implied-parent directories are now + forced non-content on the receiver. Reported by Mitchell Benjamin (Revamp + Studio). + +- CVE-2026-53791 (CRITICAL): With `proxy protocol = true`, a client connecting + directly (not via the trusted proxy) could send a PROXY header to spoof its + source address and bypass host-based access control. A forwarded address is + now honoured only from a configured trusted-proxy peer. + +Injection and memory safety: + +- CVE-2026-53790 (HIGH): Command / argument injection via unquoted peer- or + host-controlled values -- the `RSYNC_CONNECT_PROG` `%H` host substitution, the + daemon exec-hook `%RSYNC_*%` expansions, rsync-ssl hostspecs, and a missing + newline/CR in remote-shell argument quoting. Each sink is now quoted or + validated (the hook escaping is confined to the shell-executed hooks, so + ordinary daemon string parameters such as `path` are unaffected). + +- CVE-2026-53792 (MEDIUM): A malicious receiver sending a checksum header with a + block count > 0 but block length == 0 drove the sender's rolling-match + arithmetic negative. A zero block length is now rejected. + +- CVE-2026-53794 (MEDIUM): `--max-alloc=0` disabled the per-allocation size + cap (the defense behind CVE-2024-12084) and could be forwarded on the wire to + an unpatched daemon. A zero max-alloc is now rejected at both the client and + the daemon. Reported by Azizcan Dastan (Milenium Security). + +Peer-triggerable memory corruption in the daemon protocol, found by a +daemon-protocol fuzzing pass and reported by Greg Kroah-Hartman. Each is a +WRITE reachable from the wire, which is why these were split out from the +crash-only findings in the same pass: + +- CVE-2026-70461 (HIGH): a one-byte heap out-of-bounds write in + `add_implied_include()`, driven by a peer-supplied filter rule whose trailing + backslash was not counted when sizing the copy. + +- CVE-2026-70458 (HIGH): an out-of-bounds write from a file entry marked + `FLAG_HLINKED` that the receiver accepted even though `-H` was not in effect, + so the hard-link extra slots it then wrote were never allocated. + +- CVE-2026-70456 (HIGH): an out-of-bounds heap write in `read_args()` when the + peer's argument count lands exactly on `maxargs` -- the trailing NULL went one + past the end of the array. + +- CVE-2026-70457 (MEDIUM): an attacker-chosen-offset write in + `parse_size_arg()`'s error formatting, reachable through an over-large + `--max-size` / `--min-size` / `--max-alloc` forwarded to a daemon. + +- CVE-2026-70459 (MEDIUM): a wild-pointer read crashing the per-connection + daemon child, from a crafted first incremental file list whose transfer root + is "." with a non-directory mode -- `parent_ndx` stayed 0 while `dir_flist` + was still empty, so the generator dereferenced a never-written slot. + Companion to CVE-2026-43620; reproduced on released 3.2.7, 3.4.0 and 3.4.1. + +Daemon availability and access control: + +- CVE-2026-70464 (HIGH): an unauthenticated peer could complete the `@RSYNCD` + greeting and then stall forever -- sending a line with no terminator, or + trickling NUL-terminated arguments into `read_args()` one byte at a time -- + holding a per-connection child open past the module's `max connections` + limit. The `timeout` parameter did not cover it, because `set_io_timeout()` + ran after the `read_args()` calls that needed covering. A separate deadline + now spans both, and the early-protocol argument count is bounded. Reported + independently by Chamal De Silva and by Michal Ruprich (Red Hat QE). + +- CVE-2026-70455 (HIGH): a daemon client could request an arbitrary Zstandard + worker count via `--compress-threads`; 256 was measured as 257 threads in a + single connection. Now capped at 8 on a daemon, while local and + remote-shell invocations keep the operator's value. Reported, fixed and + tested by Filipe Casal of Trail of Bits, in collaboration with OpenAI. + +- CVE-2026-70453 (HIGH): quadratic CPU exhaustion in `hash_search()` from a + crafted chain of equal weak checksums. The chain walk is now bounded. First + reported as a performance problem in public rsync issue #217 by heyciao + (2021); recognised as a security issue, bounded and regression-tested by + Stuart Inglis. This one was already public and was not embargoed. + +- CVE-2026-70452 (HIGH): `hosts deny` failed OPEN when a configured hostname + could not be resolved -- with `forward lookup` enabled, which is the default, + an unresolvable deny token admitted the host it was meant to block. It now + fails closed. Sibling of CVE-2026-43617. Reported by Leonid Bugaev. + +- CVE-2026-70463 (HIGH): `auth users` ignored its documented comma-only + parsing. With a leading comma the split should be on commas alone, so that a + group name containing a space can be written; it split on whitespace too, so + a `deny` or `:ro` rule naming such a group was broken into two meaningless + tokens and never fired. Reported by Andres Berbescu. + +- CVE-2026-70460 (HIGH): a peer-supplied `--partial-dir` or `--backup-dir` was + resolved by pathname, so an in-module symlink could redirect it and place + files outside the daemon's module root. Those paths are now confined. + Reported by Omar Elsayed (seks99x). + +Client-side: + +- CVE-2026-70462 (MEDIUM): a peer-supplied `MSG_IO_TIMEOUT` defeated the + client's own I/O timeout -- a large value overflowed signed arithmetic, and a + non-positive value disabled the timeout outright. The value is now capped on + receipt and the arithmetic made overflow-safe. Reported by Z3R0S! (z3r0s6); + the non-positive case was reported by Leonid Bugaev. + +- CVE-2026-70454 (MEDIUM): `rsync-ssl` established an unauthenticated TLS + connection. In stunnel mode it neither required CA verification nor bound + the certificate to the requested hostname, so an active network attacker + could impersonate the server; the openssl backend had a matching hostname + gap in 3.2.0 through 3.2.3 (found and fixed in 2020 by Matt McCutchen). + stunnel mode now requires certificate verification and hostname binding + unless an explicit insecure opt-out is set, and the GnuTLS backend is + refused conservatively rather than used unverified (Greg Kroah-Hartman). + +Robustness hardening (no CVE assigned): the `RSYNC_PROXY` CONNECT request and +proxy response headers are length-bounded, and peer-requested xattr expansion is +capped. + +A second-pass source audit (reported by Leonid Bugaev) hardened several memory- +safety and robustness paths: the hashtable and file-list size computations are +guarded against a 32-bit integer overflow that a peer's entry count could +otherwise wrap into an under-allocation, and the +`SIGUSR2` handler is now async-signal-safe (it only sets a flag, deferring the +summary/close-out work to safe poll points). Separately, the xattr/ACL metadata +copy now reads the *source* through a held no-follow fd as well as writing the +destination through one -- closing a parent-symlink race on the `--copy-dest` and +backup source -- and the cross-tree operator-path metadata apply is now fd-pinned +under `--fake-super` too (previously it fell back to a path-based set for a +`fake super = yes` daemon staging through an absolute `--temp-dir`/`--backup-dir`). + +### SECURITY RELATED: + +- Mask a peer-supplied I/O-error value to the defined `IOERR_*` bits, both the + incoming `MSG_IO_ERROR` message (`io.c`) and the file-list trailer (`flist.c`), + so a malicious peer cannot set arbitrary (undefined) error flags that would be + stored in the local `io_error` and re-forwarded upstream. (Undefined bits + never reached the exit code, which maps only the defined bits.) Reported by + Leonid Bugaev. + +- Escape control characters in filenames written to the log file (CWE-117 log + injection): a transferred name containing control bytes -- C0 (tab excepted) + and C1 `0x80`-`0x9f`, including CSI `0x9b` -- could otherwise inject terminal + escape sequences into an administrator's terminal when the log is viewed. + Reported by Leonid Bugaev. + +- Stop `safe_arg()` leaking an uninitialized byte into a quoted filename. In + filename mode the writer suppresses the escaping backslash before a wildcard, + but the counter that sized the buffer reserved a slot for every backslash, so + the two disagreed and left an uninitialized heap byte in the returned string + -- which is handed to the remote shell when `--protect-args` is off. The + counter now mirrors the writer, and guarding the wildcard test with `f[1]` + also fixes a trailing backslash (previously `strchr()` matched the string + terminator, so the backslash was not doubled). Reported by Leonid Bugaev. + +- Close a `--safe-links` bypass in `--backup`: when symlinks can be hard-linked, + `make_backup()`'s link/rename fast path hard-linked an unsafe (out-of-tree) + symlink into the backup area and skipped the `safe_symlinks` check the copy + path applies, silently preserving a link `--safe-links` was meant to drop. The + safe-links check now runs before the fast path, and a symlink whose target is + unreadable is failed closed rather than backed up unchecked. Reported by + Leonid Bugaev. + +- Extend the operator-directory ownership walk to the backup leaf sinks: + `do_symlink_at()` (backing a symlink up into an operator `--backup-dir`) and + `do_rmdir_at()` (removing a pre-existing backup directory) now resolve their + parent through the same ownership walk, so a foreign-owned parent symlink no + longer redirects the backup symlink-create or directory-removal outside the + backup tree. `--insecure-links` (or a module's `insecure links = yes`) restores + the legacy follow. Reported by Omar Elsayed (seks99x). + +- Confine an absolute operator source/destination through the ownership walk in + `robust_rename()`'s cross-filesystem (EXDEV) copy fallback, so a raced parent + symlink cannot redirect the fallback copy or its source unlink out of the tree. + Reported by Leonid Bugaev. + +- Bound the number of equal-weak-checksum blocks examined per offset in + `hash_search()` (issue #217), so a crafted or degenerate checksum set with a + very long equal-checksum chain cannot drive the sender's per-offset + match-verify into a quadratic blow-up (CPU DoS). Fix by Stuart Inglis. + +### BUG FIXES: + +- Fix an off-by-one in `clean_fname()`'s `..`-collapse path normalization. + Reported by Leonid Bugaev. + +- The AVX2 rolling-checksum assembly (`--enable-roll-asm`) read up to 64 bytes + past the end of the buffer it was given. The loop is software-pipelined and + preloaded the 64 bytes after the ones it was folding in, so its last iteration + always reached beyond the data -- the remainder is by construction under 64 + bytes. It normally landed in slack inside rsync's map window and went + unnoticed; where the buffer ended at a page boundary it was a SIGSEGV mid + transfer, reported on macOS x86-64 by Roland Kletzing. Reported checksums are + unchanged. + +- `--link-dest` no longer fails the transfer when the destination refuses to + hard-link a symlink, device node, FIFO or socket. Whether rsync hard-links + those at all was decided at build time, on whatever filesystem the source tree + happened to sit on, and one host can hold both answers -- macOS builds on + APFS, which can, and backs up to HFS+, which returns ENOTSUP. Such an entry + is now copied, exactly as it already is in a build that cannot link them and + as a regular file in the same position already was; the run used to exit 23 + even though the entry was then created correctly. The fallback covers any + refusal, since the error does not identify one on its own: link(2) documents + EPERM both for a filesystem without hard links and for a permission refusal. + Still outstanding: under `-H`, a group of such entries hard-linked to each + other also needs a link within the destination, and where the destination + cannot hard-link the type at all, the members after the first are still lost. + +- `--out-format` / `--log-file-format` now emit a literal `%` for `%%` instead of + mis-parsing the following character (added by Leonid Bugaev); a follow-up bounds + `log_format_has()`'s width-digit scan to match `log_formatted()`, closing a `%C` + read past the checksum field. + +- A CVS `.cvsignore` (or `-C`) file containing a `!` clear-list token no longer + aborts with a spurious "rule has trailing characters" error. Reported by + Leonid Bugaev. + +- `--chmod=a+s` now sets both the setuid and setgid bits, matching `chmod(1)` + (it previously set setuid only). Reported by Leonid Bugaev. + +- Case-insensitive wildcard matching (used by daemon `hosts allow`/`hosts deny` + rules) now folds characters inside a `[...]` bracket expression, not just + literal pattern characters. Reported by Leonid Bugaev. + +### BEHAVIOR CHANGES: + +- A non-daemon receiver follows an operator-named symlinked destination directory + only when the symlink is owned by root or the running user (e.g. `rsync -a src/ + /backup/` where `/backup -> /mnt/disk`); a destination symlinked by another uid + is now refused, closing a chdir TOCTOU where an attacker raced the named + destination into a symlink. `--insecure-links` restores the unconditional + follow. + +- On platforms without a race-safe way to create a unix socket in a subdirectory + (the BSDs, macOS, Solaris, which lack `bindat()`), a nested socket transferred + under `--specials` is skipped with a warning instead of failing the whole + transfer. Top-level sockets are unaffected. + +- `proxy protocol = true` with no `proxy protocol hosts` rejects all connections + (fail-closed); the daemon now warns about this at startup. + +- `support/rrsync` in a restricted subdirectory forces `--no-D` (device/special + semantics are stripped, so a plain `rsync -a` still works) and denies + `--copy-unsafe-links`. + +- The path resolver now follows in-tree directory symlinks uniformly on every + platform via a single race-free per-component `O_NOFOLLOW` walk, so `-K` / + `-L` / `-k` and `-R` through an in-tree symlinked parent behave the same + everywhere. + +# NEWS for rsync 3.4.4 (8 Jun 2026) + +## Changes in this version: + +This is a conservative point release that backports regression fixes +on top of 3.4.3. No new features are included. + +### BUG FIXES: + +- Honour a relative alt-basis directory (e.g. `--link-dest=../sibling`, + `--copy-dest`, `--compare-dest`) on a daemon receiver running with + `use chroot = no`. Such a path is re-anchored at the module root but + was then rejected by the receiver's secure open; it now works where + kernel-enforced confinement is available. See the PORTABILITY note + below for the platform limitation. Fixes #915. + +- sender: open a module-root-absolute path for a `path = /` module so a + daemon serving the filesystem root can satisfy absolute request + paths again. Fixes #897. + +- flist: accept the missing-args mode-0 entry in recv_file_entry. + Fixes #910. + +- receiver: fix a false "failed verification -- update discarded" when + resuming a delta transfer with an absolute `--partial-dir`. + +- receiver: fix a NULL dereference on the delta discard path. + +- generator: cap the block s2length at the negotiated checksum length. + +- main: fix `--mkpath` with `--dry-run` for a file-to-file copy. + Fixes #880. + +- daemon: un-backslash escaped option args. Fixes #829. + +- token: drain the matched-block insert deflate. Fixes #951. + +- Fix the "update skips a file of a different type" case and the + daemon upload delete stats. + +- alloc: revert "zero all new memory from allocations". Fixes #959. + +- Always clear the stat buffer and validate nanoseconds before use. + +### PORTABILITY / BUILD: + +- The relative alt-basis fix for daemon receivers (#915) relies on + kernel "stay below dirfd" path resolution -- `openat2(RESOLVE_BENEATH)` + on Linux 5.6+, or `openat()` with `O_RESOLVE_BENEATH` on FreeBSD 13+ + and macOS 15+. On platforms that lack it (Solaris, OpenBSD, NetBSD, + Cygwin and older Linux) `secure_relative_open()` deliberately rejects + any path with a `..` component, so relative alt-basis directories + remain unavailable there -- function traded for safety, matching the + trade-off already documented for the #715 fix. Absolute alt-basis + paths are unaffected on every platform. + +- openat2 is now autodetected at configure time (HAVE_OPENAT2): the + `openat2(RESOLVE_BENEATH)` resolver is compiled in only when both + `` and the `SYS_openat2` syscall number are present, + fixing the build on older kernels/headers. Fixes #924, #905, #900, + #904. + +- Fall back to do_mknod() when mknodat() / mkfifoat() are unavailable. + Fixes #896. + +- Install generated manpages correctly in an out-of-tree build. + +### DEVELOPER RELATED: + +- Added a CI workflow that builds this stable branch and runs the + `v34-stable-testsuite` regression suite against the built binary, + giving regression coverage without importing the full master test + suite into the stable branch. + +- Added a check-progs target for fleettest and extended the build + workflows to run on `*-stable` release branches. + +### CREDITS: + +Thanks to everyone who helped with this release: + +- Code contributions from Zen Dodd (steadytao), Mike-Goutokuji, + pterror, and Stiliyan Tonev (Bark). + +- Zen Dodd (steadytao) also reviewed the 3.4.4 backport set (PR #980). + +- Bug reports from @mmayer (#924), @fda77 (#905), @darkshram (#900), + @ketas (#904), @pkzc (#880), @brabalan (#951), @elcamlost (#829), + @debohman (#896), @guilherme-puida (#959), @fufu65 (#915), + @JetAppsClark (#928), @moonlitbugs (#897), @mgkeeley (#910), and + @sylvain-ilm (#724, #725). + +# NEWS for rsync 3.4.3 (20 May 2026) + +## Changes in this version: + +### SECURITY FIXES: + +Six CVEs are fixed in this release. All six are assigned by +VulnCheck as CNA. Affected versions are 3.4.2 and earlier in every +case. Three of the six (CVE-2026-29518, CVE-2026-43617, +CVE-2026-43619) require non-default daemon configuration to reach: +the first and third need `use chroot = no` for a module, the second +needs `daemon chroot = ...` set in rsyncd.conf. Two (CVE-2026-43618, +CVE-2026-43620) are reachable from a normal pull or a normal +authenticated daemon connection. The sixth (CVE-2026-45232) is +reachable only when `RSYNC_PROXY` is set and the proxy (or a MITM) +returns a pathological response. Many thanks to the external +researchers who reported these issues. + +- CVE-2026-29518 (CVSS v4.0 7.3, HIGH): TOCTOU symlink race condition + allowing local privilege escalation in daemon mode without chroot. + An rsync daemon configured with "use chroot = no" was exposed to a + time-of-check / time-of-use race on parent path components: a local + attacker with write access to a module could replace a parent + directory component with a symlink between the receiver's check and + its open(), redirecting reads (basis-file disclosure) and writes + (file overwrite) outside the module. Default "use chroot = yes" is + not exposed. `secure_relative_open()` (added in 3.4.0 for + CVE-2024-12086) was previously unused in the daemon-no-chroot + case; the fix enables it there and reroutes the sender's + read-path opens through it. Reported by Nullx3D (Batuhan Sancak), + Damien Neil and Michael Stapelberg. + +- CVE-2026-43617 (CVSS v3.1 4.8, MEDIUM): Hostname/ACL bypass on an + rsync daemon configured with `daemon chroot = /X` in rsyncd.conf + when the chroot tree lacks DNS resolution support. The + reverse-DNS lookup of the connecting client was performed *after* + the daemon chroot had been entered; if /X did not contain the + libc resolver fixtures (`/etc/resolv.conf`, `/etc/nsswitch.conf`, + `/etc/hosts`, NSS service modules) the lookup failed and the + connecting hostname was set to "UNKNOWN", causing hostname-based + deny rules to silently fail open. IP-based ACLs are unaffected. + The per-module `use chroot` setting is unrelated to this issue. + The fix performs the lookup before entering the daemon chroot. + Reported by MegaManSec. + +- CVE-2026-43618 (CVSS v3.1 8.1, HIGH): Integer overflow in the + compressed-token decoder enabling remote memory disclosure to an + authenticated daemon peer. The receiver accumulated a 32-bit + signed counter without overflow checking; a malicious sender could + trigger an overflow that, with careful manipulation, leaked process + memory contents to the attacker -- environment variables, + passwords, heap and library pointers -- significantly weakening + ASLR. The fix bounds the counter and adds wire-input validation in + several adjacent places (defence-in-depth). Workaround for older + releases: `refuse options = compress` in rsyncd.conf. Reported by + Omar Elsayed. + +- CVE-2026-43619 (CVSS v3.1 6.3, MEDIUM): Symlink races on path-based + system calls in "use chroot = no" daemon mode (generalisation of + CVE-2026-29518). Earlier fixes for symlink races on the receiver's + open() call missed the same race class on every other path-based + system call: chmod, lchown, utimes, rename, unlink, mkdir, symlink, + mknod, link, rmdir and lstat. The fix routes each affected + path-based syscall through a parent dirfd opened under + RESOLVE_BENEATH-equivalent kernel-enforced confinement (openat2 on + Linux 5.6+, O_RESOLVE_BENEATH on FreeBSD 13+ and macOS 15+, + per-component O_NOFOLLOW walk elsewhere). Default "use chroot = + yes" is not exposed. Reported by Andrew Tridgell as a follow-on + audit of CVE-2026-29518. + +- CVE-2026-43620 (CVSS v3.1 6.5, MEDIUM): Out-of-bounds read in the + receiver's recv_files() enabling remote denial-of-service of any + client pulling from a malicious server (incomplete fix of commit + 797e17f). The earlier parent_ndx<0 guard added to send_files() was + not applied to the visually-identical block in recv_files(). A + malicious rsync server can drive any connecting client into a + deterministic SIGSEGV by setting CF_INC_RECURSE in the + compatibility flags and sending a crafted file list and transfer + record. inc_recurse is the protocol-30+ default, so no special + options are required on the victim. Workaround for older + releases: `--no-inc-recursive` on the client. Reported by Pratham + Gupta. + +- CVE-2026-45232 (CVSS v3.1 3.1, LOW): Off-by-one out-of-bounds stack + write in the rsync client's HTTP CONNECT proxy handler + (`establish_proxy_connection()` in `socket.c`). After issuing the + CONNECT request, rsync read the proxy's first response line one + byte at a time into a 1024-byte stack buffer with the bound + `cp < &buffer[sizeof buffer - 1]`. If the proxy (or a MITM in + front of it) returned 1023+ bytes on that first line without a + newline terminator, `cp` exited the loop pointing at a buffer slot + the loop never wrote, leaving `*cp` holding stale stack data from + the earlier `snprintf()` of the outgoing CONNECT request. The + post-loop logic then wrote a single `\0` one byte past the end of + the buffer on the stack. Reach is client-side only, and only when + `RSYNC_PROXY` is set so rsync tunnels an `rsync://` connection + through an HTTP CONNECT proxy. The written byte is always `\0` + and the offset is fixed by the buffer size, not attacker-chosen, + so this is not an arbitrary-write primitive: practical impact is + corruption of one adjacent stack byte and possible later + misbehaviour or crash. The fix detects the "buffer filled without + finding `\n`" case explicitly by position and refuses the response + with "proxy response line too long". Reported by Aisle Research + via Michal Ruprich (rsync-3.4.1-2.el10 QE). + +In addition to the six CVE fixes, this release adds defence-in-depth +hardening on several adjacent paths: bounded wire-supplied counts and +lengths in flist/io/acls/xattrs, a guard against length underflow in +cumulative `snprintf()` callers, a parent block-index bounds check on +the receiver, a NULL check in `read_delay_line()`, a lower ceiling on +`MAX_WIRE_DEL_STAT` to avoid signed-int overflow in the +`read_del_stats()` accumulator, rejection of hyphen-prefixed +remote-shell hostnames (defence-in-depth against argv-injection in +tooling that forwards untrusted input into the hostspec position; +reported by Aisle Research via Michal Ruprich), and a NULL-check on +`localtime_r()` in `timestring()` to keep a malicious server from +crashing the client by advertising a file with an out-of-range +modtime. + +### BUG FIXES: + +- Fixed a bypass of `--safe-links` when `--backup` is also used on a system that supports hard-linking symlinks (Linux, macOS). An escaping symlink that should have been skipped was silently preserved in the backup area. + +- Fixed a spurious abort when using `-C` (cvs-exclude) mode with a `.cvsignore` file that contained a `!` (clear-list) token. + +- Updated the `--max-alloc` documentation to reflect that 0 is now rejected (CVE-2026-53794). + +- Fixed the EXIT VALUES table: removed nonexistent code 6, added missing codes 15/16/19, corrected SIGUSR1 classification. + +- Fixed a regression introduced by the 3.4.0 secure_relative_open() + CVE fix where legitimate directory symlinks on the receiver side + (e.g. when using `-K` / `--copy-dirlinks`) caused "failed + verification -- update discarded" errors on delta transfers. The + old code rejected every symlink in the path with a per-component + `O_NOFOLLOW` walk; the receiver now uses kernel-enforced "stay + below dirfd" path resolution where available. Fixes #715. + +### PORTABILITY / BUILD: + +- secure_relative_open() now uses `openat2(RESOLVE_BENEATH | + RESOLVE_NO_MAGICLINKS)` on Linux 5.6+, and `openat()` with + `O_RESOLVE_BENEATH` on FreeBSD 13+ and macOS 15+ (Sequoia) / + iOS 18+. The kernel rejects ".." escapes, absolute symlinks, and + symlinks whose target lies outside the starting directory, while + still following symlinks that resolve within it -- the same + trade-off that fixes the issue #715 regression without weakening + the original CVE protection. Other platforms (Solaris, OpenBSD, + NetBSD, Cygwin) retain the previous per-component `O_NOFOLLOW` + walk; on those platforms the issue #715 regression remains + visible. + +- testsuite/xattrs: ignore `SUNWattr_*` in the Solaris `xls` + helper. + +### DEVELOPER RELATED: + +- Added testsuite/symlink-dirlink-basis.test (taken from PR #864 + by Samuel Henrique) covering the issue #715 regression and + several edge cases (`--backup`, `--inplace`, `--partial-dir` + with protocol < 29, top-level files). The test skips on + platforms without a RESOLVE_BENEATH equivalent. + +- Added regression tests for the new security fixes: + `chmod-symlink-race.test`, `chdir-symlink-race.test`, + `bare-do-open-symlink-race.test`, `alt-dest-symlink-race.test`, + `copy-dest-source-symlink.test`, `sender-flist-symlink-leak.test`, + `secure-relpath-validation.test`, `daemon-chroot-acl.test` and + `daemon-refuse-compress.test`. The symlink-race tests skip on + Cygwin, Solaris, OpenBSD and NetBSD (no RESOLVE_BENEATH + equivalent on those platforms). + +- runtests.py now errors early with a clear message when any of + the test helper programs (`tls`, `trimslash`, `t_unsafe`, + `t_chmod_secure`, `t_secure_relpath`, `wildtest`, `getgroups`, + `getfsdev`) are missing, instead of letting many tests fail with + confusing "not found" errors. + +- Added OpenBSD and NetBSD CI jobs that run `make check` on those + platforms. + +- Added Ubuntu 22.04 and AlmaLinux 8 CI workflows so future + backports to the two mainstream LTS families build and test on + the same CI surface as trunk. + +- testsuite/protected-regular.test now runs unprivileged via + `unshare` with user-namespace UID mapping, falling back to skip + if `unshare`/`uidmap` is not available; previously it required + real root. + +- Added `symlink-dirlink-basis` to the Cygwin CI's expected-skipped + list. + +- Removed the old release system (replaced by the new release + script in 3.4.2). + +------------------------------------------------------------------------------ + +# NEWS for rsync 3.4.2 (28 Apr 2026) + +## Changes in this version: + +### SECURITY RELATED: + +Several security-relevant defects were reported and fixed since 3.4.1. +None were assigned a CVE — rsync's fork-per-connection design scopes +the impact of each of these to the attacker's own connection, which is +equivalent to the client closing the socket itself — but they are +fixed here as a matter of hygiene and to reduce the chances of a +future exploitable combination. Many thanks to the external +researchers who reported these issues. + +- Fixed a signed integer overflow in the PROXY protocol v2 header + parser: a negative `len` field could bypass the size check and cause + a stack buffer overflow in `read_buf()`. Reported by John Walker of + ZeroPath. + +- Fixed an invalid access to the files array. Reported by Calum + Hutton of Rapid7. + +- Reject negative token values in the compressed-stream token + decoder; a negative value could cause callers to misinterpret a + missing data pointer as literal data. Reported by Will Sergeant. + +- Fixed the element count passed to the xattr `qsort()` (see + https://www.openwall.com/lists/oss-security/2026/04/16/2). + +- Fixed a buffer underflow in `clean_fname()`, and added a regression + test. + +- Fixed an uninitialized `mul_one` in the AVX2 get_checksum1 path + (undefined behaviour), and added a SIMD-checksum self-test that + cross-checks SSE2, SSSE3 and AVX2 against the C reference on both + aligned and unaligned buffers. + +- Fixed an uninitialized `buf1` on the first call to + `get_checksum2()` in the MD4 path (fixes #673). + +- Zero all new memory from internal allocations: `my_alloc()` now uses + `calloc`, and `expand_item_list()` zeros the expanded portion after + `realloc`. This gives more predictable behaviour if stale or + uninitialised memory is ever accidentally read. + +### BUG FIXES: + +- Call `tzset()` before chroot so that log timestamps continue to + reflect the configured local timezone after the daemon chroots + (glibc needs `/etc/localtime`, which is unreachable post-chroot). + +- Use the correct time when writing to the log file. + +- Do not clear `DISPLAY` unconditionally. + +- Fixed a Y2038 bug in `syscall.c` by replacing the `Int32x32To64` + macro (which truncates its arguments to 32 bits) with a plain + 64-bit multiplication. + +- Fixed ACL ID mapping for non-root users (closes #618). + +- Fixed handling of objects with many xattrs on FreeBSD. + +- Fixed `--open-noatime` not taking effect when opening regular + files: `O_NOATIME` is now also passed to `do_open_nofollow()`, which + has been used for regular files since the CVE fix "fixed symlink + race condition in sender". + +- Ignore "directory has vanished" errors. + +- Fixed the removal of multiple leading slashes. + +- Added the missing `--dirs` long option. + +- Fixed a segfault if `poptGetContext()` returns NULL (e.g. under + OOM) by not passing NULL to `poptReadDefaultConfig()`. Reported by + Ronnie Sahlberg; found with `malloc-fail-tester`. + +- Fixed a build error on ia64 NonStop (which treats missing + prototypes as an error, not a warning). + +- Fixed a flaky hardlinks test (fixes #735). + +### ENHANCEMENTS: + +- Added multi-threaded `zstd` compression, gated by a new + `--compress-threads=N` option, with validation and man-page + coverage. + +- Documented the `temp dir` parameter in the rsyncd.conf man page + (fixes #820). + +- Improved rendering of interior dashes in long-option names in + `md-convert` (perhaps fixes #686). + +### PORTABILITY / BUILD: + +- Fixed glibc 2.43 const-preserving overloads of `strtok()`, + `strchr()` etc. by declaring the affected locals with the right + constness. Contributed by Holger Hoffstätte. + +- Converted the bundled zlib 1.2.8 from K&R-style function + definitions to ANSI prototypes, so it builds with clang 16+. + +- Avoid using `bool` as an identifier; it is a keyword in C23. + +- `configure.ac`: check for xattr functions in libc first and only + fall back to `-lattr`, avoiding spurious overlinking when `-lattr` + happens to be installed. Contributed by Eli Schwartz. + +- Made the build reproducible by honouring `SOURCE_DATE_EPOCH` for + the manpage date. + +- Removed obsolete `popt/findme.c` and `popt/findme.h` that upstream + popt 1.14 folded into `popt.c` (fixes #710). Contributed by Alan + Coopersmith. + +### INTERNAL: + +- Made many module-global variables `const` so they can live in + `.rodata` and enable additional compiler optimization. + +### DEVELOPER RELATED: + +- Replaced `runtests.sh` with `runtests.py`, a Python test runner + that supports `--valgrind` (with per-process log files so valgrind + output no longer interferes with output comparisons) and + `-j/--parallel` execution for roughly a 7× speed-up on typical + hardware. + +- Added a SIMD checksum self-test and a `clean-fname-underflow` + regression test. + +- Various CI fixes for macOS and Cygwin (including adding + `simd-checksum` to the expected-skipped lists on platforms without + SIMD), and tests now run on `ubuntu-latest`. + +- removed support for the unmaintained rsync-patches archive + +------------------------------------------------------------------------------ + # NEWS for rsync 3.4.1 (16 Jan 2025) Release 3.4.1 is a fix for regressions introduced in 3.4.0 @@ -19,6 +900,7 @@ - fix to permissions handling in the developer release script ------------------------------------------------------------------------------ + # NEWS for rsync 3.4.0 (15 Jan 2025) Release 3.4.0 is a security release that fixes a number of important vulnerabilities. @@ -73,6 +955,7 @@ - added FreeBSD and Solaris CI builds ------------------------------------------------------------------------------ + # NEWS for rsync 3.3.0 (6 Apr 2024) ## Changes in this version: @@ -4837,8 +5720,12 @@ | RELEASE DATE | VER. | DATE OF COMMIT\* | PROTOCOL | |--------------|--------|------------------|-------------| +| 13 Aug 2026 | 3.5.0 | | 32 | +| 08 Jun 2026 | 3.4.4 | | 32 | +| 20 May 2026 | 3.4.3 | | 32 | +| 28 Apr 2026 | 3.4.2 | | 32 | | 16 Jan 2025 | 3.4.1 | | 32 | -| 15 Jan 2025 | 3.4.0 | | 32 | +| 15 Jan 2025 | 3.4.0 | 15 Jan 2025 | 32 | | 06 Apr 2024 | 3.3.0 | | 31 | | 20 Oct 2022 | 3.2.7 | | 31 | | 09 Sep 2022 | 3.2.6 | | 31 | diff -Nru rsync-3.4.1+ds1/README.md rsync-3.5.0+ds1/README.md --- rsync-3.4.1+ds1/README.md 2024-12-17 21:55:45.000000000 +0000 +++ rsync-3.5.0+ds1/README.md 2026-07-20 04:05:31.000000000 +0000 @@ -93,6 +93,15 @@ [3]: https://rsync.samba.org/lists.html +DISCORD +------- + +There is also an rsync [Discord server][d] for real-time chat about rsync +and its development. + +[d]: https://discord.gg/Avfvy9zhdp + + BUG REPORTS ----------- @@ -136,6 +145,8 @@ Rsync was originally written by Andrew Tridgell and Paul Mackerras. Many people from around the world have helped to maintain and improve it. +Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024. + Rsync may be used, modified and redistributed only under the terms of the GNU General Public License, found in the file [COPYING][9] in this distribution, or at [the Free Software Foundation][10]. diff -Nru rsync-3.4.1+ds1/SECURITY.md rsync-3.5.0+ds1/SECURITY.md --- rsync-3.4.1+ds1/SECURITY.md 2025-01-15 01:13:41.000000000 +0000 +++ rsync-3.5.0+ds1/SECURITY.md 2026-08-01 09:19:42.000000000 +0000 @@ -11,3 +11,532 @@ Rsync Project + +## Approach to platform residuals + +rsync hardens its security-sensitive operations — path resolution, metadata +application, file/socket creation — against local attacks such as parent-symlink +TOCTOU races. Some of these operations can only be made race-safe with a +primitive the underlying OS provides (an `*at()` syscall on a held directory fd, +an fdescfs-style `/proc/self/fd` magic symlink, `mknodat()`, the `*xattrat` +syscalls, and so on), and that primitive is not available on every supported +platform. + +The guiding rule for those cases is: + +> **On a modern Linux system every issue described in this document is fully +> addressed.** Where an operation *can* be secured on some platforms but *cannot* +> be secured on others, and the residual risk is a *local* privilege-escalation +> or data-disclosure class (an attacker who already has write access inside the +> transferred tree), rsync prefers keeping the operation functional on the +> platforms that lack the primitive over disabling a long-standing feature for +> everyone on those platforms. + +So a hardened operation takes the race-safe path wherever the platform offers one +and falls back to the historical (path-based, unconfined) behaviour only where it +does not — rather than refusing the operation outright. Each such fallback is an +accepted residual, documented under "Known residuals" below, and on the daemon it +can be turned off per feature with `refuse options = ...`. The residuals are +therefore confined to non-Linux platforms (the BSDs, macOS, Solaris/illumos), +Cygwin, and — for a few features — pre-6.13 Linux kernels; a current, normally +configured Linux deployment carries none of them. (The `/proc/self/fd`-based +fallbacks assume a mounted `/proc`, which every standard Linux provides; a +deliberately `/proc`-less container is the one Linux case that can still hit a +residual.) + +The one deliberate exception is an operation whose unconfined fallback would +*create a new filesystem object at an attacker-influenceable path* rather than set +metadata on the object rsync already transferred: the nested-socket `bind()` on +platforms without a race-safe socket-create (no `bindat()`). There the unsafe path +is an out-of-tree write/create primitive, not a same-object metadata race, and a +transferred socket inode is a worthless placeholder, so rsync refuses (skips) it +rather than keeping it functional. A leaf permission change is likewise failed +closed rather than applied through a raced symlink, but only as a rare backstop: +the common file/dir/FIFO case is secured on every platform via `fchmod` on a held +fd, so no real functionality is lost. + +This trade-off applies only to these local-attacker residual classes. Remotely +reachable defects — memory safety, authentication bypass, protocol parsing, input +bounds — are fixed unconditionally on all platforms, never left as a residual. + + +## Robustness against malicious peers + +rsync treats everything the peer sends — the file list, checksum headers, +multiplexed messages, forwarded daemon arguments, filter rules — as untrusted, +and bounds-checks it before use. A peer-triggerable crash of a connection's +worker process is treated as a defect to be fixed, even though the daemon's +fork-per-connection model confines such a fault to that one connection rather +than the whole service. + +Alongside the issues enumerated elsewhere in this document, the code is hardened +continuously through protocol fuzzing (driving the daemon protocol against a +writable module) and static analysis, with a CI gate. This release closes a +batch of peer-triggerable faults found that way: NULL-dereference and +reachable-assert crashes from crafted file lists or indices, reads past a +file-list allocation (mostly bounded over-reads of an entry's extra slots), +unbounded merge-file and suffix-list recursion, and several bounded +out-of-bounds writes driven by peer-supplied lengths or option arguments. Each +is fixed at the root with a bounds or validity check plus a defence-in-depth +guard at the use site, and carries a regression test. + +Two further peer-input hardenings in this release: a peer-supplied I/O-error +value (the `MSG_IO_ERROR` message and the file-list trailer) is masked to the +defined `IOERR_*` bits, so a peer cannot set arbitrary error flags in the local +`io_error` that would be stored and re-forwarded upstream; and control +characters in a (peer-controlled) filename written to the log file are escaped, +so a name carrying C0/C1 terminal-escape bytes cannot inject sequences into an +administrator's terminal when the log is viewed (CWE-117). The number of +equal-weak-checksum blocks `hash_search()` examines per offset is also bounded +(issue #217), so a crafted or degenerate checksum set with a very long +equal-checksum chain cannot drive the sender's per-offset match-verify into a +quadratic walk and pin one connection's CPU. + +Contributors adding code that consumes peer input should validate it at the +point of receipt rather than relying on a downstream check. + + +## Symlink-race-safe path resolution + +This section documents how rsync defends against parent-directory symlink races +(a TOCTOU / confused-deputy class) and the per-platform approach it takes, so +that contributors and automated agents extend the code consistently rather than +reintroducing the weakness. + +### The threat + +Many rsync operations resolve pathnames that an unprivileged party can partially +control: a receiver writing into a destination tree, a sender reading a source +tree, and temp and partial files, and so on. (The operator-chosen directory +paths — `--link-dest`/`--compare-dest`/`--copy-dest`/`--backup-dir`/`--temp-dir`/ +`--partial-dir` — may legitimately point outside the tree, so they are resolved +by the ownership walk described under *Symlink defense for operator-supplied +paths* below rather than the strict transfer-path resolver here.) If someone who +can write inside that tree races a +parent directory component between a real directory and a symlink ("symlink +flipping"), a path-based syscall — `open`, `stat`, `chmod`, `chown`, `utimes`, +`rename`, `unlink`, `mkdir`, `mknod`, `symlink`, hard-link creation — can be +redirected to a target *outside* the intended tree. When rsync resolves that +path with more authority than the component's controller and without a +confinement boundary, this is a confused-deputy bug (e.g. a root nightly backup +capturing `/etc/shadow`, or a root receiver chmod/chown/unlink-ing a system +file). + +`O_NOFOLLOW` on the final component is **not** sufficient: the *parent* +components must be resolved safely. + +The boundary that matters is **authority plus confinement**, not "daemon vs +non-daemon". A non-chroot daemon module, a root-run local transfer, and a +two-user transfer are all unconfined privileged path resolvers. Where a real +confinement boundary already exists (e.g. a per-module `chroot`) that is the +strongest protection; otherwise rsync must resolve paths defensively. + +A `chroot` is only a boundary for the *outer* path it confines. A daemon module +written as `path = /outer/./inner` (`use chroot = yes`) chroots to `/outer` but +treats `/inner` as the module root, so a symlink inside the module that points to +a sibling of `/inner` is still inside the chroot yet outside the module — the +inner module therefore needs the same defensive resolution as a non-chroot +module. The single gate that decides when hardened resolution applies is +"unconfined privileged resolver": `am_daemon && (!am_chrooted || module_dirlen)` +for the daemon (any non-chroot module, plus a `/./` inner-module chroot), and any +non-chroot receiver. The local sender's content open is confined the same way for +default symlink handling; only the symlink-following modes (`-L`/`--copy-links`/ +`--copy-unsafe-links`/`-k`) and `--insecure-links` are excluded, so those keep +following symlinks by design. + +### The mechanism + +Resolution of attacker-influenceable paths goes through `secure_relative_open()` +and the `do_*_at()` wrappers in `syscall.c`, never a raw `open()`/`rename()`/ +`chmod()` on a full path string. The principle is: **trust the operator-named +transfer root, and confine all resolution beneath it**, rejecting escapes via +`..` above the anchor, absolute symlinks, or out-of-tree symlinks. +`secure_relative_open()` resolves the parent directory by walking it one +component at a time on a stack of held directory fds, then operates on the final +component with an at-style call on the resulting directory fd. + +For per-entry work the receiver and generator go one step further and hold the +parent directory open: `open_dir_secure()` resolves an entry's directory once +(via `secure_relative_open()`), `held_dfd_for()` caches that descriptor for the +duration of the entry, and every operation on the entry — `lstat`, the temp-file +`mkstemp`, the temp->final `rename`, `chmod`/`chown`/`utimes`, `mkdir`, special- +file and symlink creation, the delta-basis open, and the recursive delete — runs +through that one held fd via an `*at()` call (`do_*_atfd()`). Because the +descriptor is pinned to the directory inode, a parent component flipped to a +symlink *after* the open cannot redirect any of those operations. The alternate- +destination lookups are confined the same way (`basis_link_stat()` in +`generator.c` and `secure_basis_open()` in `receiver.c`), so a peer-chosen +`--link-dest`/`--compare-dest`/`--copy-dest` basis index cannot reach an +out-of-module file through a symlinked parent. + +The sender's source-directory *enumeration* is confined the same way as its +content open. `send_directory()` opens each scanned directory through +`secure_opendir()` — which resolves it via `secure_relative_open()` / +`secure_relative_open_at()` and turns the held fd into the `DIR*` with +`fdopendir()` — so a parent component raced into a symlink, or (for a daemon +following mode) an in-module symlink pointing outside the module, cannot redirect +the scan to enumerate an out-of-tree directory and leak its entry names, metadata +and symlink targets. For a daemon, both the enumeration and the content open +anchor at the served module root **pinned by identity**: `module_dirfd` is opened +(`open(".")`) the moment the daemon `chdir`s into the module, while still +privileged, and module-relative paths resolve beneath that fd via +`secure_relative_open_at()`. Anchoring at the held fd rather than re-resolving the +absolute module path keeps the confinement working after the daemon drops to the +module uid even when the module sits under a directory that uid cannot traverse +(e.g. a `0700` home — re-resolving the absolute path would `EACCES`), and is +immune to the logical-path-versus-real-cwd skew a followed in-tree directory +symlink would otherwise introduce. + +### Path resolution + +`secure_relative_open()` resolves a path with a single portable mechanism on +every platform: a per-component walk on a stack of held directory fds. Each +component is opened relative to the held parent with `openat(parent_fd, +"component", O_NOFOLLOW)`; descending into a real subdirectory pushes its fd, a +`..` pops back to the already-held parent (a pop at the anchor is refused), and an +in-tree directory symlink is followed by reading its target and walking that off +the same stack (absolute targets refused, symlink hops bounded). The final +component is opened `O_NOFOLLOW`. + +Because every component is opened relative to a *pinned* fd under `O_NOFOLLOW`, +and `..` is resolved by the held-fd stack rather than by the kernel, the walk is +race-free by construction: no rename or symlink swap of any path name can redirect +resolution outside the anchor subtree, and no kernel "beneath" primitive +(`openat2(RESOLVE_BENEATH)` / `openat(O_RESOLVE_BENEATH)`) is required. The +confinement is therefore uniform across Linux, the BSDs, macOS and +Solaris/illumos, on old and new kernels alike, with nothing to probe or fall back +to at runtime (and so no `openat2`/seccomp interaction to worry about in sandboxed +environments). Cygwin is the exception, because its directory descriptors and +symlink emulation do not give the held-fd walk the same inode pinning — see the +Cygwin residual below. + +Legitimate *in-tree* directory symlinks are followed, so `--keep-dirlinks` / +`--copy-links` and a symlinked module path keep working. A relative alternate-dest +such as `--compare-dest=../01` may legitimately climb to a sibling still inside the +module; such a `..` path is re-anchored at the module root and its in-module climb +adjudicated by the walk (the `..` pops to the held parent), while escapes above the +anchor are still rejected. + +### Leaf operations + +The final operation is hardened as well, following `cp`: reads use `O_NOFOLLOW` +so a flipped leaf symlink is not followed, and new or destination files are +created with `O_CREAT|O_EXCL` (rsync's temporary files use `mkstemp`) so a +planted symlink at the target cannot be written through. A leaf `chmod` is the +one operation with no portable no-follow form: it is closed by opening the leaf +`O_RDONLY|O_NOFOLLOW` and `fchmod`-ing the held fd (refusing a symlink leaf with +`ELOOP`), falling back to `fchmodat(AT_SYMLINK_NOFOLLOW)` and then the +`fchmodat2()` syscall, and failing closed with a warning rather than ever +chmod-ing through a raced leaf symlink. + +### Guidance for contributors + +* When adding code that performs a path-based syscall on a path that can be + influenced by the remote peer or by another local user, use a `do_*_at()` + wrapper (or `secure_relative_open()`), not a raw full-path syscall. +* When introducing a new operation, add a matching `do__at()` wrapper that + resolves the parent with `secure_relative_open()` and acts via an at-style call + on the returned dirfd. +* Do not assume a non-daemon transfer is safe; the question is whether rsync has + more authority than whoever controls the path components. +* On platforms whose API lacks an at-style equivalent (e.g. `setattrlist()`), + follow the residuals policy at the top of this document: for a metadata + operation on the already-transferred object (ACLs, xattrs, crtimes, permissions) + fall back to the path-based call to keep the feature functional and document the + residual; but where the unsafe fallback would *create a new object on an + unconfined path* (the nested-socket `bind()` case), refuse it instead — that is + an out-of-tree write/create primitive, not a same-object metadata race, and the + lost functionality is negligible. + + +## Symlink defense for operator-supplied paths + +rsync opens several operator-supplied paths during normal operation. These fall +into two groups, both governed by the same ownership-walk policy below: + +* operator **files**: `--log-file`, `--password-file`, `--early-input` (a client + read whose contents are forwarded to the daemon's early-exec), `--files-from`, + `--include-from`, `--exclude-from`, `--filter=. file`, `--write-batch`, + `--read-batch`, per-directory filter merge files (`-C` / `-F` / `dir-merge`), + and on the daemon side `motd file =`, `secrets file =`, `lock file =`, and + `rsyncd.conf` itself. +* operator **directories**: `--backup-dir`, `--temp-dir`/`-T`, `--partial-dir`, + and the `--link-dest`/`--compare-dest`/`--copy-dest` basis lookup. These take + a directory the operator chose, which may legitimately point outside the + transfer tree (`--backup-dir=/var/backups`), so they are resolved with the + ownership walk rather than the strict transfer-path resolver. + +The daemon module-root `chdir()` under +`use chroot = no` and the non-daemon receiver's `chdir()` into the +operator-named destination directory are in the same class: both follow +the operator's/root's own symlinked target (the `/backup -> /mnt/disk` +admin pattern) but refuse one an attacker raced in from another uid, +unless `--insecure-links` restores the legacy plain `chdir()`. + +Each of these reads or writes a path the operator or sender chose, which +may transit attacker-influenceable parent directories (the `/tmp/somedir/` +class) or be planted directly (the `/home/$user/.cvsignore` class when +root runs `rsync -a /home /backup`). + +rsync's defense, applied uniformly to all of the above, is a +component-by-component path walk (`open_no_attacker_symlinks` in +`util1.c`) that allows symlinks **only** when the symlink itself is owned +by uid 0 or the running process's effective uid. Symlinks owned by any +other uid are refused with `ELOOP` at any path component (parent or leaf). +Plain `O_NOFOLLOW` would be leaf-only and would not defend the +`/tmp/somedir/log` parent-component plant; this walk does. + +The trust model preserves legitimate setups such as `/var/log -> /data/log` +(root-owned dir-symlink) and a non-root user's own `~/log -> /data/me` +symlink; it refuses an attacker's `/tmp/somedir -> /attack/path` plant. +For `--read-batch` an additional `fstat()` check refuses non-regular +files (FIFOs, devices) at the batch path, since the batch content drives +the receiver's protocol parser. + +**Policy.** A symlink at **any** path component (parent or leaf) is **followed +iff it is owned by uid 0 or the process's effective uid, and refused (`ELOOP`) +otherwise**, identically for **absolute and relative** operator paths. The trust +signal is **authority (ownership)**, not **location**: an operator path may +legitimately point outside the transfer tree, so it cannot be confined by +location the way a transfer path is. This is deliberately distinct from the +transfer-path resolver `secure_relative_open()` (see *Symlink-race-safe path +resolution* above), which refuses **all** symlinks and anchors **beneath the +transfer root** — correct for peer-named paths, which never legitimately escape. +For the operator directory paths, a refused symlink simply makes the target look +absent (no backup/temp/basis is taken through it) and the transfer proceeds +normally; the operator's own symlinked target keeps working. + +**The daemon `exclude`/`filter` chain is not a symlink boundary.** The daemon +filter chain (`exclude`, `exclude from`, `filter`, …) matches the *logical* +module-relative **name** of each item, not the physical file it resolves to. It +is a visibility/tamper filter — a peer cannot *name* a daemon-excluded path to +pull, push to, or delete it — but it is **not** a security boundary against +symlinks: an in-module symlink whose own name is not excluded can be followed to +an excluded target (the name the filter sees, e.g. `link`, is not the excluded +name, e.g. `secret`). This is by design and is the long-standing behaviour of +stock rsync; the defense for a writable module against symlink trickery is +`munge symlinks` (enabled by default for a writable, non-chrooted module), **not** +the filter. Do not rely on `exclude`/`filter` to confine a peer who can introduce +or traverse a symlink; see `rsyncd.conf(5)` ("filter" and "munge symlinks"). + +What *is* enforced for a *peer-supplied* operator path (`--partial-dir`, +`--backup-dir`, the alt-dest basis) is confinement to the **module root**: the +ownership walk refuses a foreign-uid symlink (the symlink-race defense) and +refuses a resolved target *outside* the module. That module-boundary confinement +is independent of `exclude`/`filter` — it holds whether or not the module sets an +exclude — and is what the operator-path tests cover. + +**`--insecure-links`.** This flag is a **local** opt-out that restores the legacy +follow-any-symlink behaviour for the paths above. It is **not forwarded** to the +remote (a remote-shell peer that wants the opt-out must set it on its own side, +e.g. via `--rsync-path`), and a **daemon never honors it**: the opt-out predicate +reads the client-controllable flag only off a daemon, so a peer-forwarded or +`-M`-injected `--insecure-links` cannot weaken a daemon's confinement — the daemon +additionally hard-refuses it (drops the connection) via the refused-options path. +A daemon admin who wants the legacy behaviour for one isolated/trusted module +sets `insecure links = yes` in that module's `rsyncd.conf` stanza (see +`rsyncd.conf(5)`); this is admin-only and re-opens the symlink-escape +vulnerabilities for that module on purpose. The `operator-path-*` and +`insecure-links-*` tests enforce this consistency across every path-taking +option and across absolute/relative, leaf/parent, and same-uid/cross-uid plants. + +For `support/rrsync` (the SSH-restricted-rsync wrapper), the same TOCTOU +class is closed in Python by opening each validated path component with +`O_RDONLY|O_NOFOLLOW`, verifying via `readlink('/proc/self/fd/N')` that the +pinned inode is still in-tree, and passing `/proc/self/fd/N` as the exec'd +rsync's argument (so the kernel routes the child's open through the pinned +inode rather than re-resolving the path). A receiver-side new destination +has no inode of its own yet, so its existing parent directory is pinned the +same way and the leaf is created at `/proc/self/fd//`. This pin +relies on an fdescfs-style magic symlink and is not available on every +platform -- see the rrsync residual below. + +### Known residuals + +The following are documented as out of scope for this release: + +* The source-directory *enumeration* confinement needs `fdopendir()` (to form a + `DIR*` from the securely-resolved held fd) and `dirfd()`; on a platform lacking + either, `send_directory()` falls back to the legacy `opendir()` on the path, so + the scan is unconfined there — the same resolver-fallback shape as the other + `*at()`-less residuals. Every current target provides both; the per-entry + operations and the content open remain confined regardless. + +* On **Cygwin**, the per-component held-fd walk does not provide the same + inode-pinning guarantee as on a POSIX kernel: Cygwin tracks a process's + current directory and resolves directory descriptors by path name rather than + by a pinned inode, and emulates symlinks as special files. Static out-of-tree + symlinks are still refused (the walk sees and rejects them), and a daemon + module path anchored at an absolute `module_dir` is confined; but an entry + whose parent component is *raced* from a directory to a symlink mid-resolution + can still slip past confinement that is anchored at the process CWD (e.g. the + sender's content open), because the descriptor is not bound to the original + inode. The parent-component symlink-race tests are therefore not enforced on + Cygwin (see `RSYNC_EXPECT_SKIPPED` in `.github/workflows/cygwin-build.yml` and + the Cygwin-only xfail in `symlink-race-source_test.py`). Cygwin is a + development/interoperability target, not a privilege boundary host, so this is + accepted for this release. + +* On a platform with no `mknodat()` at all -- macOS before 13 is the + supported example, where `mknod()` and `mkfifo()` exist but neither + `mknodat()` nor `mkfifoat()` does -- creating a device node or FIFO + falls back to plain `do_mknod()`, which resolves the whole path by name. + What is lost is the *pinned parent*: the directory components are + re-resolved by the kernel at create time, so an attacker who can swap a + parent component races the create and can place the node outside the + transfer. The final component is not at risk -- `mknod()` and + `mkfifo()` do not follow a symlink at the leaf, they fail `EEXIST`. + Where `AT_FDCWD` exists -- which is every platform rsync 3.5.0 supports, + macOS 10.13 included -- fake-super placeholders still return through + `openat(..., O_NOFOLLOW)`, reached before either `*at` primitive is + tested, so ordinary in-tree placeholder creation stays confined; + fake-super loses parent confinement and the `O_NOFOLLOW` leaf only on the + paths that reach plain `do_mknod()` (the cache-declined/cross-tree + wrapper and the backup paths). On a build with no `AT_FDCWD` at all + there is no fd-relative primitive of any kind, so nothing above applies + and every special-file create, fake-super included, is unconfined. Transferring specials there (`--devices`, `--specials`) carries + the parent-component race. `symlink-mknod-fakesuper-symlink-race` skips + itself on such a build, since the property it asserts is one the build + deliberately does not have. + +* On platforms where `mknod()`/`mknodat()` cannot create a socket inode + (the BSDs, macOS, Solaris), a transferred socket is recreated with + `socket()` + `unlink` + `bind(path)`, which cannot be confined (there is + no portable `bindat()`). Linux creates it race-safely with `mknodat()` + on a held dirfd; on the others a *nested* socket is skipped with a + warning rather than bound on an unconfined path, leaving only a + top-level, operator-named socket binding by path. + +* `support/rrsync`'s race-free inode-pin -- of both existing path + components and a new destination's parent -- depends on materialising a + held fd as a path that the exec'd rsync re-resolves to the same inode. + rrsync validates and pins in its own process, but it then *exec*s a + separate rsync that re-resolves the paths from `argv`, so the confining + reference must be expressible as an argument. A held dirfd is not: it is + usable as a path only through an fdescfs-style magic symlink. rrsync + implements this for Linux only, via `/proc/self/fd/N`; it does not use the + `/dev/fd/N` equivalent that macOS/FreeBSD expose with `fdescfs` mounted. So on + every non-Linux platform (the BSDs, macOS, Solaris -- whose `/proc/self/fd` + entries are not magic symlinks -- and Cygwin), and on a `/proc`-less Linux + namespace, rrsync falls through to the realpath-validated path unpinned, + so a parent-component or between-pin-and-exec flip remains possible there; + a deeper `-R` new path whose parent does not exist yet is likewise + unpinned. The portable closure is an rsync-side fd-passing API -- rrsync + hands rsync the confined dirfd (inherited across `exec`) and rsync + resolves that argument relative to it with the same `secure_relative_open` + resolver the daemon uses, needing no magic-symlink filesystem -- a + protocol/CLI addition under discussion on the rsync-security list. + +* The operator-directory ownership walk refuses a foreign-owned symlink on a + `--backup-dir`/`--temp-dir`/`--partial-dir`/`--link/compare/copy-dest` path, so + a *statically planted* symlink is rejected and the dependent operation does not + escape. Both the data writes and the *source-metadata reads* of those + operations are now confined to held no-follow fds: the `--copy-dest` + `copy_file()`/`copy_xattrs()` source read goes through the held basis content fd + (`sys_fgetxattr`), and `make_backup()` reads the backed-up file's ACL/xattrs + through a `backup_source_fd()`-pinned fd -- so a parent-component flip can no + longer redirect them to disclose an out-of-module value. The cross-tree + metadata *apply* on those leaves (the `%stat`/ACL/xattr write on a + `--temp-dir`/`--backup-dir` staging file) is fd-pinned the same way, now + including under `--fake-super`: the `set_file_attrs()` no-follow leaf fd was + previously opened only when `am_root >= 0`, so a `fake super = yes` daemon fell + back to a path-based `sys_lsetxattr()`/chmod a raced parent could redirect; the + pin is now opened for fake-super too (a raced leaf is refused, not redirected). + Two narrow follow-ons + re-resolve the (now-validated) operator path by name and remain a + *post-validation* parent-component race: + * the in-place backup (`--inplace --backup`) writes the backup file's data + through a confined create, but its `set_file_attrs()` metadata set + (chmod/chown/times) re-resolves the `--backup-dir` path by name afterwards + (it is not placed under operator mode, which would force the shared + `set_file_attrs()` path off its held-O_NOFOLLOW-fd xattr write and re-open + the very parent-symlink xattr race `copy-xattrs-symlink-race` pins closed); and + * the abbreviated-xattr optimisation reuses a basis xattr value for the + destination only when its checksum matches the digest the sender sent; that + basis read (`rsync_xal_set()`) re-resolves the basis path by name. This is a + *constrained checksum-oracle*, not a disclosure: it confirms that some raced + out-of-module xattr hashes to a value the sender already chose, rather than + copying an unknown value onto a readable file, and needs a colluding sender + plus a local racer. + An attacker who flips a parent component in the window *after* the confined data + write/stat can thus still affect those narrow metadata/oracle operations. This + is the same local-attacker post-confinement TOCTOU class as the ACL/crtimes + residuals below; the data-write and direct source-read escapes are closed, and + `--insecure-links` (or a module's `insecure links = yes`) is orthogonal to it. + +* POSIX ACL application (`-A`/`--acls`) is race-safe on every Linux kernel — + 6.13+ via the `*xattrat` syscalls (or a patched libacl's `*_at` bindings), and + older kernels via the `/proc/self/fd` compat that pins the same inode, provided + `procfs` is mounted — and a transferred file/dir/FIFO has its xattrs (`-X`) + applied through the held no-follow fd, so the apply cannot be redirected by a + raced parent component. Where neither primitive is available — the BSDs, + Solaris and macOS (no `*xattrat` syscalls and no `/proc/self/fd` magic + symlinks), plus the edge case of a Linux instance with no usable `/proc` (a + `/proc`-less container/namespace) — the ACL apply falls back to the path-based + `acl_set_file()` / + `sys_acl_*file()` calls — the long-standing 3.4.x behaviour — to keep `--acls` + functional rather than silently skipping it, so a parent-component flip can + have the received ACL written onto an object outside the module/destination + boundary (and, because the attacker controls the ACL bytes, granted to a chosen + uid). As with the macOS crtime tier below, this is an accepted residual under + the functionality-over-refusal policy; a daemon operator who does not want it + can disable the feature with `refuse options = acls`. + +* macOS creation-time (`--crtimes`) preservation uses the path-based + `setattrlist()`/`getattrlist()` with `FSOPT_NOFOLLOW`, which protects only + the final component; there is no `setattrlistat()` targeting + `ATTR_CMN_CRTIME`. As with POSIX ACLs where the OS offers no race-safe + primitive, `--crtimes` is kept functional (daemon and non-daemon) and the + parent-component symlink race is an accepted residual: an attacker who + flips a parent component can have a crtime read/write target an object + outside the module/destination boundary. The mtime/atime path is *not* + affected -- `set_times()` resolves it race-safely through `utimensat()` on a + held dirfd in hardened mode. A daemon operator who does not want the crtime + residual can disable the feature with `refuse options = crtimes` in + `rsyncd.conf`. + +* Pulling with `-o`/`-g` (or `-a`) **as root from an untrusted sender** is by + design a trust relationship, not a confinement boundary: the sender dictates + each received file's owner/group, including uid/gid 0. rsync maps the + sender's id/name pairs through the local id database; an empty or unknown + sender name falls back to the sender's numeric id (the value `--numeric-ids` + would use), and a sender can equally request root via the literal name + `root`. A root receiver must therefore only pull with `-o`/`-g` from a + trusted source (or use a non-root receiver / a uid-gid policy). The daemon + *name-converter* path is guarded separately — an unknown name there maps to + the sender's numeric id rather than 0 (see `clientserver.c`). + +## Daemon authentication digest + +Daemon authentication is a secret-prefix challenge-response: the client returns +`base64(H(secret || challenge))`, where `H` is a digest the two sides negotiate. +The negotiation is unauthenticated and ordered by the connecting side, and the +`md5`/`md4` digests remain available for backward compatibility, so a peer that +sends no digest list (any rsync before 3.2.0, including the openrsync that ships +with macOS) falls back to `md5` (or `md4` below protocol 30), and an on-path +attacker can rewrite the negotiation to force `md5`/`md4` even between two modern +peers. This is **not** an authentication bypass — `md4`/`md5` have no practical +preimage break — but a weak digest makes a *captured* `(challenge, response)` +pair far cheaper to brute-force offline, recovering a guessable shared secret. + +The challenge itself is seeded from the kernel CSPRNG (`/dev/urandom`), so it is +an unpredictable per-connection nonce. An earlier time/pid-based challenge was +low-entropy enough (~35 bits) that recovering the `(sec, usec, pid)` tuple from +one observed challenge let an on-path observer predict every subsequent challenge +from that daemon process and pre-compute a dictionary against a captured +response. (If `/dev/urandom` is unavailable the daemon logs a warning and falls +back to the legacy time-based challenge rather than a constant.) + +A daemon operator whose clients are all modern (rsync 3.2.7+ built with openssl, +when the SHA digests were added) can require a strong digest with the `auth +digest` module parameter, e.g. `auth digest = sha256`, which refuses any +connection that negotiates — or falls back to — a weaker digest (see +`rsyncd.conf`). + +Residual: there is **no default floor**, because requiring one would break every +pre-3.2.0 client (notably the macOS-bundled openrsync, which authenticates only +with `md4`). An operator who cannot raise the floor should run the daemon behind +a verified TLS transport (`rsync-ssl`/stunnel) or over ssh — which removes the +on-path capture/downgrade vector at the transport layer — and should use a +high-entropy shared secret, which is infeasible to brute-force regardless of the +digest. diff -Nru rsync-3.4.1+ds1/TODO rsync-3.5.0+ds1/TODO --- rsync-3.4.1+ds1/TODO 2020-06-22 21:21:15.000000000 +0000 +++ rsync-3.5.0+ds1/TODO 2026-06-05 01:09:36.000000000 +0000 @@ -15,7 +15,6 @@ DOCUMENTATION -------------------------------------------------------- Keep list of open issues and todos on the web site -Perhaps redo manual as SGML LOGGING -------------------------------------------------------------- Memory accounting @@ -213,16 +212,6 @@ Keep list of open issues and todos on the web site - -- -- - - -Perhaps redo manual as SGML - - The man page is getting rather large, and there is more information - that ought to be added. - - TexInfo source is probably a dying format. - Linuxdoc looks like the most likely contender. I know DocBook is favoured by some people, but it's so bloody verbose, even with emacs support. diff -Nru rsync-3.4.1+ds1/access.c rsync-3.5.0+ds1/access.c --- rsync-3.4.1+ds1/access.c 2022-01-16 01:21:01.000000000 +0000 +++ rsync-3.5.0+ds1/access.c 2026-07-20 04:05:31.000000000 +0000 @@ -28,7 +28,7 @@ extern const char undetermined_hostname[]; -static int match_hostname(const char **host_ptr, const char *addr, const char *tok) +static int match_hostname(const char **host_ptr, const char *addr, const char *tok, int deny) { struct hostent *hp; unsigned int i; @@ -54,8 +54,14 @@ return 0; /* Now try forward-DNS on the token (config-specified hostname) and see if the IP matches. */ - if (!(hp = gethostbyname(tok))) - return 0; + if (!(hp = gethostbyname(tok))) { + /* A deny-list hostname token we cannot resolve must fail CLOSED: + * we can't prove the peer isn't the denied host, so treat the + * unresolvable token as a match (deny). Allow-list tokens keep + * failing as a non-match. Sibling of CVE-2026-43617, which fixed + * only the reverse-lookup path. */ + return deny; + } for (i = 0; hp->h_addr_list[i] != NULL; i++) { if (strcmp(addr, inet_ntoa(*(struct in_addr*)(hp->h_addr_list[i]))) == 0) { @@ -99,7 +105,7 @@ return; } -static int match_address(const char *addr, const char *tok) +static int match_address(const char *addr, char *tok) { char *p; struct addrinfo hints, *resa, *rest; @@ -243,7 +249,7 @@ return ret; } -static int access_match(const char *list, const char *addr, const char **host_ptr) +static int access_match(const char *list, const char *addr, const char **host_ptr, int deny) { char *tok; char *list2 = strdup(list); @@ -251,7 +257,7 @@ strlower(list2); for (tok = strtok(list2, " ,\t"); tok; tok = strtok(NULL, " ,\t")) { - if (match_hostname(host_ptr, addr, tok) || match_address(addr, tok)) { + if (match_hostname(host_ptr, addr, tok, deny) || match_address(addr, tok)) { free(list2); return 1; } @@ -275,7 +281,7 @@ /* If we match an allow-list item, we always allow access. */ if (allow_list) { - if (access_match(allow_list, addr, host_ptr)) + if (access_match(allow_list, addr, host_ptr, 0)) return 1; /* For an allow-list w/o a deny-list, disallow non-matches. */ if (!deny_list) @@ -284,9 +290,17 @@ /* If we match a deny-list item (and got past any allow-list * items), we always disallow access. */ - if (deny_list && access_match(deny_list, addr, host_ptr)) + if (deny_list && access_match(deny_list, addr, host_ptr, 1)) return 0; /* Allow all other access. */ return 1; } + +int allow_proxy_protocol_peer(const char *list, const char *addr, const char **host_ptr) +{ + if (!list || !*list) + return 0; + allow_forward_dns = 0; + return access_match(list, addr, host_ptr, 0); +} diff -Nru rsync-3.4.1+ds1/aclocal.m4 rsync-3.5.0+ds1/aclocal.m4 --- rsync-3.4.1+ds1/aclocal.m4 2025-01-14 03:35:02.000000000 +0000 +++ rsync-3.5.0+ds1/aclocal.m4 2026-05-21 23:56:27.000000000 +0000 @@ -1,6 +1,6 @@ -# generated automatically by aclocal 1.16.5 -*- Autoconf -*- +# generated automatically by aclocal 1.18.1 -*- Autoconf -*- -# Copyright (C) 1996-2021 Free Software Foundation, Inc. +# Copyright (C) 1996-2025 Free Software Foundation, Inc. # This file is free software; the Free Software Foundation # gives unlimited permission to copy and/or distribute it, diff -Nru rsync-3.4.1+ds1/acls.c rsync-3.5.0+ds1/acls.c --- rsync-3.4.1+ds1/acls.c 2024-11-13 20:15:14.000000000 +0000 +++ rsync-3.5.0+ds1/acls.c 2026-07-20 04:05:32.000000000 +0000 @@ -21,6 +21,10 @@ #include "rsync.h" #include "lib/sysacls.h" +#include "lib/acl.h" +#ifdef HAVE_LIBACL_AT +#include /* AT_EMPTY_PATH / AT_SYMLINK_NOFOLLOW */ +#endif #ifdef SUPPORT_ACLS @@ -469,11 +473,129 @@ return *match; } -static int get_rsync_acl(const char *fname, rsync_acl *racl, - SMB_ACL_TYPE_T type, mode_t mode) +/* These two bridge lib/acl.c's neutral (tag,perm,id) entry array; with + * HAVE_LIBACL_AT the libacl *_at path uses unpack_smb_acl/pack_smb_acl directly, + * so they are unused there. */ +#if defined(SUPPORT_ACL_FD) && !defined(HAVE_LIBACL_AT) +/* Convert a packed system ACL into the neutral (tag,perm,id) entry array that + * lib/acl.c serializes. Reuses pack_smb_acl()+change_sacl_perms() output so + * the bytes we write match exactly what acl_set_file() would have written. + * Returns the entry count and a malloc'd array in *ents_p, or -1 on error. */ +static int sacl_to_entries(SMB_ACL_T sacl, rsync_acl_ent **ents_p) +{ + static item_list ent_list = EMPTY_ITEM_LIST; + SMB_ACL_ENTRY_T entry; + rsync_acl_ent *out; + int rc; + + ent_list.count = 0; + for (rc = sys_acl_get_entry(sacl, SMB_ACL_FIRST_ENTRY, &entry); rc == 1; + rc = sys_acl_get_entry(sacl, SMB_ACL_NEXT_ENTRY, &entry)) { + SMB_ACL_TAG_T tag_type; + uint32 access; + id_t g_u_id; + rsync_acl_ent *e; + uint16_t tag; + + if ((rc = sys_acl_get_info(entry, &tag_type, &access, &g_u_id)) != 0) + break; + switch (tag_type) { + case SMB_ACL_USER_OBJ: tag = RACL_USER_OBJ; break; + case SMB_ACL_USER: tag = RACL_USER; break; + case SMB_ACL_GROUP_OBJ: tag = RACL_GROUP_OBJ; break; + case SMB_ACL_GROUP: tag = RACL_GROUP; break; + case SMB_ACL_MASK: tag = RACL_MASK; break; + case SMB_ACL_OTHER: tag = RACL_OTHER; break; + default: continue; /* skip an unrecognized tag */ + } + e = EXPAND_ITEM_LIST(&ent_list, rsync_acl_ent, -10); + e->tag = tag; + e->perm = access & 7; + e->id = (tag == RACL_USER || tag == RACL_GROUP) ? (uint32_t)g_u_id : RACL_UNDEFINED_ID; + } + if (rc) { + rsyserr(FERROR_XFER, errno, "sacl_to_entries: sys_acl_get_entry/info()"); + return -1; + } + + out = new_array(rsync_acl_ent, ent_list.count ? ent_list.count : 1); + if (ent_list.count) + memcpy(out, ent_list.items, ent_list.count * sizeof (rsync_acl_ent)); + *ents_p = out; + return ent_list.count; +} + +/* Unpack a neutral entry array (from lib/acl.c) into an rsync_acl, mirroring + * unpack_smb_acl()'s tag handling. */ +static BOOL unpack_acl_entries(const rsync_acl_ent *ents, int n, rsync_acl *racl) +{ + static item_list temp_ida_list = EMPTY_ITEM_LIST; + int i; + + temp_ida_list.count = 0; + for (i = 0; i < n; i++) { + uint32 access = ents[i].perm & 7; + id_access *ida; + + switch (ents[i].tag) { + case RACL_USER_OBJ: + if (racl->user_obj == NO_ENTRY) + racl->user_obj = access; + continue; + case RACL_GROUP_OBJ: + if (racl->group_obj == NO_ENTRY) + racl->group_obj = access; + continue; + case RACL_MASK: + if (racl->mask_obj == NO_ENTRY) + racl->mask_obj = access; + continue; + case RACL_OTHER: + if (racl->other_obj == NO_ENTRY) + racl->other_obj = access; + continue; + case RACL_USER: + access |= NAME_IS_USER; + break; + case RACL_GROUP: + break; + default: + continue; + } + ida = EXPAND_ITEM_LIST(&temp_ida_list, id_access, -10); + ida->id = ents[i].id; + ida->access = access; + } + + if (temp_ida_list.count) { +#ifdef SMB_ACL_NEED_SORT + if (temp_ida_list.count > 1) + qsort(temp_ida_list.items, temp_ida_list.count, sizeof (id_access), id_access_sorter); +#endif + racl->names.idas = new_array(id_access, temp_ida_list.count); + memcpy(racl->names.idas, temp_ida_list.items, temp_ida_list.count * sizeof (id_access)); + } else + racl->names.idas = NULL; + racl->names.count = temp_ida_list.count; + temp_ida_list.count = 0; + + return True; +} +#endif /* SUPPORT_ACL_FD */ + +static int get_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname, + rsync_acl *racl, SMB_ACL_TYPE_T type, mode_t mode) { SMB_ACL_T sacl; +#ifndef SUPPORT_ACL_FD +#ifndef HAVE_SOLARIS_ACLS + (void)fd; /* Solaris drives the ACL via facl(2) on fd but has no SUPPORT_ACL_FD. */ +#endif + (void)dirfd; + (void)leaf; +#endif + #ifdef SUPPORT_XATTRS /* --fake-super support: load ACLs from an xattr. */ if (am_root < 0) { @@ -481,7 +603,7 @@ size_t len; int cnt; - if ((buf = get_xattr_acl(fname, type == SMB_ACL_TYPE_ACCESS, &len)) == NULL) + if ((buf = get_xattr_acl(fname, fd, type == SMB_ACL_TYPE_ACCESS, &len)) == NULL) return 0; cnt = (len - 4*4) / (4+4); if (len < 4*4 || len != (size_t)cnt*(4+4) + 4*4) { @@ -514,6 +636,107 @@ } #endif +#ifdef HAVE_SOLARIS_ACLS + /* Solaris has no libacl *_at; read the ACL through the held fd via facl(2) + * when we have one. With no held fd this branch is skipped and the path-based + * call below reads the ACL (acceptable: a read can't redirect a write out of + * the tree). */ + if (fd >= 0) { + if ((sacl = sys_acl_get_fd_type(fd, type)) != 0) { + BOOL ok = unpack_smb_acl(sacl, racl); + + sys_acl_free_acl(sacl); + if (!ok) { + rsyserr(FERROR_XFER, errno, "get_acl: unpack_smb_acl(%s)", fname); + return -1; + } + return 0; + } + if (no_acl_syscall_error(errno)) { + if (type == SMB_ACL_TYPE_ACCESS) + rsync_acl_fake_perms(racl, mode); + return 0; + } + rsyserr(FERROR_XFER, errno, "get_acl: sys_acl_get_fd_type(%s, %s)", + fname, str_acl_type(type)); + return -1; + } +#endif + +#ifdef SUPPORT_ACL_FD +#ifdef HAVE_LIBACL_AT + /* Read the ACL via the new libacl *_at calls; fd<0 && dirfd<0 + * (e.g. a synthetic dir) falls through to the path-based call below. */ + if (fd >= 0 || dirfd >= 0) { + if (fd >= 0) + sacl = sys_acl_get_file_at(fd, "", AT_EMPTY_PATH, type); + else + sacl = sys_acl_get_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW, type); + if (sacl != 0) { + BOOL ok = unpack_smb_acl(sacl, racl); + sys_acl_free_acl(sacl); + if (!ok) { + rsyserr(FERROR_XFER, errno, "get_acl: unpack_smb_acl(%s)", fname); + return -1; + } + return 0; + } + if (no_acl_syscall_error(errno)) { + if (type == SMB_ACL_TYPE_ACCESS) + rsync_acl_fake_perms(racl, mode); + return 0; + } + rsyserr(FERROR_XFER, errno, "get_acl: acl_get_file_at(%s, %s)", + fname, str_acl_type(type)); + return -1; + } +#else + /* Race-safe path: read the ACL through the held O_NOFOLLOW fd, or via + * setxattrat(AT_SYMLINK_NOFOLLOW) on dirfd+leaf, instead of re-resolving + * fname. Only for real-root ACLs (am_root >= 0; the fake-super branch + * above already returned). */ + if (fd >= 0 || (dirfd >= 0 && xacl_at_available())) { + int is_def = type == SMB_ACL_TYPE_DEFAULT; + rsync_acl_ent *ents = NULL; + int n = 0, rc; + + if (fd >= 0) + rc = xacl_get_fd(fd, is_def, &ents, &n); + else + rc = xacl_get_at(dirfd, leaf, is_def, &ents, &n); + if (rc < 0) { + if (no_acl_syscall_error(errno)) { + if (type == SMB_ACL_TYPE_ACCESS) + rsync_acl_fake_perms(racl, mode); + return 0; + } + rsyserr(FERROR_XFER, errno, "get_acl: xacl_get(%s, %s)", + fname, str_acl_type(type)); + return -1; + } + if (n == 0) { + /* No explicit ACL: mirror libacl's mode-derived access ACL + * (an absent default ACL stays empty). */ + if (type == SMB_ACL_TYPE_ACCESS) + rsync_acl_fake_perms(racl, mode); + } else if (!unpack_acl_entries(ents, n, racl)) { + if (ents) + free(ents); + rsyserr(FERROR_XFER, errno, "get_acl: unpack_acl_entries(%s)", fname); + return -1; + } + if (ents) + free(ents); + return 0; + } + /* Neither a held fd nor a usable dirfd path (xacl_at_available() covers the + * *xattrat syscalls AND the pre-6.13 /proc/self/fd compat, so this is the + * BSDs / a /proc-less namespace / an un-pinnable entry): read the real + * destination ACL via the path-based call rather than a mode-only fake, so + * --acls stays functional where the race-safe primitive is unavailable. */ +#endif /* HAVE_LIBACL_AT */ +#endif + if ((sacl = sys_acl_get_file(fname, type)) != 0) { BOOL ok = unpack_smb_acl(sacl, racl); @@ -535,8 +758,10 @@ return 0; } -/* Return the Access Control List for the given filename. */ -int get_acl(const char *fname, stat_x *sxp) +/* Return the Access Control List for the given filename. When a held + * O_NOFOLLOW fd (or a dirfd+leaf) is available, the ACL is read race-safely + * through it; otherwise (fd < 0 && dirfd < 0) the path-based fallback is used. */ +int get_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname, stat_x *sxp) { sxp->acc_acl = create_racl(); @@ -557,7 +782,7 @@ } else if (IS_MISSING_FILE(sxp->st)) return 0; - if (get_rsync_acl(fname, sxp->acc_acl, SMB_ACL_TYPE_ACCESS, + if (get_rsync_acl(fd, dirfd, leaf, fname, sxp->acc_acl, SMB_ACL_TYPE_ACCESS, sxp->st.st_mode) < 0) { free_acl(sxp); return -1; @@ -565,7 +790,7 @@ if (S_ISDIR(sxp->st.st_mode)) { sxp->def_acl = create_racl(); - if (get_rsync_acl(fname, sxp->def_acl, SMB_ACL_TYPE_DEFAULT, + if (get_rsync_acl(fd, dirfd, leaf, fname, sxp->def_acl, SMB_ACL_TYPE_DEFAULT, sxp->st.st_mode) < 0) { free_acl(sxp); return -1; @@ -575,6 +800,11 @@ return 0; } +int get_acl(const char *fname, stat_x *sxp) +{ + return get_acl_fdat(-1, -1, NULL, fname, sxp); +} + /* === Send functions === */ /* Send the ida list over the file descriptor. */ @@ -697,7 +927,7 @@ static uchar recv_ida_entries(int f, ida_entries *ent) { uchar computed_mask_bits = 0; - int i, count = read_varint(f); + int i, count = read_varint_bounded(f, 0, MAX_WIRE_ACL_COUNT, "ACL count"); ent->idas = count ? new_array(id_access, count) : NULL; ent->count = count; @@ -713,7 +943,7 @@ else id = recv_group_name(f, id, NULL); } else if (access & NAME_IS_USER) { - if (inc_recurse && am_root && !numeric_ids) + if (inc_recurse && !numeric_ids) id = match_uid(id); } else { if (inc_recurse && (!am_root || !numeric_ids)) @@ -933,18 +1163,61 @@ } #endif -static int set_rsync_acl(const char *fname, acl_duo *duo_item, - SMB_ACL_TYPE_T type, stat_x *sxp, mode_t mode) +static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname, + acl_duo *duo_item, SMB_ACL_TYPE_T type, stat_x *sxp, mode_t mode) { +#ifndef SUPPORT_ACL_FD +#ifndef HAVE_SOLARIS_ACLS + (void)fd; /* Solaris drives the ACL via facl(2) on fd but has no SUPPORT_ACL_FD. */ +#endif + (void)dirfd; + (void)leaf; +#endif if (type == SMB_ACL_TYPE_DEFAULT && duo_item->racl.user_obj == NO_ENTRY) { int rc; #ifdef SUPPORT_XATTRS /* --fake-super support: delete default ACL from xattrs. */ if (am_root < 0) - rc = del_def_xattr_acl(fname); + rc = del_def_xattr_acl(fd, fname); else #endif +#ifdef SUPPORT_ACL_FD +#ifdef HAVE_LIBACL_AT + /* Race-safe default-ACL delete via the new libacl *_at + * calls (held fd via AT_EMPTY_PATH, dirfd+leaf via AT_SYMLINK_NOFOLLOW) + * -- race-safe on every Linux kernel. fd<0 && dirfd<0 falls to path. */ + if (fd >= 0) + rc = sys_acl_delete_def_file_at(fd, "", AT_EMPTY_PATH); + else if (dirfd >= 0) + rc = sys_acl_delete_def_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW); + else +#else + /* Race-safe default-ACL delete via the held fd or dirfd+leaf. Where + * neither is available (xacl_at_available() is false -- the BSDs, a + * /proc-less namespace, an un-pinnable entry; every Linux with procfs + * takes the dirfd path via *xattrat or the /proc/self/fd compat) -- fall + * back to the path-based call, preferring the documented --acls behaviour + * over refusing it where the race-safe primitive is unavailable. */ + if (fd >= 0) + rc = xacl_del_default_fd(fd); + else if (dirfd >= 0 && xacl_at_available()) + rc = xacl_del_default_at(dirfd, leaf); + else +#endif /* HAVE_LIBACL_AT */ +#endif +#ifdef HAVE_SOLARIS_ACLS + /* Solaris: delete the default ACL through the held fd via facl(2). For a + * root receiver a missing held fd means the leaf was raced, so refuse rather + * than let the path-based delete follow it; a plain non-root receiver keeps + * 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) { + errno = ELOOP; + rc = -1; + } else +#endif rc = sys_acl_delete_def_file(fname); if (rc < 0) { rsyserr(FERROR_XFER, errno, "set_acl: sys_acl_delete_def_file(%s)", @@ -972,7 +1245,7 @@ SIVAL(bp, 4, ida->access); } } - rc = set_xattr_acl(fname, type == SMB_ACL_TYPE_ACCESS, buf, len); + rc = set_xattr_acl(fd, fname, type == SMB_ACL_TYPE_ACCESS, buf, len); free(buf); return rc; #endif @@ -990,6 +1263,92 @@ return 0; } #endif +#ifdef SUPPORT_ACL_FD +#ifdef HAVE_LIBACL_AT + /* Apply the packed/perm-reconciled ACL (duo_item->sacl) + * through the new libacl *_at calls -- held fd via AT_EMPTY_PATH, + * dirfd+leaf via AT_SYMLINK_NOFOLLOW -- race-safe on every Linux kernel, + * and byte-identical to the path-based sys_acl_set_file() below. */ + if (fd >= 0 || dirfd >= 0) { + int rc; + + if (fd >= 0) + rc = sys_acl_set_file_at(fd, "", AT_EMPTY_PATH, type, duo_item->sacl); + else + rc = sys_acl_set_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW, type, duo_item->sacl); + if (rc < 0) { + rsyserr(FERROR_XFER, errno, "set_acl: acl_set_file_at(%s, %s)", + fname, str_acl_type(type)); + return -1; + } + if (type == SMB_ACL_TYPE_ACCESS) + sxp->st.st_mode = cur_mode; + return 0; + } +#else + /* Race-safe write: serialize the packed (and perm-reconciled) + * system ACL to the kernel xattr format and apply it through the + * held fd or dirfd+leaf -- never re-resolving fname. This matches + * exactly what sys_acl_set_file() would have written. */ + if (fd >= 0 || (dirfd >= 0 && xacl_at_available())) { + int is_def = type == SMB_ACL_TYPE_DEFAULT; + rsync_acl_ent *ents; + int n = sacl_to_entries(duo_item->sacl, &ents); + int rc; + + if (n < 0) + return -1; + if (fd >= 0) + rc = xacl_set_fd(fd, is_def, ents, n); + else + rc = xacl_set_at(dirfd, leaf, is_def, ents, n); + free(ents); + if (rc < 0) { + rsyserr(FERROR_XFER, errno, "set_acl: xacl_set(%s, %s)", + fname, str_acl_type(type)); + return -1; + } + if (type == SMB_ACL_TYPE_ACCESS) + sxp->st.st_mode = cur_mode; + return 0; + } + /* No held fd and no usable dirfd path (xacl_at_available() is false -- + * the BSDs, a /proc-less namespace, an un-pinnable entry; every Linux + * with procfs took xacl_set_at() above via *xattrat or the /proc/self/fd + * compat): prefer the documented --acls behaviour over refusing it and + * fall back to the path-based set. This re-resolves fname, so it still + * carries the parent-symlink-race exposure on those remaining platforms; + * it is the only way to honour --acls where no race-safe primitive + * exists. */ +#endif /* HAVE_LIBACL_AT */ +#endif +#ifdef HAVE_SOLARIS_ACLS + /* Solaris: apply the ACL through the held fd via facl(2). */ + if (fd >= 0) { + if (sys_acl_set_fd_type(fd, type, duo_item->sacl) < 0) { + rsyserr(FERROR_XFER, errno, "set_acl: sys_acl_set_fd_type(%s, %s)", + fname, str_acl_type(type)); + return -1; + } + if (type == SMB_ACL_TYPE_ACCESS) + sxp->st.st_mode = cur_mode; + return 0; + } + if (secure_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 + * than write the attacker-supplied ACL onto a redirected inode (covers + * the top-level no-slash entry the caller's slashed-path xattr_refuse + * gate misses). A plain non-root receiver keeps the path-based fallback + * for a legitimately un-pinnable owned leaf (e.g. a 0300 dir), matching + * the operator-path op_pin rule (am_root != 0). */ + errno = ELOOP; + rsyserr(FERROR_XFER, errno, "set_acl: refusing path-based ACL on %s (no held fd)", + fname); + return -1; + } +#endif if (sys_acl_set_file(fname, type, duo_item->sacl) < 0) { rsyserr(FERROR_XFER, errno, "set_acl: sys_acl_set_file(%s, %s)", fname, str_acl_type(type)); @@ -1006,11 +1365,16 @@ * dir), and the regular mode bits on the file. Call this with fname set to * NULL to just check if the ACL is different. * + * When a held O_NOFOLLOW fd (or a dirfd+leaf) is supplied, the ACL is applied + * race-safely through it; otherwise (fd < 0 && dirfd < 0) the path-based + * fallback is used. + * * If the ACL operation has a side-effect of changing the file's mode, the * sxp->st.st_mode value will be changed to match. * * Returns 0 for an unchanged ACL, 1 for changed, -1 for failed. */ -int set_acl(const char *fname, const struct file_struct *file, stat_x *sxp, mode_t new_mode) +int set_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname, + const struct file_struct *file, stat_x *sxp, mode_t new_mode) { int changed = 0; int32 ndx; @@ -1030,7 +1394,7 @@ if (!eq) { changed = 1; if (!dry_run && fname - && set_rsync_acl(fname, duo_item, SMB_ACL_TYPE_ACCESS, + && set_rsync_acl(fd, dirfd, leaf, fname, duo_item, SMB_ACL_TYPE_ACCESS, sxp, new_mode) < 0) return -1; } @@ -1047,7 +1411,7 @@ if (!eq) { changed = 1; if (!dry_run && fname - && set_rsync_acl(fname, duo_item, SMB_ACL_TYPE_DEFAULT, + && set_rsync_acl(fd, dirfd, leaf, fname, duo_item, SMB_ACL_TYPE_DEFAULT, sxp, new_mode) < 0) return -1; } @@ -1056,6 +1420,11 @@ return changed; } +int set_acl(const char *fname, const struct file_struct *file, stat_x *sxp, mode_t new_mode) +{ + return set_acl_fdat(-1, -1, NULL, fname, file, sxp, new_mode); +} + /* Non-incremental recursion needs to convert all the received IDs. * This is done in a single pass after receiving the whole file-list. */ static void match_racl_ids(const item_list *racl_list) diff -Nru rsync-3.4.1+ds1/authenticate.c rsync-3.5.0+ds1/authenticate.c --- rsync-3.4.1+ds1/authenticate.c 2022-09-30 19:36:21.000000000 +0000 +++ rsync-3.5.0+ds1/authenticate.c 2026-07-29 03:10:54.000000000 +0000 @@ -22,6 +22,13 @@ #include "itypes.h" #include "ifuncs.h" +/* O_CLOEXEC is absent on some still-supported targets. The random-source fd + * is read and closed synchronously, so the established zero-value fallback is + * sufficient without adding a configure dependency. */ +#ifndef O_CLOEXEC +#define O_CLOEXEC 0 +#endif + extern int read_only; extern char *password_file; extern struct name_num_obj valid_auth_checksums; @@ -57,10 +64,31 @@ out[i] = '\0'; } +/* Fill buf with len bytes from the kernel CSPRNG. Returns 1 on success. + * We read /dev/urandom directly rather than depending on getrandom()/ + * arc4random_buf() availability so this works on every platform rsync + * targets without new configure probes. */ +static int get_random_bytes(char *buf, int len) +{ + int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC); + int got = 0; + if (fd < 0) + return 0; + while (got < len) { + int n = read(fd, buf + got, len - got); + if (n <= 0) + break; + got += n; + } + close(fd); + return got == len; +} + /* Generate a challenge buffer and return it base64-encoded. */ static void gen_challenge(const char *addr, char *challenge) { char input[32]; + char rnd[32]; char digest[MAX_DIGEST_LEN]; struct timeval tv; int len; @@ -74,6 +102,16 @@ SIVAL(input, 24, getpid()); len = sum_init(valid_auth_checksums.negotiated_nni, 0); + /* The challenge must be unpredictable to a network observer; addr+time + * +pid alone is ~35 bits and lets an attacker enumerate the preimage + * offline. Hash 32 bytes from the kernel RNG first so the digest + * carries full entropy, keeping the legacy inputs as a mix-in so a + * urandom failure degrades to (never below) the old behaviour. */ + if (get_random_bytes(rnd, sizeof rnd)) + sum_update(rnd, sizeof rnd); + else + rprintf(FWARNING, "gen_challenge: /dev/urandom unavailable, " + "falling back to time-based challenge\n"); sum_update(input, sizeof input); sum_end(digest); @@ -110,8 +148,23 @@ char *err; FILE *fh; - if (!fname || !*fname || (fh = fopen(fname, "r")) == NULL) + /* Daemon 'secrets file = PATH' open. A planted symlink would be + * followed and the strict-modes fstat() check below runs on the target + * inode, so a symlink to /etc/shadow (0640 root:shadow) would pass and + * the daemon would auth against shadow hashes. Refuse symlinks not + * owned by uid 0 or our euid. */ + if (!fname || !*fname) return "no secrets file"; + { + int fd = open_no_attacker_symlinks(fname, O_RDONLY, 0); + if (fd < 0) + return "no secrets file"; + fh = fdopen(fd, "r"); + if (!fh) { + close(fd); + return "no secrets file"; + } + } if (do_fstat(fileno(fh), &st) == -1) { rsyserr(FLOG, errno, "fstat(%s)", fname); @@ -184,13 +237,23 @@ } else { int fd; - if ((fd = open(filename,O_RDONLY)) < 0) { + /* --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() + * 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) { rsyserr(FERROR, errno, "could not open password file %s", filename); exit_cleanup(RERR_SYNTAX); } - if (do_stat(filename, &st) == -1) { - rsyserr(FERROR, errno, "stat(%s)", filename); + /* fstat the opened fd, not the pathname: a same-object check + * (matching check_secret() above) so an attacker who swaps the + * 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) { + rsyserr(FERROR, errno, "fstat(%s)", filename); exit_cleanup(RERR_SYNTAX); } if ((st.st_mode & 06) != 0) { @@ -240,6 +303,35 @@ return ""; negotiate_daemon_auth(f_out, 0); + + /* Enforce a configured minimum auth digest (default: none). This refuses + * a peer that negotiated -- or, via an omitted digest list / old protocol, + * fell back to -- a digest weaker than the operator-required floor, e.g. a + * client downgraded to md5/md4. Lower rank == stronger (the auth list is + * ordered strongest-first), so a higher rank than the floor is too weak. */ + { + const char *min_digest = lp_auth_digest(module); + if (min_digest && *min_digest) { + int floor_rank = auth_digest_rank(min_digest); + int got_rank = auth_digest_rank(valid_auth_checksums.negotiated_nni->name); + if (floor_rank < 0) { + rprintf(FLOG, "auth failed on module %s from %s (%s): the " + "configured 'auth digest = %s' is not a supported digest " + "on this build\n", + lp_name(module), host, addr, min_digest); + return NULL; + } + if (got_rank < 0 || got_rank > floor_rank) { + rprintf(FLOG, "auth failed on module %s from %s (%s): negotiated " + "auth digest %s is weaker than the required " + "'auth digest = %s'\n", + lp_name(module), host, addr, + valid_auth_checksums.negotiated_nni->name, min_digest); + return NULL; + } + } + } + gen_challenge(addr, challenge); io_printf(f_out, "%s%s\n", leader, challenge); @@ -255,7 +347,13 @@ users = strdup(users); - for (tok = strtok(users, " ,\t"); tok; tok = strtok(NULL, " ,\t")) { + /* conf_strtok() honours the documented leading-comma form: a value that + * starts with a comma splits on commas ALONE, so an entry may contain + * spaces -- which is how a group name with a space is written. Splitting + * on whitespace here tore such an entry apart, so the rule the admin wrote + * never matched and a rule they never wrote appeared from its tail. The + * daemon's gid field already uses this parser (clientserver.c). */ + for (tok = conf_strtok(users); tok; tok = conf_strtok(NULL)) { char *opts; /* See if the user appended :deny, :ro, or :rw. */ if ((opts = strchr(tok, ':')) != NULL) { diff -Nru rsync-3.4.1+ds1/backup.c rsync-3.5.0+ds1/backup.c --- rsync-3.4.1+ds1/backup.c 2022-01-16 01:21:01.000000000 +0000 +++ rsync-3.5.0+ds1/backup.c 2026-08-01 00:43:58.000000000 +0000 @@ -30,16 +30,43 @@ 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; +/* Pin a backup SOURCE leaf with a confined O_NOFOLLOW fd (via the operator + * owner-walk resolver, like set_file_attrs's op_leaf_fd) so the ACL/xattr the + * backup caches off it are read through the held fd -- a parent-symlink race + * can't redirect the read out of the module. Returns -1 for a non-hardened + * receiver (caller path-reads) or for a raced/absent leaf on a hardened one + * (caller skips the cache rather than read through a flippable path; use + * backup_metadata_hardened() to tell the two -1 cases apart). */ +int backup_metadata_hardened(void) +{ + return secure_relpath_active() && !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; + } +#endif + return -1; +} + /* Returns -1 on error, 0 on missing dir, and 1 on present dir. */ static int validate_backup_dir(void) { STRUCT_STAT st; - if (do_lstat(backup_dir_buf, &st) < 0) { + if (do_lstat_at(backup_dir_buf, &st) < 0) { if (errno == ENOENT) return 0; rsyserr(FERROR, errno, "backup lstat %s failed", backup_dir_buf); @@ -98,7 +125,7 @@ for ( ; b; name = b + 1, b = strchr(name, '/')) { *b = '\0'; - while (do_mkdir(backup_dir_buf, ACCESSPERMS) < 0) { + while (do_mkdir_at(backup_dir_buf, ACCESSPERMS) < 0) { if (errno == EEXIST) { val = validate_backup_dir(); if (val > 0) @@ -120,18 +147,27 @@ struct file_struct *file; if (!(file = make_file(rel, NULL, NULL, 0, NO_FILTERS))) continue; -#ifdef SUPPORT_ACLS - if (preserve_acls && !S_ISLNK(file->mode)) { - get_acl(rel, &sx); - cache_tmp_acl(file, &sx); - free_acl(&sx); +#if defined SUPPORT_ACLS || defined SUPPORT_XATTRS + { /* read the source dir's ACL/xattr through a confined fd */ + int bfd = backup_source_fd(rel); + if (!backup_metadata_hardened() || bfd >= 0) { +# ifdef SUPPORT_ACLS + if (preserve_acls && !S_ISLNK(file->mode)) { + get_acl_fdat(bfd, -1, NULL, rel, &sx); + cache_tmp_acl(file, &sx); + free_acl(&sx); + } +# endif +# ifdef SUPPORT_XATTRS + if (preserve_xattrs) { + get_xattr(rel, bfd, &sx); + cache_tmp_xattr(file, &sx); + free_xattr(&sx); + } +# endif } -#endif -#ifdef SUPPORT_XATTRS - if (preserve_xattrs) { - get_xattr(rel, &sx); - cache_tmp_xattr(file, &sx); - free_xattr(&sx); + if (bfd >= 0) + close(bfd); } #endif set_file_attrs(backup_dir_buf, file, NULL, NULL, 0); @@ -159,12 +195,15 @@ if (backup_dir) { static int initialized = 0; if (!initialized) { + char dirbuf[MAXPATHLEN]; int ret; + if (strlcpy(dirbuf, backup_dir_buf, sizeof dirbuf) >= sizeof dirbuf) { + errno = ENAMETOOLONG; + return NULL; + } if (backup_dir_len > 1) - backup_dir_buf[backup_dir_len-1] = '\0'; - ret = make_path(backup_dir_buf, 0); - if (backup_dir_len > 1) - backup_dir_buf[backup_dir_len-1] = '/'; + dirbuf[backup_dir_len-1] = '\0'; + ret = make_path(dirbuf, 0); if (ret < 0) return NULL; initialized = 1; @@ -197,7 +236,7 @@ if (IS_SPECIAL(stp->st_mode) || IS_DEVICE(stp->st_mode)) return 0; /* Use copy code. */ #endif - if (do_link(from, to) == 0) { + if (do_link_at(from, to) == 0) { if (DEBUG_GTE(BACKUP, 1)) rprintf(FINFO, "make_backup: HLINK %s successful.\n", from); return 2; @@ -207,7 +246,7 @@ return 0; } #endif - if (do_rename(from, to) == 0) { + if (do_rename_at(from, to) == 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! */ @@ -223,7 +262,7 @@ /* Hard-link, rename, or copy an item to the backup name. Returns 0 for * failure, 1 if item was moved, 2 if item was duplicated or hard linked * into backup area, or 3 if item doesn't exist or isn't a regular file. */ -int make_backup(const char *fname, BOOL prefer_rename) +static int make_backup_inner(const char *fname, BOOL prefer_rename) { stat_x sx; struct file_struct *file; @@ -239,6 +278,38 @@ if (!(buf = get_backup_name(fname))) return 0; +#ifdef SUPPORT_LINKS + /* Honor --safe-links BEFORE the hard-link / rename fast path. When + * CAN_HARDLINK_SYMLINK is defined, link_or_rename() would otherwise + * hard-link an escaping symlink (e.g. ../../etc/passwd) into the backup + * area and "goto success", skipping the safe_symlinks check in the + * copy-fallback path below -- silently preserving an unsafe link that + * --safe-links was meant to drop. Match the copy path: don't back up an + * unsafe symlink. */ + if (preserve_links && S_ISLNK(sx.st.st_mode) && safe_symlinks) { + char lnkbuf[MAXPATHLEN]; + int llen = do_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. */ + if (llen <= 0) { + if (INFO_GTE(SYMSAFE, 1)) + rprintf(FINFO, "not backing up symlink with unreadable target \"%s\"\n", fname); + ret = 2; + goto success; + } + lnkbuf[llen] = '\0'; + if (unsafe_symlink(lnkbuf, fname)) { + if (INFO_GTE(SYMSAFE, 1)) { + rprintf(FINFO, "not backing up unsafe symlink \"%s\" -> \"%s\"\n", + fname, lnkbuf); + } + ret = 2; + goto success; + } + } +#endif + /* Try a hard-link or a rename first. Using rename is not atomic, but * is more efficient than forcing a copy for larger files when no hard- * linking is possible. */ @@ -246,7 +317,7 @@ goto success; if (errno == EEXIST || errno == EISDIR) { STRUCT_STAT bakst; - if (do_lstat(buf, &bakst) == 0) { + if (do_lstat_at(buf, &bakst) == 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; @@ -259,25 +330,34 @@ if (!(file = make_file(fname, NULL, &sx.st, 0, NO_FILTERS))) return 3; /* the file could have disappeared */ -#ifdef SUPPORT_ACLS - if (preserve_acls && !S_ISLNK(file->mode)) { - get_acl(fname, &sx); - cache_tmp_acl(file, &sx); - free_acl(&sx); +#if defined SUPPORT_ACLS || defined SUPPORT_XATTRS + { /* read the source file's ACL/xattr through a confined fd */ + int bfd = backup_source_fd(fname); + if (!backup_metadata_hardened() || bfd >= 0) { +# ifdef SUPPORT_ACLS + if (preserve_acls && !S_ISLNK(file->mode)) { + get_acl_fdat(bfd, -1, NULL, fname, &sx); + cache_tmp_acl(file, &sx); + free_acl(&sx); + } +# endif +# ifdef SUPPORT_XATTRS + if (preserve_xattrs) { + get_xattr(fname, bfd, &sx); + cache_tmp_xattr(file, &sx); + free_xattr(&sx); + } +# endif } -#endif -#ifdef SUPPORT_XATTRS - if (preserve_xattrs) { - get_xattr(fname, &sx); - cache_tmp_xattr(file, &sx); - free_xattr(&sx); + if (bfd >= 0) + close(bfd); } #endif /* 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(buf, file->mode, sx.st.st_rdev) < 0) + if (do_mknod_at(buf, file->mode, sx.st.st_rdev) < 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); @@ -294,7 +374,7 @@ } ret = 2; } else { - if (do_symlink(sl, buf) < 0) + if (do_symlink_at(sl, buf) < 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); @@ -353,3 +433,17 @@ rprintf(FINFO, "backed up %s to %s\n", fname, buf); return ret; } + +int make_backup(const char *fname, BOOL prefer_rename) +{ + int ret; + /* The --backup-dir is an operator-supplied path: resolve it (and the + * tail/rename beneath it) with the ownership walk so a foreign-owned + * 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 -Nru rsync-3.4.1+ds1/batch.c rsync-3.5.0+ds1/batch.c --- rsync-3.4.1+ds1/batch.c 2022-08-14 17:15:08.000000000 +0000 +++ rsync-3.5.0+ds1/batch.c 2026-07-20 04:05:31.000000000 +0000 @@ -75,7 +75,7 @@ NULL }; -static char *flag_name[] = { +static const char *const flag_name[] = { "--recurse (-r)", "--owner (-o)", "--group (-g)", @@ -166,25 +166,33 @@ const char *x, *s; int len, err = 0; + /* Emit a "--opt=" prefix unquoted only when it is a plain option token; + * a metacharacter before '=' (an attacker-shaped arg) must be quoted + * along with the rest, or it would run raw in the replay script. */ if (*arg == '-' && (x = strchr(arg, '=')) != NULL) { - err |= write(batch_sh_fd, arg, x - arg + 1) != x - arg + 1; - arg += x - arg + 1; - } - - if (strpbrk(arg, " \"'&;|[]()$#!*?^\\") != NULL) { - err |= write(batch_sh_fd, "'", 1) != 1; - for (s = arg; (x = strchr(s, '\'')) != NULL; s = x + 1) { - err |= write(batch_sh_fd, s, x - s + 1) != x - s + 1; - err |= write(batch_sh_fd, "'", 1) != 1; + const char *p = arg; + while (p < x && (*p == '-' || *p == '_' + || (*p >= '0' && *p <= '9') + || (*p >= 'A' && *p <= 'Z') + || (*p >= 'a' && *p <= 'z'))) + p++; + if (p == x) { + err |= write(batch_sh_fd, arg, x - arg + 1) != x - arg + 1; + arg += x - arg + 1; } - len = strlen(s); - err |= write(batch_sh_fd, s, len) != len; - err |= write(batch_sh_fd, "'", 1) != 1; - return err; } - len = strlen(arg); - err |= write(batch_sh_fd, arg, len) != len; + /* Single-quote unconditionally so every shell metacharacter (backtick, + * newline, redirection, ...) stays literal in the replay script. An + * embedded ' is emitted as the '\'' close/escape/reopen sequence. */ + err |= write(batch_sh_fd, "'", 1) != 1; + for (s = arg; (x = strchr(s, '\'')) != NULL; s = x + 1) { + err |= write(batch_sh_fd, s, x - s) != x - s; + err |= write(batch_sh_fd, "'\\''", 4) != 4; + } + len = strlen(s); + err |= write(batch_sh_fd, s, len) != len; + err |= write(batch_sh_fd, "'", 1) != 1; return err; } @@ -194,7 +202,7 @@ { int len = strlen(opt); int err = write(batch_sh_fd, " ", 1) != 1; - err = write(batch_sh_fd, opt, len) != len ? 1 : 0; + err |= write(batch_sh_fd, opt, len) != len; if (arg) { err |= write(batch_sh_fd, "=", 1) != 1; err |= write_arg(arg); @@ -210,6 +218,16 @@ for (ent = filter_list.head; ent; ent = ent->next) { unsigned int plen; char *p = get_rule_prefix(ent, "- ", 0, &plen); + /* A filter pattern is one here-doc line; an embedded newline would let + * a crafted pattern (e.g. from a dir-merge/--exclude-from file in an + * untrusted tree) forge the "#E#" terminator on its own line and inject + * shell commands into the generated replay script. Such a pattern also + * can't round-trip the line-delimited here-doc, so refuse it fail-closed + * rather than emit an injectable script. */ + if (ent->pattern && strchr(ent->pattern, '\n')) { + rprintf(FERROR, "cannot write a filter rule containing a newline to the batch replay script\n"); + exit_cleanup(RERR_SYNTAX); + } write_buf(fd, p, plen); write_sbuf(fd, ent->pattern); if (ent->rflags & FILTRULE_DIRECTORY) @@ -224,27 +242,45 @@ /* This sets batch_fd and (for --write-batch) batch_sh_fd. */ void open_batch_files(void) { + /* --write-batch/--read-batch are operator-supplied; a planted symlink + * could truncate+overwrite an arbitrary file (write side) or stream + * attacker bytes into the protocol parser (read side). Refuse symlinks + * not owned by uid 0 or our euid anywhere in the path. */ if (write_batch) { char filename[MAXPATHLEN]; stringjoin(filename, sizeof filename, batch_name, ".sh", NULL); - batch_sh_fd = do_open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IXUSR); + batch_sh_fd = open_no_attacker_symlinks(filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR | S_IXUSR); if (batch_sh_fd < 0) { rsyserr(FERROR, errno, "Batch file %s open error", full_fname(filename)); exit_cleanup(RERR_FILESELECT); } - batch_fd = do_open(batch_name, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); + /* 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); } else if (strcmp(batch_name, "-") == 0) batch_fd = STDIN_FILENO; else - batch_fd = do_open(batch_name, O_RDONLY, S_IRUSR | S_IWUSR); + batch_fd = open_no_attacker_symlinks(batch_name, O_RDONLY | O_BINARY, S_IRUSR | S_IWUSR); if (batch_fd < 0) { rsyserr(FERROR, errno, "Batch file %s open error", full_fname(batch_name)); exit_cleanup(RERR_FILEIO); } + + /* --read-batch: the file's bytes drive the protocol parser, so refuse + * 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)) { + rprintf(FERROR, "Batch file %s is not a regular file\n", + full_fname(batch_name)); + exit_cleanup(RERR_FILEIO); + } + } } /* This routine tries to write out an equivalent --read-batch command diff -Nru rsync-3.4.1+ds1/byteorder.h rsync-3.5.0+ds1/byteorder.h --- rsync-3.4.1+ds1/byteorder.h 2022-08-14 17:15:08.000000000 +0000 +++ rsync-3.5.0+ds1/byteorder.h 2026-06-08 10:54:57.000000000 +0000 @@ -68,10 +68,26 @@ #else /* !CAREFUL_ALIGNMENT */ +/* We don't want false positives about alignment from UBSAN, see: + https://github.com/WayneD/rsync/issues/427#issuecomment-1375132291 +*/ + +/* From https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html */ +#ifndef GCC_VERSION +#define GCC_VERSION (__GNUC__ * 10000 \ + + __GNUC_MINOR__ * 100 \ + + __GNUC_PATCHLEVEL__) +#endif + /* This handles things for architectures like the 386 that can handle alignment errors. * WARNING: This section is dependent on the length of an int32 (and thus a uint32) * being correct (4 bytes)! Set CAREFUL_ALIGNMENT if it is not. */ +#ifdef __clang__ +__attribute__((no_sanitize("undefined"))) +#elif GCC_VERSION >= 409 +__attribute__((no_sanitize_undefined)) +#endif static inline uint32 IVALu(const uchar *buf, int pos) { @@ -83,6 +99,11 @@ return *u.num; } +#ifdef __clang__ +__attribute__((no_sanitize("undefined"))) +#elif GCC_VERSION >= 409 +__attribute__((no_sanitize_undefined)) +#endif static inline void SIVALu(uchar *buf, int pos, uint32 val) { @@ -94,6 +115,11 @@ *u.num = val; } +#ifdef __clang__ +__attribute__((no_sanitize("undefined"))) +#elif GCC_VERSION >= 409 +__attribute__((no_sanitize_undefined)) +#endif static inline int64 IVAL64(const char *buf, int pos) { @@ -105,6 +131,11 @@ return *u.num; } +#ifdef __clang__ +__attribute__((no_sanitize("undefined"))) +#elif GCC_VERSION >= 409 +__attribute__((no_sanitize_undefined)) +#endif static inline void SIVAL64(char *buf, int pos, int64 val) { diff -Nru rsync-3.4.1+ds1/checksum.c rsync-3.5.0+ds1/checksum.c --- rsync-3.4.1+ds1/checksum.c 2025-01-14 18:30:32.000000000 +0000 +++ rsync-3.5.0+ds1/checksum.c 2026-07-20 04:05:31.000000000 +0000 @@ -87,6 +87,24 @@ "daemon auth checksum", NULL, 0, 0, valid_auth_checksums_items }; +/* Return the strength rank (0 = strongest) of a daemon-auth digest by name in + * valid_auth_checksums_items[], which is listed strongest-first; -1 if the name + * is not a supported auth digest on this build. Used by the daemon's + * "auth digest" floor to compare the negotiated digest against the minimum. */ +int auth_digest_rank(const char *name) +{ + struct name_num_item *nni; + int rank = 0; + + if (!name || !*name) + return -1; + for (nni = valid_auth_checksums_items; nni->name; nni++, rank++) { + if (strcasecmp(nni->name, name) == 0) + return rank; + } + return -1; +} + /* These cannot make use of openssl, so they're marked just as built-in */ struct name_num_item implied_checksum_md4 = { CSUM_MD4, NNI_BUILTIN, "md4", NULL }; @@ -176,7 +194,7 @@ if (valid_checksums.negotiated_nni) xfer_sum_nni = file_sum_nni = valid_checksums.negotiated_nni; else { - char *cp = checksum_choice ? strchr(checksum_choice, ',') : NULL; + const char *cp = checksum_choice ? strchr(checksum_choice, ',') : NULL; if (cp) { xfer_sum_nni = parse_csum_name(checksum_choice, cp - checksum_choice); file_sum_nni = parse_csum_name(cp+1, -1); @@ -366,9 +384,8 @@ mdfour_begin(&m); - if (len > len1) { - if (buf1) - free(buf1); + if (len > len1 || !buf1) { + free(buf1); buf1 = new_array(char, len+4); len1 = len; } diff -Nru rsync-3.4.1+ds1/chmod.c rsync-3.5.0+ds1/chmod.c --- rsync-3.4.1+ds1/chmod.c 2020-04-16 16:31:02.000000000 +0000 +++ rsync-3.5.0+ds1/chmod.c 2026-07-20 04:05:32.000000000 +0000 @@ -29,7 +29,7 @@ struct chmod_mode_struct { struct chmod_mode_struct *next; - int ModeAND, ModeOR; + int ModeAND, ModeOR, ModeCOPY_SRC, ModeCOPY_DST, ModeCOPY_AND, ModeOP; char flags; }; @@ -43,6 +43,20 @@ #define STATE_2ND_HALF 2 #define STATE_OCTAL_NUM 3 +static int mode_dest_special_bits(int where) +{ + int bits = 0; + + if (where & 0100) + bits |= S_ISUID; + if (where & 0010) + bits |= S_ISGID; + if (where & 0001) + bits |= S_ISVTX; + + return bits; +} + /* Parse a chmod-style argument, and break it down into one or more AND/OR * pairs in a linked list. We return a pointer to new items on success * (appending the items to the specified list), or NULL on error. */ @@ -50,13 +64,13 @@ struct chmod_mode_struct **root_mode_ptr) { int state = STATE_1ST_HALF; - int where = 0, what = 0, op = 0, topbits = 0, topoct = 0, flags = 0; + int where = 0, what = 0, op = 0, topbits = 0, topoct = 0, flags = 0, copybits = 0; struct chmod_mode_struct *first_mode = NULL, *curr_mode = NULL, *prev_mode = NULL; while (state != STATE_ERROR) { if (!*modestr || *modestr == ',') { - int bits; + int bits, where_specified; if (!op) { state = STATE_ERROR; @@ -70,9 +84,10 @@ first_mode = curr_mode; curr_mode->next = NULL; - if (where) + where_specified = where; + if (where) { bits = where * what; - else { + } else { where = 0111; bits = (where * what) & ~orig_umask; } @@ -81,18 +96,35 @@ case CHMOD_ADD: curr_mode->ModeAND = CHMOD_BITS; curr_mode->ModeOR = bits + topoct; + curr_mode->ModeCOPY_SRC = copybits; + curr_mode->ModeCOPY_DST = where; + curr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask; + curr_mode->ModeOP = op; break; case CHMOD_SUB: curr_mode->ModeAND = CHMOD_BITS - bits - topoct; curr_mode->ModeOR = 0; + curr_mode->ModeCOPY_SRC = copybits; + curr_mode->ModeCOPY_DST = where; + curr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask; + curr_mode->ModeOP = op; break; case CHMOD_EQ: - curr_mode->ModeAND = CHMOD_BITS - (where * 7) - (topoct ? topbits : 0); + curr_mode->ModeAND = CHMOD_BITS - (where * 7) - (topoct ? topbits : 0) + - (copybits ? mode_dest_special_bits(where) : 0); curr_mode->ModeOR = bits + topoct; + curr_mode->ModeCOPY_SRC = copybits; + curr_mode->ModeCOPY_DST = where; + curr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask; + curr_mode->ModeOP = op; break; case CHMOD_SET: curr_mode->ModeAND = 0; curr_mode->ModeOR = bits; + curr_mode->ModeCOPY_SRC = 0; + curr_mode->ModeCOPY_DST = 0; + curr_mode->ModeCOPY_AND = CHMOD_BITS; + curr_mode->ModeOP = op; break; } @@ -103,7 +135,7 @@ modestr++; state = STATE_1ST_HALF; - where = what = op = topoct = topbits = flags = 0; + where = what = op = topoct = topbits = flags = copybits = 0; } switch (state) { @@ -132,6 +164,7 @@ break; case 'a': where |= 0111; + topbits |= 06000; /* a+s sets BOTH setuid and setgid (like chmod(1)) */ break; case '+': op = CHMOD_ADD; @@ -159,26 +192,53 @@ case STATE_2ND_HALF: switch (*modestr) { case 'r': + if (copybits) + state = STATE_ERROR; what |= 4; break; case 'w': + if (copybits) + state = STATE_ERROR; what |= 2; break; case 'X': + if (copybits) + state = STATE_ERROR; flags |= FLAG_X_KEEP; /* FALL THROUGH */ case 'x': + if (copybits) + state = STATE_ERROR; what |= 1; break; case 's': + if (copybits) + state = STATE_ERROR; if (topbits) topoct |= topbits; else topoct = 04000; break; case 't': + if (copybits) + state = STATE_ERROR; topoct |= 01000; break; + case 'u': + if (what || topoct || copybits) + state = STATE_ERROR; + copybits = 0100; + break; + case 'g': + if (what || topoct || copybits) + state = STATE_ERROR; + copybits = 0010; + break; + case 'o': + if (what || topoct || copybits) + state = STATE_ERROR; + copybits = 0001; + break; default: state = STATE_ERROR; break; @@ -212,6 +272,20 @@ return first_mode; } +static int mode_copy_bits(int mode, int copy_src, int copy_dst, int copy_and) +{ + int copy_bits = 0; + + if (copy_src & 0100) + copy_bits |= (mode >> 6) & 7; + if (copy_src & 0010) + copy_bits |= (mode >> 3) & 7; + if (copy_src & 0001) + copy_bits |= mode & 7; + + return (copy_dst * copy_bits) & copy_and; +} + /* Takes an existing file permission and a list of AND/OR changes, and * create a new permissions. */ @@ -219,17 +293,25 @@ { int IsX = mode & 0111; int NonPerm = mode & ~CHMOD_BITS; + int copy_bits; for ( ; chmod_modes; chmod_modes = chmod_modes->next) { if ((chmod_modes->flags & FLAG_DIRS_ONLY) && !S_ISDIR(NonPerm)) continue; if ((chmod_modes->flags & FLAG_FILES_ONLY) && S_ISDIR(NonPerm)) continue; + copy_bits = mode_copy_bits(mode, chmod_modes->ModeCOPY_SRC, + chmod_modes->ModeCOPY_DST, + chmod_modes->ModeCOPY_AND); mode &= chmod_modes->ModeAND; if ((chmod_modes->flags & FLAG_X_KEEP) && !IsX && !S_ISDIR(NonPerm)) mode |= chmod_modes->ModeOR & ~0111; else mode |= chmod_modes->ModeOR; + if (chmod_modes->ModeOP == CHMOD_SUB) + mode &= CHMOD_BITS - copy_bits; + else + mode |= copy_bits; } return mode | NonPerm; diff -Nru rsync-3.4.1+ds1/cleanup.c rsync-3.5.0+ds1/cleanup.c --- rsync-3.4.1+ds1/cleanup.c 2020-07-13 06:25:21.000000000 +0000 +++ rsync-3.5.0+ds1/cleanup.c 2026-05-24 02:31:52.000000000 +0000 @@ -198,7 +198,7 @@ switch_step++; if (cleanup_fname) - do_unlink(cleanup_fname); + do_unlink_at(cleanup_fname); if (exit_code) kill_all(SIGUSR1); if (cleanup_pid && cleanup_pid == getpid()) { @@ -269,8 +269,16 @@ break; } - if (called_from_signal_handler) + if (called_from_signal_handler) { +#ifdef GCOV_COVERAGE + /* _exit() bypasses the gcov atexit flush; rsync's generator (and + * other processes) normally finish via the signal handler, so + * without this they would write no .gcda. Harmless otherwise. */ + extern void __gcov_dump(void); + __gcov_dump(); +#endif _exit(exit_code); + } exit(exit_code); } diff -Nru rsync-3.4.1+ds1/clientname.c rsync-3.5.0+ds1/clientname.c --- rsync-3.4.1+ds1/clientname.c 2022-01-16 01:21:01.000000000 +0000 +++ rsync-3.5.0+ds1/clientname.c 2026-04-16 03:59:52.000000000 +0000 @@ -167,7 +167,7 @@ char sig[PROXY_V2_SIG_SIZE]; char ver_cmd; char fam; - char len[2]; + unsigned char len[2]; union { struct { char src_addr[4]; diff -Nru rsync-3.4.1+ds1/clientserver.c rsync-3.5.0+ds1/clientserver.c --- rsync-3.4.1+ds1/clientserver.c 2022-09-30 19:34:58.000000000 +0000 +++ rsync-3.5.0+ds1/clientserver.c 2026-08-02 03:26:41.000000000 +0000 @@ -30,6 +30,7 @@ extern int am_sender; extern int am_server; extern int am_daemon; +extern int am_chrooted; extern int am_root; extern int msgs2stderr; extern int rsync_port; @@ -38,8 +39,10 @@ extern int preserve_xattrs; extern int kluge_around_eof; extern int munge_symlinks; +extern int use_secure_symlinks; extern int open_noatime; extern int sanitize_paths; +extern int daemon_config_filter_file; extern int numeric_ids; extern int filesfrom_fd; extern int remote_protocol; @@ -68,6 +71,8 @@ char *auth_user; char *daemon_auth_choices; +/* read_args() enforces MAX_DAEMON_ARGS and reports "too many daemon arguments" + * before a daemon client can grow argv without bound. */ int read_only = 0; int module_id = -1; int pid_file_fd = -1; @@ -79,11 +84,32 @@ #define EARLY_INPUT_CMD "#early_input=" #define EARLY_INPUT_CMDLEN (sizeof EARLY_INPUT_CMD - 1) +/* Fallback bound on each peer-driven daemon handshake phase when no positive + * "timeout" is configured. A module value can shorten the pre-auth and + * argument-read phases, but cannot extend either beyond this limit. */ +#define DAEMON_HANDSHAKE_TIMEOUT 60 + +static int daemon_handshake_timeout(int module) +{ + int timeout = lp_timeout(module); + + /* "timeout" is parsed with atoi(), so negative values are possible. */ + if (timeout <= 0 || timeout > DAEMON_HANDSHAKE_TIMEOUT) + timeout = DAEMON_HANDSHAKE_TIMEOUT; + return timeout; +} + /* module_dirlen is the length of the module_dir string when in daemon * mode and module_dir is not "/"; otherwise 0. (Note that a chroot- * enabled module can have a non-"/" module_dir these days.) */ char *module_dir = NULL; unsigned int module_dirlen = 0; +/* An fd held open on the served module root, captured while the daemon is still + * positioned there (and privileged) -- so the sender's directory scan can be + * confined beneath the module by resolving module-relative paths against this fd, + * without re-walking (and re-permission-checking) the absolute module path as the + * dropped-privilege module uid. -1 when not a daemon or not yet captured. */ +int module_dirfd = -1; char *full_module_path; @@ -156,7 +182,12 @@ if (!am_client) { char *motd = lp_motd_file(); if (motd && *motd) { - FILE *f = fopen(motd, "r"); + /* '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); + FILE *f = motd_fd >= 0 ? fdopen(motd_fd, "r") : NULL; + if (!f && motd_fd >= 0) close(motd_fd); while (f && !feof(f)) { int len = fread(buf, 1, bufsiz - 1, f); if (len > 0) @@ -260,19 +291,30 @@ if (!user) user = getenv("LOGNAME"); - if (exchange_protocols(f_in, f_out, line, sizeof line, 1) < 0) + if (exchange_protocols(f_in, f_out, line, sizeof line, 1) < 0) { + free(modname); return -1; + } if (early_input_file) { STRUCT_STAT st; - FILE *f = fopen(early_input_file, "rb"); + /* --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); + 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) { rsyserr(FERROR, errno, "failed to open %s", early_input_file); + if (f) + fclose(f); + free(modname); return -1; } early_input_len = st.st_size; if (early_input_len > (int)sizeof line) { rprintf(FERROR, "%s is > %d bytes.\n", early_input_file, (int)sizeof line); + fclose(f); + free(modname); return -1; } if (early_input_len > 0) { @@ -281,6 +323,8 @@ int len; if (feof(f)) { rprintf(FERROR, "Early EOF in %s\n", early_input_file); + fclose(f); + free(modname); return -1; } len = fread(line, 1, early_input_len, f); @@ -357,6 +401,7 @@ while (1) { if (!read_line_old(f_in, line, sizeof line, 0)) { rprintf(FERROR, "rsync: didn't get server startup line\n"); + free(modname); return -1; } @@ -380,6 +425,7 @@ rprintf(FERROR, "%s\n", line); /* This is always fatal; the server will now * close the socket. */ + free(modname); return -1; } @@ -541,6 +587,7 @@ status = shell_exec(cmd); + gcov_flush(); if (!WIFEXITED(status)) _exit(1); _exit(WEXITSTATUS(status)); @@ -756,6 +803,9 @@ } read_only = lp_read_only(i); /* may also be overridden by auth_server() */ + /* The module is now known, so its local timeout policy can tighten the + * absolute deadline while the claimed slot is awaiting authentication. */ + set_daemon_handshake_timeout(daemon_handshake_timeout(i)); auth_user = auth_server(f_in, f_out, i, host, addr, "@RSYNCD: AUTHREQD "); if (!auth_user) { @@ -763,6 +813,10 @@ return -1; } set_env_str("RSYNC_USER_NAME", auth_user); + /* Do not count local setup or operator hooks against a peer's read time. + * In particular, the post-xfer parent and pre-xfer/name-converter children + * are forked below and must never inherit an armed asynchronous deadline. */ + set_daemon_handshake_timeout(0); module_id = i; @@ -871,6 +925,11 @@ } else set_filter_dir(module_dir, module_dirlen); + /* 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. */ + daemon_config_filter_file = 1; + p = lp_filter(module_id); parse_filter_str(&daemon_filter_list, p, rule_template(FILTRULE_WORD_SPLIT), XFLG_ABS_IF_SLASH | XFLG_DIR2WILD3); @@ -892,6 +951,8 @@ parse_filter_str(&daemon_filter_list, p, rule_template(FILTRULE_WORD_SPLIT), XFLG_ABS_IF_SLASH | XFLG_DIR2WILD3 | XFLG_OLD_PREFIXES); + daemon_config_filter_file = 0; + log_init(1); #if defined HAVE_SETENV || defined HAVE_PUTENV @@ -925,6 +986,7 @@ set_env_num("RSYNC_EXIT_STATUS", status); if (shell_exec(lp_postxfer_exec(module_id)) < 0) status = -1; + gcov_flush(); _exit(status); } } @@ -976,16 +1038,32 @@ } if (use_chroot) { + /* Cache timezone data before chroot makes /etc/localtime inaccessible */ + tzset(); + /* Flush gcov counters now: after chroot the build-tree .gcda + * paths are unreachable, so everything this child has executed + * so far (the whole rsync_module() pre-chroot path) would + * otherwise be lost. Post-chroot coverage from this child is + * still unrecordable -- accepted, documented in + * testsuite/COVERAGE.md. */ + gcov_flush(); if (chroot(module_chdir)) { rsyserr(FLOG, errno, "chroot(\"%s\") failed", module_chdir); io_printf(f_out, "@ERROR: chroot failed\n"); return -1; } + am_chrooted = 1; module_chdir = module_dir; } if (!change_dir(module_chdir, CD_NORMAL)) return path_failure(f_out, module_chdir, True); + /* Pin the module root by identity now -- cwd is the served root and we are + * still privileged -- so the sender's later directory scans resolve against + * this fd rather than re-walking the absolute module path post-setuid. */ +#if defined HAVE_FDOPENDIR && defined O_DIRECTORY + module_dirfd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC); +#endif if (module_dirlen) sanitize_paths = 1; @@ -1003,6 +1081,18 @@ } } + /* Enable secure symlink handling for any non-chrooted daemon module, and + * for a chroot module with a /./ inner boundary (module_dirlen) -- there + * the kernel chroot confines the outer path but not the inner module, so + * 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 + * 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(); + if (gid_list.count) { gid_t *gid_array = gid_list.items; if (setgid(gid_array[0])) { @@ -1054,9 +1144,14 @@ } } + /* This deadline is checked only in the read path, so the preceding local + * setup and hooks can take as long as necessary. Keep one absolute bound + * across both read_args() calls: anonymous modules must not be able to pin + * a max-connections slot by trickling an unterminated argument forever. */ + set_daemon_handshake_timeout(daemon_handshake_timeout(module_id)); io_printf(f_out, "@RSYNCD: OK\n"); - read_args(f_in, name, line, sizeof line, rl_nulls, &argv, &argc, &request); + read_args(f_in, name, line, sizeof line, rl_nulls, 1, &argv, &argc, &request); orig_argv = argv; save_munge_symlinks = munge_symlinks; @@ -1066,11 +1161,12 @@ if (protect_args && ret) { orig_early_argv = orig_argv; protect_args = 2; - read_args(f_in, name, line, sizeof line, 1, &argv, &argc, &request); + read_args(f_in, name, line, sizeof line, 1, 0, &argv, &argc, &request); orig_argv = argv; ret = parse_arguments(&argc, (const char ***) &argv); } else orig_early_argv = NULL; + set_daemon_handshake_timeout(0); /* The default is to use the user's setting unless the module sets True or False. */ if (lp_open_noatime(module_id) >= 0) @@ -1210,14 +1306,20 @@ return 0; } +static BOOL namecvt_safe_token(const char *s); + BOOL namecvt_call(const char *cmd, const char **name_p, id_t *id_p) { char buf[1024]; int got, len; - if (*name_p) + if (*name_p) { + if (!namecvt_safe_token(*name_p)) { + rprintf(FERROR, "invalid name-converter token: %s\n", *name_p); + return False; + } len = snprintf(buf, sizeof buf, "%s %s\n", cmd, *name_p); - else + } else len = snprintf(buf, sizeof buf, "%s %ld\n", cmd, (long)*id_p); if (len >= (int)sizeof buf) { rprintf(FERROR, "namecvt_call() request was too large.\n"); @@ -1234,14 +1336,39 @@ if (!read_line_old(namecvt_fd_ans, buf, sizeof buf, 0)) return False; - if (*name_p) - *id_p = (id_t)atol(buf); - else + if (*name_p) { + /* Name-to-id: an unknown name returns an empty line and atol("")=0 + * would map it to root, so validate strictly below (all digits, no + * ERANGE, fits id_t). */ + const char *p; + unsigned long v; + if (!*buf) + return False; + for (p = buf; *p; p++) { + if (*p < '0' || *p > '9') + return False; + } + errno = 0; + v = strtoul(buf, NULL, 10); + if (errno == ERANGE || v > (unsigned long)(id_t)-1) + return False; + *id_p = (id_t)v; + } else *name_p = strdup(buf); return True; } +static BOOL namecvt_safe_token(const char *s) +{ + for (; *s; s++) { + unsigned char ch = (unsigned char)*s; + if (ch < ' ' || ch == 0x7f) + return False; + } + return True; +} + /* send a list of available modules to the client. Don't list those with "list = False". */ static void send_listing(int fd) @@ -1258,6 +1385,18 @@ io_printf(fd,"@RSYNCD: EXIT\n"); } +static int proxy_peer_allowed(int fd) +{ + const char *host = undetermined_hostname; + const char *addr = client_addr(fd); + + if (!allow_proxy_protocol_peer(lp_proxy_protocol_hosts(), addr, &host)) { + rprintf(FLOG, "proxy protocol rejected from untrusted peer %s (%s)\n", host, addr); + return 0; + } + return 1; +} + static int load_config(int globals_only) { if (!config_file) { @@ -1295,16 +1434,60 @@ if (!load_config(0)) exit_cleanup(RERR_SYNTAX); - if (lp_proxy_protocol() && !read_proxy_protocol_header(f_in)) - return -1; + /* Bound the handshake before ANY peer input is read -- the PROXY-protocol + * header below is peer-supplied too, and was previously unbounded. An + * rsh-run daemon is not a listener and has no shared slot to exhaust. */ + if (am_daemon > 0) + set_daemon_handshake_timeout(daemon_handshake_timeout(-1)); + + if (lp_proxy_protocol()) { + if (!proxy_peer_allowed(f_in) || !read_proxy_protocol_header(f_in)) + return -1; + } + + /* Do reverse DNS lookup before chroot/setuid. The result is cached, + * so the later client_name() call will use this cached value. This + * ensures hostname-based ACLs work even when DNS is unavailable + * after chroot. + * + * "reverse lookup" can be set globally OR per-module, so we also + * scan each module: a deployment with "reverse lookup = no" in the + * global section but "reverse lookup = yes" in a specific module + * still triggers a post-chroot lookup at access-check time + * (rsync_module() in this file), which would also fail in the + * chroot and turn hostname-based deny rules into silent bypasses. */ + { + int need_reverse = lp_reverse_lookup(-1); + int j, num_modules = lp_num_modules(); + for (j = 0; !need_reverse && j < num_modules; j++) { + if (lp_reverse_lookup(j)) + need_reverse = 1; + } + if (need_reverse) + (void)client_name(client_addr(f_in)); + } p = lp_daemon_chroot(); if (*p) { log_init(0); /* Make use we've initialized syslog before chrooting. */ + tzset(); if (chroot(p) < 0) { rsyserr(FLOG, errno, "daemon chroot(\"%s\") failed", p); return -1; } + /* 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 + * 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, + * not to any individual module path -- modules sharing the + * daemon chroot are still distinguishable filesystem + * 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() + * still fires for "use chroot = no" modules. */ if (chdir("/") < 0) { rsyserr(FLOG, errno, "daemon chdir(\"/\") failed"); return -1; @@ -1347,6 +1530,7 @@ set_nonblocking(f_in); } + if (exchange_protocols(f_in, f_out, line, sizeof line, 0) < 0) return -1; @@ -1401,21 +1585,58 @@ char pidbuf[32]; STRUCT_STAT st1, st2; char *fail = NULL; + const char *base = pid_file; + int pdfd = -1; if (!pid_file || !*pid_file) return; #ifdef O_NOFOLLOW -#define SAFE_OPEN_FLAGS (O_CREAT|O_NOFOLLOW) +#define SAFE_NOFOLLOW O_NOFOLLOW #else -#define SAFE_OPEN_FLAGS (O_CREAT) +#define SAFE_NOFOLLOW 0 +#endif + +#ifdef AT_FDCWD + /* Pin the parent directory so the existence check, open and re-stat below + * all resolve the leaf against one stable directory inode, removing the + * lstat->open path race. The parent is operator-configured and trusted, so + * it is opened following symlinks (e.g. a /var/run -> /run); only the leaf + * is opened/checked O_NOFOLLOW (the do_*_atfd wrappers force that). */ + { + const char *slash = strrchr(pid_file, '/'); + char dirbuf[MAXPATHLEN]; + const char *dir = "."; + if (slash) { + size_t dlen = slash == pid_file ? 1 : (size_t)(slash - pid_file); + if (dlen >= sizeof dirbuf) { + rprintf(FLOG, "pid file path is too long: %s\n", pid_file); + exit_cleanup(RERR_FILEIO); + } + memcpy(dirbuf, pid_file, dlen); + dirbuf[dlen] = '\0'; + dir = dirbuf; + base = slash + 1; + } + if ((pdfd = do_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) +#else +#define PID_LSTAT(stp) do_lstat(base, stp) +#define PID_UNLINK() unlink(base) +#define PID_OPEN() do_open(base, O_RDWR|O_CREAT|SAFE_NOFOLLOW, 0664) #endif /* These tests make sure that a temp-style lock dir is handled safely. */ st1.st_mode = 0; - if (do_lstat(pid_file, &st1) == 0 && !S_ISREG(st1.st_mode) && unlink(pid_file) < 0) + if (PID_LSTAT(&st1) == 0 && !S_ISREG(st1.st_mode) && PID_UNLINK() < 0) fail = "unlink"; - else if ((pid_file_fd = do_open(pid_file, O_RDWR|SAFE_OPEN_FLAGS, 0664)) < 0) + else if ((pid_file_fd = PID_OPEN()) < 0) fail = S_ISREG(st1.st_mode) ? "open" : "create"; else if (!lock_range(pid_file_fd, 0, 4)) fail = "lock"; @@ -1423,7 +1644,7 @@ fail = "fstat opened"; else if (st1.st_size > (int)sizeof pidbuf) fail = "find small"; - else if (do_lstat(pid_file, &st2) < 0) + else if (PID_LSTAT(&st2) < 0) fail = "lstat"; else if (!S_ISREG(st1.st_mode)) fail = "avoid file overwrite race for"; @@ -1447,6 +1668,13 @@ cleanup_set_pid(pid); /* Mark the file for removal on exit, even if the write failed. */ } +#undef PID_LSTAT +#undef PID_UNLINK +#undef PID_OPEN +#undef SAFE_NOFOLLOW + if (pdfd >= 0) + close(pdfd); + if (fail) { char msg[1024]; snprintf(msg, sizeof msg, "failed to %s pid file %s: %s\n", @@ -1470,6 +1698,7 @@ fprintf(stderr, "failed to fork: %s\n", strerror(errno)); exit_cleanup(RERR_FILEIO); } + gcov_flush(); _exit(0); } @@ -1515,6 +1744,17 @@ } set_dparams(0); + /* "proxy protocol = true" with no trusted-proxy list rejects every + * connection as an untrusted proxy peer (fail-closed). That is intended, + * but silent at startup, so warn the operator while stderr is still open. */ + if (lp_proxy_protocol() + && (!lp_proxy_protocol_hosts() || !*lp_proxy_protocol_hosts())) { + rprintf(FWARNING, + "\"proxy protocol = true\" but \"proxy protocol hosts\" is unset:" + " all connections will be rejected as untrusted proxy peers." + " Set \"proxy protocol hosts\" to your trusted proxy's address.\n"); + } + if (no_detach) create_pid_file(); else diff -Nru rsync-3.4.1+ds1/compat.c rsync-3.5.0+ds1/compat.c --- rsync-3.4.1+ds1/compat.c 2022-10-25 16:04:45.000000000 +0000 +++ rsync-3.5.0+ds1/compat.c 2026-07-20 04:05:31.000000000 +0000 @@ -52,6 +52,7 @@ extern int delete_mode, delete_before, delete_during, delete_after; extern int do_compression; extern int do_compression_level; +extern int do_compression_threads; extern int saw_stderr_opt; extern int msgs2stderr; extern char *shell_cmd; @@ -131,7 +132,7 @@ * of that protocol for it to be advertised as available. */ static void check_sub_protocol(void) { - char *dot; + const char *dot; int their_protocol, their_sub; int our_sub = get_subprotocol_version(); @@ -350,7 +351,7 @@ continue; ret = nni; best = nno->saw[nni->num]; - if (best == 1 || am_server) /* The server side stops at the first acceptable client choice */ + if (best == 1) /* Can't improve on our own #1 preference */ break; } if (ret) { @@ -414,7 +415,7 @@ env_str = ntype == NSTR_COMPRESS ? "zlib" : protocol_version >= 30 ? "md5" : "md4"; if (am_server && env_str) { - char *cp = strchr(env_str, '&'); + const char *cp = strchr(env_str, '&'); if (cp) env_str = cp + 1; } @@ -525,8 +526,11 @@ rprintf(FINFO, "Client %s list (on client): %s\n", nno->type, tmpbuf); } - /* Each side sends their list of valid names to the other side and then both sides - * pick the first name in the client's list that is also in the server's list. */ + /* Each side sends their list of valid names to the other side and then each + * side picks its own most-preferred name that also appears in the peer's + * list. Honest peers emit their list in table (strongest-first) order via + * get_default_nno_list(), so both sides converge on the strongest mutual + * choice; a peer that front-loads a weaker name only desyncs itself. */ if (do_negotiated_strings) write_vstring(f_out, tmpbuf, len); } @@ -584,14 +588,13 @@ pathname_ndx = (file_extra_cnt += PTR_EXTRA_CNT); else depth_ndx = ++file_extra_cnt; - if (preserve_uid) - uid_ndx = ++file_extra_cnt; - if (preserve_gid) - gid_ndx = ++file_extra_cnt; - if (preserve_acls && !am_sender) - acls_ndx = ++file_extra_cnt; - if (preserve_xattrs) - xattrs_ndx = ++file_extra_cnt; + /* uid_ndx/gid_ndx/acls_ndx/xattrs_ndx are assigned AFTER + * check_batch_flags() below: a batch file's stream-flags can flip + * preserve_uid/gid/acls/xattrs on, and computing the *_ndx slots + * before that leaves e.g. preserve_xattrs=1 with xattrs_ndx=0 -- so + * F_XATTR(file) (= REQ_EXTRA(file, 0)) writes at offset 0 of every + * file_struct, clobbering file->dirname. Nothing between here and + * check_batch_flags() reads file_extra_cnt or the *_ndx values. */ if (am_server) set_allow_inc_recurse(); @@ -638,6 +641,15 @@ if (read_batch) check_batch_flags(); + if (preserve_uid) + uid_ndx = ++file_extra_cnt; + if (preserve_gid) + gid_ndx = ++file_extra_cnt; + if (preserve_acls && !am_sender) + acls_ndx = ++file_extra_cnt; + if (preserve_xattrs) + xattrs_ndx = ++file_extra_cnt; + if (!saw_stderr_opt && protocol_version <= 28 && am_server) msgs2stderr = 0; /* The client side may not have stderr setup for us. */ diff -Nru rsync-3.4.1+ds1/config.h.in rsync-3.5.0+ds1/config.h.in --- rsync-3.4.1+ds1/config.h.in 2025-01-15 20:49:24.000000000 +0000 +++ rsync-3.5.0+ds1/config.h.in 2026-08-13 00:05:09.000000000 +0000 @@ -24,22 +24,33 @@ /* Used to make "checker" understand that FD_ZERO() clears memory. */ #undef FORCE_FD_ZERO_MEMSET +/* Flush gcov counters at exit_cleanup: rsync's children exit via _exit(), + which bypasses the gcov atexit handler, so without this no .gcda is written + for the receiver/generator/daemon-worker processes. */ +#undef GCOV_COVERAGE + /* Define to the type of elements in the array set by `getgroups'. Usually this is either `int' or `gid_t'. */ #undef GETGROUPS_T -/* Define to 1 if the `getpgrp' function requires zero arguments. */ +/* Define to 1 if the 'getpgrp' function requires zero arguments. */ #undef GETPGRP_VOID -/* Define to 1 if you have the `aclsort' function. */ +/* Define to 1 if you have the 'aclsort' function. */ #undef HAVE_ACLSORT +/* Define to 1 if you have the 'acl_delete_def_file_at' function. */ +#undef HAVE_ACL_DELETE_DEF_FILE_AT + /* true if you have acl_get_perm_np */ #undef HAVE_ACL_GET_PERM_NP /* Define to 1 if you have the header file. */ #undef HAVE_ACL_LIBACL_H +/* Define to 1 if you have the 'acl_set_file_at' function. */ +#undef HAVE_ACL_SET_FILE_AT + /* true if you have AIX ACLs */ #undef HAVE_AIX_ACLS @@ -55,10 +66,10 @@ /* Define to 1 if you have the header file. */ #undef HAVE_ARPA_NAMESER_H -/* Define to 1 if you have the `asprintf' function. */ +/* Define to 1 if you have the 'asprintf' function. */ #undef HAVE_ASPRINTF -/* Define to 1 if you have the `attropen' function. */ +/* Define to 1 if you have the 'attropen' function. */ #undef HAVE_ATTROPEN /* Define to 1 if you have the header file. */ @@ -73,13 +84,13 @@ /* Define to 1 if vsprintf has a C99-compatible return value */ #undef HAVE_C99_VSNPRINTF -/* Define to 1 if you have the `chflags' function. */ +/* Define to 1 if you have the 'chflags' function. */ #undef HAVE_CHFLAGS -/* Define to 1 if you have the `chmod' function. */ +/* Define to 1 if you have the 'chmod' function. */ #undef HAVE_CHMOD -/* Define to 1 if you have the `chown' function. */ +/* Define to 1 if you have the 'chown' function. */ #undef HAVE_CHOWN /* Define to 1 if you have the header file. */ @@ -91,10 +102,13 @@ /* Define to 1 if you have the header file. */ #undef HAVE_CTYPE_H -/* Define to 1 if you have the header file, and it defines `DIR'. +/* Define to 1 if you have the header file, and it defines 'DIR'. */ #undef HAVE_DIRENT_H +/* Define to 1 if you have a working dirfd() (function or macro). */ +#undef HAVE_DIRFD + /* Define to 1 if you have the header file. */ #undef HAVE_DL_H @@ -104,7 +118,7 @@ /* Define to 1 if errno is declared in errno.h */ #undef HAVE_ERRNO_DECL -/* Define to 1 if you have the `extattr_get_link' function. */ +/* Define to 1 if you have the 'extattr_get_link' function. */ #undef HAVE_EXTATTR_GET_LINK /* Define to 1 if you have the fallocate function and it compiles and links @@ -117,55 +131,64 @@ /* Define if FALLOC_FL_ZERO_RANGE is available. */ #undef HAVE_FALLOC_FL_ZERO_RANGE -/* Define to 1 if you have the `fchmod' function. */ +/* Define to 1 if you have the 'fchmod' function. */ #undef HAVE_FCHMOD /* Define to 1 if you have the header file. */ #undef HAVE_FCNTL_H +/* Define to 1 if you have the 'fdopendir' function. */ +#undef HAVE_FDOPENDIR + /* Define to 1 if you have the header file. */ #undef HAVE_FLOAT_H /* True if you have FreeBSD xattrs */ #undef HAVE_FREEBSD_XATTRS -/* Define to 1 if you have the `fstat' function. */ +/* Define to 1 if you have the 'fstat' function. */ #undef HAVE_FSTAT -/* Define to 1 if you have the `ftruncate' function. */ +/* Define to 1 if you have the 'ftruncate' function. */ #undef HAVE_FTRUNCATE +/* Define to 1 if you have the 'futimens' function. */ +#undef HAVE_FUTIMENS + /* Define to 1 if you have the "getaddrinfo" function and required types. */ #undef HAVE_GETADDRINFO -/* Define to 1 if you have the `getattrlist' function. */ +/* Define to 1 if you have the 'getattrlist' function. */ #undef HAVE_GETATTRLIST -/* Define to 1 if you have the `getcwd' function. */ +/* Define to 1 if you have the 'getcwd' function. */ #undef HAVE_GETCWD -/* Define to 1 if you have the `getegid' function. */ +/* Define to 1 if you have the 'getegid' function. */ #undef HAVE_GETEGID -/* Define to 1 if you have the `geteuid' function. */ +/* Define to 1 if you have the 'geteuid' function. */ #undef HAVE_GETEUID -/* Define to 1 if you have the `getgrouplist' function. */ +/* Define to 1 if you have the 'getgrouplist' function. */ #undef HAVE_GETGROUPLIST -/* Define to 1 if you have the `getgroups' function. */ +/* Define to 1 if you have the 'getgroups' function. */ #undef HAVE_GETGROUPS -/* Define to 1 if you have the `getpass' function. */ +/* Define to 1 if you have the 'getpass' function. */ #undef HAVE_GETPASS -/* Define to 1 if you have the `getpgrp' function. */ +/* Define to 1 if you have the 'getpgrp' function. */ #undef HAVE_GETPGRP +/* Define to 1 if you have the 'getrlimit' function. */ +#undef HAVE_GETRLIMIT + /* Define to 1 if gettimeofday() takes a time-zone arg */ #undef HAVE_GETTIMEOFDAY_TZ -/* Define to 1 if you have the `getxattr' function. */ +/* Define to 1 if you have the 'getxattr' function. */ #undef HAVE_GETXATTR /* Define to 1 if you have the header file. */ @@ -177,22 +200,22 @@ /* Define to 1 if you have the header file. */ #undef HAVE_ICONV_H -/* Define to 1 if you have the `iconv_open' function. */ +/* Define to 1 if you have the 'iconv_open' function. */ #undef HAVE_ICONV_OPEN -/* Define to 1 if the system has the type `id_t'. */ +/* Define to 1 if the system has the type 'id_t'. */ #undef HAVE_ID_T -/* Define to 1 if you have the `inet_ntop' function. */ +/* Define to 1 if you have the 'inet_ntop' function. */ #undef HAVE_INET_NTOP -/* Define to 1 if you have the `inet_pton' function. */ +/* Define to 1 if you have the 'inet_pton' function. */ #undef HAVE_INET_PTON -/* Define to 1 if you have the `initgroups' function. */ +/* Define to 1 if you have the 'initgroups' function. */ #undef HAVE_INITGROUPS -/* Define to 1 if you have the `innetgr' function. */ +/* Define to 1 if you have the 'innetgr' function. */ #undef HAVE_INNETGR /* Define to 1 if you have the header file. */ @@ -204,52 +227,53 @@ /* Define to 1 if you have the header file. */ #undef HAVE_LANGINFO_H -/* Define to 1 if you have the `lchmod' function. */ +/* Define to 1 if you have the 'lchmod' function. */ #undef HAVE_LCHMOD -/* Define to 1 if you have the `lchown' function. */ +/* Define to 1 if you have the 'lchown' function. */ #undef HAVE_LCHOWN -/* Define to 1 if you have the `acl' library (-lacl). */ +/* Define to 1 if you have the 'acl' library (-lacl). */ #undef HAVE_LIBACL -/* Define to 1 if you have the `attr' library (-lattr). */ -#undef HAVE_LIBATTR +/* Define to 1 if libacl provides + acl_get_file_at/acl_set_file_at/acl_delete_def_file_at */ +#undef HAVE_LIBACL_AT /* Define to 1 if you have the header file. */ #undef HAVE_LIBCHARSET_H -/* Define to 1 if you have the `inet' library (-linet). */ +/* Define to 1 if you have the 'inet' library (-linet). */ #undef HAVE_LIBINET -/* Define to 1 if you have the `nsl' library (-lnsl). */ +/* Define to 1 if you have the 'nsl' library (-lnsl). */ #undef HAVE_LIBNSL -/* Define to 1 if you have the `nsl_s' library (-lnsl_s). */ +/* Define to 1 if you have the 'nsl_s' library (-lnsl_s). */ #undef HAVE_LIBNSL_S -/* Define to 1 if you have the `popt' library (-lpopt). */ +/* Define to 1 if you have the 'popt' library (-lpopt). */ #undef HAVE_LIBPOPT -/* Define to 1 if you have the `resolv' library (-lresolv). */ +/* Define to 1 if you have the 'resolv' library (-lresolv). */ #undef HAVE_LIBRESOLV -/* Define to 1 if you have the `sec' library (-lsec). */ +/* Define to 1 if you have the 'sec' library (-lsec). */ #undef HAVE_LIBSEC -/* Define to 1 if you have the `socket' library (-lsocket). */ +/* Define to 1 if you have the 'socket' library (-lsocket). */ #undef HAVE_LIBSOCKET -/* Define to 1 if you have the `z' library (-lz). */ +/* Define to 1 if you have the 'z' library (-lz). */ #undef HAVE_LIBZ /* Define to 1 if you have the header file. */ #undef HAVE_LIMITS_H -/* Define to 1 if you have the `link' function. */ +/* Define to 1 if you have the 'link' function. */ #undef HAVE_LINK -/* Define to 1 if you have the `linkat' function. */ +/* Define to 1 if you have the 'linkat' function. */ #undef HAVE_LINKAT /* Define to 1 if you have the header file. */ @@ -258,7 +282,7 @@ /* True if you have Linux xattrs (or equivalent) */ #undef HAVE_LINUX_XATTRS -/* Define to 1 if you have the `locale_charset' function. */ +/* Define to 1 if you have the 'locale_charset' function. */ #undef HAVE_LOCALE_CHARSET /* Define to 1 if you have the header file. */ @@ -268,23 +292,23 @@ than `double'. */ #undef HAVE_LONG_DOUBLE -/* Define to 1 if the type `long double' works and has more range or precision - than `double'. */ +/* Define to 1 if the type 'long double' works and has more range or precision + than 'double'. */ #undef HAVE_LONG_DOUBLE_WIDER -/* Define to 1 if you have the `lseek64' function. */ +/* Define to 1 if you have the 'lseek64' function. */ #undef HAVE_LSEEK64 -/* Define to 1 if you have the `lutimes' function. */ +/* Define to 1 if you have the 'lutimes' function. */ #undef HAVE_LUTIMES /* Define to 1 if you have the header file. */ #undef HAVE_LZ4_H -/* Define to 1 if you have the `mallinfo' function. */ +/* Define to 1 if you have the 'mallinfo' function. */ #undef HAVE_MALLINFO -/* Define to 1 if you have the `mallinfo2' function. */ +/* Define to 1 if you have the 'mallinfo2' function. */ #undef HAVE_MALLINFO2 /* Define to 1 if you have the header file. */ @@ -293,31 +317,37 @@ /* Define to 1 if you have the header file. */ #undef HAVE_MCHECK_H -/* Define to 1 if you have the `memmove' function. */ +/* Define to 1 if you have the 'memmove' function. */ #undef HAVE_MEMMOVE -/* Define to 1 if you have the `mkfifo' function. */ +/* Define to 1 if you have the 'mkfifo' function. */ #undef HAVE_MKFIFO -/* Define to 1 if you have the `mknod' function. */ +/* Define to 1 if you have the 'mkfifoat' function. */ +#undef HAVE_MKFIFOAT + +/* Define to 1 if you have the 'mknod' function. */ #undef HAVE_MKNOD -/* Define to 1 if you have the `mkstemp64' function. */ +/* Define to 1 if you have the 'mknodat' function. */ +#undef HAVE_MKNODAT + +/* Define to 1 if you have the 'mkstemp64' function. */ #undef HAVE_MKSTEMP64 -/* Define to 1 if you have the `mktime' function. */ +/* Define to 1 if you have the 'mktime' function. */ #undef HAVE_MKTIME -/* Define to 1 if the system has the type `mode_t'. */ +/* Define to 1 if the system has the type 'mode_t'. */ #undef HAVE_MODE_T -/* Define to 1 if you have the `mtrace' function. */ +/* Define to 1 if you have the 'mtrace' function. */ #undef HAVE_MTRACE -/* Define to 1 if you have the `nanosleep' function. */ +/* Define to 1 if you have the 'nanosleep' function. */ #undef HAVE_NANOSLEEP -/* Define to 1 if you have the header file, and it defines `DIR'. */ +/* Define to 1 if you have the header file, and it defines 'DIR'. */ #undef HAVE_NDIR_H /* Define to 1 if you have the header file. */ @@ -332,15 +362,19 @@ /* Define to 1 if you have the header file. */ #undef HAVE_NETINET_IP_H -/* Define to 1 if you have the `nl_langinfo' function. */ +/* Define to 1 if you have the 'nl_langinfo' function. */ #undef HAVE_NL_LANGINFO -/* Define to 1 if the system has the type `off_t'. */ +/* Define to 1 if the system has the type 'off_t'. */ #undef HAVE_OFF_T -/* Define to 1 if you have the `open64' function. */ +/* Define to 1 if you have the 'open64' function. */ #undef HAVE_OPEN64 +/* Define to use Linux openat2(RESOLVE_BENEATH) in secure_relative_open where + available. */ +#undef HAVE_OPENAT2 + /* Define to 1 if you have the header file. */ #undef HAVE_OPENSSL_MD4_H @@ -353,9 +387,15 @@ /* True if you have Mac OS X xattrs */ #undef HAVE_OSX_XATTRS -/* Define to 1 if the system has the type `pid_t'. */ +/* Define to 1 if the system has the type 'pid_t'. */ #undef HAVE_PID_T +/* Define to 1 if you have the 'poll' function. */ +#undef HAVE_POLL + +/* Define to 1 if you have the header file. */ +#undef HAVE_POLL_H + /* Define to 1 if you have the header file. */ #undef HAVE_POPT_H @@ -365,13 +405,13 @@ /* true if you have posix ACLs */ #undef HAVE_POSIX_ACLS -/* Define to 1 if you have the `posix_fallocate' function. */ +/* Define to 1 if you have the 'posix_fallocate' function. */ #undef HAVE_POSIX_FALLOCATE -/* Define to 1 if you have the `putenv' function. */ +/* Define to 1 if you have the 'putenv' function. */ #undef HAVE_PUTENV -/* Define to 1 if you have the `readlink' function. */ +/* Define to 1 if you have the 'readlink' function. */ #undef HAVE_READLINK /* Define to 1 if remote shell is remsh, not rsh */ @@ -380,40 +420,43 @@ /* Define to 1 if mkstemp() is available and works right */ #undef HAVE_SECURE_MKSTEMP -/* Define to 1 if you have the `setattrlist' function. */ +/* Define to 1 if you have the 'setattrlist' function. */ #undef HAVE_SETATTRLIST -/* Define to 1 if you have the `setenv' function. */ +/* Define to 1 if you have the 'setenv' function. */ #undef HAVE_SETENV -/* Define to 1 if you have the `seteuid' function. */ +/* Define to 1 if you have the 'seteuid' function. */ #undef HAVE_SETEUID -/* Define to 1 if you have the `setgroups' function. */ +/* Define to 1 if you have the 'setgroups' function. */ #undef HAVE_SETGROUPS -/* Define to 1 if you have the `setlocale' function. */ +/* Define to 1 if you have the 'setlocale' function. */ #undef HAVE_SETLOCALE -/* Define to 1 if you have the `setmode' function. */ +/* Define to 1 if you have the 'setmode' function. */ #undef HAVE_SETMODE -/* Define to 1 if you have the `setsid' function. */ +/* Define to 1 if you have the 'setrlimit' function. */ +#undef HAVE_SETRLIMIT + +/* Define to 1 if you have the 'setsid' function. */ #undef HAVE_SETSID -/* Define to 1 if you have the `setvbuf' function. */ +/* Define to 1 if you have the 'setvbuf' function. */ #undef HAVE_SETVBUF -/* Define to 1 if you have the `sigaction' function. */ +/* Define to 1 if you have the 'sigaction' function. */ #undef HAVE_SIGACTION -/* Define to 1 if you have the `sigprocmask' function. */ +/* Define to 1 if you have the 'sigprocmask' function. */ #undef HAVE_SIGPROCMASK -/* Define to 1 if the system has the type `size_t'. */ +/* Define to 1 if the system has the type 'size_t'. */ #undef HAVE_SIZE_T -/* Define to 1 if you have the `snprintf' function. */ +/* Define to 1 if you have the 'snprintf' function. */ #undef HAVE_SNPRINTF /* Do we have sockaddr_in6.sin6_scope_id? */ @@ -446,19 +489,19 @@ /* Define to 1 if you have the header file. */ #undef HAVE_STDLIB_H -/* Define to 1 if you have the `stpcpy' function. */ +/* Define to 1 if you have the 'stpcpy' function. */ #undef HAVE_STPCPY -/* Define to 1 if you have the `strcasecmp' function. */ +/* Define to 1 if you have the 'strcasecmp' function. */ #undef HAVE_STRCASECMP -/* Define to 1 if you have the `strchr' function. */ +/* Define to 1 if you have the 'strchr' function. */ #undef HAVE_STRCHR -/* Define to 1 if you have the `strerror' function. */ +/* Define to 1 if you have the 'strerror' function. */ #undef HAVE_STRERROR -/* Define to 1 if you have the `strftime' function. */ +/* Define to 1 if you have the 'strftime' function. */ #undef HAVE_STRFTIME /* Define to 1 if you have the header file. */ @@ -467,37 +510,37 @@ /* Define to 1 if you have the header file. */ #undef HAVE_STRING_H -/* Define to 1 if you have the `strlcat' function. */ +/* Define to 1 if you have the 'strlcat' function. */ #undef HAVE_STRLCAT -/* Define to 1 if you have the `strlcpy' function. */ +/* Define to 1 if you have the 'strlcpy' function. */ #undef HAVE_STRLCPY -/* Define to 1 if you have the `strpbrk' function. */ +/* Define to 1 if you have the 'strpbrk' function. */ #undef HAVE_STRPBRK -/* Define to 1 if you have the `strtol' function. */ +/* Define to 1 if you have the 'strtol' function. */ #undef HAVE_STRTOL -/* Define to 1 if the system has the type `struct addrinfo'. */ +/* Define to 1 if the system has the type 'struct addrinfo'. */ #undef HAVE_STRUCT_ADDRINFO -/* Define to 1 if the system has the type `struct sockaddr_storage'. */ +/* Define to 1 if the system has the type 'struct sockaddr_storage'. */ #undef HAVE_STRUCT_SOCKADDR_STORAGE -/* Define to 1 if the system has the type `struct stat64'. */ +/* Define to 1 if the system has the type 'struct stat64'. */ #undef HAVE_STRUCT_STAT64 -/* Define to 1 if `st_mtimensec' is a member of `struct stat'. */ +/* Define to 1 if 'st_mtimensec' is a member of 'struct stat'. */ #undef HAVE_STRUCT_STAT_ST_MTIMENSEC -/* Define to 1 if `st_mtimespec.tv_nsec' is a member of `struct stat'. */ +/* Define to 1 if 'st_mtimespec.tv_nsec' is a member of 'struct stat'. */ #undef HAVE_STRUCT_STAT_ST_MTIMESPEC_TV_NSEC -/* Define to 1 if `st_mtim.tv_nsec' is a member of `struct stat'. */ +/* Define to 1 if 'st_mtim.tv_nsec' is a member of 'struct stat'. */ #undef HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC -/* Define to 1 if `st_rdev' is a member of `struct stat'. */ +/* Define to 1 if 'st_rdev' is a member of 'struct stat'. */ #undef HAVE_STRUCT_STAT_ST_RDEV /* Define to 1 if you have the "struct utimbuf" type */ @@ -509,7 +552,7 @@ /* Define to 1 if you have the header file. */ #undef HAVE_SYS_ATTR_H -/* Define to 1 if you have the header file, and it defines `DIR'. +/* Define to 1 if you have the header file, and it defines 'DIR'. */ #undef HAVE_SYS_DIR_H @@ -534,13 +577,16 @@ /* Define to 1 if you have the header file. */ #undef HAVE_SYS_MODE_H -/* Define to 1 if you have the header file, and it defines `DIR'. +/* Define to 1 if you have the header file, and it defines 'DIR'. */ #undef HAVE_SYS_NDIR_H /* Define to 1 if you have the header file. */ #undef HAVE_SYS_PARAM_H +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_RESOURCE_H + /* Define to 1 if you have the header file. */ #undef HAVE_SYS_SELECT_H @@ -568,7 +614,7 @@ /* Define to 1 if you have the header file. */ #undef HAVE_SYS_XATTR_H -/* Define to 1 if you have the `tcgetpgrp' function. */ +/* Define to 1 if you have the 'tcgetpgrp' function. */ #undef HAVE_TCGETPGRP /* true if you have Tru64 ACLs */ @@ -580,42 +626,46 @@ /* true if you have UnixWare ACLs */ #undef HAVE_UNIXWARE_ACLS -/* Define to 1 if you have the `unsetenv' function. */ +/* Define to 1 if you have the 'unsetenv' function. */ #undef HAVE_UNSETENV -/* Define to 1 if you have the `usleep' function. */ +/* Define to 1 if you have the 'usleep' function. */ #undef HAVE_USLEEP -/* Define to 1 if you have the `utime' function. */ +/* Define to 1 if you have the 'utime' function. */ #undef HAVE_UTIME -/* Define to 1 if you have the `utimensat' function. */ +/* Define to 1 if you have the 'utimensat' function. */ #undef HAVE_UTIMENSAT -/* Define to 1 if you have the `utimes' function. */ +/* Define to 1 if you have the 'utimes' function. */ #undef HAVE_UTIMES /* Define to 1 if you have the header file. */ #undef HAVE_UTIME_H -/* Define to 1 if `utime(file, NULL)' sets file's timestamp to the present. */ +/* Define to 1 if 'utime(file, NULL)' sets file's timestamp to the present. */ #undef HAVE_UTIME_NULL -/* Define to 1 if you have the `vasprintf' function. */ +/* Define to 1 if you have the 'vasprintf' function. */ #undef HAVE_VASPRINTF -/* Define to 1 if you have the `va_copy' function. */ +/* Define to 1 if you have the 'va_copy' function. */ #undef HAVE_VA_COPY -/* Define to 1 if you have the `vsnprintf' function. */ +/* Define to 1 if you have the 'vsnprintf' function. */ #undef HAVE_VSNPRINTF -/* Define to 1 if you have the `wait4' function. */ +/* Define to 1 if you have the 'wait4' function. */ #undef HAVE_WAIT4 -/* Define to 1 if you have the `waitpid' function. */ +/* Define to 1 if you have the 'waitpid' function. */ #undef HAVE_WAITPID +/* Define to 1 if the setxattrat/getxattrat/removexattrat syscalls are + available */ +#undef HAVE_XATTRAT_SYSCALLS + /* Define to 1 if you have the header file. */ #undef HAVE_XXHASH_H @@ -625,19 +675,19 @@ /* Define to 1 if you have the header file. */ #undef HAVE_ZSTD_H -/* Define to 1 if you have the `_acl' function. */ +/* Define to 1 if you have the '_acl' function. */ #undef HAVE__ACL -/* Define to 1 if you have the `_facl' function. */ +/* Define to 1 if you have the '_facl' function. */ #undef HAVE__FACL -/* Define to 1 if you have the `__acl' function. */ +/* Define to 1 if you have the '__acl' function. */ #undef HAVE___ACL -/* Define to 1 if you have the `__facl' function. */ +/* Define to 1 if you have the '__facl' function. */ #undef HAVE___FACL -/* Define to 1 if you have the `__va_copy' function. */ +/* Define to 1 if you have the '__va_copy' function. */ #undef HAVE___VA_COPY /* Define as const if the declaration of iconv() needs const. */ @@ -661,12 +711,6 @@ /* Define to 1 if makedev() takes 3 args */ #undef MAKEDEV_TAKES_3_ARGS -/* Define to 1 if mknod() can create FIFOs. */ -#undef MKNOD_CREATES_FIFOS - -/* Define to 1 if mknod() can create sockets. */ -#undef MKNOD_CREATES_SOCKETS - /* unprivileged group for unprivileged user */ #undef NOBODY_GROUP @@ -727,43 +771,43 @@ /* Define to 1 if "signed char" is a valid type */ #undef SIGNED_CHAR_OK -/* The size of `char*', as computed by sizeof. */ +/* The size of 'char*', as computed by sizeof. */ #undef SIZEOF_CHARP -/* The size of `int', as computed by sizeof. */ +/* The size of 'int', as computed by sizeof. */ #undef SIZEOF_INT -/* The size of `int16_t', as computed by sizeof. */ +/* The size of 'int16_t', as computed by sizeof. */ #undef SIZEOF_INT16_T -/* The size of `int32_t', as computed by sizeof. */ +/* The size of 'int32_t', as computed by sizeof. */ #undef SIZEOF_INT32_T -/* The size of `int64_t', as computed by sizeof. */ +/* The size of 'int64_t', as computed by sizeof. */ #undef SIZEOF_INT64_T -/* The size of `long', as computed by sizeof. */ +/* The size of 'long', as computed by sizeof. */ #undef SIZEOF_LONG -/* The size of `long long', as computed by sizeof. */ +/* The size of 'long long', as computed by sizeof. */ #undef SIZEOF_LONG_LONG -/* The size of `off64_t', as computed by sizeof. */ +/* The size of 'off64_t', as computed by sizeof. */ #undef SIZEOF_OFF64_T -/* The size of `off_t', as computed by sizeof. */ +/* The size of 'off_t', as computed by sizeof. */ #undef SIZEOF_OFF_T -/* The size of `short', as computed by sizeof. */ +/* The size of 'short', as computed by sizeof. */ #undef SIZEOF_SHORT -/* The size of `time_t', as computed by sizeof. */ +/* The size of 'time_t', as computed by sizeof. */ #undef SIZEOF_TIME_T -/* The size of `uint16_t', as computed by sizeof. */ +/* The size of 'uint16_t', as computed by sizeof. */ #undef SIZEOF_UINT16_T -/* The size of `uint32_t', as computed by sizeof. */ +/* The size of 'uint32_t', as computed by sizeof. */ #undef SIZEOF_UINT32_T /* If using the C implementation of alloca, define if you know the @@ -774,7 +818,7 @@ STACK_DIRECTION = 0 => direction of growth unknown */ #undef STACK_DIRECTION -/* Define to 1 if all of the C90 standard headers exist (not just the ones +/* Define to 1 if all of the C89 standard headers exist (not just the ones required in a freestanding environment). This macro is provided for backward compatibility; new code need not use it. */ #undef STDC_HEADERS @@ -782,6 +826,9 @@ /* Define to 1 to add support for ACLs */ #undef SUPPORT_ACLS +/* Define to 1 to do POSIX ACL ops via fd/at xattr syscalls (lib/acl.c) */ +#undef SUPPORT_ACL_FD + /* Undefine if you do not want LZ4 compression. By default this is defined. */ #undef SUPPORT_LZ4 @@ -834,23 +881,29 @@ /* Define _GNU_SOURCE so that we get all necessary prototypes */ #undef _GNU_SOURCE -/* Define for large files, on AIX-style hosts. */ +/* Define to 1 on platforms where this makes off_t a 64-bit type. */ #undef _LARGE_FILES -/* Define to `int' if doesn't define. */ +/* Number of bits in time_t, on hosts where this is settable. */ +#undef _TIME_BITS + +/* Define to 1 on platforms where this makes time_t a 64-bit type. */ +#undef __MINGW_USE_VC2005_COMPAT + +/* Define as 'int' if doesn't define. */ #undef gid_t -/* Define to `__inline__' or `__inline' if that's what the C compiler +/* Define to '__inline__' or '__inline' if that's what the C compiler calls it, or to nothing if 'inline' is not supported under any name. */ #ifndef __cplusplus #undef inline #endif -/* Define to `unsigned int' if does not define. */ +/* Define as 'unsigned int' if doesn't define. */ #undef size_t /* type to use in place of socklen_t if not defined */ #undef socklen_t -/* Define to `int' if doesn't define. */ +/* Define as 'int' if doesn't define. */ #undef uid_t diff -Nru rsync-3.4.1+ds1/configure.ac rsync-3.5.0+ds1/configure.ac --- rsync-3.4.1+ds1/configure.ac 2024-11-20 05:40:14.000000000 +0000 +++ rsync-3.5.0+ds1/configure.ac 2026-08-01 10:02:02.000000000 +0000 @@ -5,7 +5,7 @@ AC_C_BIGENDIAN AC_HEADER_DIRENT AC_HEADER_SYS_WAIT -AC_CHECK_HEADERS(sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h \ +AC_CHECK_HEADERS(poll.h sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h \ unistd.h utime.h compat.h sys/param.h ctype.h sys/wait.h sys/stat.h \ sys/ioctl.h sys/filio.h string.h stdlib.h sys/socket.h sys/mode.h grp.h \ sys/un.h sys/attr.h arpa/inet.h arpa/nameser.h locale.h sys/types.h \ @@ -13,7 +13,7 @@ sys/acl.h acl/libacl.h attr/xattr.h sys/xattr.h sys/extattr.h dl.h \ popt.h popt/popt.h linux/falloc.h netinet/in_systm.h netgroup.h \ zlib.h xxhash.h openssl/md4.h openssl/md5.h zstd.h lz4.h sys/file.h \ - bsd/string.h) + sys/resource.h bsd/string.h) AC_CHECK_HEADERS([netinet/ip.h], [], [], [[#include ]]) AC_HEADER_MAJOR_FIXED @@ -82,6 +82,28 @@ CFLAGS="$CFLAGS -pg" fi +dnl Coverage build (gcov) for `make coverage`. NOTE: --enable-profile above is +dnl gprof (-pg) and is NOT coverage. -O0 keeps branch coverage meaningful; +dnl -fprofile-update=atomic keeps the shared .gcda counters correct while the +dnl suite runs many rsync processes in parallel. +AC_ARG_ENABLE(coverage, + AS_HELP_STRING([--enable-coverage],[build with gcov instrumentation for `make coverage`])) +if test x"$enable_coverage" = x"yes"; then + CFLAGS="$CFLAGS --coverage -fprofile-update=atomic -O0" + CXXFLAGS="$CXXFLAGS --coverage -fprofile-update=atomic -O0" + LDFLAGS="$LDFLAGS --coverage" + AC_DEFINE([GCOV_COVERAGE], 1, + [Flush gcov counters at exit_cleanup: rsync's children exit via _exit(), which bypasses the gcov atexit handler, so without this no .gcda is written for the receiver/generator/daemon-worker processes.]) +fi + +dnl openat2(RESOLVE_BENEATH) is used on Linux 5.6+ for the secure resolver. +dnl --disable-openat2 forces the portable per-component O_NOFOLLOW fallback to +dnl run as the primary resolver on ordinary Linux, so that tier is exercised +dnl (and coverage-counted) without needing a pre-5.6 kernel. Behaviour-neutral +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_MSG_CHECKING([if md2man can create manpages]) if test x"$ac_cv_path_PYTHON3" = x; then AC_MSG_RESULT(no - python3 not found) @@ -331,6 +353,28 @@ CFLAGS="$OLD_CFLAGS" AC_SUBST(NOEXECSTACK) +dnl We need both the SYS_openat2 syscall number and (for +dnl struct open_how / RESOLVE_BENEATH); some setups have one without the other. +AC_CACHE_CHECK([for openat2],rsync_cv_HAVE_OPENAT2,[ + AC_COMPILE_IFELSE([ + AC_LANG_PROGRAM([[ +#include +#include +]], [[ +struct open_how how; +how.resolve = RESOLVE_BENEATH; +return SYS_openat2 + (int)how.resolve; +]]) + ], + [rsync_cv_HAVE_OPENAT2=yes], [rsync_cv_HAVE_OPENAT2=no]) +]) +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.]) + fi +fi + # arrgh. libc in some old debian version screwed up the largefile # stuff, getting byte range locking wrong AC_CACHE_CHECK([for broken largefile support],rsync_cv_HAVE_BROKEN_LARGEFILE,[ @@ -388,21 +432,17 @@ ;; esac ], - AC_RUN_IFELSE([AC_LANG_SOURCE([[ /* AF_INET6 availability check */ -#include + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ #include #include -int main() -{ - if (socket(AF_INET6, SOCK_STREAM, 0) < 0) - exit(1); - else - exit(0); -} +#include +]], [[ +struct sockaddr_in6 sa6; +(void)sa6; +(void)AF_INET6; ]])], [AC_MSG_RESULT(yes) - AC_DEFINE(INET6, 1, true if you have IPv6)], - [AC_MSG_RESULT(no)], + AC_DEFINE(INET6, 1, [true if you have IPv6])], [AC_MSG_RESULT(no)] )) @@ -869,6 +909,19 @@ # if we can't find strcasecmp, look in -lresolv (for Unixware at least) # +dnl rsync's I/O readiness loops use poll() rather than select() so that a +dnl file descriptor at or above FD_SETSIZE cannot overflow an fd_set (which +dnl is undefined behaviour and could hang the transfer). poll() is in +dnl POSIX.1-2001; fail early and clearly if this target lacks it. +dnl +dnl io.c and socket.c include unconditionally, so the HEADER has to +dnl be required too: a system that exposes poll() through some other header +dnl would otherwise pass configure and then fail to compile. +AC_CHECK_FUNCS([poll], , [AC_MSG_ERROR([rsync requires poll(); please report the platform to the rsync developers])]) +if test x"$ac_cv_header_poll_h" != x"yes"; then + AC_MSG_ERROR([rsync requires ; please report the platform to the rsync developers]) +fi + AC_CHECK_FUNCS(strcasecmp) if test x"$ac_cv_func_strcasecmp" = x"no"; then AC_CHECK_LIB(resolv, strcasecmp) @@ -886,17 +939,30 @@ AC_FUNC_UTIME_NULL AC_FUNC_ALLOCA -AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo \ +AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo fdopendir \ + getrlimit setrlimit \ fchmod fstat ftruncate strchr readlink link utime utimes lutimes strftime \ - chflags getattrlist mktime innetgr linkat \ + chflags getattrlist mktime innetgr linkat mknodat mkfifoat \ memmove lchown vsnprintf snprintf vasprintf asprintf setsid strpbrk \ strlcat strlcpy stpcpy strtol mallinfo mallinfo2 getgroups setgroups geteuid getegid \ setlocale setmode open64 lseek64 mkstemp64 mtrace va_copy __va_copy \ seteuid strerror putenv iconv_open locale_charset nl_langinfo getxattr \ extattr_get_link sigaction sigprocmask setattrlist getgrouplist \ - initgroups utimensat posix_fallocate attropen setvbuf nanosleep usleep \ + initgroups utimensat futimens posix_fallocate attropen setvbuf nanosleep usleep \ setenv unsetenv) +dnl dirfd() is a macro or static inline on several systems (the BSDs), so the +dnl default AC_CHECK_FUNCS link probe -- which declares `char dirfd(void);` and +dnl links against a bare symbol -- gives a false negative there. Probe it with a +dnl real compile+link that includes and actually calls dirfd(). +AC_CACHE_CHECK([for dirfd], rsync_cv_HAVE_DIRFD, + [AC_LINK_IFELSE([AC_LANG_PROGRAM([[#include ]], + [[DIR *d = opendir("."); return d ? dirfd(d) < -1 : 0;]])], + [rsync_cv_HAVE_DIRFD=yes], [rsync_cv_HAVE_DIRFD=no])]) +if test x"$rsync_cv_HAVE_DIRFD" = x"yes"; then + AC_DEFINE([HAVE_DIRFD], 1, [Define to 1 if you have a working dirfd() (function or macro).]) +fi + dnl cygwin iconv.h defines iconv_open as libiconv_open if test x"$ac_cv_func_iconv_open" != x"yes"; then AC_CHECK_FUNC(libiconv_open, [ac_cv_func_iconv_open=yes; AC_DEFINE(HAVE_ICONV_OPEN, 1)]) @@ -1218,37 +1284,14 @@ fi -AC_CACHE_CHECK([if mknod creates FIFOs],rsync_cv_MKNOD_CREATES_FIFOS,[ -AC_RUN_IFELSE([AC_LANG_SOURCE([[ -#include -#include -#include -#if HAVE_UNISTD_H -# include -#endif -int main(void) { int rc, ec; char *fn = "fifo-test"; -unlink(fn); rc = mknod(fn,S_IFIFO,0600); ec = errno; unlink(fn); -if (rc) {printf("(%d %d) ",rc,ec); return ec;} -return 0;}]])],[rsync_cv_MKNOD_CREATES_FIFOS=yes],[rsync_cv_MKNOD_CREATES_FIFOS=no],[rsync_cv_MKNOD_CREATES_FIFOS=cross])]) -if test x"$rsync_cv_MKNOD_CREATES_FIFOS" = x"yes"; then - AC_DEFINE(MKNOD_CREATES_FIFOS, 1, [Define to 1 if mknod() can create FIFOs.]) -fi - -AC_CACHE_CHECK([if mknod creates sockets],rsync_cv_MKNOD_CREATES_SOCKETS,[ -AC_RUN_IFELSE([AC_LANG_SOURCE([[ -#include -#include -#include -#if HAVE_UNISTD_H -# include -#endif -int main(void) { int rc, ec; char *fn = "sock-test"; -unlink(fn); rc = mknod(fn,S_IFSOCK,0600); ec = errno; unlink(fn); -if (rc) {printf("(%d %d) ",rc,ec); return ec;} -return 0;}]])],[rsync_cv_MKNOD_CREATES_SOCKETS=yes],[rsync_cv_MKNOD_CREATES_SOCKETS=no],[rsync_cv_MKNOD_CREATES_SOCKETS=cross])]) -if test x"$rsync_cv_MKNOD_CREATES_SOCKETS" = x"yes"; then - AC_DEFINE(MKNOD_CREATES_SOCKETS, 1, [Define to 1 if mknod() can create sockets.]) -fi +# Whether mknod()/mknodat() can create a FIFO or a unix-domain socket is a +# property of the target filesystem, not a build-time constant -- e.g. mknod +# makes sockets on Linux but not the BSDs/macOS/Solaris, and a single transfer +# can write to filesystems with different capabilities. So rsync no longer +# probes this at configure time (a run-test that also misfired when cross- +# compiling); do_mknod*() just try mknod[at]() and, on failure, fall back to +# mkfifo[at]()/socket+bind() per call. We only need the libc symbols, checked +# above via AC_CHECK_FUNCS (mknod mknodat mkfifo mkfifoat) -- all link tests. # # The following test was mostly taken from the tcl/tk plus patches @@ -1392,7 +1435,7 @@ AC_DEFINE(HAVE_LINUX_XATTRS, 1, [True if you have Linux xattrs (or equivalent)]) AC_DEFINE(SUPPORT_XATTRS, 1) AC_DEFINE(NO_SYMLINK_USER_XATTRS, 1, [True if symlinks do not support user xattrs]) - AC_CHECK_LIB(attr,getxattr) + AC_SEARCH_LIBS(getxattr,attr) ;; darwin*) AC_MSG_RESULT(Using OS X xattrs) @@ -1420,6 +1463,66 @@ fi ;; esac +fi + +################################################# +# On Linux, POSIX ACLs are stored as the "system.posix_acl_{access,default}" +# xattrs, so we can get/set them through a held O_NOFOLLOW fd (fsetxattr) or a +# dirfd+leaf (setxattrat, AT_SYMLINK_NOFOLLOW) instead of the path-based libacl +# acl_*_file() calls -- making the operation safe against a parent-symlink race. +# This needs POSIX ACLs and the f/at xattr syscalls, which on Linux are +# available whenever (or ) is -- independent of the +# -X feature (--disable-xattr-support), so we gate on the header, not +# enable_xattr_support. +AH_TEMPLATE([SUPPORT_ACL_FD], +[Define to 1 to do POSIX ACL ops via fd/at xattr syscalls (lib/acl.c)]) +AH_TEMPLATE([HAVE_XATTRAT_SYSCALLS], +[Define to 1 if the setxattrat/getxattrat/removexattrat syscalls are available]) +if test x"$samba_cv_HAVE_POSIX_ACLS" = x"yes" \ + && { test x"$ac_cv_header_sys_xattr_h" = x"yes" || test x"$ac_cv_header_attr_xattr_h" = x"yes"; }; then + case "$host_os" in + *linux*) + AC_DEFINE(SUPPORT_ACL_FD, 1) + AC_CACHE_CHECK([for SYS_setxattrat],rsync_cv_have_sys_setxattrat,[ + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include +#include +#ifdef HAVE_UNISTD_H +#include +#endif +struct xattr_args { uint64_t value; uint32_t size; uint32_t flags; };]], + [[struct xattr_args a; a.value = 0; a.size = 0; a.flags = 0; + syscall(SYS_setxattrat, 0, ".", 0, "n", &a, sizeof a); + syscall(SYS_getxattrat, 0, ".", 0, "n", &a, sizeof a); + syscall(SYS_removexattrat, 0, ".", 0, "n");]])],[rsync_cv_have_sys_setxattrat=yes],[rsync_cv_have_sys_setxattrat=no])]) + if test x"$rsync_cv_have_sys_setxattrat" = x"yes"; then + AC_DEFINE(HAVE_XATTRAT_SYSCALLS, 1) + fi + ;; + esac +fi + +################################################# +# Detect a patched libacl providing the race-safe +# *_at ACL entry points (acl_get_file_at/acl_set_file_at/acl_delete_def_file_at, +# ACL_1.3, unreleased upstream). When present we route the race-safe ACL get/ +# set/delete through them on Linux -- race-safe on every kernel (6.13+ uses +# *xattrat; older uses libacl's /proc/self/fd compat). A stock -lacl lacks these +# symbols, so this stays undefined and the build falls back to lib/acl.c; +# detection must therefore run against the patched lib (CPPFLAGS/LDFLAGS). +AH_TEMPLATE([HAVE_LIBACL_AT], +[Define to 1 if libacl provides acl_get_file_at/acl_set_file_at/acl_delete_def_file_at]) +if test x"$samba_cv_HAVE_POSIX_ACLS" = x"yes"; then + case "$host_os" in + *linux*) + AC_CHECK_LIB(acl, acl_get_file_at, [rsync_have_libacl_at=yes], [rsync_have_libacl_at=no]) + if test x"$rsync_have_libacl_at" = x"yes"; then + AC_CHECK_FUNCS([acl_set_file_at acl_delete_def_file_at], [], [rsync_have_libacl_at=no]) + fi + if test x"$rsync_have_libacl_at" = x"yes"; then + AC_DEFINE(HAVE_LIBACL_AT, 1) + fi + ;; + esac fi if test x"$enable_acl_support" = x"no" || test x"$enable_xattr_support" = x"no" || test x"$enable_iconv" = x"no"; then diff -Nru rsync-3.4.1+ds1/configure.sh rsync-3.5.0+ds1/configure.sh --- rsync-3.4.1+ds1/configure.sh 2025-01-15 20:49:24.000000000 +0000 +++ rsync-3.5.0+ds1/configure.sh 2026-08-13 00:05:08.000000000 +0000 @@ -1,11 +1,11 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.71 for rsync. +# Generated by GNU Autoconf 2.72 for rsync. # # Report bugs to . # # -# Copyright (C) 1992-1996, 1998-2017, 2020-2021 Free Software Foundation, +# Copyright (C) 1992-1996, 1998-2017, 2020-2023 Free Software Foundation, # Inc. # # @@ -17,7 +17,6 @@ # Be more Bourne compatible DUALCASE=1; export DUALCASE # for MKS sh -as_nop=: if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1 then : emulate sh @@ -26,12 +25,13 @@ # is contrary to our usage. Disable this feature. alias -g '${1+"$@"}'='"$@"' setopt NO_GLOB_SUBST -else $as_nop - case `(set -o) 2>/dev/null` in #( +else case e in #( + e) case `(set -o) 2>/dev/null` in #( *posix*) : set -o posix ;; #( *) : ;; +esac ;; esac fi @@ -103,7 +103,7 @@ ;; esac -# We did not find ourselves, most probably we were run as `sh COMMAND' +# We did not find ourselves, most probably we were run as 'sh COMMAND' # in which case we are not to be found in the path. if test "x$as_myself" = x; then as_myself=$0 @@ -133,15 +133,14 @@ esac exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} # Admittedly, this is quite paranoid, since all the known shells bail -# out after a failed `exec'. +# out after a failed 'exec'. printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2 exit 255 fi # We don't want this to propagate to other subprocesses. { _as_can_reexec=; unset _as_can_reexec;} if test "x$CONFIG_SHELL" = x; then - as_bourne_compatible="as_nop=: -if test \${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1 + as_bourne_compatible="if test \${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1 then : emulate sh NULLCMD=: @@ -149,12 +148,13 @@ # is contrary to our usage. Disable this feature. alias -g '\${1+\"\$@\"}'='\"\$@\"' setopt NO_GLOB_SUBST -else \$as_nop - case \`(set -o) 2>/dev/null\` in #( +else case e in #( + e) case \`(set -o) 2>/dev/null\` in #( *posix*) : set -o posix ;; #( *) : ;; +esac ;; esac fi " @@ -172,8 +172,9 @@ if ( set x; as_fn_ret_success y && test x = \"\$1\" ) then : -else \$as_nop - exitcode=1; echo positional parameters were not saved. +else case e in #( + e) exitcode=1; echo positional parameters were not saved. ;; +esac fi test x\$exitcode = x0 || exit 1 blah=\$(echo \$(echo blah)) @@ -187,14 +188,15 @@ if (eval "$as_required") 2>/dev/null then : as_have_required=yes -else $as_nop - as_have_required=no +else case e in #( + e) as_have_required=no ;; +esac fi if test x$as_have_required = xyes && (eval "$as_suggested") 2>/dev/null then : -else $as_nop - as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +else case e in #( + e) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR as_found=false for as_dir in /bin$PATH_SEPARATOR/usr/bin$PATH_SEPARATOR$PATH do @@ -227,12 +229,13 @@ if $as_found then : -else $as_nop - if { test -f "$SHELL" || test -f "$SHELL.exe"; } && +else case e in #( + e) if { test -f "$SHELL" || test -f "$SHELL.exe"; } && as_run=a "$SHELL" -c "$as_bourne_compatible""$as_required" 2>/dev/null then : CONFIG_SHELL=$SHELL as_have_required=yes -fi +fi ;; +esac fi @@ -254,7 +257,7 @@ esac exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} # Admittedly, this is quite paranoid, since all the known shells bail -# out after a failed `exec'. +# out after a failed 'exec'. printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2 exit 255 fi @@ -274,7 +277,8 @@ $0: the script under such a shell if you do have one." fi exit 1 -fi +fi ;; +esac fi fi SHELL=${CONFIG_SHELL-/bin/sh} @@ -313,14 +317,6 @@ as_fn_set_status $1 exit $1 } # as_fn_exit -# as_fn_nop -# --------- -# Do nothing but, unlike ":", preserve the value of $?. -as_fn_nop () -{ - return $? -} -as_nop=as_fn_nop # as_fn_mkdir_p # ------------- @@ -389,11 +385,12 @@ { eval $1+=\$2 }' -else $as_nop - as_fn_append () +else case e in #( + e) as_fn_append () { eval $1=\$$1\$2 - } + } ;; +esac fi # as_fn_append # as_fn_arith ARG... @@ -407,21 +404,14 @@ { as_val=$(( $* )) }' -else $as_nop - as_fn_arith () +else case e in #( + e) as_fn_arith () { as_val=`expr "$@" || test $? -eq 1` - } + } ;; +esac fi # as_fn_arith -# as_fn_nop -# --------- -# Do nothing but, unlike ":", preserve the value of $?. -as_fn_nop () -{ - return $? -} -as_nop=as_fn_nop # as_fn_error STATUS ERROR [LINENO LOG_FD] # ---------------------------------------- @@ -495,6 +485,8 @@ /[$]LINENO/= ' <$as_myself | sed ' + t clear + :clear s/[$]LINENO.*/&-/ t lineno b @@ -543,7 +535,6 @@ as_echo='printf %s\n' as_echo_n='printf %s' - rm -f conf$$ conf$$.exe conf$$.file if test -d conf$$.dir; then rm -f conf$$.dir/conf$$.file @@ -555,9 +546,9 @@ if ln -s conf$$.file conf$$ 2>/dev/null; then as_ln_s='ln -s' # ... but there are two gotchas: - # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. - # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. - # In both cases, we have to default to `cp -pR'. + # 1) On MSYS, both 'ln -s file dir' and 'ln file dir' fail. + # 2) DJGPP < 2.04 has no symlinks; 'ln -s' creates a wrapper executable. + # In both cases, we have to default to 'cp -pR'. ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || as_ln_s='cp -pR' elif ln conf$$.file conf$$ 2>/dev/null; then @@ -582,10 +573,12 @@ as_executable_p=as_fn_executable_p # Sed expression to map a string onto a valid CPP name. -as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" +as_sed_cpp="y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g" +as_tr_cpp="eval sed '$as_sed_cpp'" # deprecated # Sed expression to map a string onto a valid variable name. -as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" +as_sed_sh="y%*+%pp%;s%[^_$as_cr_alnum]%_%g" +as_tr_sh="eval sed '$as_sed_sh'" # deprecated test -n "$DJDIR" || exec 7<&0 /dev/null && - as_fn_error $? "invalid feature name: \`$ac_useropt'" + as_fn_error $? "invalid feature name: '$ac_useropt'" ac_useropt_orig=$ac_useropt ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in @@ -915,7 +912,7 @@ ac_useropt=`expr "x$ac_option" : 'x-*enable-\([^=]*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error $? "invalid feature name: \`$ac_useropt'" + as_fn_error $? "invalid feature name: '$ac_useropt'" ac_useropt_orig=$ac_useropt ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in @@ -1128,7 +1125,7 @@ ac_useropt=`expr "x$ac_option" : 'x-*with-\([^=]*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error $? "invalid package name: \`$ac_useropt'" + as_fn_error $? "invalid package name: '$ac_useropt'" ac_useropt_orig=$ac_useropt ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in @@ -1144,7 +1141,7 @@ ac_useropt=`expr "x$ac_option" : 'x-*without-\(.*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error $? "invalid package name: \`$ac_useropt'" + as_fn_error $? "invalid package name: '$ac_useropt'" ac_useropt_orig=$ac_useropt ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in @@ -1174,8 +1171,8 @@ | --x-librar=* | --x-libra=* | --x-libr=* | --x-lib=* | --x-li=* | --x-l=*) x_libraries=$ac_optarg ;; - -*) as_fn_error $? "unrecognized option: \`$ac_option' -Try \`$0 --help' for more information" + -*) as_fn_error $? "unrecognized option: '$ac_option' +Try '$0 --help' for more information" ;; *=*) @@ -1183,7 +1180,7 @@ # Reject names that are not valid shell variable names. case $ac_envvar in #( '' | [0-9]* | *[!_$as_cr_alnum]* ) - as_fn_error $? "invalid variable name: \`$ac_envvar'" ;; + as_fn_error $? "invalid variable name: '$ac_envvar'" ;; esac eval $ac_envvar=\$ac_optarg export $ac_envvar ;; @@ -1233,7 +1230,7 @@ as_fn_error $? "expected an absolute directory name for --$ac_var: $ac_val" done -# There might be people who depend on the old broken behavior: `$host' +# There might be people who depend on the old broken behavior: '$host' # used to hold the argument of --host etc. # FIXME: To remove some day. build=$build_alias @@ -1301,7 +1298,7 @@ test "$ac_srcdir_defaulted" = yes && srcdir="$ac_confdir or .." as_fn_error $? "cannot find sources ($ac_unique_file) in $srcdir" fi -ac_msg="sources are in $srcdir, but \`cd $srcdir' does not work" +ac_msg="sources are in $srcdir, but 'cd $srcdir' does not work" ac_abs_confdir=`( cd "$srcdir" && test -r "./$ac_unique_file" || as_fn_error $? "$ac_msg" pwd)` @@ -1329,7 +1326,7 @@ # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures rsync to adapt to many kinds of systems. +'configure' configures rsync to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1343,11 +1340,11 @@ --help=short display options specific to this package --help=recursive display the short help of all the included packages -V, --version display version information and exit - -q, --quiet, --silent do not print \`checking ...' messages + -q, --quiet, --silent do not print 'checking ...' messages --cache-file=FILE cache test results in FILE [disabled] - -C, --config-cache alias for \`--cache-file=config.cache' + -C, --config-cache alias for '--cache-file=config.cache' -n, --no-create do not create output files - --srcdir=DIR find the sources in DIR [configure dir or \`..'] + --srcdir=DIR find the sources in DIR [configure dir or '..'] Installation directories: --prefix=PREFIX install architecture-independent files in PREFIX @@ -1355,10 +1352,10 @@ --exec-prefix=EPREFIX install architecture-dependent files in EPREFIX [PREFIX] -By default, \`make install' will install all the files in -\`$ac_default_prefix/bin', \`$ac_default_prefix/lib' etc. You can specify -an installation prefix other than \`$ac_default_prefix' using \`--prefix', -for instance \`--prefix=\$HOME'. +By default, 'make install' will install all the files in +'$ac_default_prefix/bin', '$ac_default_prefix/lib' etc. You can specify +an installation prefix other than '$ac_default_prefix' using '--prefix', +for instance '--prefix=\$HOME'. For better control, use the options below. @@ -1405,6 +1402,9 @@ --enable-FEATURE[=ARG] include FEATURE [ARG=yes] --disable-debug disable to omit debugging symbols and features --enable-profile enable to turn on CPU profiling + --enable-coverage build with gcov instrumentation for `make coverage` + --disable-openat2 do not use Linux openat2(RESOLVE_BENEATH); force the + portable resolver (for exercising the fallback tier) --disable-md2man disable to omit manpage creation --enable-maintainer-mode enable to turn on extra debug features @@ -1424,6 +1424,7 @@ --disable-iconv disable to omit the --iconv option --disable-acl-support disable to omit ACL support --disable-xattr-support disable to omit extended attributes + --enable-year2038 support timestamps after 2038 Optional Packages: --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] @@ -1455,7 +1456,7 @@ CXX C++ compiler command CXXFLAGS C++ compiler flags -Use these variables to override the choices made by `configure' or to help +Use these variables to override the choices made by 'configure' or to help it to find libraries and programs with nonstandard names/locations. Report bugs to . @@ -1523,9 +1524,9 @@ if $ac_init_version; then cat <<\_ACEOF rsync configure -generated by GNU Autoconf 2.71 +generated by GNU Autoconf 2.72 -Copyright (C) 2021 Free Software Foundation, Inc. +Copyright (C) 2023 Free Software Foundation, Inc. This configure script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it. _ACEOF @@ -1564,17 +1565,66 @@ } && test -s conftest.$ac_objext then : ac_retval=0 -else $as_nop - printf "%s\n" "$as_me: failed program was:" >&5 +else case e in #( + e) printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 - ac_retval=1 + ac_retval=1 ;; +esac fi eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno as_fn_set_status $ac_retval } # ac_fn_c_try_compile +# ac_fn_c_try_link LINENO +# ----------------------- +# Try to link conftest.$ac_ext, and return whether this succeeded. +ac_fn_c_try_link () +{ + as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + rm -f conftest.$ac_objext conftest.beam conftest$ac_exeext + if { { ac_try="$ac_link" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +printf "%s\n" "$ac_try_echo"; } >&5 + (eval "$ac_link") 2>conftest.err + ac_status=$? + if test -s conftest.err; then + grep -v '^ *+' conftest.err >conftest.er1 + cat conftest.er1 >&5 + mv -f conftest.er1 conftest.err + fi + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } && { + test -z "$ac_c_werror_flag" || + test ! -s conftest.err + } && test -s conftest$ac_exeext && { + test "$cross_compiling" = yes || + test -x conftest$ac_exeext + } +then : + ac_retval=0 +else case e in #( + e) printf "%s\n" "$as_me: failed program was:" >&5 +sed 's/^/| /' conftest.$ac_ext >&5 + + ac_retval=1 ;; +esac +fi + # Delete the IPA/IPO (Inter Procedural Analysis/Optimization) information + # created by the PGI compiler (conftest_ipa8_conftest.oo), as it would + # interfere with the next link command; also delete a directory that is + # left behind by Apple's compiler. We do this before executing the actions. + rm -rf conftest.dSYM conftest_ipa8_conftest.oo + eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno + as_fn_set_status $ac_retval + +} # ac_fn_c_try_link + # ac_fn_c_try_run LINENO # ---------------------- # Try to run conftest.$ac_ext, and return whether this succeeded. Assumes that @@ -1605,12 +1655,13 @@ test $ac_status = 0; }; } then : ac_retval=0 -else $as_nop - printf "%s\n" "$as_me: program exited with status $ac_status" >&5 +else case e in #( + e) printf "%s\n" "$as_me: program exited with status $ac_status" >&5 printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 - ac_retval=$ac_status + ac_retval=$ac_status ;; +esac fi rm -rf conftest.dSYM conftest_ipa8_conftest.oo eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno @@ -1630,8 +1681,8 @@ if eval test \${$3+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $4 #include <$2> @@ -1639,10 +1690,12 @@ if ac_fn_c_try_compile "$LINENO" then : eval "$3=yes" -else $as_nop - eval "$3=no" +else case e in #( + e) eval "$3=no" ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi eval ac_res=\$$3 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 @@ -1651,53 +1704,6 @@ } # ac_fn_c_check_header_compile -# ac_fn_c_try_link LINENO -# ----------------------- -# Try to link conftest.$ac_ext, and return whether this succeeded. -ac_fn_c_try_link () -{ - as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - rm -f conftest.$ac_objext conftest.beam conftest$ac_exeext - if { { ac_try="$ac_link" -case "(($ac_try" in - *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; - *) ac_try_echo=$ac_try;; -esac -eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf "%s\n" "$ac_try_echo"; } >&5 - (eval "$ac_link") 2>conftest.err - ac_status=$? - if test -s conftest.err; then - grep -v '^ *+' conftest.err >conftest.er1 - cat conftest.er1 >&5 - mv -f conftest.er1 conftest.err - fi - printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; } && { - test -z "$ac_c_werror_flag" || - test ! -s conftest.err - } && test -s conftest$ac_exeext && { - test "$cross_compiling" = yes || - test -x conftest$ac_exeext - } -then : - ac_retval=0 -else $as_nop - printf "%s\n" "$as_me: failed program was:" >&5 -sed 's/^/| /' conftest.$ac_ext >&5 - - ac_retval=1 -fi - # Delete the IPA/IPO (Inter Procedural Analysis/Optimization) information - # created by the PGI compiler (conftest_ipa8_conftest.oo), as it would - # interfere with the next link command; also delete a directory that is - # left behind by Apple's compiler. We do this before executing the actions. - rm -rf conftest.dSYM conftest_ipa8_conftest.oo - eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno - as_fn_set_status $ac_retval - -} # ac_fn_c_try_link - # ac_fn_c_try_cpp LINENO # ---------------------- # Try to preprocess conftest.$ac_ext, and return whether this succeeded. @@ -1725,11 +1731,12 @@ } then : ac_retval=0 -else $as_nop - printf "%s\n" "$as_me: failed program was:" >&5 +else case e in #( + e) printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 - ac_retval=1 + ac_retval=1 ;; +esac fi eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno as_fn_set_status $ac_retval @@ -1764,11 +1771,12 @@ } && test -s conftest.$ac_objext then : ac_retval=0 -else $as_nop - printf "%s\n" "$as_me: failed program was:" >&5 +else case e in #( + e) printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 - ac_retval=1 + ac_retval=1 ;; +esac fi eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno as_fn_set_status $ac_retval @@ -1805,12 +1813,13 @@ test $ac_status = 0; }; } then : ac_retval=0 -else $as_nop - printf "%s\n" "$as_me: program exited with status $ac_status" >&5 +else case e in #( + e) printf "%s\n" "$as_me: program exited with status $ac_status" >&5 printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 - ac_retval=$ac_status + ac_retval=$ac_status ;; +esac fi rm -rf conftest.dSYM conftest_ipa8_conftest.oo eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno @@ -1863,18 +1872,19 @@ if ac_fn_c_try_compile "$LINENO" then : ac_hi=$ac_mid; break -else $as_nop - as_fn_arith $ac_mid + 1 && ac_lo=$as_val +else case e in #( + e) as_fn_arith $ac_mid + 1 && ac_lo=$as_val if test $ac_lo -le $ac_mid; then ac_lo= ac_hi= break fi - as_fn_arith 2 '*' $ac_mid + 1 && ac_mid=$as_val + as_fn_arith 2 '*' $ac_mid + 1 && ac_mid=$as_val ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext done -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $4 int @@ -1909,20 +1919,23 @@ if ac_fn_c_try_compile "$LINENO" then : ac_lo=$ac_mid; break -else $as_nop - as_fn_arith '(' $ac_mid ')' - 1 && ac_hi=$as_val +else case e in #( + e) as_fn_arith '(' $ac_mid ')' - 1 && ac_hi=$as_val if test $ac_mid -le $ac_hi; then ac_lo= ac_hi= break fi - as_fn_arith 2 '*' $ac_mid && ac_mid=$as_val + as_fn_arith 2 '*' $ac_mid && ac_mid=$as_val ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext done -else $as_nop - ac_lo= ac_hi= +else case e in #( + e) ac_lo= ac_hi= ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext # Binary search between lo and hi bounds. @@ -1945,8 +1958,9 @@ if ac_fn_c_try_compile "$LINENO" then : ac_hi=$ac_mid -else $as_nop - as_fn_arith '(' $ac_mid ')' + 1 && ac_lo=$as_val +else case e in #( + e) as_fn_arith '(' $ac_mid ')' + 1 && ac_lo=$as_val ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext done @@ -1994,8 +2008,9 @@ if ac_fn_c_try_run "$LINENO" then : echo >>conftest.val; read $3 &6 -else $as_nop - eval "$3=no" +else case e in #( + e) eval "$3=no" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $4 @@ -2050,12 +2065,14 @@ if ac_fn_c_try_compile "$LINENO" then : -else $as_nop - eval "$3=yes" +else case e in #( + e) eval "$3=yes" ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi eval ac_res=\$$3 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 @@ -2076,8 +2093,8 @@ if eval test \${$4+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $5 int @@ -2093,8 +2110,8 @@ if ac_fn_c_try_compile "$LINENO" then : eval "$4=yes" -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $5 int @@ -2110,12 +2127,15 @@ if ac_fn_c_try_compile "$LINENO" then : eval "$4=yes" -else $as_nop - eval "$4=no" +else case e in #( + e) eval "$4=no" ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi eval ac_res=\$$4 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 @@ -2135,15 +2155,15 @@ if eval test \${$3+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Define $2 to an innocuous variant, in case declares $2. For example, HP-UX 11i declares gettimeofday. */ #define $2 innocuous_$2 /* System header to define __stub macros and hopefully few prototypes, - which can conflict with char $2 (); below. */ + which can conflict with char $2 (void); below. */ #include #undef $2 @@ -2154,7 +2174,7 @@ #ifdef __cplusplus extern "C" #endif -char $2 (); +char $2 (void); /* The GNU C library defines this for functions which it implements to always fail with ENOSYS. Some functions are actually named something starting with __ and the normal name is an alias. */ @@ -2173,11 +2193,13 @@ if ac_fn_c_try_link "$LINENO" then : eval "$3=yes" -else $as_nop - eval "$3=no" +else case e in #( + e) eval "$3=no" ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ - conftest$ac_exeext conftest.$ac_ext + conftest$ac_exeext conftest.$ac_ext ;; +esac fi eval ac_res=\$$3 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 @@ -2210,7 +2232,7 @@ running configure, to aid debugging if configure makes a mistake. It was created by rsync $as_me, which was -generated by GNU Autoconf 2.71. Invocation command line was +generated by GNU Autoconf 2.72. Invocation command line was $ $0$ac_configure_args_raw @@ -2456,10 +2478,10 @@ printf "%s\n" "$as_me: loading site script $ac_site_file" >&6;} sed 's/^/| /' "$ac_site_file" >&5 . "$ac_site_file" \ - || { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} + || { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "failed to load site script $ac_site_file -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } fi done @@ -2495,9 +2517,7 @@ /* Most of the following tests are stolen from RCS 5.7 src/conf.sh. */ struct buf { int x; }; struct buf * (*rcsopen) (struct buf *, struct stat *, int); -static char *e (p, i) - char **p; - int i; +static char *e (char **p, int i) { return p[i]; } @@ -2511,6 +2531,21 @@ return s; } +/* C89 style stringification. */ +#define noexpand_stringify(a) #a +const char *stringified = noexpand_stringify(arbitrary+token=sequence); + +/* C89 style token pasting. Exercises some of the corner cases that + e.g. old MSVC gets wrong, but not very hard. */ +#define noexpand_concat(a,b) a##b +#define expand_concat(a,b) noexpand_concat(a,b) +extern int vA; +extern int vbee; +#define aye A +#define bee B +int *pvA = &expand_concat(v,aye); +int *pvbee = &noexpand_concat(v,bee); + /* OSF 4.0 Compaq cc is some sort of almost-ANSI by default. It has function prototypes and stuff, but not \xHH hex character constants. These do not provoke an error unfortunately, instead are silently treated @@ -2538,16 +2573,19 @@ # Test code for whether the C compiler supports C99 (global declarations) ac_c_conftest_c99_globals=' -// Does the compiler advertise C99 conformance? +/* Does the compiler advertise C99 conformance? */ #if !defined __STDC_VERSION__ || __STDC_VERSION__ < 199901L # error "Compiler does not advertise C99 conformance" #endif +// See if C++-style comments work. + #include extern int puts (const char *); extern int printf (const char *, ...); extern int dprintf (int, const char *, ...); extern void *malloc (size_t); +extern void free (void *); // Check varargs macros. These examples are taken from C99 6.10.3.5. // dprintf is used instead of fprintf to avoid needing to declare @@ -2597,7 +2635,6 @@ static inline int test_restrict (ccp restrict text) { - // See if C++-style comments work. // Iterate through items via the restricted pointer. // Also check for declarations in for loops. for (unsigned int i = 0; *(text+i) != '\''\0'\''; ++i) @@ -2663,6 +2700,8 @@ ia->datasize = 10; for (int i = 0; i < ia->datasize; ++i) ia->data[i] = i * 1.234; + // Work around memory leak warnings. + free (ia); // Check named initializers. struct named_init ni = { @@ -2684,7 +2723,7 @@ # Test code for whether the C compiler supports C11 (global declarations) ac_c_conftest_c11_globals=' -// Does the compiler advertise C11 conformance? +/* Does the compiler advertise C11 conformance? */ #if !defined __STDC_VERSION__ || __STDC_VERSION__ < 201112L # error "Compiler does not advertise C11 conformance" #endif @@ -3093,8 +3132,9 @@ if $as_found then : -else $as_nop - as_fn_error $? "cannot find required auxiliary files:$ac_missing_aux_files" "$LINENO" 5 +else case e in #( + e) as_fn_error $? "cannot find required auxiliary files:$ac_missing_aux_files" "$LINENO" 5 ;; +esac fi @@ -3122,12 +3162,12 @@ eval ac_new_val=\$ac_env_${ac_var}_value case $ac_old_set,$ac_new_set in set,) - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&5 -printf "%s\n" "$as_me: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: '$ac_var' was set to '$ac_old_val' in the previous run" >&5 +printf "%s\n" "$as_me: error: '$ac_var' was set to '$ac_old_val' in the previous run" >&2;} ac_cache_corrupted=: ;; ,set) - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was not set in the previous run" >&5 -printf "%s\n" "$as_me: error: \`$ac_var' was not set in the previous run" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: '$ac_var' was not set in the previous run" >&5 +printf "%s\n" "$as_me: error: '$ac_var' was not set in the previous run" >&2;} ac_cache_corrupted=: ;; ,);; *) @@ -3136,18 +3176,18 @@ ac_old_val_w=`echo x $ac_old_val` ac_new_val_w=`echo x $ac_new_val` if test "$ac_old_val_w" != "$ac_new_val_w"; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' has changed since the previous run:" >&5 -printf "%s\n" "$as_me: error: \`$ac_var' has changed since the previous run:" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: '$ac_var' has changed since the previous run:" >&5 +printf "%s\n" "$as_me: error: '$ac_var' has changed since the previous run:" >&2;} ac_cache_corrupted=: else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&5 -printf "%s\n" "$as_me: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in '$ac_var' since the previous run:" >&5 +printf "%s\n" "$as_me: warning: ignoring whitespace changes in '$ac_var' since the previous run:" >&2;} eval $ac_var=\$ac_old_val fi - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: former value: \`$ac_old_val'" >&5 -printf "%s\n" "$as_me: former value: \`$ac_old_val'" >&2;} - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: current value: \`$ac_new_val'" >&5 -printf "%s\n" "$as_me: current value: \`$ac_new_val'" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: former value: '$ac_old_val'" >&5 +printf "%s\n" "$as_me: former value: '$ac_old_val'" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: current value: '$ac_new_val'" >&5 +printf "%s\n" "$as_me: current value: '$ac_new_val'" >&2;} fi;; esac # Pass precious variables to config.status. @@ -3163,11 +3203,11 @@ fi done if $ac_cache_corrupted; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 printf "%s\n" "$as_me: error: changes in the environment can compromise the build" >&2;} - as_fn_error $? "run \`${MAKE-make} distclean' and/or \`rm $cache_file' + as_fn_error $? "run '${MAKE-make} distclean' and/or 'rm $cache_file' and start over" "$LINENO" 5 fi ## -------------------- ## @@ -3204,8 +3244,8 @@ if test ${ac_cv_prog_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$CC"; then +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -3227,7 +3267,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then @@ -3249,8 +3290,8 @@ if test ${ac_cv_prog_ac_ct_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$ac_ct_CC"; then +else case e in #( + e) if test -n "$ac_ct_CC"; then ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -3272,7 +3313,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then @@ -3307,8 +3349,8 @@ if test ${ac_cv_prog_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$CC"; then +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -3330,7 +3372,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then @@ -3352,8 +3395,8 @@ if test ${ac_cv_prog_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$CC"; then +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else ac_prog_rejected=no @@ -3392,7 +3435,8 @@ ac_cv_prog_CC="$as_dir$ac_word${1+' '}$@" fi fi -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then @@ -3416,8 +3460,8 @@ if test ${ac_cv_prog_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$CC"; then +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -3439,7 +3483,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then @@ -3465,8 +3510,8 @@ if test ${ac_cv_prog_ac_ct_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$ac_ct_CC"; then +else case e in #( + e) if test -n "$ac_ct_CC"; then ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -3488,7 +3533,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then @@ -3526,8 +3572,8 @@ if test ${ac_cv_prog_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$CC"; then +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -3549,7 +3595,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then @@ -3571,8 +3618,8 @@ if test ${ac_cv_prog_ac_ct_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$ac_ct_CC"; then +else case e in #( + e) if test -n "$ac_ct_CC"; then ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -3594,7 +3641,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then @@ -3623,10 +3671,10 @@ fi -test -z "$CC" && { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +test -z "$CC" && { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "no acceptable C compiler found in \$PATH -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } # Provide some information about the compiler. printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5 @@ -3698,8 +3746,8 @@ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } then : - # Autoconf-2.13 could set the ac_cv_exeext variable to `no'. -# So ignore a value of `no', otherwise this would lead to `EXEEXT = no' + # Autoconf-2.13 could set the ac_cv_exeext variable to 'no'. +# So ignore a value of 'no', otherwise this would lead to 'EXEEXT = no' # in a Makefile. We should not override ac_cv_exeext if it was cached, # so that the user can short-circuit this test for compilers unknown to # Autoconf. @@ -3719,7 +3767,7 @@ ac_cv_exeext=`expr "$ac_file" : '[^.]*\(\..*\)'` fi # We set ac_cv_exeext here because the later test for it is not - # safe: cross compilers may not add the suffix if given an `-o' + # safe: cross compilers may not add the suffix if given an '-o' # argument, so we may need to know it at that point already. # Even if this section looks crufty: it has the advantage of # actually working. @@ -3730,8 +3778,9 @@ done test "$ac_cv_exeext" = no && ac_cv_exeext= -else $as_nop - ac_file='' +else case e in #( + e) ac_file='' ;; +esac fi if test -z "$ac_file" then : @@ -3740,13 +3789,14 @@ printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 -{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "C compiler cannot create executables -See \`config.log' for more details" "$LINENO" 5; } -else $as_nop - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "yes" >&6; } +See 'config.log' for more details" "$LINENO" 5; } +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler default output file name" >&5 printf %s "checking for C compiler default output file name... " >&6; } @@ -3770,10 +3820,10 @@ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } then : - # If both `conftest.exe' and `conftest' are `present' (well, observable) -# catch `conftest.exe'. For instance with Cygwin, `ls conftest' will -# work properly (i.e., refer to `conftest.exe'), while it won't with -# `rm'. + # If both 'conftest.exe' and 'conftest' are 'present' (well, observable) +# catch 'conftest.exe'. For instance with Cygwin, 'ls conftest' will +# work properly (i.e., refer to 'conftest.exe'), while it won't with +# 'rm'. for ac_file in conftest.exe conftest conftest.*; do test -f "$ac_file" || continue case $ac_file in @@ -3783,11 +3833,12 @@ * ) break;; esac done -else $as_nop - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot compute suffix of executables: cannot compile and link -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } ;; +esac fi rm -f conftest conftest$ac_cv_exeext { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_exeext" >&5 @@ -3803,6 +3854,8 @@ main (void) { FILE *f = fopen ("conftest.out", "w"); + if (!f) + return 1; return ferror (f) || fclose (f) != 0; ; @@ -3842,26 +3895,27 @@ if test "$cross_compiling" = maybe; then cross_compiling=yes else - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot run C compiled programs. -If you meant to cross compile, use \`--host'. -See \`config.log' for more details" "$LINENO" 5; } +If you meant to cross compile, use '--host'. +See 'config.log' for more details" "$LINENO" 5; } fi fi fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $cross_compiling" >&5 printf "%s\n" "$cross_compiling" >&6; } -rm -f conftest.$ac_ext conftest$ac_cv_exeext conftest.out +rm -f conftest.$ac_ext conftest$ac_cv_exeext \ + conftest.o conftest.obj conftest.out ac_clean_files=$ac_clean_files_save { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for suffix of object files" >&5 printf %s "checking for suffix of object files... " >&6; } if test ${ac_cv_objext+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ int @@ -3893,16 +3947,18 @@ break;; esac done -else $as_nop - printf "%s\n" "$as_me: failed program was:" >&5 +else case e in #( + e) printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 -{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot compute suffix of object files: cannot compile -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } ;; +esac fi -rm -f conftest.$ac_cv_objext conftest.$ac_ext +rm -f conftest.$ac_cv_objext conftest.$ac_ext ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_objext" >&5 printf "%s\n" "$ac_cv_objext" >&6; } @@ -3913,8 +3969,8 @@ if test ${ac_cv_c_compiler_gnu+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ int @@ -3931,12 +3987,14 @@ if ac_fn_c_try_compile "$LINENO" then : ac_compiler_gnu=yes -else $as_nop - ac_compiler_gnu=no +else case e in #( + e) ac_compiler_gnu=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ac_cv_c_compiler_gnu=$ac_compiler_gnu - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5 printf "%s\n" "$ac_cv_c_compiler_gnu" >&6; } @@ -3954,8 +4012,8 @@ if test ${ac_cv_prog_cc_g+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_save_c_werror_flag=$ac_c_werror_flag +else case e in #( + e) ac_save_c_werror_flag=$ac_c_werror_flag ac_c_werror_flag=yes ac_cv_prog_cc_g=no CFLAGS="-g" @@ -3973,8 +4031,8 @@ if ac_fn_c_try_compile "$LINENO" then : ac_cv_prog_cc_g=yes -else $as_nop - CFLAGS="" +else case e in #( + e) CFLAGS="" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -3989,8 +4047,8 @@ if ac_fn_c_try_compile "$LINENO" then : -else $as_nop - ac_c_werror_flag=$ac_save_c_werror_flag +else case e in #( + e) ac_c_werror_flag=$ac_save_c_werror_flag CFLAGS="-g" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4007,12 +4065,15 @@ then : ac_cv_prog_cc_g=yes fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext - ac_c_werror_flag=$ac_save_c_werror_flag + ac_c_werror_flag=$ac_save_c_werror_flag ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5 printf "%s\n" "$ac_cv_prog_cc_g" >&6; } @@ -4039,8 +4100,8 @@ if test ${ac_cv_prog_cc_c11+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_cv_prog_cc_c11=no +else case e in #( + e) ac_cv_prog_cc_c11=no ac_save_CC=$CC cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4057,25 +4118,28 @@ test "x$ac_cv_prog_cc_c11" != "xno" && break done rm -f conftest.$ac_ext -CC=$ac_save_CC +CC=$ac_save_CC ;; +esac fi if test "x$ac_cv_prog_cc_c11" = xno then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 printf "%s\n" "unsupported" >&6; } -else $as_nop - if test "x$ac_cv_prog_cc_c11" = x +else case e in #( + e) if test "x$ac_cv_prog_cc_c11" = x then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 printf "%s\n" "none needed" >&6; } -else $as_nop - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11" >&5 +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11" >&5 printf "%s\n" "$ac_cv_prog_cc_c11" >&6; } - CC="$CC $ac_cv_prog_cc_c11" + CC="$CC $ac_cv_prog_cc_c11" ;; +esac fi ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c11 - ac_prog_cc_stdc=c11 + ac_prog_cc_stdc=c11 ;; +esac fi fi if test x$ac_prog_cc_stdc = xno @@ -4085,8 +4149,8 @@ if test ${ac_cv_prog_cc_c99+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_cv_prog_cc_c99=no +else case e in #( + e) ac_cv_prog_cc_c99=no ac_save_CC=$CC cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4103,25 +4167,28 @@ test "x$ac_cv_prog_cc_c99" != "xno" && break done rm -f conftest.$ac_ext -CC=$ac_save_CC +CC=$ac_save_CC ;; +esac fi if test "x$ac_cv_prog_cc_c99" = xno then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 printf "%s\n" "unsupported" >&6; } -else $as_nop - if test "x$ac_cv_prog_cc_c99" = x +else case e in #( + e) if test "x$ac_cv_prog_cc_c99" = x then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 printf "%s\n" "none needed" >&6; } -else $as_nop - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5 +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5 printf "%s\n" "$ac_cv_prog_cc_c99" >&6; } - CC="$CC $ac_cv_prog_cc_c99" + CC="$CC $ac_cv_prog_cc_c99" ;; +esac fi ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c99 - ac_prog_cc_stdc=c99 + ac_prog_cc_stdc=c99 ;; +esac fi fi if test x$ac_prog_cc_stdc = xno @@ -4131,8 +4198,8 @@ if test ${ac_cv_prog_cc_c89+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_cv_prog_cc_c89=no +else case e in #( + e) ac_cv_prog_cc_c89=no ac_save_CC=$CC cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4149,25 +4216,28 @@ test "x$ac_cv_prog_cc_c89" != "xno" && break done rm -f conftest.$ac_ext -CC=$ac_save_CC +CC=$ac_save_CC ;; +esac fi if test "x$ac_cv_prog_cc_c89" = xno then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 printf "%s\n" "unsupported" >&6; } -else $as_nop - if test "x$ac_cv_prog_cc_c89" = x +else case e in #( + e) if test "x$ac_cv_prog_cc_c89" = x then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 printf "%s\n" "none needed" >&6; } -else $as_nop - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 printf "%s\n" "$ac_cv_prog_cc_c89" >&6; } - CC="$CC $ac_cv_prog_cc_c89" + CC="$CC $ac_cv_prog_cc_c89" ;; +esac fi ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c89 - ac_prog_cc_stdc=c89 + ac_prog_cc_stdc=c89 ;; +esac fi fi @@ -4212,8 +4282,8 @@ if test ${ac_cv_c_bigendian+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_cv_c_bigendian=unknown +else case e in #( + e) ac_cv_c_bigendian=unknown # See if we're dealing with a universal compiler. cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4259,8 +4329,8 @@ int main (void) { -#if ! (defined BYTE_ORDER && defined BIG_ENDIAN \ - && defined LITTLE_ENDIAN && BYTE_ORDER && BIG_ENDIAN \ +#if ! (defined BYTE_ORDER && defined BIG_ENDIAN \\ + && defined LITTLE_ENDIAN && BYTE_ORDER && BIG_ENDIAN \\ && LITTLE_ENDIAN) bogus endian macros #endif @@ -4291,8 +4361,9 @@ if ac_fn_c_try_compile "$LINENO" then : ac_cv_c_bigendian=yes -else $as_nop - ac_cv_c_bigendian=no +else case e in #( + e) ac_cv_c_bigendian=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext fi @@ -4336,8 +4407,9 @@ if ac_fn_c_try_compile "$LINENO" then : ac_cv_c_bigendian=yes -else $as_nop - ac_cv_c_bigendian=no +else case e in #( + e) ac_cv_c_bigendian=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext fi @@ -4364,22 +4436,23 @@ int use_ebcdic (int i) { return ebcdic_mm[i] + ebcdic_ii[i]; } - extern int foo; - -int -main (void) -{ -return use_ascii (foo) == use_ebcdic (foo); - ; - return 0; -} + int + main (int argc, char **argv) + { + /* Intimidate the compiler so that it does not + optimize the arrays away. */ + char *p = argv[0]; + ascii_mm[1] = *p++; ebcdic_mm[1] = *p++; + ascii_ii[1] = *p++; ebcdic_ii[1] = *p++; + return use_ascii (argc) == use_ebcdic (*p); + } _ACEOF -if ac_fn_c_try_compile "$LINENO" +if ac_fn_c_try_link "$LINENO" then : - if grep BIGenDianSyS conftest.$ac_objext >/dev/null; then + if grep BIGenDianSyS conftest$ac_exeext >/dev/null; then ac_cv_c_bigendian=yes fi - if grep LiTTleEnDian conftest.$ac_objext >/dev/null ; then + if grep LiTTleEnDian conftest$ac_exeext >/dev/null ; then if test "$ac_cv_c_bigendian" = unknown; then ac_cv_c_bigendian=no else @@ -4388,9 +4461,10 @@ fi fi fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $ac_includes_default int @@ -4413,14 +4487,17 @@ if ac_fn_c_try_run "$LINENO" then : ac_cv_c_bigendian=no -else $as_nop - ac_cv_c_bigendian=yes +else case e in #( + e) ac_cv_c_bigendian=yes ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - fi + fi ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_bigendian" >&5 printf "%s\n" "$ac_cv_c_bigendian" >&6; } @@ -4442,14 +4519,14 @@ ac_header_dirent=no for ac_hdr in dirent.h sys/ndir.h sys/dir.h ndir.h; do - as_ac_Header=`printf "%s\n" "ac_cv_header_dirent_$ac_hdr" | $as_tr_sh` + as_ac_Header=`printf "%s\n" "ac_cv_header_dirent_$ac_hdr" | sed "$as_sed_sh"` { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_hdr that defines DIR" >&5 printf %s "checking for $ac_hdr that defines DIR... " >&6; } if eval test \${$as_ac_Header+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include #include <$ac_hdr> @@ -4466,10 +4543,12 @@ if ac_fn_c_try_compile "$LINENO" then : eval "$as_ac_Header=yes" -else $as_nop - eval "$as_ac_Header=no" +else case e in #( + e) eval "$as_ac_Header=no" ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi eval ac_res=\$$as_ac_Header { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 @@ -4477,7 +4556,7 @@ if eval test \"x\$"$as_ac_Header"\" = x"yes" then : cat >>confdefs.h <<_ACEOF -#define `printf "%s\n" "HAVE_$ac_hdr" | $as_tr_cpp` 1 +#define `printf "%s\n" "HAVE_$ac_hdr" | sed "$as_sed_cpp"` 1 _ACEOF ac_header_dirent=$ac_hdr; break @@ -4491,15 +4570,21 @@ if test ${ac_cv_search_opendir+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_func_search_save_LIBS=$LIBS +else case e in #( + e) ac_func_search_save_LIBS=$LIBS cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char opendir (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char opendir (void); int main (void) { @@ -4530,11 +4615,13 @@ if test ${ac_cv_search_opendir+y} then : -else $as_nop - ac_cv_search_opendir=no +else case e in #( + e) ac_cv_search_opendir=no ;; +esac fi rm conftest.$ac_ext -LIBS=$ac_func_search_save_LIBS +LIBS=$ac_func_search_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_opendir" >&5 printf "%s\n" "$ac_cv_search_opendir" >&6; } @@ -4551,15 +4638,21 @@ if test ${ac_cv_search_opendir+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_func_search_save_LIBS=$LIBS +else case e in #( + e) ac_func_search_save_LIBS=$LIBS cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char opendir (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char opendir (void); int main (void) { @@ -4590,11 +4683,13 @@ if test ${ac_cv_search_opendir+y} then : -else $as_nop - ac_cv_search_opendir=no +else case e in #( + e) ac_cv_search_opendir=no ;; +esac fi rm conftest.$ac_ext -LIBS=$ac_func_search_save_LIBS +LIBS=$ac_func_search_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_opendir" >&5 printf "%s\n" "$ac_cv_search_opendir" >&6; } @@ -4612,8 +4707,8 @@ if test ${ac_cv_header_sys_wait_h+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include #include @@ -4637,10 +4732,12 @@ if ac_fn_c_try_compile "$LINENO" then : ac_cv_header_sys_wait_h=yes -else $as_nop - ac_cv_header_sys_wait_h=no +else case e in #( + e) ac_cv_header_sys_wait_h=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_header_sys_wait_h" >&5 printf "%s\n" "$ac_cv_header_sys_wait_h" >&6; } @@ -4650,6 +4747,12 @@ fi +ac_fn_c_check_header_compile "$LINENO" "poll.h" "ac_cv_header_poll_h" "$ac_includes_default" +if test "x$ac_cv_header_poll_h" = xyes +then : + printf "%s\n" "#define HAVE_POLL_H 1" >>confdefs.h + +fi ac_fn_c_check_header_compile "$LINENO" "sys/fcntl.h" "ac_cv_header_sys_fcntl_h" "$ac_includes_default" if test "x$ac_cv_header_sys_fcntl_h" = xyes then : @@ -4956,6 +5059,12 @@ printf "%s\n" "#define HAVE_SYS_FILE_H 1" >>confdefs.h fi +ac_fn_c_check_header_compile "$LINENO" "sys/resource.h" "ac_cv_header_sys_resource_h" "$ac_includes_default" +if test "x$ac_cv_header_sys_resource_h" = xyes +then : + printf "%s\n" "#define HAVE_SYS_RESOURCE_H 1" >>confdefs.h + +fi ac_fn_c_check_header_compile "$LINENO" "bsd/string.h" "ac_cv_header_bsd_string_h" "$ac_includes_default" if test "x$ac_cv_header_bsd_string_h" = xyes then : @@ -4976,8 +5085,8 @@ if test ${ac_cv_header_sys_types_h_makedev+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include int @@ -4995,12 +5104,14 @@ else ac_cv_header_sys_types_h_makedev=yes fi -else $as_nop - ac_cv_header_sys_types_h_makedev=no +else case e in #( + e) ac_cv_header_sys_types_h_makedev=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_header_sys_types_h_makedev" >&5 printf "%s\n" "$ac_cv_header_sys_types_h_makedev" >&6; } @@ -5053,15 +5164,16 @@ if test ${ac_cv_build+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_build_alias=$build_alias +else case e in #( + e) ac_build_alias=$build_alias test "x$ac_build_alias" = x && ac_build_alias=`$SHELL "${ac_aux_dir}config.guess"` test "x$ac_build_alias" = x && as_fn_error $? "cannot guess build type; you must specify one" "$LINENO" 5 ac_cv_build=`$SHELL "${ac_aux_dir}config.sub" $ac_build_alias` || as_fn_error $? "$SHELL ${ac_aux_dir}config.sub $ac_build_alias failed" "$LINENO" 5 - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_build" >&5 printf "%s\n" "$ac_cv_build" >&6; } @@ -5088,14 +5200,15 @@ if test ${ac_cv_host+y} then : printf %s "(cached) " >&6 -else $as_nop - if test "x$host_alias" = x; then +else case e in #( + e) if test "x$host_alias" = x; then ac_cv_host=$ac_cv_build else ac_cv_host=`$SHELL "${ac_aux_dir}config.sub" $host_alias` || as_fn_error $? "$SHELL ${ac_aux_dir}config.sub $host_alias failed" "$LINENO" 5 fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_host" >&5 printf "%s\n" "$ac_cv_host" >&6; } @@ -5157,8 +5270,8 @@ if test ${ac_cv_prog_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$CC"; then +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -5180,7 +5293,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then @@ -5202,8 +5316,8 @@ if test ${ac_cv_prog_ac_ct_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$ac_ct_CC"; then +else case e in #( + e) if test -n "$ac_ct_CC"; then ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -5225,7 +5339,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then @@ -5260,8 +5375,8 @@ if test ${ac_cv_prog_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$CC"; then +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -5283,7 +5398,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then @@ -5305,8 +5421,8 @@ if test ${ac_cv_prog_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$CC"; then +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else ac_prog_rejected=no @@ -5345,7 +5461,8 @@ ac_cv_prog_CC="$as_dir$ac_word${1+' '}$@" fi fi -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then @@ -5369,8 +5486,8 @@ if test ${ac_cv_prog_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$CC"; then +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -5392,7 +5509,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then @@ -5418,8 +5536,8 @@ if test ${ac_cv_prog_ac_ct_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$ac_ct_CC"; then +else case e in #( + e) if test -n "$ac_ct_CC"; then ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -5441,7 +5559,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then @@ -5479,8 +5598,8 @@ if test ${ac_cv_prog_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$CC"; then +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -5502,7 +5621,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then @@ -5524,8 +5644,8 @@ if test ${ac_cv_prog_ac_ct_CC+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$ac_ct_CC"; then +else case e in #( + e) if test -n "$ac_ct_CC"; then ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -5547,7 +5667,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then @@ -5576,10 +5697,10 @@ fi -test -z "$CC" && { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +test -z "$CC" && { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "no acceptable C compiler found in \$PATH -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } # Provide some information about the compiler. printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5 @@ -5611,8 +5732,8 @@ if test ${ac_cv_c_compiler_gnu+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ int @@ -5629,12 +5750,14 @@ if ac_fn_c_try_compile "$LINENO" then : ac_compiler_gnu=yes -else $as_nop - ac_compiler_gnu=no +else case e in #( + e) ac_compiler_gnu=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ac_cv_c_compiler_gnu=$ac_compiler_gnu - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5 printf "%s\n" "$ac_cv_c_compiler_gnu" >&6; } @@ -5652,8 +5775,8 @@ if test ${ac_cv_prog_cc_g+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_save_c_werror_flag=$ac_c_werror_flag +else case e in #( + e) ac_save_c_werror_flag=$ac_c_werror_flag ac_c_werror_flag=yes ac_cv_prog_cc_g=no CFLAGS="-g" @@ -5671,8 +5794,8 @@ if ac_fn_c_try_compile "$LINENO" then : ac_cv_prog_cc_g=yes -else $as_nop - CFLAGS="" +else case e in #( + e) CFLAGS="" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -5687,8 +5810,8 @@ if ac_fn_c_try_compile "$LINENO" then : -else $as_nop - ac_c_werror_flag=$ac_save_c_werror_flag +else case e in #( + e) ac_c_werror_flag=$ac_save_c_werror_flag CFLAGS="-g" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -5705,12 +5828,15 @@ then : ac_cv_prog_cc_g=yes fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext - ac_c_werror_flag=$ac_save_c_werror_flag + ac_c_werror_flag=$ac_save_c_werror_flag ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5 printf "%s\n" "$ac_cv_prog_cc_g" >&6; } @@ -5737,8 +5863,8 @@ if test ${ac_cv_prog_cc_c11+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_cv_prog_cc_c11=no +else case e in #( + e) ac_cv_prog_cc_c11=no ac_save_CC=$CC cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -5755,25 +5881,28 @@ test "x$ac_cv_prog_cc_c11" != "xno" && break done rm -f conftest.$ac_ext -CC=$ac_save_CC +CC=$ac_save_CC ;; +esac fi if test "x$ac_cv_prog_cc_c11" = xno then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 printf "%s\n" "unsupported" >&6; } -else $as_nop - if test "x$ac_cv_prog_cc_c11" = x +else case e in #( + e) if test "x$ac_cv_prog_cc_c11" = x then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 printf "%s\n" "none needed" >&6; } -else $as_nop - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11" >&5 +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11" >&5 printf "%s\n" "$ac_cv_prog_cc_c11" >&6; } - CC="$CC $ac_cv_prog_cc_c11" + CC="$CC $ac_cv_prog_cc_c11" ;; +esac fi ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c11 - ac_prog_cc_stdc=c11 + ac_prog_cc_stdc=c11 ;; +esac fi fi if test x$ac_prog_cc_stdc = xno @@ -5783,8 +5912,8 @@ if test ${ac_cv_prog_cc_c99+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_cv_prog_cc_c99=no +else case e in #( + e) ac_cv_prog_cc_c99=no ac_save_CC=$CC cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -5801,25 +5930,28 @@ test "x$ac_cv_prog_cc_c99" != "xno" && break done rm -f conftest.$ac_ext -CC=$ac_save_CC +CC=$ac_save_CC ;; +esac fi if test "x$ac_cv_prog_cc_c99" = xno then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 printf "%s\n" "unsupported" >&6; } -else $as_nop - if test "x$ac_cv_prog_cc_c99" = x +else case e in #( + e) if test "x$ac_cv_prog_cc_c99" = x then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 printf "%s\n" "none needed" >&6; } -else $as_nop - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5 +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5 printf "%s\n" "$ac_cv_prog_cc_c99" >&6; } - CC="$CC $ac_cv_prog_cc_c99" + CC="$CC $ac_cv_prog_cc_c99" ;; +esac fi ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c99 - ac_prog_cc_stdc=c99 + ac_prog_cc_stdc=c99 ;; +esac fi fi if test x$ac_prog_cc_stdc = xno @@ -5829,8 +5961,8 @@ if test ${ac_cv_prog_cc_c89+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_cv_prog_cc_c89=no +else case e in #( + e) ac_cv_prog_cc_c89=no ac_save_CC=$CC cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -5847,25 +5979,28 @@ test "x$ac_cv_prog_cc_c89" != "xno" && break done rm -f conftest.$ac_ext -CC=$ac_save_CC +CC=$ac_save_CC ;; +esac fi if test "x$ac_cv_prog_cc_c89" = xno then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 printf "%s\n" "unsupported" >&6; } -else $as_nop - if test "x$ac_cv_prog_cc_c89" = x +else case e in #( + e) if test "x$ac_cv_prog_cc_c89" = x then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 printf "%s\n" "none needed" >&6; } -else $as_nop - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 printf "%s\n" "$ac_cv_prog_cc_c89" >&6; } - CC="$CC $ac_cv_prog_cc_c89" + CC="$CC $ac_cv_prog_cc_c89" ;; +esac fi ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c89 - ac_prog_cc_stdc=c89 + ac_prog_cc_stdc=c89 ;; +esac fi fi @@ -5890,8 +6025,8 @@ if test ${ac_cv_prog_CPP+y} then : printf %s "(cached) " >&6 -else $as_nop - # Double quotes because $CC needs to be expanded +else case e in #( + e) # Double quotes because $CC needs to be expanded for CPP in "$CC -E" "$CC -E -traditional-cpp" cpp /lib/cpp do ac_preproc_ok=false @@ -5909,9 +6044,10 @@ if ac_fn_c_try_cpp "$LINENO" then : -else $as_nop - # Broken: fails on valid input. -continue +else case e in #( + e) # Broken: fails on valid input. +continue ;; +esac fi rm -f conftest.err conftest.i conftest.$ac_ext @@ -5925,15 +6061,16 @@ then : # Broken: success on invalid input. continue -else $as_nop - # Passes both tests. +else case e in #( + e) # Passes both tests. ac_preproc_ok=: -break +break ;; +esac fi rm -f conftest.err conftest.i conftest.$ac_ext done -# Because of `break', _AC_PREPROC_IFELSE's cleaning code was skipped. +# Because of 'break', _AC_PREPROC_IFELSE's cleaning code was skipped. rm -f conftest.i conftest.err conftest.$ac_ext if $ac_preproc_ok then : @@ -5942,7 +6079,8 @@ done ac_cv_prog_CPP=$CPP - + ;; +esac fi CPP=$ac_cv_prog_CPP else @@ -5965,9 +6103,10 @@ if ac_fn_c_try_cpp "$LINENO" then : -else $as_nop - # Broken: fails on valid input. -continue +else case e in #( + e) # Broken: fails on valid input. +continue ;; +esac fi rm -f conftest.err conftest.i conftest.$ac_ext @@ -5981,24 +6120,26 @@ then : # Broken: success on invalid input. continue -else $as_nop - # Passes both tests. +else case e in #( + e) # Passes both tests. ac_preproc_ok=: -break +break ;; +esac fi rm -f conftest.err conftest.i conftest.$ac_ext done -# Because of `break', _AC_PREPROC_IFELSE's cleaning code was skipped. +# Because of 'break', _AC_PREPROC_IFELSE's cleaning code was skipped. rm -f conftest.i conftest.err conftest.$ac_ext if $ac_preproc_ok then : -else $as_nop - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "C preprocessor \"$CPP\" fails sanity check -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } ;; +esac fi ac_ext=c @@ -6032,8 +6173,8 @@ if test ${ac_cv_prog_CXX+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$CXX"; then +else case e in #( + e) if test -n "$CXX"; then ac_cv_prog_CXX="$CXX" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -6055,7 +6196,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi CXX=$ac_cv_prog_CXX if test -n "$CXX"; then @@ -6081,8 +6223,8 @@ if test ${ac_cv_prog_ac_ct_CXX+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$ac_ct_CXX"; then +else case e in #( + e) if test -n "$ac_ct_CXX"; then ac_cv_prog_ac_ct_CXX="$ac_ct_CXX" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -6104,7 +6246,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi ac_ct_CXX=$ac_cv_prog_ac_ct_CXX if test -n "$ac_ct_CXX"; then @@ -6164,8 +6307,8 @@ if test ${ac_cv_cxx_compiler_gnu+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ int @@ -6182,12 +6325,14 @@ if ac_fn_cxx_try_compile "$LINENO" then : ac_compiler_gnu=yes -else $as_nop - ac_compiler_gnu=no +else case e in #( + e) ac_compiler_gnu=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ac_cv_cxx_compiler_gnu=$ac_compiler_gnu - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_cxx_compiler_gnu" >&5 printf "%s\n" "$ac_cv_cxx_compiler_gnu" >&6; } @@ -6205,8 +6350,8 @@ if test ${ac_cv_prog_cxx_g+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_save_cxx_werror_flag=$ac_cxx_werror_flag +else case e in #( + e) ac_save_cxx_werror_flag=$ac_cxx_werror_flag ac_cxx_werror_flag=yes ac_cv_prog_cxx_g=no CXXFLAGS="-g" @@ -6224,8 +6369,8 @@ if ac_fn_cxx_try_compile "$LINENO" then : ac_cv_prog_cxx_g=yes -else $as_nop - CXXFLAGS="" +else case e in #( + e) CXXFLAGS="" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -6240,8 +6385,8 @@ if ac_fn_cxx_try_compile "$LINENO" then : -else $as_nop - ac_cxx_werror_flag=$ac_save_cxx_werror_flag +else case e in #( + e) ac_cxx_werror_flag=$ac_save_cxx_werror_flag CXXFLAGS="-g" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -6258,12 +6403,15 @@ then : ac_cv_prog_cxx_g=yes fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext - ac_cxx_werror_flag=$ac_save_cxx_werror_flag + ac_cxx_werror_flag=$ac_save_cxx_werror_flag ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cxx_g" >&5 printf "%s\n" "$ac_cv_prog_cxx_g" >&6; } @@ -6287,11 +6435,11 @@ then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CXX option to enable C++11 features" >&5 printf %s "checking for $CXX option to enable C++11 features... " >&6; } -if test ${ac_cv_prog_cxx_11+y} +if test ${ac_cv_prog_cxx_cxx11+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_cv_prog_cxx_11=no +else case e in #( + e) ac_cv_prog_cxx_cxx11=no ac_save_CXX=$CXX cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -6308,36 +6456,39 @@ test "x$ac_cv_prog_cxx_cxx11" != "xno" && break done rm -f conftest.$ac_ext -CXX=$ac_save_CXX +CXX=$ac_save_CXX ;; +esac fi if test "x$ac_cv_prog_cxx_cxx11" = xno then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 printf "%s\n" "unsupported" >&6; } -else $as_nop - if test "x$ac_cv_prog_cxx_cxx11" = x +else case e in #( + e) if test "x$ac_cv_prog_cxx_cxx11" = x then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 printf "%s\n" "none needed" >&6; } -else $as_nop - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cxx_cxx11" >&5 +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cxx_cxx11" >&5 printf "%s\n" "$ac_cv_prog_cxx_cxx11" >&6; } - CXX="$CXX $ac_cv_prog_cxx_cxx11" + CXX="$CXX $ac_cv_prog_cxx_cxx11" ;; +esac fi ac_cv_prog_cxx_stdcxx=$ac_cv_prog_cxx_cxx11 - ac_prog_cxx_stdcxx=cxx11 + ac_prog_cxx_stdcxx=cxx11 ;; +esac fi fi if test x$ac_prog_cxx_stdcxx = xno then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CXX option to enable C++98 features" >&5 printf %s "checking for $CXX option to enable C++98 features... " >&6; } -if test ${ac_cv_prog_cxx_98+y} +if test ${ac_cv_prog_cxx_cxx98+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_cv_prog_cxx_98=no +else case e in #( + e) ac_cv_prog_cxx_cxx98=no ac_save_CXX=$CXX cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -6354,25 +6505,28 @@ test "x$ac_cv_prog_cxx_cxx98" != "xno" && break done rm -f conftest.$ac_ext -CXX=$ac_save_CXX +CXX=$ac_save_CXX ;; +esac fi if test "x$ac_cv_prog_cxx_cxx98" = xno then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 printf "%s\n" "unsupported" >&6; } -else $as_nop - if test "x$ac_cv_prog_cxx_cxx98" = x +else case e in #( + e) if test "x$ac_cv_prog_cxx_cxx98" = x then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 printf "%s\n" "none needed" >&6; } -else $as_nop - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cxx_cxx98" >&5 +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cxx_cxx98" >&5 printf "%s\n" "$ac_cv_prog_cxx_cxx98" >&6; } - CXX="$CXX $ac_cv_prog_cxx_cxx98" + CXX="$CXX $ac_cv_prog_cxx_cxx98" ;; +esac fi ac_cv_prog_cxx_stdcxx=$ac_cv_prog_cxx_cxx98 - ac_prog_cxx_stdcxx=cxx98 + ac_prog_cxx_stdcxx=cxx98 ;; +esac fi fi @@ -6391,8 +6545,8 @@ if test ${ac_cv_prog_AWK+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$AWK"; then +else case e in #( + e) if test -n "$AWK"; then ac_cv_prog_AWK="$AWK" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -6414,7 +6568,8 @@ done IFS=$as_save_IFS -fi +fi ;; +esac fi AWK=$ac_cv_prog_AWK if test -n "$AWK"; then @@ -6434,8 +6589,8 @@ if test ${ac_cv_path_GREP+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -z "$GREP"; then +else case e in #( + e) if test -z "$GREP"; then ac_path_GREP_found=false # Loop through the user's path and test for each of PROGNAME-LIST as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -6454,9 +6609,10 @@ as_fn_executable_p "$ac_path_GREP" || continue # Check for GNU ac_path_GREP and select it if it is found. # Check for GNU $ac_path_GREP -case `"$ac_path_GREP" --version 2>&1` in +case `"$ac_path_GREP" --version 2>&1` in #( *GNU*) ac_cv_path_GREP="$ac_path_GREP" ac_path_GREP_found=:;; +#( *) ac_count=0 printf %s 0123456789 >"conftest.in" @@ -6491,7 +6647,8 @@ else ac_cv_path_GREP=$GREP fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_GREP" >&5 printf "%s\n" "$ac_cv_path_GREP" >&6; } @@ -6503,8 +6660,8 @@ if test ${ac_cv_path_EGREP+y} then : printf %s "(cached) " >&6 -else $as_nop - if echo a | $GREP -E '(a|b)' >/dev/null 2>&1 +else case e in #( + e) if echo a | $GREP -E '(a|b)' >/dev/null 2>&1 then ac_cv_path_EGREP="$GREP -E" else if test -z "$EGREP"; then @@ -6526,9 +6683,10 @@ as_fn_executable_p "$ac_path_EGREP" || continue # Check for GNU ac_path_EGREP and select it if it is found. # Check for GNU $ac_path_EGREP -case `"$ac_path_EGREP" --version 2>&1` in +case `"$ac_path_EGREP" --version 2>&1` in #( *GNU*) ac_cv_path_EGREP="$ac_path_EGREP" ac_path_EGREP_found=:;; +#( *) ac_count=0 printf %s 0123456789 >"conftest.in" @@ -6564,12 +6722,15 @@ ac_cv_path_EGREP=$EGREP fi - fi + fi ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_EGREP" >&5 printf "%s\n" "$ac_cv_path_EGREP" >&6; } EGREP="$ac_cv_path_EGREP" + EGREP_TRADITIONAL=$EGREP + ac_cv_path_EGREP_TRADITIONAL=$EGREP # Find a good install program. We prefer a C program (faster), @@ -6592,8 +6753,8 @@ if test ${ac_cv_path_install+y} then : printf %s "(cached) " >&6 -else $as_nop - as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +else case e in #( + e) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS @@ -6647,7 +6808,8 @@ IFS=$as_save_IFS rm -rf conftest.one conftest.two conftest.dir - + ;; +esac fi if test ${ac_cv_path_install+y}; then INSTALL=$ac_cv_path_install @@ -6677,8 +6839,8 @@ if test ${ac_cv_path_mkdir+y} then : printf %s "(cached) " >&6 -else $as_nop - as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +else case e in #( + e) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH$PATH_SEPARATOR/opt/sfw/bin do IFS=$as_save_IFS @@ -6692,7 +6854,7 @@ as_fn_executable_p "$as_dir$ac_prog$ac_exec_ext" || continue case `"$as_dir$ac_prog$ac_exec_ext" --version 2>&1` in #( 'mkdir ('*'coreutils) '* | \ - 'BusyBox '* | \ + *'BusyBox '* | \ 'mkdir (fileutils) '4.1*) ac_cv_path_mkdir=$as_dir$ac_prog$ac_exec_ext break 3;; @@ -6701,18 +6863,17 @@ done done IFS=$as_save_IFS - + ;; +esac fi test -d ./--version && rmdir ./--version if test ${ac_cv_path_mkdir+y}; then MKDIR_P="$ac_cv_path_mkdir -p" else - # As a last resort, use the slow shell script. Don't cache a - # value for MKDIR_P within a source directory, because that will - # break other packages using the cache if that directory is - # removed, or if the value is a relative name. - MKDIR_P="$ac_install_sh -d" + # As a last resort, use plain mkdir -p, + # in the hope it doesn't have the bugs of ancient mkdir. + MKDIR_P='mkdir -p' fi fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $MKDIR_P" >&5 @@ -6726,8 +6887,8 @@ if test ${ac_cv_path_PERL+y} then : printf %s "(cached) " >&6 -else $as_nop - case $PERL in +else case e in #( + e) case $PERL in [\\/]* | ?:[\\/]*) ac_cv_path_PERL="$PERL" # Let the user override the test with a path. ;; @@ -6752,6 +6913,7 @@ IFS=$as_save_IFS ;; +esac ;; esac fi PERL=$ac_cv_path_PERL @@ -6771,8 +6933,8 @@ if test ${ac_cv_path_PYTHON3+y} then : printf %s "(cached) " >&6 -else $as_nop - case $PYTHON3 in +else case e in #( + e) case $PYTHON3 in [\\/]* | ?:[\\/]*) ac_cv_path_PYTHON3="$PYTHON3" # Let the user override the test with a path. ;; @@ -6797,6 +6959,7 @@ IFS=$as_save_IFS ;; +esac ;; esac fi PYTHON3=$ac_cv_path_PYTHON3 @@ -6834,6 +6997,28 @@ CFLAGS="$CFLAGS -pg" fi +# Check whether --enable-coverage was given. +if test ${enable_coverage+y} +then : + enableval=$enable_coverage; +fi + +if test x"$enable_coverage" = x"yes"; then + CFLAGS="$CFLAGS --coverage -fprofile-update=atomic -O0" + CXXFLAGS="$CXXFLAGS --coverage -fprofile-update=atomic -O0" + LDFLAGS="$LDFLAGS --coverage" + +printf "%s\n" "#define GCOV_COVERAGE 1" >>confdefs.h + +fi + +# Check whether --enable-openat2 was given. +if test ${enable_openat2+y} +then : + enableval=$enable_openat2; +fi + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if md2man can create manpages" >&5 printf %s "checking if md2man can create manpages... " >&6; } if test x"$ac_cv_path_PYTHON3" = x; then @@ -6967,8 +7152,9 @@ if test ${with_rsync_path+y} then : withval=$with_rsync_path; RSYNC_PATH="$with_rsync_path" -else $as_nop - RSYNC_PATH="rsync" +else case e in #( + e) RSYNC_PATH="rsync" ;; +esac fi @@ -6995,8 +7181,9 @@ else RSYNCD_SYSCONF="/etc/rsyncd.conf" fi -else $as_nop - RSYNCD_SYSCONF="/etc/rsyncd.conf" +else case e in #( + e) RSYNCD_SYSCONF="/etc/rsyncd.conf" ;; +esac fi @@ -7019,8 +7206,8 @@ if test ${ac_cv_prog_HAVE_REMSH+y} then : printf %s "(cached) " >&6 -else $as_nop - if test -n "$HAVE_REMSH"; then +else case e in #( + e) if test -n "$HAVE_REMSH"; then ac_cv_prog_HAVE_REMSH="$HAVE_REMSH" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR @@ -7043,7 +7230,8 @@ IFS=$as_save_IFS test -z "$ac_cv_prog_HAVE_REMSH" && ac_cv_prog_HAVE_REMSH="0" -fi +fi ;; +esac fi HAVE_REMSH=$ac_cv_prog_HAVE_REMSH if test -n "$HAVE_REMSH"; then @@ -7078,8 +7266,8 @@ if test ${ac_cv_path_SHELL_PATH+y} then : printf %s "(cached) " >&6 -else $as_nop - case $SHELL_PATH in +else case e in #( + e) case $SHELL_PATH in [\\/]* | ?:[\\/]*) ac_cv_path_SHELL_PATH="$SHELL_PATH" # Let the user override the test with a path. ;; @@ -7105,6 +7293,7 @@ test -z "$ac_cv_path_SHELL_PATH" && ac_cv_path_SHELL_PATH="/bin/sh" ;; +esac ;; esac fi SHELL_PATH=$ac_cv_path_SHELL_PATH @@ -7124,8 +7313,8 @@ if test ${ac_cv_path_FAKEROOT_PATH+y} then : printf %s "(cached) " >&6 -else $as_nop - case $FAKEROOT_PATH in +else case e in #( + e) case $FAKEROOT_PATH in [\\/]* | ?:[\\/]*) ac_cv_path_FAKEROOT_PATH="$FAKEROOT_PATH" # Let the user override the test with a path. ;; @@ -7151,6 +7340,7 @@ test -z "$ac_cv_path_FAKEROOT_PATH" && ac_cv_path_FAKEROOT_PATH="/usr/bin/fakeroot" ;; +esac ;; esac fi FAKEROOT_PATH=$ac_cv_path_FAKEROOT_PATH @@ -7169,8 +7359,9 @@ if test ${with_nobody_user+y} then : withval=$with_nobody_user; NOBODY_USER="$with_nobody_user" -else $as_nop - NOBODY_USER="nobody" +else case e in #( + e) NOBODY_USER="nobody" ;; +esac fi @@ -7239,12 +7430,12 @@ if test "$cross_compiling" = yes then : - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot run test program while cross compiling -See \`config.log' for more details" "$LINENO" 5; } -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +See 'config.log' for more details" "$LINENO" 5; } +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include #if HAVE_STDLIB_H @@ -7281,11 +7472,13 @@ if ac_fn_cxx_try_run "$LINENO" then : CXX_OK=yes -else $as_nop - CXX_OK=no +else case e in #( + e) CXX_OK=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi else @@ -7326,8 +7519,9 @@ if ac_fn_cxx_try_compile "$LINENO" then : CXX_OK=yes -else $as_nop - CXX_OK=no +else case e in #( + e) CXX_OK=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext fi @@ -7394,14 +7588,63 @@ then : NOEXECSTACK='-Wa,--noexecstack' ; { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 printf "%s\n" "yes" >&6; } -else $as_nop - NOEXECSTACK='' ; { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } +else case e in #( + e) NOEXECSTACK='' ; { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext CFLAGS="$OLD_CFLAGS" +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for openat2" >&5 +printf %s "checking for openat2... " >&6; } +if test ${rsync_cv_HAVE_OPENAT2+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + + +#include +#include + +int +main (void) +{ + +struct open_how how; +how.resolve = RESOLVE_BENEATH; +return SYS_openat2 + (int)how.resolve; + + ; + return 0; +} + +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + rsync_cv_HAVE_OPENAT2=yes +else case e in #( + e) rsync_cv_HAVE_OPENAT2=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_OPENAT2" >&5 +printf "%s\n" "$rsync_cv_HAVE_OPENAT2" >&6; } +if test x"$enable_openat2" != x"no"; then + if test x"$rsync_cv_HAVE_OPENAT2" = x"yes"; then + +printf "%s\n" "#define HAVE_OPENAT2 1" >>confdefs.h + + fi +fi + # arrgh. libc in some old debian version screwed up the largefile # stuff, getting byte range locking wrong { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for broken largefile support" >&5 @@ -7409,13 +7652,13 @@ if test ${rsync_cv_HAVE_BROKEN_LARGEFILE+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) if test "$cross_compiling" = yes then : rsync_cv_HAVE_BROKEN_LARGEFILE=cross -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #define _FILE_OFFSET_BITS 64 @@ -7459,13 +7702,16 @@ if ac_fn_c_try_run "$LINENO" then : rsync_cv_HAVE_BROKEN_LARGEFILE=yes -else $as_nop - rsync_cv_HAVE_BROKEN_LARGEFILE=no +else case e in #( + e) rsync_cv_HAVE_BROKEN_LARGEFILE=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_BROKEN_LARGEFILE" >&5 printf "%s\n" "$rsync_cv_HAVE_BROKEN_LARGEFILE" >&6; } @@ -7475,31 +7721,34 @@ then : enableval=$enable_largefile; fi - -if test "$enable_largefile" != no; then - - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for special C compiler options needed for large files" >&5 -printf %s "checking for special C compiler options needed for large files... " >&6; } -if test ${ac_cv_sys_largefile_CC+y} +if test "$enable_largefile,$enable_year2038" != no,no +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CPPFLAGS option for large files" >&5 +printf %s "checking for $CPPFLAGS option for large files... " >&6; } +if test ${ac_cv_sys_largefile_opts+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_cv_sys_largefile_CC=no - if test "$GCC" != yes; then - ac_save_CC=$CC - while :; do - # IRIX 6.2 and later do not support large files by default, - # so use the C compiler's -n32 option if that helps. - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) ac_save_CPPFLAGS=$CPPFLAGS + ac_opt_found=no + for ac_opt in "none needed" "-D_FILE_OFFSET_BITS=64" "-D_LARGE_FILES=1"; do + if test x"$ac_opt" != x"none needed" +then : + CPPFLAGS="$ac_save_CPPFLAGS $ac_opt" +fi + cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include - /* Check that off_t can represent 2**63 - 1 correctly. - We can't simply define LARGE_OFF_T to be 9223372036854775807, +#ifndef FTYPE +# define FTYPE off_t +#endif + /* Check that FTYPE can represent 2**63 - 1 correctly. + We can't simply define LARGE_FTYPE to be 9223372036854775807, since some C++ compilers masquerading as C compilers incorrectly reject 9223372036854775807. */ -#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31)) - int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721 - && LARGE_OFF_T % 2147483647 == 1) +#define LARGE_FTYPE (((FTYPE) 1 << 31 << 31) - 1 + ((FTYPE) 1 << 31 << 31)) + int FTYPE_is_large[(LARGE_FTYPE % 2147483629 == 721 + && LARGE_FTYPE % 2147483647 == 1) ? 1 : -1]; int main (void) @@ -7509,142 +7758,86 @@ return 0; } _ACEOF - if ac_fn_c_try_compile "$LINENO" +if ac_fn_c_try_compile "$LINENO" then : - break -fi -rm -f core conftest.err conftest.$ac_objext conftest.beam - CC="$CC -n32" + if test x"$ac_opt" = x"none needed" +then : + # GNU/Linux s390x and alpha need _FILE_OFFSET_BITS=64 for wide ino_t. + CPPFLAGS="$CPPFLAGS -DFTYPE=ino_t" if ac_fn_c_try_compile "$LINENO" then : - ac_cv_sys_largefile_CC=' -n32'; break + +else case e in #( + e) CPPFLAGS="$CPPFLAGS -D_FILE_OFFSET_BITS=64" + if ac_fn_c_try_compile "$LINENO" +then : + ac_opt='-D_FILE_OFFSET_BITS=64' +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam - break - done - CC=$ac_save_CC - rm -f conftest.$ac_ext - fi fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_largefile_CC" >&5 -printf "%s\n" "$ac_cv_sys_largefile_CC" >&6; } - if test "$ac_cv_sys_largefile_CC" != no; then - CC=$CC$ac_cv_sys_largefile_CC - fi - - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for _FILE_OFFSET_BITS value needed for large files" >&5 -printf %s "checking for _FILE_OFFSET_BITS value needed for large files... " >&6; } -if test ${ac_cv_sys_file_offset_bits+y} -then : - printf %s "(cached) " >&6 -else $as_nop - while :; do - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#include - /* Check that off_t can represent 2**63 - 1 correctly. - We can't simply define LARGE_OFF_T to be 9223372036854775807, - since some C++ compilers masquerading as C compilers - incorrectly reject 9223372036854775807. */ -#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31)) - int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721 - && LARGE_OFF_T % 2147483647 == 1) - ? 1 : -1]; -int -main (void) -{ - - ; - return 0; -} -_ACEOF -if ac_fn_c_try_compile "$LINENO" -then : - ac_cv_sys_file_offset_bits=no; break + ac_cv_sys_largefile_opts=$ac_opt + ac_opt_found=yes fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#define _FILE_OFFSET_BITS 64 -#include - /* Check that off_t can represent 2**63 - 1 correctly. - We can't simply define LARGE_OFF_T to be 9223372036854775807, - since some C++ compilers masquerading as C compilers - incorrectly reject 9223372036854775807. */ -#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31)) - int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721 - && LARGE_OFF_T % 2147483647 == 1) - ? 1 : -1]; -int -main (void) -{ + test $ac_opt_found = no || break + done + CPPFLAGS=$ac_save_CPPFLAGS - ; - return 0; -} -_ACEOF -if ac_fn_c_try_compile "$LINENO" -then : - ac_cv_sys_file_offset_bits=64; break -fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext - ac_cv_sys_file_offset_bits=unknown - break -done + test $ac_opt_found = yes || ac_cv_sys_largefile_opts="support not detected" ;; +esac fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_file_offset_bits" >&5 -printf "%s\n" "$ac_cv_sys_file_offset_bits" >&6; } -case $ac_cv_sys_file_offset_bits in #( - no | unknown) ;; - *) -printf "%s\n" "#define _FILE_OFFSET_BITS $ac_cv_sys_file_offset_bits" >>confdefs.h -;; +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_largefile_opts" >&5 +printf "%s\n" "$ac_cv_sys_largefile_opts" >&6; } + +ac_have_largefile=yes +case $ac_cv_sys_largefile_opts in #( + "none needed") : + ;; #( + "supported through gnulib") : + ;; #( + "support not detected") : + ac_have_largefile=no ;; #( + "-D_FILE_OFFSET_BITS=64") : + +printf "%s\n" "#define _FILE_OFFSET_BITS 64" >>confdefs.h + ;; #( + "-D_LARGE_FILES=1") : + +printf "%s\n" "#define _LARGE_FILES 1" >>confdefs.h + ;; #( + *) : + as_fn_error $? "internal error: bad value for \$ac_cv_sys_largefile_opts" "$LINENO" 5 ;; esac -rm -rf conftest* - if test $ac_cv_sys_file_offset_bits = unknown; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for _LARGE_FILES value needed for large files" >&5 -printf %s "checking for _LARGE_FILES value needed for large files... " >&6; } -if test ${ac_cv_sys_large_files+y} + +if test "$enable_year2038" != no +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CPPFLAGS option for timestamps after 2038" >&5 +printf %s "checking for $CPPFLAGS option for timestamps after 2038... " >&6; } +if test ${ac_cv_sys_year2038_opts+y} then : printf %s "(cached) " >&6 -else $as_nop - while :; do - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#include - /* Check that off_t can represent 2**63 - 1 correctly. - We can't simply define LARGE_OFF_T to be 9223372036854775807, - since some C++ compilers masquerading as C compilers - incorrectly reject 9223372036854775807. */ -#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31)) - int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721 - && LARGE_OFF_T % 2147483647 == 1) - ? 1 : -1]; -int -main (void) -{ - - ; - return 0; -} -_ACEOF -if ac_fn_c_try_compile "$LINENO" +else case e in #( + e) ac_save_CPPFLAGS="$CPPFLAGS" + ac_opt_found=no + for ac_opt in "none needed" "-D_TIME_BITS=64" "-D__MINGW_USE_VC2005_COMPAT" "-U_USE_32_BIT_TIME_T -D__MINGW_USE_VC2005_COMPAT"; do + if test x"$ac_opt" != x"none needed" then : - ac_cv_sys_large_files=no; break + CPPFLAGS="$ac_save_CPPFLAGS $ac_opt" fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext - cat confdefs.h - <<_ACEOF >conftest.$ac_ext + cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ -#define _LARGE_FILES 1 -#include - /* Check that off_t can represent 2**63 - 1 correctly. - We can't simply define LARGE_OFF_T to be 9223372036854775807, - since some C++ compilers masquerading as C compilers - incorrectly reject 9223372036854775807. */ -#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31)) - int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721 - && LARGE_OFF_T % 2147483647 == 1) - ? 1 : -1]; + + #include + /* Check that time_t can represent 2**32 - 1 correctly. */ + #define LARGE_TIME_T \\ + ((time_t) (((time_t) 1 << 30) - 1 + 3 * ((time_t) 1 << 30))) + int verify_time_t_range[(LARGE_TIME_T / 65537 == 65535 + && LARGE_TIME_T % 65537 == 0) + ? 1 : -1]; + int main (void) { @@ -7655,26 +7848,48 @@ _ACEOF if ac_fn_c_try_compile "$LINENO" then : - ac_cv_sys_large_files=1; break + ac_cv_sys_year2038_opts="$ac_opt" + ac_opt_found=yes fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext - ac_cv_sys_large_files=unknown - break -done + test $ac_opt_found = no || break + done + CPPFLAGS="$ac_save_CPPFLAGS" + test $ac_opt_found = yes || ac_cv_sys_year2038_opts="support not detected" ;; +esac fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_large_files" >&5 -printf "%s\n" "$ac_cv_sys_large_files" >&6; } -case $ac_cv_sys_large_files in #( - no | unknown) ;; - *) -printf "%s\n" "#define _LARGE_FILES $ac_cv_sys_large_files" >>confdefs.h -;; +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_year2038_opts" >&5 +printf "%s\n" "$ac_cv_sys_year2038_opts" >&6; } + +ac_have_year2038=yes +case $ac_cv_sys_year2038_opts in #( + "none needed") : + ;; #( + "support not detected") : + ac_have_year2038=no ;; #( + "-D_TIME_BITS=64") : + +printf "%s\n" "#define _TIME_BITS 64" >>confdefs.h + ;; #( + "-D__MINGW_USE_VC2005_COMPAT") : + +printf "%s\n" "#define __MINGW_USE_VC2005_COMPAT 1" >>confdefs.h + ;; #( + "-U_USE_32_BIT_TIME_T"*) : + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} +as_fn_error $? "the 'time_t' type is currently forced to be 32-bit. It +will stop working after mid-January 2038. Remove +_USE_32BIT_TIME_T from the compiler flags. +See 'config.log' for more details" "$LINENO" 5; } ;; #( + *) : + as_fn_error $? "internal error: bad value for \$ac_cv_sys_year2038_opts" "$LINENO" 5 ;; esac -rm -rf conftest* - fi + fi fi +fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable ipv6" >&5 printf %s "checking whether to enable ipv6... " >&6; } @@ -7693,43 +7908,41 @@ ;; esac -else $as_nop - if test "$cross_compiling" = yes -then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } - -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ - /* AF_INET6 availability check */ -#include + #include #include -int main() +#include + +int +main (void) { - if (socket(AF_INET6, SOCK_STREAM, 0) < 0) - exit(1); - else - exit(0); -} +struct sockaddr_in6 sa6; +(void)sa6; +(void)AF_INET6; + + ; + return 0; +} _ACEOF -if ac_fn_c_try_run "$LINENO" +if ac_fn_c_try_compile "$LINENO" then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 printf "%s\n" "yes" >&6; } printf "%s\n" "#define INET6 1" >>confdefs.h -else $as_nop - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } + ;; +esac fi -rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext -fi - +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi @@ -7770,16 +7983,22 @@ if test ${ac_cv_lib_z_deflateParams+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_check_lib_save_LIBS=$LIBS +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lz $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char deflateParams (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char deflateParams (void); int main (void) { @@ -7791,12 +8010,14 @@ if ac_fn_c_try_link "$LINENO" then : ac_cv_lib_z_deflateParams=yes -else $as_nop - ac_cv_lib_z_deflateParams=no +else case e in #( + e) ac_cv_lib_z_deflateParams=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_z_deflateParams" >&5 printf "%s\n" "$ac_cv_lib_z_deflateParams" >&6; } @@ -7806,8 +8027,9 @@ LIBS="-lz $LIBS" -else $as_nop - with_included_zlib=yes +else case e in #( + e) with_included_zlib=yes ;; +esac fi fi @@ -7845,15 +8067,21 @@ if test ${ac_cv_search_EVP_MD_CTX_copy+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_func_search_save_LIBS=$LIBS +else case e in #( + e) ac_func_search_save_LIBS=$LIBS cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char EVP_MD_CTX_copy (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char EVP_MD_CTX_copy (void); int main (void) { @@ -7884,11 +8112,13 @@ if test ${ac_cv_search_EVP_MD_CTX_copy+y} then : -else $as_nop - ac_cv_search_EVP_MD_CTX_copy=no +else case e in #( + e) ac_cv_search_EVP_MD_CTX_copy=no ;; +esac fi rm conftest.$ac_ext -LIBS=$ac_func_search_save_LIBS +LIBS=$ac_func_search_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_EVP_MD_CTX_copy" >&5 printf "%s\n" "$ac_cv_search_EVP_MD_CTX_copy" >&6; } @@ -7899,9 +8129,10 @@ printf "%s\n" "#define USE_OPENSSL 1" >>confdefs.h enable_openssl=yes -else $as_nop - err_msg="$err_msg$nl- Failed to find EVP_MD_CTX_copy function in openssl crypto lib."; - no_lib="$no_lib openssl" +else case e in #( + e) err_msg="$err_msg$nl- Failed to find EVP_MD_CTX_copy function in openssl crypto lib."; + no_lib="$no_lib openssl" ;; +esac fi else @@ -8009,15 +8240,21 @@ if test ${ac_cv_search_XXH64_createState+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_func_search_save_LIBS=$LIBS +else case e in #( + e) ac_func_search_save_LIBS=$LIBS cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char XXH64_createState (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char XXH64_createState (void); int main (void) { @@ -8048,11 +8285,13 @@ if test ${ac_cv_search_XXH64_createState+y} then : -else $as_nop - ac_cv_search_XXH64_createState=no +else case e in #( + e) ac_cv_search_XXH64_createState=no ;; +esac fi rm conftest.$ac_ext -LIBS=$ac_func_search_save_LIBS +LIBS=$ac_func_search_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_XXH64_createState" >&5 printf "%s\n" "$ac_cv_search_XXH64_createState" >&6; } @@ -8062,9 +8301,10 @@ test "$ac_res" = "none required" || LIBS="$ac_res $LIBS" printf "%s\n" "#define SUPPORT_XXHASH 1" >>confdefs.h -else $as_nop - err_msg="$err_msg$nl- Failed to find XXH64_createState function in xxhash lib."; - no_lib="$no_lib xxhash" +else case e in #( + e) err_msg="$err_msg$nl- Failed to find XXH64_createState function in xxhash lib."; + no_lib="$no_lib xxhash" ;; +esac fi else @@ -8096,15 +8336,21 @@ if test ${ac_cv_search_ZSTD_minCLevel+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_func_search_save_LIBS=$LIBS +else case e in #( + e) ac_func_search_save_LIBS=$LIBS cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char ZSTD_minCLevel (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char ZSTD_minCLevel (void); int main (void) { @@ -8135,11 +8381,13 @@ if test ${ac_cv_search_ZSTD_minCLevel+y} then : -else $as_nop - ac_cv_search_ZSTD_minCLevel=no +else case e in #( + e) ac_cv_search_ZSTD_minCLevel=no ;; +esac fi rm conftest.$ac_ext -LIBS=$ac_func_search_save_LIBS +LIBS=$ac_func_search_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_ZSTD_minCLevel" >&5 printf "%s\n" "$ac_cv_search_ZSTD_minCLevel" >&6; } @@ -8149,9 +8397,10 @@ test "$ac_res" = "none required" || LIBS="$ac_res $LIBS" printf "%s\n" "#define SUPPORT_ZSTD 1" >>confdefs.h -else $as_nop - err_msg="$err_msg$nl- Failed to find ZSTD_minCLevel function in zstd lib."; - no_lib="$no_lib zstd" +else case e in #( + e) err_msg="$err_msg$nl- Failed to find ZSTD_minCLevel function in zstd lib."; + no_lib="$no_lib zstd" ;; +esac fi else @@ -8183,15 +8432,21 @@ if test ${ac_cv_search_LZ4_compress_default+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_func_search_save_LIBS=$LIBS +else case e in #( + e) ac_func_search_save_LIBS=$LIBS cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char LZ4_compress_default (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char LZ4_compress_default (void); int main (void) { @@ -8222,11 +8477,13 @@ if test ${ac_cv_search_LZ4_compress_default+y} then : -else $as_nop - ac_cv_search_LZ4_compress_default=no +else case e in #( + e) ac_cv_search_LZ4_compress_default=no ;; +esac fi rm conftest.$ac_ext -LIBS=$ac_func_search_save_LIBS +LIBS=$ac_func_search_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_LZ4_compress_default" >&5 printf "%s\n" "$ac_cv_search_LZ4_compress_default" >&6; } @@ -8236,9 +8493,10 @@ test "$ac_res" = "none required" || LIBS="$ac_res $LIBS" printf "%s\n" "#define SUPPORT_LZ4 1" >>confdefs.h -else $as_nop - err_msg="$err_msg$nl- Failed to find LZ4_compress_default function in lz4 lib."; - no_lib="$no_lib lz4" +else case e in #( + e) err_msg="$err_msg$nl- Failed to find LZ4_compress_default function in lz4 lib."; + no_lib="$no_lib lz4" ;; +esac fi else @@ -8274,13 +8532,13 @@ if test ${rsync_cv_MAKEDEV_TAKES_3_ARGS+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) if test "$cross_compiling" = yes then : rsync_cv_MAKEDEV_TAKES_3_ARGS=no -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #ifdef HAVE_SYS_TYPES_H @@ -8307,13 +8565,16 @@ if ac_fn_c_try_run "$LINENO" then : rsync_cv_MAKEDEV_TAKES_3_ARGS=yes -else $as_nop - rsync_cv_MAKEDEV_TAKES_3_ARGS=no +else case e in #( + e) rsync_cv_MAKEDEV_TAKES_3_ARGS=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_MAKEDEV_TAKES_3_ARGS" >&5 printf "%s\n" "$rsync_cv_MAKEDEV_TAKES_3_ARGS" >&6; } @@ -8325,28 +8586,30 @@ # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of int" >&5 printf %s "checking size of int... " >&6; } if test ${ac_cv_sizeof_int+y} then : printf %s "(cached) " >&6 -else $as_nop - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (int))" "ac_cv_sizeof_int" "$ac_includes_default" +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (int))" "ac_cv_sizeof_int" "$ac_includes_default" then : -else $as_nop - if test "$ac_cv_type_int" = yes; then - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) if test "$ac_cv_type_int" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (int) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_int=0 - fi + fi ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_int" >&5 printf "%s\n" "$ac_cv_sizeof_int" >&6; } @@ -8358,28 +8621,30 @@ # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of long" >&5 printf %s "checking size of long... " >&6; } if test ${ac_cv_sizeof_long+y} then : printf %s "(cached) " >&6 -else $as_nop - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (long))" "ac_cv_sizeof_long" "$ac_includes_default" +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (long))" "ac_cv_sizeof_long" "$ac_includes_default" then : -else $as_nop - if test "$ac_cv_type_long" = yes; then - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) if test "$ac_cv_type_long" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (long) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_long=0 - fi + fi ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_long" >&5 printf "%s\n" "$ac_cv_sizeof_long" >&6; } @@ -8391,28 +8656,30 @@ # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of long long" >&5 printf %s "checking size of long long... " >&6; } if test ${ac_cv_sizeof_long_long+y} then : printf %s "(cached) " >&6 -else $as_nop - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (long long))" "ac_cv_sizeof_long_long" "$ac_includes_default" +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (long long))" "ac_cv_sizeof_long_long" "$ac_includes_default" then : -else $as_nop - if test "$ac_cv_type_long_long" = yes; then - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) if test "$ac_cv_type_long_long" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (long long) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_long_long=0 - fi + fi ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_long_long" >&5 printf "%s\n" "$ac_cv_sizeof_long_long" >&6; } @@ -8424,28 +8691,30 @@ # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of short" >&5 printf %s "checking size of short... " >&6; } if test ${ac_cv_sizeof_short+y} then : printf %s "(cached) " >&6 -else $as_nop - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (short))" "ac_cv_sizeof_short" "$ac_includes_default" +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (short))" "ac_cv_sizeof_short" "$ac_includes_default" then : -else $as_nop - if test "$ac_cv_type_short" = yes; then - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) if test "$ac_cv_type_short" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (short) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_short=0 - fi + fi ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_short" >&5 printf "%s\n" "$ac_cv_sizeof_short" >&6; } @@ -8457,28 +8726,30 @@ # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of int16_t" >&5 printf %s "checking size of int16_t... " >&6; } if test ${ac_cv_sizeof_int16_t+y} then : printf %s "(cached) " >&6 -else $as_nop - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (int16_t))" "ac_cv_sizeof_int16_t" "$ac_includes_default" +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (int16_t))" "ac_cv_sizeof_int16_t" "$ac_includes_default" then : -else $as_nop - if test "$ac_cv_type_int16_t" = yes; then - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) if test "$ac_cv_type_int16_t" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (int16_t) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_int16_t=0 - fi + fi ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_int16_t" >&5 printf "%s\n" "$ac_cv_sizeof_int16_t" >&6; } @@ -8490,28 +8761,30 @@ # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of uint16_t" >&5 printf %s "checking size of uint16_t... " >&6; } if test ${ac_cv_sizeof_uint16_t+y} then : printf %s "(cached) " >&6 -else $as_nop - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (uint16_t))" "ac_cv_sizeof_uint16_t" "$ac_includes_default" +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (uint16_t))" "ac_cv_sizeof_uint16_t" "$ac_includes_default" then : -else $as_nop - if test "$ac_cv_type_uint16_t" = yes; then - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) if test "$ac_cv_type_uint16_t" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (uint16_t) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_uint16_t=0 - fi + fi ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_uint16_t" >&5 printf "%s\n" "$ac_cv_sizeof_uint16_t" >&6; } @@ -8523,28 +8796,30 @@ # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of int32_t" >&5 printf %s "checking size of int32_t... " >&6; } if test ${ac_cv_sizeof_int32_t+y} then : printf %s "(cached) " >&6 -else $as_nop - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (int32_t))" "ac_cv_sizeof_int32_t" "$ac_includes_default" +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (int32_t))" "ac_cv_sizeof_int32_t" "$ac_includes_default" then : -else $as_nop - if test "$ac_cv_type_int32_t" = yes; then - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) if test "$ac_cv_type_int32_t" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (int32_t) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_int32_t=0 - fi + fi ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_int32_t" >&5 printf "%s\n" "$ac_cv_sizeof_int32_t" >&6; } @@ -8556,28 +8831,30 @@ # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of uint32_t" >&5 printf %s "checking size of uint32_t... " >&6; } if test ${ac_cv_sizeof_uint32_t+y} then : printf %s "(cached) " >&6 -else $as_nop - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (uint32_t))" "ac_cv_sizeof_uint32_t" "$ac_includes_default" +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (uint32_t))" "ac_cv_sizeof_uint32_t" "$ac_includes_default" then : -else $as_nop - if test "$ac_cv_type_uint32_t" = yes; then - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) if test "$ac_cv_type_uint32_t" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (uint32_t) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_uint32_t=0 - fi + fi ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_uint32_t" >&5 printf "%s\n" "$ac_cv_sizeof_uint32_t" >&6; } @@ -8589,28 +8866,30 @@ # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of int64_t" >&5 printf %s "checking size of int64_t... " >&6; } if test ${ac_cv_sizeof_int64_t+y} then : printf %s "(cached) " >&6 -else $as_nop - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (int64_t))" "ac_cv_sizeof_int64_t" "$ac_includes_default" +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (int64_t))" "ac_cv_sizeof_int64_t" "$ac_includes_default" then : -else $as_nop - if test "$ac_cv_type_int64_t" = yes; then - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) if test "$ac_cv_type_int64_t" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (int64_t) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_int64_t=0 - fi + fi ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_int64_t" >&5 printf "%s\n" "$ac_cv_sizeof_int64_t" >&6; } @@ -8622,28 +8901,30 @@ # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of off_t" >&5 printf %s "checking size of off_t... " >&6; } if test ${ac_cv_sizeof_off_t+y} then : printf %s "(cached) " >&6 -else $as_nop - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (off_t))" "ac_cv_sizeof_off_t" "$ac_includes_default" +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (off_t))" "ac_cv_sizeof_off_t" "$ac_includes_default" then : -else $as_nop - if test "$ac_cv_type_off_t" = yes; then - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) if test "$ac_cv_type_off_t" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (off_t) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_off_t=0 - fi + fi ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_off_t" >&5 printf "%s\n" "$ac_cv_sizeof_off_t" >&6; } @@ -8655,28 +8936,30 @@ # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of off64_t" >&5 printf %s "checking size of off64_t... " >&6; } if test ${ac_cv_sizeof_off64_t+y} then : printf %s "(cached) " >&6 -else $as_nop - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (off64_t))" "ac_cv_sizeof_off64_t" "$ac_includes_default" +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (off64_t))" "ac_cv_sizeof_off64_t" "$ac_includes_default" then : -else $as_nop - if test "$ac_cv_type_off64_t" = yes; then - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) if test "$ac_cv_type_off64_t" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (off64_t) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_off64_t=0 - fi + fi ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_off64_t" >&5 printf "%s\n" "$ac_cv_sizeof_off64_t" >&6; } @@ -8688,28 +8971,30 @@ # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of time_t" >&5 printf %s "checking size of time_t... " >&6; } if test ${ac_cv_sizeof_time_t+y} then : printf %s "(cached) " >&6 -else $as_nop - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (time_t))" "ac_cv_sizeof_time_t" "$ac_includes_default" +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (time_t))" "ac_cv_sizeof_time_t" "$ac_includes_default" then : -else $as_nop - if test "$ac_cv_type_time_t" = yes; then - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) if test "$ac_cv_type_time_t" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (time_t) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_time_t=0 - fi + fi ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_time_t" >&5 printf "%s\n" "$ac_cv_sizeof_time_t" >&6; } @@ -8721,28 +9006,30 @@ # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of char*" >&5 printf %s "checking size of char*... " >&6; } if test ${ac_cv_sizeof_charp+y} then : printf %s "(cached) " >&6 -else $as_nop - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (char*))" "ac_cv_sizeof_charp" "$ac_includes_default" +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (char*))" "ac_cv_sizeof_charp" "$ac_includes_default" then : -else $as_nop - if test "$ac_cv_type_charp" = yes; then - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) if test "$ac_cv_type_charp" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (char*) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_charp=0 - fi + fi ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_charp" >&5 printf "%s\n" "$ac_cv_sizeof_charp" >&6; } @@ -8758,8 +9045,8 @@ if test ${ac_cv_c_inline+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_cv_c_inline=no +else case e in #( + e) ac_cv_c_inline=no for ac_kw in inline __inline__ __inline; do cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -8777,7 +9064,8 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext test "$ac_cv_c_inline" != no && break done - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_inline" >&5 printf "%s\n" "$ac_cv_c_inline" >&6; } @@ -8804,8 +9092,8 @@ if test ${ac_cv_type_long_double_wider+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include long double const a[] = @@ -8839,10 +9127,12 @@ if ac_fn_c_try_compile "$LINENO" then : ac_cv_type_long_double_wider=yes -else $as_nop - ac_cv_type_long_double_wider=no +else case e in #( + e) ac_cv_type_long_double_wider=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_type_long_double_wider" >&5 printf "%s\n" "$ac_cv_type_long_double_wider" >&6; } @@ -8859,37 +9149,26 @@ fi - -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for uid_t in sys/types.h" >&5 -printf %s "checking for uid_t in sys/types.h... " >&6; } -if test ${ac_cv_type_uid_t+y} +ac_fn_c_check_type "$LINENO" "uid_t" "ac_cv_type_uid_t" "$ac_includes_default" +if test "x$ac_cv_type_uid_t" = xyes then : - printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#include - -_ACEOF -if (eval "$ac_cpp conftest.$ac_ext") 2>&5 | - $EGREP "uid_t" >/dev/null 2>&1 -then : - ac_cv_type_uid_t=yes -else $as_nop - ac_cv_type_uid_t=no -fi -rm -rf conftest* - -fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_type_uid_t" >&5 -printf "%s\n" "$ac_cv_type_uid_t" >&6; } -if test $ac_cv_type_uid_t = no; then +else case e in #( + e) printf "%s\n" "#define uid_t int" >>confdefs.h + ;; +esac +fi +ac_fn_c_check_type "$LINENO" "gid_t" "ac_cv_type_gid_t" "$ac_includes_default" +if test "x$ac_cv_type_gid_t" = xyes +then : +else case e in #( + e) printf "%s\n" "#define gid_t int" >>confdefs.h - + ;; +esac fi ac_fn_c_check_type "$LINENO" "mode_t" "ac_cv_type_mode_t" "$ac_includes_default" @@ -8939,67 +9218,118 @@ if test ${ac_cv_type_getgroups+y} then : printf %s "(cached) " >&6 -else $as_nop - if test "$cross_compiling" = yes +else case e in #( + e) # If AC_TYPE_UID_T says there isn't any gid_t typedef, then we can skip +# everything below. +if test $ac_cv_type_gid_t = no then : - ac_cv_type_getgroups=cross -else $as_nop + ac_cv_type_getgroups=int +else case e in #( + e) # Test programs below rely on strict type checking of extern declarations: + # 'extern int getgroups(int, int *); extern int getgroups(int, pid_t *);' + # is valid in C89 if and only if pid_t is a typedef for int. Unlike + # anything involving either an assignment or a function call, compilers + # tend to make this kind of type mismatch a hard error, not just an + # "incompatible pointer types" warning. cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ -/* Thanks to Mike Rendell for this test. */ $ac_includes_default -#define NGID 256 -#undef MAX -#define MAX(x, y) ((x) > (y) ? (x) : (y)) - +extern int getgroups(int, gid_t *); int main (void) { - gid_t gidset[NGID]; - int i, n; - union { gid_t gval; long int lval; } val; - - val.lval = -1; - for (i = 0; i < NGID; i++) - gidset[i] = val.gval; - n = getgroups (sizeof (gidset) / MAX (sizeof (int), sizeof (gid_t)) - 1, - gidset); - /* Exit non-zero if getgroups seems to require an array of ints. This - happens when gid_t is short int but getgroups modifies an array - of ints. */ - return n > 0 && gidset[n] != val.gval; +return !(getgroups(0, 0) >= 0); + ; + return 0; } _ACEOF -if ac_fn_c_try_run "$LINENO" +if ac_fn_c_try_compile "$LINENO" then : - ac_cv_type_getgroups=gid_t -else $as_nop - ac_cv_type_getgroups=int + ac_getgroups_gidarray=yes +else case e in #( + e) ac_getgroups_gidarray=no ;; +esac fi -rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$ac_includes_default +extern int getgroups(int, int *); +int +main (void) +{ +return !(getgroups(0, 0) >= 0); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_getgroups_intarray=yes +else case e in #( + e) ac_getgroups_intarray=no ;; +esac fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext -if test $ac_cv_type_getgroups = cross; then - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#include + case int:$ac_getgroups_intarray,gid:$ac_getgroups_gidarray in #( + int:yes,gid:no) : + ac_cv_type_getgroups=int ;; #( + int:no,gid:yes) : + ac_cv_type_getgroups=gid_t ;; #( + int:yes,gid:yes) : + # Both programs compiled - this means *either* that getgroups + # was declared with no prototype, in which case we should use int, + # or that it was declared prototyped but gid_t is a typedef for int, + # in which case we should use gid_t. Distinguish the two cases + # by testing if the compiler catches a blatantly incorrect function + # signature for getgroups. + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$ac_includes_default +extern int getgroups(int, float); +int +main (void) +{ +return !(getgroups(0, 0) >= 0); + ; + return 0; +} _ACEOF -if (eval "$ac_cpp conftest.$ac_ext") 2>&5 | - $EGREP "getgroups.*int.*gid_t" >/dev/null 2>&1 +if ac_fn_c_try_compile "$LINENO" then : - ac_cv_type_getgroups=gid_t -else $as_nop - ac_cv_type_getgroups=int + + # Compiler did not catch incorrect argument list; + # getgroups is unprototyped. + ac_cv_type_getgroups=int + +else case e in #( + e) + # Compiler caught incorrect argument list; + # gid_t is a typedef for int. + ac_cv_type_getgroups=gid_t + ;; +esac fi -rm -rf conftest* +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; #( + *) : + # Both programs failed to compile - this probably means getgroups + # wasn't declared at all. Use 'int', as this is probably a very + # old system where the type _would have been_ int. + ac_cv_type_getgroups=int + ;; +esac + ;; +esac fi + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_type_getgroups" >&5 printf "%s\n" "$ac_cv_type_getgroups" >&6; } - printf "%s\n" "#define GETGROUPS_T $ac_cv_type_getgroups" >>confdefs.h @@ -9089,15 +9419,15 @@ if test "x$ac_cv_type_socklen_t" = xyes then : -else $as_nop - +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for socklen_t equivalent" >&5 printf %s "checking for socklen_t equivalent... " >&6; } if test ${rsync_cv_socklen_t_equiv+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) # Systems have either "struct sockaddr *" or # "void *" as the second argument to getpeername rsync_cv_socklen_t_equiv= @@ -9136,14 +9466,16 @@ if test "x$rsync_cv_socklen_t_equiv" = x; then as_fn_error $? "Cannot find a type to use in place of socklen_t" "$LINENO" 5 fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_socklen_t_equiv" >&5 printf "%s\n" "$rsync_cv_socklen_t_equiv" >&6; } printf "%s\n" "#define socklen_t $rsync_cv_socklen_t_equiv" >>confdefs.h - + ;; +esac fi @@ -9153,8 +9485,8 @@ if test ${rsync_cv_errno+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include @@ -9169,10 +9501,12 @@ if ac_fn_c_try_compile "$LINENO" then : rsync_cv_errno=yes -else $as_nop - rsync_cv_have_errno_decl=no +else case e in #( + e) rsync_cv_have_errno_decl=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_errno" >&5 printf "%s\n" "$rsync_cv_errno" >&6; } @@ -9205,16 +9539,22 @@ if test ${ac_cv_lib_nsl_s_printf+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_check_lib_save_LIBS=$LIBS +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lnsl_s $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char printf (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char printf (void); int main (void) { @@ -9226,12 +9566,14 @@ if ac_fn_c_try_link "$LINENO" then : ac_cv_lib_nsl_s_printf=yes -else $as_nop - ac_cv_lib_nsl_s_printf=no +else case e in #( + e) ac_cv_lib_nsl_s_printf=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_nsl_s_printf" >&5 printf "%s\n" "$ac_cv_lib_nsl_s_printf" >&6; } @@ -9251,16 +9593,22 @@ if test ${ac_cv_lib_nsl_printf+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_check_lib_save_LIBS=$LIBS +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lnsl $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char printf (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char printf (void); int main (void) { @@ -9272,12 +9620,14 @@ if ac_fn_c_try_link "$LINENO" then : ac_cv_lib_nsl_printf=yes -else $as_nop - ac_cv_lib_nsl_printf=no +else case e in #( + e) ac_cv_lib_nsl_printf=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_nsl_printf" >&5 printf "%s\n" "$ac_cv_lib_nsl_printf" >&6; } @@ -9297,16 +9647,22 @@ if test ${ac_cv_lib_socket_connect+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_check_lib_save_LIBS=$LIBS +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lsocket $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char connect (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char connect (void); int main (void) { @@ -9318,12 +9674,14 @@ if ac_fn_c_try_link "$LINENO" then : ac_cv_lib_socket_connect=yes -else $as_nop - ac_cv_lib_socket_connect=no +else case e in #( + e) ac_cv_lib_socket_connect=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_socket_connect" >&5 printf "%s\n" "$ac_cv_lib_socket_connect" >&6; } @@ -9343,16 +9701,22 @@ if test ${ac_cv_lib_inet_connect+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_check_lib_save_LIBS=$LIBS +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-linet $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char connect (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char connect (void); int main (void) { @@ -9364,12 +9728,14 @@ if ac_fn_c_try_link "$LINENO" then : ac_cv_lib_inet_connect=yes -else $as_nop - ac_cv_lib_inet_connect=no +else case e in #( + e) ac_cv_lib_inet_connect=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_inet_connect" >&5 printf "%s\n" "$ac_cv_lib_inet_connect" >&6; } @@ -9397,15 +9763,21 @@ if test ${ac_cv_search_inet_ntop+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_func_search_save_LIBS=$LIBS +else case e in #( + e) ac_func_search_save_LIBS=$LIBS cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char inet_ntop (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char inet_ntop (void); int main (void) { @@ -9436,11 +9808,13 @@ if test ${ac_cv_search_inet_ntop+y} then : -else $as_nop - ac_cv_search_inet_ntop=no +else case e in #( + e) ac_cv_search_inet_ntop=no ;; +esac fi rm conftest.$ac_ext -LIBS=$ac_func_search_save_LIBS +LIBS=$ac_func_search_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_inet_ntop" >&5 printf "%s\n" "$ac_cv_search_inet_ntop" >&6; } @@ -9460,15 +9834,21 @@ if test ${ac_cv_search_iconv_open+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_func_search_save_LIBS=$LIBS +else case e in #( + e) ac_func_search_save_LIBS=$LIBS cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char iconv_open (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char iconv_open (void); int main (void) { @@ -9499,11 +9879,13 @@ if test ${ac_cv_search_iconv_open+y} then : -else $as_nop - ac_cv_search_iconv_open=no +else case e in #( + e) ac_cv_search_iconv_open=no ;; +esac fi rm conftest.$ac_ext -LIBS=$ac_func_search_save_LIBS +LIBS=$ac_func_search_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_iconv_open" >&5 printf "%s\n" "$ac_cv_search_iconv_open" >&6; } @@ -9519,15 +9901,21 @@ if test ${ac_cv_search_libiconv_open+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_func_search_save_LIBS=$LIBS +else case e in #( + e) ac_func_search_save_LIBS=$LIBS cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char libiconv_open (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char libiconv_open (void); int main (void) { @@ -9558,11 +9946,13 @@ if test ${ac_cv_search_libiconv_open+y} then : -else $as_nop - ac_cv_search_libiconv_open=no +else case e in #( + e) ac_cv_search_libiconv_open=no ;; +esac fi rm conftest.$ac_ext -LIBS=$ac_func_search_save_LIBS +LIBS=$ac_func_search_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_libiconv_open" >&5 printf "%s\n" "$ac_cv_search_libiconv_open" >&6; } @@ -9579,8 +9969,8 @@ if test ${am_cv_proto_iconv+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -9609,11 +9999,13 @@ if ac_fn_c_try_compile "$LINENO" then : am_cv_proto_iconv_arg1="" -else $as_nop - am_cv_proto_iconv_arg1="const" +else case e in #( + e) am_cv_proto_iconv_arg1="const" ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext - am_cv_proto_iconv="extern size_t iconv (iconv_t cd, $am_cv_proto_iconv_arg1 char * *inbuf, size_t *inbytesleft, char * *outbuf, size_t *outbytesleft);" + am_cv_proto_iconv="extern size_t iconv (iconv_t cd, $am_cv_proto_iconv_arg1 char * *inbuf, size_t *inbytesleft, char * *outbuf, size_t *outbytesleft);" ;; +esac fi am_cv_proto_iconv=`echo "$am_cv_proto_iconv" | tr -s ' ' | sed 's/( /(/'` @@ -9631,26 +10023,28 @@ then : printf "%s\n" "#define HAVE_INET_NTOP 1" >>confdefs.h -else $as_nop - case " $LIBOBJS " in +else case e in #( + e) case " $LIBOBJS " in *" inet_ntop.$ac_objext "* ) ;; *) LIBOBJS="$LIBOBJS inet_ntop.$ac_objext" ;; esac - + ;; +esac fi ac_fn_c_check_func "$LINENO" "inet_pton" "ac_cv_func_inet_pton" if test "x$ac_cv_func_inet_pton" = xyes then : printf "%s\n" "#define HAVE_INET_PTON 1" >>confdefs.h -else $as_nop - case " $LIBOBJS " in +else case e in #( + e) case " $LIBOBJS " in *" inet_pton.$ac_objext "* ) ;; *) LIBOBJS="$LIBOBJS inet_pton.$ac_objext" ;; esac - + ;; +esac fi @@ -9661,8 +10055,8 @@ if eval test \${ac_cv_type_$cv+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $ac_includes_default @@ -9678,10 +10072,12 @@ if ac_fn_c_try_compile "$LINENO" then : eval "ac_cv_type_$cv=yes" -else $as_nop - eval "ac_cv_type_$cv=no" +else case e in #( + e) eval "ac_cv_type_$cv=no" ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi ac_foo=`eval echo \\$ac_cv_type_$cv` { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_foo" >&5 @@ -9711,8 +10107,8 @@ if eval test \${ac_cv_type_$cv+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $ac_includes_default @@ -9734,10 +10130,12 @@ if ac_fn_c_try_compile "$LINENO" then : eval "ac_cv_type_$cv=yes" -else $as_nop - eval "ac_cv_type_$cv=no" +else case e in #( + e) eval "ac_cv_type_$cv=no" ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi ac_foo=`eval echo \\$ac_cv_type_$cv` { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_foo" >&5 @@ -9763,13 +10161,148 @@ # Irix 6.5 has getaddrinfo but not the corresponding defines, so use # builtin getaddrinfo if one of the defines don't exist + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for egrep -e" >&5 +printf %s "checking for egrep -e... " >&6; } +if test ${ac_cv_path_EGREP_TRADITIONAL+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -z "$EGREP_TRADITIONAL"; then + ac_path_EGREP_TRADITIONAL_found=false + # Loop through the user's path and test for each of PROGNAME-LIST + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH$PATH_SEPARATOR/usr/xpg4/bin +do + IFS=$as_save_IFS + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + for ac_prog in grep ggrep + do + for ac_exec_ext in '' $ac_executable_extensions; do + ac_path_EGREP_TRADITIONAL="$as_dir$ac_prog$ac_exec_ext" + as_fn_executable_p "$ac_path_EGREP_TRADITIONAL" || continue +# Check for GNU ac_path_EGREP_TRADITIONAL and select it if it is found. + # Check for GNU $ac_path_EGREP_TRADITIONAL +case `"$ac_path_EGREP_TRADITIONAL" --version 2>&1` in #( +*GNU*) + ac_cv_path_EGREP_TRADITIONAL="$ac_path_EGREP_TRADITIONAL" ac_path_EGREP_TRADITIONAL_found=:;; +#( +*) + ac_count=0 + printf %s 0123456789 >"conftest.in" + while : + do + cat "conftest.in" "conftest.in" >"conftest.tmp" + mv "conftest.tmp" "conftest.in" + cp "conftest.in" "conftest.nl" + printf "%s\n" 'EGREP_TRADITIONAL' >> "conftest.nl" + "$ac_path_EGREP_TRADITIONAL" -E 'EGR(EP|AC)_TRADITIONAL$' < "conftest.nl" >"conftest.out" 2>/dev/null || break + diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break + as_fn_arith $ac_count + 1 && ac_count=$as_val + if test $ac_count -gt ${ac_path_EGREP_TRADITIONAL_max-0}; then + # Best one so far, save it but keep looking for a better one + ac_cv_path_EGREP_TRADITIONAL="$ac_path_EGREP_TRADITIONAL" + ac_path_EGREP_TRADITIONAL_max=$ac_count + fi + # 10*(2^10) chars as input seems more than enough + test $ac_count -gt 10 && break + done + rm -f conftest.in conftest.tmp conftest.nl conftest.out;; +esac + + $ac_path_EGREP_TRADITIONAL_found && break 3 + done + done + done +IFS=$as_save_IFS + if test -z "$ac_cv_path_EGREP_TRADITIONAL"; then + : + fi +else + ac_cv_path_EGREP_TRADITIONAL=$EGREP_TRADITIONAL +fi + + if test "$ac_cv_path_EGREP_TRADITIONAL" +then : + ac_cv_path_EGREP_TRADITIONAL="$ac_cv_path_EGREP_TRADITIONAL -E" +else case e in #( + e) if test -z "$EGREP_TRADITIONAL"; then + ac_path_EGREP_TRADITIONAL_found=false + # Loop through the user's path and test for each of PROGNAME-LIST + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH$PATH_SEPARATOR/usr/xpg4/bin +do + IFS=$as_save_IFS + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + for ac_prog in egrep + do + for ac_exec_ext in '' $ac_executable_extensions; do + ac_path_EGREP_TRADITIONAL="$as_dir$ac_prog$ac_exec_ext" + as_fn_executable_p "$ac_path_EGREP_TRADITIONAL" || continue +# Check for GNU ac_path_EGREP_TRADITIONAL and select it if it is found. + # Check for GNU $ac_path_EGREP_TRADITIONAL +case `"$ac_path_EGREP_TRADITIONAL" --version 2>&1` in #( +*GNU*) + ac_cv_path_EGREP_TRADITIONAL="$ac_path_EGREP_TRADITIONAL" ac_path_EGREP_TRADITIONAL_found=:;; +#( +*) + ac_count=0 + printf %s 0123456789 >"conftest.in" + while : + do + cat "conftest.in" "conftest.in" >"conftest.tmp" + mv "conftest.tmp" "conftest.in" + cp "conftest.in" "conftest.nl" + printf "%s\n" 'EGREP_TRADITIONAL' >> "conftest.nl" + "$ac_path_EGREP_TRADITIONAL" 'EGR(EP|AC)_TRADITIONAL$' < "conftest.nl" >"conftest.out" 2>/dev/null || break + diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break + as_fn_arith $ac_count + 1 && ac_count=$as_val + if test $ac_count -gt ${ac_path_EGREP_TRADITIONAL_max-0}; then + # Best one so far, save it but keep looking for a better one + ac_cv_path_EGREP_TRADITIONAL="$ac_path_EGREP_TRADITIONAL" + ac_path_EGREP_TRADITIONAL_max=$ac_count + fi + # 10*(2^10) chars as input seems more than enough + test $ac_count -gt 10 && break + done + rm -f conftest.in conftest.tmp conftest.nl conftest.out;; +esac + + $ac_path_EGREP_TRADITIONAL_found && break 3 + done + done + done +IFS=$as_save_IFS + if test -z "$ac_cv_path_EGREP_TRADITIONAL"; then + as_fn_error $? "no acceptable egrep could be found in $PATH$PATH_SEPARATOR/usr/xpg4/bin" "$LINENO" 5 + fi +else + ac_cv_path_EGREP_TRADITIONAL=$EGREP_TRADITIONAL +fi + ;; +esac +fi ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_EGREP_TRADITIONAL" >&5 +printf "%s\n" "$ac_cv_path_EGREP_TRADITIONAL" >&6; } + EGREP_TRADITIONAL=$ac_cv_path_EGREP_TRADITIONAL + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether defines needed by getaddrinfo exist" >&5 printf %s "checking whether defines needed by getaddrinfo exist... " >&6; } if test ${rsync_cv_HAVE_GETADDR_DEFINES+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -9787,14 +10320,16 @@ #endif _ACEOF if (eval "$ac_cpp conftest.$ac_ext") 2>&5 | - $EGREP "yes" >/dev/null 2>&1 + $EGREP_TRADITIONAL "yes" >/dev/null 2>&1 then : rsync_cv_HAVE_GETADDR_DEFINES=yes -else $as_nop - rsync_cv_HAVE_GETADDR_DEFINES=no +else case e in #( + e) rsync_cv_HAVE_GETADDR_DEFINES=no ;; +esac fi rm -rf conftest* - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_GETADDR_DEFINES" >&5 printf "%s\n" "$rsync_cv_HAVE_GETADDR_DEFINES" >&6; } @@ -9812,8 +10347,8 @@ then : printf "%s\n" "#define HAVE_GETADDRINFO 1" >>confdefs.h -else $as_nop - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for getaddrinfo by including " >&5 +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for getaddrinfo by including " >&5 printf %s "checking for getaddrinfo by including ... " >&6; } cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -9840,29 +10375,32 @@ printf "%s\n" "#define HAVE_GETADDRINFO 1" >>confdefs.h -else $as_nop - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } case " $LIBOBJS " in *" getaddrinfo.$ac_objext "* ) ;; *) LIBOBJS="$LIBOBJS getaddrinfo.$ac_objext" ;; esac - + ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ - conftest$ac_exeext conftest.$ac_ext + conftest$ac_exeext conftest.$ac_ext ;; +esac fi done -else $as_nop - case " $LIBOBJS " in +else case e in #( + e) case " $LIBOBJS " in *" getaddrinfo.$ac_objext "* ) ;; *) LIBOBJS="$LIBOBJS getaddrinfo.$ac_objext" ;; esac - + ;; +esac fi ac_fn_c_check_member "$LINENO" "struct sockaddr" "sa_len" "ac_cv_member_struct_sockaddr_sa_len" " @@ -9943,8 +10481,8 @@ if eval test \${ac_cv_type_$cv+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $ac_includes_default @@ -9975,10 +10513,12 @@ if ac_fn_c_try_compile "$LINENO" then : eval "ac_cv_type_$cv=yes" -else $as_nop - eval "ac_cv_type_$cv=no" +else case e in #( + e) eval "ac_cv_type_$cv=no" ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi ac_foo=`eval echo \\$ac_cv_type_$cv` { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_foo" >&5 @@ -10004,6 +10544,24 @@ # if we can't find strcasecmp, look in -lresolv (for Unixware at least) # + + for ac_func in poll +do : + ac_fn_c_check_func "$LINENO" "poll" "ac_cv_func_poll" +if test "x$ac_cv_func_poll" = xyes +then : + printf "%s\n" "#define HAVE_POLL 1" >>confdefs.h + +else case e in #( + e) as_fn_error $? "rsync requires poll(); please report the platform to the rsync developers" "$LINENO" 5 ;; +esac +fi + +done +if test x"$ac_cv_header_poll_h" != x"yes"; then + as_fn_error $? "rsync requires ; please report the platform to the rsync developers" "$LINENO" 5 +fi + ac_fn_c_check_func "$LINENO" "strcasecmp" "ac_cv_func_strcasecmp" if test "x$ac_cv_func_strcasecmp" = xyes then : @@ -10017,16 +10575,22 @@ if test ${ac_cv_lib_resolv_strcasecmp+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_check_lib_save_LIBS=$LIBS +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lresolv $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char strcasecmp (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char strcasecmp (void); int main (void) { @@ -10038,12 +10602,14 @@ if ac_fn_c_try_link "$LINENO" then : ac_cv_lib_resolv_strcasecmp=yes -else $as_nop - ac_cv_lib_resolv_strcasecmp=no +else case e in #( + e) ac_cv_lib_resolv_strcasecmp=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_resolv_strcasecmp" >&5 printf "%s\n" "$ac_cv_lib_resolv_strcasecmp" >&6; } @@ -10070,16 +10636,22 @@ if test ${ac_cv_lib_sec_aclsort+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_check_lib_save_LIBS=$LIBS +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lsec $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char aclsort (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char aclsort (void); int main (void) { @@ -10091,12 +10663,14 @@ if ac_fn_c_try_link "$LINENO" then : ac_cv_lib_sec_aclsort=yes -else $as_nop - ac_cv_lib_sec_aclsort=no +else case e in #( + e) ac_cv_lib_sec_aclsort=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_sec_aclsort" >&5 printf "%s\n" "$ac_cv_lib_sec_aclsort" >&6; } @@ -10118,14 +10692,14 @@ if test ${ac_cv_func_utime_null+y} then : printf %s "(cached) " >&6 -else $as_nop - rm -f conftest.data; >conftest.data +else case e in #( + e) rm -f conftest.data; >conftest.data # Sequent interprets utime(file, 0) to mean use start of epoch. Wrong. if test "$cross_compiling" = yes then : ac_cv_func_utime_null='guessing yes' -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $ac_includes_default #ifdef HAVE_UTIME_H @@ -10147,13 +10721,16 @@ if ac_fn_c_try_run "$LINENO" then : ac_cv_func_utime_null=yes -else $as_nop - ac_cv_func_utime_null=no +else case e in #( + e) ac_cv_func_utime_null=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_func_utime_null" >&5 printf "%s\n" "$ac_cv_func_utime_null" >&6; } @@ -10169,10 +10746,11 @@ if test "x$ac_cv_type_size_t" = xyes then : -else $as_nop - +else case e in #( + e) printf "%s\n" "#define size_t unsigned int" >>confdefs.h - + ;; +esac fi # The Ultrix 4.2 mips builtin alloca declared by alloca.h only works @@ -10182,8 +10760,8 @@ if test ${ac_cv_working_alloca_h+y} then : printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include int @@ -10198,11 +10776,13 @@ if ac_fn_c_try_link "$LINENO" then : ac_cv_working_alloca_h=yes -else $as_nop - ac_cv_working_alloca_h=no +else case e in #( + e) ac_cv_working_alloca_h=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ - conftest$ac_exeext conftest.$ac_ext + conftest$ac_exeext conftest.$ac_ext ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_working_alloca_h" >&5 printf "%s\n" "$ac_cv_working_alloca_h" >&6; } @@ -10217,10 +10797,10 @@ if test ${ac_cv_func_alloca_works+y} then : printf %s "(cached) " >&6 -else $as_nop - if test $ac_cv_working_alloca_h = yes; then - ac_cv_func_alloca_works=yes -else +else case e in #( + e) ac_cv_func_alloca_works=$ac_cv_working_alloca_h +if test "$ac_cv_func_alloca_works" != yes +then : cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include @@ -10251,15 +10831,14 @@ if ac_fn_c_try_link "$LINENO" then : ac_cv_func_alloca_works=yes -else $as_nop - ac_cv_func_alloca_works=no fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext +fi ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_func_alloca_works" >&5 printf "%s\n" "$ac_cv_func_alloca_works" >&6; } -fi if test $ac_cv_func_alloca_works = yes; then @@ -10281,12 +10860,12 @@ if test ${ac_cv_c_stack_direction+y} then : printf %s "(cached) " >&6 -else $as_nop - if test "$cross_compiling" = yes +else case e in #( + e) if test "$cross_compiling" = yes then : ac_cv_c_stack_direction=0 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $ac_includes_default int @@ -10309,13 +10888,16 @@ if ac_fn_c_try_run "$LINENO" then : ac_cv_c_stack_direction=1 -else $as_nop - ac_cv_c_stack_direction=-1 +else case e in #( + e) ac_cv_c_stack_direction=-1 ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_stack_direction" >&5 printf "%s\n" "$ac_cv_c_stack_direction" >&6; } @@ -10372,6 +10954,24 @@ printf "%s\n" "#define HAVE_MKFIFO 1" >>confdefs.h fi +ac_fn_c_check_func "$LINENO" "fdopendir" "ac_cv_func_fdopendir" +if test "x$ac_cv_func_fdopendir" = xyes +then : + printf "%s\n" "#define HAVE_FDOPENDIR 1" >>confdefs.h + +fi +ac_fn_c_check_func "$LINENO" "getrlimit" "ac_cv_func_getrlimit" +if test "x$ac_cv_func_getrlimit" = xyes +then : + printf "%s\n" "#define HAVE_GETRLIMIT 1" >>confdefs.h + +fi +ac_fn_c_check_func "$LINENO" "setrlimit" "ac_cv_func_setrlimit" +if test "x$ac_cv_func_setrlimit" = xyes +then : + printf "%s\n" "#define HAVE_SETRLIMIT 1" >>confdefs.h + +fi ac_fn_c_check_func "$LINENO" "fchmod" "ac_cv_func_fchmod" if test "x$ac_cv_func_fchmod" = xyes then : @@ -10462,6 +11062,18 @@ printf "%s\n" "#define HAVE_LINKAT 1" >>confdefs.h fi +ac_fn_c_check_func "$LINENO" "mknodat" "ac_cv_func_mknodat" +if test "x$ac_cv_func_mknodat" = xyes +then : + printf "%s\n" "#define HAVE_MKNODAT 1" >>confdefs.h + +fi +ac_fn_c_check_func "$LINENO" "mkfifoat" "ac_cv_func_mkfifoat" +if test "x$ac_cv_func_mkfifoat" = xyes +then : + printf "%s\n" "#define HAVE_MKFIFOAT 1" >>confdefs.h + +fi ac_fn_c_check_func "$LINENO" "memmove" "ac_cv_func_memmove" if test "x$ac_cv_func_memmove" = xyes then : @@ -10702,6 +11314,12 @@ printf "%s\n" "#define HAVE_UTIMENSAT 1" >>confdefs.h fi +ac_fn_c_check_func "$LINENO" "futimens" "ac_cv_func_futimens" +if test "x$ac_cv_func_futimens" = xyes +then : + printf "%s\n" "#define HAVE_FUTIMENS 1" >>confdefs.h + +fi ac_fn_c_check_func "$LINENO" "posix_fallocate" "ac_cv_func_posix_fallocate" if test "x$ac_cv_func_posix_fallocate" = xyes then : @@ -10746,6 +11364,42 @@ fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for dirfd" >&5 +printf %s "checking for dirfd... " >&6; } +if test ${rsync_cv_HAVE_DIRFD+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include +int +main (void) +{ +DIR *d = opendir("."); return d ? dirfd(d) < -1 : 0; + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO" +then : + rsync_cv_HAVE_DIRFD=yes +else case e in #( + e) rsync_cv_HAVE_DIRFD=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_DIRFD" >&5 +printf "%s\n" "$rsync_cv_HAVE_DIRFD" >&6; } +if test x"$rsync_cv_HAVE_DIRFD" = x"yes"; then + +printf "%s\n" "#define HAVE_DIRFD 1" >>confdefs.h + +fi + if test x"$ac_cv_func_iconv_open" != x"yes"; then ac_fn_c_check_func "$LINENO" "libiconv_open" "ac_cv_func_libiconv_open" if test "x$ac_cv_func_libiconv_open" = xyes @@ -10762,8 +11416,8 @@ if test ${rsync_cv_have_fallocate+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include @@ -10781,11 +11435,13 @@ if ac_fn_c_try_link "$LINENO" then : rsync_cv_have_fallocate=yes -else $as_nop - rsync_cv_have_fallocate=no +else case e in #( + e) rsync_cv_have_fallocate=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ - conftest$ac_exeext conftest.$ac_ext + conftest$ac_exeext conftest.$ac_ext ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_have_fallocate" >&5 printf "%s\n" "$rsync_cv_have_fallocate" >&6; } @@ -10816,12 +11472,13 @@ printf "%s\n" "#define HAVE_FALLOC_FL_PUNCH_HOLE 1" >>confdefs.h -else $as_nop - +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } - + ;; +esac fi rm -f conftest.err conftest.i conftest.$ac_ext @@ -10846,12 +11503,13 @@ printf "%s\n" "#define HAVE_FALLOC_FL_ZERO_RANGE 1" >>confdefs.h -else $as_nop - +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } - + ;; +esac fi rm -f conftest.err conftest.i conftest.$ac_ext @@ -10860,8 +11518,8 @@ if test ${rsync_cv_have_sys_fallocate+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include @@ -10882,10 +11540,12 @@ if ac_fn_c_try_compile "$LINENO" then : rsync_cv_have_sys_fallocate=yes -else $as_nop - rsync_cv_have_sys_fallocate=no +else case e in #( + e) rsync_cv_have_sys_fallocate=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_have_sys_fallocate" >&5 printf "%s\n" "$rsync_cv_have_sys_fallocate" >&6; } @@ -10933,8 +11593,8 @@ if test ${ac_cv_func_getpgrp_void+y} then : printf %s "(cached) " >&6 -else $as_nop - # Use it with a single arg. +else case e in #( + e) # Use it with a single arg. cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $ac_includes_default @@ -10949,11 +11609,13 @@ if ac_fn_c_try_compile "$LINENO" then : ac_cv_func_getpgrp_void=no -else $as_nop - ac_cv_func_getpgrp_void=yes +else case e in #( + e) ac_cv_func_getpgrp_void=yes ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_func_getpgrp_void" >&5 printf "%s\n" "$ac_cv_func_getpgrp_void" >&6; } @@ -10969,8 +11631,9 @@ if test ${enable_iconv_open+y} then : enableval=$enable_iconv_open; -else $as_nop - enable_iconv_open=$ac_cv_func_iconv_open +else case e in #( + e) enable_iconv_open=$ac_cv_func_iconv_open ;; +esac fi @@ -10984,8 +11647,9 @@ if test ${enable_iconv+y} then : enableval=$enable_iconv; -else $as_nop - enable_iconv=$enable_iconv_open +else case e in #( + e) enable_iconv=$enable_iconv_open ;; +esac fi @@ -11007,13 +11671,13 @@ if test ${rsync_cv_chown_modifies_symlink+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) if test "$cross_compiling" = yes then : rsync_cv_chown_modifies_symlink=no -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #if HAVE_UNISTD_H @@ -11032,13 +11696,16 @@ if ac_fn_c_try_run "$LINENO" then : rsync_cv_chown_modifies_symlink=yes -else $as_nop - rsync_cv_chown_modifies_symlink=no +else case e in #( + e) rsync_cv_chown_modifies_symlink=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_chown_modifies_symlink" >&5 printf "%s\n" "$rsync_cv_chown_modifies_symlink" >&6; } @@ -11053,13 +11720,13 @@ if test ${rsync_cv_can_hardlink_symlink+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) if test "$cross_compiling" = yes then : rsync_cv_can_hardlink_symlink=no -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #ifdef HAVE_FCNTL_H @@ -11088,13 +11755,16 @@ if ac_fn_c_try_run "$LINENO" then : rsync_cv_can_hardlink_symlink=yes -else $as_nop - rsync_cv_can_hardlink_symlink=no +else case e in #( + e) rsync_cv_can_hardlink_symlink=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_can_hardlink_symlink" >&5 printf "%s\n" "$rsync_cv_can_hardlink_symlink" >&6; } @@ -11109,13 +11779,13 @@ if test ${rsync_cv_can_hardlink_special+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) if test "$cross_compiling" = yes then : rsync_cv_can_hardlink_special=no -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #if HAVE_UNISTD_H @@ -11138,13 +11808,16 @@ if ac_fn_c_try_run "$LINENO" then : rsync_cv_can_hardlink_special=yes -else $as_nop - rsync_cv_can_hardlink_special=no +else case e in #( + e) rsync_cv_can_hardlink_special=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_can_hardlink_special" >&5 printf "%s\n" "$rsync_cv_can_hardlink_special" >&6; } @@ -11159,13 +11832,13 @@ if test ${rsync_cv_HAVE_SOCKETPAIR+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) if test "$cross_compiling" = yes then : rsync_cv_HAVE_SOCKETPAIR=cross -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #ifdef HAVE_SYS_TYPES_H @@ -11183,13 +11856,16 @@ if ac_fn_c_try_run "$LINENO" then : rsync_cv_HAVE_SOCKETPAIR=yes -else $as_nop - rsync_cv_HAVE_SOCKETPAIR=no +else case e in #( + e) rsync_cv_HAVE_SOCKETPAIR=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_SOCKETPAIR" >&5 printf "%s\n" "$rsync_cv_HAVE_SOCKETPAIR" >&6; } @@ -11204,13 +11880,14 @@ then : printf "%s\n" "#define HAVE_GETPASS 1" >>confdefs.h -else $as_nop - case " $LIBOBJS " in +else case e in #( + e) case " $LIBOBJS " in *" getpass.$ac_objext "* ) ;; *) LIBOBJS="$LIBOBJS getpass.$ac_objext" ;; esac - + ;; +esac fi @@ -11220,16 +11897,22 @@ if test ${ac_cv_lib_popt_poptGetContext+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_check_lib_save_LIBS=$LIBS +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lpopt $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char poptGetContext (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char poptGetContext (void); int main (void) { @@ -11241,12 +11924,14 @@ if ac_fn_c_try_link "$LINENO" then : ac_cv_lib_popt_poptGetContext=yes -else $as_nop - ac_cv_lib_popt_poptGetContext=no +else case e in #( + e) ac_cv_lib_popt_poptGetContext=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_popt_poptGetContext" >&5 printf "%s\n" "$ac_cv_lib_popt_poptGetContext" >&6; } @@ -11256,8 +11941,9 @@ LIBS="-lpopt $LIBS" -else $as_nop - with_included_popt=yes +else case e in #( + e) with_included_popt=yes ;; +esac fi fi @@ -11301,8 +11987,8 @@ if test ${rsync_cv_SIGNED_CHAR_OK+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -11317,10 +12003,12 @@ if ac_fn_c_try_compile "$LINENO" then : rsync_cv_SIGNED_CHAR_OK=yes -else $as_nop - rsync_cv_SIGNED_CHAR_OK=no +else case e in #( + e) rsync_cv_SIGNED_CHAR_OK=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_SIGNED_CHAR_OK" >&5 printf "%s\n" "$rsync_cv_SIGNED_CHAR_OK" >&6; } @@ -11335,13 +12023,13 @@ if test ${rsync_cv_HAVE_BROKEN_READDIR+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) if test "$cross_compiling" = yes then : rsync_cv_HAVE_BROKEN_READDIR=cross -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #ifdef HAVE_SYS_TYPES_H @@ -11355,13 +12043,16 @@ if ac_fn_c_try_run "$LINENO" then : rsync_cv_HAVE_BROKEN_READDIR=yes -else $as_nop - rsync_cv_HAVE_BROKEN_READDIR=no +else case e in #( + e) rsync_cv_HAVE_BROKEN_READDIR=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_BROKEN_READDIR" >&5 printf "%s\n" "$rsync_cv_HAVE_BROKEN_READDIR" >&6; } @@ -11376,8 +12067,8 @@ if test ${rsync_cv_HAVE_STRUCT_UTIMBUF+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -11396,10 +12087,12 @@ if ac_fn_c_try_compile "$LINENO" then : rsync_cv_HAVE_STRUCT_UTIMBUF=yes -else $as_nop - rsync_cv_HAVE_STRUCT_UTIMBUF=no +else case e in #( + e) rsync_cv_HAVE_STRUCT_UTIMBUF=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_STRUCT_UTIMBUF" >&5 printf "%s\n" "$rsync_cv_HAVE_STRUCT_UTIMBUF" >&6; } @@ -11414,8 +12107,8 @@ if test ${rsync_cv_HAVE_GETTIMEOFDAY_TZ+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include @@ -11433,10 +12126,12 @@ if ac_fn_c_try_compile "$LINENO" then : rsync_cv_HAVE_GETTIMEOFDAY_TZ=yes -else $as_nop - rsync_cv_HAVE_GETTIMEOFDAY_TZ=no +else case e in #( + e) rsync_cv_HAVE_GETTIMEOFDAY_TZ=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_GETTIMEOFDAY_TZ" >&5 printf "%s\n" "$rsync_cv_HAVE_GETTIMEOFDAY_TZ" >&6; } @@ -11451,13 +12146,13 @@ if test ${rsync_cv_HAVE_C99_VSNPRINTF+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) if test "$cross_compiling" = yes then : rsync_cv_HAVE_C99_VSNPRINTF=cross -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #ifdef HAVE_SYS_TYPES_H @@ -11487,13 +12182,16 @@ if ac_fn_c_try_run "$LINENO" then : rsync_cv_HAVE_C99_VSNPRINTF=yes -else $as_nop - rsync_cv_HAVE_C99_VSNPRINTF=no +else case e in #( + e) rsync_cv_HAVE_C99_VSNPRINTF=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_C99_VSNPRINTF" >&5 printf "%s\n" "$rsync_cv_HAVE_C99_VSNPRINTF" >&6; } @@ -11509,13 +12207,13 @@ if test ${rsync_cv_HAVE_SECURE_MKSTEMP+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) if test "$cross_compiling" = yes then : rsync_cv_HAVE_SECURE_MKSTEMP=cross -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include #ifdef HAVE_SYS_TYPES_H @@ -11539,13 +12237,16 @@ if ac_fn_c_try_run "$LINENO" then : rsync_cv_HAVE_SECURE_MKSTEMP=yes -else $as_nop - rsync_cv_HAVE_SECURE_MKSTEMP=no +else case e in #( + e) rsync_cv_HAVE_SECURE_MKSTEMP=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_SECURE_MKSTEMP" >&5 printf "%s\n" "$rsync_cv_HAVE_SECURE_MKSTEMP" >&6; } @@ -11564,93 +12265,14 @@ fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if mknod creates FIFOs" >&5 -printf %s "checking if mknod creates FIFOs... " >&6; } -if test ${rsync_cv_MKNOD_CREATES_FIFOS+y} -then : - printf %s "(cached) " >&6 -else $as_nop - -if test "$cross_compiling" = yes -then : - rsync_cv_MKNOD_CREATES_FIFOS=cross -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -#include -#include -#include -#if HAVE_UNISTD_H -# include -#endif -int main(void) { int rc, ec; char *fn = "fifo-test"; -unlink(fn); rc = mknod(fn,S_IFIFO,0600); ec = errno; unlink(fn); -if (rc) {printf("(%d %d) ",rc,ec); return ec;} -return 0;} -_ACEOF -if ac_fn_c_try_run "$LINENO" -then : - rsync_cv_MKNOD_CREATES_FIFOS=yes -else $as_nop - rsync_cv_MKNOD_CREATES_FIFOS=no -fi -rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext -fi - -fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_MKNOD_CREATES_FIFOS" >&5 -printf "%s\n" "$rsync_cv_MKNOD_CREATES_FIFOS" >&6; } -if test x"$rsync_cv_MKNOD_CREATES_FIFOS" = x"yes"; then - -printf "%s\n" "#define MKNOD_CREATES_FIFOS 1" >>confdefs.h - -fi - -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if mknod creates sockets" >&5 -printf %s "checking if mknod creates sockets... " >&6; } -if test ${rsync_cv_MKNOD_CREATES_SOCKETS+y} -then : - printf %s "(cached) " >&6 -else $as_nop - -if test "$cross_compiling" = yes -then : - rsync_cv_MKNOD_CREATES_SOCKETS=cross -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -#include -#include -#include -#if HAVE_UNISTD_H -# include -#endif -int main(void) { int rc, ec; char *fn = "sock-test"; -unlink(fn); rc = mknod(fn,S_IFSOCK,0600); ec = errno; unlink(fn); -if (rc) {printf("(%d %d) ",rc,ec); return ec;} -return 0;} -_ACEOF -if ac_fn_c_try_run "$LINENO" -then : - rsync_cv_MKNOD_CREATES_SOCKETS=yes -else $as_nop - rsync_cv_MKNOD_CREATES_SOCKETS=no -fi -rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext -fi - -fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_MKNOD_CREATES_SOCKETS" >&5 -printf "%s\n" "$rsync_cv_MKNOD_CREATES_SOCKETS" >&6; } -if test x"$rsync_cv_MKNOD_CREATES_SOCKETS" = x"yes"; then - -printf "%s\n" "#define MKNOD_CREATES_SOCKETS 1" >>confdefs.h - -fi +# Whether mknod()/mknodat() can create a FIFO or a unix-domain socket is a +# property of the target filesystem, not a build-time constant -- e.g. mknod +# makes sockets on Linux but not the BSDs/macOS/Solaris, and a single transfer +# can write to filesystems with different capabilities. So rsync no longer +# probes this at configure time (a run-test that also misfired when cross- +# compiling); do_mknod*() just try mknod[at]() and, on failure, fall back to +# mkfifo[at]()/socket+bind() per call. We only need the libc symbols, checked +# above via AC_CHECK_FUNCS (mknod mknodat mkfifo mkfifoat) -- all link tests. # # The following test was mostly taken from the tcl/tk plus patches @@ -11660,8 +12282,8 @@ if test ${rsync_cv_DASHC_WORKS_WITH_DASHO+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) rm -rf conftest* cat > conftest.$ac_ext <&5 printf "%s\n" "$rsync_cv_DASHC_WORKS_WITH_DASHO" >&6; } @@ -11812,16 +12435,22 @@ if test ${ac_cv_lib_acl_acl_get_file+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_check_lib_save_LIBS=$LIBS +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lacl $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char acl_get_file (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char acl_get_file (void); int main (void) { @@ -11833,12 +12462,14 @@ if ac_fn_c_try_link "$LINENO" then : ac_cv_lib_acl_acl_get_file=yes -else $as_nop - ac_cv_lib_acl_acl_get_file=no +else case e in #( + e) ac_cv_lib_acl_acl_get_file=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_acl_acl_get_file" >&5 printf "%s\n" "$ac_cv_lib_acl_acl_get_file" >&6; } @@ -11855,8 +12486,8 @@ if test ${samba_cv_HAVE_POSIX_ACLS+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -11877,11 +12508,13 @@ if ac_fn_c_try_link "$LINENO" then : samba_cv_HAVE_POSIX_ACLS=yes -else $as_nop - samba_cv_HAVE_POSIX_ACLS=no +else case e in #( + e) samba_cv_HAVE_POSIX_ACLS=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ - conftest$ac_exeext conftest.$ac_ext + conftest$ac_exeext conftest.$ac_ext ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $samba_cv_HAVE_POSIX_ACLS" >&5 printf "%s\n" "$samba_cv_HAVE_POSIX_ACLS" >&6; } @@ -11900,8 +12533,8 @@ if test ${samba_cv_HAVE_ACL_GET_PERM_NP+y} then : printf %s "(cached) " >&6 -else $as_nop - +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -11922,11 +12555,13 @@ if ac_fn_c_try_link "$LINENO" then : samba_cv_HAVE_ACL_GET_PERM_NP=yes -else $as_nop - samba_cv_HAVE_ACL_GET_PERM_NP=no +else case e in #( + e) samba_cv_HAVE_ACL_GET_PERM_NP=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ - conftest$ac_exeext conftest.$ac_ext + conftest$ac_exeext conftest.$ac_ext ;; +esac fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $samba_cv_HAVE_ACL_GET_PERM_NP" >&5 printf "%s\n" "$samba_cv_HAVE_ACL_GET_PERM_NP" >&6; } @@ -11955,11 +12590,12 @@ if test ${enable_xattr_support+y} then : enableval=$enable_xattr_support; -else $as_nop - case "$ac_cv_func_getxattr$ac_cv_func_extattr_get_link$ac_cv_func_attropen" in +else case e in #( + e) case "$ac_cv_func_getxattr$ac_cv_func_extattr_get_link$ac_cv_func_attropen" in *yes*) enable_xattr_support=maybe ;; *) enable_xattr_support=no ;; - esac + esac ;; +esac fi @@ -11979,21 +12615,26 @@ printf "%s\n" "#define NO_SYMLINK_USER_XATTRS 1" >>confdefs.h - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for getxattr in -lattr" >&5 -printf %s "checking for getxattr in -lattr... " >&6; } -if test ${ac_cv_lib_attr_getxattr+y} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for library containing getxattr" >&5 +printf %s "checking for library containing getxattr... " >&6; } +if test ${ac_cv_search_getxattr+y} then : printf %s "(cached) " >&6 -else $as_nop - ac_check_lib_save_LIBS=$LIBS -LIBS="-lattr $LIBS" +else case e in #( + e) ac_func_search_save_LIBS=$LIBS cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -char getxattr (); + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char getxattr (void); int main (void) { @@ -12002,23 +12643,42 @@ return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO" +for ac_lib in '' attr +do + if test -z "$ac_lib"; then + ac_res="none required" + else + ac_res=-l$ac_lib + LIBS="-l$ac_lib $ac_func_search_save_LIBS" + fi + if ac_fn_c_try_link "$LINENO" then : - ac_cv_lib_attr_getxattr=yes -else $as_nop - ac_cv_lib_attr_getxattr=no + ac_cv_search_getxattr=$ac_res fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ - conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS + conftest$ac_exeext + if test ${ac_cv_search_getxattr+y} +then : + break fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_attr_getxattr" >&5 -printf "%s\n" "$ac_cv_lib_attr_getxattr" >&6; } -if test "x$ac_cv_lib_attr_getxattr" = xyes +done +if test ${ac_cv_search_getxattr+y} then : - printf "%s\n" "#define HAVE_LIBATTR 1" >>confdefs.h - LIBS="-lattr $LIBS" +else case e in #( + e) ac_cv_search_getxattr=no ;; +esac +fi +rm conftest.$ac_ext +LIBS=$ac_func_search_save_LIBS ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_getxattr" >&5 +printf "%s\n" "$ac_cv_search_getxattr" >&6; } +ac_res=$ac_cv_search_getxattr +if test "$ac_res" != no +then : + test "$ac_res" = "none required" || LIBS="$ac_res $LIBS" fi @@ -12070,6 +12730,159 @@ esac fi +################################################# +# On Linux, POSIX ACLs are stored as the "system.posix_acl_{access,default}" +# xattrs, so we can get/set them through a held O_NOFOLLOW fd (fsetxattr) or a +# dirfd+leaf (setxattrat, AT_SYMLINK_NOFOLLOW) instead of the path-based libacl +# acl_*_file() calls -- making the operation safe against a parent-symlink race. +# This needs POSIX ACLs and the f/at xattr syscalls, which on Linux are +# available whenever (or ) is -- independent of the +# -X feature (--disable-xattr-support), so we gate on the header, not +# enable_xattr_support. + + +if test x"$samba_cv_HAVE_POSIX_ACLS" = x"yes" \ + && { test x"$ac_cv_header_sys_xattr_h" = x"yes" || test x"$ac_cv_header_attr_xattr_h" = x"yes"; }; then + case "$host_os" in + *linux*) + printf "%s\n" "#define SUPPORT_ACL_FD 1" >>confdefs.h + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for SYS_setxattrat" >&5 +printf %s "checking for SYS_setxattrat... " >&6; } +if test ${rsync_cv_have_sys_setxattrat+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include +#include +#ifdef HAVE_UNISTD_H +#include +#endif +struct xattr_args { uint64_t value; uint32_t size; uint32_t flags; }; +int +main (void) +{ +struct xattr_args a; a.value = 0; a.size = 0; a.flags = 0; + syscall(SYS_setxattrat, 0, ".", 0, "n", &a, sizeof a); + syscall(SYS_getxattrat, 0, ".", 0, "n", &a, sizeof a); + syscall(SYS_removexattrat, 0, ".", 0, "n"); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + rsync_cv_have_sys_setxattrat=yes +else case e in #( + e) rsync_cv_have_sys_setxattrat=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $rsync_cv_have_sys_setxattrat" >&5 +printf "%s\n" "$rsync_cv_have_sys_setxattrat" >&6; } + if test x"$rsync_cv_have_sys_setxattrat" = x"yes"; then + printf "%s\n" "#define HAVE_XATTRAT_SYSCALLS 1" >>confdefs.h + + fi + ;; + esac +fi + +################################################# +# Detect a patched libacl providing the race-safe +# *_at ACL entry points (acl_get_file_at/acl_set_file_at/acl_delete_def_file_at, +# ACL_1.3, unreleased upstream). When present we route the race-safe ACL get/ +# set/delete through them on Linux -- race-safe on every kernel (6.13+ uses +# *xattrat; older uses libacl's /proc/self/fd compat). A stock -lacl lacks these +# symbols, so this stays undefined and the build falls back to lib/acl.c; +# detection must therefore run against the patched lib (CPPFLAGS/LDFLAGS). + +if test x"$samba_cv_HAVE_POSIX_ACLS" = x"yes"; then + case "$host_os" in + *linux*) + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for acl_get_file_at in -lacl" >&5 +printf %s "checking for acl_get_file_at in -lacl... " >&6; } +if test ${ac_cv_lib_acl_acl_get_file_at+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS +LIBS="-lacl $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char acl_get_file_at (void); +int +main (void) +{ +return acl_get_file_at (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO" +then : + ac_cv_lib_acl_acl_get_file_at=yes +else case e in #( + e) ac_cv_lib_acl_acl_get_file_at=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_acl_acl_get_file_at" >&5 +printf "%s\n" "$ac_cv_lib_acl_acl_get_file_at" >&6; } +if test "x$ac_cv_lib_acl_acl_get_file_at" = xyes +then : + rsync_have_libacl_at=yes +else case e in #( + e) rsync_have_libacl_at=no ;; +esac +fi + + if test x"$rsync_have_libacl_at" = x"yes"; then + + for ac_func in acl_set_file_at acl_delete_def_file_at +do : + as_ac_var=`printf "%s\n" "ac_cv_func_$ac_func" | sed "$as_sed_sh"` +ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var" +if eval test \"x\$"$as_ac_var"\" = x"yes" +then : + cat >>confdefs.h <<_ACEOF +#define `printf "%s\n" "HAVE_$ac_func" | sed "$as_sed_cpp"` 1 +_ACEOF + +else case e in #( + e) rsync_have_libacl_at=no ;; +esac +fi + +done + fi + if test x"$rsync_have_libacl_at" = x"yes"; then + printf "%s\n" "#define HAVE_LIBACL_AT 1" >>confdefs.h + + fi + ;; + esac +fi + if test x"$enable_acl_support" = x"no" || test x"$enable_xattr_support" = x"no" || test x"$enable_iconv" = x"no"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $CC supports -Wno-unused-parameter" >&5 printf %s "checking whether $CC supports -Wno-unused-parameter... " >&6; } @@ -12089,8 +12902,9 @@ if ac_fn_c_try_link "$LINENO" then : rsync_warn_flag=yes -else $as_nop - rsync_warn_flag=no +else case e in #( + e) rsync_warn_flag=no ;; +esac fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext @@ -12121,8 +12935,8 @@ # config.status only pays attention to the cache file if you give it # the --recheck option to rerun configure. # -# `ac_cv_env_foo' variables (set or unset) will be overridden when -# loading this file, other *unset* `ac_cv_foo' will be assigned the +# 'ac_cv_env_foo' variables (set or unset) will be overridden when +# loading this file, other *unset* 'ac_cv_foo' will be assigned the # following values. _ACEOF @@ -12152,14 +12966,14 @@ (set) 2>&1 | case $as_nl`(ac_space=' '; set) 2>&1` in #( *${as_nl}ac_space=\ *) - # `set' does not quote correctly, so add quotes: double-quote + # 'set' does not quote correctly, so add quotes: double-quote # substitution turns \\\\ into \\, and sed turns \\ into \. sed -n \ "s/'/'\\\\''/g; s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\\2'/p" ;; #( *) - # `set' quotes correctly as required by POSIX, so do not add quotes. + # 'set' quotes correctly as required by POSIX, so do not add quotes. sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" ;; esac | @@ -12221,6 +13035,12 @@ +# Check whether --enable-year2038 was given. +if test ${enable_year2038+y} +then : + enableval=$enable_year2038; +fi + : "${CONFIG_STATUS=./config.status}" ac_write_fail=0 @@ -12250,7 +13070,6 @@ # Be more Bourne compatible DUALCASE=1; export DUALCASE # for MKS sh -as_nop=: if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1 then : emulate sh @@ -12259,12 +13078,13 @@ # is contrary to our usage. Disable this feature. alias -g '${1+"$@"}'='"$@"' setopt NO_GLOB_SUBST -else $as_nop - case `(set -o) 2>/dev/null` in #( +else case e in #( + e) case `(set -o) 2>/dev/null` in #( *posix*) : set -o posix ;; #( *) : ;; +esac ;; esac fi @@ -12336,7 +13156,7 @@ ;; esac -# We did not find ourselves, most probably we were run as `sh COMMAND' +# We did not find ourselves, most probably we were run as 'sh COMMAND' # in which case we are not to be found in the path. if test "x$as_myself" = x; then as_myself=$0 @@ -12365,7 +13185,6 @@ } # as_fn_error - # as_fn_set_status STATUS # ----------------------- # Set $? to STATUS, without forking. @@ -12405,11 +13224,12 @@ { eval $1+=\$2 }' -else $as_nop - as_fn_append () +else case e in #( + e) as_fn_append () { eval $1=\$$1\$2 - } + } ;; +esac fi # as_fn_append # as_fn_arith ARG... @@ -12423,11 +13243,12 @@ { as_val=$(( $* )) }' -else $as_nop - as_fn_arith () +else case e in #( + e) as_fn_arith () { as_val=`expr "$@" || test $? -eq 1` - } + } ;; +esac fi # as_fn_arith @@ -12510,9 +13331,9 @@ if ln -s conf$$.file conf$$ 2>/dev/null; then as_ln_s='ln -s' # ... but there are two gotchas: - # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. - # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. - # In both cases, we have to default to `cp -pR'. + # 1) On MSYS, both 'ln -s file dir' and 'ln file dir' fail. + # 2) DJGPP < 2.04 has no symlinks; 'ln -s' creates a wrapper executable. + # In both cases, we have to default to 'cp -pR'. ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || as_ln_s='cp -pR' elif ln conf$$.file conf$$ 2>/dev/null; then @@ -12593,10 +13414,12 @@ as_executable_p=as_fn_executable_p # Sed expression to map a string onto a valid CPP name. -as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" +as_sed_cpp="y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g" +as_tr_cpp="eval sed '$as_sed_cpp'" # deprecated # Sed expression to map a string onto a valid variable name. -as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" +as_sed_sh="y%*+%pp%;s%[^_$as_cr_alnum]%_%g" +as_tr_sh="eval sed '$as_sed_sh'" # deprecated exec 6>&1 @@ -12612,7 +13435,7 @@ # values after options handling. ac_log=" This file was extended by rsync $as_me, which was -generated by GNU Autoconf 2.71. Invocation command line was +generated by GNU Autoconf 2.72. Invocation command line was CONFIG_FILES = $CONFIG_FILES CONFIG_HEADERS = $CONFIG_HEADERS @@ -12643,7 +13466,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 ac_cs_usage="\ -\`$as_me' instantiates files and other configuration actions +'$as_me' instantiates files and other configuration actions from templates according to the current configuration. Unless the files and actions are specified as TAGs, all are instantiated by default. @@ -12676,10 +13499,10 @@ ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\ rsync config.status -configured by $0, generated by GNU Autoconf 2.71, +configured by $0, generated by GNU Autoconf 2.72, with options \\"\$ac_cs_config\\" -Copyright (C) 2021 Free Software Foundation, Inc. +Copyright (C) 2023 Free Software Foundation, Inc. This config.status script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it." @@ -12741,8 +13564,8 @@ ac_need_defaults=false;; --he | --h) # Conflict between --help and --header - as_fn_error $? "ambiguous option: \`$1' -Try \`$0 --help' for more information.";; + as_fn_error $? "ambiguous option: '$1' +Try '$0 --help' for more information.";; --help | --hel | -h ) printf "%s\n" "$ac_cs_usage"; exit ;; -q | -quiet | --quiet | --quie | --qui | --qu | --q \ @@ -12750,8 +13573,8 @@ ac_cs_silent=: ;; # This is an error. - -*) as_fn_error $? "unrecognized option: \`$1' -Try \`$0 --help' for more information." ;; + -*) as_fn_error $? "unrecognized option: '$1' +Try '$0 --help' for more information." ;; *) as_fn_append ac_config_targets " $1" ac_need_defaults=false ;; @@ -12806,7 +13629,7 @@ "popt/dummy") CONFIG_FILES="$CONFIG_FILES popt/dummy" ;; "shconfig") CONFIG_FILES="$CONFIG_FILES shconfig" ;; - *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;; + *) as_fn_error $? "invalid argument: '$ac_config_target'" "$LINENO" 5;; esac done @@ -12825,7 +13648,7 @@ # creating and moving files from /tmp can sometimes cause problems. # Hook for its removal unless debugging. # Note that there is a small window in which the directory will not be cleaned: -# after its creation but before its name has been assigned to `$tmp'. +# after its creation but before its name has been assigned to '$tmp'. $debug || { tmp= ac_tmp= @@ -12849,7 +13672,7 @@ # Set up the scripts for CONFIG_FILES section. # No need to generate them if there are no CONFIG_FILES. -# This happens for instance with `./config.status config.h'. +# This happens for instance with './config.status config.h'. if test -n "$CONFIG_FILES"; then @@ -13007,13 +13830,13 @@ # Set up the scripts for CONFIG_HEADERS section. # No need to generate them if there are no CONFIG_HEADERS. -# This happens for instance with `./config.status Makefile'. +# This happens for instance with './config.status Makefile'. if test -n "$CONFIG_HEADERS"; then cat >"$ac_tmp/defines.awk" <<\_ACAWK || BEGIN { _ACEOF -# Transform confdefs.h into an awk script `defines.awk', embedded as +# Transform confdefs.h into an awk script 'defines.awk', embedded as # here-document in config.status, that substitutes the proper values into # config.h.in to produce config.h. @@ -13123,7 +13946,7 @@ esac case $ac_mode$ac_tag in :[FHL]*:*);; - :L* | :C*:*) as_fn_error $? "invalid tag \`$ac_tag'" "$LINENO" 5;; + :L* | :C*:*) as_fn_error $? "invalid tag '$ac_tag'" "$LINENO" 5;; :[FH]-) ac_tag=-:-;; :[FH]*) ac_tag=$ac_tag:$ac_tag.in;; esac @@ -13145,19 +13968,19 @@ -) ac_f="$ac_tmp/stdin";; *) # Look for the file first in the build tree, then in the source tree # (if the path is not absolute). The absolute path cannot be DOS-style, - # because $ac_f cannot contain `:'. + # because $ac_f cannot contain ':'. test -f "$ac_f" || case $ac_f in [\\/$]*) false;; *) test -f "$srcdir/$ac_f" && ac_f="$srcdir/$ac_f";; esac || - as_fn_error 1 "cannot find input file: \`$ac_f'" "$LINENO" 5;; + as_fn_error 1 "cannot find input file: '$ac_f'" "$LINENO" 5;; esac case $ac_f in *\'*) ac_f=`printf "%s\n" "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac as_fn_append ac_file_inputs " '$ac_f'" done - # Let's still pretend it is `configure' which instantiates (i.e., don't + # Let's still pretend it is 'configure' which instantiates (i.e., don't # use $as_me), people would be surprised to read: # /* config.h. Generated by config.status. */ configure_input='Generated from '` @@ -13290,7 +14113,7 @@ esac _ACEOF -# Neutralize VPATH when `$srcdir' = `.'. +# Neutralize VPATH when '$srcdir' = '.'. # Shell code in configure.ac might set extrasub. # FIXME: do we really want to maintain this feature? cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 @@ -13321,9 +14144,9 @@ { ac_out=`sed -n '/\${datarootdir}/p' "$ac_tmp/out"`; test -n "$ac_out"; } && { ac_out=`sed -n '/^[ ]*datarootdir[ ]*:*=/p' \ "$ac_tmp/out"`; test -z "$ac_out"; } && - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable \`datarootdir' + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable 'datarootdir' which seems to be undefined. Please make sure it is defined" >&5 -printf "%s\n" "$as_me: WARNING: $ac_file contains a reference to the variable \`datarootdir' +printf "%s\n" "$as_me: WARNING: $ac_file contains a reference to the variable 'datarootdir' which seems to be undefined. Please make sure it is defined" >&2;} rm -f "$ac_tmp/stdin" diff -Nru rsync-3.4.1+ds1/connection.c rsync-3.5.0+ds1/connection.c --- rsync-3.4.1+ds1/connection.c 2020-04-09 22:11:37.000000000 +0000 +++ rsync-3.5.0+ds1/connection.c 2026-07-20 04:05:31.000000000 +0000 @@ -30,7 +30,9 @@ if (max_connections == 0) return 1; - if ((fd = open(fname, O_RDWR|O_CREAT, 0600)) < 0) + /* '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) return 0; /* Find a free spot. */ diff -Nru rsync-3.4.1+ds1/daemon-parm.awk rsync-3.5.0+ds1/daemon-parm.awk --- rsync-3.4.1+ds1/daemon-parm.awk 2020-07-04 21:21:15.000000000 +0000 +++ rsync-3.5.0+ds1/daemon-parm.awk 2026-07-20 04:05:31.000000000 +0000 @@ -6,7 +6,7 @@ BEGIN { heading = "/* DO NOT EDIT THIS FILE! It is auto-generated from a list of values in " ARGV[1] "! */\n\n" sect = psect = defines = accessors = prior_ptype = "" - parms = "\nstatic struct parm_struct parm_table[] = {" + parms = "\nstatic const struct parm_struct parm_table[] = {" comment_fmt = "\n/********** %s **********/\n" tdstruct = "typedef struct {" } @@ -84,7 +84,14 @@ defines = defines "\t" vtype " " name ";\n" values = values "\t" $0 ", /* " name " */\n" parms = parms " {\"" pubname "\", P_" ptype psect name ", " enum ", 0},\n" - accessors = accessors "FN_" sect "_" atype "(lp_" name ", " name ")\n" + # The shell-executed hook params (whose %RSYNC_*% expansion is fed to + # /bin/sh) use the _SHELL accessor, which single-quotes peer-controlled + # values to prevent injection. Ordinary string params must NOT quote -- + # it would corrupt a documented `path = /home/%RSYNC_USER_NAME%` etc. + if (atype == "STRING" && (name == "early_exec" || name == "prexfer_exec" || name == "postxfer_exec" || name == "name_converter")) + accessors = accessors "FN_" sect "_STRING_SHELL(lp_" name ", " name ")\n" + else + accessors = accessors "FN_" sect "_" atype "(lp_" name ", " name ")\n" if (vtype == "char*") { exps = exps "\tBOOL " name "_EXP;\n" diff -Nru rsync-3.4.1+ds1/daemon-parm.txt rsync-3.5.0+ds1/daemon-parm.txt --- rsync-3.4.1+ds1/daemon-parm.txt 2022-09-20 07:08:16.000000000 +0000 +++ rsync-3.5.0+ds1/daemon-parm.txt 2026-07-20 04:05:31.000000000 +0000 @@ -6,6 +6,7 @@ STRING daemon_uid NULL STRING motd_file NULL STRING pid_file NULL +STRING proxy_protocol_hosts NULL STRING socket_options NULL INTEGER listen_backlog 5 @@ -15,6 +16,7 @@ Locals: ================================================================= +STRING auth_digest NULL STRING auth_users NULL STRING charset NULL STRING comment NULL @@ -55,6 +57,7 @@ BOOL forward_lookup True BOOL ignore_errors False BOOL ignore_nonreadable False +BOOL insecure_links False BOOL list True BOOL read_only True BOOL reverse_lookup True diff -Nru rsync-3.4.1+ds1/debian/changelog rsync-3.5.0+ds1/debian/changelog --- rsync-3.4.1+ds1/debian/changelog 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/changelog 2026-09-16 01:46:30.000000000 +0000 @@ -1,3 +1,111 @@ +rsync (3.5.0+ds1-0+deb13u1) trixie-security; urgency=medium + + * New upstream release, pull the same upstream patches applied in Debian + Unstable, fixing 33 CVEs; + - CVE-2026-53783: rrsync restricted-directory escape + (validation-vs-exec race + unsafe option allowlist) + - CVE-2026-53784: Daemon module-root chdir escape under "use chroot = + no" + - CVE-2026-53785: --relative implied-parent creation escapes the + destination tree + - CVE-2026-53786: Daemon --filter merge file bypasses the module filter + list + - CVE-2026-53788: Daemon name-converter accepts newline-bearing names + into its line protocol + - CVE-2026-53789: Malicious sender expands --delete scope by + reclassifying an implied parent + - CVE-2026-53790: Command / argument injection via unquoted peer- or + host-controlled values + - CVE-2026-53791: PROXY-protocol mode lets a direct client spoof the + daemon's source address + - CVE-2026-53792: Receiver-supplied zero checksum block length drives + sender matching negative + - CVE-2026-53793: Chroot "/./" inner-module escape via a + parent-component symlink + - CVE-2026-53794: Remote peer disables the per-allocation sanity cap + via --max-alloc=0 + - CVE-2026-53795: Receiver write escape via an absolute --temp-dir / + --link-dest disabling rename/link confinement + - CVE-2026-53796: Non-daemon receiver destination-chdir symlink race + (TOCTOU) + - CVE-2026-53797: Sender source-tree parent-component symlink race -> + out-of-tree disclosure + - CVE-2026-53798: Daemon name-converter empty response maps an unknown + name to uid/gid 0 + - CVE-2026-53799: Receiver ACL/xattr application follows a symlink-race + -> arbitrary ACL set (local privilege escalation) + - CVE-2026-53800: Sender --remove-source-files unlink follows a + parent-component symlink race -> arbitrary file deletion outside the + source tree + - CVE-2026-53801: Sender/daemon directory-scan enumeration escapes the + transfer root / module -> out-of-tree disclosure + - CVE-2026-53802: Arbitrary file read / transfer-shaping via symlinked + operator-supplied input files + - CVE-2026-53803: Arbitrary file write / privilege escalation via + symlinked operator-supplied output paths + - CVE-2026-70452: `hosts deny` fails OPEN when a configured hostname + cannot be resolved, admitting the host it was meant to block + - CVE-2026-70453: Quadratic CPU exhaustion in hash_search() from a + crafted equal-weak-checksum chain + - CVE-2026-70454: rsync-ssl establishes an unauthenticated TLS + connection (no CA verification; no stunnel hostname binding) + - CVE-2026-70455: Peer-controlled Zstandard worker exhaustion on an + rsync daemon + - CVE-2026-70456: Remote out-of-bounds heap write in read_args() when + the argument count lands exactly on maxargs + - CVE-2026-70457: Attacker-chosen-offset write in parse_size_arg() + error formatting + - CVE-2026-70458: Out-of-bounds write from a FLAG_HLINKED file entry + accepted without -H + - CVE-2026-70459: Per-connection daemon child crash from a crafted + first incremental file list with a non-directory transfer root + - CVE-2026-70460: Daemon module-root escape through a peer-supplied + --partial-dir / --backup-dir resolving via an in-module symlink + - CVE-2026-70461: Peer-driven one-byte heap out-of-bounds write in + add_implied_include() + - CVE-2026-70462: Peer-supplied MSG_IO_TIMEOUT defeats the client's own + I/O timeout (signed overflow, and a non-positive value) + - CVE-2026-70463: "auth users" ignores documented comma-only parsing, + silently skipping a deny/read-only rule + - CVE-2026-70464: Unauthenticated pre-transfer handshake DoS locks out + an rsync daemon module + * d/rsync.NEWS: Add a notice explaining the reasoning behind the version + bump in Stable. + * Pull six more patches from 3.5.1 to address regressions from 3.5.0: + - syscall_use_O_PATH_for_directory..., syscall_use_O_PATH_for_held...: + Fix the regression where a path through a directory that can be + searched but not read (mode 0711 or 0111) failed with "Permission + denied". + - rrsync_restore_restricted-root...: Fix the regression where rrsync + refused "/" and resolved option paths such as --link-dest=/previous + relative to the destination, so backups silently became full copies + instead of hard links + - options_c_Fix_files-from_confinement...: Stop refusing a --files-from + list outside --confine-root for local and remote-shell transfers + - syscall_follow_trusted_sender...: Fix the regression where a + --relative or --files-from source whose path goes through a symlink + failed with "Too many levels of symbolic links" + - receiver_c_Tighten_alt-dest...: Stop following a symlink as the + --link-dest/--copy-dest/--compare-dest basis file, which let a + malicious sender make the receiver copy the symlink's target into the + destination + * d/patches: Remove leftover patches from the 3.4.1 series, all of them + applied upstream in 3.5.0 + + [ Arnaud Rebillout ] + * d/control: Switch back to python3-cmarkgfm for all architectures + + [ Alexandre Detiste ] + * delete d/rules-pre-dh that shows up on Debian Code Search + * d/copyright: runtests.sh was refactored to runtests.py + * d/t/upstream-tests: runtests.sh was refactored to runtests.py + + [ Sylvain Beucler ] + * autopkgtest improvements + * Drop allow-stderr autopkgtest restriction + + -- Samuel Henrique Tue, 15 Sep 2026 18:46:30 -0700 + rsync (3.4.1+ds1-5+deb13u4) trixie; urgency=medium * Non-maintainer upload. diff -Nru rsync-3.4.1+ds1/debian/control rsync-3.5.0+ds1/debian/control --- rsync-3.4.1+ds1/debian/control 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/control 2026-09-16 01:46:30.000000000 +0000 @@ -12,7 +12,7 @@ zlib1g-dev, libssl-dev, python3:native, - python3-cmarkgfm:native [!hppa] | python3-commonmark [hppa], + python3-cmarkgfm:native, acl , attr , Standards-Version: 4.7.0 diff -Nru rsync-3.4.1+ds1/debian/copyright rsync-3.5.0+ds1/debian/copyright --- rsync-3.4.1+ds1/debian/copyright 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/copyright 2026-09-16 01:46:30.000000000 +0000 @@ -218,9 +218,10 @@ Copyright: 2001, 2002 Martin Pool License: GPL-2+ -Files: runtests.sh +Files: runtests.py Copyright: 2001, 2002 Martin Pool - 2003-2022 Wayne Davison + 2003-2022 Wayne Davison + 2026 Andrew Tridgell License: GPL-2 Files: testsuite/rsync.fns diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0001-bool-is-a-keyword-in-C23.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0001-bool-is-a-keyword-in-C23.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0001-bool-is-a-keyword-in-C23.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0001-bool-is-a-keyword-in-C23.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -From 86c59ba94407cc0fe56cb4b00d6fa84fdd494218 Mon Sep 17 00:00:00 2001 -From: Michal Ruprich -Date: Fri, 17 Jan 2025 12:37:57 +0100 -Subject: [PATCH 01/38] bool is a keyword in C23 - ---- - wildtest.c | 2 ++ - 1 file changed, 2 insertions(+) - -diff --git a/wildtest.c b/wildtest.c -index bea4cebb..482cdf17 100644 ---- a/wildtest.c -+++ b/wildtest.c -@@ -32,7 +32,9 @@ int fnmatch_errors = 0; - - int wildmatch_errors = 0; - -+#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 202311L - typedef char bool; -+#endif - - int output_iterations = 0; - int explode_mod = 0; --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0002-syscall-fix-a-Y2038-bug-by-replacing-Int32x32To64-wi.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0002-syscall-fix-a-Y2038-bug-by-replacing-Int32x32To64-wi.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0002-syscall-fix-a-Y2038-bug-by-replacing-Int32x32To64-wi.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0002-syscall-fix-a-Y2038-bug-by-replacing-Int32x32To64-wi.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,29 +0,0 @@ -From 4ce604114a80b1a7ce5580bbebffbfaa6df84bfb Mon Sep 17 00:00:00 2001 -From: Silent -Date: Mon, 13 Jan 2025 15:01:06 +0100 -Subject: [PATCH 02/38] syscall: fix a Y2038 bug by replacing Int32x32To64 with - multiplication - -Int32x32To64 macro internally truncates the arguments to int32, -while time_t is 64-bit on most/all modern platforms. -Therefore, usage of this macro creates a Year 2038 bug. ---- - syscall.c | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/syscall.c b/syscall.c -index 34a9bba0..604cebe2 100644 ---- a/syscall.c -+++ b/syscall.c -@@ -480,7 +480,7 @@ int do_SetFileTime(const char *path, time_t crtime) - free(pathw); - if (handle == INVALID_HANDLE_VALUE) - return -1; -- int64 temp_time = Int32x32To64(crtime, 10000000) + 116444736000000000LL; -+ int64 temp_time = (crtime * 10000000LL) + 116444736000000000LL; - FILETIME birth_time; - birth_time.dwLowDateTime = (DWORD)temp_time; - birth_time.dwHighDateTime = (DWORD)(temp_time >> 32); --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0003-options.c-Fix-segv-if-poptGetContext-returns-NULL.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0003-options.c-Fix-segv-if-poptGetContext-returns-NULL.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0003-options.c-Fix-segv-if-poptGetContext-returns-NULL.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0003-options.c-Fix-segv-if-poptGetContext-returns-NULL.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -From bbecd5bc1ad23d83bf83e8b107c1e16b05901a79 Mon Sep 17 00:00:00 2001 -From: Ronnie Sahlberg -Date: Thu, 30 Jan 2025 13:27:38 +1000 -Subject: [PATCH 03/38] options.c: Fix segv if poptGetContext returns NULL - -If poptGetContext returns NULL, perhaps due to OOM, -a NULL pointer is passed into poptReadDefaultConfig() -which in turns SEGVs when trying to dereference it. - -This was found using https://github.com/sahlberg/malloc-fail-tester.git -$ ./test_malloc_failure.sh rsync -Pav crash crosh - -Signed-off-by: Ronnie Sahlberg ---- - options.c | 4 ++++ - 1 file changed, 4 insertions(+) - -diff --git a/options.c b/options.c -index 578507c6..7cfe6391 100644 ---- a/options.c -+++ b/options.c -@@ -1369,6 +1369,10 @@ int parse_arguments(int *argc_p, const char ***argv_p) - /* TODO: Call poptReadDefaultConfig; handle errors. */ - - pc = poptGetContext(RSYNC_NAME, argc, argv, long_options, 0); -+ if (pc == NULL) { -+ strlcpy(err_buf, "poptGetContext returned NULL\n", sizeof err_buf); -+ return 0; -+ } - if (!am_server) { - poptReadDefaultConfig(pc, 0); - popt_unalias(pc, "--daemon"); --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0004-Using-a-correct-time-in-log-file.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0004-Using-a-correct-time-in-log-file.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0004-Using-a-correct-time-in-log-file.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0004-Using-a-correct-time-in-log-file.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,53 +0,0 @@ -From c966f3864d79aae6a4b97e78403f7441325edded Mon Sep 17 00:00:00 2001 -From: Michal Ruprich -Date: Fri, 31 Jan 2025 14:35:18 +0100 -Subject: [PATCH 04/38] Using a correct time in log file - ---- - options.c | 2 +- - tls.c | 2 +- - util1.c | 2 +- - 3 files changed, 3 insertions(+), 3 deletions(-) - -diff --git a/options.c b/options.c -index 7cfe6391..4ae1c58c 100644 ---- a/options.c -+++ b/options.c -@@ -1156,7 +1156,7 @@ static time_t parse_time(const char *arg) - { - const char *cp; - time_t val, now = time(NULL); -- struct tm t, *today = localtime(&now); -+ struct tm t, tmp, *today = localtime_r(&now, &tmp); - int in_date, old_mday, n; - - memset(&t, 0, sizeof t); -diff --git a/tls.c b/tls.c -index 858f8f10..7811e1fc 100644 ---- a/tls.c -+++ b/tls.c -@@ -127,7 +127,7 @@ static void storetime(char *dest, size_t destsize, time_t t, int nsecs) - { - if (t) { - int len; -- struct tm *mt = gmtime(&t); -+ struct tm tmp, *mt = gmtime_r(&t, &tmp); - - len = snprintf(dest, destsize, - " %04d-%02d-%02d %02d:%02d:%02d", -diff --git a/util1.c b/util1.c -index d84bc414..231d2206 100644 ---- a/util1.c -+++ b/util1.c -@@ -1389,7 +1389,7 @@ char *timestring(time_t t) - static int ndx = 0; - static char buffers[4][20]; /* We support 4 simultaneous timestring results. */ - char *TimeBuf = buffers[ndx = (ndx + 1) % 4]; -- struct tm *tm = localtime(&t); -+ struct tm tmp, *tm = localtime_r(&t, &tmp); - int len = snprintf(TimeBuf, sizeof buffers[0], "%4d/%02d/%02d %02d:%02d:%02d", - (int)tm->tm_year + 1900, (int)tm->tm_mon + 1, (int)tm->tm_mday, - (int)tm->tm_hour, (int)tm->tm_min, (int)tm->tm_sec); --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0005-configure.ac-check-for-xattr-support-both-in-libc-an.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0005-configure.ac-check-for-xattr-support-both-in-libc-an.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0005-configure.ac-check-for-xattr-support-both-in-libc-an.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0005-configure.ac-check-for-xattr-support-both-in-libc-an.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,54 +0,0 @@ -From ca987c47fb9dcef471ba43132b1ca3ee5ed5a6cd Mon Sep 17 00:00:00 2001 -From: Eli Schwartz -Date: Tue, 22 Apr 2025 16:17:55 -0400 -Subject: [PATCH 05/38] configure.ac: check for xattr support both in libc and - in -lattr - -In 2015, the attr/xattr.h header was fully removed from upstream attr. - -In 2020, rsync started preferring the standard header, if it exists: -https://github.com/RsyncProject/rsync/pull/22 - -But the fix was incomplete. We still looked for the getxattr function in --lattr, and used it if -lattr exists. This was the case even if the -system libc was sufficient to provide the needed functions. Result: -overlinking to -lattr, if it happened to be installed for any other -reason. - -``` -checking whether to support extended attributes... Using Linux xattrs -checking for getxattr in -lattr... yes -``` - -Instead, use a different autoconf macro that first checks if the -function is available for use without any libraries (e.g. it is in -libc). - -Result: - -``` -checking whether to support extended attributes... Using Linux xattrs -checking for library containing getxattr... none required -``` - -Signed-off-by: Eli Schwartz ---- - configure.ac | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/configure.ac b/configure.ac -index d2bcb471..4062651d 100644 ---- a/configure.ac -+++ b/configure.ac -@@ -1392,7 +1392,7 @@ else - AC_DEFINE(HAVE_LINUX_XATTRS, 1, [True if you have Linux xattrs (or equivalent)]) - AC_DEFINE(SUPPORT_XATTRS, 1) - AC_DEFINE(NO_SYMLINK_USER_XATTRS, 1, [True if symlinks do not support user xattrs]) -- AC_CHECK_LIB(attr,getxattr) -+ AC_SEARCH_LIBS(getxattr,attr) - ;; - darwin*) - AC_MSG_RESULT(Using OS X xattrs) --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0006-util-fixed-issue-in-clean_fname.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0006-util-fixed-issue-in-clean_fname.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0006-util-fixed-issue-in-clean_fname.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0006-util-fixed-issue-in-clean_fname.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,43 +0,0 @@ -From 21e0496559fb3b0209099c52977efe3516ea6ca3 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Sat, 23 Aug 2025 19:14:59 +1000 -Subject: [PATCH 06/38] util: fixed issue in clean_fname() - -fixes buffer underflow (not exploitable) in clean_fname ---- - util1.c | 12 ++++++++---- - 1 file changed, 8 insertions(+), 4 deletions(-) - -diff --git a/util1.c b/util1.c -index 231d2206..de634a84 100644 ---- a/util1.c -+++ b/util1.c -@@ -942,7 +942,7 @@ int count_dir_elements(const char *p) - * resulting name would be empty, returns ".". */ - int clean_fname(char *name, int flags) - { -- char *limit = name - 1, *t = name, *f = name; -+ char *limit = name, *t = name, *f = name; - int anchored; - - if (!name) -@@ -987,9 +987,13 @@ int clean_fname(char *name, int flags) - f += 2; - continue; - } -- while (s > limit && *--s != '/') {} -- if (s != t - 1 && (s < name || *s == '/')) { -- t = s + 1; -+ /* backing up for ".." — avoid reading before 'name' */ -+ while (s > limit && s[-1] != '/') -+ s--; -+ -+ /* If found prior '/', or we reached the start, adjust t. */ -+ if (s != t - 1 && (s <= name || *s == '/')) { -+ t = (s == name) ? name : s + 1; - f += 2; - continue; - } --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0007-testsuite-added-clean-fname-underflow-test.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0007-testsuite-added-clean-fname-underflow-test.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0007-testsuite-added-clean-fname-underflow-test.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0007-testsuite-added-clean-fname-underflow-test.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,85 +0,0 @@ -From 0df583089dc09a0a74a7434f493225d30f929102 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Sat, 23 Aug 2025 18:29:06 +1000 -Subject: [PATCH 07/38] testsuite: added clean-fname-underflow test - ---- - testsuite/clean-fname-underflow.test | 66 ++++++++++++++++++++++++++++ - 1 file changed, 66 insertions(+) - create mode 100644 testsuite/clean-fname-underflow.test - -diff --git a/testsuite/clean-fname-underflow.test b/testsuite/clean-fname-underflow.test -new file mode 100644 -index 00000000..56d4fece ---- /dev/null -+++ b/testsuite/clean-fname-underflow.test -@@ -0,0 +1,66 @@ -+#!/bin/sh -+# clean-fname-underflow.test -+# Ensure clean_fname() does not read-before-buffer when collapsing "..". -+# This exercises the --server path where a crafted merge filename hits clean_fname(). -+# -+# Usage: -+# ./configure && make -+# make check TESTS='clean-fname-underflow.test' -+ -+set -eu -+ -+# Try to find the just-built rsync binary if RSYNC_BIN isn't set. -+if [ -z "${RSYNC_BIN:-}" ]; then -+ if [ -x "./rsync" ]; then -+ RSYNC_BIN=./rsync -+ elif [ -x "../rsync" ]; then -+ RSYNC_BIN=../rsync -+ else -+ RSYNC_BIN=rsync -+ fi -+fi -+ -+workdir="${TMPDIR:-/tmp}/rsync-clean-fname.$$" -+mkdir -p "$workdir" -+trap 'rm -rf "$workdir"' EXIT INT TERM -+cd "$workdir" -+ -+# Minimal rsyncd.conf using chroot so the crafted path reaches the server parser. -+cat > rsyncd.conf <<'EOF' -+pid file = rsyncd.pid -+use chroot = true -+[mod] -+ path = ./mod -+ read only = false -+EOF -+mkdir -p mod -+ -+# Start daemon on a random high port. -+PORT=$(awk 'BEGIN{srand(); printf "%d", 20000+int(rand()*20000)}') -+"$RSYNC_BIN" --daemon --no-detach --config=rsyncd.conf --port="$PORT" >/dev/null 2>&1 & -+DAEMON_PID=$! -+# Give the daemon a moment to come up. -+sleep 0.3 -+ -+# Invoke the server-side path. We don't need a real transfer; we just want to -+# ensure clean_fname() doesn't crash when given "a/../test" via --filter=merge. -+EXIT_OK=0 -+if "$RSYNC_BIN" --server --sender -vlr --filter='merge a/../test' . mod/ >/dev/null 2>&1; then -+ EXIT_OK=1 -+else -+ status=$? -+ # Non-zero exit is expected for bogus input; ensure it wasn't a signal/crash. -+ if [ $status -lt 128 ]; then -+ EXIT_OK=1 -+ fi -+fi -+ -+kill "$DAEMON_PID" >/dev/null 2>&1 || true -+ -+if [ "$EXIT_OK" -ne 1 ]; then -+ echo "clean-fname-underflow.test: rsync exited due to a signal or unexpected status" -+ exit 1 -+fi -+ -+echo "OK: clean_fname() handled 'a/../test' without crashing" -+exit 0 --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0008-fixed-an-invalid-access-to-files-array.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0008-fixed-an-invalid-access-to-files-array.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0008-fixed-an-invalid-access-to-files-array.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0008-fixed-an-invalid-access-to-files-array.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,30 +0,0 @@ -From 82fe213f7f0b5e7221e248dd398bbb682de5800e Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Sat, 23 Aug 2025 17:26:53 +1000 -Subject: [PATCH 08/38] fixed an invalid access to files array - -this was found by Calum Hutton from Rapid7. It is a real bug, but -analysis shows it can't be leverged into an exploit. Worth fixing -though. - -Many thanks to Calum and Rapid7 for finding and reporting this ---- - sender.c | 2 ++ - 1 file changed, 2 insertions(+) - -diff --git a/sender.c b/sender.c -index a4d46c39..b1588b70 100644 ---- a/sender.c -+++ b/sender.c -@@ -262,6 +262,8 @@ void send_files(int f_in, int f_out) - - if (ndx - cur_flist->ndx_start >= 0) - file = cur_flist->files[ndx - cur_flist->ndx_start]; -+ else if (cur_flist->parent_ndx < 0) -+ exit_cleanup(RERR_PROTOCOL); - else - file = dir_flist->files[cur_flist->parent_ndx]; - if (F_PATHNAME(file)) { --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0009-fix-uninitialized-buf1-in-get_checksum2-MD4-path.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0009-fix-uninitialized-buf1-in-get_checksum2-MD4-path.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0009-fix-uninitialized-buf1-in-get_checksum2-MD4-path.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0009-fix-uninitialized-buf1-in-get_checksum2-MD4-path.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,33 +0,0 @@ -From 487a548f70fa418d99e96f4ec77c6139d7e03ed6 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Tue, 30 Dec 2025 16:21:41 +1100 -Subject: [PATCH 09/38] fix uninitialized buf1 in get_checksum2() MD4 path - -The static buf1 pointer was only allocated when len > len1, but on -first call with len == 0, this condition is false (0 > 0), leaving -buf1 NULL when passed to memcpy(). - -Fixes #673 ---- - checksum.c | 5 ++--- - 1 file changed, 2 insertions(+), 3 deletions(-) - -diff --git a/checksum.c b/checksum.c -index 66e80896..6f0f95ab 100644 ---- a/checksum.c -+++ b/checksum.c -@@ -366,9 +366,8 @@ void get_checksum2(char *buf, int32 len, char *sum) - - mdfour_begin(&m); - -- if (len > len1) { -- if (buf1) -- free(buf1); -+ if (len > len1 || !buf1) { -+ free(buf1); - buf1 = new_array(char, len+4); - len1 = len; - } --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0010-reject-negative-token-values-in-compressed-stream-re.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0010-reject-negative-token-values-in-compressed-stream-re.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0010-reject-negative-token-values-in-compressed-stream-re.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0010-reject-negative-token-values-in-compressed-stream-re.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,72 +0,0 @@ -From 4585f8a6f22fc93993002c7bae7ddd174a49dc86 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Tue, 30 Dec 2025 18:49:34 +1100 -Subject: [PATCH 10/38] reject negative token values in compressed stream - receivers - -Validate that token numbers read from compressed streams are -non-negative. A negative token value would cause the return value -of recv_*_token() to become positive, which callers interpret as -literal data length, but no data pointer is set on this code path. - -While this only causes the receiver to crash (which is process-isolated -and only affects the attacker's own connection), it's still undefined -behavior. - -Reported-by: Will Sergeant ---- - token.c | 21 ++++++++++++++++++--- - 1 file changed, 18 insertions(+), 3 deletions(-) - -diff --git a/token.c b/token.c -index c108b3af..b7a02ea1 100644 ---- a/token.c -+++ b/token.c -@@ -589,8 +589,13 @@ static int32 recv_deflated_token(int f, char **data) - if (flag & TOKEN_REL) { - rx_token += flag & 0x3f; - flag >>= 6; -- } else -+ } else { - rx_token = read_int(f); -+ if (rx_token < 0) { -+ rprintf(FERROR, "invalid token number in compressed stream\n"); -+ exit_cleanup(RERR_PROTOCOL); -+ } -+ } - if (flag & 1) { - rx_run = read_byte(f); - rx_run += read_byte(f) << 8; -@@ -831,8 +836,13 @@ static int32 recv_zstd_token(int f, char **data) - if (flag & TOKEN_REL) { - rx_token += flag & 0x3f; - flag >>= 6; -- } else -+ } else { - rx_token = read_int(f); -+ if (rx_token < 0) { -+ rprintf(FERROR, "invalid token number in compressed stream\n"); -+ exit_cleanup(RERR_PROTOCOL); -+ } -+ } - if (flag & 1) { - rx_run = read_byte(f); - rx_run += read_byte(f) << 8; -@@ -995,8 +1005,13 @@ static int32 recv_compressed_token(int f, char **data) - if (flag & TOKEN_REL) { - rx_token += flag & 0x3f; - flag >>= 6; -- } else -+ } else { - rx_token = read_int(f); -+ if (rx_token < 0) { -+ rprintf(FERROR, "invalid token number in compressed stream\n"); -+ exit_cleanup(RERR_PROTOCOL); -+ } -+ } - if (flag & 1) { - rx_run = read_byte(f); - rx_run += read_byte(f) << 8; --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0011-acl-fixed-ACL-ID-mapping-for-non-root.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0011-acl-fixed-ACL-ID-mapping-for-non-root.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0011-acl-fixed-ACL-ID-mapping-for-non-root.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0011-acl-fixed-ACL-ID-mapping-for-non-root.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -From 79ffc5e3c5b9337a5840ab019107ca8c44a97eb4 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Mon, 19 Jan 2026 11:14:40 +1100 -Subject: [PATCH 11/38] acl: fixed ACL ID mapping for non-root - -closes issue #618 ---- - acls.c | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/acls.c b/acls.c -index bd119e8e..4d67ff4d 100644 ---- a/acls.c -+++ b/acls.c -@@ -713,7 +713,7 @@ static uchar recv_ida_entries(int f, ida_entries *ent) - else - id = recv_group_name(f, id, NULL); - } else if (access & NAME_IS_USER) { -- if (inc_recurse && am_root && !numeric_ids) -+ if (inc_recurse && !numeric_ids) - id = match_uid(id); - } else { - if (inc_recurse && (!am_root || !numeric_ids)) --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0012-fix-uninitialized-mul_one-in-AVX2-checksum-and-add-S.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0012-fix-uninitialized-mul_one-in-AVX2-checksum-and-add-S.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0012-fix-uninitialized-mul_one-in-AVX2-checksum-and-add-S.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0012-fix-uninitialized-mul_one-in-AVX2-checksum-and-add-S.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,205 +0,0 @@ -From 350469f7cf1c25a4fcf991f1337202f4bcac8912 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Sun, 1 Mar 2026 08:42:04 +1100 -Subject: [PATCH 12/38] fix uninitialized mul_one in AVX2 checksum and add SIMD - checksum test - -The AVX2 get_checksum1_avx2_64() read mul_one before initializing it, -which is undefined behavior. Replace the cmpeq/abs trick with -_mm256_set1_epi8(1) to match the SSSE3 and SSE2 versions. - -Add a TEST_SIMD_CHECKSUM1 test mode that verifies all SIMD paths -(SSE2, SSSE3, AVX2, and the full dispatch chain) produce identical -results to the C reference, across multiple buffer sizes with both -aligned and unaligned buffers. - -Co-Authored-By: Claude Opus 4.6 ---- - Makefile.in | 11 +++- - simd-checksum-x86_64.cpp | 115 ++++++++++++++++++++++++++++++++++- - testsuite/simd-checksum.test | 11 ++++ - 3 files changed, 134 insertions(+), 3 deletions(-) - create mode 100755 testsuite/simd-checksum.test - -diff --git a/Makefile.in b/Makefile.in -index 7c75c261..75fd03af 100644 ---- a/Makefile.in -+++ b/Makefile.in -@@ -57,7 +57,8 @@ TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/perms - - # Programs we must have to run the test cases - CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \ -- testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) wildtest$(EXEEXT) -+ testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) wildtest$(EXEEXT) \ -+ simdtest$(EXEEXT) - - CHECK_SYMLINKS = testsuite/chown-fake.test testsuite/devices-fake.test testsuite/xattrs-hlink.test - -@@ -326,6 +327,14 @@ wildtest.o: wildtest.c t_stub.o lib/wildmatch.c rsync.h config.h - wildtest$(EXEEXT): wildtest.o lib/compat.o lib/snprintf.o @BUILD_POPT@ - $(CC) $(CFLAGS) $(LDFLAGS) -o $@ wildtest.o lib/compat.o lib/snprintf.o @BUILD_POPT@ $(LIBS) - -+simdtest$(EXEEXT): simd-checksum-x86_64.cpp $(HEADERS) -+ @if test x"@ROLL_SIMD@" != x; then \ -+ $(CXX) -I. $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) -DTEST_SIMD_CHECKSUM1 \ -+ -o $@ $(srcdir)/simd-checksum-x86_64.cpp @ROLL_ASM@ $(LIBS); \ -+ else \ -+ touch $@; \ -+ fi -+ - testsuite/chown-fake.test: - ln -s chown.test $(srcdir)/testsuite/chown-fake.test - -diff --git a/simd-checksum-x86_64.cpp b/simd-checksum-x86_64.cpp -index d649091e..99391cbe 100644 ---- a/simd-checksum-x86_64.cpp -+++ b/simd-checksum-x86_64.cpp -@@ -347,8 +347,7 @@ __attribute__ ((target("avx2"))) MVSTATIC int32 get_checksum1_avx2_64(schar* buf - __m128i tmp = _mm_load_si128((__m128i*) mul_t1_buf); - __m256i mul_t1 = _mm256_cvtepu8_epi16(tmp); - __m256i mul_const = _mm256_broadcastd_epi32(_mm_cvtsi32_si128(4 | (3 << 8) | (2 << 16) | (1 << 24))); -- __m256i mul_one; -- mul_one = _mm256_abs_epi8(_mm256_cmpeq_epi16(mul_one,mul_one)); // set all vector elements to 1 -+ __m256i mul_one = _mm256_set1_epi8(1); - - for (; i < (len-64); i+=64) { - // Load ... 4*[int8*16] -@@ -548,6 +547,118 @@ int main() { - #pragma clang optimize on - #endif /* BENCHMARK_SIMD_CHECKSUM1 */ - -+#ifdef TEST_SIMD_CHECKSUM1 -+ -+static uint32 checksum_via_default(char *buf, int32 len) -+{ -+ uint32 s1 = 0, s2 = 0; -+ get_checksum1_default_1((schar*)buf, len, 0, &s1, &s2); -+ return (s1 & 0xffff) + (s2 << 16); -+} -+ -+static uint32 checksum_via_sse2(char *buf, int32 len) -+{ -+ int32 i; -+ uint32 s1 = 0, s2 = 0; -+ i = get_checksum1_sse2_32((schar*)buf, len, 0, &s1, &s2); -+ get_checksum1_default_1((schar*)buf, len, i, &s1, &s2); -+ return (s1 & 0xffff) + (s2 << 16); -+} -+ -+static uint32 checksum_via_ssse3(char *buf, int32 len) -+{ -+ int32 i; -+ uint32 s1 = 0, s2 = 0; -+ i = get_checksum1_ssse3_32((schar*)buf, len, 0, &s1, &s2); -+ get_checksum1_default_1((schar*)buf, len, i, &s1, &s2); -+ return (s1 & 0xffff) + (s2 << 16); -+} -+ -+static uint32 checksum_via_avx2(char *buf, int32 len) -+{ -+ int32 i; -+ uint32 s1 = 0, s2 = 0; -+#ifdef USE_ROLL_ASM -+ i = get_checksum1_avx2_asm((schar*)buf, len, 0, &s1, &s2); -+#else -+ i = get_checksum1_avx2_64((schar*)buf, len, 0, &s1, &s2); -+#endif -+ get_checksum1_default_1((schar*)buf, len, i, &s1, &s2); -+ return (s1 & 0xffff) + (s2 << 16); -+} -+ -+int main() -+{ -+ static const int sizes[] = {1, 4, 31, 32, 33, 63, 64, 65, 128, 129, 256, 700, 1024, 4096, 65536}; -+ int num_sizes = sizeof(sizes) / sizeof(sizes[0]); -+ int max_size = sizes[num_sizes - 1]; -+ int failures = 0; -+ -+ /* Allocate with extra bytes for unaligned test */ -+ unsigned char *raw = (unsigned char *)malloc(max_size + 64 + 1); -+ if (!raw) { -+ fprintf(stderr, "malloc failed\n"); -+ return 1; -+ } -+ -+ /* Fill with deterministic data */ -+ for (int i = 0; i < max_size + 64 + 1; i++) -+ raw[i] = (i + (i % 3) + (i % 11)) % 256; -+ -+ /* Test with aligned buffer (64-byte aligned) */ -+ unsigned char *aligned = raw + (64 - ((uintptr_t)raw % 64)); -+ -+ /* Test with unaligned buffer (+1 byte offset) */ -+ unsigned char *unaligned = aligned + 1; -+ -+ struct { const char *name; unsigned char *buf; } buffers[] = { -+ {"aligned", aligned}, -+ {"unaligned", unaligned}, -+ }; -+ -+ for (int b = 0; b < 2; b++) { -+ char *buf = (char *)buffers[b].buf; -+ const char *bname = buffers[b].name; -+ -+ for (int s = 0; s < num_sizes; s++) { -+ int32 len = sizes[s]; -+ uint32 ref = checksum_via_default(buf, len); -+ uint32 cs_sse2 = checksum_via_sse2(buf, len); -+ uint32 cs_ssse3 = checksum_via_ssse3(buf, len); -+ uint32 cs_avx2 = checksum_via_avx2(buf, len); -+ uint32 cs_auto = get_checksum1(buf, len); -+ -+ if (cs_sse2 != ref) { -+ printf("FAIL %-9s size=%5d: SSE2=%08x ref=%08x\n", bname, len, cs_sse2, ref); -+ failures++; -+ } -+ if (cs_ssse3 != ref) { -+ printf("FAIL %-9s size=%5d: SSSE3=%08x ref=%08x\n", bname, len, cs_ssse3, ref); -+ failures++; -+ } -+ if (cs_avx2 != ref) { -+ printf("FAIL %-9s size=%5d: AVX2=%08x ref=%08x\n", bname, len, cs_avx2, ref); -+ failures++; -+ } -+ if (cs_auto != ref) { -+ printf("FAIL %-9s size=%5d: auto=%08x ref=%08x\n", bname, len, cs_auto, ref); -+ failures++; -+ } -+ } -+ } -+ -+ free(raw); -+ -+ if (failures) { -+ printf("%d checksum mismatches!\n", failures); -+ return 1; -+ } -+ printf("All SIMD checksum tests passed.\n"); -+ return 0; -+} -+ -+#endif /* TEST_SIMD_CHECKSUM1 */ -+ - #endif /* } USE_ROLL_SIMD */ - #endif /* } __cplusplus */ - #endif /* } __x86_64__ */ -diff --git a/testsuite/simd-checksum.test b/testsuite/simd-checksum.test -new file mode 100755 -index 00000000..cf7dba2e ---- /dev/null -+++ b/testsuite/simd-checksum.test -@@ -0,0 +1,11 @@ -+#!/bin/sh -+ -+# Test SIMD checksum implementations against the C reference -+ -+. "$suitedir/rsync.fns" -+ -+if ! test -x "$TOOLDIR/simdtest"; then -+ test_skipped "simdtest not built (SIMD not available)" -+fi -+ -+"$TOOLDIR/simdtest" --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0013-Fix-glibc-2.43-constness-warnings.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0013-Fix-glibc-2.43-constness-warnings.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0013-Fix-glibc-2.43-constness-warnings.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0013-Fix-glibc-2.43-constness-warnings.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,115 +0,0 @@ -From 6994fdf50ef0dde11b8b014a6d198c6f423d67fc Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Holger=20Hoffst=C3=A4tte?= -Date: Mon, 6 Apr 2026 00:44:02 +0200 -Subject: [PATCH 13/38] Fix glibc-2.43 constness warnings -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -Glibc 2.43 added C23 const-preserving overloads to various string functions, -which change the return type depending on the constness of the argument(s). -Currently this leads to warnings from calls to strtok() or strchr(). -Fix this by properly declaring the respective variable types. - -Signed-off-by: Holger Hoffstätte ---- - access.c | 2 +- - checksum.c | 2 +- - compat.c | 4 ++-- - exclude.c | 2 +- - io.c | 4 ++-- - loadparm.c | 2 +- - 6 files changed, 8 insertions(+), 8 deletions(-) - -diff --git a/access.c b/access.c -index b6afce37..b924e0a3 100644 ---- a/access.c -+++ b/access.c -@@ -99,7 +99,7 @@ static void make_mask(char *mask, int plen, int addrlen) - return; - } - --static int match_address(const char *addr, const char *tok) -+static int match_address(const char *addr, char *tok) - { - char *p; - struct addrinfo hints, *resa, *rest; -diff --git a/checksum.c b/checksum.c -index 6f0f95ab..24e46bfb 100644 ---- a/checksum.c -+++ b/checksum.c -@@ -176,7 +176,7 @@ void parse_checksum_choice(int final_call) - if (valid_checksums.negotiated_nni) - xfer_sum_nni = file_sum_nni = valid_checksums.negotiated_nni; - else { -- char *cp = checksum_choice ? strchr(checksum_choice, ',') : NULL; -+ const char *cp = checksum_choice ? strchr(checksum_choice, ',') : NULL; - if (cp) { - xfer_sum_nni = parse_csum_name(checksum_choice, cp - checksum_choice); - file_sum_nni = parse_csum_name(cp+1, -1); -diff --git a/compat.c b/compat.c -index 4ce8c6d0..37d20f4f 100644 ---- a/compat.c -+++ b/compat.c -@@ -131,7 +131,7 @@ static const char *client_info; - * of that protocol for it to be advertised as available. */ - static void check_sub_protocol(void) - { -- char *dot; -+ const char *dot; - int their_protocol, their_sub; - int our_sub = get_subprotocol_version(); - -@@ -414,7 +414,7 @@ static const char *getenv_nstr(int ntype) - env_str = ntype == NSTR_COMPRESS ? "zlib" : protocol_version >= 30 ? "md5" : "md4"; - - if (am_server && env_str) { -- char *cp = strchr(env_str, '&'); -+ const char *cp = strchr(env_str, '&'); - if (cp) - env_str = cp + 1; - } -diff --git a/exclude.c b/exclude.c -index 87edbcf7..24de64f8 100644 ---- a/exclude.c -+++ b/exclude.c -@@ -904,7 +904,7 @@ static int rule_matches(const char *fname, filter_rule *ex, int name_flags) - { - int slash_handling, str_cnt = 0, anchored_match = 0; - int ret_match = ex->rflags & FILTRULE_NEGATE ? 0 : 1; -- char *p, *pattern = ex->pattern; -+ const char *p, *pattern = ex->pattern; - const char *strings[16]; /* more than enough */ - const char *name = fname + (*fname == '/'); - -diff --git a/io.c b/io.c -index bb60eeca..8d1cf7f2 100644 ---- a/io.c -+++ b/io.c -@@ -1158,8 +1158,8 @@ void set_io_timeout(int secs) - - static void check_for_d_option_error(const char *msg) - { -- static char rsync263_opts[] = "BCDHIKLPRSTWabceghlnopqrtuvxz"; -- char *colon; -+ static const char rsync263_opts[] = "BCDHIKLPRSTWabceghlnopqrtuvxz"; -+ const char *colon; - int saw_d = 0; - - if (*msg != 'r' -diff --git a/loadparm.c b/loadparm.c -index 3906bc0f..4f371d77 100644 ---- a/loadparm.c -+++ b/loadparm.c -@@ -178,7 +178,7 @@ static char *expand_vars(const char *str) - - for (t = buf, f = str; bufsize && *f; ) { - if (*f == '%' && isUpper(f+1)) { -- char *percent = strchr(f+1, '%'); -+ const char *percent = strchr(f+1, '%'); - if (percent && percent - f < bufsize) { - char *val; - strlcpy(t, f+1, percent - f); --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0015-fix-signed-integer-overflow-in-proxy-protocol-v2-hea.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0015-fix-signed-integer-overflow-in-proxy-protocol-v2-hea.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0015-fix-signed-integer-overflow-in-proxy-protocol-v2-hea.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0015-fix-signed-integer-overflow-in-proxy-protocol-v2-hea.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,38 +0,0 @@ -From c35df318adc2429ca0b2b34c75eae02f29e8edcc Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Thu, 16 Apr 2026 10:50:49 +1000 -Subject: [PATCH 15/38] fix signed integer overflow in proxy protocol v2 header - parsing - -The len field in the proxy v2 header was declared as signed char, -allowing a negative size to bypass the validation check and cause -a stack buffer overflow when passed to read_buf() as size_t. - -This bug was reported by John Walker from ZeroPath, many thanks for -the clear report! - -With the current code this bug does not represent a security issue as -it only results in the exit of the forked process that is specific to -the attached client, so it is equivalent to the client closing the -socket, so no CVE for this, but it is good to fix it to prevent a -future issue. ---- - clientname.c | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/clientname.c b/clientname.c -index ea94894b..dbac38b9 100644 ---- a/clientname.c -+++ b/clientname.c -@@ -167,7 +167,7 @@ int read_proxy_protocol_header(int fd) - char sig[PROXY_V2_SIG_SIZE]; - char ver_cmd; - char fam; -- char len[2]; -+ unsigned char len[2]; - union { - struct { - char src_addr[4]; --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0016-zero-all-new-memory-from-allocations.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0016-zero-all-new-memory-from-allocations.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0016-zero-all-new-memory-from-allocations.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0016-zero-all-new-memory-from-allocations.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,48 +0,0 @@ -From dff93c92d1b0d576b385688db9c66773da4ab0a1 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Wed, 22 Apr 2026 10:59:11 +1000 -Subject: [PATCH 16/38] zero all new memory from allocations - -Change my_alloc() to use calloc instead of malloc so all fresh -allocations return zeroed memory. Also zero the expanded portion -in expand_item_list() after realloc, since it knows both old and -new sizes. This gives more predictable behaviour in case of bugs -where uninitialised or stale memory is accidentally accessed. - -Co-Authored-By: Claude Opus 4.6 (1M context) ---- - util1.c | 2 ++ - util2.c | 4 +--- - 2 files changed, 3 insertions(+), 3 deletions(-) - -diff --git a/util1.c b/util1.c -index de634a84..25ac7c9b 100644 ---- a/util1.c -+++ b/util1.c -@@ -1718,6 +1718,8 @@ void *expand_item_list(item_list *lp, size_t item_size, const char *desc, int in - new_ptr == lp->items ? " not" : ""); - } - -+ memset((char *)new_ptr + lp->malloced * item_size, 0, -+ (expand_size - lp->malloced) * item_size); - lp->items = new_ptr; - lp->malloced = expand_size; - } -diff --git a/util2.c b/util2.c -index b59bff0a..ce6f7de1 100644 ---- a/util2.c -+++ b/util2.c -@@ -79,9 +79,7 @@ void *my_alloc(void *ptr, size_t num, size_t size, const char *file, int line) - who_am_i(), do_big_num(max_alloc, 0, NULL), src_file(file), line); - exit_cleanup(RERR_MALLOC); - } -- if (!ptr) -- ptr = malloc(num * size); -- else if (ptr == do_calloc) -+ if (!ptr || ptr == do_calloc) - ptr = calloc(num, size); - else - ptr = realloc(ptr, num * size); --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0017-xattrs-fixed-count-in-qsort.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0017-xattrs-fixed-count-in-qsort.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0017-xattrs-fixed-count-in-qsort.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0017-xattrs-fixed-count-in-qsort.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -From c009fcc8e6e61f20231e4a512faa174fa7c2eecd Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Wed, 22 Apr 2026 09:57:45 +1000 -Subject: [PATCH 17/38] xattrs: fixed count in qsort - -this fixes the count passed to the sort of the xattr list. This issue -was reported here: - -https://www.openwall.com/lists/oss-security/2026/04/16/2 - -the bug is not exploitable due to the fork-per-connection design of -rsync, the attack is the equivalent of the user closing the socket -themselves. ---- - xattrs.c | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/xattrs.c b/xattrs.c -index 26e50a6f..65166eed 100644 ---- a/xattrs.c -+++ b/xattrs.c -@@ -860,8 +860,8 @@ void receive_xattr(int f, struct file_struct *file) - rxa->num = num; - } - -- if (need_sort && count > 1) -- qsort(temp_xattr.items, count, sizeof (rsync_xa), rsync_xal_compare_names); -+ if (need_sort && temp_xattr.count > 1) -+ qsort(temp_xattr.items, temp_xattr.count, sizeof (rsync_xa), rsync_xal_compare_names); - - ndx = rsync_xal_store(&temp_xattr); /* adds item to rsync_xal_l */ - --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0018-call-tzset-before-chroot-to-cache-timezone-data.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0018-call-tzset-before-chroot-to-cache-timezone-data.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0018-call-tzset-before-chroot-to-cache-timezone-data.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0018-call-tzset-before-chroot-to-cache-timezone-data.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,39 +0,0 @@ -From 892b48a60b9e6d604de54034002bee65d7bcb4e2 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Wed, 22 Apr 2026 12:53:13 +1000 -Subject: [PATCH 18/38] call tzset() before chroot to cache timezone data - -localtime/localtime_r need /etc/localtime for timezone info. -After chroot this file is inaccessible, causing log timestamps -to fall back to UTC. Calling tzset() before chroot ensures the -timezone data is cached by glibc for subsequent calls. - -Co-Authored-By: Claude Opus 4.6 (1M context) ---- - clientserver.c | 3 +++ - 1 file changed, 3 insertions(+) - -diff --git a/clientserver.c b/clientserver.c -index 7c897abc..3800f0d6 100644 ---- a/clientserver.c -+++ b/clientserver.c -@@ -976,6 +976,8 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char - } - - if (use_chroot) { -+ /* Cache timezone data before chroot makes /etc/localtime inaccessible */ -+ tzset(); - if (chroot(module_chdir)) { - rsyserr(FLOG, errno, "chroot(\"%s\") failed", module_chdir); - io_printf(f_out, "@ERROR: chroot failed\n"); -@@ -1301,6 +1303,7 @@ int start_daemon(int f_in, int f_out) - p = lp_daemon_chroot(); - if (*p) { - log_init(0); /* Make use we've initialized syslog before chrooting. */ -+ tzset(); - if (chroot(p) < 0) { - rsyserr(FLOG, errno, "daemon chroot(\"%s\") failed", p); - return -1; --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0019-testsuite-xattrs-ignore-SUNWattr_-in-the-Solaris-xls.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0019-testsuite-xattrs-ignore-SUNWattr_-in-the-Solaris-xls.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0019-testsuite-xattrs-ignore-SUNWattr_-in-the-Solaris-xls.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0019-testsuite-xattrs-ignore-SUNWattr_-in-the-Solaris-xls.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,45 +0,0 @@ -From 3e5e159459fbb46b01517d5f9e6f98d914bab861 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Thu, 30 Apr 2026 08:18:01 +1000 -Subject: [PATCH 19/38] testsuite/xattrs: ignore SUNWattr_* in the Solaris xls - helper - -The Solaris xls() function listed every entry in the file's xattr -directory, which on Solaris includes OS-managed SUNWattr_ro and -SUNWattr_rw pseudo-attributes. SUNWattr_rw embeds the file creation -time, so its bytes naturally differ between the source and destination -files, making the xattrs and xattrs-hlink tests fail with diffs that -have nothing to do with rsync. - -Rsync's own listxattr wrapper already filters these out -(lib/sysxattrs.c), so the right fix is to filter them in the test -display too. Other platforms are unaffected because each has its own -xls() branch in the case statement. - -With the test now actually passing on Solaris, drop the CI hack that -overwrote testsuite/xattrs.test with a skip stub. - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - testsuite/xattrs.test | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) - -diff --git a/testsuite/xattrs.test b/testsuite/xattrs.test -index d94d5f95..c0f3784b 100644 ---- a/testsuite/xattrs.test -+++ b/testsuite/xattrs.test -@@ -38,7 +38,10 @@ EOF - xls() { - for fn in "${@}"; do - runat "$fn" "$SHELL_PATH" < -Date: Thu, 30 Apr 2026 08:39:22 +1000 -Subject: [PATCH 20/38] syscall: use openat2(RESOLVE_BENEATH) on Linux for - secure_relative_open - -The CVE fix in commit c35e283 made secure_relative_open() walk every -component of relpath with O_NOFOLLOW. That blocks every symlink in the -path, which is stricter than the threat model required: legitimate -directory symlinks within the destination tree (e.g. when using -K / ---copy-dirlinks) are also rejected, breaking delta transfers with -"failed verification -- update discarded". See issue #715. - -On Linux 5.6+, openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS) gives -us exactly what we want: the kernel rejects any resolution that would -escape the starting directory (via "..", absolute paths, or symlinks -pointing outside dirfd) while still following symlinks that resolve -within it. /proc magic-links are blocked too. - -Use openat2 first; fall back to the existing per-component O_NOFOLLOW -walk on ENOSYS (kernel < 5.6). The lexical "../" checks at the head -of the function are kept as defense in depth. The Linux gate is -plain #ifdef __linux__: the runtime ENOSYS fallback covers the only -case that actually matters (header present + old kernel), and any -Linux build environment without linux/openat2.h will fail with a -clear "no such file" error rather than silently disabling the -protection. - -Verified manually that openat2(RESOLVE_BENEATH) blocks all four -escape patterns (absolute symlink, ../ symlink, lexical .., absolute -path) while allowing direct and within-tree symlinks. The new -testsuite/symlink-dirlink-basis.test (taken from PR #864 by Samuel -Henrique) exercises the issue #715 regression and passes; full -make check passes 47/47. - -Test: testsuite/symlink-dirlink-basis.test (8 scenarios) -Fixes: https://github.com/RsyncProject/rsync/issues/715 - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - syscall.c | 62 ++++++- - testsuite/symlink-dirlink-basis.test | 247 +++++++++++++++++++++++++++ - 2 files changed, 304 insertions(+), 5 deletions(-) - create mode 100755 testsuite/symlink-dirlink-basis.test - -diff --git a/syscall.c b/syscall.c -index 604cebe2..0881c7ab 100644 ---- a/syscall.c -+++ b/syscall.c -@@ -33,6 +33,11 @@ - #include - #endif - -+#ifdef __linux__ -+#include -+#include -+#endif -+ - #include "ifuncs.h" - - extern int dry_run; -@@ -715,12 +720,49 @@ int do_open_nofollow(const char *pathname, int flags) - /* - 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, and the relpath must not contain any -- elements in the path which follow symlinks (ie. like O_NOFOLLOW, but -- applies to all path components, not just the last component) -- -- The relpath must also not contain any ../ elements in the path -+ must be a relative path. The kernel must guarantee that resolution -+ cannot escape basedir (or the cwd, when basedir is NULL): no ".." -+ jumps above the start, no symlinks pointing outside, no absolute -+ paths, no /proc magic-link tricks. -+ -+ 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). The escape -+ prevention is handled by the kernel via openat2(RESOLVE_BENEATH) -+ on Linux 5.6+; older systems fall back to the per-component -+ O_NOFOLLOW walk below. -+ -+ The relpath must also not contain any ../ elements in the path. - */ -+ -+#ifdef __linux__ -+static int secure_relative_open_linux(const char *basedir, const char *relpath, int flags, mode_t mode) -+{ -+ struct open_how how; -+ int dirfd, retfd; -+ -+ memset(&how, 0, sizeof how); -+ how.flags = flags; -+ how.mode = mode; -+ how.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS; -+ -+ if (basedir == NULL) { -+ dirfd = AT_FDCWD; -+ } else { -+ dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); -+ if (dirfd == -1) -+ return -1; -+ } -+ -+ retfd = syscall(SYS_openat2, dirfd, relpath, &how, sizeof how); -+ -+ if (dirfd != AT_FDCWD) -+ close(dirfd); -+ return retfd; -+} -+#endif -+ - int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode) - { - if (!relpath || relpath[0] == '/') { -@@ -734,6 +776,16 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo - return -1; - } - -+#ifdef __linux__ -+ { -+ int fd = secure_relative_open_linux(basedir, relpath, flags, mode); -+ /* ENOSYS = kernel < 5.6 doesn't have the syscall even though -+ * glibc/kernel-headers do; fall through to the portable path. */ -+ if (fd != -1 || errno != ENOSYS) -+ return fd; -+ } -+#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) { -diff --git a/testsuite/symlink-dirlink-basis.test b/testsuite/symlink-dirlink-basis.test -new file mode 100755 -index 00000000..9065dd81 ---- /dev/null -+++ b/testsuite/symlink-dirlink-basis.test -@@ -0,0 +1,247 @@ -+#!/bin/sh -+ -+# Test that updating a file through a directory symlink works when using -+# -K (--copy-dirlinks). This is a regression test for: -+# https://github.com/RsyncProject/rsync/issues/715 -+# -+# The CVE fix in commit c35e283 introduced secure_relative_open() which -+# uses O_NOFOLLOW on all path components, breaking legitimate directory -+# symlinks on the receiver side. The fix splits the path into basedir -+# (dirname, symlinks followed) and basename (O_NOFOLLOW) so that -+# directory symlinks are traversed while the final file component is -+# still protected. -+# -+# The regression only manifests when delta matching is triggered (i.e., -+# the sender finds matching blocks in the old file). Small files with -+# completely different content are transferred in full and don't trigger -+# the bug. We use a large file with a small modification to ensure -+# delta transfer is used. -+# -+# In addition to the original regression, this test covers edge cases -+# in the fix itself: -+# - --backup with directory symlinks (finish_transfer pointer identity) -+# - --partial-dir with protocol < 29 (fnamecmp != partialptr guard) -+# - --inplace with directory symlinks (updating_basis_or_equiv check) -+# - Files without a dirname (top-level files, no split needed) -+ -+. "$suitedir/rsync.fns" -+ -+RSYNC_RSH="$scratchdir/src/support/lsh.sh" -+export RSYNC_RSH -+ -+# $HOME is set to $scratchdir by rsync.fns -+# localhost: destination will cd to $HOME (i.e., $scratchdir) -+ -+# Helper: create a large file suitable for delta transfers. -+# ~32KB is large enough for rsync's block matching to find matches. -+make_testfile() { -+ dd if=/dev/urandom of="$1" bs=1024 count=32 2>/dev/null \ -+ || test_fail "failed to create test file $1" -+} -+ -+# Set up source tree -+srcbase="$tmpdir/src" -+ -+###################################################################### -+# Test 1: Basic directory symlink update (the original issue #715) -+###################################################################### -+ -+mkdir -p "$HOME/real-dir" -+ln -s real-dir "$HOME/dir" -+ -+mkdir -p "$srcbase/dir" -+make_testfile "$srcbase/dir/file" -+ -+# First transfer (initial): should create the file through the symlink -+(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" dir/file localhost:) \ -+ || test_fail "test 1: initial transfer failed" -+ -+if [ ! -f "$HOME/real-dir/file" ]; then -+ test_fail "test 1: initial transfer did not create file through symlink" -+fi -+ -+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ -+ || test_fail "test 1: initial transfer content mismatch" -+ -+# Small modification to trigger delta transfer -+echo "appended update" >> "$srcbase/dir/file" -+sleep 1 -+touch "$srcbase/dir/file" -+ -+# Second transfer (update): was failing with "failed verification" -+(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" dir/file localhost:) \ -+ || test_fail "test 1: update through directory symlink failed" -+ -+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ -+ || test_fail "test 1: update transfer content mismatch" -+ -+###################################################################### -+# Test 2: Compression (-z) as in the original reproducer -+###################################################################### -+ -+echo "another line" >> "$srcbase/dir/file" -+sleep 1 -+touch "$srcbase/dir/file" -+ -+(cd "$srcbase" && $RSYNC -KRlptzv --rsync-path="$RSYNC" dir/file localhost:) \ -+ || test_fail "test 2: compressed update through directory symlink failed" -+ -+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ -+ || test_fail "test 2: compressed update content mismatch" -+ -+###################################################################### -+# Test 3: Nested directory symlinks (nested/sub/data.txt where -+# "nested" is a symlink to "nested_real") -+###################################################################### -+ -+mkdir -p "$HOME/nested_real/sub" -+ln -s nested_real "$HOME/nested" -+ -+mkdir -p "$srcbase/nested/sub" -+make_testfile "$srcbase/nested/sub/data.txt" -+ -+(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" nested/sub/data.txt localhost:) \ -+ || test_fail "test 3: initial nested transfer failed" -+ -+echo "appended nested" >> "$srcbase/nested/sub/data.txt" -+sleep 1 -+touch "$srcbase/nested/sub/data.txt" -+ -+(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" nested/sub/data.txt localhost:) \ -+ || test_fail "test 3: update through nested directory symlink failed" -+ -+diff "$srcbase/nested/sub/data.txt" "$HOME/nested_real/sub/data.txt" >/dev/null \ -+ || test_fail "test 3: nested update content mismatch" -+ -+###################################################################### -+# Test 4: --backup with directory symlinks -+# -+# Exercises the finish_transfer() "fnamecmp == fname" pointer -+# comparison that determines whether to update fnamecmp to the -+# backup name. If broken, --backup would reference a renamed file -+# for xattr handling. -+###################################################################### -+ -+# Reset destination -+rm -f "$HOME/real-dir/file" "$HOME/real-dir/file~" -+ -+make_testfile "$srcbase/dir/file" -+ -+(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" dir/file localhost:) \ -+ || test_fail "test 4: initial transfer for backup test failed" -+ -+echo "backup update" >> "$srcbase/dir/file" -+sleep 1 -+touch "$srcbase/dir/file" -+ -+(cd "$srcbase" && $RSYNC -KRlptv --backup --rsync-path="$RSYNC" dir/file localhost:) \ -+ || test_fail "test 4: update with --backup through directory symlink failed" -+ -+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ -+ || test_fail "test 4: backup update content mismatch" -+ -+if [ ! -f "$HOME/real-dir/file~" ]; then -+ test_fail "test 4: backup file was not created" -+fi -+ -+###################################################################### -+# Test 5: --inplace with directory symlinks -+# -+# Exercises the updating_basis_or_equiv check which uses -+# "fnamecmp == fname". With --inplace, rsync writes directly to -+# the destination file instead of a temp file. -+###################################################################### -+ -+rm -f "$HOME/real-dir/file" "$HOME/real-dir/file~" -+ -+make_testfile "$srcbase/dir/file" -+ -+(cd "$srcbase" && $RSYNC -KRlptv --inplace --rsync-path="$RSYNC" dir/file localhost:) \ -+ || test_fail "test 5: initial inplace transfer failed" -+ -+echo "inplace update" >> "$srcbase/dir/file" -+sleep 1 -+touch "$srcbase/dir/file" -+ -+(cd "$srcbase" && $RSYNC -KRlptv --inplace --rsync-path="$RSYNC" dir/file localhost:) \ -+ || test_fail "test 5: inplace update through directory symlink failed" -+ -+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ -+ || test_fail "test 5: inplace update content mismatch" -+ -+###################################################################### -+# Test 6: Top-level file (no dirname, no split needed) -+# -+# Ensures the dirname/basename split is not attempted for files -+# at the top level (file->dirname is NULL). -+###################################################################### -+ -+make_testfile "$srcbase/topfile" -+mkdir -p "$HOME" -+ -+(cd "$srcbase" && $RSYNC -Rlptv --rsync-path="$RSYNC" topfile localhost:) \ -+ || test_fail "test 6: initial top-level transfer failed" -+ -+echo "toplevel update" >> "$srcbase/topfile" -+sleep 1 -+touch "$srcbase/topfile" -+ -+(cd "$srcbase" && $RSYNC -Rlptv --rsync-path="$RSYNC" topfile localhost:) \ -+ || test_fail "test 6: top-level update failed" -+ -+diff "$srcbase/topfile" "$HOME/topfile" >/dev/null \ -+ || test_fail "test 6: top-level update content mismatch" -+ -+###################################################################### -+# Test 7: --partial-dir with protocol < 29 -+# -+# For protocol < 29, fnamecmp_type stays FNAMECMP_FNAME even when -+# fnamecmp is set to partialptr. The dirname/basename split must -+# NOT trigger in this case (guarded by "fnamecmp == fname"). -+###################################################################### -+ -+rm -f "$HOME/real-dir/file" -+make_testfile "$srcbase/dir/file" -+ -+(cd "$srcbase" && $RSYNC -KRlptv --protocol=28 --partial-dir=.rsync-partial \ -+ --rsync-path="$RSYNC" dir/file localhost:) \ -+ || test_fail "test 7: initial proto28 partial-dir transfer failed" -+ -+echo "partial-dir update" >> "$srcbase/dir/file" -+sleep 1 -+touch "$srcbase/dir/file" -+ -+(cd "$srcbase" && $RSYNC -KRlptv --protocol=28 --partial-dir=.rsync-partial \ -+ --rsync-path="$RSYNC" dir/file localhost:) \ -+ || test_fail "test 7: proto28 partial-dir update through dirlink failed" -+ -+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ -+ || test_fail "test 7: proto28 partial-dir update content mismatch" -+ -+###################################################################### -+# Test 8: Protocol < 29 basic directory symlink update -+# -+# Exercises the protocol < 29 code path and its fallback logic -+# (clearing basedir on retry). -+###################################################################### -+ -+rm -f "$HOME/real-dir/file" -+make_testfile "$srcbase/dir/file" -+ -+(cd "$srcbase" && $RSYNC -KRlptv --protocol=28 \ -+ --rsync-path="$RSYNC" dir/file localhost:) \ -+ || test_fail "test 8: initial proto28 transfer failed" -+ -+echo "proto28 update" >> "$srcbase/dir/file" -+sleep 1 -+touch "$srcbase/dir/file" -+ -+(cd "$srcbase" && $RSYNC -KRlptv --protocol=28 \ -+ --rsync-path="$RSYNC" dir/file localhost:) \ -+ || test_fail "test 8: proto28 update through directory symlink failed" -+ -+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ -+ || test_fail "test 8: proto28 update content mismatch" -+ -+# The script would have aborted on error, so getting here means we've won. -+exit 0 --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0021-syscall-also-use-O_RESOLVE_BENEATH-on-FreeBSD-and-Ma.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0021-syscall-also-use-O_RESOLVE_BENEATH-on-FreeBSD-and-Ma.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0021-syscall-also-use-O_RESOLVE_BENEATH-on-FreeBSD-and-Ma.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0021-syscall-also-use-O_RESOLVE_BENEATH-on-FreeBSD-and-Ma.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,96 +0,0 @@ -From b32ba3ddb3736114887a452f8a89e33fe8e5dda8 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Thu, 30 Apr 2026 08:44:11 +1000 -Subject: [PATCH 21/38] syscall: also use O_RESOLVE_BENEATH on FreeBSD and - MacOS - -FreeBSD and MacOS have O_RESOLVE_BENEATH as an openat() flag with the same -"must not escape dirfd" semantics as Linux's RESOLVE_BENEATH. The -kernel rejects ".." escapes, absolute symlinks, and symlinks whose -target lies outside dirfd, while still following symlinks that -resolve within it -- the same trade-off that fixes issue #715 on -Linux. - -Add a parallel BSD path in secure_relative_open(), gated on -declared. Unlike Linux, BSD doesn't have the header/runtime split -where the symbol can exist without kernel support, so no runtime -fallback is needed: if the flag compiles in, the kernel honours it. - -OpenBSD and NetBSD have no equivalent kernel primitive and continue -to use the existing per-component O_NOFOLLOW walk; issue #715 -remains visible on those platforms (a userland resolver or -unveil(2)-based fence would be follow-up work). - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - syscall.c | 40 +++++++++++++++++++++++++++++++++++++--- - 1 file changed, 37 insertions(+), 3 deletions(-) - -diff --git a/syscall.c b/syscall.c -index 0881c7ab..045c6ce3 100644 ---- a/syscall.c -+++ b/syscall.c -@@ -729,9 +729,13 @@ int do_open_nofollow(const char *pathname, int flags) - 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). The escape -- prevention is handled by the kernel via openat2(RESOLVE_BENEATH) -- on Linux 5.6+; older systems fall back to the per-component -- O_NOFOLLOW walk below. -+ prevention is handled by: -+ Linux 5.6+: openat2(RESOLVE_BENEATH) -+ FreeBSD 13+: openat() with O_RESOLVE_BENEATH -+ macOS 15+ / iOS 18+: openat() with O_RESOLVE_BENEATH (same -+ flag name, picked up by the same #ifdef; -+ flag value differs from FreeBSD) -+ Other systems fall back to the per-component O_NOFOLLOW walk below. - - The relpath must also not contain any ../ elements in the path. - */ -@@ -763,6 +767,32 @@ static int secure_relative_open_linux(const char *basedir, const char *relpath, - } - #endif - -+#ifdef O_RESOLVE_BENEATH -+/* FreeBSD 13+ and macOS 15+ (Sequoia) / iOS 18+: O_RESOLVE_BENEATH is -+ * an openat() flag with the same "must not escape dirfd" semantics as -+ * Linux's RESOLVE_BENEATH. The kernel rejects ".." escapes, absolute -+ * symlinks, and symlinks whose target lies outside dirfd. (FreeBSD and -+ * Apple use different flag bit values, but the same symbolic name.) */ -+static int secure_relative_open_resolve_beneath(const char *basedir, const char *relpath, int flags, mode_t mode) -+{ -+ int dirfd, retfd; -+ -+ if (basedir == NULL) { -+ dirfd = AT_FDCWD; -+ } else { -+ dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); -+ if (dirfd == -1) -+ return -1; -+ } -+ -+ retfd = openat(dirfd, relpath, flags | O_RESOLVE_BENEATH, mode); -+ -+ if (dirfd != AT_FDCWD) -+ close(dirfd); -+ return retfd; -+} -+#endif -+ - int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode) - { - if (!relpath || relpath[0] == '/') { -@@ -786,6 +816,10 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo - } - #endif - -+#ifdef O_RESOLVE_BENEATH -+ return secure_relative_open_resolve_beneath(basedir, relpath, flags, mode); -+#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) { --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0022-testsuite-skip-symlink-dirlink-basis-on-platforms-wi.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0022-testsuite-skip-symlink-dirlink-basis-on-platforms-wi.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0022-testsuite-skip-symlink-dirlink-basis-on-platforms-wi.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0022-testsuite-skip-symlink-dirlink-basis-on-platforms-wi.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,49 +0,0 @@ -From 6226386332e126d22231c7b9a0d40abdc8b0837b Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Thu, 30 Apr 2026 09:00:09 +1000 -Subject: [PATCH 22/38] testsuite: skip symlink-dirlink-basis on platforms - without RESOLVE_BENEATH - -secure_relative_open() has a kernel-enforced "stay below dirfd" path -on Linux 5.6+ (openat2 RESOLVE_BENEATH) and FreeBSD 13+ (openat -O_RESOLVE_BENEATH). On Solaris, OpenBSD, NetBSD, and Cygwin the code -falls back to the per-component O_NOFOLLOW walk, which by design -rejects every directory symlink in the path -- the very case this -test exercises. Mark the test skipped there rather than have it -fail with a known regression that's tracked separately. - -macOS is intentionally not in the skip list: although it does not -have O_RESOLVE_BENEATH either, the test passes there in practice; -investigation of the underlying reason is left as follow-up. - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - testsuite/symlink-dirlink-basis.test | 12 ++++++++++++ - 1 file changed, 12 insertions(+) - -diff --git a/testsuite/symlink-dirlink-basis.test b/testsuite/symlink-dirlink-basis.test -index 9065dd81..a14eb5cf 100755 ---- a/testsuite/symlink-dirlink-basis.test -+++ b/testsuite/symlink-dirlink-basis.test -@@ -26,6 +26,18 @@ - - . "$suitedir/rsync.fns" - -+# secure_relative_open() uses kernel-enforced "stay below dirfd" via -+# openat2(RESOLVE_BENEATH) on Linux 5.6+ and openat(O_RESOLVE_BENEATH) -+# on FreeBSD 13+. Other platforms fall back to a per-component -+# O_NOFOLLOW walk that rejects every symlink including legitimate -+# directory symlinks -- the very case this test exercises. Skip on -+# those rather than report a known failure. -+case "$(uname -s)" in -+ SunOS|OpenBSD|NetBSD|CYGWIN*) -+ test_skipped "secure_relative_open lacks RESOLVE_BENEATH equivalent on $(uname -s); issue #715 still affects this platform" -+ ;; -+esac -+ - RSYNC_RSH="$scratchdir/src/support/lsh.sh" - export RSYNC_RSH - --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0023-syscall-clientserver-am_chrooted-and-use_secure_syml.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0023-syscall-clientserver-am_chrooted-and-use_secure_syml.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0023-syscall-clientserver-am_chrooted-and-use_secure_syml.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0023-syscall-clientserver-am_chrooted-and-use_secure_syml.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,325 +0,0 @@ -From f629772cc686903773fe7b78b57f4011c427f6ef Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Wed, 31 Dec 2025 10:01:23 +1100 -Subject: [PATCH 23/38] syscall+clientserver: am_chrooted and - use_secure_symlinks for daemon-no-chroot (CVE-2026-29518) - -CVE-2026-29518: an rsync daemon configured with "use chroot = no" -is exposed to a TOCTOU race on parent path components. A local -attacker with write access to a module can replace a parent -directory component with a symlink between the receiver's check -and its open(), redirecting reads (basis-file disclosure) and -writes (file overwrite) outside the module. Under elevated daemon -privilege this allows privilege escalation. Default -"use chroot = yes" is not exposed. - -Add secure_relative_open() in syscall.c. It walks the parent -components under RESOLVE_BENEATH (Linux 5.6+) / -O_RESOLVE_BENEATH (FreeBSD 13+, macOS 15+) / per-component -O_NOFOLLOW elsewhere, anchored at a trusted dirfd, so a parent- -symlink swap is rejected by the kernel. Route the receiver's -basis-file open in receiver.c through it when use_secure_symlinks -is set in clientserver.c rsync_module(). - -Reporters: Nullx3D (Batuhan SANCAK); Damien Neil; Michael Stapelberg. - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - clientserver.c | 25 +++++++++ - options.c | 9 ++++ - receiver.c | 22 ++++++-- - syscall.c | 139 +++++++++++++++++++++++++++++++++++++++++++++++++ - 4 files changed, 192 insertions(+), 3 deletions(-) - -diff --git a/clientserver.c b/clientserver.c -index 3800f0d6..e8dfddb1 100644 ---- a/clientserver.c -+++ b/clientserver.c -@@ -30,6 +30,7 @@ extern int list_only; - extern int am_sender; - extern int am_server; - extern int am_daemon; -+extern int am_chrooted; - extern int am_root; - extern int msgs2stderr; - extern int rsync_port; -@@ -38,6 +39,7 @@ extern int ignore_errors; - extern int preserve_xattrs; - extern int kluge_around_eof; - extern int munge_symlinks; -+extern int use_secure_symlinks; - extern int open_noatime; - extern int sanitize_paths; - extern int numeric_ids; -@@ -983,6 +985,7 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char - io_printf(f_out, "@ERROR: chroot failed\n"); - return -1; - } -+ am_chrooted = 1; - module_chdir = module_dir; - } - -@@ -1005,6 +1008,15 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char - } - } - -+ /* Enable secure symlink handling for any non-chrooted daemon module. -+ * This prevents TOCTOU race attacks where an attacker could switch a -+ * directory to a symlink between path validation and file open. -+ * Match the gate used by the do_*_at() wrappers in syscall.c -+ * (am_daemon && !am_chrooted) -- 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; -+ - if (gid_list.count) { - gid_t *gid_array = gid_list.items; - if (setgid(gid_array[0])) { -@@ -1308,6 +1320,19 @@ int start_daemon(int f_in, int f_out) - rsyserr(FLOG, errno, "daemon chroot(\"%s\") failed", p); - return -1; - } -+ /* 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 -+ * 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, -+ * not to any individual module path -- modules sharing the -+ * daemon chroot are still distinguishable filesystem -+ * 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() -+ * still fires for "use chroot = no" modules. */ - if (chdir("/") < 0) { - rsyserr(FLOG, errno, "daemon chdir(\"/\") failed"); - return -1; -diff --git a/options.c b/options.c -index 4ae1c58c..bebb5018 100644 ---- a/options.c -+++ b/options.c -@@ -113,11 +113,20 @@ int mkpath_dest_arg = 0; - int allow_inc_recurse = 1; - int xfer_dirs = -1; - int am_daemon = 0; -+/* Set after a successful per-module chroot ("use chroot = yes") in -+ * 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 -+ * syscall.c, gated by `am_daemon && !am_chrooted`) must still fire -+ * even when the daemon is inside a daemon chroot. */ -+int am_chrooted = 0; - int connect_timeout = 0; - int keep_partial = 0; - int safe_symlinks = 0; - int copy_unsafe_links = 0; - int munge_symlinks = 0; -+int use_secure_symlinks = 0; - int size_only = 0; - int daemon_bwlimit = 0; - int bwlimit = 0; -diff --git a/receiver.c b/receiver.c -index edfbb210..5a2c8c5a 100644 ---- a/receiver.c -+++ b/receiver.c -@@ -70,6 +70,7 @@ extern int fuzzy_basis; - - extern struct name_num_item *xfer_sum_nni; - extern int xfer_sum_len; -+extern int use_secure_symlinks; - - static struct bitbag *delayed_bits = NULL; - static int phase = 0, redoing = 0; -@@ -214,7 +215,12 @@ 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.) */ -- fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS); -+ /* When use_secure_symlinks is on (non-chroot daemon with munge_symlinks), -+ * use secure_mkstemp to prevent symlink race attacks on parent directories. */ -+ if (use_secure_symlinks) -+ fd = secure_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS); -+ else -+ fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS); - - #if 0 - /* In most cases parent directories will already exist because their -@@ -854,11 +860,21 @@ 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; -- fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600); -+ /* When use_secure_symlinks is on (non-chroot daemon), -+ * use secure open to prevent symlink race attacks where an -+ * attacker could switch a directory to a symlink between -+ * path validation and file open. */ -+ if (use_secure_symlinks) -+ fd2 = secure_relative_open(NULL, fnametmp, O_WRONLY|O_CREAT, 0600); -+ else -+ fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600); - #ifdef linux - if (fd2 == -1 && errno == EACCES) { - /* Maybe the error was due to protected_regular setting? */ -- fd2 = do_open(fname, O_WRONLY, 0600); -+ if (use_secure_symlinks) -+ fd2 = secure_relative_open(NULL, fname, O_WRONLY, 0600); -+ else -+ fd2 = do_open(fname, O_WRONLY, 0600); - } - #endif - if (fd2 == -1) { -diff --git a/syscall.c b/syscall.c -index 045c6ce3..bfaaaa63 100644 ---- a/syscall.c -+++ b/syscall.c -@@ -877,6 +877,145 @@ cleanup: - #endif // O_NOFOLLOW, O_DIRECTORY - } - -+/* 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(); -+ } -+} -+ -+/* -+ 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) -+{ -+#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; -+ } -+ -+ /* For absolute paths, start the secure walk from "/" rather than CWD. */ -+ if (template[0] == '/') { -+ dirfd = open("/", O_RDONLY | O_DIRECTORY | O_NOFOLLOW); -+ if (dirfd < 0) -+ return -1; -+ } -+ -+ /* Find the last slash to separate directory from filename */ -+ lastslash = strrchr(template, '/'); -+ if (lastslash) { -+ char *path_copy = my_strdup(template, __FILE__, __LINE__); -+ if (!path_copy) -+ return -1; -+ -+ /* Null-terminate at the last slash to get directory part */ -+ path_copy[lastslash - template] = '\0'; -+ -+ /* Walk the directory path securely */ -+ for (const char *part = strtok(path_copy, "/"); -+ part != NULL; -+ part = strtok(NULL, "/")) -+ { -+ int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); -+ if (next_fd == -1) { -+ int save_errno = errno; -+ free(path_copy); -+ if (dirfd != AT_FDCWD) close(dirfd); -+ errno = (save_errno == ELOOP) ? ELOOP : save_errno; -+ return -1; -+ } -+ if (dirfd != AT_FDCWD) close(dirfd); -+ dirfd = next_fd; -+ } -+ free(path_copy); -+ } -+ -+ /* Now create the temp file in the securely-opened directory */ -+ perms |= S_IWUSR; -+ -+ /* Generate unique filename - we need to modify the template in place */ -+ char *filename = lastslash ? lastslash + 1 : template; -+ size_t filename_len = strlen(filename); -+ -+ if (filename_len < 6) { -+ if (dirfd != AT_FDCWD) close(dirfd); -+ errno = EINVAL; -+ return -1; -+ } -+ char *suffix = filename + filename_len - 6; /* Points to XXXXXX */ -+ if (strcmp(suffix, "XXXXXX") != 0) { -+ if (dirfd != AT_FDCWD) close(dirfd); -+ errno = EINVAL; -+ return -1; -+ } -+ -+ /* Try random suffixes until we find one that works */ -+ static const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; -+ 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(dirfd, filename, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, perms); -+ if (fd >= 0) -+ break; -+ if (errno != EEXIST) { -+ if (dirfd != AT_FDCWD) close(dirfd); -+ return -1; -+ } -+ } -+ -+ if (fd >= 0) { -+ if (fchmod(fd, perms) != 0 && preserve_perms) { -+ int errno_save = errno; -+ close(fd); -+ unlinkat(dirfd, filename, 0); -+ if (dirfd != AT_FDCWD) close(dirfd); -+ errno = errno_save; -+ return -1; -+ } -+#if defined HAVE_SETMODE && O_BINARY -+ setmode(fd, O_BINARY); -+#endif -+ } -+ -+ if (dirfd != AT_FDCWD) close(dirfd); -+ 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 --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0024-sender-fix-read-path-TOCTOU-by-opening-from-module-r.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0024-sender-fix-read-path-TOCTOU-by-opening-from-module-r.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0024-sender-fix-read-path-TOCTOU-by-opening-from-module-r.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0024-sender-fix-read-path-TOCTOU-by-opening-from-module-r.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,68 +0,0 @@ -From e57fd03284bbb2c1feaaf7fa1e71ba7a6e1c16ce Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Sun, 1 Mar 2026 09:28:40 +1100 -Subject: [PATCH 24/38] sender: fix read-path TOCTOU by opening from module - root (CVE-2026-29518) - -The sender's file open was vulnerable to the same TOCTOU symlink -race as the receiver-side basis-file open. change_pathname() calls -chdir() into subdirectories, which follows symlinks; an attacker -could race to swap a directory for a symlink between the chdir and -the file open, allowing reads of privileged files through the -daemon. - -Reconstruct the full relative path (F_PATHNAME + fname) and open -via secure_relative_open() from the trusted module_dir, which -walks each path component without following symlinks. This is -independent of CWD, so the chdir race is neutralised. - -CVE-2026-29518. - -Co-Authored-By: Claude Opus 4.6 ---- - sender.c | 22 +++++++++++++++++++++- - 1 file changed, 21 insertions(+), 1 deletion(-) - -diff --git a/sender.c b/sender.c -index b1588b70..99f431fe 100644 ---- a/sender.c -+++ b/sender.c -@@ -48,6 +48,8 @@ extern int make_backups; - extern int inplace; - extern int inplace_partial; - extern int batch_fd; -+extern int use_secure_symlinks; -+extern char *module_dir; - extern int write_batch; - extern int file_old_total; - extern BOOL want_progress_now; -@@ -352,7 +354,25 @@ void send_files(int f_in, int f_out) - exit_cleanup(RERR_PROTOCOL); - } - -- fd = do_open_checklinks(fname); -+ if (use_secure_symlinks) { -+ /* 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 -+ * from F_PATHNAME (path) and f_name (fname). */ -+ char secure_path[MAXPATHLEN]; -+ int slen = snprintf(secure_path, sizeof secure_path, "%s%s%s", path, slash, fname); -+ if (slen >= (int)sizeof secure_path) { -+ io_error |= IOERR_GENERAL; -+ rprintf(FERROR_XFER, "path too long: %s%s%s\n", path, slash, fname); -+ free_sums(s); -+ if (protocol_version >= 30) -+ send_msg_int(MSG_NO_SEND, ndx); -+ continue; -+ } -+ fd = secure_relative_open(module_dir, secure_path, O_RDONLY, 0); -+ } else { -+ fd = do_open_checklinks(fname); -+ } - if (fd == -1) { - if (errno == ENOENT) { - enum logcode c = am_daemon && protocol_version < 28 ? FERROR : FWARNING; --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0025-syscall-receiver-secure-receiver-side-do_chmod-again.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0025-syscall-receiver-secure-receiver-side-do_chmod-again.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0025-syscall-receiver-secure-receiver-side-do_chmod-again.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0025-syscall-receiver-secure-receiver-side-do_chmod-again.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,463 +0,0 @@ -From d870b43a32289c760c1c34d933bfb7d81922fdc2 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Mon, 4 May 2026 21:53:14 +1000 -Subject: [PATCH 25/38] syscall+receiver: secure receiver-side do_chmod against - symlink-race TOCTOU - -CVE-2026-29518's fix routed the receiver's open() through -secure_relative_open(), but every other path-based syscall the -receiver runs on sender-controllable paths is vulnerable to the -same TOCTOU primitive. This commit closes the chmod variant. - -Add do_chmod_at() that opens the parent of fname under -secure_relative_open() and uses fchmodat() against the resulting -dirfd. Gate the secure path on am_daemon && !am_chrooted (the same -gate use_secure_symlinks already uses for the receiver basis-file -open), so non-daemon callers and chrooted daemons keep the original -do_chmod() fast path. - -Migrate the receiver-side do_chmod() call sites in delete.c, -generator.c, rsync.c, and xattrs.c. - -Adds testsuite/chmod-symlink-race.test (with t_chmod_secure helper) -as regression coverage. - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - Makefile.in | 10 ++- - delete.c | 4 +- - generator.c | 4 +- - rsync.c | 2 +- - syscall.c | 80 ++++++++++++++++++++ - t_chmod_secure.c | 117 ++++++++++++++++++++++++++++++ - t_stub.c | 2 + - testsuite/chmod-symlink-race.test | 68 +++++++++++++++++ - xattrs.c | 6 +- - 9 files changed, 282 insertions(+), 11 deletions(-) - create mode 100644 t_chmod_secure.c - create mode 100755 testsuite/chmod-symlink-race.test - -diff --git a/Makefile.in b/Makefile.in -index 75fd03af..fe0a5494 100644 ---- a/Makefile.in -+++ b/Makefile.in -@@ -57,13 +57,13 @@ TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/perms - - # Programs we must have to run the test cases - CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \ -- testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) wildtest$(EXEEXT) \ -- simdtest$(EXEEXT) -+ testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) t_chmod_secure$(EXEEXT) \ -+ wildtest$(EXEEXT) simdtest$(EXEEXT) - - CHECK_SYMLINKS = testsuite/chown-fake.test testsuite/devices-fake.test testsuite/xattrs-hlink.test - - # Objects for CHECK_PROGS to clean --CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o trimslash.o wildtest.o -+CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o t_chmod_secure.o trimslash.o wildtest.o - - # note that the -I. is needed to handle config.h when using VPATH - .c.o: -@@ -179,6 +179,10 @@ T_UNSAFE_OBJ = t_unsafe.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/sn - t_unsafe$(EXEEXT): $(T_UNSAFE_OBJ) - $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_UNSAFE_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$(EXEEXT): $(T_CHMOD_SECURE_OBJ) -+ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CHMOD_SECURE_OBJ) $(LIBS) -+ - .PHONY: conf - conf: configure.sh config.h.in - -diff --git a/delete.c b/delete.c -index 89c1f8d6..ded0ab2a 100644 ---- a/delete.c -+++ b/delete.c -@@ -98,7 +98,7 @@ static enum delret delete_dir_contents(char *fname, uint16 flags) - - strlcpy(p, fp->basename, remainder); - if (!(fp->mode & S_IWUSR) && !am_root && fp->flags & FLAG_OWNED_BY_US) -- do_chmod(fname, fp->mode | S_IWUSR); -+ do_chmod_at(fname, fp->mode | S_IWUSR); - /* Save stack by recursing to ourself directly. */ - if (S_ISDIR(fp->mode)) { - if (delete_dir_contents(fname, flags | DEL_RECURSE) != DR_SUCCESS) -@@ -139,7 +139,7 @@ enum delret delete_item(char *fbuf, uint16 mode, uint16 flags) - } - - if (flags & DEL_NO_UID_WRITE) -- do_chmod(fbuf, mode | S_IWUSR); -+ do_chmod_at(fbuf, mode | S_IWUSR); - - if (S_ISDIR(mode) && !(flags & DEL_DIR_IS_EMPTY)) { - /* This only happens on the first call to delete_item() since -diff --git a/generator.c b/generator.c -index b56fa569..e5aff654 100644 ---- a/generator.c -+++ b/generator.c -@@ -1499,7 +1499,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, - #ifdef HAVE_CHMOD - if (!am_root && (file->mode & S_IRWXU) != S_IRWXU && dir_tweaking) { - mode_t mode = file->mode | S_IRWXU; -- if (do_chmod(fname, mode) < 0) { -+ if (do_chmod_at(fname, mode) < 0) { - rsyserr(FERROR_XFER, errno, - "failed to modify permissions on %s", - full_fname(fname)); -@@ -2111,7 +2111,7 @@ static void touch_up_dirs(struct file_list *flist, int ndx) - continue; - fname = f_name(file, NULL); - if (fix_dir_perms) -- do_chmod(fname, file->mode); -+ do_chmod_at(fname, file->mode); - if (need_retouch_dir_times) { - STRUCT_STAT st; - if (link_stat(fname, &st, 0) == 0 && mtime_differs(&st, file)) { -diff --git a/rsync.c b/rsync.c -index b130aba5..cc46a2f9 100644 ---- a/rsync.c -+++ b/rsync.c -@@ -657,7 +657,7 @@ 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 : do_chmod(fname, new_mode); -+ int ret = am_root < 0 ? 0 : do_chmod_at(fname, new_mode); - if (ret < 0) { - rsyserr(FERROR_XFER, errno, - "failed to set permissions on %s", -diff --git a/syscall.c b/syscall.c -index bfaaaa63..167aae0e 100644 ---- a/syscall.c -+++ b/syscall.c -@@ -281,6 +281,86 @@ int do_chmod(const char *path, mode_t mode) - return code; - return 0; - } -+ -+/* -+ 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() -+ (which uses openat2(RESOLVE_BENEATH) on Linux 5.6+, openat() with -+ O_RESOLVE_BENEATH on FreeBSD 13+ and macOS 15+ (Sequoia), or a per-component -+ O_NOFOLLOW walk elsewhere) 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 by the -+ kernel (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; -+ -+ /* 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 (!am_daemon || am_chrooted) -+ 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 = fchmodat(dfd, bname, mode, 0); -+ 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) -diff --git a/t_chmod_secure.c b/t_chmod_secure.c -new file mode 100644 -index 00000000..114dfb2d ---- /dev/null -+++ b/t_chmod_secure.c -@@ -0,0 +1,117 @@ -+/* -+ * Test harness for do_chmod_at(). 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 -+ * 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). -+ * -+ * Not linked into rsync itself. -+ * -+ * This program is free software; you can redistribute it and/or modify -+ * it under the terms of the GNU General Public License version 2 as -+ * published by the Free Software Foundation. -+ */ -+ -+#include "rsync.h" -+ -+#include -+ -+int dry_run = 0; -+int am_root = 0; -+int am_sender = 0; -+int read_only = 0; -+int list_only = 0; -+int copy_links = 0; -+int copy_unsafe_links = 0; -+extern int am_daemon, am_chrooted; -+ -+short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG]; -+ -+static int errs = 0; -+ -+static void check(const char *label, int actual_rc, int expect_ok, -+ const char *path, mode_t expected_mode) -+{ -+ struct stat st; -+ int got_ok = (actual_rc == 0); -+ if (got_ok != expect_ok) { -+ fprintf(stderr, "FAIL [%s]: rc=%d errno=%d (%s), expected %s\n", -+ label, actual_rc, errno, strerror(errno), -+ expect_ok ? "success" : "rejection"); -+ errs++; -+ return; -+ } -+ if (path && stat(path, &st) < 0) { -+ fprintf(stderr, "FAIL [%s]: stat(%s) failed: %s\n", -+ label, path, strerror(errno)); -+ errs++; -+ return; -+ } -+ if (path && (st.st_mode & 07777) != expected_mode) { -+ fprintf(stderr, -+ "FAIL [%s]: %s mode is 0%o, expected 0%o\n", -+ label, path, st.st_mode & 07777, expected_mode); -+ errs++; -+ return; -+ } -+ fprintf(stderr, "OK [%s]\n", label); -+} -+ -+int main(int argc, char **argv) -+{ -+ if (argc != 2) { -+ fprintf(stderr, "usage: %s \n", argv[0]); -+ return 2; -+ } -+ if (chdir(argv[1]) < 0) { -+ perror("chdir"); -+ return 2; -+ } -+ -+ /* Simulate the daemon-without-chroot deployment that do_chmod_at() -+ * defends. With am_daemon=0 or am_chrooted=1 the wrapper falls -+ * through to plain do_chmod() and the symlink-race test would be -+ * meaningless. */ -+ am_daemon = 1; -+ am_chrooted = 0; -+ -+ /* Test layout (all inside the directory we just chdir'd to): -+ * -+ * ./realdir/sentinel -- regular target file -+ * ./inside_link -> realdir -- legitimate dir-symlink within the tree -+ * ./escape_link -> ../trap -- attacker swap, target outside tree -+ * ../trap/sentinel -- the file the attacker wants to alter -+ * -+ * The shell wrapper that calls this helper has set both sentinel -+ * files to mode 0600 so we have a clean baseline to compare. -+ */ -+ -+ /* Scenario A: legitimate parent dir-symlink, chmod must succeed. */ -+ int rc = do_chmod_at("inside_link/sentinel", 0640); -+ check("A: legit dir-symlink within tree", -+ 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); -+ 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); -+ 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); -+ check("D: top-level file, no parent component", -+ rc, 1, "topfile", 0640); -+ -+ if (errs) -+ fprintf(stderr, "%d failure(s)\n", errs); -+ return errs ? 1 : 0; -+} -diff --git a/t_stub.c b/t_stub.c -index eee92729..63bc144c 100644 ---- a/t_stub.c -+++ b/t_stub.c -@@ -23,6 +23,8 @@ - - int do_fsync = 0; - int inplace = 0; -+int am_daemon = 0; -+int am_chrooted = 0; - int modify_window = 0; - int preallocate_files = 0; - int protect_args = 0; -diff --git a/testsuite/chmod-symlink-race.test b/testsuite/chmod-symlink-race.test -new file mode 100755 -index 00000000..48bbfbb4 ---- /dev/null -+++ b/testsuite/chmod-symlink-race.test -@@ -0,0 +1,68 @@ -+#!/bin/sh -+ -+# Copyright (C) 2026 by Andrew Tridgell -+ -+# This program is distributable under the terms of the GNU GPL (see -+# COPYING). -+ -+# Regression test for the symlink-TOCTOU class of bug applied to -+# chmod() on the receiver side. The CVE-2026-29518 fix used -+# secure_relative_open() for the basis-file open, but every other -+# path-based syscall the receiver runs on sender-controllable paths -+# is vulnerable to the same primitive: a local attacker swaps a -+# symlink into one of the parent directory components between the -+# receiver's check and its act, and the syscall escapes the module. -+# -+# This test exercises the new do_chmod_at() wrapper via the -+# t_chmod_secure helper. The helper sets up two scenarios: -+# - a parent dir-symlink that resolves WITHIN the module tree -+# (legitimate -K-style use, must continue to work) -+# - a parent dir-symlink that escapes the module tree (the -+# attack, must be rejected) -+# plus two regression scenarios (plain relative path, top-level -+# file) that just confirm the safe wrapper doesn't break the -+# normal case. -+# -+# The kernel-enforced "stay below dirfd" path resolution is -+# only available on Linux 5.6+, FreeBSD 13+, and macOS 15+. -+# Skip on platforms that fall back to per-component O_NOFOLLOW -+# (Solaris, OpenBSD, NetBSD, Cygwin); the per-component fallback -+# would also reject the attack but the legitimate dir-symlink -+# scenario would fail there. -+ -+. "$suitedir/rsync.fns" -+ -+case "$(uname -s)" in -+ SunOS|OpenBSD|NetBSD|CYGWIN*) -+ test_skipped "do_chmod_at relies on RESOLVE_BENEATH-equivalent kernel support not available on $(uname -s)" -+ ;; -+esac -+ -+mod="$scratchdir/module" -+trap_outside="$scratchdir/trap" -+rm -rf "$mod" "$trap_outside" -+mkdir -p "$mod/realdir" "$trap_outside" -+ -+# Set up the four file-system objects the helper expects: -+echo bystander > "$mod/realdir/sentinel" -+chmod 0600 "$mod/realdir/sentinel" -+echo target > "$trap_outside/sentinel" -+chmod 0600 "$trap_outside/sentinel" -+ln -s realdir "$mod/inside_link" -+ln -s ../trap "$mod/escape_link" -+echo top > "$mod/topfile" -+chmod 0600 "$mod/topfile" -+ -+"$TOOLDIR/t_chmod_secure" "$mod" || \ -+ test_fail "t_chmod_secure reported failures (see stderr above)" -+ -+# Sanity-check from the shell side too: the outside file's mode must -+# still be 0600 -- the helper checked this, but a second look from -+# the shell guards against a helper-internal stat() bug. -+mode=$(stat -c '%a' "$trap_outside/sentinel" 2>/dev/null \ -+ || stat -f '%Lp' "$trap_outside/sentinel" 2>/dev/null) -+if [ "$mode" != "600" ]; then -+ test_fail "outside sentinel mode changed from 600 to $mode -- chmod escaped the module" -+fi -+ -+exit 0 -diff --git a/xattrs.c b/xattrs.c -index 65166eed..e5d0dd43 100644 ---- a/xattrs.c -+++ b/xattrs.c -@@ -1086,7 +1086,7 @@ int set_xattr(const char *fname, const struct file_struct *file, const char *fna - && !S_ISLNK(sxp->st.st_mode) - #endif - && access(fname, W_OK) < 0 -- && do_chmod(fname, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR) == 0) -+ && do_chmod_at(fname, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR) == 0) - added_write_perm = 1; - - ndx = F_XATTR(file); -@@ -1094,7 +1094,7 @@ int set_xattr(const char *fname, const struct file_struct *file, const char *fna - lst = &glst->xa_items; - int return_value = rsync_xal_set(fname, lst, fnamecmp, sxp); - if (added_write_perm) /* remove the temporary write permission */ -- do_chmod(fname, sxp->st.st_mode); -+ do_chmod_at(fname, sxp->st.st_mode); - return return_value; - } - -@@ -1211,7 +1211,7 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode) - mode = (fst.st_mode & _S_IFMT) | (fmode & ACCESSPERMS) - | (S_ISDIR(fst.st_mode) ? 0700 : 0600); - if (fst.st_mode != mode) -- do_chmod(fname, mode); -+ do_chmod_at(fname, mode); - if (!IS_DEVICE(fst.st_mode)) - fst.st_rdev = 0; /* just in case */ - --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0026-util1-secure-change_dir-against-symlink-race-chdir-e.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0026-util1-secure-change_dir-against-symlink-race-chdir-e.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0026-util1-secure-change_dir-against-symlink-race-chdir-e.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0026-util1-secure-change_dir-against-symlink-race-chdir-e.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,203 +0,0 @@ -From a2c1b98c2a0886b989f768464c61c4c93009c1f4 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Tue, 5 May 2026 14:34:33 +1000 -Subject: [PATCH 26/38] util1: secure change_dir() against symlink-race - chdir-escape - -The receiver's chdir(2) into a destination subdirectory followed -attacker-planted symlinks at every path component. Once CWD -escaped the module, every subsequent path-relative syscall (open, -chmod, lchown, ...) inherited the escape -- defeating -secure_relative_open's RESOLVE_BENEATH anchor against AT_FDCWD, -since the anchor itself was now outside the module. - -Route change_dir's relative target through secure_relative_open() -and fchdir() to the resulting dirfd in am_daemon && !am_chrooted -mode, so the chdir step itself can no longer follow a parent- -symlink. Same treatment applied to the CD_SKIP_CHDIR / -set_path_only path so it also can't follow attacker symlinks -during path tracking. - -Adds testsuite/sender-flist-symlink-leak.test covering the -sender-side flist resolution variant of the same primitive. - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - testsuite/sender-flist-symlink-leak.test | 90 ++++++++++++++++++++++++ - util1.c | 56 +++++++++++++-- - 2 files changed, 142 insertions(+), 4 deletions(-) - create mode 100755 testsuite/sender-flist-symlink-leak.test - -diff --git a/testsuite/sender-flist-symlink-leak.test b/testsuite/sender-flist-symlink-leak.test -new file mode 100755 -index 00000000..011d93d0 ---- /dev/null -+++ b/testsuite/sender-flist-symlink-leak.test -@@ -0,0 +1,90 @@ -+#!/bin/sh -+ -+# Copyright (C) 2026 by Andrew Tridgell -+ -+# This program is distributable under the terms of the GNU GPL (see -+# COPYING). -+ -+# Regression test for codex re-check finding: the sender-side file- -+# list generator can still follow an attacker-planted symlink out of -+# the module via change_pathname() -> change_dir(...,CD_SKIP_CHDIR) -+# followed by change_dir(...,CD_NORMAL). The CD_SKIP_CHDIR sets -+# skipped_chdir=1, and the next CD_NORMAL call's secure-branch in -+# util1.c is gated on `!skipped_chdir`, so the secure path is -+# bypassed and a raw chdir(curr_dir) follows attacker-controlled -+# symlinks during flist generation. -+# -+# Reach: rsync daemon module with `use chroot = no`. A local -+# attacker plants module/cd -> /outside. A client (innocent or -+# malicious) pulls rsync:////cd/. The daemon, as -+# sender, enumerates files in /outside and ships their metadata -+# (names, sizes, modes, mtimes) to the client. The actual content -+# transfer fails later at the secure_relative_open step with EXDEV, -+# but by then the metadata has already leaked. -+# -+# We detect by running a dry-run pull of the symlinked subdir and -+# checking whether the client's --list-only output mentions any -+# file from /outside. With the bug, /outside/secret.txt appears in -+# the list with its size; with the fix, the daemon's chdir into -+# the symlinked subdir is rejected and no /outside file is listed. -+ -+. "$suitedir/rsync.fns" -+ -+case "$(uname -s)" in -+ SunOS|OpenBSD|NetBSD|CYGWIN*) -+ test_skipped "secure change_dir relies on RESOLVE_BENEATH-equivalent kernel support not available on $(uname -s)" -+ ;; -+esac -+ -+mod="$scratchdir/module" -+outside="$scratchdir/outside" -+listfile="$scratchdir/listed.txt" -+conf="$scratchdir/test-rsyncd.conf" -+ -+rm -rf "$mod" "$outside" -+mkdir -p "$mod" "$outside" -+ -+# Outside-the-module file the daemon should NOT enumerate to clients. -+# A distinctive name + non-trivial size makes the leak easy to spot. -+echo "OUTSIDE_PROTECTED_FILE_USED_AS_LEAK_DETECTOR" > "$outside/leak_marker.txt" -+chmod 0644 "$outside/leak_marker.txt" -+ -+# The symlink trap planted by the local attacker. -+ln -s "$outside" "$mod/cd" -+ -+my_uid=`get_testuid` -+root_uid=`get_rootuid` -+root_gid=`get_rootgid` -+uid_setting="uid = $root_uid" -+gid_setting="gid = $root_gid" -+if test x"$my_uid" != x"$root_uid"; then -+ uid_setting="#$uid_setting" -+ gid_setting="#$gid_setting" -+fi -+ -+cat > "$conf" < "$listfile" 2>&1 || true -+ -+if grep -q "leak_marker\.txt" "$listfile"; then -+ echo "----- leaked listing follows" >&2 -+ sed 's/^/ /' "$listfile" >&2 -+ echo "----- leaked listing ends" >&2 -+ test_fail "sender flist leak: outside/leak_marker.txt was enumerated to the client (daemon's chdir followed the cd symlink during flist generation)" -+fi -+ -+exit 0 -diff --git a/util1.c b/util1.c -index 25ac7c9b..796604f6 100644 ---- a/util1.c -+++ b/util1.c -@@ -1116,6 +1116,7 @@ char *sanitize_path(char *dest, const char *p, const char *rootdir, int depth, i - * Also cleans the path using the clean_fname() function. */ - int change_dir(const char *dir, int set_path_only) - { -+ extern int am_daemon, am_chrooted; - static int initialised, skipped_chdir; - unsigned int len; - -@@ -1154,10 +1155,57 @@ int change_dir(const char *dir, int set_path_only) - curr_dir[curr_dir_len++] = '/'; - memcpy(curr_dir + curr_dir_len, dir, len + 1); - -- if (!set_path_only && chdir(curr_dir)) { -- curr_dir_len = save_dir_len; -- curr_dir[curr_dir_len] = '\0'; -- return 0; -+ if (!set_path_only) { -+ int chdir_failed; -+ /* In the daemon-without-chroot deployment we must not -+ * follow a symlink in any component of the chdir -+ * 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 -+ * 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 -+ * (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) { -+ const char *basedir = NULL; -+ char prefix[MAXPATHLEN]; -+ int dfd; -+ if (skipped_chdir) { -+ if (save_dir_len >= sizeof prefix) { -+ errno = ENAMETOOLONG; -+ chdir_failed = 1; -+ goto chdir_cleanup; -+ } -+ memcpy(prefix, curr_dir, save_dir_len); -+ prefix[save_dir_len] = '\0'; -+ basedir = prefix; -+ } -+ dfd = secure_relative_open(basedir, dir, -+ O_RDONLY | O_DIRECTORY, 0); -+ if (dfd < 0) { -+ chdir_failed = 1; -+ } else { -+ chdir_failed = fchdir(dfd) != 0; -+ close(dfd); -+ } -+ } else { -+ chdir_failed = chdir(curr_dir) != 0; -+ } -+ chdir_cleanup: -+ if (chdir_failed) { -+ curr_dir_len = save_dir_len; -+ curr_dir[curr_dir_len] = '\0'; -+ return 0; -+ } - } - skipped_chdir = set_path_only; - } --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0027-syscall-add-symlink-race-safe-do_-_at-wrappers-and-h.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0027-syscall-add-symlink-race-safe-do_-_at-wrappers-and-h.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0027-syscall-add-symlink-race-safe-do_-_at-wrappers-and-h.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0027-syscall-add-symlink-race-safe-do_-_at-wrappers-and-h.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,1774 +0,0 @@ -From 72a6634479b6f9b980efdb40815abd04c6c20d9d Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Tue, 5 May 2026 15:02:48 +1000 -Subject: [PATCH 27/38] syscall: add symlink-race-safe do_*_at() wrappers and - harden secure_relative_open - -Add the rest of the path-based syscall wrappers and migrate every -receiver-side caller: - - do_lchown_at, do_rename_at, do_mkdir_at, do_symlink_at, - do_mknod_at, do_link_at, do_unlink_at, do_rmdir_at, - do_utimensat_at, do_stat_at, do_lstat_at - -Same shape as do_chmod_at: open each parent under -secure_relative_open(), call the *at() variant against the dirfd, -fall through to the bare path-based syscall in non-daemon / -chrooted / absolute-path / no-parent cases. macOS's -setattrlist-based set_times tier is also routed through the -utimensat_at path on daemon-no-chroot. - -Hardenings to secure_relative_open() itself: - - confine basedir resolution under the same kernel mechanism - used for relpath (basedirs from --copy-dest / --link-dest are - sender-controllable in daemon mode) - - reject any '..' component (bare '..', 'foo/..', 'subdir/..') - so the per-component O_NOFOLLOW fallback can't escape - - return the dirfd we built up from the per-component fallback - when the caller passed O_DIRECTORY (otherwise every do_*_at - failed with EINVAL on platforms without RESOLVE_BENEATH) - -Adds testsuite/alt-dest-symlink-race.test and -testsuite/secure-relpath-validation.test (with t_secure_relpath -helper) as regression coverage for the new hardenings. - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - Makefile.in | 8 +- - backup.c | 14 +- - cleanup.c | 2 +- - delete.c | 2 +- - generator.c | 22 +- - hlink.c | 2 +- - receiver.c | 8 +- - rsync.c | 6 +- - syscall.c | 841 ++++++++++++++++++++++- - t_secure_relpath.c | 151 ++++ - testsuite/alt-dest-symlink-race.test | 96 +++ - testsuite/secure-relpath-validation.test | 34 + - util1.c | 20 +- - xattrs.c | 9 +- - 14 files changed, 1164 insertions(+), 51 deletions(-) - create mode 100644 t_secure_relpath.c - create mode 100755 testsuite/alt-dest-symlink-race.test - create mode 100755 testsuite/secure-relpath-validation.test - -diff --git a/Makefile.in b/Makefile.in -index fe0a5494..624c4d69 100644 ---- a/Makefile.in -+++ b/Makefile.in -@@ -58,12 +58,12 @@ TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/perms - # Programs we must have to run the test cases - CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \ - testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) t_chmod_secure$(EXEEXT) \ -- wildtest$(EXEEXT) simdtest$(EXEEXT) -+ t_secure_relpath$(EXEEXT) wildtest$(EXEEXT) simdtest$(EXEEXT) - - CHECK_SYMLINKS = testsuite/chown-fake.test testsuite/devices-fake.test testsuite/xattrs-hlink.test - - # 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 trimslash.o wildtest.o -+CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o t_chmod_secure.o t_secure_relpath.o trimslash.o wildtest.o - - # note that the -I. is needed to handle config.h when using VPATH - .c.o: -@@ -183,6 +183,10 @@ T_CHMOD_SECURE_OBJ = t_chmod_secure.o syscall.o util1.o util2.o t_stub.o lib/com - t_chmod_secure$(EXEEXT): $(T_CHMOD_SECURE_OBJ) - $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CHMOD_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$(EXEEXT): $(T_SECURE_RELPATH_OBJ) -+ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SECURE_RELPATH_OBJ) $(LIBS) -+ - .PHONY: conf - conf: configure.sh config.h.in - -diff --git a/backup.c b/backup.c -index 686cb297..ae8cb49e 100644 ---- a/backup.c -+++ b/backup.c -@@ -39,7 +39,7 @@ static int validate_backup_dir(void) - { - STRUCT_STAT st; - -- if (do_lstat(backup_dir_buf, &st) < 0) { -+ if (do_lstat_at(backup_dir_buf, &st) < 0) { - if (errno == ENOENT) - return 0; - rsyserr(FERROR, errno, "backup lstat %s failed", backup_dir_buf); -@@ -98,7 +98,7 @@ static BOOL copy_valid_path(const char *fname) - for ( ; b; name = b + 1, b = strchr(name, '/')) { - *b = '\0'; - -- while (do_mkdir(backup_dir_buf, ACCESSPERMS) < 0) { -+ while (do_mkdir_at(backup_dir_buf, ACCESSPERMS) < 0) { - if (errno == EEXIST) { - val = validate_backup_dir(); - if (val > 0) -@@ -197,7 +197,7 @@ 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(from, to) == 0) { -+ if (do_link_at(from, to) == 0) { - if (DEBUG_GTE(BACKUP, 1)) - rprintf(FINFO, "make_backup: HLINK %s successful.\n", from); - return 2; -@@ -207,7 +207,7 @@ static inline int link_or_rename(const char *from, const char *to, - return 0; - } - #endif -- if (do_rename(from, to) == 0) { -+ if (do_rename_at(from, to) == 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! */ -@@ -246,7 +246,7 @@ int make_backup(const char *fname, BOOL prefer_rename) - goto success; - if (errno == EEXIST || errno == EISDIR) { - STRUCT_STAT bakst; -- if (do_lstat(buf, &bakst) == 0) { -+ if (do_lstat_at(buf, &bakst) == 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; -@@ -277,7 +277,7 @@ int make_backup(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(buf, file->mode, sx.st.st_rdev) < 0) -+ if (do_mknod_at(buf, file->mode, sx.st.st_rdev) < 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); -@@ -294,7 +294,7 @@ int make_backup(const char *fname, BOOL prefer_rename) - } - ret = 2; - } else { -- if (do_symlink(sl, buf) < 0) -+ if (do_symlink_at(sl, buf) < 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); -diff --git a/cleanup.c b/cleanup.c -index 40d26baa..0493fbbb 100644 ---- a/cleanup.c -+++ b/cleanup.c -@@ -198,7 +198,7 @@ NORETURN void _exit_cleanup(int code, const char *file, int line) - switch_step++; - - if (cleanup_fname) -- do_unlink(cleanup_fname); -+ do_unlink_at(cleanup_fname); - if (exit_code) - kill_all(SIGUSR1); - if (cleanup_pid && cleanup_pid == getpid()) { -diff --git a/delete.c b/delete.c -index ded0ab2a..4a52122d 100644 ---- a/delete.c -+++ b/delete.c -@@ -160,7 +160,7 @@ enum delret delete_item(char *fbuf, uint16 mode, uint16 flags) - - if (S_ISDIR(mode)) { - what = "rmdir"; -- ok = do_rmdir(fbuf) == 0; -+ ok = do_rmdir_at(fbuf) == 0; - } else { - if (make_backups > 0 && !(flags & DEL_FOR_BACKUP) && (backup_dir || !is_backup_file(fbuf))) { - what = "make_backup"; -diff --git a/generator.c b/generator.c -index e5aff654..e5b2d176 100644 ---- a/generator.c -+++ b/generator.c -@@ -984,7 +984,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(fname) < 0 && errno != ENOENT) -+ if (do_unlink_at(fname) < 0 && errno != ENOENT) - goto got_nothing_for_ya; - } - #ifdef SUPPORT_HARD_LINKS -@@ -1112,7 +1112,7 @@ 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(cmpbuf, fname) < 0) { -+ if (do_link_at(cmpbuf, fname) < 0) { - rsyserr(FERROR_XFER, errno, - "failed to hard-link %s with %s", - cmpbuf, fname); -@@ -1315,7 +1315,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, - } - } - if (relative_paths && !implied_dirs && file->mode != 0 -- && do_stat(dn, &sx.st) < 0) { -+ && do_stat_at(dn, &sx.st) < 0) { - if (dry_run) - goto parent_is_dry_missing; - if (make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0) { -@@ -1427,7 +1427,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, - && (stype == FT_DIR - || delete_item(fname, sx.st.st_mode, del_opts | DEL_FOR_DIR) != 0)) - goto cleanup; /* Any errors get reported later. */ -- if (do_mkdir(fname, (file->mode|added_perms) & 0700) == 0) -+ if (do_mkdir_at(fname, (file->mode|added_perms) & 0700) == 0) - file->flags |= FLAG_DIR_CREATED; - goto cleanup; - } -@@ -1469,10 +1469,10 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, - itemize(fnamecmp, file, ndx, statret, &sx, - statret ? ITEM_LOCAL_CHANGE : 0, 0, NULL); - } -- if (real_ret != 0 && do_mkdir(fname,file->mode|added_perms) < 0 && errno != EEXIST) { -+ if (real_ret != 0 && do_mkdir_at(fname,file->mode|added_perms) < 0 && errno != EEXIST) { - if (!relative_paths || errno != ENOENT - || make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0 -- || (do_mkdir(fname, file->mode|added_perms) < 0 && errno != EEXIST)) { -+ || (do_mkdir_at(fname, file->mode|added_perms) < 0 && errno != EEXIST)) { - rsyserr(FERROR_XFER, errno, - "recv_generator: mkdir %s failed", - full_fname(fname)); -@@ -1808,7 +1808,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, - ; - else if (quick_check_ok(FT_REG, fnamecmp, file, &sx.st)) { - if (partialptr) { -- do_unlink(partialptr); -+ do_unlink_at(partialptr); - handle_partial_dir(partialptr, PDIR_DELETE); - } - set_file_attrs(fname, file, &sx, NULL, maybe_ATTRS_REPORT | maybe_ATTRS_ACCURATE_TIME); -@@ -2016,7 +2016,7 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const - - if (slnk) { - #ifdef SUPPORT_LINKS -- if (do_symlink(slnk, create_name) < 0) { -+ if (do_symlink_at(slnk, create_name) < 0) { - rsyserr(FERROR_XFER, errno, "symlink %s -> \"%s\" failed", - full_fname(create_name), slnk); - return 0; -@@ -2032,7 +2032,7 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const - return 0; - #endif - } else { -- if (do_mknod(create_name, file->mode, rdev) < 0) { -+ if (do_mknod_at(create_name, file->mode, rdev) < 0) { - rsyserr(FERROR_XFER, errno, "mknod %s failed", - full_fname(create_name)); - return 0; -@@ -2040,14 +2040,14 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const - } - - if (!skip_atomic) { -- if (do_rename(tmpname, fname) < 0) { -+ if (do_rename_at(tmpname, fname) < 0) { - char *full_tmpname = strdup(full_fname(tmpname)); - if (full_tmpname == NULL) - out_of_memory("atomic_create"); - rsyserr(FERROR_XFER, errno, "rename %s -> \"%s\" failed", - full_tmpname, full_fname(fname)); - free(full_tmpname); -- do_unlink(tmpname); -+ do_unlink_at(tmpname); - return 0; - } - } -diff --git a/hlink.c b/hlink.c -index 2c14407a..eb36730f 100644 ---- a/hlink.c -+++ b/hlink.c -@@ -454,7 +454,7 @@ 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) - { -- if (do_link(oldname, fname) < 0) { -+ if (do_link_at(oldname, fname) < 0) { - enum logcode code; - if (terse) { - if (!INFO_GTE(NAME, 1)) -diff --git a/receiver.c b/receiver.c -index 5a2c8c5a..8cf8366b 100644 ---- a/receiver.c -+++ b/receiver.c -@@ -442,7 +442,7 @@ static void handle_delayed_updates(char *local_name) - } - /* We don't use robust_rename() here because the - * partial-dir must be on the same drive. */ -- if (do_rename(partialptr, fname) < 0) { -+ if (do_rename_at(partialptr, fname) < 0) { - rsyserr(FERROR_XFER, errno, - "rename failed for %s (from %s)", - full_fname(fname), partialptr); -@@ -926,7 +926,7 @@ int recv_files(int f_in, int f_out, char *local_name) - recv_ok = -1; - else if (fnamecmp == partialptr) { - if (!one_inplace) -- do_unlink(partialptr); -+ do_unlink_at(partialptr); - handle_partial_dir(partialptr, PDIR_DELETE); - } - } else if (keep_partial && partialptr && (!one_inplace || delay_updates)) { -@@ -935,7 +935,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(fnametmp); -+ do_unlink_at(fnametmp); - recv_ok = -1; - } else if (!finish_transfer(partialptr, fnametmp, fnamecmp, NULL, - file, recv_ok, !partial_dir)) -@@ -946,7 +946,7 @@ int recv_files(int f_in, int f_out, char *local_name) - } else - partialptr = NULL; - } else if (!one_inplace) -- do_unlink(fnametmp); -+ do_unlink_at(fnametmp); - - cleanup_disable(); - -diff --git a/rsync.c b/rsync.c -index cc46a2f9..1d2ae82a 100644 ---- a/rsync.c -+++ b/rsync.c -@@ -547,7 +547,7 @@ 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 (do_lchown(fname, uid, gid) != 0) { -+ if (do_lchown_at(fname, uid, gid) != 0) { - /* We shouldn't have attempted to change uid - * or gid unless have the privilege. */ - rsyserr(FERROR_XFER, errno, "%s %s failed", -@@ -758,7 +758,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) < 0) -- do_unlink(fnametmp); -+ do_unlink_at(fnametmp); - return 0; - } - if (ret == 0) { -@@ -774,7 +774,7 @@ 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(fnametmp, fname) < 0) { -+ if (do_rename_at(fnametmp, fname) < 0) { - rsyserr(FERROR_XFER, errno, "rename %s -> \"%s\"", - full_fname(fnametmp), fname); - return 0; -diff --git a/syscall.c b/syscall.c -index 167aae0e..2cff0b38 100644 ---- a/syscall.c -+++ b/syscall.c -@@ -93,6 +93,63 @@ int do_unlink(const char *path) - 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; -+ -+ if (!am_daemon || am_chrooted) -+ 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) - { -@@ -117,6 +174,70 @@ int do_symlink(const char *lnk, const char *path) - 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. Only the parent -+ directory of `path` needs protection -- symlinkat() does not resolve -+ the final component (it creates it). Defence: open parent of `path` -+ under secure_relative_open() and call symlinkat() against that -+ dirfd. The link target string `lnk` is stored verbatim and not -+ resolved at creation time, so it doesn't need scrutiny here. -+ -+ Falls through to do_symlink() for the --fake-super (am_root < 0) -+ path -- that code path opens `path` with do_open() which has its -+ own (separate) symlink-race exposure tracked elsewhere. -+*/ -+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, ret, e; -+ size_t dlen; -+ -+ if (dry_run) return 0; -+ RETURN_ERROR_IF_RO_OR_LO; -+ -+ if (!am_daemon || am_chrooted) -+ return do_symlink(lnk, path); -+ -+#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS -+ if (am_root < 0) -+ return do_symlink(lnk, path); -+#endif -+ -+ if (!path || !*path || *path == '/') -+ return do_symlink(lnk, path); -+ -+ slash = strrchr(path, '/'); -+ if (!slash) -+ return do_symlink(lnk, 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 = symlinkat(lnk, dfd, bname); -+ e = errno; -+ close(dfd); -+ errno = e; -+ return ret; -+#else -+ return do_symlink(lnk, path); -+#endif -+} -+ - #if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS - ssize_t do_readlink(const char *path, char *buf, size_t bufsiz) - { -@@ -153,6 +274,106 @@ int do_link(const char *old_path, const char *new_path) - 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 (!am_daemon || am_chrooted) -+ return do_link(old_path, new_path); -+ -+ if (!old_path || !*old_path || *old_path == '/' -+ || !new_path || !*new_path || *new_path == '/') -+ return do_link(old_path, new_path); -+ -+ 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. */ -+ 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_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) -@@ -165,6 +386,66 @@ int do_lchown(const char *path, uid_t owner, gid_t group) - 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) -+{ -+#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 (!am_daemon || am_chrooted) -+ 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; -@@ -215,6 +496,76 @@ int do_mknod(const char *pathname, mode_t mode, dev_t dev) - #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. -+ -+ Falls through to do_mknod() for fake-super (am_root < 0) and for -+ sockets, both of which use auxiliary path-based syscalls that -+ don't have an *at() variant in any portable form. -+*/ -+int do_mknod_at(const char *pathname, mode_t mode, dev_t dev) -+{ -+#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 (!am_daemon || am_chrooted) -+ return do_mknod(pathname, mode, dev); -+ -+ if (am_root < 0) -+ return do_mknod(pathname, mode, dev); -+ -+#if !defined MKNOD_CREATES_SOCKETS && defined HAVE_SYS_UN_H -+ if (S_ISSOCK(mode)) -+ return do_mknod(pathname, mode, dev); -+#endif -+ -+ if (!pathname || !*pathname || *pathname == '/') -+ return do_mknod(pathname, mode, dev); -+ -+ slash = strrchr(pathname, '/'); -+ if (!slash) -+ return do_mknod(pathname, mode, dev); -+ -+ 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; -+ -+#if !defined MKNOD_CREATES_FIFOS && defined HAVE_MKFIFO -+ if (S_ISFIFO(mode)) -+ ret = mkfifoat(dfd, bname, mode); -+ else -+#endif -+ ret = mknodat(dfd, bname, mode, dev); -+ e = errno; -+ 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; -@@ -222,6 +573,57 @@ int do_rmdir(const char *pathname) - 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; -+ -+ if (!am_daemon || am_chrooted) -+ 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) - { - if (flags != O_RDONLY) { -@@ -370,6 +772,89 @@ int do_rename(const char *old_path, const char *new_path) - 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, no- -+ parent 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 = -1, new_dfd = -1, ret = -1, e; -+ size_t old_dlen, new_dlen; -+ -+ if (dry_run) return 0; -+ RETURN_ERROR_IF_RO_OR_LO; -+ -+ if (!am_daemon || am_chrooted) -+ return do_rename(old_path, new_path); -+ -+ if (!old_path || !*old_path || *old_path == '/' -+ || !new_path || !*new_path || *new_path == '/') -+ return do_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); -+ -+ old_dlen = old_slash - old_path; -+ new_dlen = new_slash - new_path; -+ if (old_dlen >= sizeof old_dirpath || new_dlen >= sizeof new_dirpath) { -+ errno = ENAMETOOLONG; -+ return -1; -+ } -+ memcpy(old_dirpath, old_path, old_dlen); -+ old_dirpath[old_dlen] = '\0'; -+ memcpy(new_dirpath, new_path, new_dlen); -+ new_dirpath[new_dlen] = '\0'; -+ old_bname = old_slash + 1; -+ new_bname = new_slash + 1; -+ -+ old_dfd = secure_relative_open(NULL, old_dirpath, O_RDONLY | O_DIRECTORY, 0); -+ if (old_dfd < 0) -+ return -1; -+ -+ if (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; -+ close(old_dfd); -+ errno = e; -+ return -1; -+ } -+ } -+ -+ ret = renameat(old_dfd, old_bname, new_dfd, new_bname); -+ e = errno; -+ if (new_dfd != old_dfd) -+ close(new_dfd); -+ 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) - { -@@ -412,6 +897,66 @@ int do_mkdir(char *path, mode_t mode) - 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; -+ trim_trailing_slashes(path); -+ -+ if (!am_daemon || am_chrooted) -+ 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) - { -@@ -465,6 +1010,76 @@ int do_lstat(const char *path, STRUCT_STAT *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 (!am_daemon || am_chrooted) -+ 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 -+ 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) -+{ -+#ifdef SUPPORT_LINKS -+ return do_xstat_at(path, st, AT_SYMLINK_NOFOLLOW, do_lstat); -+#else -+ return do_xstat_at(path, st, 0, do_stat); -+#endif -+} -+ - int do_fstat(int fd, STRUCT_STAT *st) - { - #ifdef USE_STAT64_FUNCS -@@ -486,12 +1101,26 @@ OFF_T do_lseek(int fd, OFF_T offset, int whence) - #ifdef HAVE_SETATTRLIST - int do_setattrlist_times(const char *path, STRUCT_STAT *stp) - { -+ extern int am_daemon, am_chrooted; - 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). On a -+ * daemon-no-chroot deployment, return ENOSYS so set_times()' -+ * tier walk falls through to do_utimensat_at(), which routes -+ * the timestamp update through a secure parent dirfd. The -+ * macOS-specific attribute set this function would have used -+ * (ATTR_CMN_MODTIME / ATTR_CMN_ACCTIME) is the same set -+ * utimensat() handles, so no functionality is lost. */ -+ if (am_daemon && !am_chrooted) { -+ 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; -@@ -508,12 +1137,25 @@ int do_setattrlist_times(const char *path, STRUCT_STAT *stp) - #ifdef SUPPORT_CRTIMES - int do_setattrlist_crtime(const char *path, time_t crtime) - { -+ extern int am_daemon, am_chrooted; - struct attrlist attrList; - struct timespec ts; - - if (dry_run) return 0; - RETURN_ERROR_IF_RO_OR_LO; - -+ /* Same path-follows-parent-symlinks concern as -+ * do_setattrlist_times. There is no portable at-aware variant -+ * of setattrlist that targets ATTR_CMN_CRTIME, so on a -+ * daemon-no-chroot deployment we return -1 and accept that -+ * crtime preservation is silently dropped for that file (the -+ * caller treats this as "crtime not updated"). The transfer -+ * itself continues normally. */ -+ if (am_daemon && !am_chrooted) { -+ errno = ENOSYS; -+ return -1; -+ } -+ - ts.tv_sec = crtime; - ts.tv_nsec = 0; - -@@ -529,10 +1171,19 @@ int do_setattrlist_crtime(const char *path, time_t crtime) - time_t get_create_time(const char *path, STRUCT_STAT *stp) - { - #ifdef HAVE_GETATTRLIST -+ extern int am_daemon, am_chrooted; - static struct create_time attrBuf; - struct attrlist attrList; - - (void)stp; -+ /* getattrlist() is also path-based and follows parent -+ * symlinks. In daemon-no-chroot, refuse rather than read the -+ * crtime of a file the parent-symlink chain might point at -+ * outside the module. The caller's "no crtime available" -+ * path returns 0; the file gets a fresh crtime instead of -+ * preserving the source's. */ -+ if (am_daemon && !am_chrooted) -+ return 0; - memset(&attrList, 0, sizeof attrList); - attrList.bitmapcount = ATTR_BIT_MAP_COUNT; - attrList.commonattr = ATTR_CMN_CRTIME; -@@ -598,6 +1249,81 @@ int do_utimensat(const char *path, STRUCT_STAT *stp) - #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 (!am_daemon || am_chrooted) -+ 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 -@@ -820,6 +1546,30 @@ int do_open_nofollow(const char *pathname, int flags) - The relpath must also not contain any ../ elements in the path. - */ - -+/* 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 that the per-component walk fallback -+ * would otherwise resolve through ".." -- e.g. bare "..", "foo/..", -+ * "subdir/.." -- which RESOLVE_BENEATH-equivalent kernels reject in -+ * the kernel but the per-component fallback (NetBSD/OpenBSD/Solaris/ -+ * Cygwin/pre-5.6 Linux) does not. */ -+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; -+} -+ - #ifdef __linux__ - static int secure_relative_open_linux(const char *basedir, const char *relpath, int flags, mode_t mode) - { -@@ -833,10 +1583,25 @@ static int secure_relative_open_linux(const char *basedir, const char *relpath, - - if (basedir == NULL) { - dirfd = AT_FDCWD; -- } else { -+ } else if (basedir[0] == '/') { -+ /* Absolute basedir: operator-trusted (module_dir and the -+ * like). Plain openat. */ - dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); - if (dirfd == -1) - return -1; -+ } else { -+ /* Relative basedir: may be wire-influenced via -+ * --link-dest / --copy-dest / --compare-dest. Resolve it -+ * under the same RESOLVE_BENEATH guarantee as relpath, so -+ * a parent symlink on basedir cannot redirect the dirfd -+ * outside the CWD anchor. */ -+ struct open_how bhow; -+ memset(&bhow, 0, sizeof bhow); -+ bhow.flags = O_RDONLY | O_DIRECTORY; -+ bhow.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS; -+ dirfd = syscall(SYS_openat2, AT_FDCWD, basedir, &bhow, sizeof bhow); -+ if (dirfd == -1) -+ return -1; - } - - retfd = syscall(SYS_openat2, dirfd, relpath, &how, sizeof how); -@@ -859,10 +1624,17 @@ static int secure_relative_open_resolve_beneath(const char *basedir, const char - - if (basedir == NULL) { - dirfd = AT_FDCWD; -- } else { -+ } else if (basedir[0] == '/') { -+ /* Absolute basedir: operator-trusted, plain openat. */ - dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); - if (dirfd == -1) - return -1; -+ } else { -+ /* Relative basedir: confine its resolution beneath CWD -+ * (see secure_relative_open_linux for the rationale). */ -+ dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY | O_RESOLVE_BENEATH); -+ if (dirfd == -1) -+ return -1; - } - - retfd = openat(dirfd, relpath, flags | O_RESOLVE_BENEATH, mode); -@@ -880,8 +1652,20 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo - errno = EINVAL; - return -1; - } -- if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../")) { -- // no ../ elements allowed in the relpath -+ /* Reject any path with a literal ".." component (bare "..", -+ * "../foo", "foo/..", "foo/../bar", "subdir/.."). The previous -+ * substring-based check caught only "../" prefix and "/../" -+ * substring; bare ".." and trailing "/.." escape on the per- -+ * component walk fallback used by NetBSD/OpenBSD/Solaris/Cygwin -+ * and pre-5.6 Linux. RESOLVE_BENEATH on Linux/FreeBSD/macOS -+ * catches some of these in-kernel with EXDEV, but the front -+ * door must reject them consistently with EINVAL across all -+ * platforms so callers can rely on the validation. */ -+ if (path_has_dotdot_component(relpath)) { -+ errno = EINVAL; -+ return -1; -+ } -+ if (basedir && basedir[0] != '/' && path_has_dotdot_component(basedir)) { - errno = EINVAL; - return -1; - } -@@ -911,15 +1695,47 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo - #else - int dirfd = AT_FDCWD; - if (basedir != NULL) { -- dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); -- if (dirfd == -1) { -- return -1; -+ if (basedir[0] == '/') { -+ /* Absolute basedir: operator-trusted, plain openat. */ -+ dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); -+ if (dirfd == -1) { -+ return -1; -+ } -+ } else { -+ /* Relative basedir: walk it component-by-component -+ * with O_NOFOLLOW. This is the per-component -+ * RESOLVE_BENEATH equivalent for platforms without -+ * kernel-supported confinement, and matches the -+ * relpath walk below. Symlinks in basedir are -+ * rejected outright on this fallback path; the -+ * Linux openat2 / O_RESOLVE_BENEATH paths above -+ * still allow within-tree symlinks. */ -+ char *bcopy = my_strdup(basedir, __FILE__, __LINE__); -+ if (!bcopy) -+ return -1; -+ for (const char *part = strtok(bcopy, "/"); -+ part != NULL; -+ part = strtok(NULL, "/")) -+ { -+ int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); -+ if (next_fd == -1) { -+ int save_errno = errno; -+ if (dirfd != AT_FDCWD) close(dirfd); -+ free(bcopy); -+ errno = save_errno; -+ return -1; -+ } -+ if (dirfd != AT_FDCWD) close(dirfd); -+ dirfd = next_fd; -+ } -+ free(bcopy); - } - } - int retfd = -1; - - char *path_copy = my_strdup(relpath, __FILE__, __LINE__); - if (!path_copy) { -+ if (dirfd != AT_FDCWD) close(dirfd); - return -1; - } - -@@ -945,8 +1761,15 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo - dirfd = next_fd; - } - -- // the path must be a directory -- errno = EINVAL; -+ /* All components walked as directories. If the caller asked for -+ * O_DIRECTORY, return the dirfd we built up; otherwise the path -+ * resolved to a directory but the caller wanted a regular file. */ -+ if ((flags & O_DIRECTORY) && dirfd != AT_FDCWD) { -+ retfd = dirfd; -+ dirfd = AT_FDCWD; -+ goto cleanup; -+ } -+ errno = EISDIR; - - cleanup: - free(path_copy); -diff --git a/t_secure_relpath.c b/t_secure_relpath.c -new file mode 100644 -index 00000000..a0fdf0d2 ---- /dev/null -+++ b/t_secure_relpath.c -@@ -0,0 +1,151 @@ -+/* -+ * Test harness for secure_relative_open()'s front-door input -+ * validation. Codex audit Finding 5 noted that the existing check -+ * -+ * if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../")) -+ * -+ * catches "../foo" and "foo/../bar" but misses bare ".." (an actual -+ * one-level escape on platforms that fall back to the per-component -+ * walk), as well as "a/..", "foo/..", and any other form that -+ * decomposes to a ".." component when split on "/". The kernel- -+ * enforced RESOLVE_BENEATH (Linux 5.6+) and O_RESOLVE_BENEATH -+ * (FreeBSD 13+, macOS 15+) reject these in-kernel; the per- -+ * component fallback used on NetBSD, OpenBSD, Solaris, Cygwin and -+ * pre-5.6 Linux does not, so the validation must happen at the -+ * front door. -+ * -+ * This helper invokes secure_relative_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); -+ * post-fix, the front-door check catches every variant up front -+ * with a consistent EINVAL across platforms. -+ * -+ * Not linked into rsync itself. -+ */ -+ -+#include "rsync.h" -+ -+#include -+ -+int dry_run = 0; -+int am_root = 0; -+int am_sender = 0; -+int read_only = 0; -+int list_only = 0; -+int copy_links = 0; -+int copy_unsafe_links = 0; -+extern int am_daemon, am_chrooted; -+ -+short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG]; -+ -+static int errs = 0; -+ -+static void check_relpath(const char *relpath) -+{ -+ int fd; -+ int saved_errno; -+ -+ errno = 0; -+ fd = secure_relative_open(NULL, relpath, O_RDONLY | O_DIRECTORY, 0); -+ saved_errno = errno; -+ -+ if (fd >= 0) { -+ fprintf(stderr, -+ "FAIL [relpath=%-12s]: returned valid fd %d (escape) -- expected -1 EINVAL\n", -+ relpath, fd); -+ close(fd); -+ errs++; -+ return; -+ } -+ -+ if (saved_errno != EINVAL) { -+ fprintf(stderr, -+ "FAIL [relpath=%-12s]: rejected but errno=%d (%s), expected EINVAL\n", -+ relpath, saved_errno, strerror(saved_errno)); -+ errs++; -+ return; -+ } -+ -+ fprintf(stderr, "OK [relpath=%-12s]: rejected with EINVAL\n", relpath); -+} -+ -+static void check_basedir(const char *basedir) -+{ -+ int fd; -+ int saved_errno; -+ -+ errno = 0; -+ fd = secure_relative_open(basedir, "ok", O_RDONLY | O_DIRECTORY, 0); -+ saved_errno = errno; -+ -+ if (fd >= 0) { -+ fprintf(stderr, -+ "FAIL [basedir=%-12s]: returned valid fd %d -- expected -1 EINVAL\n", -+ basedir, fd); -+ close(fd); -+ errs++; -+ return; -+ } -+ -+ if (saved_errno != EINVAL) { -+ fprintf(stderr, -+ "FAIL [basedir=%-12s]: rejected but errno=%d (%s), expected EINVAL\n", -+ basedir, saved_errno, strerror(saved_errno)); -+ errs++; -+ return; -+ } -+ -+ fprintf(stderr, "OK [basedir=%-12s]: rejected with EINVAL\n", basedir); -+} -+ -+int main(int argc, char **argv) -+{ -+ if (argc != 2) { -+ fprintf(stderr, "usage: %s \n", argv[0]); -+ return 2; -+ } -+ if (chdir(argv[1]) < 0) { -+ perror("chdir"); -+ return 2; -+ } -+ -+ /* secure_relative_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 -+ * helper exercises the same code shape the receiver does. */ -+ am_daemon = 1; -+ am_chrooted = 0; -+ -+ mkdir("subdir", 0755); -+ -+ /* Each of these relpaths must be rejected with EINVAL at the -+ * secure_relative_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 -+ * is suspicious and the caller should normalise before passing -+ * it in. The "../foo" / "foo/../bar" / "/foo" / "/" cases are -+ * regression checks for the existing checks. */ -+ check_relpath(".."); -+ check_relpath("../foo"); -+ check_relpath("subdir/.."); -+ check_relpath("subdir/../subdir"); -+ check_relpath("foo/../bar"); -+ check_relpath("/foo"); -+ check_relpath("/"); -+ -+ /* Same checks against basedir (which the codex Finding 2 fix -+ * routes through the same RESOLVE_BENEATH-equivalent). Absolute -+ * basedirs are operator-trusted and intentionally not validated -+ * here. */ -+ check_basedir(".."); -+ check_basedir("../subdir"); -+ check_basedir("subdir/.."); -+ check_basedir("foo/../bar"); -+ -+ if (errs) -+ fprintf(stderr, "\n%d failure(s)\n", errs); -+ return errs ? 1 : 0; -+} -diff --git a/testsuite/alt-dest-symlink-race.test b/testsuite/alt-dest-symlink-race.test -new file mode 100755 -index 00000000..2256f2f2 ---- /dev/null -+++ b/testsuite/alt-dest-symlink-race.test -@@ -0,0 +1,96 @@ -+#!/bin/sh -+ -+# Copyright (C) 2026 by Andrew Tridgell -+ -+# This program is distributable under the terms of the GNU GPL (see -+# COPYING). -+ -+# Regression test for the basedir-confinement gap in -+# secure_relative_open(). The function opens basedir with a plain -+# openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY), without -+# RESOLVE_BENEATH or a per-component O_NOFOLLOW walk, so a parent -+# symlink ON basedir is followed unrestrictedly. RESOLVE_BENEATH is -+# then applied only to relpath, anchored at the wrong directory. -+# -+# The receiver's basis-file lookup at receiver.c passes -+# basis_dir[fnamecmp_type] (from --copy-dest / --link-dest / -+# --compare-dest -- all sender-controllable in daemon mode) as -+# basedir. A daemon-module attacker with write access can plant a -+# symlink at module/cd -> /outside, then run --link-dest=cd to -+# make the daemon's basis-file lookup resolve into /outside, -+# leaking the contents of daemon-readable files via the rsync -+# delta-rolling read-disclosure primitive. -+# -+# We detect the escape by leveraging --link-dest: when basis -+# matches source exactly (content + mtime + mode), --link-dest -+# hard-links the destination to the basis file. With the bug, the -+# destination ends up as a hard link to the outside-the-module -+# file (same inode). With the fix, no basis is found and the -+# destination is a fresh copy (different inode). -+# -+# The vulnerable code path is the same on every platform -+# (including the per-component fallback on systems without -+# RESOLVE_BENEATH), so this test is not platform-gated. -+ -+. "$suitedir/rsync.fns" -+ -+mod="$scratchdir/module" -+outside="$scratchdir/outside" -+src="$scratchdir/src" -+conf="$scratchdir/test-rsyncd.conf" -+ -+rm -rf "$mod" "$outside" "$src" -+mkdir -p "$mod" "$outside" "$src" -+ -+# Portable inode-number helper (GNU coreutils stat -c, BSD stat -f). -+file_inode() { -+ stat -c %i "$1" 2>/dev/null || stat -f %i "$1" -+} -+ -+# Outside-the-module file an attacker would like the daemon to -+# treat as a basis. -+echo "OUTSIDE_SECRET_DATA" > "$outside/target.txt" -+chmod 0644 "$outside/target.txt" -+ -+# The symlink trap planted in the module by the local attacker. -+ln -s "$outside" "$mod/cd" -+ -+# Source file matches outside/target.txt exactly (content + mtime -+# + mode) so --link-dest will hard-link the destination to the -+# basis file iff the daemon's basedir lookup reaches outside/. -+echo "OUTSIDE_SECRET_DATA" > "$src/target.txt" -+touch -r "$outside/target.txt" "$src/target.txt" -+chmod 0644 "$src/target.txt" -+ -+cat > "$conf" </dev/null 2>&1 || true -+ -+if [ ! -f "$mod/target.txt" ]; then -+ test_fail "destination file was not created -- daemon transfer failed before the test could observe the basedir behaviour" -+fi -+ -+outside_inode=$(file_inode "$outside/target.txt") -+dst_inode=$(file_inode "$mod/target.txt") -+ -+if [ "$outside_inode" = "$dst_inode" ]; then -+ test_fail "basedir-escape: --link-dest hard-linked module/target.txt to outside/target.txt (inode $outside_inode); daemon's basis-file lookup followed the parent symlink on the basedir" -+fi -+ -+exit 0 -diff --git a/testsuite/secure-relpath-validation.test b/testsuite/secure-relpath-validation.test -new file mode 100755 -index 00000000..5b77f7cc ---- /dev/null -+++ b/testsuite/secure-relpath-validation.test -@@ -0,0 +1,34 @@ -+#!/bin/sh -+ -+# Copyright (C) 2026 by Andrew Tridgell -+ -+# This program is distributable under the terms of the GNU GPL (see -+# COPYING). -+ -+# Regression test for codex audit Finding 5: secure_relative_open()'s -+# front-door input check rejects "../foo" and "foo/../bar" but -+# misses bare "..", "subdir/..", and other variants whose "/"-split -+# components contain a literal "..". The kernel-enforced -+# RESOLVE_BENEATH (Linux 5.6+) and O_RESOLVE_BENEATH -+# (FreeBSD 13+, macOS 15+) reject these in-kernel; the per-component -+# walk fallback used on NetBSD, OpenBSD, Solaris, Cygwin and pre-5.6 -+# Linux does not -- so the validation must happen at the front door. -+# -+# This test invokes the t_secure_relpath helper, which calls -+# secure_relative_open() with each suspect input and verifies the -+# return value is -1 with errno == EINVAL. EINVAL is the marker -+# that the front-door rejected the input, not the kernel; pre-fix -+# the kernel returns -1 with EXDEV (or, on the per-component -+# fallback, may return a valid fd at all -- "escape"). -+ -+. "$suitedir/rsync.fns" -+ -+testdir="$scratchdir/relpath-test" -+rm -rf "$testdir" -+mkdir -p "$testdir" -+ -+if ! "$TOOLDIR/t_secure_relpath" "$testdir"; then -+ test_fail "t_secure_relpath rejected one or more inputs incorrectly (see stderr above for the specific case)" -+fi -+ -+exit 0 -diff --git a/util1.c b/util1.c -index 796604f6..f85f33e9 100644 ---- a/util1.c -+++ b/util1.c -@@ -141,7 +141,7 @@ int set_times(const char *fname, STRUCT_STAT *stp) - - #ifdef HAVE_UTIMENSAT - #include "case_N.h" -- if (do_utimensat(fname, stp) == 0) -+ if (do_utimensat_at(fname, stp) == 0) - break; - if (errno != ENOSYS) - return -1; -@@ -479,13 +479,13 @@ int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode) - int robust_unlink(const char *fname) - { - #ifndef ETXTBSY -- return do_unlink(fname); -+ return do_unlink_at(fname); - #else - static int counter = 1; - int rc, pos, start; - char path[MAXPATHLEN]; - -- rc = do_unlink(fname); -+ rc = do_unlink_at(fname); - if (rc == 0 || errno != ETXTBSY) - return rc; - -@@ -515,7 +515,7 @@ int robust_unlink(const char *fname) - } - - /* maybe we should return rename()'s exit status? Nah. */ -- if (do_rename(fname, path) != 0) { -+ if (do_rename_at(fname, path) != 0) { - errno = ETXTBSY; - return -1; - } -@@ -538,7 +538,7 @@ int robust_rename(const char *from, const char *to, const char *partialptr, - return 0; - - while (tries--) { -- if (do_rename(from, to) == 0) -+ if (do_rename_at(from, to) == 0) - return 0; - - switch (errno) { -@@ -559,7 +559,7 @@ int robust_rename(const char *from, const char *to, const char *partialptr, - } - if (copy_file(from, to, -1, mode) != 0) - return -2; -- do_unlink(from); -+ do_unlink_at(from); - return 1; - default: - return -1; -@@ -1333,20 +1333,20 @@ int handle_partial_dir(const char *fname, int create) - dir = partial_fname; - if (create) { - STRUCT_STAT st; -- int statret = do_lstat(dir, &st); -+ int statret = do_lstat_at(dir, &st); - if (statret == 0 && !S_ISDIR(st.st_mode)) { -- if (do_unlink(dir) < 0) { -+ if (do_unlink_at(dir) < 0) { - *fn = '/'; - return 0; - } - statret = -1; - } -- if (statret < 0 && do_mkdir(dir, 0700) < 0) { -+ if (statret < 0 && do_mkdir_at(dir, 0700) < 0) { - *fn = '/'; - return 0; - } - } else -- do_rmdir(dir); -+ do_rmdir_at(dir); - *fn = '/'; - - return 1; -diff --git a/xattrs.c b/xattrs.c -index e5d0dd43..5f740bb5 100644 ---- a/xattrs.c -+++ b/xattrs.c -@@ -1249,7 +1249,12 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode) - - int x_stat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst) - { -- int ret = do_stat(fname, fst); -+ /* 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); - if ((ret < 0 || get_stat_xattr(fname, -1, fst, xst) < 0) && xst) - xst->st_mode = 0; - return ret; -@@ -1257,7 +1262,7 @@ int x_stat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst) - - int x_lstat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst) - { -- int ret = do_lstat(fname, fst); -+ int ret = do_lstat_at(fname, fst); - if ((ret < 0 || get_stat_xattr(fname, -1, fst, xst) < 0) && xst) - xst->st_mode = 0; - return ret; --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0028-util1-syscall-secure-copy_file-source-dest-opens-bar.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0028-util1-syscall-secure-copy_file-source-dest-opens-bar.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0028-util1-syscall-secure-copy_file-source-dest-opens-bar.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0028-util1-syscall-secure-copy_file-source-dest-opens-bar.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,565 +0,0 @@ -From 90495eecd0039cab6f59f203afec2f37c47bc165 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Wed, 6 May 2026 09:45:30 +1000 -Subject: [PATCH 28/38] util1+syscall: secure copy_file source/dest opens; - bare-path defence-in-depth - -Three related codex audit findings: - - Finding 3a: copy_file()'s source open in util1.c used - do_open_nofollow(), which only rejects a final-component - symlink. A parent-component symlink (e.g. --copy-dest=cd where - cd -> /outside) follows freely and reads outside the module. - Route through secure_relative_open() with O_NOFOLLOW. - - Finding 3b: generator.c's in-place backup-file create still - used a bare do_open with O_CREAT, leaving a tiny but reachable - parent-symlink window between the secure unlink (already - through do_unlink_at) and the create. Add do_open_at() that - goes through a secure parent dirfd, and route the call site - through it. - - Finding 3c: copy_file()'s destination open in - unlink_and_reopen() had the same bare-do_open pattern; route - through do_open_at as well. - -Adds testsuite/copy-dest-source-symlink.test and -testsuite/bare-do-open-symlink-race.test as regression coverage -for both attack shapes. - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - generator.c | 2 +- - syscall.c | 138 +++++++++++++++-- - testsuite/bare-do-open-symlink-race.test | 186 +++++++++++++++++++++++ - testsuite/copy-dest-source-symlink.test | 83 ++++++++++ - util1.c | 21 ++- - 5 files changed, 416 insertions(+), 14 deletions(-) - create mode 100755 testsuite/bare-do-open-symlink-race.test - create mode 100755 testsuite/copy-dest-source-symlink.test - -diff --git a/generator.c b/generator.c -index e5b2d176..311e9b78 100644 ---- a/generator.c -+++ b/generator.c -@@ -1896,7 +1896,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, - back_file = NULL; - goto cleanup; - } -- if ((f_copy = do_open(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600)) < 0) { -+ if ((f_copy = do_open_at(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600)) < 0) { - rsyserr(FERROR_XFER, errno, "open %s", full_fname(backupptr)); - unmake_file(back_file); - back_file = NULL; -diff --git a/syscall.c b/syscall.c -index 2cff0b38..47777350 100644 ---- a/syscall.c -+++ b/syscall.c -@@ -203,11 +203,6 @@ int do_symlink_at(const char *lnk, const char *path) - if (!am_daemon || am_chrooted) - return do_symlink(lnk, path); - --#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS -- if (am_root < 0) -- return do_symlink(lnk, path); --#endif -- - if (!path || !*path || *path == '/') - return do_symlink(lnk, path); - -@@ -228,6 +223,34 @@ int do_symlink_at(const char *lnk, const char *path) - if (dfd < 0) - return -1; - -+#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 dirfd, with O_NOFOLLOW so a pre- -+ * planted symlink at the basename can't redirect the file -+ * creation. (Previously the fake-super branch fell through to -+ * the bare-path do_symlink at the top of the function.) */ -+ 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; -+ close(dfd); -+ errno = e; -+ return -1; -+ } -+ ret = (write(fd, lnk, len) == len) ? 0 : -1; -+ if (close(fd) < 0) -+ ret = -1; -+ e = errno; -+ close(dfd); -+ errno = e; -+ return ret; -+ } -+#endif -+ - ret = symlinkat(lnk, dfd, bname); - e = errno; - close(dfd); -@@ -503,9 +526,12 @@ int do_mknod(const char *pathname, mode_t mode, dev_t dev) - mknodat() against that dirfd. mknodat() covers both regular-file - (S_IFREG with dev=0) and FIFO (S_IFIFO) and device-node creation. - -- Falls through to do_mknod() for fake-super (am_root < 0) and for -- sockets, both of which use auxiliary path-based syscalls that -- don't have an *at() variant in any portable form. -+ Fake-super (am_root < 0) is handled inline against the secure -+ parent dirfd: it creates a regular empty file (the same file-as- -+ metadata-placeholder pattern do_mknod uses) via openat() with -+ O_NOFOLLOW. Sockets fall through to do_mknod() because their -+ bind(2) takes a path argument with no portable bindat() variant; -+ this is documented as a residual. - */ - int do_mknod_at(const char *pathname, mode_t mode, dev_t dev) - { -@@ -523,9 +549,6 @@ int do_mknod_at(const char *pathname, mode_t mode, dev_t dev) - if (!am_daemon || am_chrooted) - return do_mknod(pathname, mode, dev); - -- if (am_root < 0) -- return do_mknod(pathname, mode, dev); -- - #if !defined MKNOD_CREATES_SOCKETS && defined HAVE_SYS_UN_H - if (S_ISSOCK(mode)) - return do_mknod(pathname, mode, dev); -@@ -551,6 +574,29 @@ int do_mknod_at(const char *pathname, mode_t mode, dev_t dev) - if (dfd < 0) - return -1; - -+ 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 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; -+ close(dfd); -+ errno = e; -+ return -1; -+ } -+ ret = (close(fd) < 0) ? -1 : 0; -+ e = errno; -+ close(dfd); -+ errno = e; -+ return ret; -+ } -+ - #if !defined MKNOD_CREATES_FIFOS && defined HAVE_MKFIFO - if (S_ISFIFO(mode)) - ret = mkfifoat(dfd, bname, mode); -@@ -639,6 +685,76 @@ int do_open(const char *pathname, int flags, mode_t mode) - 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 (!am_daemon || am_chrooted) -+ 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) - { -diff --git a/testsuite/bare-do-open-symlink-race.test b/testsuite/bare-do-open-symlink-race.test -new file mode 100755 -index 00000000..b8c51bbe ---- /dev/null -+++ b/testsuite/bare-do-open-symlink-race.test -@@ -0,0 +1,186 @@ -+#!/bin/sh -+ -+# Copyright (C) 2026 by Andrew Tridgell -+ -+# This program is distributable under the terms of the GNU GPL (see -+# COPYING). -+ -+# Regression test for codex audit Findings 3b and 3c: -+# -+# 3b: generator.c:1905 -- the in-place backup creation opens -+# backupptr via bare do_open(O_WRONLY|O_CREAT|O_TRUNC|O_EXCL). -+# With --backup-dir set to an attacker-planted parent symlink, -+# the backup file is written outside the module under the -+# daemon's authority. -+# -+# 3c-symlink: syscall.c:207 -- do_symlink_at falls through to bare -+# do_symlink for am_root < 0 (fake-super), which then opens -+# the destination path with bare open() (final-component -+# fake-super file). A parent symlink on the destination path -+# redirects the file creation outside the module. -+# -+# 3c-mknod: syscall.c:506 -- do_mknod_at falls through to bare -+# do_mknod for am_root < 0, same path-based open(). For -+# FIFOs/sockets/devices the bare path is also used. -+# -+# Each scenario plants a "secret" file outside the module at a -+# location the symlink trap points to. The check is that the -+# outside file's content and mode are unchanged after the attack -+# attempt. -+ -+. "$suitedir/rsync.fns" -+ -+# All three scenarios depend on receiver-side daemon code paths -+# that are only secured on platforms with a working -+# secure_relative_open. The chdir/chmod tests already skip the -+# same set; mirror that. -+case "$(uname -s)" in -+ SunOS|OpenBSD|NetBSD|CYGWIN*) -+ test_skipped "secure_relative_open relies on RESOLVE_BENEATH-equivalent kernel support not available on $(uname -s)" -+ ;; -+esac -+ -+mod="$scratchdir/module" -+outside="$scratchdir/outside" -+src="$scratchdir/src" -+conf="$scratchdir/test-rsyncd.conf" -+ -+# Portable inode-and-mode helpers. -+file_mode() { -+ stat -c %a "$1" 2>/dev/null || stat -f %Lp "$1" -+} -+ -+setup() { -+ rm -rf "$mod" "$outside" "$src" -+ mkdir -p "$mod" "$outside" "$src" -+ -+ echo "OUTSIDE_PROTECTED_DATA" > "$outside/target.txt" -+ chmod 0644 "$outside/target.txt" -+ outside_pristine="$scratchdir/outside-pristine.txt" -+ cp -p "$outside/target.txt" "$outside_pristine" -+ -+ ln -s "$outside" "$mod/cd" -+} -+ -+verify_outside_unchanged() { -+ label="$1" -+ mode=$(file_mode "$outside/target.txt") -+ case "$mode" in -+ 644|0644) ;; -+ *) test_fail "$label: outside/target.txt mode changed from 644 to $mode" ;; -+ esac -+ if ! cmp -s "$outside/target.txt" "$outside_pristine"; then -+ test_fail "$label: outside/target.txt content changed -- daemon followed the cd symlink" -+ fi -+} -+ -+verify_outside_unchanged_or_absent() { -+ label="$1" -+ target="$2" # specific file under outside/ to check absence of -+ if [ -e "$outside/$target" ]; then -+ test_fail "$label: outside/$target was created -- daemon followed the cd symlink" -+ fi -+} -+ -+ -+############################################################ -+# Scenario 3b: --inplace --backup --backup-dir=cd -+# -+# Pre-create module/target.txt so the receiver enters the in-place -+# update path; a backup of the existing content must be made -+# before the update. With --backup-dir=cd, backupptr resolves to -+# "cd/target.txt"; with the bug, robust_unlink and the bare -+# do_open at generator.c:1905 both follow the cd symlink, the -+# unlink deletes outside/target.txt and the create writes the -+# pre-existing module/target.txt content there. -+############################################################ -+ -+setup -+echo "EXISTING_MODULE_DATA" > "$mod/target.txt" -+chmod 0666 "$mod/target.txt" -+echo "NEW_DATA_FROM_SENDER" > "$src/target.txt" -+chmod 0644 "$src/target.txt" -+ -+cat > "$conf" </dev/null 2>&1 || true -+ -+verify_outside_unchanged "3b inplace+backup-dir=cd" -+ -+ -+############################################################ -+# Scenario 3c-symlink: fake-super symlink push to a path with a -+# symlinked parent -+# -+# With "fake super = yes" set on the module, the receiver -+# represents symlinks as fake-super files (regular files with the -+# link target written to them). The path-based open() in -+# do_symlink's fake-super branch follows parent symlinks. We push -+# a single symlink to the destination path "cd/sym" so the -+# receiver's create-file call lands at "cd/sym" relative to the -+# module root, where cd is the symlink trap. -+############################################################ -+ -+setup -+ -+mkdir -p "$src/cd" -+ln -s /etc/passwd "$src/cd/sym" -+ -+cat > "$conf" </dev/null 2>&1 || true -+ -+verify_outside_unchanged_or_absent "3c-symlink fake-super symlink push" "sym" -+ -+ -+############################################################ -+# Scenario 3c-mknod: fake-super FIFO push to a path with a -+# symlinked parent -+# -+# Similar to 3c-symlink but for special files. mkfifo works -+# without root; we push a FIFO and verify the receiver doesn't -+# create a fake-super file at outside/fifo. -+############################################################ -+ -+setup -+ -+mkdir -p "$src/cd" -+mkfifo "$src/cd/fifo" 2>/dev/null -+if [ ! -p "$src/cd/fifo" ]; then -+ test_skipped "mkfifo unavailable; cannot exercise 3c-mknod" -+fi -+ -+cat > "$conf" </dev/null 2>&1 || true -+ -+verify_outside_unchanged_or_absent "3c-mknod fake-super FIFO push" "fifo" -+ -+exit 0 -diff --git a/testsuite/copy-dest-source-symlink.test b/testsuite/copy-dest-source-symlink.test -new file mode 100755 -index 00000000..2d20fab4 ---- /dev/null -+++ b/testsuite/copy-dest-source-symlink.test -@@ -0,0 +1,83 @@ -+#!/bin/sh -+ -+# Copyright (C) 2026 by Andrew Tridgell -+ -+# This program is distributable under the terms of the GNU GPL (see -+# COPYING). -+ -+# Regression test for codex audit Finding 3a: copy_file()'s source -+# open in copy_altdest_file() is via do_open_nofollow(), which only -+# refuses a final-component symlink. Parent components are still -+# resolved with normal symlink-following. A daemon module attacker -+# who plants a parent symlink at module/cd -> /outside, then runs -+# --copy-dest=cd against a source file matching the size+mtime of -+# /outside/target.txt, drives the receiver to: -+# -+# 1. Find a match-level >= 2 basis at "cd/target.txt" -+# 2. Call copy_altdest_file -> copy_file(src="cd/target.txt", ...) -+# 3. do_open_nofollow follows the "cd" parent symlink and reads -+# the contents of /outside/target.txt under the daemon's -+# authority -+# 4. Copy that content into the module destination -+# -+# Result: outside/target.txt content lands at module/target.txt, -+# accessible to the attacker on a subsequent pull. -+# -+# We detect by content: src/target.txt and outside/target.txt have -+# identical metadata (size + mtime + mode) but different content. -+# After the transfer, module/target.txt should match src (no -+# basedir escape) -- if it matches outside, the bug copied across -+# the symlink boundary. -+ -+. "$suitedir/rsync.fns" -+ -+mod="$scratchdir/module" -+outside="$scratchdir/outside" -+src="$scratchdir/src" -+conf="$scratchdir/test-rsyncd.conf" -+ -+rm -rf "$mod" "$outside" "$src" -+mkdir -p "$mod" "$outside" "$src" -+ -+# Outside-the-module file the daemon should not read on the -+# attacker's behalf. -+echo "OUTSIDE_LEAKED_DATA!" > "$outside/target.txt" -+chmod 0644 "$outside/target.txt" -+ -+# The symlink trap. -+ln -s "$outside" "$mod/cd" -+ -+# Source: same size, same mtime, same mode as outside -- so the -+# generator's link_stat + quick_check_ok finds a match-level >= 2 -+# basis and calls copy_altdest_file. -+echo "ATTACKER_KNOWN_DATA!" > "$src/target.txt" -+touch -r "$outside/target.txt" "$src/target.txt" -+chmod 0644 "$src/target.txt" -+ -+cat > "$conf" </dev/null 2>&1 || true -+ -+if [ ! -f "$mod/target.txt" ]; then -+ test_fail "destination file was not created -- daemon transfer failed before the test could observe the basedir behaviour" -+fi -+ -+if cmp -s "$mod/target.txt" "$outside/target.txt"; then -+ test_fail "basedir-escape via copy_file source: module/target.txt now contains the contents of outside/target.txt -- daemon read /outside via the cd symlink and copied it into the module" -+fi -+ -+if ! cmp -s "$mod/target.txt" "$src/target.txt"; then -+ test_fail "destination doesn't match source content (and isn't outside content either): unexpected state" -+fi -+ -+exit 0 -diff --git a/util1.c b/util1.c -index f85f33e9..49ead492 100644 ---- a/util1.c -+++ b/util1.c -@@ -336,7 +336,13 @@ static int unlink_and_reopen(const char *dest, mode_t mode) - mode |= S_IWUSR; - #endif - mode &= INITACCESSPERMS; -- if ((ofd = do_open(dest, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, mode)) < 0) { -+ /* 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; -@@ -360,12 +366,23 @@ static int unlink_and_reopen(const char *dest, mode_t mode) - * --copy-dest options. */ - int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode) - { -+ extern int am_daemon, am_chrooted; - int ifd, ofd; - char buf[1024 * 8]; - int len; /* Number of bytes read into `buf'. */ - OFF_T prealloc_len = 0, offset = 0; - -- if ((ifd = do_open_nofollow(source, O_RDONLY)) < 0) { -+ /* On a daemon without chroot, 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 daemon can -+ * see but the attacker should not. Plain do_open_nofollow only -+ * refuses a final-component symlink; parents are still followed. */ -+ if (am_daemon && !am_chrooted && source && *source && source[0] != '/') -+ ifd = secure_relative_open(NULL, source, O_RDONLY | O_NOFOLLOW, 0); -+ 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; --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0029-testsuite-end-to-end-regression-test-for-chdir-symli.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0029-testsuite-end-to-end-regression-test-for-chdir-symli.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0029-testsuite-end-to-end-regression-test-for-chdir-symli.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0029-testsuite-end-to-end-regression-test-for-chdir-symli.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,289 +0,0 @@ -From 0cac014f894c7a11e3841a7fd7459e90f4b1c0bb Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Tue, 5 May 2026 14:34:50 +1000 -Subject: [PATCH 29/38] testsuite: end-to-end regression test for - chdir-symlink-race - -testsuite/chdir-symlink-race.test runs an actual rsync daemon -(via RSYNC_CONNECT_PROG to avoid the network) configured with -"use chroot = no", plants a symlink at module/subdir -> ../outside, -and runs four flavours of attacker-shaped transfer (single-file -poc_chmod, -r push into the symlinked subdir with --size-only and -without, -r push into the module root). All four must leave the -outside-the-module sentinel file's mode AND content unchanged. - -Portability: - - file_mode() helper falls back to BSD stat -f %Lp when GNU - stat -c %a is unavailable (macOS, FreeBSD). - - Pre-saved pristine copy + cmp(1) replaces sha1sum, which - differs across platforms (sha1sum / shasum / sha1). - -Tests are kept running as root in the user-namespace re-exec -wrapper used by symlink-race tests so the daemon's setuid path -doesn't drop into the test user's identity (which on Linux -would mean the chmod-escape code path can't trigger because -the test user doesn't have CAP_FOWNER over the outside file). - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - testsuite/alt-dest-symlink-race.test | 17 +++ - testsuite/bare-do-open-symlink-race.test | 20 ++++ - testsuite/chdir-symlink-race.test | 135 +++++++++++++++++++++++ - testsuite/copy-dest-source-symlink.test | 15 +++ - 4 files changed, 187 insertions(+) - create mode 100755 testsuite/chdir-symlink-race.test - -diff --git a/testsuite/alt-dest-symlink-race.test b/testsuite/alt-dest-symlink-race.test -index 2256f2f2..fd36c6e6 100755 ---- a/testsuite/alt-dest-symlink-race.test -+++ b/testsuite/alt-dest-symlink-race.test -@@ -62,8 +62,25 @@ echo "OUTSIDE_SECRET_DATA" > "$src/target.txt" - touch -r "$outside/target.txt" "$src/target.txt" - chmod 0644 "$src/target.txt" - -+# When running as root the daemon would drop to "nobody" by -+# default, which can't write into the test scratch dir. Force the -+# daemon to keep our uid/gid in that case so the basis-link -+# transfer can actually create the destination file. (Non-root -+# can't specify uid/gid in rsyncd.conf -- comment them out then.) -+my_uid=`get_testuid` -+root_uid=`get_rootuid` -+root_gid=`get_rootgid` -+uid_setting="uid = $root_uid" -+gid_setting="gid = $root_gid" -+if test x"$my_uid" != x"$root_uid"; then -+ uid_setting="#$uid_setting" -+ gid_setting="#$gid_setting" -+fi -+ - cat > "$conf" < "$conf" < "$conf" < "$conf" < ../outside, and runs four flavours of -+# rsync transfer that previously all reached files in ../outside: -+# -+# 1. single-file dest = subdir/target.txt (the original poc_chmod) -+# 2. -r src/subdir/ to upload/subdir/ (the chdir-escape case) -+# 3. -r src/subdir/ to upload/subdir/ (no --size-only: forces basis read+write) -+# 4. -r src/ to upload/ (was already protected by the -+# original CVE-2026-29518 fix; -+# regression-checked here) -+# -+# All four must leave the outside-the-module sentinel file's mode AND -+# content unchanged. -+ -+. "$suitedir/rsync.fns" -+ -+case "$(uname -s)" in -+ SunOS|OpenBSD|NetBSD|CYGWIN*) -+ test_skipped "secure chdir relies on RESOLVE_BENEATH-equivalent kernel support not available on $(uname -s)" -+ ;; -+esac -+ -+mod="$scratchdir/module" -+outside="$scratchdir/outside" -+src="$scratchdir/src" -+conf="$scratchdir/test-rsyncd.conf" -+ -+rm -rf "$mod" "$outside" "$src" -+mkdir -p "$mod" "$outside" "$src" "$src/subdir" -+ -+# Portable octal-mode helper -- macOS and FreeBSD's stat use -f, GNU -+# coreutils stat uses -c. -+file_mode() { -+ stat -c %a "$1" 2>/dev/null || stat -f %Lp "$1" -+} -+ -+# The "secret" file outside the module the attacker is trying to alter. -+# Save a pristine copy alongside it so we can compare with cmp(1) rather -+# than depending on sha1sum/shasum/sha1, which differ across platforms. -+echo "OUTSIDE_SECRET_DATA" > "$outside/target.txt" -+chmod 0600 "$outside/target.txt" -+outside_pristine="$scratchdir/outside-pristine.txt" -+cp -p "$outside/target.txt" "$outside_pristine" -+ -+# Symlink trap planted in the module by the local attacker. -+ln -s "$outside" "$mod/subdir" -+ -+# Source files the sender will push: same size as the outside target, -+# different content, mode 0666 (the perms the attacker tries to push). -+SIZE=$(stat -c %s "$outside/target.txt" 2>/dev/null \ -+ || stat -f %z "$outside/target.txt") -+head -c "$SIZE" /dev/urandom > "$src/target.txt" -+head -c "$SIZE" /dev/urandom > "$src/subdir/target.txt" -+chmod 0666 "$src/target.txt" "$src/subdir/target.txt" -+ -+cat > "$conf" < "$outside/target.txt" -+} -+ -+verify_unchanged() { -+ label="$1" -+ mode=$(file_mode "$outside/target.txt") -+ case "$mode" in -+ 600|0600) ;; -+ *) test_fail "$label: outside file mode changed from 600 to $mode (chmod escape)" ;; -+ esac -+ if ! cmp -s "$outside/target.txt" "$outside_pristine"; then -+ test_fail "$label: outside file content changed (write escape)" -+ fi -+} -+ -+run_attack() { -+ label="$1"; shift -+ reset_outside -+ RSYNC_CONNECT_PROG="$RSYNC --config=$conf --daemon" \ -+ $RSYNC "$@" >/dev/null 2>&1 || true -+ verify_unchanged "$label" -+} -+ -+# 1. The original poc_chmod scenario: single file, dest path with -+# the symlinked subdir as a path component. With --size-only the -+# receiver normally skips the basis open and goes straight to chmod -+# -- only the chdir-escape blocks the chmod from reaching outside. -+run_attack "single-file --size-only" \ -+ -tp --size-only \ -+ "$src/target.txt" rsync://localhost/upload/subdir/target.txt -+ -+# 2. -r push into the symlinked subdir: receiver chdir's into "subdir", -+# follows the symlink, ends up in outside. -+run_attack "-r --size-only into subdir/" \ -+ -rtp --size-only \ -+ "$src/subdir/" rsync://localhost/upload/subdir/ -+ -+# 3. Same but no --size-only -- forces the basis-file open and a real -+# rename, so this exercises the read-disclosure and write-escape -+# paths together. -+run_attack "-r without --size-only into subdir/" \ -+ -rtp \ -+ "$src/subdir/" rsync://localhost/upload/subdir/ -+ -+# 4. -r src/ to upload/ -- this case was already covered by the -+# original CVE-2026-29518 fix because the receiver stays at module -+# root and operates on slashed paths. Regression check. -+run_attack "-r --size-only into upload/ root" \ -+ -rtp --size-only \ -+ "$src/" rsync://localhost/upload/ -+ -+exit 0 -diff --git a/testsuite/copy-dest-source-symlink.test b/testsuite/copy-dest-source-symlink.test -index 2d20fab4..f91ee986 100755 ---- a/testsuite/copy-dest-source-symlink.test -+++ b/testsuite/copy-dest-source-symlink.test -@@ -54,8 +54,23 @@ echo "ATTACKER_KNOWN_DATA!" > "$src/target.txt" - touch -r "$outside/target.txt" "$src/target.txt" - chmod 0644 "$src/target.txt" - -+# When running as root the daemon would drop to "nobody" by -+# default and fail to mkstemp in the scratch dir; force it to -+# keep our uid/gid in that case. -+my_uid=`get_testuid` -+root_uid=`get_rootuid` -+root_gid=`get_rootgid` -+uid_setting="uid = $root_uid" -+gid_setting="gid = $root_gid" -+if test x"$my_uid" != x"$root_uid"; then -+ uid_setting="#$uid_setting" -+ gid_setting="#$gid_setting" -+fi -+ - cat > "$conf" < -Date: Wed, 29 Apr 2026 11:10:59 +1000 -Subject: [PATCH 30/38] token: harden compressed-token decoding against integer - overflow - -The receiver's three compressed-token decoders -- -recv_deflated_token (zlib), recv_zstd_token, and -recv_compressed_token (lz4) -- accumulated rx_token (a 32-bit -signed counter) without overflow checking. A malicious sender -could craft a compressed-token stream that walked rx_token past -INT32_MAX, with careful manipulation leaking process memory -contents to the wire (environment variables, passwords, heap -pointers, library pointers -- significantly weakening ASLR -and facilitating further exploitation). - -Cap rx_token at MAX_TOKEN_INDEX = 0x7ffffffe. Fold the -bookkeeping into recv_compressed_token_num() and -recv_compressed_token_run() shared by all three decoders. Reject -negative or out-of-range token values explicitly. Also cap the -simple_recv_token literal-block length at the source: any -wire-supplied length > CHUNK_SIZE is ill-formed (the matching -simple_send_token never writes a chunk larger than CHUNK_SIZE), -so reject before looping on attacker-controlled bytes. - -Reach: an authenticated daemon connection with compression -enabled (the default for protocols >= 30 when both peers -advertise it). Disabling compression on the daemon -("refuse options = compress" in rsyncd.conf) is the available -workaround. - -Reporter: Omar Elsayed (seks99x). - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - receiver.c | 11 ++++- - token.c | 117 ++++++++++++++++++++++++++--------------------------- - 2 files changed, 67 insertions(+), 61 deletions(-) - -diff --git a/receiver.c b/receiver.c -index 8cf8366b..a487ad5a 100644 ---- a/receiver.c -+++ b/receiver.c -@@ -318,7 +318,12 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r, - } - } - -- while ((i = recv_token(f_in, &data)) != 0) { -+ while (1) { -+ data = NULL; -+ i = recv_token(f_in, &data); -+ if (i == 0) -+ break; -+ - if (INFO_GTE(PROGRESS, 1)) - show_progress(offset, total_size); - -@@ -326,6 +331,10 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r, - maybe_send_keepalive(time(NULL), MSK_ALLOW_FLUSH | MSK_ACTIVE_RECEIVER); - - if (i > 0) { -+ if (!data) { -+ rprintf(FERROR, "Invalid literal token with no data [%s]\n", who_am_i()); -+ exit_cleanup(RERR_PROTOCOL); -+ } - if (DEBUG_GTE(DELTASUM, 3)) { - rprintf(FINFO,"data recv %d at %s\n", - i, big_num(offset)); -diff --git a/token.c b/token.c -index b7a02ea1..02dabd8d 100644 ---- a/token.c -+++ b/token.c -@@ -291,6 +291,14 @@ static int32 simple_recv_token(int f, char **data) - int32 i = read_int(f); - if (i <= 0) - return i; -+ /* simple_send_token caps each literal chunk at CHUNK_SIZE; -+ * reject anything larger so a hostile peer cannot drive the -+ * read_buf below past our static CHUNK_SIZE buffer. */ -+ if (i > CHUNK_SIZE) { -+ rprintf(FERROR, "invalid uncompressed token length %ld [%s]\n", -+ (long)i, who_am_i()); -+ exit_cleanup(RERR_PROTOCOL); -+ } - residue = i; - } - -@@ -493,9 +501,52 @@ static char *cbuf; - static char *dbuf; - - /* for decoding runs of tokens */ -+#define MAX_TOKEN_INDEX ((int32)0x7ffffffe) -+ - static int32 rx_token; - static int32 rx_run; - -+static NORETURN void invalid_compressed_token(void) -+{ -+ rprintf(FERROR, "invalid token number in compressed stream\n"); -+ exit_cleanup(RERR_PROTOCOL); -+} -+ -+static int32 recv_compressed_token_num(int f, int32 flag) -+{ -+ if (flag & TOKEN_REL) { -+ int32 incr = flag & 0x3f; -+ if (rx_token > MAX_TOKEN_INDEX - incr) -+ invalid_compressed_token(); -+ rx_token += incr; -+ flag >>= 6; -+ } else { -+ rx_token = read_int(f); -+ if (rx_token < 0 || rx_token > MAX_TOKEN_INDEX) -+ invalid_compressed_token(); -+ } -+ -+ if (flag & 1) { -+ rx_run = read_byte(f); -+ rx_run += read_byte(f) << 8; -+ if (rx_run <= 0 || rx_token > MAX_TOKEN_INDEX - rx_run) -+ invalid_compressed_token(); -+ recv_state = r_running; -+ } -+ -+ return -1 - rx_token; -+} -+ -+static int32 recv_compressed_token_run(void) -+{ -+ if (rx_run <= 0 || rx_token >= MAX_TOKEN_INDEX) -+ invalid_compressed_token(); -+ ++rx_token; -+ if (--rx_run == 0) -+ recv_state = r_idle; -+ return -1 - rx_token; -+} -+ - /* Receive a deflated token and inflate it */ - static int32 recv_deflated_token(int f, char **data) - { -@@ -586,22 +637,7 @@ static int32 recv_deflated_token(int f, char **data) - } - - /* here we have a token of some kind */ -- if (flag & TOKEN_REL) { -- rx_token += flag & 0x3f; -- flag >>= 6; -- } else { -- rx_token = read_int(f); -- if (rx_token < 0) { -- rprintf(FERROR, "invalid token number in compressed stream\n"); -- exit_cleanup(RERR_PROTOCOL); -- } -- } -- if (flag & 1) { -- rx_run = read_byte(f); -- rx_run += read_byte(f) << 8; -- recv_state = r_running; -- } -- return -1 - rx_token; -+ return recv_compressed_token_num(f, flag); - - case r_inflating: - rx_strm.next_out = (Bytef *)dbuf; -@@ -621,10 +657,7 @@ static int32 recv_deflated_token(int f, char **data) - break; - - case r_running: -- ++rx_token; -- if (--rx_run == 0) -- recv_state = r_idle; -- return -1 - rx_token; -+ return recv_compressed_token_run(); - } - } - } -@@ -833,22 +866,7 @@ static int32 recv_zstd_token(int f, char **data) - return 0; - } - /* here we have a token of some kind */ -- if (flag & TOKEN_REL) { -- rx_token += flag & 0x3f; -- flag >>= 6; -- } else { -- rx_token = read_int(f); -- if (rx_token < 0) { -- rprintf(FERROR, "invalid token number in compressed stream\n"); -- exit_cleanup(RERR_PROTOCOL); -- } -- } -- if (flag & 1) { -- rx_run = read_byte(f); -- rx_run += read_byte(f) << 8; -- recv_state = r_running; -- } -- return -1 - rx_token; -+ return recv_compressed_token_num(f, flag); - - case r_inflated: /* zstd doesn't get into this state */ - break; -@@ -879,10 +897,7 @@ static int32 recv_zstd_token(int f, char **data) - break; - - case r_running: -- ++rx_token; -- if (--rx_run == 0) -- recv_state = r_idle; -- return -1 - rx_token; -+ return recv_compressed_token_run(); - } - } - } -@@ -1002,22 +1017,7 @@ static int32 recv_compressed_token(int f, char **data) - } - - /* here we have a token of some kind */ -- if (flag & TOKEN_REL) { -- rx_token += flag & 0x3f; -- flag >>= 6; -- } else { -- rx_token = read_int(f); -- if (rx_token < 0) { -- rprintf(FERROR, "invalid token number in compressed stream\n"); -- exit_cleanup(RERR_PROTOCOL); -- } -- } -- if (flag & 1) { -- rx_run = read_byte(f); -- rx_run += read_byte(f) << 8; -- recv_state = r_running; -- } -- return -1 - rx_token; -+ return recv_compressed_token_num(f, flag); - - case r_inflating: - avail_out = LZ4_decompress_safe(next_in, dbuf, avail_in, size); -@@ -1033,10 +1033,7 @@ static int32 recv_compressed_token(int f, char **data) - break; - - case r_running: -- ++rx_token; -- if (--rx_run == 0) -- recv_state = r_idle; -- return -1 - rx_token; -+ return recv_compressed_token_run(); - } - } - } --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0031-testsuite-cover-refuse-options-compress-for-the-daem.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0031-testsuite-cover-refuse-options-compress-for-the-daem.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0031-testsuite-cover-refuse-options-compress-for-the-daem.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0031-testsuite-cover-refuse-options-compress-for-the-daem.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,81 +0,0 @@ -From 275258bd76bfaacf58fc647bd7b9af6117280fa9 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Fri, 1 May 2026 10:56:17 +1000 -Subject: [PATCH 31/38] testsuite: cover 'refuse options = compress' for the - daemon - -Add a daemon-refuse-compress test that builds a module configured with -'refuse options = compress' and asserts that: - 1. an attempted -z transfer to that module fails with an error - mentioning --compress, and - 2. the same transfer without -z still succeeds. - -This pins down the documented way to disable all compression on a -daemon, which previously had no automated coverage. - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - testsuite/daemon-refuse-compress.test | 51 +++++++++++++++++++++++++++ - 1 file changed, 51 insertions(+) - create mode 100644 testsuite/daemon-refuse-compress.test - -diff --git a/testsuite/daemon-refuse-compress.test b/testsuite/daemon-refuse-compress.test -new file mode 100644 -index 00000000..a24e50d1 ---- /dev/null -+++ b/testsuite/daemon-refuse-compress.test -@@ -0,0 +1,51 @@ -+#!/bin/sh -+ -+# Copyright (C) 2026 by Andrew Tridgell -+ -+# This program is distributable under the terms of the GNU GPL (see -+# COPYING). -+ -+# Test that a daemon module configured with "refuse options = compress" -+# rejects clients that ask for compression and still serves the same -+# transfer when the client does not. -+ -+. "$suitedir/rsync.fns" -+ -+build_rsyncd_conf -+ -+# Append a module that refuses --compress (-z). -+cat >>"$conf" </dev/null 2>"$errlog"; then -+ cat "$errlog" >&2 -+ test_fail "compressed transfer was not refused" -+fi -+ -+grep -- '--compress' "$errlog" >/dev/null || { -+ cat "$errlog" >&2 -+ test_fail "expected refuse error mentioning --compress" -+} -+ -+# The same transfer without -z must succeed. -+rm -rf "$todir" -+mkdir "$todir" -+checkit "$RSYNC -av localhost::no-compress/ '$todir/'" "$chkdir" "$todir" -+ -+# The script would have aborted on error, so getting here means we've won. -+exit 0 --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0032-receiver-add-parent_ndx-0-guard-mirroring-797e17f.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0032-receiver-add-parent_ndx-0-guard-mirroring-797e17f.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0032-receiver-add-parent_ndx-0-guard-mirroring-797e17f.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0032-receiver-add-parent_ndx-0-guard-mirroring-797e17f.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,119 +0,0 @@ -From 5564c88150087eef6437e6c7aca00ed2593ca26f Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Tue, 5 May 2026 16:48:16 +1000 -Subject: [PATCH 32/38] receiver: add parent_ndx<0 guard, mirroring 797e17f - -Commit 797e17f ("fixed an invalid access to files array") added a -parent_ndx < 0 guard to send_files() in sender.c, but the visually- -identical block in recv_files() in receiver.c was not updated. A -malicious rsync:// server can therefore drive any connecting client -into the same out-of-bounds dir_flist->files[-1] read followed by a -file_struct dereference in f_name() one line later. - -Reach: protocol-30+ default (inc_recurse) makes flist.c:2745 set -parent_ndx = -1 on the first received flist when the sender omits a -leading "." entry; rsync.c flist_for_ndx() does not reject ndx == 0 -in that state because the range check evaluates 0 < 0 = false; and -read_ndx_and_attrs() only validates ndx with the ITEM_TRANSFER bit -set, so iflags=ITEM_IS_NEW (or any other non-transfer iflag word) -bypasses the check. - -Apply the same guard receiver-side. Confirmed: the same PoC (a -minimal Python rsyncd that handshakes with CF_INC_RECURSE, sends a -no-leading-"." flist, and emits ndx=0 with ITEM_IS_NEW) crashes -unpatched 3.4.2 with SEGV_MAPERR si_addr=0x4101a-class in the -receiver child; with this guard it exits cleanly with code 2 -(RERR_PROTOCOL). - -The attack surface delta over the sender variant is large: -the original was malicious-client -> daemon, this is -malicious-server -> any rsync client doing a normal rsync:// -or remote-shell pull. - -Reported by Pratham Gupta (alchemy1729). - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - generator.c | 4 ++++ - io.c | 3 +++ - receiver.c | 7 ++++++- - sender.c | 2 ++ - 4 files changed, 15 insertions(+), 1 deletion(-) - -diff --git a/generator.c b/generator.c -index 311e9b78..89f99db4 100644 ---- a/generator.c -+++ b/generator.c -@@ -2146,6 +2146,8 @@ void check_for_finished_files(int itemizing, enum logcode code, int check_redo) - if (send_failed) - ndx = get_hlink_num(); - flist = flist_for_ndx(ndx, "check_for_finished_files.1"); -+ if (ndx < flist->ndx_start) -+ exit_cleanup(RERR_PROTOCOL); - file = flist->files[ndx - flist->ndx_start]; - assert(file->flags & FLAG_HLINKED); - if (send_failed) -@@ -2174,6 +2176,8 @@ void check_for_finished_files(int itemizing, enum logcode code, int check_redo) - - flist = cur_flist; - cur_flist = flist_for_ndx(ndx, "check_for_finished_files.2"); -+ if (ndx < cur_flist->ndx_start) -+ exit_cleanup(RERR_PROTOCOL); - - file = cur_flist->files[ndx - cur_flist->ndx_start]; - if (solo_file) -diff --git a/io.c b/io.c -index 8d1cf7f2..2d94c1f4 100644 ---- a/io.c -+++ b/io.c -@@ -1090,6 +1090,9 @@ static void got_flist_entry_status(enum festatus status, int ndx) - { - struct file_list *flist = flist_for_ndx(ndx, "got_flist_entry_status"); - -+ if (ndx < flist->ndx_start) -+ exit_cleanup(RERR_PROTOCOL); -+ - if (remove_source_files) { - active_filecnt--; - active_bytecnt -= F_LENGTH(flist->files[ndx - flist->ndx_start]); -diff --git a/receiver.c b/receiver.c -index a487ad5a..3fa68d71 100644 ---- a/receiver.c -+++ b/receiver.c -@@ -467,7 +467,10 @@ static void handle_delayed_updates(char *local_name) - static void no_batched_update(int ndx, BOOL is_redo) - { - struct file_list *flist = flist_for_ndx(ndx, "no_batched_update"); -- struct file_struct *file = flist->files[ndx - flist->ndx_start]; -+ struct file_struct *file; -+ if (ndx < flist->ndx_start) -+ exit_cleanup(RERR_PROTOCOL); -+ file = flist->files[ndx - flist->ndx_start]; - - rprintf(FERROR_XFER, "(No batched update for%s \"%s\")\n", - is_redo ? " resend of" : "", f_name(file, NULL)); -@@ -604,6 +607,8 @@ int recv_files(int f_in, int f_out, char *local_name) - - if (ndx - cur_flist->ndx_start >= 0) - file = cur_flist->files[ndx - cur_flist->ndx_start]; -+ else if (cur_flist->parent_ndx < 0) -+ exit_cleanup(RERR_PROTOCOL); - else - file = dir_flist->files[cur_flist->parent_ndx]; - fname = local_name ? local_name : f_name(file, fbuf); -diff --git a/sender.c b/sender.c -index 99f431fe..033f87e5 100644 ---- a/sender.c -+++ b/sender.c -@@ -140,6 +140,8 @@ void successful_send(int ndx) - return; - - flist = flist_for_ndx(ndx, "successful_send"); -+ if (ndx < flist->ndx_start) -+ exit_cleanup(RERR_PROTOCOL); - file = flist->files[ndx - flist->ndx_start]; - if (!change_pathname(file, NULL, 0)) - return; --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0033-clientserver-fix-hostname-ACL-bypass-when-using-daem.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0033-clientserver-fix-hostname-ACL-bypass-when-using-daem.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0033-clientserver-fix-hostname-ACL-bypass-when-using-daem.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0033-clientserver-fix-hostname-ACL-bypass-when-using-daem.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,192 +0,0 @@ -From ac735929dc577e1b0da3b263d47df5a35b891f2e Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Wed, 31 Dec 2025 13:50:35 +1100 -Subject: [PATCH 33/38] clientserver: fix hostname ACL bypass when using daemon - chroot - -On an rsync daemon configured with "daemon chroot", the reverse-DNS -lookup of the connecting client was performed *after* the chroot -had been entered. If the chroot did not contain the files glibc -needs for resolution (/etc/resolv.conf, /etc/nsswitch.conf, -/etc/hosts, NSS service modules), the lookup failed and -client_name() returned "UNKNOWN". Hostname-based deny rules -("hosts deny = *.evil.example") therefore could not match, and -an attacker controlling their PTR record could connect from a -hostname the administrator had intended to deny. IP-based ACLs -were unaffected. - -Do the reverse DNS lookup before chroot/setuid; client_name() -caches its result, so the post-chroot call uses the cached value -and hostname-based ACLs work even when DNS is unavailable -post-chroot. - -Adds testsuite/daemon-chroot-acl.test as end-to-end regression -coverage. The test sets up an empty chroot directory, configures -"hosts deny = " with daemon chroot, and -asserts the connection is refused with @ERROR access denied. -Uses unshare --user --map-root-user for non-root CAP_SYS_CHROOT; -skips cleanly on non-Linux or when user namespaces aren't -available. - -Reporter: Joshua Rogers (MegaManSec). - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - clientserver.c | 22 ++++++ - testsuite/daemon-chroot-acl.test | 111 +++++++++++++++++++++++++++++++ - 2 files changed, 133 insertions(+) - create mode 100644 testsuite/daemon-chroot-acl.test - -diff --git a/clientserver.c b/clientserver.c -index e8dfddb1..14daba3c 100644 ---- a/clientserver.c -+++ b/clientserver.c -@@ -1312,6 +1312,28 @@ int start_daemon(int f_in, int f_out) - if (lp_proxy_protocol() && !read_proxy_protocol_header(f_in)) - return -1; - -+ /* Do reverse DNS lookup before chroot/setuid. The result is cached, -+ * so the later client_name() call will use this cached value. This -+ * ensures hostname-based ACLs work even when DNS is unavailable -+ * after chroot. -+ * -+ * "reverse lookup" can be set globally OR per-module, so we also -+ * scan each module: a deployment with "reverse lookup = no" in the -+ * global section but "reverse lookup = yes" in a specific module -+ * still triggers a post-chroot lookup at access-check time -+ * (rsync_module() in this file), which would also fail in the -+ * chroot and turn hostname-based deny rules into silent bypasses. */ -+ { -+ int need_reverse = lp_reverse_lookup(-1); -+ int j, num_modules = lp_num_modules(); -+ for (j = 0; !need_reverse && j < num_modules; j++) { -+ if (lp_reverse_lookup(j)) -+ need_reverse = 1; -+ } -+ if (need_reverse) -+ (void)client_name(client_addr(f_in)); -+ } -+ - p = lp_daemon_chroot(); - if (*p) { - log_init(0); /* Make use we've initialized syslog before chrooting. */ -diff --git a/testsuite/daemon-chroot-acl.test b/testsuite/daemon-chroot-acl.test -new file mode 100644 -index 00000000..9d1c1b63 ---- /dev/null -+++ b/testsuite/daemon-chroot-acl.test -@@ -0,0 +1,111 @@ -+#!/bin/sh -+ -+# Copyright (C) 2026 by Andrew Tridgell -+ -+# This program is distributable under the terms of the GNU GPL (see -+# COPYING). -+ -+# Regression test for GHSA-rjfm-3w2m-jf4f: a hostname-based "hosts deny" -+# rule must still match when the daemon performs a 'daemon chroot' and -+# the chroot does not contain the NSS files glibc needs for reverse DNS. -+# -+# Pre-fix, reverse DNS happened *after* the daemon chroot. With an empty -+# chroot the NSS lookup failed, client_name() returned "UNKNOWN", and a -+# deny rule referring to the connecting hostname silently failed to -+# match. -+# -+# Two scenarios are exercised so we can distinguish the case the fix -+# definitely covers from the per-module path that may still be -+# vulnerable: -+# A. global "reverse lookup = yes" (covered by b6abdb4c) -+# B. only module "reverse lookup = yes" (gap to verify) -+ -+. "$suitedir/rsync.fns" -+ -+case `uname -s` in -+Linux*) ;; -+*) test_skipped "test is Linux-specific (uses chroot+unshare)" ;; -+esac -+ -+# We need CAP_SYS_CHROOT. Re-exec under a user namespace if not root. -+if ! chroot / /bin/true 2>/dev/null; then -+ if [ -z "$RSYNC_UNSHARED" ] && unshare --user --map-root-user true 2>/dev/null; then -+ echo "Re-running under unshare --user --map-root-user..." -+ RSYNC_UNSHARED=1 exec unshare --user --map-root-user "$SHELL_PATH" $RUNSHFLAGS "$0" -+ fi -+ test_skipped "need CAP_SYS_CHROOT (root or unshare --user --map-root-user)" -+fi -+ -+# We need 127.0.0.1 to reverse-resolve to a real hostname while NSS is -+# still working (i.e. before the daemon's chroot). The daemon will -+# look that name up itself as part of its hostname-based ACL check; -+# we then deny that name and assert the connection is rejected. -+client_hostname=`getent hosts 127.0.0.1 2>/dev/null | awk 'NR==1 {print $2}'` -+if [ -z "$client_hostname" ] || [ "$client_hostname" = "127.0.0.1" ]; then -+ test_skipped "no reverse DNS for 127.0.0.1" -+fi -+ -+chrootdir="$scratchdir/chroot" -+rm -rf "$chrootdir" -+mkdir -p "$chrootdir/modroot" -+echo "from chroot" > "$chrootdir/modroot/file1" -+ -+conf="$scratchdir/test-rsyncd.conf" -+logfile="$scratchdir/rsyncd.log" -+ -+write_conf() { -+ cat >"$conf" <"$out" 2>&1 -+ rc=$? -+ -+ echo "----- $label (rsync exit $rc):" -+ cat "$out" -+ echo "----- daemon log:" -+ [ -f "$logfile" ] && cat "$logfile" -+ echo "-----" -+ -+ grep -q '@ERROR.*access denied' "$out" -+} -+ -+# Scenario A: global reverse lookup. Covered by b6abdb4c. -+write_conf yes yes -+if ! run_check "Scenario A (global reverse lookup = yes)"; then -+ test_fail "Scenario A: hostname deny rule was bypassed" -+fi -+ -+# Scenario B: only the per-module reverse-lookup setting is enabled. -+# The b6abdb4c fix only pre-warms client_name()'s cache when the -+# global setting is on, so the post-chroot lookup in this path may -+# still produce "UNKNOWN" and bypass the deny rule. -+write_conf no yes -+if ! run_check "Scenario B (per-module reverse lookup only)"; then -+ test_fail "Scenario B: hostname deny rule was bypassed (per-module reverse lookup with daemon chroot still has the bypass)" -+fi -+ -+exit 0 --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0034-defence-in-depth-bound-wire-supplied-counts-and-leng.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0034-defence-in-depth-bound-wire-supplied-counts-and-leng.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0034-defence-in-depth-bound-wire-supplied-counts-and-leng.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0034-defence-in-depth-bound-wire-supplied-counts-and-leng.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,261 +0,0 @@ -From ddd7b59a4f0492d957b4a4f0c318e7d81da31c6b Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Wed, 31 Dec 2025 12:56:54 +1100 -Subject: [PATCH 34/38] defence-in-depth: bound wire-supplied counts and - lengths - -Multiple receiver-side fields read from the wire were trusted -without upper-bound checks. A hostile peer could either request -extreme allocations (DoS via --max-alloc) or, on platforms where -read_varint returned a negative value, push ~SIZE_MAX through the -size_t conversion to wrap downstream length checks. - -Introduce read_int_bounded(), read_varint_bounded() and -read_varint_size() in io.c so wire-derived integer ranges are -checked at the read site rather than scattered across each -caller, with RERR_PROTOCOL on out-of-range input. - -Apply the bounded primitives to: - - sum->count (checksum count -- previously could overflow - (size_t)count * xfer_sum_len on 32-bit with raised max-alloc) - - xattrs: count, name_len, datum_len, plus rel_pos overflow - detect to stop chain wrapping the num accumulator - - acls: ida-entry count - - flist: file mode S_IFMT validation, modtime_nsec range check - - delete-stat counters in main: per-summand cap so the total - can't overflow a signed 32-bit accumulator - -Reporters include Joshua Rogers (checksum-count overflow finding). - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - acls.c | 2 +- - flist.c | 17 ++++++++++++++--- - io.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ - main.c | 10 +++++----- - rsync.h | 17 +++++++++++++++++ - xattrs.c | 16 ++++++++++++---- - 6 files changed, 103 insertions(+), 13 deletions(-) - -diff --git a/acls.c b/acls.c -index 4d67ff4d..c60a7087 100644 ---- a/acls.c -+++ b/acls.c -@@ -697,7 +697,7 @@ static uint32 recv_acl_access(int f, uchar *name_follows_ptr) - static uchar recv_ida_entries(int f, ida_entries *ent) - { - uchar computed_mask_bits = 0; -- int i, count = read_varint(f); -+ int i, count = read_varint_bounded(f, 0, MAX_WIRE_ACL_COUNT, "ACL count"); - - ent->idas = count ? new_array(id_access, count) : NULL; - ent->count = count; -diff --git a/flist.c b/flist.c -index 17832533..30eeada6 100644 ---- a/flist.c -+++ b/flist.c -@@ -840,9 +840,9 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x - } - if (xflags & XMIT_MOD_NSEC) - #ifndef CAN_SET_NSEC -- (void)read_varint(f); -+ (void)read_varint_bounded(f, 0, MAX_WIRE_NSEC, "modtime_nsec"); - #else -- modtime_nsec = read_varint(f); -+ modtime_nsec = read_varint_bounded(f, 0, MAX_WIRE_NSEC, "modtime_nsec"); - else - modtime_nsec = 0; - #endif -@@ -861,8 +861,19 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x - #endif - } - #endif -- if (!(xflags & XMIT_SAME_MODE)) -+ if (!(xflags & XMIT_SAME_MODE)) { - mode = from_wire_mode(read_int(f)); -+ /* Reject modes whose type bits are not one of the standard -+ * file types; otherwise garbage mode values propagate through -+ * the file-type checks below unpredictably. */ -+ if (!S_ISREG(mode) && !S_ISDIR(mode) && !S_ISLNK(mode) -+ && !S_ISCHR(mode) && !S_ISBLK(mode) -+ && !S_ISFIFO(mode) && !S_ISSOCK(mode)) { -+ rprintf(FERROR, "invalid file mode 0%o for %s [%s]\n", -+ (unsigned)mode, lastname, who_am_i()); -+ exit_cleanup(RERR_PROTOCOL); -+ } -+ } - if (atimes_ndx && !S_ISDIR(mode) && !(xflags & XMIT_SAME_ATIME)) { - atime = read_varlong(f, 4); - #if SIZEOF_TIME_T < SIZEOF_INT64 -diff --git a/io.c b/io.c -index 2d94c1f4..eb316383 100644 ---- a/io.c -+++ b/io.c -@@ -1868,6 +1868,45 @@ int64 read_varlong(int f, uchar min_bytes) - return u.x; - } - -+/* Read an int32 and verify lo <= v <= hi. On out-of-range, abort with a -+ * protocol error naming "what". The bound is co-located with the read so it -+ * cannot be forgotten by a downstream user. */ -+int32 read_int_bounded(int f, int32 lo, int32 hi, const char *what) -+{ -+ int32 v = read_int(f); -+ if (v < lo || v > hi) { -+ rprintf(FERROR, "wire value %s out of range: %ld not in [%ld,%ld] [%s]\n", -+ what, (long)v, (long)lo, (long)hi, who_am_i()); -+ exit_cleanup(RERR_PROTOCOL); -+ } -+ return v; -+} -+ -+/* As read_int_bounded but for varint-encoded values. */ -+int32 read_varint_bounded(int f, int32 lo, int32 hi, const char *what) -+{ -+ int32 v = read_varint(f); -+ if (v < lo || v > hi) { -+ rprintf(FERROR, "wire value %s out of range: %ld not in [%ld,%ld] [%s]\n", -+ what, (long)v, (long)lo, (long)hi, who_am_i()); -+ exit_cleanup(RERR_PROTOCOL); -+ } -+ return v; -+} -+ -+/* Read a varint that will be used as a size_t. Rejects negative values -+ * (which would wrap to ~SIZE_MAX) and values exceeding the supplied max. */ -+size_t read_varint_size(int f, size_t max, const char *what) -+{ -+ int32 v = read_varint(f); -+ if (v < 0 || (size_t)v > max) { -+ rprintf(FERROR, "wire size %s out of range: %ld > %lu [%s]\n", -+ what, (long)v, (unsigned long)max, who_am_i()); -+ exit_cleanup(RERR_PROTOCOL); -+ } -+ return (size_t)v; -+} -+ - int64 read_longint(int f) - { - #if SIZEOF_INT64 >= 8 -@@ -1974,6 +2013,21 @@ void read_sum_head(int f, struct sum_struct *sum) - (long)sum->count, who_am_i()); - exit_cleanup(RERR_PROTOCOL); - } -+ /* Guard against integer overflow in downstream allocations sized by -+ * count*element_size. my_alloc uses divide-not-multiply so it is -+ * already wraparound-safe, but checking here gives a clearer error -+ * and also covers the (size_t)count * xfer_sum_len arithmetic that -+ * is performed *before* reaching my_alloc. */ -+ if (xfer_sum_len > 0 && (size_t)sum->count > SIZE_MAX / (size_t)xfer_sum_len) { -+ rprintf(FERROR, "Invalid checksum count %ld (too large) [%s]\n", -+ (long)sum->count, who_am_i()); -+ exit_cleanup(RERR_PROTOCOL); -+ } -+ if ((size_t)sum->count > SIZE_MAX / sizeof(struct sum_buf)) { -+ rprintf(FERROR, "Invalid checksum count %ld (sum_buf overflow) [%s]\n", -+ (long)sum->count, who_am_i()); -+ exit_cleanup(RERR_PROTOCOL); -+ } - sum->blength = read_int(f); - if (sum->blength < 0 || sum->blength > max_blength) { - rprintf(FERROR, "Invalid block length %ld [%s]\n", -diff --git a/main.c b/main.c -index 4f070acc..e6dc134c 100644 ---- a/main.c -+++ b/main.c -@@ -239,11 +239,11 @@ void write_del_stats(int f) - - void read_del_stats(int f) - { -- stats.deleted_files = read_varint(f); -- stats.deleted_files += stats.deleted_dirs = read_varint(f); -- stats.deleted_files += stats.deleted_symlinks = read_varint(f); -- stats.deleted_files += stats.deleted_devices = read_varint(f); -- stats.deleted_files += stats.deleted_specials = read_varint(f); -+ stats.deleted_files = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_files"); -+ stats.deleted_files += stats.deleted_dirs = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_dirs"); -+ stats.deleted_files += stats.deleted_symlinks = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_symlinks"); -+ stats.deleted_files += stats.deleted_devices = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_devices"); -+ stats.deleted_files += stats.deleted_specials = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_specials"); - } - - static void become_copy_as_user() -diff --git a/rsync.h b/rsync.h -index 479ac484..4d40542e 100644 ---- a/rsync.h -+++ b/rsync.h -@@ -163,6 +163,23 @@ - /* For compatibility with older rsyncs */ - #define OLD_MAX_BLOCK_SIZE ((int32)1 << 29) - -+/* Policy ceilings on attacker-controlled wire values. Picked well above any -+ * legitimate filesystem / protocol traffic but well below sizes that could -+ * cause integer overflow or DoS-grade allocations. See input_checking.txt. -+ * -+ * Note on MAX_WIRE_XATTR_DATALEN: xattr datum size is bounded only by the -+ * wire-format maximum (signed int32 varint, ~2GB). macOS resource forks -+ * are transferred as the com.apple.ResourceFork xattr and can legitimately -+ * be many GB; --max-alloc (default 1GB, configurable) is the real -+ * allocation cap. read_varint_size() still rejects negative values so a -+ * hostile peer cannot wrap to ~SIZE_MAX. */ -+#define MAX_WIRE_XATTR_COUNT 65536 -+#define MAX_WIRE_XATTR_NAMELEN 4096 -+#define MAX_WIRE_XATTR_DATALEN ((int32)0x7fffffff) -+#define MAX_WIRE_ACL_COUNT 65536 -+#define MAX_WIRE_NSEC 999999999 -+#define MAX_WIRE_DEL_STAT ((int32)1 << 30) -+ - #define ROUND_UP_1024(siz) ((siz) & (1024-1) ? ((siz) | (1024-1)) + 1 : (siz)) - - #define IOERR_GENERAL (1<<0) /* For backward compatibility, this must == 1 */ -diff --git a/xattrs.c b/xattrs.c -index 5f740bb5..99795f24 100644 ---- a/xattrs.c -+++ b/xattrs.c -@@ -697,6 +697,13 @@ int recv_xattr_request(struct file_struct *file, int f_in) - rxa = lst->items; - num = 0; - while ((rel_pos = read_varint(f_in)) != 0) { -+ /* Detect signed overflow before the accumulating add. A hostile -+ * peer could otherwise wrap 'num' to land on an arbitrary value. */ -+ if ((rel_pos > 0 && num > INT_MAX - rel_pos) -+ || (rel_pos < 0 && num < INT_MIN - rel_pos)) { -+ rprintf(FERROR, "xattr rel_pos accumulation overflow [%s]\n", who_am_i()); -+ exit_cleanup(RERR_PROTOCOL); -+ } - num += rel_pos; - if (am_sender) { - /* The sender-related num values are only in order on the sender. -@@ -742,7 +749,7 @@ int recv_xattr_request(struct file_struct *file, int f_in) - } - - old_datum = rxa->datum; -- rxa->datum_len = read_varint(f_in); -+ rxa->datum_len = read_varint_size(f_in, MAX_WIRE_XATTR_DATALEN, "xattr datum_len"); - - if (SIZE_MAX - rxa->name_len < rxa->datum_len) - overflow_exit("recv_xattr_request"); -@@ -783,7 +790,8 @@ void receive_xattr(int f, struct file_struct *file) - return; - } - -- if ((count = read_varint(f)) != 0) { -+ count = read_varint_bounded(f, 0, MAX_WIRE_XATTR_COUNT, "xattr count"); -+ if (count != 0) { - (void)EXPAND_ITEM_LIST(&temp_xattr, rsync_xa, count); - temp_xattr.count = 0; - } -@@ -791,8 +799,8 @@ void receive_xattr(int f, struct file_struct *file) - for (num = 1; num <= count; num++) { - char *ptr, *name; - rsync_xa *rxa; -- size_t name_len = read_varint(f); -- size_t datum_len = read_varint(f); -+ size_t name_len = read_varint_size(f, MAX_WIRE_XATTR_NAMELEN, "xattr name_len"); -+ size_t datum_len = read_varint_size(f, MAX_WIRE_XATTR_DATALEN, "xattr datum_len"); - size_t dget_len = datum_len > MAX_FULL_DATUM ? 1 + (size_t)xattr_sum_len : datum_len; - size_t extra_len = MIGHT_NEED_RPRE ? RPRE_LEN : 0; - if (SIZE_MAX - dget_len < extra_len || SIZE_MAX - dget_len - extra_len < name_len) --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0035-defence-in-depth-guard-cumulative-snprintf-against-l.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0035-defence-in-depth-guard-cumulative-snprintf-against-l.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0035-defence-in-depth-guard-cumulative-snprintf-against-l.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0035-defence-in-depth-guard-cumulative-snprintf-against-l.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,79 +0,0 @@ -From 3d5a5a6568a90ef47d5b9d54ef6253effee19a6f Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Fri, 1 May 2026 09:30:31 +1000 -Subject: [PATCH 35/38] defence-in-depth: guard cumulative snprintf against - length underflow - -Two cumulative-snprintf patterns in log.c (rsyserr) and main.c -(output_itemized_counts) had the shape - - len = snprintf(buf, sizeof buf, ...); - len += snprintf(buf+len, sizeof buf - len, ...); - -with no guard between calls. snprintf returns the would-have-been -length on truncation, so a truncated first call leaves -"sizeof buf - len" as a negative-then-promoted-to-size_t value, -underflowing into a huge size_t and writing past buf. - -Realistic exposure is small in both cases (log header well under -buffer, only ~5 itemized iterations writing ~25 chars each into a -1024-byte buffer) but the defect class matches bb0a8118 and the -fix is cheap. Guard before each subsequent call. - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - log.c | 12 +++++++++--- - main.c | 9 +++++++++ - 2 files changed, 18 insertions(+), 3 deletions(-) - -diff --git a/log.c b/log.c -index e4ba1cce..b948f16a 100644 ---- a/log.c -+++ b/log.c -@@ -456,11 +456,17 @@ void rsyserr(enum logcode code, int errcode, const char *format, ...) - char buf[BIGPATHBUFLEN]; - size_t len; - -+ /* snprintf returns the would-have-been length on truncation, so -+ * each cumulative call must be guarded; if not, sizeof buf - len -+ * can underflow when promoted to size_t and the next call writes -+ * past the buffer. */ - len = snprintf(buf, sizeof buf, RSYNC_NAME ": [%s] ", who_am_i()); - -- va_start(ap, format); -- len += vsnprintf(buf + len, sizeof buf - len, format, ap); -- va_end(ap); -+ if (len < sizeof buf) { -+ va_start(ap, format); -+ len += vsnprintf(buf + len, sizeof buf - len, format, ap); -+ va_end(ap); -+ } - - if (len < sizeof buf) { - len += snprintf(buf + len, sizeof buf - len, -diff --git a/main.c b/main.c -index e6dc134c..549b1da5 100644 ---- a/main.c -+++ b/main.c -@@ -394,9 +394,18 @@ static void output_itemized_counts(const char *prefix, int *counts) - counts[0] -= counts[1] + counts[2] + counts[3] + counts[4]; - for (j = 0; j < 5; j++) { - if (counts[j]) { -+ /* snprintf can return more than its size arg -+ * on truncation; keep len <= sizeof buf - 2 so -+ * the closing ')' and trailing NUL always -+ * have room and the next iteration's -+ * sizeof buf - len - 2 cannot underflow. */ -+ if (len >= (int)sizeof buf - 2) -+ break; - len += snprintf(buf+len, sizeof buf - len - 2, - "%s%s: %s", - pre, labels[j], comma_num(counts[j])); -+ if (len > (int)sizeof buf - 2) -+ len = (int)sizeof buf - 2; - pre = ", "; - } - } --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/2026-05-20/0036-defence-in-depth-receiver-block-index-bounds-read_de.patch rsync-3.5.0+ds1/debian/patches/2026-05-20/0036-defence-in-depth-receiver-block-index-bounds-read_de.patch --- rsync-3.4.1+ds1/debian/patches/2026-05-20/0036-defence-in-depth-receiver-block-index-bounds-read_de.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/2026-05-20/0036-defence-in-depth-receiver-block-index-bounds-read_de.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,80 +0,0 @@ -From fc9a5d64521756dabfd9a17d4daa536fda556f1e Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Wed, 31 Dec 2025 14:01:34 +1100 -Subject: [PATCH 36/38] defence-in-depth: receiver block-index bounds + - read_delay_line null check - -Two assorted audit findings: - - - receive_data() never bounds-checked the block index returned - by recv_token() against sum.count before computing offset2 - and feeding it to map_ptr(). An out-of-bounds index from a - hostile sender produces invalid memory access. Add a - sum.count bounds check. - - - read_delay_line()'s strchr() call could return NULL when no - space was found, but the code unconditionally added 1 to the - result before dereferencing. Low impact (just a disconnect on - exit of the client-specific forked process) but the NULL - deref is real. Guard the NULL. - -Both reported by Joshua Rogers. - -Co-Authored-By: Claude Opus 4.7 (1M context) ---- - generator.c | 14 ++++++++++---- - receiver.c | 5 +++++ - 2 files changed, 15 insertions(+), 4 deletions(-) - -diff --git a/generator.c b/generator.c -index 89f99db4..4d4ae72e 100644 ---- a/generator.c -+++ b/generator.c -@@ -229,11 +229,13 @@ static int read_delay_line(char *buf, int *flags_p) - *flags_p = 0; - - if (sscanf(bp, "%x ", &mode) != 1) { -- invalid_data: -- rprintf(FERROR, "ERROR: invalid data in delete-delay file.\n"); -- return -1; -+ goto invalid_data; -+ } -+ past_space = strchr(bp, ' '); -+ if (!past_space) { -+ goto invalid_data; - } -- past_space = strchr(bp, ' ') + 1; -+ past_space++; - len = j - read_pos - (past_space - bp) + 1; /* count the '\0' */ - read_pos = j + 1; - -@@ -247,6 +249,10 @@ static int read_delay_line(char *buf, int *flags_p) - memcpy(buf, past_space, len); - - return mode; -+ -+invalid_data: -+ rprintf(FERROR, "ERROR: invalid data in delete-delay file.\n"); -+ return -1; - } - - static void do_delayed_deletions(char *delbuf) -diff --git a/receiver.c b/receiver.c -index 3fa68d71..f49931bf 100644 ---- a/receiver.c -+++ b/receiver.c -@@ -352,6 +352,11 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r, - } - - i = -(i+1); -+ if (i < 0 || i >= sum.count) { -+ rprintf(FERROR, "Invalid block index %d (count=%ld) [%s]\n", -+ i, (long)sum.count, who_am_i()); -+ exit_cleanup(RERR_PROTOCOL); -+ } - offset2 = i * (OFF_T)sum.blength; - len = sum.blength; - if (i == (int)sum.count-1 && sum.remainder != 0) --- -2.51.0 - diff -Nru rsync-3.4.1+ds1/debian/patches/CVE-2026-45232.patch rsync-3.5.0+ds1/debian/patches/CVE-2026-45232.patch --- rsync-3.4.1+ds1/debian/patches/CVE-2026-45232.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/CVE-2026-45232.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,236 +0,0 @@ -From a5fc5ebe7a8ef1aa72f6e344599f97fd4427ecba Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Wed, 13 May 2026 20:35:35 +1000 -Subject: [PATCH] socket: reject over-long proxy response line - -fixes a one byte stack overflow when using RSYNC_PROXY with a -malicious proxy. - -Reach: only when RSYNC_PROXY is set and a malicious or MITM'd -proxy returns the pathological response. The byte written is -always '\0' and the attacker doesn't choose the offset, so impact -is corruption of one adjacent stack byte and possible later -misbehaviour or crash -- no information disclosure beyond the -existing rprintf of buffer contents. - -Reported by Aisle Research via Michal Ruprich - -(cherry picked from commit a5fc5ebe7a8ef1aa72f6e344599f97fd4427ecba) ---- - socket.c | 30 ++++--- - testsuite/proxy-response-line-too-long.test | 128 ++++++++++++++++++++++++++++ - 2 files changed, 145 insertions(+), 13 deletions(-) - create mode 100755 testsuite/proxy-response-line-too-long.test - -diff --git a/socket.c b/socket.c -index c2075ad..6a8f6f4 100644 ---- a/socket.c -+++ b/socket.c -@@ -47,21 +47,23 @@ static struct sigaction sigact; - - static int sock_exec(const char *prog); - -+#define PROXY_BUF_SIZE 1024 -+ - /* Establish a proxy connection on an open socket to a web proxy by using the - * CONNECT method. If proxy_user and proxy_pass are not NULL, they are used to - * authenticate to the proxy using the "Basic" proxy-authorization protocol. */ - static int establish_proxy_connection(int fd, char *host, int port, char *proxy_user, char *proxy_pass) - { -- char *cp, buffer[1024]; -- char *authhdr, authbuf[1024]; -+ char *cp, buffer[PROXY_BUF_SIZE + 1]; -+ char *authhdr, authbuf[PROXY_BUF_SIZE + 1]; - int len; - - if (proxy_user && proxy_pass) { -- stringjoin(buffer, sizeof buffer, -+ stringjoin(buffer, PROXY_BUF_SIZE, - proxy_user, ":", proxy_pass, NULL); - len = strlen(buffer); - -- if ((len*8 + 5) / 6 >= (int)sizeof authbuf - 3) { -+ if ((len*8 + 5) / 6 >= PROXY_BUF_SIZE - 3) { - rprintf(FERROR, - "authentication information is too long\n"); - return -1; -@@ -74,14 +76,14 @@ static int establish_proxy_connection(int fd, char *host, int port, char *proxy_ - authhdr = ""; - } - -- len = snprintf(buffer, sizeof buffer, "CONNECT %s:%d HTTP/1.0%s%s\r\n\r\n", host, port, authhdr, authbuf); -- assert(len > 0 && len < (int)sizeof buffer); -+ len = snprintf(buffer, PROXY_BUF_SIZE, "CONNECT %s:%d HTTP/1.0%s%s\r\n\r\n", host, port, authhdr, authbuf); -+ assert(len > 0 && len < PROXY_BUF_SIZE); - if (write(fd, buffer, len) != len) { - rsyserr(FERROR, errno, "failed to write to proxy"); - return -1; - } - -- for (cp = buffer; cp < &buffer[sizeof buffer - 1]; cp++) { -+ for (cp = buffer; cp < &buffer[PROXY_BUF_SIZE - 1]; cp++) { - if (read(fd, cp, 1) != 1) { - rsyserr(FERROR, errno, "failed to read from proxy"); - return -1; -@@ -90,11 +92,13 @@ static int establish_proxy_connection(int fd, char *host, int port, char *proxy_ - break; - } - -- if (*cp != '\n') -- cp++; -- *cp-- = '\0'; -- if (*cp == '\r') -- *cp = '\0'; -+ if (cp == &buffer[PROXY_BUF_SIZE - 1]) { -+ rprintf(FERROR, "proxy response line too long\n"); -+ return -1; -+ } -+ *cp = '\0'; -+ if (cp > buffer && cp[-1] == '\r') -+ cp[-1] = '\0'; - if (strncmp(buffer, "HTTP/", 5) != 0) { - rprintf(FERROR, "bad response from proxy -- %s\n", - buffer); -@@ -110,7 +114,7 @@ static int establish_proxy_connection(int fd, char *host, int port, char *proxy_ - } - /* throw away the rest of the HTTP header */ - while (1) { -- for (cp = buffer; cp < &buffer[sizeof buffer - 1]; cp++) { -+ for (cp = buffer; cp < &buffer[PROXY_BUF_SIZE]; cp++) { - if (read(fd, cp, 1) != 1) { - rsyserr(FERROR, errno, - "failed to read from proxy"); -diff --git a/testsuite/proxy-response-line-too-long.test b/testsuite/proxy-response-line-too-long.test -new file mode 100755 -index 0000000..7f55c43 ---- /dev/null -+++ b/testsuite/proxy-response-line-too-long.test -@@ -0,0 +1,128 @@ -+#!/bin/sh -+ -+# Copyright (C) 2026 by Andrew Tridgell -+ -+# This program is distributable under the terms of the GNU GPL (see -+# COPYING). -+ -+# Regression test for the off-by-one stack OOB write in -+# establish_proxy_connection() in socket.c when a malicious or -+# man-in-the-middle HTTP proxy returns a first response line of -+# 1023+ bytes without a '\n' terminator. -+# -+# Pre-fix: the read loop walked buffer[0..sizeof-2] one byte at a -+# time, then post-loop logic did "if (*cp != '\n') cp++; *cp-- = -+# '\0';". If no newline arrived before the loop filled the buffer, -+# cp was left at &buffer[sizeof-1] (never written by the loop), -+# *cp held stale stack bytes, and cp++ pushed cp one past the array. -+# The null-termination then wrote one byte out of bounds on the -+# stack. AddressSanitizer reports stack-buffer-overflow at the -+# null-termination site. -+# -+# Post-fix: the bound-exhaustion case is detected by position and -+# rejected with an "proxy response line too long" message, so no -+# OOB write occurs and rsync exits with a non-signal status. -+ -+. "$suitedir/rsync.fns" -+ -+command -v python3 >/dev/null 2>&1 || test_skipped "python3 not available" -+ -+workdir="$scratchdir/workdir" -+mkdir -p "$workdir" -+cd "$workdir" -+ -+port_file="$workdir/port" -+proxy_log="$workdir/proxy.log" -+ -+# A minimal TCP listener: binds to an ephemeral port on 127.0.0.1, -+# writes the chosen port to $port_file *before* accept() so the test -+# can synchronise without a sleep, accepts one connection, reads -+# until end-of-headers or 64 KiB, sends exactly 1023 bytes of 'X' -+# with no '\n', then closes. -+python3 - "$port_file" >"$proxy_log" 2>&1 <<'PYEOF' & -+import socket, sys, os -+port_file = sys.argv[1] -+s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -+s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) -+s.bind(("127.0.0.1", 0)) -+port = s.getsockname()[1] -+tmp = port_file + ".tmp" -+with open(tmp, "w") as fp: -+ fp.write("%d\n" % port) -+os.rename(tmp, port_file) # atomic visibility to the shell side -+s.listen(1) -+conn, _ = s.accept() -+conn.settimeout(5) -+try: -+ data = b"" -+ while b"\r\n\r\n" not in data and len(data) < 65536: -+ chunk = conn.recv(8192) -+ if not chunk: -+ break -+ data += chunk -+except socket.timeout: -+ pass -+conn.sendall(b"X" * 1023) # exactly the buffer-1 trigger size -+try: -+ conn.shutdown(socket.SHUT_RDWR) -+except OSError: -+ pass -+conn.close() -+s.close() -+PYEOF -+proxy_pid=$! -+ -+# Wait up to ~10s for the listener to publish its port. -+i=0 -+while [ ! -s "$port_file" ] && [ $i -lt 10 ]; do -+ sleep 1 -+ i=$((i + 1)) -+done -+ -+if [ ! -s "$port_file" ]; then -+ kill "$proxy_pid" 2>/dev/null -+ cat "$proxy_log" >&2 2>/dev/null -+ test_fail "proxy listener never published a port" -+fi -+ -+port=`cat "$port_file"` -+case "$port" in -+ *[!0-9]*|"") kill "$proxy_pid" 2>/dev/null; test_fail "bogus port from listener: '$port'" ;; -+esac -+ -+# Run rsync through the malicious proxy. Any rsync:// URL works: -+# the proxy intercepts the CONNECT and never forwards anywhere. -+rsync_err="$workdir/rsync.err" -+ -+# rsync MUST exit non-zero here (the proxy is misbehaving). -+# Use `|| status=$?` so we capture the real exit code under `sh -e`; -+# `if ! cmd; then status=$?` would only ever see 0 because the `!` -+# is the last command before `$?`. -+status=0 -+RSYNC_PROXY="127.0.0.1:$port" \ -+ $RSYNC rsync://example.invalid:873/whatever/ "$workdir/out/" \ -+ >/dev/null 2>"$rsync_err" || status=$? -+ -+# Reap the listener. -+wait "$proxy_pid" 2>/dev/null || true -+ -+# 1. rsync must not have crashed (SIGSEGV/SIGABRT report >= 128). -+if [ "$status" -ge 128 ]; then -+ cat "$rsync_err" >&2 -+ test_fail "rsync killed by signal (status=$status) -- possible stack OOB regression" -+fi -+ -+# 2. rsync must have actually exited non-zero (i.e. saw the bad proxy). -+if [ "$status" -eq 0 ]; then -+ cat "$rsync_err" >&2 -+ test_fail "rsync returned success despite malformed proxy response" -+fi -+ -+# 3. The new error message must appear. -+if ! grep -q "proxy response line too long" "$rsync_err"; then -+ cat "$rsync_err" >&2 -+ test_fail "expected 'proxy response line too long' in rsync stderr" -+fi -+ -+echo "OK: over-long proxy response line rejected cleanly without crashing" -+exit 0 diff -Nru rsync-3.4.1+ds1/debian/patches/Fix_ENOENT_when_resolving_kernel_pseudo_paths_in_ona_open_patch rsync-3.5.0+ds1/debian/patches/Fix_ENOENT_when_resolving_kernel_pseudo_paths_in_ona_open_patch --- rsync-3.4.1+ds1/debian/patches/Fix_ENOENT_when_resolving_kernel_pseudo_paths_in_ona_open_patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/Fix_ENOENT_when_resolving_kernel_pseudo_paths_in_ona_open_patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,312 @@ +From: Omar Elsayed +Date: Thu, 3 Sep 2026 01:11:16 +0300 +Subject: [PATCH] Fix ENOENT when resolving kernel pseudo-paths in ona_open + (#1054) + +* Fix ona_open to safely resolve bash process substitution pseudo-paths +Bash process substitution (e.g., `<(...)` or `>(...)`) exposes file +descriptors as symlinks under `/proc/self/fd/X` pointing to kernel +pseudo-paths such as `pipe:[12345]`. Previously, `ona_open()` would read +this target and attempt to resolve it as a literal file path on disk, +causing the operation to fail with `ENOENT` and breaking legitimate local +process substitution. + +This patch safely intercepts and resolves these pseudo-paths while +maintaining strict confinement boundaries and averting TOCTOU risks: + +- Detects kernel pseudo-paths (`pipe:[`, `socket:[`, `anon_inode:`) + only when `fd_pin_tail` confirms the path resolves precisely to a + direct child of a valid FD directory. +- Categorically rejects pseudo-path resolution if `confine_root` is + active (yielding `ENOENT`). +- Strips `O_NOFOLLOW` for legitimate leaf pseudo-paths, allowing + `openat()` to correctly delegate resolution. +- Reverts `fd_pin_tail` to its upstream signature, as manual PID + validation is no longer required due to the secure `openat()` design. + +* testsuite: expect pseudo-path skip on Alma +* syscall: reject trailing pseudo-path components + +--------- + +Co-authored-by: Zen Dodd + +Backported-by: Samuel Henrique + * Drop the .github/workflows/almalinux-8-build.yml hunk, the directory + is not present in the +ds tarball + * Refresh context +--- + syscall.c | 41 ++++++- + testsuite/pseudo-paths_test.py | 138 ++++++++++++++++++++++ + testsuite/skiplist/README.md | 1 + + testsuite/skiplist/{linux.txt => almalinux-8.txt} | 6 +- + testsuite/skiplist/cygwin.txt | 1 + + testsuite/skiplist/macos.txt | 1 + + 6 files changed, 181 insertions(+), 7 deletions(-) + create mode 100644 testsuite/pseudo-paths_test.py + copy testsuite/skiplist/{linux.txt => almalinux-8.txt} (53%) + +diff --git a/syscall.c b/syscall.c +index 68a7eea..54ac051 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -314,8 +314,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + * (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'; ++ char abspath[MAXPATHLEN] = {0}; + if (am_daemon && module_dir && module_dir[0] == '/') + strlcpy(abspath, module_dir, sizeof abspath); /* "/" for a path=/ module */ + else if (confine_root) { +@@ -339,7 +338,8 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + * 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; ++ const char *ptail = fd_pin_tail(path); ++ int pin_transit = !am_daemon && confine_root && ptail != NULL; + + /* Path-walk state. `remaining` is the unconsumed tail; we splice + * symlink targets back into it as we go. Sized 2x MAXPATHLEN so a +@@ -429,6 +429,41 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + } + target[n] = '\0'; + ++ /* Detect Linux kernel pseudo-paths (pipes, sockets, anon_inodes). ++ * These are not real paths on disk and never contain slashes. */ ++ const char *abstail = fd_pin_tail(abspath); ++ int is_fd_dir = (abstail != NULL && *abstail == '\0' && ptail != NULL); ++ if (is_fd_dir && (strncmp(target, "pipe:[", 6) == 0 ++ || strncmp(target, "socket:[", 8) == 0 ++ || strncmp(target, "anon_inode:", 11) == 0)) { ++ if (!is_last) { ++ saved_errno = ENOTDIR; ++ goto out; ++ } ++ if (confine_root) { ++ /* Anonymous objects cannot be proven to reside beneath ++ * the confinement root. */ ++ saved_errno = ENOENT; ++ goto out; ++ } ++ /* Process substitution exposes /dev/fd/X as a symlink to a ++ * kernel object. Reopen the validated leaf without O_NOFOLLOW ++ * so the kernel applies the caller's requested open flags. */ ++ retfd = openat(dfd, comp, (flags & ~O_NOFOLLOW) | O_CLOEXEC, mode); ++ /* Refuse a descriptor that changed to a filesystem object ++ * between validation and openat(). */ ++ if (retfd >= 0) { ++ STRUCT_STAT pst; ++ if (fstat(retfd, &pst) < 0 || S_ISREG(pst.st_mode) || S_ISDIR(pst.st_mode)) { ++ close(retfd); ++ retfd = -1; ++ errno = ELOOP; ++ } ++ } ++ saved_errno = retfd < 0 ? errno : 0; ++ goto out; ++ } ++ + /* Splice: new `remaining` = + . + * Absolute target restarts the walk from "/". */ + char tail[MAXPATHLEN]; +diff --git a/testsuite/pseudo-paths_test.py b/testsuite/pseudo-paths_test.py +new file mode 100644 +index 0000000..f72b48e +--- /dev/null ++++ b/testsuite/pseudo-paths_test.py +@@ -0,0 +1,138 @@ ++"""Process substitution /dev/fd/ write pipe pseudo-paths for --log-file must not crash and must successfully write logs, but must be rejected if confined root.""" ++ ++import shlex ++import shutil ++import subprocess ++import sys ++from pathlib import Path ++ ++from rsyncfns import ( ++ SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail, test_skipped, ++) ++if not sys.platform.startswith('linux'): ++ test_skipped('Kernel pseudo-path string is a Linux-specific procfs feature') ++ raise SystemExit(0) ++ ++# We require bash specifically because standard POSIX /bin/sh does not ++# guarantee support for >(...) process substitution syntax. ++bash = shutil.which('bash') ++if bash is None: ++ test_skipped('bash is unavailable, cannot test process substitution') ++ ++# Verify the host bash actually supports process substitution ++probe = subprocess.run( ++ [bash, '-c', 'echo "probe" > >(cat > /dev/null)'], ++ capture_output=True ++) ++if probe.returncode != 0: ++ test_skipped('bash process substitution is not supported on this system') ++ ++base = Path(SCRATCHDIR / 'rsync-pseudo-path').resolve() ++src = base / 'src' ++dest = base / 'dest' ++log_out = base / 'test_log.txt' ++log_out_confined = base / 'test_log_confined.txt' ++makepath(src, dest) ++ ++(src / 'transfer_me.txt').write_text('sync this\n') ++ ++rsync_base_cmd = shlex.join(rsync_argv('-a')) ++src_path = shlex.quote(str(src) + '/') ++dest_path = shlex.quote(str(dest) + '/') ++ ++log_path = shlex.quote(str(log_out)) ++log_path_confined = shlex.quote(str(log_out_confined)) ++ ++# ------------------------------------------------------------------------- ++# TEST 1: Unconfined process substitution (Should Succeed) ++# ------------------------------------------------------------------------- ++bash_script = f"{rsync_base_cmd} -v --log-file=>(cat > {log_path}) {src_path} {dest_path}" ++ ++try: ++ proc = subprocess.run( ++ [bash, '-c', bash_script], ++ capture_output=True, ++ text=True, ++ timeout=10, ++ ) ++except subprocess.TimeoutExpired: ++ rmtree(base) ++ test_fail('process substitution test timed out') ++ ++ctx = f'rc={proc.returncode}, stderr={proc.stderr.strip()!r}' ++ ++if proc.returncode != 0: ++ rmtree(base) ++ test_fail(f'rsync crashed writing to a pseudo-path log pipe ({ctx})') ++ ++if not (dest / 'transfer_me.txt').is_file(): ++ rmtree(base) ++ test_fail(f'rsync failed to transfer the allowed file ({ctx})') ++ ++if not log_out.exists() or log_out.stat().st_size == 0: ++ rmtree(base) ++ test_fail(f'rsync survived, but failed to write data to the log pipe ({ctx})') ++ ++log_data = log_out.read_text() ++if "transfer_me.txt" not in log_data: ++ rmtree(base) ++ test_fail(f'Log pipe received data, but is missing expected output: {log_data[:100]}') ++ ++print('Test 1 Passed: rsync successfully wrote logs to a process substitution pseudo-path') ++ ++# ------------------------------------------------------------------------- ++# TEST 2: Confined Root (Should Reject Pseudo-path) ++# ------------------------------------------------------------------------- ++bash_script_confined = f"{rsync_base_cmd} --confine-root={dest_path} -v --log-file=>(cat > {log_path_confined}) {src_path} {dest_path}" ++ ++try: ++ proc_confined = subprocess.run( ++ [bash, '-c', bash_script_confined], ++ capture_output=True, ++ text=True, ++ timeout=10, ++ ) ++except subprocess.TimeoutExpired: ++ rmtree(base) ++ test_fail('confined process substitution test timed out') ++ ++ctx_confined = f'rc={proc_confined.returncode}, stderr={proc_confined.stderr.strip()!r}' ++ ++# Rsync considers log-file failure a warning, so it still exits 0. ++stderr_lower = proc_confined.stderr.lower() ++if "no such file or directory" in stderr_lower and "failed to open" in stderr_lower: ++ if log_out_confined.exists() and log_out_confined.stat().st_size > 0: ++ rmtree(base) ++ test_fail(f'rsync printed an error but still wrote the confined log! ({ctx_confined})') ++ print('Test 2 Passed: rsync correctly rejected the pseudo-path when confine_root was active') ++else: ++ rmtree(base) ++ test_fail(f'rsync failed to reject the pseudo-path or had an unexpected error ({ctx_confined})') ++ ++# A pseudo-path is valid only when its descriptor number is the final component. ++rmtree(dest) ++makepath(dest) ++trailing_script = ( ++ f'pipe_path=<(printf "transfer_me.txt\\n"); ' ++ f'{rsync_base_cmd} --exclude-from="$pipe_path/trailing" {src_path} {dest_path}' ++) ++try: ++ proc_trailing = subprocess.run( ++ [bash, '-c', trailing_script], ++ capture_output=True, ++ text=True, ++ timeout=10, ++ ) ++except subprocess.TimeoutExpired: ++ rmtree(base) ++ test_fail('trailing-component pseudo-path test timed out') ++ ++if proc_trailing.returncode == 0: ++ rmtree(base) ++ test_fail('/dev/fd/N/trailing unexpectedly opened descriptor N') ++if (dest / 'transfer_me.txt').exists(): ++ rmtree(base) ++ test_fail('transfer continued after accepting a trailing pseudo-path component') ++ ++rmtree(base) ++raise SystemExit(0) +diff --git a/testsuite/skiplist/README.md b/testsuite/skiplist/README.md +index 31dd457..2e822ef 100644 +--- a/testsuite/skiplist/README.md ++++ b/testsuite/skiplist/README.md +@@ -23,6 +23,7 @@ different tests merge cleanly. + | file | contents | + | --- | --- | + | `common.txt` | skipped on every platform that runs the oracle — mostly `require_tcp` / `require_asan` tests, which the default stdio-pipe `make check` cannot satisfy | ++| `almalinux-8.txt` | AlmaLinux 8 container additions | + | `linux.txt` | Linux-only additions | + | `macos.txt` | macOS-only additions | + | `cygwin.txt` | Cygwin-only additions | +diff --git a/testsuite/skiplist/linux.txt b/testsuite/skiplist/almalinux-8.txt +similarity index 53% +copy from testsuite/skiplist/linux.txt +copy to testsuite/skiplist/almalinux-8.txt +index b89100d..483ac8c 100644 +--- a/testsuite/skiplist/linux.txt ++++ b/testsuite/skiplist/almalinux-8.txt +@@ -3,8 +3,6 @@ + # from a workflow as RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/[,@...]. + # See testsuite/skiplist/README.md. + # +-# Linux-only additions to common.txt. ++# AlmaLinux 8 container additions to common.txt and linux.txt. + +-crtimes # Rsync is configured without crtimes support +-partial-protected-regular-retry-policy # deterministic partial EACCES recovery uses dyld interposing +-readonly-partial-abort-mode-regression # ++pseudo-paths # Bash process substitution is unavailable in the AlmaLinux 8 container +diff --git a/testsuite/skiplist/cygwin.txt b/testsuite/skiplist/cygwin.txt +index 46060e1..84dbb7b 100644 +--- a/testsuite/skiplist/cygwin.txt ++++ b/testsuite/skiplist/cygwin.txt +@@ -52,6 +52,7 @@ partial-protected-regular-retry-linux + partial-protected-regular-retry-policy # deterministic partial EACCES recovery uses dyld interposing + password-file-symlink + protected-regular ++pseudo-paths + rename-mixed-parent-transfer + rrsync-sender-leaf-flip + rrsync-sender-parent-pin +diff --git a/testsuite/skiplist/macos.txt b/testsuite/skiplist/macos.txt +index 5952c81..fcc41a4 100644 +--- a/testsuite/skiplist/macos.txt ++++ b/testsuite/skiplist/macos.txt +@@ -22,6 +22,7 @@ open-noatime + partial-protected-regular-retry-linux + preallocate + protected-regular ++pseudo-paths # dynamically skips on runners lacking bash process substitution + readonly-partial-abort-mode-regression # + rrsync-sender-leaf-flip + rrsync-sender-parent-pin diff -Nru rsync-3.4.1+ds1/debian/patches/Fix_handling_of_dev_std_in_out_err_pseudo-paths_and_namespace_overflow_UID_checks.patch rsync-3.5.0+ds1/debian/patches/Fix_handling_of_dev_std_in_out_err_pseudo-paths_and_namespace_overflow_UID_checks.patch --- rsync-3.4.1+ds1/debian/patches/Fix_handling_of_dev_std_in_out_err_pseudo-paths_and_namespace_overflow_UID_checks.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/Fix_handling_of_dev_std_in_out_err_pseudo-paths_and_namespace_overflow_UID_checks.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,673 @@ +From a287b7e5429aeb5e66904a520f0074cceaaa5dc9 Mon Sep 17 00:00:00 2001 +From: seks99x +Date: Tue, 8 Sep 2026 00:37:06 +0300 +Subject: [PATCH] Fix handling of /dev/std{in,out,err} pseudo-paths and + namespace overflow UID checks + +Using --log-file or --files-from with /dev/stdin, /dev/stdout, or /dev/stderr +attached to a pipe previously failed in two ways: + +1. On standard hosts, it failed with ENOENT because the symlink target + (pipe:[N]) was treated as a relative file path rather than a secure + kernel pseudo-path. +2. Inside user namespaces (e.g., rootless podman, unshare), it aborted + with ELOOP ("refusing to follow a symlink owned by an untrusted user"). + The symlink ownership reported the kernel overflow UID (65534), which + completely blocked the walker from following the pseudo-paths. + +This patch resolves the issues by refining the symlink path walker and +trust mechanisms: + +* Updated fd_pin_tail() to natively recognize /dev/stdin, /dev/stdout, + and /dev/stderr, parsing them directly to their corresponding /0, /1, + and /2 descriptor tails. +* Introduced the is_anchored variable to strictly enforce absolute paths, + ensuring malformed paths cannot bypass the check using relative forms + like dev/fd/ or proc/self/. +* Removed pin_transit from the namespace_pin check and replaced it by + adding the is_anchored variable to the check. Checking for root confinement + or daemon status is unnecessary when we are already validating and restricting + traversal to known safe paths (Daemon also refuses any symlinks pointing to /). +* Added new cases to the test suite to validate standard stream pseudo-path + handling and ensure namespace overflow UID bypasses work correctly + without regression. +--- + syscall.c | 83 ++++++---- + testsuite/pseudo-paths-daemon_test.py | 223 ++++++++++++++++++++++++++ + testsuite/pseudo-paths_test.py | 218 +++++++++++++++++++++++++ + testsuite/skiplist/almalinux-8.txt | 1 + + testsuite/skiplist/cygwin.txt | 1 + + testsuite/skiplist/macos.txt | 1 + + 6 files changed, 497 insertions(+), 30 deletions(-) + create mode 100644 testsuite/pseudo-paths-daemon_test.py + +diff --git a/syscall.c b/syscall.c +index 81e12f906..1553f4716 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -162,29 +162,43 @@ static const char *confinement_root(unsigned int *lenp) + * is not in an fd-pin namespace. */ + static const char *fd_pin_tail(const char *p) + { +- const char *s; +- +- if (strncmp(p, "/dev/fd", 7) == 0) { +- s = p + 7; +- return (*s == '\0' || *s == '/') ? s : NULL; +- } +- +- 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; ++ const char *s; ++ ++ /* Group all /dev/ checks under a single prefix comparison */ ++ if (strncmp(p, "/dev/", 5) == 0) { ++ s = p + 5; ++ if (strncmp(s, "fd", 2) == 0) { ++ s += 2; ++ return (*s == '\0' || *s == '/') ? s : NULL; ++ } ++ if (strncmp(s, "std", 3) == 0) { ++ s += 3; ++ if (strncmp(s, "in", 3) == 0) ++ return "/0"; ++ if (strncmp(s, "out", 4) == 0) ++ return "/1"; ++ if (strncmp(s, "err", 4) == 0) ++ return "/2"; ++ } ++ return NULL; /* Instantly reject any other /dev/ path */ ++ } ++ ++ 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, such as "/proc/self/fd/7" or "/dev/fd/7", whose target is +@@ -347,6 +361,10 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + return -1; + } + ++ /* Tracker: 1 if we are genuinely walking from the system root, ++ * 0 if we are walking a relative path where abspath_step will fake a '/' */ ++ int is_anchored = (abspath[0] != '\0'); ++ + /* 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 +@@ -373,6 +391,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + return -1; + dfd_owns = 1; + abspath[0] = '\0'; /* now resolving from "/" */ ++ is_anchored = 1; + char *p = remaining; + while (*p == '/') p++; + memmove(remaining, p, strlen(p) + 1); +@@ -422,12 +441,15 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + + if (S_ISLNK(lst.st_mode)) { + /* Symlink: untrusted owner is refused; trusted owner is followed +- * via readlinkat + splice. In a user namespace the /proc/self and +- * /dev/fd symlinks may report the overflow uid, so +- * allow those exact components while traversing a recognised pin. */ +- int namespace_pin = pin_transit +- && ((strcmp(abspath, "/proc") == 0 && strcmp(comp, "self") == 0) +- || (strcmp(abspath, "/dev") == 0 && strcmp(comp, "fd") == 0)); ++ * via readlinkat + splice. In a user namespace the /proc/self, ++ * /dev/fd and /dev/std* symlinks may report the overflow uid, so ++ * allow those exact components while traversing a recognised pin. */ ++ int namespace_pin = is_anchored ++ && ((strcmp(abspath, "/proc") == 0 && strcmp(comp, "self") == 0) ++ || (strcmp(abspath, "/dev") == 0 && (strcmp(comp, "fd") == 0 ++ || strcmp(comp, "stdin") == 0 ++ || strcmp(comp, "stdout") == 0 ++ || strcmp(comp, "stderr") == 0))); + if (!namespace_pin && lst.st_uid != 0 && lst.st_uid != trusted_uid) { + rprintf(FERROR, + "refusing to follow a symlink owned by an untrusted user; " +@@ -451,7 +473,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + /* Detect Linux kernel pseudo-paths (pipes, sockets, anon_inodes). + * These are not real paths on disk and never contain slashes. */ + const char *abstail = fd_pin_tail(abspath); +- int is_fd_dir = (abstail != NULL && *abstail == '\0' && ptail != NULL); ++ int is_fd_dir = (abstail != NULL && *abstail == '\0' && is_anchored); + if (is_fd_dir && (strncmp(target, "pipe:[", 6) == 0 + || strncmp(target, "socket:[", 8) == 0 + || strncmp(target, "anon_inode:", 11) == 0)) { +@@ -510,6 +532,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + /* "self" resolves to "", still inside the pin; + * the magic link itself lands elsewhere and ends the + * exemption. Never turns back on. */ ++ is_anchored = 1; + pin_transit = pin_transit && fd_pin_tail(rebuilt) != NULL; + char *p = rebuilt; + while (*p == '/') p++; +diff --git a/testsuite/pseudo-paths-daemon_test.py b/testsuite/pseudo-paths-daemon_test.py +new file mode 100644 +index 000000000..daab15fd2 +--- /dev/null ++++ b/testsuite/pseudo-paths-daemon_test.py +@@ -0,0 +1,223 @@ ++"""Regression test for daemon log file silent failures with process substitution and pipes.""" ++ ++import os ++import signal ++import shutil ++import socket ++import subprocess ++import sys ++import tempfile ++import time ++from pathlib import Path ++ ++from rsyncfns import makepath, rmtree, rsync_argv, test_fail, test_skipped ++ ++if not sys.platform.startswith('linux'): ++ test_skipped('Namespace daemon testing is a Linux-specific feature') ++ raise SystemExit(0) ++ ++bash = shutil.which('bash') ++if not bash: ++ test_skipped('bash is not installed') ++ raise SystemExit(0) ++ ++probe_bash = subprocess.run([bash, '-c', 'echo "probe" > >(cat > /dev/null)'], capture_output=True) ++if probe_bash.returncode != 0: ++ test_skipped('bash process substitution is not supported on this system') ++ raise SystemExit(0) ++ ++def kill_daemon(proc): ++ """Safely terminate the daemon process group if it is still running.""" ++ if proc.poll() is not None: ++ return ++ try: ++ os.killpg(proc.pid, signal.SIGTERM) ++ except ProcessLookupError: ++ pass ++ try: ++ proc.wait(timeout=3) ++ except subprocess.TimeoutExpired: ++ try: ++ os.killpg(proc.pid, signal.SIGKILL) ++ except ProcessLookupError: ++ pass ++ proc.wait() ++ ++ws_base = Path(tempfile.mkdtemp(prefix='rsync-pseudo-paths-daemon-')) ++ws_base.chmod(0o777) ++ ++try: ++ ws_src = ws_base / 'src' ++ makepath(ws_src) ++ ws_src.chmod(0o777) ++ ++ tf = ws_src / 'file.txt' ++ tf.write_text('data\n') ++ tf.chmod(0o777) ++ ++ rsync_bin = rsync_argv()[0] ++ if not Path(rsync_bin).exists(): ++ test_fail(f"rsync binary not found at {rsync_bin}") ++ ++ base_port = 20000 + (os.getpid() % 10000) ++ ++ # ------------------------------------------------------------------------- ++ # TEST 1: Host Daemon (Normal Root) with > >(...) process substitution ++ # ------------------------------------------------------------------------- ++ port_host = base_port ++ print(f"Running Test 1: Host Daemon (Normal Root) with > >(cat > out) (Port {port_host})...", flush=True) ++ ++ dest_host = ws_base / 'dest_host' ++ makepath(dest_host) ++ dest_host.chmod(0o777) ++ ++ out_host = ws_base / 'out_host' ++ err_host = ws_base / 'err_host' ++ conf_host = ws_base / 'host.conf' ++ ++ conf_host.write_text(f"""pid file = {ws_base}/host.pid ++log file = /dev/stdout ++[test-from] ++path = {dest_host} ++read only = no ++use chroot = no ++""") ++ ++ cmd_host = f"{rsync_bin} --daemon --no-detach --config={conf_host} --port={port_host} --address=127.0.0.1 > >(cat > {out_host}) 2> {err_host} < /dev/null" ++ ++ # Executes directly as the host user ++ daemon_host = subprocess.Popen([bash, '-c', cmd_host], start_new_session=True) ++ ++ try: ++ # Bounded readiness polling (Wait for daemon to bind to the port) ++ for _ in range(50): ++ if daemon_host.poll() is not None: ++ test_fail("Host Daemon crashed immediately upon startup.") ++ try: ++ with socket.create_connection(('127.0.0.1', port_host), timeout=0.1): ++ break # Port is open, daemon is ready ++ except OSError: ++ time.sleep(0.1) ++ else: ++ test_fail("Host Daemon failed to bind to port within the timeout period.") ++ ++ client_cmd_host = [rsync_bin, '-a', str(ws_src) + '/', f'rsync://127.0.0.1:{port_host}/test-from/'] ++ client_proc = subprocess.run(client_cmd_host, capture_output=True, text=True) ++ ++ if client_proc.returncode != 0: ++ test_fail(f"Client transfer failed against Host Daemon. Stderr: {client_proc.stderr.strip()}") ++ ++ # Poll up to a second for logs to flush through the pipe ++ for _ in range(10): ++ out_host_data = out_host.read_text() if out_host.exists() else "" ++ if "rsyncd version" in out_host_data and "test-from" in out_host_data: ++ break ++ time.sleep(0.1) ++ ++ finally: ++ kill_daemon(daemon_host) ++ ++ out_host_data = out_host.read_text() if out_host.exists() else "" ++ err_host_data = err_host.read_text() if err_host.exists() else "" ++ ++ if "rsyncd version" not in out_host_data or "test-from" not in out_host_data: ++ test_fail(f"Bug reproduced: Host Daemon silently dropped logs.\n'out' file: {out_host_data}\n'err' file: {err_host_data}") ++ ++ print("Test 1 Passed: Daemon successfully logged to out file on host.", flush=True) ++ ++ # ------------------------------------------------------------------------- ++ # TEST 2 SETUP: Namespace capabilities and configuration ++ # ------------------------------------------------------------------------- ++ unshare = shutil.which('unshare') ++ if not unshare: ++ print("Test 2 Skipped: unshare is not installed", flush=True) ++ raise SystemExit(0) ++ ++ launcher = [] ++ if os.geteuid() == 0: ++ setpriv = shutil.which('setpriv') ++ if setpriv is None: ++ print("Test 2 Skipped: setpriv is unavailable for the root-run testsuite", flush=True) ++ raise SystemExit(0) ++ launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups'] ++ ++ unshare_argv = [unshare, '--user', '--map-root-user', '--mount', '--pid', '--fork', '--mount-proc'] ++ ++ probe_unshare = subprocess.run(launcher + unshare_argv + ['true'], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) ++ if probe_unshare.returncode != 0: ++ print("Test 2 Skipped: user namespace is unavailable with the required unprivileged launcher", flush=True) ++ raise SystemExit(0) ++ ++ check_ns = ( ++ "import os, sys\n" ++ "proc_uid = os.lstat('/proc/self').st_uid\n" ++ "if proc_uid in (0, os.geteuid()):\n" ++ " sys.exit(22)\n" ++ ) ++ probe_uid = subprocess.run(launcher + unshare_argv + [sys.executable, '-c', check_ns]) ++ if probe_uid.returncode == 22: ++ print("Test 2 Skipped: /proc/self does not expose an overflow uid in this namespace", flush=True) ++ raise SystemExit(0) ++ elif probe_uid.returncode != 0: ++ test_fail(f'Namespace uid check failed (rc={probe_uid.returncode})') ++ ++ ns_rsync_bin = ws_base / 'rsync-bin' ++ shutil.copy2(rsync_bin, ns_rsync_bin) ++ ns_rsync_bin.chmod(0o777) ++ ++ # ------------------------------------------------------------------------- ++ # TEST 2: Namespace Daemon with > >(...) process substitution ++ # ------------------------------------------------------------------------- ++ port_ns = base_port + 1 ++ print(f"Running Test 2: Namespace Daemon inside unshare --user (Port {port_ns})...", flush=True) ++ ++ dest_ns = ws_base / 'dest_ns' ++ makepath(dest_ns) ++ dest_ns.chmod(0o777) ++ ++ out_ns = ws_base / 'out_ns' ++ err_ns = ws_base / 'err_ns' ++ conf_ns = ws_base / 'ns.conf' ++ ++ conf_ns.write_text(f"""pid file = {ws_base}/ns.pid ++log file = /dev/stdout ++[test-from] ++path = {dest_ns} ++read only = no ++use chroot = no ++""") ++ ++ cmd_ns = f"{ns_rsync_bin} --daemon --no-detach --config={conf_ns} --port={port_ns} --address=127.0.0.1 > >(cat > {out_ns}) 2> {err_ns} < /dev/null" ++ ++ namespace_cmd = launcher + unshare_argv + [bash, '-c', cmd_ns] ++ daemon_ns = subprocess.Popen(namespace_cmd, stdin=subprocess.DEVNULL, start_new_session=True) ++ ++ try: ++ for _ in range(50): ++ if daemon_ns.poll() is not None: ++ test_fail("Namespace Daemon crashed immediately upon startup.") ++ try: ++ with socket.create_connection(('127.0.0.1', port_ns), timeout=0.1): ++ break # Port is open, daemon is ready ++ except OSError: ++ time.sleep(0.1) ++ else: ++ test_fail("Namespace Daemon failed to bind to port within the timeout.") ++ ++ time.sleep(0.2) ++ ++ finally: ++ kill_daemon(daemon_ns) ++ ++ out_ns_data = out_ns.read_text() if out_ns.exists() else "" ++ err_ns_data = err_ns.read_text() if err_ns.exists() else "" ++ ++ if "rsyncd version" not in out_ns_data: ++ test_fail(f"Bug reproduced: Namespace Daemon silently dropped logs.\n'out' file: {out_ns_data}\n'err' file: {err_ns_data}") ++ ++ print("Test 2 Passed: Daemon successfully logged inside unprivileged namespace.", flush=True) ++ ++finally: ++ rmtree(ws_base) ++ ++raise SystemExit(0) +diff --git a/testsuite/pseudo-paths_test.py b/testsuite/pseudo-paths_test.py +index f72b48ef2..65dce2459 100644 +--- a/testsuite/pseudo-paths_test.py ++++ b/testsuite/pseudo-paths_test.py +@@ -1,9 +1,11 @@ + """Process substitution /dev/fd/ write pipe pseudo-paths for --log-file must not crash and must successfully write logs, but must be rejected if confined root.""" + ++import os + import shlex + import shutil + import subprocess + import sys ++import tempfile + from pathlib import Path + + from rsyncfns import ( +@@ -134,5 +136,221 @@ + rmtree(base) + test_fail('transfer continued after accepting a trailing pseudo-path component') + ++# ------------------------------------------------------------------------- ++# TEST 3: Standard I/O Symlinks (/dev/stdin, /dev/stdout) - Unconfined ++# ------------------------------------------------------------------------- ++print("Running Test 3: Standard I/O Symlinks (/dev/stdin)...", flush=True) ++ ++rmtree(dest) ++makepath(dest) ++(src / 'stdin_test.txt').write_text('stdin data\n') ++ ++# 3A: --files-from=/dev/stdin ++# Piping printf directly into rsync forces /dev/stdin to resolve to pipe:[N] ++stdin_script = f'printf "stdin_test.txt\\n" | {rsync_base_cmd} --files-from=/dev/stdin {src_path} {dest_path}' ++proc_stdin = subprocess.run([bash, '-c', stdin_script], capture_output=True, text=True, timeout=10) ++ ++if proc_stdin.returncode != 0: ++ test_fail(f'rsync failed to read --files-from=/dev/stdin (rc={proc_stdin.returncode}, stderr={proc_stdin.stderr.strip()!r})') ++ ++if not (dest / 'stdin_test.txt').is_file(): ++ test_fail(f'rsync failed to transfer file specified via /dev/stdin') ++ ++print('Test 3A Passed: rsync successfully read files-from via /dev/stdin', flush=True) ++ ++# ------------------------------------------------------------------------- ++# TEST 3B: Nested Trusted Symlink to /dev/stdin ++# ------------------------------------------------------------------------- ++rmtree(dest) ++makepath(dest) ++ ++symlink_list = base / 'rsync.list' ++if symlink_list.is_symlink() or symlink_list.exists(): ++ symlink_list.unlink() ++ ++# Create the trusted nested symlink pointing to the kernel pipe ++symlink_list.symlink_to('/dev/stdin') ++ ++symlink_script = f'printf "stdin_test.txt\\n" | {rsync_base_cmd} --files-from={shlex.quote(str(symlink_list))} {src_path} {dest_path}' ++proc_symlink = subprocess.run([bash, '-c', symlink_script], capture_output=True, text=True, timeout=10) ++ ++if proc_symlink.returncode != 0: ++ test_fail(f'rsync failed with --files-from=rsync.list -> /dev/stdin (rc={proc_symlink.returncode}, stderr={proc_symlink.stderr.strip()!r})') ++ ++if not (dest / 'stdin_test.txt').is_file(): ++ test_fail(f'rsync failed to follow trusted symlink to /dev/stdin to read files-from list') ++ ++print('Test 3B Passed: rsync successfully resolved a trusted nested symlink to /dev/stdin', flush=True) ++ ++# ------------------------------------------------------------------------- ++# TEST 3C: Spoofed Relative Path to Pseudo-pipe (Spoofed dev/fd/x) ++# ------------------------------------------------------------------------- ++print("Running Test 3C: Spoofed Relative Path to Pseudo-pipe (dev/fd/99)...", flush=True) ++ ++rmtree(dest) ++makepath(dest) ++ ++# Create a local, relative 'dev/fd' structure inside our working directory ++spoofed_dev_fd = base / 'dev' / 'fd' ++makepath(spoofed_dev_fd) ++ ++# Create a real file called 'pipe:[' with valid data ++spoofed_target = spoofed_dev_fd / 'pipe:[' ++spoofed_target.write_text('transfer_me.txt\n') ++ ++# Create a symlink named '99' that points to the literal file 'pipe:[' ++spoofed_symlink = spoofed_dev_fd / '99' ++if spoofed_symlink.is_symlink() or spoofed_symlink.exists(): ++ spoofed_symlink.unlink() ++spoofed_symlink.symlink_to('pipe:[') ++ ++# Run rsync using --files-from targeting the spoofed path. ++spoofed_script = f'cd {shlex.quote(str(base))} && {rsync_base_cmd} --files-from=dev/fd/99 {src_path} {dest_path}' ++proc_spoofed = subprocess.run([bash, '-c', spoofed_script], capture_output=True, text=True, timeout=10) ++ ++stderr_lower = proc_spoofed.stderr.lower() ++ ++if "levels of symbolic links" in stderr_lower or "eloop" in stderr_lower: ++ test_fail(f'rsync failed with non-expected ELOOP error. The spoofed path was not treated as a normal file. (rc={proc_spoofed.returncode}, stderr={proc_spoofed.stderr.strip()!r})') ++ ++if proc_spoofed.returncode != 0: ++ test_fail(f'rsync unexpectedly did not complete the transfer! (stderr={proc_spoofed.stderr.strip()!r})') ++ ++print('Test 3C Passed: rsync correctly treated the un-anchored spoofed path as a normal file rather than a kernel pseudo-path.', flush=True) ++ ++# ------------------------------------------------------------------------- ++# SETUP FOR NAMESPACE TESTS (TEST 4) ++# ------------------------------------------------------------------------- ++unshare = shutil.which('unshare') ++if unshare is None: ++ print('unshare is unavailable') ++ rmtree(base) ++ raise SystemExit(0) ++ ++launcher = [] ++if os.geteuid() == 0: ++ setpriv = shutil.which('setpriv') ++ if setpriv is None: ++ print('setpriv is unavailable for the root-run testsuite') ++ rmtree(base) ++ raise SystemExit(0) ++ launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups'] ++ ++unshare_argv = [unshare, '--user', '--map-root-user', '--mount', '--pid', ++ '--fork', '--mount-proc'] ++ ++probe = subprocess.run( ++ launcher + unshare_argv + ['true'], ++ stdin=subprocess.DEVNULL, ++ stdout=subprocess.DEVNULL, ++ stderr=subprocess.DEVNULL, ++) ++if probe.returncode != 0: ++ rmtree(base) ++ print(f'user namespaces unavailable (rc={probe.returncode})') ++ raise SystemExit(0) ++ ++# Verify the namespace exposes the overflow UID using the exact logic from the original test ++check_ns = ( ++ "import os\n" ++ "proc_uid = os.lstat('/proc/self').st_uid\n" ++ "if proc_uid in (0, os.geteuid()):\n" ++ " exit(22)\n" ++) ++probe_uid = subprocess.run(launcher + unshare_argv + [sys.executable, '-c', check_ns]) ++if probe_uid.returncode == 22: ++ rmtree(base) ++ print('/proc/self does not expose an overflow uid in this namespace') ++ raise SystemExit(0) ++ ++# Pivot workspace for dropped root privileges ++ws_base = Path(tempfile.mkdtemp(prefix='rsync-unshare-')) ++ws_base.chmod(0o777) ++ ++try: ++ ws_src = ws_base / 'src' ++ ws_dest = ws_base / 'dest' ++ makepath(ws_src, ws_dest) ++ ws_src.chmod(0o777) ++ ws_dest.chmod(0o777) ++ ++ tf = ws_src / 'transfer_me.txt' ++ tf.write_text('sync this\n') ++ tf.chmod(0o777) ++ ++ sf = ws_src / 'stdin_test.txt' ++ sf.write_text('stdin data\n') ++ sf.chmod(0o777) ++ ++ local_bin = ws_base / 'rsync-bin' ++ shutil.copy2(rsync_argv()[0], local_bin) ++ local_bin.chmod(0o777) ++ ++ cmd_prefix = shlex.join([str(local_bin), '-a']) ++ s_path = shlex.quote(str(ws_src) + '/') ++ d_path = shlex.quote(str(ws_dest) + '/') ++ ++ # ------------------------------------------------------------------------- ++ # TEST 4A: User Namespace Overflow UID with Process Substitution ++ # ------------------------------------------------------------------------- ++ # Use process substitution <(...) which resolves to /dev/fd/N ++ inner_script_4a = f'{cmd_prefix} --no-o --no-g --files-from=<(printf "stdin_test.txt\\n") {s_path} {d_path}' ++ unshare_cmd_4a = launcher + unshare_argv + [bash, '-c', inner_script_4a] ++ ++ proc_unshare_4a = subprocess.run( ++ unshare_cmd_4a, ++ capture_output=True, ++ text=True, ++ timeout=10, ++ ) ++ ++ ctx_unshare_4a = f'rc={proc_unshare_4a.returncode}, stderr={proc_unshare_4a.stderr.strip()!r}' ++ ++ if proc_unshare_4a.returncode != 0: ++ test_fail(f'rsync failed reading process substitution pseudo-path inside user namespace ({ctx_unshare_4a})') ++ ++ if not (ws_dest / 'stdin_test.txt').is_file(): ++ test_fail(f'rsync failed to transfer file specified via process substitution inside user namespace ({ctx_unshare_4a})') ++ ++ print('Test 4A Passed: rsync successfully resolved process substitution pseudo-paths inside user namespace', flush=True) ++ ++ # ------------------------------------------------------------------------- ++ # TEST 4B: Nested Trusted Symlink to /dev/stdin in User Namespace ++ # ------------------------------------------------------------------------- ++ # Clear the destination so we have fresh files to transfer ++ rmtree(ws_dest) ++ makepath(ws_dest) ++ ws_dest.chmod(0o777) ++ ++ symlink_list_ns = ws_base / 'rsync.list' ++ if symlink_list_ns.is_symlink() or symlink_list_ns.exists(): ++ symlink_list_ns.unlink() ++ ++ # Create the trusted nested symlink pointing to the kernel pipe ++ symlink_list_ns.symlink_to('/dev/stdin') ++ ++ if os.geteuid() == 0: ++ os.lchown(symlink_list_ns, 65534, 65534) ++ ++ inner_script_4b = f'printf "stdin_test.txt\\n" | {cmd_prefix} --no-o --no-g --files-from={shlex.quote(str(symlink_list_ns))} {s_path} {d_path}' ++ unshare_cmd_4b = launcher + unshare_argv + [bash, '-c', inner_script_4b] ++ ++ proc_unshare_4b = subprocess.run( ++ unshare_cmd_4b, ++ capture_output=True, ++ text=True, ++ timeout=10, ++ ) ++ ctx_unshare_4b = f'rc={proc_unshare_4b.returncode}, stderr={proc_unshare_4b.stderr.strip()!r}' ++ if proc_unshare_4b.returncode != 0: ++ test_fail(f'rsync failed with --files-from=rsync.list -> /dev/stdin inside user namespace ({ctx_unshare_4b})') ++ ++ if not (ws_dest / 'stdin_test.txt').is_file(): ++ test_fail(f'rsync failed to transfer the file specified via nested /dev/stdin symlink in namespace ({ctx_unshare_4b})') ++ print('Test 4B Passed: rsync successfully resolved a trusted nested symlink to /dev/stdin in user namespace', flush=True) ++ ++finally: ++ rmtree(ws_base) ++ + rmtree(base) + raise SystemExit(0) +diff --git a/testsuite/skiplist/almalinux-8.txt b/testsuite/skiplist/almalinux-8.txt +index 71e0d65ba..c814b9563 100644 +--- a/testsuite/skiplist/almalinux-8.txt ++++ b/testsuite/skiplist/almalinux-8.txt +@@ -6,4 +6,5 @@ + # AlmaLinux 8 container additions to common.txt and linux.txt. + + pseudo-paths # Bash process substitution is unavailable in the AlmaLinux 8 container ++pseudo-paths-daemon + read-batch-pipe +diff --git a/testsuite/skiplist/cygwin.txt b/testsuite/skiplist/cygwin.txt +index 4a56c4229..f1a693e75 100644 +--- a/testsuite/skiplist/cygwin.txt ++++ b/testsuite/skiplist/cygwin.txt +@@ -52,6 +52,7 @@ partial-protected-regular-retry-policy # deterministic partial EACCES recovery + password-file-symlink + protected-regular + pseudo-paths ++pseudo-paths-daemon + read-batch-pipe + rename-mixed-parent-transfer + rrsync-sender-leaf-flip +diff --git a/testsuite/skiplist/macos.txt b/testsuite/skiplist/macos.txt +index 2a868dfab..6a82dbf6a 100644 +--- a/testsuite/skiplist/macos.txt ++++ b/testsuite/skiplist/macos.txt +@@ -22,6 +22,7 @@ partial-protected-regular-retry-linux + preallocate + protected-regular + pseudo-paths # dynamically skips on runners lacking bash process substitution ++pseudo-paths-daemon + read-batch-pipe + readonly-partial-abort-mode-regression # + rrsync-sender-leaf-flip diff -Nru rsync-3.4.1+ds1/debian/patches/Honor_STRIP_in_install-strip_for_cross-compilation.patch rsync-3.5.0+ds1/debian/patches/Honor_STRIP_in_install-strip_for_cross-compilation.patch --- rsync-3.4.1+ds1/debian/patches/Honor_STRIP_in_install-strip_for_cross-compilation.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/Honor_STRIP_in_install-strip_for_cross-compilation.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,70 @@ +From a49f085a4fad61ba9dc4afc925a603888c4c3123 Mon Sep 17 00:00:00 2001 +From: Alessandro Di Nepi +Date: Sun, 16 Aug 2026 13:14:14 +0200 +Subject: [PATCH] Honor $(STRIP) in install-strip for cross-compilation (#1024) + +* Honor $(STRIP) in install-strip for cross-compilation + +The install-strip target hard-coded `install -s`, which strips via the +install program using the build host's strip and ignores the STRIP +variable. When cross-compiling this runs the host strip against a +target binary and fails. + +Pass --strip-program=$(or $(STRIP),strip) so the target strip is used +when STRIP is set (as cross toolchains and build systems provide), +falling back to plain `strip` for native builds. A plain `make install` +is unaffected. + +* Make install-strip portable (address review) + +- Detect the target strip via AC_CHECK_TOOL([STRIP],[strip],[strip]) in + configure.ac (picks up the cross-prefixed strip when cross-compiling, + defaults to plain strip otherwise) and substitute @STRIP@ in Makefile.in. +- Rewrite install-strip to run a normal install then $(STRIP) on the + installed rsync binary, dropping the GNU Make $(or ...) and the GNU + install --strip-program extension that broke with install-sh/BSD install. + +Co-Authored-By: Claude Opus 4.8 (1M context) + +--------- + +Co-authored-by: Claude Opus 4.8 (1M context) +--- + Makefile.in | 4 +++- + configure.ac | 1 + + 2 files changed, 4 insertions(+), 1 deletion(-) + +diff --git a/Makefile.in b/Makefile.in +index 801e8643e..716171db1 100644 +--- a/Makefile.in ++++ b/Makefile.in +@@ -21,6 +21,7 @@ LIBOBJDIR=lib/ + + INSTALLCMD=@INSTALL@ + INSTALLMAN=@INSTALL@ ++STRIP=@STRIP@ + + srcdir=@srcdir@ + MKDIR_P=@MKDIR_P@ +@@ -116,7 +117,8 @@ install-ssl-daemon: stunnel-rsyncd.conf + install-all: install install-ssl-daemon + + install-strip: +- $(MAKE) INSTALL_STRIP='-s' install ++ $(MAKE) install ++ $(STRIP) $(DESTDIR)$(bindir)/rsync$(EXEEXT) + + .PHONY: uninstall + uninstall: +diff --git a/configure.ac b/configure.ac +index 57cf2828d..2131bfef0 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -59,6 +59,7 @@ AC_PROG_CXX + AC_PROG_AWK + AC_PROG_EGREP + AC_PROG_INSTALL ++AC_CHECK_TOOL([STRIP], [strip], [strip]) + AC_PROG_MKDIR_P + AC_SUBST(SHELL) + AC_PATH_PROG([PERL], [perl]) diff -Nru rsync-3.4.1+ds1/debian/patches/batch_c_Allow_FIFO_pipes_in_batch_file_processing.patch rsync-3.5.0+ds1/debian/patches/batch_c_Allow_FIFO_pipes_in_batch_file_processing.patch --- rsync-3.4.1+ds1/debian/patches/batch_c_Allow_FIFO_pipes_in_batch_file_processing.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/batch_c_Allow_FIFO_pipes_in_batch_file_processing.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,169 @@ +From a93490077daecd686f84959f279d7072ac480939 Mon Sep 17 00:00:00 2001 +From: Omar Elsayed +Date: Thu, 3 Sep 2026 10:03:54 +0300 +Subject: [PATCH] batch.c: Allow FIFO pipes in batch file processing. + Regression fix (#1060) + +--- + batch.c | 8 +-- + testsuite/batch-file-symlink_test.py | 2 +- + testsuite/read-batch-pipe_test.py | 80 ++++++++++++++++++++++++++++ + testsuite/skiplist/almalinux-8.txt | 1 + + testsuite/skiplist/cygwin.txt | 1 + + testsuite/skiplist/macos.txt | 1 + + 6 files changed, 88 insertions(+), 5 deletions(-) + create mode 100644 testsuite/read-batch-pipe_test.py + +diff --git a/batch.c b/batch.c +index a4d19c058..502c6ef6c 100644 +--- a/batch.c ++++ b/batch.c +@@ -271,12 +271,12 @@ void open_batch_files(void) + exit_cleanup(RERR_FILEIO); + } + +- /* --read-batch: the file's bytes drive the protocol parser, so refuse +- * non-regular files (FIFO, device, socket) at the batch path. */ ++ /* --read-batch: the file's bytes drive the protocol parser, ++ * allow FIFOs used by shell process substitution while continuing to reject other non-regular inputs. */ + if (!write_batch && batch_fd != STDIN_FILENO) { + STRUCT_STAT st; +- if (do_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode)) { +- rprintf(FERROR, "Batch file %s is not a regular file\n", ++ if (do_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode) && !S_ISFIFO(st.st_mode)) { ++ rprintf(FERROR, "Batch file %s is neither a regular file nor a FIFO\n", + full_fname(batch_name)); + exit_cleanup(RERR_FILEIO); + } +diff --git a/testsuite/batch-file-symlink_test.py b/testsuite/batch-file-symlink_test.py +index f26160dd0..d169152d3 100644 +--- a/testsuite/batch-file-symlink_test.py ++++ b/testsuite/batch-file-symlink_test.py +@@ -209,7 +209,7 @@ def run_write_batch(batch_plant_path): + # node fails first with ENXIO, which is an equally valid refusal at the open. + # A clean (returncode 0) run would be the real failure -- rsync accepting the + # device as a batch file. +-refused = ('is not a regular file' in proc.stderr ++refused = ('is neither a regular file nor a FIFO' in proc.stderr + or ('open error' in proc.stderr and proc.returncode != 0)) + if not refused: + test_fail( +diff --git a/testsuite/read-batch-pipe_test.py b/testsuite/read-batch-pipe_test.py +new file mode 100644 +index 000000000..b40638313 +--- /dev/null ++++ b/testsuite/read-batch-pipe_test.py +@@ -0,0 +1,80 @@ ++#!/usr/bin/env python3 ++"""--read-batch process substitution /dev/fd/ pipe must not crash with strict file-type checks.""" ++import os ++import shlex ++import shutil ++import subprocess ++import tempfile ++import sys ++from pathlib import Path ++ ++from rsyncfns import SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail, test_skipped ++ ++# We require bash specifically because standard POSIX /bin/sh does not ++# guarantee support for <(...) process substitution syntax. ++if not sys.platform.startswith('linux'): ++ test_skipped('This test requires Linux platform') ++ ++bash = shutil.which('bash') ++if bash is None: ++ test_skipped('bash is unavailable, cannot test process substitution') ++ ++# Verify the host bash actually supports process substitution ++probe = subprocess.run( ++ [bash, '-c', 'cat <(echo "probe")'], ++ capture_output=True) ++ ++if probe.returncode != 0: ++ test_skipped('bash process substitution is not supported on this system') ++ ++base = Path(SCRATCHDIR / 'rsync-batch-fifo') ++src = base / 'src' ++dest = base / 'dest' ++batch_file = base / 'update.batch' ++makepath(src, dest) ++ ++# 1. Create dummy data ++(src / 'payload.txt').write_text('batch payload data\n') ++ ++# 2. Generate a valid batch file so `cat` actually has a real file to read. ++# Note: This operation also copies the file to `dest` as a side effect. ++subprocess.run([*rsync_argv('-a', f'--write-batch={batch_file}'), f'{src}/', f'{dest}/'], check=True) ++ ++# must wipe and recreate the destination directory so the test can ++# properly prove that --read-batch recreates the files from scratch. ++rmtree(dest) ++makepath(dest) ++ ++# 3. Now we can test reading it via bash process substitution ++rsync_base_cmd = shlex.join(rsync_argv('-a')) ++batch_path = shlex.quote(str(batch_file)) ++dest_path = shlex.quote(str(dest) + '/') ++ ++# Construct the bash command: rsync -a --read-batch=<(cat /path/to/batch) /dest/ ++bash_script = f"{rsync_base_cmd} --read-batch=<(cat {batch_path}) {dest_path}" ++ ++try: ++ proc_read = subprocess.run( ++ [bash, '-c', bash_script], ++ capture_output=True, ++ text=True, ++ timeout=10, ++ ) ++except subprocess.TimeoutExpired: ++ rmtree(base) ++ test_fail('process substitution batch test timed out') ++ ++ctx = f'rc={proc_read.returncode}, stderr={proc_read.stderr.strip()!r}' ++ ++# Evaluate result against the strict S_ISREG check bug ++if proc_read.returncode != 0: ++ rmtree(base) ++ test_fail(f'rsync crashed reading batch file from pipe ({ctx})') ++ ++if not (dest / 'payload.txt').is_file(): ++ rmtree(base) ++ test_fail(f'rsync exited successfully but payload is missing in target ({ctx})') ++ ++rmtree(base) ++print('rsync successfully parsed batch stream via process substitution pseudo-path') ++raise SystemExit(0) +diff --git a/testsuite/skiplist/almalinux-8.txt b/testsuite/skiplist/almalinux-8.txt +index 483ac8cef..71e0d65ba 100644 +--- a/testsuite/skiplist/almalinux-8.txt ++++ b/testsuite/skiplist/almalinux-8.txt +@@ -6,3 +6,4 @@ + # AlmaLinux 8 container additions to common.txt and linux.txt. + + pseudo-paths # Bash process substitution is unavailable in the AlmaLinux 8 container ++read-batch-pipe +diff --git a/testsuite/skiplist/cygwin.txt b/testsuite/skiplist/cygwin.txt +index 23456d25e..4a56c4229 100644 +--- a/testsuite/skiplist/cygwin.txt ++++ b/testsuite/skiplist/cygwin.txt +@@ -52,6 +52,7 @@ partial-protected-regular-retry-policy # deterministic partial EACCES recovery + password-file-symlink + protected-regular + pseudo-paths ++read-batch-pipe + rename-mixed-parent-transfer + rrsync-sender-leaf-flip + rrsync-sender-parent-pin +diff --git a/testsuite/skiplist/macos.txt b/testsuite/skiplist/macos.txt +index 75b292617..2a868dfab 100644 +--- a/testsuite/skiplist/macos.txt ++++ b/testsuite/skiplist/macos.txt +@@ -22,6 +22,7 @@ partial-protected-regular-retry-linux + preallocate + protected-regular + pseudo-paths # dynamically skips on runners lacking bash process substitution ++read-batch-pipe + readonly-partial-abort-mode-regression # + rrsync-sender-leaf-flip + rrsync-sender-parent-pin diff -Nru rsync-3.4.1+ds1/debian/patches/disable_reconfigure_req.diff rsync-3.5.0+ds1/debian/patches/disable_reconfigure_req.diff --- rsync-3.4.1+ds1/debian/patches/disable_reconfigure_req.diff 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/disable_reconfigure_req.diff 2026-09-16 01:46:30.000000000 +0000 @@ -10,10 +10,10 @@ 1 file changed, 20 deletions(-) diff --git a/Makefile.in b/Makefile.in -index 7c75c26..96d7d3a 100644 +index 801e864..813c0b2 100644 --- a/Makefile.in +++ b/Makefile.in -@@ -204,15 +204,6 @@ configure.sh config.h.in: configure.ac aclocal.m4 +@@ -285,15 +285,6 @@ configure.sh config.h.in: configure.ac aclocal.m4 else \ echo "config.h.in has CHANGED."; \ fi @@ -29,7 +29,7 @@ .PHONY: reconfigure reconfigure: configure.sh -@@ -226,17 +217,6 @@ restatus: +@@ -307,17 +298,6 @@ restatus: Makefile: Makefile.in config.status configure.sh config.h.in @if test -f Makefile; then cp -p Makefile Makefile.old; else touch Makefile.old; fi @./config.status diff -Nru rsync-3.4.1+ds1/debian/patches/env_shebang.patch rsync-3.5.0+ds1/debian/patches/env_shebang.patch --- rsync-3.4.1+ds1/debian/patches/env_shebang.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/env_shebang.patch 2026-09-16 01:46:30.000000000 +0000 @@ -32,7 +32,7 @@ # that the code in options.c might send to the server. The resulting code is then # included in the rrsync script. diff --git a/rsync-ssl b/rsync-ssl -index 56ee7df..d76f0ab 100755 +index 8aa2ae6..3880f99 100755 --- a/rsync-ssl +++ b/rsync-ssl @@ -1,4 +1,4 @@ @@ -82,7 +82,7 @@ # directives that can be used by rsync to copy just the indicated files using # an --exclude-from=FILE or -f'. FILE' option. To be able to delete files on diff --git a/support/git-set-file-times b/support/git-set-file-times -index e06f073..56d4ba0 100755 +index 23fdbd1..20d613f 100755 --- a/support/git-set-file-times +++ b/support/git-set-file-times @@ -1,4 +1,4 @@ @@ -90,7 +90,7 @@ +#!/usr/bin/python3 import os, re, argparse, subprocess - from datetime import datetime + from datetime import datetime, timezone diff --git a/support/logfilter b/support/logfilter index 29cfe69..282914a 100755 --- a/support/logfilter @@ -132,7 +132,7 @@ # "/rsyncd-munged/" or remove that prefix. diff --git a/support/rrsync b/support/rrsync -index e8b0cc0..d3ccd48 100755 +index e114570..13549a1 100755 --- a/support/rrsync +++ b/support/rrsync @@ -1,4 +1,4 @@ @@ -142,7 +142,7 @@ # Restricts rsync to subdirectory declared in .ssh/authorized_keys. See # the rrsync man page for details of how to make use of this script. diff --git a/support/rsync-no-vanished b/support/rsync-no-vanished -index b31a5d2..c85d488 100755 +index 892c90b..8da38e7 100755 --- a/support/rsync-no-vanished +++ b/support/rsync-no-vanished @@ -1,4 +1,4 @@ diff -Nru rsync-3.4.1+ds1/debian/patches/fix-flaky-hardlinks-test.patch rsync-3.5.0+ds1/debian/patches/fix-flaky-hardlinks-test.patch --- rsync-3.4.1+ds1/debian/patches/fix-flaky-hardlinks-test.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/fix-flaky-hardlinks-test.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,60 +0,0 @@ -From: Arnaud Rebillout -Date: Thu, 6 Mar 2025 10:54:05 +0700 -Subject: Fix flaky hardlinks test - -The test was added in dc34990, it turns out that it's flaky. It failed -once on the Debian build infra, cf. [1]. - -The problem is that the command `rsync -aH '$fromdir/sym' '$todir'` -updates the mod time of `$todir`, so there might be a diff between the -output of `rsync_ls_lR $fromdir` and `rsync_ls_lR $todir`, if ever rsync -runs 1 second (or more) after the directories were created. - -To clarify: it's easy to make the test fails 100% of the times with this -change: - -``` - makepath "$fromdir/sym" "$todir" -+sleep 5 - checkit "$RSYNC -aH '$fromdir/sym' '$todir'" "$fromdir" "$todir" -``` - -With the fix proposed here, we don't use `checkit` anymore, instead we -just run the rsync command, then a simple `diff` to compare the two -directories. This is exactly what the other `-H` test just above does. - -In case there's some doubts, `diff` fails if `sym` is missing: - -``` -$ mkdir -p foo/sym bar -$ diff foo bar || echo KO! -Only in foo: sym -KO! -``` - -I tested that, after this commit, the test still catches the `-H` -regression in rsync 3.4.0. - -Fixes: https://github.com/RsyncProject/rsync/issues/735 - -[1]: https://buildd.debian.org/status/fetch.php?pkg=rsync&arch=ppc64el&ver=3.4.1%2Bds1-1&stamp=1741147156&raw=0 - -Forwarded: https://github.com/RsyncProject/rsync/pull/737 ---- - testsuite/hardlinks.test | 3 ++- - 1 file changed, 2 insertions(+), 1 deletion(-) - -diff --git a/testsuite/hardlinks.test b/testsuite/hardlinks.test -index 68fd270..c02db3f 100644 ---- a/testsuite/hardlinks.test -+++ b/testsuite/hardlinks.test -@@ -81,7 +81,8 @@ diff $diffopt "$name1" "$todir" || test_fail "solo copy of name1 failed" - # enabled (this has broken in 3.4.0 so far, so we need this test). - rm -rf "$fromdir" "$todir" - makepath "$fromdir/sym" "$todir" --checkit "$RSYNC -aH '$fromdir/sym' '$todir'" "$fromdir" "$todir" -+$RSYNC -aH "$fromdir/sym" "$todir" -+diff $diffopt "$fromdir" "$todir" || test_fail "solo copy of sym failed" - - # The script would have aborted on error, so getting here means we've won. - exit 0 diff -Nru rsync-3.4.1+ds1/debian/patches/fix_rrsync_man_generation.patch rsync-3.5.0+ds1/debian/patches/fix_rrsync_man_generation.patch --- rsync-3.4.1+ds1/debian/patches/fix_rrsync_man_generation.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/fix_rrsync_man_generation.patch 2026-09-16 01:46:30.000000000 +0000 @@ -12,7 +12,7 @@ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/maybe-make-man b/maybe-make-man -index c7af739..d0b9660 100755 +index b751ebc..1888559 100755 --- a/maybe-make-man +++ b/maybe-make-man @@ -23,12 +23,8 @@ if [ ! -f "$flagfile" ]; then diff -Nru rsync-3.4.1+ds1/debian/patches/options_accept_max_alloc_0_again_resolved_to_the_parsers_own_ceiling.patch rsync-3.5.0+ds1/debian/patches/options_accept_max_alloc_0_again_resolved_to_the_parsers_own_ceiling.patch --- rsync-3.4.1+ds1/debian/patches/options_accept_max_alloc_0_again_resolved_to_the_parsers_own_ceiling.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/options_accept_max_alloc_0_again_resolved_to_the_parsers_own_ceiling.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,383 @@ +From c529eccff7e45a995fb2180d122e701fe530c5bb Mon Sep 17 00:00:00 2001 +From: Samuel Henrique +Date: Wed, 2 Sep 2026 14:42:34 -0700 +Subject: [PATCH] options: accept --max-alloc=0 again, resolved to the parser's + own ceiling (#1069) + +* options: accept --max-alloc=0 again, resolved to the parser's own ceiling + +3.5.0 rejected --max-alloc=0 (CVE-2026-53794). The stated rationale was that a +zero cap "disabled the per-allocation size cap (the defense behind +CVE-2024-12084)". That was true up to 3.2.7, where the check short-circuited: + + if (max_alloc && num >= max_alloc/size) + +but 2f9b963a ("Make `--max-alloc=0` safer", 3.3.0) removed the short-circuit and +mapped 0 to SIZE_MAX at parse time, leaving the guard unconditional. Since the +guard admits an allocation only when num < max_alloc/size, num*size stays below +max_alloc at every setting, so the num*size overflow check was armed for 0 just +as for any other value. From 3.3.0 onward, 0 raised the magnitude ceiling and +nothing else -- and an explicit 8191P raises it exactly as far, is accepted, and +is forwarded to the peer, so rejecting 0 removed no capability. + +What it did remove is the only portable spelling. The parser's ceiling is +SIZE_MAX/2, so it tracks the build's word size: on ILP32 the suffix multiplier +alone exceeds the bound, making every P and T value an error whatever the digits +and capping the option at 2047M. Because max_alloc_arg goes on the wire +un-normalized, a 0 was re-resolved by each side against its own SIZE_MAX; any +literal is resolved once on the client and shipped verbatim, so nothing above +2047M survives a 64-bit client talking to a 32-bit daemon. There is no number a +user can compute that does what 0 did, which is what #1056 ran into. + +So accept 0 again, but resolve it to SIZE_ARG_MAX (SIZE_MAX/2) rather than +SIZE_MAX, so it lands exactly on the largest value that could also be typed and +is no longer a limit only the 0 spelling can reach. Keep forwarding it +verbatim: that per-side resolution is the property worth having. This leaves +the substantive half of the 3.5.0 hardening -- bounding parse_size_arg() against +the unbounded `size *= atof(size_arg)` -- untouched. + +The residual concern, a 0 forwarded to a <= 3.2.7 daemon that honours it, is not +something a client-side check can address: the client is the attacker's own +code, as daemon-max-alloc-zero_test.py noted in its own docstring. Operators +who want peers kept off their cap have `refuse options = max-alloc`. + +Also drops the now-unreachable rejection message, which left the min-value error +recommending a value the parser refused ("min: 1.00M or 0 for unlimited"). + +Tests: max-alloc-zero replaces max-alloc-zero-rejected and +daemon-max-alloc-zero, whose assertions are the behaviour being reverted. It +checks that 0 is accepted, that it reaches the peer as the literal "0" rather +than a resolved number (verified against a negative control that normalizes it), +and that the parser's upper bound still rejects an out-of-range value. + +Fixes #1056 + +Co-Authored-By: Claude Opus 5 (1M context) + +* testsuite: refresh Cygwin expectations +* docs: note max-alloc safety +--- + options.c | 21 ++++-- + rsync.1.md | 22 ++++-- + testsuite/daemon-max-alloc-zero_test.py | 83 --------------------- + testsuite/max-alloc-zero-rejected_test.py | 9 --- + testsuite/max-alloc-zero_test.py | 88 +++++++++++++++++++++++ + testsuite/skiplist/cygwin.txt | 2 - + testsuite/skiplist/macos.txt | 1 - + 7 files changed, 119 insertions(+), 107 deletions(-) + delete mode 100644 testsuite/daemon-max-alloc-zero_test.py + delete mode 100644 testsuite/max-alloc-zero-rejected_test.py + create mode 100755 testsuite/max-alloc-zero_test.py + +diff --git a/options.c b/options.c +index 0efd02eb0..e222fa58a 100644 +--- a/options.c ++++ b/options.c +@@ -1157,6 +1157,12 @@ static int count_args(const char **argv) + return i; + } + ++/* The largest value parse_size_arg() will accept when no explicit max_value is ++ * given. It is SIZE_MAX/2 rather than SIZE_MAX because the parser computes and ++ * returns the size as a signed ssize_t (with a negative return meaning error), ++ * so this keeps every accepted size representable as a positive ssize_t. */ ++#define SIZE_ARG_MAX ((ssize_t)(SIZE_MAX / 2)) ++ + /* If the size_arg is an invalid string or the value is < min_value, an error + * is put into err_buf & the return is -1. Note that this parser does NOT + * support negative numbers, so a min_value < 0 doesn't make any sense. */ +@@ -1166,7 +1172,7 @@ static ssize_t parse_size_arg(const char *size_arg, char def_suf, const char *op + int reps, mult, len; + const char *arg, *err = "invalid", *min_max = NULL; + ssize_t limit = -1, size = 1; +- ssize_t size_max = max_value >= 0 ? max_value : (ssize_t)(SIZE_MAX / 2); ++ ssize_t size_max = max_value >= 0 ? max_value : SIZE_ARG_MAX; + double dsize; + + for (arg = size_arg; isDigit(arg); arg++) {} +@@ -2067,14 +2073,17 @@ int parse_arguments(int *argc_p, const char ***argv_p) + ssize_t size = parse_size_arg(max_alloc_arg, 'B', "max-alloc", 1024*1024, -1, True); + if (size < 0) + goto cleanup; +- if (size == 0) { +- snprintf(err_buf, sizeof err_buf, "max-alloc must be greater than zero\n"); +- goto cleanup; +- } + max_alloc = size; + } ++ /* A 0 value means "as large as this build allows". We resolve it to the ++ * same ceiling parse_size_arg() enforces, so that --max-alloc=0 is exactly ++ * the largest value a user could also have typed, and never a limit that ++ * only the 0 spelling can reach. Note that max_alloc_arg is forwarded to ++ * the peer un-normalized (see server_options()), which is what lets each ++ * side resolve 0 against its own SIZE_MAX -- a 64-bit client and a 32-bit ++ * daemon each get their own ceiling from the one portable spelling. */ + if (!max_alloc) +- max_alloc = SIZE_MAX; ++ max_alloc = SIZE_ARG_MAX; + + if (old_style_args < 0) { + if (!am_server && protect_args <= 0 && (arg = getenv("RSYNC_OLD_ARGS")) != NULL && *arg) { +diff --git a/rsync.1.md b/rsync.1.md +index ad686b646..41b134efe 100644 +--- a/rsync.1.md ++++ b/rsync.1.md +@@ -2339,12 +2339,22 @@ sign) if you want the local shell to expand it. + See the [`--max-size`](#opt) option for a description of how SIZE can be + specified. The default suffix if none is given is bytes. + +- Beginning in 3.2.7, a value of 0 was an easy way to specify SIZE_MAX (the +- largest limit possible). However, beginning with 3.5.0, a value of 0 is +- rejected as invalid for security reasons (a 0-byte cap could be used to +- disable the allocation limit, which could lead to a denial-of-service via +- memory exhaustion). Use an explicit very large value if you want a very +- high limit. ++ A value of 0 is an easy way to say "the largest limit this build supports". ++ It resolves to the same ceiling an explicit SIZE is checked against, so it ++ is never a higher limit than one you could have typed out yourself. ++ ++ Because the option is passed to the remote rsync as you wrote it, each side ++ resolves a 0 against its own maximum. That makes 0 the only spelling that ++ is correct for both ends of a transfer between hosts of different word ++ sizes: a literal value large enough to be useful on a 64-bit client is ++ rejected as too large by a 32-bit daemon. ++ ++ A value of 0 was accepted beginning in 3.2.3 and rejected in 3.5.0; the ++ release after 3.5.0 accepts it again. ++ ++ A daemon administrator who does not want clients to change the configured ++ allocation ceiling can set `refuse options = max-alloc` in the module's ++ `rsyncd.conf`. This refuses every client-supplied value, including 0. + + You can set a default value using the environment variable + [`RSYNC_MAX_ALLOC`](#) using the same SIZE values as supported by this +diff --git a/testsuite/daemon-max-alloc-zero_test.py b/testsuite/daemon-max-alloc-zero_test.py +deleted file mode 100644 +index c082c4969..000000000 +--- a/testsuite/daemon-max-alloc-zero_test.py ++++ /dev/null +@@ -1,83 +0,0 @@ +-#!/usr/bin/env python3 +-"""Daemon-mode: the server must reject a wire-supplied --max-alloc=0. +- +-max-alloc-zero-rejected_test.py only proves the *local* client refuses +---max-alloc=0. That alone doesn't protect a daemon: a modified or older client +-still forwards --max-alloc=0 on the wire, and an unpatched daemon honours it and +-disables its my_alloc() allocation cap (the defence behind CVE-2024-12084 and +-friends). This test drives an older rsync client -- which lacks the reject-zero +-check and so forwards the option -- against the current rsync daemon, and +-asserts the *daemon* refuses it. +- +-It uses the in-tree old_versions/rsync_3.2.7 as the client (3.2.7 predates the +-reject-zero fix, so it forwards --max-alloc=0 on the wire). If that binary is +-missing or can't run here (e.g. a non-Linux host that can't run the static +-archive) the test skips. +-""" +- +-import subprocess +-from pathlib import Path +- +-from rsyncfns import ( +- FROMDIR, RSYNC, SCRATCHDIR, +- makepath, rmtree, start_test_daemon, test_fail, test_skipped, +- write_daemon_conf, +-) +- +-DAEMON_PORT = 12932 +-REJECT_MSG = 'max-alloc must be greater than zero' +- +-OLD_CLIENT = Path(__file__).resolve().parents[1] / 'old_versions' / 'rsync_3.2.7' +- +-if not OLD_CLIENT.exists(): +- test_skipped(f"{OLD_CLIENT} not present") +- +-# Confirm the static binary actually runs as rsync on this OS/arch before we +-# depend on it: exec of a foreign-arch/OS binary raises OSError, while one that +-# loads but can't run won't print the rsync banner. (3.2.7 predates the +-# reject-zero fix, so once it runs it forwards --max-alloc=0 on the wire.) +-try: +- probe = subprocess.run([str(OLD_CLIENT), '--version'], +- stdout=subprocess.PIPE, stderr=subprocess.STDOUT, +- text=True) +-except OSError as e: +- test_skipped(f"cannot run {OLD_CLIENT.name} on this OS/arch: {e}") +-if probe.returncode != 0 or 'version 3.2.7' not in probe.stdout: +- test_skipped(f"{OLD_CLIENT.name} does not run as rsync on this OS/arch") +- +-# Module served by the *current* (patched) daemon. +-src = FROMDIR +-rmtree(src) +-makepath(src) +-(src / 'file.txt').write_text('hello\n') +- +-conf = write_daemon_conf([('mod', {'path': str(src), 'read only': 'yes'})]) +-url = start_test_daemon(conf, DAEMON_PORT, rsync_cmd=RSYNC) +- +-dest = SCRATCHDIR / 'out.txt' +- +- +-def run_client(*extra): +- argv = [str(OLD_CLIENT), *extra, f'{url}mod/file.txt', str(dest)] +- return subprocess.run(argv, stdout=subprocess.DEVNULL, +- stderr=subprocess.PIPE, text=True) +- +- +-# Positive control: the old client and current daemon transfer fine without the +-# option, so the failure below is specifically the daemon refusing the option. +-dest.unlink(missing_ok=True) +-ctrl = run_client() +-if ctrl.returncode != 0: +- test_fail(f"old client could not talk to the current daemon:\n{ctrl.stderr}") +- +-# The attack: a forwarded --max-alloc=0 must be refused by the daemon. +-dest.unlink(missing_ok=True) +-proc = run_client('--max-alloc=0') +-if proc.returncode == 0: +- test_fail("daemon accepted a wire-supplied --max-alloc=0") +-if REJECT_MSG not in proc.stderr: +- test_fail("daemon did not reject --max-alloc=0 with the expected message; " +- f"stderr:\n{proc.stderr}") +- +-print("daemon-max-alloc-zero: daemon refuses a wire-supplied --max-alloc=0 " +- f"(client {OLD_CLIENT.name})") +diff --git a/testsuite/max-alloc-zero-rejected_test.py b/testsuite/max-alloc-zero-rejected_test.py +deleted file mode 100644 +index 496d982e2..000000000 +--- a/testsuite/max-alloc-zero-rejected_test.py ++++ /dev/null +@@ -1,9 +0,0 @@ +-#!/usr/bin/env python3 +-from rsyncfns import SCRATCHDIR, rsync_argv +-from rsyncfns import expect_fail +- +-expect_fail( +- rsync_argv('--max-alloc=0', str(SCRATCHDIR / 'missing-src'), str(SCRATCHDIR / 'missing-dst')), +- 'max-alloc must be greater than zero', +-) +-print("max-alloc-zero-rejected: --max-alloc=0 is rejected") +diff --git a/testsuite/max-alloc-zero_test.py b/testsuite/max-alloc-zero_test.py +new file mode 100755 +index 000000000..b73e64ffa +--- /dev/null ++++ b/testsuite/max-alloc-zero_test.py +@@ -0,0 +1,88 @@ ++#!/usr/bin/env python3 ++"""``--max-alloc=0`` means "the largest limit this build supports". ++ ++Three things are asserted, and the second is the reason 0 is worth keeping as a ++spelling at all: ++ ++ 1. 0 is accepted, and a transfer using it works. ++ ++ 2. 0 reaches the peer as the literal "0", not as a resolved number. Each side ++ then resolves it against its own SIZE_MAX. That is what makes 0 the only ++ value correct for both ends of a mixed-word-size pairing: the ceiling is ++ SIZE_MAX/2, so any number large enough to be worth setting on a 64-bit ++ client (over 2047M) is refused as "too large" by a 32-bit daemon. ++ ++ 3. The parser's upper bound is still enforced. Accepting 0 again must not ++ bring back the unbounded ``size *= atof(size_arg)`` that was fixed in ++ 3.5.0, so an out-of-range value is still rejected rather than wrapping. ++ ++The forwarding check in (2) deliberately inspects the argv the remote shell is ++handed rather than a transfer outcome: a resolved number also copies files ++happily on a same-word-size pair, so an outcome-based assertion would pass on ++exactly the configuration this behaviour does not matter for. ++""" ++ ++import os ++import shlex ++ ++from rsyncfns import ( ++ SCRATCHDIR, SRCDIR, expect_fail, rsh_cmd, rmtree, rsync_argv, ++ rsync_path_arg, run_rsync, test_fail, ++) ++ ++base = SCRATCHDIR / 'max-alloc-zero' ++rmtree(base) ++src = base / 'from' ++dst = base / 'to' ++src.mkdir(parents=True) ++dst.mkdir(parents=True) ++(src / 'file.txt').write_text('hello\n') ++ ++# --- 1. 0 is accepted ------------------------------------------------------- ++ ++run_rsync('-r', '--max-alloc=0', f'{src}/', f'{dst}/') ++if (dst / 'file.txt').read_text() != 'hello\n': ++ test_fail('--max-alloc=0 did not copy the file') ++ ++# --- 2. 0 goes on the wire un-normalized ------------------------------------ ++ ++argv_log = base / 'server-argv' ++wrapper = base / 'log-rsh.sh' ++wrapper.write_text( ++ '#!/bin/sh\n' ++ '# Log the command line built for the peer, then behave like lsh.sh.\n' ++ f'printf \'%s\\n\' "$*" >> {shlex.quote(str(argv_log))}\n' ++ f'exec {shlex.quote(str(SRCDIR / "support" / "lsh.sh"))} "$@"\n' ++) ++wrapper.chmod(0o755) ++ ++rmtree(dst) ++dst.mkdir() ++os.environ['RSYNC_RSH'] = rsh_cmd(str(wrapper)) ++run_rsync('-r', '--max-alloc=0', f'--rsync-path={rsync_path_arg()}', ++ f'localhost:{src}/', f'{dst}/') ++del os.environ['RSYNC_RSH'] ++ ++if (dst / 'file.txt').read_text() != 'hello\n': ++ test_fail('--max-alloc=0 did not copy the file over the remote shell') ++ ++logged = argv_log.read_text() if argv_log.exists() else '' ++if not logged: ++ test_fail('the remote-shell wrapper logged no command line') ++if '--max-alloc=0' not in f' {logged} '.replace('\n', ' '): ++ test_fail('--max-alloc=0 was not forwarded verbatim; the peer was sent:\n' ++ f'{logged}' ++ '\nA resolved number here would be rejected as "too large" by a ' ++ 'peer with a smaller SIZE_MAX.') ++ ++# --- 3. the upper bound still holds ----------------------------------------- ++ ++# 8192P is one step past SIZE_ARG_MAX (SIZE_MAX/2) on a 64-bit build; on a ++# 32-bit one the P multiplier alone already exceeds it. Either way: too large. ++expect_fail(rsync_argv('--max-alloc=8192P', f'{src}/', f'{dst}/'), 'is too large') ++ ++# And the min-value message must keep advertising a spelling that works. ++expect_fail(rsync_argv('--max-alloc=1', f'{src}/', f'{dst}/'), ++ 'or 0 for unlimited') ++ ++print('max-alloc-zero: 0 is accepted, forwarded verbatim, and the bound holds') +diff --git a/testsuite/skiplist/cygwin.txt b/testsuite/skiplist/cygwin.txt +index 0d3ee5385..2648c7c4f 100644 +--- a/testsuite/skiplist/cygwin.txt ++++ b/testsuite/skiplist/cygwin.txt +@@ -25,7 +25,6 @@ copy-xattrs-symlink-race + daemon-auth-group + daemon-chroot-munge-default + daemon-config-symlink +-daemon-max-alloc-zero + daemon-module-chdir-symlink + daemon-module-private-parent + daemon-secrets-file-symlink +@@ -63,7 +62,6 @@ sender-remove-source-root-anchor + simd-checksum + source-change-size-continues + symlink-dest-backupdir +-symlink-exclude-xattr + symlink-race-dest + symlink-race-relative-dest + temp-dir-symlink-injection +diff --git a/testsuite/skiplist/macos.txt b/testsuite/skiplist/macos.txt +index f1aff5804..995fb211f 100644 +--- a/testsuite/skiplist/macos.txt ++++ b/testsuite/skiplist/macos.txt +@@ -14,7 +14,6 @@ backup-crossdev-copy + chmod-temp-dir + copy-xattrs-symlink-race + daemon-auth-group +-daemon-max-alloc-zero + dir-sgid + fake-super-acl-xattr + link-dest-symlink-enotsup # the ENOTSUP hard-link hook is an LD_PRELOAD, Linux-only diff -Nru rsync-3.4.1+ds1/debian/patches/options_c_Fix_files-from_confinement_for_local_and_SSH_transfers.patch rsync-3.5.0+ds1/debian/patches/options_c_Fix_files-from_confinement_for_local_and_SSH_transfers.patch --- rsync-3.4.1+ds1/debian/patches/options_c_Fix_files-from_confinement_for_local_and_SSH_transfers.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/options_c_Fix_files-from_confinement_for_local_and_SSH_transfers.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,134 @@ +From 8b8de5232efbf851689d4c2ecd942521cc795d1f Mon Sep 17 00:00:00 2001 +From: Omar Elsayed +Date: Sat, 19 Sep 2026 07:56:36 +0300 +Subject: [PATCH] options.c: Fix --files-from confinement for local and SSH + transfers (#1094) + +* Fix --files-from confinement for local and SSH transfers + +The recent path confinement patch caused an ELOOP error when local or SSH users tried to use a --files-from list located outside the confined root. + +This happened because the code treated the argument as if its always an operator-path peer. CLI arguments provided locally or over SSH are trusted, so the strict boundary check should only apply to untrusted clients connecting to a background daemon. + +* tests: assert local files-from copy + +--------- + +Co-authored-by: Zen Dodd +--- + options.c | 3 +- + testsuite/files-from-leak_test.py | 26 ++++++++++++++++ + testsuite/rrsync-userns-procfs_test.py | 42 +++++++++++++++++--------- + 3 files changed, 55 insertions(+), 16 deletions(-) + +diff --git a/options.c b/options.c +index e222fa58a..3a2ce2bda 100644 +--- a/options.c ++++ b/options.c +@@ -2659,7 +2659,8 @@ int parse_arguments(int *argc_p, const char ***argv_p) + * 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; ++ /* The daemon process is the only case that the files-from path comes from an untrusted argument */ ++ operator_path_resolve = am_daemon ? 1 : 0; + filesfrom_fd = open_no_attacker_symlinks(files_from, O_RDONLY|O_BINARY, 0); + operator_path_resolve = save_opr; + if (filesfrom_fd < 0) { +diff --git a/testsuite/files-from-leak_test.py b/testsuite/files-from-leak_test.py +index e7a03725d..b310860e4 100644 +--- a/testsuite/files-from-leak_test.py ++++ b/testsuite/files-from-leak_test.py +@@ -179,3 +179,29 @@ def root_owned_backup_symlink(): + test_fail(f"setup failed: backup symlink {files_from} -> {tgt}, not the out-of-module secret") + + leak() ++ ++# ---- LOCAL COMMAND CONFINEMENT EDGE CASE ----------------------------------- ++# A local operator invoking --confine-root should still be able to specify a ++# --files-from file that resides outside the confined boundary. The file is ++# opened locally by the operator, not as an operator-path via a daemon, so ++# it should not trip the path-walker ELOOP block. ++ ++local_files_from = root / 'local_files_from.txt' ++local_file = src / 'sub' / 'files-from-local' ++local_file.write_text('local files-from content\n') ++local_files_from.write_text('sub/files-from-local\n') ++ ++local_proc = subprocess.run( ++ rsync_argv('-a', f'--confine-root={base}', f'--files-from={local_files_from}', ++ f'{src}/', f'{dest}/'), ++ stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, ++) ++ ++copied = dest / 'sub' / 'files-from-local' ++if (local_proc.returncode != 0 or not copied.is_file() ++ or copied.read_text() != 'local files-from content\n'): ++ test_fail( ++ "Local --files-from transfer did not copy the listed file while the " ++ "list was outside --confine-root.\n" ++ f"Output: {local_proc.stdout}" ++ ) +diff --git a/testsuite/rrsync-userns-procfs_test.py b/testsuite/rrsync-userns-procfs_test.py +index fa2aaf3dc..7bd1cdb06 100644 +--- a/testsuite/rrsync-userns-procfs_test.py ++++ b/testsuite/rrsync-userns-procfs_test.py +@@ -79,8 +79,9 @@ + test_skipped('/proc/self does not expose an overflow uid in this namespace') + + base = Path(tempfile.mkdtemp(prefix='rsync-userns-procfs-')) +-src = base / 'src' +-dest = base / 'dest' ++root = base / 'root' ++src = root / 'src' ++dest = root / 'dest' + outside = base / 'outside' + makepath(src, dest, outside) + (src / 'file').write_text('content\n') +@@ -112,22 +113,33 @@ + finally: + os.close(dest_fd) + +-outside_list = outside / 'files-from' +-outside_list.write_text('file\n') +-for fd_root in fd_roots: +- outside_fd = os.open(outside_list, os.O_RDONLY) +- try: ++backup_dir = root / 'backup' ++backup_dir.symlink_to(outside) ++(dest / 'test').write_text('old_content\n') ++(src / 'test').write_text('new_different_content\n') ++ ++(outside / 'test').write_text('existing_backup\n') ++ ++# Open an FD pointing to the confined root ++root_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY) ++try: ++ for fd_root in fd_roots: + proc = subprocess.run( +- rsync_argv('-a', f'--confine-root={dest}', +- f'--files-from={fd_root}/{outside_fd}', ++ rsync_argv('-a', '--backup', f'--confine-root={root}', ++ f'--backup-dir={fd_root}/{root_fd}/backup/', + str(src) + '/', str(dest) + '/'), +- pass_fds=(outside_fd,), ++ pass_fds=(root_fd,), + capture_output=True, + text=True, + ) +- finally: +- os.close(outside_fd) +- if proc.returncode == 0 or 'failed to open files-from file' not in proc.stderr: +- test_fail(f'outside {fd_root} pin was not observably refused: ' +- f'rc={proc.returncode}, stderr={proc.stderr!r}') ++ ++ # The transfer must fail because rsync attempts to unlink the pre-existing target file, ++ # forcing the path-walker to evaluate the FD pin + symlink and catching the escape. ++ if proc.returncode == 0: ++ test_fail(f'backup to outside symlink via {fd_root} pin was not observably refused: ' ++ f'rc={proc.returncode}, stderr={proc.stderr!r}') ++finally: ++ os.close(root_fd) ++ + rmtree(base) ++ diff -Nru rsync-3.4.1+ds1/debian/patches/receiver_c_Tighten_alt-dest_path_resolution_and_partial-dir_state_validation.patch rsync-3.5.0+ds1/debian/patches/receiver_c_Tighten_alt-dest_path_resolution_and_partial-dir_state_validation.patch --- rsync-3.4.1+ds1/debian/patches/receiver_c_Tighten_alt-dest_path_resolution_and_partial-dir_state_validation.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/receiver_c_Tighten_alt-dest_path_resolution_and_partial-dir_state_validation.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,343 @@ +From d9a1cfd0675bfb5751d468a7444582cb787c6923 Mon Sep 17 00:00:00 2001 +From: Omar Elsayed +Date: Mon, 21 Sep 2026 00:51:55 +0300 +Subject: [PATCH] receiver.c: Tighten alt-dest path resolution and partial-dir + state validation (#1077) + +* Tighten alt-dest path resolution and partial-dir state validation + +This introduces architectural best-practices to harden the receiver's state machine +and harmonize symlink handling across the delta-basis engine, addressing protocol +edge-cases reported by Fyyre (James). + +- receiver.c (recv_files): Added explicit validation to ensure one_inplace is + only triggered when partial_dir is configured and the FNAMECMP_PARTIAL_DIR + token is legitimate. +- receiver.c (secure_basis_open): Enforced O_NOFOLLOW on leaf components when + resolving operator-supplied paths, aligning it on operator-path behavior. + +Co-authored-by: Fyyre + +* receiver: validate partial-dir basis state + +* fix: reject alt-dest leaf symlinks + +* fix: preserve basis open flags + +--------- + +Co-authored-by: Zen Dodd +--- + receiver.c | 20 ++- + testsuite/strict-basis_test.py | 253 +++++++++++++++++++++++++++++++++ + 2 files changed, 270 insertions(+), 3 deletions(-) + create mode 100644 testsuite/strict-basis_test.py + +diff --git a/receiver.c b/receiver.c +index 28d663acc..a39108704 100644 +--- a/receiver.c ++++ b/receiver.c +@@ -124,10 +124,15 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags + * 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. */ ++ * the module's exclude hides -- closing the partial-dir exclude bypass. The ++ * final component is opened with O_NOFOLLOW so an operator-path leaf symlink ++ * cannot be selected as an alternate basis. */ ++#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY + if (operator_path_resolve) { + char fullpath[MAXPATHLEN]; + const char *p = relpath; ++ const char *leaf; ++ int dfd, fd, saved_errno; + if (basedir) { + if (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) { + errno = ENAMETOOLONG; +@@ -135,8 +140,16 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags + } + p = fullpath; + } +- return open_no_attacker_symlinks(p, flags, mode); ++ dfd = owner_walk_parent(p, &leaf); ++ if (dfd < 0) ++ return -1; ++ fd = do_open_atfd(dfd, leaf, flags, mode); ++ saved_errno = fd < 0 ? errno : 0; ++ close(dfd); ++ errno = saved_errno; ++ return fd; + } ++#endif + + /* The confined resolver is needed for the sanitizing daemon + * (am_daemon && !am_chrooted) and for a /./ inner-module chroot +@@ -1134,7 +1147,8 @@ int recv_files(int f_in, int f_out, char *local_name) + + /* A peer's basis selector cannot enable direct output through a path + * that the confined basis open did not validate. */ +- one_inplace = inplace_partial && fnamecmp_type == FNAMECMP_PARTIAL_DIR ++ one_inplace = inplace_partial && partial_dir ++ && fnamecmp_type == FNAMECMP_PARTIAL_DIR + && fd1 != -1; + updating_basis_or_equiv = one_inplace + || (inplace && (fnamecmp == fname || fnamecmp_type == FNAMECMP_BACKUP)); +diff --git a/testsuite/strict-basis_test.py b/testsuite/strict-basis_test.py +new file mode 100644 +index 000000000..100a5673b +--- /dev/null ++++ b/testsuite/strict-basis_test.py +@@ -0,0 +1,253 @@ ++#!/usr/bin/env python3 ++"""Ensure forged partial-dir basis tokens cannot enable in-place writes.""" ++ ++import hashlib ++import os ++import socket ++import struct ++import subprocess ++ ++from rsyncfns import ( ++ TODIR, ++ hands_setup, ++ makepath, ++ rmtree, ++ rsync_argv, ++ test_fail, ++ test_skipped, ++) ++import rsync_proto as rp ++ ++hands_setup() ++ ++CF_INPLACE_PARTIAL_DIR = 1 << 6 ++FNAMECMP_BASIS_DIR_LOW = 0x00 ++FNAMECMP_PARTIAL_DIR = 0x81 ++ITEM_BASIS_TYPE_FOLLOWS = 1 << 11 ++ITEM_XNAME_FOLLOWS = 1 << 12 ++ ++ORIGINAL_DATA = b"ORIGINAL_DEST_BYTES_KEEP_ME\n" ++MODIFIED_DATA = b"MODIFIED_INPLACE_DESPITE_BAD_CHECKSUM\n" ++INVALID_MD5 = b"\x00" * 16 ++MODTIME = 1_700_000_000 ++EXPECTED_PROTOCOL_ERROR = "error in rsync protocol data stream" ++TEST_DATA = b"STRICT_NOFOLLOW_REQUIRED\n" ++ ++ ++def drain_argv(peer): ++ nul_run = 0 ++ while nul_run < 2: ++ byte = peer._recv_exact(1) ++ nul_run = nul_run + 1 if byte == b"\0" else 0 ++ ++ ++def read_mux_bytes(peer, buf, count): ++ while len(buf) < count: ++ word = struct.unpack("> 24) - rp.MPLEX_BASE == rp.MSG_DATA: ++ buf.extend(payload) ++ result = bytes(buf[:count]) ++ del buf[:count] ++ return result ++ ++ ++def read_mux_int(peer, buf): ++ return struct.unpack(" +Date: Wed, 16 Sep 2026 20:19:31 +1000 +Subject: [PATCH] rrsync: restore restricted-root paths (#1055) + +--- + support/rrsync | 7 +- + testsuite/rrsync-root-relative-paths_test.py | 128 +++++++++++++++++++ + 2 files changed, 134 insertions(+), 1 deletion(-) + create mode 100644 testsuite/rrsync-root-relative-paths_test.py + +diff --git a/support/rrsync b/support/rrsync +index e11457009..060c8f086 100755 +--- a/support/rrsync ++++ b/support/rrsync +@@ -694,8 +694,13 @@ def validated_arg(opt, arg, typ=3, wild=False): + if arg.startswith('./'): + arg = arg[1:] + arg = arg.replace('//', '/') ++ # One leading slash means the root of the restricted tree. ++ arg = re.sub(r'^/+', '/', arg) + is_absolute_arg = args.absolute and opt == 'arg' and args.dir != '/' and (arg == args.dir or arg.startswith(args.dir_slash)) +- if not is_absolute_arg: ++ is_root_relative_arg = arg.startswith('/') and not is_absolute_arg ++ if is_root_relative_arg and args.dir != '/': ++ arg = args.dir + arg ++ elif not is_absolute_arg: + arg = arg.lstrip('/') + if args.dir != '/': + if HAS_DOT_DOT_RE.search(arg): +diff --git a/testsuite/rrsync-root-relative-paths_test.py b/testsuite/rrsync-root-relative-paths_test.py +new file mode 100644 +index 000000000..56ddbf8c0 +--- /dev/null ++++ b/testsuite/rrsync-root-relative-paths_test.py +@@ -0,0 +1,128 @@ ++#!/usr/bin/env python3 ++"""Leading slashes through rrsync must remain relative to the restricted root.""" ++ ++import os ++import shlex ++import subprocess ++ ++from rsyncfns import ( ++ RSYNC, SCRATCHDIR, makepath, patched_rrsync, rmtree, rsync_argv, ++ rsync_path_arg, test_fail, ++) ++ ++T = 1234567890 ++ ++base = SCRATCHDIR / 'rrsync-root-relative-paths' ++rmtree(base) ++restricted = base / 'restricted' ++source = base / 'source' ++outside = base / 'outside' ++pulled = base / 'pulled' ++makepath(restricted / 'previous', source, outside, pulled) ++ ++(restricted / 'previous' / 'file').write_text('unchanged\n') ++(source / 'file').write_text('unchanged\n') ++(outside / 'secret').write_text('outside\n') ++os.utime(restricted / 'previous' / 'file', (T, T)) ++os.utime(source / 'file', (T, T)) ++ ++shim = base / 'rsync-shim' ++shim.write_text('#!/bin/sh\nexec ' + rsync_path_arg(RSYNC) + ' "$@"\n') ++shim.chmod(0o755) ++rrsync = patched_rrsync(base, rsync_path=str(shim)) ++ ++rsh = base / 'fake-rsh' ++rsh.write_text( ++ '#!/bin/sh\n' ++ 'shift\n' ++ 'SSH_ORIGINAL_COMMAND="$*"\n' ++ 'export SSH_ORIGINAL_COMMAND\n' ++ 'exec %s %s\n' % (shlex.quote(str(rrsync)), shlex.quote(str(restricted)))) ++rsh.chmod(0o755) ++ ++rsh_absolute = base / 'fake-rsh-absolute' ++rsh_absolute.write_text( ++ '#!/bin/sh\n' ++ 'shift\n' ++ 'SSH_ORIGINAL_COMMAND="$*"\n' ++ 'export SSH_ORIGINAL_COMMAND\n' ++ 'exec %s -absolute %s\n' ++ % (shlex.quote(str(rrsync)), shlex.quote(str(restricted)))) ++rsh_absolute.chmod(0o755) ++ ++ ++def run(*args): ++ return subprocess.run( ++ rsync_argv('-e', str(rsh), *args), ++ capture_output=True, text=True, timeout=30, ++ ) ++ ++ ++# A leading slash denotes the root of the restricted tree, not an empty path. ++listed = run('--list-only', 'dummy:/') ++listed_ctx = (f'rc={listed.returncode}, stdout={listed.stdout.strip()!r}, ' ++ f'stderr={listed.stderr.strip()!r}') ++if listed.returncode != 0 or 'previous' not in listed.stdout: ++ test_fail(f'listing the restricted root with / failed ({listed_ctx})') ++ ++# Option operands use the same convention. The basis is a sibling of the ++# destination, so stripping the slash and leaving a relative name makes rsync ++# look for destination/previous and silently transfer a full copy. ++linked = run('-a', '--link-dest=/previous', str(source) + '/', 'dummy:/current') ++linked_ctx = (f'rc={linked.returncode}, stdout={linked.stdout.strip()!r}, ' ++ f'stderr={linked.stderr.strip()!r}') ++current = restricted / 'current' / 'file' ++previous = restricted / 'previous' / 'file' ++if linked.returncode != 0 or not current.is_file(): ++ test_fail(f'root-relative --link-dest transfer failed ({linked_ctx})') ++current_stat = os.stat(current) ++previous_stat = os.stat(previous) ++if ((current_stat.st_dev, current_stat.st_ino) ++ != (previous_stat.st_dev, previous_stat.st_ino)): ++ test_fail(f'root-relative --link-dest did not hard-link to its basis ' ++ f'({linked_ctx})') ++ ++# Repeated leading slashes have the same restricted-root meaning. Assert the ++# authorised path still works rather than satisfying confinement by rejecting ++# every repeated-slash path. ++repeated = run('-a', 'dummy:///previous/file', str(pulled) + '/') ++if (repeated.returncode != 0 ++ or not (pulled / 'file').is_file() ++ or (pulled / 'file').read_text() != 'unchanged\n'): ++ test_fail('repeated leading slashes did not resolve beneath the ' ++ f'restricted root (rc={repeated.returncode}, ' ++ f'stderr={repeated.stderr.strip()!r})') ++ ++# The explicit -absolute mode continues to accept a complete server path under ++# the restricted directory; root-relative handling must not reinterpret it. ++rmtree(pulled) ++pulled.mkdir() ++absolute = subprocess.run( ++ rsync_argv('-a', '-e', str(rsh_absolute), ++ 'dummy:' + str(restricted / 'previous' / 'file'), ++ str(pulled) + '/'), ++ capture_output=True, text=True, timeout=30, ++) ++if (absolute.returncode != 0 ++ or not (pulled / 'file').is_file() ++ or (pulled / 'file').read_text() != 'unchanged\n'): ++ test_fail('-absolute no longer accepts an in-tree server path ' ++ f'(rc={absolute.returncode}, stderr={absolute.stderr.strip()!r})') ++ ++# Repeated leading slashes must not regain host-root meaning in either transfer ++# direction. The requested path is the absolute spelling of a file outside ++# the restricted tree; a vulnerable wrapper would read or replace that file. ++outside_arg = '///' + str(outside / 'secret').lstrip('/') ++rmtree(pulled) ++pulled.mkdir() ++escaped_pull = run('-a', 'dummy:' + outside_arg, str(pulled) + '/') ++if (pulled / 'secret').exists(): ++ test_fail('repeated leading slashes escaped the restricted root on pull') ++ ++replacement = base / 'replacement' ++replacement.write_text('replacement\n') ++escaped_push = run('-a', str(replacement), 'dummy:' + outside_arg) ++if (outside / 'secret').read_text() != 'outside\n': ++ test_fail('repeated leading slashes escaped the restricted root on push') ++ ++print('rrsync preserves restricted-root paths without repeated-slash escapes') diff -Nru rsync-3.4.1+ds1/debian/patches/rrsync_support_fd_pins_in_user_namespaces.patch rsync-3.5.0+ds1/debian/patches/rrsync_support_fd_pins_in_user_namespaces.patch --- rsync-3.4.1+ds1/debian/patches/rrsync_support_fd_pins_in_user_namespaces.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/rrsync_support_fd_pins_in_user_namespaces.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,233 @@ +From 324a1f0716aa75d311f879864f48408d5723d963 Mon Sep 17 00:00:00 2001 +From: Zen Dodd +Date: Sun, 16 Aug 2026 21:51:07 +1000 +Subject: [PATCH] rrsync: support fd pins in user namespaces (#1048) + +* rrsync: support fd pins in user namespaces +* rrsync: support /dev/fd pins in user namespaces +--- + syscall.c | 26 +++-- + testsuite/rrsync-userns-procfs_test.py | 133 +++++++++++++++++++++++++ + testsuite/skiplist/cygwin.txt | 1 + + testsuite/skiplist/macos.txt | 1 + + 4 files changed, 153 insertions(+), 8 deletions(-) + create mode 100644 testsuite/rrsync-userns-procfs_test.py + +diff --git a/syscall.c b/syscall.c +index 5e92edca0..68a7eea31 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -143,13 +143,18 @@ static const char *confinement_root(unsigned int *lenp) + 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. */ ++/* Split a recognised fd-pin prefix off `p`, returning the tail -- "" for the ++ * pin directory itself, otherwise a string starting with '/'. NULL when `p` ++ * is not in an fd-pin namespace. */ + static const char *fd_pin_tail(const char *p) + { + const char *s; + ++ if (strncmp(p, "/dev/fd", 7) == 0) { ++ s = p + 7; ++ return (*s == '\0' || *s == '/') ? s : NULL; ++ } ++ + if (strncmp(p, "/proc/", 6) != 0) + return NULL; + s = p + 6; +@@ -168,8 +173,8 @@ static const char *fd_pin_tail(const char *p) + 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 ++/* An EXACT pin entry, such as "/proc/self/fd/7" or "/dev/fd/7", 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. */ +@@ -401,9 +406,14 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + } + + 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) { ++ /* Symlink: untrusted owner is refused; trusted owner is followed ++ * via readlinkat + splice. In a user namespace the /proc/self and ++ * /dev/fd symlinks may report the overflow uid, so ++ * allow those exact components while traversing a recognised pin. */ ++ int namespace_pin = pin_transit ++ && ((strcmp(abspath, "/proc") == 0 && strcmp(comp, "self") == 0) ++ || (strcmp(abspath, "/dev") == 0 && strcmp(comp, "fd") == 0)); ++ if (!namespace_pin && lst.st_uid != 0 && lst.st_uid != trusted_uid) { + saved_errno = ELOOP; + goto out; + } +diff --git a/testsuite/rrsync-userns-procfs_test.py b/testsuite/rrsync-userns-procfs_test.py +new file mode 100644 +index 000000000..fa2aaf3dc +--- /dev/null ++++ b/testsuite/rrsync-userns-procfs_test.py +@@ -0,0 +1,133 @@ ++#!/usr/bin/env python3 ++"""A confined fd pin must remain usable inside a Linux user namespace.""" ++ ++import os ++import shlex ++import shutil ++import subprocess ++import sys ++import tempfile ++from pathlib import Path ++ ++from rsyncfns import makepath, rmtree, rsync_argv, test_fail, test_skipped ++ ++ ++if not sys.platform.startswith('linux'): ++ test_skipped('rrsync-userns-procfs is Linux-specific') ++ ++if not os.environ.get('RSYNC_USERNS_PROCFS'): ++ unshare = shutil.which('unshare') ++ if unshare is None: ++ test_skipped('unshare is unavailable') ++ env = os.environ.copy() ++ env['RSYNC_USERNS_PROCFS'] = '1' ++ launch_dir = Path(tempfile.mkdtemp(prefix='rsync-userns-launch-')) ++ launch_dir.chmod(0o755) ++ testdir = Path(__file__).resolve().parent ++ child_test = launch_dir / Path(__file__).name ++ for source in (Path(__file__), testdir / 'rsyncfns.py', ++ testdir / 'exitcodes.py'): ++ shutil.copy2(source, launch_dir / source.name) ++ rsync_cmd = shlex.split(env['RSYNC']) ++ for i, arg in enumerate(rsync_cmd): ++ if Path(arg).name in ('rsync', 'rsync.exe') and Path(arg).is_file(): ++ staged_rsync = launch_dir / Path(arg).name ++ shutil.copy2(arg, staged_rsync) ++ staged_rsync.chmod(0o755) ++ rsync_cmd[i] = str(staged_rsync) ++ break ++ else: ++ rmtree(launch_dir) ++ test_fail(f'cannot locate the rsync executable in {env["RSYNC"]!r}') ++ env['RSYNC'] = shlex.join(rsync_cmd) ++ launcher = [] ++ if os.geteuid() == 0: ++ setpriv = shutil.which('setpriv') ++ if setpriv is None: ++ test_skipped('setpriv is unavailable for the root-run testsuite') ++ launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups'] ++ unshare_argv = [unshare, '--user', '--map-root-user', '--mount', '--pid', ++ '--fork', '--mount-proc'] ++ probe = subprocess.run( ++ launcher + unshare_argv + ['true'], ++ stdin=subprocess.DEVNULL, ++ stdout=subprocess.DEVNULL, ++ stderr=subprocess.DEVNULL, ++ ) ++ if probe.returncode != 0: ++ rmtree(launch_dir) ++ print(f'user namespaces unavailable (rc={probe.returncode})') ++ raise SystemExit(0) ++ try: ++ proc = subprocess.run( ++ launcher + unshare_argv ++ + [sys.executable, str(child_test)], ++ env=env, ++ timeout=30, ++ ) ++ except subprocess.TimeoutExpired: ++ test_fail('user-namespace regression test timed out') ++ finally: ++ rmtree(launch_dir) ++ if proc.returncode != 0: ++ test_fail(f'user-namespace regression test failed (rc={proc.returncode})') ++ print('rrsync fd pin works inside a user namespace') ++ raise SystemExit(0) ++ ++proc_uid = os.lstat('/proc/self').st_uid ++if proc_uid in (0, os.geteuid()): ++ test_skipped('/proc/self does not expose an overflow uid in this namespace') ++ ++base = Path(tempfile.mkdtemp(prefix='rsync-userns-procfs-')) ++src = base / 'src' ++dest = base / 'dest' ++outside = base / 'outside' ++makepath(src, dest, outside) ++(src / 'file').write_text('content\n') ++ ++fd_roots = ['/proc/self/fd'] ++if Path('/dev/fd').exists(): ++ fd_roots.append('/dev/fd') ++ ++dest_fd = os.open(dest, os.O_RDONLY | os.O_DIRECTORY) ++try: ++ for index, fd_root in enumerate(fd_roots): ++ log_file = dest / f'rsync-{index}.log' ++ proc = subprocess.run( ++ rsync_argv('-a', f'--confine-root={dest}', ++ f'--log-file={fd_root}/{dest_fd}/{log_file.name}', ++ str(src) + '/', str(dest) + '/'), ++ pass_fds=(dest_fd,), ++ capture_output=True, ++ text=True, ++ ) ++ ctx = (f'fd_root={fd_root!r}, rc={proc.returncode}, ' ++ f'stderr={proc.stderr.strip()[:300]!r}') ++ if proc.returncode != 0: ++ test_fail(f'confined transfer through an fd pin failed ({ctx})') ++ if not log_file.is_file(): ++ test_fail(f'confined log path through an fd pin was rejected ({ctx})') ++ if (dest / 'file').read_text() != 'content\n': ++ test_fail(f'confined transfer did not deliver the file ({ctx})') ++finally: ++ os.close(dest_fd) ++ ++outside_list = outside / 'files-from' ++outside_list.write_text('file\n') ++for fd_root in fd_roots: ++ outside_fd = os.open(outside_list, os.O_RDONLY) ++ try: ++ proc = subprocess.run( ++ rsync_argv('-a', f'--confine-root={dest}', ++ f'--files-from={fd_root}/{outside_fd}', ++ str(src) + '/', str(dest) + '/'), ++ pass_fds=(outside_fd,), ++ capture_output=True, ++ text=True, ++ ) ++ finally: ++ os.close(outside_fd) ++ if proc.returncode == 0 or 'failed to open files-from file' not in proc.stderr: ++ test_fail(f'outside {fd_root} pin was not observably refused: ' ++ f'rc={proc.returncode}, stderr={proc.stderr!r}') ++rmtree(base) +diff --git a/testsuite/skiplist/cygwin.txt b/testsuite/skiplist/cygwin.txt +index ba499cc54..46060e16f 100644 +--- a/testsuite/skiplist/cygwin.txt ++++ b/testsuite/skiplist/cygwin.txt +@@ -56,6 +56,7 @@ rename-mixed-parent-transfer + rrsync-sender-leaf-flip + rrsync-sender-parent-pin + rrsync-symlink ++rrsync-userns-procfs + sender-remove-source-root-anchor + simd-checksum + source-change-size-continues +diff --git a/testsuite/skiplist/macos.txt b/testsuite/skiplist/macos.txt +index 3e7653ce8..5952c8143 100644 +--- a/testsuite/skiplist/macos.txt ++++ b/testsuite/skiplist/macos.txt +@@ -26,6 +26,7 @@ readonly-partial-abort-mode-regression # + rrsync-sender-leaf-flip + rrsync-sender-parent-pin + rrsync-symlink ++rrsync-userns-procfs + sender-remove-source-root-anchor + simd-checksum + source-change-size-continues diff -Nru rsync-3.4.1+ds1/debian/patches/series rsync-3.5.0+ds1/debian/patches/series --- rsync-3.4.1+ds1/debian/patches/series 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/series 2026-09-16 01:46:30.000000000 +0000 @@ -1,41 +1,27 @@ disable_reconfigure_req.diff -skip_devices_test_non_linux.patch env_shebang.patch fix_rrsync_man_generation.patch -fix-flaky-hardlinks-test.patch -2026-05-20/0001-bool-is-a-keyword-in-C23.patch -2026-05-20/0002-syscall-fix-a-Y2038-bug-by-replacing-Int32x32To64-wi.patch -2026-05-20/0003-options.c-Fix-segv-if-poptGetContext-returns-NULL.patch -2026-05-20/0004-Using-a-correct-time-in-log-file.patch -2026-05-20/0005-configure.ac-check-for-xattr-support-both-in-libc-an.patch -2026-05-20/0006-util-fixed-issue-in-clean_fname.patch -2026-05-20/0007-testsuite-added-clean-fname-underflow-test.patch -2026-05-20/0008-fixed-an-invalid-access-to-files-array.patch -2026-05-20/0009-fix-uninitialized-buf1-in-get_checksum2-MD4-path.patch -2026-05-20/0010-reject-negative-token-values-in-compressed-stream-re.patch -2026-05-20/0011-acl-fixed-ACL-ID-mapping-for-non-root.patch -2026-05-20/0012-fix-uninitialized-mul_one-in-AVX2-checksum-and-add-S.patch -2026-05-20/0013-Fix-glibc-2.43-constness-warnings.patch -2026-05-20/0015-fix-signed-integer-overflow-in-proxy-protocol-v2-hea.patch -2026-05-20/0016-zero-all-new-memory-from-allocations.patch -2026-05-20/0017-xattrs-fixed-count-in-qsort.patch -2026-05-20/0018-call-tzset-before-chroot-to-cache-timezone-data.patch -2026-05-20/0019-testsuite-xattrs-ignore-SUNWattr_-in-the-Solaris-xls.patch -2026-05-20/0020-syscall-use-openat2-RESOLVE_BENEATH-on-Linux-for-sec.patch -2026-05-20/0021-syscall-also-use-O_RESOLVE_BENEATH-on-FreeBSD-and-Ma.patch -2026-05-20/0022-testsuite-skip-symlink-dirlink-basis-on-platforms-wi.patch -2026-05-20/0023-syscall-clientserver-am_chrooted-and-use_secure_syml.patch -2026-05-20/0024-sender-fix-read-path-TOCTOU-by-opening-from-module-r.patch -2026-05-20/0025-syscall-receiver-secure-receiver-side-do_chmod-again.patch -2026-05-20/0026-util1-secure-change_dir-against-symlink-race-chdir-e.patch -2026-05-20/0027-syscall-add-symlink-race-safe-do_-_at-wrappers-and-h.patch -2026-05-20/0028-util1-syscall-secure-copy_file-source-dest-opens-bar.patch -2026-05-20/0029-testsuite-end-to-end-regression-test-for-chdir-symli.patch -2026-05-20/0030-token-harden-compressed-token-decoding-against-integ.patch -2026-05-20/0031-testsuite-cover-refuse-options-compress-for-the-daem.patch -2026-05-20/0032-receiver-add-parent_ndx-0-guard-mirroring-797e17f.patch -2026-05-20/0033-clientserver-fix-hostname-ACL-bypass-when-using-daem.patch -2026-05-20/0034-defence-in-depth-bound-wire-supplied-counts-and-leng.patch -2026-05-20/0035-defence-in-depth-guard-cumulative-snprintf-against-l.patch -2026-05-20/0036-defence-in-depth-receiver-block-index-bounds-read_de.patch -CVE-2026-45232.patch +# Post 3.5.0 regresion fixes +Honor_STRIP_in_install-strip_for_cross-compilation.patch +rrsync_support_fd_pins_in_user_namespaces.patch +testsuite_bound_the_unshare_probe.patch +testsuite_make_basis_xname_oracle_deterministic.patch + +# Testsuite fixes for arch-specific FTBFS +testsuite_interpose_lfs_open_symbols.patch +testsuite_punch_granularity_guard.patch + +# Another set of post 3.5.0 regression fixes +Fix_ENOENT_when_resolving_kernel_pseudo_paths_in_ona_open_patch +batch_c_Allow_FIFO_pipes_in_batch_file_processing.patch +options_accept_max_alloc_0_again_resolved_to_the_parsers_own_ceiling.patch +syscall_explain_untrusted_symlink_refusal.patch +Fix_handling_of_dev_std_in_out_err_pseudo-paths_and_namespace_overflow_UID_checks.patch + +# Post 3.5.0 regression fixes from 3.5.1 +syscall_use_O_PATH_for_directory_traversal.patch +syscall_use_O_PATH_for_held_directory_traversal.patch +rrsync_restore_restricted-root_paths.patch +options_c_Fix_files-from_confinement_for_local_and_SSH_transfers.patch +syscall_follow_trusted_sender_ancestor_links.patch +receiver_c_Tighten_alt-dest_path_resolution_and_partial-dir_state_validation.patch diff -Nru rsync-3.4.1+ds1/debian/patches/skip_devices_test_non_linux.patch rsync-3.5.0+ds1/debian/patches/skip_devices_test_non_linux.patch --- rsync-3.4.1+ds1/debian/patches/skip_devices_test_non_linux.patch 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/skip_devices_test_non_linux.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,61 +0,0 @@ -From: Samuel Henrique -Date: Fri, 12 Apr 2024 00:11:07 +0100 -Subject: Skip "devices" test as it fails on kfreebsd and hurd - -Error log: - sent 160 bytes received 117 bytes 554.00 bytes/sec - total size is 0 speedup is 0.00 - check how the directory listings compare with diff: - - + + /<>/rsynctee -aii /<>/testtmp/devices/rsync.out --link-dest=/<>/testtmp/devices/to - /<>/testtmp/devices/from/ /<>/testtmp/devices/chk/ - rsync: failed to hard-link /<>/testtmp/devices/to/fifo with fifo: Invalid cross-device link (1073741842) - cd ./ - hD block - hD block2 - hD block3 - hD block3.5 - hD char - hD char2 - hD char3 - cSc........ fifo - rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1207) [sender=3.1.3] - + cat - + diff -u /<>/testtmp/devices/rsync.chk /<>/testtmp/devices/rsync.out - /--- /<>/testtmp/devices/rsync.chk 2019-10-15 01:26:43.000000000 +0000 - /+++ /<>/testtmp/devices/rsync.out 2019-10-15 01:26:43.000000000 +0000 - @@ -6,4 +6,4 @@ - hD char - hD char2 - hD char3 - -hS fifo - +cSc........ fifo - + test_fail test 5 failed - + echo test 5 failed - test 5 failed - + exit 1 - /bin/fakeauth: Error 1 for child 7541 - /bin/settrans: Error 1 for child 7540 - ----- devices log ends - FAIL devices ---- - testsuite/devices.test | 6 ++++++ - 1 file changed, 6 insertions(+) - -diff --git a/testsuite/devices.test b/testsuite/devices.test -index ad5f936..1fff93f 100644 ---- a/testsuite/devices.test -+++ b/testsuite/devices.test -@@ -9,6 +9,12 @@ - - . "$suitedir/rsync.fns" - -+# Debian patch to skip this test as it fails on kfreebsd and hurd -+case "$HOST_OS" in -+ *linux*) ;; -+ *) test_skipped "Patched by Debian to skip as it fails on kfreebsd and hurd" ;; -+esac -+ - # Build some hardlinks - - case $0 in diff -Nru rsync-3.4.1+ds1/debian/patches/syscall_explain_untrusted_symlink_refusal.patch rsync-3.5.0+ds1/debian/patches/syscall_explain_untrusted_symlink_refusal.patch --- rsync-3.4.1+ds1/debian/patches/syscall_explain_untrusted_symlink_refusal.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/syscall_explain_untrusted_symlink_refusal.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,54 @@ +From 59be391641de544a293c221ab96b6c0f4e04f366 Mon Sep 17 00:00:00 2001 +From: Zen Dodd +Date: Thu, 3 Sep 2026 17:05:03 +1000 +Subject: [PATCH] syscall: explain untrusted symlink refusal (#1074) + +* syscall: explain untrusted symlink refusal +* syscall: emphasise insecure-links warning +--- + syscall.c | 4 ++++ + testsuite/symlink-race-dest_test.py | 16 ++++++++++++++-- + 2 files changed, 18 insertions(+), 2 deletions(-) + +diff --git a/syscall.c b/syscall.c +index e5ee205f5..81e12f906 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -429,6 +429,10 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + && ((strcmp(abspath, "/proc") == 0 && strcmp(comp, "self") == 0) + || (strcmp(abspath, "/dev") == 0 && strcmp(comp, "fd") == 0)); + if (!namespace_pin && lst.st_uid != 0 && lst.st_uid != trusted_uid) { ++ rprintf(FERROR, ++ "refusing to follow a symlink owned by an untrusted user; " ++ "use --insecure-links locally or \"insecure links = yes\" in a " ++ "daemon module ONLY if every path component is trusted\n"); + saved_errno = ELOOP; + goto out; + } +diff --git a/testsuite/symlink-race-dest_test.py b/testsuite/symlink-race-dest_test.py +index 04df413ae..7b500dd8d 100644 +--- a/testsuite/symlink-race-dest_test.py ++++ b/testsuite/symlink-race-dest_test.py +@@ -50,8 +50,20 @@ + os.symlink(outside, dest / 'sub') # attacker-owned dest component + os.lchown(dest / 'sub', ATT_UID, ATT_UID) + +-subprocess.run(rsync_argv('-a', f'{src}/sub/', f'{dest}/sub/'), +- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) ++proc = subprocess.run( ++ rsync_argv('-a', f'{src}/sub/', f'{dest}/sub/'), ++ stdout=subprocess.DEVNULL, ++ stderr=subprocess.PIPE, ++ text=True, ++) ++ ++if proc.returncode == 0: ++ test_fail("attacker-owned destination symlink was not rejected") ++if "refusing to follow a symlink owned by an untrusted user" not in proc.stderr: ++ test_fail( ++ "untrusted destination symlink failure omitted the actionable " ++ f"diagnostic: {proc.stderr!r}" ++ ) + + escaped = sorted(p.name for p in outside.iterdir()) + if escaped: diff -Nru rsync-3.4.1+ds1/debian/patches/syscall_follow_trusted_sender_ancestor_links.patch rsync-3.5.0+ds1/debian/patches/syscall_follow_trusted_sender_ancestor_links.patch --- rsync-3.4.1+ds1/debian/patches/syscall_follow_trusted_sender_ancestor_links.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/syscall_follow_trusted_sender_ancestor_links.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,1177 @@ +From 7c7e1bee860a97b1357ec29eac9656768beb2e63 Mon Sep 17 00:00:00 2001 +From: Zen Dodd +Date: Sun, 20 Sep 2026 23:36:00 +1000 +Subject: [PATCH] syscall: follow trusted sender ancestor links (#1088) + +* syscall: follow trusted sender ancestor links +* testsuite: accept BSD symlink errors +* syscall: anchor sender paths at explicit source roots +* flist: pin explicit file source parents +* flist: preserve search-only source traversal +* sender: follow trusted files-from ancestors +* sender: enforce files-from symlink ownership +* sender: harden files-from path resolution +--- + SECURITY.md | 7 + + flist.c | 259 +++++++++++++++++- + rsync.1.md | 11 +- + sender.c | 46 ++-- + syscall.c | 141 +++++++--- + t_secure_relpath.c | 1 - + t_stub.c | 2 + + testsuite/relative-source-ancestor_test.py | 296 +++++++++++++++++++++ + 8 files changed, 695 insertions(+), 68 deletions(-) + create mode 100644 testsuite/relative-source-ancestor_test.py + +diff --git a/SECURITY.md b/SECURITY.md +index bcc849196..b77e1473c 100644 +--- a/SECURITY.md ++++ b/SECURITY.md +@@ -188,6 +188,13 @@ module uid even when the module sits under a directory that uid cannot traverse + immune to the logical-path-versus-real-cwd skew a followed in-tree directory + symlink would otherwise introduce. + ++When a sender receives a `--files-from` list in the default symlink mode, the ++source base remains the operator's choice but each path selected by the list is ++checked with the ownership walk. A trusted ancestor symlink may still be ++followed; with `--confine-root` its resolved target must also remain under that ++root. The final content open remains `O_NOFOLLOW`, so a list entry cannot turn a ++leaf symlink into a file read. ++ + ### Path resolution + + `secure_relative_open()` resolves a path with a single portable mechanism on +diff --git a/flist.c b/flist.c +index 9276c65fc..17ad5a57d 100644 +--- a/flist.c ++++ b/flist.c +@@ -46,6 +46,7 @@ extern int recurse; + extern int use_qsort; + extern int xfer_dirs; + extern int filesfrom_fd; ++extern char *files_from; + extern int one_file_system; + extern int copy_devices; + extern int copy_dirlinks; +@@ -70,6 +71,7 @@ extern int prune_empty_dirs; + extern int copy_links; + extern int copy_unsafe_links; + extern int insecure_links; ++extern int filesfrom_owner_walk_override; + extern int protocol_version; + extern int sanitize_paths; + extern int munge_symlinks; +@@ -230,27 +232,250 @@ static int scan_dirfd = -1; + static const char *scan_dir_prefix; + static int scan_dir_prefix_len; + ++struct sender_source_root { ++ struct sender_source_root *next; ++ dev_t dev; ++ ino_t ino; ++ char path[1]; ++}; ++ ++static struct sender_source_root *sender_source_roots; ++static struct sender_source_root *sender_source_root_fd_owner; ++static int sender_source_root_fd = -1; ++ ++static int sender_source_full_path(const char *path, char *full, size_t full_size) ++{ ++ size_t len; ++ ++ if (*path == '/') ++ len = strlcpy(full, path, full_size); ++ else ++ len = pathjoin(full, full_size, curr_dir, path); ++ if (len >= full_size) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ clean_fname(full, CFN_COLLAPSE_DOT_DOT_DIRS | CFN_DROP_TRAILING_DOT_DIR); ++ return 0; ++} ++ ++static void remember_sender_source_root(const char *path, const STRUCT_STAT *st) ++{ ++ struct sender_source_root *root; ++ char full[MAXPATHLEN]; ++ size_t len; ++ ++ if (sender_source_full_path(path, full, sizeof full) < 0) ++ overflow_exit("remember_sender_source_root"); ++ len = strlen(full); ++ ++ for (root = sender_source_roots; root; root = root->next) { ++ if (strcmp(root->path, full) == 0) ++ return; ++ } ++ root = (struct sender_source_root *)new_array(char, sizeof *root + len); ++ root->next = sender_source_roots; ++ root->dev = st->st_dev; ++ root->ino = st->st_ino; ++ memcpy(root->path, full, len + 1); ++ sender_source_roots = root; ++} ++ ++static void remember_sender_source_arg(const char *path, const STRUCT_STAT *st) ++{ ++ STRUCT_STAT parent_st; ++ char full[MAXPATHLEN], *slash; ++ ++ if (S_ISDIR(st->st_mode)) { ++ remember_sender_source_root(path, st); ++ return; ++ } ++ if (sender_source_full_path(path, full, sizeof full) < 0) ++ overflow_exit("remember_sender_source_arg"); ++ slash = strrchr(full, '/'); ++ if (!slash) ++ return; ++ if (slash == full) ++ slash[1] = '\0'; ++ else ++ *slash = '\0'; ++ /* The operator selected this parent as part of the source argument. Pin ++ * its resolved identity while the file leaf remains O_NOFOLLOW later. */ ++ if (do_stat(full, &parent_st) == 0 && S_ISDIR(parent_st.st_mode)) ++ remember_sender_source_root(full, &parent_st); ++} ++ ++#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY ++/* Keep one validated source root open at a time. A descriptor per explicit ++ * argument would make a large argument list exhaust the process limit. */ ++static int sender_source_root_fd_for(struct sender_source_root *root) ++{ ++ STRUCT_STAT st; ++ int fd, fl, saved_errno; ++ ++ if (sender_source_root_fd_owner == root && sender_source_root_fd >= 0) ++ return sender_source_root_fd; ++ if (sender_source_root_fd >= 0) ++ close(sender_source_root_fd); ++ sender_source_root_fd = -1; ++ sender_source_root_fd_owner = NULL; ++ ++ fd = open_anchor_dirfd(root->path); ++ if (fd < 0) ++ return -1; ++ if (do_fstat(fd, &st) < 0) ++ saved_errno = errno; ++ else if (st.st_dev != root->dev || st.st_ino != root->ino) ++ saved_errno = ELOOP; ++ else ++ saved_errno = 0; ++ if (saved_errno) { ++ close(fd); ++ errno = saved_errno; ++ return -1; ++ } ++ if ((fl = fcntl(fd, F_GETFD)) >= 0) ++ fcntl(fd, F_SETFD, fl | FD_CLOEXEC); ++ sender_source_root_fd_owner = root; ++ sender_source_root_fd = fd; ++ return fd; ++} ++#endif ++ ++int open_sender_source_path(const char *path, int flags, int *matched) ++{ ++#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY ++ struct sender_source_root *root, *best = NULL; ++ char full[MAXPATHLEN], *rel; ++ size_t best_len = 0; ++ int rootfd, fd, saved_errno; ++ ++ *matched = 0; ++ if (!sender_source_roots) ++ return -1; ++ if (sender_source_full_path(path, full, sizeof full) < 0) ++ return -1; ++ for (root = sender_source_roots; root; root = root->next) { ++ size_t len = strlen(root->path); ++ if (len > best_len && strncmp(full, root->path, len) == 0 ++ && (root->path[len-1] == '/' || full[len] == '\0' || full[len] == '/')) { ++ best = root; ++ best_len = len; ++ } ++ } ++ if (!best) ++ return -1; ++ ++ *matched = 1; ++ rootfd = sender_source_root_fd_for(best); ++ if (rootfd < 0) ++ return -1; ++ rel = full + best_len; ++ while (*rel == '/') ++ rel++; ++ fd = secure_relative_open_at(rootfd, *rel ? rel : ".", flags, 0); ++ saved_errno = errno; ++ errno = saved_errno; ++ return fd; ++#else ++ *matched = 0; ++ errno = ENOSYS; ++ return -1; ++#endif ++} ++ ++void clear_sender_source_roots(void) ++{ ++ if (sender_source_root_fd >= 0) ++ close(sender_source_root_fd); ++ sender_source_root_fd = -1; ++ sender_source_root_fd_owner = NULL; ++ while (sender_source_roots) { ++ struct sender_source_root *root = sender_source_roots; ++ sender_source_roots = root->next; ++ free(root); ++ } ++} ++ ++static int filesfrom_link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks) ++{ ++#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY ++ const char *bname; ++ int dfd, ret, save_errno; ++ ++ dfd = owner_walk_parent(path, &bname); ++ if (dfd < 0) ++ return -1; ++ if (am_root < 0) { ++ close(dfd); ++ return link_stat(path, stp, follow_dirlinks); ++ } ++ ret = do_lstat_atfd(dfd, bname, stp); ++ /* A list-selected directory symlink is a path component too. Resolve it ++ * through the ownership and confinement walk before following it. */ ++ if (ret == 0 && S_ISLNK(stp->st_mode) ++ && (follow_dirlinks || copy_links)) { ++ int targetfd = open_no_attacker_symlinks_dirfd(path); ++ if (targetfd >= 0) { ++ ret = do_fstat(targetfd, stp); ++ close(targetfd); ++ } else if (errno != ENOENT && errno != ENOTDIR && errno != EACCES) { ++ ret = -1; ++ } ++ } ++ save_errno = ret < 0 ? errno : 0; ++ close(dfd); ++ errno = save_errno; ++ return ret; ++#else ++ return link_stat(path, stp, follow_dirlinks); ++#endif ++} ++ ++static int filesfrom_readlink(const char *path, char *linkbuf, size_t bufsiz) ++{ ++#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY ++ const char *bname; ++ int dfd, ret, save_errno; ++ ++ dfd = owner_walk_parent(path, &bname); ++ if (dfd < 0) ++ return -1; ++ ret = do_readlink_atfd(dfd, bname, linkbuf, bufsiz); ++ save_errno = ret < 0 ? errno : 0; ++ close(dfd); ++ errno = save_errno; ++ return ret; ++#else ++ return do_readlink(path, linkbuf, bufsiz); ++#endif ++} ++ + static int scan_link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks) + { + /* Use the held scan fd only for a single component directly inside the + * scanned dir, and only when am_root >= 0 (link_stat_at folds in no + * fake-super %stat xattr; link_stat does so via get_stat_xattr, a no-op + * once am_root >= 0). */ +- if (scan_dirfd >= 0 && am_root >= 0 ++ if (scan_dirfd >= 0 && am_root >= 0 && !filesfrom_owner_walk_active() + && strncmp(path, scan_dir_prefix, scan_dir_prefix_len) == 0 + && path[scan_dir_prefix_len] == '/' + && strchr(path + scan_dir_prefix_len + 1, '/') == NULL) + return link_stat_at(scan_dirfd, path + scan_dir_prefix_len + 1, stp, follow_dirlinks); ++ if (filesfrom_owner_walk_active()) ++ return filesfrom_link_stat(path, stp, follow_dirlinks); + return link_stat(path, stp, follow_dirlinks); + } + + static int scan_readlink(const char *path, char *linkbuf, size_t bufsiz) + { +- if (scan_dirfd >= 0 && am_root >= 0 ++ if (scan_dirfd >= 0 && am_root >= 0 && !filesfrom_owner_walk_active() + && 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); ++ if (filesfrom_owner_walk_active()) ++ return filesfrom_readlink(path, linkbuf, bufsiz); + return do_readlink(path, linkbuf, bufsiz); + } + +@@ -2014,7 +2239,7 @@ 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. ++/* Open a source directory for scanning under the applicable source authority. + * secure_relative_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 +@@ -2025,13 +2250,24 @@ static void interpret_stat_error(const char *fname, int is_dir) + * O_NOFOLLOW makes secure_relative_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). ++ * Files-from entries instead use the ownership walk: their source base is ++ * operator-selected, but each list entry may not be, so only trusted-owned ++ * symlinks are followed and a trusted link may retain its legacy target. + * Returns NULL with errno set on failure, like opendir(). */ + static DIR *secure_opendir(const char *fbuf) + { +- int dfd, fl; ++ int dfd, fl, matched; + DIR *d; + +- if (am_daemon && (!am_chrooted || module_dirlen) ++ if (filesfrom_owner_walk_active()) { ++ /* The source base is operator-selected, while each list entry may not ++ * be. Follow only trusted-owned symlinks while opening the directory. */ ++ dfd = open_no_attacker_symlinks(fbuf, O_RDONLY | O_DIRECTORY, 0); ++ } else if (!am_daemon && am_sender ++ && (dfd = open_sender_source_path(fbuf, O_RDONLY | O_DIRECTORY, &matched), matched)) { ++ /* The command-line directory is the operator-selected transfer root. ++ * Follow that root, then keep every recursive scan beneath its held fd. */ ++ } else 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 +@@ -2251,8 +2487,11 @@ static void send_implied_dirs(int f, struct file_list *flist, char *fname, + if (need_new_dir) { + int save_copy_links = copy_links; + int save_xfer_dirs = xfer_dirs; ++ int save_filesfrom_owner_walk = filesfrom_owner_walk_override; + char *slash; + ++ if (filesfrom_owner_walk_active()) ++ filesfrom_owner_walk_override = 1; + copy_links = xfer_dirs = 1; + + *limit = '\0'; +@@ -2281,6 +2520,7 @@ static void send_implied_dirs(int f, struct file_list *flist, char *fname, + + copy_links = save_copy_links; + xfer_dirs = save_xfer_dirs; ++ filesfrom_owner_walk_override = save_filesfrom_owner_walk; + + if (!inc_recurse) + goto done; +@@ -2333,7 +2573,7 @@ static void send1extra(int f, struct file_struct *file, struct file_list *flist) + if (file->flags & FLAG_CONTENT_DIR) { + if (one_file_system) { + STRUCT_STAT st; +- if (link_stat(fbuf, &st, copy_dirlinks) != 0) { ++ if (scan_link_stat(fbuf, &st, copy_dirlinks) != 0) { + interpret_stat_error(fbuf, True); + return; + } +@@ -2369,7 +2609,7 @@ static void send1extra(int f, struct file_struct *file, struct file_list *flist) + if (name_type != NORMAL_NAME) { + STRUCT_STAT st = {0}; + +- if (name_type != MISSING_NAME && link_stat(fbuf, &st, 1) != 0) { ++ if (name_type != MISSING_NAME && scan_link_stat(fbuf, &st, 1) != 0) { + interpret_stat_error(fbuf, True); + continue; + } +@@ -2694,7 +2934,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[]) + if (fn != fbuf) + memmove(fbuf, fn, len + 1); + +- if (link_stat(fbuf, &st, copy_dirlinks || name_type != NORMAL_NAME) != 0 ++ if (scan_link_stat(fbuf, &st, copy_dirlinks || name_type != NORMAL_NAME) != 0 + || (name_type != DOTDIR_NAME && is_excluded(fbuf, S_ISDIR(st.st_mode) != 0, SERVER_FILTERS)) + || (relative_paths && path_is_daemon_excluded(fbuf, 1))) { + if (errno != ENOENT || missing_args == 0) { +@@ -2724,6 +2964,9 @@ struct file_list *send_file_list(int f, int argc, char *argv[]) + rprintf(FINFO, "skipping directory %s\n", fbuf); + continue; + } ++ if (!am_daemon && !use_ff_fd && st.st_mode != 0 ++ && (relative_paths || S_ISDIR(st.st_mode))) ++ remember_sender_source_arg(fbuf, &st); + + if (inc_recurse && relative_paths && *fbuf) { + if ((p = strchr(fbuf+1, '/')) != NULL) { +diff --git a/rsync.1.md b/rsync.1.md +index 8ef6a1c7b..ebc504b70 100644 +--- a/rsync.1.md ++++ b/rsync.1.md +@@ -1424,10 +1424,13 @@ sign) if you want the local shell to expand it. + + 0. `--confine-root=DIR` + +- This bounds where the paths listed under [`--insecure-links`](#opt) are +- allowed to resolve: one that ends up outside DIR is refused, even if every +- symlink along it was owned by a trusted user. The ownership walk asks who +- planted a link; this asks where the path came out. ++ In the default symlink mode this bounds where operator option paths and ++ paths selected from a [`--files-from`](#opt) list are allowed to resolve: ++ one that ends up outside DIR is refused, even if every symlink along it was ++ owned by a trusted user. Explicit source roots remain operator-selected ++ transfer roots. ++ The ownership walk asks who planted a link; this asks where the path came ++ out. + + DIR must be absolute. Nothing is confined by default, and + `--confine-root=/` is a no-op. +diff --git a/sender.c b/sender.c +index ba40b9468..1ff6a1d63 100644 +--- a/sender.c ++++ b/sender.c +@@ -56,6 +56,7 @@ extern char *module_dir; + extern int module_dirfd; + extern int write_batch; + extern int file_old_total; ++extern char *files_from; + extern BOOL want_progress_now; + extern struct stats stats; + extern struct file_list *cur_flist, *first_flist, *dir_flist; +@@ -90,6 +91,9 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname, + return -1; + } + ++ if (filesfrom_owner_walk_active()) ++ return owner_walk_parent(fname, bname_p); ++ + if (!am_daemon || !module_dir || module_dir[0] != '/') { + /* Local (non-daemon) sender: there is no module root to anchor at, but + * still confine the parent via the shared held ancestor-dirfd stack +@@ -680,26 +684,27 @@ void send_files(int f_in, int f_out) + else + fd = sender_open_confined(module_dir, relp, O_RDONLY); + } else if (!copy_links && !copy_unsafe_links && !copy_dirlinks && !insecure_links) { +- /* Default symlink handling (no dir-link following): the scan +- * recorded this as a regular file. Open it confined beneath the +- * transfer root: an in-tree symlinked parent (e.g. -R keeps one in +- * the path) is followed beneath the root, a parent raced into a +- * symlink pointing out of the tree is refused, and O_NOFOLLOW +- * governs the leaf so a raced leaf symlink is refused. A +- * symlink-following mode (-L/--copy-unsafe-links/-k) or +- * --insecure-links keeps the legacy open below. */ +- if (fname[0] == '/') { +- /* --relative (or a --files-from absolute name) keeps the +- * full absolute path as fname; the transfer root is then "/", +- * so anchor the confined open there and strip the leading +- * slash to the module-relative path the resolver wants -- it +- * rejects an absolute relpath outright. */ +- const char *relp = fname; +- while (*relp == '/') +- relp++; +- fd = sender_open_confined("/", relp, O_RDONLY); +- } else +- fd = sender_open_confined(NULL, fname, O_RDONLY); ++ int matched; ++ /* A files-from entry follows only trusted-owned ancestors because ++ * its source base is operator-selected but the entry itself may not ++ * be. Other paths stay confined beneath their explicit transfer root. ++ * Every file leaf remains O_NOFOLLOW. */ ++ if (files_from) { ++ fd = do_open_checklinks(fname); ++ } else { ++ fd = open_sender_source_path(fname, O_RDONLY | O_NOFOLLOW, &matched); ++ if (!matched) { ++ if (fname[0] == '/') { ++ /* --relative keeps the full absolute path as fname; ++ * anchor at "/" and pass the resolver a relative path. */ ++ const char *relp = fname; ++ while (*relp == '/') ++ relp++; ++ fd = sender_open_confined("/", relp, O_RDONLY); ++ } else ++ fd = sender_open_confined(NULL, fname, O_RDONLY); ++ } ++ } + } else { + fd = do_open_checklinks(fname); + } +@@ -809,6 +814,7 @@ void send_files(int f_in, int f_out) + if (DEBUG_GTE(SEND, 1)) + rprintf(FINFO, "send files finished\n"); + ++ clear_sender_source_roots(); + match_report(); + + write_ndx(f_out, NDX_DONE); +diff --git a/syscall.c b/syscall.c +index 1553f4716..8e96bebe4 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -62,6 +62,7 @@ extern int preserve_executability; + extern int open_noatime; + extern int copy_links; + extern int copy_unsafe_links; ++extern int copy_dirlinks; + extern int am_daemon; + extern int am_chrooted; + extern int insecure_links; +@@ -69,10 +70,12 @@ 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 *files_from; + 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 */ ++extern int filesfrom_owner_walk_override; /* defined below; used by files-from implied dirs */ + + /* A directory fd used only for pathname traversal, fchdir(), or as *at() + * authority does not need read permission on Linux. Keep the portable +@@ -96,7 +99,7 @@ static int directory_traverse_flags(void) + * 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) ++int open_anchor_dirfd(const char *path) + { + if (module_dirfd >= 0 && am_daemon && module_dir && strcmp(path, module_dir) == 0) + return dup(module_dirfd); +@@ -140,6 +143,16 @@ int symlink_optout_allowed(void) + return insecure_links; + } + ++/* A files-from entry is peer-selected, even when its source base was supplied ++ * by the operator. Keep its ownership walk inside --confine-root when one is ++ * active. */ ++int filesfrom_owner_walk_active(void) ++{ ++ return !am_daemon && am_sender && files_from ++ && !copy_unsafe_links && !copy_dirlinks && !insecure_links ++ && (!copy_links || filesfrom_owner_walk_override); ++} ++ + /* 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. +@@ -218,25 +231,29 @@ static int is_exact_fd_pin(const char *p) + + /* 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 ++ * (--partial-dir/--backup-dir/alt-basis/merge files or files-from entries). 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. ++ * `final` distinguishes a completed target from an ancestor crossed on the way ++ * to it. + * + * 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) ++static int abspath_outside_confinement(const char *abspath, int final) + { + unsigned int rootlen; + const char *root = confinement_root(&rootlen); + char pinned[MAXPATHLEN]; ++ int enforce; + + if (!root || !abspath) + return 0; + if (rootlen <= 1) /* root is "/": nothing is outside */ + return 0; ++ enforce = operator_path_resolve || filesfrom_owner_walk_active(); + /* 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, +@@ -250,7 +267,7 @@ static int abspath_outside_confinement(const char *abspath) + 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; ++ return enforce ? 1 : 0; + pinned[n] = '\0'; + abspath = pinned; + } +@@ -262,14 +279,43 @@ static int abspath_outside_confinement(const char *abspath) + * ("/", "/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) ++ * (operator_path_resolve or filesfrom_owner_walk_active()); 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; ++ return enforce && final ? 1 : 0; /* an ancestor is valid only while descending */ ++ return enforce ? 1 : 0; ++} ++ ++/* Check a completed path made from a tracked directory and one leaf. */ ++static int check_abspath_leaf(const char *base, const char *leaf) ++{ ++ char leafabs[MAXPATHLEN]; ++ size_t baselen; ++ int n; ++ ++ if (!base || !*base) { ++ if (abspath_outside_confinement(base, 1)) { ++ errno = ELOOP; ++ return -1; ++ } ++ return 0; ++ } ++ baselen = strlen(base); ++ n = snprintf(leafabs, sizeof leafabs, "%s%s%s", base, ++ base[baselen - 1] == '/' ? "" : "/", leaf); ++ if (n < 0 || (size_t)n >= sizeof leafabs) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ if (abspath_outside_confinement(leafabs, 1)) { ++ errno = ELOOP; ++ return -1; ++ } ++ return 0; + } + + /* Advance the tracked absolute path `abspath` by one resolved component, +@@ -427,7 +473,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + saved_errno = errno; + goto out; + } +- if (!pin_transit && abspath_outside_confinement(abspath)) { ++ if (!pin_transit && abspath_outside_confinement(abspath, 1)) { + saved_errno = ELOOP; + goto out; + } +@@ -549,7 +595,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + saved_errno = errno; + goto out; + } +- if (!pin_transit && abspath_outside_confinement(abspath)) { ++ if (!pin_transit && abspath_outside_confinement(abspath, 1)) { + saved_errno = ELOOP; + goto out; + } +@@ -560,7 +606,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + 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); ++ strlcpy(out_abs, !abspath[0] ? "/" : abspath, out_cap); + goto out; + } + +@@ -573,7 +619,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + saved_errno = errno; + goto out; + } +- if (!pin_transit && abspath_outside_confinement(abspath)) { ++ if (!pin_transit && abspath_outside_confinement(abspath, 0)) { + saved_errno = ELOOP; + goto out; + } +@@ -601,12 +647,16 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + * an O_PATH fd is sufficient for traversal and fchdir but not operations + * such as fchmod. */ + if (flags & O_DIRECTORY) { ++ if (!pin_transit && abspath_outside_confinement(abspath, 1)) { ++ saved_errno = ELOOP; ++ goto out; ++ } + retfd = openat(dfd, ".", flags | O_NOFOLLOW, mode); + saved_errno = retfd < 0 ? 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); ++ strlcpy(out_abs, !abspath[0] ? "/" : abspath, out_cap); + } else { + saved_errno = EISDIR; + } +@@ -645,6 +695,7 @@ int open_no_attacker_symlinks_dirfd(const char *path) + * 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; ++int filesfrom_owner_walk_override = 0; + + #if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY + /* For an operator-supplied path: open its parent directory via the ownership +@@ -678,15 +729,8 @@ int owner_walk_parent(const char *path, const char **bname) + * 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)) { ++ if (check_abspath_leaf(pabs, *bname) < 0) { + close(dfd); +- errno = ELOOP; + return -1; + } + } +@@ -2913,12 +2957,14 @@ static int ds_path_push(struct dirstack *ds, const char *comp) + if (al == 0) + return 0; /* unseeded: tracking disabled for this walk */ + size_t cl = strlen(comp); +- if (al + 1 + cl >= sizeof ds->abspath) { ++ size_t off = (al > 0 && ds->abspath[al - 1] == '/') ? al : al + 1; ++ if (off + cl >= sizeof ds->abspath) { + errno = ENAMETOOLONG; + return -1; + } +- ds->abspath[al] = '/'; +- memcpy(ds->abspath + al + 1, comp, cl + 1); ++ if (off != al) ++ ds->abspath[al] = '/'; ++ memcpy(ds->abspath + off, comp, cl + 1); + return 0; + } + +@@ -3005,7 +3051,7 @@ static int ds_descend(struct dirstack *ds, const char *part, int *hops) + 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)) { ++ if (abspath_outside_confinement(ds->abspath, 0)) { + errno = ELOOP; + return -1; + } +@@ -3119,7 +3165,10 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath, + if (ds_descend(&ds, part, hops) < 0) + goto cleanup; + if (is_last) { +- if (flags & O_DIRECTORY) ++ if ((flags & O_DIRECTORY) ++ && abspath_outside_confinement(ds.abspath, 1)) ++ errno = ELOOP; ++ else if (flags & O_DIRECTORY) + retfd = openat(ds_cur(&ds), ".", flags | O_NOFOLLOW, mode); + else + errno = EISDIR; +@@ -3131,15 +3180,8 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath, + /* 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; +- } +- } ++ if (check_abspath_leaf(ds.abspath, part) < 0) ++ goto cleanup; + int next_fd = openat(ds_cur(&ds), part, + directory_traverse_flags() | O_NOFOLLOW); + if (next_fd == -1 && (errno == ENOTDIR || errno == ENOENT)) { +@@ -3155,6 +3197,8 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath, + + /* O_DIRECTORY|O_NOFOLLOW leaf: the caller's O_NOFOLLOW governs the leaf. */ + if (is_last && (flags & O_NOFOLLOW)) { ++ if (check_abspath_leaf(ds.abspath, part) < 0) ++ goto cleanup; + retfd = openat(ds_cur(&ds), part, flags | O_NOFOLLOW, mode); + goto cleanup; + } +@@ -3167,6 +3211,10 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath, + goto cleanup; + } + if (is_last) { ++ if (abspath_outside_confinement(ds.abspath, 1)) { ++ errno = ELOOP; ++ goto cleanup; ++ } + retfd = openat(ds_cur(&ds), ".", flags | O_NOFOLLOW, mode); + goto cleanup; + } +@@ -3176,7 +3224,10 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath, + * access, 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) ++ if ((flags & O_DIRECTORY) ++ && abspath_outside_confinement(ds.abspath, 1)) ++ errno = ELOOP; ++ else if ((flags & O_DIRECTORY) && anchor_fd != AT_FDCWD) + retfd = openat(anchor_fd, ".", flags | O_NOFOLLOW, mode); + else + errno = EISDIR; +@@ -3568,6 +3619,26 @@ int do_open_checklinks(const char *pathname) + if (copy_links || copy_unsafe_links) { + return do_open(pathname, O_RDONLY, 0); + } ++#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY ++ if (am_sender && !am_daemon && files_from ++ && !copy_dirlinks && !symlink_optout_allowed()) { ++ const char *bname; ++ int dfd, fd, save_errno, open_flags = O_RDONLY | O_NOFOLLOW; ++ ++ dfd = owner_walk_parent(pathname, &bname); ++ if (dfd < 0) ++ return -1; ++#ifdef O_NOATIME ++ if (open_noatime) ++ open_flags |= O_NOATIME; ++#endif ++ fd = openat(dfd, bname, open_flags, 0); ++ save_errno = fd < 0 ? errno : 0; ++ close(dfd); ++ errno = save_errno; ++ return fd; ++ } ++#endif + return do_open_nofollow(pathname, O_RDONLY); + } + +diff --git a/t_secure_relpath.c b/t_secure_relpath.c +index d20570d61..184acef67 100644 +--- a/t_secure_relpath.c ++++ b/t_secure_relpath.c +@@ -224,7 +224,6 @@ int main(int argc, char **argv) + * literal '..'. Its dedicated fd-anchored entry point must preserve an + * in-tree climb while refusing to pop above the anchor. */ + check_beneath_dotdot(); +- + if (errs) + fprintf(stderr, "\n%d failure(s)\n", errs); + return errs ? 1 : 0; +diff --git a/t_stub.c b/t_stub.c +index 1518c7932..c5a39bfab 100644 +--- a/t_stub.c ++++ b/t_stub.c +@@ -26,6 +26,7 @@ int inplace = 0; + int am_daemon = 0; + int am_chrooted = 0; + int insecure_links = 0; ++int copy_dirlinks = 0; + int modify_window = 0; + int preallocate_files = 0; + int sparse_files = 0; +@@ -45,6 +46,7 @@ size_t max_alloc = (size_t)-1; /* test helpers are not memory-constrained; + * per-component fallback of secure_relative_open() + * hits at its first my_strdup() call. */ + char *partial_dir; ++char *files_from; + char *module_dir; + int module_dirfd = -1; + char *confine_root; +diff --git a/testsuite/relative-source-ancestor_test.py b/testsuite/relative-source-ancestor_test.py +new file mode 100644 +index 000000000..3864ededc +--- /dev/null ++++ b/testsuite/relative-source-ancestor_test.py +@@ -0,0 +1,296 @@ ++#!/usr/bin/env python3 ++"""Explicit source directory symlinks remain transfer roots.""" ++ ++import os ++import pwd ++import subprocess ++ ++from rsyncfns import SCRATCHDIR, rsync_argv, test_fail ++ ++real = SCRATCHDIR / 'real' ++source = real / 'My_Documents' ++source.mkdir(parents=True) ++(source / 'marker').write_text('source contents\n') ++absolute_link = SCRATCHDIR / 'home' ++relative_link = SCRATCHDIR / 'relative-home' ++os.symlink(str(real), absolute_link) ++os.symlink(str(real), relative_link) ++ ++cases = ( ++ (('-r',), False), ++ (('-rR',), True), ++ (('-rR', '--no-inc-recursive'), True), ++) ++ ++for link_name, link, cwd in ( ++ ('absolute', absolute_link, None), ++ ('relative', relative_link, SCRATCHDIR), ++): ++ source_arg = (str(SCRATCHDIR) + '/./home/My_Documents/' if cwd is None ++ else 'relative-home/My_Documents/') ++ for index, (options, relative) in enumerate(cases): ++ dest = SCRATCHDIR / f'dest-{link_name}-descendant-{index}' ++ dest.mkdir() ++ proc = subprocess.run( ++ rsync_argv(*options, source_arg, str(dest) + '/'), ++ cwd=cwd, capture_output=True, text=True, ++ ) ++ if proc.returncode: ++ test_fail(f'{link_name} descendant source transfer with {options} ' ++ f'failed: {proc.stdout}{proc.stderr}') ++ expected = (dest / link.name / 'My_Documents' / 'marker' if relative ++ else dest / 'marker') ++ if not expected.is_file() or expected.read_text() != 'source contents\n': ++ test_fail('relative source layout or contents changed') ++ ++file_sources = ( ++ ('absolute-file', str(absolute_link / 'My_Documents' / 'marker'), None), ++ ('relative-file', 'relative-home/My_Documents/marker', SCRATCHDIR), ++) ++for name, source_arg, cwd in file_sources: ++ dest = SCRATCHDIR / f'dest-{name}' ++ dest.mkdir() ++ proc = subprocess.run( ++ rsync_argv('-R', source_arg, str(dest) + '/'), ++ cwd=cwd, capture_output=True, text=True, ++ ) ++ if proc.returncode: ++ test_fail(f'{name} transfer failed: {proc.stdout}{proc.stderr}') ++ if cwd is None: ++ expected = dest / str(absolute_link.relative_to('/')) / 'My_Documents' / 'marker' ++ else: ++ expected = dest / source_arg ++ if not expected.is_file() or expected.read_text() != 'source contents\n': ++ test_fail(f'{name} layout or contents changed') ++ ++files_from = SCRATCHDIR / 'files-from' ++files_from.write_text('relative-home/My_Documents/marker\n') ++dest = SCRATCHDIR / 'dest-files-from' ++dest.mkdir() ++proc = subprocess.run( ++ rsync_argv('-r', f'--files-from={files_from}', ++ str(SCRATCHDIR) + '/', str(dest) + '/'), ++ capture_output=True, text=True, ++) ++expected = dest / 'relative-home' / 'My_Documents' / 'marker' ++if proc.returncode or not expected.is_file() or expected.read_text() != 'source contents\n': ++ test_fail(f'files-from trusted ancestor transfer failed: ' ++ f'{proc.stdout}{proc.stderr}') ++ ++files_from.write_text('relative-home/My_Documents/\n') ++dest = SCRATCHDIR / 'dest-files-from-dir' ++dest.mkdir() ++proc = subprocess.run( ++ rsync_argv('-r', f'--files-from={files_from}', ++ str(SCRATCHDIR) + '/', str(dest) + '/'), ++ capture_output=True, text=True, ++) ++expected = dest / 'relative-home' / 'My_Documents' / 'marker' ++if proc.returncode or not expected.is_file() or expected.read_text() != 'source contents\n': ++ test_fail(f'files-from trusted directory transfer failed: ' ++ f'{proc.stdout}{proc.stderr}') ++ ++remove_source = real / 'remove-marker' ++remove_source.write_text('remove contents\n') ++files_from.write_text('relative-home/remove-marker\n') ++dest = SCRATCHDIR / 'dest-files-from-remove' ++dest.mkdir() ++proc = subprocess.run( ++ rsync_argv('-r', '--remove-source-files', f'--files-from={files_from}', ++ str(SCRATCHDIR) + '/', str(dest) + '/'), ++ capture_output=True, text=True, ++) ++expected = dest / 'relative-home' / 'remove-marker' ++if proc.returncode or remove_source.exists() or not expected.is_file(): ++ test_fail(f'files-from remove-source-files failed: ' ++ f'{proc.stdout}{proc.stderr}') ++ ++confined_root = SCRATCHDIR / 'confined-root' ++confined_root.mkdir() ++confined_outside = SCRATCHDIR / 'confined-outside' ++confined_outside.mkdir() ++(confined_outside / 'marker').write_text('outside contents\n') ++os.symlink(str(confined_outside), confined_root / 'outside-link') ++confined_inside = confined_root / 'inside' ++confined_inside.mkdir() ++(confined_inside / 'marker').write_text('inside contents\n') ++os.symlink(str(confined_inside), confined_root / 'inside-link') ++confined_files_from = confined_root / 'files-from' ++ ++confined_files_from.write_text('inside-link/marker\n') ++dest = confined_root / 'confined-inside-dest' ++dest.mkdir() ++proc = subprocess.run( ++ rsync_argv('-r', f'--confine-root={confined_root}', ++ f'--files-from={confined_files_from}', ++ str(confined_root) + '/', str(dest) + '/'), ++ capture_output=True, text=True, ++) ++expected = dest / 'inside-link' / 'marker' ++if proc.returncode or not expected.is_file() or expected.read_text() != 'inside contents\n': ++ test_fail(f'files-from trusted in-root link failed under confinement: ' ++ f'{proc.stdout}{proc.stderr}') ++ ++confined_files_from.write_text('outside-link/marker\n') ++dest = confined_root / 'confined-outside-dest' ++dest.mkdir() ++proc = subprocess.run( ++ rsync_argv('-r', f'--confine-root={confined_root}', ++ f'--files-from={confined_files_from}', ++ str(confined_root) + '/', str(dest) + '/'), ++ capture_output=True, text=True, ++) ++escaped = dest / 'outside-link' / 'marker' ++if proc.returncode == 0 or escaped.exists(): ++ test_fail(f'files-from followed a trusted link outside --confine-root: ' ++ f'{proc.stdout}{proc.stderr}') ++ ++confined_files_from.write_text('outside-link/\n') ++dest = confined_root / 'confined-outside-dir-dest' ++dest.mkdir() ++proc = subprocess.run( ++ rsync_argv('-r', f'--confine-root={confined_root}', ++ f'--files-from={confined_files_from}', ++ str(confined_root) + '/', str(dest) + '/'), ++ capture_output=True, text=True, ++) ++escaped = dest / 'outside-link' / 'marker' ++if proc.returncode == 0 or escaped.exists(): ++ test_fail(f'files-from enumerated a trusted link outside --confine-root: ' ++ f'{proc.stdout}{proc.stderr}') ++ ++os.symlink(str(confined_root.parent), confined_root / 'ancestor-link') ++confined_files_from.write_text('ancestor-link/\n') ++dest = confined_root / 'confined-ancestor-dest' ++dest.mkdir() ++proc = subprocess.run( ++ rsync_argv('-d', f'--confine-root={confined_root}', ++ f'--files-from={confined_files_from}', ++ str(confined_root) + '/', str(dest) + '/'), ++ capture_output=True, text=True, ++) ++if proc.returncode == 0 or (dest / 'ancestor-link').exists(): ++ test_fail(f'files-from accepted a trusted ancestor of --confine-root: ' ++ f'{proc.stdout}{proc.stderr}') ++ ++root_real = SCRATCHDIR / 'root-real' ++root_real.mkdir() ++(root_real / 'marker').write_text('source contents\n') ++absolute_root_link = SCRATCHDIR / 'root-home' ++relative_root_link = SCRATCHDIR / 'relative-root-home' ++os.symlink(str(root_real), absolute_root_link) ++os.symlink(str(root_real), relative_root_link) ++ ++for link_name, link, cwd in ( ++ ('absolute', absolute_root_link, None), ++ ('relative', relative_root_link, SCRATCHDIR), ++): ++ source_arg = (str(SCRATCHDIR) + '/./root-home/' if cwd is None ++ else 'relative-root-home/') ++ for index, (options, relative) in enumerate(cases): ++ dest = SCRATCHDIR / f'dest-{link_name}-root-{index}' ++ dest.mkdir() ++ proc = subprocess.run( ++ rsync_argv(*options, source_arg, str(dest) + '/'), ++ cwd=cwd, capture_output=True, text=True, ++ ) ++ if proc.returncode: ++ test_fail(f'{link_name} root source transfer with {options} failed: ' ++ f'{proc.stdout}{proc.stderr}') ++ expected = dest / link.name / 'marker' if relative else dest / 'marker' ++ if not expected.is_file() or expected.read_text() != 'source contents\n': ++ test_fail('explicit source-root layout or contents changed') ++ ++if os.geteuid() == 0: ++ untrusted_uid = next((entry.pw_uid for entry in pwd.getpwall() ++ if entry.pw_uid != 0), None) ++ if untrusted_uid is not None: ++ untrusted_link = SCRATCHDIR / 'untrusted-home' ++ os.symlink(str(real), untrusted_link) ++ os.lchown(untrusted_link, untrusted_uid, -1) ++ source_arg = str(SCRATCHDIR) + '/./untrusted-home/' ++ for index, (options, relative) in enumerate(cases[:2]): ++ dest = SCRATCHDIR / f'untrusted-dest-{index}' ++ dest.mkdir() ++ proc = subprocess.run( ++ rsync_argv(*options, source_arg, str(dest) + '/'), ++ capture_output=True, text=True, ++ ) ++ expected = dest / 'My_Documents' / 'marker' ++ if relative: ++ expected = dest / 'untrusted-home' / 'My_Documents' / 'marker' ++ if proc.returncode or not expected.is_file(): ++ test_fail(f'explicit untrusted-owned source link with {options} ' ++ f'failed: {proc.stdout}{proc.stderr}') ++ ++ dest = SCRATCHDIR / 'untrusted-file-dest' ++ dest.mkdir() ++ source_arg = str(untrusted_link / 'My_Documents' / 'marker') ++ proc = subprocess.run( ++ rsync_argv('-R', source_arg, str(dest) + '/'), ++ capture_output=True, text=True, ++ ) ++ expected = dest / str(untrusted_link.relative_to('/')) / 'My_Documents' / 'marker' ++ if proc.returncode or not expected.is_file(): ++ test_fail(f'explicit untrusted-owned file path failed: ' ++ f'{proc.stdout}{proc.stderr}') ++ ++ files_from.write_text('untrusted-home/My_Documents/marker\n') ++ dest = SCRATCHDIR / 'untrusted-files-from-dest' ++ dest.mkdir() ++ proc = subprocess.run( ++ rsync_argv('-r', f'--files-from={files_from}', ++ str(SCRATCHDIR) + '/', str(dest) + '/'), ++ capture_output=True, text=True, ++ ) ++ escaped = dest / 'untrusted-home' / 'My_Documents' / 'marker' ++ if proc.returncode == 0 or escaped.exists(): ++ test_fail('files-from followed an untrusted-owned ancestor symlink') ++ ++ files_from.write_text('untrusted-home/My_Documents/\n') ++ dest = SCRATCHDIR / 'untrusted-files-from-dir-dest' ++ dest.mkdir() ++ proc = subprocess.run( ++ rsync_argv('-r', f'--files-from={files_from}', ++ str(SCRATCHDIR) + '/', str(dest) + '/'), ++ capture_output=True, text=True, ++ ) ++ escaped = dest / 'untrusted-home' / 'My_Documents' / 'marker' ++ if proc.returncode == 0 or escaped.exists(): ++ test_fail('files-from enumerated an untrusted-owned ancestor symlink') ++ ++ files_from.write_text('untrusted-home/\n') ++ dest = SCRATCHDIR / 'untrusted-files-from-leaf-dir-dest' ++ dest.mkdir() ++ proc = subprocess.run( ++ rsync_argv('-r', f'--files-from={files_from}', ++ str(SCRATCHDIR) + '/', str(dest) + '/'), ++ capture_output=True, text=True, ++ ) ++ escaped = dest / 'untrusted-home' / 'marker' ++ if proc.returncode == 0 or escaped.exists(): ++ test_fail('files-from followed an untrusted-owned directory link') ++ ++ files_from.write_text('untrusted-home/My_Documents/marker\n') ++ dest = SCRATCHDIR / 'insecure-files-from-dest' ++ dest.mkdir() ++ proc = subprocess.run( ++ rsync_argv('-r', '--insecure-links', f'--files-from={files_from}', ++ str(SCRATCHDIR) + '/', str(dest) + '/'), ++ capture_output=True, text=True, ++ ) ++ expected = dest / 'untrusted-home' / 'My_Documents' / 'marker' ++ if proc.returncode or not expected.is_file(): ++ test_fail(f'files-from insecure-links opt-out failed: ' ++ f'{proc.stdout}{proc.stderr}') ++ ++dest = SCRATCHDIR / 'remove-dest' ++dest.mkdir() ++proc = subprocess.run( ++ rsync_argv('-r', '--remove-source-files', str(absolute_link / 'My_Documents') + '/', str(dest) + '/'), ++ capture_output=True, text=True, ++) ++expected = dest / 'marker' ++if proc.returncode or (source / 'marker').exists() or not expected.is_file(): ++ test_fail(f'remove-source-files failed: {proc.stdout}{proc.stderr}') ++(source / 'marker').write_text('source contents\n') diff -Nru rsync-3.4.1+ds1/debian/patches/syscall_use_O_PATH_for_directory_traversal.patch rsync-3.5.0+ds1/debian/patches/syscall_use_O_PATH_for_directory_traversal.patch --- rsync-3.4.1+ds1/debian/patches/syscall_use_O_PATH_for_directory_traversal.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/syscall_use_O_PATH_for_directory_traversal.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,159 @@ +From: Zen Dodd +Date: Sun, 16 Aug 2026 21:52:11 +1000 +Subject: [PATCH] syscall: use O_PATH for directory traversal (#1052) + +Backported-by: Samuel Henrique + * Drop the testsuite/skiplist/cygwin.txt and testsuite/skiplist/macos.txt + hunks, they add the new test out of order, which makes the skiplist-spec + test fail, and only upstream's CI uses those lists + * Refresh context +--- + syscall.c | 21 +++++---- + testsuite/search-only-destination_test.py | 77 +++++++++++++++++++++++++++++++ + 2 files changed, 90 insertions(+), 8 deletions(-) + create mode 100644 testsuite/search-only-destination_test.py + +diff --git a/syscall.c b/syscall.c +index 98a01fb..897974e 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -309,6 +309,11 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + * fallback in syscall.c so a build without it still compiles. */ + #ifndef O_CLOEXEC + #define O_CLOEXEC 0 ++#endif ++#ifdef O_PATH ++ const int dir_traverse_flags = O_PATH | O_DIRECTORY | O_CLOEXEC; ++#else ++ const int dir_traverse_flags = O_RDONLY | O_DIRECTORY | O_CLOEXEC; + #endif + if (!path || !*path) { + errno = EINVAL; +@@ -371,7 +376,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + + /* Absolute path: pin "/" as the starting dfd. */ + if (remaining[0] == '/') { +- dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); ++ dfd = open("/", dir_traverse_flags); + if (dfd < 0) + return -1; + dfd_owns = 1; +@@ -506,7 +511,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + + if (target[0] == '/') { + if (dfd_owns) close(dfd); +- dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); ++ dfd = open("/", dir_traverse_flags); + if (dfd < 0) { + saved_errno = errno; + dfd_owns = 0; +@@ -562,7 +567,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + saved_errno = ELOOP; + goto out; + } +- int next = openat(dfd, comp, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); ++ int next = openat(dfd, comp, dir_traverse_flags | O_NOFOLLOW); + if (next < 0) { + saved_errno = errno; + goto out; +@@ -582,12 +587,12 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz + } + + /* 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. */ ++ * Reopen the held traversal fd with the caller's requested access mode; ++ * an O_PATH fd is sufficient for traversal and fchdir but not operations ++ * such as fchmod. */ + if (flags & O_DIRECTORY) { +- retfd = dfd; +- dfd_owns = 0; /* caller now owns it */ +- saved_errno = 0; ++ retfd = openat(dfd, ".", flags | O_NOFOLLOW, mode); ++ saved_errno = retfd < 0 ? 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). */ +diff --git a/testsuite/search-only-destination_test.py b/testsuite/search-only-destination_test.py +new file mode 100644 +index 0000000..46108e2 +--- /dev/null ++++ b/testsuite/search-only-destination_test.py +@@ -0,0 +1,77 @@ ++#!/usr/bin/env python3 ++"""The receiver must traverse a searchable but unreadable destination parent. ++ ++Android exposes /sdcard through such a path, so the race-safe destination walk ++must use directory descriptors that require search permission only. ++""" ++ ++import os ++import shutil ++import subprocess ++import sys ++import tempfile ++from pathlib import Path ++ ++from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped ++ ++if not sys.platform.startswith('linux'): ++ test_skipped('search-only-destination is Linux-specific') ++ ++launcher = [] ++if os.geteuid() == 0: ++ setpriv = shutil.which('setpriv') ++ if setpriv is None: ++ test_skipped('setpriv is unavailable for the root-run testsuite') ++ launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups'] ++ ++external_base = os.geteuid() == 0 ++if external_base: ++ base = Path(tempfile.mkdtemp(prefix='rsync-search-only-')) ++ base.chmod(0o755) ++else: ++ base = SCRATCHDIR / 'search-only-destination' ++src = base / 'src' ++parent = base / 'search-only' ++dest = parent / 'dest' ++rmtree(base) ++src.mkdir(parents=True) ++dest.mkdir(parents=True) ++(src / 'probe').write_text('search-only destination\n') ++ ++if os.geteuid() == 0: ++ for path in (src, src / 'probe', dest): ++ os.chown(path, 65534, 65534) ++ ++try: ++ parent.chmod(0o111) ++ try: ++ probe = subprocess.run( ++ launcher + ['test', '-r', str(parent)], ++ stdout=subprocess.DEVNULL, ++ stderr=subprocess.DEVNULL, ++ ) ++ if probe.returncode == 0: ++ test_skipped('filesystem does not enforce the search-only test mode') ++ if probe.returncode != 1: ++ test_fail(f'search-only permission probe failed with exit {probe.returncode}') ++ ++ proc = subprocess.run( ++ launcher + rsync_argv('-a', f'{src}/', f'{dest}/'), ++ stdout=subprocess.PIPE, ++ stderr=subprocess.PIPE, ++ text=True, ++ ) ++ finally: ++ parent.chmod(0o755) ++ copied = (dest / 'probe').read_text() if (dest / 'probe').is_file() else None ++finally: ++ if external_base: ++ rmtree(base) ++ ++if proc.returncode != 0: ++ test_fail( ++ 'receiver could not enter a destination below a searchable, unreadable ' ++ f'parent (exit {proc.returncode}): {proc.stderr.strip()}' ++ ) ++if copied != 'search-only destination\n': ++ test_fail('receiver did not copy into the search-only destination') diff -Nru rsync-3.4.1+ds1/debian/patches/syscall_use_O_PATH_for_held_directory_traversal.patch rsync-3.5.0+ds1/debian/patches/syscall_use_O_PATH_for_held_directory_traversal.patch --- rsync-3.4.1+ds1/debian/patches/syscall_use_O_PATH_for_held_directory_traversal.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/syscall_use_O_PATH_for_held_directory_traversal.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,790 @@ +From: Zayd Rajab +Date: Fri, 28 Aug 2026 02:37:01 -0700 +Subject: [PATCH] syscall: use O_PATH for held directory traversal (#1065) + +Use O_PATH on Linux for directory descriptors used only for path +traversal, fchdir(), or as *at() parents. Reopen final directory +endpoints with the caller-requested access mode and retain the existing +O_RDONLY fallback elsewhere. + +Add coverage for exact sources and readable descendants beneath +search-only directories, known-file creation beneath write/search-only +destinations, and retained refusal to enumerate unreadable directories. + +Fixes #1064. + +Backported-by: Samuel Henrique + * Drop the testsuite/skiplist/cygwin.txt and testsuite/skiplist/macos.txt + hunks, they don't apply and only upstream's CI uses those lists + * Refresh context +--- + SECURITY.md | 62 +++++---- + generator.c | 2 +- + sender.c | 15 +-- + syscall.c | 111 ++++++++++------ + testsuite/search-only-held-dirfd_test.py | 215 +++++++++++++++++++++++++++++++ + util1.c | 10 +- + 6 files changed, 332 insertions(+), 83 deletions(-) + create mode 100644 testsuite/search-only-held-dirfd_test.py + +diff --git a/SECURITY.md b/SECURITY.md +index 2323bbf..bcc8491 100644 +--- a/SECURITY.md ++++ b/SECURITY.md +@@ -146,28 +146,29 @@ following symlinks by design. + + ### The mechanism + +-Resolution of attacker-influenceable paths goes through `secure_relative_open()` +-and the `do_*_at()` wrappers in `syscall.c`, never a raw `open()`/`rename()`/ +-`chmod()` on a full path string. The principle is: **trust the operator-named +-transfer root, and confine all resolution beneath it**, rejecting escapes via +-`..` above the anchor, absolute symlinks, or out-of-tree symlinks. +-`secure_relative_open()` resolves the parent directory by walking it one +-component at a time on a stack of held directory fds, then operates on the final +-component with an at-style call on the resulting directory fd. ++Resolution of attacker-influenceable paths goes through `secure_relative_open()`, ++`secure_relative_dirfd()`, and the `do_*_at()` wrappers in `syscall.c`, never a ++raw `open()`/`rename()`/`chmod()` on a full path string. The principle is: ++**trust the operator-named transfer root, and confine all resolution beneath ++it**, rejecting escapes via `..` above the anchor, absolute symlinks, or ++out-of-tree symlinks. `secure_relative_open()` opens the resolved endpoint with ++the caller's requested access. `secure_relative_dirfd()` instead returns ++traversal authority for `fchdir()` or an at-style operation on a known child; ++it does not imply permission to enumerate the directory. + + For per-entry work the receiver and generator go one step further and hold the + parent directory open: `open_dir_secure()` resolves an entry's directory once +-(via `secure_relative_open()`), `held_dfd_for()` caches that descriptor for the +-duration of the entry, and every operation on the entry — `lstat`, the temp-file +-`mkstemp`, the temp->final `rename`, `chmod`/`chown`/`utimes`, `mkdir`, special- +-file and symlink creation, the delta-basis open, and the recursive delete — runs +-through that one held fd via an `*at()` call (`do_*_atfd()`). Because the +-descriptor is pinned to the directory inode, a parent component flipped to a +-symlink *after* the open cannot redirect any of those operations. The alternate- +-destination lookups are confined the same way (`basis_link_stat()` in +-`generator.c` and `secure_basis_open()` in `receiver.c`), so a peer-chosen +-`--link-dest`/`--compare-dest`/`--copy-dest` basis index cannot reach an +-out-of-module file through a symlinked parent. ++as traversal authority, `held_dfd_for()` caches that descriptor for the ++duration of the entry, and every operation on the entry — `lstat`, the ++temp-file `mkstemp`, the temp->final `rename`, `chmod`/`chown`/`utimes`, ++`mkdir`, special-file and symlink creation, the delta-basis open, and the ++recursive delete — runs through that one held fd via an `*at()` call ++(`do_*_atfd()`). Because the descriptor is pinned to the directory inode, a ++parent component flipped to a symlink *after* the open cannot redirect any of ++those operations. The alternate-destination lookups are confined the same way ++(`basis_link_stat()` in `generator.c` and `secure_basis_open()` in ++`receiver.c`), so a peer-chosen `--link-dest`/`--compare-dest`/`--copy-dest` ++basis index cannot reach an out-of-module file through a symlinked parent. + + The sender's source-directory *enumeration* is confined the same way as its + content open. `send_directory()` opens each scanned directory through +@@ -190,13 +191,17 @@ symlink would otherwise introduce. + ### Path resolution + + `secure_relative_open()` resolves a path with a single portable mechanism on +-every platform: a per-component walk on a stack of held directory fds. Each +-component is opened relative to the held parent with `openat(parent_fd, +-"component", O_NOFOLLOW)`; descending into a real subdirectory pushes its fd, a +-`..` pops back to the already-held parent (a pop at the anchor is refused), and an +-in-tree directory symlink is followed by reading its target and walking that off +-the same stack (absolute targets refused, symlink hops bounded). The final +-component is opened `O_NOFOLLOW`. ++every platform: a per-component walk on a stack of held directory fds. On ++Linux, anchors and traversal components use ++`O_PATH|O_DIRECTORY|O_NOFOLLOW`; other platforms retain the ++`O_RDONLY|O_DIRECTORY` fallback. Descending into a real subdirectory pushes ++its fd, a `..` pops back to the already-held parent (a pop at the anchor is ++refused), and an in-tree directory symlink is followed by reading its target ++and walking that off the same stack (absolute targets refused, symlink hops ++bounded). A final directory endpoint is reopened with the caller's requested ++flags. Thus `secure_opendir()` still receives a readable fd, while known-name ++operations beneath a searchable but unreadable directory do not require ++permission to list it. + + Because every component is opened relative to a *pinned* fd under `O_NOFOLLOW`, + and `..` is resolved by the held-fd stack rather than by the kernel, the walk is +@@ -235,8 +240,9 @@ chmod-ing through a raced leaf symlink. + influenced by the remote peer or by another local user, use a `do_*_at()` + wrapper (or `secure_relative_open()`), not a raw full-path syscall. + * When introducing a new operation, add a matching `do__at()` wrapper that +- resolves the parent with `secure_relative_open()` and acts via an at-style call +- on the returned dirfd. ++ resolves a parent used only as at-style authority with ++ `secure_relative_dirfd()`. Use `secure_relative_open()` when the returned fd ++ itself must be readable or otherwise support the caller's requested access. + * Do not assume a non-daemon transfer is safe; the question is whether rsync has + more authority than whoever controls the path components. + * On platforms whose API lacks an at-style equivalent (e.g. `setattrlist()`), +diff --git a/generator.c b/generator.c +index 7e5ad60..6f28188 100644 +--- a/generator.c ++++ b/generator.c +@@ -1052,7 +1052,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 = secure_relative_dirfd(NULL, dir)) < 0) + return -1; + r = link_stat_at(dfd, slash + 1, stp, 0); + e = errno; +diff --git a/sender.c b/sender.c +index bb1b137..ba40b94 100644 +--- a/sender.c ++++ b/sender.c +@@ -127,8 +127,7 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname, + #endif + while (*rel == '/') + rel++; +- return secure_relative_open("/", rel, +- O_RDONLY | O_DIRECTORY, 0); ++ return secure_relative_dirfd("/", rel); + } + /* held_dir_path_fd returns a cache-OWNED fd; the caller closes + * what we return, so hand back an owned dup and leave the cache's +@@ -141,7 +140,7 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname, + return dup(dfd); + if (errno != 0) + return -1; +- return secure_relative_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0); ++ return secure_relative_dirfd(NULL, dir); + } + errno = 0; /* top-level file: no parent component to confine */ + return -1; +@@ -176,9 +175,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 = secure_relative_dirfd(module_dir, dir); + } else +- dfd = secure_relative_open(module_dir, "", O_RDONLY | O_DIRECTORY, 0); ++ dfd = secure_relative_dirfd(module_dir, ""); + + /* The leaf is the same last component either way; take it from the caller's + * persistent fname buffer, not the local secure_path. */ +@@ -292,11 +291,9 @@ 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, +- O_RDONLY | O_DIRECTORY, 0); ++ pdfd = secure_relative_dirfd_at_beneath(module_dirfd, dir); + else +- pdfd = secure_relative_open(anchor, dir, +- O_RDONLY | O_DIRECTORY, 0); ++ pdfd = secure_relative_dirfd(anchor, dir); + if (pdfd < 0) + return -1; + n = do_readlink_atfd(pdfd, bname, tgt, sizeof tgt - 1); +diff --git a/syscall.c b/syscall.c +index 897974e..1553f47 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -74,6 +74,20 @@ 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 */ + ++/* A directory fd used only for pathname traversal, fchdir(), or as *at() ++ * authority does not need read permission on Linux. Keep the portable ++ * O_RDONLY fallback for systems without O_PATH. */ ++static int directory_traverse_flags(void) ++{ ++#if defined O_PATH && defined O_DIRECTORY ++ return O_PATH | O_DIRECTORY; ++#elif defined O_DIRECTORY ++ return O_RDONLY | O_DIRECTORY; ++#else ++ return O_RDONLY; ++#endif ++} ++ + #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 +@@ -86,7 +100,7 @@ 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); ++ return openat(AT_FDCWD, path, directory_traverse_flags()); + } + #endif + +@@ -304,17 +318,13 @@ static int abspath_step(char *abspath, size_t cap, const char *comp, size_t comp + * 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 ++#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY + /* 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 +-#ifdef O_PATH +- const int dir_traverse_flags = O_PATH | O_DIRECTORY | O_CLOEXEC; +-#else +- const int dir_traverse_flags = O_RDONLY | O_DIRECTORY | O_CLOEXEC; +-#endif ++ const int dir_traverse_flags = directory_traverse_flags() | O_CLOEXEC; + if (!path || !*path) { + errno = EINVAL; + return -1; +@@ -617,6 +627,14 @@ int open_no_attacker_symlinks(const char *path, int flags, mode_t mode) + return ona_open(path, flags, mode, NULL, 0); + } + ++/* Open a directory for traversal or as *at()/fchdir() authority. Unlike an ++ * O_RDONLY directory endpoint, this accepts a searchable but unreadable ++ * directory on Linux. */ ++int open_no_attacker_symlinks_dirfd(const char *path) ++{ ++ return ona_open(path, directory_traverse_flags(), 0, 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, +@@ -642,7 +660,7 @@ int owner_walk_parent(const char *path, const char **bname) + *bname = slash ? slash + 1 : path; + pabs[0] = '\0'; + if (!slash) +- dfd = ona_open(".", O_RDONLY | O_DIRECTORY, 0, pabs, sizeof pabs); ++ dfd = ona_open(".", directory_traverse_flags(), 0, pabs, sizeof pabs); + else { + dlen = slash == path ? 1 : (size_t)(slash - path); /* "/x" -> parent "/" */ + if (dlen >= sizeof dir) { +@@ -651,7 +669,7 @@ int owner_walk_parent(const char *path, const char **bname) + } + memcpy(dir, path, dlen); + dir[dlen] = '\0'; +- dfd = ona_open(dir, O_RDONLY | O_DIRECTORY, 0, pabs, sizeof pabs); ++ dfd = ona_open(dir, directory_traverse_flags(), 0, pabs, sizeof pabs); + } + if (dfd < 0) + return -1; +@@ -780,7 +798,7 @@ int do_unlink_at(const char *path) + dirpath[dlen] = '\0'; + bname = slash + 1; + +- dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ dfd = secure_relative_dirfd(NULL, dirpath); + if (dfd < 0) + return -1; + +@@ -887,7 +905,7 @@ int do_symlink_at(const char *lnk, const char *path) + memcpy(dirpath, path, dlen); + dirpath[dlen] = '\0'; + bname = slash + 1; +- dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ dfd = secure_relative_dirfd(NULL, dirpath); + if (dfd < 0) + return -1; + owns = True; +@@ -1089,7 +1107,7 @@ int do_link_at(const char *old_path, const char *new_path) + 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); ++ old_dfd = secure_relative_dirfd(NULL, old_dirpath); + if (old_dfd < 0) + return -1; + old_owns = True; +@@ -1128,7 +1146,7 @@ int do_link_at(const char *old_path, const char *new_path) + && 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); ++ new_dfd = secure_relative_dirfd(NULL, new_dirpath); + if (new_dfd < 0) { + e = errno; + if (old_owns) close(old_dfd); +@@ -1231,7 +1249,7 @@ int do_lchown_at(const char *fname, uid_t owner, gid_t group) + dirpath[dlen] = '\0'; + bname = slash + 1; + +- dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ dfd = secure_relative_dirfd(NULL, dirpath); + if (dfd < 0) + return -1; + +@@ -1402,7 +1420,7 @@ int do_mknod_at(const char *pathname, mode_t mode, dev_t dev) + memcpy(dirpath, pathname, dlen); + dirpath[dlen] = '\0'; + bname = slash + 1; +- dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ dfd = secure_relative_dirfd(NULL, dirpath); + if (dfd < 0) + return -1; + owns = True; +@@ -1524,7 +1542,7 @@ int do_rmdir_at(const char *pathname) + dirpath[dlen] = '\0'; + bname = slash + 1; + +- dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ dfd = secure_relative_dirfd(NULL, dirpath); + if (dfd < 0) + return -1; + +@@ -1620,7 +1638,7 @@ int do_open_at(const char *pathname, int flags, mode_t mode) + dirpath[dlen] = '\0'; + bname = slash + 1; + +- dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ dfd = secure_relative_dirfd(NULL, dirpath); + if (dfd < 0) + return -1; + +@@ -1902,7 +1920,7 @@ int do_chmod_at(const char *fname, mode_t mode) + dirpath[dlen] = '\0'; + bname = slash + 1; + +- dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ dfd = secure_relative_dirfd(NULL, dirpath); + if (dfd < 0) + return -1; + +@@ -2019,7 +2037,7 @@ int do_rename_at(const char *old_path, const char *new_path) + 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); ++ old_dfd = secure_relative_dirfd(NULL, old_dirpath); + if (old_dfd < 0) + return -1; + old_owns = True; +@@ -2058,7 +2076,7 @@ int do_rename_at(const char *old_path, const char *new_path) + && 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); ++ new_dfd = secure_relative_dirfd(NULL, new_dirpath); + if (new_dfd < 0) { + e = errno; + if (old_owns) close(old_dfd); +@@ -2189,7 +2207,7 @@ int do_mkdir_at(char *path, mode_t mode) + dirpath[dlen] = '\0'; + bname = slash + 1; + +- dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ dfd = secure_relative_dirfd(NULL, dirpath); + if (dfd < 0) + return -1; + +@@ -2315,7 +2333,7 @@ static int do_xstat_at(const char *path, STRUCT_STAT *st, int at_flags, int (*fa + dirpath[dlen] = '\0'; + bname = slash + 1; + +- dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ dfd = secure_relative_dirfd(NULL, dirpath); + if (dfd < 0) + return -1; + +@@ -2567,7 +2585,7 @@ int do_utimensat_at(const char *path, STRUCT_STAT *stp) + t[1].tv_nsec = 0; + #endif + +- dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ dfd = secure_relative_dirfd(NULL, dirpath); + if (dfd < 0) + return -1; + +@@ -2948,13 +2966,13 @@ static int ds_push(struct dirstack *ds, int 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. */ ++/* Detach the current traversal dirfd as an owned fd the caller must close. At ++ * the anchor (top 0) the anchor is borrowed, so open a fresh traversal fd. */ + 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); ++ return openat(ds->fds[0], ".", directory_traverse_flags()); + } + + static int ds_walk_path(struct dirstack *ds, char *path, int *hops); +@@ -2979,7 +2997,7 @@ static int ds_descend(struct dirstack *ds, const char *part, int *hops) + return 0; + } + +- int fd = openat(ds_cur(ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); ++ int fd = openat(ds_cur(ds), part, directory_traverse_flags() | O_NOFOLLOW); + if (fd != -1) { /* a real subdirectory */ + if (ds_push(ds, fd) < 0) + return -1; +@@ -3102,7 +3120,7 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath, + goto cleanup; + if (is_last) { + if (flags & O_DIRECTORY) +- retfd = ds_take(&ds); ++ retfd = openat(ds_cur(&ds), ".", flags | O_NOFOLLOW, mode); + else + errno = EISDIR; + goto cleanup; +@@ -3122,7 +3140,8 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath, + goto cleanup; + } + } +- int next_fd = openat(ds_cur(&ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); ++ int next_fd = openat(ds_cur(&ds), part, ++ directory_traverse_flags() | O_NOFOLLOW); + if (next_fd == -1 && (errno == ENOTDIR || errno == ENOENT)) { + retfd = openat(ds_cur(&ds), part, flags | O_NOFOLLOW, mode); + goto cleanup; +@@ -3136,7 +3155,7 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath, + + /* 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); ++ retfd = openat(ds_cur(&ds), part, flags | O_NOFOLLOW, mode); + goto cleanup; + } + +@@ -3148,17 +3167,17 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath, + goto cleanup; + } + if (is_last) { +- retfd = ds_take(&ds); ++ retfd = openat(ds_cur(&ds), ".", flags | O_NOFOLLOW, mode); + 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. */ ++ /* Empty relpath: reopen the anchor with the caller's requested directory ++ * access, 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); ++ retfd = openat(anchor_fd, ".", flags | O_NOFOLLOW, mode); + else + errno = EISDIR; + } +@@ -3311,6 +3330,14 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo + #endif // O_NOFOLLOW, O_DIRECTORY + } + ++/* Resolve a directory for traversal or as *at()/fchdir() authority. Callers ++ * that read directory entries or need a read-capable fd must continue to use ++ * secure_relative_open(..., O_RDONLY | O_DIRECTORY, ...). */ ++int secure_relative_dirfd(const char *basedir, const char *relpath) ++{ ++ return secure_relative_open(basedir, relpath, directory_traverse_flags(), 0); ++} ++ + /* 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 +@@ -3365,6 +3392,12 @@ int secure_relative_open_at_beneath(int anchor_fd, const char *relpath, + return secure_relative_open_at_internal(anchor_fd, relpath, flags, mode, 1); + } + ++int secure_relative_dirfd_at_beneath(int anchor_fd, const char *relpath) ++{ ++ return secure_relative_open_at_internal(anchor_fd, relpath, ++ directory_traverse_flags(), 0, 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 +@@ -3501,8 +3534,8 @@ int secure_mkstemp(char *template, mode_t perms, int operator_path) + dir = dirbuf; + } + dirfd = operator_path +- ? open_no_attacker_symlinks(dir, O_RDONLY | O_DIRECTORY, 0) +- : secure_relative_open(dir, ".", O_RDONLY | O_DIRECTORY, 0); ++ ? open_no_attacker_symlinks_dirfd(dir) ++ : secure_relative_dirfd(dir, "."); + if (dirfd < 0) + return -1; + } +@@ -3571,14 +3604,14 @@ int open_dir_secure(const char *dirname) + + if (!dirname || !*dirname) { + /* The transfer root itself (file->dirname == NULL): the cwd. */ +- dfd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY); ++ dfd = openat(AT_FDCWD, ".", directory_traverse_flags()); + } 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); ++ dfd = secure_relative_dirfd(NULL, dirname); + } + + if (dfd >= 0) { +diff --git a/testsuite/search-only-held-dirfd_test.py b/testsuite/search-only-held-dirfd_test.py +new file mode 100644 +index 0000000..e4da46f +--- /dev/null ++++ b/testsuite/search-only-held-dirfd_test.py +@@ -0,0 +1,215 @@ ++#!/usr/bin/env python3 ++"""Known-name operations must not require permission to list parent dirs. ++ ++The confined resolver holds directory descriptors to prevent symlink races. ++On Linux, those traversal and *at() anchor descriptors can use O_PATH: opening ++a known file beneath a searchable directory, or creating one beneath a ++writable/searchable directory, does not require directory read permission. ++""" ++ ++import os ++import shutil ++import subprocess ++import sys ++import tempfile ++from pathlib import Path ++ ++from rsyncfns import ( ++ SCRATCHDIR, forced_protocol, rmtree, rsync_argv, test_fail, test_skipped, ++) ++ ++if not sys.platform.startswith('linux'): ++ test_skipped('search-only held-dirfd coverage is Linux-specific') ++ ++launcher = [] ++if os.geteuid() == 0: ++ setpriv = shutil.which('setpriv') ++ if setpriv is None: ++ test_skipped('setpriv is unavailable for the root-run testsuite') ++ launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups'] ++ ++external_base = os.geteuid() == 0 ++base = ( ++ Path(tempfile.mkdtemp(prefix='rsync-search-only-held-dirfd-')) ++ if external_base ++ else SCRATCHDIR / 'search-only-held-dirfd' ++) ++rmtree(base) ++ ++src = base / 'src' ++xonly = src / 'xonly' ++readable = xonly / 'readable' ++nested_src = src / 'nested' ++exact_dest = base / 'exact-dest' ++tree_dest = base / 'tree-dest' ++unreadable_dest = base / 'unreadable-dest' ++write_only_dest = base / 'write-only-dest' ++nested_dest = base / 'nested-dest' ++nested_parent = nested_dest / 'nested' ++for path in ( ++ readable, ++ nested_src, ++ exact_dest, ++ tree_dest, ++ unreadable_dest, ++ write_only_dest, ++ nested_parent, ++): ++ path.mkdir(parents=True, exist_ok=True) ++ ++(xonly / 'exact').write_text('known file beneath search-only parent\n') ++(readable / 'nested').write_text('enumerated below search-only ancestor\n') ++incoming = src / 'incoming' ++incoming.write_text('created beneath write-search-only destination\n') ++(nested_src / 'known').write_text( ++ 'created beneath nested write-search-only parent\n' ++) ++ ++if os.geteuid() == 0: ++ for root, dirs, files in os.walk(base): ++ os.chown(root, 65534, 65534) ++ for name in dirs + files: ++ os.chown(Path(root) / name, 65534, 65534) ++ ++ ++def permission_probe(path, flag, expected, label): ++ proc = subprocess.run( ++ launcher + ['test', flag, str(path)], ++ stdout=subprocess.DEVNULL, ++ stderr=subprocess.DEVNULL, ++ ) ++ if proc.returncode != expected: ++ test_skipped( ++ f'filesystem does not enforce {label}: test {flag} returned ' ++ f'{proc.returncode}, expected {expected}' ++ ) ++ ++ ++failures = [] ++try: ++ xonly.chmod(0o111) ++ write_only_dest.chmod(0o333) ++ nested_parent.chmod(0o333) ++ ++ permission_probe(xonly, '-r', 1, 'search-only mode') ++ permission_probe(xonly, '-x', 0, 'search-only mode') ++ permission_probe(write_only_dest, '-r', 1, 'write-search-only mode') ++ permission_probe(write_only_dest, '-w', 0, 'write-search-only mode') ++ permission_probe(write_only_dest, '-x', 0, 'write-search-only mode') ++ permission_probe(nested_parent, '-r', 1, 'nested write-search-only mode') ++ permission_probe(nested_parent, '-w', 0, 'nested write-search-only mode') ++ permission_probe(nested_parent, '-x', 0, 'nested write-search-only mode') ++ ++ # Keep received implied dirs usable on systems without a safe fchmodat2. ++ # The source remains mode 0111, so sender traversal coverage is unchanged. ++ exact = subprocess.run( ++ launcher + rsync_argv( ++ '-aR', '--chmod=Du+rw', 'xonly/exact', f'{exact_dest}/', ++ ), ++ cwd=src, ++ stdout=subprocess.PIPE, ++ stderr=subprocess.PIPE, ++ text=True, ++ ) ++ exact_path = exact_dest / 'xonly' / 'exact' ++ exact_content = exact_path.read_text() if exact_path.is_file() else None ++ if exact.returncode != 0 or exact_content != ( ++ 'known file beneath search-only parent\n' ++ ): ++ failures.append( ++ 'exact -R source beneath mode 0111 failed: ' ++ f'rc={exact.returncode}, stderr={exact.stderr.strip()!r}, ' ++ f'content={exact_content!r}' ++ ) ++ ++ tree = subprocess.run( ++ launcher + rsync_argv( ++ '-aR', '--chmod=Du+rw', 'xonly/readable/', f'{tree_dest}/', ++ ), ++ cwd=src, ++ stdout=subprocess.PIPE, ++ stderr=subprocess.PIPE, ++ text=True, ++ ) ++ tree_path = tree_dest / 'xonly' / 'readable' / 'nested' ++ tree_content = tree_path.read_text() if tree_path.is_file() else None ++ if tree.returncode != 0 or tree_content != ( ++ 'enumerated below search-only ancestor\n' ++ ): ++ failures.append( ++ 'readable directory beneath mode 0111 ancestor failed: ' ++ f'rc={tree.returncode}, stderr={tree.stderr.strip()!r}, ' ++ f'content={tree_content!r}' ++ ) ++ ++ unreadable = subprocess.run( ++ launcher + rsync_argv( ++ '-a', 'xonly/', f'{unreadable_dest}/', ++ ), ++ cwd=src, ++ stdout=subprocess.PIPE, ++ stderr=subprocess.PIPE, ++ text=True, ++ ) ++ if unreadable.returncode == 0: ++ failures.append( ++ 'mode 0111 source directory was enumerable without read permission' ++ ) ++ ++ receiver = subprocess.run( ++ launcher + rsync_argv( ++ '-t', str(incoming), f'{write_only_dest}/', ++ ), ++ stdout=subprocess.PIPE, ++ stderr=subprocess.PIPE, ++ text=True, ++ ) ++ received = write_only_dest / 'incoming' ++ received_content = received.read_text() if received.is_file() else None ++ if receiver.returncode != 0 or received_content != ( ++ 'created beneath write-search-only destination\n' ++ ): ++ failures.append( ++ 'known-file creation beneath mode 0333 destination failed: ' ++ f'rc={receiver.returncode}, stderr={receiver.stderr.strip()!r}, ' ++ f'content={received_content!r}' ++ ) ++ ++ # Protocol 29 rejects this nested -R shape before the resolver is reached. ++ proto = forced_protocol() ++ if proto is None or proto >= 30: ++ nested_receiver = subprocess.run( ++ launcher + rsync_argv( ++ '-tR', '--no-implied-dirs', 'nested/known', f'{nested_dest}/', ++ ), ++ cwd=src, ++ stdout=subprocess.PIPE, ++ stderr=subprocess.PIPE, ++ text=True, ++ ) ++ nested_received = nested_parent / 'known' ++ nested_content = ( ++ nested_received.read_text() if nested_received.is_file() else None ++ ) ++ if nested_receiver.returncode != 0 or nested_content != ( ++ 'created beneath nested write-search-only parent\n' ++ ): ++ failures.append( ++ 'known-file creation beneath nested mode 0333 destination ' ++ f'failed: rc={nested_receiver.returncode}, ' ++ f'stderr={nested_receiver.stderr.strip()!r}, ' ++ f'content={nested_content!r}' ++ ) ++finally: ++ xonly.chmod(0o755) ++ write_only_dest.chmod(0o755) ++ nested_parent.chmod(0o755) ++ for dest in (exact_dest, tree_dest): ++ copied_xonly = dest / 'xonly' ++ if copied_xonly.is_dir(): ++ copied_xonly.chmod(0o755) ++ if external_base: ++ rmtree(base) ++ ++if failures: ++ test_fail('\n'.join(failures)) +diff --git a/util1.c b/util1.c +index 87ff225..955d204 100644 +--- a/util1.c ++++ b/util1.c +@@ -1252,7 +1252,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 = open_no_attacker_symlinks_dirfd(dir); + if (dfd < 0) + return 0; + if (fchdir(dfd) != 0) { +@@ -1283,7 +1283,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 = open_no_attacker_symlinks_dirfd(nf); + if (dfd < 0) + return 0; + if (fchdir(dfd) != 0) { +@@ -1344,8 +1344,7 @@ int change_dir(const char *dir, int set_path_only) + prefix[save_dir_len] = '\0'; + basedir = prefix; + } +- dfd = secure_relative_open(basedir, dir, +- O_RDONLY | O_DIRECTORY, 0); ++ dfd = secure_relative_dirfd(basedir, dir); + if (dfd < 0) { + chdir_failed = 1; + } else { +@@ -1363,8 +1362,7 @@ int change_dir(const char *dir, int set_path_only) + * 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 = open_no_attacker_symlinks_dirfd(curr_dir); + if (dfd < 0) + chdir_failed = 1; + else { diff -Nru rsync-3.4.1+ds1/debian/patches/testsuite_bound_the_unshare_probe.patch rsync-3.5.0+ds1/debian/patches/testsuite_bound_the_unshare_probe.patch --- rsync-3.4.1+ds1/debian/patches/testsuite_bound_the_unshare_probe.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/testsuite_bound_the_unshare_probe.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,36 @@ +From 3d2caaa0cb1b316f60b57a4036b42cd687eea561 Mon Sep 17 00:00:00 2001 +From: Zen Dodd +Date: Sun, 16 Aug 2026 21:55:00 +1000 +Subject: [PATCH] testsuite: bound the unshare probe (#1049) + +--- + testsuite/protected-regular_test.py | 16 +++++++++++----- + 1 file changed, 11 insertions(+), 5 deletions(-) + +diff --git a/testsuite/protected-regular_test.py b/testsuite/protected-regular_test.py +index b7d982582..4094534b2 100644 +--- a/testsuite/protected-regular_test.py ++++ b/testsuite/protected-regular_test.py +@@ -49,11 +49,17 @@ def _chown_5001(path: Path) -> bool: + if not os.environ.get('RSYNC_UNSHARED'): + unshare = shutil.which('unshare') + if unshare is not None: +- probe = subprocess.run( +- [unshare, '--user', '--map-root-user', +- '--map-users', '5001:100000:1', 'true'], +- capture_output=True, +- ) ++ try: ++ probe = subprocess.run( ++ [unshare, '--user', '--map-root-user', ++ '--map-users', '5001:100000:1', 'true'], ++ stdin=subprocess.DEVNULL, ++ stdout=subprocess.DEVNULL, ++ stderr=subprocess.DEVNULL, ++ timeout=5, ++ ) ++ except subprocess.TimeoutExpired: ++ test_skipped("Can't chown (unshare probe timed out)") + if probe.returncode == 0: + print("Re-running under unshare with UID mapping...") + env = os.environ.copy() diff -Nru rsync-3.4.1+ds1/debian/patches/testsuite_interpose_lfs_open_symbols.patch rsync-3.5.0+ds1/debian/patches/testsuite_interpose_lfs_open_symbols.patch --- rsync-3.4.1+ds1/debian/patches/testsuite_interpose_lfs_open_symbols.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/testsuite_interpose_lfs_open_symbols.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,207 @@ +From: Samuel Henrique +Date: Mon, 17 Aug 2026 22:00:00 -0700 +Subject: testsuite: interpose the large-file spellings of open/openat/fstatat + +Which symbol names this test's LD_PRELOAD hook exports is decided by the +compiler that builds it, and which names rsync imports is decided by +configure -- and the two do not have to agree. + +Where off_t is not already 64 bits, configure's AC_SYS_LARGEFILE adds +-D_FILE_OFFSET_BITS=64 (i386 and alpha in Debian), so glibc redirects every +open()/openat()/fstatat() call in rsync to open64(), openat64() and +fstatat64(). The hook is compiled by a bare "cc", so on those architectures +it defines only the unsuffixed names: the receiver's opens never reach it, no +EACCES is injected, the marker the positive control looks for is never +written, and the test fails with + + positive control failed: receiver did not open the existing partial file + with O_CREAT (rc=0, output='') + +It happens to work on Debian's 64-bit time_t ports (armhf, hppa, powerpc, +...) only by luck: their gcc predefines -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64, +so glibc's __REDIRECT renames the hook's own DEFINITIONS as well and it ends +up exporting exactly the *64 names rsync imports. + +Define both spellings explicitly so the hook interposes whichever set the +rsync under test was linked against, and #undef the two macros at the top of +the hook so that renaming cannot happen -- otherwise, on precisely those ports +where the compiler predefines them, open() would be emitted as open64() and +collide with the explicit wrapper ("symbol `open64' is already defined"), +which would leave the hook unbuildable and the test skipped. + +The new pointers are resolved through hook_resolve(), so the existing +nested-dlsym recursion guard covers them as well. + +Forwarded: https://github.com/RsyncProject/rsync/pull/1063 +--- +--- a/testsuite/partial-protected-regular-retry-linux_test.py ++++ b/testsuite/partial-protected-regular-retry-linux_test.py +@@ -37,6 +37,17 @@ if _proto is not None and _proto < 30: + + hook_code = r''' + #define _GNU_SOURCE ++/* Build the hook itself WITHOUT large-file redirection, whatever the compiler ++ * defaults to. Debian's armhf/hppa/powerpc gcc predefines ++ * -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64 (check with "gcc -v -E -"), and under ++ * those macros glibc's __REDIRECT renames the DEFINITIONS below -- open() ++ * becomes open64(), fstatat() becomes __fstatat64_time64() -- which then ++ * collide with the explicit large-file wrappers further down ("symbol `open64' ++ * is already defined"). Undefining them here keeps each name declared exactly ++ * once, so the hook always exports both spellings and interposes whichever set ++ * the rsync under test was linked against. */ ++#undef _FILE_OFFSET_BITS ++#undef _TIME_BITS + #include + #include + #include +@@ -56,6 +67,9 @@ static int (*real_open)(const char *, in + static int (*real_openat)(int, const char *, int, ...); + static int (*real_fstatat)(int, const char *, struct stat *, int); + static int (*real_fxstatat)(int, int, const char *, struct stat *, int); ++static int (*real_fstatat64)(int, const char *, struct stat64 *, int); ++static int (*real_fxstatat64)(int, int, const char *, struct stat64 *, int); ++static int (*real_fstatat64_time64)(int, const char *, struct stat64 *, int); + + /* Resolve on demand rather than trusting our constructor to have run. A + * preloaded open() interposes for the whole process the moment the loader maps +@@ -77,6 +91,10 @@ static void hook_resolve(void) + if (!real_openat) real_openat = dlsym(RTLD_NEXT, "openat"); + if (!real_fstatat) real_fstatat = dlsym(RTLD_NEXT, "fstatat"); + if (!real_fxstatat) real_fxstatat = dlsym(RTLD_NEXT, "__fxstatat"); ++ if (!real_fstatat64) real_fstatat64 = dlsym(RTLD_NEXT, "fstatat64"); ++ if (!real_fxstatat64) real_fxstatat64 = dlsym(RTLD_NEXT, "__fxstatat64"); ++ if (!real_fstatat64_time64) ++ real_fstatat64_time64 = dlsym(RTLD_NEXT, "__fstatat64_time64"); + hook_resolving = 0; + } + +@@ -151,6 +169,10 @@ static int swap_and_deny(void) + # define HOOK_TAKES_MODE(f) ((f) & O_CREAT) + #endif + ++#ifndef O_LARGEFILE ++# define O_LARGEFILE 0 ++#endif ++ + static int is_victim_write(const char *path, int flags) + { + return path && strcmp(path, "victim") == 0 +@@ -231,6 +253,49 @@ int open(const char *path, int flags, .. + return fd; + } + ++/* --- large-file spellings ------------------------------------------------- ++ * Which names the RECEIVER calls is settled by its own build: where off_t is ++ * not already 64 bits, configure's AC_SYS_LARGEFILE adds -D_FILE_OFFSET_BITS=64 ++ * (i386, alpha, ...), and distro CPPFLAGS add it -- along with -D_TIME_BITS=64 ++ * -- on the 64-bit time_t ports, so glibc redirects each open()/openat()/ ++ * fstatat() call to open64()/openat64()/fstatat64()/__fstatat64_time64(). ++ * "objdump -T rsync | grep UND" says which set a given build imports. ++ * ++ * Which names THIS HOOK exports is a different question with a different ++ * answer, settled by whatever the "cc" below it defaults to -- see the #undef ++ * at the top. Nothing keeps the two in step, so define every spelling and let ++ * the loader match them up. With only the unsuffixed ones the receiver's opens ++ * sail straight past the hook, no EACCES is ever injected, and the test reports ++ * "positive control failed" having exercised nothing at all. ++ * ++ * O_LARGEFILE is the only thing open64() adds over open(), so the wrappers ++ * below can hand the call to the unsuffixed interposer above. */ ++int open64(const char *path, int flags, ...) ++{ ++ mode_t mode = 0; ++ ++ if (HOOK_TAKES_MODE(flags)) { ++ va_list ap; ++ va_start(ap, flags); ++ mode = (mode_t)va_arg(ap, int); ++ va_end(ap); ++ } ++ return open(path, flags | O_LARGEFILE, mode); ++} ++ ++int openat64(int dfd, const char *path, int flags, ...) ++{ ++ mode_t mode = 0; ++ ++ if (HOOK_TAKES_MODE(flags)) { ++ va_list ap; ++ va_start(ap, flags); ++ mode = (mode_t)va_arg(ap, int); ++ va_end(ap); ++ } ++ return openat(dfd, path, flags | O_LARGEFILE, mode); ++} ++ + /* ona_open() decides via fstatat(..., AT_SYMLINK_NOFOLLOW) and refuses a + * component owned by neither root nor the euid. Model the attacker as a + * different uid so a retry that kept the ownership walk refuses the swap. */ +@@ -275,6 +340,68 @@ int __fxstatat(int ver, int dfd, const c + errno = saved_errno; + return rc; + } ++ ++/* The stat family's large-file spellings. st_mode and st_uid sit ahead of the ++ * timestamps in every glibc struct stat layout, so the time32/time64 variants ++ * of the buffer are interchangeable for the two fields touched here. */ ++static void model_foreign_owner64(int rc, const char *path, struct stat64 *st) ++{ ++ if (rc == 0 && swapped && path && strcmp(path, "pdir") == 0 ++ && S_ISLNK(st->st_mode)) { ++ st->st_uid = geteuid() + 1; ++ mark(getenv("RSYNC_PARTIAL_RETRY_FOREIGN_MARKER")); ++ } ++} ++ ++int fstatat64(int dfd, const char *path, struct stat64 *st, int flags) ++{ ++ int rc, saved_errno; ++ ++ hook_resolve(); ++ if (!real_fstatat64) { ++ errno = ENOSYS; ++ return -1; ++ } ++ rc = real_fstatat64(dfd, path, st, flags); ++ saved_errno = errno; ++ model_foreign_owner64(rc, path, st); ++ errno = saved_errno; ++ return rc; ++} ++ ++int __fxstatat64(int ver, int dfd, const char *path, struct stat64 *st, int flags) ++{ ++ int rc, saved_errno; ++ ++ hook_resolve(); ++ if (!real_fxstatat64) { ++ errno = ENOSYS; ++ return -1; ++ } ++ rc = real_fxstatat64(ver, dfd, path, st, flags); ++ saved_errno = errno; ++ model_foreign_owner64(rc, path, st); ++ errno = saved_errno; ++ return rc; ++} ++ ++/* A 32-bit port built with -D_TIME_BITS=64 (Debian's armhf/armel/hppa/powerpc, ++ * ...) reaches fstatat() under this third name. */ ++int __fstatat64_time64(int dfd, const char *path, struct stat64 *st, int flags) ++{ ++ int rc, saved_errno; ++ ++ hook_resolve(); ++ if (!real_fstatat64_time64) { ++ errno = ENOSYS; ++ return -1; ++ } ++ rc = real_fstatat64_time64(dfd, path, st, flags); ++ saved_errno = errno; ++ model_foreign_owner64(rc, path, st); ++ errno = saved_errno; ++ return rc; ++} + + __attribute__((constructor)) static void hook_loaded(void) + { diff -Nru rsync-3.4.1+ds1/debian/patches/testsuite_make_basis_xname_oracle_deterministic.patch rsync-3.5.0+ds1/debian/patches/testsuite_make_basis_xname_oracle_deterministic.patch --- rsync-3.4.1+ds1/debian/patches/testsuite_make_basis_xname_oracle_deterministic.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/testsuite_make_basis_xname_oracle_deterministic.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,269 @@ +From 195b4c6d305f7ad9b8b4b9ff1a62d525d907e241 Mon Sep 17 00:00:00 2001 +From: Zen Dodd +Date: Sat, 15 Aug 2026 16:19:15 +1000 +Subject: [PATCH] testsuite: make basis xname oracle deterministic (#1051) + +--- + testsuite/basis-xname-traversal_test.py | 198 ++++++++---------------- + 1 file changed, 61 insertions(+), 137 deletions(-) + +diff --git a/testsuite/basis-xname-traversal_test.py b/testsuite/basis-xname-traversal_test.py +index de004c0e..1ab28c4b 100644 +--- a/testsuite/basis-xname-traversal_test.py ++++ b/testsuite/basis-xname-traversal_test.py +@@ -16,30 +16,16 @@ + # *symlink* components. The fix sanitizes the wire xname itself (for basis + # types only, leaving the hard-link "=> target" xname alone). + # +-# Test: build an instrumented daemon-sender (env-gated sender.c edit that, when ++# Test: build an instrumented rsync (env-gated sender.c edit that, when + # RSYNC_MAL_XNAME is set, injects ITEM_XNAME_FOLLOWS|ITEM_BASIS_TYPE_FOLLOWS + +-# fnamecmp_type=FNAMECMP_FUZZY+1 (== basis_dir[0]) + xname onto each transfer), +-# run it via RSYNC_CONNECT_PROG with the production rsync as the receiver pulling +-# with --link-dest, and observe where the receiver opens the basis. +-# +-# Two FIFOs, each with a helper blocked in open(O_WRONLY) that drops a flag when +-# some reader opens it, tell RED from GREEN without hanging the receiver (it +-# reads EOF and finishes): +-# * ESCAPE base/secret reached only by an unsanitized "../secret" +-# * DECOY linkdest/secret where the SANITIZED "secret" lands +-# Injected xname is "../secret": +-# - vulnerable receiver opens ESCAPE -> escape flag -> FAIL (traversal) +-# - fixed receiver sanitizes to "secret", opens DECOY -> decoy flag -> PASS +-# (the decoy flag also proves the crafted xname actually crossed the wire, +-# so a stale/failed injection build can't false-PASS as "confined") +-# - neither flag -> the injection never took effect -> FAIL (vacuous) ++# fnamecmp_type=FNAMECMP_FUZZY+1 (== basis_dir[0]) + xname onto each transfer). ++# An env-gated receiver.c edit records the exact basedir and relpath passed to ++# secure_basis_open(). This observes the security decision directly without ++# relying on timing-sensitive FIFO rendezvous behaviour across operating systems. + + import os + import shlex + import subprocess +-import time +-from pathlib import Path +-import sys + + from rsyncfns import ( + SCRATCHDIR, build_patched_rsync, forced_protocol, makepath, rmtree, +@@ -53,11 +39,7 @@ from rsyncfns import ( + _proto = forced_protocol() + if _proto is not None and _proto < 29: + test_skipped("basis-xname-traversal: xname/item flags need protocol >= 29") +-if not hasattr(os, 'mkfifo'): +- test_skipped("basis-xname-traversal: os.mkfifo unavailable on this platform") +- +- +-# -- Build the instrumented sender (shared helper: Cygwin skip, CCACHE_DISABLE, ++# -- Build the instrumented peer (shared helper: Cygwin skip, CCACHE_DISABLE, + # forced rebuild of the patched unit) ------------------------------------- + PATCH_OLD = ("\t\twrite_ndx_and_attrs(f_out, ndx, iflags, fname, file, fnamecmp_type, xname, xlen);\n" + "\t\twrite_sum_head(f_xfer, s);") +@@ -68,14 +50,32 @@ PATCH_NEW = ("\t\tif (getenv(\"RSYNC_MAL_XNAME\")) { /* basis-xname-traversal Po + "\t\t}\n" + "\t\twrite_ndx_and_attrs(f_out, ndx, iflags, fname, file, fnamecmp_type, xname, xlen);\n" + "\t\twrite_sum_head(f_xfer, s);") +-mal_rsync = build_patched_rsync('mal-xname-rsync', [('sender.c', PATCH_OLD, PATCH_NEW)]) ++TRACE_OLD = ("static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)\n" ++ "{\n" ++ "\textern int am_daemon, am_chrooted;") ++TRACE_NEW = ("static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)\n" ++ "{\n" ++ "\tconst char *trace_path = getenv(\"RSYNC_BASIS_TRACE\");\n" ++ "\tif (trace_path) {\n" ++ "\t\tFILE *trace = fopen(trace_path, \"a\");\n" ++ "\t\tif (trace) {\n" ++ "\t\t\tfprintf(trace, \"%s\\t%s\\n\", basedir ? basedir : \"\", relpath);\n" ++ "\t\t\tfclose(trace);\n" ++ "\t\t}\n" ++ "\t}\n" ++ "\textern int am_daemon, am_chrooted;") ++mal_rsync = build_patched_rsync( ++ 'mal-xname-rsync', ++ [('sender.c', PATCH_OLD, PATCH_NEW), ++ ('receiver.c', TRACE_OLD, TRACE_NEW)], ++) + + + # -- Workspace ---------------------------------------------------------------- + # base/serversrc/file the file the instrumented daemon offers + # base/linkdest/ the client's --link-dest (basis_dir[0]) +-# base/linkdest/secret DECOY fifo -- where a sanitized "secret" resolves +-# base/secret ESCAPE fifo -- where an unsanitized "../secret" lands ++# base/linkdest/secret where a sanitized "secret" resolves ++# base/secret where an unsanitized "../secret" resolves + # base/dest/ the client's destination + base = SCRATCHDIR / 'xname-race' + rmtree(base) +@@ -84,133 +84,57 @@ linkdest = base / 'linkdest' + dest = base / 'dest' + escape = base / 'secret' # linkdest/../secret + decoy = linkdest / 'secret' # linkdest/secret +-esc_flag = base / 'escape.flag' +-dec_flag = base / 'decoy.flag' ++trace_file = base / 'basis.trace' + makepath(serversrc) + makepath(linkdest) + makepath(dest) + (serversrc / 'file').write_text("from the server\n") ++escape.write_text("escaped basis\n") ++decoy.write_text("confined basis\n") + +- +-# A helper that blocks in open(fifo, O_WRONLY) until some reader opens the FIFO, +-# then records the flag. Terminated below if no reader ever appears. +-WRITER = ("import os,sys\n" +- "open(sys.argv[3],'w').close()\n" # ready: about to block in open() +- "fd=os.open(sys.argv[1],os.O_WRONLY)\n" +- "open(sys.argv[2],'w').close()\n" +- "os.close(fd)\n") +- +- +-def spawn(fifo, flag): +- ready = Path(str(flag) + '.ready') +- if ready.exists(): +- ready.unlink() +- proc = subprocess.Popen( +- [sys.executable, '-c', WRITER, str(fifo), str(flag), str(ready)]) +- # Wait until the helper is actually at its blocking open(). Starting the +- # transfer before that lets the receiver come and go while nothing is +- # watching the FIFO, and the run reports a vacuous result -- which is what +- # made this test flaky on the slower fleet VMs. +- deadline = time.time() + 30 +- while not ready.exists() and proc.poll() is None and time.time() < deadline: +- time.sleep(0.02) +- return proc +- +- +-def settle(w): +- """Give a rendezvoused helper a bounded chance to record its flag; a still- +- blocked one just times out. (Closes the terminate-before-flag race.)""" +- try: +- w.wait(timeout=15) +- except subprocess.TimeoutExpired: +- pass +- +- +-def reap(w): +- if w.poll() is None: +- w.terminate() +- try: +- w.wait(timeout=10) +- except subprocess.TimeoutExpired: +- w.kill() +- w.wait() +- +- +-def attempt(): +- """One injection run. Returns the receiver's CompletedProcess. +- +- Re-creates the FIFOs and flags each time so a retry starts clean. +- """ +- for f in (escape, decoy, esc_flag, dec_flag): +- if os.path.lexists(f): +- os.unlink(f) +- rmtree(dest) +- makepath(dest) +- os.mkfifo(escape) +- os.mkfifo(decoy) +- esc_w = spawn(escape, esc_flag) +- dec_w = spawn(decoy, dec_flag) +- proc = None +- try: +- conf = write_daemon_conf( +- [('m', {'path': str(serversrc), 'read only': 'yes', 'use chroot': 'no'})], +- name='mal-xname-rsyncd.conf') +- os.environ['RSYNC_CONNECT_PROG'] = f'{shlex.quote(str(mal_rsync))} --config={shlex.quote(str(conf))} --daemon' +- os.environ['RSYNC_MAL_XNAME'] = '../secret' # from basis_dir[0] == linkdest +- proc = subprocess.run( +- rsync_argv('-a', f'--link-dest={linkdest}', +- 'rsync://localhost/m/file', str(dest) + '/'), +- stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120) +- settle(esc_w) +- settle(dec_w) +- finally: +- os.environ.pop('RSYNC_MAL_XNAME', None) +- os.environ.pop('RSYNC_CONNECT_PROG', None) +- reap(esc_w) +- reap(dec_w) +- for f in (escape, decoy): +- if os.path.lexists(f): +- os.unlink(f) +- return proc +- +- +-# A run where NEITHER fifo was opened proves nothing: the injection did not +-# take effect, so there was no traversal attempt to confine. That is a setup +-# failure, not a security signal, and on the slower fleet VMs it happens often +-# enough to make the test unusable -- so retry it. An ESCAPE is never retried: +-# the loop stops the moment the escape flag appears. +-attempts = 0 +-for _try in range(6): +- attempts += 1 +- proc = attempt() +- if esc_flag.is_file() or dec_flag.is_file(): +- break ++conf = write_daemon_conf( ++ [('m', {'path': str(serversrc), 'read only': 'yes', 'use chroot': 'no'})], ++ name='mal-xname-rsyncd.conf') ++os.environ['RSYNC_CONNECT_PROG'] = f'{shlex.quote(str(mal_rsync))} --config={shlex.quote(str(conf))} --daemon' ++os.environ['RSYNC_MAL_XNAME'] = '../secret' # from basis_dir[0] == linkdest ++os.environ['RSYNC_BASIS_TRACE'] = str(trace_file) ++try: ++ argv = rsync_argv('-a', f'--link-dest={linkdest}', ++ 'rsync://localhost/m/file', str(dest) + '/') ++ argv[0] = str(mal_rsync) ++ proc = subprocess.run( ++ argv, ++ stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120) ++finally: ++ os.environ.pop('RSYNC_BASIS_TRACE', None) ++ os.environ.pop('RSYNC_MAL_XNAME', None) ++ os.environ.pop('RSYNC_CONNECT_PROG', None) + + + # -- Oracle ------------------------------------------------------------------- +-out_tail = '\n'.join((proc.stdout if proc else '').splitlines()[-20:]) ++out_tail = '\n'.join(proc.stdout.splitlines()[-20:]) ++trace = trace_file.read_text().splitlines() if trace_file.is_file() else [] ++escaped = f'{linkdest}\t../secret' ++confined = f'{linkdest}\tsecret' + +-if esc_flag.is_file(): ++if escaped in trace: + test_fail( + "malicious server traversed the client's filesystem via the alt-dest " +- f"xname: the receiver opened {escape} (one level above the --link-dest " ++ f"xname: the receiver attempted {escape} (one level above the --link-dest " + "dir) as the delta basis. A server-supplied xname of '../secret' was " + "not sanitized on the client (sanitize_paths==0 off-daemon). Fix: " + "sanitize a basis-type xname in read_ndx_and_attrs(). Receiver output " + f"tail:\n{out_tail}") + +-# The decoy flag proves the crafted xname reached the receiver AND was confined +-# to the basedir (sanitized "../secret" -> "secret" -> linkdest/secret). Its +-# absence means the injection never took effect (e.g. a stale patched build), +-# so a clear escape flag alone would be a vacuous pass. +-if not dec_flag.is_file(): ++# The trace proves the crafted xname reached the receiver and was confined to ++# the basedir (sanitized "../secret" -> "secret" -> linkdest/secret). Its ++# absence means the injection never took effect (e.g. a stale patched build). ++if confined not in trace: + test_fail( +- "the crafted xname never reached the receiver's basis open (neither the " +- "escape nor the decoy FIFO was opened) -- the instrumented-sender " +- f"injection did not take effect, so this run is vacuous after " +- f"{attempts} attempt(s). This is a harness failure, NOT a traversal: " +- "an escape is reported separately and is never retried. Receiver rc=" +- f"{proc.returncode if proc else 'n/a'}. Output tail:\n{out_tail}") ++ "the crafted xname never reached the receiver's confined basis open; " ++ "the instrumented injection did not take effect, so this run is " ++ f"vacuous. Trace={trace!r}. Receiver rc={proc.returncode}. " ++ f"Output tail:\n{out_tail}") + + if proc.returncode != 0: + test_fail( +-- +2.53.0 + diff -Nru rsync-3.4.1+ds1/debian/patches/testsuite_punch_granularity_guard.patch rsync-3.5.0+ds1/debian/patches/testsuite_punch_granularity_guard.patch --- rsync-3.4.1+ds1/debian/patches/testsuite_punch_granularity_guard.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/debian/patches/testsuite_punch_granularity_guard.patch 2026-09-16 01:46:30.000000000 +0000 @@ -0,0 +1,137 @@ +From: Samuel Henrique +Date: Mon, 17 Aug 2026 22:00:00 -0700 +Subject: testsuite: probe the punch granularity each hole assertion relies on + +The last section of preallocate_test.py writes 256 blocks of +[4 KiB data][24 KiB zeros][4 KiB data] and requires --inplace --sparse to +deallocate at least half of the file. A punch only frees whole allocation +units, and the 24 KiB interior run sits 4 KiB into each 32 KiB block: with a +4 KiB unit it covers six whole units (24 KiB freed per block), but with a +16 KiB unit it covers none at all, so st_blocks does not move. + +That is what happens on loong64, whose kernel uses 16 KiB pages +(arch/loongarch/Kconfig defaults to 16KB_3LEVEL on 64-bit and Debian's +config does not override it), and the build fails with + + --inplace --sparse left matching interior zero runs allocated: 8388608 of + 8388608 bytes remain allocated after a 8388608-byte matched-block update + +i.e. exactly "nothing was freed", which is the arithmetic of a punch +granularity >= 16 KiB rather than an rsync regression. fs_can_punch_holes() +does not catch it because it punches a whole 64 KiB file, which frees blocks +at any granularity. + +Generalise that helper into punch_frees(offset, length, size) so each +assertion can probe the shape it actually depends on, and gate the interior +one on the exact [4 KiB data][24 KiB zeros][4 KiB data] layout. Where the +filesystem can free that run the assertion runs exactly as before; where it +cannot, the test says so rather than reporting a regression. + +While here, call fallocate64() instead of fallocate(). ctypes declares the +offset and length as c_longlong, but where off_t is 32 bits glibc's +fallocate() takes 32-bit offsets -- so the callee reads the high half of +`offset` as its `length`, the probe fails with EINVAL, and can_punch comes +back False. That is why none of this file's hole-punching assertions have +ever run on a 32-bit port; on i386: + + fs_can_punch_holes() as written : before=128 ret=-1 errno=22 -> False + the same probe via fallocate64 : before=128 ret=0 -> True + +fallocate64() takes off64_t on every architecture and is a plain alias of +fallocate() where off_t is already 64 bits. The probe now writes urandom +rather than a repeated byte as well, so a filesystem that compresses does not +answer "nothing was freed" merely because nothing was allocated. + +Forwarded: https://github.com/RsyncProject/rsync/pull/1062 +--- +--- a/testsuite/preallocate_test.py ++++ b/testsuite/preallocate_test.py +@@ -38,12 +38,33 @@ if run_rsync('-a', '--preallocate', f'{s + check=False, capture_output=True).returncode != 0: + test_skipped("--preallocate not supported on this platform") + +-def fs_can_punch_holes(): +- """True only where the kernel can deallocate blocks via FALLOC_FL_PUNCH_HOLE +- -- the mechanism do_punch_hole uses for --sparse. A filesystem may report +- seek-based sparseness yet still keep every block on a punch (e.g. where +- rsync's punch falls back to writing zeros), so probe the real capability and +- assert the hole only where it actually frees blocks.""" ++def punch_frees(offset, length, size): ++ """True where punching [offset, offset+length) out of a `size`-byte file ++ really deallocates blocks -- the mechanism do_punch_hole uses for --sparse. ++ ++ Two separate things can leave st_blocks untouched, so each assertion below ++ probes the exact shape it relies on. A filesystem may report seek-based ++ sparseness yet still keep every block on a punch (e.g. where rsync's punch ++ falls back to writing zeros), which a whole-file probe catches. And a punch ++ only frees storage in whole allocation units, which are not always 4 KiB: a ++ tmpfs frees whole pages, 16 KiB on loongarch/loong64 (and 64 KiB on a ++ 64k-page ppc64el or arm64 kernel), and a filesystem may be formatted with a ++ block size above the page size. An interior run spanning no whole unit is ++ zeroed rather than deallocated, so st_blocks does not move and an assertion ++ phrased in st_blocks would report a hole-punching regression that is really ++ just the filesystem's granularity. ++ ++ fallocate64() rather than fallocate(): where off_t is 32 bits the latter ++ takes 32-bit offsets, so ctypes' 64-bit arguments do not line up with what ++ it reads (on i386 it takes the high half of `offset` as its `length`) and ++ every probe fails with EINVAL -- which is why every assertion below has ++ silently done nothing on all the 32-bit ports. fallocate64() takes off64_t ++ everywhere and is a plain alias of fallocate() where off_t is already 64 ++ bits wide. ++ ++ The probe data has to be incompressible: a filesystem that compresses ++ (btrfs with compress=) stores a run of one repeated byte in almost no ++ blocks, leaving a successful punch with nothing to free.""" + import ctypes + import ctypes.util + KEEP_SIZE, PUNCH_HOLE = 0x01, 0x02 +@@ -52,12 +73,12 @@ def fs_can_punch_holes(): + try: + libc = ctypes.CDLL(ctypes.util.find_library('c') or 'libc.so.6', + use_errno=True) +- libc.fallocate.argtypes = [ctypes.c_int, ctypes.c_int, +- ctypes.c_longlong, ctypes.c_longlong] ++ libc.fallocate64.argtypes = [ctypes.c_int, ctypes.c_int, ++ ctypes.c_longlong, ctypes.c_longlong] + fd = os.open(p, os.O_CREAT | os.O_RDWR | os.O_TRUNC, 0o644) +- os.write(fd, b'\xff' * 65536) ++ os.write(fd, os.urandom(size)) + before = os.fstat(fd).st_blocks +- ret = libc.fallocate(fd, PUNCH_HOLE | KEEP_SIZE, 0, 65536) ++ ret = libc.fallocate64(fd, PUNCH_HOLE | KEEP_SIZE, offset, length) + return ret == 0 and os.fstat(fd).st_blocks < before + except (OSError, AttributeError, ValueError): + return False +@@ -70,7 +91,7 @@ def fs_can_punch_holes(): + pass + + +-can_punch = fs_can_punch_holes() ++can_punch = punch_frees(0, 65536, 65536) + + + def seed_plain(size=1_000_000): +@@ -139,6 +160,13 @@ with open(src / deep, 'wb') as source, o + source.write(block) + dest.write(block) + ++# Only assert the interior punch where the filesystem can free a 24 KiB run ++# sitting 4 KiB into a 32 KiB block -- the exact shape written just above. ++can_punch_interior = can_punch and punch_frees(4096, 24576, 32768) ++if can_punch and not can_punch_interior: ++ print("preallocate: interior-hole assertion skipped: this filesystem's " ++ "allocation unit cannot free a 24 KiB run inside a 32 KiB block") ++ + matched_size = os.path.getsize(TODIR / deep) + matched_before = allocated(TODIR / deep) + run_rsync('-a', '--ignore-times', '--inplace', '--sparse', '--no-whole-file', +@@ -146,7 +174,7 @@ run_rsync('-a', '--ignore-times', '--inp + assert_same(TODIR / deep, src / deep, + label='--inplace --sparse matched-block content') + matched_after = allocated(TODIR / deep) +-if (can_punch and matched_before >= matched_size ++if (can_punch_interior and matched_before >= matched_size + and matched_after * 2 >= matched_before): + test_fail(f"--inplace --sparse left matching interior zero runs allocated: " + f"{matched_after} of {matched_before} bytes remain allocated " diff -Nru rsync-3.4.1+ds1/debian/rsync.NEWS rsync-3.5.0+ds1/debian/rsync.NEWS --- rsync-3.4.1+ds1/debian/rsync.NEWS 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/rsync.NEWS 2026-09-16 01:46:30.000000000 +0000 @@ -1,18 +1,60 @@ -rsync (3.2.3-5) unstable; urgency=medium +rsync (3.5.0+ds1-0+deb13u1) trixie-security; urgency=medium - The --copy-devices option has been reintroduced, it was previously removed in - favor of the new one --write-devices, but it turns out they are not equivalent - enough and upstream is providing the copy-devices patch on rsync-patches. + In order to fix 33 CVEs, I have decided to bump the package to 3.5.0 rather + than backporting all patches individually. After analysing the extra changes + from the bump, not included in the CVE fixes, I have concluded this approach + carries the lower amount of risk compared to the alternative. + + This update contains behavior changes, all of which stems from the CVE fixes + themselves, not exclusive to the version bump. The ones most likely to break + an existing setup are listed here; /usr/share/doc/rsync/NEWS.md.gz has the + full list. + + Operator-supplied paths are no longer followed through untrusted symlinks. + The destination directory and the arguments to --backup-dir, --temp-dir, + --partial-dir, --link-dest, --compare-dest, --copy-dest, --log-file, + --password-file, --files-from, --include-from, --exclude-from, + --write-batch, --read-batch and --filter merge files are now resolved one + component at a time, following a symlink only when it is owned by root or + by the user running rsync; one owned by anyone else is refused with + "refusing to follow a symlink owned by an untrusted user". + --insecure-links restores the old behaviour, but it is local only and a + daemon never honours it. For a single trusted module, set + "insecure links = yes" in that module instead. + + rrsync now refuses --debug on every invocation. When restricted to a + subdirectory it additionally denies --copy-unsafe-links, passes the new + --confine-root so the server will not resolve a client-named filter merge + file outside that directory, and passes --drop-D when receiving, so an + upload can no longer create devices or special files there ("skipping + non-regular file"). A plain "rsync -a" otherwise still works. + + --chmod=a+s now sets both the setuid and setgid bits, matching chmod(1); + it previously set setuid alone. + + rsyncd changes that can change who gets in: + * "proxy protocol = true" without "proxy protocol hosts" now rejects + every connection and warns at startup, instead of trusting a + client-supplied PROXY header. + * "hosts deny" now fails closed when a configured hostname cannot be + resolved (with "forward lookup", the default), so a host previously + admitted by an unresolvable deny entry is now blocked. + * "auth users" values that start with a comma now split on commas alone, + as documented, so a deny or :ro rule naming a group whose name contains + a space now takes effect where it was silently ignored. + * "hosts allow" / "hosts deny" patterns now fold case inside a [...] + bracket expression as well, so a rule such as [A-Z]* matches hosts it + used to miss. + * A client-requested --compress-threads is capped at 8. + + rsync-ssl now verifies the server certificate. The default openssl + backend additionally binds it to the requested hostname, so a certificate + valid for some other name is now rejected. The stunnel and gnutls + backends refuse to run unless RSYNC_SSL_CA_CERT is set, or + RSYNC_SSL_ALLOW_INSECURE_STUNNEL=1 / RSYNC_SSL_ALLOW_INSECURE_GNUTLS=1 is + set to opt out. - Please beware that although the --copy-devices option is provided by - upstream, it is not part of the official rsync package and it could be - dropped or changed in ways that are not backwards compatible, though this would - only happen between Debian releases. - - That being said, we will not drop this option from the Debian packaging as - long as upstream keeps providing the patch under rsync-patches. - - -- Samuel Henrique Sun, 12 Sep 2021 17:25:37 +0100 + -- Samuel Henrique Tue, 15 Sep 2026 18:46:30 -0700 rsync (3.2.0-1) unstable; urgency=low diff -Nru rsync-3.4.1+ds1/debian/rules-pre-dh rsync-3.5.0+ds1/debian/rules-pre-dh --- rsync-3.4.1+ds1/debian/rules-pre-dh 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/rules-pre-dh 1970-01-01 00:00:00.000000000 +0000 @@ -1,131 +0,0 @@ -#!/usr/bin/make -f -# debian.rules file for rsync -# Copyright 1996 by Philip Hands. -# Copyright 2001 Colin Walters -# Based on the sample debian.rules file - for GNU Hello (1.3). -# Copyright 1994,1995 by Ian Jackson. -# I hereby give you perpetual unlimited permission to copy, -# modify and relicense this file, provided that you do not remove -# my name from the file itself. (I assert my moral right of -# paternity under the Copyright, Designs and Patents Act 1988.) - - -SHELL = /bin/bash -BINS = rsync -INSTALL = install -INSTALL_FILE = $(INSTALL) -p -o root -g root -m 644 -INSTALL_PROGRAM = $(INSTALL) -p -o root -g root -m 755 -INSTALL_SCRIPT = $(INSTALL) -p -o root -g root -m 755 -INSTALL_DIR = $(INSTALL) -p -d -o root -g root -m 755 - -export DEB_BUILD_MAINT_OPTIONS = hardening=+all - -dpkg_buildflags = DEB_BUILD_MAINT_OPTIONS=$(DEB_BUILD_MAINT_OPTIONS) dpkg-buildflags -CPPFLAGS := -Izlib $(shell $(dpkg_buildflags) --get CPPFLAGS) -CFLAGS := -Wall $(shell $(dpkg_buildflags) --get CFLAGS) -LDFLAGS := $(shell $(dpkg_buildflags) --get LDFLAGS) - -ifeq ($(DEB_BUILD_GNU_TYPE),$(DEB_HOST_GNU_TYPE)) -INSTALL_CROSS := -else -INSTALL_CROSS := INSTALLCMD='$(INSTALL) --strip-program=$(DEB_HOST_GNU_TYPE)-strip' -endif - -# keep lintian happy: -build: build-arch build-indep -build-arch: build-stamp -build-indep: build-stamp - -build-stamp: - @echo building build tree - -rm -rf debian/buildtree - mkdir debian/buildtree - cp -p * debian/buildtree || true - cp -pr lib m4 popt support testsuite zlib packaging debian/buildtree - # update config.guess/sub - cp /usr/share/misc/config.guess /usr/share/misc/config.sub debian/buildtree - # work around newer autoconf stuff (runstatedir) - touch debian/buildtree/aclocal.m4 - @echo applying misc Debian patches - for i in debian/patches/*.patch debian/patches/*.diff; do if [ -s $$i ]; then echo " $$i ..."; cat $$i | (cd debian/buildtree; patch -p1) || exit 1; fi; done - # work around newer autoconf stuff (runstatedir) - touch debian/buildtree/configure.sh debian/buildtree/config.h.in - @echo configuring - (cd debian/buildtree; ./configure --with-included-zlib=yes --prefix=/usr --mandir='$${prefix}/share/man' --build=$(DEB_BUILD_GNU_TYPE) --host=$(DEB_HOST_GNU_TYPE) LDFLAGS="$(LDFLAGS)") - @echo building - $(MAKE) --directory=debian/buildtree CFLAGS="$(CFLAGS)" CPPFLAGS="$(CPPFLAGS)" LDFLAGS="$(LDFLAGS)" all - touch build-stamp - @echo done - -clean: checkdir - -rm -f build-stamp - -rm -rf debian/buildtree - -rm -rf *~ debian/tmp debian/*~ debian/*.bak debian/files* debian/substvars - -binary-indep: checkroot build -# nothing to do - -binary-arch: checkroot build - -rm -rf debian/tmp - $(INSTALL_DIR) debian/tmp \ - debian/tmp/DEBIAN \ - debian/tmp/usr/bin \ - debian/tmp/usr/share/doc/rsync/examples \ - debian/tmp/usr/share/doc/rsync/scripts \ - debian/tmp/usr/share/man/man1 \ - debian/tmp/usr/share/man/man5 \ - debian/tmp/usr/share/lintian/overrides \ - debian/tmp/lib/systemd/system \ - debian/tmp/etc \ - debian/tmp/etc/default \ - debian/tmp/etc/init.d - # debian/tmp/usr/lib/debian-test/tests -ifeq (,$(findstring nostrip,$(DEB_BUILD_OPTIONS))) - $(MAKE) --directory=debian/buildtree install-strip prefix=`pwd`/debian/tmp/usr exec_prefix=`pwd`/debian/tmp/usr $(INSTALL_CROSS) -ifeq ($(DEB_BUILD_GNU_TYPE),$(DEB_HOST_GNU_TYPE)) - strip --strip-unneeded --remove-section=.comment --remove-section=.note debian/tmp/usr/bin/rsync -else - $(DEB_HOST_GNU_TYPE)-strip --strip-unneeded --remove-section=.comment --remove-section=.note debian/tmp/usr/bin/rsync -endif -else - $(MAKE) --directory=debian/buildtree install prefix=`pwd`/debian/tmp/usr exec_prefix=`pwd`/debian/tmp/usr -endif - $(INSTALL_FILE) debian/changelog debian/tmp/usr/share/doc/rsync/changelog.Debian - $(INSTALL_FILE) README tech_report.tex debian/tmp/usr/share/doc/rsync/ - $(INSTALL_FILE) TODO debian/tmp/usr/share/doc/rsync/ - $(INSTALL_FILE) NEWS debian/tmp/usr/share/doc/rsync/changelog - $(INSTALL_FILE) packaging/cull_options debian/tmp/usr/share/doc/rsync/scripts/ - $(INSTALL_FILE) support/atomic-rsync support/cvs2includes support/file-attr-restore support/files-to-excludes support/git-set-file-times support/logfilter support/lsh support/mnt-excl support/munge-symlinks support/rrsync support/rsyncstats debian/tmp/usr/share/doc/rsync/scripts/ - $(INSTALL_FILE) debian/README.Debian debian/tmp/usr/share/doc/rsync/ - echo -e '\n\f' >> debian/tmp/usr/share/doc/rsync/changelog - cat OLDNEWS >> debian/tmp/usr/share/doc/rsync/changelog - find debian/tmp/usr/share/doc/ debian/tmp/usr/share/man/ -name scripts -prune -o -type f -exec gzip -9frn {} + - $(INSTALL_FILE) debian/rsyncd.conf debian/logrotate.conf.rsync debian/tmp/usr/share/doc/rsync/examples/ - $(INSTALL_FILE) debian/copyright debian/tmp/usr/share/doc/rsync/ - # $(INSTALL_SCRIPT) test.sh debian/tmp/usr/lib/debian-test/tests/rsync - $(INSTALL_SCRIPT) debian/postinst debian/tmp/DEBIAN/ - $(INSTALL_SCRIPT) debian/prerm debian/tmp/DEBIAN/ - $(INSTALL_SCRIPT) debian/postrm debian/tmp/DEBIAN/ - $(INSTALL_FILE) debian/buildtree/packaging/systemd/rsync.service debian/tmp/lib/systemd/system/ - $(INSTALL_FILE) debian/default debian/tmp/etc/default/rsync - $(INSTALL_SCRIPT) debian/init.d debian/tmp/etc/init.d/rsync - $(INSTALL_FILE) debian/lintian.overrides debian/tmp/usr/share/lintian/overrides/rsync - (cd debian/tmp; find ./etc -type f | LC_ALL=C sort | sed s,.,,) > debian/tmp/DEBIAN/conffiles - (cd debian/tmp; find lib usr -type f -print0 | LC_ALL=C sort -z | xargs -0r md5sum) > debian/tmp/DEBIAN/md5sums - dpkg-shlibdeps debian/tmp/usr/bin/$(BINS) - dpkg-gencontrol -isp - chown -R root.root debian/tmp - chmod -R go=rX debian/tmp - dpkg --build debian/tmp .. - -# Below here is fairly generic really - -binary: binary-indep binary-arch - -checkdir: - @test -f rsync.c -a -f debian/rules - -checkroot: checkdir - @test 0 = `id -u` || { echo "Error: not super-user"; exit 1; } - -.PHONY: binary binary-arch binary-indep clean checkroot checkdir build build-arch build-indep diff -Nru rsync-3.4.1+ds1/debian/tests/control rsync-3.5.0+ds1/debian/tests/control --- rsync-3.4.1+ds1/debian/tests/control 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/tests/control 2026-09-16 01:46:30.000000000 +0000 @@ -1,17 +1,17 @@ -Tests: local-tests -Depends: @, @builddeps@, build-essential, diffoscope, shunit2 - -Tests: upstream-tests -Depends: @, @builddeps@, build-essential - Test-Command: rsync -h Features: test-name=rsync-help Restrictions: superficial +Tests: local-tests +Depends: @, @builddeps@, build-essential, diffoscope, shunit2 + Tests: remote-tests Depends: @, @builddeps@, build-essential, diffoscope, shunit2, openssh-server, adduser Restrictions: needs-root, isolation-container +Tests: upstream-tests +Depends: @, @builddeps@, build-essential, fakeroot + Tests: upstream-tests-as-root Depends: @, @builddeps@, build-essential Restrictions: needs-root diff -Nru rsync-3.4.1+ds1/debian/tests/local-tests rsync-3.5.0+ds1/debian/tests/local-tests --- rsync-3.4.1+ds1/debian/tests/local-tests 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/tests/local-tests 2026-09-16 01:46:30.000000000 +0000 @@ -66,14 +66,18 @@ } testMtimeSyncForFileIfTargetNewerUpdate(){ - startSkipping # FIXME: remove skipping once 285 <= diffoscope > 300 (in all suites?) + # Skipped on trixie: diffoscope 297 exits 0 for byte-identical files + # that only differ in metadata (reproducible-builds/diffoscope#411), + # so the mtime difference this test relies on is never reported. + # Fixed in diffoscope 300, which is not available in trixie. + startSkipping echo foo > src cp src target touch -d "next minute" target $RSYNC_BIN --times --update src "$TARGET_DIR"/target - $DIFF_CMD --exclude-directory-metadata=no src target 2>&1 + $DIFF_CMD --exclude-directory-metadata=no src target > /dev/null 2>&1 status=$? assertFalse 'Failed mtime sync for newer target with -u flag' $status endSkipping @@ -97,7 +101,7 @@ touch -d "@1750044444" src.txt $RSYNC_BIN src.txt "$TARGET_DIR"/target.txt - $DIFF_CMD src.txt target.txt 2>&1 + $DIFF_CMD src.txt target.txt > /dev/null 2>&1 assertFalse "Rsync unexpectedly synced file of same size and mtime" "$?" $RSYNC_BIN --checksum src.txt "$TARGET_DIR"/target.txt diff -Nru rsync-3.4.1+ds1/debian/tests/remote-tests rsync-3.5.0+ds1/debian/tests/remote-tests --- rsync-3.4.1+ds1/debian/tests/remote-tests 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/tests/remote-tests 2026-09-16 01:46:30.000000000 +0000 @@ -6,6 +6,10 @@ adduser --home "$HOMEDIR" --disabled-password --gecos autopkgtest rsync mkdir -m 700 "$HOMEDIR/.ssh" +if ! systemctl --quiet is-active ssh.service; then + systemctl start ssh.service +fi + ssh-keyscan localhost > "$HOMEDIR/.ssh/known_hosts" 2>/dev/null ssh-keygen -q -N '' -f "$HOMEDIR/.ssh/id_rsa" cp "$HOMEDIR/.ssh/id_rsa.pub" "$HOMEDIR/.ssh/authorized_keys" diff -Nru rsync-3.4.1+ds1/debian/tests/upstream-tests rsync-3.5.0+ds1/debian/tests/upstream-tests --- rsync-3.4.1+ds1/debian/tests/upstream-tests 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/tests/upstream-tests 2026-09-16 01:46:30.000000000 +0000 @@ -4,8 +4,10 @@ echo "debian/rules override_dh_auto_configure " debian/rules override_dh_auto_configure -# Supress gcc warnings (autopkg treats them as failures) -make tls getgroups getfsdev t_chmod_secure t_secure_relpath trimslash t_unsafe wildtest testrun 2>/dev/null +# Suppress gcc warnings (autopkgtest treats them as failures) +make check-progs 2>/dev/null # Run tests -rsync_bin="/usr/bin/rsync" ./runtests.sh +# installcheck "does *not* depend on building or installing: you can +# use it to check a version installed from a binary" +make installcheck diff -Nru rsync-3.4.1+ds1/debian/tests/upstream-tests-as-root rsync-3.5.0+ds1/debian/tests/upstream-tests-as-root --- rsync-3.4.1+ds1/debian/tests/upstream-tests-as-root 2026-06-01 18:03:48.000000000 +0000 +++ rsync-3.5.0+ds1/debian/tests/upstream-tests-as-root 2026-09-16 01:46:30.000000000 +0000 @@ -4,8 +4,10 @@ echo "debian/rules override_dh_auto_configure " debian/rules override_dh_auto_configure -# Supress gcc warnings (autopkg treats them as failures) -make tls getgroups getfsdev t_chmod_secure t_secure_relpath trimslash t_unsafe wildtest testrun 2>/dev/null +# Suppress gcc warnings (autopkgtest treats them as failures) +make check-progs 2>/dev/null # Run tests -rsync_bin="/usr/bin/rsync" ./runtests.sh +# installcheck "does *not* depend on building or installing: you can +# use it to check a version installed from a binary" +make installcheck diff -Nru rsync-3.4.1+ds1/delete.c rsync-3.5.0+ds1/delete.c --- rsync-3.4.1+ds1/delete.c 2024-04-06 16:30:21.000000000 +0000 +++ rsync-3.5.0+ds1/delete.c 2026-07-20 04:05:30.000000000 +0000 @@ -34,6 +34,49 @@ int non_perishable_cnt = 0; int skipped_deletes = 0; +/* Held fd of the directory whose contents delete_dir_contents() is currently + * removing, so delete_item()'s per-entry rmdir/unlink/chmod go through it + * instead of re-resolving the full path for every entry. Set (with save/ + * restore across the recursion) around the delete loop; -1 outside a recursive + * delete or when the secure resolver is gated off (chroot / non-receiver) or + * the path doesn't live directly in that dir. */ +static int del_dirfd = -1; +static const char *del_dir_prefix; +static int del_dir_prefix_len; + +/* If `path` is a single component directly inside the dir being deleted, + * point *leaf at its basename and return the held dir fd; else return -1. */ +static int del_held_dfd(const char *path, const char **leaf) +{ + if (del_dirfd >= 0 + && strncmp(path, del_dir_prefix, del_dir_prefix_len) == 0 + && path[del_dir_prefix_len] == '/' + && strchr(path + del_dir_prefix_len + 1, '/') == NULL) { + *leaf = path + del_dir_prefix_len + 1; + return del_dirfd; + } + return -1; +} + +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); + else + do_chmod_at(fbuf, mode); +} + +static int del_unlink(const char *fbuf) +{ + const char *leaf; + int dfd = del_held_dfd(fbuf, &leaf); + if (dfd >= 0 && do_unlink_atfd(dfd, leaf, 0) == 0) + return 0; + return robust_unlink(fbuf); /* fall back (ETXTBSY retry, or not held) */ +} + static inline int is_backup_file(char *fn) { int k = strlen(fn) - backup_suffix_len; @@ -83,6 +126,18 @@ flags = (flags & ~(DEL_RECURSE|DEL_MAKE_ROOM|DEL_NO_UID_WRITE)) | DEL_DIR_IS_EMPTY; + /* Hold this dir open so the per-entry chmod/rmdir/unlink below (and in + * delete_item) become *at() calls against it rather than re-resolving the + * full path for every entry. Save/restore around the recursion. */ + int save_del_dirfd = del_dirfd; + 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); + fname[dlen] = '/'; + del_dir_prefix = fname; + del_dir_prefix_len = dlen; + for (j = dirlist->used; j--; ) { struct file_struct *fp = dirlist->files[j]; @@ -98,7 +153,7 @@ strlcpy(p, fp->basename, remainder); if (!(fp->mode & S_IWUSR) && !am_root && fp->flags & FLAG_OWNED_BY_US) - do_chmod(fname, fp->mode | S_IWUSR); + del_chmod(fname, fp->mode | S_IWUSR); /* Save stack by recursing to ourself directly. */ if (S_ISDIR(fp->mode)) { if (delete_dir_contents(fname, flags | DEL_RECURSE) != DR_SUCCESS) @@ -108,6 +163,12 @@ ret = DR_NOT_EMPTY; } + if (del_dirfd >= 0) + close(del_dirfd); + del_dirfd = save_del_dirfd; + del_dir_prefix = save_del_prefix; + del_dir_prefix_len = save_del_prefix_len; + fname[dlen] = '\0'; done: @@ -139,7 +200,7 @@ } if (flags & DEL_NO_UID_WRITE) - do_chmod(fbuf, mode | S_IWUSR); + del_chmod(fbuf, mode | S_IWUSR); if (S_ISDIR(mode) && !(flags & DEL_DIR_IS_EMPTY)) { /* This only happens on the first call to delete_item() since @@ -159,19 +220,21 @@ } if (S_ISDIR(mode)) { + const char *leaf; + int dfd = del_held_dfd(fbuf, &leaf); what = "rmdir"; - ok = do_rmdir(fbuf) == 0; + ok = (dfd >= 0 ? do_unlink_atfd(dfd, leaf, AT_REMOVEDIR) : do_rmdir_at(fbuf)) == 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 = robust_unlink(fbuf) == 0; + ok = del_unlink(fbuf) == 0; } } else { what = "unlink"; - ok = robust_unlink(fbuf) == 0; + ok = del_unlink(fbuf) == 0; } } diff -Nru rsync-3.4.1+ds1/doc/README-SGML rsync-3.5.0+ds1/doc/README-SGML --- rsync-3.4.1+ds1/doc/README-SGML 2002-03-11 00:27:42.000000000 +0000 +++ rsync-3.5.0+ds1/doc/README-SGML 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ -Handling the rsync SGML documentation - -rsync documentation is now primarily in Docbook format. Docbook is an -SGML/XML documentation format that is becoming standard on free -operating systems. It's also used for Samba documentation. - -The SGML files are source code that can be translated into various -useful output formats, primarily PDF, HTML, Postscript and plain text. - -To do this transformation on Debian, you should install the -docbook-utils package. Having done that, you can say - - docbook2pdf rsync.sgml - -and so on. - -On other systems you probably need James Clark's "sp" and "JadeTeX" -packages. Work it out for yourself and send a note to the mailing -list. - diff -Nru rsync-3.4.1+ds1/doc/profile.txt rsync-3.5.0+ds1/doc/profile.txt --- rsync-3.4.1+ds1/doc/profile.txt 2002-02-25 18:06:33.000000000 +0000 +++ rsync-3.5.0+ds1/doc/profile.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,42 +0,0 @@ -Notes on rsync profiling - -strlcpy is hot: - - 0.00 0.00 1/7735635 push_dir [68] - 0.00 0.00 1/7735635 pop_dir [71] - 0.00 0.00 1/7735635 send_file_list [15] - 0.01 0.00 18857/7735635 send_files [4] - 0.04 0.00 129260/7735635 send_file_entry [18] - 0.04 0.00 129260/7735635 make_file [20] - 0.04 0.00 141666/7735635 send_directory [36] - 2.29 0.00 7316589/7735635 f_name [13] -[14] 11.7 2.42 0.00 7735635 strlcpy [14] - - -Here's the top few functions: - - 46.23 9.57 9.57 13160929 0.00 0.00 mdfour64 - 14.78 12.63 3.06 13160929 0.00 0.00 copy64 - 11.69 15.05 2.42 7735635 0.00 0.00 strlcpy - 10.05 17.13 2.08 41438 0.05 0.38 sum_update - 4.11 17.98 0.85 13159996 0.00 0.00 mdfour_update - 1.50 18.29 0.31 file_compare - 1.45 18.59 0.30 129261 0.00 0.01 send_file_entry - 1.23 18.84 0.26 2557585 0.00 0.00 f_name - 1.11 19.07 0.23 1483750 0.00 0.00 u_strcmp - 1.11 19.30 0.23 118129 0.00 0.00 writefd_unbuffered - 0.92 19.50 0.19 1085011 0.00 0.00 writefd - 0.43 19.59 0.09 156987 0.00 0.00 read_timeout - 0.43 19.68 0.09 129261 0.00 0.00 clean_fname - 0.39 19.75 0.08 32887 0.00 0.38 matched - 0.34 19.82 0.07 1 70.00 16293.92 send_files - 0.29 19.89 0.06 129260 0.00 0.00 make_file - 0.29 19.95 0.06 75430 0.00 0.00 read_unbuffered - - - -mdfour could perhaps be made faster: - -/* NOTE: This code makes no attempt to be fast! */ - -There might be an optimized version somewhere that we can borrow. diff -Nru rsync-3.4.1+ds1/doc/rsync.sgml rsync-3.5.0+ds1/doc/rsync.sgml --- rsync-3.4.1+ds1/doc/rsync.sgml 2020-04-16 00:42:23.000000000 +0000 +++ rsync-3.5.0+ds1/doc/rsync.sgml 1970-01-01 00:00:00.000000000 +0000 @@ -1,351 +0,0 @@ - - - - rsync - - 1996 -- 2002 - Martin Pool - Andrew Tridgell - - - Martin - Pool - - - - - Introduction - - rsync is a flexible program for efficiently copying files or - directory trees. - - rsync has many options to select which files will be copied - and how they are to be transferred. It may be used as an - alternative to ftp, http, scp or rcp. - - The rsync remote-update protocol allows rsync to transfer just - the differences between two sets of files across the network link, - using an efficient checksum-search algorithm described in the - technical report that accompanies this package. - - Some of the additional features of rsync are: - - - - - support for copying links, devices, owners, groups and - permissions - - - - - - exclude and exclude-from options similar to GNU tar - - - - - - a CVS exclude mode for ignoring the same files that CVS would ignore - - - - - can use any transparent remote shell, including rsh or ssh - - - - - does not require root privileges - - - - - pipelining of file transfers to minimize latency costs - - - - - support for anonymous or authenticated rsync servers (ideal for - mirroring) - - - - - - - - - Using rsync -
- - Introductory example - - - - Probably the most common case of rsync usage is to copy files - to or from a remote machine using - ssh as a network transport. In - this situation rsync is a good alternative to - scp. - - - - The most commonly used arguments for rsync are - - - - - - - Be verbose. Primarily, display the name of each file as it is copied. - - - - - - - - - Reproduce the structure and attributes of the origin files as exactly - as possible: this includes copying subdirectories, symlinks, special - files, ownership and permissions. (@xref{Attributes to - copy}.) - - - - - - - - - - - Compress network traffic, using a modified version of the - @command{zlib} library. - - - Display a progress indicator while files are transferred. This should - normally be omitted if rsync is not run on a terminal. - -
- - - - -
- Local and remote - - There are six different ways of using rsync. They - are: - - - - - - - - for copying local files. This is invoked when neither - source nor destination path contains a @code{:} separator - - - - for copying from the local machine to a remote machine using - a remote shell program as the transport (such as rsh or - ssh). This is invoked when the destination path contains a - single @code{:} separator. - - - - for copying from a remote machine to the local machine - using a remote shell program. This is invoked when the source - contains a @code{:} separator. - - - - for copying from a remote rsync server to the local - machine. This is invoked when the source path contains a @code{::} - separator or a @code{rsync://} URL. - - - - for copying from the local machine to a remote rsync - server. This is invoked when the destination path contains a @code{::} - separator. - - - - for listing files on a remote machine. This is done the - same way as rsync transfers except that you leave off the - local destination. - - - - -Note that in all cases (other than listing) at least one of the source -and destination paths must be local. - - -Any one invocation of rsync makes a copy in a single direction. rsync -currently has no equivalent of @command{ftp}'s interactive mode. - -@cindex @sc{nfs} -@cindex network filesystems -@cindex remote filesystems - - -rsync's network protocol is generally faster at copying files than -network filesystems such as @sc{nfs} or @sc{cifs}. It is better to -run rsync on the file server either as a daemon or over ssh than -running rsync giving the network directory. - -
-
- - - - - Frequently asked questions - - - - - - - - - - Are there mailing lists for rsync? - - - - Yes, and you can subscribe and unsubscribe through a - web interface at - http://lists.samba.org/ - - - - If you are having trouble with the mailing list, please - send mail to the administrator - - rsync-admin@lists.samba.org - - not to the list itself. - - - - The mailing list archives are searchable. Use - Google and prepend - the search with site:lists.samba.org - rsync, plus relevant keywords. - - - - - - - - - Why is rsync so much bigger when I build it with - gcc? - - - - - On gcc, rsync builds by default with debug symbols - included. If you strip both executables, they should end - up about the same size. (Use make - install-strip.) - - - - - - - - Is rsync useful for a single large file like an ISO image? - - - - Yes, but note the following: - - - Background: A common use of rsync is to update a file (or set of files) in one location from a more - correct or up-to-date copy in another location, taking advantage of portions of the files that are - identical to speed up the process. (Note that rsync will transfer a file in its entirety if no copy - exists at the destination.) - - - (This discussion is written in terms of updating a local copy of a file from a correct file in a - remote location, although rsync can work in either direction.) - - - The file to be updated (the local file) must be in a destination directory that has enough space for - two copies of the file. (In addition, keep an extra copy of the file to be updated in a different - location for safety -- see the discussion (below) about rsync's behavior when the rsync process is - interrupted before completion.) - - - The local file must have the same name as the remote file being sync'd to (I think?). If you are - trying to upgrade an iso from, for example, beta1 to beta2, rename the local file to the same name - as the beta2 file. *(This is a useful thing to do -- only the changed portions will be - transmitted.)* - - - The extra copy of the local file kept in a different location is because of rsync's behavior if - interrupted before completion: - - - * If you specify the --partial option and rsync is interrupted, rsync will save the partially - rsync'd file and throw away the original local copy. (The partially rsync'd file is correct but - truncated.) If rsync is restarted, it will not have a local copy of the file to check for duplicate - blocks beyond the section of the file that has already been rsync'd, thus the remainder of the rsync - process will be a "pure transfer" of the file rather than taking advantage of the rsync algorithm. - - - * If you don't specify the --partial option and rsync is interrupted, rsync will throw away the - partially rsync'd file, and, when rsync is restarted starts the rsync process over from the - beginning. - - - Which of these is most desirable depends on the degree of commonality between the local and remote - copies of the file *and how much progress was made before the interruption*. - - - The ideal approach after an interruption would be to create a new file by taking the original file - and deleting a portion equal in size to the portion already rsync'd and then appending *the - remaining* portion to the portion of the file that has already been rsync'd. (There has been some - discussion about creating an option to do this automatically.) - - The --compare-dest option is useful when transferring multiple files, but is of no benefit in - transferring a single file. (AFAIK) - - *Other potentially useful information can be found at: - -[3]http://twiki.org/cgi-bin/view/Wikilearn/RsyncingALargeFile - - This answer, formatted with "real" bullets, can be found at: - -[4]http://twiki.org/cgi-bin/view/Wikilearn/RsyncingALargeFileFAQ* - - - - - - - - - - Other Resources - - - -
diff -Nru rsync-3.4.1+ds1/exclude.c rsync-3.5.0+ds1/exclude.c --- rsync-3.4.1+ds1/exclude.c 2024-04-06 16:30:21.000000000 +0000 +++ rsync-3.5.0+ds1/exclude.c 2026-08-02 10:32:33.000000000 +0000 @@ -41,6 +41,100 @@ 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; + +/* Where the rule text now being parsed came from, when that is a file's + * CONTENTS rather than an argument. A rule that fails to parse used to be + * echoed back verbatim, and the peer chooses which file gets merged (a + * per-directory merge rule travels over the protocol, so no argument of ours + * ever names it), which made the filter parser a read-any-line oracle: any + * line that is not valid filter syntax came straight back in the error. + * Report where the bad rule is, not what it says. */ +static int rule_src_in_file = 0; /* parsing a file's contents right now */ +static const char *rule_src_file = NULL; /* ...and its name is safe to show */ +static int rule_src_line = 0; +/* Where a file whose own name we must NOT print was named, which is a location + * we CAN print: it keeps the diagnostic useful without echoing the pathname a + * merge rule supplied. */ +static const char *rule_src_named_at = NULL; + +/* True while the text we are handling came out of a file's contents: either we + * are parsing that file right now, or this is a deferred per-dir merge whose + * NAME came from one and which carries the provenance on the rule. */ +#define TEXT_FROM_FILE(template) \ + (rule_src_in_file \ + || ((template) && (template)->rflags & FILTRULE_FROM_FILE)) + +/* "FILE line N", or just "FILE" when the count is not a line count. */ +static const char *rule_src_where(void) +{ + static char buf[MAXPATHLEN + 32]; + + if (!rule_src_file) { + if (!rule_src_named_at) + return "a file read earlier"; /* origin not retained */ + snprintf(buf, sizeof buf, "a file named at %s", rule_src_named_at); + return buf; + } + if (rule_src_line < 0) + return rule_src_file; + snprintf(buf, sizeof buf, "%s line %d", rule_src_file, rule_src_line); + return buf; +} + +/* THE chokepoint. Every diagnostic string that is, or is built from, a filter + * rule's own text -- a pattern, a merge-file name, a path composed from one -- + * must be passed through rule_text() on its way to rprintf(). When the rule + * came from an argument the text is returned unchanged, because it is the + * user's own and hiding it only makes typos harder to fix. When it came from + * a FILE's contents it is replaced by a description of where it came from, + * because the peer chooses which file gets merged and any line of it that + * reaches a message is a line the peer can read back. + * + * Doing it here rather than at each site is the point: a message added later + * cannot reintroduce the leak by forgetting to check, and there is one place + * to audit. `template' is the rule the text belongs to, or NULL when the only + * thing that matters is whether we are parsing a file right now. + * + * The returned buffer is rotated, so two calls in one rprintf() are safe. */ +static const char *rule_text_len(const filter_rule *template, + const char *text, int len) +{ + static char buf[2][BIGPATHBUFLEN]; + static int which = 0; + char *b = buf[which]; + + which ^= 1; + if (!TEXT_FROM_FILE(template)) { + if (len < 0) + return text; + snprintf(b, sizeof buf[0], "%.*s", len, text); + return b; + } + snprintf(b, sizeof buf[0], "", rule_src_where()); + return b; +} + +static const char *rule_text(const filter_rule *template, const char *text) +{ + return rule_text_len(template, text, -1); +} + +/* For the extra detail some messages add ABOUT the text -- a character of it, + * an offset into it. Dropped along with the text it describes. */ +static const char *rule_detail(const filter_rule *template, const char *detail) +{ + return TEXT_FROM_FILE(template) ? "" : detail; +} + +static void filter_rule_err(const char *msg, const char *rulestr) +{ + rprintf(FERROR, "%s: %s\n", msg, rule_text(NULL, rulestr)); + exit_cleanup(RERR_SYNTAX); +} extern char curr_dir[MAXPATHLEN]; extern unsigned int curr_dir_len; @@ -71,6 +165,9 @@ /* This is True when we're scanning parent dirs for per-dir merge-files. */ static BOOL parent_dirscan = False; +#define MAX_MERGE_DEPTH 32 +static int merge_depth = 0; + /* This array contains a list of all the currently active per-dir merge * files. This makes it easier to save the appropriate values when we * "push" down into each subdirectory. */ @@ -171,10 +268,10 @@ else mention_rule_suffix = DEBUG_GTE(FILTER, 2) ? "" : NULL; if (mention_rule_suffix) { - rprintf(FINFO, "[%s] add_rule(%s%.*s%s)%s%s\n", - who_am_i(), get_rule_prefix(rule, pat, 0, NULL), - (int)pat_len, pat, (rule->rflags & FILTRULE_DIRECTORY) ? "/" : "", - listp->debug_type, mention_rule_suffix); + rprintf(FINFO, "[%s] add_rule(%s%s)%s%s\n", + who_am_i(), rule_detail(rule, get_rule_prefix(rule, pat, 0, NULL)), + rule_text_len(rule, pat, (int)pat_len), + listp->debug_type, rule_detail(rule, mention_rule_suffix)); } /* These flags also indicate that we're reading a list that @@ -279,7 +376,7 @@ } lp = new_array0(filter_rule_list, 1); - if (asprintf(&lp->debug_type, " [per-dir %s]", cp) < 0) + if (asprintf(&lp->debug_type, " [per-dir %s]", rule_text(rule, cp)) < 0) out_of_memory("add_rule"); rule->u.mergelist = lp; @@ -427,7 +524,7 @@ if (cp[1] == ']') { if (!saw_wild) cp++; /* A \] in a non-wild filter causes a problem, so drop the \ . */ - } else if (!strchr("*[?", cp[1])) { + } else if (!cp[1] || !strchr("*[?", cp[1])) { backslash_cnt++; if (saw_wild) *p++ = '\\'; @@ -596,7 +693,8 @@ * value and will be updated with the length of the resulting name. We * always return a name that is null terminated, even if the merge_file * name was not. */ -static char *parse_merge_name(const char *merge_file, unsigned int *len_ptr, +static char *parse_merge_name(const filter_rule *template, + const char *merge_file, unsigned int *len_ptr, unsigned int prefix_skip) { static char buf[MAXPATHLEN]; @@ -627,7 +725,7 @@ } if (!sanitize_path(fn, merge_file, r, dirbuf_depth, SP_DEFAULT)) { rprintf(FERROR, "merge-file name overflows: %s\n", - merge_file); + rule_text(template, merge_file)); return NULL; } fn_len = strlen(fn); @@ -640,7 +738,8 @@ if (fn != buf) { int d_len = dirbuf_len - prefix_skip; if (d_len + fn_len >= MAXPATHLEN) { - rprintf(FERROR, "merge-file name overflows: %s\n", fn); + rprintf(FERROR, "merge-file name overflows: %s\n", + rule_text(template, fn)); return NULL; } memcpy(buf, dirbuf + prefix_skip, d_len); @@ -690,7 +789,7 @@ char *x, *y, *pat = ex->pattern; unsigned int len; - if (!(x = parse_merge_name(pat, NULL, 0)) || *x != '/') + if (!(x = parse_merge_name(ex, pat, NULL, 0)) || *x != '/') return 0; if (DEBUG_GTE(FILTER, 2)) { @@ -816,7 +915,7 @@ io_error |= IOERR_GENERAL; rprintf(FERROR, "cannot add local filter rules in long-named directory: %s\n", - full_fname(dirbuf)); + rule_text(ex, full_fname(dirbuf))); } dirbuf[dirbuf_len] = '\0'; } @@ -904,7 +1003,7 @@ { int slash_handling, str_cnt = 0, anchored_match = 0; int ret_match = ex->rflags & FILTRULE_NEGATE ? 0 : 1; - char *p, *pattern = ex->pattern; + const char *p, *pattern = ex->pattern; const char *strings[16]; /* more than enough */ const char *name = fname + (*fname == '/'); @@ -999,8 +1098,8 @@ : "file"; rprintf(code, "[%s] %sing %s %s because of pattern %s%s%s\n", w, actions[*w=='g'][!(ent->rflags & FILTRULE_INCLUDE)], - t, name, ent->pattern, - ent->rflags & FILTRULE_DIRECTORY ? "/" : "", type); + t, name, rule_text(ent, ent->pattern), + rule_detail(ent, ent->rflags & FILTRULE_DIRECTORY ? "/" : ""), type); } } @@ -1033,6 +1132,56 @@ return ret; } +/* Returns 1 if `name` matches an implied-parent rule (a directory component + * seeded by add_implied_include() with FILTRULE_DIRECTORY) but not a leaf + * rule -- i.e. the client asked for something under the dir, never the dir + * itself as content. + * + * The receiver uses this to refuse a malicious sender that sets XMIT_TOP_DIR + * without XMIT_NO_CONTENT_DIR on such a dir: the honest encoding is both flags + * (flist.c send path), so otherwise the receiver would set FLAG_CONTENT_DIR + * and delete_in_dir() could sweep pre-existing siblings under --delete. */ +int is_implied_parent_dir(const char *name) +{ + filter_rule *ent; + int parent_match = 0; + + if (!implied_filter_list.head) + return 0; + + /* The receiver exempts its synthetic transfer-root entry from the + * requested-name filter. Treat it as parent-only unless an empty/root + * source argument added the root-content rule. */ + if ((name[0] == '.' && name[1] == '\0') + || (name[0] == '/' && name[1] == '.' && name[2] == '\0')) { + for (ent = implied_filter_list.head; ent; ent = ent->next) { + if (!(ent->rflags & FILTRULE_INCLUDE)) + continue; + if (strcmp(ent->pattern, "/**") == 0 + || strcmp(ent->pattern, "/*") == 0) + return 0; + } + return 1; + } + + for (ent = implied_filter_list.head; ent; ent = ent->next) { + if (ent->rflags & (FILTRULE_PERDIR_MERGE | FILTRULE_CVS_IGNORE)) + continue; + if (!rule_matches(name, ent, NAME_IS_DIR)) + continue; + if (!(ent->rflags & FILTRULE_INCLUDE)) + continue; + if (ent->rflags & FILTRULE_DIRECTORY) { + parent_match = 1; + continue; + } + /* A non-DIRECTORY include rule = a leaf the client asked for, so + * the dir is legitimately in the list, not parent-only. */ + return 0; + } + return parent_match; +} + /* Return -1 if file "name" is defined to be excluded by the specified * exclude list, 1 if it is included, and 0 if it was not matched. */ int check_filter(filter_rule_list *listp, enum logcode code, @@ -1112,6 +1261,8 @@ /* Inherit from the template. Don't inherit FILTRULES_SIDES; we check * that later. */ rule->rflags = template->rflags & FILTRULES_FROM_CONTAINER; + if (rule_src_in_file) + rule->rflags |= FILTRULE_FROM_FILE; /* before parse_merge_name() */ /* Figure out what kind of a filter rule "s" is pointing at. Note * that if FILTRULE_NO_PREFIXES is set, the rule is either an include @@ -1209,8 +1360,7 @@ rule->rflags |= FILTRULE_CLEAR_LIST; break; default: - rprintf(FERROR, "Unknown filter rule: `%s'\n", *rulestr_ptr); - exit_cleanup(RERR_SYNTAX); + filter_rule_err("Unknown filter rule", *rulestr_ptr); } while (ch != '!' && *++s && *s != ' ' && *s != '_') { if (template->rflags & FILTRULE_WORD_SPLIT && isspace(*s)) { @@ -1219,11 +1369,15 @@ } switch (*s) { default: - invalid: - rprintf(FERROR, - "invalid modifier '%c' at position %d in filter rule: %s\n", - *s, (int)(s - (const uchar *)*rulestr_ptr), *rulestr_ptr); + invalid: { + char where[32]; + snprintf(where, sizeof where, " '%c' at position %d", + *s, (int)(s - (const uchar *)*rulestr_ptr)); + rprintf(FERROR, "invalid modifier%s in filter rule: %s\n", + rule_detail(NULL, where), + rule_text(NULL, *rulestr_ptr)); exit_cleanup(RERR_SYNTAX); + } case '-': if (!BITS_SETnUNSET(rule->rflags, FILTRULE_MERGE_FILE, FILTRULE_NO_PREFIXES)) goto invalid; @@ -1295,10 +1449,8 @@ /* The filter and template both specify side(s). This * is dodgy (and won't work correctly if the template is * a one-sided per-dir merge rule), so reject it. */ - rprintf(FERROR, - "specified-side merge file contains specified-side filter: %s\n", - *rulestr_ptr); - exit_cleanup(RERR_SYNTAX); + filter_rule_err("specified-side merge file contains specified-side filter", + *rulestr_ptr); } rule->rflags |= template->rflags & FILTRULES_SIDES; } @@ -1313,17 +1465,14 @@ len = strlen((char*)s); if (rule->rflags & FILTRULE_CLEAR_LIST) { - if (!(rule->rflags & FILTRULE_NO_PREFIXES) + if (!(template->rflags & FILTRULE_NO_PREFIXES) && !(xflags & XFLG_OLD_PREFIXES) && len) { - rprintf(FERROR, - "'!' rule has trailing characters: %s\n", *rulestr_ptr); - exit_cleanup(RERR_SYNTAX); + filter_rule_err("'!' rule has trailing characters", *rulestr_ptr); } if (len > 1) rule->rflags &= ~FILTRULE_CLEAR_LIST; } else if (!len && !(rule->rflags & FILTRULE_CVS_IGNORE)) { - rprintf(FERROR, "unexpected end of filter rule: %s\n", *rulestr_ptr); - exit_cleanup(RERR_SYNTAX); + filter_rule_err("unexpected end of filter rule", *rulestr_ptr); } /* --delete-excluded turns an un-modified include/exclude into a sender-side rule. */ @@ -1382,8 +1531,8 @@ break; if (pat_len >= MAXPATHLEN) { - rprintf(FERROR, "discarding over-long filter: %.*s\n", - (int)pat_len, pat); + rprintf(FERROR, "discarding over-long filter: %s\n", + rule_text_len(NULL, pat, (int)pat_len)); free_continue: free_filter(rule); continue; @@ -1411,6 +1560,11 @@ filter_rule *excl_self; excl_self = new0(filter_rule); + /* The pattern below is the merge rule's own text, so it + * inherits that rule's provenance. Built by hand, this + * rule looked argument-origin once parsing finished and + * the match trace echoed a merge file's contents at -vv. */ + excl_self->rflags = rule->rflags & FILTRULE_FROM_FILE; /* Find the beginning of the basename and add an exclude for it. */ for (name = pat + pat_len; name > pat && name[-1] != '/'; name--) {} add_rule(listp, name, (pat + pat_len) - name, excl_self, 0); @@ -1420,7 +1574,7 @@ if (parent_dirscan) { const char *p; unsigned int len = pat_len; - if ((p = parse_merge_name(pat, &len, module_dirlen))) + if ((p = parse_merge_name(rule, pat, &len, module_dirlen))) add_rule(listp, p, len, rule, 0); else free_filter(rule); @@ -1429,7 +1583,7 @@ } else { const char *p; unsigned int len = pat_len; - if ((p = parse_merge_name(pat, &len, 0))) + if ((p = parse_merge_name(rule, pat, &len, 0))) parse_filter_file(listp, p, rule, XFLG_FATAL_ERRORS); free_filter(rule); continue; @@ -1450,46 +1604,165 @@ char line[BIGPATHBUFLEN]; char *eob = line + sizeof line - 1; BOOL word_split = (template->rflags & FILTRULE_WORD_SPLIT) != 0; + const char *save_src_file, *save_src_named_at; + int save_src_line, save_src_in_file; + int named_by_file; + int pending = EOF; + char named_at[MAXPATHLEN + 32]; + /* Our own copy: fname may point into parse_merge_name()'s static buffer, + * which a merge rule inside THIS file overwrites while we still need it. */ + char src_name[MAXPATHLEN]; if (!fname || !*fname) return; + if (merge_depth >= MAX_MERGE_DEPTH) { + rprintf(FERROR, + "[%s] merge-file include depth limit (%d) exceeded at %s\n", + who_am_i(), MAX_MERGE_DEPTH, rule_text(template, fname)); + /* Match the failed-open path below: abort under a fatal + * (operator-supplied) merge, otherwise drop the rule. */ + if (xflags & XFLG_FATAL_ERRORS) + exit_cleanup(RERR_FILEIO); + return; + } + merge_depth++; + if (*fname != '-' || fname[1] || am_server) { + /* This path is operator- and (via per-directory merge files like + * .cvsignore) sender-controlled: a planted symlink could leak a + * root-readable file through the filter parser, or redirect an + * --exclude-from open via a planted parent. Refuse symlinks not + * owned by uid 0 or our euid. */ + const char *open_path; + int fd; if (daemon_filter_list.head) { + char *dir; strlcpy(line, fname, sizeof line); - clean_fname(line, CFN_COLLAPSE_DOT_DOT_DIRS); - if (check_filter(&daemon_filter_list, FLOG, line, 0) < 0) - fp = NULL; - else - fp = fopen(line, "rb"); + /* parse_merge_name() prepends module_dir for absolute paths, + * so strip module_dirlen back off before the check or the + * anchored module-relative daemon rule won't match (as + * options.c does for --exclude-from/--include-from). The + * original absolute path is still used for the open below. */ + dir = line + (*line == '/' ? module_dirlen : 0); + clean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS); + if (check_filter(&daemon_filter_list, FLOG, dir, 0) < 0) { + /* Hidden by the daemon filter: treat the merge file as + * non-existent rather than tripping XFLG_FATAL_ERRORS + * below, so it neither errors out nor leaks a + * fatal-vs-silent oracle. */ + if (DEBUG_GTE(FILTER, 2)) { + /* Same rule as everywhere else: the name is + * file content when a rule we read named it, + * and so is "the daemon filter hides it". */ + rprintf(FINFO, "[%s] parse_filter_file(%s)%s\n", + who_am_i(), rule_text(template, fname), + rule_detail(template, " hidden by daemon filter")); + } + merge_depth--; + return; + } + open_path = line; } else - fp = fopen(fname, "rb"); + 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). + * + * 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; + + if (fd < 0) + fp = NULL; + else if (!(fp = fdopen(fd, "rb"))) + close(fd); } else fp = stdin; if (DEBUG_GTE(FILTER, 2)) { + /* The name is file CONTENT when a rule we read named it, and a + * word-split per-dir merge turns every word of a file into one + * of these -- so the trace would echo what the syntax errors no + * longer do. Say where it came from instead. */ rprintf(FINFO, "[%s] parse_filter_file(%s,%x,%x)%s\n", - who_am_i(), fname, template->rflags, xflags, - fp ? "" : " [not found]"); + who_am_i(), rule_text(template, fname), template->rflags, xflags, + rule_detail(template, fp ? "" : " [not found]")); } if (!fp) { if (xflags & XFLG_FATAL_ERRORS) { - rsyserr(FERROR, errno, - "failed to open %sclude file %s", - template->rflags & FILTRULE_INCLUDE ? "in" : "ex", - fname); + /* rule_src_file is still the PARENT's context here: when it + * is set, this name came out of a file we read, so neither + * the name nor errno (an existence oracle) may be shown. */ + if (TEXT_FROM_FILE(template)) { + /* errno too: it answers "does this path exist". */ + rprintf(FERROR, "failed to open %sclude file %s\n", + template->rflags & FILTRULE_INCLUDE ? "in" : "ex", + rule_text(template, fname)); + } else { + rsyserr(FERROR, errno, + "failed to open %sclude file %s", + template->rflags & FILTRULE_INCLUDE ? "in" : "ex", + fname); + } exit_cleanup(RERR_FILEIO); } + merge_depth--; return; } + /* Before dirbuf is cut back: a per-directory fname points INTO dirbuf, + * so truncating first leaves only the directory and the location we + * report loses the filename. */ + strlcpy(src_name, fname, sizeof src_name); dirbuf[dirbuf_len] = '\0'; + /* Rule text from here on is this file's contents, not an argument, so + * a syntax error must not echo it. Saved and restored because a merge + * rule inside this file can bring us back in for another file. */ + save_src_in_file = rule_src_in_file; + save_src_file = rule_src_file; + save_src_line = rule_src_line; + /* If a rule we read named THIS file, our own path is file content too: + * track the location for provenance but do not put it in a message. */ + named_by_file = TEXT_FROM_FILE(template); + save_src_named_at = rule_src_named_at; + if (named_by_file) { + /* Snapshot where we were told to merge this, before that state + * is replaced below (rule_src_where returns a static buffer). + * A DEFERRED merge has no live location to point at -- the file + * that named it was read and finished long ago -- so leave the + * generic description rather than nesting two vague ones. */ + if (rule_src_in_file) { + strlcpy(named_at, rule_src_where(), sizeof named_at); + rule_src_named_at = named_at; + } else + rule_src_named_at = NULL; + } + rule_src_in_file = 1; + rule_src_file = named_by_file ? NULL : src_name; + rule_src_line = word_split ? -1 : 0; /* -1: tokens, not lines */ + while (1) { char *s = line; int ch, overflow = 0; + if (rule_src_line >= 0) + rule_src_line++; while (1) { - if ((ch = getc(fp)) == EOF) { + if (pending != EOF) { /* a CR lookahead we could not push back */ + ch = pending; + pending = EOF; + } else if ((ch = getc(fp)) == EOF) { if (ferror(fp) && errno == EINTR) { clearerr(fp); continue; @@ -1498,25 +1771,51 @@ } if (word_split && isspace(ch)) break; - if (eol_nulls? !ch : (ch == '\n' || ch == '\r')) + if (eol_nulls? !ch : (ch == '\n' || ch == '\r')) { + if (ch == '\r') { /* CRLF is one line, not two */ + int nxt; + while ((nxt = getc(fp)) == EOF + && ferror(fp) && errno == EINTR) + clearerr(fp); + if (nxt == EOF) { + if (!ferror(fp)) + ch = EOF; /* real end of file */ + } else if (nxt != '\n' && ungetc(nxt, fp) == EOF) { + /* Pushback failed: hand it to the + * NEXT rule, where it belongs -- + * appending it here would both + * corrupt this rule and skip the + * s < eob bound below. */ + pending = nxt; + } + } break; + } if (s < eob) *s++ = ch; else overflow = 1; } if (overflow) { - rprintf(FERROR, "discarding over-long filter: %s...\n", line); + rprintf(FERROR, "discarding over-long filter: %s\n", + rule_text_len(NULL, line, 0)); s = line; } *s = '\0'; /* Skip an empty token and (when line parsing) comments. */ - if (*line && (word_split || (*line != ';' && *line != '#'))) + if (*line && (word_split || (*line != ';' && *line != '#'))) { + rule_src_file = named_by_file ? NULL : src_name; parse_filter_str(listp, line, template, xflags); + } if (ch == EOF) break; } + rule_src_in_file = save_src_in_file; + rule_src_file = save_src_file; + rule_src_line = save_src_line; + rule_src_named_at = save_src_named_at; fclose(fp); + merge_depth--; } /* If the "for_xfer" flag is set, the prefix is made compatible with the diff -Nru rsync-3.4.1+ds1/fileio.c rsync-3.5.0+ds1/fileio.c --- rsync-3.4.1+ds1/fileio.c 2023-04-29 16:01:43.000000000 +0000 +++ rsync-3.5.0+ds1/fileio.c 2026-08-01 10:02:02.000000000 +0000 @@ -75,11 +75,60 @@ /* Note that the offset is just the caller letting us know where * 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). */ +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) { + sparse_seek = 0; + return -1; + } + } else if (do_punch_hole(f, sparse_past_write, sparse_seek) < 0) { + sparse_seek = 0; + return -1; + } + sparse_seek = 0; + return 0; +} + +static int full_sparse_write(int f, const char *buf, int len) +{ + while (len > 0) { + int ret = write(f, buf, len); + if (ret <= 0) { + if (ret < 0 && errno == EINTR) + continue; + sparse_seek = 0; + return -1; + } + buf += ret; + len -= ret; + } + return 0; +} + +/* Emit one span of data that is not being turned into a hole. For an in-place + * update (use_seek) the bytes on disk already match, so we only need to move + * past them; otherwise we write them out. Either way a deferred hole is + * flushed first so that the span lands at the right offset. */ +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 full_sparse_write(f, buf, len); +} + static int write_sparse(int f, int use_seek, OFF_T offset, const char *buf, int len) { - int l1 = 0, l2 = 0; - int ret; + int l1, l2, i, start, end; + /* Always treat a leading and trailing run of zeros as a (deferred) + * hole, since they may merge with holes in the adjacent write calls. */ for (l1 = 0; l1 < len && buf[l1] == 0; l1++) {} for (l2 = 0; l2 < len-l1 && buf[len-(l2+1)] == 0; l2++) {} @@ -88,36 +137,45 @@ if (l1 == len) return len; - if (sparse_seek) { - if (sparse_past_write >= preallocated_len) { - if (do_lseek(f, sparse_seek, SEEK_CUR) < 0) + /* Scan the middle [l1, len-l2) for interior runs of zeros that are at + * least SPARSE_WRITE_SIZE long (the hole granularity rsync has always + * used) and defer those as holes. Everything in between -- which may + * include shorter zero runs not worth a hole -- is emitted in one go, + * rather than being chopped into SPARSE_WRITE_SIZE-byte pieces, which + * made copying a large non-sparse file cost ~one write() per KiB. + * + * The matched (use_seek) case runs through the same scan: its interior + * zero runs still have to be punched out, which is what --inplace + * --sparse relies on to keep a hole-y basis file sparse. */ + start = l1; + end = len - l2; + for (i = l1; i < end; ) { + int z; + if (buf[i] != 0) { + i++; + continue; + } + for (z = 1; i + z < end && buf[i+z] == 0; z++) {} + if (z < SPARSE_WRITE_SIZE) { + i += z; + continue; + } + if (i > start) { + if (emit_sparse_span(f, use_seek, buf + start, i - start) < 0) return -1; - } else if (do_punch_hole(f, sparse_past_write, sparse_seek) < 0) { - sparse_seek = 0; - return -1; + sparse_past_write = offset + i; } + sparse_seek += z; + i += z; + start = i; } - sparse_seek = l2; - sparse_past_write = offset + len - l2; - - if (use_seek) { - /* The in-place data already matches. */ - if (do_lseek(f, len - (l1+l2), SEEK_CUR) < 0) + if (end > start) { + if (emit_sparse_span(f, use_seek, buf + start, end - start) < 0) return -1; - return len; - } - - while ((ret = write(f, buf + l1, len - (l1+l2))) <= 0) { - if (ret < 0 && errno == EINTR) - continue; - sparse_seek = 0; - return ret; } - if (ret != (int)(len - (l1+l2))) { - sparse_seek = 0; - return l1+ret; - } + sparse_seek = l2; + sparse_past_write = offset + len - l2; return len; } @@ -153,8 +211,10 @@ while (len > 0) { int r1; if (sparse_files > 0) { - int len1 = MIN(len, SPARSE_WRITE_SIZE); - r1 = write_sparse(f, use_seek, offset, buf, len1); + /* write_sparse() handles the whole span itself, scanning + * for holes and coalescing the non-zero data into large + * write()s instead of SPARSE_WRITE_SIZE-byte dribbles. */ + r1 = write_sparse(f, use_seek, offset, buf, len); offset += r1; } else { if (!wf_writeBuf) { diff -Nru rsync-3.4.1+ds1/flist.c rsync-3.5.0+ds1/flist.c --- rsync-3.4.1+ds1/flist.c 2025-01-14 18:30:32.000000000 +0000 +++ rsync-3.5.0+ds1/flist.c 2026-07-24 05:14:37.000000000 +0000 @@ -29,6 +29,11 @@ extern int am_root; extern int am_server; extern int am_daemon; +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; @@ -64,6 +69,7 @@ extern int prune_empty_dirs; extern int copy_links; extern int copy_unsafe_links; +extern int insecure_links; extern int protocol_version; extern int sanitize_paths; extern int munge_symlinks; @@ -132,6 +138,18 @@ #endif static char tmp_sum[MAX_DIGEST_LEN]; +#ifdef ST_MTIME_NSEC +/* Return st_mtim nsec if it is in the wire-valid range, else 0. */ +static inline uint32 wire_mtime_nsec_from_stat(const STRUCT_STAT *stp) +{ + unsigned long nsec = (unsigned long)stp->ST_MTIME_NSEC; + + if (nsec > MAX_WIRE_NSEC) + return 0; + return (uint32)nsec; +} +#endif + static char empty_sum[MAX_DIGEST_LEN]; static int flist_count_offset; /* for --delete --progress */ static int show_filelist_progress; @@ -202,13 +220,47 @@ * * The stat structure pointed to by stp will contain information about the * link or the referent as appropriate, if they exist. */ +/* Set by send_directory() to the fd of the directory it is currently scanning + * (and that dir's path prefix), so the per-entry stat can go through the + * already-open dir fd instead of re-resolving the full path for every entry. + * Pure performance and sender-side only -- the scanned dir is already open, so + * fstatat(scan_dirfd, basename) is identical to lstat(scandir/basename); no + * confinement is implied or needed. */ +static int scan_dirfd = -1; +static const char *scan_dir_prefix; +static int scan_dir_prefix_len; + +static int scan_link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks) +{ + /* Use the held scan fd only for a single component directly inside the + * scanned dir, and only when am_root >= 0 (link_stat_at folds in no + * fake-super %stat xattr; link_stat does so via get_stat_xattr, a no-op + * once am_root >= 0). */ + if (scan_dirfd >= 0 && am_root >= 0 + && strncmp(path, scan_dir_prefix, scan_dir_prefix_len) == 0 + && path[scan_dir_prefix_len] == '/' + && strchr(path + scan_dir_prefix_len + 1, '/') == NULL) + return link_stat_at(scan_dirfd, path + scan_dir_prefix_len + 1, stp, follow_dirlinks); + return link_stat(path, stp, follow_dirlinks); +} + +static int scan_readlink(const char *path, char *linkbuf, size_t bufsiz) +{ + if (scan_dirfd >= 0 && am_root >= 0 + && 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); +} + static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf) { #ifdef SUPPORT_LINKS - if (link_stat(path, stp, copy_dirlinks) < 0) + if (scan_link_stat(path, stp, copy_dirlinks) < 0) return -1; if (S_ISLNK(stp->st_mode)) { - int llen = do_readlink(path, linkbuf, MAXPATHLEN - 1); + int llen = scan_readlink(path, linkbuf, MAXPATHLEN - 1); if (llen < 0) return -1; linkbuf[llen] = '\0'; @@ -249,6 +301,30 @@ #endif } +/* Held-dirfd variant of link_stat(): stat single-component `name` relative to + * directory fd `dfd`, instead of re-resolving a full path. Equivalent to + * link_stat() only when NOT in --fake-super mode -- x_stat/x_lstat fold the + * fake-super %stat xattr into the result via get_stat_xattr(), which is a + * path-based no-op once am_root >= 0. Callers therefore use this only when + * am_root >= 0 (and a valid dfd), falling back to link_stat() otherwise. */ +int link_stat_at(int dfd, const char *name, STRUCT_STAT *stp, int follow_dirlinks) +{ +#ifdef SUPPORT_LINKS + if (copy_links) + return do_stat_atfd(dfd, name, stp); + if (do_lstat_atfd(dfd, name, stp) < 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)) + *stp = st; + } + return 0; +#else + return do_stat_atfd(dfd, name, stp); +#endif +} + static inline int path_is_daemon_excluded(char *path, int ignore_filename) { if (daemon_filter_list.head) { @@ -291,17 +367,31 @@ { struct file_struct **new_ptr; + /* Refuse BEFORE any int arithmetic below can overflow: used+extra (computed + * in the early-return and the cap below) and the malloced growth math. Only + * reachable past INT_MAX entries (my_alloc's --max-alloc cap normally stops + * the list growing anywhere near there). */ + if (extra < 0 || flist->used < 0 || flist->used > INT_MAX - extra) + goto too_large; + if (flist->used + extra <= flist->malloced) return; if (flist->malloced < FLIST_START) flist->malloced = FLIST_START; - else if (flist->malloced >= FLIST_LINEAR) + else if (flist->malloced >= FLIST_LINEAR) { + if (flist->malloced > INT_MAX - FLIST_LINEAR) + goto too_large; flist->malloced += FLIST_LINEAR; - else if (flist->malloced < FLIST_START_LARGE/16) + } else if (flist->malloced < FLIST_START_LARGE/16) { + if (flist->malloced > INT_MAX/4) + goto too_large; flist->malloced *= 4; - else + } else { + if (flist->malloced > INT_MAX/2) + goto too_large; flist->malloced *= 2; + } /* In case count jumped or we are starting the list * with a known size just set it. */ @@ -318,6 +408,11 @@ } flist->files = new_ptr; + return; + + too_large: + rprintf(FERROR, "[%s] file list has grown too large to expand\n", who_am_i()); + exit_cleanup(RERR_MALLOC); } static void flist_done_allocating(struct file_list *flist) @@ -764,7 +859,7 @@ if ((basename = strrchr(thisname, '/')) != NULL) { int len = basename++ - thisname; - if (len != lastdir_len || memcmp(thisname, lastdir, len) != 0) { + if (len != lastdir_len || !lastdir || memcmp(thisname, lastdir, len) != 0) { lastdir = new_array(char, len + 1); memcpy(lastdir, thisname, len); lastdir[len] = '\0'; @@ -813,9 +908,17 @@ rdev_major = DEV_MAJOR(devp); rdev = MAKEDEV(rdev_major, DEV_MINOR(devp)); extra_len += DEV_EXTRA_CNT * EXTRA_LEN; + } else if (IS_DEVICE(mode)) { + /* Abbrev-branch counterpart to the !preserve_devices + * stub-alloc below: zeroed F_RDEV_P slots. */ + extra_len += DEV_EXTRA_CNT * EXTRA_LEN; } if (preserve_links && S_ISLNK(mode)) linkname_len = strlen(F_SYMLINK(first)) + 1; + else if (S_ISLNK(mode)) + /* Abbrev-branch counterpart to the !preserve_links + * stub-alloc below: empty linkname. */ + linkname_len = 1; else linkname_len = 0; real_ISREG_entry = S_ISREG(mode) ? 1 : 0; @@ -840,9 +943,9 @@ } if (xflags & XMIT_MOD_NSEC) #ifndef CAN_SET_NSEC - (void)read_varint(f); + (void)read_varint_bounded(f, 0, MAX_WIRE_NSEC, "modtime_nsec"); #else - modtime_nsec = read_varint(f); + modtime_nsec = read_varint_bounded(f, 0, MAX_WIRE_NSEC, "modtime_nsec"); else modtime_nsec = 0; #endif @@ -861,8 +964,24 @@ #endif } #endif - if (!(xflags & XMIT_SAME_MODE)) + if (!(xflags & XMIT_SAME_MODE)) { mode = from_wire_mode(read_int(f)); + /* Reject modes whose type bits are not one of the standard + * file types; otherwise garbage mode values propagate through + * the file-type checks below unpredictably. mode 0 is the one + * legitimate exception: --delete-missing-args (missing_args==2) + * sends a missing arg as a mode-0 entry (IS_MISSING_FILE), the + * generator's delete signal (#910). */ + if (mode != 0 || missing_args != 2) { + if (!S_ISREG(mode) && !S_ISDIR(mode) && !S_ISLNK(mode) + && !S_ISCHR(mode) && !S_ISBLK(mode) + && !S_ISFIFO(mode) && !S_ISSOCK(mode)) { + rprintf(FERROR, "invalid file mode 0%o for %s [%s]\n", + (unsigned)mode, lastname, who_am_i()); + exit_cleanup(RERR_PROTOCOL); + } + } + } if (atimes_ndx && !S_ISDIR(mode) && !(xflags & XMIT_SAME_ATIME)) { atime = read_varlong(f, 4); #if SIZEOF_TIME_T < SIZEOF_INT64 @@ -921,6 +1040,15 @@ if (IS_DEVICE(mode)) extra_len += DEV_EXTRA_CNT * EXTRA_LEN; file_length = 0; + } else if (IS_DEVICE(mode)) { + /* Peer/batch sent an S_IFCHR/S_IFBLK entry but we are not + * preserving devices. A cooperating sender wouldn't do this; + * a crafted batch can. Allocate (and zero, via the memset + * below) the DEV_EXTRA_CNT slots so F_RDEV_P() callers + * (set_stat_xattr under --fake-super, generator IS_DEVICE + * paths) read {0,0} instead of the previous pool slot. */ + extra_len += DEV_EXTRA_CNT * EXTRA_LEN; + file_length = 0; } else if (protocol_version < 28) rdev = MAKEDEV(0, 0); @@ -941,6 +1069,14 @@ #endif if (munge_symlinks) linkname_len += SYMLINK_PREFIX_LEN; + } else if (S_ISLNK(mode)) { + /* Peer/batch sent an S_IFLNK entry but we are not preserving + * links (no -l, and the batch stream-flags didn't set it). A + * cooperating sender wouldn't do this; a crafted batch can. + * Allocate one byte for an empty linkname so F_SYMLINK() + * callers (log.c %L, generator.c) read a valid "" instead of + * the next pool slot's redzone. */ + linkname_len = 1; } else #endif @@ -988,6 +1124,15 @@ exit_cleanup(RERR_UNSUPPORTED); } + /* "." is the synthetic transfer root. Reinterpreting it as a file lets + * --force recursively remove the real destination directory before the + * receiver creates that file. */ + if ((!strcmp(thisname, ".") || !strcmp(thisname, "/.")) && !S_ISDIR(mode)) { + rprintf(FERROR, "ERROR: rejecting non-directory transfer-root entry: %s\n", + thisname); + exit_cleanup(RERR_PROTOCOL); + } + if (*thisname == '/' ? thisname[1] != '.' || thisname[2] != '\0' : *thisname != '.' || thisname[1] != '\0') { int filt_flags = S_ISDIR(mode) ? NAME_IS_DIR : NAME_IS_FILE; if (!trust_sender_filter /* a per-dir filter rule means we must trust the sender's filtering */ @@ -1027,7 +1172,8 @@ memcpy(bp, basename, basename_len); #ifdef SUPPORT_HARD_LINKS - if (xflags & XMIT_HLINKED + if (preserve_hard_links && xflags & XMIT_HLINKED + && !S_ISDIR(mode) #ifndef CAN_HARDLINK_SYMLINK && !S_ISLNK(mode) #endif @@ -1082,6 +1228,26 @@ if (basename_len == 1+1 && *basename == '.') /* +1 for '\0' */ F_DEPTH(file)--; if (protocol_version >= 30) { + /* Stop a malicious sender expanding --delete scope by flagging + * an implied parent as a content dir: if we only allowed this + * entry as a parent of the requested leaf, force the flags back + * to the honest implied-parent encoding (XMIT_TOP_DIR | + * XMIT_NO_CONTENT_DIR) so it lands in FLAG_IMPLIED_DIR, not + * FLAG_CONTENT_DIR, and delete_in_dir() can't sweep siblings. + * Not gated on trust_sender_filter: implied_filter_list is + * receiver-owned state, so a per-dir filter must not be able to + * downgrade this defense. */ + if (implied_filter_list.head + && is_implied_parent_dir(thisname) + && (!(xflags & XMIT_NO_CONTENT_DIR) || !(xflags & XMIT_TOP_DIR))) { + if (DEBUG_GTE(FILTER, 1)) { + rprintf(FINFO, + "[%s] receiver downgraded implied-parent dir %s " + "to non-content (sender xflags=0x%x)\n", + who_am_i(), thisname, xflags); + } + xflags |= XMIT_NO_CONTENT_DIR | XMIT_TOP_DIR; + } if (!(xflags & XMIT_NO_CONTENT_DIR)) { if (xflags & XMIT_TOP_DIR) file->flags |= FLAG_TOP_DIR; @@ -1089,13 +1255,17 @@ } else if (xflags & XMIT_TOP_DIR) file->flags |= FLAG_IMPLIED_DIR; } else if (xflags & XMIT_TOP_DIR) { - in_del_hier = recurse; - del_hier_name_len = F_DEPTH(file) == 0 ? 0 : l1 + l2; - if (relative_paths && del_hier_name_len > 2 - && lastname[del_hier_name_len-1] == '.' - && lastname[del_hier_name_len-2] == '/') - del_hier_name_len -= 2; - file->flags |= FLAG_TOP_DIR | FLAG_CONTENT_DIR; + if (implied_filter_list.head && is_implied_parent_dir(thisname)) + file->flags |= FLAG_IMPLIED_DIR; + else { + in_del_hier = recurse; + del_hier_name_len = F_DEPTH(file) == 0 ? 0 : l1 + l2; + if (relative_paths && del_hier_name_len > 2 + && lastname[del_hier_name_len-1] == '.' + && lastname[del_hier_name_len-2] == '/') + del_hier_name_len -= 2; + file->flags |= FLAG_TOP_DIR | FLAG_CONTENT_DIR; + } } else if (in_del_hier) { if (!relative_paths || !del_hier_name_len || (l1 >= del_hier_name_len @@ -1115,7 +1285,11 @@ #ifdef SUPPORT_LINKS if (linkname_len) { bp += basename_len; - if (first_hlink_ndx >= flist->ndx_start) { + if (!preserve_links) { + /* The empty-linkname case allocated above; nothing on + * the wire to read. Just terminate it. */ + *bp = '\0'; + } else if (first_hlink_ndx >= flist->ndx_start) { struct file_struct *first = flist->files[first_hlink_ndx - flist->ndx_start]; memcpy(bp, F_SYMLINK(first), linkname_len); } else { @@ -1239,7 +1413,7 @@ int extra_len = file_extra_cnt * EXTRA_LEN; const char *basename; alloc_pool_t *pool; - STRUCT_STAT st; + STRUCT_STAT st = {0}; char *bp; if (strlcpy(thisname, fname, sizeof thisname) >= sizeof thisname) { @@ -1372,7 +1546,7 @@ if ((basename = strrchr(thisname, '/')) != NULL) { int len = basename++ - thisname; - if (len != lastdir_len || memcmp(thisname, lastdir, len) != 0) { + if (len != lastdir_len || !lastdir || memcmp(thisname, lastdir, len) != 0) { lastdir = new_array(char, len + 1); memcpy(lastdir, thisname, len); lastdir[len] = '\0'; @@ -1401,8 +1575,12 @@ } #ifdef ST_MTIME_NSEC - if (st.ST_MTIME_NSEC && protocol_version >= 31) - extra_len += EXTRA_LEN; + { + uint32 nsec = wire_mtime_nsec_from_stat(&st); + + if (nsec && protocol_version >= 31) + extra_len += EXTRA_LEN; + } #endif #if SIZEOF_CAPITAL_OFF_T >= 8 if (st.st_size > 0xFFFFFFFFu && S_ISREG(st.st_mode)) @@ -1415,6 +1593,18 @@ extra_len += SUM_EXTRA_CNT * EXTRA_LEN; } +#ifdef HAVE_STRUCT_STAT_ST_RDEV + /* The sender path historically passes rdev via the tmp_rdev static + * (read by send_file_entry()), so make_file() never reserved + * DEV_EXTRA_CNT in the file_struct itself. But receiver-side callers + * (recv_generator's --inplace --backup back_file, backup.c make_backup) + * hand this struct to set_file_attrs() -> set_stat_xattr(), which reads + * F_RDEV_P(file) under --fake-super. Reserve and populate the slots + * so the struct is self-contained, matching recv_file_entry(). */ + if (IS_DEVICE(st.st_mode)) + extra_len += DEV_EXTRA_CNT * EXTRA_LEN; +#endif + #if EXTRA_ROUNDING > 0 if (extra_len & (EXTRA_ROUNDING * EXTRA_LEN)) extra_len = (extra_len | (EXTRA_ROUNDING * EXTRA_LEN)) + EXTRA_LEN; @@ -1448,7 +1638,10 @@ #ifdef HAVE_STRUCT_STAT_ST_RDEV if (IS_DEVICE(st.st_mode)) { + uint32 *devp = F_RDEV_P(file); tmp_rdev = st.st_rdev; + DEV_MAJOR(devp) = major(st.st_rdev); + DEV_MINOR(devp) = minor(st.st_rdev); st.st_size = 0; } else if (IS_SPECIAL(st.st_mode)) st.st_size = 0; @@ -1457,9 +1650,13 @@ file->flags = flags; file->modtime = st.st_mtime; #ifdef ST_MTIME_NSEC - if (st.ST_MTIME_NSEC && protocol_version >= 31) { - file->flags |= FLAG_MOD_NSEC; - F_MOD_NSEC(file) = st.ST_MTIME_NSEC; + { + uint32 nsec = wire_mtime_nsec_from_stat(&st); + + if (nsec && protocol_version >= 31) { + file->flags |= FLAG_MOD_NSEC; + F_MOD_NSEC(file) = nsec; + } } #endif file->len32 = (uint32)st.st_size; @@ -1629,6 +1826,7 @@ sx.st.st_mode = file->mode; if (get_acl(fname, &sx) < 0) { io_error |= IOERR_GENERAL; + free_acl(&sx); return NULL; } } @@ -1636,8 +1834,11 @@ #ifdef SUPPORT_XATTRS if (preserve_xattrs) { sx.st.st_mode = file->mode; - if (get_xattr(fname, &sx) < 0) { + if (get_xattr(fname, -1, &sx) < 0) { io_error |= IOERR_GENERAL; +#ifdef SUPPORT_ACLS + free_acl(&sx); /* get_acl() above may have loaded one */ +#endif return NULL; } } @@ -1812,6 +2013,77 @@ } } +#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 + * 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 + * 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(). */ +static DIR *secure_opendir(const char *fbuf) +{ + int dfd, fl; + DIR *d; + + 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] == '/')) { + /* 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 + * scan target beneath it. This re-follows the same in-module path -- so a + * 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 + * 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; + char modrel[MAXPATHLEN]; + while (*p == '/') + p++; + if ((size_t)snprintf(modrel, sizeof modrel, "%s%s%s", + p, *p ? "/" : "", fbuf) >= sizeof modrel) { + errno = ENAMETOOLONG; + return NULL; + } + dfd = secure_relative_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 + * "/" transfer root): anchor at "/" -- operator-named, trusted. */ + const char *relp = fbuf; + while (*relp == '/') + relp++; + dfd = secure_relative_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); + } + + if (dfd < 0) + return NULL; + if ((fl = fcntl(dfd, F_GETFD)) >= 0) + fcntl(dfd, F_SETFD, fl | FD_CLOEXEC); + if (!(d = fdopendir(dfd))) { + int save = errno; + close(dfd); + errno = save; + } + return d; +} +#endif + /* This function is normally called by the sender, but the receiving side also * calls it from get_dirlist() with f set to -1 so that we just construct the * file list in memory without sending it over the wire. Also, get_dirlist() @@ -1830,7 +2102,31 @@ assert(flist != NULL); - if (!(d = opendir(fbuf))) { +#if defined HAVE_FDOPENDIR && defined HAVE_DIRFD + /* 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 + * 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; + * - a non-daemon sender is confined in the default no-follow mode; its + * symlink-following modes intentionally dereference out of the + * operator's own tree, so they keep the legacy opendir(). + * f >= 0 is the sender's outgoing scan; get_dirlist() passes f < 0 and keeps + * the legacy opendir(). A module opted out of confinement ("insecure links = + * 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() + || !(copy_links || copy_unsafe_links || copy_dirlinks || insecure_links))) + d = secure_opendir(fbuf); + else + d = opendir(fbuf); +#else + d = opendir(fbuf); +#endif + if (!d) { if (errno == ENOENT) { if (am_sender) /* Can abuse this for vanished error w/ENOENT: */ interpret_stat_error(fbuf, True); @@ -1853,6 +2149,14 @@ } else remainder = 0; +#ifdef HAVE_DIRFD + /* Let the per-entry stat (readlink_stat -> scan_link_stat) go through the + * already-open directory fd instead of re-resolving fbuf for each name. */ + scan_dirfd = dirfd(d); + scan_dir_prefix = fbuf; + scan_dir_prefix_len = len; +#endif + for (errno = 0, di = readdir(d); di; errno = 0, di = readdir(d)) { unsigned name_len; char *dname = d_name(di); @@ -1881,6 +2185,9 @@ send_file_name(f, flist, fbuf, NULL, flags, filter_level); } + scan_dirfd = -1; /* fbuf is about to be reused / d closed */ + scan_dir_prefix = NULL; /* and don't leave the global pointing into fbuf */ + scan_dir_prefix_len = 0; fbuf[len] = '\0'; if (errno) { @@ -2014,7 +2321,8 @@ int len, dlen, flags = FLAG_DIVERT_DIRS | FLAG_CONTENT_DIR; size_t j; - f_name(file, fbuf); + if (!f_name(file, fbuf)) + return; dlen = strlen(fbuf); if (!change_pathname(file, NULL, 0)) @@ -2059,10 +2367,9 @@ } if (name_type != NORMAL_NAME) { - STRUCT_STAT st; - if (name_type == MISSING_NAME) - memset(&st, 0, sizeof st); - else if (link_stat(fbuf, &st, 1) != 0) { + STRUCT_STAT st = {0}; + + if (name_type != MISSING_NAME && link_stat(fbuf, &st, 1) != 0) { interpret_stat_error(fbuf, True); continue; } @@ -2194,7 +2501,7 @@ static const char *lastdir; static int lastdir_len = -1; int len, dirlen; - STRUCT_STAT st; + STRUCT_STAT st = {0}; char *p, *dir; struct file_list *flist; struct timeval start_tv, end_tv; @@ -2585,11 +2892,33 @@ #endif if (inc_recurse && dir_ndx >= 0) { + if (!first_flist) { + /* All flists have already been freed via the NDX_DONE + * chain, so dir_flist is stale: its files[] entries + * point into a destroyed pool. A sub-flist marker now + * is a protocol violation (and would otherwise UAF the + * stale dir entry below, then deref an uninitialised + * slot in the freshly reset dir_flist further down). */ + rprintf(FERROR_XFER, + "rsync: refusing sub-flist after final flist was freed\n"); + exit_cleanup(RERR_PROTOCOL); + } if (dir_ndx >= dir_flist->used) { rprintf(FERROR_XFER, "rsync: refusing invalid dir_ndx %u >= %u\n", dir_ndx, dir_flist->used); exit_cleanup(RERR_PROTOCOL); } struct file_struct *file = dir_flist->files[dir_ndx]; + if (!F_IS_ACTIVE(file)) { + /* flist_sort_and_clean() can clear_file() a directory + * entry that was a duplicate or otherwise pruned, but + * the cleared file_struct stays in dir_flist. A peer + * that then sends a sub-flist for that slot would make + * f_name() return NULL into the dirname strcmp() below. */ + rprintf(FERROR_XFER, + "rsync: refusing flist for cleared dir_ndx %d\n", + dir_ndx); + exit_cleanup(RERR_PROTOCOL); + } if (file->flags & FLAG_GOT_DIR_FLIST) { rprintf(FERROR_XFER, "rsync: refusing malicious duplicate flist for dir %d\n", dir_ndx); exit_cleanup(RERR_PROTOCOL); @@ -2618,7 +2947,7 @@ if ((flags = read_varint(f)) == 0) { int err = read_varint(f); if (!ignore_errors) - io_error |= err; + io_error |= err & IOERR_VALID_MASK; break; } } else { @@ -2636,7 +2965,7 @@ } err = read_varint(f); if (!ignore_errors) - io_error |= err; + io_error |= err & IOERR_VALID_MASK; break; } } @@ -2651,7 +2980,7 @@ cur_dir++; if (cur_dir != good_dirname) { const char *d = dir_ndx >= 0 ? f_name(dir_flist->files[dir_ndx], NULL) : empty_dir; - if (strcmp(cur_dir, d) != 0) { + if (!d || strcmp(cur_dir, d) != 0) { rprintf(FERROR, "ABORTING due to invalid path from sender: %s/%s\n", cur_dir, file->basename); @@ -2739,9 +3068,17 @@ /* Recv the io_error flag */ int err = read_int(f); if (!ignore_errors) - io_error |= err; + io_error |= err & IOERR_VALID_MASK; } else if (inc_recurse && flist->ndx_start == 1) { - if (!file_total || strcmp(flist->sorted[flist->low]->basename, ".") != 0) + /* The first inc_recurse flist has no parent in dir_flist; a + * malicious peer can send a "." entry whose mode is not a + * directory, so it never lands in dir_flist (used stays 0) yet + * the basename test below still passes. That left parent_ndx at + * its default 0 and the consumers dereferenced dir_flist->files[0] + * = uninitialised heap. Require dir_flist to actually hold an + * entry before trusting index 0. */ + if (!file_total || !dir_flist->used + || strcmp(flist->sorted[flist->low]->basename, ".") != 0) flist->parent_ndx = -1; } @@ -3167,8 +3504,8 @@ } else *uidbuf = '\0'; if (gid_ndx) { - static char parens[] = "(\0)\0\0\0"; - char *pp = parens + (file->flags & FLAG_SKIP_GROUP ? 0 : 3); + static const char parens[] = "(\0)\0\0\0"; + const char *pp = parens + (file->flags & FLAG_SKIP_GROUP ? 0 : 3); snprintf(gidbuf, sizeof gidbuf, " gid=%s%u%s", pp, F_GROUP(file), pp + 2); } else diff -Nru rsync-3.4.1+ds1/generator.c rsync-3.5.0+ds1/generator.c --- rsync-3.4.1+ds1/generator.c 2025-01-15 19:27:26.000000000 +0000 +++ rsync-3.5.0+ds1/generator.c 2026-08-02 21:10:00.000000000 +0000 @@ -29,6 +29,7 @@ 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; @@ -41,6 +42,7 @@ extern int preserve_links; extern int preserve_devices; extern int preserve_specials; +extern int drop_devices; extern int preserve_hard_links; extern int preserve_executability; extern int preserve_perms; @@ -66,6 +68,7 @@ extern int append_mode; extern int make_backups; extern int csum_length; +extern int xfer_sum_len; extern int ignore_times; extern int size_only; extern OFF_T max_size; @@ -229,12 +232,18 @@ *flags_p = 0; if (sscanf(bp, "%x ", &mode) != 1) { - invalid_data: - rprintf(FERROR, "ERROR: invalid data in delete-delay file.\n"); - return -1; + goto invalid_data; } - past_space = strchr(bp, ' ') + 1; - len = j - read_pos - (past_space - bp) + 1; /* count the '\0' */ + past_space = strchr(bp, ' '); + if (!past_space) { + goto invalid_data; + } + past_space++; + /* Name length + NUL. Computed from past_space directly: the old + * `j - read_pos - (past_space - bp)` form was off by +1 when a '!' + * prefix had advanced bp past read_pos, over-reading deldelay_buf + * by one byte on a buffer-filling final entry. */ + len = (deldelay_buf + j) - past_space + 1; read_pos = j + 1; if (len > MAXPATHLEN) { @@ -247,6 +256,10 @@ memcpy(buf, past_space, len); return mode; + +invalid_data: + rprintf(FERROR, "ERROR: invalid data in delete-delay file.\n"); + return -1; } static void do_delayed_deletions(char *delbuf) @@ -449,7 +462,7 @@ { if (preserve_xattrs) { if (!XATTR_READY(*sxp)) - get_xattr(fname, sxp); + get_xattr(fname, -1, sxp); if (xattr_diff(file, sxp, 0)) return 1; } @@ -558,7 +571,7 @@ #ifdef SUPPORT_XATTRS if (preserve_xattrs) { if (!XATTR_READY(*sxp)) - get_xattr(fnamecmp, sxp); + get_xattr(fnamecmp, -1, sxp); if (xattr_diff(file, sxp, 1)) iflags |= ITEM_REPORT_XATTR; } @@ -691,6 +704,11 @@ { int32 blength; int s2length; + /* The strong sum can be no longer than the negotiated checksum digest: + * a short checksum (e.g. xxh64 = 8 bytes, when xxh128/xxh3 are absent) + * makes xfer_sum_len < SUM_LENGTH, and the sender rejects an s2length + * larger than xfer_sum_len (io.c). */ + int max_s2length = MIN(SUM_LENGTH, xfer_sum_len); int64 l; if (len < 0) { @@ -706,8 +724,7 @@ else { int32 max_blength = protocol_version < 30 ? OLD_MAX_BLOCK_SIZE : MAX_BLOCK_SIZE; int32 c; - int cnt; - for (c = 1, l = len, cnt = 0; l >>= 2; c <<= 1, cnt++) {} + for (c = 1, l = len; l >>= 2; c <<= 1) {} if (c < 0 || c >= max_blength) blength = max_blength; else { @@ -725,7 +742,7 @@ if (protocol_version < 27) { s2length = csum_length; } else if (csum_length == SUM_LENGTH) { - s2length = SUM_LENGTH; + s2length = max_s2length; } else { int32 c; int b = BLOCKSUM_BIAS; @@ -734,7 +751,7 @@ /* add a bit, subtract rollsum, round up. */ s2length = (b + 1 - 32 + 7) / 8; /* --optimize in compiler-- */ s2length = MAX(s2length, csum_length); - s2length = MIN(s2length, SUM_LENGTH); + s2length = MIN(s2length, max_s2length); } sum->flength = len; @@ -920,8 +937,10 @@ rsyserr(FINFO, errno, "copy_file %s => %s", full_fname(src), copy_to); } - /* Try to clean up. */ - unlink(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); cleanup_disable(); return -1; } @@ -933,6 +952,118 @@ return ok ? 0 : -1; } +/* 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 + * 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- + * supplied path. For a non-daemon receiver, resolve it with the ownership + * walk: a symlink component owned by uid 0 or the euid (the operator's own + * basis dir, e.g. --link-dest=/var/backups) is followed, a foreign-owned one + * is refused -- absolute and relative alike. Refusing it makes the basis + * look absent, so the file transfers normally instead of being read/linked/ + * skipped through an attacker's symlink. --insecure-links restores legacy + * following; a daemon keeps its stronger confinement (chroot / secure + * 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()) { + const char *leaf; + int dfd = owner_walk_parent(path, &leaf); + int r, e; + if (dfd < 0) + return -1; + r = link_stat_at(dfd, leaf, stp, 0); + e = errno; + close(dfd); + errno = e; + 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 + * 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 + * oracle). "insecure links = yes" falls through to the legacy link_stat() + * below, restoring 3.2.7 following. Only an ABSOLUTE basis (rooted under the + * module by check_alt_basis_dirs, so it can reach an in-module symlink) is + * confined here; a RELATIVE basis (--link-dest=../01) is a dest-relative + * sibling whose "../" is already clamped to the module root by sanitize_path, + * 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()) { + const char *leaf; + int dfd, e, save = operator_path_resolve; + operator_path_resolve = 1; + dfd = owner_walk_parent(path, &leaf); + operator_path_resolve = save; + if (dfd < 0) + return -1; + if (am_root >= 0) { + int r = do_lstat_atfd(dfd, leaf, stp); + e = errno; + close(dfd); + errno = e; + return r; + } +#ifdef SUPPORT_XATTRS + { + /* --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); + 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 (get_stat_xattr(NULL, lfd, stp, &xst) == 0) + *stp = xst; + close(lfd); + return 0; + } +#else + { + int r = do_lstat_atfd(dfd, leaf, stp); + e = errno; + close(dfd); + errno = e; + return r; + } +#endif + } +#endif + if (am_daemon && am_chrooted && module_dirlen && path[0] != '/' && !symlink_optout_allowed()) { + const char *slash = strrchr(path, '/'); + if (slash) { + char dir[MAXPATHLEN]; + size_t dlen = (size_t)(slash - path); + int dfd, r, e; + 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) + return -1; + r = link_stat_at(dfd, slash + 1, stp, 0); + e = errno; + close(dfd); + errno = e; + return r; + } + } + return link_stat(path, stp, 0); +} + /* This is only called for regular files. We return -2 if we've finished * handling the file, -1 if no dest-linking occurred, or a non-negative * value if we found an alternate basis file. If we're called with the @@ -950,7 +1081,7 @@ do { pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname); - if (link_stat(cmpbuf, &sxp->st, 0) < 0 || !S_ISREG(sxp->st.st_mode)) + if (basis_link_stat(cmpbuf, &sxp->st) < 0 || !S_ISREG(sxp->st.st_mode)) continue; if (match_level == 0) { best_match = j; @@ -976,7 +1107,7 @@ if (j != best_match) { j = best_match; pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname); - if (link_stat(cmpbuf, &sxp->st, 0) < 0) + if (basis_link_stat(cmpbuf, &sxp->st) < 0) goto got_nothing_for_ya; } @@ -984,12 +1115,25 @@ 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(fname) < 0 && errno != ENOENT) + if (do_unlink_at(fname) < 0 && errno != ENOENT) goto got_nothing_for_ya; } #ifdef SUPPORT_HARD_LINKS if (alt_dest_type == LINK_DEST) { - if (!hard_link_one(file, fname, cmpbuf, 1)) + /* For a NON-daemon receiver the basis dir is an operator path: + * 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 + * 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; + if (!hlok) goto try_a_copy; if (atimes_ndx) set_file_attrs(fname, file, sxp, NULL, 0); @@ -1018,6 +1162,13 @@ #ifdef SUPPORT_HARD_LINKS try_a_copy: /* Copy the file locally. */ #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 + * 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) { if (find_exact_for_existing) /* Can get here via hard-link failure */ goto got_nothing_for_ya; @@ -1047,8 +1198,10 @@ } /* This is only called for non-regular files. We return -2 if we've finished - * handling the file, or -1 if no dest-linking occurred, or a non-negative - * value if we found an alternate basis file. */ + * handling the file, -3 if we matched one but the destination refused to + * hard-link it (the caller creates it instead, and must not report it again), + * or -1 if no dest-linking occurred, or a non-negative value if we found an + * alternate basis file. */ static int try_dests_non(struct file_struct *file, char *fname, int ndx, char *cmpbuf, stat_x *sxp, int itemizing, enum logcode code) @@ -1071,7 +1224,7 @@ do { pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname); - if (link_stat(cmpbuf, &sxp->st, 0) < 0) + if (basis_link_stat(cmpbuf, &sxp->st) < 0) continue; if (ftype != get_file_type(sxp->st.st_mode)) continue; @@ -1098,11 +1251,12 @@ if (j != best_match) { j = best_match; pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname); - if (link_stat(cmpbuf, &sxp->st, 0) < 0) + if (basis_link_stat(cmpbuf, &sxp->st) < 0) return -1; } if (match_level == 3) { + int cannot_hardlink = 0; #ifdef SUPPORT_HARD_LINKS if (alt_dest_type == LINK_DEST #ifndef CAN_HARDLINK_SYMLINK @@ -1112,13 +1266,30 @@ && !IS_SPECIAL(file->mode) && !IS_DEVICE(file->mode) #endif && !S_ISDIR(file->mode)) { - if (do_link(cmpbuf, fname) < 0) { - rsyserr(FERROR_XFER, errno, - "failed to hard-link %s with %s", - cmpbuf, fname); - return j; - } - if (preserve_hard_links && F_IS_HLINKED(file)) + if (do_link_at(cmpbuf, fname) < 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 + * builds on APFS, backs up to HFS+). A refusal here is + * that same answer arriving late, so fall back to a copy + * as a build without the macro does -- the caller creates + * the entry either way, so failing the transfer only cost + * the exit status. + * + * Every errno, as the regular-file path next door already + * does (try_dests_reg -> hard_link_one -> try_a_copy). + * Picking out the "cannot" errnos is not possible anyway: + * link(2) documents EPERM both for a filesystem with no + * hard-link support and for an ordinary permission + * refusal, and FUSE reports ENOSYS for the same thing. + * + * The rest report themselves: ENOSPC/EDQUOT/EROFS fail the + * copy too, EMLINK and EXDEV mean it was never linkable. + * EIO alone goes unremarked, deliberately -- a diagnostic + * here lands in --link-dest's itemised output. */ + cannot_hardlink = 1; + match_level = 2; + } else if (preserve_hard_links && F_IS_HLINKED(file)) finish_hard_link(file, fname, ndx, NULL, itemizing, code, -1); } else #endif @@ -1134,7 +1305,11 @@ rprintf(FCLIENT, "%s%s is uptodate\n", fname, ftype == FT_DIR ? "/" : ""); } - return -2; + /* -2 tells the caller the entry is already up to date, which for + * --link-dest means "skip it". We could not link it, so say -3 + * instead: no caller claims that, and the fall-through creates the + * entry -- the same place a build without the macro ends up. */ + return cannot_hardlink ? -3 : -2; } return j; @@ -1204,6 +1379,232 @@ * * Note that f_out is set to -1 when doing final directory-permission and * modification-time repair. */ + +/* 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 + * 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, + STRUCT_STAT *stp, int follow_dirlinks) +{ + 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) { + const char *slash = strrchr(fname, '/'); + return link_stat_at(dfd, slash ? slash + 1 : fname, stp, follow_dirlinks); + } + return link_stat(fname, stp, follow_dirlinks); +} + +static int gen_entry_mkdir(char *fname, struct file_struct *file, mode_t mode) +{ + int dfd = held_dfd_for(fname, file); + if (dfd >= 0) { + const char *slash = strrchr(fname, '/'); + return do_mkdir_atfd(dfd, slash ? slash + 1 : fname, mode); + } + return do_mkdir_at(fname, mode); +} + +static int gen_entry_chmod(const char *fname, struct file_struct *file, mode_t mode) +{ + int dfd = held_dfd_for(fname, file); + if (dfd >= 0) { + const char *slash = strrchr(fname, '/'); + return do_chmod_atfd(dfd, slash ? slash + 1 : fname, mode); + } + return do_chmod_at(fname, mode); +} + +static void gen_entry_set_times(const char *fname, struct file_struct *file, STRUCT_STAT *stp) +{ + int dfd = held_dfd_for(fname, file); + if (dfd >= 0) { + const char *slash = strrchr(fname, '/'); + if (set_times_at(dfd, slash ? slash + 1 : fname, stp) != -2) + return; /* handled (success or error) by the at-on-dfd tier */ + } + set_times(fname, stp); +} + +static int gen_entry_symlink(const char *slnk, const char *path, struct file_struct *file) +{ + int dfd = held_dfd_for(path, file); + if (dfd >= 0) { + const char *slash = strrchr(path, '/'); + return do_symlink_atfd(slnk, dfd, slash ? slash + 1 : path); + } + return do_symlink_at(slnk, path); +} + +/* True when this build compiled no fd-relative primitive able to create this + * kind of node. That is a property of the build, not of the call, so it is + * decided here rather than inferred from an errno. */ +static int no_atfd_mknod_primitive(mode_t mode) +{ +#ifndef AT_FDCWD + (void)mode; + return 1; +#else + /* --fake-super creates the placeholder with openat(), which exists + * wherever AT_FDCWD does, so a failure there is a real failure and is + * deliberately NOT retried unconfined -- even though a runtime denial + * (a seccomp policy permitting open() but not openat()) would then fail + * a create that the errno-based test used to let through. */ + if (am_root < 0) + return 0; +# ifdef HAVE_MKNODAT + (void)mode; + return 0; +# elif defined(HAVE_MKFIFOAT) + return !S_ISFIFO(mode); /* FIFOs are covered; device nodes are not */ +# else + (void)mode; + return 1; +# endif +#endif +} + +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) { + const char *slash = strrchr(path, '/'); + int ret = do_mknod_atfd(dfd, slash ? slash + 1 : path, mode, rdev); + /* 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. + * Testing errno == ENOSYS is not that test: a live mknodat() or + * mkfifoat() can return ENOSYS too (an unimplemented FUSE mknod, + * or seccomp), which would drop confinement on a platform that + * does have the secure primitive. */ + if (ret == 0 || !no_atfd_mknod_primitive(mode)) + return ret; + } + return do_mknod_at(path, mode, rdev); +} + +static int gen_entry_unlink(const char *path, struct file_struct *file) +{ + int dfd = held_dfd_for(path, file); + if (dfd >= 0) { + const char *slash = strrchr(path, '/'); + return do_unlink_atfd(dfd, slash ? slash + 1 : path, 0); + } + return do_unlink_at(path); +} + +/* opath and npath are both expected to live in the entry's directory (the + * tmp -> final rename); when both resolve to the held dir fd the rename is a + * 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); + 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 do_rename_at(opath, npath); +} + +#ifdef SUPPORT_XATTRS +/* Copy xattrs from src onto fname through a held, O_NOFOLLOW-opened fd so a + * parent-symlink race can't redirect the setxattr. A hardened receiver always + * uses a confined fd -- via the cached dir fd, or a secure re-pin when that + * misses -- and refuses rather than path-write if it can't pin; only a + * non-hardened receiver falls back to the path-based copy (matches + * 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 xfd = -1, sfd = -1, ret; + if (dfd >= 0) { + const char *slash = strrchr(fname, '/'); + xfd = openat(dfd, slash ? slash + 1 : fname, + O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC); + if (xfd < 0) { + /* We hold a confined parent dirfd but couldn't pin the + * leaf (e.g. it was raced to a symlink) -- refuse rather + * than fall back to a path-based set that would follow the + * parent components. */ + rsyserr(FERROR_XFER, errno, + "gen_entry_copy_xattrs: openat(%s) failed", + full_fname(fname)); + return -1; + } + } +#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. */ + int odir = 0; +# ifdef O_DIRECTORY + if (S_ISDIR(file->mode)) + odir = O_DIRECTORY; +# endif + xfd = secure_relative_open(NULL, fname, + O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC | odir, 0); + if (xfd < 0) { + rsyserr(FERROR_XFER, errno, + "gen_entry_copy_xattrs: secure open of %s failed", + full_fname(fname)); + return -1; + } + } +#endif + /* Pin the SOURCE (alt-dest basis) leaf too so the xattr READ can't be raced + * out of tree (copy_file does the same for its content+xattr source). A + * relative basis goes through the RESOLVE_BENEATH resolver; an absolute one + * 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()) { + int odir = 0; +#ifdef O_DIRECTORY + if (S_ISDIR(file->mode)) /* secure_relative_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); + else { + int save = operator_path_resolve, sdfd, e; + const char *leaf; + operator_path_resolve = 1; + sdfd = owner_walk_parent(src, &leaf); + operator_path_resolve = save; + if (sdfd >= 0) { + sfd = openat(sdfd, leaf, O_RDONLY | O_NOFOLLOW | odir | O_NONBLOCK | O_NOCTTY | O_CLOEXEC); + e = errno; close(sdfd); errno = e; + } + } + if (sfd < 0) { + rsyserr(FERROR_XFER, errno, + "gen_entry_copy_xattrs: secure open of basis %s failed", + full_fname(src)); + if (xfd >= 0) + close(xfd); + return -1; + } + } +#endif + ret = copy_xattrs(src, sfd, fname, xfd); + if (sfd >= 0) + close(sfd); + if (xfd >= 0) + close(xfd); + return ret; +} +#endif + static void recv_generator(char *fname, struct file_struct *file, int ndx, int itemizing, enum logcode code, int f_out) { @@ -1218,7 +1619,7 @@ static int need_fuzzy_dirlist = 0; struct file_struct *fuzzy_file = NULL; int fd = -1, f_copy = -1; - stat_x sx, real_sx; + stat_x sx = {0}, real_sx = {0}; STRUCT_STAT partial_st; struct file_struct *back_file = NULL; int statret, real_ret, stat_errno; @@ -1315,7 +1716,7 @@ } } if (relative_paths && !implied_dirs && file->mode != 0 - && do_stat(dn, &sx.st) < 0) { + && do_stat_at(dn, &sx.st) < 0) { if (dry_run) goto parent_is_dry_missing; if (make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0) { @@ -1341,7 +1742,7 @@ } parent_dirname = dn; - statret = link_stat(fname, &sx.st, keep_dirlinks && is_dir); + statret = gen_entry_stat(fname, file, &sx.st, keep_dirlinks && is_dir); stat_errno = errno; } @@ -1427,7 +1828,7 @@ && (stype == FT_DIR || delete_item(fname, sx.st.st_mode, del_opts | DEL_FOR_DIR) != 0)) goto cleanup; /* Any errors get reported later. */ - if (do_mkdir(fname, (file->mode|added_perms) & 0700) == 0) + if (gen_entry_mkdir(fname, file, (file->mode|added_perms) & 0700) == 0) file->flags |= FLAG_DIR_CREATED; goto cleanup; } @@ -1469,10 +1870,13 @@ itemize(fnamecmp, file, ndx, statret, &sx, statret ? ITEM_LOCAL_CHANGE : 0, 0, NULL); } - if (real_ret != 0 && do_mkdir(fname,file->mode|added_perms) < 0 && errno != EEXIST) { + 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(); if (!relative_paths || errno != ENOENT || make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0 - || (do_mkdir(fname, file->mode|added_perms) < 0 && errno != EEXIST)) { + || (gen_entry_mkdir(fname, file, file->mode|added_perms) < 0 && errno != EEXIST)) { rsyserr(FERROR_XFER, errno, "recv_generator: mkdir %s failed", full_fname(fname)); @@ -1486,7 +1890,7 @@ #ifdef SUPPORT_XATTRS if (preserve_xattrs && statret == 1) - copy_xattrs(fnamecmpbuf, fname); + gen_entry_copy_xattrs(fnamecmpbuf, fname, file); #endif if (set_file_attrs(fname, file, real_ret ? NULL : &real_sx, NULL, 0) && INFO_GTE(NAME, 1) && code != FNONE && f_out != -1) @@ -1499,7 +1903,7 @@ #ifdef HAVE_CHMOD if (!am_root && (file->mode & S_IRWXU) != S_IRWXU && dir_tweaking) { mode_t mode = file->mode | S_IRWXU; - if (do_chmod(fname, mode) < 0) { + if (gen_entry_chmod(fname, file, mode) < 0) { rsyserr(FERROR_XFER, errno, "failed to modify permissions on %s", full_fname(fname)); @@ -1573,7 +1977,14 @@ } } else if (basis_dir[0] != NULL) { int j = try_dests_non(file, fname, ndx, fnamecmpbuf, &sx, itemizing, code); - if (j == -2) { + if (j == -3) { + /* The destination cannot hard-link this type. Land exactly + * where a build without CAN_HARDLINK_SYMLINK lands: create + * the entry, but leave the reporting to the itemisation + * try_dests_non() already emitted. */ + itemizing = 0; + code = FNONE; + } else if (j == -2) { #ifndef CAN_HARDLINK_SYMLINK if (alt_dest_type == LINK_DEST) { /* Resort to --copy-dest behavior. */ @@ -1612,10 +2023,20 @@ goto cleanup; } - if ((am_root && preserve_devices && ftype == FT_DEVICE) - || (preserve_specials && ftype == FT_SPECIAL)) { + /* --drop-D refuses to CREATE devices/specials without touching + * preserve_devices/preserve_specials, which also frame the file list's + * rdev fields -- clearing those on one end of a connection alone + * desynchronises it. Falls through to the "skipping non-regular file" + * path below, exactly as --no-D reaches it. */ + if (!drop_devices + && ((am_root && preserve_devices && ftype == FT_DEVICE) + || (preserve_specials && ftype == FT_SPECIAL))) { dev_t rdev; int del_for_flag; + /* Whether the dest existed, captured before the type-mismatch + * flip below clears statret -- so atomic_create() gets a delete + * flag (and reads sx.st) only when sx.st was actually stat'd. */ + int dest_existed = (statret == 0); if (ftype == FT_DEVICE) { uint32 *devp = F_RDEV_P(file); rdev = MAKEDEV(DEV_MAJOR(devp), DEV_MINOR(devp)); @@ -1642,7 +2063,12 @@ } } else if (basis_dir[0] != NULL) { int j = try_dests_non(file, fname, ndx, fnamecmpbuf, &sx, itemizing, code); - if (j == -2) { + if (j == -3) { + /* As above: a destination that cannot hard-link this type + * behaves like a build without CAN_HARDLINK_SPECIAL. */ + itemizing = 0; + code = FNONE; + } else if (j == -2) { #ifndef CAN_HARDLINK_SPECIAL if (alt_dest_type == LINK_DEST) { /* Resort to --copy-dest behavior. */ @@ -1662,7 +2088,7 @@ fname, (int)file->mode, (long)major(rdev), (long)minor(rdev)); } - if (atomic_create(file, fname, NULL, NULL, rdev, &sx, del_for_flag)) { + if (atomic_create(file, fname, NULL, NULL, rdev, &sx, dest_existed ? del_for_flag : 0)) { set_file_attrs(fname, file, NULL, NULL, 0); if (itemizing) { itemize(fnamecmp, file, ndx, statret, &sx, @@ -1706,7 +2132,8 @@ goto cleanup; } - if (update_only > 0 && statret == 0 && file->modtime - sx.st.st_mtime < modify_window) { + if (update_only > 0 && statret == 0 && stype == ftype + && file->modtime - sx.st.st_mtime < modify_window) { if (INFO_GTE(SKIP, 1)) rprintf(FINFO, "%s is newer\n", fname); #ifdef SUPPORT_HARD_LINKS @@ -1808,7 +2235,12 @@ ; else if (quick_check_ok(FT_REG, fnamecmp, file, &sx.st)) { if (partialptr) { - do_unlink(partialptr); + /* 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; handle_partial_dir(partialptr, PDIR_DELETE); } set_file_attrs(fname, file, &sx, NULL, maybe_ATTRS_REPORT | maybe_ATTRS_ACCURATE_TIME); @@ -1847,15 +2279,26 @@ if (read_batch || whole_file) { if (inplace && make_backups > 0 && fnamecmp_type == FNAMECMP_FNAME) { - if (!(backupptr = get_backup_name(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; + if (!(backupptr = get_backup_name(fname))) { + operator_path_resolve = 0; goto cleanup; - if (!(back_file = make_file(fname, NULL, NULL, 0, NO_FILTERS))) + } + 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; unmake_file(back_file); back_file = NULL; goto cleanup; } + operator_path_resolve = 0; } goto notify_others; } @@ -1883,25 +2326,33 @@ } 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; rsyserr(FERROR_XFER, errno, "unlink %s", full_fname(backupptr)); unmake_file(back_file); back_file = NULL; goto cleanup; } - if ((f_copy = do_open(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600)) < 0) { + if ((f_copy = do_open_at(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600)) < 0) { + operator_path_resolve = 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; } @@ -1964,14 +2415,34 @@ close(fd); if (back_file) { int save_preserve_xattrs = preserve_xattrs; - if (f_copy >= 0) - close(f_copy); #ifdef SUPPORT_XATTRS - if (preserve_xattrs) { - copy_xattrs(fname, backupptr); + /* The delta-backup path wrote backupptr via the held f_copy, so + * copy its xattrs through that fd here. The whole-file/inplace + * path (f_copy < 0) backed it up via copy_file(), which already + * copied the xattrs through its own held fd -- don't repeat it + * with a path-based set a parent-symlink race could redirect. */ + if (preserve_xattrs && f_copy >= 0) { + /* Read fname's xattrs through a confined fd so the copy onto the + * held backup fd can't be fed an out-of-module source by a raced + * parent symlink; a hardened race skips rather than path-reads. */ + int bfd = backup_source_fd(fname); + if (!backup_metadata_hardened() || bfd >= 0) + copy_xattrs(fname, bfd, backupptr, f_copy); + if (bfd >= 0) + close(bfd); preserve_xattrs = 0; } #endif + 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 + * 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 + * try_dests_reg). The static --backup-dir escape is already closed; a + * parent-symlink flipped in after the confined create races only these + * chmod/chown/times -- a documented residual. */ set_file_attrs(backupptr, back_file, NULL, NULL, 0); preserve_xattrs = save_preserve_xattrs; if (INFO_GTE(BACKUP, 1)) { @@ -1982,6 +2453,7 @@ } free_stat_x(&sx); + free_stat_x(&real_sx); } /* If we are replacing an existing hard link, symlink, device, or special file, @@ -2016,7 +2488,7 @@ if (slnk) { #ifdef SUPPORT_LINKS - if (do_symlink(slnk, create_name) < 0) { + if (gen_entry_symlink(slnk, create_name, file) < 0) { rsyserr(FERROR_XFER, errno, "symlink %s -> \"%s\" failed", full_fname(create_name), slnk); return 0; @@ -2032,22 +2504,35 @@ return 0; #endif } else { - if (do_mknod(create_name, file->mode, rdev) < 0) { - rsyserr(FERROR_XFER, errno, "mknod %s failed", + if (gen_entry_mknod(create_name, file, file->mode, rdev) < 0) { + int e = errno; + /* A nested socket can't be created race-safely where there is no + * bindat() (the BSDs, macOS, Solaris): syscall.c returns EOPNOTSUPP + * rather than re-resolving an unsafe parent. A socket inode is only + * a placeholder -- a live socket isn't usefully transferred -- so + * skip it with a warning instead of failing the whole transfer + * (got_xfer_error -> exit 23). Top-level sockets still create via + * the path-based bind() fallback. */ + if (S_ISSOCK(file->mode) && (e == EOPNOTSUPP || e == ENOSYS)) { + rprintf(FWARNING, "skipping socket (creation unsupported here): %s\n", + full_fname(create_name)); + return 0; + } + rsyserr(FERROR_XFER, e, "mknod %s failed", full_fname(create_name)); return 0; } } if (!skip_atomic) { - if (do_rename(tmpname, fname) < 0) { + if (gen_entry_rename(tmpname, fname, file) < 0) { char *full_tmpname = strdup(full_fname(tmpname)); if (full_tmpname == NULL) out_of_memory("atomic_create"); rsyserr(FERROR_XFER, errno, "rename %s -> \"%s\" failed", full_tmpname, full_fname(fname)); free(full_tmpname); - do_unlink(tmpname); + gen_entry_unlink(tmpname, file); return 0; } } @@ -2111,15 +2596,15 @@ continue; fname = f_name(file, NULL); if (fix_dir_perms) - do_chmod(fname, file->mode); + gen_entry_chmod(fname, file, file->mode); if (need_retouch_dir_times) { STRUCT_STAT st; - if (link_stat(fname, &st, 0) == 0 && mtime_differs(&st, file)) { + if (gen_entry_stat(fname, file, &st, 0) == 0 && mtime_differs(&st, file)) { st.st_mtime = file->modtime; #ifdef ST_MTIME_NSEC st.ST_MTIME_NSEC = F_MOD_NSEC_or_0(file); #endif - set_times(fname, &st); + gen_entry_set_times(fname, file, &st); } } if (counter >= loopchk_limit) { @@ -2146,6 +2631,8 @@ if (send_failed) ndx = get_hlink_num(); flist = flist_for_ndx(ndx, "check_for_finished_files.1"); + if (ndx < flist->ndx_start) + exit_cleanup(RERR_PROTOCOL); file = flist->files[ndx - flist->ndx_start]; assert(file->flags & FLAG_HLINKED); if (send_failed) @@ -2174,6 +2661,8 @@ flist = cur_flist; cur_flist = flist_for_ndx(ndx, "check_for_finished_files.2"); + if (ndx < cur_flist->ndx_start) + exit_cleanup(RERR_PROTOCOL); file = cur_flist->files[ndx - cur_flist->ndx_start]; if (solo_file) @@ -2216,7 +2705,8 @@ if (delete_during == 2 || !dir_tweaking) { /* Skip directory touch-up. */ - } else if (first_flist->parent_ndx >= 0) + } else if (first_flist->parent_ndx >= 0 + && first_flist->parent_ndx < dir_flist->used) touch_up_dirs(dir_flist, first_flist->parent_ndx); flist_free(first_flist); /* updates first_flist */ @@ -2287,7 +2777,8 @@ } #endif - if (inc_recurse && cur_flist->parent_ndx >= 0) { + if (inc_recurse && cur_flist->parent_ndx >= 0 + && cur_flist->parent_ndx < dir_flist->used) { struct file_struct *fp = dir_flist->files[cur_flist->parent_ndx]; if (solo_file) strlcpy(fbuf, solo_file, sizeof fbuf); @@ -2374,7 +2865,7 @@ write_ndx(f_out, NDX_DONE); if (protocol_version >= 31 && EARLY_DELETE_DONE_MSG()) { - if ((INFO_GTE(STATS, 2) && (delete_mode || force_delete)) || read_batch) + if (delete_mode || force_delete || read_batch) write_del_stats(f_out); if (EARLY_DELAY_DONE_MSG()) /* Can't send this before delay */ write_ndx(f_out, NDX_DONE); @@ -2419,7 +2910,7 @@ if (protocol_version >= 31) { if (!EARLY_DELETE_DONE_MSG()) { - if (INFO_GTE(STATS, 2) || read_batch) + if (delete_mode || force_delete || read_batch) write_del_stats(f_out); write_ndx(f_out, NDX_DONE); } diff -Nru rsync-3.4.1+ds1/getgroups.c rsync-3.5.0+ds1/getgroups.c --- rsync-3.4.1+ds1/getgroups.c 2020-06-28 04:19:52.000000000 +0000 +++ rsync-3.5.0+ds1/getgroups.c 2026-06-15 22:55:39.000000000 +0000 @@ -57,5 +57,6 @@ printf("%lu", (unsigned long)gid); printf("\n"); + free(list); return 0; } diff -Nru rsync-3.4.1+ds1/hashtable.c rsync-3.5.0+ds1/hashtable.c --- rsync-3.4.1+ds1/hashtable.c 2022-09-30 19:36:21.000000000 +0000 +++ rsync-3.5.0+ds1/hashtable.c 2026-07-20 04:05:32.000000000 +0000 @@ -19,7 +19,7 @@ #include "rsync.h" -#define HASH_LOAD_LIMIT(size) ((size)*3/4) +#define HASH_LOAD_LIMIT(size) ((size)/4*3) /* /4 first: never overflows int */ struct hashtable *hashtable_create(int size, int key64) { @@ -28,15 +28,25 @@ int node_size = key64 ? sizeof (struct ht_int64_node) : sizeof (struct ht_int32_node); - /* Pick a power of 2 that can hold the requested size. */ - if (size & (size-1) || size < 16) { + /* Pick a power of 2 that can hold the requested size. Test size < 16 first + * so a negative/zero req short-circuits before the size-1 (INT_MIN is UB). */ + if (size < 16 || (size & (size-1))) { size = 16; - while (size < req) + while (size < req) { + if (size > INT_MAX/2) { /* the next doubling would overflow int */ + rprintf(FERROR, "[%s] hashtable_create: requested size %d is too large\n", + who_am_i(), req); + exit_cleanup(RERR_MALLOC); + } size *= 2; + } } tbl = new(struct hashtable); - tbl->nodes = new_array0(char, size * node_size); + /* Pass size and node_size as SEPARATE factors so my_alloc's overflow / + * --max-alloc guard sees both; computing size*node_size as int would wrap to + * a tiny count and under-allocate (heap overflow on later node access). */ + tbl->nodes = my_alloc(do_calloc, size, node_size, __FILE__, __LINE__); tbl->size = size; tbl->entries = 0; tbl->node_size = node_size; @@ -90,10 +100,15 @@ if (data_when_new && tbl->entries > HASH_LOAD_LIMIT(tbl->size)) { void *old_nodes = tbl->nodes; - int size = tbl->size * 2; - int i; + int size, i; - tbl->nodes = new_array0(char, size * tbl->node_size); + if (tbl->size > INT_MAX/2) { /* doubling would overflow int */ + rprintf(FERROR, "[%s] hashtable grow: size overflow\n", who_am_i()); + exit_cleanup(RERR_MALLOC); + } + size = tbl->size * 2; + /* Separate factors so my_alloc's guard sees both (see hashtable_create). */ + tbl->nodes = my_alloc(do_calloc, size, tbl->node_size, __FILE__, __LINE__); tbl->size = size; tbl->entries = 0; @@ -120,7 +135,7 @@ if (!key64) { /* Based on Jenkins One-at-a-time hash. */ - uchar buf[4], *keyp = buf; + uchar buf[4] = {0}, *keyp = buf; /* {0} only to satisfy the analyzer (SIVALu fills buf) */ int i; SIVALu(buf, 0, key); @@ -351,7 +366,7 @@ */ #define NON_ZERO_32(x) ((x) ? (x) : (uint32_t)1) -#define NON_ZERO_64(x, y) ((x) || (y) ? (y) | (int64)(x) << 32 | (y) : (int64)1) +#define NON_ZERO_64(x, y) ((x) || (y) ? (y) | (uint64_t)(x) << 32 | (y) : (int64)1) uint32_t hashlittle(const void *key, size_t length) { diff -Nru rsync-3.4.1+ds1/hlink.c rsync-3.5.0+ds1/hlink.c --- rsync-3.4.1+ds1/hlink.c 2024-12-17 21:56:27.000000000 +0000 +++ rsync-3.5.0+ds1/hlink.c 2026-07-20 04:05:32.000000000 +0000 @@ -125,8 +125,22 @@ if (inc_recurse) { node = hashtable_find(prior_hlinks, gnum, data_when_new); if (node->data == data_when_new) { + if (gnum < hlink_flist->ndx_start) { + /* A non-first hard-link entry whose + * gnum points before this flist's + * ndx_start should already have been + * recorded in prior_hlinks by an + * earlier flist. A peer that sends + * such a back-reference on the first + * flist (or to a gnum that was never + * declared XMIT_HLINK_FIRST) is + * misbehaving. */ + rprintf(FERROR, + "hard-link gnum %d precedes flist start %d\n", + (int)gnum, (int)hlink_flist->ndx_start); + exit_cleanup(RERR_PROTOCOL); + } node->data = new_array0(char, 5); - assert(gnum >= hlink_flist->ndx_start); file->flags |= FLAG_HLINK_FIRST; prev = -1; } else if (CVAL(node->data, 0) == 0) { @@ -406,7 +420,14 @@ } break; } - if (!quick_check_ok(FT_REG, cmpbuf, file, &alt_sx.st)) + /* Content-based basis match only applies to regular + * files: for a hard-linked symlink/device/special the + * exact-inode check above is the only meaningful test, + * and quick_check_ok(FT_REG, ...) would read F_SUM() + * on a file_struct that has no SUM_EXTRA_CNT space + * (recv_file_entry only allocates it for S_ISREG). */ + if (!S_ISREG(file->mode) + || !quick_check_ok(FT_REG, cmpbuf, file, &alt_sx.st)) continue; statret = 1; if (unchanged_attrs(cmpbuf, file, &alt_sx)) @@ -430,7 +451,7 @@ if (preserve_xattrs) { free_xattr(sxp); if (!XATTR_READY(alt_sx)) - get_xattr(cmpbuf, sxp); + get_xattr(cmpbuf, -1, sxp); else { sxp->xattr = alt_sx.xattr; alt_sx.xattr = NULL; @@ -454,7 +475,7 @@ int hard_link_one(struct file_struct *file, const char *fname, const char *oldname, int terse) { - if (do_link(oldname, fname) < 0) { + if (do_link_at(oldname, fname) < 0) { enum logcode code; if (terse) { if (!INFO_GTE(NAME, 1)) diff -Nru rsync-3.4.1+ds1/io.c rsync-3.5.0+ds1/io.c --- rsync-3.4.1+ds1/io.c 2024-10-30 06:06:34.000000000 +0000 +++ rsync-3.5.0+ds1/io.c 2026-08-02 03:26:41.000000000 +0000 @@ -31,7 +31,15 @@ #include "ifuncs.h" #include "inums.h" -/** If no timeout is specified then use a 60 second select timeout */ +#include + +/* Readiness bits we act on. poll() can report POLLERR/POLLHUP/POLLNVAL even + * when they were not requested, and POLLPRI stands in for select()'s old + * exception set. */ +#define POLL_RD_BITS (POLLIN | POLLPRI | POLLERR | POLLHUP) +#define POLL_WR_BITS (POLLOUT | POLLERR | POLLHUP) + +/** If no timeout is specified then use a 60 second I/O timeout */ #define SELECT_TIMEOUT 60 extern int bwlimit; @@ -59,6 +67,7 @@ extern int daemon_connection; extern int protocol_version; extern int remove_source_files; +extern int write_batch; extern int preserve_hard_links; extern BOOL extra_flist_sending_enabled; extern BOOL flush_ok_after_signal; @@ -79,6 +88,7 @@ /* Ignore an EOF error if non-zero. See whine_about_eof(). */ int kluge_around_eof = 0; int got_kill_signal = -1; /* is set to 0 only after multiplexed I/O starts */ +volatile sig_atomic_t got_sigusr2 = 0; /* set by the async-signal-safe SIGUSR2 handler */ int sock_f_in = -1; int sock_f_out = -1; @@ -102,6 +112,11 @@ static time_t last_io_in; static time_t last_io_out; +/* Absolute wall-clock bound for peer-controlled daemon handshake reads. + * This is deliberately separate from io_timeout: the latter is an idle + * transfer timeout and may be supplied by the module or client. */ +static time_t daemon_handshake_deadline; + static int write_batch_monitor_in = -1; static int write_batch_monitor_out = -1; @@ -113,11 +128,43 @@ static xbuf iconv_buf = EMPTY_XBUF; #endif static int select_timeout = SELECT_TIMEOUT; + +/* Turn select_timeout (in seconds) into a poll() millisecond count, keeping it + * positive and bounded. A negative count means "wait forever" to poll(), which + * would bypass our keepalives and timeout enforcement entirely. */ +static int poll_timeout_ms(void) +{ + int secs = select_timeout; + + if (secs <= 0 || secs > SELECT_TIMEOUT) + secs = SELECT_TIMEOUT; + return secs * 1000; +} + +static int handshake_poll_timeout_ms(void) +{ + time_t now, left; + int timeout = poll_timeout_ms(); + + if (!daemon_handshake_deadline) + return timeout; + + now = time(NULL); + left = daemon_handshake_deadline - now; + if (left <= 0) { + rprintf(FERROR, "[%s] daemon handshake timeout -- exiting\n", who_am_i()); + exit_cleanup(RERR_TIMEOUT); + } + if (left <= INT_MAX / 1000 && left * 1000 < timeout) + timeout = (int)left * 1000; + return timeout; +} + static int active_filecnt = 0; static OFF_T active_bytecnt = 0; static int first_message = 1; -static char int_byte_extra[64] = { +static const char int_byte_extra[64] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* (00 - 3F)/4 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* (40 - 7F)/4 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* (80 - BF)/4 */ @@ -220,9 +267,15 @@ int i; if (kluge_around_eof > 0) exit_cleanup(0); - /* If we're still here after 10 seconds, exit with an error. */ - for (i = 10*1000/20; i--; ) + /* The receiver is waiting here for the generator's SIGUSR2; act on it + * (exit cleanly) the moment it arrives rather than sleeping the full + * 10s and then erroring. The async-signal-safe handler only sets the + * flag, so this loop must poll it. */ + for (i = 10*1000/20; i--; ) { + if (got_sigusr2) + receive_sigusr2(); msleep(20); + } } rprintf(FERROR, RSYNC_NAME ": connection unexpectedly closed " @@ -243,31 +296,35 @@ assert(fd != iobuf.in_fd); while (1) { - struct timeval tv; - fd_set r_fds, e_fds; + struct pollfd pfd; int cnt; - FD_ZERO(&r_fds); - FD_SET(fd, &r_fds); - FD_ZERO(&e_fds); - FD_SET(fd, &e_fds); - tv.tv_sec = select_timeout; - tv.tv_usec = 0; + if (got_sigusr2) /* receiver told to wrap up (e.g. a --read-batch fd) */ + receive_sigusr2(); + + /* We use poll() rather than select() so that a high-numbered fd + * (>= FD_SETSIZE) cannot overflow an fd_set bitmap. */ + pfd.fd = fd; + pfd.events = POLLIN | POLLPRI; + pfd.revents = 0; - cnt = select(fd+1, &r_fds, NULL, &e_fds, &tv); + cnt = poll(&pfd, 1, handshake_poll_timeout_ms()); if (cnt <= 0) { - if (cnt < 0 && errno == EBADF) { - rsyserr(FERROR, errno, "safe_read select failed"); + if (cnt < 0 && errno != EINTR && errno != EAGAIN) { + rsyserr(FERROR, errno, "safe_read poll failed"); exit_cleanup(RERR_FILEIO); } check_timeout(1, MSK_ALLOW_FLUSH); continue; } - /*if (FD_ISSET(fd, &e_fds)) - rprintf(FINFO, "select exception on fd %d\n", fd); */ + /* An invalid fd is reported here rather than via poll()'s return. */ + if (pfd.revents & POLLNVAL) { + rsyserr(FERROR, EBADF, "safe_read poll failed"); + exit_cleanup(RERR_FILEIO); + } - if (FD_ISSET(fd, &r_fds)) { + if (pfd.revents & POLL_RD_BITS) { ssize_t n = read(fd, buf + got, len - got); if (DEBUG_GTE(IO, 2)) { rprintf(FINFO, "[%s] safe_read(%d)=%" SIZE_T_FMT_MOD "d\n", @@ -315,6 +372,9 @@ assert(fd != iobuf.out_fd); + if (got_sigusr2) /* receiver told to wrap up before this (batch) write */ + receive_sigusr2(); + n = write(fd, buf, len); if ((size_t)n == len) return; @@ -332,19 +392,21 @@ } while (len) { - struct timeval tv; - fd_set w_fds; + struct pollfd pfd; int cnt; - FD_ZERO(&w_fds); - FD_SET(fd, &w_fds); - tv.tv_sec = select_timeout; - tv.tv_usec = 0; + if (got_sigusr2) /* receiver told to wrap up (e.g. a --write-batch fd) */ + receive_sigusr2(); + + /* poll() avoids the FD_SETSIZE limit that select() imposes. */ + pfd.fd = fd; + pfd.events = POLLOUT; + pfd.revents = 0; - cnt = select(fd + 1, NULL, &w_fds, NULL, &tv); + cnt = poll(&pfd, 1, poll_timeout_ms()); if (cnt <= 0) { - if (cnt < 0 && errno == EBADF) { - rsyserr(FERROR, errno, "safe_write select failed on %s", what_fd_is(fd)); + if (cnt < 0 && errno != EINTR && errno != EAGAIN) { + rsyserr(FERROR, errno, "safe_write poll failed on %s", what_fd_is(fd)); exit_cleanup(RERR_FILEIO); } if (io_timeout) @@ -352,7 +414,12 @@ continue; } - if (FD_ISSET(fd, &w_fds)) { + if (pfd.revents & POLLNVAL) { + rsyserr(FERROR, EBADF, "safe_write poll failed on %s", what_fd_is(fd)); + exit_cleanup(RERR_FILEIO); + } + + if (pfd.revents & POLL_WR_BITS) { n = write(fd, buf, len); if (n < 0) { if (errno == EINTR) @@ -561,9 +628,8 @@ * unused raw data in the buf would prevent the reading of socket data. */ static char *perform_io(size_t needed, int flags) { - fd_set r_fds, e_fds, w_fds; - struct timeval tv; - int cnt, max_fd; + struct pollfd pfds[3]; + int cnt, max_fd, npfds, poll_timeout, in_pollpos, out_pollpos, ff_pollpos; size_t empty_buf_len = 0; xbuf *out; char *data; @@ -656,13 +722,15 @@ } max_fd = -1; + npfds = 0; + in_pollpos = out_pollpos = ff_pollpos = -1; - FD_ZERO(&r_fds); - FD_ZERO(&e_fds); if (iobuf.in_fd >= 0 && iobuf.in.size - iobuf.in.len) { if (!read_batch || batch_fd >= 0) { - FD_SET(iobuf.in_fd, &r_fds); - FD_SET(iobuf.in_fd, &e_fds); + pfds[npfds].fd = iobuf.in_fd; + pfds[npfds].events = POLLIN | POLLPRI; + pfds[npfds].revents = 0; + in_pollpos = npfds++; } if (iobuf.in_fd > max_fd) max_fd = iobuf.in_fd; @@ -670,12 +738,14 @@ /* Only do more filesfrom processing if there is enough room in the out buffer. */ if (ff_forward_fd >= 0 && iobuf.out.size - iobuf.out.len > FILESFROM_BUFLEN*2) { - FD_SET(ff_forward_fd, &r_fds); + pfds[npfds].fd = ff_forward_fd; + pfds[npfds].events = POLLIN; + pfds[npfds].revents = 0; + ff_pollpos = npfds++; if (ff_forward_fd > max_fd) max_fd = ff_forward_fd; } - FD_ZERO(&w_fds); if (iobuf.out_fd >= 0) { if (iobuf.raw_flushing_ends_before || (!iobuf.msg.len && iobuf.out.len > iobuf.out_empty_len && !(flags & PIO_NEED_MSGROOM))) { @@ -715,7 +785,18 @@ } else out = NULL; if (out) { - FD_SET(iobuf.out_fd, &w_fds); + /* A direct daemon connection uses one fd for both + * directions; give it a single row with both events + * rather than two rows carrying different masks. */ + if (in_pollpos >= 0 && iobuf.out_fd == iobuf.in_fd) { + pfds[in_pollpos].events |= POLLOUT; + out_pollpos = in_pollpos; + } else { + pfds[npfds].fd = iobuf.out_fd; + pfds[npfds].events = POLLOUT; + pfds[npfds].revents = 0; + out_pollpos = npfds++; + } if (iobuf.out_fd > max_fd) max_fd = iobuf.out_fd; } @@ -749,19 +830,20 @@ if (got_kill_signal > 0) handle_kill_signal(True); + if (got_sigusr2) + receive_sigusr2(); if (extra_flist_sending_enabled) { if (file_total - file_old_total < MAX_FILECNT_LOOKAHEAD && IN_MULTIPLEXED_AND_READY) - tv.tv_sec = 0; + poll_timeout = 0; else { extra_flist_sending_enabled = False; - tv.tv_sec = select_timeout; + poll_timeout = poll_timeout_ms(); } } else - tv.tv_sec = select_timeout; - tv.tv_usec = 0; + poll_timeout = poll_timeout_ms(); - cnt = select(max_fd + 1, &r_fds, &w_fds, &e_fds, &tv); + cnt = poll(pfds, npfds, poll_timeout); if (cnt <= 0) { if (cnt < 0 && errno == EBADF) { @@ -774,11 +856,29 @@ extra_flist_sending_enabled = !flist_eof; } else check_timeout((flags & PIO_NEED_INPUT) != 0, 0); - FD_ZERO(&r_fds); /* Just in case... */ - FD_ZERO(&w_fds); + /* Just in case... */ + if (in_pollpos >= 0) + pfds[in_pollpos].revents = 0; + if (ff_pollpos >= 0) + pfds[ff_pollpos].revents = 0; + if (out_pollpos >= 0) + pfds[out_pollpos].revents = 0; } - if (iobuf.in_fd >= 0 && FD_ISSET(iobuf.in_fd, &r_fds)) { + if (cnt > 0) { + /* poll() reports a bad fd here, not via its return value. */ + int p; + for (p = 0; p < npfds; p++) { + if (pfds[p].revents & POLLNVAL) { + msgs2stderr = 1; + rsyserr(FERROR, EBADF, "perform_io: poll reported an invalid fd"); + exit_cleanup(RERR_SOCKETIO); + } + } + } + + if (iobuf.in_fd >= 0 && in_pollpos >= 0 + && pfds[in_pollpos].revents & POLL_RD_BITS) { size_t len, pos = iobuf.in.pos + iobuf.in.len; ssize_t n; if (pos >= iobuf.in.size) { @@ -827,7 +927,7 @@ exit_cleanup(RERR_TIMEOUT); } - if (out && FD_ISSET(iobuf.out_fd, &w_fds)) { + if (out && out_pollpos >= 0 && pfds[out_pollpos].revents & POLL_WR_BITS) { size_t len = iobuf.raw_flushing_ends_before ? iobuf.raw_flushing_ends_before - out->pos : out->len; ssize_t n; @@ -878,6 +978,8 @@ if (got_kill_signal > 0) handle_kill_signal(True); + if (got_sigusr2) + receive_sigusr2(); /* We need to help prevent deadlock by doing what reading * we can whenever we are here trying to write. */ @@ -888,7 +990,8 @@ wait_for_receiver(); /* generator only */ } - if (ff_forward_fd >= 0 && FD_ISSET(ff_forward_fd, &r_fds)) { + if (ff_forward_fd >= 0 && ff_pollpos >= 0 + && pfds[ff_pollpos].revents & POLL_RD_BITS) { /* This can potentially flush all output and enable * multiplexed output, so keep this last in the loop * and be sure to not cache anything that would break @@ -900,6 +1003,8 @@ if (got_kill_signal > 0) handle_kill_signal(True); + if (got_sigusr2) + receive_sigusr2(); data = iobuf.in.buf + iobuf.in.pos; @@ -1070,17 +1175,30 @@ void send_msg_success(const char *fname, int num) { + /* Batch-only mode has not duplicated anything on the receiving side yet. + * The receiver still reports success to the generator for file-list and + * hard-link bookkeeping, but the generator must not turn that status into + * sender-side removal. */ + if (am_generator && write_batch < 0 && remove_source_files) + return; + if (local_server) { STRUCT_STAT st; if (DEBUG_GTE(IO, 1)) rprintf(FINFO, "[%s] send_msg_success(%d)\n", who_am_i(), num); - if (stat(fname, &st) < 0) - memset(&st, 0, sizeof (STRUCT_STAT)); + /* The dev/ino is consumed only by the sender's --remove-source-files + * same-file safety check (successful_send), so skip the per-file + * stat entirely otherwise -- it's sent but never read. */ + if (remove_source_files && stat(fname, &st) == 0) { + SIVAL64(num_dev_ino_buf, 4, st.st_dev); + SIVAL64(num_dev_ino_buf, 4+8, st.st_ino); + } else { + SIVAL64(num_dev_ino_buf, 4, 0); + SIVAL64(num_dev_ino_buf, 4+8, 0); + } SIVAL(num_dev_ino_buf, 0, num); - SIVAL64(num_dev_ino_buf, 4, st.st_dev); - SIVAL64(num_dev_ino_buf, 4+8, st.st_ino); send_msg(MSG_SUCCESS, num_dev_ino_buf, sizeof num_dev_ino_buf, -1); } else send_msg_int(MSG_SUCCESS, num); @@ -1090,6 +1208,9 @@ { struct file_list *flist = flist_for_ndx(ndx, "got_flist_entry_status"); + if (ndx < flist->ndx_start) + exit_cleanup(RERR_PROTOCOL); + if (remove_source_files) { active_filecnt--; active_bytecnt -= F_LENGTH(flist->files[ndx - flist->ndx_start]); @@ -1100,7 +1221,7 @@ switch (status) { case FES_SUCCESS: - if (remove_source_files) { + if (remove_source_files && write_batch >= 0) { if (local_server) send_msg(MSG_SUCCESS, num_dev_ino_buf, sizeof num_dev_ino_buf, -1); else @@ -1144,8 +1265,26 @@ void set_io_timeout(int secs) { + /* A negative timeout is meaningless; treat it as "no timeout" rather than + * letting it drive allowed_lull / select_timeout negative (a tight loop). + * (--timeout is parsed by options.c as a plain int, so it can be negative.) */ + if (secs < 0) + secs = 0; io_timeout = secs; - allowed_lull = (io_timeout + 1) / 2; + /* Compute ceil(io_timeout/2) in a wider type: io_timeout can be INT_MAX + * (a peer's MSG_IO_TIMEOUT -- now capped in read_a_msg() -- or an operator + * --timeout, which options.c parses unbounded), and a plain "io_timeout + 1" + * would overflow to a negative allowed_lull / select_timeout. poll() now + * takes a millisecond count where negative means "wait forever", so this + * would hang the process rather than spin it -- and it still fires a + * keepalive flood. poll_timeout_ms() clamps as well; keep both. */ + allowed_lull = (int)(((int64)io_timeout + 1) / 2); + /* The generator and sender derive an int loop-check limit as + * allowed_lull * 5; keep allowed_lull small enough that that product can't + * overflow either. The cap is invisible to real use -- allowed_lull is the + * keep-alive half-interval and INT_MAX/5 seconds is over 13 years. */ + if (allowed_lull > INT_MAX / 5) + allowed_lull = INT_MAX / 5; if (!io_timeout || allowed_lull > SELECT_TIMEOUT) select_timeout = SELECT_TIMEOUT; @@ -1156,10 +1295,18 @@ allowed_lull = 0; } +void set_daemon_handshake_timeout(int secs) +{ + if (secs > 0) + daemon_handshake_deadline = time(NULL) + secs; + else + daemon_handshake_deadline = 0; +} + static void check_for_d_option_error(const char *msg) { - static char rsync263_opts[] = "BCDHIKLPRSTWabceghlnopqrtuvxz"; - char *colon; + static const char rsync263_opts[] = "BCDHIKLPRSTWabceghlnopqrtuvxz"; + const char *colon; int saw_d = 0; if (*msg != 'r' @@ -1289,8 +1436,23 @@ return s - buf; } +/* Reverse safe_arg()'s backslash escaping of a daemon option arg, the way a + * remote shell un-escapes args for the ssh transport. In place; \X -> X. */ +static void unbackslash_arg(char *s) +{ + char *f = s, *t = s; + while (*f) { + if (*f == '\\' && f[1]) + f++; + *t++ = *f++; + } + *t = '\0'; +} + +#define MAX_DAEMON_ARGS (MAX_ARGS * 16) + void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls, - char ***argv_p, int *argc_p, char **request_p) + int unescape, char ***argv_p, int *argc_p, char **request_p) { int maxargs = MAX_ARGS; int dot_pos = 0, argc = 0, request_len = 0; @@ -1312,6 +1474,11 @@ if (read_line(f_in, buf, bufsiz, rl_flags) == 0) break; + if (mod_name && argc >= MAX_DAEMON_ARGS - 1) { + rprintf(FERROR, "too many daemon arguments\n"); + exit_cleanup(RERR_PROTOCOL); + } + if (argc == maxargs-1) { maxargs += MAX_ARGS; argv = realloc_array(argv, char *, maxargs); @@ -1332,11 +1499,23 @@ glob_expand(buf, &argv, &argc, &maxargs); } else { p = strdup(buf); + /* An option arg the client escaped with safe_arg() (no + * remote shell un-escapes it for a daemon). File args + * after the dot are handled by glob_expand() below. */ + if (unescape) + unbackslash_arg(p); argv[argc++] = p; if (*p == '.' && p[1] == '\0') dot_pos = argc; } } + /* glob_expand()/glob_match() reserve glob.argc+1 slots -- room for the + * entry being added but not for this trailing NULL. A post-dot line + * whose " mod/" splits land argc on exactly maxargs (or any later + * ENSURE_MEMSPACE doubling boundary) would otherwise make the next + * store an 8-byte NULL write one slot past the argv allocation. */ + if (argc >= maxargs) + argv = realloc_array(argv, char *, maxargs = argc + 1); argv[argc] = NULL; glob_expand(NULL, NULL, NULL, NULL); @@ -1353,8 +1532,9 @@ if (iobuf.out.buf) { if (iobuf.out_fd == -1) iobuf.out_fd = f_out; - else + else if (iobuf.out_fd >= 0) assert(f_out == iobuf.out_fd); + /* else out_fd == -2: peer already gone; leave it dead. */ return False; } @@ -1372,8 +1552,9 @@ if (iobuf.in.buf) { if (iobuf.in_fd == -1) iobuf.in_fd = f_in; - else + else if (iobuf.in_fd >= 0) assert(f_in == iobuf.in_fd); + /* else in_fd == -2: peer already EOF'd; leave it dead. */ return False; } @@ -1522,16 +1703,26 @@ if (msg_bytes != 4) goto invalid_msg; val = raw_read_int(); - iobuf.in_multiplexed = 1; + val &= IOERR_VALID_MASK; io_error |= val; if (am_receiver) send_msg_int(MSG_IO_ERROR, val); + iobuf.in_multiplexed = 1; break; case MSG_IO_TIMEOUT: if (msg_bytes != 4 || am_server || am_generator) goto invalid_msg; val = raw_read_int(); iobuf.in_multiplexed = 1; + /* The peer may only ask us to use a SHORTER timeout (a stricter cap); a + * non-positive value would disable our --timeout entirely, letting a + * malicious server hang the client indefinitely, so ignore it. A very + * large value (near INT_MAX) would overflow the (io_timeout + 1) / 2 + * computation in set_io_timeout(), wrapping allowed_lull and + * select_timeout negative -- which poll() reads as "wait forever", + * hanging the client. Cap at 24 hours. */ + if (val <= 0 || val > 86400) + break; if (!io_timeout || io_timeout > val) { if (INFO_GTE(MISC, 2)) rprintf(FINFO, "Setting --timeout=%d to match server\n", val); @@ -1542,17 +1733,17 @@ /* Support protocol-30 keep-alive method. */ if (msg_bytes != 0) goto invalid_msg; - iobuf.in_multiplexed = 1; if (am_sender) maybe_send_keepalive(time(NULL), MSK_ALLOW_FLUSH); + iobuf.in_multiplexed = 1; break; case MSG_DELETED: if (msg_bytes >= sizeof data) goto overflow; if (am_generator) { raw_read_buf(data, msg_bytes); - iobuf.in_multiplexed = 1; send_msg(MSG_DELETED, data, msg_bytes, 1); + iobuf.in_multiplexed = 1; break; } #ifdef ICONV_OPTION @@ -1590,7 +1781,6 @@ } else #endif raw_read_buf(data, msg_bytes); - iobuf.in_multiplexed = 1; /* A directory name was sent with the trailing null */ if (msg_bytes > 0 && !data[msg_bytes-1]) log_delete(data, S_IFDIR); @@ -1598,6 +1788,7 @@ data[msg_bytes] = '\0'; log_delete(data, S_IFREG); } + iobuf.in_multiplexed = 1; break; case MSG_SUCCESS: if (msg_bytes != (local_server ? 4+8+8 : 4)) { @@ -1619,11 +1810,11 @@ if (msg_bytes != 4) goto invalid_msg; val = raw_read_int(); - iobuf.in_multiplexed = 1; if (am_generator) got_flist_entry_status(FES_NO_SEND, val); else send_msg_int(MSG_NO_SEND, val); + iobuf.in_multiplexed = 1; break; case MSG_ERROR_SOCKET: case MSG_ERROR_UTF8: @@ -1865,6 +2056,45 @@ return u.x; } +/* Read an int32 and verify lo <= v <= hi. On out-of-range, abort with a + * protocol error naming "what". The bound is co-located with the read so it + * cannot be forgotten by a downstream user. */ +int32 read_int_bounded(int f, int32 lo, int32 hi, const char *what) +{ + int32 v = read_int(f); + if (v < lo || v > hi) { + rprintf(FERROR, "wire value %s out of range: %ld not in [%ld,%ld] [%s]\n", + what, (long)v, (long)lo, (long)hi, who_am_i()); + exit_cleanup(RERR_PROTOCOL); + } + return v; +} + +/* As read_int_bounded but for varint-encoded values. */ +int32 read_varint_bounded(int f, int32 lo, int32 hi, const char *what) +{ + int32 v = read_varint(f); + if (v < lo || v > hi) { + rprintf(FERROR, "wire value %s out of range: %ld not in [%ld,%ld] [%s]\n", + what, (long)v, (long)lo, (long)hi, who_am_i()); + exit_cleanup(RERR_PROTOCOL); + } + return v; +} + +/* Read a varint that will be used as a size_t. Rejects negative values + * (which would wrap to ~SIZE_MAX) and values exceeding the supplied max. */ +size_t read_varint_size(int f, size_t max, const char *what) +{ + int32 v = read_varint(f); + if (v < 0 || (size_t)v > max) { + rprintf(FERROR, "wire size %s out of range: %ld > %lu [%s]\n", + what, (long)v, (unsigned long)max, who_am_i()); + exit_cleanup(RERR_PROTOCOL); + } + return (size_t)v; +} + int64 read_longint(int f) { #if SIZEOF_INT64 >= 8 @@ -1971,12 +2201,42 @@ (long)sum->count, who_am_i()); exit_cleanup(RERR_PROTOCOL); } + /* Guard against integer overflow in downstream allocations sized by + * count*element_size. my_alloc uses divide-not-multiply so it is + * already wraparound-safe, but checking here gives a clearer error + * and also covers the (size_t)count * xfer_sum_len arithmetic that + * is performed *before* reaching my_alloc. */ + if (xfer_sum_len > 0 && (size_t)sum->count > SIZE_MAX / (size_t)xfer_sum_len) { + rprintf(FERROR, "Invalid checksum count %ld (too large) [%s]\n", + (long)sum->count, who_am_i()); + exit_cleanup(RERR_PROTOCOL); + } + if ((size_t)sum->count > SIZE_MAX / sizeof(struct sum_buf)) { + rprintf(FERROR, "Invalid checksum count %ld (sum_buf overflow) [%s]\n", + (long)sum->count, who_am_i()); + exit_cleanup(RERR_PROTOCOL); + } sum->blength = read_int(f); if (sum->blength < 0 || sum->blength > max_blength) { rprintf(FERROR, "Invalid block length %ld [%s]\n", (long)sum->blength, who_am_i()); exit_cleanup(RERR_PROTOCOL); } + if (sum->count && sum->blength == 0) { + rprintf(FERROR, "Invalid zero block length [%s]\n", + who_am_i()); + exit_cleanup(RERR_PROTOCOL); + } +#if SIZEOF_CAPITAL_OFF_T < 8 + /* The append-mode callers compute (OFF_T)count * blength; on a 32-bit + * OFF_T that product can wrap even though both factors are individually + * in range, corrupting the lseek/loop bounds. Reject it early. */ + if (sum->blength > 0 && sum->count > MAX_INT32 / sum->blength) { + rprintf(FERROR, "checksum count*blength overflows OFF_T [%s]\n", + who_am_i()); + exit_cleanup(RERR_PROTOCOL); + } +#endif sum->s2length = protocol_version < 27 ? csum_length : (int)read_int(f); if (sum->s2length < 0 || sum->s2length > xfer_sum_len) { rprintf(FERROR, "Invalid checksum length %d [%s]\n", @@ -2088,7 +2348,7 @@ void write_varint(int f, int32 x) { - char b[5]; + char b[5] = {0}; /* {0} only to satisfy the analyzer: it doesn't model SIVAL initialising b[1..4] */ uchar bit; int cnt; @@ -2110,7 +2370,7 @@ void write_varlong(int f, int64 x, uchar min_bytes) { - char b[9]; + char b[9] = {0}; /* {0} only to satisfy the analyzer: it doesn't model SIVAL64 initialising b[1..8] */ uchar bit; int cnt = 8; @@ -2291,6 +2551,7 @@ { static int32 prev_positive = -1, prev_negative = 1; int32 *prev_ptr, num; + uint32 unum; char b[4]; if (protocol_version < 30) @@ -2310,11 +2571,20 @@ b[3] = CVAL(b, 0) & ~0x80; b[0] = b[1]; read_buf(f, b+1, 2); - num = IVAL(b, 0); + unum = IVAL(b, 0); } else - num = (UVAL(b,0)<<8) + UVAL(b,1) + *prev_ptr; + unum = (UVAL(b,0)<<8) + UVAL(b,1) + (uint32)*prev_ptr; } else - num = UVAL(b, 0) + *prev_ptr; + unum = UVAL(b, 0) + (uint32)*prev_ptr; + /* A peer-supplied index that overflows a signed int32 (used unchecked as a + * file-list index) is a protocol violation -- reject it here rather than + * relying on every downstream consumer to bounds-check. */ + if (unum > (uint32)MAX_INT32) { + rprintf(FERROR, "Invalid file index: %lu [%s]\n", + (unsigned long)unum, who_am_i()); + exit_cleanup(RERR_PROTOCOL); + } + num = (int32)unum; *prev_ptr = num; if (prev_ptr == &prev_negative) num = -num; diff -Nru rsync-3.4.1+ds1/latest-year.h rsync-3.5.0+ds1/latest-year.h --- rsync-3.4.1+ds1/latest-year.h 2025-01-14 18:53:23.000000000 +0000 +++ rsync-3.5.0+ds1/latest-year.h 2026-04-28 04:29:48.000000000 +0000 @@ -1 +1 @@ -#define LATEST_YEAR "2025" +#define LATEST_YEAR "2026" diff -Nru rsync-3.4.1+ds1/lib/acl.c rsync-3.5.0+ds1/lib/acl.c --- rsync-3.4.1+ds1/lib/acl.c 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/lib/acl.c 2026-07-20 04:05:30.000000000 +0000 @@ -0,0 +1,451 @@ +/* + * POSIX ACL get/set/delete via the generic xattr syscalls. + * + * POSIX ACLs are stored by the kernel as the "system.posix_acl_access" and + * "system.posix_acl_default" extended attributes, in a fixed little-endian + * wire format (see include/acl_ea.h in the acl package). By serializing that + * format ourselves and using fgetxattr/fsetxattr on a held O_NOFOLLOW fd -- or + * getxattrat/setxattrat(AT_SYMLINK_NOFOLLOW) on a dirfd+leaf -- we get a + * symlink-race-safe ACL primitive that also covers the *default* ACL, which + * libacl's fd API (acl_get_fd/acl_set_fd, access-only) cannot. + * + * This file knows nothing about rsync's globals or its internal ACL form: it + * speaks a neutral (tag, perm, id) entry array, which makes it directly + * comparable against the system libacl in the t_acl unit test. + * + * Copyright (C) 2026 Wayne Davison & the rsync project + * + * 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" +#include "acl.h" + +#ifdef SUPPORT_ACL_FD + +#include +#include +#include +#include +#include /* AT_SYMLINK_NOFOLLOW */ + +#if defined HAVE_SYS_XATTR_H +#include +#elif defined HAVE_ATTR_XATTR_H +#include +#endif + +#ifdef HAVE_XATTRAT_SYSCALLS +#include +/* Self-contained copy of the kernel's struct xattr_args (stable ABI: an + * 8-byte-aligned u64 pointer, then two u32s). Defined locally to avoid + * pulling , whose XATTR_* macros clash with . */ +struct rsync_xattr_args { + uint64_t value __attribute__((aligned(8))); + uint32_t size; + uint32_t flags; +}; +#endif + +/* Linux 2.4 didn't have a distinct ENOATTR. */ +#ifndef ENOATTR +#define ENOATTR ENODATA +#endif + +#define ACL_XATTR_ACCESS "system.posix_acl_access" +#define ACL_XATTR_DEFAULT "system.posix_acl_default" + +/* On-disk layout: a 4-byte LE version header followed by 8-byte LE entries. */ +#define ACL_EA_VERSION 0x0002 +#define ACL_EA_HDR_LEN 4 +#define ACL_EA_ENT_LEN 8 + +/* === little-endian (de)serialization (host-endianness independent) === */ + +static void put_le16(unsigned char *p, uint16_t v) +{ + p[0] = (unsigned char)(v & 0xff); + p[1] = (unsigned char)((v >> 8) & 0xff); +} + +static void put_le32(unsigned char *p, uint32_t v) +{ + p[0] = (unsigned char)(v & 0xff); + p[1] = (unsigned char)((v >> 8) & 0xff); + p[2] = (unsigned char)((v >> 16) & 0xff); + p[3] = (unsigned char)((v >> 24) & 0xff); +} + +static uint16_t get_le16(const unsigned char *p) +{ + return (uint16_t)(p[0] | (p[1] << 8)); +} + +static uint32_t get_le32(const unsigned char *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) + | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static int is_named_tag(uint16_t tag) +{ + return tag == RACL_USER || tag == RACL_GROUP; +} + +/* Canonical order: tag ascending, then id ascending within a tag. This is + * the order libacl's __acl_reorder_obj_p() produces and what the kernel's + * validator expects (USER_OBJ, USER*, GROUP_OBJ, GROUP*, MASK, OTHER). */ +static int ent_compare(const void *a, const void *b) +{ + const rsync_acl_ent *x = a, *y = b; + if (x->tag != y->tag) + return x->tag < y->tag ? -1 : 1; + if (x->id != y->id) + return x->id < y->id ? -1 : 1; + return 0; +} + +/* Serialize entries into a freshly-malloc'd xattr buffer (canonical order). */ +static unsigned char *acl_to_xattr(const rsync_acl_ent *ents, int count, size_t *len_out) +{ + size_t len = ACL_EA_HDR_LEN + (size_t)count * ACL_EA_ENT_LEN; + unsigned char *buf = malloc(len); + rsync_acl_ent *sorted = NULL; + unsigned char *p; + int i; + + if (!buf) + return NULL; + if (count > 1) { + sorted = malloc((size_t)count * sizeof sorted[0]); + if (!sorted) { + free(buf); + return NULL; + } + memcpy(sorted, ents, (size_t)count * sizeof sorted[0]); + qsort(sorted, count, sizeof sorted[0], ent_compare); + ents = sorted; + } + + put_le32(buf, ACL_EA_VERSION); + p = buf + ACL_EA_HDR_LEN; + for (i = 0; i < count; i++, p += ACL_EA_ENT_LEN) { + put_le16(p, ents[i].tag); + put_le16(p + 2, ents[i].perm); + put_le32(p + 4, is_named_tag(ents[i].tag) ? ents[i].id : RACL_UNDEFINED_ID); + } + + if (sorted) + free(sorted); + *len_out = len; + return buf; +} + +/* Parse an xattr buffer into a malloc'd entry array (canonical order). */ +static int xattr_to_acl(const unsigned char *buf, size_t len, + rsync_acl_ent **out, int *count_out) +{ + rsync_acl_ent *ents; + const unsigned char *p; + int n, i; + + if (len < ACL_EA_HDR_LEN || (len - ACL_EA_HDR_LEN) % ACL_EA_ENT_LEN != 0 + || get_le32(buf) != ACL_EA_VERSION) { + errno = EINVAL; + return -1; + } + n = (int)((len - ACL_EA_HDR_LEN) / ACL_EA_ENT_LEN); + + ents = n ? malloc((size_t)n * sizeof ents[0]) : NULL; + if (n && !ents) + return -1; + p = buf + ACL_EA_HDR_LEN; + for (i = 0; i < n; i++, p += ACL_EA_ENT_LEN) { + ents[i].tag = get_le16(p); + ents[i].perm = get_le16(p + 2); + ents[i].id = is_named_tag(ents[i].tag) ? get_le32(p + 4) : RACL_UNDEFINED_ID; + } + if (n > 1) + qsort(ents, n, sizeof ents[0], ent_compare); + + *out = ents; + *count_out = n; + return 0; +} + +/* === syscall dispatchers (fd-variant vs at-variant) === */ + +/* Pre-6.13 fallback for the dirfd+leaf at-variants: address the leaf as + * /proc/self/fd// and use the l*xattr (no-follow-leaf) calls. The + * /proc/self/fd/ magic symlink resolves to the pinned parent inode -- a + * raced parent symlink cannot redirect it -- and l*xattr does not follow a raced + * leaf symlink, so this is race-safe without the Linux 6.13 *xattrat syscalls, as + * long as procfs is mounted. (`leaf` is a single component, <= NAME_MAX.) + * Returns 0 and fills `buf`, or -1 with ENAMETOOLONG. */ +static int proc_fd_leaf_path(char *buf, size_t buflen, int dirfd, const char *leaf) +{ + int n = snprintf(buf, buflen, "/proc/self/fd/%d/%s", dirfd, leaf); + if (n < 0 || (size_t)n >= buflen) { + errno = ENAMETOOLONG; + return -1; + } + return 0; +} + +static ssize_t do_getxattr(int fd, int dirfd, const char *leaf, + const char *name, void *val, size_t size) +{ + char p[MAXPATHLEN]; + + if (fd >= 0) + return fgetxattr(fd, name, val, size); +#ifdef HAVE_XATTRAT_SYSCALLS + { + struct rsync_xattr_args args; + ssize_t ret; + args.value = (uint64_t)(uintptr_t)val; + args.size = (uint32_t)size; + args.flags = 0; + ret = syscall(SYS_getxattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW, + name, &args, sizeof args); + if (ret != -1 || errno != ENOSYS) + return ret; + /* ENOSYS: kernel < 6.13 -- fall through to the /proc compat. */ + } +#endif + if (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0) + return -1; + return lgetxattr(p, name, val, size); +} + +static int do_setxattr(int fd, int dirfd, const char *leaf, + const char *name, const void *val, size_t size) +{ + char p[MAXPATHLEN]; + + if (fd >= 0) + return fsetxattr(fd, name, val, size, 0); +#ifdef HAVE_XATTRAT_SYSCALLS + { + struct rsync_xattr_args args; + int ret; + args.value = (uint64_t)(uintptr_t)val; + args.size = (uint32_t)size; + args.flags = 0; /* replace */ + ret = syscall(SYS_setxattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW, + name, &args, sizeof args); + if (ret != -1 || errno != ENOSYS) + return ret; + } +#endif + if (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0) + return -1; + return lsetxattr(p, name, val, size, 0); +} + +static int do_removexattr(int fd, int dirfd, const char *leaf, const char *name) +{ + char p[MAXPATHLEN]; + + if (fd >= 0) + return fremovexattr(fd, name); +#ifdef HAVE_XATTRAT_SYSCALLS + { + int ret = syscall(SYS_removexattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW, name); + if (ret != -1 || errno != ENOSYS) + return ret; + } +#endif + if (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0) + return -1; + return lremovexattr(p, name); +} + +/* Read the whole named xattr into a malloc'd buffer, growing as needed. */ +static int read_full_xattr(int fd, int dirfd, const char *leaf, + const char *name, unsigned char **buf_out, size_t *len_out) +{ + unsigned char *buf = NULL; + size_t size = 0; + int tries; + + for (tries = 0; tries < 8; tries++) { + ssize_t n = do_getxattr(fd, dirfd, leaf, name, size ? buf : NULL, size); + if (n >= 0) { + if (size == 0) { + /* First call just learned the length. */ + size = n ? (size_t)n : 1; + buf = malloc(size); + if (!buf) + return -1; + continue; + } + *buf_out = buf; + *len_out = (size_t)n; + return 0; + } + if (errno == ERANGE) { /* grew under us: re-probe the size */ + if (buf) + free(buf); + buf = NULL; + size = 0; + continue; + } + if (buf) + free(buf); + return -1; /* ENODATA / EOPNOTSUPP / ENOSYS / ... in errno */ + } + if (buf) + free(buf); + errno = ERANGE; + return -1; +} + +/* === public API === */ + +static int acl_get_common(int fd, int dirfd, const char *leaf, + int want_default, rsync_acl_ent **entries, int *count) +{ + const char *name = want_default ? ACL_XATTR_DEFAULT : ACL_XATTR_ACCESS; + unsigned char *buf; + size_t len; + int rc; + + *entries = NULL; + *count = 0; + + if (read_full_xattr(fd, dirfd, leaf, name, &buf, &len) < 0) { + if (errno == ENODATA || errno == ENOATTR) + return 0; /* no explicit ACL present */ + return -1; /* EOPNOTSUPP / ENOSYS / real error */ + } + + rc = xattr_to_acl(buf, len, entries, count); + free(buf); + return rc; +} + +int xacl_get_fd(int fd, int want_default, rsync_acl_ent **entries, int *count) +{ + return acl_get_common(fd, -1, NULL, want_default, entries, count); +} + +int xacl_get_at(int dirfd, const char *leaf, int want_default, + rsync_acl_ent **entries, int *count) +{ + return acl_get_common(-1, dirfd, leaf, want_default, entries, count); +} + +static int acl_set_common(int fd, int dirfd, const char *leaf, + int want_default, const rsync_acl_ent *ents, int count) +{ + const char *name = want_default ? ACL_XATTR_DEFAULT : ACL_XATTR_ACCESS; + unsigned char *buf; + size_t len; + int rc, save_errno; + + buf = acl_to_xattr(ents, count, &len); + if (!buf) { + errno = ENOMEM; + return -1; + } + rc = do_setxattr(fd, dirfd, leaf, name, buf, len); + save_errno = errno; + free(buf); + errno = save_errno; + return rc < 0 ? -1 : 0; +} + +int xacl_set_fd(int fd, int want_default, const rsync_acl_ent *ents, int count) +{ + return acl_set_common(fd, -1, NULL, want_default, ents, count); +} + +int xacl_set_at(int dirfd, const char *leaf, int want_default, + const rsync_acl_ent *ents, int count) +{ + return acl_set_common(-1, dirfd, leaf, want_default, ents, count); +} + +static int acl_del_default_common(int fd, int dirfd, const char *leaf) +{ + if (do_removexattr(fd, dirfd, leaf, ACL_XATTR_DEFAULT) < 0) { + if (errno == ENODATA || errno == ENOATTR) + return 0; /* already absent: success, like acl_delete_def_file */ + return -1; + } + return 0; +} + +int xacl_del_default_fd(int fd) +{ + return acl_del_default_common(fd, -1, NULL); +} + +int xacl_del_default_at(int dirfd, const char *leaf) +{ + return acl_del_default_common(-1, dirfd, leaf); +} + +/* True iff /proc/self/fd magic symlinks are usable, so the dirfd+leaf at-variants + * work race-safely via the /proc compat on a pre-6.13 kernel. */ +static int proc_self_fd_usable(void) +{ + int dfd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + char p[64]; + int usable = 0; + + if (dfd < 0) + return 0; + if (snprintf(p, sizeof p, "/proc/self/fd/%d/.", dfd) < (int)sizeof p) { + /* The probe attr is absent; the path resolving (any errno but + * ENOENT/ENOTDIR -- e.g. ENODATA/ENOTSUP/EACCES) means procfs gives us + * the magic fd-symlink we need. */ + errno = 0; + lgetxattr(p, "user.rsync_acl_probe", NULL, 0); + usable = !(errno == ENOENT || errno == ENOTDIR); + } + close(dfd); + return usable; +} + +int xacl_at_available(void) +{ + static int avail = -1; + + if (avail < 0) { +#ifdef HAVE_XATTRAT_SYSCALLS + /* Probe the *xattrat syscall directly (not via do_getxattr's /proc + * fallback): any errno other than ENOSYS means it is present (6.13+). */ + struct rsync_xattr_args args; + args.value = 0; + args.size = 0; + args.flags = 0; + errno = 0; + syscall(SYS_getxattrat, AT_FDCWD, ".", AT_SYMLINK_NOFOLLOW, + "user.rsync_acl_probe", &args, sizeof args); + if (errno != ENOSYS) { + avail = 1; + return avail; + } +#endif + /* No *xattrat syscalls (pre-6.13, or a kernel built without them): the dirfd+leaf ACL ops + * are still race-safe via /proc/self/fd if procfs is mounted, closing + * the parent-symlink-race gap that otherwise forces the path-based set. */ + avail = proc_self_fd_usable(); + } + return avail; +} + +#endif /* SUPPORT_ACL_FD */ diff -Nru rsync-3.4.1+ds1/lib/acl.h rsync-3.5.0+ds1/lib/acl.h --- rsync-3.4.1+ds1/lib/acl.h 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/lib/acl.h 2026-07-20 04:05:30.000000000 +0000 @@ -0,0 +1,74 @@ +/* + * POSIX ACL get/set/delete via the generic xattr syscalls, addressing the + * kernel "system.posix_acl_{access,default}" attributes directly so that the + * operation can be confined to a held O_NOFOLLOW fd (fsetxattr) or a + * dirfd+leaf with AT_SYMLINK_NOFOLLOW (setxattrat). This replaces the path- + * based libacl acl_*_file() calls on Linux, where those would re-resolve the + * path and could be redirected by a parent-component symlink race. + * + * Copyright (C) 2026 Wayne Davison & the rsync project + * + * 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. + */ + +#ifdef SUPPORT_ACL_FD + +#include + +/* A single logical POSIX ACL entry in host-native form. The tag values are + * the stable kernel ABI numbers (== the libacl ACL_* constants), so they map + * straight onto the on-disk e_tag without translation. */ +typedef struct { + uint16_t tag; /* RACL_USER_OBJ / USER / GROUP_OBJ / GROUP / MASK / OTHER */ + uint16_t perm; /* permission bits: read=4, write=2, execute=1 */ + uint32_t id; /* uid/gid for USER/GROUP entries; RACL_UNDEFINED_ID otherwise */ +} rsync_acl_ent; + +#define RACL_USER_OBJ 0x01 +#define RACL_USER 0x02 +#define RACL_GROUP_OBJ 0x04 +#define RACL_GROUP 0x08 +#define RACL_MASK 0x10 +#define RACL_OTHER 0x20 + +#define RACL_UNDEFINED_ID ((uint32_t)-1) + +/* Read the access (want_default==0) or default (want_default!=0) ACL. + * + * On success returns 0 and sets *entries to a malloc()ed array of *count + * entries (the caller frees it with free(); *entries may be NULL when + * *count==0, which means "no explicit ACL present" -- e.g. ENODATA). + * + * On failure returns -1 with errno set. Callers distinguish: + * ENOTSUP/EOPNOTSUPP - this filesystem has no ACL support (may differ per fs) + * ENOSYS - the at-variant syscalls are unavailable on this kernel + * The fd-variant operates on a held, already-NOFOLLOW-opened descriptor. The + * at-variant resolves leaf relative to dirfd and never follows a leaf symlink. */ +int xacl_get_fd(int fd, int want_default, rsync_acl_ent **entries, int *count); +int xacl_get_at(int dirfd, const char *leaf, int want_default, rsync_acl_ent **entries, int *count); + +/* Write the given entries as the access/default ACL. The entries are emitted + * in canonical order; the kernel validates them (a malformed set -> EINVAL). */ +int xacl_set_fd(int fd, int want_default, const rsync_acl_ent *entries, int count); +int xacl_set_at(int dirfd, const char *leaf, int want_default, const rsync_acl_ent *entries, int count); + +/* Delete a directory's default ACL. A missing default ACL is success. */ +int xacl_del_default_fd(int fd); +int xacl_del_default_at(int dirfd, const char *leaf); + +/* Cached runtime probe: are the *xattrat syscalls usable on this kernel? + * Returns 0 when they are absent (so callers can fall back) or unbuilt. */ +int xacl_at_available(void); + +#endif /* SUPPORT_ACL_FD */ diff -Nru rsync-3.4.1+ds1/lib/md5-asm-x86_64.S rsync-3.5.0+ds1/lib/md5-asm-x86_64.S --- rsync-3.4.1+ds1/lib/md5-asm-x86_64.S 2022-03-04 01:00:57.000000000 +0000 +++ rsync-3.5.0+ds1/lib/md5-asm-x86_64.S 2026-06-09 02:26:24.000000000 +0000 @@ -34,7 +34,9 @@ #endif .text -.align 16 +/* .balign = N bytes everywhere; bare .align means 2^N on Mach-O (would ask + * for 64KB alignment and trip a macOS linker warning). */ +.balign 16 .globl md5_process_asm md5_process_asm: diff -Nru rsync-3.4.1+ds1/lib/md5.c rsync-3.5.0+ds1/lib/md5.c --- rsync-3.4.1+ds1/lib/md5.c 2022-09-10 18:39:37.000000000 +0000 +++ rsync-3.5.0+ds1/lib/md5.c 2025-08-23 07:31:40.000000000 +0000 @@ -197,7 +197,7 @@ memcpy(ctx->buffer + left, input, length); } -static uchar md5_padding[CSUM_CHUNK] = { 0x80 }; +static const uchar md5_padding[CSUM_CHUNK] = { 0x80 }; void md5_result(md_context *ctx, uchar digest[MD5_DIGEST_LEN]) { diff -Nru rsync-3.4.1+ds1/lib/mdfour.c rsync-3.5.0+ds1/lib/mdfour.c --- rsync-3.4.1+ds1/lib/mdfour.c 2020-05-25 05:50:51.000000000 +0000 +++ rsync-3.5.0+ds1/lib/mdfour.c 2026-06-08 10:54:57.000000000 +0000 @@ -89,8 +89,8 @@ int i; for (i = 0; i < MD4_DIGEST_LEN; i++) { - M[i] = (in[i*4+3] << 24) | (in[i*4+2] << 16) - | (in[i*4+1] << 8) | (in[i*4+0] << 0); + M[i] = ((uint32)in[i*4+3] << 24) | ((uint32)in[i*4+2] << 16) + | ((uint32)in[i*4+1] << 8) | ((uint32)in[i*4+0] << 0); } } diff -Nru rsync-3.4.1+ds1/lib/pool_alloc.c rsync-3.5.0+ds1/lib/pool_alloc.c --- rsync-3.4.1+ds1/lib/pool_alloc.c 2024-11-20 05:33:30.000000000 +0000 +++ rsync-3.5.0+ds1/lib/pool_alloc.c 2026-07-20 04:05:31.000000000 +0000 @@ -44,6 +44,32 @@ #define PTR_ADD(b,o) ( (void*) ((char*)(b) + (o)) ) #define PTR_SUB(b,o) ( (void*) ((char*)(b) - (o)) ) +/* Under AddressSanitizer, fence each pool_alloc() chunk with a poisoned + * redzone just below it (allocations grow downward from the top of an extent). + * A bump allocator hands out chunks from one big malloc, so ASan cannot see a + * write that underflows one chunk into its neighbour -- e.g. a miscomputed + * F_SUM() reaching before a file_struct's extras. The redzone turns that into + * a hard ASan report. We unpoison a whole extent whenever its space is reused + * (reset/reclaim), so legitimate later allocations never trip over old + * redzones; ASan unpoisons freed extents itself via free(). */ +#if defined(__SANITIZE_ADDRESS__) +# define POOL_ASAN 1 +#elif defined(__has_feature) +# if __has_feature(address_sanitizer) +# define POOL_ASAN 1 +# endif +#endif + +#ifdef POOL_ASAN +# include +# define POOL_REDZONE 16 /* >= the largest pool-relative underflow we guard */ +# define POOL_POISON(p,n) ASAN_POISON_MEMORY_REGION((p), (n)) +# define POOL_UNPOISON(p,n) ASAN_UNPOISON_MEMORY_REGION((p), (n)) +#else +# define POOL_POISON(p,n) ((void)0) +# define POOL_UNPOISON(p,n) ((void)0) +#endif + alloc_pool_t pool_create(size_t size, size_t quantum, void (*bomb)(const char*, const char*, int), int flags) { @@ -165,7 +191,18 @@ pool->extents->free -= len; - return PTR_ADD(pool->extents->start, pool->extents->free); + { + void *ret = PTR_ADD(pool->extents->start, pool->extents->free); +#ifdef POOL_ASAN + size_t rz = pool->extents->free < POOL_REDZONE + ? pool->extents->free : POOL_REDZONE; + if (rz) { + pool->extents->free -= rz; + POOL_POISON(PTR_ADD(pool->extents->start, pool->extents->free), rz); + } +#endif + return ret; + } bomb_out: if (pool->bomb) @@ -215,6 +252,10 @@ if (!cur) return; + /* This extent's space may be reused (and POOL_CLEAR may memset it) + * below, so drop any redzones in it first. */ + POOL_UNPOISON(cur->start, pool->size); + if (!prev) { /* The "live" extent is kept ready for more allocations. */ if (cur->free + cur->bound + len >= pool->size) { @@ -272,6 +313,8 @@ if (!cur) return; + POOL_UNPOISON(cur->start, pool->size); + if (addr == PTR_ADD(cur->start, cur->free)) { if (prev) { prev->next = NULL; diff -Nru rsync-3.4.1+ds1/lib/sysacls.c rsync-3.5.0+ds1/lib/sysacls.c --- rsync-3.4.1+ds1/lib/sysacls.c 2022-08-14 17:15:08.000000000 +0000 +++ rsync-3.5.0+ds1/lib/sysacls.c 2026-07-20 04:05:32.000000000 +0000 @@ -180,6 +180,26 @@ return acl_free(the_acl); } +#ifdef HAVE_LIBACL_AT +/* Dirfd/AT-flag ACL ops via the new libacl, + * race-safe on every Linux kernel. at_flags is AT_SYMLINK_NOFOLLOW (dirfd+leaf) + * or AT_EMPTY_PATH (operate on an open fd passed as dirfd, path ""). */ +SMB_ACL_T sys_acl_get_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type) +{ + return acl_get_file_at(dirfd, path_p, at_flags, type); +} + +int sys_acl_set_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type, SMB_ACL_T theacl) +{ + return acl_set_file_at(dirfd, path_p, at_flags, type, theacl); +} + +int sys_acl_delete_def_file_at(int dirfd, const char *path_p, int at_flags) +{ + return acl_delete_def_file_at(dirfd, path_p, at_flags); +} +#endif /* HAVE_LIBACL_AT */ + #elif defined(HAVE_TRU64_ACLS) /*--------------------------------------------*/ /* * The interface to DEC/Compaq Tru64 UNIX ACLs @@ -479,12 +499,20 @@ return acl_d; } -#if 0 -SMB_ACL_T sys_acl_get_fd(int fd) +#ifdef HAVE_SOLARIS_ACLS +/* facl(2)-based ACL read on a held fd (no path re-resolution). Solaris stores + * the access and default ACLs as one combined ACL; split out the requested half. */ +SMB_ACL_T sys_acl_get_fd_type(int fd, SMB_ACL_TYPE_T type) { SMB_ACL_T acl_d; int count; /* # of ACL entries allocated */ int naccess; /* # of access ACL entries */ + int ndefault; /* # of default ACL entries */ + + if (type != SMB_ACL_TYPE_ACCESS && type != SMB_ACL_TYPE_DEFAULT) { + errno = EINVAL; + return NULL; + } count = INITIAL_ACL_SIZE; if ((acl_d = sys_acl_init(count)) == NULL) { @@ -511,17 +539,39 @@ } /* - * calculate the number of access ACL entries + * calculate the number of access and default ACL entries */ for (naccess = 0; naccess < count; naccess++) { if (acl_d->acl[naccess].a_type & ACL_DEFAULT) break; } + ndefault = count - naccess; - acl_d->count = naccess; + if (type == SMB_ACL_TYPE_DEFAULT) { + int i, j; + + /* + * Default ACL entries follow the access entries in the combined + * Solaris ACL; move them to the front of the wrapper and clear + * ACL_DEFAULT so the caller sees a plain default ACL. + */ + for (i = 0, j = naccess; i < ndefault; i++, j++) { + acl_d->acl[i] = acl_d->acl[j]; + acl_d->acl[i].a_type &= ~ACL_DEFAULT; + } + + acl_d->count = ndefault; + } else { + acl_d->count = naccess; + } return acl_d; } + +SMB_ACL_T sys_acl_get_fd(int fd) +{ + return sys_acl_get_fd_type(fd, SMB_ACL_TYPE_ACCESS); +} #endif int sys_acl_get_info(SMB_ACL_ENTRY_T entry, SMB_ACL_TAG_T *tag_type_p, uint32 *bits_p, id_t *u_g_id_p) @@ -728,14 +778,108 @@ return ret; } -#if 0 -int sys_acl_set_fd(int fd, SMB_ACL_T acl_d) +#ifdef HAVE_SOLARIS_ACLS +/* facl(2)-based ACL write on a held fd (no path re-resolution). Setting an ACL + * on a directory replaces the combined access+default set, so for a dir read the + * other half through the fd, merge, and write the combined ACL back. Mirrors the + * path-based sys_acl_set_file() below. */ +int sys_acl_set_fd_type(int fd, SMB_ACL_TYPE_T type, SMB_ACL_T acl_d) { + struct stat s; + struct acl *acl_p; + int acl_count; + struct acl *acl_buf = NULL; + int ret; + + if (type != SMB_ACL_TYPE_ACCESS && type != SMB_ACL_TYPE_DEFAULT) { + errno = EINVAL; + return -1; + } + if (acl_sort(acl_d) != 0) { return -1; } - return facl(fd, SETACL, acl_d->count, &acl_d->acl[0]); + acl_p = &acl_d->acl[0]; + acl_count = acl_d->count; + + if (fstat(fd, &s) != 0) { + return -1; + } + if (S_ISDIR(s.st_mode)) { + SMB_ACL_T acc_acl; + SMB_ACL_T def_acl; + SMB_ACL_T tmp_acl; + int i; + + if (type == SMB_ACL_TYPE_ACCESS) { + acc_acl = acl_d; + def_acl = tmp_acl = sys_acl_get_fd_type(fd, SMB_ACL_TYPE_DEFAULT); + } else { + def_acl = acl_d; + acc_acl = tmp_acl = sys_acl_get_fd_type(fd, SMB_ACL_TYPE_ACCESS); + } + + if (tmp_acl == NULL) { + return -1; + } + + acl_count = acc_acl->count + def_acl->count; + acl_p = acl_buf = SMB_MALLOC_ARRAY(struct acl, acl_count); + + if (acl_buf == NULL) { + sys_acl_free_acl(tmp_acl); + errno = ENOMEM; + return -1; + } + + /* Concatenate access + default, then mark the default half. */ + memcpy(&acl_buf[0], &acc_acl->acl[0], + acc_acl->count * sizeof acl_buf[0]); + memcpy(&acl_buf[acc_acl->count], &def_acl->acl[0], + def_acl->count * sizeof acl_buf[0]); + + for (i = acc_acl->count; i < acl_count; i++) { + acl_buf[i].a_type |= ACL_DEFAULT; + } + + sys_acl_free_acl(tmp_acl); + + } else if (type != SMB_ACL_TYPE_ACCESS) { + errno = EINVAL; + return -1; + } + + ret = facl(fd, SETACL, acl_count, acl_p); + + SAFE_FREE(acl_buf); + + return ret; +} + +int sys_acl_set_fd(int fd, SMB_ACL_T acl_d) +{ + return sys_acl_set_fd_type(fd, SMB_ACL_TYPE_ACCESS, acl_d); +} + +int sys_acl_delete_def_fd(int fd) +{ + SMB_ACL_T acl_d; + int ret; + + /* + * Fetching the access ACL through the fd and rewriting it deletes the + * default ACL, without re-resolving the path. + */ + if ((acl_d = sys_acl_get_fd_type(fd, SMB_ACL_TYPE_ACCESS)) == NULL) { + return -1; + } + + ret = facl(fd, SETACL, acl_d->count, acl_d->acl); + + sys_acl_free_acl(acl_d); + + return ret; } #endif diff -Nru rsync-3.4.1+ds1/lib/sysacls.h rsync-3.5.0+ds1/lib/sysacls.h --- rsync-3.4.1+ds1/lib/sysacls.h 2022-08-14 17:15:08.000000000 +0000 +++ rsync-3.5.0+ds1/lib/sysacls.h 2026-07-20 04:05:32.000000000 +0000 @@ -301,7 +301,18 @@ int sys_acl_set_file(const char *name, SMB_ACL_TYPE_T acltype, SMB_ACL_T theacl); int sys_acl_set_fd(int fd, SMB_ACL_T theacl); int sys_acl_delete_def_file(const char *name); +#ifdef HAVE_SOLARIS_ACLS +SMB_ACL_T sys_acl_get_fd_type(int fd, SMB_ACL_TYPE_T type); +int sys_acl_set_fd_type(int fd, SMB_ACL_TYPE_T type, SMB_ACL_T theacl); +int sys_acl_delete_def_fd(int fd); +#endif int sys_acl_free_acl(SMB_ACL_T the_acl); int no_acl_syscall_error(int err); +#ifdef HAVE_LIBACL_AT +SMB_ACL_T sys_acl_get_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type); +int sys_acl_set_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type, SMB_ACL_T theacl); +int sys_acl_delete_def_file_at(int dirfd, const char *path_p, int at_flags); +#endif + #endif /* SUPPORT_ACLS */ diff -Nru rsync-3.4.1+ds1/lib/sysxattrs.c rsync-3.5.0+ds1/lib/sysxattrs.c --- rsync-3.4.1+ds1/lib/sysxattrs.c 2022-01-16 01:21:01.000000000 +0000 +++ rsync-3.5.0+ds1/lib/sysxattrs.c 2026-07-20 04:05:32.000000000 +0000 @@ -45,16 +45,31 @@ return lsetxattr(path, name, value, size, 0); } +int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size) +{ + return fsetxattr(filedes, name, value, size, 0); +} + int sys_lremovexattr(const char *path, const char *name) { return lremovexattr(path, name); } +int sys_fremovexattr(int filedes, const char *name) +{ + return fremovexattr(filedes, name); +} + ssize_t sys_llistxattr(const char *path, char *list, size_t size) { return llistxattr(path, list, size); } +ssize_t sys_flistxattr(int filedes, char *list, size_t size) +{ + return flistxattr(filedes, list, size); +} + #elif HAVE_OSX_XATTRS ssize_t sys_lgetxattr(const char *path, const char *name, void *value, size_t size) @@ -89,16 +104,31 @@ return setxattr(path, name, value, size, 0, XATTR_NOFOLLOW); } +int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size) +{ + return fsetxattr(filedes, name, value, size, 0, 0); +} + int sys_lremovexattr(const char *path, const char *name) { return removexattr(path, name, XATTR_NOFOLLOW); } +int sys_fremovexattr(int filedes, const char *name) +{ + return fremovexattr(filedes, name, 0); +} + ssize_t sys_llistxattr(const char *path, char *list, size_t size) { return listxattr(path, list, size, XATTR_NOFOLLOW); } +ssize_t sys_flistxattr(int filedes, char *list, size_t size) +{ + return flistxattr(filedes, list, size, 0); +} + #elif HAVE_FREEBSD_XATTRS ssize_t sys_lgetxattr(const char *path, const char *name, void *value, size_t size) @@ -116,27 +146,46 @@ return extattr_set_link(path, EXTATTR_NAMESPACE_USER, name, value, size); } +int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size) +{ + return extattr_set_fd(filedes, EXTATTR_NAMESPACE_USER, name, value, size); +} + int sys_lremovexattr(const char *path, const char *name) { return extattr_delete_link(path, EXTATTR_NAMESPACE_USER, name); } -ssize_t sys_llistxattr(const char *path, char *list, size_t size) +int sys_fremovexattr(int filedes, const char *name) +{ + return extattr_delete_fd(filedes, EXTATTR_NAMESPACE_USER, name); +} + +/* Turn the FreeBSD extattr_list_xx() output (a single length byte before each + * name, no '\0' terminator) into the series of null-terminated strings that the + * rest of rsync expects. Since the size is unchanged, transform in place. + * Shared by the path and fd list variants. */ +static ssize_t freebsd_list_finish(char *list, size_t size, ssize_t len) { unsigned char keylen; - ssize_t off, len = extattr_list_link(path, EXTATTR_NAMESPACE_USER, list, size); + ssize_t off; - if (len <= 0 || (size_t)len > size) + if (len <= 0 || size == 0) return len; - /* FreeBSD puts a single-byte length before each string, with no '\0' - * terminator. We need to change this into a series of null-terminted - * strings. Since the size is the same, we can simply transform the - * output in place. */ + if ((size_t)len >= size) { + /* FreeBSD extattr_list_xx() returns 'size' as 'len' in case there are + more data available, truncating the output, we solve this by signalling + ERANGE in case len == size so that the code in xattrs.c will retry with + a bigger buffer */ + errno = ERANGE; + return -1; + } + for (off = 0; off < len; off += keylen + 1) { keylen = ((unsigned char*)list)[off]; if (off + keylen >= len) { - /* Should be impossible, but kernel bugs happen! */ + /* Should be impossible, but bugs happen! */ errno = EINVAL; return -1; } @@ -147,6 +196,18 @@ return len; } +ssize_t sys_llistxattr(const char *path, char *list, size_t size) +{ + return freebsd_list_finish(list, size, + extattr_list_link(path, EXTATTR_NAMESPACE_USER, list, size)); +} + +ssize_t sys_flistxattr(int filedes, char *list, size_t size) +{ + return freebsd_list_finish(list, size, + extattr_list_fd(filedes, EXTATTR_NAMESPACE_USER, list, size)); +} + #elif HAVE_SOLARIS_XATTRS static ssize_t read_xattr(int attrfd, void *buf, size_t buflen) @@ -208,29 +269,59 @@ return read_xattr(attrfd, value, size); } -int sys_lsetxattr(const char *path, const char *name, const void *value, size_t size) +/* Write a datum to the already-opened attribute fd, closing it. Shared by the + * path- and fd-keyed setters below. */ +static int write_xattr(int attrfd, const void *value, size_t size) { - int attrfd; size_t bufpos; - mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP; - - if ((attrfd = attropen(path, name, O_CREAT|O_TRUNC|O_WRONLY, mode)) < 0) - return -1; + int ret = 0, saved_errno = 0; for (bufpos = 0; bufpos < size; ) { - ssize_t cnt = write(attrfd, (char*)value + bufpos, size); - if (cnt <= 0) { - if (cnt < 0 && errno == EINTR) + ssize_t cnt = write(attrfd, (const char *)value + bufpos, size - bufpos); + if (cnt < 0) { + if (errno == EINTR) continue; - bufpos = -1; + ret = -1; + saved_errno = errno; + break; + } + if (cnt == 0) { + ret = -1; + saved_errno = EIO; break; } bufpos += cnt; } - close(attrfd); + /* Don't let close() clobber the write error; do report a close() failure. */ + if (close(attrfd) < 0 && ret == 0) + return -1; + if (ret < 0 && saved_errno) + errno = saved_errno; + + return ret; +} + +int sys_lsetxattr(const char *path, const char *name, const void *value, size_t size) +{ + int attrfd; + mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP; + + if ((attrfd = attropen(path, name, O_CREAT|O_TRUNC|O_WRONLY, mode)) < 0) + return -1; + + return write_xattr(attrfd, value, size); +} + +int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size) +{ + int attrfd; + mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP; + + if ((attrfd = openat(filedes, name, O_CREAT|O_TRUNC|O_WRONLY|O_XATTR, mode)) < 0) + return -1; - return bufpos > 0 ? 0 : -1; + return write_xattr(attrfd, value, size); } int sys_lremovexattr(const char *path, const char *name) @@ -248,18 +339,29 @@ return ret; } -ssize_t sys_llistxattr(const char *path, char *list, size_t size) +int sys_fremovexattr(int filedes, const char *name) { int attrdirfd; + int ret; + + if ((attrdirfd = openat(filedes, ".", O_RDONLY|O_XATTR, 0)) < 0) + return -1; + + ret = unlinkat(attrdirfd, name, 0); + + close(attrdirfd); + + return ret; +} + +/* List the names in an already-opened attribute-dir fd, consuming it. Shared + * by the path- and fd-keyed listers below. */ +static ssize_t list_xattr(int attrdirfd, char *list, size_t size) +{ DIR *dirp; struct dirent *dp; ssize_t ret = 0; - if ((attrdirfd = attropen(path, ".", O_RDONLY)) < 0) { - errno = ENOTSUP; - return -1; - } - if ((dirp = fdopendir(attrdirfd)) == NULL) { close(attrdirfd); return -1; @@ -287,11 +389,34 @@ } closedir(dirp); - close(attrdirfd); return ret; } +ssize_t sys_llistxattr(const char *path, char *list, size_t size) +{ + int attrdirfd; + + if ((attrdirfd = attropen(path, ".", O_RDONLY)) < 0) { + errno = ENOTSUP; + return -1; + } + + return list_xattr(attrdirfd, list, size); +} + +ssize_t sys_flistxattr(int filedes, char *list, size_t size) +{ + int attrdirfd; + + if ((attrdirfd = openat(filedes, ".", O_RDONLY|O_XATTR, 0)) < 0) { + errno = ENOTSUP; + return -1; + } + + return list_xattr(attrdirfd, list, size); +} + #else #error You need to create xattr compatibility functions. diff -Nru rsync-3.4.1+ds1/lib/sysxattrs.h rsync-3.5.0+ds1/lib/sysxattrs.h --- rsync-3.4.1+ds1/lib/sysxattrs.h 2020-06-19 15:22:54.000000000 +0000 +++ rsync-3.5.0+ds1/lib/sysxattrs.h 2026-07-20 04:05:30.000000000 +0000 @@ -16,8 +16,11 @@ ssize_t sys_lgetxattr(const char *path, const char *name, void *value, size_t size); ssize_t sys_fgetxattr(int filedes, const char *name, void *value, size_t size); int sys_lsetxattr(const char *path, const char *name, const void *value, size_t size); +int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size); int sys_lremovexattr(const char *path, const char *name); +int sys_fremovexattr(int filedes, const char *name); ssize_t sys_llistxattr(const char *path, char *list, size_t size); +ssize_t sys_flistxattr(int filedes, char *list, size_t size); #else diff -Nru rsync-3.4.1+ds1/lib/wildmatch.c rsync-3.5.0+ds1/lib/wildmatch.c --- rsync-3.4.1+ds1/lib/wildmatch.c 2007-05-22 05:58:59.000000000 +0000 +++ rsync-3.5.0+ds1/lib/wildmatch.c 2026-07-20 04:05:32.000000000 +0000 @@ -89,6 +89,11 @@ p_ch = *++p; /* FALLTHROUGH */ default: + /* iwildmatch() folds the text to lower case above; fold the pattern + * char too so matching is truly case-insensitive (not just text-side). + * Without this an upper-case "hosts deny" token fails OPEN. */ + if (force_lower_case && ISUPPER(p_ch)) + p_ch = tolower(p_ch); if (t_ch != p_ch) return FALSE; continue; @@ -150,6 +155,8 @@ p_ch = *++p; if (!p_ch) return ABORT_ALL; + if (force_lower_case && ISUPPER(p_ch)) + p_ch = tolower(p_ch); if (t_ch == p_ch) matched = TRUE; } else if (p_ch == '-' && prev_ch && p[1] && p[1] != ']') { @@ -159,6 +166,8 @@ if (!p_ch) return ABORT_ALL; } + if (force_lower_case && ISUPPER(p_ch)) + p_ch = tolower(p_ch); if (t_ch <= p_ch && t_ch >= prev_ch) matched = TRUE; p_ch = 0; /* This makes "prev_ch" get set to 0. */ @@ -216,8 +225,12 @@ } else /* malformed [:class:] string */ return ABORT_ALL; p_ch = 0; /* This makes "prev_ch" get set to 0. */ - } else if (t_ch == p_ch) - matched = TRUE; + } else { + if (force_lower_case && ISUPPER(p_ch)) + p_ch = tolower(p_ch); + if (t_ch == p_ch) + matched = TRUE; + } } while (prev_ch = p_ch, (p_ch = *++p) != ']'); if (matched == special || t_ch == '/') return FALSE; diff -Nru rsync-3.4.1+ds1/loadparm.c rsync-3.5.0+ds1/loadparm.c --- rsync-3.4.1+ds1/loadparm.c 2020-09-29 20:18:28.000000000 +0000 +++ rsync-3.5.0+ds1/loadparm.c 2026-07-31 23:52:27.000000000 +0000 @@ -65,7 +65,7 @@ struct enum_list { int value; - char *name; + const char *name; }; struct parm_struct { @@ -73,7 +73,7 @@ parm_type type; parm_class class; void *ptr; - struct enum_list *enum_list; + const struct enum_list *enum_list; unsigned flags; }; @@ -95,7 +95,7 @@ static int iSectionIndex = -1; static BOOL bInGlobalSection = True; -static struct enum_list enum_syslog_facility[] = { +static const struct enum_list enum_syslog_facility[] = { #ifdef LOG_AUTH { LOG_AUTH, "auth" }, #endif @@ -164,11 +164,81 @@ /* Expand %VAR% references. Any unknown vars or unrecognized * syntax leaves the raw chars unchanged. */ -static char *expand_vars(const char *str) +enum shell_quote_context { + SHELL_UNQUOTED, + SHELL_SINGLE_QUOTED, + SHELL_DOUBLE_QUOTED +}; + +/* Characters that can turn a substituted value into shell syntax rather than + * data, in any quoting context. Quoting alone cannot be relied on here: + * context-aware escaping is correct for exactly one level of shell parsing, + * and a hook such as `sh -c '... %RSYNC_USER_NAME% ...'` re-parses the word in + * a second shell that sees the value bare. Peer-supplied values carrying any + * of these are refused instead. */ +static int shell_unsafe_value(const char *val) +{ + const char *s; + + for (s = val; *s; s++) { + /* '!' negates in command position (a hook `sh -c '%VAR% false'` + * becomes `! false` and reports success, inverting an access + * check); '~' is tilde-expanded; '{' and '}' brace-expand in + * bash and zsh. None of them execute anything on their own, + * which is why a set built from the obvious metacharacters + * missed them. */ + if (strchr("'\"`$\\;&|<>()*?[]# !~{}", *s) + || (unsigned char)*s < 0x20 || (unsigned char)*s == 0x7f) + return 1; + } + return 0; +} + +static char *expand_vars_shell_escape(const char *val, int quote_context) +{ + const char *s; + char *ret, *t; + /* A double-quoted value is deliberately BOTH backslash-escaped and + * wrapped in single quotes. The wrap is redundant for one level of + * shell parsing (and shows up as literal quotes in the value), but a + * hook such as `sh -c "... %RSYNC_USER_NAME% ..."` re-parses the word + * in a second shell, where the backslashes are already gone and only + * the quotes still protect it. */ + size_t len = quote_context == SHELL_SINGLE_QUOTED ? 0 : 2; + + for (s = val; *s; s++) { + if (quote_context == SHELL_DOUBLE_QUOTED + && strchr("\\\"`$", *s)) + len += 2; + else + len += *s == '\'' ? 4 : 1; + } + ret = new_array(char, len + 1); + t = ret; + if (quote_context != SHELL_SINGLE_QUOTED) + *t++ = '\''; + for (s = val; *s; s++) { + if (quote_context == SHELL_DOUBLE_QUOTED + && strchr("\\\"`$", *s)) { + *t++ = '\\'; + *t++ = *s; + } else if (*s == '\'') { + memcpy(t, "'\\''", 4); + t += 4; + } else + *t++ = *s; + } + if (quote_context != SHELL_SINGLE_QUOTED) + *t++ = '\''; + *t = '\0'; + return ret; +} + +static char *expand_vars(const char *str, int shell_escape) { char *buf, *t; const char *f; - int bufsize; + int bufsize, quote_context = SHELL_UNQUOTED, escaped_char = 0; if (!str || !strchr(str, '%')) return (char *)str; /* TODO change return value to const char* at some point. */ @@ -178,13 +248,35 @@ for (t = buf, f = str; bufsize && *f; ) { if (*f == '%' && isUpper(f+1)) { - char *percent = strchr(f+1, '%'); + const char *percent = strchr(f+1, '%'); if (percent && percent - f < bufsize) { char *val; strlcpy(t, f+1, percent - f); val = getenv(t); if (val) { - int len = strlcpy(t, val, bufsize+1); + char *escaped = NULL; + int len; + /* %RSYNC_*% values originate from the peer request/args. + * When the result is fed to a shell-executed hook, escape it + * for the template's current shell quote context so a value + * containing shell metacharacters can't inject. For ordinary string + * params (path, uid, gid, ...) leave them verbatim -- + * quoting there would corrupt the value (e.g. a documented + * `path = /home/%RSYNC_USER_NAME%` would become /home/'x'). */ + if (shell_escape && strncmp(t, "RSYNC_", 6) == 0) { + if (shell_unsafe_value(val)) { + /* Fail closed: the hook may be an access + * check, so skipping it is not an option. */ + rprintf(FLOG, + "refusing to run shell hook: %%%s%% holds a shell metacharacter\n", + t); + exit_cleanup(RERR_UNSUPPORTED); + } + val = escaped = expand_vars_shell_escape(val, quote_context); + } + len = strlcpy(t, val, bufsize+1); + if (escaped) + free(escaped); if (len > bufsize) break; bufsize -= len; @@ -194,6 +286,28 @@ } } } + if (shell_escape) { + if (quote_context == SHELL_SINGLE_QUOTED) { + /* Nothing is special inside '...', not even a backslash; + * only the closing quote ends it. */ + if (*f == '\'') + quote_context = SHELL_UNQUOTED; + } else if (escaped_char) + escaped_char = 0; + else if (*f == '\\') + escaped_char = 1; + else if (quote_context == SHELL_DOUBLE_QUOTED) { + /* A single quote inside "..." is literal and must not be + * taken as opening a single-quoted run -- doing so would + * de-sync the tracker and escape a later value for the + * wrong context. */ + if (*f == '"') + quote_context = SHELL_UNQUOTED; + } else if (*f == '\'') + quote_context = SHELL_SINGLE_QUOTED; + else if (*f == '"') + quote_context = SHELL_DOUBLE_QUOTED; + } *t++ = *f++; bufsize--; } @@ -213,7 +327,10 @@ /* Each "char* foo" has an associated "BOOL foo_EXP" that tracks if the string has been expanded yet or not. */ /* NOTE: use this function and all the FN_{GLOBAL,LOCAL} ones WITHOUT a trailing semicolon! */ -#define RETURN_EXPANDED(val) {if (!val ## _EXP) {val = expand_vars(val); val ## _EXP = True;} return val ? val : "";} +#define RETURN_EXPANDED(val) {if (!val ## _EXP) {val = expand_vars(val, 0); val ## _EXP = True;} return val ? val : "";} +/* Variant for params whose expansion is fed to a shell-executed hook: quote + * %RSYNC_*% peer-controlled values to prevent shell injection. */ +#define RETURN_EXPANDED_SHELL(val) {if (!val ## _EXP) {val = expand_vars(val, 1); val ## _EXP = True;} return val ? val : "";} /* In this section all the functions that are used to access the * parameters from the rest of the program are defined. */ @@ -229,6 +346,8 @@ #define FN_LOCAL_STRING(fn_name, val) \ char *fn_name(int i) {if (LP_SNUM_OK(i) && iSECTION(i).val) RETURN_EXPANDED(iSECTION(i).val) else RETURN_EXPANDED(Vars.l.val)} +#define FN_LOCAL_STRING_SHELL(fn_name, val) \ + char *fn_name(int i) {if (LP_SNUM_OK(i) && iSECTION(i).val) RETURN_EXPANDED_SHELL(iSECTION(i).val) else RETURN_EXPANDED_SHELL(Vars.l.val)} #define FN_LOCAL_BOOL(fn_name, val) \ BOOL fn_name(int i) {return LP_SNUM_OK(i)? iSECTION(i).val : Vars.l.val;} #define FN_LOCAL_CHAR(fn_name, val) \ @@ -410,7 +529,7 @@ break; default: /* expand any %VAR% strings now */ - parmvalue = expand_vars(parmvalue); + parmvalue = expand_vars(parmvalue, 0); break; } diff -Nru rsync-3.4.1+ds1/log.c rsync-3.5.0+ds1/log.c --- rsync-3.4.1+ds1/log.c 2022-09-10 18:39:37.000000000 +0000 +++ rsync-3.5.0+ds1/log.c 2026-07-20 04:07:05.000000000 +0000 @@ -22,6 +22,7 @@ #include "rsync.h" #include "itypes.h" #include "inums.h" +#include "rounding.h" /* EXTRA_ROUNDING, so log_delete() aligns its file_struct */ extern int dry_run; extern int am_daemon; @@ -119,12 +120,20 @@ return NULL; } +static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isprint, int escape_c1, char end_char); + static void logit(int priority, const char *buf) { if (logfile_was_closed) logfile_reopen(); if (logfile_fp) { - fprintf(logfile_fp, "%s [%d] %s", timestring(time(NULL)), (int)getpid(), buf); + /* Escape control chars in the message so an attacker-controlled + * filename can't inject terminal escapes into the log an admin later + * cat's (CWE-117); keep the trailing newline raw via end_char. */ + int len = strlen(buf); + char trailing = len && (buf[len-1] == '\n' || buf[len-1] == '\r') ? buf[--len] : '\0'; + fprintf(logfile_fp, "%s [%d] ", timestring(time(NULL)), (int)getpid()); + filtered_fwrite(logfile_fp, buf, len, 0, 1, trailing); fflush(logfile_fp); } else { syslog(priority, "%s", buf); @@ -153,7 +162,15 @@ static void logfile_open(void) { mode_t old_umask = umask(022 | orig_umask); - logfile_fp = fopen(logfile_name, "a"); + /* --log-file/`log file =` are operator-supplied paths that may transit + * 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); + logfile_fp = fd >= 0 ? fdopen(fd, "a") : NULL; + if (!logfile_fp && fd >= 0) + close(fd); umask(old_umask); if (!logfile_fp) { int fopen_errno = errno; @@ -222,7 +239,7 @@ } } -static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isprint, char end_char) +static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isprint, int escape_c1, char end_char) { char outbuf[1024], *ob = outbuf; const char *end = in_buf + in_len; @@ -234,7 +251,8 @@ } if ((in_buf < end - 4 && *in_buf == '\\' && in_buf[1] == '#' && isDigit(in_buf + 2) && isDigit(in_buf + 3) && isDigit(in_buf + 4)) - || (*in_buf != '\t' && ((use_isprint && !isPrint(in_buf)) || *(uchar*)in_buf < ' '))) + || (*in_buf != '\t' && ((use_isprint && !isPrint(in_buf)) || *(uchar*)in_buf < ' ' + || (escape_c1 && *(uchar*)in_buf >= 0x80 && *(uchar*)in_buf <= 0x9f)))) ob += snprintf(ob, 6, "\\#%03o", *(uchar*)in_buf++); else *ob++ = *in_buf++; @@ -272,8 +290,12 @@ if (am_daemon > 0 && code != FCLIENT) code = FLOG; } else if (send_msgs_to_gen) { - assert(!is_utf8); - /* Pass the message to our sibling in native charset. */ + /* Pass the message to our sibling in native charset. is_utf8 + * may be set here if a malicious peer sends MSG_INFO/MSG_ERROR + * to a daemon receiver (read_a_msg passes !am_generator); the + * old assert(!is_utf8) made that a remotely-reachable abort. + * Forwarding the bytes raw is safe -- the generator's rwrite() + * gets is_utf8=0 and filtered_fwrite escapes non-printables. */ send_msg((enum msgcode)code, buf, len, 0); return; } @@ -297,7 +319,12 @@ in_block = 1; if (!log_initialised) log_init(0); - strlcpy(msg, buf, MIN((int)sizeof msg, len + 1)); + /* buf holds exactly len bytes and is not necessarily NUL-terminated + * (e.g. a forwarded MSG_* payload from read_a_msg), so copy by length + * rather than strlcpy(), which would strlen() past the end of buf. */ + int mlen = MIN((int)sizeof msg - 1, len); + memcpy(msg, buf, mlen); + msg[mlen] = '\0'; logit(priority, msg); in_block = 0; @@ -372,7 +399,7 @@ ierrno = errno; if (outbuf.len) { char trailing = inbuf.len ? '\0' : trailing_CR_or_NL; - filtered_fwrite(f, convbuf, outbuf.len, 0, trailing); + filtered_fwrite(f, convbuf, outbuf.len, 0, 0, trailing); if (trailing) { trailing_CR_or_NL = '\0'; fflush(f); @@ -395,7 +422,7 @@ } else #endif { - filtered_fwrite(f, buf, len, !allow_8bit_chars, trailing_CR_or_NL); + filtered_fwrite(f, buf, len, !allow_8bit_chars, 0, trailing_CR_or_NL); if (trailing_CR_or_NL) fflush(f); } @@ -456,11 +483,17 @@ char buf[BIGPATHBUFLEN]; size_t len; + /* snprintf returns the would-have-been length on truncation, so + * each cumulative call must be guarded; if not, sizeof buf - len + * can underflow when promoted to size_t and the next call writes + * past the buffer. */ len = snprintf(buf, sizeof buf, RSYNC_NAME ": [%s] ", who_am_i()); - va_start(ap, format); - len += vsnprintf(buf + len, sizeof buf - len, format, ap); - va_end(ap); + if (len < sizeof buf) { + va_start(ap, format); + len += vsnprintf(buf + len, sizeof buf - len, format, ap); + va_end(ap); + } if (len < sizeof buf) { len += snprintf(buf + len, sizeof buf - len, @@ -493,12 +526,17 @@ initial_data_written = total_data_written; } +/* Size of log_formatted()'s per-escape "fmt" scratch buffer. log_format_has() + * must bound its width-digit scan to the same limit so the two parsers agree on + * where an escape letter falls (see the digit loop in each). */ +#define LOG_FMT_SIZE 32 + /* A generic logging routine for send/recv, with parameter substitiution. */ static void log_formatted(enum logcode code, const char *format, const char *op, struct file_struct *file, const char *fname, int iflags, const char *hlink) { - char buf[MAXPATHLEN+1024], buf2[MAXPATHLEN], fmt[32]; + char buf[MAXPATHLEN+1024], buf2[MAXPATHLEN], fmt[LOG_FMT_SIZE]; char *p, *s, *c; const char *n; size_t len, total; @@ -679,7 +717,7 @@ case 'C': n = NULL; if (S_ISREG(file->mode)) { - if (always_checksum) + if (always_checksum && !(iflags & ITEM_DELETED)) n = sum_as_hex(file_sum_nni->num, F_SUM(file), 1); else if (iflags & ITEM_TRANSFER) n = sum_as_hex(xfer_sum_nni->num, sender_file_sum, 0); @@ -744,6 +782,9 @@ } } break; + case '%': + n = "%"; + break; } /* "n" is the string to be inserted in place of this % code. */ @@ -787,21 +828,33 @@ int log_format_has(const char *format, char esc) { const char *p; + int width; if (!format) return 0; for (p = format; (p = strchr(p, '%')) != NULL; ) { for (p++; *p == '\''; p++) {} /*SHARED ITERATOR*/ - if (*p == '-') + /* Mirror log_formatted()'s width-digit scan exactly (c starts at + * fmt+1, so width starts at 1): both must stop at the same digit + * or they disagree on where the escape letter is, which for %C + * can leave sender_keeps_checksum unset and over-read F_SUM. */ + width = 1; + if (*p == '-') { p++; - while (isDigit(p)) + width++; + } + while (isDigit(p) && width < LOG_FMT_SIZE - 8) { p++; + width++; + } while (*p == '\'') p++; if (!*p) break; if (*p == esc) return 1; + if (*p == '%') /* %% is a literal '%', not the start of an escape */ + p++; } return 0; } diff -Nru rsync-3.4.1+ds1/main.c rsync-3.5.0+ds1/main.c --- rsync-3.4.1+ds1/main.c 2024-11-14 19:42:24.000000000 +0000 +++ rsync-3.5.0+ds1/main.c 2026-07-20 04:05:32.000000000 +0000 @@ -31,6 +31,9 @@ #ifdef __TANDEM #include #endif +#ifdef HAVE_SYS_RESOURCE_H +#include +#endif extern int dry_run; extern int list_only; @@ -48,6 +51,7 @@ extern int need_messages_from_generator; extern int kluge_around_eof; extern int got_xfer_error; +extern volatile sig_atomic_t got_sigusr2; extern int old_style_args; extern int msgs2stderr; extern int module_id; @@ -239,11 +243,11 @@ void read_del_stats(int f) { - stats.deleted_files = read_varint(f); - stats.deleted_files += stats.deleted_dirs = read_varint(f); - stats.deleted_files += stats.deleted_symlinks = read_varint(f); - stats.deleted_files += stats.deleted_devices = read_varint(f); - stats.deleted_files += stats.deleted_specials = read_varint(f); + stats.deleted_files = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_files"); + stats.deleted_files += stats.deleted_dirs = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_dirs"); + stats.deleted_files += stats.deleted_symlinks = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_symlinks"); + stats.deleted_files += stats.deleted_devices = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_devices"); + stats.deleted_files += stats.deleted_specials = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_specials"); } static void become_copy_as_user() @@ -386,7 +390,7 @@ static void output_itemized_counts(const char *prefix, int *counts) { - static char *labels[] = { "reg", "dir", "link", "dev", "special" }; + static char *const labels[] = { "reg", "dir", "link", "dev", "special" }; char buf[1024], *pre = " ("; int j, len = 0; int total = counts[0]; @@ -394,9 +398,18 @@ counts[0] -= counts[1] + counts[2] + counts[3] + counts[4]; for (j = 0; j < 5; j++) { if (counts[j]) { + /* snprintf can return more than its size arg + * on truncation; keep len <= sizeof buf - 2 so + * the closing ')' and trailing NUL always + * have room and the next iteration's + * sizeof buf - len - 2 cannot underflow. */ + if (len >= (int)sizeof buf - 2) + break; len += snprintf(buf+len, sizeof buf - len - 2, "%s%s: %s", pre, labels[j], comma_num(counts[j])); + if (len > (int)sizeof buf - 2) + len = (int)sizeof buf - 2; pre = ", "; } } @@ -703,20 +716,24 @@ dest_path = dot_dir_or_error(); if (daemon_filter_list.head) { - char *slash = strrchr(dest_path, '/'); + /* Collapse ".." for the NAME-based daemon filter check so a "../excluded" + * destination is matched by name, as stock rsync does on its sanitized + * arg. Done on a copy: the daemon exclude/filter is name-based (a symlink + * whose own name is not excluded is still followed -- see rsyncd.conf(5) + * "munge symlinks"), and the real dest_path is left for the resolver. */ + char cleaned[MAXPATHLEN], *slash; + if (!sanitize_path(cleaned, dest_path, NULL, 0, SP_KEEP_DOT_DIRS)) + strlcpy(cleaned, dest_path, sizeof cleaned); + slash = strrchr(cleaned, '/'); if (slash && (slash[1] == '\0' || (slash[1] == '.' && slash[2] == '\0'))) *slash = '\0'; - else - slash = NULL; - if ((*dest_path != '.' || dest_path[1] != '\0') - && (check_filter(&daemon_filter_list, FLOG, dest_path, 0) < 0 - || check_filter(&daemon_filter_list, FLOG, dest_path, 1) < 0)) { + if ((*cleaned != '.' || cleaned[1] != '\0') + && (check_filter(&daemon_filter_list, FLOG, cleaned, 0) < 0 + || check_filter(&daemon_filter_list, FLOG, cleaned, 1) < 0)) { rprintf(FERROR, "ERROR: daemon has excluded destination \"%s\"\n", dest_path); exit_cleanup(RERR_FILESELECT); } - if (slash) - *slash = '/'; } /* See what currently exists at the destination. */ @@ -730,10 +747,10 @@ if (ret < 0) goto mkdir_error; if (ret && (INFO_GTE(NAME, 1) || stdout_format_has_i)) { - if (file_total == 1 || trailing_slash) + if (cp && (file_total == 1 || trailing_slash)) *cp = '\0'; rprintf(FINFO, "created %d director%s for %s\n", ret, ret == 1 ? "y" : "ies", dest_path); - if (file_total == 1 || trailing_slash) + if (cp && (file_total == 1 || trailing_slash)) *cp = '/'; } if (ret) @@ -823,7 +840,16 @@ dest_path = "/"; *cp = '\0'; - if (!change_dir(dest_path, CD_NORMAL)) { + if (dry_run && mkpath_dest_arg && do_stat(dest_path, &st) < 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 + * doesn't try to compare against the missing tree (#880). Only + * the missing-parent case is touched, so an ordinary file-to-file + * dry run still itemizes against an existing destination. */ + dry_run++; + change_dir(dest_path, CD_SKIP_CHDIR); + } else if (!change_dir(dest_path, CD_NORMAL)) { rsyserr(FERROR, errno, "change_dir#3 %s failed", full_fname(dest_path)); exit_cleanup(RERR_FILESELECT); @@ -846,13 +872,20 @@ for (j = 0; j < basis_dir_cnt; j++) { char *bdir = basis_dir[j]; + assert(bdir != NULL); /* option-supplied root; never NULL */ int bd_len = strlen(bdir); if (bd_len > 1 && bdir[bd_len-1] == '/') bdir[--bd_len] = '\0'; - if (dry_run > 1 && *bdir != '/') { + /* Make a relative --link-dest/--copy-dest/--compare-dest absolute + * (vs the destination 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; char *new = new_array(char, len); - if (slash && strncmp(bdir, "../", 3) == 0) { + if (dry_run > 1 && slash && strncmp(bdir, "../", 3) == 0) { /* We want to remove only one leading "../" prefix for * the directory we couldn't create in dry-run mode: * this ensures that any other ".." references get @@ -1079,11 +1112,13 @@ exit_cleanup(RERR_PROTOCOL); } - /* Finally, we go to sleep until our parent kills us with a - * USR2 signal. We sleep for a short time, as on some OSes - * a signal won't interrupt a sleep! */ - while (1) + /* Finally, we go to sleep until our parent tells us to wrap up + * with a USR2 signal. We sleep for a short time, as on some OSes + * a signal won't interrupt a sleep, then act on the flag the + * (async-signal-safe) handler set. */ + while (!got_sigusr2) msleep(20); + receive_sigusr2(); } am_generator = 1; @@ -1209,15 +1244,25 @@ char **dir_p; filter_rule_list *elp = &daemon_filter_list; + /* Collapse ".." and strip the module-dir prefix to get the module-relative + * name, but keep a leading "/" for a "path = /" module (module_dirlen <= 1) + * so an absolute (module-rooted) filter rule still matches. */ + char clean[MAXPATHLEN], *dir; for (dir_p = basis_dir; *dir_p; dir_p++) { - char *dir = *dir_p; - if (*dir == '/') - dir += module_dirlen; + if (!sanitize_path(clean, *dir_p, "/", 0, SP_DEFAULT)) + strlcpy(clean, *dir_p, sizeof clean); + dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0); if (check_filter(elp, FLOG, dir, 1) < 0) goto options_rejected; } - if (partial_dir && *partial_dir == '/' - && check_filter(elp, FLOG, partial_dir + module_dirlen, 1) < 0) { + if (partial_dir && *partial_dir == '/') { + if (!sanitize_path(clean, partial_dir, "/", 0, SP_DEFAULT)) + strlcpy(clean, partial_dir, sizeof clean); + dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0); + if (check_filter(elp, FLOG, dir, 1) < 0) + goto options_rejected; + } + if (0) { options_rejected: rprintf(FERROR, "Your options have been rejected by the server.\n"); exit_cleanup(RERR_SYNTAX); @@ -1251,6 +1296,17 @@ if (am_sender) { keep_dirlinks = 0; /* Must be disabled on the sender. */ + + /* Mirror client_run()'s sender_keeps_checksum check: a daemon- + * as-sender with -c and a `log format` containing %C will read + * F_SUM(file) in log_formatted(), so make_file() must allocate + * SUM_EXTRA_CNT. Without this, F_SUM() reads past the pool slot + * and hex-encodes adjacent heap into the transfer log. */ + if (always_checksum + && (log_format_has(stdout_format, 'C') + || log_format_has(logfile_format, 'C'))) + sender_keeps_checksum = 1; + if (need_messages_from_generator) io_start_multiplex_in(f_in); else @@ -1314,7 +1370,7 @@ become_copy_as_user(); - flist = send_file_list(f_out, argc, argv); + send_file_list(f_out, argc, argv); if (DEBUG_GTE(FLIST, 3)) rprintf(FINFO,"file list sent\n"); @@ -1559,6 +1615,10 @@ shell_user = shell_machine; shell_machine = p+1; } + if (*shell_machine == '-') { + rprintf(FERROR, "Invalid remote host: hostnames may not start with '-'.\n"); + exit_cleanup(RERR_SYNTAX); + } } if (DEBUG_GTE(CMD, 2)) { @@ -1600,11 +1660,26 @@ exit_cleanup(RERR_SIGNAL1); } +/* SIGUSR2 tells the receiver child to wrap up. A signal handler must be + * async-signal-safe, so it only sets a flag here; receive_sigusr2() does the + * actual summary + shutdown (which use stdio/malloc/close) at a safe point in + * the receiver's post-transfer wait loops (read_final_goodbye via perform_io, + * and the trailing sleep). */ static void sigusr2_handler(UNUSED(int val)) { + got_sigusr2 = 1; +} + +void receive_sigusr2(void) +{ if (!am_server) output_summary(); close_all(); +#ifdef GCOV_COVERAGE + /* The receiver child exits with _exit() here, bypassing the gcov atexit + * flush; without this it writes no .gcda. */ + { extern void __gcov_dump(void); __gcov_dump(); } +#endif if (got_xfer_error) _exit(RERR_PARTIAL); _exit(0); @@ -1707,6 +1782,31 @@ } +/* The symlink-race-safe path resolver (secure_relative_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. + * Raise the soft RLIMIT_NOFILE toward the hard limit (unprivileged, per + * process; inherited by the sender/generator/receiver forks and daemon + * children), but cap it: some systems set an enormous hard limit (2^20+) that + * we don't want to adopt wholesale. */ +static void raise_fd_limit(void) +{ +#if defined HAVE_GETRLIMIT && defined HAVE_SETRLIMIT && defined RLIMIT_NOFILE + struct rlimit rl; + rlim_t want = 4096; /* covers a MAXPATHLEN-deep walk + cache + headroom */ + + if (getrlimit(RLIMIT_NOFILE, &rl) < 0) + return; + if (want > rl.rlim_max) + want = rl.rlim_max; /* never exceed the (admin-set) hard limit */ + if (rl.rlim_cur < want) { /* only ever raise, never lower an inherited limit */ + rl.rlim_cur = want; + (void)setrlimit(RLIMIT_NOFILE, &rl); /* best-effort */ + } +#endif +} + int main(int argc,char *argv[]) { int ret; @@ -1714,6 +1814,8 @@ raw_argc = argc; raw_argv = argv; + raise_fd_limit(); + #ifdef HAVE_SIGACTION # ifdef HAVE_SIGPROCMASK sigset_t sigmask; @@ -1743,7 +1845,9 @@ our_gid = MY_GID(); am_root = our_uid == ROOT_UID; - unset_env_var("DISPLAY"); + // DISPLAY should not be emptied unconditionally + if (!getenv("SSH_ASKPASS")) + unset_env_var("DISPLAY"); #if defined USE_OPENSSL && defined SET_OPENSSL_CONF #define TO_STR2(x) #x @@ -1825,7 +1929,7 @@ if (am_server && protect_args) { char buf[MAXPATHLEN]; protect_args = 2; - read_args(STDIN_FILENO, NULL, buf, sizeof buf, 1, &argv, &argc, NULL); + read_args(STDIN_FILENO, NULL, buf, sizeof buf, 1, 0, &argv, &argc, NULL); if (!parse_arguments(&argc, (const char ***) &argv)) { option_error(); exit_cleanup(RERR_SYNTAX); diff -Nru rsync-3.4.1+ds1/match.c rsync-3.5.0+ds1/match.c --- rsync-3.4.1+ds1/match.c 2025-01-14 18:30:32.000000000 +0000 +++ rsync-3.5.0+ds1/match.c 2026-07-20 04:05:31.000000000 +0000 @@ -44,6 +44,29 @@ #define TRADITIONAL_TABLESIZE (1<<16) +/* The maximum number of same-weak-checksum candidates we will compare + * against at a single file offset before giving up and rolling forward a + * byte. A weak checksum that collides thousands of times (very common in + * disk/VM images, which contain large runs of identical blocks) would + * otherwise turn hash_search()'s inner loop into an O(file_size * + * chain_length) scan, pegging a CPU at 100% for hours with no apparent + * progress (issue #217). + * + * Concretely, a synthetic 40000-block basis whose blocks all share one weak + * checksum took ~18.4s to sync a 60KB source on a modern x86_64 box before + * this cap and ~0.7s after it -- and the unbounded cost grows with the + * square of the file size, which is what produced the multi-hour "hangs" + * reported against real multi-GB images. + * + * Capping the per-offset work keeps the search bounded; any block we skip + * over is simply sent as literal data, so the result is always correct -- + * only the transfer size is (slightly) affected. This is purely a + * sender-side search limit: it changes no checksum, emitted byte, or + * protocol field, so a capped sender interoperates with any receiver. */ +#ifndef MAX_CHAIN_LEN +#define MAX_CHAIN_LEN 1024 +#endif + static uint32 tablesize; static int32 *hash_table; @@ -182,6 +205,7 @@ int done_csum2 = 0; uint32 hash_entry; int32 i, *prev; + int32 chain_len = 0; if (DEBUG_GTE(DELTASUM, 4)) { rprintf(FINFO, "offset=%s sum=%04x%04x\n", @@ -218,6 +242,14 @@ if (sum != s->sums[i].sum1) continue; + /* Bound the work spent on a single pathological hash + * bucket. If this weak checksum matches more than + * MAX_CHAIN_LEN records, stop scanning and treat this + * offset as a non-match (issue #217). The skipped data + * is sent literally, never corrupted. */ + if (++chain_len > MAX_CHAIN_LEN) + break; + /* also make sure the two blocks are the same length */ l = (int32)MIN((OFF_T)s->blength, len-offset); if (l != s->sums[i].len) @@ -293,6 +325,7 @@ && (!updating_basis_file || s->sums[want_i].offset >= offset || s->sums[want_i].flags & SUMFLG_SAME_OFFSET) && sum == s->sums[want_i].sum1 + && l == s->sums[want_i].len && memcmp(sum2, sum2_at(s, want_i), s->s2length) == 0) { /* we've found an adjacent match - the RLL coder * will be happy */ @@ -370,6 +403,14 @@ sum_init(xfer_sum_nni, checksum_seed); if (append_mode > 0) { + if (s->flength > len) { + /* A hostile or confused peer can claim a verified-prefix + * length that exceeds what we have on disk -- including + * for an empty local file, where buf is NULL and the + * map_ptr() calls below would dereference it. Clamp to + * what we can actually read. */ + s->flength = len; + } if (append_mode == 2) { OFF_T j = 0; for (j = CHUNK_SIZE; j < s->flength; j += CHUNK_SIZE) { diff -Nru rsync-3.4.1+ds1/maybe-make-man rsync-3.5.0+ds1/maybe-make-man --- rsync-3.4.1+ds1/maybe-make-man 2023-04-23 15:26:32.000000000 +0000 +++ rsync-3.5.0+ds1/maybe-make-man 2026-06-06 04:17:00.000000000 +0000 @@ -15,7 +15,7 @@ if "$srcdir/md-convert" --test "$srcdir/rsync-ssl.1.md" >/dev/null 2>&1; then touch $flagfile else - outname=`echo "$inname" | sed 's/\.md$//'` + outname=`basename "$inname" .md` if [ -f "$outname" ]; then exit 0 elif [ -f "$srcdir/$outname" ]; then diff -Nru rsync-3.4.1+ds1/md-convert rsync-3.5.0+ds1/md-convert --- rsync-3.4.1+ds1/md-convert 2023-04-23 15:26:32.000000000 +0000 +++ rsync-3.5.0+ds1/md-convert 2025-08-23 06:40:34.000000000 +0000 @@ -120,6 +120,7 @@ VAR_REF_RE = re.compile(r'\$\{(\w+)\}') VERSION_RE = re.compile(r' (\d[.\d]+)[, ]') BIN_CHARS_RE = re.compile(r'[\1-\7]+') +LONG_OPT_DASH_RE = re.compile(r'(--\w[-\w]+)') SPACE_DOUBLE_DASH_RE = re.compile(r'\s--(\s)') NON_SPACE_SINGLE_DASH_RE = re.compile(r'(^|\W)-') WHITESPACE_RE = re.compile(r'\s') @@ -247,6 +248,9 @@ env_subs['date'] = time.strftime('%d %b %Y', time.gmtime(mtime + tz_offset)).lstrip('0') + if 'SOURCE_DATE_EPOCH' in os.environ: + env_subs['date'] = time.strftime('%d %b %Y', time.gmtime(int(os.environ.get('SOURCE_DATE_EPOCH', time.time())))) + def html_via_commonmark(txt): return commonmark.HtmlRenderer().render(commonmark.Parser().parse(txt)) @@ -540,6 +544,7 @@ if st.in_pre: html = htmlify(txt) else: + txt = LONG_OPT_DASH_RE.sub(lambda x: x.group(1).replace('-', NBR_DASH[0]), txt) txt = SPACE_DOUBLE_DASH_RE.sub(NBR_SPACE[0] + r'--\1', txt).replace('--', NBR_DASH[0]*2) txt = NON_SPACE_SINGLE_DASH_RE.sub(r'\1' + NBR_DASH[0], txt) html = htmlify(txt) diff -Nru rsync-3.4.1+ds1/md2man rsync-3.5.0+ds1/md2man --- rsync-3.4.1+ds1/md2man 2023-04-23 15:26:32.000000000 +0000 +++ rsync-3.5.0+ds1/md2man 2025-08-23 06:40:34.000000000 +0000 @@ -120,6 +120,7 @@ VAR_REF_RE = re.compile(r'\$\{(\w+)\}') VERSION_RE = re.compile(r' (\d[.\d]+)[, ]') BIN_CHARS_RE = re.compile(r'[\1-\7]+') +LONG_OPT_DASH_RE = re.compile(r'(--\w[-\w]+)') SPACE_DOUBLE_DASH_RE = re.compile(r'\s--(\s)') NON_SPACE_SINGLE_DASH_RE = re.compile(r'(^|\W)-') WHITESPACE_RE = re.compile(r'\s') @@ -247,6 +248,9 @@ env_subs['date'] = time.strftime('%d %b %Y', time.gmtime(mtime + tz_offset)).lstrip('0') + if 'SOURCE_DATE_EPOCH' in os.environ: + env_subs['date'] = time.strftime('%d %b %Y', time.gmtime(int(os.environ.get('SOURCE_DATE_EPOCH', time.time())))) + def html_via_commonmark(txt): return commonmark.HtmlRenderer().render(commonmark.Parser().parse(txt)) @@ -540,6 +544,7 @@ if st.in_pre: html = htmlify(txt) else: + txt = LONG_OPT_DASH_RE.sub(lambda x: x.group(1).replace('-', NBR_DASH[0]), txt) txt = SPACE_DOUBLE_DASH_RE.sub(NBR_SPACE[0] + r'--\1', txt).replace('--', NBR_DASH[0]*2) txt = NON_SPACE_SINGLE_DASH_RE.sub(r'\1' + NBR_DASH[0], txt) html = htmlify(txt) diff -Nru rsync-3.4.1+ds1/mkgitver rsync-3.5.0+ds1/mkgitver --- rsync-3.4.1+ds1/mkgitver 2022-10-16 18:14:15.000000000 +0000 +++ rsync-3.5.0+ds1/mkgitver 2026-07-20 04:05:30.000000000 +0000 @@ -7,10 +7,20 @@ fi if test -d "$srcdir/.git" || test -f "$srcdir/.git"; then - gitver=`git describe --abbrev=8 2>/dev/null` - # NOTE: I'm avoiding "|" in sed since I'm not sure if sed -r is portable and "\|" fails on some OSes. - verchk=`echo "$gitver-" | sed -n '/^v3\.[0-9][0-9]*\.[0-9][0-9]*\(pre[0-9]*\)*-/p'` - if [ -n "$verchk" ]; then + # Identify a git build by the development version from version.h plus the + # exact commit (e.g. "3.5.0dev-g1234abcd"), rather than the nearest release + # tag that `git describe` would pick: that tag can sit far behind a rebased + # development branch and then misnames the line you are actually on (showing, + # say, 3.4.3 for a 3.5.0dev tree). This also works in a shallow/tag-less + # clone. A release tarball has no .git, so git-version.h stays empty and + # rsync prints the plain RSYNC_VERSION. + # cd into the subshell rather than "git -C" (avoids needing a newer git). + gitsha=`(cd "$srcdir" && git rev-parse --short=8 HEAD) 2>/dev/null` + # Tolerate any preprocessor spacing and a trailing comment; capture only the + # quoted value. Empty (define missing/unmatched) -> leave RSYNC_GITVER unset. + rsyncver=`sed -n 's/^[[:space:]]*#[[:space:]]*define[[:space:]][[:space:]]*RSYNC_VERSION[[:space:]][[:space:]]*"\([^"]*\)".*/\1/p' "$srcdir/version.h"` + if [ -n "$gitsha" ] && [ -n "$rsyncver" ]; then + gitver="$rsyncver-g$gitsha" echo "#define RSYNC_GITVER \"$gitver\"" >git-version.h.new if ! diff git-version.h.new git-version.h >/dev/null; then echo "Updating git-version.h" diff -Nru rsync-3.4.1+ds1/mkproto.awk rsync-3.5.0+ds1/mkproto.awk --- rsync-3.4.1+ds1/mkproto.awk 2020-08-03 20:33:46.000000000 +0000 +++ rsync-3.5.0+ds1/mkproto.awk 2026-07-20 04:05:31.000000000 +0000 @@ -18,6 +18,7 @@ sub(/^CHAR\(/, "char ") sub(/^INTEGER\(/, "int ") sub(/^STRING\(/, "char *") + sub(/^STRING_SHELL\(/, "char *") protos = protos "\n" $0 (local ? "(int module_id);" : "(void);") next } diff -Nru rsync-3.4.1+ds1/old_versions/README.md rsync-3.5.0+ds1/old_versions/README.md --- rsync-3.4.1+ds1/old_versions/README.md 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/old_versions/README.md 2026-06-01 09:21:35.000000000 +0000 @@ -0,0 +1,87 @@ +# Old rsync version archive + +Static rsync binaries built from historical release tags. Two uses: + +1. **Cross-version behaviour checks** — confirming whether a behaviour a user + reported on an old release is version-specific or option-driven. +2. **The version-mixing test suite** — `runtests.py --rsync-bin2=...` runs the + current code against one of these as the daemon / remote-shell peer; CI + (`.github/workflows/ubuntu-version-mix.yml`) does this for every binary + here against the per-version manifests in `testsuite/expect/`. + +Binaries are **statically linked** so they run regardless of the host's +shared libraries, and named `rsync_`: + +| Binary | Version | Protocol | Notes | +|----------------|---------|----------|-----------------------------------------| +| `rsync_2.6.0` | 2.6.0 | 27 | 2004; needs autoconf regen (see below) | +| `rsync_3.0.0` | 3.0.0 | 30 | 2008 | +| `rsync_3.1.0` | 3.1.0 | 31 | 2013 | +| `rsync_3.1.3` | 3.1.3 | 31 | Ubuntu 18.04 / Debian buster era (2018) | +| `rsync_3.2.0` | 3.2.0 | 31 | 2020 (zstd/lz4/xxhash negotiation added)| +| `rsync_3.2.7` | 3.2.7 | 31 | 2022 | +| `rsync_3.3.0` | 3.3.0 | 31 | 2024 | +| `rsync_3.4.0` | 3.4.0 | 32 | 2025 | +| `rsync_3.4.1` | 3.4.1 | 32 | 2025 | + +These are every `x.y.0` release from 2.6.0 (2004) onward plus a few point +releases. 2.6.0 is the practical floor: older tags need progressively more +porting to build on a current toolchain. + +All built `--disable-openssl` and with `_FORTIFY_SOURCE` disabled (see below); +xxhash/zstd/lz4 are compiled in where the version supports them. + +## Adding a version + +```bash +./build_static.sh 3.2.7 # uses git tag v3.2.7 +./build_static.sh 3.0.9 v3.0.9 # explicit tag if naming differs +``` + +The script checks out the tag into a throwaway `git worktree`, applies the +minimal patches needed to compile old sources on a modern toolchain, links +statically, verifies the result is static and reports the requested version, +then installs `rsync_` here and removes the worktree. + +Override the source repo with `RSYNC_REPO=/path/to/rsync ./build_static.sh ...` +(defaults to `../rsync.4`). + +## Why the patches? + +Modern GCC (>= 14, C23 default) and glibc reject things old rsync relied on. +`build_static.sh` handles these, each guarded so it's a no-op when not needed: + +1. **K&R `lseek64()` redeclaration** in `syscall.c` clashes with glibc's real + prototype — removed. +2. **`gettimeofday()`** — glibc only has the 2-arg form; configure misdetects + the 1-arg form, so `HAVE_GETTIMEOFDAY_TZ` is forced on in `config.h`. +3. **C23 `()` == `(void)`** breaks K&R prototypes called with arguments + (`qsort` comparator, `pool->bomb`, etc.) — built with `-std=gnu11`. +4. Assorted modern `-Werror` promotions (incompatible pointer types, implicit + declarations) downgraded to warnings; bundled zlib/popt used to keep the + static link self-contained. + +5. **OpenSSL (3.2+)** is disabled with `--disable-openssl`: linking + `libcrypto.a` statically drags in jitterentropy (`jent_*`) and zlib's + `uncompress` (OpenSSL's COMP module), which don't resolve here. OpenSSL only + provided optional MD4/MD5, which rsync implements natively, so checksum + behaviour is unaffected. + +6. **`_FORTIFY_SOURCE` disabled** (`-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0`): + modern Ubuntu defaults it to `=3`, whose stricter object-size checks turn + latent (historically benign) over-reads in OLD rsync into hard + `*** buffer overflow detected ***` aborts when the binary runs as a + server/daemon — which made e.g. 3.1.3 and 3.2.7 unusable as peers. Disabling + it makes the archival binaries behave as the released versions did. + +7. **Pre-3.0 tags (e.g. 2.6.0)** ship `configure.in`, not a generated + `configure`. The script runs `autoheader`/`autoconf` to generate it, after + neutralizing the `AC_CHECK_FUNCS(fn,,AC_LIBOBJ(lib/...))` fallbacks for + `inet_ntop`/`inet_pton`/`getaddrinfo`/`getnameinfo` — modern autoconf emits + broken shell for those never-taken branches (the funcs exist in glibc). It + also generates `proto.h` (no make rule in that era) and stubs the vendored + `lib/addrinfo.h` the tag dropped (modern glibc supplies `struct addrinfo`). + All guarded so they no-op on 3.x. + +Newer versions may need fewer or different tweaks; if a build fails, the +script prints the first compiler errors from its log. diff -Nru rsync-3.4.1+ds1/old_versions/build_static.sh rsync-3.5.0+ds1/old_versions/build_static.sh --- rsync-3.4.1+ds1/old_versions/build_static.sh 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/old_versions/build_static.sh 2026-06-01 09:21:35.000000000 +0000 @@ -0,0 +1,128 @@ +#!/bin/bash +# Build a static rsync binary from a historical git tag, for cross-version +# behaviour testing. Produces ./rsync_ in this directory. +# +# Usage: ./build_static.sh [git-tag] +# Example: ./build_static.sh 3.1.3 # uses tag v3.1.3 +# ./build_static.sh 3.2.7 v3.2.7 +# +# Old rsync releases don't compile cleanly on a modern toolchain (GCC >= 14 +# defaults to C23, where an empty () prototype means (void); glibc dropped the +# 1-arg gettimeofday; lseek64 K&R redeclarations clash). This script applies +# the minimal, best-effort workarounds and links statically so the result is +# self-contained and reproducible regardless of the host's shared libraries. +# +# Each workaround is guarded so it's a no-op on versions that don't need it. +set -euo pipefail + +VERSION="${1:?usage: build_static.sh [git-tag]}" +TAG="${2:-v$VERSION}" + +ARCHIVE_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO="${RSYNC_REPO:-/home/tridge/project/rsync/rsync.4}" # any rsync worktree +WORKTREE="$(mktemp -d /tmp/rsync-build-XXXXXX)" +OUT="$ARCHIVE_DIR/rsync_$VERSION" + +# C standard restores K&R () semantics; permissive flags downgrade the pile of +# modern -Werror promotions (incompatible pointers, implicit decls) to warnings. +# _FORTIFY_SOURCE is forced OFF: modern Ubuntu defaults it to =3, whose stricter +# object-size checks turn latent (historically benign) over-reads in OLD rsync +# into hard "*** buffer overflow detected ***" aborts when the binary acts as a +# server/daemon. Disabling it makes these archival binaries behave the way the +# released versions did, which is the whole point of the archive. +CFLAGS_OLD="-I. -I./zlib -O2 -g -std=gnu11 -fcommon -DHAVE_CONFIG_H -Wno-error \ +-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \ +-Wno-incompatible-pointer-types -Wno-implicit-function-declaration -Wno-int-conversion" + +cleanup() { + cd "$REPO" + git worktree remove --force "$WORKTREE" 2>/dev/null || true + git worktree prune 2>/dev/null || true +} +trap cleanup EXIT + +echo ">>> checking out $TAG into $WORKTREE" +# prefer an exact tag to avoid ambiguity with similarly-named branches +REF="$TAG" +if git -C "$REPO" rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then + REF="refs/tags/$TAG" +fi +git -C "$REPO" worktree add --detach "$WORKTREE" "$REF" +cd "$WORKTREE" + +# --- workaround 1: K&R lseek64 redeclaration clashes with glibc's prototype --- +if grep -q 'off64_t lseek64();' syscall.c 2>/dev/null; then + echo ">>> patching syscall.c lseek64 redeclaration" + perl -0pi -e 's/#ifdef HAVE_LSEEK64\n#if !SIZEOF_OFF64_T\n\tOFF_T lseek64\(\);\n#else\n\toff64_t lseek64\(\);\n#endif\n\treturn lseek64/#ifdef HAVE_LSEEK64\n\treturn lseek64/' syscall.c +fi + +# --- workaround 0: pre-3.0 tags ship configure.in, not a generated configure. +# Generate it. Modern autoconf emits broken shell for their +# AC_CHECK_FUNCS(fn,,AC_LIBOBJ(lib/...)) fallbacks -- but those branches are +# dead on a modern host (glibc has inet_ntop/inet_pton/getaddrinfo/getnameinfo), +# so neutralize the AC_LIBOBJ replacements before regenerating. +OLD_TREE=0 +if [ ! -f ./configure ] && { [ -f configure.in ] || [ -f configure.ac ]; }; then + OLD_TREE=1 + acsrc=configure.ac; [ -f configure.in ] && acsrc=configure.in + echo ">>> generating configure for an old tag (autoheader/autoconf)" + sed -i 's#AC_LIBOBJ(lib/[a-zA-Z_]*)#:#g' "$acsrc" + autoheader 2>/dev/null || true + autoconf 2>/dev/null || { echo "autoconf failed"; exit 1; } +fi + +CONF_ARGS=(--disable-md2man --with-included-zlib=yes --with-included-popt=yes) +# OpenSSL (3.2+) only adds optional MD4/MD5 that rsync already implements, but +# linking libcrypto.a statically drags in jitterentropy + zlib's uncompress, +# which aren't resolvable here. Drop it when the flag exists. +if ./configure --help 2>/dev/null | grep -q -- '--disable-openssl'; then + echo ">>> disabling openssl for self-contained static link" + CONF_ARGS+=(--disable-openssl) +fi + +echo ">>> configure (bundled zlib + popt, static-friendly)" +./configure "${CONF_ARGS[@]}" \ + >"$WORKTREE/conf.log" 2>&1 || { tail -20 "$WORKTREE/conf.log"; exit 1; } + +# --- workaround 2: modern glibc only has the 2-arg gettimeofday --------------- +if grep -q '/\* #undef HAVE_GETTIMEOFDAY_TZ \*/' config.h; then + echo ">>> forcing HAVE_GETTIMEOFDAY_TZ (configure misdetects it)" + sed -i 's|/\* #undef HAVE_GETTIMEOFDAY_TZ \*/|#define HAVE_GETTIMEOFDAY_TZ 1|' config.h +fi + +# --- workaround 4 (old trees only): generate proto.h if the tree has no make +# rule for it, and stub a vendored lib/addrinfo.h that the git tag dropped +# (modern glibc supplies struct addrinfo / sockaddr_storage, so empty is right). +if [ "$OLD_TREE" = 1 ]; then + if [ ! -f proto.h ] && [ -f mkproto.awk ]; then + echo ">>> generating proto.h" + cat ./*.c ./lib/compat.c 2>/dev/null | awk -f ./mkproto.awk > proto.h + fi + if grep -q 'include "lib/addrinfo.h"' rsync.h 2>/dev/null && [ ! -f lib/addrinfo.h ]; then + echo ">>> stubbing lib/addrinfo.h" + echo '/* emptied: modern glibc provides struct addrinfo */' > lib/addrinfo.h + fi +fi + +echo ">>> building (static)" +make -j"$(nproc)" CFLAGS="$CFLAGS_OLD" LDFLAGS="-static" \ + >"$WORKTREE/make.log" 2>&1 || { grep -E 'error:|\*\*\*' "$WORKTREE/make.log" | head; exit 1; } + +# verify it's actually static before we keep it +if ldd ./rsync 2>&1 | grep -qv 'not a dynamic executable'; then + echo "ERROR: binary is not statically linked:" >&2 + ldd ./rsync >&2 + exit 1 +fi + +GOT="$(./rsync --version | head -1 | awk '{print $3}')" +if [ "$GOT" != "$VERSION" ]; then + echo "ERROR: built version '$GOT' != requested '$VERSION'" >&2 + exit 1 +fi + +cp ./rsync "$OUT" +strip "$OUT" +echo ">>> installed $OUT" +"$OUT" --version | head -1 +file "$OUT" diff -Nru rsync-3.4.1+ds1/options.c rsync-3.5.0+ds1/options.c --- rsync-3.4.1+ds1/options.c 2024-11-05 19:20:17.000000000 +0000 +++ rsync-3.5.0+ds1/options.c 2026-08-02 19:35:03.000000000 +0000 @@ -27,6 +27,7 @@ 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; @@ -59,6 +60,9 @@ 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 */ +unsigned int confine_rootlen = 0; int preserve_uid = 0; int preserve_gid = 0; int preserve_mtimes = 0; @@ -86,6 +90,8 @@ int preallocate_files = 0; int do_compression = 0; int do_compression_level = CLVL_NOT_SPECIFIED; +int do_compression_threads = 0; /*n = 0 use rsync thread, n >= 1 spawn n threads for compression */ +#define MAX_DAEMON_COMPRESSION_THREADS 8 int am_root = 0; /* 0 = normal, 1 = root, 2 = --super, -1 = --fake-super */ int am_server = 0; int am_sender = 0; @@ -113,11 +119,21 @@ int allow_inc_recurse = 1; int xfer_dirs = -1; int am_daemon = 0; +/* Set after a successful per-module chroot ("use chroot = yes") in + * 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 + * syscall.c, gated by `am_daemon && !am_chrooted`) must still fire + * even when the daemon is inside a daemon chroot. */ +int am_chrooted = 0; int connect_timeout = 0; int keep_partial = 0; int safe_symlinks = 0; int copy_unsafe_links = 0; +int insecure_links = 0; int munge_symlinks = 0; +int use_secure_symlinks = 0; int size_only = 0; int daemon_bwlimit = 0; int bwlimit = 0; @@ -225,7 +241,7 @@ struct chmod_mode_struct *chmod_modes = NULL; -static const char *debug_verbosity[] = { +static const char *const debug_verbosity[] = { /*0*/ NULL, /*1*/ NULL, /*2*/ "BIND,CMD,CONNECT,DEL,DELTASUM,DUP,FILTER,FLIST,ICONV", @@ -236,7 +252,7 @@ #define MAX_VERBOSITY ((int)(sizeof debug_verbosity / sizeof debug_verbosity[0]) - 1) -static const char *info_verbosity[1+MAX_VERBOSITY] = { +static const char *const info_verbosity[1+MAX_VERBOSITY] = { /*0*/ "NONREG", /*1*/ "COPY,DEL,FLIST,MISC,NAME,STATS,SYMSAFE", /*2*/ "BACKUP,MISC2,MOUNT,NAME2,REMOVE,SKIP", @@ -441,7 +457,10 @@ len--; } lev = isDigit(str+len) ? atoi(str+len) : 1; - if (lev > MAX_OUT_LEVEL) + /* atoi() of an overflowing positive digit string can return a + * negative int (LONG_MAX truncated on LP64); a negative lev + * here later indexes counts[lev] in make_output_option(). */ + if (lev > MAX_OUT_LEVEL || lev < 0) lev = MAX_OUT_LEVEL; if (len == 4 && strncasecmp(str, "help", 4) == 0) { output_item_help(words); @@ -474,7 +493,7 @@ static void output_item_help(struct output_struct *words) { short *levels = words == info_words ? info_levels : debug_levels; - const char **verbosity = words == info_words ? info_verbosity : debug_verbosity; + const char *const*verbosity = words == info_words ? info_verbosity : debug_verbosity; char buf[128], *opt, *fmt = "%-10s %s\n"; int j; @@ -666,12 +685,17 @@ {"no-write-devices", 0, POPT_ARG_VAL, &write_devices, 0, 0, 0 }, {"specials", 0, POPT_ARG_VAL, &preserve_specials, 1, 0, 0 }, {"no-specials", 0, POPT_ARG_VAL, &preserve_specials, 0, 0, 0 }, + {"drop-D", 0, POPT_ARG_VAL, &drop_devices, 1, 0, 0 }, + {"no-drop-D", 0, POPT_ARG_VAL, &drop_devices, 0, 0, 0 }, + {"confine-root", 0, POPT_ARG_STRING, &confine_root, 0, 0, 0 }, {"links", 'l', POPT_ARG_VAL, &preserve_links, 1, 0, 0 }, {"no-links", 0, POPT_ARG_VAL, &preserve_links, 0, 0, 0 }, {"no-l", 0, POPT_ARG_VAL, &preserve_links, 0, 0, 0 }, {"copy-links", 'L', POPT_ARG_NONE, ©_links, 0, 0, 0 }, {"copy-unsafe-links",0, POPT_ARG_NONE, ©_unsafe_links, 0, 0, 0 }, {"safe-links", 0, POPT_ARG_NONE, &safe_symlinks, 0, 0, 0 }, + {"insecure-links", 0, POPT_ARG_VAL, &insecure_links, 1, 0, 0 }, + {"no-insecure-links",0, POPT_ARG_VAL, &insecure_links, 0, 0, 0 }, {"munge-links", 0, POPT_ARG_VAL, &munge_symlinks, 1, 0, 0 }, {"no-munge-links", 0, POPT_ARG_VAL, &munge_symlinks, 0, 0, 0 }, {"copy-dirlinks", 'k', POPT_ARG_NONE, ©_dirlinks, 0, 0, 0 }, @@ -756,6 +780,8 @@ {"skip-compress", 0, POPT_ARG_STRING, &skip_compress, 0, 0, 0 }, {"compress-level", 0, POPT_ARG_INT, &do_compression_level, 0, 0, 0 }, {"zl", 0, POPT_ARG_INT, &do_compression_level, 0, 0, 0 }, + {"compress-threads", 0, POPT_ARG_INT, &do_compression_threads, 0, 0, 0 }, + {"zt", 0, POPT_ARG_INT, &do_compression_threads, 0, 0, 0 }, {0, 'P', POPT_ARG_NONE, 0, 'P', 0, 0 }, {"progress", 0, POPT_ARG_VAL, &do_progress, 1, 0, 0 }, {"no-progress", 0, POPT_ARG_VAL, &do_progress, 0, 0, 0 }, @@ -844,7 +870,7 @@ {0,0,0,0, 0, 0, 0} }; -static struct poptOption long_daemon_options[] = { +static const struct poptOption long_daemon_options[] = { /* longName, shortName, argInfo, argPtr, value, descrip, argDesc */ {"address", 0, POPT_ARG_STRING, &bind_address, 0, 0, 0 }, {"bwlimit", 0, POPT_ARG_INT, &daemon_bwlimit, 0, 0, 0 }, @@ -892,9 +918,54 @@ } +/* Does this row store a compile-time constant, and if so which? + * + * popt's `val` is not comparable across argInfo kinds. For POPT_ARG_VAL it IS + * the value stored in `arg`; for the others a nonzero `val` is an action code + * handed to the parser's switch, and POPT_ARG_NONE with a destination stores 1 + * regardless. Comparing the raw field therefore misses aliases spelled with + * different table shapes -- --del is POPT_ARG_NONE/&delete_during/0 and + * --delete-during is POPT_ARG_VAL/&delete_during/1, and both set it to 1. */ +static int refuse_const_assign(const struct poptOption *op, int *valp) +{ + if (!op->arg) + return 0; + if (op->argInfo == POPT_ARG_VAL) { + *valp = op->val; + return 1; + } + /* A nonzero val here means the row ALSO runs a parser action, so it is + * not merely an assignment and must not be folded in with one. */ + if (op->argInfo == POPT_ARG_NONE && op->val == 0) { + *valp = 1; + return 1; + } + return 0; +} + +/* Do two table rows name the same capability? An exact refuse rule names a + * capability, not one spelling of it. */ +static int same_refuse_action(const struct poptOption *a, const struct poptOption *b) +{ + int a_val, b_val; + + /* Constant assignments: same destination, same resulting value. The + * value check keeps opposite switches such as --foo and --no-foo apart, + * since they differ only in what they store. */ + if (refuse_const_assign(a, &a_val) && refuse_const_assign(b, &b_val)) + return a->arg == b->arg && a_val == b_val; + + /* Anything else has to match as a table entry: a row storing a runtime + * value (POPT_ARG_INT, POPT_ARG_STRING) needs the same destination and + * action code, and an action-only row the same nonzero code. */ + if (a->argInfo != b->argInfo || a->val != b->val) + return 0; + return a->arg ? a->arg == b->arg : !b->arg && a->val != 0; +} + static void parse_one_refuse_match(int negated, const char *ref, const struct poptOption *list_end) { - struct poptOption *op; + struct poptOption *op, *matched_op = NULL; char shortName[2]; int is_wild = strpbrk(ref, "*?[") != NULL; int found_match = 0; @@ -915,8 +986,21 @@ else if (!is_wild) op->descrip = negated ? "a=" : "r="; found_match = 1; - if (!is_wild) + if (!is_wild) { + matched_op = op; break; + } + } + } + + if (matched_op) { + for (op = long_options; op != list_end; op++) { + if (op == matched_op || !same_refuse_action(op, matched_op)) + continue; + if (op->descrip[1] == '*') + op->descrip = negated ? "a*" : "r*"; + else + op->descrip = negated ? "a=" : "r="; } } @@ -996,6 +1080,11 @@ parse_one_refuse_match(0, "iconv", list_end); #endif parse_one_refuse_match(0, "log-file*", list_end); + /* A client must never disable the daemon's symlink confinement: + * --insecure-links is a local-only flag, so the daemon hard-refuses it + * (dropping the connection). The daemon's own opt-out is the + * "insecure links" module parameter, not this flag. */ + parse_one_refuse_match(0, "insecure-links", list_end); } #ifndef SUPPORT_ATIMES @@ -1077,6 +1166,8 @@ int reps, mult, len; const char *arg, *err = "invalid", *min_max = NULL; ssize_t limit = -1, size = 1; + ssize_t size_max = max_value >= 0 ? max_value : (ssize_t)(SIZE_MAX / 2); + double dsize; for (arg = size_arg; isDigit(arg); arg++) {} if (*arg == '.' || *arg == get_decimal_point()) /* backward compatibility: always allow '.' */ @@ -1111,11 +1202,38 @@ mult = 1024, arg += 2; else goto failure; - while (reps--) + while (reps--) { + if (size > size_max / mult) { + err = "too large"; + min_max = "max"; + limit = max_value; + goto failure; + } size *= mult; - size *= atof(size_arg); - if ((*arg == '+' || *arg == '-') && arg[1] == '1' && arg != size_arg) - size += atoi(arg), arg += 2; + } + errno = 0; + dsize = strtod(size_arg, NULL); + if (errno == ERANGE || dsize < 0 || dsize > (double)size_max / size + || (max_value < 0 && dsize >= (double)size_max / size)) { + err = "too large"; + min_max = "max"; + limit = max_value; + goto failure; + } + size = (ssize_t)(dsize * size); + if ((*arg == '+' || *arg == '-') && arg[1] == '1' && arg != size_arg) { + if (*arg == '+') { + if (size == size_max) { + err = "too large"; + min_max = "max"; + limit = max_value; + goto failure; + } + size++; + } else + size--; + arg += 2; + } if (*arg) goto failure; if (size < 0 || (max_value >= 0 && size > max_value)) { @@ -1139,6 +1257,8 @@ min_max, do_big_num(limit, 3, NULL), unlimited_0 && min_max[1] == 'i' ? " or 0 for unlimited" : ""); } + if (len < 0 || len > (int)sizeof err_buf - 2) + len = sizeof err_buf - 2; err_buf[len] = '\n'; err_buf[len+1] = '\0'; return -1; @@ -1156,7 +1276,7 @@ { const char *cp; time_t val, now = time(NULL); - struct tm t, *today = localtime(&now); + struct tm t, tmp, *today = localtime_r(&now, &tmp); int in_date, old_mday, n; memset(&t, 0, sizeof t); @@ -1369,6 +1489,10 @@ /* TODO: Call poptReadDefaultConfig; handle errors. */ pc = poptGetContext(RSYNC_NAME, argc, argv, long_options, 0); + if (pc == NULL) { + strlcpy(err_buf, "poptGetContext returned NULL\n", sizeof err_buf); + return 0; + } if (!am_server) { poptReadDefaultConfig(pc, 0); popt_unalias(pc, "--daemon"); @@ -1473,7 +1597,6 @@ *argc_p = 0; } else if (poptDupArgv(argc, argv, argc_p, argv_p) != 0) out_of_memory("parse_arguments"); - argv = *argv_p; poptFreeContext(pc); am_starting_up = 0; @@ -1944,6 +2067,10 @@ ssize_t size = parse_size_arg(max_alloc_arg, 'B', "max-alloc", 1024*1024, -1, True); if (size < 0) goto cleanup; + if (size == 0) { + snprintf(err_buf, sizeof err_buf, "max-alloc must be greater than zero\n"); + goto cleanup; + } max_alloc = size; } if (!max_alloc) @@ -2006,6 +2133,14 @@ create_refuse_error(refused_compress); goto cleanup; } + if (do_compression_threads < 0) + do_compression_threads = 0; + /* A daemon client controls the server-side sender arguments. Keep one + * unauthenticated connection from asking Zstandard to materialize its + * implementation maximum (currently hundreds) of worker threads. Local + * and remote-shell invocations retain the operator-requested value. */ + if (am_daemon && do_compression_threads > MAX_DAEMON_COMPRESSION_THREADS) + do_compression_threads = MAX_DAEMON_COMPRESSION_THREADS; } #ifdef HAVE_SETVBUF @@ -2244,6 +2379,26 @@ } } + if (confine_root) { + /* A daemon already has module_dir for this job, and honouring a + * peer-supplied root there could only loosen the module boundary. */ + if (am_daemon) + confine_root = NULL; + else if (*confine_root != '/') { + snprintf(err_buf, sizeof err_buf, + "--confine-root must be an absolute path\n"); + return 0; + } else if (insecure_links) { + /* The opt-out restores the legacy open, which short-circuits the + * walk that enforces the root -- so the pair would silently mean + * no confinement at all. Say so instead. */ + snprintf(err_buf, sizeof err_buf, + "--insecure-links cannot be combined with --confine-root\n"); + return 0; + } else + confine_root = normalize_path(confine_root, True, &confine_rootlen); + } + if (sanitize_paths) { int i; for (i = argc; i-- > 0; ) @@ -2255,21 +2410,26 @@ } if (daemon_filter_list.head && !am_sender) { filter_rule_list *elp = &daemon_filter_list; + /* Strip the module-dir prefix to get the module-relative name, but keep a + * leading "/" for a "path = /" module (module_dirlen <= 1) so an absolute + * (module-rooted) filter rule still matches. */ if (tmpdir) { - char *dir; + char clean[MAXPATHLEN], *dir; if (!*tmpdir) goto options_rejected; - dir = tmpdir + (*tmpdir == '/' ? module_dirlen : 0); - clean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS); + if (!sanitize_path(clean, tmpdir, "/", 0, SP_DEFAULT)) + strlcpy(clean, tmpdir, sizeof clean); + dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0); if (check_filter(elp, FLOG, dir, 1) < 0) goto options_rejected; } if (backup_dir) { - char *dir; + char clean[MAXPATHLEN], *dir; if (!*backup_dir) goto options_rejected; - dir = backup_dir + (*backup_dir == '/' ? module_dirlen : 0); - clean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS); + if (!sanitize_path(clean, backup_dir, "/", 0, SP_DEFAULT)) + strlcpy(clean, backup_dir, sizeof clean); + dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0); if (check_filter(elp, FLOG, dir, 1) < 0) goto options_rejected; } @@ -2446,7 +2606,7 @@ if (files_from) { char *h, *p; - int q; + int q = 0; if (argc > 2 || (!am_daemon && !am_server && argc == 1)) { usage(FERROR); exit_cleanup(RERR_SYNTAX); @@ -2480,7 +2640,19 @@ if (check_filter(&daemon_filter_list, FLOG, dir, 0) < 0) goto options_rejected; } - filesfrom_fd = open(files_from, O_RDONLY|O_BINARY); + /* Operator-supplied path that may transit attacker-writable + * parents; refuse symlinks not owned by uid 0 or our euid, + * 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; if (filesfrom_fd < 0) { snprintf(err_buf, sizeof err_buf, "failed to open files-from file %s: %s\n", @@ -2520,7 +2692,7 @@ **/ char *safe_arg(const char *opt, const char *arg) { -#define SHELL_CHARS "!#$&;|<>(){}\"'` \t\\" +#define SHELL_CHARS "!#$&;|<>(){}\"\'` \t\n\r\\" #define WILD_CHARS "*?[]" /* We don't allow remote brace expansion */ BOOL is_filename_arg = !opt; char *escapes = is_filename_arg ? SHELL_CHARS : WILD_CHARS SHELL_CHARS; @@ -2539,7 +2711,16 @@ escape_leading_tilde = 1; } for (f = arg; *f; f++) { - if (strchr(escapes, *f)) + if (*f == '\\') { + /* Mirror the writer below: in filename mode a backslash + * before a wildcard is not doubled, so don't reserve a slot + * for it. The "f[1] &&" also avoids the strchr(WILD_CHARS, + * '\0') footgun (which matches the terminator) on a trailing + * backslash -- otherwise the counter and writer disagree and + * an uninitialized heap byte leaks into the result. */ + if (!is_filename_arg || !(f[1] && strchr(WILD_CHARS, f[1]))) + extras++; + } else if (strchr(escapes, *f)) extras++; } } @@ -2564,7 +2745,7 @@ *t++ = '\\'; while (*f) { if (*f == '\\') { - if (!is_filename_arg || !strchr(WILD_CHARS, f[1])) + if (!is_filename_arg || !(f[1] && strchr(WILD_CHARS, f[1]))) *t++ = '\\'; } else if (strchr(escapes, *f)) *t++ = '\\'; @@ -2604,7 +2785,10 @@ if (protect_args) argstr[x++] = 's'; - for (i = 0; i < verbose; i++) + /* `verbose` is unbounded (one increment per -v on our own command + * line), so an uncapped loop walks past argstr[64]. Anything beyond + * level ~5 is meaningless to the server anyway. */ + for (i = 0; i < verbose && i < 9; i++) argstr[x++] = 'v'; if (quiet && msgs2stderr) @@ -2881,6 +3065,11 @@ if (copy_unsafe_links) args[ac++] = "--copy-unsafe-links"; + /* --insecure-links is NOT forwarded: it is a local-only opt-out. A daemon + * governs its own confinement via the "insecure links" module parameter and + * drops a connection that sends --insecure-links; a remote-shell peer that + * wants it must be given it on its own side (e.g. via --rsync-path). */ + if (safe_symlinks) args[ac++] = "--safe-links"; diff -Nru rsync-3.4.1+ds1/packaging/auto-Makefile rsync-3.5.0+ds1/packaging/auto-Makefile --- rsync-3.4.1+ds1/packaging/auto-Makefile 2024-04-10 19:23:58.000000000 +0000 +++ rsync-3.5.0+ds1/packaging/auto-Makefile 2026-06-06 06:07:20.000000000 +0000 @@ -1,4 +1,4 @@ -TARGETS := all install install-ssl-daemon install-all install-strip conf gen reconfigure restatus \ +TARGETS := all install install-ssl-daemon install-all install-strip uninstall uninstall-ssl-daemon uninstall-all conf gen reconfigure restatus \ proto man clean cleantests distclean test check check29 check30 installcheck splint \ doxygen doxygen-upload finddead rrsync diff -Nru rsync-3.4.1+ds1/packaging/branch-from-patch rsync-3.5.0+ds1/packaging/branch-from-patch --- rsync-3.4.1+ds1/packaging/branch-from-patch 2024-11-05 19:20:28.000000000 +0000 +++ rsync-3.5.0+ds1/packaging/branch-from-patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,174 +0,0 @@ -#!/usr/bin/env -S python3 -B - -# This script turns one or more diff files in the patches dir (which is -# expected to be a checkout of the rsync-patches git repo) into a branch -# in the main rsync git checkout. This allows the applied patch to be -# merged with the latest rsync changes and tested. To update the diff -# with the resulting changes, see the patch-update script. - -import os, sys, re, argparse, glob - -sys.path = ['packaging'] + sys.path - -from pkglib import * - -def main(): - global created, info, local_branch - - cur_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir) - - local_branch = get_patch_branches(args.base_branch) - - if args.delete_local_branches: - for name in sorted(local_branch): - branch = f"patch/{args.base_branch}/{name}" - cmd_chk(['git', 'branch', '-D', branch]) - local_branch = set() - - if args.add_missing: - for fn in sorted(glob.glob(f"{args.patches_dir}/*.diff")): - name = re.sub(r'\.diff$', '', re.sub(r'.+/', '', fn)) - if name not in local_branch and fn not in args.patch_files: - args.patch_files.append(fn) - - if not args.patch_files: - return - - for fn in args.patch_files: - if not fn.endswith('.diff'): - die(f"Filename is not a .diff file: {fn}") - if not os.path.isfile(fn): - die(f"File not found: {fn}") - - scanned = set() - info = { } - - patch_list = [ ] - for fn in args.patch_files: - m = re.match(r'^(?P.*?)(?P[^/]+)\.diff$', fn) - patch = argparse.Namespace(**m.groupdict()) - if patch.name in scanned: - continue - patch.fn = fn - - lines = [ ] - commit_hash = None - with open(patch.fn, 'r', encoding='utf-8') as fh: - for line in fh: - m = re.match(r'^based-on: (\S+)', line) - if m: - commit_hash = m[1] - break - if (re.match(r'^index .*\.\..* \d', line) - or re.match(r'^diff --git ', line) - or re.match(r'^--- (old|a)/', line)): - break - lines.append(re.sub(r'\s*\Z', "\n", line, 1)) - info_txt = ''.join(lines).strip() + "\n" - lines = None - - parent = args.base_branch - patches = re.findall(r'patch -p1 <%s/(\S+)\.diff' % args.patches_dir, info_txt) - if patches: - last = patches.pop() - if last != patch.name: - warn(f"No identity patch line in {patch.fn}") - patches.append(last) - if patches: - parent = patches.pop() - if parent not in scanned: - diff_fn = patch.dir + parent + '.diff' - if not os.path.isfile(diff_fn): - die(f"Failed to find parent of {patch.fn}: {parent}") - # Add parent to args.patch_files so that we will look for the - # parent's parent. Any duplicates will be ignored. - args.patch_files.append(diff_fn) - else: - warn(f"No patch lines found in {patch.fn}") - - info[patch.name] = [ parent, info_txt, commit_hash ] - - patch_list.append(patch) - - created = set() - for patch in patch_list: - create_branch(patch) - - cmd_chk(['git', 'checkout', args.base_branch]) - - -def create_branch(patch): - if patch.name in created: - return - created.add(patch.name) - - parent, info_txt, commit_hash = info[patch.name] - parent = argparse.Namespace(dir=patch.dir, name=parent, fn=patch.dir + parent + '.diff') - - if parent.name == args.base_branch: - parent_branch = commit_hash if commit_hash else args.base_branch - else: - create_branch(parent) - parent_branch = '/'.join(['patch', args.base_branch, parent.name]) - - branch = '/'.join(['patch', args.base_branch, patch.name]) - print("\n" + '=' * 64) - print(f"Processing {branch} ({parent_branch})") - - if patch.name in local_branch: - cmd_chk(['git', 'branch', '-D', branch]) - - cmd_chk(['git', 'checkout', '-b', branch, parent_branch]) - - info_fn = 'PATCH.' + patch.name - with open(info_fn, 'w', encoding='utf-8') as fh: - fh.write(info_txt) - cmd_chk(['git', 'add', info_fn]) - - with open(patch.fn, 'r', encoding='utf-8') as fh: - patch_txt = fh.read() - - cmd_run('patch -p1'.split(), input=patch_txt) - - for fn in glob.glob('*.orig') + glob.glob('*/*.orig'): - os.unlink(fn) - - pos = 0 - new_file_re = re.compile(r'\nnew file mode (?P\d+)\s+--- /dev/null\s+\+\+\+ b/(?P.+)') - while True: - m = new_file_re.search(patch_txt, pos) - if not m: - break - os.chmod(m['fn'], int(m['mode'], 8)) - cmd_chk(['git', 'add', m['fn']]) - pos = m.end() - - while True: - cmd_chk('git status'.split()) - ans = input('Press Enter to commit, Ctrl-C to abort, or type a wild-name to add a new file: ') - if ans == '': - break - cmd_chk("git add " + ans, shell=True) - - while True: - s = cmd_run(['git', 'commit', '-a', '-m', f"Creating branch from {patch.name}.diff."]) - if not s.returncode: - break - s = cmd_run([os.environ.get('SHELL', '/bin/sh')]) - if s.returncode: - die('Aborting due to shell error code') - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Create a git patch branch from an rsync patch file.", add_help=False) - parser.add_argument('--branch', '-b', dest='base_branch', metavar='BASE_BRANCH', default='master', help="The branch the patch is based on. Default: master.") - parser.add_argument('--add-missing', '-a', action='store_true', help="Add a branch for every patches/*.diff that doesn't have a branch.") - parser.add_argument('--skip-check', action='store_true', help="Skip the check that ensures starting with a clean branch.") - parser.add_argument('--delete', dest='delete_local_branches', action='store_true', help="Delete all the local patch/BASE/* branches, not just the ones that are being recreated.") - parser.add_argument('--patches-dir', '-p', metavar='DIR', default='patches', help="Override the location of the rsync-patches dir. Default: patches.") - parser.add_argument('patch_files', metavar='patches/DIFF_FILE', nargs='*', help="Specify what patch diff files to process. Default: all of them.") - parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.") - args = parser.parse_args() - main() - -# vim: sw=4 et ft=python diff -Nru rsync-3.4.1+ds1/packaging/ftp.filt rsync-3.5.0+ds1/packaging/ftp.filt --- rsync-3.4.1+ds1/packaging/ftp.filt 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/packaging/ftp.filt 2026-05-20 05:36:44.000000000 +0000 @@ -0,0 +1,2 @@ +- /generated-files/ +- /binaries/ diff -Nru rsync-3.4.1+ds1/packaging/lsb/rsync.spec rsync-3.5.0+ds1/packaging/lsb/rsync.spec --- rsync-3.4.1+ds1/packaging/lsb/rsync.spec 2025-01-15 20:49:23.000000000 +0000 +++ rsync-3.5.0+ds1/packaging/lsb/rsync.spec 2026-08-13 00:05:47.000000000 +0000 @@ -1,6 +1,6 @@ Summary: A fast, versatile, remote (and local) file-copying tool Name: rsync -Version: 3.4.1 +Version: 3.5.0 %define fullversion %{version} Release: 1 %define srcdir src @@ -79,5 +79,5 @@ %dir /etc/rsync-ssl/certs %changelog -* Thu Jan 16 2025 Rsync Project -Released 3.4.1. +* Thu Aug 13 2026 Rsync Project +Released 3.5.0. diff -Nru rsync-3.4.1+ds1/packaging/patch-update rsync-3.5.0+ds1/packaging/patch-update --- rsync-3.4.1+ds1/packaging/patch-update 2020-11-01 19:27:08.000000000 +0000 +++ rsync-3.5.0+ds1/packaging/patch-update 1970-01-01 00:00:00.000000000 +0000 @@ -1,244 +0,0 @@ -#!/usr/bin/env -S python3 -B - -# This script is used to turn one or more of the "patch/BASE/*" branches -# into one or more diffs in the "patches" directory. Pass the option -# --gen if you want generated files in the diffs. Pass the name of -# one or more diffs if you want to just update a subset of all the -# diffs. - -import os, sys, re, argparse, time, shutil - -sys.path = ['packaging'] + sys.path - -from pkglib import * - -MAKE_GEN_CMDS = [ - './prepare-source'.split(), - 'cd build && if test -f config.status ; then ./config.status ; else ../configure ; fi', - 'make -C build gen'.split(), - ] -TMP_DIR = "patches.gen" - -os.environ['GIT_MERGE_AUTOEDIT'] = 'no' - -def main(): - global master_commit, parent_patch, description, completed, last_touch - - if not os.path.isdir(args.patches_dir): - die(f'No "{args.patches_dir}" directory was found.') - if not os.path.isdir('.git'): - die('No ".git" directory present in the current dir.') - - starting_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir) - - master_commit = latest_git_hash(args.base_branch) - - if cmd_txt_chk(['packaging/prep-auto-dir']).out == '': - die('You must setup an auto-build-save dir to use this script.') - - if args.gen: - if os.path.lexists(TMP_DIR): - die(f'"{TMP_DIR}" must not exist in the current directory.') - gen_files = get_gen_files() - os.mkdir(TMP_DIR, 0o700) - for cmd in MAKE_GEN_CMDS: - cmd_chk(cmd) - cmd_chk(['rsync', '-a', *gen_files, f'{TMP_DIR}/master/']) - - last_touch = int(time.time()) - - # Start by finding all patches so that we can load all possible parents. - patches = sorted(list(get_patch_branches(args.base_branch))) - - parent_patch = { } - description = { } - - for patch in patches: - branch = f"patch/{args.base_branch}/{patch}" - desc = '' - proc = cmd_pipe(['git', 'diff', '-U1000', f"{args.base_branch}...{branch}", '--', f"PATCH.{patch}"]) - in_diff = False - for line in proc.stdout: - if in_diff: - if not re.match(r'^[ +]', line): - continue - line = line[1:] - m = re.search(r'patch -p1 = int(time.time()): - time.sleep(1) - cmd_chk(['git', 'checkout', starting_branch]) - cmd_chk(['packaging/prep-auto-dir'], discard='output') - - -def update_patch(patch): - global last_touch - - completed.add(patch) # Mark it as completed early to short-circuit any (bogus) dependency loops. - - parent = parent_patch.get(patch, None) - if parent: - if parent not in completed: - if not update_patch(parent): - return 0 - based_on = parent = f"patch/{args.base_branch}/{parent}" - else: - parent = args.base_branch - based_on = master_commit - - print(f"======== {patch} ========") - - while args.gen and last_touch >= int(time.time()): - time.sleep(1) - - branch = f"patch/{args.base_branch}/{patch}" - s = cmd_run(['git', 'checkout', branch]) - if s.returncode != 0: - return 0 - - s = cmd_run(['git', 'merge', based_on]) - ok = s.returncode == 0 - skip_shell = False - if not ok or args.cmd or args.make or args.shell: - cmd_chk(['packaging/prep-auto-dir'], discard='output') - if not ok: - print(f'"git merge {based_on}" incomplete -- please fix.') - if not run_a_shell(parent, patch): - return 0 - if not args.make and not args.cmd: - skip_shell = True - if args.make: - if cmd_run(['packaging/smart-make']).returncode != 0: - if not run_a_shell(parent, patch): - return 0 - if not args.cmd: - skip_shell = True - if args.cmd: - if cmd_run(args.cmd).returncode != 0: - if not run_a_shell(parent, patch): - return 0 - skip_shell = True - if args.shell and not skip_shell: - if not run_a_shell(parent, patch): - return 0 - - with open(f"{args.patches_dir}/{patch}.diff", 'w', encoding='utf-8') as fh: - fh.write(description[patch]) - fh.write(f"\nbased-on: {based_on}\n") - - if args.gen: - gen_files = get_gen_files() - for cmd in MAKE_GEN_CMDS: - cmd_chk(cmd) - cmd_chk(['rsync', '-a', *gen_files, f"{TMP_DIR}/{patch}/"]) - else: - gen_files = [ ] - last_touch = int(time.time()) - - proc = cmd_pipe(['git', 'diff', based_on]) - skipping = False - for line in proc.stdout: - if skipping: - if not re.match(r'^diff --git a/', line): - continue - skipping = False - elif re.match(r'^diff --git a/PATCH', line): - skipping = True - continue - if not re.match(r'^index ', line): - fh.write(line) - proc.communicate() - - if args.gen: - e_tmp_dir = re.escape(TMP_DIR) - diff_re = re.compile(r'^(diff -Nurp) %s/[^/]+/(.*?) %s/[^/]+/(.*)' % (e_tmp_dir, e_tmp_dir)) - minus_re = re.compile(r'^\-\-\- %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir) - plus_re = re.compile(r'^\+\+\+ %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir) - - if parent == args.base_branch: - parent_dir = 'master' - else: - m = re.search(r'([^/]+)$', parent) - parent_dir = m[1] - - proc = cmd_pipe(['diff', '-Nurp', f"{TMP_DIR}/{parent_dir}", f"{TMP_DIR}/{patch}"]) - for line in proc.stdout: - line = diff_re.sub(r'\1 a/\2 b/\3', line) - line = minus_re.sub(r'--- a/\1', line) - line = plus_re.sub(r'+++ b/\1', line) - fh.write(line) - proc.communicate() - - return 1 - - -def run_a_shell(parent, patch): - m = re.search(r'([^/]+)$', parent) - parent_dir = m[1] - os.environ['PS1'] = f"[{parent_dir}] {patch}: " - - while True: - s = cmd_run([os.environ.get('SHELL', '/bin/sh')]) - if s.returncode != 0: - ans = input("Abort? [n/y] ") - if re.match(r'^y', ans, flags=re.I): - return False - continue - cur_branch, is_clean, status_txt = check_git_status(0) - if is_clean: - break - print(status_txt, end='') - - cmd_run('rm -f build/*.o build/*/*.o') - - return True - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Turn a git branch back into a diff files in the patches dir.", add_help=False) - parser.add_argument('--branch', '-b', dest='base_branch', metavar='BASE_BRANCH', default='master', help="The branch the patch is based on. Default: master.") - parser.add_argument('--skip-check', action='store_true', help="Skip the check that ensures starting with a clean branch.") - parser.add_argument('--make', '-m', action='store_true', help="Run the smart-make script in every patch branch.") - parser.add_argument('--cmd', '-c', help="Run a command in every patch branch.") - parser.add_argument('--shell', '-s', action='store_true', help="Launch a shell for every patch/BASE/* branch updated, not just when a conflict occurs.") - parser.add_argument('--gen', metavar='DIR', nargs='?', const='', help='Include generated files. Optional DIR value overrides the default of using the "patches" dir.') - parser.add_argument('--patches-dir', '-p', metavar='DIR', default='patches', help="Override the location of the rsync-patches dir. Default: patches.") - parser.add_argument('patch_files', metavar='patches/DIFF_FILE', nargs='*', help="Specify what patch diff files to process. Default: all of them.") - parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.") - args = parser.parse_args() - if args.gen == '': - args.gen = args.patches_dir - elif args.gen is not None: - args.patches_dir = args.gen - main() - -# vim: sw=4 et ft=python diff -Nru rsync-3.4.1+ds1/packaging/pkglib.py rsync-3.5.0+ds1/packaging/pkglib.py --- rsync-3.4.1+ds1/packaging/pkglib.py 2024-04-10 19:23:58.000000000 +0000 +++ rsync-3.5.0+ds1/packaging/pkglib.py 2026-08-12 23:16:21.000000000 +0000 @@ -206,7 +206,14 @@ die("Unable to find RSYNC_VERSION define in version.h") -def get_NEWS_version_info(): +def get_NEWS_version_info(skip_version=None): + """Return (last_version, its protocol version, {version: protocol-change date}). + + skip_version lets the caller exclude the version it is about to release. + Its NEWS entry may already carry a release date -- dated by hand, or by an + earlier run of --step-3-tweak -- and would otherwise be reported as the + PREVIOUS release, which is both wrong and fatal when it has no table row yet. + """ rel_re = re.compile(r'^\| \S{2} \w{3} \d{4}\s+\|\s+(?P\d+\.\d+\.\d+)\s+\|\s+(?P\d{2} \w{3} \d{4})?\s+\|\s+(?P\d+)\s+\|') last_version = last_protocol_version = None pdate = { } @@ -215,7 +222,7 @@ for line in fh: if not last_version: # Find the first non-dev|pre version with a release date. m = re.search(r'rsync (\d+\.\d+\.\d+) .*\d\d\d\d', line) - if m: + if m and m[1] != skip_version: last_version = m[1] m = rel_re.match(line) if m: diff -Nru rsync-3.4.1+ds1/packaging/release-rsync rsync-3.5.0+ds1/packaging/release-rsync --- rsync-3.4.1+ds1/packaging/release-rsync 2025-01-14 18:50:22.000000000 +0000 +++ rsync-3.5.0+ds1/packaging/release-rsync 1970-01-01 00:00:00.000000000 +0000 @@ -1,414 +0,0 @@ -#!/usr/bin/env -S python3 -B - -# This script expects the directory ~/samba-rsync-ftp to exist and to be a -# copy of the /home/ftp/pub/rsync dir on samba.org. When the script is done, -# the git repository in the current directory will be updated, and the local -# ~/samba-rsync-ftp dir will be ready to be rsynced to samba.org. See the -# script samba-rsync for an easy way to initialize the local ftp copy and to -# thereafter update the remote files from your local copy. - -# This script also expects to be able to gpg sign the resulting tar files -# using your default gpg key. Make sure that the html download.html file -# has a link to the relevant keys that are authorized to sign the tar files -# and also make sure that the following commands work as expected: -# -# touch TeMp -# gpg --sign TeMp -# gpg --verify TeMp.gpg -# gpg --sign TeMp -# rm TeMp* -# -# The second time you sign the file it should NOT prompt you for your password -# (unless the timeout period has passed). It will prompt about overriding the -# existing TeMp.gpg file, though. - -import os, sys, re, argparse, glob, shutil, signal -from datetime import datetime -from getpass import getpass - -sys.path = ['packaging'] + sys.path - -from pkglib import * - -os.environ['LESS'] = 'mqeiXR'; # Make sure that -F is turned off and -R is turned on. -dest = os.environ['HOME'] + '/samba-rsync-ftp' -ORIGINAL_PATH = os.environ['PATH'] - -def main(): - if not os.path.isfile('packaging/release-rsync'): - die('You must run this script from the top of your rsync checkout.') - - now = datetime.now() - cl_today = now.strftime('* %a %b %d %Y') - year = now.strftime('%Y') - ztoday = now.strftime('%d %b %Y') - today = ztoday.lstrip('0') - - curdir = os.getcwd() - - signal.signal(signal.SIGINT, signal_handler) - - if cmd_txt_chk(['packaging/prep-auto-dir']).out == '': - die('You must setup an auto-build-save dir to use this script.'); - - auto_dir, gen_files = get_gen_files(True) - gen_pathnames = [ os.path.join(auto_dir, fn) for fn in gen_files ] - - dash_line = '=' * 74 - - print(f"""\ -{dash_line} -== This will release a new version of rsync onto an unsuspecting world. == -{dash_line} -""") - - with open('build/rsync.1') as fh: - for line in fh: - if line.startswith(r'.\" prefix='): - doc_prefix = line.split('=')[1].strip() - if doc_prefix != '/usr': - warn(f"*** The documentation was built with prefix {doc_prefix} instead of /usr ***") - die("*** Read the md2man script for a way to override this. ***") - break - if line.startswith('.P'): - die("Failed to find the prefix comment at the start of the rsync.1 manpage.") - - if not os.path.isdir(dest): - die(dest, "dest does not exist") - if not os.path.isdir('.git'): - die("There is no .git dir in the current directory.") - if os.path.lexists('a'): - die('"a" must not exist in the current directory.') - if os.path.lexists('b'): - die('"b" must not exist in the current directory.') - if os.path.lexists('patches.gen'): - die('"patches.gen" must not exist in the current directory.') - - check_git_state(args.master_branch, True, 'patches') - - curversion = get_rsync_version() - - # All version values are strings! - lastversion, last_protocol_version, pdate = get_NEWS_version_info() - protocol_version, subprotocol_version = get_protocol_versions() - - version = curversion - m = re.search(r'pre(\d+)', version) - if m: - version = re.sub(r'pre\d+', 'pre' + str(int(m[1]) + 1), version) - else: - version = version.replace('dev', 'pre1') - - ans = input(f"Please enter the version number of this release: [{version}] ") - if ans == '.': - version = re.sub(r'pre\d+', '', version) - elif ans != '': - version = ans - if not re.match(r'^[\d.]+(pre\d+)?$', version): - die(f'Invalid version: "{version}"') - - v_ver = 'v' + version - rsync_ver = 'rsync-' + version - - if os.path.lexists(rsync_ver): - die(f'"{rsync_ver}" must not exist in the current directory.') - - out = cmd_txt_chk(['git', 'tag', '-l', v_ver]).out - if out != '': - print(f"Tag {v_ver} already exists.") - ans = input("\nDelete tag or quit? [Q/del] ") - if not re.match(r'^del', ans, flags=re.I): - die("Aborted") - cmd_chk(['git', 'tag', '-d', v_ver]) - if os.path.isdir('patches/.git'): - cmd_chk(f"cd patches && git tag -d '{v_ver}'") - - version = re.sub(r'[-.]*pre[-.]*', 'pre', version) - if 'pre' in version and not curversion.endswith('dev'): - lastversion = curversion - - ans = input(f"Enter the previous version to produce a patch against: [{lastversion}] ") - if ans != '': - lastversion = ans - lastversion = re.sub(r'[-.]*pre[-.]*', 'pre', lastversion) - - rsync_lastver = 'rsync-' + lastversion - if os.path.lexists(rsync_lastver): - die(f'"{rsync_lastver}" must not exist in the current directory.') - - m = re.search(r'(pre\d+)', version) - pre = m[1] if m else '' - - release = '0.1' if pre else '1' - ans = input(f"Please enter the RPM release number of this release: [{release}] ") - if ans != '': - release = ans - if pre: - release += '.' + pre - - finalversion = re.sub(r'pre\d+', '', version) - proto_changed = protocol_version != last_protocol_version - if proto_changed: - if finalversion in pdate: - proto_change_date = pdate[finalversion] - else: - while True: - ans = input("On what date did the protocol change to {protocol_version} get checked in? (dd Mmm yyyy) ") - if re.match(r'^\d\d \w\w\w \d\d\d\d$', ans): - break - proto_change_date = ans - else: - proto_change_date = ' ' * 11 - - if 'pre' in lastversion: - if not pre: - die("You should not diff a release version against a pre-release version.") - srcdir = srcdiffdir = lastsrcdir = 'src-previews' - skipping = ' ** SKIPPING **' - elif pre: - srcdir = srcdiffdir = 'src-previews' - lastsrcdir = 'src' - skipping = ' ** SKIPPING **' - else: - srcdir = lastsrcdir = 'src' - srcdiffdir = 'src-diffs' - skipping = '' - - print(f""" -{dash_line} -version is "{version}" -lastversion is "{lastversion}" -dest is "{dest}" -curdir is "{curdir}" -srcdir is "{srcdir}" -srcdiffdir is "{srcdiffdir}" -lastsrcdir is "{lastsrcdir}" -release is "{release}" - -About to: - - tweak SUBPROTOCOL_VERSION in rsync.h, if needed - - tweak the version in version.h and the spec files - - tweak NEWS.md to ensure header values are correct - - generate configure.sh, config.h.in, and proto.h - - page through the differences -""") - ans = input(" ") - - specvars = { - 'Version:': finalversion, - 'Release:': release, - '%define fullversion': f'%{{version}}{pre}', - 'Released': version + '.', - '%define srcdir': srcdir, - } - - tweak_files = 'version.h rsync.h'.split() - tweak_files += glob.glob('packaging/*.spec') - tweak_files += glob.glob('packaging/*/*.spec') - - for fn in tweak_files: - with open(fn, 'r', encoding='utf-8') as fh: - old_txt = txt = fh.read() - if fn == 'version.h': - x_re = re.compile(r'^(#define RSYNC_VERSION).*', re.M) - msg = f"Unable to update RSYNC_VERSION in {fn}" - txt = replace_or_die(x_re, r'\1 "%s"' % version, txt, msg) - elif '.spec' in fn: - for var, val in specvars.items(): - x_re = re.compile(r'^%s .*' % re.escape(var), re.M) - txt = replace_or_die(x_re, var + ' ' + val, txt, f"Unable to update {var} in {fn}") - x_re = re.compile(r'^\* \w\w\w \w\w\w \d\d \d\d\d\d (.*)', re.M) - txt = replace_or_die(x_re, r'%s \1' % cl_today, txt, f"Unable to update ChangeLog header in {fn}") - elif fn == 'rsync.h': - x_re = re.compile('(#define\s+SUBPROTOCOL_VERSION)\s+(\d+)') - repl = lambda m: m[1] + ' ' + ('0' if not pre or not proto_changed else '1' if m[2] == '0' else m[2]) - txt = replace_or_die(x_re, repl, txt, f"Unable to find SUBPROTOCOL_VERSION define in {fn}") - elif fn == 'NEWS.md': - efv = re.escape(finalversion) - x_re = re.compile(r'^# NEWS for rsync %s \(UNRELEASED\)\s+## Changes in this version:\n' % efv - + r'(\n### PROTOCOL NUMBER:\s+- The protocol number was changed to \d+\.\n)?') - rel_day = 'UNRELEASED' if pre else today - repl = (f'# NEWS for rsync {finalversion} ({rel_day})\n\n' - + '## Changes in this version:\n') - if proto_changed: - repl += f'\n### PROTOCOL NUMBER:\n\n - The protocol number was changed to {protocol_version}.\n' - good_top = re.sub(r'\(.*?\)', '(UNRELEASED)', repl, 1) - msg = f"The top lines of {fn} are not in the right format. It should be:\n" + good_top - txt = replace_or_die(x_re, repl, txt, msg) - x_re = re.compile(r'^(\| )(\S{2} \S{3} \d{4})(\s+\|\s+%s\s+\| ).{11}(\s+\| )\S{2}(\s+\|+)$' % efv, re.M) - repl = lambda m: m[1] + (m[2] if pre else ztoday) + m[3] + proto_change_date + m[4] + protocol_version + m[5] - txt = replace_or_die(x_re, repl, txt, f'Unable to find "| ?? ??? {year} | {finalversion} | ... |" line in {fn}') - else: - die(f"Unrecognized file in tweak_files: {fn}") - - if txt != old_txt: - print(f"Updating {fn}") - with open(fn, 'w', encoding='utf-8') as fh: - fh.write(txt) - - cmd_chk(['packaging/year-tweak']) - - print(dash_line) - cmd_run("git diff".split()) - - srctar_name = f"{rsync_ver}.tar.gz" - pattar_name = f"rsync-patches-{version}.tar.gz" - diff_name = f"{rsync_lastver}-{version}.diffs.gz" - srctar_file = os.path.join(dest, srcdir, srctar_name) - pattar_file = os.path.join(dest, srcdir, pattar_name) - diff_file = os.path.join(dest, srcdiffdir, diff_name) - lasttar_file = os.path.join(dest, lastsrcdir, rsync_lastver + '.tar.gz') - - print(f"""\ -{dash_line} - -About to: - - git commit all changes - - run a full build, ensuring that the manpages & configure.sh are up-to-date - - merge the {args.master_branch} branch into the patch/{args.master_branch}/* branches - - update the files in the "patches" dir and OPTIONALLY (if you type 'y') to - run patch-update with the --make option (which opens a shell on error) -""") - ans = input(" ") - - s = cmd_run(['git', 'commit', '-a', '-m', f'Preparing for release of {version} [buildall]']) - if s.returncode: - die('Aborting') - - cmd_chk('touch configure.ac && packaging/smart-make && make gen') - - print('Creating any missing patch branches.') - s = cmd_run(f'packaging/branch-from-patch --branch={args.master_branch} --add-missing') - if s.returncode: - die('Aborting') - - print('Updating files in "patches" dir ...') - s = cmd_run(f'packaging/patch-update --branch={args.master_branch}') - if s.returncode: - die('Aborting') - - if re.match(r'^y', ans, re.I): - print(f'\nRunning smart-make on all "patch/{args.master_branch}/*" branches ...') - cmd_run(f"packaging/patch-update --branch={args.master_branch} --skip-check --make") - - if os.path.isdir('patches/.git'): - s = cmd_run(f"cd patches && git commit -a -m 'The patches for {version}.'") - if s.returncode: - die('Aborting') - - print(f"""\ -{dash_line} - -About to: - - create signed tag for this release: {v_ver} - - create release diffs, "{diff_name}" - - create release tar, "{srctar_name}" - - generate {rsync_ver}/patches/* files - - create patches tar, "{pattar_name}" - - update top-level README.md, NEWS.md, TODO, and ChangeLog - - update top-level rsync*.html manpages - - gpg-sign the release files - - update hard-linked top-level release files{skipping} -""") - ans = input(" ") - - # TODO: is there a better way to ensure that our passphrase is in the agent? - cmd_run("touch TeMp; gpg --sign TeMp; rm TeMp*") - - out = cmd_txt(f"git tag -s -m 'Version {version}.' {v_ver}", capture='combined').out - print(out, end='') - if 'bad passphrase' in out or 'failed' in out: - die('Aborting') - - if os.path.isdir('patches/.git'): - out = cmd_txt(f"cd patches && git tag -s -m 'Version {version}.' {v_ver}", capture='combined').out - print(out, end='') - if 'bad passphrase' in out or 'failed' in out: - die('Aborting') - - os.environ['PATH'] = ORIGINAL_PATH - - # Extract the generated files from the old tar. - tweaked_gen_files = [ os.path.join(rsync_lastver, fn) for fn in gen_files ] - cmd_run(['tar', 'xzf', lasttar_file, *tweaked_gen_files]) - os.rename(rsync_lastver, 'a') - - print(f"Creating {diff_file} ...") - cmd_chk(['rsync', '-a', *gen_pathnames, 'b/']) - - sed_script = r's:^((---|\+\+\+) [ab]/[^\t]+)\t.*:\1:' # CAUTION: must not contain any single quotes! - cmd_chk(f"(git diff v{lastversion} {v_ver} -- ':!.github'; diff -upN a b | sed -r '{sed_script}') | gzip -9 >{diff_file}") - shutil.rmtree('a') - os.rename('b', rsync_ver) - - print(f"Creating {srctar_file} ...") - cmd_chk(f"git archive --format=tar --prefix={rsync_ver}/ {v_ver} | tar xf -") - cmd_chk(f"support/git-set-file-times --quiet --prefix={rsync_ver}/") - cmd_chk(['fakeroot', 'tar', 'czf', srctar_file, '--exclude=.github', rsync_ver]) - shutil.rmtree(rsync_ver) - - print(f'Updating files in "{rsync_ver}/patches" dir ...') - os.mkdir(rsync_ver, 0o755) - os.mkdir(f"{rsync_ver}/patches", 0o755) - cmd_chk(f"packaging/patch-update --skip-check --branch={args.master_branch} --gen={rsync_ver}/patches".split()) - - print(f"Creating {pattar_file} ...") - cmd_chk(['fakeroot', 'tar', 'chzf', pattar_file, rsync_ver + '/patches']) - shutil.rmtree(rsync_ver) - - print(f"Updating the other files in {dest} ...") - md_files = 'README.md NEWS.md INSTALL.md'.split() - html_files = [ fn for fn in gen_pathnames if fn.endswith('.html') ] - cmd_chk(['rsync', '-a', *md_files, *html_files, dest]) - cmd_chk(["./md-convert", "--dest", dest, *md_files]) - - cmd_chk(f"git log --name-status | gzip -9 >{dest}/ChangeLog.gz") - - for fn in (srctar_file, pattar_file, diff_file): - asc_fn = fn + '.asc' - if os.path.lexists(asc_fn): - os.unlink(asc_fn) - res = cmd_run(['gpg', '--batch', '-ba', fn]) - if res.returncode != 0 and res.returncode != 2: - die("gpg signing failed") - - if not pre: - for find in f'{dest}/rsync-*.gz {dest}/rsync-*.asc {dest}/src-previews/rsync-*diffs.gz*'.split(): - for fn in glob.glob(find): - os.unlink(fn) - top_link = [ - srctar_file, f"{srctar_file}.asc", - pattar_file, f"{pattar_file}.asc", - diff_file, f"{diff_file}.asc", - ] - for fn in top_link: - os.link(fn, re.sub(r'/src(-\w+)?/', '/', fn)) - - print(f"""\ -{dash_line} - -Local changes are done. When you're satisfied, push the git repository -and rsync the release files. Remember to announce the release on *BOTH* -rsync-announce@lists.samba.org and rsync@lists.samba.org (and the web)! -""") - - -def replace_or_die(regex, repl, txt, die_msg): - m = regex.search(txt) - if not m: - die(die_msg) - return regex.sub(repl, txt, 1) - - -def signal_handler(sig, frame): - die("\nAborting due to SIGINT.") - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Prepare a new release of rsync in the git repo & ftp dir.", add_help=False) - parser.add_argument('--branch', '-b', dest='master_branch', default='master', help="The branch to release. Default: master.") - parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.") - args = parser.parse_args() - main() - -# vim: sw=4 et ft=python diff -Nru rsync-3.4.1+ds1/packaging/release.py rsync-3.5.0+ds1/packaging/release.py --- rsync-3.4.1+ds1/packaging/release.py 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/packaging/release.py 2026-08-12 23:16:21.000000000 +0000 @@ -0,0 +1,714 @@ +#!/usr/bin/env python3 + +# Step-based release script for rsync. Each step is a separate invocation +# selected by a --step-N-XX option, so the maintainer drives the release +# manually one piece at a time. +# +# All persistent state and working files live in ../release/ (a sibling of +# the rsync git checkout): +# +# ../release/rsync-ftp/ mirror of samba.org:/home/ftp/pub/rsync +# ../release/rsync-html/ release-time snapshot of the html site +# ../release/work/ scratch space for tarball / diff staging +# ../release/release-state.json info shared between steps +# +# The rsync-patches archive is no longer maintained and has been dropped. +# +# Run "packaging/release.py --list" to see the step list. + +import os, sys, re, argparse, glob, shutil, json, signal, subprocess +from datetime import datetime + +sys.path = ['packaging'] + sys.path + +from pkglib import ( + warn, die, cmd_run, cmd_chk, cmd_txt, cmd_txt_chk, cmd_pipe, + check_git_state, get_rsync_version, + get_NEWS_version_info, get_protocol_versions, +) + +# ---------- Paths ---------- + +RELEASE_DIR = os.path.realpath('../release') +FTP_DIR = os.path.join(RELEASE_DIR, 'rsync-ftp') +HTML_DIR = os.path.join(RELEASE_DIR, 'rsync-html') +WORK_DIR = os.path.join(RELEASE_DIR, 'work') +STATE_FILE = os.path.join(RELEASE_DIR, 'release-state.json') + +# The rsync-web/ subdirectory in the rsync source tree is the source-of-truth +# for the git-tracked html content. step-1-fetch snapshots it into HTML_DIR +# for the release flow, where it can be edited or augmented with server-side +# content before step-11-push-html sends it to samba.org. +HTML_SRC = os.path.realpath('rsync-web') + +FTP_REMOTE_PATH = '/home/ftp/pub/rsync' +HTML_REMOTE_PATH = '/home/httpd/html/rsync' + +# Files that ./configure + make produce and that the release tarball / diff +# need to bundle alongside the git-tracked source. Mirrors the GENFILES +# definition in Makefile.in (with rrsync.1{,.html} since we always configure +# --with-rrsync in --step-4-build). +GEN_FILES = [ + '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', + 'rrsync.1', 'rrsync.1.html', +] + +# ---------- Step registry ---------- + +STEPS = [ + ('step-1-fetch', 'mirror ../release/rsync-ftp from samba.org and snapshot ../release/rsync-html from rsync-web/'), + ('step-2-prepare', 'gather release info interactively and write release-state.json'), + ('step-3-tweak', 'update version.h, rsync.h, NEWS.md, and packaging/*.spec'), + ('step-4-build', 'run smart-make + make gen'), + ('step-5-commit', 'git commit -a (commit the prepared release changes)'), + ('step-6-tag', 'create the gpg-signed git tag'), + ('step-7-tarball', 'build the source tarball and diffs.gz against the previous release'), + ('step-8-update-ftp', 'refresh README/NEWS/INSTALL/html in the ftp dir, regen ChangeLog.gz, gpg-sign tarballs'), + ('step-9-toplinks', 'hard-link top-level release files (final releases only)'), + ('step-10-push-ftp', 'rsync ../release/rsync-ftp/ to samba.org'), + ('step-11-push-html', 'rsync ../release/rsync-html/ to samba.org (after any manual edits)'), + ('step-12-push-git', 'print the git push commands for you to run'), +] +STEP_FLAGS = [s[0] for s in STEPS] + +DASH_LINE = '=' * 74 + +# ---------- State helpers ---------- + +def load_state(): + if not os.path.isfile(STATE_FILE): + die(f"{STATE_FILE} not found. Run --step-2-prepare first.") + with open(STATE_FILE, 'r', encoding='utf-8') as fh: + return json.load(fh) + + +def save_state(state): + os.makedirs(RELEASE_DIR, exist_ok=True) + with open(STATE_FILE, 'w', encoding='utf-8') as fh: + json.dump(state, fh, indent=2, sort_keys=True) + fh.write('\n') + + +def require_samba_host(): + host = os.environ.get('RSYNC_SAMBA_HOST', '') + if not host.endswith('.samba.org'): + die("Set RSYNC_SAMBA_HOST in your environment to the samba hostname (e.g. hr3.samba.org).") + return host + + +def require_top_of_checkout(): + if not os.path.isfile('packaging/release.py'): + die("Run this script from the top of your rsync checkout.") + if not os.path.exists('.git'): + die("There is no .git in the current directory (run from the top of a git checkout or worktree).") + + +def replace_or_die(regex, repl, txt, die_msg): + m = regex.search(txt) + if not m: + die(die_msg) + return regex.sub(repl, txt, 1) + + +def section(title): + print(f"\n{DASH_LINE}\n== {title}\n{DASH_LINE}") + + +def confirm(prompt, default_no=True): + suffix = '[n] ' if default_no else '[y] ' + ans = input(f"{prompt} {suffix}").strip().lower() + if default_no: + return ans.startswith('y') + return ans == '' or ans.startswith('y') + + +# ---------- Step 1: fetch ftp + html ---------- + +def step_1_fetch(args): + host = require_samba_host() + os.makedirs(RELEASE_DIR, exist_ok=True) + os.makedirs(WORK_DIR, exist_ok=True) + + section(f"Fetching ftp dir into {FTP_DIR}") + if not os.path.isdir(FTP_DIR): + os.makedirs(FTP_DIR) + # packaging/ftp.filt is the authoritative copy of the .filt filter file + # that controls which subtrees rsync excludes from the FTP mirror. + # Seed FTP_DIR/.filt from it so the bundled version is what step-1's + # rsync uses here, and so step-10-push-ftp propagates it back to the + # server. --exclude=/.filt below stops the server's copy from + # overwriting our bundled one on the way down. + filt = os.path.join(FTP_DIR, '.filt') + bundled_filt = os.path.realpath('packaging/ftp.filt') + if not os.path.isfile(bundled_filt): + die(f"{bundled_filt} not found; cannot seed .filt for the FTP pull.") + shutil.copyfile(bundled_filt, filt) + cmd_chk(['rsync', '-aivOHP', f'-f:_{filt}', '--exclude=/.filt', + f'{host}:{FTP_REMOTE_PATH}/', f'{FTP_DIR}/']) + + section(f"Snapshotting html dir from {HTML_SRC} into {HTML_DIR}") + if not os.path.isdir(HTML_SRC): + die(f"{HTML_SRC} not found. This should be the in-tree rsync-web/ " + f"subdirectory; something is wrong with your checkout.") + os.makedirs(HTML_DIR, exist_ok=True) + cmd_chk(['rsync', '-aiv', f'{HTML_SRC}/', f'{HTML_DIR}/']) + + # Then mirror non-git html content from the server, skipping files that + # the html git already provides (driven by the 'filt' file in HTML_DIR). + filt = os.path.join(HTML_DIR, 'filt') + if os.path.exists(filt): + tmp_filt = os.path.join(HTML_DIR, 'tmp-filt') + cmd_chk(f"sed -n -e 's/[-P]/H/p' '{filt}' >'{tmp_filt}'") + cmd_chk(['rsync', '-aivOHP', f'-f._{tmp_filt}', + f'{host}:{HTML_REMOTE_PATH}/', f'{HTML_DIR}/']) + os.unlink(tmp_filt) + + print(f"\nFetch complete. Local dirs are now in {RELEASE_DIR}.") + + +# ---------- Step 2: prepare ---------- + +def step_2_prepare(args): + require_top_of_checkout() + os.makedirs(RELEASE_DIR, exist_ok=True) + + if not os.path.isdir(FTP_DIR): + die(f"{FTP_DIR} does not exist. Run --step-1-fetch first.") + + now = datetime.now().astimezone() + cl_today = now.strftime('* %a %b %d %Y') + year = now.strftime('%Y') + ztoday = now.strftime('%d %b %Y') + today = ztoday.lstrip('0') + tz_now = now.strftime('%z') + tz_num = tz_now[0:1].replace('+', '') + str(float(tz_now[1:3]) + float(tz_now[3:]) / 60) + + curversion = get_rsync_version() + # Skip the version we are releasing: its NEWS entry may already be dated, + # in which case it would otherwise be taken for the previous release. + lastversion, last_protocol_version, pdate = get_NEWS_version_info( + skip_version=re.sub(r'(pre\d+|dev)$', '', curversion)) + protocol_version, subprotocol_version = get_protocol_versions() + + # Default next version: bump preN, or move dev -> pre1. + version = curversion + m = re.search(r'pre(\d+)', version) + if m: + version = re.sub(r'pre\d+', 'pre' + str(int(m[1]) + 1), version) + else: + version = version.replace('dev', 'pre1') + + print(f"\nCurrent version (version.h): {curversion}") + print(f"Last released version (NEWS.md): {lastversion}") + print(f"Current protocol version: {protocol_version} (last released: {last_protocol_version})") + + ans = input(f"\nVersion to release [{version}, '.' to drop the preN suffix]: ").strip() + if ans == '.': + version = re.sub(r'pre\d+', '', version) + elif ans: + version = ans + if not re.match(r'^[\d.]+(pre\d+)?$', version): + die(f'Invalid version: "{version}"') + version = re.sub(r'[-.]*pre[-.]*', 'pre', version) + + if 'pre' in version and not curversion.endswith('dev'): + lastversion = curversion + + ans = input(f"Previous version to diff against [{lastversion}]: ").strip() + if ans: + lastversion = ans + lastversion = re.sub(r'[-.]*pre[-.]*', 'pre', lastversion) + + m = re.search(r'(pre\d+)', version) + pre = m[1] if m else '' + finalversion = re.sub(r'pre\d+', '', version) + + release = '0.1' if pre else '1' + ans = input(f"RPM release number [{release}]: ").strip() + if ans: + release = ans + if pre: + release += '.' + pre + + proto_changed = protocol_version != last_protocol_version + if proto_changed: + if finalversion in pdate: + proto_change_date = pdate[finalversion] + else: + while True: + ans = input(f"Date the protocol changed to {protocol_version} (dd Mmm yyyy): ").strip() + if re.match(r'^\d\d \w\w\w \d\d\d\d$', ans): + break + proto_change_date = ans + else: + proto_change_date = ' ' * 11 + + if 'pre' in lastversion: + if not pre: + die("Refusing to diff a release version against a pre-release version.") + srcdir = srcdiffdir = lastsrcdir = 'src-previews' + elif pre: + srcdir = srcdiffdir = 'src-previews' + lastsrcdir = 'src' + else: + srcdir = lastsrcdir = 'src' + srcdiffdir = 'src-diffs' + + state = { + 'version': version, + 'lastversion': lastversion, + 'finalversion': finalversion, + 'pre': pre, + 'release': release, + 'protocol_version': protocol_version, + 'subprotocol_version': subprotocol_version, + 'proto_changed': proto_changed, + 'proto_change_date': proto_change_date, + 'srcdir': srcdir, + 'srcdiffdir': srcdiffdir, + 'lastsrcdir': lastsrcdir, + 'today': today, + 'ztoday': ztoday, + 'cl_today': cl_today, + 'year': year, + 'tz_num': tz_num, + 'master_branch': args.master_branch, + } + save_state(state) + + section("Release info") + for k in ('version', 'lastversion', 'release', 'srcdir', 'srcdiffdir', 'lastsrcdir', + 'protocol_version', 'proto_changed', 'proto_change_date'): + print(f" {k}: {state[k]}") + print(f"\nWrote {STATE_FILE}. Re-run --step-2-prepare to change anything.") + + +# ---------- Step 3: tweak version files ---------- + +def step_3_tweak(args): + require_top_of_checkout() + state = load_state() + + version = state['version'] + finalversion = state['finalversion'] + pre = state['pre'] + release = state['release'] + today = state['today'] + ztoday = state['ztoday'] + cl_today = state['cl_today'] + year = state['year'] + tz_num = state['tz_num'] + proto_changed = state['proto_changed'] + proto_change_date = state['proto_change_date'] + protocol_version = state['protocol_version'] + srcdir = state['srcdir'] + + specvars = { + 'Version:': finalversion, + 'Release:': release, + '%define fullversion': f'%{{version}}{pre}', + 'Released': version + '.', + '%define srcdir': srcdir, + } + + tweak_files = ['version.h', 'rsync.h', 'NEWS.md'] + tweak_files += glob.glob('packaging/*.spec') + tweak_files += glob.glob('packaging/*/*.spec') + + for fn in tweak_files: + with open(fn, 'r', encoding='utf-8') as fh: + old_txt = txt = fh.read() + if fn == 'version.h': + x_re = re.compile(r'^(#define RSYNC_VERSION).*', re.M) + txt = replace_or_die(x_re, r'\1 "%s"' % version, txt, + f"Unable to update RSYNC_VERSION in {fn}") + x_re = re.compile(r'^(#define MAINTAINER_TZ_OFFSET).*', re.M) + txt = replace_or_die(x_re, r'\1 ' + tz_num, txt, + f"Unable to update MAINTAINER_TZ_OFFSET in {fn}") + elif fn == 'rsync.h': + x_re = re.compile(r'(#define\s+SUBPROTOCOL_VERSION)\s+(\d+)') + repl = lambda m: m[1] + ' ' + ( + '0' if not pre or not proto_changed + else '1' if m[2] == '0' + else m[2]) + txt = replace_or_die(x_re, repl, txt, + f"Unable to find SUBPROTOCOL_VERSION in {fn}") + elif fn == 'NEWS.md': + efv = re.escape(finalversion) + # Accept either "(UNRELEASED)" or an already-filled date, so a + # release entry that was dated by hand (or by an earlier run of + # this step) does not have to be reverted before releasing. + x_re = re.compile( + r'^# NEWS for rsync %s \((?:UNRELEASED|\d+ \w{3} \d{4})\)\s+## Changes in this version:\n' % efv + + r'(\n### PROTOCOL NUMBER:\s+- The protocol number was changed to \d+\.\n)?') + rel_day = 'UNRELEASED' if pre else today + repl = (f'# NEWS for rsync {finalversion} ({rel_day})\n\n' + + '## Changes in this version:\n') + if proto_changed: + repl += f'\n### PROTOCOL NUMBER:\n\n - The protocol number was changed to {protocol_version}.\n' + good_top = re.sub(r'\(.*?\)', '(UNRELEASED)', repl, 1) + msg = (f"The top of {fn} is not in the right format. It should be:\n" + good_top + + "(an already-filled release date in place of UNRELEASED is also accepted)") + txt = replace_or_die(x_re, repl, txt, msg) + x_re = re.compile( + r'^(\| )(\S{2} \S{3} \d{4})(\s+\|\s+%s\s+\| ).{11}(\s+\| )\S{2}(\s+\|+)$' % efv, + re.M) + repl = lambda m: (m[1] + (m[2] if pre else ztoday) + m[3] + + proto_change_date + m[4] + protocol_version + m[5]) + txt = replace_or_die(x_re, repl, txt, + f'Unable to find "| ?? ??? {year} | {finalversion} | ... |" line in {fn}') + elif '.spec' in fn: + for var, val in specvars.items(): + x_re = re.compile(r'^%s .*' % re.escape(var), re.M) + txt = replace_or_die(x_re, var + ' ' + val, txt, + f"Unable to update {var} in {fn}") + x_re = re.compile(r'^\* \w\w\w \w\w\w \d\d \d\d\d\d (.*)', re.M) + txt = replace_or_die(x_re, r'%s \1' % cl_today, txt, + f"Unable to update ChangeLog header in {fn}") + else: + die(f"Unrecognized file in tweak_files: {fn}") + + if txt != old_txt: + print(f"Updating {fn}") + with open(fn, 'w', encoding='utf-8') as fh: + fh.write(txt) + + cmd_chk(['packaging/year-tweak']) + + section("git diff after tweaks") + cmd_run(['git', '--no-pager', 'diff']) + + +# ---------- Step 4: build ---------- + +def step_4_build(args): + require_top_of_checkout() + load_state() # just to ensure we've prepared + + section("Running prepare-source + configure --prefix=/usr --with-rrsync + make + make gen") + # Always re-prepare so configure.sh is current; we run configure ourselves + # with the release-required flags rather than relying on the cached + # config.status (which may have been produced with different options). + if os.path.isfile('.fetch'): + cmd_chk(['./prepare-source', 'fetch']) + else: + cmd_chk(['./prepare-source']) + + cmd_chk(['./configure', '--prefix=/usr', '--with-rrsync']) + cmd_chk(['make']) + cmd_chk(['make', 'gen']) + + +# ---------- Step 5: commit ---------- + +def step_5_commit(args): + require_top_of_checkout() + state = load_state() + version = state['version'] + + section("git status") + cmd_run(['git', 'status']) + if not confirm("Commit all current changes with the release message?"): + die("Aborted.") + cmd_chk(['git', 'commit', '-a', '-m', f'Preparing for release of {version} [buildall]']) + + +# ---------- Step 6: tag ---------- + +def step_6_tag(args): + require_top_of_checkout() + state = load_state() + version = state['version'] + v_ver = 'v' + version + + out = cmd_txt_chk(['git', 'tag', '-l', v_ver]).out + if out.strip(): + if not confirm(f"Tag {v_ver} already exists. Delete and recreate?"): + die("Aborted.") + cmd_chk(['git', 'tag', '-d', v_ver]) + + # Prime the gpg agent so the actual tag signing won't prompt. + section("Priming gpg agent") + cmd_run("touch TeMp; gpg --sign TeMp; rm -f TeMp TeMp.gpg") + + section(f"Creating signed tag {v_ver}") + out = cmd_txt(['git', 'tag', '-s', '-m', f'Version {version}.', v_ver], + capture='combined').out + print(out, end='') + if 'bad passphrase' in out.lower() or 'failed' in out.lower(): + die("Tag creation failed.") + + +# ---------- Step 7: tarball + diff ---------- + +def step_7_tarball(args): + require_top_of_checkout() + state = load_state() + + version = state['version'] + lastversion = state['lastversion'] + pre = state['pre'] + srcdir = state['srcdir'] + srcdiffdir = state['srcdiffdir'] + lastsrcdir = state['lastsrcdir'] + + rsync_ver = 'rsync-' + version + rsync_lastver = 'rsync-' + lastversion + v_ver = 'v' + version + + srctar_name = f"{rsync_ver}.tar.gz" + diff_name = f"{rsync_lastver}-{version}.diffs.gz" + + srctar_file = os.path.join(FTP_DIR, srcdir, srctar_name) + diff_file = os.path.join(FTP_DIR, srcdiffdir, diff_name) + lasttar_file = os.path.join(FTP_DIR, lastsrcdir, rsync_lastver + '.tar.gz') + + for d in (os.path.dirname(srctar_file), os.path.dirname(diff_file)): + os.makedirs(d, exist_ok=True) + if not os.path.isfile(lasttar_file): + die(f"Previous tarball not found: {lasttar_file}") + + # Stage in ../release/work to keep the source checkout clean. + if os.path.isdir(WORK_DIR): + shutil.rmtree(WORK_DIR) + os.makedirs(WORK_DIR) + + a_dir = os.path.join(WORK_DIR, 'a') + b_dir = os.path.join(WORK_DIR, 'b') + + # Extract gen files from the previous tarball into work/a/. + tweaked_gen_files = [os.path.join(rsync_lastver, fn) for fn in GEN_FILES] + cmd_chk(['tar', '-C', WORK_DIR, '-xzf', lasttar_file, *tweaked_gen_files]) + os.rename(os.path.join(WORK_DIR, rsync_lastver), a_dir) + + # Copy current gen files (built in the top-level checkout) into work/b/. + os.makedirs(b_dir) + cmd_chk(['rsync', '-a', *GEN_FILES, b_dir + '/']) + + section(f"Creating {diff_file}") + sed_script = r's:^((---|\+\+\+) [ab]/[^\t]+)\t.*:\1:' # no single quotes! + cmd_chk( + f"(git diff v{lastversion} {v_ver} -- ':!.github'; " + f"diff -upN {a_dir} {b_dir} | sed -r '{sed_script}') | gzip -9 >{diff_file}") + + section(f"Creating {srctar_file}") + # Reuse work/b/ (which already holds the fresh gen files) as the release + # staging dir, then let "git archive" overlay the git-tracked source files + # on top. That way the tarball ends up with both gen files and source. + rsync_ver_dir = os.path.join(WORK_DIR, rsync_ver) + shutil.rmtree(a_dir) + os.rename(b_dir, rsync_ver_dir) + cmd_chk(f"git archive --format=tar --prefix={rsync_ver}/ {v_ver} | " + f"tar -C {WORK_DIR} -xf -") + cmd_chk(f"support/git-set-file-times --quiet --prefix={rsync_ver_dir}/") + cmd_chk(['fakeroot', 'tar', '-C', WORK_DIR, '-czf', srctar_file, + '--exclude=.github', rsync_ver]) + + # Leave staging in place; --step-8-update-ftp does its own thing. + print(f"\nCreated:\n {srctar_file}\n {diff_file}") + + +# ---------- Step 8: update ftp ---------- + +def step_8_update_ftp(args): + require_top_of_checkout() + state = load_state() + + version = state['version'] + lastversion = state['lastversion'] + srcdir = state['srcdir'] + srcdiffdir = state['srcdiffdir'] + + rsync_ver = 'rsync-' + version + rsync_lastver = 'rsync-' + lastversion + srctar_file = os.path.join(FTP_DIR, srcdir, f"{rsync_ver}.tar.gz") + diff_file = os.path.join(FTP_DIR, srcdiffdir, + f"{rsync_lastver}-{version}.diffs.gz") + + section(f"Refreshing top-of-tree files in {FTP_DIR}") + md_files = ['README.md', 'NEWS.md', 'INSTALL.md'] + html_files = [fn for fn in GEN_FILES if fn.endswith('.html')] + cmd_chk(['rsync', '-a', *md_files, *html_files, FTP_DIR + '/']) + cmd_chk(['./md-convert', '--dest', FTP_DIR, *md_files]) + + section(f"Regenerating {FTP_DIR}/ChangeLog.gz") + cmd_chk(f"git log --name-status | gzip -9 >{FTP_DIR}/ChangeLog.gz") + + # Prime gpg agent and then sign the tar + diff. + section("Priming gpg agent") + cmd_run("touch TeMp; gpg --sign TeMp; rm -f TeMp TeMp.gpg") + + for fn in (srctar_file, diff_file): + if not os.path.isfile(fn): + die(f"Missing file to sign: {fn}. Did --step-7-tarball run successfully?") + asc_fn = fn + '.asc' + if os.path.lexists(asc_fn): + os.unlink(asc_fn) + section(f"GPG-signing {fn}") + res = cmd_run(['gpg', '--batch', '-ba', fn]) + if res.returncode not in (0, 2): + die("gpg signing failed.") + + +# ---------- Step 9: top-level hard links ---------- + +def step_9_toplinks(args): + require_top_of_checkout() + state = load_state() + + pre = state['pre'] + if pre: + print("Skipping: pre-releases do not get top-level hard links.") + return + + version = state['version'] + lastversion = state['lastversion'] + srcdir = state['srcdir'] + srcdiffdir = state['srcdiffdir'] + + rsync_ver = 'rsync-' + version + rsync_lastver = 'rsync-' + lastversion + srctar_file = os.path.join(FTP_DIR, srcdir, f"{rsync_ver}.tar.gz") + diff_file = os.path.join(FTP_DIR, srcdiffdir, + f"{rsync_lastver}-{version}.diffs.gz") + + section("Removing stale top-level rsync-* files") + for find in [f'{FTP_DIR}/rsync-*.gz', + f'{FTP_DIR}/rsync-*.asc', + f'{FTP_DIR}/src-previews/rsync-*diffs.gz*']: + for fn in glob.glob(find): + os.unlink(fn) + + top_link = [ + srctar_file, srctar_file + '.asc', + diff_file, diff_file + '.asc', + ] + for fn in top_link: + target = re.sub(r'/src(-\w+)?/', '/', fn) + if os.path.lexists(target): + os.unlink(target) + os.link(fn, target) + print(f" linked {target}") + + +# ---------- Step 10: push ftp ---------- + +def step_10_push_ftp(args): + host = require_samba_host() + if not os.path.isdir(FTP_DIR): + die(f"{FTP_DIR} does not exist. Run --step-1-fetch first.") + section(f"rsync ftp dir to {host}") + rsync_with_confirm(['-aivOHP', '--chown=:rsync', '--del', + f'-f._{os.path.join(FTP_DIR, ".filt")}', + f'{FTP_DIR}/', f'{host}:{FTP_REMOTE_PATH}/']) + + +# ---------- Step 11: push html ---------- + +def step_11_push_html(args): + host = require_samba_host() + if not os.path.isdir(HTML_DIR): + die(f"{HTML_DIR} does not exist. Run --step-1-fetch first.") + section(f"rsync html dir to {host}") + filt = os.path.join(HTML_DIR, 'filt') + rsync_with_confirm(['-aivOHP', '--chown=:rsync', '--del', + f'-f._{filt}', + f'{HTML_DIR}/', f'{host}:{HTML_REMOTE_PATH}/']) + + +# ---------- Step 12: print push-git instructions ---------- + +def step_12_push_git(args): + state = load_state() + version = state['version'] + master_branch = state['master_branch'] + v_ver = 'v' + version + + print(f"""\ +{DASH_LINE} +Run these from the rsync-git checkout (this script does not push for you): + + git push origin {master_branch} + git push origin {v_ver} + +If you have a 'samba' remote configured (git.samba.org:/data/git/rsync.git): + + git push samba {master_branch} + git push samba {v_ver} + +Then upload the tarball + .asc to the GitHub release for {v_ver}, +and announce on rsync-announce@, rsync@, and Discord. + +NOTE! Also update the PPAs if needed +""") + + +# ---------- shared rsync-with-confirm ---------- + +def rsync_with_confirm(rsync_args): + """Run an rsync command in dry-run mode, then ask before running for real.""" + cmd_run(['rsync', '--dry-run', *rsync_args]) + if confirm("Run without --dry-run?"): + cmd_run(['rsync', *rsync_args]) + + +# ---------- dispatch ---------- + +STEP_FUNCS = { + 'step-1-fetch': step_1_fetch, + 'step-2-prepare': step_2_prepare, + 'step-3-tweak': step_3_tweak, + 'step-4-build': step_4_build, + 'step-5-commit': step_5_commit, + 'step-6-tag': step_6_tag, + 'step-7-tarball': step_7_tarball, + 'step-8-update-ftp': step_8_update_ftp, + 'step-9-toplinks': step_9_toplinks, + 'step-10-push-ftp': step_10_push_ftp, + 'step-11-push-html': step_11_push_html, + 'step-12-push-git': step_12_push_git, +} + + +def signal_handler(sig, frame): + die("\nAborting due to SIGINT.") + + +def main(): + parser = argparse.ArgumentParser( + description="Step-based release script for rsync.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="Run --list to see the steps. Each invocation runs exactly one --step-* option.") + parser.add_argument('--branch', '-b', dest='master_branch', default='master', + help="The branch to release (default: master).") + parser.add_argument('--list', action='store_true', + help="List all release steps and exit.") + grp = parser.add_mutually_exclusive_group() + for flag, descr in STEPS: + grp.add_argument('--' + flag, dest='step', action='store_const', + const=flag, help=descr) + args = parser.parse_args() + + if args.list: + print("Release steps:") + for flag, descr in STEPS: + print(f" --{flag:18s} {descr}") + return + + if not args.step: + parser.error("pick one --step-N-XX option (or --list to see them).") + + signal.signal(signal.SIGINT, signal_handler) + os.environ['LESS'] = 'mqeiXR' + STEP_FUNCS[args.step](args) + + +if __name__ == '__main__': + main() + +# vim: sw=4 et ft=python diff -Nru rsync-3.4.1+ds1/packaging/samba-rsync rsync-3.5.0+ds1/packaging/samba-rsync --- rsync-3.4.1+ds1/packaging/samba-rsync 2025-01-14 21:09:33.000000000 +0000 +++ rsync-3.5.0+ds1/packaging/samba-rsync 1970-01-01 00:00:00.000000000 +0000 @@ -1,124 +0,0 @@ -#!/bin/bash -# This script makes it easy to update the ftp & html directories on the samba.org server. -# It expects the 2 *_DEST directories to contain updated files that need to be sent to -# the remote server. If these directories don't exist yet, they will be copied from the -# remote server (while also making the html dir a git checkout). - -FTP_SRC="$HOME/samba-rsync-ftp" -HTML_SRC="$HOME/samba-rsync-html" - -FTP_DEST="/home/ftp/pub/rsync" -HTML_DEST="/home/httpd/html/rsync" - -HTML_GIT='git.samba.org:/data/git/rsync-web.git' - -export RSYNC_PARTIAL_DIR='' - -case "$RSYNC_SAMBA_HOST" in - *.samba.org) ;; - *) - echo "You must set RSYNC_SAMBA_HOST in your environment to the samba hostname to use." >&2 - exit 1 - ;; -esac - -MODE='' -REVERSE='' -while (( $# )); do - case "$1" in - -R|--reverse) REVERSE=yes ;; - f|ftp) MODE=ftp ;; - h|html) MODE=html ;; - -h|--help) - echo "Usage: [-R] [f|ftp|h|html]" - echo "-R --reverse Copy the files from the server to the local host." - echo " The default is to update the remote files." - echo "-h --help Output this help message." - echo " " - echo "The script will prompt if ftp or html is not specified on the command line." - echo "Only one category can be copied at a time. When pulling html files, a git" - echo "checkout will be either created or updated prior to the rsync copy." - exit - ;; - *) - echo "Invalid option: $1" >&2 - exit 1 - ;; - esac - shift -done - -while [ ! "$MODE" ]; do - if [ "$REVERSE" = yes ]; then - DIRECTION=FROM - else - DIRECTION=TO - fi - echo -n "Copy which files $DIRECTION the server? ftp or html? " - read ans - case "$ans" in - f*) MODE=ftp ;; - h*) MODE=html ;; - '') exit 1 ;; - *) echo "You must answer f or h to copy the ftp or html data." ;; - esac -done - -if [ "$MODE" = ftp ]; then - SRC_DIR="$FTP_SRC" - DEST_DIR="$FTP_DEST" - FILT=".filt" -else - SRC_DIR="$HTML_SRC" - DEST_DIR="$HTML_DEST" - FILT="filt" -fi - -function do_rsync { - rsync --dry-run "${@}" | grep -v 'is uptodate$' - echo '' - echo -n "Run without --dry-run? [n] " - read ans - case "$ans" in - y*) rsync "${@}" | grep -v 'is uptodate$' ;; - esac -} - -if [ -d "$SRC_DIR" ]; then - REVERSE_RSYNC=do_rsync -else - echo "The directory $SRC_DIR does not exist yet." - echo -n "Do you want to create it? [n] " - read ans - case "$ans" in - y*) ;; - *) exit 1 ;; - esac - REVERSE=yes - REVERSE_RSYNC=rsync -fi - -if [ "$REVERSE" = yes ]; then - OPTS='-aivOHP' - TMP_FILT="$SRC_DIR/tmp-filt" - echo "Copying files from $RSYNC_SAMBA_HOST to $SRC_DIR ..." - if [ "$MODE" = html ]; then - if [ $REVERSE_RSYNC = rsync ]; then - git clone "$HTML_GIT" "$SRC_DIR" || exit 1 - else - cd "$SRC_DIR" || exit 1 - git pull || exit 1 - fi - sed -n -e 's/[-P]/H/p' "$SRC_DIR/$FILT" >"$TMP_FILT" - OPTS="${OPTS}f._$TMP_FILT" - else - OPTS="${OPTS}f:_$FILT" - fi - $REVERSE_RSYNC "$OPTS" "$RSYNC_SAMBA_HOST:$DEST_DIR/" "$SRC_DIR/" - rm -f "$TMP_FILT" - exit -fi - -cd "$SRC_DIR" || exit 1 -echo "Copying files from $SRC_DIR to $RSYNC_SAMBA_HOST ..." -do_rsync -aivOHP --chown=:rsync --del -f._$FILT . "$RSYNC_SAMBA_HOST:$DEST_DIR/" diff -Nru rsync-3.4.1+ds1/packaging/send-news rsync-3.5.0+ds1/packaging/send-news --- rsync-3.4.1+ds1/packaging/send-news 2024-11-05 21:44:17.000000000 +0000 +++ rsync-3.5.0+ds1/packaging/send-news 1970-01-01 00:00:00.000000000 +0000 @@ -1,33 +0,0 @@ -#!/bin/bash -e - -# This script expects the ~/src/rsync directory to contain the rsync -# source that has been updated. It also expects the auto-build-save -# directory to have been created prior to the running of configure so -# that each branch has its own build directory underneath. This supports -# the maintainer workflow for the rsync-patches files maintenace. - -FTP_SRC="$HOME/samba-rsync-ftp" -FTP_DEST="/home/ftp/pub/rsync" -MD_FILES="README.md INSTALL.md NEWS.md" - -case "$RSYNC_SAMBA_HOST" in - *.samba.org) ;; - *) - echo "You must set RSYNC_SAMBA_HOST in your environment to the samba hostname to use." >&2 - exit 1 - ;; -esac - -if [ ! -d "$FTP_SRC" ]; then - packaging/samba-rsync ftp # Ask to initialize the local ftp dir -fi - -cd ~/src/rsync - -make man -./md-convert --dest="$FTP_SRC" $MD_FILES -rsync -aiic $MD_FILES auto-build-save/master/*.?.html "$FTP_SRC" - -cd "$FTP_SRC" - -rsync -aiic README.* INSTALL.* NEWS.* *.?.html "$RSYNC_SAMBA_HOST:$FTP_DEST/" diff -Nru rsync-3.4.1+ds1/packaging/year-tweak rsync-3.5.0+ds1/packaging/year-tweak --- rsync-3.4.1+ds1/packaging/year-tweak 2020-06-17 01:27:48.000000000 +0000 +++ rsync-3.5.0+ds1/packaging/year-tweak 2025-01-16 06:30:32.000000000 +0000 @@ -7,9 +7,6 @@ import sys, os, re, argparse, subprocess from datetime import datetime -MAINTAINER_NAME = 'Wayne Davison' -MAINTAINER_SUF = ' ' + MAINTAINER_NAME + "\n" - def main(): latest_year = '2000' @@ -22,10 +19,6 @@ m = argparse.Namespace(**m.groupdict()) if m.year > latest_year: latest_year = m.year - if m.fn.startswith('zlib/') or m.fn.startswith('popt/'): - continue - if re.search(r'\.(c|h|sh|test)$', m.fn): - maybe_edit_copyright_year(m.fn, m.year) proc.communicate() fn = 'latest-year.h' @@ -39,55 +32,8 @@ fh.write(txt) -def maybe_edit_copyright_year(fn, year): - opening_lines = [ ] - copyright_line = None - - with open(fn, 'r', encoding='utf-8') as fh: - for lineno, line in enumerate(fh): - opening_lines.append(line) - if lineno > 3 and not re.search(r'\S', line): - break - m = re.match(r'^(?P
.*Copyright\s+\S+\s+)(?P\d\d\d\d(?:-\d\d\d\d)?(,\s+\d\d\d\d)*)(?P.+)', line)
-            if not m:
-                continue
-            copyright_line = argparse.Namespace(**m.groupdict())
-            copyright_line.lineno = len(opening_lines)
-            copyright_line.is_maintainer_line = MAINTAINER_NAME in copyright_line.suf
-            copyright_line.txt = line
-            if copyright_line.is_maintainer_line:
-                break
-
-        if not copyright_line:
-            return
-
-        if copyright_line.is_maintainer_line:
-            cyears = copyright_line.year.split('-')
-            if year == cyears[0]:
-                cyears = [ year ]
-            else:
-                cyears = [ cyears[0], year ]
-            txt = copyright_line.pre + '-'.join(cyears) + MAINTAINER_SUF
-            if txt == copyright_line.txt:
-                return
-            opening_lines[copyright_line.lineno - 1] = txt
-        else:
-            if fn.startswith('lib/') or fn.startswith('testsuite/'):
-                return
-            txt = copyright_line.pre + year + MAINTAINER_SUF
-            opening_lines[copyright_line.lineno - 1] += txt
-
-        remaining_txt = fh.read()
-
-    print(f"Updating {fn} with year {year}")
-
-    with open(fn, 'w', encoding='utf-8') as fh:
-        fh.write(''.join(opening_lines))
-        fh.write(remaining_txt)
-
-
 if __name__ == '__main__':
-    parser = argparse.ArgumentParser(description="Grab the year of last mod for our c & h files and make sure the Copyright comment is up-to-date.")
+    parser = argparse.ArgumentParser(description="Grab the year of the last mod for our c & h files and make sure the LATEST_YEAR value is accurate.")
     args = parser.parse_args()
     main()
 
diff -Nru rsync-3.4.1+ds1/params.c rsync-3.5.0+ds1/params.c
--- rsync-3.4.1+ds1/params.c	2020-06-26 03:54:21.000000000 +0000
+++ rsync-3.5.0+ds1/params.c	2026-07-20 04:05:31.000000000 +0000
@@ -580,7 +580,14 @@
     return( NULL );
     }
 
-  OpenedFile = fopen( FileName, "r" );
+  /* 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 );
+    OpenedFile = cfg_fd >= 0 ? fdopen( cfg_fd, "r" ) : NULL;
+    if( !OpenedFile && cfg_fd >= 0 )
+      close( cfg_fd );
+  }
   if( NULL == OpenedFile )
     {
     rsyserr(FLOG, errno, "unable to open config file \"%s\"",
diff -Nru rsync-3.4.1+ds1/receiver.c rsync-3.5.0+ds1/receiver.c
--- rsync-3.4.1+ds1/receiver.c	2025-01-14 18:30:32.000000000 +0000
+++ rsync-3.5.0+ds1/receiver.c	2026-07-28 03:50:37.000000000 +0000
@@ -25,6 +25,7 @@
 extern int dry_run;
 extern int do_xfers;
 extern int am_root;
+extern int am_daemon;
 extern int am_server;
 extern int inc_recurse;
 extern int log_before_transfer;
@@ -70,6 +71,8 @@
 
 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;
@@ -82,6 +85,207 @@
 #define MAX_UNIQUE_NUMBER 999999
 #define MAX_UNIQUE_LOOP 100
 
+/* 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
+ * 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
+ * verification checksum -> a spurious "failed verification" that strands the
+ * (correct) data in the partial-dir forever.
+ *
+ * The operator's directory is trusted; only the leaf basename is peer-supplied.
+ * So when basedir is NULL and relpath is absolute, split it into its directory
+ * (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)
+{
+	extern int am_daemon, am_chrooted;
+	extern unsigned int module_dirlen;
+
+	/* "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 (basedir) {
+			char fullpath[MAXPATHLEN];
+			if (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) {
+				errno = ENAMETOOLONG;
+				return -1;
+			}
+			return do_open(fullpath, flags, mode);
+		}
+		return do_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) {
+		char fullpath[MAXPATHLEN];
+		const char *p = relpath;
+		if (basedir) {
+			if (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) {
+				errno = ENAMETOOLONG;
+				return -1;
+			}
+			p = fullpath;
+		}
+		return open_no_attacker_symlinks(p, flags, mode);
+	}
+
+	/* The confined resolver is needed for the sanitizing daemon
+	 * (am_daemon && !am_chrooted) and for a /./ inner-module chroot
+	 * (am_chrooted && module_dirlen) -- in the latter the kernel chroot confines
+	 * only the outer path, so a peer-chosen alt-dest basis index (fnamecmp_type)
+	 * could otherwise reach an outside-inner-module file through a symlinked
+	 * parent.  Local / remote-shell mode has no module boundary, and a plain
+	 * "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
+	 * in-module ".." climb for the inner-module case too. */
+	if (!am_daemon || (am_chrooted && !module_dirlen)) {
+		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 do_open(relpath, flags, mode);
+	}
+
+	if (!basedir && relpath && *relpath == '/') {
+		const char *slash = strrchr(relpath, '/');
+		const char *leaf = slash + 1;
+		char dirbuf[MAXPATHLEN];
+		const char *dir;
+		if (slash == relpath)
+			dir = "/";
+		else {
+			size_t dlen = slash - relpath;
+			if (dlen >= sizeof dirbuf) {
+				errno = ENAMETOOLONG;
+				return -1;
+			}
+			memcpy(dirbuf, relpath, dlen);
+			dirbuf[dlen] = '\0';
+			dir = dirbuf;
+		}
+		return secure_relative_open(dir, leaf, flags, mode);
+	}
+	return secure_relative_open(basedir, relpath, flags, mode);
+}
+
+/* Keep the ownership policy for every attempt to open a one-inplace partial
+ * output.  In particular, Linux's protected_regular compatibility retries
+ * 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;
+}
+
+/* Open a read-only regular file for an in-place update without leaving its
+ * mode relaxed while protocol data is received.  Once a writable descriptor is
+ * open, later writes no longer depend on the pathname's permission bits, so
+ * restore the exact prior mode before returning to the transfer loop -- an
+ * abort (peer EOF, checksum failure, a signal) must not strand the file at
+ * 0600.  Requires a regular file: O_NOFOLLOW refuses a symlink at the leaf but
+ * not a directory swapped in after the type probe. */
+static int open_readonly_inplace(const char *fname, int one_inplace)
+{
+	STRUCT_STAT cst;
+	mode_t prior_mode;
+	int cfd = -1, fd = -1;
+	int open_errno, restore_errno;
+
+	if (use_secure_symlinks || one_inplace) {
+#ifdef O_NOFOLLOW
+		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)) {
+			errno = EACCES;	/* refused: not the read-only regular file we recover */
+			goto failed;
+		}
+		prior_mode = cst.st_mode & CHMOD_BITS;
+		if (prior_mode & S_IWUSR) {
+			/* Already owner-writable, so adding S_IWUSR cannot be what an
+			 * EACCES is about (an ACL, or the parent dir).  Don't touch the
+			 * mode for nothing: each chmod risks losing a special bit. */
+			errno = EACCES;
+			goto failed;
+		}
+		if (fchmod(cfd, prior_mode | S_IWUSR) < 0)
+			goto failed;
+		fd = secure_recv_open(fname, O_WRONLY, 0600, one_inplace);
+		open_errno = errno;
+		if (fchmod(cfd, prior_mode) < 0) {
+			restore_errno = errno;
+			if (fd >= 0)
+				close(fd);
+			fd = -1;
+			open_errno = restore_errno;
+		}
+		close(cfd);
+		errno = open_errno;
+		return fd;
+#else
+		/* Without O_NOFOLLOW the resolver's oldest fallback would follow a
+		 * raced symlink, so fail closed rather than chmod through it. */
+		errno = EACCES;
+		return -1;
+#endif
+	}
+
+	/* 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
+	 * 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) {
+		errno = EACCES;
+		return -1;
+	}
+	if (!S_ISREG(cst.st_mode) || (cst.st_mode & CHMOD_BITS & S_IWUSR)) {
+		errno = EACCES;
+		return -1;
+	}
+	prior_mode = cst.st_mode & CHMOD_BITS;
+	if (do_chmod_at(fname, prior_mode | S_IWUSR) < 0)
+		return -1;
+	fd = do_open(fname, O_WRONLY, 0600);
+	open_errno = errno;
+	if (do_chmod_at(fname, prior_mode) < 0) {
+		restore_errno = errno;
+		if (fd >= 0)
+			close(fd);
+		fd = -1;
+		open_errno = restore_errno;
+	}
+	errno = open_errno;
+	return fd;
+
+  failed:
+	open_errno = errno;
+	if (cfd >= 0)
+		close(cfd);
+	errno = open_errno;
+	return -1;
+}
+
 /* get_tmpname() - create a tmp filename for a given filename
  *
  * If a tmpdir is defined, use that as the directory to put it in.  Otherwise,
@@ -214,7 +418,24 @@
 	 * 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.) */
-	fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
+	/* For any non-chrooted receiver (secure_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
+	 * 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 (dfd >= 0) {
+			char *slash = strrchr(fnametmp, '/');
+			fd = do_mkstemp_atfd(dfd, slash ? slash + 1 : fnametmp,
+					     (file->mode|added_perms) & INITACCESSPERMS);
+		} else
+			fd = secure_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS,
+					    tmpdir != NULL);
+	} else
+		fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
 
 #if 0
 	/* In most cases parent directories will already exist because their
@@ -312,7 +533,12 @@
 		}
 	}
 
-	while ((i = recv_token(f_in, &data)) != 0) {
+	while (1) {
+		data = NULL;
+		i = recv_token(f_in, &data);
+		if (i == 0)
+			break;
+
 		if (INFO_GTE(PROGRESS, 1))
 			show_progress(offset, total_size);
 
@@ -320,6 +546,10 @@
 			maybe_send_keepalive(time(NULL), MSK_ALLOW_FLUSH | MSK_ACTIVE_RECEIVER);
 
 		if (i > 0) {
+			if (!data) {
+				rprintf(FERROR, "Invalid literal token with no data [%s]\n", who_am_i());
+				exit_cleanup(RERR_PROTOCOL);
+			}
 			if (DEBUG_GTE(DELTASUM, 3)) {
 				rprintf(FINFO,"data recv %d at %s\n",
 					i, big_num(offset));
@@ -337,6 +567,11 @@
 		}
 
 		i = -(i+1);
+		if (i < 0 || i >= sum.count) {
+			rprintf(FERROR, "Invalid block index %d (count=%ld) [%s]\n",
+				i, (long)sum.count, who_am_i());
+			exit_cleanup(RERR_PROTOCOL);
+		}
 		offset2 = i * (OFF_T)sum.blength;
 		len = sum.blength;
 		if (i == (int)sum.count-1 && sum.remainder != 0)
@@ -344,6 +579,34 @@
 
 		stats.matched_data += len;
 
+		/* A block match with no mapped basis is a protocol inconsistency
+		 * ONLY when we are actually producing output (fd != -1): the
+		 * generator told the sender a basis existed but the receiver could
+		 * not open it, so honoring the match would silently omit these
+		 * bytes from the verification checksum (a spurious failure) or
+		 * leave a hole in the output. Fail cleanly in that case.
+		 *
+		 * On the DISCARD path (fd == -1, fname == NULL) there is no output
+		 * and no verification: discard_receive_data() deliberately drains a
+		 * delta the receiver never intends to write (basis fstat failed,
+		 * basis is a directory, output open failed, batch skip, ...). The
+		 * sender does not know the data is being discarded and streams an
+		 * ordinary delta, so a match token here is NORMAL protocol, not
+		 * malformed. Absorb it benignly (advance the offset and continue),
+		 * as the pre-existing "if (mapbuf)" guards did before this check was
+		 * added in 31fbb17d -- erroring would wrongly break legitimate
+		 * transfers, and full_fname(fname) with fname==NULL would
+		 * dereference NULL (a receiver crash on a normal transfer). */
+		if (!mapbuf) {
+			if (fd != -1) {
+				rprintf(FERROR, "got a block match with no basis file for %s [%s]\n",
+					full_fname(fname), who_am_i());
+				exit_cleanup(RERR_PROTOCOL);
+			}
+			offset += len;
+			continue;
+		}
+
 		if (DEBUG_GTE(DELTASUM, 3)) {
 			rprintf(FINFO,
 				"chunk[%d] of size %ld at %s offset=%s%s\n",
@@ -435,8 +698,15 @@
 					partialptr, fname);
 			}
 			/* We don't use robust_rename() here because the
-			 * partial-dir must be on the same drive. */
-			if (do_rename(partialptr, fname) < 0) {
+			 * partial-dir must be on the same drive.  Resolve the
+			 * --partial-dir source through the exclude-aware ownership
+			 * 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;
+			if (rret < 0) {
 				rsyserr(FERROR_XFER, errno,
 					"rename failed for %s (from %s)",
 					full_fname(fname), partialptr);
@@ -452,7 +722,10 @@
 static void no_batched_update(int ndx, BOOL is_redo)
 {
 	struct file_list *flist = flist_for_ndx(ndx, "no_batched_update");
-	struct file_struct *file = flist->files[ndx - flist->ndx_start];
+	struct file_struct *file;
+	if (ndx < flist->ndx_start)
+		exit_cleanup(RERR_PROTOCOL);
+	file = flist->files[ndx - flist->ndx_start];
 
 	rprintf(FERROR_XFER, "(No batched update for%s \"%s\")\n",
 		is_redo ? " resend of" : "", f_name(file, NULL));
@@ -538,7 +811,8 @@
 #ifdef SUPPORT_ACLS
 	const char *parent_dirname = "";
 #endif
-	int ndx, recv_ok, one_inplace;
+	int ndx, recv_ok, one_inplace, write_to_device;
+	mode_t write_devices_saved_mode = 0;
 
 	if (DEBUG_GTE(RECV, 1))
 		rprintf(FINFO, "recv_files(%d) starting\n", cur_flist->used);
@@ -589,8 +863,22 @@
 
 		if (ndx - cur_flist->ndx_start >= 0)
 			file = cur_flist->files[ndx - cur_flist->ndx_start];
+		else if (cur_flist->parent_ndx < 0
+		      || cur_flist->parent_ndx >= dir_flist->used)
+			exit_cleanup(RERR_PROTOCOL);
 		else
 			file = dir_flist->files[cur_flist->parent_ndx];
+		if (!F_IS_ACTIVE(file)) {
+			/* A peer that sends duplicate file-list entries gets
+			 * one of them clear_file()'d by flist_sort_and_clean();
+			 * referencing that slot here yields fname == NULL and
+			 * a crash in the first deref (daemon filter check,
+			 * set_file_attrs → full_fname, …). */
+			rprintf(FERROR,
+				"rsync: refusing transfer of cleared file index %d\n",
+				ndx);
+			exit_cleanup(RERR_PROTOCOL);
+		}
 		fname = local_name ? local_name : f_name(file, fbuf);
 
 		if (DEBUG_GTE(RECV, 1))
@@ -768,8 +1056,55 @@
 				fnamecmp = fname;
 		}
 
-		/* open the file */
-		fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);
+		/* Open the delta basis.  When it lives in the entry's own dir (no
+		 * alternate basedir), read it via the held dir fd with O_NOFOLLOW: a
+		 * regular-file basis is never legitimately a symlink, and refusing a
+		 * planted one avoids both an escape and a basis-content info-leak.
+		 * Otherwise use secure_basis_open, which also tolerates an operator-
+		 * trusted absolute fnamecmp (e.g. an absolute --partial-dir basis). */
+		{
+			int bdfd;
+			if (fnamecmp_type == FNAMECMP_PARTIAL_DIR
+			 && 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) {
+				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);
+			} else {
+				/* An operator-supplied basis -- a --partial-dir, or an
+				 * alt-dest basedir (--copy-dest/--compare-dest/--link-dest) --
+				 * is a peer/operator path: resolve it with the exclude-aware
+				 * ownership walk so a flipped foreign-owned parent symlink can't
+				 * read (and feed back as delta) an out-of-tree / excluded file.
+				 * The walk still allows the legitimate "../sibling" basis (#915)
+				 * and the operator's own uid0/euid symlinks.  A daemon keeps its
+				 * stronger confinement branch in secure_basis_open(), so only
+				 * route the alt-dest basedir read through the walk off-daemon. */
+				if ((basedir && !am_daemon) || fnamecmp_type == FNAMECMP_PARTIAL_DIR)
+					operator_path_resolve = 1;
+				fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
+				operator_path_resolve = 0;
+			}
+		}
+		if (fnamecmp_type == FNAMECMP_PARTIAL_DIR && fd1 == -1) {
+			/* The sender may claim a partial basis even when the generator's
+			 * confined lookup rejected it.  Drop that path as the delta basis
+			 * and in-place output target.  A daemon that negotiated in-place
+			 * partial updates rejects the unsafe peer option; otherwise (a pre-30
+			 * peer, where no in-place partial redirect is possible, or a pull
+			 * client) fall back to a safe no-basis update. */
+			if (am_daemon && inplace_partial) {
+				rprintf(FERROR,
+					"rsync: refusing unconfined partial basis for %s\n", fname);
+				exit_cleanup(RERR_PROTOCOL);
+			}
+			fnamecmp = fname;
+			fnamecmp_type = FNAMECMP_FNAME;
+		}
 
 		if (fd1 == -1 && protocol_version < 29) {
 			if (fnamecmp != fname) {
@@ -783,7 +1118,10 @@
 				basedir = basis_dir[0];
 				fnamecmp = fname;
 				fnamecmp_type = FNAMECMP_BASIS_DIR_LOW;
-				fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);
+				if (!am_daemon)
+					operator_path_resolve = 1;
+				fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
+				operator_path_resolve = 0;
 			}
 		}
 
@@ -794,7 +1132,10 @@
 			fnamecmp = fnamecmpbuf;
 		}
 
-		one_inplace = inplace_partial && fnamecmp_type == FNAMECMP_PARTIAL_DIR;
+		/* A peer's basis selector cannot enable direct output through a path
+		 * that the confined basis open did not validate. */
+		one_inplace = inplace_partial && fnamecmp_type == FNAMECMP_PARTIAL_DIR
+			   && fd1 != -1;
 		updating_basis_or_equiv = one_inplace
 		    || (inplace && (fnamecmp == fname || fnamecmp_type == FNAMECMP_BACKUP));
 
@@ -826,11 +1167,10 @@
 			continue;
 		}
 
-		if (write_devices && IS_DEVICE(st.st_mode)) {
+		write_to_device = write_devices && IS_DEVICE(st.st_mode);
+		if (write_to_device) {
 			if (fd1 != -1 && st.st_size == 0)
 				st.st_size = get_device_size(fd1, fname);
-			/* Mark the file entry as a device so that we don't try to truncate it later on. */
-			file->mode = S_IFBLK | (file->mode & ACCESSPERMS);
 		} else if (fd1 != -1 && !(S_ISREG(st.st_mode))) {
 			close(fd1);
 			fd1 = -1;
@@ -854,13 +1194,34 @@
 		/* We now check to see if we are writing the file "inplace" */
 		if (inplace || one_inplace)  {
 			fnametmp = one_inplace ? partialptr : fname;
-			fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600);
+			/* For any non-chrooted receiver (secure_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())
+				fd2 = secure_recv_open(fnametmp, O_WRONLY|O_CREAT, 0600,
+						      one_inplace);
+			else
+				fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600);
 #ifdef linux
 			if (fd2 == -1 && errno == EACCES) {
 				/* Maybe the error was due to protected_regular setting? */
-				fd2 = do_open(fname, O_WRONLY, 0600);
+				if (use_secure_symlinks || one_inplace)
+					fd2 = secure_recv_open(fnametmp, O_WRONLY, 0600,
+							      one_inplace);
+				else
+					fd2 = do_open(fnametmp, O_WRONLY, 0600);
 			}
 #endif
+			if (fd2 == -1 && errno == EACCES) {
+				/* Temporarily add owner-write access only long enough to open
+				 * a writable descriptor; the helper restores the old mode
+				 * before any network data is consumed, including on failure. */
+				fd2 = open_readonly_inplace(fnametmp, one_inplace);
+			}
 			if (fd2 == -1) {
 				rsyserr(FERROR_XFER, errno, "open %s failed",
 					full_fname(fnametmp));
@@ -888,9 +1249,27 @@
 		else if (!am_server && INFO_GTE(NAME, 1) && INFO_EQ(PROGRESS, 1))
 			rprintf(FINFO, "%s\n", fname);
 
+		/* --write-devices writes a regular source file's content into an
+		 * existing destination device.  Flip file->mode to S_IFBLK just for
+		 * receive_data()'s ftruncate gate (!IS_DEVICE), then restore it right
+		 * after -- the file_struct was built as a regular file with no
+		 * DEV_EXTRA_CNT, so leaving it S_IFBLK would make set_stat_xattr's
+		 * F_RDEV_P() read past the allocation.  Kept tight here (after the
+		 * pre-transfer log and the fd2 bail-out) so no continue skips the
+		 * restore and dest_mode() above ran on the real mode. */
+		if (write_to_device) {
+			write_devices_saved_mode = file->mode;
+			file->mode = S_IFBLK | (file->mode & ACCESSPERMS);
+		}
+
 		/* recv file data */
 		recv_ok = receive_data(f_in, fnamecmp, fd1, st.st_size, fname, fd2, file, inplace || one_inplace);
 
+		if (write_to_device) {
+			file->mode = write_devices_saved_mode;
+			write_devices_saved_mode = 0;
+		}
+
 		log_item(log_code, file, iflags, NULL);
 		if (want_progress_now)
 			instant_progress(fname);
@@ -909,8 +1288,14 @@
 			if (!finish_transfer(fname, fnametmp, fnamecmp, partialptr, file, recv_ok, 1))
 				recv_ok = -1;
 			else if (fnamecmp == partialptr) {
-				if (!one_inplace)
-					do_unlink(partialptr);
+				if (!one_inplace) {
+					/* 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;
+				}
 				handle_partial_dir(partialptr, PDIR_DELETE);
 			}
 		} else if (keep_partial && partialptr && (!one_inplace || delay_updates)) {
@@ -919,7 +1304,7 @@
 					"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(fnametmp);
+				do_unlink_at(fnametmp);
 				recv_ok = -1;
 			} else if (!finish_transfer(partialptr, fnametmp, fnamecmp, NULL,
 						    file, recv_ok, !partial_dir))
@@ -930,7 +1315,7 @@
 			} else
 				partialptr = NULL;
 		} else if (!one_inplace)
-			do_unlink(fnametmp);
+			do_unlink_at(fnametmp);
 
 		cleanup_disable();
 
diff -Nru rsync-3.4.1+ds1/rrsync.1 rsync-3.5.0+ds1/rrsync.1
--- rsync-3.4.1+ds1/rrsync.1	2025-01-15 20:49:30.000000000 +0000
+++ rsync-3.5.0+ds1/rrsync.1	2026-08-13 00:05:31.000000000 +0000
@@ -1,4 +1,4 @@
-.TH "rrsync" "1" "15 Jan 2025" "rrsync from rsync 3.4.1" "User Commands"
+.TH "rrsync" "1" "13 Aug 2026" "rrsync from rsync 3.5.0" "User Commands"
 .\" prefix=/usr
 .P
 .SH "NAME"
@@ -8,7 +8,7 @@
 .SH "SYNOPSIS"
 .P
 .nf
-rrsync [-ro|-wo] [-munge] [-no-del] [-no-lock] [-no-overwrite]  DIR
+rrsync [-ro|-wo] [-munge] [-absolute] [-no-del] [-no-lock] [-no-overwrite] DIR
 .fi
 .P
 The single non-option argument specifies the restricted \fIDIR\fP to use. It can be
@@ -81,14 +81,26 @@
 .IP "\fB\-wo\fP"
 Allow only writing to the DIR.
 .IP "\fB\-munge\fP"
-Enable rsync's \fB\-\-munge-links\fP on the server side.
+Enable rsync's \fB\-\-munge\-links\fP on the server side.
+.IP "\fB\-absolute\fP"
+Allow file-transfer arguments to name the restricted directory using its
+absolute server path. For example, with \fBrrsync\ \-absolute\ /path/to/root\fP,
+the transfer arg \fB/path/to/root/dir1\fP is accepted as an alias for \fBdir1\fP.
 .IP "\fB\-no-del\fP"
 Disable rsync's \fB\-\-delete*\fP and \fB\-\-remove*\fP options.
 .IP "\fB\-no-lock\fP"
 Avoid the single-run (per-user) lock check.  Useful with \fB\-munge\fP.
 .IP "\fB\-no-overwrite\fP"
-Enforce \fB\-\-ignore-existing\fP on the server. Prevents overwriting existing
+Enforce \fB\-\-ignore\-existing\fP on the server. Prevents overwriting existing
 files when the server is the receiver.
+.IP
+Because \fB\-\-ignore\-existing\fP protects only the file being transferred, this
+also refuses the options that can reach a \fIdifferent\fP existing file in the
+restricted dir: \fB\-\-log\-file\fP, \fB\-\-partial\-dir\fP, \fB\-\-delay\-updates\fP, and
+backup mode (\fB\-b\fP, \fB\-\-backup\-dir\fP, whose published backup replaces whatever
+already occupies the backup name). Resumable uploads with an explicit
+\fB\-\-partial\-dir\fP, \fB\-\-delay\-updates\fP, and server-side logging are therefore
+unavailable under this option.
 .IP "\fB\-help\fP, \fB\-h\fP"
 Output this help message and exit.
 .P
@@ -97,13 +109,13 @@
 The rrsync script validates the path arguments it is sent to try to restrict
 them to staying within the specified DIR.
 .P
-The rrsync script rejects rsync's \fB\-\-copy-links\fP option (by
+The rrsync script rejects rsync's \fB\-\-copy\-links\fP option (by
 default) so that a copy cannot dereference a symlink within the DIR to get to a
 file outside the DIR.
 .P
-The rrsync script rejects rsync's \fB\-\-protect-args\fP (\fB\-s\fP) option
+The rrsync script rejects rsync's \fB\-\-protect\-args\fP (\fB\-s\fP) option
 because it would allow options to be sent to the server-side that the script
-cannot check.  If you want to support \fB\-\-protect-args\fP, use a daemon-over-ssh
+cannot check.  If you want to support \fB\-\-protect\-args\fP, use a daemon-over-ssh
 setup.
 .P
 The rrsync script accepts just a subset of rsync's options that the real rsync
@@ -155,7 +167,7 @@
 .P
 .SH "VERSION"
 .P
-This manpage is current for version 3.4.1 of rsync.
+This manpage is current for version 3.5.0 of rsync.
 .P
 .SH "CREDITS"
 .P
diff -Nru rsync-3.4.1+ds1/rrsync.1.html rsync-3.5.0+ds1/rrsync.1.html
--- rsync-3.4.1+ds1/rrsync.1.html	2025-01-15 20:49:30.000000000 +0000
+++ rsync-3.5.0+ds1/rrsync.1.html	2026-08-13 00:05:31.000000000 +0000
@@ -34,7 +34,7 @@
 

NAME

rrsync -⁠ a script to setup restricted rsync users via ssh logins

SYNOPSIS

-
rrsync [-ro|-wo] [-munge] [-no-del] [-no-lock] [-no-overwrite]  DIR
+
rrsync [-ro|-wo] [-munge] [-absolute] [-no-del] [-no-lock] [-no-overwrite] DIR
 

The single non-option argument specifies the restricted DIR to use. It can be relative to the user's home directory or an absolute path.

@@ -96,6 +96,12 @@

Enable rsync's --munge-links on the server side.

+
-absolute
+

Allow file-transfer arguments to name the restricted directory using its +absolute server path. For example, with rrsync -absolute /path/to/root, +the transfer arg /path/to/root/dir1 is accepted as an alias for dir1.

+
+
-no-del

Disable rsync's --delete* and --remove* options.

@@ -107,6 +113,13 @@
-no-overwrite

Enforce --ignore-existing on the server. Prevents overwriting existing files when the server is the receiver.

+

Because --ignore-existing protects only the file being transferred, this +also refuses the options that can reach a different existing file in the +restricted dir: --log-file, --partial-dir, --delay-updates, and +backup mode (-b, --backup-dir, whose published backup replaces whatever +already occupies the backup name). Resumable uploads with an explicit +--partial-dir, --delay-updates, and server-side logging are therefore +unavailable under this option.

-help, -h
@@ -156,7 +169,7 @@

SEE ALSO

rsync(1), rsyncd.conf(5)

VERSION

-

This manpage is current for version 3.4.1 of rsync.

+

This manpage is current for version 3.5.0 of rsync.

CREDITS

rsync is distributed under the GNU General Public License. See the file COPYING for details.

@@ -165,5 +178,5 @@

AUTHOR

The original rrsync perl script was written by Joe Smith. Many people have later contributed to it. The python version was created by Wayne Davison.

-

15 Jan 2025

+

13 Aug 2026

diff -Nru rsync-3.4.1+ds1/rsync-ssl rsync-3.5.0+ds1/rsync-ssl --- rsync-3.4.1+ds1/rsync-ssl 2021-09-26 23:44:00.000000000 +0000 +++ rsync-3.5.0+ds1/rsync-ssl 2026-08-01 22:13:25.000000000 +0000 @@ -26,7 +26,7 @@ ;; esac - exec rsync --rsh="$0 --HELPER" "${@}" + exec rsync --rsh="'$0' --HELPER" "${@}" } function rsync_ssl_helper { @@ -90,6 +90,9 @@ # openssl: caopt="-verify_return_error -verify 4" # gnutls: + # gnutls-cli has no reliable "use the system trust store AND make + # verification fatal" switch across versions, so the gnutls path + # refuses below unless RSYNC_SSL_ALLOW_INSECURE_GNUTLS is set. gnutls_opts="" # stunnel: # Since there is no way of using the default CA certificate collection, @@ -135,26 +138,59 @@ echo "Usage: rsync-ssl --HELPER HOSTNAME rsync --server --daemon ." 1>&2 exit 1 fi + validate_ssl_hostname "$hostname" + + if [[ $RSYNC_SSL_TYPE == stunnel && -z ${RSYNC_SSL_CA_CERT+x} && "$RSYNC_SSL_ALLOW_INSECURE_STUNNEL" != 1 ]]; then + echo "stunnel requires RSYNC_SSL_CA_CERT for server certificate validation (or set RSYNC_SSL_ALLOW_INSECURE_STUNNEL=1 to opt out)" 1>&2 + exit 1 + fi + + # Bind the server certificate to the requested host (openssl -verify_hostname, + # stunnel checkHost) so a cert that is CA-valid for a *different* name can't be + # used to MITM. Set RSYNC_SSL_SKIP_HOSTNAME_CHECK=1 to keep CA chain + # verification but skip the identity check (e.g. bare-IP or internal-CA setups). + # checkHost only applies while the chain is being verified. + openssl_host_opt="-verify_hostname $hostname" + stunnel_host_opt="" + [[ "$verify" == "verifyChain = yes" ]] && stunnel_host_opt="checkHost = $hostname" + if [[ "$RSYNC_SSL_SKIP_HOSTNAME_CHECK" == 1 ]]; then + openssl_host_opt="" + stunnel_host_opt="" + fi + + if [[ $RSYNC_SSL_TYPE == gnutls && -z ${RSYNC_SSL_CA_CERT+x} && "$RSYNC_SSL_ALLOW_INSECURE_GNUTLS" != 1 ]]; then + echo "gnutls-cli requires RSYNC_SSL_CA_CERT for server certificate validation (or set RSYNC_SSL_ALLOW_INSECURE_GNUTLS=1 to opt out)" 1>&2 + exit 1 + fi if [[ $RSYNC_SSL_TYPE == openssl ]]; then - exec $RSYNC_SSL_OPENSSL s_client $caopt $certopt $keyopt -quiet -verify_quiet -servername $hostname -verify_hostname $hostname -connect $hostname:$port + exec "$RSYNC_SSL_OPENSSL" s_client $caopt $certopt $keyopt -quiet -verify_quiet -servername $hostname $openssl_host_opt -connect $hostname:$port elif [[ $RSYNC_SSL_TYPE == gnutls ]]; then - exec $RSYNC_SSL_GNUTLS --logfile=/dev/null $gnutls_cert_opt $gnutls_key_opt $gnutls_opts $hostname:$port + exec "$RSYNC_SSL_GNUTLS" --logfile=/dev/null $gnutls_cert_opt $gnutls_key_opt $gnutls_opts $hostname:$port else # devzero@web.de came up with this no-tmpfile calling syntax: - exec $RSYNC_SSL_STUNNEL -fd 10 11<&0 <&2 + exit 1 + fi +} + function path_search { IFS_SAVE="$IFS" IFS=: diff -Nru rsync-3.4.1+ds1/rsync-ssl.1 rsync-3.5.0+ds1/rsync-ssl.1 --- rsync-3.4.1+ds1/rsync-ssl.1 2025-01-15 20:49:30.000000000 +0000 +++ rsync-3.5.0+ds1/rsync-ssl.1 2026-08-13 00:05:31.000000000 +0000 @@ -1,4 +1,4 @@ -.TH "rsync-ssl" "1" "15 Jan 2025" "rsync-ssl from rsync 3.4.1" "User Commands" +.TH "rsync-ssl" "1" "13 Aug 2026" "rsync-ssl from rsync 3.5.0" "User Commands" .\" prefix=/usr .P .SH "NAME" @@ -62,7 +62,24 @@ certificate to use for the connection. .IP "\fBRSYNC_SSL_CA_CERT\fP" If specified, the value is a filename that contains a certificate authority -certificate that is used to validate the connection. +certificate that is used to validate the connection. When set, the server +certificate is verified against this CA \fBand\fP its name is checked against +the host you are connecting to (the chain and the identity), for all of the +openssl, gnutls, and stunnel backends. Set it to an empty string to disable +certificate validation entirely (an encrypt-only connection). +.IP "\fBRSYNC_SSL_ALLOW_INSECURE_STUNNEL\fP" +Set to \fB1\fP to allow the stunnel backend to run without a CA certificate (and +thus with no server-certificate validation at all). Without this, stunnel +mode refuses to start unless \fBRSYNC_SSL_CA_CERT\fP is set, since an unvalidated +TLS connection can be silently man-in-the-middled. +.IP "\fBRSYNC_SSL_SKIP_HOSTNAME_CHECK\fP" +Set to \fB1\fP to verify the certificate \fBchain\fP but skip the \fBhostname\fP +(identity) check, for the openssl and stunnel backends. Use this only when +the server certificate legitimately cannot match the host you connect to\ \-\- +for example connecting by bare IP address, or to a server whose internal-CA +certificate carries a different name. The CA chain is still validated, so +this is much narrower than disabling validation with an empty +\fBRSYNC_SSL_CA_CERT\fP. .IP "\fBRSYNC_SSL_OPENSSL\fP" Specifies the openssl executable to run when the connection type is set to openssl. If unspecified, the $PATH is searched for "openssl". @@ -130,7 +147,7 @@ .P .SH "VERSION" .P -This manpage is current for version 3.4.1 of rsync. +This manpage is current for version 3.5.0 of rsync. .P .SH "CREDITS" .P diff -Nru rsync-3.4.1+ds1/rsync-ssl.1.html rsync-3.5.0+ds1/rsync-ssl.1.html --- rsync-3.4.1+ds1/rsync-ssl.1.html 2025-01-15 20:49:30.000000000 +0000 +++ rsync-3.5.0+ds1/rsync-ssl.1.html 2026-08-13 00:05:31.000000000 +0000 @@ -86,7 +86,28 @@
RSYNC_SSL_CA_CERT

If specified, the value is a filename that contains a certificate authority -certificate that is used to validate the connection.

+certificate that is used to validate the connection. When set, the server +certificate is verified against this CA and its name is checked against +the host you are connecting to (the chain and the identity), for all of the +openssl, gnutls, and stunnel backends. Set it to an empty string to disable +certificate validation entirely (an encrypt-only connection).

+
+ +
RSYNC_SSL_ALLOW_INSECURE_STUNNEL
+

Set to 1 to allow the stunnel backend to run without a CA certificate (and +thus with no server-certificate validation at all). Without this, stunnel +mode refuses to start unless RSYNC_SSL_CA_CERT is set, since an unvalidated +TLS connection can be silently man-in-the-middled.

+
+ +
RSYNC_SSL_SKIP_HOSTNAME_CHECK
+

Set to 1 to verify the certificate chain but skip the hostname +(identity) check, for the openssl and stunnel backends. Use this only when +the server certificate legitimately cannot match the host you connect to -⁠-⁠ +for example connecting by bare IP address, or to a server whose internal-CA +certificate carries a different name. The CA chain is still validated, so +this is much narrower than disabling validation with an empty +RSYNC_SSL_CA_CERT.

RSYNC_SSL_OPENSSL
@@ -140,7 +161,7 @@

BUGS

Please report bugs! See the web site at https://rsync.samba.org/.

VERSION

-

This manpage is current for version 3.4.1 of rsync.

+

This manpage is current for version 3.5.0 of rsync.

CREDITS

Rsync is distributed under the GNU General Public License. See the file COPYING for details.

@@ -150,5 +171,5 @@

This manpage was written by Wayne Davison.

Mailing lists for support and development are available at https://lists.samba.org/.

-

15 Jan 2025

+

13 Aug 2026

diff -Nru rsync-3.4.1+ds1/rsync-ssl.1.md rsync-3.5.0+ds1/rsync-ssl.1.md --- rsync-3.4.1+ds1/rsync-ssl.1.md 2022-06-19 23:55:18.000000000 +0000 +++ rsync-3.5.0+ds1/rsync-ssl.1.md 2026-07-20 04:05:31.000000000 +0000 @@ -66,7 +66,28 @@ 0. `RSYNC_SSL_CA_CERT` If specified, the value is a filename that contains a certificate authority - certificate that is used to validate the connection. + certificate that is used to validate the connection. When set, the server + certificate is verified against this CA **and** its name is checked against + the host you are connecting to (the chain and the identity), for all of the + openssl, gnutls, and stunnel backends. Set it to an empty string to disable + certificate validation entirely (an encrypt-only connection). + +0. `RSYNC_SSL_ALLOW_INSECURE_STUNNEL` + + Set to `1` to allow the stunnel backend to run without a CA certificate (and + thus with no server-certificate validation at all). Without this, stunnel + mode refuses to start unless `RSYNC_SSL_CA_CERT` is set, since an unvalidated + TLS connection can be silently man-in-the-middled. + +0. `RSYNC_SSL_SKIP_HOSTNAME_CHECK` + + Set to `1` to verify the certificate **chain** but skip the **hostname** + (identity) check, for the openssl and stunnel backends. Use this only when + the server certificate legitimately cannot match the host you connect to -- + for example connecting by bare IP address, or to a server whose internal-CA + certificate carries a different name. The CA chain is still validated, so + this is much narrower than disabling validation with an empty + `RSYNC_SSL_CA_CERT`. 0. `RSYNC_SSL_OPENSSL` diff -Nru rsync-3.4.1+ds1/rsync.1 rsync-3.5.0+ds1/rsync.1 --- rsync-3.4.1+ds1/rsync.1 2025-01-15 20:49:30.000000000 +0000 +++ rsync-3.5.0+ds1/rsync.1 2026-08-13 00:05:31.000000000 +0000 @@ -1,9 +1,9 @@ -.TH "rsync" "1" "15 Jan 2025" "rsync 3.4.1" "User Commands" +.TH "rsync" "1" "13 Aug 2026" "rsync 3.5.0" "User Commands" .\" prefix=/usr .P .SH "NAME" .P -rsync \- a fast, versatile, remote (and local) file-copying tool +rsync \- a fast, versatile, local/remote file-copying tool .P .SH "SYNOPSIS" .P @@ -23,13 +23,13 @@ rsync [OPTION...] rsync://[USER@]HOST[:PORT]/SRC... [DEST] Push: rsync [OPTION...] SRC... [USER@]HOST::DEST - rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST) + rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST .fi .P -Usages with just one SRC arg and no DEST arg will list the source files instead +Usages with just one SRC argument and no DEST argument will list the source files instead of copying. .P -The online version of this manpage (that includes cross-linking of topics) +The online version of this manpage (which includes cross-linking of topics) is available at .UR https://download.samba.org/pub/rsync/rsync.1 .UE . @@ -37,7 +37,7 @@ .SH "DESCRIPTION" .P Rsync is a fast and extraordinarily versatile file copying tool. It can copy -locally, to/from another host over any remote shell, or to/from a remote rsync +locally, to/from another host over a remote shell, or to/from a remote rsync daemon. It offers a large number of options that control every aspect of its behavior and permit very flexible specification of the set of files to be copied. It is famous for its delta-transfer algorithm, which reduces the @@ -55,7 +55,7 @@ Some of the additional features of rsync are: .P .IP o -support for copying links, devices, owners, groups, and permissions +support for copying hard and soft links, devices, owners, groups, and permissions .IP o exclude and exclude-from options similar to GNU tar .IP o @@ -72,46 +72,62 @@ .SH "GENERAL" .P Rsync copies files either to or from a remote host, or locally on the current -host (it does not support copying files between two remote hosts). +host. It does not support copying files between two different remote hosts. .P There are two different ways for rsync to contact a remote system: using a -remote-shell program as the transport (such as ssh or rsh) or contacting an +remote-shell program as the transport (such as ssh or rsh), or by contacting an rsync daemon directly via TCP. The remote-shell transport is used whenever the source or destination path contains a single colon (:) separator after a host -specification. Contacting an rsync daemon directly happens when the source or +specification. In this case, the local rsync process uses the remote +shell program to start an rsync process on the remote system. The two +rsync processes then communicate via the remote shell program to +accomplish the desired transfers. In this case, the files that are +accessible on the remote system are those accessible to the user ID +under which the remote shell starts the remote rsync process. +.P +Contacting an rsync daemon directly happens when the source or destination path contains a double colon (::) separator after a host -specification, OR when an rsync:// URL is specified (see also the USING -RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION section for an -exception to this latter rule). +specification, OR when an rsync:// URL is specified. +In this case, the remote rsync daemon process will +provide access only to specific sets of files, called modules, +according to its configuration. +It is also possible to contact an rsync daemon via a remote-shell +transport; see the USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL +CONNECTION section for details. .P -As a special case, if a single source arg is specified without a destination, +As a special case, if a single source argument is specified without a destination, the files are listed in an output format similar to "\fBls\ \-l\fP". .P -As expected, if neither the source or destination path specify a remote host, -the copy occurs locally (see also the \fB\-\-list-only\fP option). +If neither the source or destination path specify a remote host, +the copy occurs locally (see also the \fB\-\-list\-only\fP option). .P -Rsync refers to the local side as the client and the remote side as the server. -Don't confuse server with an rsync daemon. A daemon is always a server, but a -server can be either a daemon or a remote-shell spawned process. +Rsync refers to the local side as the client and the remote side as +the server. The server is not to be confused with an rsync daemon. +A daemon is always a server, but a server can be either a daemon or a +remote-shell spawned process. If the transfer is local, then the +rsync subprocess which receives the contents of the files being +transferred is the server. .P .SH "SETUP" .P -See the file README.md for installation instructions. +Most Linux distributions include rsync as a package that can be +installed using the package manager. +If for any reason you need to build and install rsync from source, +see the file README.md for installation instructions. .P Once installed, you can use rsync to any machine that you can access via a -remote shell (as well as some that you can access using the rsync daemon-mode -protocol). For remote transfers, a modern rsync uses ssh for its -communications, but it may have been configured to use a different remote shell -by default, such as rsh or remsh. -.P -You can also specify any remote shell you like, either by using the \fB\-e\fP +remote shell, or that has an rsync daemon available. +For remote transfers, rsync normally uses ssh for its +communications. Alternatively, +you can specify any remote shell you like, either by using the \fB\-e\fP command line option, or by setting the \fBRSYNC_RSH\fP environment variable. .P Note that rsync must be installed on both the source and destination machines. .P .SH "USAGE" .P -You use rsync in the same way you use rcp. You must specify a source and a +You use rsync in a similar way to other file-copying commands such +as cp and rcp. You must specify a source and a destination, one of which may be remote. .P Perhaps the best way to explain the syntax is with some examples: @@ -127,8 +143,8 @@ exist on the remote system then the rsync remote-update protocol is used to update the file by sending only the differences in the data. Note that the expansion of wildcards on the command-line (\fB*.c\fP) into a list of files is -handled by the shell before it runs rsync and not by rsync itself (exactly the -same as all other Posix-style programs). +handled by the shell before it runs rsync and not by rsync itself (as for +other Posix-style programs). .RS 4 .P .nf @@ -200,8 +216,8 @@ .fi .RE .P -Rsync also has the ability to customize a destination file's name when copying -a single item. The rules for this are: +Rsync also has the ability to copy a single file to a +destination file with a different name. The rules for this are: .P .IP o The transfer list must consist of a single item (either a file or an empty @@ -211,12 +227,13 @@ .IP o The destination path must not have been specified with a trailing slash .P -Under those circumstances, rsync will set the name of the destination's single -item to the last element of the destination path. Keep in mind that it is best -to only use this idiom when copying a file and use the above trailing-slash +Under those circumstances, rsync will use the specified destination +path as the filename of the destination (rather than constructing a +filename using the last element of the source path). It is best +to use this idiom only when copying a file, and to use the above trailing-slash idiom when copying a directory. .P -The following example copies the \fBfoo.c\fP file as \fBbar.c\fP in the \fBsave\fP dir +The following example copies the \fBfoo.c\fP file as \fBbar.c\fP in the \fBsave\fP directory (assuming that \fBbar.c\fP isn't a directory): .RS 4 .P @@ -225,10 +242,13 @@ .fi .RE .P -The single-item copy rule might accidentally bite you if you unknowingly copy a -single item and specify a destination dir that doesn't exist (without using a -trailing slash). For example, if \fBsrc/*.c\fP matches one file and \fBsave/dir\fP -doesn't exist, this will confuse you by naming the destination file \fBsave/dir\fP: +The single-item copy rule can give unexpected results if a wildcard +pattern for the source yields a single item, and the destination, +though specified without a trailing slash, is intended to be a +directory, but that directory does not exist. +For example, if \fBsrc/*.c\fP matches one file and \fBsave/dir\fP +doesn't exist, this will perhaps confusingly name the destination file +\fBsave/dir\fP: .RS 4 .P .nf @@ -236,7 +256,7 @@ .fi .RE .P -To prevent such an accident, either make sure the destination dir exists or +To prevent such an accident, either make sure the destination directory exists or specify the destination path with a trailing slash: .RS 4 .P @@ -249,34 +269,54 @@ .P Rsync always sorts the specified filenames into its internal transfer list. This handles the merging together of the contents of identically named -directories, makes it easy to remove duplicate filenames. It can, however, -confuse someone when the files are transferred in a different order than what -was given on the command-line. -.P -If you need a particular file to be transferred prior to another, either -separate the files into different rsync calls, or consider using -\fB\-\-delay-updates\fP (which doesn't affect the sorted transfer order, but -does make the final file-updating phase happen much more rapidly). -.P -.SH "MULTI-HOST SECURITY" -.P -Rsync takes steps to ensure that the file requests that are shared in a -transfer are protected against various security issues. Most of the potential -problems arise on the receiving side where rsync takes steps to ensure that the -list of files being transferred remains within the bounds of what was -requested. -.P -Toward this end, rsync 3.1.2 and later have aborted when a file list contains -an absolute or relative path that tries to escape out of the top of the -transfer. Also, beginning with version 3.2.5, rsync does two more safety -checks of the file list to (1) ensure that no extra source arguments were added -into the transfer other than those that the client requested and (2) ensure -that the file list obeys the exclude rules that were sent to the sender. -.P -For those that don't yet have a 3.2.5 client rsync (or those that want to be -extra careful), it is safest to do a copy into a dedicated destination -directory for the remote files when you don't trust the remote host. For -example, instead of doing an rsync copy into your home directory: +directories and makes it easy to remove duplicate filenames. It can, however, +result in files being transferred in a different order from that +specified on the command-line. +.P +If you need a particular file to be transferred prior to another, the +only way to be guaranteed of that is to separate the files into +different rsync calls. If it is sufficient for the destination files to +appear at almost the same time, consider using +\fB\-\-delay\-updates\fP, which doesn't affect the sorted transfer order, but +does make the final file-updating phase happen much more rapidly. +.P +.SH "SECURITY" +.P +Rsync is frequently run across a network and with elevated privileges, so it is +worth thinking about who you are trusting and with what. This section is a +practical guide for safe use; the project's \fBSECURITY.md\fP describes the full +threat model and the per-platform residuals. +.P +.SS "Use an authenticated, encrypted transport" +.P +A plain \fBhost:path\fP transfer runs over your remote shell (ssh by default), which +authenticates the peer and encrypts the connection\ \-\- this is the safe default. +A direct daemon connection (\fBhost::module\fP or \fBrsync://\fP) is \fBnot encrypted\fP +and its authentication is comparatively weak, so do not send sensitive data +across an untrusted network. +Instead tunnel it over ssh +(see USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION) or wrap it in +TLS with \fBrsync-ssl\fP(1), setting \fBRSYNC_SSL_CA_CERT\fP so that the +server certificate's chain \fBand\fP hostname are verified. When you must supply a +daemon password non-interactively, put it in a \fB\-\-password\-file\fP that is +readable only by you (mode 600) rather than on the command line. +.P +.SS "Copying from an untrusted sending host" +.P +When receiving from a remote host, rsync takes steps to ensure that a +corrupted or compromised remote rsync process can't cause the local +rsync process to access files beyond the bounds of what was requested. +Rsync ensures that the list of files being +transferred stays within the requested tree, and will abort when a +file list contains an absolute or relative path that tries to escape +the top of the transfer. It also verifies that no extra source arguments +were added to the transfer and that the file list obeys the exclude rules +that were sent to the sender. +.P +For those who want to be extra careful, +it is safest to copy untrusted remote files into a dedicated +destination directory. For example, instead of copying into your home +directory: .RS 4 .P .nf @@ -284,7 +324,7 @@ .fi .RE .P -Dedicate a "host1-files" dir to the remote content: +dedicate a "host1-files" directory to the remote content: .RS 4 .P .nf @@ -292,30 +332,72 @@ .fi .RE .P -See the \fB\-\-trust-sender\fP option for additional details. +See the \fB\-\-trust\-sender\fP option for additional details. +.P +.SS "Copying between case-preserving and case-insensitive filesystems" .P -CAUTION: it is not particularly safe to use rsync to copy files from a -case-preserving filesystem to a case-ignoring filesystem. If you must perform -such a copy, you should either disable symlinks via \fB\-\-no-links\fP or enable the -munging of symlinks via \fB\-\-munge-links\fP (and make sure you use the -right local or remote option). This will prevent rsync from doing potentially -dangerous things if a symlink name overlaps with a file or directory. It does -not, however, ensure that you get a full copy of all the files (since that may -not be possible when the names overlap). A potentially better solution is to -list all the source files and create a safe list of filenames that you pass to -the \fB\-\-files-from\fP option. Any files that conflict in name would need -to be copied to different destination directories using more than one copy. -.P -While a copy of a case-ignoring filesystem to a case-ignoring filesystem can -work out fairly well, if no \fB\-\-delete-during\fP or \fB\-\-delete-before\fP option is -active, rsync can potentially update an existing file on the receiving side -without noticing that the upper-/lower-case of the filename should be changed -to match the sender. +Copying from case-preserving to case-insensitive filesystems requires +caution, because it is possible for the name of a symbolic link to +overlap with the name of a file or directory once the case +distinctions are removed, which can cause potentially dangerous +results such as files being written outside the destination directory +hierarchy. +If you must perform +such a copy, either disable symlinks via \fB\-\-no\-links\fP or enable the munging of +symlinks via \fB\-\-munge\-links\fP (and make sure you use the right local or +remote option). +This does not, however, ensure +that you get a full copy of all the files (since that may not be possible when +the names overlap); a potentially better solution is to build a safe list of +filenames and pass it to \fB\-\-files\-from\fP. +.P +.SS "Symbolic links" +.P +A malicious sender can include symlinks that point outside the destination tree +(for example at \fB/etc/passwd\fP). Use \fB\-\-safe\-links\fP to ignore any symlink +that points outside the set of files +being transferred, or \fB\-\-munge\-links\fP to store every symlink in a +form that is unusable on disk but recoverable later; \fB\-\-no\-links\fP drops symlinks +entirely. See the SYMBOLIC LINKS section for how these interact. +.P +Separately, the directory and file paths that \fIyou\fP supply on the command line\ \-\- +\fB\-\-backup\-dir\fP, \fB\-\-temp\-dir\fP, \fB\-\-partial\-dir\fP, the +\fB\-\-link\-dest\fP/\fB\-\-compare\-dest\fP/\fB\-\-copy\-dest\fP basis directories, +\fB\-\-log\-file\fP, \fB\-\-files\-from\fP/\fB\-\-include\-from\fP/\fB\-\-exclude\-from\fP, +\fB\-\-filter\fP merge files, \fB\-\-write\-batch\fP/\fB\-\-read\-batch\fP, +and the destination itself\ \-\- are resolved so that a symlink component is followed +only when it is owned by you or by root; an attacker-planted symlink along one of +those paths is refused. \fB\-\-insecure\-links\fP turns that protection off +(restoring the historical follow-any-symlink behaviour); use it only when every +directory along those paths is trusted, never on a path an unprivileged user can +write. +.P +.SS "Use strong checksums" +.P +Rsync auto-negotiates the strongest checksum that both ends support, so keeping +both local and remote rsync versions reasonably current is usually all +that is needed. You can +pin the choice with \fB\-\-checksum\-choice\fP (\fB\-\-cc\fP, e.g. \fB\-\-cc=sha1\fP or one +of the xxHash variants) or constrain negotiation with the +\fBRSYNC_CHECKSUM_LIST\fP environment variable; only very old peers fall back to +MD4/MD5. This pre-transfer "does this file need updating?" checksum is separate +from the whole-file checksum rsync normally computes to verify each transferred +file afterward (verification is off when \fB\-\-checksum\-choice=none\fP is forced). +.P +.SS "Protocol version" +.P +The client and server automatically negotiate the newest protocol they both +support, so a current pair is already using the most recent version; +\fB\-\-protocol\fP only \fIforces an older\fP version for compatibility and should +not be used to downgrade a connection. Avoiding old protocols is therefore a +matter of running a current rsync on both ends (a protocol below 30 also forces +the weak MD4 authentication digest on a daemon connection\ \-\- see the AUTHENTICATION +STRENGTH section of \fBrsyncd.conf\fP(5)). .P .SH "ADVANCED USAGE" .P The syntax for requesting multiple files from a remote host is done by -specifying additional remote-host args in the same style as the first, or with +specifying additional remote-host arguments in the same style as the first, or with the hostname omitted. For instance, all these work: .RS 4 .P @@ -331,13 +413,14 @@ modname of the first path, it is assumed to be a path in the module (such as the extra-file1 & extra-file2 that are grabbed above). .P -Really old versions of rsync (2.6.9 and before) only allowed specifying one -remote-source arg, so some people have instead relied on the remote-shell -performing space splitting to break up an arg into multiple paths. Such -unintuitive behavior is no longer supported by default (though you can request -it, as described below). -.P -Starting in 3.2.4, filenames are passed to a remote shell in such a way as to +Because really old versions of rsync (prior to 3.0.0, released +March 2008) only allowed specifying one +remote-source argument, some people have come to rely on the remote shell +performing space splitting to break a single argument into multiple paths. Such +unintuitive behavior is no longer supported by default, though you can request +it, as described in the next paragraph. +Rsync now (since 3.2.4) passes filenames to a remote +shell in such a way as to preserve the characters you give it. Thus, if you ask for a file with spaces in the name, that's what the remote rsync looks for: .RS 4 @@ -348,22 +431,28 @@ .RE .P If you use scripts that have been written to manually apply extra quoting to -the remote rsync args (or to require remote arg splitting), you can ask rsync +the remote rsync arguments, or to rely on remote argument splitting, you can ask rsync to let your script handle the extra escaping. This is done by either adding -the \fB\-\-old-args\fP option to the rsync runs in the script (which requires +the \fB\-\-old\-args\fP option to the rsync runs in the script (which requires a new rsync) or exporting RSYNC_OLD_ARGS=1 and RSYNC_PROTECT_ARGS=0 (which works with old or new rsync versions). .P .SH "CONNECTING TO AN RSYNC DAEMON" .P -It is also possible to use rsync without a remote shell as the transport. In -this case you will directly connect to a remote rsync daemon, typically using -TCP port 873. (This obviously requires the daemon to be running on the remote -system, so refer to the STARTING AN RSYNC DAEMON TO ACCEPT CONNECTIONS -section below for information on that.) +An rsync daemon provides a way to give access to one or more directory +hierarchies on a system in a controlled way, without having to provide +shell access to the system. Each directory hierarchy is called a +"module". The names, directories, access permissions and so on for +each module are defined by the rsync daemon configuration file, +described in \fBrsyncd.conf\fP(5). +Connections to an rsync daemon typically use TCP port 873. The +administrator of the system would normally arrange for the rsync +daemon to be running; +refer to the STARTING AN RSYNC DAEMON TO ACCEPT CONNECTIONS +section below for information on how to do that. .P -Using rsync in this way is the same as using it with a remote shell except -that: +From the client point of view, using rsync to access an rsync daemon +is similar to using it with a remote shell except that: .P .IP o Use either double-colon syntax or rsync:// URL syntax instead of the @@ -371,7 +460,7 @@ .IP o The first element of the "path" is actually a module name. .IP o -Additional remote source args can use an abbreviated syntax that omits the +Additional remote source arguments can use an abbreviated syntax that omits the hostname and/or the module name, as discussed in ADVANCED USAGE. .IP o The remote daemon may print a "message of the day" when you connect. @@ -382,9 +471,11 @@ If you specify a remote source path but no destination, a listing of the matching files on the remote daemon is output. .IP o -The \fB\-\-rsh\fP (\fB\-e\fP) option must be omitted to avoid changing the -connection style from using a socket connection to USING RSYNC-DAEMON -FEATURES VIA A REMOTE-SHELL CONNECTION. +To contact an rsync daemon directly, the \fB\-\-rsh\fP (\fB\-e\fP) +option must be omitted. If it is used, rsync will use the specified +remote shell program to start an rsync daemon on the remote system. +See the USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL +CONNECTION section for information on this mode of operation. .P An example that copies all the files in a remote module named "src": .RS 4 @@ -397,11 +488,11 @@ Some modules on the remote daemon may require authentication. If so, you will receive a password prompt when you connect. You can avoid the password prompt by setting the environment variable \fBRSYNC_PASSWORD\fP to the password you -want to use or using the \fB\-\-password-file\fP option. This may be useful +want to use or using the \fB\-\-password\-file\fP option. This may be useful when scripting rsync. .P -WARNING: On some systems environment variables are visible to all users. On -those systems using \fB\-\-password-file\fP is recommended. +WARNING: On some systems, environment variables are visible to all users. On +those systems using \fB\-\-password\-file\fP is recommended. .P You may establish the connection via a web proxy by setting the environment variable \fBRSYNC_PROXY\fP to a hostname:port pair pointing to your web proxy. @@ -433,19 +524,20 @@ .P It is sometimes useful to use various features of an rsync daemon (such as named modules) without actually allowing any new socket connections into a -system (other than what is already required to allow remote-shell access). +system, other than what is already required to allow remote-shell access. Rsync supports connecting to a host using a remote shell and then spawning a -single-use "daemon" server that expects to read its config file in the home dir +single-use "daemon" server that expects to read its config file in the home directory of the remote user. This can be useful if you want to encrypt a daemon-style -transfer's data, but since the daemon is started up fresh by the remote user, -you may not be able to use features such as chroot or change the uid used by -the daemon. (For another way to encrypt a daemon transfer, consider using ssh -to tunnel a local port to a remote machine and configure a normal rsync daemon -on that remote host to only allow connections from "localhost".) +transfer's data. However, since the daemon is started by the remote user, +you may not be able to use features which require root privilege, such +as chroot or setting the user ID used by the daemon. +(For another way to encrypt a daemon transfer, consider using ssh +to tunnel a local port to a remote machine, and configure a normal rsync daemon +on that remote host to allow connections only from "localhost".) .P From the user's perspective, a daemon transfer via a remote-shell connection uses nearly the same command-line syntax as a normal rsync-daemon transfer, -with the only exception being that you must explicitly set the remote shell +with the only difference being that you must explicitly set the remote shell program on the command-line with the \fB\-\-rsh=COMMAND\fP option. (Setting the RSYNC_RSH in the environment will not turn on this functionality.) For example: .RS 4 @@ -455,11 +547,25 @@ .fi .RE .P -If you need to specify a different remote-shell user, keep in mind that the -user@ prefix in front of the host is specifying the rsync-user value (for a -module that requires user-based authentication). This means that you must give -the '\-l user' option to ssh when specifying the remote-shell, as in this -example that uses the short version of the \fB\-\-rsh\fP option: +A "user@" prefix in front of the host serves two purposes at once in this mode: +it is the rsync-user value (used to log in to a module that requires user-based +authentication) and, by default, it is also passed to the remote shell as the +login user. So the simple form +.RS 4 +.P +.nf +rsync -av --rsh=ssh user@host::module /dest +.fi +.RE +.P +runs \fBssh\ \-l\ user\ host\fP and offers "user" as the rsync-user to the module; the +two are forced to be the same name. +.P +If you need the remote-shell (ssh) login to use a different name than the +rsync-user, give an explicit '\-l' option to ssh in the remote-shell command. +When ssh is already told which user to log in as, rsync does not add its own +\&'\-l', so the "user@" prefix is then used only as the rsync-user. For example, +using the short version of the \fB\-\-rsh\fP option: .RS 4 .P .nf @@ -467,8 +573,8 @@ .fi .RE .P -The "ssh-user" will be used at the ssh level; the "rsync-user" will be used to -log-in to the "module". +Here "ssh-user" is used at the ssh level while "rsync-user" is used to log in to +the "module". .P In this setup, the daemon is started by the ssh command that is accessing the system (which can be forced via the \fB~/.ssh/authorized_keys\fP file, if desired). @@ -480,9 +586,9 @@ daemon already running (or it needs to have configured something like inetd to spawn an rsync daemon for incoming connections on a particular port). For full information on how to start a daemon that will handling incoming socket -connections, see the \fBrsyncd.conf\fP(5) manpage\ \-\- that is -the config file for the daemon, and it contains the full details for how to run -the daemon (including stand-alone and inetd configurations). +connections, see the manpage for \fBrsyncd.conf\fP(5), +the config file for the daemon. The manpage contains the full details for how to run +the daemon, including stand-alone and inetd configurations. .P If you're using one of the remote-shell transports for the transfer, there is no need to manually start an rsync daemon. @@ -524,8 +630,11 @@ --archive, -a archive mode is -rlptgoD (no -A,-X,-U,-N,-H) --no-OPTION turn off an implied OPTION (e.g. --no-D) --recursive, -r recurse into directories +--inc-recursive, --i-r enable incremental recursion +--no-inc-recursive disable incremental recursion +--no-i-r same as --no-inc-recursive --relative, -R use relative path names ---no-implied-dirs don't send implied dirs with --relative +--no-implied-dirs don't send implied directories with --relative --backup, -b make backups (see --suffix & --backup-dir) --backup-dir=DIR make backups into hierarchy based in DIR --suffix=SUFFIX backup suffix (default ~ w/o --backup-dir) @@ -534,15 +643,18 @@ --append append data onto shorter files --append-verify --append w/old data in file checksum --dirs, -d transfer directories without recursing ---old-dirs, --old-d works like --dirs when talking to old rsync +--old-dirs works like --dirs when talking to old rsync +--old-d same as --old-dirs --mkpath create destination's missing path components --links, -l copy symlinks as symlinks ---copy-links, -L transform symlink into referent file/dir +--copy-links, -L transform symlink into referent file/directory --copy-unsafe-links only "unsafe" symlinks are transformed --safe-links ignore symlinks that point outside the tree +--insecure-links follow attacker-owned symlinks in operator paths +--confine-root=DIR refuse operator paths resolving outside DIR --munge-links munge symlinks to make them safe & unusable ---copy-dirlinks, -k transform symlink to dir into referent dir ---keep-dirlinks, -K treat symlinked dir on receiver as dir +--copy-dirlinks, -k transform symlink to directory into referent directory +--keep-dirlinks, -K treat symlinked directory on receiver as directory --hard-links, -H preserve hard links --perms, -p preserve permissions --executability, -E preserve executability @@ -555,6 +667,7 @@ --copy-devices copy device contents as a regular file --write-devices write to devices as files (implies --inplace) --specials preserve special files +--drop-D receiver refuses to create devices/specials -D same as --devices --specials --times, -t preserve modification times --atimes, -U preserve access (use) times @@ -568,25 +681,27 @@ --preallocate allocate dest files before writing them --dry-run, -n perform a trial run with no changes made --whole-file, -W copy files whole (w/o delta-xfer algorithm) +--no-whole-file, --no-W use the delta-xfer algorithm --checksum-choice=STR choose the checksum algorithm (aka --cc) --one-file-system, -x don't cross filesystem boundaries --block-size=SIZE, -B force a fixed checksum block-size --rsh=COMMAND, -e specify the remote shell to use --rsync-path=PROGRAM specify the rsync to run on remote machine --existing skip creating new files on receiver +--ignore-non-existing skip creating new files on receiver --ignore-existing skip updating files that exist on receiver ---remove-source-files sender removes synchronized files (non-dir) +--remove-source-files sender removes synchronized files (non-directory) --del an alias for --delete-during ---delete delete extraneous files from dest dirs +--delete delete extraneous files from dest directories --delete-before receiver deletes before xfer, not during --delete-during receiver deletes during the transfer --delete-delay find deletions during, delete after --delete-after receiver deletes after transfer, not during ---delete-excluded also delete excluded files from dest dirs ---ignore-missing-args ignore missing source args without error ---delete-missing-args delete missing source args from destination +--delete-excluded also delete excluded files from dest directories +--ignore-missing-args ignore missing source arguments without error +--delete-missing-args delete missing source arguments from destination --ignore-errors delete even if there are I/O errors ---force force deletion of dirs even if not empty +--force force deletion of directories even if not empty --max-delete=NUM don't delete more than NUM files --max-size=SIZE don't transfer any file larger than SIZE --min-size=SIZE don't transfer any file smaller than SIZE @@ -612,6 +727,7 @@ --compress, -z compress file data during the transfer --compress-choice=STR choose the compression algorithm (aka --zc) --compress-level=NUM explicitly set compression level (aka --zl) +--compress-threads=NUM explicitly set compression threads (aka --zt) --skip-compress=LIST skip compressing files with suffix in LIST --cvs-exclude, -C auto-ignore files in the same way CVS does --filter=RULE, -f add a file-filtering RULE @@ -623,8 +739,8 @@ --include-from=FILE read include patterns from FILE --files-from=FILE read list of source-file names from FILE --from0, -0 all *-from/filter files are delimited by 0s ---old-args disable the modern arg-protection idiom ---secluded-args, -s use the protocol to safely send the args +--old-args disable the modern argument-protection idiom +--secluded-args, -s use the protocol to safely send the arguments --trust-sender trust the remote sender's file list --copy-as=USER[:GROUP] specify user & optional group for the copy --address=ADDRESS bind address for outgoing socket to daemon @@ -697,16 +813,16 @@ The parameter may need to be quoted in some manner for it to survive the shell's command-line parsing. Also keep in mind that a leading tilde (\fB~\fP) in a pathname is substituted by your shell, so make sure that you separate the -option name from the pathname using a space if you want the local shell to -expand it. +option name from the pathname using a space (rather than an equal +sign) if you want the local shell to expand it. .P .IP "\fB\-\-help\fP" Print a short help page describing the options available in rsync and exit. You can also use \fB\-h\fP for \fB\-\-help\fP when it is used without any other -options (since it normally means \fB\-\-human-readable\fP). +options or arguments (since otherwise it means \fB\-\-human\-readable\fP). .IP "\fB\-\-version\fP, \fB\-V\fP" Print the rsync version plus other info and exit. When repeated, the -information is output is a JSON format that is still fairly readable +information is output in a JSON format that is still fairly readable (client side only). .IP The output includes a list of compiled-in capabilities, a list of @@ -730,18 +846,19 @@ value, which is a ratio of the total file size divided by the sum of the sent and received bytes (which is really just a feel-good bigger-is-better number). Note that these byte values can be made more (or less) -human-readable by using the \fB\-\-human-readable\fP (or -\fB\-\-no-human-readable\fP) options. +human-readable by using the \fB\-\-human\-readable\fP (or +\fB\-\-no\-human\-readable\fP) options. .IP -In a modern rsync, the \fB\-v\fP option is equivalent to the setting of groups +The \fB\-v\fP option is equivalent to the setting of groups of \fB\-\-info\fP and \fB\-\-debug\fP options. You can choose to use these newer options in addition to, or in place of using \fB\-\-verbose\fP, as any fine-grained settings override the implied settings of \fB\-v\fP. Both -\fB\-\-info\fP and \fB\-\-debug\fP have a way to ask for help that -tells you exactly what flags are set for each increase in verbosity. +\fB\-\-info\fP and \fB\-\-debug\fP have help texts available that +tells you exactly what flags are set for each increase in verbosity +(use \fB\-\-info=help\fP or \fB\-\-debug=help\fP to see them). .IP However, do keep in mind that a daemon's "\fBmax\ verbosity\fP" setting will limit -how high of a level the various individual flags can be set on the daemon +how high the various individual flags can be set on the daemon side. For instance, if the max is 2, then any info and/or debug flag that is set to a higher value than what would be set by \fB\-vv\fP will be downgraded to the \fB\-vv\fP level in the daemon's logging. @@ -761,13 +878,13 @@ .fi .RE .IP -Note that \fB\-\-info=name\fP's output is affected by the \fB\-\-out-format\fP -and \fB\-\-itemize-changes\fP (\fB\-i\fP) options. See those options for more +Note that \fB\-\-info=name\fP's output is affected by the \fB\-\-out\-format\fP +and \fB\-\-itemize\-changes\fP (\fB\-i\fP) options. See those options for more information on what is output and when. .IP -This option was added to 3.1.0, so an older rsync on the server side might -reject your attempts at fine-grained control (if one or more flags needed -to be send to the server and the server was too old to understand them). +This option was added to 3.1.0 (released September 2013), +so an older rsync on the server side might +reject your attempts at fine-grained control. See also the "\fBmax\ verbosity\fP" caveat above when dealing with a daemon. .IP "\fB\-\-debug=FLAGS\fP" This option lets you have fine-grained control over the debug output you @@ -788,9 +905,9 @@ Note that some debug messages will only be output when the \fB\-\-stderr=all\fP option is specified, especially those pertaining to I/O and buffer debugging. .IP -Beginning in 3.2.0, this option is no longer auto-forwarded to the server -side in order to allow you to specify different debug values for each side -of the transfer, as well as to specify a new debug option that is only +This option is not forwarded by the client to the server side, so that +you can specify different debug values for each side of the transfer. +This also allows you to specify debug options that are only present in one of the rsync versions. If you want to duplicate the same option on both sides, using brace expansion is an easy way to save you some typing. This works in zsh and bash: @@ -801,22 +918,23 @@ .fi .RE .IP "\fB\-\-stderr=errors|all|client\fP" -This option controls which processes output to stderr and if info messages -are also changed to stderr. The mode strings can be abbreviated, so feel -free to use a single letter value. The 3 possible choices are: +This option controls which processes output to stderr, and whether +info messages are output to stderr rather than stdout. +The mode strings can be abbreviated down as far as a single letter. +The 3 possible choices are: .IP .RS .IP o -\fBerrors\fP \- (the default) causes all the rsync processes to send an -error directly to stderr, even if the process is on the remote side of +\fBerrors\fP \- (the default) causes all the rsync processes to send +errors directly to stderr, even if the process is on the remote side of the transfer. Info messages are sent to the client side via the protocol stream. If stderr is not available (i.e. when directly connecting with a daemon via a socket) errors fall back to being sent via the protocol stream. .IP o -\fBall\fP \- causes all rsync messages (info and error) to get written +\fBall\fP \- causes all rsync messages (info and error) to be written directly to stderr from all (possible) processes. This causes stderr to -become line-buffered (instead of raw) and eliminates the ability to +become line-buffered (instead of unbuffered) and eliminates the ability to divide up the info and error messages by file handle. For those doing debugging or using several levels of verbosity, this option can help to avoid clogging up the transfer stream (which should prevent any chance of @@ -832,42 +950,42 @@ been around for several releases. .RE .IP -This option was added in rsync 3.2.3. This version also began the -forwarding of a non-default setting to the remote side, though rsync uses -the backward-compatible options \fB\-\-msgs2stderr\fP and \fB\-\-no-msgs2stderr\fP to -represent the \fBall\fP and \fBclient\fP settings, respectively. A newer rsync -will continue to accept these older option names to maintain compatibility. +Rsync also accepts the old \fB\-\-msgs2stderr\fP and \fB\-\-no\-msgs2stderr\fP +options, equivalent to the \fBall\fP and \fBclient\fP settings, +respectively, and uses them when forwarding non-default settings +to the remote side, in case the remote rsync is an older version. .IP "\fB\-\-quiet\fP, \fB\-q\fP" This option decreases the amount of information you are given during the transfer, notably suppressing information messages from the remote server. This option is useful when invoking rsync from cron. -.IP "\fB\-\-no-motd\fP" +.IP "\fB\-\-no\-motd\fP" This option affects the information that is output by the client at the start of a daemon transfer. This suppresses the message-of-the-day (MOTD) text, but it also affects the list of modules that the daemon sends in response to the "rsync host::" request (due to a limitation in the rsync protocol), so omit this option if you want to request the list of modules from the daemon. -.IP "\fB\-\-ignore-times\fP, \fB\-I\fP" +.IP "\fB\-\-ignore\-times\fP, \fB\-I\fP" Normally rsync will skip any files that are already the same size and have the same modification timestamp. This option turns off this "quick check" behavior, causing all files to be updated. .IP -This option can be confusing compared to \fB\-\-ignore-existing\fP and -\fB\-\-ignore-non-existing\fP in that that they cause rsync to transfer +This option can be confusing compared to \fB\-\-ignore\-existing\fP and +\fB\-\-ignore\-non\-existing\fP in that that they cause rsync to transfer fewer files, while this option causes rsync to transfer more files. -.IP "\fB\-\-size-only\fP" +.IP "\fB\-\-size\-only\fP" This modifies rsync's "quick check" algorithm for finding files that need to be transferred, changing it from the default of transferring files with either a changed size or a changed last-modified time to just looking for files that have changed in size. This is useful when starting to use rsync after using another mirroring system which may not preserve timestamps exactly. -.IP "\fB\-\-modify-window=NUM\fP, \fB\-@\fP" +.IP "\fB\-\-modify\-window=NUM\fP, \fB\-@\fP" When comparing two timestamps, rsync treats the timestamps as being equal if they differ by no more than the modify-window value. The default is 0, which matches just integer seconds. If you specify a negative value (and -the receiver is at least version 3.1.3) then nanoseconds will also be taken +the receiver is at least version 3.1.3, released January 2018) +then nanoseconds will also be taken into account. Specifying 1 is useful for copies to/from MS Windows FAT filesystems, because FAT represents times with a 2-second resolution (allowing times to differ from the original by up to 1 second). @@ -882,7 +1000,7 @@ .fi .RE .IP -With that as the default, you'd need to specify \fB\-\-modify-window=0\fP (aka +With that as the default, you'd need to specify \fB\-\-modify\-window=0\fP (aka \fB\-@0\fP) to override it and ignore nanoseconds, e.g. if you're copying between ext3 and ext4, or if the receiving rsync is older than 3.1.3. .IP "\fB\-\-checksum\fP, \fB\-c\fP" @@ -890,7 +1008,7 @@ need of a transfer. Without this option, rsync uses a "quick check" that (by default) checks if each file's size and time of last modification match between the sender and receiver. This option changes this to compare a -128-bit checksum for each file that has a matching size. Generating the +checksum for each file that has a matching size. Generating the checksums means that both sides will expend a lot of disk I/O reading all the data in the files in the transfer, so this can slow things down significantly (and this is prior to any reading that will be done to @@ -902,14 +1020,14 @@ file that has the same size as the corresponding sender's file: files with either a changed size or a changed checksum are selected for transfer. .IP -Note that rsync always verifies that each \fItransferred\fP file was correctly +Note that rsync normally verifies that each \fItransferred\fP file was correctly reconstructed on the receiving side by checking a whole-file checksum that is generated as the file is transferred, but that automatic after-the-transfer verification has nothing to do with this option's before-the-transfer "Does this file need to be updated?" check. .IP The checksum used is auto-negotiated between the client and the server, but -can be overridden using either the \fB\-\-checksum-choice\fP (\fB\-\-cc\fP) +can be overridden using either the \fB\-\-checksum\-choice\fP (\fB\-\-cc\fP) option or an environment variable that is discussed in that option's section. .IP "\fB\-\-archive\fP, \fB\-a\fP" @@ -918,38 +1036,39 @@ \fBnot\fP include preserving ACLs (\fB\-A\fP), xattrs (\fB\-X\fP), atimes (\fB\-U\fP), crtimes (\fB\-N\fP), nor the finding and preserving of hardlinks (\fB\-H\fP). .IP -The only exception to the above equivalence is when \fB\-\-files-from\fP +The only exception to the above equivalence is when \fB\-\-files\-from\fP is specified, in which case \fB\-r\fP is not implied. -.IP "\fB\-\-no-OPTION\fP" +.IP "\fB\-\-no\-OPTION\fP" You may turn off one or more implied options by prefixing the option name with "no-". Not all positive options have a negated opposite, but a lot do, including those that can be used to disable an implied option (e.g. -\fB\-\-no-D\fP, \fB\-\-no-perms\fP) or have different defaults in various circumstances -(e.g. \fB\-\-no-whole-file\fP, \fB\-\-no-blocking-io\fP, \fB\-\-no-dirs\fP). Every +\fB\-\-no\-D\fP, \fB\-\-no\-perms\fP) or have different defaults in various circumstances +(e.g. \fB\-\-no\-whole\-file\fP, \fB\-\-no\-blocking\-io\fP, \fB\-\-no\-dirs\fP). Every valid negated option accepts both the short and the long option name after -the "no-" prefix (e.g. \fB\-\-no-R\fP is the same as \fB\-\-no-relative\fP). +the "no-" prefix (e.g. \fB\-\-no\-R\fP is the same as \fB\-\-no\-relative\fP). .IP As an example, if you want to use \fB\-\-archive\fP (\fB\-a\fP) but don't want \fB\-\-owner\fP (\fB\-o\fP), instead of converting \fB\-a\fP into \fB\-rlptgD\fP, you -can specify \fB\-a\ \-\-no-o\fP (aka \fB\-\-archive\ \-\-no-owner\fP). +can specify \fB\-a\ \-\-no\-o\fP (aka \fB\-\-archive\ \-\-no\-owner\fP). .IP -The order of the options is important: if you specify \fB\-\-no-r\ \-a\fP, the \fB\-r\fP -option would end up being turned on, the opposite of \fB\-a\ \-\-no-r\fP. Note -also that the side-effects of the \fB\-\-files-from\fP option are NOT +The order of the options is important: if you specify \fB\-\-no\-r\ \-a\fP, the \fB\-r\fP +option would end up being turned on, the opposite of \fB\-a\ \-\-no\-r\fP. Note +also that the side-effects of the \fB\-\-files\-from\fP option are NOT positional, as it affects the default state of several options and slightly -changes the meaning of \fB\-a\fP (see the \fB\-\-files-from\fP option +changes the meaning of \fB\-a\fP (see the \fB\-\-files\-from\fP option for more details). .IP "\fB\-\-recursive\fP, \fB\-r\fP" This tells rsync to copy directories recursively. See also \fB\-\-dirs\fP (\fB\-d\fP) for an option that allows the scanning of a single directory. .IP -See the \fB\-\-inc-recursive\fP option for a discussion of the +See the \fB\-\-inc\-recursive\fP option for a discussion of the incremental recursion for creating the list of files to transfer. -.IP "\fB\-\-inc-recursive\fP, \fB\-\-i-r\fP" -This option explicitly enables on incremental recursion when scanning for +.IP "\fB\-\-inc\-recursive\fP, \fB\-\-i\-r\fP" +This option explicitly enables incremental recursion when scanning for files, which is enabled by default when using the \fB\-\-recursive\fP -option and both sides of the transfer are running rsync 3.0.0 or newer. +option and both sides of the transfer are running rsync 3.0.0 +(released March 2008) or newer. .IP Incremental recursion uses much less memory than non-incremental, while also beginning the transfer more quickly (since it doesn't need to scan the @@ -961,43 +1080,41 @@ .IP .RS .IP o -\fB\-\-delete-before\fP (the old default of \fB\-\-delete\fP) +\fB\-\-delete\-before\fP (the old default of \fB\-\-delete\fP) .IP o -\fB\-\-delete-after\fP +\fB\-\-delete\-after\fP .IP o -\fB\-\-prune-empty-dirs\fP +\fB\-\-prune\-empty\-dirs\fP .IP o -\fB\-\-delay-updates\fP +\fB\-\-delay\-updates\fP .RE .IP -In order to make \fB\-\-delete\fP compatible with incremental recursion, -rsync 3.0.0 made \fB\-\-delete-during\fP the default delete mode (which -was first added in 2.6.4). +In order to be compatible with incremental recursion, +\fB\-\-delete\-during\fP is the default delete mode for \fB\-\-delete\fP. .IP One side-effect of incremental recursion is that any missing sub-directories inside a recursively-scanned directory are (by default) -created prior to recursing into the sub-dirs. This earlier creation point +created prior to recursing into the sub-directories. This earlier creation point (compared to a non-incremental recursion) allows rsync to then set the modify time of the finished directory right away (without having to delay that until a bunch of recursive copying has finished). However, these early directories don't yet have their completed mode, mtime, or ownership set\ \-\- they have more restrictive rights until the subdirectory's copying actually begins. This early-creation idiom can be avoided by using the -\fB\-\-omit-dir-times\fP option. +\fB\-\-omit\-dir\-times\fP option. .IP Incremental recursion can be disabled using the -\fB\-\-no-inc-recursive\fP (\fB\-\-no-i-r\fP) option. -.IP "\fB\-\-no-inc-recursive\fP, \fB\-\-no-i-r\fP" -Disables the new incremental recursion algorithm of the +\fB\-\-no\-inc\-recursive\fP (\fB\-\-no\-i\-r\fP) option. +.IP "\fB\-\-no\-inc\-recursive\fP, \fB\-\-no\-i\-r\fP" +Disables the incremental recursion algorithm of the \fB\-\-recursive\fP option. This makes rsync scan the full file list -before it begins to transfer files. See \fB\-\-inc-recursive\fP for more +before it begins to transfer files. See \fB\-\-inc\-recursive\fP for more info. .IP "\fB\-\-relative\fP, \fB\-R\fP" Use relative paths. This means that the full path names specified on the command line are sent to the server rather than just the last parts of the filenames. This is particularly useful when you want to send several -different directories at the same time. For example, if you used this -command: +different directories at the same time. For example, this command: .RS 4 .IP .nf @@ -1019,18 +1136,19 @@ "implied directories" (i.e. the "foo" and the "foo/bar" directories in the above example). .IP -Beginning with rsync 3.0.0, rsync always sends these implied directories as +Rsync always sends these implied directories as real directories in the file list, even if a path element is really a symlink on the sending side. This prevents some really unexpected behaviors -when copying the full path of a file that you didn't realize had a symlink +when copying the full path of a file that has a symlink in its path. If you want to duplicate a server-side symlink, include both -the symlink via its path, and referent directory via its real path. If +the symlink via its path, and the referent directory via its real path. If you're dealing with an older rsync on the sending side, you may need to use -the \fB\-\-no-implied-dirs\fP option. +the \fB\-\-no\-implied\-dirs\fP option. .IP It is also possible to limit the amount of path information that is sent as implied directories for each path you specify. With a modern rsync on the -sending side (beginning with 2.6.7), you can insert a dot and a slash into +sending side (beginning with 2.6.7, released March 2006), +you can insert a dot and a slash into the source path, like this: .RS 4 .IP @@ -1039,9 +1157,9 @@ .fi .RE .IP -That would create /tmp/bar/baz.c on the remote machine. (Note that the dot +That would create /tmp/bar/baz.c on the receiving machine. (Note that the dot must be followed by a slash, so "/foo/." would not be abbreviated.) For -older rsync versions, you would need to use a chdir to limit the source +older rsync versions, you would need to change directory to limit the source path. For example, when pushing files: .RS 4 .IP @@ -1050,7 +1168,7 @@ .fi .RE .IP -(Note that the parens put the two commands into a sub-shell, so that the +(Note that the parentheses put the two commands into a sub-shell, so that the "cd" command doesn't remain in effect for future commands.) If you're pulling files from an older rsync, use this idiom (but only for a non-daemon transfer): @@ -1061,7 +1179,7 @@ remote:bar/baz.c /tmp/ .fi .RE -.IP "\fB\-\-no-implied-dirs\fP" +.IP "\fB\-\-no\-implied\-dirs\fP" This option affects the default behavior of the \fB\-\-relative\fP option. When it is specified, the attributes of the implied directories from the source names are not included in the transfer. This means that the corresponding @@ -1070,33 +1188,41 @@ This even allows these implied path elements to have big differences, such as being a symlink to a directory on the receiving side. .IP -For instance, if a command-line arg or a files-from entry told rsync to +For instance, if a command-line argument or a files-from entry told rsync to transfer the file "path/foo/file", the directories "path" and "path/foo" are implied when \fB\-\-relative\fP is used. If "path/foo" is a symlink to "bar" on the destination system, the receiving rsync would ordinarily delete "path/foo", recreate it as a directory, and receive the file into the new -directory. With \fB\-\-no-implied-dirs\fP, the receiving rsync updates -"path/foo/file" using the existing path elements, which means that the file -ends up being created in "path/bar". Another way to accomplish this link -preservation is to use the \fB\-\-keep-dirlinks\fP option (which will also affect -symlinks to directories in the rest of the transfer). +directory. With \fB\-\-no\-implied\-dirs\fP, the receiving rsync leaves the +existing "path/foo" symlink in place and follows it, so the file is written +through the symlink and ends up in "path/bar". Note the security +implication: a pre-existing in-tree symlink-to-directory on the receiving +side will redirect where the file is written, so only use +\fB\-\-no\-implied\-dirs\fP when you trust the destination's existing path elements. +(A symlink whose target is absolute or escapes the destination tree is still +refused by the secure path resolver rather than followed.) Another way to +accomplish this link preservation is to use the \fB\-\-keep\-dirlinks\fP +option (which will also affect symlinks to directories in the rest of the +transfer). .IP -When pulling files from an rsync older than 3.0.0, you may need to use this +When pulling files from an rsync older than 3.0.0 (March 2008), +you may need to use this option if the sending side has a symlink in the path you request and you wish the implied directories to be transferred as normal directories. .IP "\fB\-\-backup\fP, \fB\-b\fP" With this option, preexisting destination files are renamed as each file is -transferred or deleted. You can control where the backup file goes and -what (if any) suffix gets appended using the \fB\-\-backup-dir\fP and +transferred or deleted (that is, each file to be deleted or overwritten +gets renamed instead). You can control where the backup file goes and +what (if any) suffix gets appended using the \fB\-\-backup\-dir\fP and \fB\-\-suffix\fP options. .IP -If you don't specify \fB\-\-backup-dir\fP: +If you don't specify \fB\-\-backup\-dir\fP: .RS .IP .IP 1. -the \fB\-\-omit-dir-times\fP option will be forced on +the \fB\-\-omit\-dir\-times\fP option will be forced on .IP 2. -the use of \fB\-\-delete\fP (without \fB\-\-delete-excluded\fP), +the use of \fB\-\-delete\fP (without \fB\-\-delete\-excluded\fP), causes rsync to add a "protect" filter-rule for the backup suffix to the end of all your existing filters that looks like this: \fB\-f\ "P\ *~"\fP. This rule prevents previously backed-up files from @@ -1108,7 +1234,7 @@ list so that it has a high enough priority to be effective (e.g. if your rules specify a trailing inclusion/exclusion of \fB*\fP, the auto-added rule would never be reached). -.IP "\fB\-\-backup-dir=DIR\fP" +.IP "\fB\-\-backup\-dir=DIR\fP" This implies the \fB\-\-backup\fP option, and tells rsync to store all backups in the specified directory on the receiving side. This can be used for incremental backups. You can additionally specify a backup suffix @@ -1118,22 +1244,23 @@ Note that if you specify a relative path, the backup directory will be relative to the destination directory, so you probably want to specify either an absolute path or a path that starts with "../". If an rsync -daemon is the receiver, the backup dir cannot go outside the module's path -hierarchy, so take extra care not to delete it or copy into it. +daemon is the receiver, the backup directory cannot go outside the module's +path hierarchy, so take extra care not to delete or copy into the +backup directory inadvertently. .IP "\fB\-\-suffix=SUFFIX\fP" This option allows you to override the default backup suffix used with the \fB\-\-backup\fP (\fB\-b\fP) option. The default suffix is a \fB~\fP if no -\fB\-\-backup-dir\fP was specified, otherwise it is an empty string. +\fB\-\-backup\-dir\fP was specified, otherwise it is an empty string. .IP "\fB\-\-update\fP, \fB\-u\fP" This forces rsync to skip any files which exist on the destination and have a modified time that is newer than the source file. (If an existing destination file has a modification time equal to the source file's, it will be updated if the sizes are different.) .IP -Note that this does not affect the copying of dirs, symlinks, or other -special files. Also, a difference of file format between the sender and +Note that this does not affect the copying of directories, symlinks, or other +special files. Also, a difference of object type between the sender and receiver is always considered to be important enough for an update, no -matter what date is on the objects. In other words, if the source has a +matter what date is on the objects. For example, if the source has a directory where the destination has a file, the transfer would occur regardless of the timestamps. .IP @@ -1190,12 +1317,11 @@ diverging the entire contents of a file that only has minor changes. .IP The option implies \fB\-\-partial\fP (since an interrupted transfer does -not delete the file), but conflicts with \fB\-\-partial-dir\fP and -\fB\-\-delay-updates\fP. Prior to rsync 2.6.4 \fB\-\-inplace\fP was also -incompatible with \fB\-\-compare-dest\fP and \fB\-\-link-dest\fP. +not delete the file), but conflicts with \fB\-\-partial\-dir\fP and +\fB\-\-delay\-updates\fP. .IP "\fB\-\-append\fP" -This special copy mode only works to efficiently update files that are -known to be growing larger where any existing content on the receiving side +This special copy mode is only applicable to update files that are +known to be growing larger, that is, where any existing content on the receiving side is also known to be the same as the content on the sender. The use of \fB\-\-append\fP \fBcan be dangerous\fP if you aren't 100% sure that all the files in the transfer are shared, growing files. You should thus use filter @@ -1204,8 +1330,8 @@ Rsync updates these growing file in-place without verifying any of the existing content in the file (it only verifies the content that it is appending). Rsync skips any files that exist on the receiving side that -are not shorter than the associated file on the sending side (which means -that new files are transferred). It also skips any files whose size on the +are not shorter than the associated file on the sending side. +It also skips any files whose size on the sending side gets shorter during the send negotiations (rsync warns about a "diminished" file when this happens). .IP @@ -1213,17 +1339,17 @@ attributes (e.g. permissions, ownership, etc.) when the file does not need to be transferred, nor does it affect the updating of any directories or non-regular files. -.IP "\fB\-\-append-verify\fP" +.IP "\fB\-\-append\-verify\fP" This special copy mode works like \fB\-\-append\fP except that all the -data in the file is included in the checksum verification (making it less -efficient but also potentially safer). This option \fBcan be dangerous\fP if +data in the file is included in the checksum verification, making it less +efficient but also potentially safer. This option \fBcan be dangerous\fP if you aren't 100% sure that all the files in the transfer are shared, growing files. See the \fB\-\-append\fP option for more details. .IP -Note: prior to rsync 3.0.0, the \fB\-\-append\fP option worked like -\fB\-\-append-verify\fP, so if you are interacting with an older rsync (or the +Note: prior to rsync 3.0.0 (March 2008), the \fB\-\-append\fP option worked like +\fB\-\-append\-verify\fP, so if you are interacting with an older rsync (or the transfer is using a protocol prior to 30), specifying either append option -will initiate an \fB\-\-append-verify\fP transfer. +will initiate an \fB\-\-append\-verify\fP transfer. .IP "\fB\-\-dirs\fP, \fB\-d\fP" Tell the sending side to include any directories that are encountered. Unlike \fB\-\-recursive\fP, a directory's contents are not copied unless @@ -1233,21 +1359,24 @@ (and output a message to that effect for each one). If you specify both \fB\-\-dirs\fP and \fB\-\-recursive\fP, \fB\-\-recursive\fP takes precedence. .IP -The \fB\-\-dirs\fP option is implied by the \fB\-\-files-from\fP option or the -\fB\-\-list-only\fP option (including an implied \fB\-\-list-only\fP +The \fB\-\-dirs\fP option is implied by the \fB\-\-files\-from\fP option or the +\fB\-\-list\-only\fP option (including an implied \fB\-\-list\-only\fP usage) if \fB\-\-recursive\fP wasn't specified (so that directories are -seen in the listing). Specify \fB\-\-no-dirs\fP (or \fB\-\-no-d\fP) if you want to +seen in the listing). Specify \fB\-\-no\-dirs\fP (or \fB\-\-no\-d\fP) if you want to turn this off. .IP -There is also a backward-compatibility helper option, \fB\-\-old-dirs\fP -(\fB\-\-old-d\fP) that tells rsync to use a hack of \fB\-r\ \-\-exclude='/*/*'\fP to get -an older rsync to list a single directory without recursing. +See also the backward-compatibility helper option \fB\-\-old\-dirs\fP. +.IP "\fB\-\-old\-dirs\fP, \fB\-\-old\-d\fP" +This backward-compatibility helper tells rsync to use a hack of +\fB\-r\ \-\-exclude='/*/*'\fP to get an older remote rsync to list a single directory +without recursing. .IP "\fB\-\-mkpath\fP" Create all missing path components of the destination path. .IP -By default, rsync allows only the final component of the destination path -to not exist, which is an attempt to help you to validate your destination -path. With this option, rsync creates all the missing destination-path +By default, rsync will create only the final component of the destination path +if it does not exist. If any other component does not exist that is an +error, as this can help to catch mistakes in the destination path specification. +With this option, rsync creates all the missing destination-path components, just as if \fBmkdir\ \-p\ $DEST_PATH\fP had been run on the receiving side. .IP @@ -1257,11 +1386,11 @@ section for full details on how rsync decides if a final destination-path component should be created as a directory or not. .IP -If you would like the newly-created destination dirs to match the dirs on +If you would like the newly-created destination directories to match the directories on the sending side, you should be using \fB\-\-relative\fP (\fB\-R\fP) instead of \fB\-\-mkpath\fP. For instance, the following two commands result in the same destination tree, but only the second command ensures that the -"some/extra/path" components match the dirs on the sending side: +"some/extra/path" components match the directories on the sending side: .RS 4 .IP .nf @@ -1272,13 +1401,13 @@ .IP "\fB\-\-links\fP, \fB\-l\fP" Add symlinks to the transferred files instead of noisily ignoring them with a "non-regular file" warning for each symlink encountered. You can -alternately silence the warning by specifying \fB\-\-info=nonreg0\fP. +alternatively silence the warning by specifying \fB\-\-info=nonreg0\fP. .IP The default handling of symlinks is to recreate each symlink's unchanged value on the receiving side. .IP See the SYMBOLIC LINKS section for multi-option info. -.IP "\fB\-\-copy-links\fP, \fB\-L\fP" +.IP "\fB\-\-copy\-links\fP, \fB\-L\fP" The sender transforms each symlink encountered in the transfer into the referent item, following the symlink chain to the file or directory that it references. If a symlink chain is broken, an error is output and the file @@ -1288,107 +1417,170 @@ transfer, since there are no symlinks left in the transfer. .IP This option does not change the handling of existing symlinks on the -receiving side, unlike versions of rsync prior to 2.6.3 which had the -side-effect of telling the receiving side to also follow symlinks. A -modern rsync won't forward this option to a remote receiver (since only the -sender needs to know about it), so this caveat should only affect someone -using an rsync client older than 2.6.7 (which is when \fB\-L\fP stopped being -forwarded to the receiver). +receiving side. .IP -See the \fB\-\-keep-dirlinks\fP (\fB\-K\fP) if you need a symlink to a +See the \fB\-\-keep\-dirlinks\fP (\fB\-K\fP) if you need a symlink to a directory to be treated as a real directory on the receiving side. .IP See the SYMBOLIC LINKS section for multi-option info. -.IP "\fB\-\-copy-unsafe-links\fP" +.IP "\fB\-\-copy\-unsafe\-links\fP" This tells rsync to copy the referent of symbolic links that point outside the copied tree. Absolute symlinks are also treated like ordinary files, and so are any symlinks in the source path itself when \fB\-\-relative\fP is used. .IP -Note that the cut-off point is the top of the transfer, which is the part -of the path that rsync isn't mentioning in the verbose output. If you copy -"/src/subdir" to "/dest/" then the "subdir" directory is a name inside the -transfer tree, not the top of the transfer (which is /src) so it is legal -for created relative symlinks to refer to other names inside the /src and -/dest directories. If you instead copy "/src/subdir/" (with a trailing -slash) to "/dest/subdir" that would not allow symlinks to any files outside -of "subdir". +A symlink is judged unsafe by a lexical test on its value, without resolving +it on disk. An absolute or empty target is always unsafe. A relative +target is unsafe if its ".." components would climb above the top of the +transfer; a ".." that appears anywhere other than as a leading prefix (an +embedded or trailing "/..") is also treated as unsafe. The top is set by +how the source is specified: a trailing slash on the source (e.g. +"/src/subdir/" copied to "/dest/subdir") makes that directory itself the +top, so a symlink may not point above it; without the trailing slash (e.g. +"/src/subdir" copied to "/dest/") the source's parent ("/src") is the top, +so "subdir" is a name inside the transfer and a relative symlink may point +to any other name within it. .IP Note that safe symlinks are only copied if \fB\-\-links\fP was also -specified or implied. The \fB\-\-copy-unsafe-links\fP option has no extra effect -when combined with \fB\-\-copy-links\fP. +specified or implied. The \fB\-\-copy\-unsafe\-links\fP option has no extra effect +when combined with \fB\-\-copy\-links\fP. .IP See the SYMBOLIC LINKS section for multi-option info. -.IP "\fB\-\-safe-links\fP" +.IP "\fB\-\-safe\-links\fP" This tells the receiving rsync to ignore any symbolic links in the transfer which point outside the copied tree. All absolute symlinks are also ignored. .IP Since this ignoring is happening on the receiving side, it will still be effective even when the sending side has munged symlinks (when it is using -\fB\-\-munge-links\fP). It also affects deletions, since the file being +\fB\-\-munge\-links\fP). It also affects deletions, since the file being present in the transfer prevents any matching file on the receiver from being deleted when the symlink is deemed to be unsafe and is skipped. .IP -This option must be combined with \fB\-\-links\fP (or -\fB\-\-archive\fP) to have any symlinks in the transfer to conditionally -ignore. Its effect is superseded by \fB\-\-copy-unsafe-links\fP. +This option has no effect unless it is combined with \fB\-\-links\fP (or +an option that implies \fB\-\-links\fP such as \fB\-\-archive\fP), +because without \fB\-\-links\fP there will not be any symlinks in the +transfer. Its effect is superseded by \fB\-\-copy\-unsafe\-links\fP. .IP Using this option in conjunction with \fB\-\-relative\fP may give unexpected results. .IP See the SYMBOLIC LINKS section for multi-option info. -.IP "\fB\-\-munge-links\fP" +.IP "\fB\-\-insecure\-links\fP" +By default rsync resolves the directory and file paths that the \fBoperator\fP +supplies on the command line with a defensive directory-tree walk that +\fBfollows a symlink component only when it is owned by uid\ 0 or by the +running user\fP, and +refuses one owned by any other user. This stops an attacker who can write +inside one of those directories from planting a symlink that +redirects a privileged rsync to a target outside the intended tree, while +still honouring the operator's own symlinks. +The rule is the same for absolute and relative paths. +This applies to the destination directory and to the parameters to the +following options: \fB\-\-backup\-dir\fP, \fB\-\-temp\-dir\fP/\fB\-T\fP, +\fB\-\-partial\-dir\fP, \fB\-\-link\-dest\fP, \fB\-\-compare\-dest\fP, \fB\-\-copy\-dest\fP, \fB\-\-log\-file\fP, +\fB\-\-password\-file\fP, \fB\-\-files\-from\fP, \fB\-\-include\-from\fP, \fB\-\-exclude\-from\fP, +\fB\-\-filter\fP merge files, \fB\-\-write\-batch\fP and \fB\-\-read\-batch\fP. +.IP +\fB\-\-insecure\-links\fP turns that defence off, restoring the historical behaviour +of following any symlink in those paths. Use it only when you fully trust +every directory along each operator-supplied path, as it re-exposes the +symlink-redirection attacks the walk prevents. +.IP +The option is \fBlocal only\fP: it is not sent to the remote side of the +transfer. A remote-shell peer that needs the opt-out must set it there, e.g. +via \fB\-\-rsync\-path\fP), and a daemon never honours it\ \-\- a daemon that +receives \fB\-\-insecure\-links\fP from a client refuses the request. A daemon +administrator who wants the legacy behaviour for a single trusted module sets +"\fBinsecure\ links\ =\ yes\fP" in that module's \fBrsyncd.conf\fP(5) section instead; +a client can never enable it. +.IP +See the SYMBOLIC LINKS section for multi-option info. +.IP "\fB\-\-confine\-root=DIR\fP" +This bounds where the paths listed under \fB\-\-insecure\-links\fP are +allowed to resolve: one that ends up outside DIR is refused, even if every +symlink along it was owned by a trusted user. The ownership walk asks who +planted a link; this asks where the path came out. +.IP +DIR must be absolute. Nothing is confined by default, and +\fB\-\-confine\-root=/\fP is a no-op. +.IP +It exists for a wrapper that serves a restricted directory over a remote +shell, \fBrrsync\fP being the one shipped here, which passes it automatically +whenever its restricted dir is not "\fB/\fP". Such a wrapper can vet the argv +it is handed, but filter rules travel over the protocol instead: a client +can name a merge file outside the restricted dir in a dir-merge rule and +have the server read it in as filter rules. On a pull that needs neither +\fB\-\-delete\fP nor any verbosity, because an exclude-only merge (the "\fB\-\fP" +modifier) makes every line a pattern, and the client reads the file's +contents off which of its own names went missing. Confining the open is +what closes that; a merge file inside DIR keeps working as before. +.IP +A daemon ignores this option\ \-\- its module directory is already the +boundary, and the option arrives in a client-supplied argv, so honouring it +could only widen the module. +.IP +\fB\-\-insecure\-links\fP is refused alongside it. That opt-out restores +the historical open, which skips the walk that enforces the root, so the two +together would silently mean no confinement at all. +.IP +Like \fB\-\-drop\-D\fP, it is not forwarded to the remote side: it is meant +to be applied to one end of a connection by itself. +.IP "\fB\-\-munge\-links\fP" This option affects just one side of the transfer and tells rsync to munge symlink values when it is receiving files or unmunge symlink values when it -is sending files. The munged values make the symlinks unusable on disk but -allows the original contents of the symlinks to be recovered. +is sending files. +"Munging" changes the symlink target so that the symlink cannot be followed +successfully, but allows the original target of the symlink to be +recovered. "Unmunging" reverses this process so that the symlink points to +the original target. .IP -The server-side rsync often enables this option without the client's +This option can be enabled on the server side without the client's knowledge, such as in an rsync daemon's configuration file or by an option given to the rrsync (restricted rsync) script. When specified on the client side, specify the option normally if it is the client side that -has/needs the munged symlinks, or use \fB\-M\-\-munge-links\fP to give the option -to the server when it has/needs the munged symlinks. Note that on a local +has or needs the munged symlinks, or use \fB\-M\-\-munge\-links\fP to give the option +to the server when it has or needs the munged symlinks. Note that on a local transfer, the client is the sender, so specifying the option directly unmunges symlinks while specifying it as a remote option munges symlinks. .IP -This option has no effect when sent to a daemon via \fB\-\-remote-option\fP +This option has no effect when sent to a daemon via \fB\-\-remote\-option\fP because the daemon configures whether it wants munged symlinks via its "\fBmunge\ symlinks\fP" parameter. .IP -The symlink value is munged/unmunged once it is in the transfer, so any +The symlink value is munged or unmunged once it is in the transfer, so any option that transforms symlinks into non-symlinks occurs prior to the -munging/unmunging \fBexcept\fP for \fB\-\-safe-links\fP, which is a choice +munging/unmunging \fBexcept\fP for \fB\-\-safe\-links\fP, which is a choice that the receiver makes, so it bases its decision on the munged/unmunged -value. This does mean that if a receiver has munging enabled, that using -\fB\-\-safe-links\fP will cause all symlinks to be ignored (since they -are all absolute). -.IP -The method that rsync uses to munge the symlinks is to prefix each one's -value with the string "/rsyncd-munged/". This prevents the links from -being used as long as the directory does not exist. When this option is +value. This does mean that if a receiver has munging enabled, using +\fB\-\-safe\-links\fP will cause all symlinks to be ignored (since munging +makes them all absolute). +.IP +The method that rsync uses to munge a symlink is to prefix its +value with the string "/rsyncd-munged/". This prevents the link from +being used, provided that the /rsyncd-munged directory does not exist. +When this option is enabled, rsync will refuse to run if that path is a directory or a symlink to a directory (though it only checks at startup). See also the "munge-symlinks" python script in the support directory of the source code for a way to munge/unmunge one or more symlinks in-place. -.IP "\fB\-\-copy-dirlinks\fP, \fB\-k\fP" +.IP "\fB\-\-copy\-dirlinks\fP, \fB\-k\fP" This option causes the sending side to treat a symlink to a directory as though it were a real directory. This is useful if you don't want symlinks to non-directories to be affected, as they would be using -\fB\-\-copy-links\fP. +\fB\-\-copy\-links\fP. .IP Without this option, if the sending side has replaced a directory with a symlink to a directory, the receiving side will delete anything that is in the way of the new symlink, including a directory hierarchy (as long as \fB\-\-force\fP or \fB\-\-delete\fP is in effect). .IP -See also \fB\-\-keep-dirlinks\fP for an analogous option for the +See also \fB\-\-keep\-dirlinks\fP for an analogous option for the receiving side. .IP -\fB\-\-copy-dirlinks\fP applies to all symlinks to directories in the source. If +\fB\-\-copy\-dirlinks\fP applies to all symlinks to directories in the source. If you want to follow only a few specified symlinks, a trick you can use is to -pass them as additional source args with a trailing slash, using +pass them as additional source arguments with a trailing slash, using \fB\-\-relative\fP to make the paths match up right. For example: .RS 4 .IP @@ -1397,27 +1589,22 @@ .fi .RE .IP -This works because rsync calls \fBlstat\fP(2) on the source arg as given, and -the trailing slash makes \fBlstat\fP(2) follow the symlink, giving rise to a -directory in the file-list which overrides the symlink found during the -scan of "src/./". -.IP See the SYMBOLIC LINKS section for multi-option info. -.IP "\fB\-\-keep-dirlinks\fP, \fB\-K\fP" +.IP "\fB\-\-keep\-dirlinks\fP, \fB\-K\fP" This option causes the receiving side to treat a symlink to a directory as though it were a real directory, but only if it matches a real directory from the sender. Without this option, the receiver's symlink would be deleted and replaced with a real directory. .IP For example, suppose you transfer a directory "foo" that contains a file -"file", but "foo" is a symlink to directory "bar" on the receiver. Without -\fB\-\-keep-dirlinks\fP, the receiver deletes symlink "foo", recreates it as a +"file", but on the receiver, "foo" is a symlink to directory "bar". Without +\fB\-\-keep\-dirlinks\fP, the receiver deletes symlink "foo", recreates it as a directory, and receives the file into the new directory. With -\fB\-\-keep-dirlinks\fP, the receiver keeps the symlink and "file" ends up in +\fB\-\-keep\-dirlinks\fP, the receiver keeps the symlink and "file" ends up in "bar". .IP -One note of caution: if you use \fB\-\-keep-dirlinks\fP, you must trust all the -symlinks in the copy or enable the \fB\-\-munge-links\fP option on the +One note of caution: if you use \fB\-\-keep\-dirlinks\fP, you must either trust all the +symlinks in the copy, or enable the \fB\-\-munge\-links\fP option on the receiving side! If it is possible for an untrusted user to create their own symlink to any real directory, the user could then (on a subsequent copy) replace the symlink with a real directory and affect the content of @@ -1425,11 +1612,11 @@ better off using something like a bind mount instead of a symlink to modify your receiving hierarchy. .IP -See also \fB\-\-copy-dirlinks\fP for an analogous option for the sending +See also \fB\-\-copy\-dirlinks\fP for an analogous option for the sending side. .IP See the SYMBOLIC LINKS section for multi-option info. -.IP "\fB\-\-hard-links\fP, \fB\-H\fP" +.IP "\fB\-\-hard\-links\fP, \fB\-H\fP" This tells rsync to look for hard-linked files in the source and link together the corresponding files on the destination. Without this option, hard-linked files in the source are treated as though they were separate @@ -1447,10 +1634,10 @@ differences, the normal file-update process will break those extra links (unless you are using the \fB\-\-inplace\fP option). .IP o -If you specify a \fB\-\-link-dest\fP directory that contains hard +If you specify a \fB\-\-link\-dest\fP directory that contains hard links, the linking of the destination files against the -\fB\-\-link-dest\fP files can cause some paths in the destination to -become linked together due to the \fB\-\-link-dest\fP associations. +\fB\-\-link\-dest\fP files can cause some paths in the destination to +become linked together due to the \fB\-\-link\-dest\fP associations. .RE .IP Note that rsync can only detect hard links between files that are inside @@ -1461,7 +1648,7 @@ certain that no unintended changes happen due to lingering hard links (and see the \fB\-\-inplace\fP option for more caveats). .IP -If incremental recursion is active (see \fB\-\-inc-recursive\fP), rsync +If incremental recursion is active (see \fB\-\-inc\-recursive\fP), rsync may transfer a missing hard-linked file before it finds that another link for that contents exists elsewhere in the hierarchy. This does not affect the accuracy of the transfer (i.e. which files are hard-linked together), @@ -1469,7 +1656,7 @@ hard-linked file that could have been found later in the transfer in another member of the hard-linked set of files). One way to avoid this inefficiency is to disable incremental recursion using the -\fB\-\-no-inc-recursive\fP option. +\fB\-\-no\-inc\-recursive\fP option. .IP "\fB\-\-perms\fP, \fB\-p\fP" This option causes the receiving rsync to set the destination permissions to be the same as the source permissions. (See also the \fB\-\-chmod\fP @@ -1503,8 +1690,8 @@ that all non-masked bits get enabled). If you'd care to make this latter behavior easier to type, you could define a popt alias for it, such as putting this line in the file \fB~/.popt\fP (the following defines the \fB\-Z\fP -option, and includes \fB\-\-no-g\fP to use the default group of the destination -dir): +option, and includes \fB\-\-no\-g\fP to use the default group of the destination +directory): .RS 4 .IP .nf @@ -1521,20 +1708,14 @@ .RE .IP (Caveat: make sure that \fB\-a\fP does not follow \fB\-Z\fP, or it will re-enable the -two \fB\-\-no-*\fP options mentioned above.) +two \fB\-\-no\-*\fP options mentioned above.) .IP -The preservation of the destination's setgid bit on newly-created -directories when \fB\-\-perms\fP is off was added in rsync 2.6.7. Older rsync -versions erroneously preserved the three special permission bits for -newly-created files when \fB\-\-perms\fP was off, while overriding the -destination's setgid bit setting on a newly-created directory. Default ACL -observance was added to the ACL patch for rsync 2.6.7, so older (or -non-ACL-enabled) rsyncs use the umask even if default ACLs are present. -(Keep in mind that it is the version of the receiving rsync that affects -these behaviors.) +Note that if the remote rsync is older than 2.6.7 (March 2006) and it is +the receiver, the behavior when \fB\-\-perms\fP is off may differ from that +described above. .IP "\fB\-\-executability\fP, \fB\-E\fP" -This option causes rsync to preserve the executability (or -non-executability) of regular files when \fB\-\-perms\fP is not enabled. +This option causes rsync to preserve the executability or +non-executability of regular files when \fB\-\-perms\fP is not enabled. A regular file is considered to be executable if at least one 'x' is turned on in its permissions. When an existing destination file's executability differs from that of the corresponding source file, rsync modifies the @@ -1554,7 +1735,7 @@ the source ACLs. The option also implies \fB\-\-perms\fP. .IP The source and destination systems must have compatible ACL entries for -this option to work properly. See the \fB\-\-fake-super\fP option for a +this option to work properly. See the \fB\-\-fake\-super\fP option for a way to backup and restore ACLs that are not compatible. .IP "\fB\-\-xattrs\fP, \fB\-X\fP" This option causes rsync to update the destination extended attributes to @@ -1563,7 +1744,7 @@ For systems that support extended-attribute namespaces, a copy being done by a super-user copies all namespaces except system.*. A normal user only copies the user.* namespace. To be able to backup and restore non-user -namespaces as a normal user, see the \fB\-\-fake-super\fP option. +namespaces as a normal user, see the \fB\-\-fake\-super\fP option. .IP The above name filtering can be overridden by using one or more filter options with the \fBx\fP modifier. When you specify an xattr-affecting @@ -1597,8 +1778,8 @@ .RE .IP Note that the \fB\-X\fP option does not copy rsync's special xattr values (e.g. -those used by \fB\-\-fake-super\fP) unless you repeat the option (e.g. \fB\-XX\fP). -This "copy all xattrs" mode cannot be used with \fB\-\-fake-super\fP. +those used by \fB\-\-fake\-super\fP) unless you repeat the option (e.g. \fB\-XX\fP). +This "copy all xattrs" mode cannot be used with \fB\-\-fake\-super\fP. .IP "\fB\-\-chmod=CHMOD\fP" This option tells rsync to apply one or more comma-separated "chmod" modes to the permission of the files in the transfer. The resulting value is @@ -1628,37 +1809,49 @@ .fi .RE .IP +Symbolic permission-copy modes are also allowed, such as \fBg=u\fP, \fBo=g\fP or +\fBg-o\fP. A permission-copy item may copy from one class only (\fBu\fP, \fBg\fP or +\fBo\fP) and cannot be combined with \fBrwxXst\fP permission letters in the same +item. Use comma-separated items when you need both behaviours, such as +\fBg=o,o=\fP. +.IP +A permission-copy \fB=\fP item also clears the special bit for each destination +class it updates (\fBu\fP clears setuid, \fBg\fP clears setgid, and \fBo\fP clears +sticky), matching GNU \fBchmod\fP behaviour. +.IP It is also legal to specify multiple \fB\-\-chmod\fP options, as each additional option is just appended to the list of changes to make. .IP See the \fB\-\-perms\fP and \fB\-\-executability\fP options for how the resulting permission value can be applied to the files in the transfer. .IP "\fB\-\-owner\fP, \fB\-o\fP" -This option causes rsync to set the owner of the destination file to be the -same as the source file, but only if the receiving rsync is being run as -the super-user (see also the \fB\-\-super\fP and \fB\-\-fake-super\fP +This option causes rsync to set the owner of each destination filesystem +object (file, directory, etc.) to be the +same as the source object, but only if the receiving rsync is being run as +the super-user (see also the \fB\-\-super\fP and \fB\-\-fake\-super\fP options). Without this option, the owner of new and/or transferred files are set to the invoking user on the receiving side. .IP The preservation of ownership will associate matching names by default, but may fall back to using the ID number in some circumstances (see also the -\fB\-\-numeric-ids\fP option for a full discussion). +\fB\-\-numeric\-ids\fP option for a full discussion). .IP "\fB\-\-group\fP, \fB\-g\fP" -This option causes rsync to set the group of the destination file to be the -same as the source file. If the receiving program is not running as the -super-user (or if \fB\-\-no-super\fP was specified), only groups that the +This option causes rsync to set the group of each destination filesystem +object (file, directory, etc.) to be the +same as the source object. If the receiving program is not running as the +super-user (or if \fB\-\-no\-super\fP was specified), only groups that the invoking user on the receiving side is a member of will be preserved. Without this option, the group is set to the default group of the invoking user on the receiving side. .IP The preservation of group information will associate matching names by default, but may fall back to using the ID number in some circumstances -(see also the \fB\-\-numeric-ids\fP option for a full discussion). +(see also the \fB\-\-numeric\-ids\fP option for a full discussion). .IP "\fB\-\-devices\fP" This option causes rsync to transfer character and block device files to the remote system to recreate these devices. If the receiving rsync is not being run as the super-user, rsync silently skips creating the device files -(see also the \fB\-\-super\fP and \fB\-\-fake-super\fP options). +(see also the \fB\-\-super\fP and \fB\-\-fake\-super\fP options). .IP By default, rsync generates a "non-regular file" warning for each device file encountered when this option is not set. You can silence the warning @@ -1667,7 +1860,7 @@ This option causes rsync to transfer special files, such as named sockets and fifos. If the receiving rsync is not being run as the super-user, rsync silently skips creating the special files (see also the -\fB\-\-super\fP and \fB\-\-fake-super\fP options). +\fB\-\-super\fP and \fB\-\-fake\-super\fP options). .IP By default, rsync generates a "non-regular file" warning for each special file encountered when this option is not set. You can silence the warning @@ -1675,13 +1868,45 @@ .IP "\fB\-D\fP" The \fB\-D\fP option is equivalent to "\fB\-\-devices\fP \fB\-\-specials\fP". -.IP "\fB\-\-copy-devices\fP" +.IP "\fB\-\-drop\-D\fP" +This tells the receiving rsync to refuse to create device files and +special files, whatever the transfer requested. Entries for them are +skipped exactly as if \fB\-D\fP had not been used, with the usual +"non-regular file" warning. +.IP +This differs from \fB\-\-no\-D\fP in that it changes only what is created, not +how the file list is encoded. \fB\-\-no\-D\fP also turns off the rdev fields +that devices and special files carry on the wire, so applying it to one +end of a connection alone leaves the two ends disagreeing about the +encoding and the transfer fails. \fB\-\-drop\-D\fP can therefore be added on the +receiving side by itself, which is what it exists for\ \-\- the \fBrrsync\fP +wrapper uses it to stop a restricted directory's clients creating device +and special files in it. Nodes already present there are left alone. +.IP +Because it only withholds creation, it has no effect on a sending rsync. +.IP +This option is not forwarded to the remote side, since its whole purpose +is to be applied to one end of a connection by itself. It therefore +affects the rsync process you give it to and no other: on a local copy, or +on the receiving end of a \fB\-\-server\fP invocation such as the one rrsync +builds. To set it on a remote receiver from the command line, send it +explicitly with \fB\-\-remote\-option\fP (\fB\-M\fP): +.RS 4 +.IP +.nf +rsync -av -M--drop-D src/ host:dest/ +.fi +.RE +.IP +Passing a plain \fB\-\-drop\-D\fP to a push affects only the local sender, where +it does nothing. +.IP "\fB\-\-copy\-devices\fP" This tells rsync to treat a device on the sending side as a regular file, allowing it to be copied to a normal destination file (or another device -if \fB\-\-write-devices\fP was also specified). +if \fB\-\-write\-devices\fP was also specified). .IP This option is refused by default by an rsync daemon. -.IP "\fB\-\-write-devices\fP" +.IP "\fB\-\-write\-devices\fP" This tells rsync to treat a device on the receiving side as a regular file, allowing the writing of file data into a device. .IP @@ -1692,36 +1917,32 @@ .IP This option is refused by default by an rsync daemon. .IP "\fB\-\-times\fP, \fB\-t\fP" -This tells rsync to transfer modification times along with the files and -update them on the remote system. Note that if this option is not used, +This tells rsync to set the modification times of the destination files +(including directories, symlinks, devices, etc.) to be the same as the +source files. +Note that if this option is not used, the optimization that excludes files that have not been modified cannot be effective; in other words, a missing \fB\-t\fP (or \fB\-a\fP) will cause the -next transfer to behave as if it used \fB\-\-ignore-times\fP (\fB\-I\fP), -causing all files to be updated (though rsync's delta-transfer algorithm +next transfer to behave as if it used \fB\-\-ignore\-times\fP (\fB\-I\fP), +causing all files to be updated. (Although rsync's delta-transfer algorithm will make the update fairly efficient if the files haven't actually -changed, you're much better off using \fB\-t\fP). +changed, you're much better off using \fB\-t\fP.) .IP -A modern rsync that is using transfer protocol 30 or 31 conveys a modify -time using up to 8-bytes. If rsync is forced to speak an older protocol -(perhaps due to the remote rsync being older than 3.0.0) a modify time is -conveyed using 4-bytes. Prior to 3.2.7, these shorter values could convey -a date range of 13-Dec-1901 to 19-Jan-2038. Beginning with 3.2.7, these -4-byte values now convey a date range of 1-Jan-1970 to 7-Feb-2106. If you +If the negotiated protocol is older than 30\ \-\- usually because the remote +rsync is older than 3.0.0 (March 2008), but also if \fB\-\-protocol\fP +forces it\ \-\- the range of modification times that can be conveyed is +restricted. If you have files dated older than 1970, make sure your rsync executables are upgraded so that the full range of dates can be conveyed. .IP "\fB\-\-atimes\fP, \fB\-U\fP" This tells rsync to set the access (use) times of the destination files to the same value as the source files. .IP -If repeated, it also sets the \fB\-\-open-noatime\fP option, which can help you +If repeated, it also sets the \fB\-\-open\-noatime\fP option, which can help you to make the sending and receiving systems have the same access times on the transferred files without needing to run rsync an extra time after a file is transferred. -.IP -Note that some older rsync versions (prior to 3.2.0) may have been built -with a pre-release \fB\-\-atimes\fP patch that does not imply -\fB\-\-open-noatime\fP when this option is repeated. -.IP "\fB\-\-open-noatime\fP" +.IP "\fB\-\-open\-noatime\fP" This tells rsync to open files with the O_NOATIME flag (on systems that support it) to avoid changing the access time of the files that are being transferred. If your OS does not support the O_NOATIME flag then rsync @@ -1733,16 +1954,16 @@ files to the same value as the source files. Your OS & filesystem must support the setting of arbitrary creation (birth) times for this option to be supported. -.IP "\fB\-\-omit-dir-times\fP, \fB\-O\fP" +.IP "\fB\-\-omit\-dir\-times\fP, \fB\-O\fP" This tells rsync to omit directories when it is preserving modification, access, and create times. If NFS is sharing the directories on the receiving side, it is a good idea to use \fB\-O\fP. This option is inferred if you use -\fB\-\-backup\fP without \fB\-\-backup-dir\fP. +\fB\-\-backup\fP without \fB\-\-backup\-dir\fP. .IP This option also has the side-effect of avoiding early creation of missing sub-directories when incremental recursion is enabled, as discussed in the -\fB\-\-inc-recursive\fP section. -.IP "\fB\-\-omit-link-times\fP, \fB\-J\fP" +\fB\-\-inc\-recursive\fP section. +.IP "\fB\-\-omit\-link\-times\fP, \fB\-J\fP" This tells rsync to omit symlinks when it is preserving modification, access, and create times. .IP "\fB\-\-super\fP" @@ -1754,8 +1975,8 @@ systems that allow such activities without being the super-user, and also for ensuring that you will get errors if the receiving side isn't being run as the super-user. To turn off super-user activities, the super-user can -use \fB\-\-no-super\fP. -.IP "\fB\-\-fake-super\fP" +use \fB\-\-no\-super\fP. +.IP "\fB\-\-fake\-super\fP" When this option is enabled, rsync simulates super-user activities by saving/restoring the privileged attributes via special extended attributes that are attached to each file (as needed). This includes the file's owner @@ -1771,9 +1992,9 @@ This is a good way to backup data without using a super-user, and to store ACLs from incompatible systems. .IP -The \fB\-\-fake-super\fP option only affects the side where the option is used. +The \fB\-\-fake\-super\fP option only affects the side where the option is used. To affect the remote side of a remote-shell connection, use the -\fB\-\-remote-option\fP (\fB\-M\fP) option: +\fB\-\-remote\-option\fP (\fB\-M\fP) option: .RS 4 .IP .nf @@ -1783,22 +2004,23 @@ .IP For a local copy, this option affects both the source and the destination. If you wish a local copy to enable this option just for the destination -files, specify \fB\-M\-\-fake-super\fP. If you wish a local copy to enable this -option just for the source files, combine \fB\-\-fake-super\fP with \fB\-M\-\-super\fP. +files, specify \fB\-M\-\-fake\-super\fP. If you wish a local copy to enable this +option just for the source files, combine \fB\-\-fake\-super\fP with \fB\-M\-\-super\fP. .IP -This option is overridden by both \fB\-\-super\fP and \fB\-\-no-super\fP. +This option is overridden by both \fB\-\-super\fP and \fB\-\-no\-super\fP. .IP See also the \fBfake\ super\fP setting in the daemon's rsyncd.conf file. .IP "\fB\-\-sparse\fP, \fB\-S\fP" Try to handle sparse files efficiently so they take up less space on the -destination. If combined with \fB\-\-inplace\fP the file created might -not end up with sparse blocks with some combinations of kernel version -and/or filesystem type. If \fB\-\-whole-file\fP is in effect (e.g. for a -local copy) then it will always work because rsync truncates the file prior -to writing out the updated version. -.IP -Note that versions of rsync older than 3.1.3 will reject the combination of +destination. This relies on the destination filesystem supporting sparse +files, that is, files where some parts of the file don't have corresponding +disk blocks allocated for them because they contain all zero bytes. +.IP +If combined with \fB\-\-inplace\fP the file created may not end up with +sparse blocks (depending on the filesystem type and kernel version), +unless the \fB\-\-whole\-file\fP option is also in effect. Note that versions +of rsync older than 3.1.3 (January 2018) will reject the combination of \fB\-\-sparse\fP and \fB\-\-inplace\fP. .IP "\fB\-\-preallocate\fP" This tells the receiver to allocate each destination file to its eventual @@ -1815,42 +2037,44 @@ If combined with \fB\-\-sparse\fP, the file will only have sparse blocks (as opposed to allocated sequences of null bytes) if the kernel version and filesystem type support creating holes in the allocated data. -.IP "\fB\-\-dry-run\fP, \fB\-n\fP" -This makes rsync perform a trial run that doesn't make any changes (and -produces mostly the same output as a real run). It is most commonly used +.IP "\fB\-\-dry\-run\fP, \fB\-n\fP" +This makes rsync perform a trial run that doesn't make any changes, and +produces mostly the same output as a real run. It is most commonly used in combination with the \fB\-\-verbose\fP (\fB\-v\fP) and/or -\fB\-\-itemize-changes\fP (\fB\-i\fP) options to see what an rsync command is +\fB\-\-itemize\-changes\fP (\fB\-i\fP) options to see what an rsync command is going to do before one actually runs it. .IP -The output of \fB\-\-itemize-changes\fP is supposed to be exactly the -same on a dry run and a subsequent real run (barring intentional trickery +The output of \fB\-\-itemize\-changes\fP is supposed to be exactly the +same on a dry run and a subsequent real run (barring external changes to +the source or destination and system call failures); if it isn't, that's a bug. Other output should be mostly unchanged, but may differ in some areas. Notably, a dry run does not send the actual data for file transfers, so \fB\-\-progress\fP has no effect, the "bytes sent", "bytes received", "literal data", and "matched data" statistics are too small, and the "speedup" value is equivalent to a run where no file transfers were needed. -.IP "\fB\-\-whole-file\fP, \fB\-W\fP" -This option disables rsync's delta-transfer algorithm, which causes all -transferred files to be sent whole. The transfer may be faster if this +.IP "\fB\-\-whole\-file\fP, \fB\-W\fP" +This option disables rsync's delta-transfer algorithm, causing the whole of +all transferred files to be sent. The transfer may be faster if this option is used when the bandwidth between the source and destination machines is higher than the bandwidth to disk (especially when the "disk" is actually a networked filesystem). This is the default when both the source and destination are specified as local paths, but only if no batch-writing option is in effect. -.IP "\fB\-\-no-whole-file\fP, \fB\-\-no-W\fP" +.IP "\fB\-\-no\-whole\-file\fP, \fB\-\-no\-W\fP" Disable whole-file updating when it is enabled by default for a local transfer. This usually slows rsync down, but it can be useful if you are trying to minimize the writes to the destination file (if combined with \fB\-\-inplace\fP) or for testing the checksum-based update algorithm. .IP -See also the \fB\-\-whole-file\fP option. -.IP "\fB\-\-checksum-choice=STR\fP, \fB\-\-cc=STR\fP" +See also the \fB\-\-whole\-file\fP option. +.IP "\fB\-\-checksum\-choice=STR\fP, \fB\-\-cc=STR\fP" This option overrides the checksum algorithms. If one algorithm name is -specified, it is used for both the transfer checksums and (assuming -\fB\-\-checksum\fP is specified) the pre-transfer checksums. If two +specified, it is used for both the transfer checksums and the pre-transfer +checksums (note that the pre-transfer checksum is only performed if +\fB\-\-checksum\fP is specified). If two comma-separated names are supplied, the first name affects the transfer -checksums, and the second name affects the pre-transfer checksums (\fB\-c\fP). +checksums, and the second name affects the pre-transfer checksums. .IP The checksum options that you may be able to use are: .IP @@ -1876,7 +2100,7 @@ Run \fBrsync\ \-\-version\fP to see the default checksum list compiled into your version (which may differ from the list above). .IP -If "none" is specified for the first (or only) name, the \fB\-\-whole-file\fP +If "none" is specified for the first (or only) name, the \fB\-\-whole\-file\fP option is forced on and no checksum verification is performed on the transferred data. If "none" is specified for the second (or only) name, the \fB\-\-checksum\fP option cannot be used. @@ -1884,9 +2108,11 @@ The "auto" option is the default, where rsync bases its algorithm choice on a negotiation between the client and the server as follows: .IP -When both sides of the transfer are at least 3.2.0, rsync chooses the first -algorithm in the client's list of choices that is also in the server's list -of choices. If no common checksum choice is found, rsync exits with +When both sides of the transfer are at least 3.2.0 (released June 2020), +each side chooses its own +most-preferred algorithm that also appears in the peer's list. Both sides +order their lists strongest-first, so they converge on the strongest mutual +choice. If no common checksum choice is found, rsync exits with an error. If the remote rsync is too old to support checksum negotiation, a value is chosen based on the protocol version (which chooses between MD5 and various flavors of MD4 based on protocol age). @@ -1901,8 +2127,8 @@ and it discards "auto" and all unknown checksum names. A list with only invalid names results in a failed negotiation. .IP -The use of the \fB\-\-checksum-choice\fP option overrides this environment list. -.IP "\fB\-\-one-file-system\fP, \fB\-x\fP" +The use of the \fB\-\-checksum\-choice\fP option overrides this environment list. +.IP "\fB\-\-one\-file\-system\fP, \fB\-x\fP" This tells rsync to avoid crossing a filesystem boundary when recursing. This does not limit the user's ability to specify items to copy from multiple filesystems, just rsync's recursion through the hierarchy of each @@ -1915,30 +2141,30 @@ encounters (using the attributes of the mounted directory because those of the underlying mount-point directory are inaccessible). .IP -If rsync has been told to collapse symlinks (via \fB\-\-copy-links\fP or -\fB\-\-copy-unsafe-links\fP), a symlink to a directory on another device +If rsync has been told to collapse symlinks (via \fB\-\-copy\-links\fP or +\fB\-\-copy\-unsafe\-links\fP), a symlink to a directory on another device is treated like a mount-point. Symlinks to non-directories are unaffected by this option. -.IP "\fB\-\-ignore-non-existing\fP, \fB\-\-existing\fP" +.IP "\fB\-\-ignore\-non\-existing\fP, \fB\-\-existing\fP" This tells rsync to skip creating files (including directories) that do not exist yet on the destination. If this option is combined with the -\fB\-\-ignore-existing\fP option, no files will be updated (which can be +\fB\-\-ignore\-existing\fP option, no files will be updated (which can be useful if all you want to do is delete extraneous files). .IP This option is a TRANSFER RULE, so don't expect any exclude side effects. -.IP "\fB\-\-ignore-existing\fP" +.IP "\fB\-\-ignore\-existing\fP" This tells rsync to skip updating files that already exist on the destination (this does \fInot\fP ignore existing directories, or nothing would -get done). See also \fB\-\-ignore-non-existing\fP. +get done). See also \fB\-\-ignore\-non\-existing\fP. .IP This option is a TRANSFER RULE, so don't expect any exclude side effects. .IP This option can be useful for those doing backups using the -\fB\-\-link-dest\fP option when they need to continue a backup run that -got interrupted. Since a \fB\-\-link-dest\fP run is copied into a new -directory hierarchy (when it is used properly), using [\fB\-\-ignore-existing\fP +\fB\-\-link\-dest\fP option when they need to continue a backup run that +got interrupted. Since a \fB\-\-link\-dest\fP run is copied into a new +directory hierarchy (when it is used properly), using [\fB\-\-ignore\-existing\fP will ensure that the already-handled files don't get tweaked (which avoids a change in permissions on the hard-linked files). This does mean that this option is only looking at the existing files in the destination @@ -1950,7 +2176,7 @@ "attr change", or "uptodate". Using \fB\-\-info=skip1\fP (which is also implied by 2 \fB\-v\fP options) outputs the exists message without the INFO suffix. -.IP "\fB\-\-remove-source-files\fP" +.IP "\fB\-\-remove\-source\-files\fP" This tells rsync to remove from the sending side the files (meaning non-directories) that are a part of the transfer and have been successfully duplicated on the receiving side. @@ -1966,46 +2192,47 @@ "foo" when it is done, and then use the option \fB\-\-exclude='*.new'\fP for the rsync transfer). .IP -Starting with 3.1.0, rsync will skip the sender-side removal (and output an -error) if the file's size or modify time has not stayed unchanged. +Rsync will skip the sender-side removal, and output an +error, if the file's size or modify time has not stayed unchanged. .IP -Starting with 3.2.6, a local rsync copy will ensure that the sender does +If the copy is local, rsync will ensure that the sender does not remove a file the receiver just verified, such as when the user accidentally makes the source and destination directory the same path. .IP "\fB\-\-delete\fP" -This tells rsync to delete extraneous files from the receiving side (ones -that aren't on the sending side), but only for the directories that are +This tells rsync to delete extraneous files from the receiving side (those +that don't exist on the sending side), but only for the directories that are being synchronized. You must have asked rsync to send the whole directory -(e.g. "\fBdir\fP" or "\fBdir/\fP") without using a wildcard for the directory's -contents (e.g. "\fBdir/*\fP") since the wildcard is expanded by the shell and +(e.g. "\fBdir\fP" or "\fBdir/\fP"), rather than asking for all the files in a +directory via a wildcard (e.g. "\fBdir/*\fP"), since the wildcard is +expanded by the shell and rsync thus gets a request to transfer individual files, not the files' parent directory. Files that are excluded from the transfer are also -excluded from being deleted unless you use the \fB\-\-delete-excluded\fP +excluded from being deleted unless you use the \fB\-\-delete\-excluded\fP option or mark the rules as only matching on the sending side (see the include/exclude modifiers in the FILTER RULES section). .IP -Prior to rsync 2.6.7, this option would have no effect unless -\fB\-\-recursive\fP was enabled. Beginning with 2.6.7, deletions will -also occur when \fB\-\-dirs\fP (\fB\-d\fP) is enabled, but only for -directories whose contents are being copied. +This option has no effect unless either \fB\-\-recursive\fP or +\fB\-\-dirs\fP (\fB\-d\fP) is enabled. In the latter case, deletions only +occur in directories whose contents are being copied. .IP This option can be dangerous if used incorrectly! It is a very good idea to -first try a run using the \fB\-\-dry-run\fP (\fB\-n\fP) option to see what +first try a run using the \fB\-\-dry\-run\fP (\fB\-n\fP) option to see what files are going to be deleted. .IP If the sending side detects any I/O errors, then the deletion of any files at the destination will be automatically disabled. This is to prevent temporary filesystem failures (such as NFS errors) on the sending side from causing a massive deletion of files on the destination. You can override -this with the \fB\-\-ignore-errors\fP option. +this with the \fB\-\-ignore\-errors\fP option. .IP -The \fB\-\-delete\fP option may be combined with one of the \-\-delete-WHEN options -without conflict, as well as \fB\-\-delete-excluded\fP. However, if none -of the \fB\-\-delete-WHEN\fP options are specified, rsync will choose the -\fB\-\-delete-during\fP algorithm when talking to rsync 3.0.0 or newer, -or the \fB\-\-delete-before\fP algorithm when talking to an older rsync. -See also \fB\-\-delete-delay\fP and \fB\-\-delete-after\fP. -.IP "\fB\-\-delete-before\fP" +The \fB\-\-delete\fP option may be combined with one of the \-\-delete\-WHEN options +without conflict, as well as \fB\-\-delete\-excluded\fP. However, if none +of the \fB\-\-delete\-WHEN\fP options are specified, rsync will choose the +\fB\-\-delete\-during\fP algorithm when talking to rsync 3.0.0 (March +2008) or newer, +or the \fB\-\-delete\-before\fP algorithm when talking to an older rsync. +See also \fB\-\-delete\-delay\fP and \fB\-\-delete\-after\fP. +.IP "\fB\-\-delete\-before\fP" Request that the file-deletions on the receiving side be done before the transfer starts. See \fB\-\-delete\fP (which is implied) for more details on file-deletion. @@ -2017,29 +2244,29 @@ \fB\-\-timeout\fP was specified). It also forces rsync to use the old, non-incremental recursion algorithm that requires rsync to scan all the files in the transfer into memory at once (see \fB\-\-recursive\fP). -.IP "\fB\-\-delete-during\fP, \fB\-\-del\fP" +.IP "\fB\-\-delete\-during\fP, \fB\-\-del\fP" Request that the file-deletions on the receiving side be done incrementally as the transfer happens. The per-directory delete scan is done right before each directory is checked for updates, so it behaves like a more -efficient \fB\-\-delete-before\fP, including doing the deletions prior to -any per-directory filter files being updated. This option was first added -in rsync version 2.6.4. See \fB\-\-delete\fP (which is implied) for more +efficient \fB\-\-delete\-before\fP, including doing the deletions prior to +any per-directory filter files being updated. +See \fB\-\-delete\fP (which is implied) for more details on file-deletion. -.IP "\fB\-\-delete-delay\fP" +.IP "\fB\-\-delete\-delay\fP" Request that the file-deletions on the receiving side be computed during -the transfer (like \fB\-\-delete-during\fP), and then removed after the +the transfer (like \fB\-\-delete\-during\fP), and then removed after the transfer completes. This is useful when combined with -\fB\-\-delay-updates\fP and/or \fB\-\-fuzzy\fP, and is more efficient -than using \fB\-\-delete-after\fP (but can behave differently, since -\fB\-\-delete-after\fP computes the deletions in a separate pass after -all updates are done). If the number of removed files overflows an +\fB\-\-delay\-updates\fP and/or \fB\-\-fuzzy\fP, and is more efficient +than using \fB\-\-delete\-after\fP, but can behave differently, since +\fB\-\-delete\-after\fP computes the deletions in a separate pass after +all updates are done. If the number of removed files overflows an internal buffer, a temporary file will be created on the receiving side to hold the names (it is removed while open, so you shouldn't see it during the transfer). If the creation of the temporary file fails, rsync will try -to fall back to using \fB\-\-delete-after\fP (which it cannot do if +to fall back to using \fB\-\-delete\-after\fP (which it cannot do if \fB\-\-recursive\fP is doing an incremental scan). See \fB\-\-delete\fP (which is implied) for more details on file-deletion. -.IP "\fB\-\-delete-after\fP" +.IP "\fB\-\-delete\-after\fP" Request that the file-deletions on the receiving side be done after the transfer has completed. This is useful if you are sending new per-directory merge files as a part of the transfer and you want their @@ -2049,9 +2276,9 @@ (see \fB\-\-recursive\fP). See \fB\-\-delete\fP (which is implied) for more details on file-deletion. .IP -See also the \fB\-\-delete-delay\fP option that might be a faster choice +See also the \fB\-\-delete\-delay\fP option that might be a faster choice for those that just want the deletions to occur at the end of the transfer. -.IP "\fB\-\-delete-excluded\fP" +.IP "\fB\-\-delete\-excluded\fP" This option turns any unqualified exclude/include rules into server-side rules that do not affect the receiver's deletions. .IP @@ -2070,16 +2297,16 @@ .IP See the FILTER RULES section for more information. See \fB\-\-delete\fP (which is implied) for more details on deletion. -.IP "\fB\-\-ignore-missing-args\fP" +.IP "\fB\-\-ignore\-missing\-args\fP" When rsync is first processing the explicitly requested source files (e.g. -command-line arguments or \fB\-\-files-from\fP entries), it is normally +command-line arguments or \fB\-\-files\-from\fP entries), it is normally an error if the file cannot be found. This option suppresses that error, and does not try to transfer the file. This does not affect subsequent vanished-file errors if a file was initially found to be present and later is no longer there. -.IP "\fB\-\-delete-missing-args\fP" +.IP "\fB\-\-delete\-missing\-args\fP" This option takes the behavior of the (implied) -\fB\-\-ignore-missing-args\fP option a step farther: each missing arg +\fB\-\-ignore\-missing\-args\fP option a step farther: each missing argument will become a deletion request of the corresponding destination file on the receiving side (should it exist). If the destination file is a non-empty directory, it will only be successfully deleted if \fB\-\-force\fP or @@ -2087,36 +2314,32 @@ independent of any other type of delete processing. .IP The missing source files are represented by special file-list entries which -display as a "\fB*missing\fP" entry in the \fB\-\-list-only\fP output. -.IP "\fB\-\-ignore-errors\fP" +display as a "\fB*missing\fP" entry in the \fB\-\-list\-only\fP output. +.IP "\fB\-\-ignore\-errors\fP" Tells \fB\-\-delete\fP to go ahead and delete files even when there are I/O errors. .IP "\fB\-\-force\fP" This option tells rsync to delete a non-empty directory when it is to be replaced by a non-directory. This is only relevant if deletions are not active (see \fB\-\-delete\fP for details). -.IP -Note for older rsync versions: \fB\-\-force\fP used to still be required when -using \fB\-\-delete-after\fP, and it used to be non-functional unless the -\fB\-\-recursive\fP option was also enabled. -.IP "\fB\-\-max-delete=NUM\fP" +.IP "\fB\-\-max\-delete=NUM\fP" This tells rsync not to delete more than NUM files or directories. If that limit is exceeded, all further deletions are skipped through the end of the transfer. At the end, rsync outputs a warning (including a count of the skipped deletions) and exits with an error code of 25 (unless some more important error condition also occurred). .IP -Beginning with version 3.0.0, you may specify \fB\-\-max-delete=0\fP to be warned +You may specify \fB\-\-max\-delete=0\fP to be warned about any extraneous files in the destination without removing any of them. -Older clients interpreted this as "unlimited", so if you don't know what -version the client is, you can use the less obvious \fB\-\-max-delete=\-1\fP as a -backward-compatible way to specify that no deletions be allowed (though -really old versions didn't warn when the limit was exceeded). -.IP "\fB\-\-max-size=SIZE\fP" +CAUTION: a client rsync older than 3.0.0 (March 2008) treats +\fB\-\-max\-delete=0\fP as unlimited, so use \fB\-\-max\-delete=\-1\fP if the command +might be run by such an old rsync. (A 3.0.0 or newer client protects an +older remote by forwarding the option as \fB\-\-max\-delete=\-1\fP.) +.IP "\fB\-\-max\-size=SIZE\fP" This tells rsync to avoid transferring any file that is larger than the specified SIZE. A numeric value can be suffixed with a string to indicate -the numeric units or left unqualified to specify bytes. Feel free to use a -fractional value along with the units, such as \fB\-\-max-size=1.5m\fP. +the numeric units or left unqualified to specify bytes, and the numeric +value can have a fractional part, such as \fB\-\-max\-size=1.5m\fP. .IP This option is a TRANSFER RULE, so don't expect any exclude side effects. @@ -2132,52 +2355,51 @@ byte in the indicated direction. The largest possible value is usually \fB8192P-1\fP. .IP -Examples: \fB\-\-max-size=1.5mb-1\fP is 1499999 bytes, and \fB\-\-max-size=2g+1\fP is +Examples: \fB\-\-max\-size=1.5mb-1\fP is 1499999 bytes, and \fB\-\-max\-size=2g+1\fP is 2147483649 bytes. -.IP -Note that rsync versions prior to 3.1.0 did not allow \fB\-\-max-size=0\fP. -.IP "\fB\-\-min-size=SIZE\fP" +.IP "\fB\-\-min\-size=SIZE\fP" This tells rsync to avoid transferring any file that is smaller than the specified SIZE, which can help in not transferring small, junk files. See -the \fB\-\-max-size\fP option for a description of SIZE and other info. -.IP -Note that rsync versions prior to 3.1.0 did not allow \fB\-\-min-size=0\fP. -.IP "\fB\-\-max-alloc=SIZE\fP" +the \fB\-\-max\-size\fP option for a description of SIZE and other info. +.IP "\fB\-\-max\-alloc=SIZE\fP" By default rsync limits an individual malloc/realloc to about 1GB in size. For most people this limit works just fine and prevents a protocol error causing rsync to request massive amounts of memory. However, if you have many millions of files in a transfer, a large amount of server memory, and you don't want to split up your transfer into multiple parts, you can -increase the per-allocation limit to something larger and rsync will +increase the per-allocation limit to something larger, allowing rsync to consume more memory. .IP Keep in mind that this is not a limit on the total size of allocated memory. It is a sanity-check value for each individual allocation. .IP -See the \fB\-\-max-size\fP option for a description of how SIZE can be +See the \fB\-\-max\-size\fP option for a description of how SIZE can be specified. The default suffix if none is given is bytes. .IP -Beginning in 3.2.7, a value of 0 is an easy way to specify SIZE_MAX (the -largest limit possible). +Beginning in 3.2.7, a value of 0 was an easy way to specify SIZE_MAX (the +largest limit possible). However, beginning with 3.5.0, a value of 0 is +rejected as invalid for security reasons (a 0-byte cap could be used to +disable the allocation limit, which could lead to a denial-of-service via +memory exhaustion). Use an explicit very large value if you want a very +high limit. .IP You can set a default value using the environment variable \fBRSYNC_MAX_ALLOC\fP using the same SIZE values as supported by this -option. If the remote rsync doesn't understand the \fB\-\-max-alloc\fP option, -you can override an environmental value by specifying \fB\-\-max-alloc=1g\fP, +option. If the remote rsync doesn't understand the \fB\-\-max\-alloc\fP option, +you can override an environmental value by specifying \fB\-\-max\-alloc=1g\fP, which will make rsync avoid sending the option to the remote side (because "1G" is the default). -.IP "\fB\-\-block-size=SIZE\fP, \fB\-B\fP" +.IP "\fB\-\-block\-size=SIZE\fP, \fB\-B\fP" This forces the block size used in rsync's delta-transfer algorithm to a fixed value. It is normally selected based on the size of each file being updated. See the technical report for details. .IP -Beginning in 3.2.3 the SIZE can be specified with a suffix as detailed in -the \fB\-\-max-size\fP option. Older versions only accepted a byte count. +The SIZE can be specified with a suffix as detailed in +the \fB\-\-max\-size\fP option. .IP "\fB\-\-rsh=COMMAND\fP, \fB\-e\fP" This option allows you to choose an alternative remote shell program to use for communication between the local and remote copies of rsync. Typically, -rsync is configured to use ssh by default, but you may prefer to use rsh on -a local network. +rsync is configured to use ssh by default. .IP If this option is used with \fB[user@]host::module/path\fP, then the remote shell \fICOMMAND\fP will be used to run an rsync daemon on the remote host, and @@ -2186,18 +2408,19 @@ remote host. See the USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION section above. .IP -Beginning with rsync 3.2.0, the \fBRSYNC_PORT\fP environment variable will +The \fBRSYNC_PORT\fP environment variable will be set when a daemon connection is being made via a remote-shell connection. It is set to 0 if the default daemon port is being assumed, or it is set to the value of the rsync port that was specified via either the -\fB\-\-port\fP option or a non-empty port value in an \fBrsync://\fP URL. -This allows the script to discern if a non-default port is being requested, -allowing for things such as an SSL or stunnel helper script to connect to a -default or alternate port. +\fB\-\-port\fP option or a non-empty port value in an \fBrsync://\fP URL (for +example \fBrsync://host.example.com:984\fP). This is useful if the program +being run is not actually rsync but rather something such as an SSL or +stunnel helper script; the script can use \fBRSYNC_PORT\fP to determine whether +it should connect to a default or alternate port. .IP Command-line arguments are permitted in COMMAND provided that COMMAND is presented to rsync as a single argument. You must use spaces (not tabs or -other whitespace) to separate the command and args from each other, and you +other whitespace) to separate the command and arguments from each other, and you can use single- and/or double-quotes to preserve spaces in an argument (but not backslashes). Note that doubling a single-quote inside a single-quoted string gives you a single-quote; likewise for double-quotes (though you @@ -2217,12 +2440,12 @@ You can also choose the remote shell program using the \fBRSYNC_RSH\fP environment variable, which accepts the same range of values as \fB\-e\fP. .IP -See also the \fB\-\-blocking-io\fP option which is affected by this +See also the \fB\-\-blocking\-io\fP option which is affected by this option. -.IP "\fB\-\-rsync-path=PROGRAM\fP" +.IP "\fB\-\-rsync\-path=PROGRAM\fP" Use this to specify what program is to be run on the remote machine to start-up rsync. Often used when rsync is not in the default remote-shell's -path (e.g. \fB\-\-rsync-path=/usr/local/bin/rsync\fP). Note that PROGRAM is run +path (e.g. \fB\-\-rsync\-path=/usr/local/bin/rsync\fP). Note that PROGRAM is run with the help of a shell, so it can be any program, script, or command sequence you'd care to run, so long as it does not corrupt the standard-in & standard-out that rsync is using to communicate. @@ -2235,10 +2458,10 @@ rsync -avR --rsync-path="cd /a/b && rsync" host:c/d /e/ .fi .RE -.IP "\fB\-\-remote-option=OPTION\fP, \fB\-M\fP" +.IP "\fB\-\-remote\-option=OPTION\fP, \fB\-M\fP" This option is used for more advanced situations where you want certain effects to be limited to one side of the transfer only. For instance, if -you want to pass \fB\-\-log-file=FILE\fP and \fB\-\-fake-super\fP to +you want to pass \fB\-\-log\-file=FILE\fP and \fB\-\-fake\-super\fP to the remote system, specify it like this: .RS 4 .IP @@ -2263,18 +2486,18 @@ .IP Note that you should use a separate \fB\-M\fP option for each remote option you want to pass. On older rsync versions, the presence of any spaces in the -remote-option arg could cause it to be split into separate remote args, but -this requires the use of \fB\-\-old-args\fP in a modern rsync. +remote-option argument could cause it to be split into separate remote arguments, but +this requires the use of \fB\-\-old\-args\fP in a modern rsync. .IP When performing a local transfer, the "local" side is the sender and the "remote" side is the receiver. .IP Note some versions of the popt option-parsing library have a bug in them -that prevents you from using an adjacent arg with an equal in it next to a -short option letter (e.g. \fB\-M\-\-log-file=/tmp/foo\fP). If this bug affects +that prevents you from using an adjacent argument with an equal in it next to a +short option letter (e.g. \fB\-M\-\-log\-file=/tmp/foo\fP). If this bug affects your version of popt, you can use the version of popt that is included with rsync. -.IP "\fB\-\-cvs-exclude\fP, \fB\-C\fP" +.IP "\fB\-\-cvs\-exclude\fP, \fB\-C\fP" This is a useful shorthand for excluding a broad range of files that you often don't want to transfer between systems. It uses a similar algorithm to CVS to determine if a file should be ignored. @@ -2350,8 +2573,8 @@ You may use as many \fB\-\-filter\fP options on the command line as you like to build up the list of files to exclude. If the filter contains whitespace, be sure to quote it so that the shell gives the rule to rsync as a single -argument. The text below also mentions that you can use an underscore to -replace the space that separates a rule from its arg. +argument. You can use an underscore instead of a space to separate a rule +from its argument. .IP See the FILTER RULES section for detailed information on this option. .IP "\fB\-F\fP" @@ -2385,7 +2608,7 @@ of normal filter rules. This is equivalent to specifying \fB\-f'\-\ PATTERN'\fP. .IP See the FILTER RULES section for detailed information on this option. -.IP "\fB\-\-exclude-from=FILE\fP" +.IP "\fB\-\-exclude\-from=FILE\fP" This option is related to the \fB\-\-exclude\fP option, but it specifies a FILE that contains exclude patterns (one per line). Blank lines in the file are ignored, as are whole-line comments that start with '\fB;\fP' or '\fB#\fP' @@ -2405,7 +2628,7 @@ of normal filter rules. This is equivalent to specifying \fB\-f'+\ PATTERN'\fP. .IP See the FILTER RULES section for detailed information on this option. -.IP "\fB\-\-include-from=FILE\fP" +.IP "\fB\-\-include\-from=FILE\fP" This option is related to the \fB\-\-include\fP option, but it specifies a FILE that contains include patterns (one per line). Blank lines in the file are ignored, as are whole-line comments that start with '\fB;\fP' or '\fB#\fP' @@ -2419,9 +2642,9 @@ before adding any further rules. .IP If \fIFILE\fP is '\fB\-\fP', the list will be read from standard input. -.IP "\fB\-\-files-from=FILE\fP" -Using this option allows you to specify the exact list of files to transfer -(as read from the specified FILE or '\fB\-\fP' for standard input). It also +.IP "\fB\-\-files\-from=FILE\fP" +Using this option tells rsync to read the exact list of files to transfer +from FILE (or standard input if FILE is '\fB\-\fP'). It also tweaks the default behavior of rsync to make transferring just the specified files and directories easier: .IP @@ -2429,24 +2652,28 @@ .IP o The \fB\-\-relative\fP (\fB\-R\fP) option is implied, which preserves the path information that is specified for each item in the file (use -\fB\-\-no-relative\fP or \fB\-\-no-R\fP if you want to turn that off). +\fB\-\-no\-relative\fP or \fB\-\-no\-R\fP if you want to turn that off). .IP o The \fB\-\-dirs\fP (\fB\-d\fP) option is implied, which will create directories specified in the list on the destination rather than noisily -skipping them (use \fB\-\-no-dirs\fP or \fB\-\-no-d\fP if you want to turn that off). +skipping them (use \fB\-\-no\-dirs\fP or \fB\-\-no\-d\fP if you want to turn that off). .IP o The \fB\-\-archive\fP (\fB\-a\fP) option's behavior does not imply \fB\-\-recursive\fP (\fB\-r\fP), so specify it explicitly, if you want it. .IP o These side-effects change the default state of rsync, so the position of -the \fB\-\-files-from\fP option on the command-line has no bearing on how other +the \fB\-\-files\-from\fP option on the command-line has no bearing on how other options are parsed (e.g. \fB\-a\fP works the same before or after -\fB\-\-files-from\fP, as does \fB\-\-no-R\fP and all other options). +\fB\-\-files\-from\fP, as does \fB\-\-no\-R\fP and all other options). .RE .IP The filenames that are read from the FILE are all relative to the source -dir\ \-\- any leading slashes are removed and no ".." references are allowed -to go higher than the source dir. For example, take this command: +directory: any leading slash is removed, and ".." components are resolved away so +an entry cannot rise above the source directory\ \-\- e.g. "../foo" is taken as "foo" +within the source directory. An entry that still contains an active ".." after +that resolution (one that cannot be collapsed) is rejected with an error. +Blank entries are ignored, as are whole-entry comments that start with '\fB;\fP' +or '\fB#\fP'. For example, take this command: .RS 4 .IP .nf @@ -2457,16 +2684,16 @@ If /tmp/foo contains the string "bin" (or even "/bin"), the /usr/bin directory will be created as /backup/bin on the remote host. If it contains "bin/" (note the trailing slash), the immediate contents of the -directory would also be sent (without needing to be explicitly mentioned in -the file\ \-\- this began in version 2.6.4). In both cases, if the -\fB\-r\fP option was enabled, that dir's entire hierarchy would also be +directory would also be sent, without needing to be explicitly mentioned in +the file. In both cases, if the +\fB\-r\fP option was enabled, that directory's entire hierarchy would also be transferred (keep in mind that \fB\-r\fP needs to be specified -explicitly with \fB\-\-files-from\fP, since it is not implied by \fB\-a\fP. +explicitly with \fB\-\-files\-from\fP, since it is not implied by \fB\-a\fP. Also note that the effect of the (enabled by default) \fB\-r\fP option is to duplicate only the path info that is read from the file\ \-\- it does not force the duplication of the source-spec path (/usr in this case). .IP -In addition, the \fB\-\-files-from\fP file can be read from the remote host +In addition, the \fB\-\-files\-from\fP file can be read from the remote host instead of the local host if you specify a "host:" in front of the file (the host must match one end of the transfer). As a short-cut, you can specify just a prefix of ":" to mean "use the remote end of the transfer". @@ -2481,37 +2708,37 @@ This would copy all the files specified in the /path/file-list file that was located on the remote "src" host. .IP -If the \fB\-\-iconv\fP and \fB\-\-secluded-args\fP options are specified -and the \fB\-\-files-from\fP filenames are being sent from one host to another, +If the \fB\-\-iconv\fP and \fB\-\-secluded\-args\fP options are specified +and the \fB\-\-files\-from\fP filenames are being sent from one host to another, the filenames will be translated from the sending host's charset to the receiving host's charset. .IP -NOTE: sorting the list of files in the \fB\-\-files-from\fP input helps rsync to +NOTE: sorting the list of files in the \fB\-\-files\-from\fP input helps rsync to be more efficient, as it will avoid re-visiting the path elements that are shared between adjacent entries. If the input is not sorted, some path elements (implied directories) may end up being scanned multiple times, and rsync will eventually unduplicate them after they get turned into file-list elements. .IP "\fB\-\-from0\fP, \fB\-0\fP" -This tells rsync that the rules/filenames it reads from a file are +This tells rsync that the rules or filenames it reads from a file are terminated by a null ('\\0') character, not a NL, CR, or CR+LF. This -affects \fB\-\-exclude-from\fP, \fB\-\-include-from\fP, -\fB\-\-files-from\fP, and any merged files specified in a -\fB\-\-filter\fP rule. It does not affect \fB\-\-cvs-exclude\fP (since +affects \fB\-\-exclude\-from\fP, \fB\-\-include\-from\fP, +\fB\-\-files\-from\fP, and any merged files specified in a +\fB\-\-filter\fP rule. It does not affect \fB\-\-cvs\-exclude\fP (since all names read from a .cvsignore file are split on whitespace). -.IP "\fB\-\-old-args\fP" -This option tells rsync to stop trying to protect the arg values on the +.IP "\fB\-\-old\-args\fP" +This option tells rsync not to protect the argument values sent to the remote side from unintended word-splitting or other misinterpretation. -It also allows the client to treat an empty arg as a "." instead of +It also allows the client to treat an empty argument as a "." instead of generating an error. .IP The default in a modern rsync is for "shell-active" characters (including -spaces) to be backslash-escaped in the args that are sent to the remote +spaces) to be backslash-escaped in the arguments that are sent to the remote shell. The wildcard characters \fB*\fP, \fB?\fP, \fB[\fP, & \fB]\fP are not escaped in -filename args (allowing them to expand into multiple filenames) while being -protected in option args, such as \fB\-\-usermap\fP. +filename arguments (allowing them to expand into multiple filenames) while being +protected in option arguments, such as \fB\-\-usermap\fP. .IP -If you have a script that wants to use old-style arg splitting in its +If you have a script that wants to use old-style argument splitting in its filenames, specify this option once. If the remote shell has a problem with any backslash escapes at all, specify this option twice. .IP @@ -2520,65 +2747,66 @@ setting. If it has the value "2" (or more), rsync will default to a repeated-option setting. If it is "0", you'll get the default escaping behavior. The environment is always overridden by manually specified -positive or negative options (the negative is \fB\-\-no-old-args\fP). +positive or negative options (the negative is \fB\-\-no\-old\-args\fP). .IP -Note that this option also disables the extra safety check added in 3.2.5 +Note that this option also disables the extra safety check that ensures that a remote sender isn't including extra top-level items in the file-list that you didn't request. This side-effect is necessary because we can't know for sure what names to expect when the remote shell -is interpreting the args. +is interpreting the arguments. .IP -This option conflicts with the \fB\-\-secluded-args\fP option. -.IP "\fB\-\-secluded-args\fP, \fB\-s\fP" +This option conflicts with the \fB\-\-secluded\-args\fP option. +.IP "\fB\-\-secluded\-args\fP, \fB\-s\fP" This option sends all filenames and most options to the remote rsync via -the protocol (not the remote shell command line) which avoids letting the -remote shell modify them. Wildcards are expanded on the remote host by -rsync instead of a shell. -.IP -This is similar to the default backslash-escaping of args that was added -in 3.2.4 (see \fB\-\-old-args\fP) in that it prevents things like space -splitting and unwanted special-character side-effects. However, it has the -drawbacks of being incompatible with older rsync versions (prior to 3.0.0) -and of being refused by restricted shells that want to be able to inspect -all the option values for safety. -.IP +the protocol (rather than via the remote shell command line), which avoids +the possibility of the remote shell modifying them. +Wildcards are expanded on the remote host by rsync instead of a shell. +.IP +Without this option, rsync does backslash-escaping of arguments to prevent +things like space splitting and unwanted special-character side-effects, +which is normally sufficient to avoid unwanted modifications by the shell. This option is useful for those times that you need the argument's character set to be converted for the remote host, if the remote shell is -incompatible with the default backslash-escpaing method, or there is some +incompatible with the default backslash-escaping method, or there is some other reason that you want the majority of the options and arguments to bypass the command-line of the remote shell. .IP -If you combine this option with \fB\-\-iconv\fP, the args related to the +This option is incompatible with remote rsync versions prior to 3.0.0 +(March 2008). It also has the drawback of being refused by restricted +shells that want to be able to inspect all the option values for safety. +.IP +If you combine this option with \fB\-\-iconv\fP, the arguments related to the remote side will be translated from the local to the remote character-set. The translation happens before wild-cards are expanded. See also the -\fB\-\-files-from\fP option. +\fB\-\-files\-from\fP option. .IP You may also control this setting via the \fBRSYNC_PROTECT_ARGS\fP environment variable. If it has a non-zero value, this setting will be enabled by default, otherwise it will be disabled by default. Either state is overridden by a manually specified positive or negative version of this -option (note that \fB\-\-no-s\fP and \fB\-\-no-secluded-args\fP are the negative +option (note that \fB\-\-no\-s\fP and \fB\-\-no\-secluded\-args\fP are the negative versions). This environment variable is also superseded by a non-zero -\fBRSYNC_OLD_ARGS\fP export. +\fBRSYNC_OLD_ARGS\fP environment variable. .IP -This option conflicts with the \fB\-\-old-args\fP option. +This option conflicts with the \fB\-\-old\-args\fP option. .IP -This option used to be called \fB\-\-protect-args\fP (before 3.2.6) and that +This option used to be called \fB\-\-protect\-args\fP (before 3.2.6, September +2022) and that older name can still be used (though specifying it as \fB\-s\fP is always the easiest and most compatible choice). -.IP "\fB\-\-trust-sender\fP" +.IP "\fB\-\-trust\-sender\fP" This option disables two extra validation checks that a local client performs on the file list generated by a remote sender. This option should only be used if you trust the sender to not put something malicious in the file list (something that could possibly be done via a modified rsync, a modified shell, or some other similar manipulation). .IP -Normally, the rsync client (as of version 3.2.5) runs two extra validation +Normally, the rsync client runs two extra validation checks when pulling files from a remote rsync: .IP .RS .IP o -It verifies that additional arg items didn't get added at the top of the +It verifies that additional argument items didn't get added at the top of the transfer. .IP o It verifies that none of the items in the file list are names that should @@ -2593,31 +2821,32 @@ Using a per-directory filter file reads filter rules that only the server knows about, so the filter checking is disabled. .IP o -Using the \fB\-\-old-args\fP option allows the sender to manipulate the -requested args, so the arg checking is disabled. +Using the \fB\-\-old\-args\fP option allows the sender to manipulate the +requested arguments, so the argument checking is disabled. .IP o Reading the files-from list from the server side means that the client -doesn't know the arg list, so the arg checking is disabled. +doesn't know the argument list, so the argument checking is disabled. .IP o -Using \fB\-\-read-batch\fP disables both checks since the batch file's +Using \fB\-\-read\-batch\fP disables both checks since the batch file's contents will have been verified when it was created. .RE .IP This option may help an under-powered client server if the extra pattern -matching is slowing things down on a huge transfer. It can also be used to -work around a currently-unknown bug in the verification logic for a transfer -from a trusted sender. +matching is slowing things down on a huge transfer. It could also be used +for a transfer from a trusted sender as a workaround if there appeared to +be a bug in the verification logic. .IP When using this option it is a good idea to specify a dedicated destination -directory, as discussed in the MULTI-HOST SECURITY section. -.IP "\fB\-\-copy-as=USER[:GROUP]\fP" +directory, as discussed in the SECURITY section. +.IP "\fB\-\-copy\-as=USER[:GROUP]\fP" This option instructs rsync to use the USER and (if specified after a colon) the GROUP for the copy operations. This only works if the user that is running rsync has the ability to change users. If the group is not specified then the user's default groups are used. .IP -This option can help to reduce the risk of an rsync being run as root into -or out of a directory that might have live changes happening to it and you +This option can help to reduce the risk in the case where rsync is being +run as root, copying into or out of a directory that might have live +changes happening to it, and you want to make sure that root-level read or write actions of system files are not possible. While you could alternatively run all of rsync as the specified user, sometimes you need the root-level host-access credentials @@ -2626,13 +2855,13 @@ .IP The option only affects one side of the transfer unless the transfer is local, in which case it affects both sides. Use the -\fB\-\-remote-option\fP to affect the remote side, such as -\fB\-M\-\-copy-as=joe\fP. For a local transfer, the lsh (or lsh.sh) support file +\fB\-\-remote\-option\fP to affect the remote side, such as +\fB\-M\-\-copy\-as=joe\fP. For a local transfer, the lsh (or lsh.sh) support file provides a local-shell helper script that can be used to allow a "localhost:" or "lh:" host-spec to be specified without needing to setup any remote shells, allowing you to specify remote options that affect the side of the transfer that is using the host-spec (and using hostname "lh" -avoids the overriding of the remote directory to the user's home dir). +avoids the overriding of the remote directory to the user's home directory). .IP For example, the following rsync writes the local files as user "joe": .RS 4 @@ -2647,21 +2876,21 @@ a timed exploit of the path to induce a change to a file that the joe user has no permissions to change. .IP -The following command does a local copy into the "dest/" dir as user "joe" -(assuming you've installed support/lsh into a dir on your $PATH): +The following command does a local copy into the "dest/" directory as user "joe" +(assuming you've installed support/lsh into a directory on your $PATH): .RS 4 .IP .nf sudo rsync -aive lsh -M--copy-as=joe src/ lh:dest/ .fi .RE -.IP "\fB\-\-temp-dir=DIR\fP, \fB\-T\fP" +.IP "\fB\-\-temp\-dir=DIR\fP, \fB\-T\fP" This option instructs rsync to use DIR as a scratch directory when creating temporary copies of the files transferred on the receiving side. The default behavior is to create each temporary file in the same directory as -the associated destination file. Beginning with rsync 3.1.1, the temp-file -names inside the specified DIR will not be prefixed with an extra dot -(though they will still have a random suffix added). +the associated destination file. The temp-file +names inside the specified DIR will not be prefixed with an extra dot, +though they will still have a random suffix added. .IP This option is most often used when the receiving disk partition does not have enough free space to hold a copy of the largest file in the transfer. @@ -2670,40 +2899,38 @@ over the top of the associated destination file, but instead must copy it into place. Rsync does this by copying the file over the top of the destination file, which means that the destination file will contain -truncated data during this copy. If this were not done this way (even if -the destination file were first removed, the data locally copied to a -temporary file in the destination directory, and then renamed into place) +truncated data during this copy. If this were not done this way, it would be possible for the old file to continue taking up disk space (if someone had it open), and thus there might not be enough room to fit the new version on the disk at the same time. .IP If you are using this option for reasons other than a shortage of disk -space, you may wish to combine it with the \fB\-\-delay-updates\fP +space, you may wish to combine it with the \fB\-\-delay\-updates\fP option, which will ensure that all copied files get put into subdirectories in the destination hierarchy, awaiting the end of the transfer. If you don't have enough room to duplicate all the arriving files on the destination partition, another way to tell rsync that you aren't overly -concerned about disk space is to use the \fB\-\-partial-dir\fP option +concerned about disk space is to use the \fB\-\-partial\-dir\fP option with a relative path; because this tells rsync that it is OK to stash off a -copy of a single file in a subdir in the destination hierarchy, rsync will +copy of a single file in a subdirectory in the destination hierarchy, rsync will use the partial-dir as a staging area to bring over the copied file, and -then rename it into place from there. (Specifying a \fB\-\-partial-dir\fP +then rename it into place from there. (Specifying a \fB\-\-partial\-dir\fP with an absolute path does not have this side-effect.) .IP "\fB\-\-fuzzy\fP, \fB\-y\fP" This option tells rsync that it should look for a basis file for any -destination file that is missing. The current algorithm looks in the same +destination file that is missing. With this option, rsync looks in the same directory as the destination file for either a file that has an identical size and modified-time, or a similarly-named file. If found, rsync uses the fuzzy basis file to try to speed up the transfer. .IP If the option is repeated, the fuzzy scan will also be done in any matching alternate destination directories that are specified via -\fB\-\-compare-dest\fP, \fB\-\-copy-dest\fP, or \fB\-\-link-dest\fP. +\fB\-\-compare\-dest\fP, \fB\-\-copy\-dest\fP, or \fB\-\-link\-dest\fP. .IP Note that the use of the \fB\-\-delete\fP option might get rid of any -potential fuzzy-match files, so either use \fB\-\-delete-after\fP or +potential fuzzy-match files, so either use \fB\-\-delete\-after\fP or specify some filename exclusions if you need to prevent this. -.IP "\fB\-\-compare-dest=DIR\fP" +.IP "\fB\-\-compare\-dest=DIR\fP" This option instructs rsync to use \fIDIR\fP on the destination machine as an additional hierarchy to compare destination files against doing transfers (if the files are missing in the destination directory). If a file is @@ -2713,7 +2940,7 @@ option is typically used to copy into an empty (or newly created) directory. .IP -Beginning in version 2.6.4, multiple \fB\-\-compare-dest\fP directories may be +Multiple \fB\-\-compare\-dest\fP directories may be provided, which will cause rsync to search the list in the order specified for an exact match. If a match is found that differs only in attributes, a local copy is made and the attributes updated. If a match is not found, a @@ -2721,28 +2948,28 @@ transfer. .IP If \fIDIR\fP is a relative path, it is relative to the destination directory. -See also \fB\-\-copy-dest\fP and \fB\-\-link-dest\fP. +See also \fB\-\-copy\-dest\fP and \fB\-\-link\-dest\fP. .IP -NOTE: beginning with version 3.1.0, rsync will remove a file from a +NOTE: rsync will remove a file from a non-empty destination hierarchy if an exact match is found in one of the -compare-dest hierarchies (making the end result more closely match a fresh -copy). -.IP "\fB\-\-copy-dest=DIR\fP" -This option behaves like \fB\-\-compare-dest\fP, but rsync will also copy +compare-dest hierarchies, making the end result more closely match a fresh +copy. +.IP "\fB\-\-copy\-dest=DIR\fP" +This option behaves like \fB\-\-compare\-dest\fP, but rsync will also copy unchanged files found in \fIDIR\fP to the destination directory using a local copy. This is useful for doing transfers to a new destination while leaving existing files intact, and then doing a flash-cutover when all files have been successfully transferred. .IP -Multiple \fB\-\-copy-dest\fP directories may be provided, which will cause rsync +Multiple \fB\-\-copy\-dest\fP directories may be provided, which will cause rsync to search the list in the order specified for an unchanged file. If a match is not found, a basis file from one of the \fIDIRs\fP will be selected to try to speed up the transfer. .IP If \fIDIR\fP is a relative path, it is relative to the destination directory. -See also \fB\-\-compare-dest\fP and \fB\-\-link-dest\fP. -.IP "\fB\-\-link-dest=DIR\fP" -This option behaves like \fB\-\-copy-dest\fP, but unchanged files are +See also \fB\-\-compare\-dest\fP and \fB\-\-link\-dest\fP. +.IP "\fB\-\-link\-dest=DIR\fP" +This option behaves like \fB\-\-copy\-dest\fP, but unchanged files are hard linked from \fIDIR\fP to the destination directory. The files must be identical in all preserved attributes (e.g. permissions, possibly ownership) in order for the files to be linked together. An example: @@ -2759,48 +2986,66 @@ with generic ownership (such as OS X's "Ignore ownership on this volume" option). .IP -Beginning in version 2.6.4, multiple \fB\-\-link-dest\fP directories may be +Multiple \fB\-\-link\-dest\fP directories may be provided, which will cause rsync to search the list in the order specified for an exact match (there is a limit of 20 such directories). If a match is found that differs only in attributes, a local copy is made and the attributes updated. If a match is not found, a basis file from one of the \fIDIRs\fP will be selected to try to speed up the transfer. .IP +Not every filesystem can hard-link a symlink, a device node, a FIFO or a +socket, and the destination need not agree with the one rsync was built on\ \-\- macOS builds on APFS, which can, and may write to HFS+, which cannot. +Where the destination refuses to link such an entry, it is copied instead +and the transfer carries on, so only that entry loses the space saving. +This applies to any refusal, because the error alone does not identify one: +link(2) reports \fBEPERM\fP both for a filesystem without hard links and for an +ordinary permission refusal. Regular files have always behaved this way. +.IP +One case is not covered. With \fB\-\-hard\-links\fP (\fB\-H\fP), a group of +such entries hard-linked to \fIeach other\fP in the source needs a second link, +from the first member to the rest, inside the destination itself. Where +that link is refused too\ \-\- a destination that cannot hard-link the type at +all\ \-\- the members after the first are not created and the transfer fails. +A \fB\-\-link\-dest\fP on another filesystem is fine: only the link to \fIDIR\fP is +impossible there, and the ones within the destination still succeed. +.IP This option works best when copying into an empty destination hierarchy, as existing files may get their attributes tweaked, and that can affect alternate destination files via hard-links. Also, itemizing of changes can -get a bit muddled. Note that prior to version 3.1.0, an +get a bit muddled. Note that prior to version 3.1.0 (September 2013), an alternate-directory exact match would never be found (nor linked into the destination) when a destination file already exists. .IP -Note that if you combine this option with \fB\-\-ignore-times\fP, rsync will not +Note that if you combine this option with \fB\-\-ignore\-times\fP, rsync will not link any files together because it only links identical files together as a substitute for transferring the file, never as an additional check after the file is updated. .IP If \fIDIR\fP is a relative path, it is relative to the destination directory. -See also \fB\-\-compare-dest\fP and \fB\-\-copy-dest\fP. +See also \fB\-\-compare\-dest\fP and \fB\-\-copy\-dest\fP. .IP -Note that rsync versions prior to 2.6.1 had a bug that could prevent -\fB\-\-link-dest\fP from working properly for a non-super-user when +Note that rsync versions prior to 2.6.1 (April 2004) had a bug that could prevent +\fB\-\-link\-dest\fP from working properly for a non-super-user when \fB\-\-owner\fP (\fB\-o\fP) was specified (or implied). You can work-around -this bug by avoiding the \fB\-o\fP option (or using \fB\-\-no-o\fP) when sending to an -old rsync. +this bug by avoiding the \fB\-o\fP option (or using \fB\-\-no\-o\fP) when sending to a +really old remote rsync. .IP "\fB\-\-compress\fP, \fB\-z\fP" With this option, rsync compresses the file data as it is sent to the destination machine, which reduces the amount of data being transmitted\ \-\- something that is useful over a slow connection. .IP Rsync supports multiple compression methods and will choose one for you -unless you force the choice using the \fB\-\-compress-choice\fP (\fB\-\-zc\fP) +unless you force the choice using the \fB\-\-compress\-choice\fP (\fB\-\-zc\fP) option. .IP Run \fBrsync\ \-\-version\fP to see the default compress list compiled into your version. .IP -When both sides of the transfer are at least 3.2.0, rsync chooses the first -algorithm in the client's list of choices that is also in the server's list -of choices. If no common compress choice is found, rsync exits with +When both sides of the transfer are at least 3.2.0 (June 2020), +each side chooses its own +most-preferred algorithm that also appears in the peer's list. Both sides +order their lists strongest-first, so they converge on the strongest mutual +choice. If no common compress choice is found, rsync exits with an error. If the remote rsync is too old to support checksum negotiation, its list is assumed to be "zlib". .IP @@ -2818,11 +3063,11 @@ not compatible with the default zlib compression method. You can usually ignore this weirdness unless the rsync server complains and tells you to specify \fB\-zz\fP. -.IP "\fB\-\-compress-choice=STR\fP, \fB\-\-zc=STR\fP" +.IP "\fB\-\-compress\-choice=STR\fP, \fB\-\-zc=STR\fP" This option can be used to override the automatic negotiation of the compression algorithm that occurs when \fB\-\-compress\fP is used. The option implies \fB\-\-compress\fP unless "none" was specified, which -instead implies \fB\-\-no-compress\fP. +instead implies \fB\-\-no\-compress\fP. .IP The compression options that you may be able to use are: .IP @@ -2842,18 +3087,18 @@ Run \fBrsync\ \-\-version\fP to see the default compress list compiled into your version (which may differ from the list above). .IP -Note that if you see an error about an option named \fB\-\-old-compress\fP or -\fB\-\-new-compress\fP, this is rsync trying to send the \fB\-\-compress-choice=zlib\fP -or \fB\-\-compress-choice=zlibx\fP option in a backward-compatible manner that +Note that if you see an error about an option named \fB\-\-old\-compress\fP or +\fB\-\-new\-compress\fP, this is rsync trying to send the \fB\-\-compress\-choice=zlib\fP +or \fB\-\-compress\-choice=zlibx\fP option in a backward-compatible manner that more rsync versions understand. This error indicates that the older rsync version on the server will not allow you to force the compression type. .IP Note that the "zlibx" compression algorithm is just the "zlib" algorithm with matched data excluded from the compression stream (to try to make it more compatible with an external zlib implementation). -.IP "\fB\-\-compress-level=NUM\fP, \fB\-\-zl=NUM\fP" +.IP "\fB\-\-compress\-level=NUM\fP, \fB\-\-zl=NUM\fP" Explicitly set the compression level to use (see \fB\-\-compress\fP, -\fB\-z\fP) instead of letting it default. The \fB\-\-compress\fP option is +\fB\-z\fP) instead of using the default. The \fB\-\-compress\fP option is implied as long as the level chosen is not a "don't compress" level for the compression algorithm that is in effect (e.g. zlib compression treats level 0 as "off"). @@ -2861,7 +3106,7 @@ The level values vary depending on the checksum in effect. Because rsync will negotiate a checksum choice by default (when the remote rsync is new enough), it can be good to combine this option with a -\fB\-\-compress-choice\fP (\fB\-\-zc\fP) option unless you're sure of the +\fB\-\-compress\-choice\fP (\fB\-\-zc\fP) option unless you're sure of the choice in effect. For example: .RS 4 .IP @@ -2888,12 +3133,29 @@ \fB\-\-debug=nstr\fP to see the "negotiated string" results. This will report something like "\fBClient\ compress:\ zstd\ (level\ 3)\fP" (along with the checksum choice in effect). -.IP "\fB\-\-skip-compress=LIST\fP" +.IP "\fB\-\-compress\-threads=NUM\fP, \fB\-\-zt=NUM\fP" +Set the number of threads to spawn when compressing data. Setting this +option to 1 or more will instruct the compression library to spawn 1 or +more threads for compression. Ideally, increasing the number of threads +will increase transfer speed if the transfer is CPU bound on the sender. +.IP +This option does not affect decompression. +.IP +Compression algorithms that allow threading: +.IP +.RS +.IP o +\fBzstd\fP (only when libzstd is compiled with threading support) +.RE +.IP +This option is ignored if one of the above alogithms is not selected as the +\fB\-\-compression\-choice\fP or if compression not enabled. +.IP "\fB\-\-skip\-compress=LIST\fP" \fBNOTE:\fP no compression method currently supports per-file compression changes, so this option has no effect. .IP Override the list of file suffixes that will be compressed as little as -possible. Rsync sets the compression level on a per-file basis based on +possible. Rsync can set the compression level on a per-file basis based on the file's suffix. If the compression algorithm has an "off" level, then no compression occurs for those files. Other algorithms that support changing the streaming level on-the-fly will have the level minimized to @@ -3020,17 +3282,17 @@ zst .RE .IP -This list will be replaced by your \fB\-\-skip-compress\fP list in all but one +This list will be replaced by your \fB\-\-skip\-compress\fP list in all but one situation: a copy from a daemon rsync will add your skipped suffixes to its list of non-compressing files (and its list may be configured to a different default). -.IP "\fB\-\-numeric-ids\fP" +.IP "\fB\-\-numeric\-ids\fP" With this option rsync will transfer numeric group and user IDs rather than using user and group names and mapping them at both ends. .IP By default rsync will use the username and groupname to determine what ownership to give files. The special uid 0 and the special group 0 are -never mapped via user/group names even if the \fB\-\-numeric-ids\fP option is not +never mapped via user/group names even if the \fB\-\-numeric\-ids\fP option is not specified. .IP If a user or group has no name on the source system or it has no match on @@ -3077,18 +3339,22 @@ .fi .RE .IP -When the \fB\-\-numeric-ids\fP option is used, the sender does not send any +An empty \fBFROM\fP value matches only sender-side IDs that have no name. It +is not a wildcard for named users or groups; use "\fB*\fP" when you want to map +every sender-side name. +.IP +When the \fB\-\-numeric\-ids\fP option is used, the sender does not send any names, so all the IDs are treated as having an empty name. This means that you will need to specify numeric \fBFROM\fP values if you want to map these nameless IDs to different values. .IP For the \fB\-\-usermap\fP option to work, the receiver will need to be running as -a super-user (see also the \fB\-\-super\fP and \fB\-\-fake-super\fP +a super-user (see also the \fB\-\-super\fP and \fB\-\-fake\-super\fP options). For the \fB\-\-groupmap\fP option to work, the receiver will need to have permissions to set that group. .IP -Starting with rsync 3.2.4, the \fB\-\-usermap\fP option implies the -\fB\-\-owner\fP (\fB\-o\fP) option while the \fB\-\-groupmap\fP option implies the +The \fB\-\-usermap\fP option implies the +\fB\-\-owner\fP (\fB\-o\fP) option, and the \fB\-\-groupmap\fP option implies the \fB\-\-group\fP (\fB\-g\fP) option (since rsync needs to have those options enabled for the mapping options to work). .IP @@ -3097,7 +3363,7 @@ .IP "\fB\-\-chown=USER:GROUP\fP" This option forces all files to be owned by USER with group GROUP. This is a simpler interface than using \fB\-\-usermap\fP & \fB\-\-groupmap\fP -directly, but it is implemented using those options internally so they +directly, but it is implemented using those options internally, thus they cannot be mixed. If either the USER or GROUP is empty, no mapping for the omitted user/group will occur. If GROUP is empty, the trailing colon may be omitted, but if USER is empty, a leading colon must be supplied. @@ -3138,7 +3404,7 @@ affects direct socket connections to a remote rsync daemon. .IP See also the daemon version of the \fB\-\-sockopts\fP option. -.IP "\fB\-\-blocking-io\fP" +.IP "\fB\-\-blocking\-io\fP" This tells rsync to use blocking I/O when launching a remote shell transport. If the remote shell is either rsh or remsh, rsync defaults to using blocking I/O, otherwise it defaults to using non-blocking I/O. (Note @@ -3150,13 +3416,13 @@ .IP The main use of this option is to change Full buffering to Line buffering when rsync's output is going to a file or pipe. -.IP "\fB\-\-itemize-changes\fP, \fB\-i\fP" +.IP "\fB\-\-itemize\-changes\fP, \fB\-i\fP" Requests a simple itemized list of the changes that are being made to each file, including attribute changes. This is exactly the same as specifying -\fB\-\-out-format='%i\ %n%L'\fP. If you repeat the option, unchanged +\fB\-\-out\-format='%i\ %n%L'\fP. If you repeat the option, unchanged files will also be output, but only if the receiving rsync is at least -version 2.6.7 (you can use \fB\-vv\fP with older versions of rsync, but that -also turns on the output of other verbose messages). +version 2.6.7 (March 2006). You can use \fB\-vv\fP with older versions of rsync, +but that also turns on the output of other verbose messages. .IP The "%i" escape has a cryptic output that is 11 letters long. The general format is like the string \fBYXcstpoguax\fP, where \fBY\fP is replaced by the type @@ -3176,7 +3442,7 @@ as the creation of a directory or the changing of a symlink, etc.). .IP o A \fBh\fP means that the item is a hard link to another item (requires -\fB\-\-hard-links\fP). +\fB\-\-hard\-links\fP). .IP o A \fB.\fP means that the item is not being updated (though it might have attributes that are being modified). @@ -3212,7 +3478,7 @@ A \fBc\fP means either that a regular file has a different checksum (requires \fB\-\-checksum\fP) or that a symlink, device, or special file has a changed value. Note that if you are sending files to an rsync prior to -3.0.1, this change flag will be present only for checksum-differing +3.0.1 (April 2008), this change flag will be present only for checksum-differing regular files. .IP o A \fBs\fP means the size of a regular file is different and will be updated @@ -3223,9 +3489,7 @@ \fBT\fP means that the modification time will be set to the transfer time, which happens when a file/symlink/device is updated without \fB\-\-times\fP and when a symlink is changed and the receiver can't -set its time. (Note: when using an rsync 3.0.0 client, you might see the -\fBs\fP flag combined with \fBt\fP instead of the proper \fBT\fP flag for this -time-setting failure.) +set its time. .IP o A \fBp\fP means the permissions are different and are being updated to the sender's value (requires \fB\-\-perms\fP). @@ -3250,16 +3514,16 @@ \fBb\fP means that both the access and create times are being updated .RE .IP o -The \fBa\fP means that the ACL information is being changed. +An \fBa\fP means that the ACL information is being changed. .IP o -The \fBx\fP means that the extended attribute information is being changed. +An \fBx\fP means that the extended attribute information is being changed. .RE .IP One other output is possible: when deleting files, the "%i" will output the string "\fB*deleting\fP" for each item that is being removed (assuming that you are talking to a recent enough rsync that it logs deletions instead of outputting them as a verbose message). -.IP "\fB\-\-out-format=FORMAT\fP" +.IP "\fB\-\-out\-format=FORMAT\fP" This allows you to specify exactly what the rsync client outputs to the user on a per-update basis. The format is a text string containing embedded single-character escape sequences prefixed with a percent (%) @@ -3268,14 +3532,14 @@ name of the file and, if the item is a link, where it points). For a full list of the possible escape characters, see the \fBlog\ format\fP setting in the rsyncd.conf manpage. .IP -Specifying the \fB\-\-out-format\fP option implies the \fB\-\-info=name\fP -option, which will mention each file, dir, etc. that gets updated in a +Specifying the \fB\-\-out\-format\fP option implies the \fB\-\-info=name\fP +option, which will mention each file, directory, etc. that gets updated in a significant way (a transferred file, a recreated symlink/device, or a touched directory). In addition, if the itemize-changes escape (%i) is -included in the string (e.g. if the \fB\-\-itemize-changes\fP option was +included in the string (e.g. if the \fB\-\-itemize\-changes\fP option was used), the logging of names increases to mention any item that is changed in any way (as long as the receiving side is at least 2.6.4). See the -\fB\-\-itemize-changes\fP option for a description of the output of "%i". +\fB\-\-itemize\-changes\fP option for a description of the output of "%i". .IP Rsync will output the out-format string prior to a file's transfer unless one of the transfer-statistic escapes is requested, in which case the @@ -3283,12 +3547,12 @@ is in effect and \fB\-\-progress\fP is also specified, rsync will also output the name of the file being transferred prior to its progress information (followed, of course, by the out-format output). -.IP "\fB\-\-log-file=FILE\fP" -This option causes rsync to log what it is doing to a file. This is +.IP "\fB\-\-log\-file=FILE\fP" +This option causes rsync to write the log of what it is doing to a file. This is similar to the logging that a daemon does, but can be requested for the client side and/or the server side of a non-daemon transfer. If specified as a client option, transfer logging will be enabled with a default format -of "%i %n%L". See the \fB\-\-log-file-format\fP option if you wish to +of "%i %n%L". See the \fB\-\-log\-file\-format\fP option if you wish to override this. .IP Here's an example command that requests the remote side to log what is @@ -3303,19 +3567,19 @@ This is very useful if you need to debug why a connection is closing unexpectedly. .IP -See also the daemon version of the \fB\-\-log-file\fP option. -.IP "\fB\-\-log-file-format=FORMAT\fP" +See also the daemon version of the \fB\-\-log\-file\fP option. +.IP "\fB\-\-log\-file\-format=FORMAT\fP" This allows you to specify exactly what per-update logging is put into the -file specified by the \fB\-\-log-file\fP option (which must also be +file specified by the \fB\-\-log\-file\fP option (which must also be specified for this option to have any effect). If you specify an empty string, updated files will not be mentioned in the log file. For a list of the possible escape characters, see the \fBlog\ format\fP setting in the rsyncd.conf manpage. .IP -The default FORMAT used if \fB\-\-log-file\fP is specified and this +The default FORMAT used if \fB\-\-log\-file\fP is specified and this option is not is '%i %n%L'. .IP -See also the daemon version of the \fB\-\-log-file-format\fP +See also the daemon version of the \fB\-\-log\-file\-format\fP option. .IP "\fB\-\-stats\fP" This tells rsync to print a verbose set of statistics on the file transfer, @@ -3333,7 +3597,7 @@ followed by a list of counts by filetype (if the total is non-zero). For example: "(reg: 5, dir: 3, link: 2, dev: 1, special: 1)" lists the totals for regular files, directories, symlinks, devices, and special files. If -any of value is 0, it is completely omitted from the list. +any value is 0, it is completely omitted from the list. .IP o \fBNumber\ of\ created\ files\fP is the count of how many "files" (generic sense) were created (as opposed to updated). The total count will be @@ -3343,12 +3607,12 @@ sense) were deleted. The total count will be followed by a list of counts by filetype (if the total is non-zero). Note that this line is only output if deletions are in effect, and only -if protocol 31 is being used (the default for rsync 3.1.x). +if the negotiated protocol is at least 31 (the default when both sides +are 3.1.0, September 2013, or newer). .IP o \fBNumber\ of\ regular\ files\ transferred\fP is the count of normal files that were updated via rsync's delta-transfer algorithm, which does not include -dirs, symlinks, etc. Note that rsync 3.1.0 added the word "regular" into -this heading. +directories, symlinks, etc. .IP o \fBTotal\ file\ size\fP is the total sum of all file sizes in the transfer. This does not count any size for directories or special files, but does @@ -3357,7 +3621,7 @@ \fBTotal\ transferred\ file\ size\fP is the total sum of all files sizes for just the transferred files. .IP o -\fBLiteral\ data\fP is how much unmatched file-update data we had to send to +\fBLiteral\ data\fP is how much unmatched file-update data the sender had to send to the receiver for it to recreate the updated files. .IP o \fBMatched\ data\fP is how much data the receiver got locally when recreating @@ -3383,17 +3647,18 @@ means that we don't count the bytes for a verbose message that the server sent to us, which makes the stats more consistent. .RE -.IP "\fB\-\-8-bit-output\fP, \fB\-8\fP" +.IP "\fB\-\-8\-bit\-output\fP, \fB\-8\fP" This tells rsync to leave all high-bit characters unescaped in the output +(to standard output or standard error) instead of trying to test them to see if they're valid in the current locale and escaping the invalid ones. All control characters (but never tabs) are always escaped, regardless of this option's setting. .IP -The escape idiom that started in 2.6.7 is to output a literal backslash +The escape idiom that started in 2.6.7 (March 2006) is to output a literal backslash (\fB\\\fP) and a hash (\fB#\fP), followed by exactly 3 octal digits. For example, a newline would output as "\fB\\#012\fP". A literal backslash that is in a filename is not escaped unless it is followed by a hash and 3 digits (0-9). -.IP "\fB\-\-human-readable\fP, \fB\-h\fP" +.IP "\fB\-\-human\-readable\fP, \fB\-h\fP" Output numbers in a more human-readable format. There are 3 possible levels: .RS .IP @@ -3410,18 +3675,18 @@ .IP The default is human-readable level 1. Each \fB\-h\fP option increases the level by one. You can take the level down to 0 (to output numbers as pure -digits) by specifying the \fB\-\-no-human-readable\fP (\fB\-\-no-h\fP) option. +digits) by specifying the \fB\-\-no\-human\-readable\fP (\fB\-\-no\-h\fP) option. .IP The unit letters that are appended in levels 2 and 3 are: \fBK\fP (kilo), \fBM\fP (mega), \fBG\fP (giga), \fBT\fP (tera), or \fBP\fP (peta). For example, a 1234567-byte file would output as 1.23M in level-2 (assuming that a period is your local decimal point). .IP -Backward compatibility note: versions of rsync prior to 3.1.0 do not +Backward compatibility note: versions of rsync prior to 3.1.0 (September 2013) do not support human-readable level 1, and they default to level 0. Thus, specifying one or two \fB\-h\fP options will behave in a comparable manner in -old and new versions as long as you didn't specify a \fB\-\-no-h\fP option prior -to one or more \fB\-h\fP options. See the \fB\-\-list-only\fP option for one +old and new versions as long as you didn't specify a \fB\-\-no\-h\fP option prior +to one or more \fB\-h\fP options. See the \fB\-\-list\-only\fP option for one difference. .IP "\fB\-\-partial\fP" By default, rsync will delete any partially transferred file if the @@ -3429,22 +3694,22 @@ keep partially transferred files. Using the \fB\-\-partial\fP option tells rsync to keep the partial file which should make a subsequent transfer of the rest of the file much faster. -.IP "\fB\-\-partial-dir=DIR\fP" +.IP "\fB\-\-partial\-dir=DIR\fP" This option modifies the behavior of the \fB\-\-partial\fP option while also implying that it be enabled. This enhanced partial-file method puts any partially transferred files into the specified \fIDIR\fP instead of writing the partial file out to the destination file. On the next transfer, rsync -will use a file found in this dir as data to speed up the resumption of the +will use a file found in this directory as data to speed up the resumption of the transfer and then delete it after it has served its purpose. .IP -Note that if \fB\-\-whole-file\fP is specified (or implied), any +Note that if \fB\-\-whole\-file\fP is specified (or implied), any partial-dir files that are found for a file that is being updated will simply be removed (since rsync is sending files without using rsync's delta-transfer algorithm). .IP -Rsync will create the \fIDIR\fP if it is missing, but just the last dir\ \-\- not +Rsync will create the \fIDIR\fP if it is missing, but just the last directory\ \-\- not the whole path. This makes it easy to use a relative path (such as -"\fB\-\-partial-dir=.rsync-partial\fP") to have rsync create the +"\fB\-\-partial\-dir=.rsync-partial\fP") to have rsync create the partial-directory in the destination file's directory when it is needed, and then remove it again when the partial file is deleted. Note that this directory removal is only done for a relative pathname, as it is expected @@ -3455,7 +3720,7 @@ rule at the end of all your existing excludes. This will prevent the sending of any partial-dir files that may exist on the sending side, and will also prevent the untimely deletion of partial-dir items on the -receiving side. An example: the above \fB\-\-partial-dir\fP option would add the +receiving side. An example: the above \fB\-\-partial\-dir\fP option would add the equivalent of this "perishable" exclude at the end of any other filter rules: \fB\-f\ '\-p\ .rsync-partial/'\fP .IP @@ -3469,21 +3734,21 @@ you may wish to override rsync's exclude choice. .RE .IP -For instance, if you want to make rsync clean-up any left-over partial-dirs -that may be lying around, you should specify \fB\-\-delete-after\fP and +For instance, if you want to make rsync clean-up any left-over partial-directories +that may be lying around, you should specify \fB\-\-delete\-after\fP and add a "risk" filter rule, e.g. \fB\-f\ 'R\ .rsync-partial/'\fP. Avoid using -\fB\-\-delete-before\fP or \fB\-\-delete-during\fP unless you don't +\fB\-\-delete\-before\fP or \fB\-\-delete\-during\fP unless you don't need rsync to use any of the left-over partial-dir data during the current run. .IP -IMPORTANT: the \fB\-\-partial-dir\fP should not be writable by other users or it +IMPORTANT: the partial-dir directory should not be writable by other users or it is a security risk! E.g. AVOID "/tmp"! .IP You can also set the partial-dir value the \fBRSYNC_PARTIAL_DIR\fP environment variable. Setting this in the environment does not force \fB\-\-partial\fP to be enabled, but rather it affects where partial files go when \fB\-\-partial\fP is specified. For instance, instead of -using \fB\-\-partial-dir=.rsync-tmp\fP along with \fB\-\-progress\fP, you could +using \fB\-\-partial\-dir=.rsync-tmp\fP along with \fB\-\-progress\fP, you could set \fBRSYNC_PARTIAL_DIR=.rsync-tmp\fP in your environment and then use the \fB\-P\fP option to turn on the use of the .rsync-tmp dir for partial transfers. The only times that the \fB\-\-partial\fP option does @@ -3492,42 +3757,42 @@ .IP .IP 1. when \fB\-\-inplace\fP was specified (since \fB\-\-inplace\fP -conflicts with \fB\-\-partial-dir\fP), and +conflicts with \fB\-\-partial\-dir\fP), and .IP 2. -when \fB\-\-delay-updates\fP was specified (see below). +when \fB\-\-delay\-updates\fP was specified (see below). .RE .IP When a modern rsync resumes the transfer of a file in the partial-dir, that partial file is now updated in-place instead of creating yet another tmp-file copy (so it maxes out at dest + tmp instead of dest + partial + tmp). This requires both ends of the transfer to be at least version -3.2.0. +3.2.0 (June 2020). .IP For the purposes of the daemon-config's "\fBrefuse\ options\fP" setting, -\fB\-\-partial-dir\fP does \fInot\fP imply \fB\-\-partial\fP. This is so that a +\fB\-\-partial\-dir\fP does \fInot\fP imply \fB\-\-partial\fP. This is so that a refusal of the \fB\-\-partial\fP option can be used to disallow the overwriting of destination files with a partial transfer, while still -allowing the safer idiom provided by \fB\-\-partial-dir\fP. -.IP "\fB\-\-delay-updates\fP" +allowing the safer idiom provided by \fB\-\-partial\-dir\fP. +.IP "\fB\-\-delay\-updates\fP" This option puts the temporary file from each updated file into a holding directory until the end of the transfer, at which time all the files are renamed into place in rapid succession. This attempts to make the updating -of the files a little more atomic. By default the files are placed into a +of the files a little closer to atomic. By default the files are placed into a directory named \fB.~tmp~\fP in each file's destination directory, but if -you've specified the \fB\-\-partial-dir\fP option, that directory will be -used instead. See the comments in the \fB\-\-partial-dir\fP section for +you've specified the \fB\-\-partial\-dir\fP option, that directory will be +used instead. See the comments in the \fB\-\-partial\-dir\fP section for a discussion of how this \fB.~tmp~\fP dir will be excluded from the transfer, -and what you can do if you want rsync to cleanup old \fB.~tmp~\fP dirs that +and what you can do if you want rsync to cleanup old \fB.~tmp~\fP directories that might be lying around. Conflicts with \fB\-\-inplace\fP and \fB\-\-append\fP. .IP -This option implies \fB\-\-no-inc-recursive\fP since it needs the full +This option implies \fB\-\-no\-inc\-recursive\fP since it needs the full file list in memory in order to be able to iterate over it at the end. .IP This option uses more memory on the receiving side (one bit per file transferred) and also requires enough free disk space on the receiving side to hold an additional copy of all the updated files. Note also that you -should not use an absolute path to \fB\-\-partial-dir\fP unless: +should not use an absolute path to \fB\-\-partial\-dir\fP unless: .RS .IP .IP 1. @@ -3539,10 +3804,10 @@ will fail if they can't be renamed into place). .RE .IP -See also the "atomic-rsync" python script in the "support" subdir for an -update algorithm that is even more atomic (it uses \fB\-\-link-dest\fP +See also the "atomic-rsync" python script in the "support" subdirectory for an +update algorithm that is even closer to atomic (it uses \fB\-\-link\-dest\fP and a parallel hierarchy of files). -.IP "\fB\-\-prune-empty-dirs\fP, \fB\-m\fP" +.IP "\fB\-\-prune\-empty\-dirs\fP, \fB\-m\fP" This option tells the receiving rsync to get rid of empty directories from the file-list, including nested directories that have no non-directory children. This is useful for avoiding the creation of a bunch of useless @@ -3660,8 +3925,8 @@ don't know which of the 3 rsync processes is the client process, it's OK to signal all of them (since the non-client processes ignore the signal). .IP -CAUTION: sending SIGVTALRM to an older rsync (pre-3.2.0) will kill it. -.IP "\fB\-\-password-file=FILE\fP" +CAUTION: sending SIGVTALRM to an older rsync (pre-3.2.0, June 2020) will kill it. +.IP "\fB\-\-password\-file=FILE\fP" This option allows you to provide a password for accessing an rsync daemon via a file or via standard input if \fBFILE\fP is \fB\-\fP. The file should contain just the password on the first line (all other lines are ignored). @@ -3674,30 +3939,30 @@ option only comes into effect after the remote shell finishes its authentication (i.e. if you have also specified a password in the daemon's config file). -.IP "\fB\-\-early-input=FILE\fP" +.IP "\fB\-\-early\-input=FILE\fP" This option allows rsync to send up to 5K of data to the "early exec" script on its stdin. One possible use of this data is to give the script a secret that can be used to mount an encrypted filesystem (which you should unmount in the the "post-xfer exec" script). .IP -The daemon must be at least version 3.2.1. -.IP "\fB\-\-list-only\fP" +The daemon must be at least version 3.2.1 (June 2020). +.IP "\fB\-\-list\-only\fP" This option will cause the source files to be listed instead of -transferred. This option is inferred if there is a single source arg and +transferred. This option is inferred if there is a single source argument and no destination specified, so its main uses are: .RS .IP .IP 1. -to turn a copy command that includes a destination arg into a +to turn a copy command that includes a destination argument into a file-listing command, or .IP 2. -to be able to specify more than one source arg. Note: be sure to +to be able to specify more than one source argument. Note: be sure to include the destination. .RE .IP -CAUTION: keep in mind that a source arg with a wild-card is expanded by the -shell into multiple args, so it is never safe to try to specify a single -wild-card arg to try to infer this option. A safe example is: +CAUTION: keep in mind that a source argument with a wild-card is expanded by the +shell into multiple arguments, so it is never safe to try to specify a single +wild-card argument to try to infer this option. A safe example is: .RS 4 .IP .nf @@ -3714,18 +3979,19 @@ .fi .RE .IP -The only option that affects this output style is (as of 3.1.0) the -\fB\-\-human-readable\fP (\fB\-h\fP) option. The default is to output sizes +The only option that affects this output style is the +\fB\-\-human\-readable\fP (\fB\-h\fP) option. The default is to output sizes as byte counts with digit separators (in a 14-character-width column). Specifying at least one \fB\-h\fP option makes the sizes output with unit suffixes. If you want old-style bytecount sizes without digit separators -(and an 11-character-width column) use \fB\-\-no-h\fP. +(and an 11-character-width column) use \fB\-\-no\-h\fP. .IP Compatibility note: when requesting a remote listing of files from an rsync -that is version 2.6.3 or older, you may encounter an error if you ask for a +that is version 2.6.3 or older (i.e., pre-2005), +you may encounter an error if you ask for a non-recursive listing. This is because a file listing implies the -\fB\-\-dirs\fP option w/o \fB\-\-recursive\fP, and older rsyncs don't -have that option. To avoid this problem, either specify the \fB\-\-no-dirs\fP +\fB\-\-dirs\fP option without \fB\-\-recursive\fP, and older rsyncs don't +have that option. To avoid this problem, either specify the \fB\-\-no\-dirs\fP option (if you don't need to expand a directory's content), or turn on recursion and exclude the content of subdirectories: \fB\-r\ \-\-exclude='/*/*'\fP. .IP "\fB\-\-bwlimit=RATE\fP" @@ -3734,7 +4000,7 @@ suffixed with a string to indicate a size multiplier, and may be a fractional value (e.g. \fB\-\-bwlimit=1.5m\fP). If no suffix is specified, the value will be assumed to be in units of 1024 bytes (as if "K" or "KiB" had -been appended). See the \fB\-\-max-size\fP option for a description of +been appended). See the \fB\-\-max\-size\fP option for a description of all the available suffixes. A value of 0 specifies no limit. .IP For backward-compatibility reasons, the rate limit will be rounded to the @@ -3754,7 +4020,7 @@ the output buffer occurs. This may be fixed in a future version. .IP See also the daemon version of the \fB\-\-bwlimit\fP option. -.IP "\fB\-\-stop-after=MINS\fP, (\fB\-\-time-limit=MINS\fP)" +.IP "\fB\-\-stop\-after=MINS\fP, (\fB\-\-time\-limit=MINS\fP)" This option tells rsync to stop copying when the specified number of minutes has elapsed. .IP @@ -3762,10 +4028,10 @@ remote rsync since it is usually enough that one side of the connection quits as specified. This allows the option's use even when only one side of the connection supports it. You can tell the remote side about the time -limit using \fB\-\-remote-option\fP (\fB\-M\fP), should the need arise. +limit using \fB\-\-remote\-option\fP (\fB\-M\fP), should the need arise. .IP -The \fB\-\-time-limit\fP version of this option is deprecated. -.IP "\fB\-\-stop-at=y-m-dTh:m\fP" +The \fB\-\-time\-limit\fP version of this option is deprecated. +.IP "\fB\-\-stop\-at=y-m-dTh:m\fP" This option tells rsync to stop copying when the specified point in time has been reached. The date & time can be fully specified in a numeric format of year-month-dayThour:minute (e.g. 2000-12-31T23:59) in the local @@ -3787,27 +4053,27 @@ remote rsync since it is usually enough that one side of the connection quits as specified. This allows the option's use even when only one side of the connection supports it. You can tell the remote side about the time -limit using \fB\-\-remote-option\fP (\fB\-M\fP), should the need arise. Do +limit using \fB\-\-remote\-option\fP (\fB\-M\fP), should the need arise. Do keep in mind that the remote host may have a different default timezone than your local host. .IP "\fB\-\-fsync\fP" Cause the receiving side to fsync each finished file. This may slow down the transfer, but can help to provide peace of mind when updating critical files. -.IP "\fB\-\-write-batch=FILE\fP" +.IP "\fB\-\-write\-batch=FILE\fP" Record a file that can later be applied to another identical destination -with \fB\-\-read-batch\fP. See the "BATCH MODE" section for details, and -also the \fB\-\-only-write-batch\fP option. +with \fB\-\-read\-batch\fP. See the "BATCH MODE" section for details, and +also the \fB\-\-only\-write\-batch\fP option. .IP This option overrides the negotiated checksum & compress lists and always -negotiates a choice based on old-school md5/md4/zlib choices. If you want -a more modern choice, use the \fB\-\-checksum-choice\fP (\fB\-\-cc\fP) and/or -\fB\-\-compress-choice\fP (\fB\-\-zc\fP) options. -.IP "\fB\-\-only-write-batch=FILE\fP" -Works like \fB\-\-write-batch\fP, except that no updates are made on the +negotiates a choice based on old-school md5/md4/zlib choices. This means +batch mode is not compatible with newer compression choices such as zstd or +lz4. +.IP "\fB\-\-only\-write\-batch=FILE\fP" +Works like \fB\-\-write\-batch\fP, except that no updates are made on the destination system when creating the batch. This lets you transport the changes to the destination system via some other means and then apply the -changes via \fB\-\-read-batch\fP. +changes via \fB\-\-read\-batch\fP. .IP Note that you can feel free to write the batch directly to some portable media: if this media fills to capacity before the end of the transfer, you @@ -3820,16 +4086,16 @@ system because this allows the batched data to be diverted from the sender into the batch file without having to flow over the wire to the receiver (when pulling, the sender is remote, and thus can't write the batch). -.IP "\fB\-\-read-batch=FILE\fP" +.IP "\fB\-\-read\-batch=FILE\fP" Apply all of the changes stored in FILE, a file previously generated by -\fB\-\-write-batch\fP. If \fIFILE\fP is \fB\-\fP, the batch data will be read +\fB\-\-write\-batch\fP. If \fIFILE\fP is \fB\-\fP, the batch data will be read from standard input. See the "BATCH MODE" section for details. .IP "\fB\-\-protocol=NUM\fP" Force an older protocol version to be used. This is useful for creating a batch file that is compatible with an older version of rsync. For -instance, if rsync 2.6.4 is being used with the \fB\-\-write-batch\fP +instance, if rsync 2.6.4 is being used with the \fB\-\-write\-batch\fP option, but rsync 2.6.3 is what will be used to run the -\fB\-\-read-batch\fP option, you should use "\-\-protocol=28" when creating +\fB\-\-read\-batch\fP option, you should use "\-\-protocol=28" when creating the batch file to force the older protocol version to be used in the batch file (assuming you can't upgrade the rsync on the reading system). .IP "\fB\-\-iconv=CONVERT_SPEC\fP" @@ -3839,7 +4105,7 @@ what conversion to do by giving a local and a remote charset separated by a comma in the order \fB\-\-iconv=LOCAL,REMOTE\fP, e.g. \fB\-\-iconv=utf8,iso88591\fP. This order ensures that the option will stay the same whether you're -pushing or pulling files. Finally, you can specify either \fB\-\-no-iconv\fP or +pushing or pulling files. Finally, you can specify either \fB\-\-no\-iconv\fP or a CONVERT_SPEC of "\-" to turn off any conversion. The default setting of this option is site-specific, and can also be affected via the \fBRSYNC_ICONV\fP environment variable. @@ -3847,9 +4113,9 @@ For a list of what charset names your local iconv library supports, you can run "\fBiconv\ \-\-list\fP". .IP -If you specify the \fB\-\-secluded-args\fP (\fB\-s\fP) option, rsync will +If you specify the \fB\-\-secluded\-args\fP (\fB\-s\fP) option, rsync will translate the filenames you specify on the command-line that are being sent -to the remote host. See also the \fB\-\-files-from\fP option. +to the remote host. See also the \fB\-\-files\-from\fP option. .IP Note that rsync does not do any conversion of names in filter files (including include/exclude files). It is up to you to ensure that you're @@ -3876,10 +4142,10 @@ If rsync was compiled without support for IPv6, the \fB\-\-ipv6\fP option will have no effect. The \fBrsync\ \-\-version\fP output will contain "\fBno\ IPv6\fP" if is the case. -.IP "\fB\-\-checksum-seed=NUM\fP" +.IP "\fB\-\-checksum\-seed=NUM\fP" Set the checksum seed to the integer NUM. This 4 byte checksum seed is included in each block and MD4 file checksum calculation (the more modern -MD5 file checksums don't use a seed). By default the checksum seed is +file checksums don't use a seed). By default the checksum seed is generated by the server and defaults to the current \fBtime\fP(). This option is used to set a specific checksum seed, which is useful for applications that want repeatable block checksums, or in the case where the @@ -3935,12 +4201,12 @@ rsync --daemon -M pidfile=/path/rsync.pid .fi .RE -.IP "\fB\-\-no-detach\fP" +.IP "\fB\-\-no\-detach\fP" When running as a daemon, this option instructs rsync to not detach itself and become a background process. This option is required when running as a service on Cygwin, and may also be useful when rsync is supervised by a program such as \fBdaemontools\fP or AIX's \fBSystem\ Resource\ Controller\fP. -\fB\-\-no-detach\fP is also recommended when rsync is run under a debugger. This +\fB\-\-no\-detach\fP is also recommended when rsync is run under a debugger. This option has no effect if rsync is run from inetd or sshd. .IP "\fB\-\-port=PORT\fP" This specifies an alternate TCP port number for the daemon to listen on @@ -3948,18 +4214,18 @@ .IP See also the client version of the \fB\-\-port\fP option and the port global setting in the rsyncd.conf manpage. -.IP "\fB\-\-log-file=FILE\fP" +.IP "\fB\-\-log\-file=FILE\fP" This option tells the rsync daemon to use the given log-file name instead of using the "\fBlog\ file\fP" setting in the config file. .IP -See also the client version of the \fB\-\-log-file\fP option. -.IP "\fB\-\-log-file-format=FORMAT\fP" +See also the client version of the \fB\-\-log\-file\fP option. +.IP "\fB\-\-log\-file\-format=FORMAT\fP" This option tells the rsync daemon to use the given FORMAT string instead of using the "\fBlog\ format\fP" setting in the config file. It also enables "\fBtransfer\ logging\fP" unless the string is empty, in which case transfer logging is turned off. .IP -See also the client version of the \fB\-\-log-file-format\fP +See also the client version of the \fB\-\-log\-file\-format\fP option. .IP "\fB\-\-sockopts\fP" This overrides the \fBsocket\ options\fP @@ -4016,9 +4282,9 @@ affect the contents of directories that rsync is "recursing" into, but they can also affect a top-level item in the transfer that was specified as a argument. .P -The default for any unmatched file/dir is for it to be included in the -transfer, which puts the file/dir into the sender's file list. The use of an -exclude rule causes one or more matching files/dirs to be left out of the +The default for any unmatched file/directory is for it to be included in the +transfer, which puts the file/directory into the sender's file list. The use of an +exclude rule causes one or more matching files/directories to be left out of the sender's file list. An include rule can be used to limit the effect of an exclude rule that is matching too many files. .P @@ -4094,7 +4360,7 @@ .P The following command does not need an include of the "x" directory because it is not a part of the transfer (note the trailing slash). Running this command -would copy just "\fB/tmp/x/file.txt\fP" because the "y" and "z" dirs get excluded: +would copy just "\fB/tmp/x/file.txt\fP" because the "y" and "z" directories get excluded: .RS 4 .P .nf @@ -4151,7 +4417,7 @@ .P Rsync supports old-style include/exclude rules and new-style filter rules. The older rules are specified using \fB\-\-include\fP and \fB\-\-exclude\fP as -well as the \fB\-\-include-from\fP and \fB\-\-exclude-from\fP. These are +well as the \fB\-\-include\-from\fP and \fB\-\-exclude\-from\fP. These are limited in behavior but they don't require a "\-" or "+" prefix. An old-style exclude rule is turned into a "\fB\-\ name\fP" filter rule (with no modifiers) and an old-style include rule is turned into a "\fB+\ name\fP" filter rule (with no @@ -4186,7 +4452,7 @@ .IP "\fBdir-merge,\ ':'\fP" specifies a per-directory merge-file. Using this kind of filter rule requires that you trust the sending side's filter checking, so -it has the side-effect mentioned under the \fB\-\-trust-sender\fP option. +it has the side-effect mentioned under the \fB\-\-trust\-sender\fP option. .IP "\fBhide,\ 'H'\fP" specifies a pattern for hiding files from the transfer. Equivalent to a sender-only exclude, so \fB\-f'H\ foo'\fP could also be specified @@ -4202,7 +4468,7 @@ files that match the pattern are not protected. Equivalent to a receiver-only include, so \fB\-f'R\ foo'\fP could also be specified as \fB\-f'+r\ foo'\fP. .IP "\fBclear,\ '!'\fP" -clears the current include/exclude list (takes no arg) +clears the current include/exclude list (takes no argument) .P When rules are being read from a file (using merge or dir-merge), empty lines are ignored, as are whole-line comments that start with a '\fB#\fP' (filename rules @@ -4211,8 +4477,8 @@ Note also that the \fB\-\-filter\fP, \fB\-\-include\fP, and \fB\-\-exclude\fP options take one rule/pattern each. To add multiple ones, you can repeat the options on the command-line, use the merge-file syntax of -the \fB\-\-filter\fP option, or the \fB\-\-include-from\fP / -\fB\-\-exclude-from\fP options. +the \fB\-\-filter\fP option, or the \fB\-\-include\-from\fP / +\fB\-\-exclude\-from\fP options. .P .SS "PATTERN MATCHING RULES" .P @@ -4280,10 +4546,10 @@ .IP o Option \fB\-f'\-\ foo/'\fP would exclude any directory named foo .IP o -Option \fB\-f'\-\ foo/*/bar'\fP would exclude any file/dir named bar which is at two +Option \fB\-f'\-\ foo/*/bar'\fP would exclude any file/directory named bar which is at two levels below a directory named foo (if foo is in the transfer) .IP o -Option \fB\-f'\-\ /foo/**/bar'\fP would exclude any file/dir named bar that was two +Option \fB\-f'\-\ /foo/**/bar'\fP would exclude any file/directory named bar that was two or more levels below a top-level directory named foo (note that /foo/bar is \fBnot\fP excluded by this) .IP o @@ -4303,19 +4569,19 @@ absolute pathname of the current item. For example, \fB\-f'\-/\ /etc/passwd'\fP would exclude the passwd file any time the transfer was sending files from the "/etc" directory, and "\-/ subdir/foo" would always exclude "foo" when it -is in a dir named "subdir", even if "foo" is at the root of the current +is in a directory named "subdir", even if "foo" is at the root of the current transfer. .IP o A \fB!\fP specifies that the include/exclude should take effect if the pattern fails to match. For instance, \fB\-f'\-!\ */'\fP would exclude all non-directories. .IP o A \fBC\fP is used to indicate that all the global CVS-exclude rules should be -inserted as excludes in place of the "\-C". No arg should follow. +inserted as excludes in place of the "\-C". No argument should follow. .IP o An \fBs\fP is used to indicate that the rule applies to the sending side. When a rule affects the sending side, it affects what files are put into the sender's file list. The default is for a rule to affect both sides unless -\fB\-\-delete-excluded\fP was specified, in which case default rules become +\fB\-\-delete\-excluded\fP was specified, in which case default rules become sender-side only. See also the hide (H) and show (S) rules, which are an alternate way to specify sending-side includes/excludes. .IP o @@ -4326,13 +4592,13 @@ .IP o A \fBp\fP indicates that a rule is perishable, meaning that it is ignored in directories that are being deleted. For instance, the -\fB\-\-cvs-exclude\fP (\fB\-C\fP) option's default rules that exclude things +\fB\-\-cvs\-exclude\fP (\fB\-C\fP) option's default rules that exclude things like "CVS" and "\fB*.o\fP" are marked as perishable, and will not prevent a directory that was removed on the source from being deleted on the destination. .IP o An \fBx\fP indicates that a rule affects xattr names in xattr copy/delete -operations (and is thus ignored when matching file/dir names). If no +operations (and is thus ignored when matching file/directory names). If no xattr-matching rules are specified, a default xattr filtering rule is used (see the \fB\-\-xattrs\fP option). .P @@ -4435,7 +4701,7 @@ transfer). .P If a per-directory merge-file is specified with a path that is a parent -directory of the first transfer directory, rsync will scan all the parent dirs +directory of the first transfer directory, rsync will scan all the parent directories from that starting point to the transfer directory for the indicated per-directory file. For instance, here is a common filter (see \fB\-F\fP): .RS 4 @@ -4463,13 +4729,13 @@ .P The first two commands above will look for ".rsync-filter" in "/" and "/src" before the normal scan begins looking for the file in "/src/path" and its -subdirectories. The last command avoids the parent-dir scan and only looks for +subdirectories. The last command avoids the parent-directory scan and only looks for the ".rsync-filter" files in each directory that is a part of the transfer. .P If you want to include the contents of a ".cvsignore" in your patterns, you should use the rule ":C", which creates a dir-merge of the .cvsignore file, but parsed in a CVS-compatible manner. You can use this to affect where the -\fB\-\-cvs-exclude\fP (\fB\-C\fP) option's inclusion of the per-directory +\fB\-\-cvs\-exclude\fP (\fB\-C\fP) option's inclusion of the per-directory \&.cvsignore file gets placed into your rules by putting the ":C" wherever you like in your filter rules. Without this, rsync would add the dir-merge rule for the .cvsignore file at the end of all your other rules (giving it a lower @@ -4488,7 +4754,7 @@ .P Both of the above rsync commands are identical. Each one will merge all the per-directory .cvsignore rules in the middle of the list rather than at the -end. This allows their dir-specific rules to supersede the rules that follow +end. This allows their directory-specific rules to supersede the rules that follow the :C instead of being subservient to all your rules. To affect the other CVS exclude rules (i.e. the default list of exclusions, the contents of $HOME/.cvsignore, and the value of $CVSIGNORE) you should omit the \fB\-C\fP @@ -4564,7 +4830,7 @@ .P The easiest way to see what name you should filter is to just look at the output when using \fB\-\-verbose\fP and put a / in front of the name (use the -\fB\-\-dry-run\fP option if you're not yet ready to copy any files). +\fB\-\-dry\-run\fP option if you're not yet ready to copy any files). .P .SS "PER-DIRECTORY RULES AND DELETE" .P @@ -4583,7 +4849,7 @@ However, if you want to do a delete on the receiving side AND you want some files to be excluded from being deleted, you'll need to be sure that the receiving side knows what files to exclude. The easiest way is to include the -per-directory merge files in the transfer and use \fB\-\-delete-after\fP, +per-directory merge files in the transfer and use \fB\-\-delete\-after\fP, because this ensures that the receiving side gets all the same exclude rules as the sending side before it tries to delete anything: .RS 4 @@ -4646,7 +4912,7 @@ "foo" if deletions are requested. .P Given that the files are still in the sender's file list, the -\fB\-\-prune-empty-dirs\fP option will not judge a directory as being empty +\fB\-\-prune\-empty\-dirs\fP option will not judge a directory as being empty even if it contains only files that the transfer rules omitted. .P Similarly, a transfer rule does not have any extra effect on which files are @@ -4655,7 +4921,7 @@ .P Examples of transfer rules include the default "quick check" algorithm (which compares size & modify time), the \fB\-\-update\fP option, the -\fB\-\-max-size\fP option, the \fB\-\-ignore-non-existing\fP option, and a +\fB\-\-max\-size\fP option, the \fB\-\-ignore\-non\-existing\fP option, and a few others. .P .SH "BATCH MODE" @@ -4682,7 +4948,7 @@ .P For your convenience, a script file is also created when the write-batch option is used: it will be named the same as the batch file with ".sh" appended. This -script file contains a command-line suitable for updating a destination tree +script file contains a command line suitable for updating a destination tree using the associated batch file. It can be executed using a Bourne (or Bourne-like) shell, optionally passing in an alternate destination tree pathname which is then used instead of the original destination path. This is @@ -4723,9 +4989,9 @@ The second example reads the batch data via standard input so that the batch file doesn't need to be copied to the remote machine first. This example avoids the foo.sh script because it needed to use a modified -\fB\-\-read-batch\fP option, but you could edit the script file if you +\fB\-\-read\-batch\fP option, but you could edit the script file if you wished to make use of it (just be sure that no other option is trying to use -standard input, such as the \fB\-\-exclude-from=\-\fP option). +standard input, such as the \fB\-\-exclude\-from=\-\fP option). .P Caveats: .P @@ -4747,14 +5013,14 @@ version in the batch file is too new for the batch-reading rsync to handle. See also the \fB\-\-protocol\fP option for a way to have the creating rsync generate a batch file that an older rsync can understand. (Note that batch -files changed format in version 2.6.3, so mixing versions older than that with -newer versions will not work.) +files changed format in version 2.6.3 (September 2004), so mixing versions +older than that with newer versions will not work.) .P When reading a batch file, rsync will force the value of certain options to match the data in the batch file if you didn't set them to the same as the batch-writing command. Other options can (and should) be changed. For -instance \fB\-\-write-batch\fP changes to \fB\-\-read-batch\fP, -\fB\-\-files-from\fP is dropped, and the \fB\-\-filter\fP / +instance \fB\-\-write\-batch\fP changes to \fB\-\-read\-batch\fP, +\fB\-\-files\-from\fP is dropped, and the \fB\-\-filter\fP / \fB\-\-include\fP / \fB\-\-exclude\fP options are not needed unless one of the \fB\-\-delete\fP options is specified. .P @@ -4763,10 +5029,7 @@ script file. An advanced user can use this to modify the exclude list if a change in what gets deleted by \fB\-\-delete\fP is desired. A normal user can ignore this detail and just use the shell script as an easy way to run the -appropriate \fB\-\-read-batch\fP command for the batched data. -.P -The original batch mode in rsync was based on "rsync+", but the latest -version uses a new implementation. +appropriate \fB\-\-read\-batch\fP command for the batched data. .P .SH "SYMBOLIC LINKS" .P @@ -4781,17 +5044,17 @@ them with the same target on the destination. Note that \fB\-\-archive\fP implies \fB\-\-links\fP. .P -If \fB\-\-copy-links\fP is specified, then symlinks are "collapsed" by +If \fB\-\-copy\-links\fP is specified, then symlinks are "collapsed" by copying their referent, rather than the symlink. .P Rsync can also distinguish "safe" and "unsafe" symbolic links. An example where this might be used is a web site mirror that wishes to ensure that the rsync module that is copied does not include symbolic links to \fB/etc/passwd\fP in -the public section of the site. Using \fB\-\-copy-unsafe-links\fP will cause +the public section of the site. Using \fB\-\-copy\-unsafe\-links\fP will cause any links to be copied as the file they point to on the destination. Using -\fB\-\-safe-links\fP will cause unsafe links to be omitted by the receiver. +\fB\-\-safe\-links\fP will cause unsafe links to be omitted by the receiver. (Note that you must specify or imply \fB\-\-links\fP for -\fB\-\-safe-links\fP to have any effect.) +\fB\-\-safe\-links\fP to have any effect.) .P Symbolic links are considered unsafe if they are absolute symlinks (start with \fB/\fP), empty, or if they contain enough ".." components to ascend from the top @@ -4801,28 +5064,28 @@ order of precedence, so if your combination of options isn't mentioned, use the first line that is a complete subset of your options: .P -.IP "\fB\-\-copy-links\fP" +.IP "\fB\-\-copy\-links\fP" Turn all symlinks into normal files and directories (leaving no symlinks in the transfer for any other options to affect). -.IP "\fB\-\-copy-dirlinks\fP" +.IP "\fB\-\-copy\-dirlinks\fP" Turn just symlinks to directories into real directories, leaving all other symlinks to be handled as described below. -.IP "\fB\-\-links\ \-\-copy-unsafe-links\fP" +.IP "\fB\-\-links\ \-\-copy\-unsafe\-links\fP" Turn all unsafe symlinks into files and create all safe symlinks. -.IP "\fB\-\-copy-unsafe-links\fP" +.IP "\fB\-\-copy\-unsafe\-links\fP" Turn all unsafe symlinks into files, noisily skip all safe symlinks. -.IP "\fB\-\-links\ \-\-safe-links\fP" +.IP "\fB\-\-links\ \-\-safe\-links\fP" The receiver skips creating unsafe symlinks found in the transfer and creates the safe ones. .IP "\fB\-\-links\fP" Create all symlinks. .P -For the effect of \fB\-\-munge-links\fP, see the discussion in that option's +For the effect of \fB\-\-munge\-links\fP, see the discussion in that option's section. .P -Note that the \fB\-\-keep-dirlinks\fP option does not effect symlinks in the +Note that the \fB\-\-keep\-dirlinks\fP option does not affect symlinks in the transfer but instead affects how rsync treats a symlink to a directory that already exists on the receiving side. See that option's section for a warning. .P @@ -4862,7 +5125,7 @@ .IP o \fB2\fP \- Protocol incompatibility .IP o -\fB3\fP \- Errors selecting input/output files, dirs +\fB3\fP \- Errors selecting input/output files, directories .IP o .P .RS @@ -4876,8 +5139,6 @@ .IP o \fB5\fP \- Error starting client-server protocol .IP o -\fB6\fP \- Daemon unable to append to log-file -.IP o \fB10\fP \- Error in socket I/O .IP o \fB11\fP \- Error in file I/O @@ -4888,7 +5149,13 @@ .IP o \fB14\fP \- Error in IPC code .IP o -\fB20\fP \- Received SIGUSR1 or SIGINT +\fB15\fP \- Sibling process crashed (e.g. core dumped). +.IP o +\fB16\fP \- Sibling process was killed by a signal. +.IP o +\fB19\fP \- Received SIGUSR1. +.IP o +\fB20\fP \- Received SIGINT, SIGTERM, or SIGHUP. .IP o \fB21\fP \- Some error returned by \fBwaitpid()\fP .IP o @@ -4898,7 +5165,7 @@ .IP o \fB24\fP \- Partial transfer due to vanished source files .IP o -\fB25\fP \- The \-\-max-delete limit stopped deletions +\fB25\fP \- The \-\-max\-delete limit stopped deletions .IP o \fB30\fP \- Timeout in data send/receive .IP o @@ -4908,30 +5175,30 @@ .P .IP "\fBCVSIGNORE\fP" The CVSIGNORE environment variable supplements any ignore patterns in -\&.cvsignore files. See the \fB\-\-cvs-exclude\fP option for more details. +\&.cvsignore files. See the \fB\-\-cvs\-exclude\fP option for more details. .IP "\fBRSYNC_ICONV\fP" Specify a default \fB\-\-iconv\fP setting using this environment -variable. First supported in 3.0.0. +variable. .IP "\fBRSYNC_OLD_ARGS\fP" -Specify a "1" if you want the \fB\-\-old-args\fP option to be enabled by +Specify a "1" if you want the \fB\-\-old\-args\fP option to be enabled by default, a "2" (or more) if you want it to be enabled in the repeated-option state, or a "0" to make sure that it is disabled by default. When this environment variable is set to a non-zero value, it supersedes the \fBRSYNC_PROTECT_ARGS\fP variable. .IP -This variable is ignored if \fB\-\-old-args\fP, \fB\-\-no-old-args\fP, or -\fB\-\-secluded-args\fP is specified on the command line. +This variable is ignored if \fB\-\-old\-args\fP, \fB\-\-no\-old\-args\fP, or +\fB\-\-secluded\-args\fP is specified on the command line. .IP -First supported in 3.2.4. +First supported in 3.2.4 (April 2022). .IP "\fBRSYNC_PROTECT_ARGS\fP" -Specify a non-zero numeric value if you want the \fB\-\-secluded-args\fP +Specify a non-zero numeric value if you want the \fB\-\-secluded\-args\fP option to be enabled by default, or a zero value to make sure that it is disabled by default. .IP -This variable is ignored if \fB\-\-secluded-args\fP, \fB\-\-no-secluded-args\fP, -or \fB\-\-old-args\fP is specified on the command line. +This variable is ignored if \fB\-\-secluded\-args\fP, \fB\-\-no\-secluded\-args\fP, +or \fB\-\-old\-args\fP is specified on the command line. .IP -First supported in 3.1.0. Starting in 3.2.4, this variable is ignored if +Starting in 3.2.4 (April 2022), this variable is ignored if \fBRSYNC_OLD_ARGS\fP is set to a non-zero value. .IP "\fBRSYNC_RSH\fP" This environment variable allows you to override the default shell used as @@ -4953,7 +5220,7 @@ .IP "\fBRSYNC_PARTIAL_DIR\fP" This environment variable specifies the directory to use for a \fB\-\-partial\fP transfer without implying that partial transfers be -enabled. See the \fB\-\-partial-dir\fP option for full details. +enabled. See the \fB\-\-partial\-dir\fP option for full details. .IP "\fBRSYNC_COMPRESS_LIST\fP" This environment variable allows you to customize the negotiation of the compression algorithm by specifying an alternate order or a reduced list of @@ -4963,10 +5230,10 @@ This environment variable allows you to customize the negotiation of the checksum algorithm by specifying an alternate order or a reduced list of names. Use the command \fBrsync\ \-\-version\fP to see the available checksum -names. See the \fB\-\-checksum-choice\fP option for full details. +names. See the \fB\-\-checksum\-choice\fP option for full details. .IP "\fBRSYNC_MAX_ALLOC\fP" This environment variable sets an allocation maximum as if you had used the -\fB\-\-max-alloc\fP option. +\fB\-\-max\-alloc\fP option. .IP "\fBRSYNC_PORT\fP" This environment variable is not read by rsync, but is instead set in its sub-environment when rsync is running the remote shell in combination @@ -4999,7 +5266,7 @@ Times are transferred as *nix time_t values. .IP o When transferring to FAT filesystems rsync may re-sync unmodified files. See -the comments on the \fB\-\-modify-window\fP option. +the comments on the \fB\-\-modify\-window\fP option. .IP o File permissions, devices, etc. are transferred as native numerical values. .IP o @@ -5011,7 +5278,7 @@ .P .SH "VERSION" .P -This manpage is current for version 3.4.1 of rsync. +This manpage is current for version 3.5.0 of rsync. .P .SH "INTERNAL OPTIONS" .P @@ -5061,6 +5328,8 @@ Rsync was originally written by Andrew Tridgell and Paul Mackerras. Many people from around the world have helped to maintain and improve it. .P +Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024. +.P Mailing lists for support and development are available at .UR https://lists.samba.org/ .UE . diff -Nru rsync-3.4.1+ds1/rsync.1.html rsync-3.5.0+ds1/rsync.1.html --- rsync-3.4.1+ds1/rsync.1.html 2025-01-15 20:49:30.000000000 +0000 +++ rsync-3.5.0+ds1/rsync.1.html 2026-08-13 00:05:31.000000000 +0000 @@ -32,7 +32,7 @@

NAME

-

rsync -⁠ a fast, versatile, remote (and local) file-copying tool

+

rsync -⁠ a fast, versatile, local/remote file-copying tool

SYNOPSIS

Local:
     rsync [OPTION...] SRC... [DEST]
@@ -49,15 +49,15 @@
         rsync [OPTION...] rsync://[USER@]HOST[:PORT]/SRC... [DEST]
     Push:
         rsync [OPTION...] SRC... [USER@]HOST::DEST
-        rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST)
+        rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST
 
-

Usages with just one SRC arg and no DEST arg will list the source files instead +

Usages with just one SRC argument and no DEST argument will list the source files instead of copying.

-

The online version of this manpage (that includes cross-linking of topics) +

The online version of this manpage (which includes cross-linking of topics) is available at https://download.samba.org/pub/rsync/rsync.1.

DESCRIPTION

Rsync is a fast and extraordinarily versatile file copying tool. It can copy -locally, to/from another host over any remote shell, or to/from a remote rsync +locally, to/from another host over a remote shell, or to/from a remote rsync daemon. It offers a large number of options that control every aspect of its behavior and permit very flexible specification of the set of files to be copied. It is famous for its delta-transfer algorithm, which reduces the @@ -72,7 +72,7 @@ the file's data does not need to be updated.

Some of the additional features of rsync are:

    -
  • support for copying links, devices, owners, groups, and permissions
  • +
  • support for copying hard and soft links, devices, owners, groups, and permissions
  • exclude and exclude-from options similar to GNU tar
  • a CVS exclude mode for ignoring the same files that CVS would ignore
  • can use any transparent remote shell, including ssh or rsh
  • @@ -82,35 +82,51 @@

GENERAL

Rsync copies files either to or from a remote host, or locally on the current -host (it does not support copying files between two remote hosts).

+host. It does not support copying files between two different remote hosts.

There are two different ways for rsync to contact a remote system: using a -remote-shell program as the transport (such as ssh or rsh) or contacting an +remote-shell program as the transport (such as ssh or rsh), or by contacting an rsync daemon directly via TCP. The remote-shell transport is used whenever the source or destination path contains a single colon (:) separator after a host -specification. Contacting an rsync daemon directly happens when the source or +specification. In this case, the local rsync process uses the remote +shell program to start an rsync process on the remote system. The two +rsync processes then communicate via the remote shell program to +accomplish the desired transfers. In this case, the files that are +accessible on the remote system are those accessible to the user ID +under which the remote shell starts the remote rsync process.

+

Contacting an rsync daemon directly happens when the source or destination path contains a double colon (::) separator after a host -specification, OR when an rsync:// URL is specified (see also the USING -RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION section for an -exception to this latter rule).

-

As a special case, if a single source arg is specified without a destination, +specification, OR when an rsync:// URL is specified. +In this case, the remote rsync daemon process will +provide access only to specific sets of files, called modules, +according to its configuration. +It is also possible to contact an rsync daemon via a remote-shell +transport; see the USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL +CONNECTION section for details.

+

As a special case, if a single source argument is specified without a destination, the files are listed in an output format similar to "ls -l".

-

As expected, if neither the source or destination path specify a remote host, +

If neither the source or destination path specify a remote host, the copy occurs locally (see also the --list-only option).

-

Rsync refers to the local side as the client and the remote side as the server. -Don't confuse server with an rsync daemon. A daemon is always a server, but a -server can be either a daemon or a remote-shell spawned process.

+

Rsync refers to the local side as the client and the remote side as +the server. The server is not to be confused with an rsync daemon. +A daemon is always a server, but a server can be either a daemon or a +remote-shell spawned process. If the transfer is local, then the +rsync subprocess which receives the contents of the files being +transferred is the server.

SETUP

-

See the file README.md for installation instructions.

+

Most Linux distributions include rsync as a package that can be +installed using the package manager. +If for any reason you need to build and install rsync from source, +see the file README.md for installation instructions.

Once installed, you can use rsync to any machine that you can access via a -remote shell (as well as some that you can access using the rsync daemon-mode -protocol). For remote transfers, a modern rsync uses ssh for its -communications, but it may have been configured to use a different remote shell -by default, such as rsh or remsh.

-

You can also specify any remote shell you like, either by using the -e +remote shell, or that has an rsync daemon available. +For remote transfers, rsync normally uses ssh for its +communications. Alternatively, +you can specify any remote shell you like, either by using the -e command line option, or by setting the RSYNC_RSH environment variable.

Note that rsync must be installed on both the source and destination machines.

USAGE

-

You use rsync in the same way you use rcp. You must specify a source and a +

You use rsync in a similar way to other file-copying commands such +as cp and rcp. You must specify a source and a destination, one of which may be remote.

Perhaps the best way to explain the syntax is with some examples:

@@ -122,8 +138,8 @@ exist on the remote system then the rsync remote-update protocol is used to update the file by sending only the differences in the data. Note that the expansion of wildcards on the command-line (*.c) into a list of files is -handled by the shell before it runs rsync and not by rsync itself (exactly the -same as all other Posix-style programs).

+handled by the shell before it runs rsync and not by rsync itself (as for +other Posix-style programs).

rsync -avz foo:src/bar /data/tmp
 
@@ -175,33 +191,37 @@
rsync -ai foo/ bar/
 
-

Rsync also has the ability to customize a destination file's name when copying -a single item. The rules for this are:

+

Rsync also has the ability to copy a single file to a +destination file with a different name. The rules for this are:

  • The transfer list must consist of a single item (either a file or an empty directory)
  • The final element of the destination path must not exist as a directory
  • The destination path must not have been specified with a trailing slash
-

Under those circumstances, rsync will set the name of the destination's single -item to the last element of the destination path. Keep in mind that it is best -to only use this idiom when copying a file and use the above trailing-slash +

Under those circumstances, rsync will use the specified destination +path as the filename of the destination (rather than constructing a +filename using the last element of the source path). It is best +to use this idiom only when copying a file, and to use the above trailing-slash idiom when copying a directory.

-

The following example copies the foo.c file as bar.c in the save dir +

The following example copies the foo.c file as bar.c in the save directory (assuming that bar.c isn't a directory):

rsync -ai src/foo.c save/bar.c
 
-

The single-item copy rule might accidentally bite you if you unknowingly copy a -single item and specify a destination dir that doesn't exist (without using a -trailing slash). For example, if src/*.c matches one file and save/dir -doesn't exist, this will confuse you by naming the destination file save/dir:

+

The single-item copy rule can give unexpected results if a wildcard +pattern for the source yields a single item, and the destination, +though specified without a trailing slash, is intended to be a +directory, but that directory does not exist. +For example, if src/*.c matches one file and save/dir +doesn't exist, this will perhaps confusingly name the destination file +save/dir:

rsync -ai src/*.c save/dir
 
-

To prevent such an accident, either make sure the destination dir exists or +

To prevent such an accident, either make sure the destination directory exists or specify the destination path with a trailing slash:

rsync -ai src/*.c save/dir/
@@ -210,58 +230,110 @@
 

SORTED TRANSFER ORDER

Rsync always sorts the specified filenames into its internal transfer list. This handles the merging together of the contents of identically named -directories, makes it easy to remove duplicate filenames. It can, however, -confuse someone when the files are transferred in a different order than what -was given on the command-line.

-

If you need a particular file to be transferred prior to another, either -separate the files into different rsync calls, or consider using ---delay-updates (which doesn't affect the sorted transfer order, but -does make the final file-updating phase happen much more rapidly).

-

MULTI-HOST SECURITY

-

Rsync takes steps to ensure that the file requests that are shared in a -transfer are protected against various security issues. Most of the potential -problems arise on the receiving side where rsync takes steps to ensure that the -list of files being transferred remains within the bounds of what was -requested.

-

Toward this end, rsync 3.1.2 and later have aborted when a file list contains -an absolute or relative path that tries to escape out of the top of the -transfer. Also, beginning with version 3.2.5, rsync does two more safety -checks of the file list to (1) ensure that no extra source arguments were added -into the transfer other than those that the client requested and (2) ensure -that the file list obeys the exclude rules that were sent to the sender.

-

For those that don't yet have a 3.2.5 client rsync (or those that want to be -extra careful), it is safest to do a copy into a dedicated destination -directory for the remote files when you don't trust the remote host. For -example, instead of doing an rsync copy into your home directory:

+directories and makes it easy to remove duplicate filenames. It can, however, +result in files being transferred in a different order from that +specified on the command-line.

+

If you need a particular file to be transferred prior to another, the +only way to be guaranteed of that is to separate the files into +different rsync calls. If it is sufficient for the destination files to +appear at almost the same time, consider using +--delay-updates, which doesn't affect the sorted transfer order, but +does make the final file-updating phase happen much more rapidly.

+

SECURITY

+

Rsync is frequently run across a network and with elevated privileges, so it is +worth thinking about who you are trusting and with what. This section is a +practical guide for safe use; the project's SECURITY.md describes the full +threat model and the per-platform residuals.

+

Use an authenticated, encrypted transport

+

A plain host:path transfer runs over your remote shell (ssh by default), which +authenticates the peer and encrypts the connection -⁠-⁠ this is the safe default. +A direct daemon connection (host::module or rsync://) is not encrypted +and its authentication is comparatively weak, so do not send sensitive data +across an untrusted network. +Instead tunnel it over ssh +(see USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION) or wrap it in +TLS with rsync-ssl(1), setting RSYNC_SSL_CA_CERT so that the +server certificate's chain and hostname are verified. When you must supply a +daemon password non-interactively, put it in a --password-file that is +readable only by you (mode 600) rather than on the command line.

+

Copying from an untrusted sending host

+

When receiving from a remote host, rsync takes steps to ensure that a +corrupted or compromised remote rsync process can't cause the local +rsync process to access files beyond the bounds of what was requested. +Rsync ensures that the list of files being +transferred stays within the requested tree, and will abort when a +file list contains an absolute or relative path that tries to escape +the top of the transfer. It also verifies that no extra source arguments +were added to the transfer and that the file list obeys the exclude rules +that were sent to the sender.

+

For those who want to be extra careful, +it is safest to copy untrusted remote files into a dedicated +destination directory. For example, instead of copying into your home +directory:

rsync -aiv host1:dir1 ~
 
-

Dedicate a "host1-files" dir to the remote content:

+

dedicate a "host1-files" directory to the remote content:

rsync -aiv host1:dir1 ~/host1-files
 

See the --trust-sender option for additional details.

-

CAUTION: it is not particularly safe to use rsync to copy files from a -case-preserving filesystem to a case-ignoring filesystem. If you must perform -such a copy, you should either disable symlinks via --no-links or enable the -munging of symlinks via --munge-links (and make sure you use the -right local or remote option). This will prevent rsync from doing potentially -dangerous things if a symlink name overlaps with a file or directory. It does -not, however, ensure that you get a full copy of all the files (since that may -not be possible when the names overlap). A potentially better solution is to -list all the source files and create a safe list of filenames that you pass to -the --files-from option. Any files that conflict in name would need -to be copied to different destination directories using more than one copy.

-

While a copy of a case-ignoring filesystem to a case-ignoring filesystem can -work out fairly well, if no --delete-during or --delete-before option is -active, rsync can potentially update an existing file on the receiving side -without noticing that the upper-/lower-case of the filename should be changed -to match the sender.

+

Copying between case-preserving and case-insensitive filesystems

+

Copying from case-preserving to case-insensitive filesystems requires +caution, because it is possible for the name of a symbolic link to +overlap with the name of a file or directory once the case +distinctions are removed, which can cause potentially dangerous +results such as files being written outside the destination directory +hierarchy. +If you must perform +such a copy, either disable symlinks via --no-links or enable the munging of +symlinks via --munge-links (and make sure you use the right local or +remote option). +This does not, however, ensure +that you get a full copy of all the files (since that may not be possible when +the names overlap); a potentially better solution is to build a safe list of +filenames and pass it to --files-from.

+ +

A malicious sender can include symlinks that point outside the destination tree +(for example at /etc/passwd). Use --safe-links to ignore any symlink +that points outside the set of files +being transferred, or --munge-links to store every symlink in a +form that is unusable on disk but recoverable later; --no-links drops symlinks +entirely. See the SYMBOLIC LINKS section for how these interact.

+

Separately, the directory and file paths that you supply on the command line -⁠-⁠ +--backup-dir, --temp-dir, --partial-dir, the +--link-dest/--compare-dest/--copy-dest basis directories, +--log-file, --files-from/--include-from/--exclude-from, +--filter merge files, --write-batch/--read-batch, +and the destination itself -⁠-⁠ are resolved so that a symlink component is followed +only when it is owned by you or by root; an attacker-planted symlink along one of +those paths is refused. --insecure-links turns that protection off +(restoring the historical follow-any-symlink behaviour); use it only when every +directory along those paths is trusted, never on a path an unprivileged user can +write.

+

Use strong checksums

+

Rsync auto-negotiates the strongest checksum that both ends support, so keeping +both local and remote rsync versions reasonably current is usually all +that is needed. You can +pin the choice with --checksum-choice (--cc, e.g. --cc=sha1 or one +of the xxHash variants) or constrain negotiation with the +RSYNC_CHECKSUM_LIST environment variable; only very old peers fall back to +MD4/MD5. This pre-transfer "does this file need updating?" checksum is separate +from the whole-file checksum rsync normally computes to verify each transferred +file afterward (verification is off when --checksum-choice=none is forced).

+

Protocol version

+

The client and server automatically negotiate the newest protocol they both +support, so a current pair is already using the most recent version; +--protocol only forces an older version for compatibility and should +not be used to downgrade a connection. Avoiding old protocols is therefore a +matter of running a current rsync on both ends (a protocol below 30 also forces +the weak MD4 authentication digest on a daemon connection -⁠-⁠ see the AUTHENTICATION +STRENGTH section of rsyncd.conf(5)).

ADVANCED USAGE

The syntax for requesting multiple files from a remote host is done by -specifying additional remote-host args in the same style as the first, or with +specifying additional remote-host arguments in the same style as the first, or with the hostname omitted. For instance, all these work:

rsync -aiv host:file1 :file2 host:file{3,4} /dest/
@@ -273,12 +345,14 @@
 command, so if the start of a follow-up path doesn't begin with the
 modname of the first path, it is assumed to be a path in the module (such as
 the extra-file1 & extra-file2 that are grabbed above).

-

Really old versions of rsync (2.6.9 and before) only allowed specifying one -remote-source arg, so some people have instead relied on the remote-shell -performing space splitting to break up an arg into multiple paths. Such -unintuitive behavior is no longer supported by default (though you can request -it, as described below).

-

Starting in 3.2.4, filenames are passed to a remote shell in such a way as to +

Because really old versions of rsync (prior to 3.0.0, released +March 2008) only allowed specifying one +remote-source argument, some people have come to rely on the remote shell +performing space splitting to break a single argument into multiple paths. Such +unintuitive behavior is no longer supported by default, though you can request +it, as described in the next paragraph. +Rsync now (since 3.2.4) passes filenames to a remote +shell in such a way as to preserve the characters you give it. Thus, if you ask for a file with spaces in the name, that's what the remote rsync looks for:

@@ -286,33 +360,41 @@

If you use scripts that have been written to manually apply extra quoting to -the remote rsync args (or to require remote arg splitting), you can ask rsync +the remote rsync arguments, or to rely on remote argument splitting, you can ask rsync to let your script handle the extra escaping. This is done by either adding the --old-args option to the rsync runs in the script (which requires a new rsync) or exporting RSYNC_OLD_ARGS=1 and RSYNC_PROTECT_ARGS=0 (which works with old or new rsync versions).

CONNECTING TO AN RSYNC DAEMON

-

It is also possible to use rsync without a remote shell as the transport. In -this case you will directly connect to a remote rsync daemon, typically using -TCP port 873. (This obviously requires the daemon to be running on the remote -system, so refer to the STARTING AN RSYNC DAEMON TO ACCEPT CONNECTIONS -section below for information on that.)

-

Using rsync in this way is the same as using it with a remote shell except -that:

+

An rsync daemon provides a way to give access to one or more directory +hierarchies on a system in a controlled way, without having to provide +shell access to the system. Each directory hierarchy is called a +"module". The names, directories, access permissions and so on for +each module are defined by the rsync daemon configuration file, +described in rsyncd.conf(5). +Connections to an rsync daemon typically use TCP port 873. The +administrator of the system would normally arrange for the rsync +daemon to be running; +refer to the STARTING AN RSYNC DAEMON TO ACCEPT CONNECTIONS +section below for information on how to do that.

+

From the client point of view, using rsync to access an rsync daemon +is similar to using it with a remote shell except that:

  • Use either double-colon syntax or rsync:// URL syntax instead of the single-colon (remote shell) syntax.
  • The first element of the "path" is actually a module name.
  • -
  • Additional remote source args can use an abbreviated syntax that omits the +
  • Additional remote source arguments can use an abbreviated syntax that omits the hostname and/or the module name, as discussed in ADVANCED USAGE.
  • The remote daemon may print a "message of the day" when you connect.
  • If you specify only the host (with no module or path) then a list of accessible modules on the daemon is output.
  • If you specify a remote source path but no destination, a listing of the matching files on the remote daemon is output.
  • -
  • The --rsh (-e) option must be omitted to avoid changing the -connection style from using a socket connection to USING RSYNC-DAEMON -FEATURES VIA A REMOTE-SHELL CONNECTION.
  • +
  • To contact an rsync daemon directly, the --rsh (-e) +option must be omitted. If it is used, rsync will use the specified +remote shell program to start an rsync daemon on the remote system. +See the USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL +CONNECTION section for information on this mode of operation.

An example that copies all the files in a remote module named "src":

@@ -324,7 +406,7 @@ by setting the environment variable RSYNC_PASSWORD to the password you want to use or using the --password-file option. This may be useful when scripting rsync.

-

WARNING: On some systems environment variables are visible to all users. On +

WARNING: On some systems, environment variables are visible to all users. On those systems using --password-file is recommended.

You may establish the connection via a web proxy by setting the environment variable RSYNC_PROXY to a hostname:port pair pointing to your web proxy. @@ -349,35 +431,46 @@

USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION

It is sometimes useful to use various features of an rsync daemon (such as named modules) without actually allowing any new socket connections into a -system (other than what is already required to allow remote-shell access). +system, other than what is already required to allow remote-shell access. Rsync supports connecting to a host using a remote shell and then spawning a -single-use "daemon" server that expects to read its config file in the home dir +single-use "daemon" server that expects to read its config file in the home directory of the remote user. This can be useful if you want to encrypt a daemon-style -transfer's data, but since the daemon is started up fresh by the remote user, -you may not be able to use features such as chroot or change the uid used by -the daemon. (For another way to encrypt a daemon transfer, consider using ssh -to tunnel a local port to a remote machine and configure a normal rsync daemon -on that remote host to only allow connections from "localhost".)

+transfer's data. However, since the daemon is started by the remote user, +you may not be able to use features which require root privilege, such +as chroot or setting the user ID used by the daemon. +(For another way to encrypt a daemon transfer, consider using ssh +to tunnel a local port to a remote machine, and configure a normal rsync daemon +on that remote host to allow connections only from "localhost".)

From the user's perspective, a daemon transfer via a remote-shell connection uses nearly the same command-line syntax as a normal rsync-daemon transfer, -with the only exception being that you must explicitly set the remote shell +with the only difference being that you must explicitly set the remote shell program on the command-line with the --rsh=COMMAND option. (Setting the RSYNC_RSH in the environment will not turn on this functionality.) For example:

rsync -av --rsh=ssh host::module /dest
 
-

If you need to specify a different remote-shell user, keep in mind that the -user@ prefix in front of the host is specifying the rsync-user value (for a -module that requires user-based authentication). This means that you must give -the '-⁠l user' option to ssh when specifying the remote-shell, as in this -example that uses the short version of the --rsh option:

+

A "user@" prefix in front of the host serves two purposes at once in this mode: +it is the rsync-user value (used to log in to a module that requires user-based +authentication) and, by default, it is also passed to the remote shell as the +login user. So the simple form

+
+
rsync -av --rsh=ssh user@host::module /dest
+
+
+

runs ssh -l user host and offers "user" as the rsync-user to the module; the +two are forced to be the same name.

+

If you need the remote-shell (ssh) login to use a different name than the +rsync-user, give an explicit '-⁠l' option to ssh in the remote-shell command. +When ssh is already told which user to log in as, rsync does not add its own +'-⁠l', so the "user@" prefix is then used only as the rsync-user. For example, +using the short version of the --rsh option:

rsync -av -e "ssh -l ssh-user" rsync-user@host::module /dest
 
-

The "ssh-user" will be used at the ssh level; the "rsync-user" will be used to -log-in to the "module".

+

Here "ssh-user" is used at the ssh level while "rsync-user" is used to log in to +the "module".

In this setup, the daemon is started by the ssh command that is accessing the system (which can be forced via the ~/.ssh/authorized_keys file, if desired). However, when accessing a daemon directly, it needs to be started beforehand.

@@ -386,9 +479,9 @@ daemon already running (or it needs to have configured something like inetd to spawn an rsync daemon for incoming connections on a particular port). For full information on how to start a daemon that will handling incoming socket -connections, see the rsyncd.conf(5) manpage -⁠-⁠ that is -the config file for the daemon, and it contains the full details for how to run -the daemon (including stand-alone and inetd configurations).

+connections, see the manpage for rsyncd.conf(5), +the config file for the daemon. The manpage contains the full details for how to run +the daemon, including stand-alone and inetd configurations.

If you're using one of the remote-shell transports for the transfer, there is no need to manually start an rsync daemon.

EXAMPLES

@@ -417,8 +510,11 @@ --archive, -a archive mode is -rlptgoD (no -A,-X,-U,-N,-H) --no-OPTION turn off an implied OPTION (e.g. --no-D) --recursive, -r recurse into directories +--inc-recursive, --i-r enable incremental recursion +--no-inc-recursive disable incremental recursion +--no-i-r same as --no-inc-recursive --relative, -R use relative path names ---no-implied-dirs don't send implied dirs with --relative +--no-implied-dirs don't send implied directories with --relative --backup, -b make backups (see --suffix & --backup-dir) --backup-dir=DIR make backups into hierarchy based in DIR --suffix=SUFFIX backup suffix (default ~ w/o --backup-dir) @@ -427,15 +523,18 @@ --append append data onto shorter files --append-verify --append w/old data in file checksum --dirs, -d transfer directories without recursing ---old-dirs, --old-d works like --dirs when talking to old rsync +--old-dirs works like --dirs when talking to old rsync +--old-d same as --old-dirs --mkpath create destination's missing path components --links, -l copy symlinks as symlinks ---copy-links, -L transform symlink into referent file/dir +--copy-links, -L transform symlink into referent file/directory --copy-unsafe-links only "unsafe" symlinks are transformed --safe-links ignore symlinks that point outside the tree +--insecure-links follow attacker-owned symlinks in operator paths +--confine-root=DIR refuse operator paths resolving outside DIR --munge-links munge symlinks to make them safe & unusable ---copy-dirlinks, -k transform symlink to dir into referent dir ---keep-dirlinks, -K treat symlinked dir on receiver as dir +--copy-dirlinks, -k transform symlink to directory into referent directory +--keep-dirlinks, -K treat symlinked directory on receiver as directory --hard-links, -H preserve hard links --perms, -p preserve permissions --executability, -E preserve executability @@ -448,6 +547,7 @@ --copy-devices copy device contents as a regular file --write-devices write to devices as files (implies --inplace) --specials preserve special files +--drop-D receiver refuses to create devices/specials -D same as --devices --specials --times, -t preserve modification times --atimes, -U preserve access (use) times @@ -461,25 +561,27 @@ --preallocate allocate dest files before writing them --dry-run, -n perform a trial run with no changes made --whole-file, -W copy files whole (w/o delta-xfer algorithm) +--no-whole-file, --no-W use the delta-xfer algorithm --checksum-choice=STR choose the checksum algorithm (aka --cc) --one-file-system, -x don't cross filesystem boundaries --block-size=SIZE, -B force a fixed checksum block-size --rsh=COMMAND, -e specify the remote shell to use --rsync-path=PROGRAM specify the rsync to run on remote machine --existing skip creating new files on receiver +--ignore-non-existing skip creating new files on receiver --ignore-existing skip updating files that exist on receiver ---remove-source-files sender removes synchronized files (non-dir) +--remove-source-files sender removes synchronized files (non-directory) --del an alias for --delete-during ---delete delete extraneous files from dest dirs +--delete delete extraneous files from dest directories --delete-before receiver deletes before xfer, not during --delete-during receiver deletes during the transfer --delete-delay find deletions during, delete after --delete-after receiver deletes after transfer, not during ---delete-excluded also delete excluded files from dest dirs ---ignore-missing-args ignore missing source args without error ---delete-missing-args delete missing source args from destination +--delete-excluded also delete excluded files from dest directories +--ignore-missing-args ignore missing source arguments without error +--delete-missing-args delete missing source arguments from destination --ignore-errors delete even if there are I/O errors ---force force deletion of dirs even if not empty +--force force deletion of directories even if not empty --max-delete=NUM don't delete more than NUM files --max-size=SIZE don't transfer any file larger than SIZE --min-size=SIZE don't transfer any file smaller than SIZE @@ -505,6 +607,7 @@ --compress, -z compress file data during the transfer --compress-choice=STR choose the compression algorithm (aka --zc) --compress-level=NUM explicitly set compression level (aka --zl) +--compress-threads=NUM explicitly set compression threads (aka --zt) --skip-compress=LIST skip compressing files with suffix in LIST --cvs-exclude, -C auto-ignore files in the same way CVS does --filter=RULE, -f add a file-filtering RULE @@ -516,8 +619,8 @@ --include-from=FILE read include patterns from FILE --files-from=FILE read list of source-file names from FILE --from0, -0 all *-from/filter files are delimited by 0s ---old-args disable the modern arg-protection idiom ---secluded-args, -s use the protocol to safely send the args +--old-args disable the modern argument-protection idiom +--secluded-args, -s use the protocol to safely send the arguments --trust-sender trust the remote sender's file list --copy-as=USER[:GROUP] specify user & optional group for the copy --address=ADDRESS bind address for outgoing socket to daemon @@ -583,19 +686,19 @@

The parameter may need to be quoted in some manner for it to survive the shell's command-line parsing. Also keep in mind that a leading tilde (~) in a pathname is substituted by your shell, so make sure that you separate the -option name from the pathname using a space if you want the local shell to -expand it.

+option name from the pathname using a space (rather than an equal +sign) if you want the local shell to expand it.

--help

Print a short help page describing the options available in rsync and exit. You can also use -h for --help when it is used without any other -options (since it normally means --human-readable).

+options or arguments (since otherwise it means --human-readable).

--version, -V

Print the rsync version plus other info and exit. When repeated, the -information is output is a JSON format that is still fairly readable +information is output in a JSON format that is still fairly readable (client side only).

The output includes a list of compiled-in capabilities, a list of optimizations, the default list of checksum algorithms, the default list of @@ -621,14 +724,15 @@ number). Note that these byte values can be made more (or less) human-readable by using the --human-readable (or --no-human-readable) options.

-

In a modern rsync, the -v option is equivalent to the setting of groups +

The -v option is equivalent to the setting of groups of --info and --debug options. You can choose to use these newer options in addition to, or in place of using --verbose, as any fine-grained settings override the implied settings of -v. Both ---info and --debug have a way to ask for help that -tells you exactly what flags are set for each increase in verbosity.

+--info and --debug have help texts available that +tells you exactly what flags are set for each increase in verbosity +(use --info=help or --debug=help to see them).

However, do keep in mind that a daemon's "max verbosity" setting will limit -how high of a level the various individual flags can be set on the daemon +how high the various individual flags can be set on the daemon side. For instance, if the max is 2, then any info and/or debug flag that is set to a higher value than what would be set by -vv will be downgraded to the -vv level in the daemon's logging.

@@ -650,9 +754,9 @@

Note that --info=name's output is affected by the --out-format and --itemize-changes (-i) options. See those options for more information on what is output and when.

-

This option was added to 3.1.0, so an older rsync on the server side might -reject your attempts at fine-grained control (if one or more flags needed -to be send to the server and the server was too old to understand them). +

This option was added to 3.1.0 (released September 2013), +so an older rsync on the server side might +reject your attempts at fine-grained control. See also the "max verbosity" caveat above when dealing with a daemon.

@@ -671,9 +775,9 @@

Note that some debug messages will only be output when the --stderr=all option is specified, especially those pertaining to I/O and buffer debugging.

-

Beginning in 3.2.0, this option is no longer auto-forwarded to the server -side in order to allow you to specify different debug values for each side -of the transfer, as well as to specify a new debug option that is only +

This option is not forwarded by the client to the server side, so that +you can specify different debug values for each side of the transfer. +This also allows you to specify debug options that are only present in one of the rsync versions. If you want to duplicate the same option on both sides, using brace expansion is an easy way to save you some typing. This works in zsh and bash:

@@ -684,22 +788,23 @@
--stderr=errors|all|client
-

This option controls which processes output to stderr and if info messages -are also changed to stderr. The mode strings can be abbreviated, so feel -free to use a single letter value. The 3 possible choices are:

+

This option controls which processes output to stderr, and whether +info messages are output to stderr rather than stdout. +The mode strings can be abbreviated down as far as a single letter. +The 3 possible choices are:

  • -

    errors -⁠ (the default) causes all the rsync processes to send an -error directly to stderr, even if the process is on the remote side of +

    errors -⁠ (the default) causes all the rsync processes to send +errors directly to stderr, even if the process is on the remote side of the transfer. Info messages are sent to the client side via the protocol stream. If stderr is not available (i.e. when directly connecting with a daemon via a socket) errors fall back to being sent via the protocol stream.

  • -

    all -⁠ causes all rsync messages (info and error) to get written +

    all -⁠ causes all rsync messages (info and error) to be written directly to stderr from all (possible) processes. This causes stderr to -become line-buffered (instead of raw) and eliminates the ability to +become line-buffered (instead of unbuffered) and eliminates the ability to divide up the info and error messages by file handle. For those doing debugging or using several levels of verbosity, this option can help to avoid clogging up the transfer stream (which should prevent any chance of @@ -716,11 +821,10 @@ been around for several releases.

-

This option was added in rsync 3.2.3. This version also began the -forwarding of a non-default setting to the remote side, though rsync uses -the backward-compatible options --msgs2stderr and --no-msgs2stderr to -represent the all and client settings, respectively. A newer rsync -will continue to accept these older option names to maintain compatibility.

+

Rsync also accepts the old --msgs2stderr and --no-msgs2stderr +options, equivalent to the all and client settings, +respectively, and uses them when forwarding non-default settings +to the remote side, in case the remote rsync is an older version.

--quiet, -q
@@ -760,7 +864,8 @@

When comparing two timestamps, rsync treats the timestamps as being equal if they differ by no more than the modify-window value. The default is 0, which matches just integer seconds. If you specify a negative value (and -the receiver is at least version 3.1.3) then nanoseconds will also be taken +the receiver is at least version 3.1.3, released January 2018) +then nanoseconds will also be taken into account. Specifying 1 is useful for copies to/from MS Windows FAT filesystems, because FAT represents times with a 2-second resolution (allowing times to differ from the original by up to 1 second).

@@ -781,7 +886,7 @@ need of a transfer. Without this option, rsync uses a "quick check" that (by default) checks if each file's size and time of last modification match between the sender and receiver. This option changes this to compare a -128-bit checksum for each file that has a matching size. Generating the +checksum for each file that has a matching size. Generating the checksums means that both sides will expend a lot of disk I/O reading all the data in the files in the transfer, so this can slow things down significantly (and this is prior to any reading that will be done to @@ -791,7 +896,7 @@ its checksums when it is scanning for changed files, and will checksum any file that has the same size as the corresponding sender's file: files with either a changed size or a changed checksum are selected for transfer.

-

Note that rsync always verifies that each transferred file was correctly +

Note that rsync normally verifies that each transferred file was correctly reconstructed on the receiving side by checking a whole-file checksum that is generated as the file is transferred, but that automatic after-the-transfer verification has nothing to do with this option's @@ -839,9 +944,10 @@

--inc-recursive, --i-r
-

This option explicitly enables on incremental recursion when scanning for +

This option explicitly enables incremental recursion when scanning for files, which is enabled by default when using the --recursive -option and both sides of the transfer are running rsync 3.0.0 or newer.

+option and both sides of the transfer are running rsync 3.0.0 +(released March 2008) or newer.

Incremental recursion uses much less memory than non-incremental, while also beginning the transfer more quickly (since it doesn't need to scan the entire transfer hierarchy before it starts transferring files). If no @@ -854,12 +960,11 @@

  • --prune-empty-dirs
  • --delay-updates
  • -

    In order to make --delete compatible with incremental recursion, -rsync 3.0.0 made --delete-during the default delete mode (which -was first added in 2.6.4).

    +

    In order to be compatible with incremental recursion, +--delete-during is the default delete mode for --delete.

    One side-effect of incremental recursion is that any missing sub-directories inside a recursively-scanned directory are (by default) -created prior to recursing into the sub-dirs. This earlier creation point +created prior to recursing into the sub-directories. This earlier creation point (compared to a non-incremental recursion) allows rsync to then set the modify time of the finished directory right away (without having to delay that until a bunch of recursive copying has finished). However, these @@ -872,7 +977,7 @@

    --no-inc-recursive, --no-i-r
    -

    Disables the new incremental recursion algorithm of the +

    Disables the incremental recursion algorithm of the --recursive option. This makes rsync scan the full file list before it begins to transfer files. See --inc-recursive for more info.

    @@ -882,8 +987,7 @@

    Use relative paths. This means that the full path names specified on the command line are sent to the server rather than just the last parts of the filenames. This is particularly useful when you want to send several -different directories at the same time. For example, if you used this -command:

    +different directories at the same time. For example, this command:

    rsync -av /foo/bar/baz.c remote:/tmp/
     
    @@ -898,31 +1002,32 @@ machine, preserving its full path. These extra path elements are called "implied directories" (i.e. the "foo" and the "foo/bar" directories in the above example).

    -

    Beginning with rsync 3.0.0, rsync always sends these implied directories as +

    Rsync always sends these implied directories as real directories in the file list, even if a path element is really a symlink on the sending side. This prevents some really unexpected behaviors -when copying the full path of a file that you didn't realize had a symlink +when copying the full path of a file that has a symlink in its path. If you want to duplicate a server-side symlink, include both -the symlink via its path, and referent directory via its real path. If +the symlink via its path, and the referent directory via its real path. If you're dealing with an older rsync on the sending side, you may need to use the --no-implied-dirs option.

    It is also possible to limit the amount of path information that is sent as implied directories for each path you specify. With a modern rsync on the -sending side (beginning with 2.6.7), you can insert a dot and a slash into +sending side (beginning with 2.6.7, released March 2006), +you can insert a dot and a slash into the source path, like this:

    rsync -avR /foo/./bar/baz.c remote:/tmp/
     
    -

    That would create /tmp/bar/baz.c on the remote machine. (Note that the dot +

    That would create /tmp/bar/baz.c on the receiving machine. (Note that the dot must be followed by a slash, so "/foo/." would not be abbreviated.) For -older rsync versions, you would need to use a chdir to limit the source +older rsync versions, you would need to change directory to limit the source path. For example, when pushing files:

    (cd /foo; rsync -avR bar/baz.c remote:/tmp/)
     
    -

    (Note that the parens put the two commands into a sub-shell, so that the +

    (Note that the parentheses put the two commands into a sub-shell, so that the "cd" command doesn't remain in effect for future commands.) If you're pulling files from an older rsync, use this idiom (but only for a non-daemon transfer):

    @@ -941,24 +1046,32 @@ and any missing implied directories are created with default attributes. This even allows these implied path elements to have big differences, such as being a symlink to a directory on the receiving side.

    -

    For instance, if a command-line arg or a files-from entry told rsync to +

    For instance, if a command-line argument or a files-from entry told rsync to transfer the file "path/foo/file", the directories "path" and "path/foo" are implied when --relative is used. If "path/foo" is a symlink to "bar" on the destination system, the receiving rsync would ordinarily delete "path/foo", recreate it as a directory, and receive the file into the new -directory. With --no-implied-dirs, the receiving rsync updates -"path/foo/file" using the existing path elements, which means that the file -ends up being created in "path/bar". Another way to accomplish this link -preservation is to use the --keep-dirlinks option (which will also affect -symlinks to directories in the rest of the transfer).

    -

    When pulling files from an rsync older than 3.0.0, you may need to use this +directory. With --no-implied-dirs, the receiving rsync leaves the +existing "path/foo" symlink in place and follows it, so the file is written +through the symlink and ends up in "path/bar". Note the security +implication: a pre-existing in-tree symlink-to-directory on the receiving +side will redirect where the file is written, so only use +--no-implied-dirs when you trust the destination's existing path elements. +(A symlink whose target is absolute or escapes the destination tree is still +refused by the secure path resolver rather than followed.) Another way to +accomplish this link preservation is to use the --keep-dirlinks +option (which will also affect symlinks to directories in the rest of the +transfer).

    +

    When pulling files from an rsync older than 3.0.0 (March 2008), +you may need to use this option if the sending side has a symlink in the path you request and you wish the implied directories to be transferred as normal directories.

    --backup, -b

    With this option, preexisting destination files are renamed as each file is -transferred or deleted. You can control where the backup file goes and +transferred or deleted (that is, each file to be deleted or overwritten +gets renamed instead). You can control where the backup file goes and what (if any) suffix gets appended using the --backup-dir and --suffix options.

    If you don't specify --backup-dir:

    @@ -986,8 +1099,9 @@

    Note that if you specify a relative path, the backup directory will be relative to the destination directory, so you probably want to specify either an absolute path or a path that starts with "../". If an rsync -daemon is the receiver, the backup dir cannot go outside the module's path -hierarchy, so take extra care not to delete it or copy into it.

    +daemon is the receiver, the backup directory cannot go outside the module's +path hierarchy, so take extra care not to delete or copy into the +backup directory inadvertently.

    --suffix=SUFFIX
    @@ -1001,10 +1115,10 @@ a modified time that is newer than the source file. (If an existing destination file has a modification time equal to the source file's, it will be updated if the sizes are different.)

    -

    Note that this does not affect the copying of dirs, symlinks, or other -special files. Also, a difference of file format between the sender and +

    Note that this does not affect the copying of directories, symlinks, or other +special files. Also, a difference of object type between the sender and receiver is always considered to be important enough for an update, no -matter what date is on the objects. In other words, if the source has a +matter what date is on the objects. For example, if the source has a directory where the destination has a file, the transfer would occur regardless of the timestamps.

    This option is a TRANSFER RULE, so don't expect any @@ -1052,13 +1166,12 @@ diverging the entire contents of a file that only has minor changes.

    The option implies --partial (since an interrupted transfer does not delete the file), but conflicts with --partial-dir and ---delay-updates. Prior to rsync 2.6.4 --inplace was also -incompatible with --compare-dest and --link-dest.

    +--delay-updates.

    --append
    -

    This special copy mode only works to efficiently update files that are -known to be growing larger where any existing content on the receiving side +

    This special copy mode is only applicable to update files that are +known to be growing larger, that is, where any existing content on the receiving side is also known to be the same as the content on the sender. The use of --append can be dangerous if you aren't 100% sure that all the files in the transfer are shared, growing files. You should thus use filter @@ -1066,8 +1179,8 @@

    Rsync updates these growing file in-place without verifying any of the existing content in the file (it only verifies the content that it is appending). Rsync skips any files that exist on the receiving side that -are not shorter than the associated file on the sending side (which means -that new files are transferred). It also skips any files whose size on the +are not shorter than the associated file on the sending side. +It also skips any files whose size on the sending side gets shorter during the send negotiations (rsync warns about a "diminished" file when this happens).

    This does not interfere with the updating of a file's non-content @@ -1078,11 +1191,11 @@

    --append-verify

    This special copy mode works like --append except that all the -data in the file is included in the checksum verification (making it less -efficient but also potentially safer). This option can be dangerous if +data in the file is included in the checksum verification, making it less +efficient but also potentially safer. This option can be dangerous if you aren't 100% sure that all the files in the transfer are shared, growing files. See the --append option for more details.

    -

    Note: prior to rsync 3.0.0, the --append option worked like +

    Note: prior to rsync 3.0.0 (March 2008), the --append option worked like --append-verify, so if you are interacting with an older rsync (or the transfer is using a protocol prior to 30), specifying either append option will initiate an --append-verify transfer.

    @@ -1101,16 +1214,21 @@ usage) if --recursive wasn't specified (so that directories are seen in the listing). Specify --no-dirs (or --no-d) if you want to turn this off.

    -

    There is also a backward-compatibility helper option, --old-dirs -(--old-d) that tells rsync to use a hack of -r --exclude='/*/*' to get -an older rsync to list a single directory without recursing.

    +

    See also the backward-compatibility helper option --old-dirs.

    +
    + +
    --old-dirs, --old-d
    +

    This backward-compatibility helper tells rsync to use a hack of +-r --exclude='/*/*' to get an older remote rsync to list a single directory +without recursing.

    --mkpath

    Create all missing path components of the destination path.

    -

    By default, rsync allows only the final component of the destination path -to not exist, which is an attempt to help you to validate your destination -path. With this option, rsync creates all the missing destination-path +

    By default, rsync will create only the final component of the destination path +if it does not exist. If any other component does not exist that is an +error, as this can help to catch mistakes in the destination path specification. +With this option, rsync creates all the missing destination-path components, just as if mkdir -p $DEST_PATH had been run on the receiving side.

    When specifying a destination path, including a trailing slash ensures that @@ -1118,11 +1236,11 @@ file list has a single item. See the COPYING TO A DIFFERENT NAME section for full details on how rsync decides if a final destination-path component should be created as a directory or not.

    -

    If you would like the newly-created destination dirs to match the dirs on +

    If you would like the newly-created destination directories to match the directories on the sending side, you should be using --relative (-R) instead of --mkpath. For instance, the following two commands result in the same destination tree, but only the second command ensures that the -"some/extra/path" components match the dirs on the sending side:

    +"some/extra/path" components match the directories on the sending side:

    rsync -ai --mkpath host:some/extra/path/*.c some/extra/path/
     rsync -aiR host:some/extra/path/*.c ./
    @@ -1133,7 +1251,7 @@
     

    Add symlinks to the transferred files instead of noisily ignoring them with a "non-regular file" warning for each symlink encountered. You can -alternately silence the warning by specifying --info=nonreg0.

    +alternatively silence the warning by specifying --info=nonreg0.

    The default handling of symlinks is to recreate each symlink's unchanged value on the receiving side.

    See the SYMBOLIC LINKS section for multi-option info.

    @@ -1147,12 +1265,7 @@

    This option supersedes any other options that affect symlinks in the transfer, since there are no symlinks left in the transfer.

    This option does not change the handling of existing symlinks on the -receiving side, unlike versions of rsync prior to 2.6.3 which had the -side-effect of telling the receiving side to also follow symlinks. A -modern rsync won't forward this option to a remote receiver (since only the -sender needs to know about it), so this caveat should only affect someone -using an rsync client older than 2.6.7 (which is when -L stopped being -forwarded to the receiver).

    +receiving side.

    See the --keep-dirlinks (-K) if you need a symlink to a directory to be treated as a real directory on the receiving side.

    See the SYMBOLIC LINKS section for multi-option info.

    @@ -1163,14 +1276,17 @@ the copied tree. Absolute symlinks are also treated like ordinary files, and so are any symlinks in the source path itself when --relative is used.

    -

    Note that the cut-off point is the top of the transfer, which is the part -of the path that rsync isn't mentioning in the verbose output. If you copy -"/src/subdir" to "/dest/" then the "subdir" directory is a name inside the -transfer tree, not the top of the transfer (which is /src) so it is legal -for created relative symlinks to refer to other names inside the /src and -/dest directories. If you instead copy "/src/subdir/" (with a trailing -slash) to "/dest/subdir" that would not allow symlinks to any files outside -of "subdir".

    +

    A symlink is judged unsafe by a lexical test on its value, without resolving +it on disk. An absolute or empty target is always unsafe. A relative +target is unsafe if its ".." components would climb above the top of the +transfer; a ".." that appears anywhere other than as a leading prefix (an +embedded or trailing "/..") is also treated as unsafe. The top is set by +how the source is specified: a trailing slash on the source (e.g. +"/src/subdir/" copied to "/dest/subdir") makes that directory itself the +top, so a symlink may not point above it; without the trailing slash (e.g. +"/src/subdir" copied to "/dest/") the source's parent ("/src") is the top, +so "subdir" is a name inside the transfer and a relative symlink may point +to any other name within it.

    Note that safe symlinks are only copied if --links was also specified or implied. The --copy-unsafe-links option has no extra effect when combined with --copy-links.

    @@ -1186,40 +1302,101 @@ --munge-links). It also affects deletions, since the file being present in the transfer prevents any matching file on the receiver from being deleted when the symlink is deemed to be unsafe and is skipped.

    -

    This option must be combined with --links (or ---archive) to have any symlinks in the transfer to conditionally -ignore. Its effect is superseded by --copy-unsafe-links.

    +

    This option has no effect unless it is combined with --links (or +an option that implies --links such as --archive), +because without --links there will not be any symlinks in the +transfer. Its effect is superseded by --copy-unsafe-links.

    Using this option in conjunction with --relative may give unexpected results.

    See the SYMBOLIC LINKS section for multi-option info.

    +
    +

    By default rsync resolves the directory and file paths that the operator +supplies on the command line with a defensive directory-tree walk that +follows a symlink component only when it is owned by uid 0 or by the +running user, and +refuses one owned by any other user. This stops an attacker who can write +inside one of those directories from planting a symlink that +redirects a privileged rsync to a target outside the intended tree, while +still honouring the operator's own symlinks. +The rule is the same for absolute and relative paths. +This applies to the destination directory and to the parameters to the +following options: --backup-dir, --temp-dir/-T, +--partial-dir, --link-dest, --compare-dest, --copy-dest, --log-file, +--password-file, --files-from, --include-from, --exclude-from, +--filter merge files, --write-batch and --read-batch.

    +

    --insecure-links turns that defence off, restoring the historical behaviour +of following any symlink in those paths. Use it only when you fully trust +every directory along each operator-supplied path, as it re-exposes the +symlink-redirection attacks the walk prevents.

    +

    The option is local only: it is not sent to the remote side of the +transfer. A remote-shell peer that needs the opt-out must set it there, e.g. +via --rsync-path), and a daemon never honours it -⁠-⁠ a daemon that +receives --insecure-links from a client refuses the request. A daemon +administrator who wants the legacy behaviour for a single trusted module sets +"insecure links = yes" in that module's rsyncd.conf(5) section instead; +a client can never enable it.

    +

    See the SYMBOLIC LINKS section for multi-option info.

    +
    + +
    --confine-root=DIR
    +

    This bounds where the paths listed under --insecure-links are +allowed to resolve: one that ends up outside DIR is refused, even if every +symlink along it was owned by a trusted user. The ownership walk asks who +planted a link; this asks where the path came out.

    +

    DIR must be absolute. Nothing is confined by default, and +--confine-root=/ is a no-op.

    +

    It exists for a wrapper that serves a restricted directory over a remote +shell, rrsync being the one shipped here, which passes it automatically +whenever its restricted dir is not "/". Such a wrapper can vet the argv +it is handed, but filter rules travel over the protocol instead: a client +can name a merge file outside the restricted dir in a dir-merge rule and +have the server read it in as filter rules. On a pull that needs neither +--delete nor any verbosity, because an exclude-only merge (the "-" +modifier) makes every line a pattern, and the client reads the file's +contents off which of its own names went missing. Confining the open is +what closes that; a merge file inside DIR keeps working as before.

    +

    A daemon ignores this option -⁠-⁠ its module directory is already the +boundary, and the option arrives in a client-supplied argv, so honouring it +could only widen the module.

    +

    --insecure-links is refused alongside it. That opt-out restores +the historical open, which skips the walk that enforces the root, so the two +together would silently mean no confinement at all.

    +

    Like --drop-D, it is not forwarded to the remote side: it is meant +to be applied to one end of a connection by itself.

    +
    +

    This option affects just one side of the transfer and tells rsync to munge symlink values when it is receiving files or unmunge symlink values when it -is sending files. The munged values make the symlinks unusable on disk but -allows the original contents of the symlinks to be recovered.

    -

    The server-side rsync often enables this option without the client's +is sending files. +"Munging" changes the symlink target so that the symlink cannot be followed +successfully, but allows the original target of the symlink to be +recovered. "Unmunging" reverses this process so that the symlink points to +the original target.

    +

    This option can be enabled on the server side without the client's knowledge, such as in an rsync daemon's configuration file or by an option given to the rrsync (restricted rsync) script. When specified on the client side, specify the option normally if it is the client side that -has/needs the munged symlinks, or use -M--munge-links to give the option -to the server when it has/needs the munged symlinks. Note that on a local +has or needs the munged symlinks, or use -M--munge-links to give the option +to the server when it has or needs the munged symlinks. Note that on a local transfer, the client is the sender, so specifying the option directly unmunges symlinks while specifying it as a remote option munges symlinks.

    This option has no effect when sent to a daemon via --remote-option because the daemon configures whether it wants munged symlinks via its "munge symlinks" parameter.

    -

    The symlink value is munged/unmunged once it is in the transfer, so any +

    The symlink value is munged or unmunged once it is in the transfer, so any option that transforms symlinks into non-symlinks occurs prior to the munging/unmunging except for --safe-links, which is a choice that the receiver makes, so it bases its decision on the munged/unmunged -value. This does mean that if a receiver has munging enabled, that using ---safe-links will cause all symlinks to be ignored (since they -are all absolute).

    -

    The method that rsync uses to munge the symlinks is to prefix each one's -value with the string "/rsyncd-munged/". This prevents the links from -being used as long as the directory does not exist. When this option is +value. This does mean that if a receiver has munging enabled, using +--safe-links will cause all symlinks to be ignored (since munging +makes them all absolute).

    +

    The method that rsync uses to munge a symlink is to prefix its +value with the string "/rsyncd-munged/". This prevents the link from +being used, provided that the /rsyncd-munged directory does not exist. +When this option is enabled, rsync will refuse to run if that path is a directory or a symlink to a directory (though it only checks at startup). See also the "munge-symlinks" python script in the support directory of the source code @@ -1239,16 +1416,12 @@ receiving side.

    --copy-dirlinks applies to all symlinks to directories in the source. If you want to follow only a few specified symlinks, a trick you can use is to -pass them as additional source args with a trailing slash, using +pass them as additional source arguments with a trailing slash, using --relative to make the paths match up right. For example:

    rsync -r --relative src/./ src/./follow-me/ dest/
     
    -

    This works because rsync calls lstat(2) on the source arg as given, and -the trailing slash makes lstat(2) follow the symlink, giving rise to a -directory in the file-list which overrides the symlink found during the -scan of "src/./".

    See the SYMBOLIC LINKS section for multi-option info.

    @@ -1258,13 +1431,13 @@ from the sender. Without this option, the receiver's symlink would be deleted and replaced with a real directory.

    For example, suppose you transfer a directory "foo" that contains a file -"file", but "foo" is a symlink to directory "bar" on the receiver. Without +"file", but on the receiver, "foo" is a symlink to directory "bar". Without --keep-dirlinks, the receiver deletes symlink "foo", recreates it as a directory, and receives the file into the new directory. With --keep-dirlinks, the receiver keeps the symlink and "file" ends up in "bar".

    -

    One note of caution: if you use --keep-dirlinks, you must trust all the -symlinks in the copy or enable the --munge-links option on the +

    One note of caution: if you use --keep-dirlinks, you must either trust all the +symlinks in the copy, or enable the --munge-links option on the receiving side! If it is possible for an untrusted user to create their own symlink to any real directory, the user could then (on a subsequent copy) replace the symlink with a real directory and affect the content of @@ -1341,7 +1514,7 @@ behavior easier to type, you could define a popt alias for it, such as putting this line in the file ~/.popt (the following defines the -Z option, and includes --no-g to use the default group of the destination -dir):

    +directory):

     rsync alias -Z --no-p --no-g --chmod=ugo=rwX
     
    @@ -1353,20 +1526,14 @@

    (Caveat: make sure that -a does not follow -Z, or it will re-enable the two --no-* options mentioned above.)

    -

    The preservation of the destination's setgid bit on newly-created -directories when --perms is off was added in rsync 2.6.7. Older rsync -versions erroneously preserved the three special permission bits for -newly-created files when --perms was off, while overriding the -destination's setgid bit setting on a newly-created directory. Default ACL -observance was added to the ACL patch for rsync 2.6.7, so older (or -non-ACL-enabled) rsyncs use the umask even if default ACLs are present. -(Keep in mind that it is the version of the receiving rsync that affects -these behaviors.)

    +

    Note that if the remote rsync is older than 2.6.7 (March 2006) and it is +the receiver, the behavior when --perms is off may differ from that +described above.

    --executability, -E
    -

    This option causes rsync to preserve the executability (or -non-executability) of regular files when --perms is not enabled. +

    This option causes rsync to preserve the executability or +non-executability of regular files when --perms is not enabled. A regular file is considered to be executable if at least one 'x' is turned on in its permissions. When an existing destination file's executability differs from that of the corresponding source file, rsync modifies the @@ -1443,6 +1610,14 @@

    --chmod=D2775,F664
     
    +

    Symbolic permission-copy modes are also allowed, such as g=u, o=g or +g-o. A permission-copy item may copy from one class only (u, g or +o) and cannot be combined with rwxXst permission letters in the same +item. Use comma-separated items when you need both behaviours, such as +g=o,o=.

    +

    A permission-copy = item also clears the special bit for each destination +class it updates (u clears setuid, g clears setgid, and o clears +sticky), matching GNU chmod behaviour.

    It is also legal to specify multiple --chmod options, as each additional option is just appended to the list of changes to make.

    See the --perms and --executability options for how the @@ -1450,8 +1625,9 @@

    --owner, -o
    -

    This option causes rsync to set the owner of the destination file to be the -same as the source file, but only if the receiving rsync is being run as +

    This option causes rsync to set the owner of each destination filesystem +object (file, directory, etc.) to be the +same as the source object, but only if the receiving rsync is being run as the super-user (see also the --super and --fake-super options). Without this option, the owner of new and/or transferred files are set to the invoking user on the receiving side.

    @@ -1461,8 +1637,9 @@
    --group, -g
    -

    This option causes rsync to set the group of the destination file to be the -same as the source file. If the receiving program is not running as the +

    This option causes rsync to set the group of each destination filesystem +object (file, directory, etc.) to be the +same as the source object. If the receiving program is not running as the super-user (or if --no-super was specified), only groups that the invoking user on the receiving side is a member of will be preserved. Without this option, the group is set to the default group of the invoking @@ -1497,6 +1674,34 @@ --specials".

    +
    --drop-D
    +

    This tells the receiving rsync to refuse to create device files and +special files, whatever the transfer requested. Entries for them are +skipped exactly as if -D had not been used, with the usual +"non-regular file" warning.

    +

    This differs from --no-D in that it changes only what is created, not +how the file list is encoded. --no-D also turns off the rdev fields +that devices and special files carry on the wire, so applying it to one +end of a connection alone leaves the two ends disagreeing about the +encoding and the transfer fails. --drop-D can therefore be added on the +receiving side by itself, which is what it exists for -⁠-⁠ the rrsync +wrapper uses it to stop a restricted directory's clients creating device +and special files in it. Nodes already present there are left alone.

    +

    Because it only withholds creation, it has no effect on a sending rsync.

    +

    This option is not forwarded to the remote side, since its whole purpose +is to be applied to one end of a connection by itself. It therefore +affects the rsync process you give it to and no other: on a local copy, or +on the receiving end of a --server invocation such as the one rrsync +builds. To set it on a remote receiver from the command line, send it +explicitly with --remote-option (-M):

    +
    +
    rsync -av -M--drop-D src/ host:dest/
    +
    +
    +

    Passing a plain --drop-D to a push affects only the local sender, where +it does nothing.

    +
    +
    --copy-devices

    This tells rsync to treat a device on the sending side as a regular file, allowing it to be copied to a normal destination file (or another device @@ -1514,20 +1719,20 @@

    --times, -t
    -

    This tells rsync to transfer modification times along with the files and -update them on the remote system. Note that if this option is not used, +

    This tells rsync to set the modification times of the destination files +(including directories, symlinks, devices, etc.) to be the same as the +source files. +Note that if this option is not used, the optimization that excludes files that have not been modified cannot be effective; in other words, a missing -t (or -a) will cause the next transfer to behave as if it used --ignore-times (-I), -causing all files to be updated (though rsync's delta-transfer algorithm +causing all files to be updated. (Although rsync's delta-transfer algorithm will make the update fairly efficient if the files haven't actually -changed, you're much better off using -t).

    -

    A modern rsync that is using transfer protocol 30 or 31 conveys a modify -time using up to 8-bytes. If rsync is forced to speak an older protocol -(perhaps due to the remote rsync being older than 3.0.0) a modify time is -conveyed using 4-bytes. Prior to 3.2.7, these shorter values could convey -a date range of 13-Dec-1901 to 19-Jan-2038. Beginning with 3.2.7, these -4-byte values now convey a date range of 1-Jan-1970 to 7-Feb-2106. If you +changed, you're much better off using -t.)

    +

    If the negotiated protocol is older than 30 -⁠-⁠ usually because the remote +rsync is older than 3.0.0 (March 2008), but also if --protocol +forces it -⁠-⁠ the range of modification times that can be conveyed is +restricted. If you have files dated older than 1970, make sure your rsync executables are upgraded so that the full range of dates can be conveyed.

    @@ -1539,9 +1744,6 @@ to make the sending and receiving systems have the same access times on the transferred files without needing to run rsync an extra time after a file is transferred.

    -

    Note that some older rsync versions (prior to 3.2.0) may have been built -with a pre-release --atimes patch that does not imply ---open-noatime when this option is repeated.

    --open-noatime
    @@ -1619,12 +1821,13 @@
    --sparse, -S

    Try to handle sparse files efficiently so they take up less space on the -destination. If combined with --inplace the file created might -not end up with sparse blocks with some combinations of kernel version -and/or filesystem type. If --whole-file is in effect (e.g. for a -local copy) then it will always work because rsync truncates the file prior -to writing out the updated version.

    -

    Note that versions of rsync older than 3.1.3 will reject the combination of +destination. This relies on the destination filesystem supporting sparse +files, that is, files where some parts of the file don't have corresponding +disk blocks allocated for them because they contain all zero bytes.

    +

    If combined with --inplace the file created may not end up with +sparse blocks (depending on the filesystem type and kernel version), +unless the --whole-file option is also in effect. Note that versions +of rsync older than 3.1.3 (January 2018) will reject the combination of --sparse and --inplace.

    @@ -1644,13 +1847,14 @@
    --dry-run, -n
    -

    This makes rsync perform a trial run that doesn't make any changes (and -produces mostly the same output as a real run). It is most commonly used +

    This makes rsync perform a trial run that doesn't make any changes, and +produces mostly the same output as a real run. It is most commonly used in combination with the --verbose (-v) and/or --itemize-changes (-i) options to see what an rsync command is going to do before one actually runs it.

    The output of --itemize-changes is supposed to be exactly the -same on a dry run and a subsequent real run (barring intentional trickery +same on a dry run and a subsequent real run (barring external changes to +the source or destination and system call failures); if it isn't, that's a bug. Other output should be mostly unchanged, but may differ in some areas. Notably, a dry run does not send the actual data for file transfers, so --progress has no @@ -1660,8 +1864,8 @@

    --whole-file, -W
    -

    This option disables rsync's delta-transfer algorithm, which causes all -transferred files to be sent whole. The transfer may be faster if this +

    This option disables rsync's delta-transfer algorithm, causing the whole of +all transferred files to be sent. The transfer may be faster if this option is used when the bandwidth between the source and destination machines is higher than the bandwidth to disk (especially when the "disk" is actually a networked filesystem). This is the default when both the @@ -1679,10 +1883,11 @@

    --checksum-choice=STR, --cc=STR

    This option overrides the checksum algorithms. If one algorithm name is -specified, it is used for both the transfer checksums and (assuming ---checksum is specified) the pre-transfer checksums. If two +specified, it is used for both the transfer checksums and the pre-transfer +checksums (note that the pre-transfer checksum is only performed if +--checksum is specified). If two comma-separated names are supplied, the first name affects the transfer -checksums, and the second name affects the pre-transfer checksums (-c).

    +checksums, and the second name affects the pre-transfer checksums.

    The checksum options that you may be able to use are:

    • auto (the default automatic choice)
    • @@ -1702,9 +1907,11 @@ the --checksum option cannot be used.

      The "auto" option is the default, where rsync bases its algorithm choice on a negotiation between the client and the server as follows:

      -

      When both sides of the transfer are at least 3.2.0, rsync chooses the first -algorithm in the client's list of choices that is also in the server's list -of choices. If no common checksum choice is found, rsync exits with +

      When both sides of the transfer are at least 3.2.0 (released June 2020), +each side chooses its own +most-preferred algorithm that also appears in the peer's list. Both sides +order their lists strongest-first, so they converge on the strongest mutual +choice. If no common checksum choice is found, rsync exits with an error. If the remote rsync is too old to support checksum negotiation, a value is chosen based on the protocol version (which chooses between MD5 and various flavors of MD4 based on protocol age).

      @@ -1782,28 +1989,28 @@ yet finished (e.g. name the file "foo.new" when it is written, rename it to "foo" when it is done, and then use the option --exclude='*.new' for the rsync transfer).

      -

      Starting with 3.1.0, rsync will skip the sender-side removal (and output an -error) if the file's size or modify time has not stayed unchanged.

      -

      Starting with 3.2.6, a local rsync copy will ensure that the sender does +

      Rsync will skip the sender-side removal, and output an +error, if the file's size or modify time has not stayed unchanged.

      +

      If the copy is local, rsync will ensure that the sender does not remove a file the receiver just verified, such as when the user accidentally makes the source and destination directory the same path.

    --delete
    -

    This tells rsync to delete extraneous files from the receiving side (ones -that aren't on the sending side), but only for the directories that are +

    This tells rsync to delete extraneous files from the receiving side (those +that don't exist on the sending side), but only for the directories that are being synchronized. You must have asked rsync to send the whole directory -(e.g. "dir" or "dir/") without using a wildcard for the directory's -contents (e.g. "dir/*") since the wildcard is expanded by the shell and +(e.g. "dir" or "dir/"), rather than asking for all the files in a +directory via a wildcard (e.g. "dir/*"), since the wildcard is +expanded by the shell and rsync thus gets a request to transfer individual files, not the files' parent directory. Files that are excluded from the transfer are also excluded from being deleted unless you use the --delete-excluded option or mark the rules as only matching on the sending side (see the include/exclude modifiers in the FILTER RULES section).

    -

    Prior to rsync 2.6.7, this option would have no effect unless ---recursive was enabled. Beginning with 2.6.7, deletions will -also occur when --dirs (-d) is enabled, but only for -directories whose contents are being copied.

    +

    This option has no effect unless either --recursive or +--dirs (-d) is enabled. In the latter case, deletions only +occur in directories whose contents are being copied.

    This option can be dangerous if used incorrectly! It is a very good idea to first try a run using the --dry-run (-n) option to see what files are going to be deleted.

    @@ -1812,10 +2019,11 @@ temporary filesystem failures (such as NFS errors) on the sending side from causing a massive deletion of files on the destination. You can override this with the --ignore-errors option.

    -

    The --delete option may be combined with one of the -⁠-⁠delete-WHEN options +

    The --delete option may be combined with one of the -⁠-⁠delete-⁠WHEN options without conflict, as well as --delete-excluded. However, if none of the --delete-WHEN options are specified, rsync will choose the ---delete-during algorithm when talking to rsync 3.0.0 or newer, +--delete-during algorithm when talking to rsync 3.0.0 (March +2008) or newer, or the --delete-before algorithm when talking to an older rsync. See also --delete-delay and --delete-after.

    @@ -1838,8 +2046,8 @@ as the transfer happens. The per-directory delete scan is done right before each directory is checked for updates, so it behaves like a more efficient --delete-before, including doing the deletions prior to -any per-directory filter files being updated. This option was first added -in rsync version 2.6.4. See --delete (which is implied) for more +any per-directory filter files being updated. +See --delete (which is implied) for more details on file-deletion.

    @@ -1848,9 +2056,9 @@ the transfer (like --delete-during), and then removed after the transfer completes. This is useful when combined with --delay-updates and/or --fuzzy, and is more efficient -than using --delete-after (but can behave differently, since +than using --delete-after, but can behave differently, since --delete-after computes the deletions in a separate pass after -all updates are done). If the number of removed files overflows an +all updates are done. If the number of removed files overflows an internal buffer, a temporary file will be created on the receiving side to hold the names (it is removed while open, so you shouldn't see it during the transfer). If the creation of the temporary file fails, rsync will try @@ -1901,7 +2109,7 @@
    --delete-missing-args

    This option takes the behavior of the (implied) ---ignore-missing-args option a step farther: each missing arg +--ignore-missing-args option a step farther: each missing argument will become a deletion request of the corresponding destination file on the receiving side (should it exist). If the destination file is a non-empty directory, it will only be successfully deleted if --force or @@ -1920,9 +2128,6 @@

    This option tells rsync to delete a non-empty directory when it is to be replaced by a non-directory. This is only relevant if deletions are not active (see --delete for details).

    -

    Note for older rsync versions: --force used to still be required when -using --delete-after, and it used to be non-functional unless the ---recursive option was also enabled.

    --max-delete=NUM
    @@ -1931,19 +2136,19 @@ transfer. At the end, rsync outputs a warning (including a count of the skipped deletions) and exits with an error code of 25 (unless some more important error condition also occurred).

    -

    Beginning with version 3.0.0, you may specify --max-delete=0 to be warned +

    You may specify --max-delete=0 to be warned about any extraneous files in the destination without removing any of them. -Older clients interpreted this as "unlimited", so if you don't know what -version the client is, you can use the less obvious --max-delete=-1 as a -backward-compatible way to specify that no deletions be allowed (though -really old versions didn't warn when the limit was exceeded).

    +CAUTION: a client rsync older than 3.0.0 (March 2008) treats +--max-delete=0 as unlimited, so use --max-delete=-1 if the command +might be run by such an old rsync. (A 3.0.0 or newer client protects an +older remote by forwarding the option as --max-delete=-1.)

    --max-size=SIZE

    This tells rsync to avoid transferring any file that is larger than the specified SIZE. A numeric value can be suffixed with a string to indicate -the numeric units or left unqualified to specify bytes. Feel free to use a -fractional value along with the units, such as --max-size=1.5m.

    +the numeric units or left unqualified to specify bytes, and the numeric +value can have a fractional part, such as --max-size=1.5m.

    This option is a TRANSFER RULE, so don't expect any exclude side effects.

    The first letter of a units string can be B (bytes), K (kilo), M @@ -1957,14 +2162,12 @@ 8192P-1.

    Examples: --max-size=1.5mb-1 is 1499999 bytes, and --max-size=2g+1 is 2147483649 bytes.

    -

    Note that rsync versions prior to 3.1.0 did not allow --max-size=0.

    --min-size=SIZE

    This tells rsync to avoid transferring any file that is smaller than the specified SIZE, which can help in not transferring small, junk files. See the --max-size option for a description of SIZE and other info.

    -

    Note that rsync versions prior to 3.1.0 did not allow --min-size=0.

    --max-alloc=SIZE
    @@ -1973,14 +2176,18 @@ causing rsync to request massive amounts of memory. However, if you have many millions of files in a transfer, a large amount of server memory, and you don't want to split up your transfer into multiple parts, you can -increase the per-allocation limit to something larger and rsync will +increase the per-allocation limit to something larger, allowing rsync to consume more memory.

    Keep in mind that this is not a limit on the total size of allocated memory. It is a sanity-check value for each individual allocation.

    See the --max-size option for a description of how SIZE can be specified. The default suffix if none is given is bytes.

    -

    Beginning in 3.2.7, a value of 0 is an easy way to specify SIZE_MAX (the -largest limit possible).

    +

    Beginning in 3.2.7, a value of 0 was an easy way to specify SIZE_MAX (the +largest limit possible). However, beginning with 3.5.0, a value of 0 is +rejected as invalid for security reasons (a 0-byte cap could be used to +disable the allocation limit, which could lead to a denial-of-service via +memory exhaustion). Use an explicit very large value if you want a very +high limit.

    You can set a default value using the environment variable RSYNC_MAX_ALLOC using the same SIZE values as supported by this option. If the remote rsync doesn't understand the --max-alloc option, @@ -1993,32 +2200,32 @@

    This forces the block size used in rsync's delta-transfer algorithm to a fixed value. It is normally selected based on the size of each file being updated. See the technical report for details.

    -

    Beginning in 3.2.3 the SIZE can be specified with a suffix as detailed in -the --max-size option. Older versions only accepted a byte count.

    +

    The SIZE can be specified with a suffix as detailed in +the --max-size option.

    --rsh=COMMAND, -e

    This option allows you to choose an alternative remote shell program to use for communication between the local and remote copies of rsync. Typically, -rsync is configured to use ssh by default, but you may prefer to use rsh on -a local network.

    +rsync is configured to use ssh by default.

    If this option is used with [user@]host::module/path, then the remote shell COMMAND will be used to run an rsync daemon on the remote host, and all data will be transmitted through that remote shell connection, rather than through a direct socket connection to a running rsync daemon on the remote host. See the USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION section above.

    -

    Beginning with rsync 3.2.0, the RSYNC_PORT environment variable will +

    The RSYNC_PORT environment variable will be set when a daemon connection is being made via a remote-shell connection. It is set to 0 if the default daemon port is being assumed, or it is set to the value of the rsync port that was specified via either the ---port option or a non-empty port value in an rsync:// URL. -This allows the script to discern if a non-default port is being requested, -allowing for things such as an SSL or stunnel helper script to connect to a -default or alternate port.

    +--port option or a non-empty port value in an rsync:// URL (for +example rsync://host.example.com:984). This is useful if the program +being run is not actually rsync but rather something such as an SSL or +stunnel helper script; the script can use RSYNC_PORT to determine whether +it should connect to a default or alternate port.

    Command-line arguments are permitted in COMMAND provided that COMMAND is presented to rsync as a single argument. You must use spaces (not tabs or -other whitespace) to separate the command and args from each other, and you +other whitespace) to separate the command and arguments from each other, and you can use single- and/or double-quotes to preserve spaces in an argument (but not backslashes). Note that doubling a single-quote inside a single-quoted string gives you a single-quote; likewise for double-quotes (though you @@ -2073,12 +2280,12 @@ the socket, and that will make it fail in a cryptic fashion.

    Note that you should use a separate -M option for each remote option you want to pass. On older rsync versions, the presence of any spaces in the -remote-option arg could cause it to be split into separate remote args, but +remote-option argument could cause it to be split into separate remote arguments, but this requires the use of --old-args in a modern rsync.

    When performing a local transfer, the "local" side is the sender and the "remote" side is the receiver.

    Note some versions of the popt option-parsing library have a bug in them -that prevents you from using an adjacent arg with an equal in it next to a +that prevents you from using an adjacent argument with an equal in it next to a short option letter (e.g. -M--log-file=/tmp/foo). If this bug affects your version of popt, you can use the version of popt that is included with rsync.

    @@ -2156,8 +2363,8 @@

    You may use as many --filter options on the command line as you like to build up the list of files to exclude. If the filter contains whitespace, be sure to quote it so that the shell gives the rule to rsync as a single -argument. The text below also mentions that you can use an underscore to -replace the space that separates a rule from its arg.

    +argument. You can use an underscore instead of a space to separate a rule +from its argument.

    See the FILTER RULES section for detailed information on this option.

    @@ -2222,8 +2429,8 @@
    --files-from=FILE
    -

    Using this option allows you to specify the exact list of files to transfer -(as read from the specified FILE or '-' for standard input). It also +

    Using this option tells rsync to read the exact list of files to transfer +from FILE (or standard input if FILE is '-'). It also tweaks the default behavior of rsync to make transferring just the specified files and directories easier:

      @@ -2241,8 +2448,12 @@ --files-from, as does --no-R and all other options).

    The filenames that are read from the FILE are all relative to the source -dir -⁠-⁠ any leading slashes are removed and no ".." references are allowed -to go higher than the source dir. For example, take this command:

    +directory: any leading slash is removed, and ".." components are resolved away so +an entry cannot rise above the source directory -⁠-⁠ e.g. "../foo" is taken as "foo" +within the source directory. An entry that still contains an active ".." after +that resolution (one that cannot be collapsed) is rejected with an error. +Blank entries are ignored, as are whole-entry comments that start with ';' +or '#'. For example, take this command:

    rsync -a --files-from=/tmp/foo /usr remote:/backup
     
    @@ -2250,9 +2461,9 @@

    If /tmp/foo contains the string "bin" (or even "/bin"), the /usr/bin directory will be created as /backup/bin on the remote host. If it contains "bin/" (note the trailing slash), the immediate contents of the -directory would also be sent (without needing to be explicitly mentioned in -the file -⁠-⁠ this began in version 2.6.4). In both cases, if the --r option was enabled, that dir's entire hierarchy would also be +directory would also be sent, without needing to be explicitly mentioned in +the file. In both cases, if the +-r option was enabled, that directory's entire hierarchy would also be transferred (keep in mind that -r needs to be specified explicitly with --files-from, since it is not implied by -a. Also note that the effect of the (enabled by default) -r option @@ -2282,7 +2493,7 @@

    --from0, -0
    -

    This tells rsync that the rules/filenames it reads from a file are +

    This tells rsync that the rules or filenames it reads from a file are terminated by a null ('\0') character, not a NL, CR, or CR+LF. This affects --exclude-from, --include-from, --files-from, and any merged files specified in a @@ -2291,16 +2502,16 @@

    --old-args
    -

    This option tells rsync to stop trying to protect the arg values on the +

    This option tells rsync not to protect the argument values sent to the remote side from unintended word-splitting or other misinterpretation. -It also allows the client to treat an empty arg as a "." instead of +It also allows the client to treat an empty argument as a "." instead of generating an error.

    The default in a modern rsync is for "shell-active" characters (including -spaces) to be backslash-escaped in the args that are sent to the remote +spaces) to be backslash-escaped in the arguments that are sent to the remote shell. The wildcard characters *, ?, [, & ] are not escaped in -filename args (allowing them to expand into multiple filenames) while being -protected in option args, such as --usermap.

    -

    If you have a script that wants to use old-style arg splitting in its +filename arguments (allowing them to expand into multiple filenames) while being +protected in option arguments, such as --usermap.

    +

    If you have a script that wants to use old-style argument splitting in its filenames, specify this option once. If the remote shell has a problem with any backslash escapes at all, specify this option twice.

    You may also control this setting via the RSYNC_OLD_ARGS environment @@ -2309,31 +2520,31 @@ repeated-option setting. If it is "0", you'll get the default escaping behavior. The environment is always overridden by manually specified positive or negative options (the negative is --no-old-args).

    -

    Note that this option also disables the extra safety check added in 3.2.5 +

    Note that this option also disables the extra safety check that ensures that a remote sender isn't including extra top-level items in the file-list that you didn't request. This side-effect is necessary because we can't know for sure what names to expect when the remote shell -is interpreting the args.

    +is interpreting the arguments.

    This option conflicts with the --secluded-args option.

    --secluded-args, -s

    This option sends all filenames and most options to the remote rsync via -the protocol (not the remote shell command line) which avoids letting the -remote shell modify them. Wildcards are expanded on the remote host by -rsync instead of a shell.

    -

    This is similar to the default backslash-escaping of args that was added -in 3.2.4 (see --old-args) in that it prevents things like space -splitting and unwanted special-character side-effects. However, it has the -drawbacks of being incompatible with older rsync versions (prior to 3.0.0) -and of being refused by restricted shells that want to be able to inspect -all the option values for safety.

    -

    This option is useful for those times that you need the argument's +the protocol (rather than via the remote shell command line), which avoids +the possibility of the remote shell modifying them. +Wildcards are expanded on the remote host by rsync instead of a shell.

    +

    Without this option, rsync does backslash-escaping of arguments to prevent +things like space splitting and unwanted special-character side-effects, +which is normally sufficient to avoid unwanted modifications by the shell. +This option is useful for those times that you need the argument's character set to be converted for the remote host, if the remote shell is -incompatible with the default backslash-escpaing method, or there is some +incompatible with the default backslash-escaping method, or there is some other reason that you want the majority of the options and arguments to bypass the command-line of the remote shell.

    -

    If you combine this option with --iconv, the args related to the +

    This option is incompatible with remote rsync versions prior to 3.0.0 +(March 2008). It also has the drawback of being refused by restricted +shells that want to be able to inspect all the option values for safety.

    +

    If you combine this option with --iconv, the arguments related to the remote side will be translated from the local to the remote character-set. The translation happens before wild-cards are expanded. See also the --files-from option.

    @@ -2343,9 +2554,10 @@ is overridden by a manually specified positive or negative version of this option (note that --no-s and --no-secluded-args are the negative versions). This environment variable is also superseded by a non-zero -RSYNC_OLD_ARGS export.

    +RSYNC_OLD_ARGS environment variable.

    This option conflicts with the --old-args option.

    -

    This option used to be called --protect-args (before 3.2.6) and that +

    This option used to be called --protect-args (before 3.2.6, September +2022) and that older name can still be used (though specifying it as -s is always the easiest and most compatible choice).

    @@ -2356,10 +2568,10 @@ only be used if you trust the sender to not put something malicious in the file list (something that could possibly be done via a modified rsync, a modified shell, or some other similar manipulation).

    -

    Normally, the rsync client (as of version 3.2.5) runs two extra validation +

    Normally, the rsync client runs two extra validation checks when pulling files from a remote rsync:

      -
    • It verifies that additional arg items didn't get added at the top of the +
    • It verifies that additional argument items didn't get added at the top of the transfer.
    • It verifies that none of the items in the file list are names that should have been excluded (if filter rules were specified).
    • @@ -2370,18 +2582,18 @@
    • Using a per-directory filter file reads filter rules that only the server knows about, so the filter checking is disabled.
    • Using the --old-args option allows the sender to manipulate the -requested args, so the arg checking is disabled.
    • +requested arguments, so the argument checking is disabled.
    • Reading the files-from list from the server side means that the client -doesn't know the arg list, so the arg checking is disabled.
    • +doesn't know the argument list, so the argument checking is disabled.
    • Using --read-batch disables both checks since the batch file's contents will have been verified when it was created.

    This option may help an under-powered client server if the extra pattern -matching is slowing things down on a huge transfer. It can also be used to -work around a currently-unknown bug in the verification logic for a transfer -from a trusted sender.

    +matching is slowing things down on a huge transfer. It could also be used +for a transfer from a trusted sender as a workaround if there appeared to +be a bug in the verification logic.

    When using this option it is a good idea to specify a dedicated destination -directory, as discussed in the MULTI-HOST SECURITY section.

    +directory, as discussed in the SECURITY section.

    --copy-as=USER[:GROUP]
    @@ -2389,8 +2601,9 @@ colon) the GROUP for the copy operations. This only works if the user that is running rsync has the ability to change users. If the group is not specified then the user's default groups are used.

    -

    This option can help to reduce the risk of an rsync being run as root into -or out of a directory that might have live changes happening to it and you +

    This option can help to reduce the risk in the case where rsync is being +run as root, copying into or out of a directory that might have live +changes happening to it, and you want to make sure that root-level read or write actions of system files are not possible. While you could alternatively run all of rsync as the specified user, sometimes you need the root-level host-access credentials @@ -2404,7 +2617,7 @@ "localhost:" or "lh:" host-spec to be specified without needing to setup any remote shells, allowing you to specify remote options that affect the side of the transfer that is using the host-spec (and using hostname "lh" -avoids the overriding of the remote directory to the user's home dir).

    +avoids the overriding of the remote directory to the user's home directory).

    For example, the following rsync writes the local files as user "joe":

    sudo rsync -aiv --copy-as=joe host1:backups/joe/ /home/joe/
    @@ -2414,8 +2627,8 @@
     are available to that user, and makes it impossible for the joe user to do
     a timed exploit of the path to induce a change to a file that the joe user
     has no permissions to change.

    -

    The following command does a local copy into the "dest/" dir as user "joe" -(assuming you've installed support/lsh into a dir on your $PATH):

    +

    The following command does a local copy into the "dest/" directory as user "joe" +(assuming you've installed support/lsh into a directory on your $PATH):

    sudo rsync -aive lsh -M--copy-as=joe src/ lh:dest/
     
    @@ -2426,9 +2639,9 @@

    This option instructs rsync to use DIR as a scratch directory when creating temporary copies of the files transferred on the receiving side. The default behavior is to create each temporary file in the same directory as -the associated destination file. Beginning with rsync 3.1.1, the temp-file -names inside the specified DIR will not be prefixed with an extra dot -(though they will still have a random suffix added).

    +the associated destination file. The temp-file +names inside the specified DIR will not be prefixed with an extra dot, +though they will still have a random suffix added.

    This option is most often used when the receiving disk partition does not have enough free space to hold a copy of the largest file in the transfer. In this case (i.e. when the scratch directory is on a different disk @@ -2436,9 +2649,7 @@ over the top of the associated destination file, but instead must copy it into place. Rsync does this by copying the file over the top of the destination file, which means that the destination file will contain -truncated data during this copy. If this were not done this way (even if -the destination file were first removed, the data locally copied to a -temporary file in the destination directory, and then renamed into place) +truncated data during this copy. If this were not done this way, it would be possible for the old file to continue taking up disk space (if someone had it open), and thus there might not be enough room to fit the new version on the disk at the same time.

    @@ -2450,7 +2661,7 @@ destination partition, another way to tell rsync that you aren't overly concerned about disk space is to use the --partial-dir option with a relative path; because this tells rsync that it is OK to stash off a -copy of a single file in a subdir in the destination hierarchy, rsync will +copy of a single file in a subdirectory in the destination hierarchy, rsync will use the partial-dir as a staging area to bring over the copied file, and then rename it into place from there. (Specifying a --partial-dir with an absolute path does not have this side-effect.)

    @@ -2458,7 +2669,7 @@
    --fuzzy, -y

    This option tells rsync that it should look for a basis file for any -destination file that is missing. The current algorithm looks in the same +destination file that is missing. With this option, rsync looks in the same directory as the destination file for either a file that has an identical size and modified-time, or a similarly-named file. If found, rsync uses the fuzzy basis file to try to speed up the transfer.

    @@ -2479,7 +2690,7 @@ sparse backup of just files that have changed from an earlier backup. This option is typically used to copy into an empty (or newly created) directory.

    -

    Beginning in version 2.6.4, multiple --compare-dest directories may be +

    Multiple --compare-dest directories may be provided, which will cause rsync to search the list in the order specified for an exact match. If a match is found that differs only in attributes, a local copy is made and the attributes updated. If a match is not found, a @@ -2487,10 +2698,10 @@ transfer.

    If DIR is a relative path, it is relative to the destination directory. See also --copy-dest and --link-dest.

    -

    NOTE: beginning with version 3.1.0, rsync will remove a file from a +

    NOTE: rsync will remove a file from a non-empty destination hierarchy if an exact match is found in one of the -compare-dest hierarchies (making the end result more closely match a fresh -copy).

    +compare-dest hierarchies, making the end result more closely match a fresh +copy.

    --copy-dest=DIR
    @@ -2521,16 +2732,30 @@ option that squishes root to a single user, or mounts a removable drive with generic ownership (such as OS X's "Ignore ownership on this volume" option).

    -

    Beginning in version 2.6.4, multiple --link-dest directories may be +

    Multiple --link-dest directories may be provided, which will cause rsync to search the list in the order specified for an exact match (there is a limit of 20 such directories). If a match is found that differs only in attributes, a local copy is made and the attributes updated. If a match is not found, a basis file from one of the DIRs will be selected to try to speed up the transfer.

    +

    Not every filesystem can hard-link a symlink, a device node, a FIFO or a +socket, and the destination need not agree with the one rsync was built on -⁠-⁠ macOS builds on APFS, which can, and may write to HFS+, which cannot. +Where the destination refuses to link such an entry, it is copied instead +and the transfer carries on, so only that entry loses the space saving. +This applies to any refusal, because the error alone does not identify one: +link(2) reports EPERM both for a filesystem without hard links and for an +ordinary permission refusal. Regular files have always behaved this way.

    +

    One case is not covered. With --hard-links (-H), a group of +such entries hard-linked to each other in the source needs a second link, +from the first member to the rest, inside the destination itself. Where +that link is refused too -⁠-⁠ a destination that cannot hard-link the type at +all -⁠-⁠ the members after the first are not created and the transfer fails. +A --link-dest on another filesystem is fine: only the link to DIR is +impossible there, and the ones within the destination still succeed.

    This option works best when copying into an empty destination hierarchy, as existing files may get their attributes tweaked, and that can affect alternate destination files via hard-links. Also, itemizing of changes can -get a bit muddled. Note that prior to version 3.1.0, an +get a bit muddled. Note that prior to version 3.1.0 (September 2013), an alternate-directory exact match would never be found (nor linked into the destination) when a destination file already exists.

    Note that if you combine this option with --ignore-times, rsync will not @@ -2539,11 +2764,11 @@ the file is updated.

    If DIR is a relative path, it is relative to the destination directory. See also --compare-dest and --copy-dest.

    -

    Note that rsync versions prior to 2.6.1 had a bug that could prevent +

    Note that rsync versions prior to 2.6.1 (April 2004) had a bug that could prevent --link-dest from working properly for a non-super-user when --owner (-o) was specified (or implied). You can work-around -this bug by avoiding the -o option (or using --no-o) when sending to an -old rsync.

    +this bug by avoiding the -o option (or using --no-o) when sending to a +really old remote rsync.

    --compress, -z
    @@ -2555,9 +2780,11 @@ option.

    Run rsync --version to see the default compress list compiled into your version.

    -

    When both sides of the transfer are at least 3.2.0, rsync chooses the first -algorithm in the client's list of choices that is also in the server's list -of choices. If no common compress choice is found, rsync exits with +

    When both sides of the transfer are at least 3.2.0 (June 2020), +each side chooses its own +most-preferred algorithm that also appears in the peer's list. Both sides +order their lists strongest-first, so they converge on the strongest mutual +choice. If no common compress choice is found, rsync exits with an error. If the remote rsync is too old to support checksum negotiation, its list is assumed to be "zlib".

    The default order can be customized by setting the environment variable @@ -2602,7 +2829,7 @@

    --compress-level=NUM, --zl=NUM

    Explicitly set the compression level to use (see --compress, --z) instead of letting it default. The --compress option is +-z) instead of using the default. The --compress option is implied as long as the level chosen is not a "don't compress" level for the compression algorithm that is in effect (e.g. zlib compression treats level 0 as "off").

    @@ -2631,11 +2858,25 @@ checksum choice in effect).

    +
    --compress-threads=NUM, --zt=NUM
    +

    Set the number of threads to spawn when compressing data. Setting this +option to 1 or more will instruct the compression library to spawn 1 or +more threads for compression. Ideally, increasing the number of threads +will increase transfer speed if the transfer is CPU bound on the sender.

    +

    This option does not affect decompression.

    +

    Compression algorithms that allow threading:

    +
      +
    • zstd (only when libzstd is compiled with threading support)
    • +
    +

    This option is ignored if one of the above alogithms is not selected as the +--compression-choice or if compression not enabled.

    +
    +
    --skip-compress=LIST

    NOTE: no compression method currently supports per-file compression changes, so this option has no effect.

    Override the list of file suffixes that will be compressed as little as -possible. Rsync sets the compression level on a per-file basis based on +possible. Rsync can set the compression level on a per-file basis based on the file's suffix. If the compression algorithm has an "off" level, then no compression occurs for those files. Other algorithms that support changing the streaming level on-the-fly will have the level minimized to @@ -2804,6 +3045,9 @@

    --usermap=:nobody --groupmap=*:nobody
     
    +

    An empty FROM value matches only sender-side IDs that have no name. It +is not a wildcard for named users or groups; use "*" when you want to map +every sender-side name.

    When the --numeric-ids option is used, the sender does not send any names, so all the IDs are treated as having an empty name. This means that you will need to specify numeric FROM values if you want to map these @@ -2812,8 +3056,8 @@ a super-user (see also the --super and --fake-super options). For the --groupmap option to work, the receiver will need to have permissions to set that group.

    -

    Starting with rsync 3.2.4, the --usermap option implies the ---owner (-o) option while the --groupmap option implies the +

    The --usermap option implies the +--owner (-o) option, and the --groupmap option implies the --group (-g) option (since rsync needs to have those options enabled for the mapping options to work).

    An older rsync client may need to use -s to avoid a complaint @@ -2823,7 +3067,7 @@

    --chown=USER:GROUP

    This option forces all files to be owned by USER with group GROUP. This is a simpler interface than using --usermap & --groupmap -directly, but it is implemented using those options internally so they +directly, but it is implemented using those options internally, thus they cannot be mixed. If either the USER or GROUP is empty, no mapping for the omitted user/group will occur. If GROUP is empty, the trailing colon may be omitted, but if USER is empty, a leading colon must be supplied.

    @@ -2891,8 +3135,8 @@ file, including attribute changes. This is exactly the same as specifying --out-format='%i %n%L'. If you repeat the option, unchanged files will also be output, but only if the receiving rsync is at least -version 2.6.7 (you can use -vv with older versions of rsync, but that -also turns on the output of other verbose messages).

    +version 2.6.7 (March 2006). You can use -vv with older versions of rsync, +but that also turns on the output of other verbose messages.

    The "%i" escape has a cryptic output that is 11 letters long. The general format is like the string YXcstpoguax, where Y is replaced by the type of update being done, X is replaced by the file-type, and the other @@ -2928,7 +3172,7 @@

  • A c means either that a regular file has a different checksum (requires --checksum) or that a symlink, device, or special file has a changed value. Note that if you are sending files to an rsync prior to -3.0.1, this change flag will be present only for checksum-differing +3.0.1 (April 2008), this change flag will be present only for checksum-differing regular files.
  • A s means the size of a regular file is different and will be updated by the file transfer.
  • @@ -2937,9 +3181,7 @@ T means that the modification time will be set to the transfer time, which happens when a file/symlink/device is updated without --times and when a symlink is changed and the receiver can't -set its time. (Note: when using an rsync 3.0.0 client, you might see the -s flag combined with t instead of the proper T flag for this -time-setting failure.) +set its time.
  • A p means the permissions are different and are being updated to the sender's value (requires --perms).
  • An o means the owner is different and is being updated to the sender's @@ -2955,8 +3197,8 @@
  • b means that both the access and create times are being updated
  • -
  • The a means that the ACL information is being changed.
  • -
  • The x means that the extended attribute information is being changed.
  • +
  • An a means that the ACL information is being changed.
  • +
  • An x means that the extended attribute information is being changed.
  • One other output is possible: when deleting files, the "%i" will output the string "*deleting" for each item that is being removed (assuming that you @@ -2973,7 +3215,7 @@ name of the file and, if the item is a link, where it points). For a full list of the possible escape characters, see the log format setting in the rsyncd.conf manpage.

    Specifying the --out-format option implies the --info=name -option, which will mention each file, dir, etc. that gets updated in a +option, which will mention each file, directory, etc. that gets updated in a significant way (a transferred file, a recreated symlink/device, or a touched directory). In addition, if the itemize-changes escape (%i) is included in the string (e.g. if the --itemize-changes option was @@ -2989,7 +3231,7 @@

    --log-file=FILE
    -

    This option causes rsync to log what it is doing to a file. This is +

    This option causes rsync to write the log of what it is doing to a file. This is similar to the logging that a daemon does, but can be requested for the client side and/or the server side of a non-daemon transfer. If specified as a client option, transfer logging will be enabled with a default format @@ -3032,7 +3274,7 @@ followed by a list of counts by filetype (if the total is non-zero). For example: "(reg: 5, dir: 3, link: 2, dev: 1, special: 1)" lists the totals for regular files, directories, symlinks, devices, and special files. If -any of value is 0, it is completely omitted from the list. +any value is 0, it is completely omitted from the list.

  • Number of created files is the count of how many "files" (generic sense) were created (as opposed to updated). The total count will be followed by a list of counts by filetype (if the total is non-zero).
  • @@ -3040,17 +3282,17 @@ sense) were deleted. The total count will be followed by a list of counts by filetype (if the total is non-zero). Note that this line is only output if deletions are in effect, and only -if protocol 31 is being used (the default for rsync 3.1.x). +if the negotiated protocol is at least 31 (the default when both sides +are 3.1.0, September 2013, or newer).
  • Number of regular files transferred is the count of normal files that were updated via rsync's delta-transfer algorithm, which does not include -dirs, symlinks, etc. Note that rsync 3.1.0 added the word "regular" into -this heading.
  • +directories, symlinks, etc.
  • Total file size is the total sum of all file sizes in the transfer. This does not count any size for directories or special files, but does include the size of symlinks.
  • Total transferred file size is the total sum of all files sizes for just the transferred files.
  • -
  • Literal data is how much unmatched file-update data we had to send to +
  • Literal data is how much unmatched file-update data the sender had to send to the receiver for it to recreate the updated files.
  • Matched data is how much data the receiver got locally when recreating the updated files.
  • @@ -3074,10 +3316,11 @@
    --8-bit-output, -8

    This tells rsync to leave all high-bit characters unescaped in the output +(to standard output or standard error) instead of trying to test them to see if they're valid in the current locale and escaping the invalid ones. All control characters (but never tabs) are always escaped, regardless of this option's setting.

    -

    The escape idiom that started in 2.6.7 is to output a literal backslash +

    The escape idiom that started in 2.6.7 (March 2006) is to output a literal backslash (\) and a hash (#), followed by exactly 3 octal digits. For example, a newline would output as "\#012". A literal backslash that is in a filename is not escaped unless it is followed by a hash and 3 digits (0-9).

    @@ -3100,7 +3343,7 @@ (mega), G (giga), T (tera), or P (peta). For example, a 1234567-byte file would output as 1.23M in level-2 (assuming that a period is your local decimal point).

    -

    Backward compatibility note: versions of rsync prior to 3.1.0 do not +

    Backward compatibility note: versions of rsync prior to 3.1.0 (September 2013) do not support human-readable level 1, and they default to level 0. Thus, specifying one or two -h options will behave in a comparable manner in old and new versions as long as you didn't specify a --no-h option prior @@ -3121,13 +3364,13 @@ also implying that it be enabled. This enhanced partial-file method puts any partially transferred files into the specified DIR instead of writing the partial file out to the destination file. On the next transfer, rsync -will use a file found in this dir as data to speed up the resumption of the +will use a file found in this directory as data to speed up the resumption of the transfer and then delete it after it has served its purpose.

    Note that if --whole-file is specified (or implied), any partial-dir files that are found for a file that is being updated will simply be removed (since rsync is sending files without using rsync's delta-transfer algorithm).

    -

    Rsync will create the DIR if it is missing, but just the last dir -⁠-⁠ not +

    Rsync will create the DIR if it is missing, but just the last directory -⁠-⁠ not the whole path. This makes it easy to use a relative path (such as "--partial-dir=.rsync-partial") to have rsync create the partial-directory in the destination file's directory when it is needed, @@ -3148,13 +3391,13 @@

  • the auto-added rule may be ineffective at the end of your other rules, or
  • you may wish to override rsync's exclude choice.
  • -

    For instance, if you want to make rsync clean-up any left-over partial-dirs +

    For instance, if you want to make rsync clean-up any left-over partial-directories that may be lying around, you should specify --delete-after and add a "risk" filter rule, e.g. -f 'R .rsync-partial/'. Avoid using --delete-before or --delete-during unless you don't need rsync to use any of the left-over partial-dir data during the current run.

    -

    IMPORTANT: the --partial-dir should not be writable by other users or it +

    IMPORTANT: the partial-dir directory should not be writable by other users or it is a security risk! E.g. AVOID "/tmp"!

    You can also set the partial-dir value the RSYNC_PARTIAL_DIR environment variable. Setting this in the environment does not force @@ -3174,7 +3417,7 @@ partial file is now updated in-place instead of creating yet another tmp-file copy (so it maxes out at dest + tmp instead of dest + partial + tmp). This requires both ends of the transfer to be at least version -3.2.0.

    +3.2.0 (June 2020).

    For the purposes of the daemon-config's "refuse options" setting, --partial-dir does not imply --partial. This is so that a refusal of the --partial option can be used to disallow the @@ -3186,12 +3429,12 @@

    This option puts the temporary file from each updated file into a holding directory until the end of the transfer, at which time all the files are renamed into place in rapid succession. This attempts to make the updating -of the files a little more atomic. By default the files are placed into a +of the files a little closer to atomic. By default the files are placed into a directory named .~tmp~ in each file's destination directory, but if you've specified the --partial-dir option, that directory will be used instead. See the comments in the --partial-dir section for a discussion of how this .~tmp~ dir will be excluded from the transfer, -and what you can do if you want rsync to cleanup old .~tmp~ dirs that +and what you can do if you want rsync to cleanup old .~tmp~ directories that might be lying around. Conflicts with --inplace and --append.

    This option implies --no-inc-recursive since it needs the full @@ -3207,8 +3450,8 @@

  • there are no mount points in the hierarchy (since the delayed updates will fail if they can't be renamed into place).
  • -

    See also the "atomic-rsync" python script in the "support" subdir for an -update algorithm that is even more atomic (it uses --link-dest +

    See also the "atomic-rsync" python script in the "support" subdirectory for an +update algorithm that is even closer to atomic (it uses --link-dest and a parallel hierarchy of files).

    @@ -3312,7 +3555,7 @@ followed by the --info=progress2 format of progress info. If you don't know which of the 3 rsync processes is the client process, it's OK to signal all of them (since the non-client processes ignore the signal).

    -

    CAUTION: sending SIGVTALRM to an older rsync (pre-3.2.0) will kill it.

    +

    CAUTION: sending SIGVTALRM to an older rsync (pre-3.2.0, June 2020) will kill it.

    --password-file=FILE
    @@ -3334,22 +3577,22 @@ script on its stdin. One possible use of this data is to give the script a secret that can be used to mount an encrypted filesystem (which you should unmount in the the "post-xfer exec" script).

    -

    The daemon must be at least version 3.2.1.

    +

    The daemon must be at least version 3.2.1 (June 2020).

    --list-only

    This option will cause the source files to be listed instead of -transferred. This option is inferred if there is a single source arg and +transferred. This option is inferred if there is a single source argument and no destination specified, so its main uses are:

      -
    1. to turn a copy command that includes a destination arg into a +
    2. to turn a copy command that includes a destination argument into a file-listing command, or
    3. -
    4. to be able to specify more than one source arg. Note: be sure to +
    5. to be able to specify more than one source argument. Note: be sure to include the destination.
    -

    CAUTION: keep in mind that a source arg with a wild-card is expanded by the -shell into multiple args, so it is never safe to try to specify a single -wild-card arg to try to infer this option. A safe example is:

    +

    CAUTION: keep in mind that a source argument with a wild-card is expanded by the +shell into multiple arguments, so it is never safe to try to specify a single +wild-card argument to try to infer this option. A safe example is:

    rsync -av --list-only foo* dest/
     
    @@ -3360,16 +3603,17 @@ -rw-rw-r-- 80 2005/01/11 10:37:37 support/Makefile
    -

    The only option that affects this output style is (as of 3.1.0) the +

    The only option that affects this output style is the --human-readable (-h) option. The default is to output sizes as byte counts with digit separators (in a 14-character-width column). Specifying at least one -h option makes the sizes output with unit suffixes. If you want old-style bytecount sizes without digit separators (and an 11-character-width column) use --no-h.

    Compatibility note: when requesting a remote listing of files from an rsync -that is version 2.6.3 or older, you may encounter an error if you ask for a +that is version 2.6.3 or older (i.e., pre-2005), +you may encounter an error if you ask for a non-recursive listing. This is because a file listing implies the ---dirs option w/o --recursive, and older rsyncs don't +--dirs option without --recursive, and older rsyncs don't have that option. To avoid this problem, either specify the --no-dirs option (if you don't need to expand a directory's content), or turn on recursion and exclude the content of subdirectories: -r --exclude='/*/*'.

    @@ -3445,9 +3689,9 @@ with --read-batch. See the "BATCH MODE" section for details, and also the --only-write-batch option.

    This option overrides the negotiated checksum & compress lists and always -negotiates a choice based on old-school md5/md4/zlib choices. If you want -a more modern choice, use the --checksum-choice (--cc) and/or ---compress-choice (--zc) options.

    +negotiates a choice based on old-school md5/md4/zlib choices. This means +batch mode is not compatible with newer compression choices such as zstd or +lz4.

    --only-write-batch=FILE
    @@ -3528,7 +3772,7 @@
    --checksum-seed=NUM

    Set the checksum seed to the integer NUM. This 4 byte checksum seed is included in each block and MD4 file checksum calculation (the more modern -MD5 file checksums don't use a seed). By default the checksum seed is +file checksums don't use a seed). By default the checksum seed is generated by the server and defaults to the current time(). This option is used to set a specific checksum seed, which is useful for applications that want repeatable block checksums, or in the case where the @@ -3671,9 +3915,9 @@ are transferred, ignoring any deletion side-effects. Filter rules mainly affect the contents of directories that rsync is "recursing" into, but they can also affect a top-level item in the transfer that was specified as a argument.

    -

    The default for any unmatched file/dir is for it to be included in the -transfer, which puts the file/dir into the sender's file list. The use of an -exclude rule causes one or more matching files/dirs to be left out of the +

    The default for any unmatched file/directory is for it to be included in the +transfer, which puts the file/directory into the sender's file list. The use of an +exclude rule causes one or more matching files/directories to be left out of the sender's file list. An include rule can be used to limit the effect of an exclude rule that is matching too many files.

    The order of the rules is important because the first rule that matches is the @@ -3732,7 +3976,7 @@

    The following command does not need an include of the "x" directory because it is not a part of the transfer (note the trailing slash). Running this command -would copy just "/tmp/x/file.txt" because the "y" and "z" dirs get excluded:

    +would copy just "/tmp/x/file.txt" because the "y" and "z" directories get excluded:

    rsync -ai -f'+ file.txt' -f'- *' x/ host:/tmp/x/
     
    @@ -3812,7 +4056,7 @@ specified as -f'-r foo'.
    risk, 'R'
    files that match the pattern are not protected. Equivalent to a receiver-only include, so -f'R foo' could also be specified as -f'+r foo'.
    -
    clear, '!'
    clears the current include/exclude list (takes no arg)
    +
    clear, '!'
    clears the current include/exclude list (takes no argument)

    When rules are being read from a file (using merge or dir-merge), empty lines are ignored, as are whole-line comments that start with a '#' (filename rules @@ -3873,9 +4117,9 @@

  • Option -f'- /foo' would exclude a file (or directory) named foo in the transfer-root directory
  • Option -f'- foo/' would exclude any directory named foo
  • -
  • Option -f'- foo/*/bar' would exclude any file/dir named bar which is at two +
  • Option -f'- foo/*/bar' would exclude any file/directory named bar which is at two levels below a directory named foo (if foo is in the transfer)
  • -
  • Option -f'- /foo/**/bar' would exclude any file/dir named bar that was two +
  • Option -f'- /foo/**/bar' would exclude any file/directory named bar that was two or more levels below a top-level directory named foo (note that /foo/bar is not excluded by this)
  • Options -f'+ */' -f'+ *.c' -f'- *' would include all directories and .c @@ -3891,12 +4135,12 @@ absolute pathname of the current item. For example, -f'-/ /etc/passwd' would exclude the passwd file any time the transfer was sending files from the "/etc" directory, and "-⁠/ subdir/foo" would always exclude "foo" when it -is in a dir named "subdir", even if "foo" is at the root of the current +is in a directory named "subdir", even if "foo" is at the root of the current transfer.
  • A ! specifies that the include/exclude should take effect if the pattern fails to match. For instance, -f'-! */' would exclude all non-directories.
  • A C is used to indicate that all the global CVS-exclude rules should be -inserted as excludes in place of the "-⁠C". No arg should follow.
  • +inserted as excludes in place of the "-⁠C". No argument should follow.
  • An s is used to indicate that the rule applies to the sending side. When a rule affects the sending side, it affects what files are put into the sender's file list. The default is for a rule to affect both sides unless @@ -3914,7 +4158,7 @@ directory that was removed on the source from being deleted on the destination.
  • An x indicates that a rule affects xattr names in xattr copy/delete -operations (and is thus ignored when matching file/dir names). If no +operations (and is thus ignored when matching file/directory names). If no xattr-matching rules are specified, a default xattr filtering rule is used (see the --xattrs option).
  • @@ -3998,7 +4242,7 @@ global anchoring rules (i.e. a leading slash matches at the root of the transfer).

    If a per-directory merge-file is specified with a path that is a parent -directory of the first transfer directory, rsync will scan all the parent dirs +directory of the first transfer directory, rsync will scan all the parent directories from that starting point to the transfer directory for the indicated per-directory file. For instance, here is a common filter (see -F):

    @@ -4019,7 +4263,7 @@

    The first two commands above will look for ".rsync-filter" in "/" and "/src" before the normal scan begins looking for the file in "/src/path" and its -subdirectories. The last command avoids the parent-dir scan and only looks for +subdirectories. The last command avoids the parent-directory scan and only looks for the ".rsync-filter" files in each directory that is a part of the transfer.

    If you want to include the contents of a ".cvsignore" in your patterns, you should use the rule ":C", which creates a dir-merge of the .cvsignore file, but @@ -4040,7 +4284,7 @@

    Both of the above rsync commands are identical. Each one will merge all the per-directory .cvsignore rules in the middle of the list rather than at the -end. This allows their dir-specific rules to supersede the rules that follow +end. This allows their directory-specific rules to supersede the rules that follow the :C instead of being subservient to all your rules. To affect the other CVS exclude rules (i.e. the default list of exclusions, the contents of $HOME/.cvsignore, and the value of $CVSIGNORE) you should omit the -C @@ -4194,7 +4438,7 @@ stored in the batch file.

    For your convenience, a script file is also created when the write-batch option is used: it will be named the same as the batch file with ".sh" appended. This -script file contains a command-line suitable for updating a destination tree +script file contains a command line suitable for updating a destination tree using the associated batch file. It can be executed using a Bourne (or Bourne-like) shell, optionally passing in an alternate destination tree pathname which is then used instead of the original destination path. This is @@ -4248,8 +4492,8 @@ version in the batch file is too new for the batch-reading rsync to handle. See also the --protocol option for a way to have the creating rsync generate a batch file that an older rsync can understand. (Note that batch -files changed format in version 2.6.3, so mixing versions older than that with -newer versions will not work.)

    +files changed format in version 2.6.3 (September 2004), so mixing versions +older than that with newer versions will not work.)

    When reading a batch file, rsync will force the value of certain options to match the data in the batch file if you didn't set them to the same as the batch-writing command. Other options can (and should) be changed. For @@ -4263,8 +4507,6 @@ change in what gets deleted by --delete is desired. A normal user can ignore this detail and just use the shell script as an easy way to run the appropriate --read-batch command for the batched data.

    -

    The original batch mode in rsync was based on "rsync+", but the latest -version uses a new implementation.

    Three basic behaviors are possible when rsync encounters a symbolic link in the source directory.

    @@ -4305,7 +4547,7 @@

    For the effect of --munge-links, see the discussion in that option's section.

    -

    Note that the --keep-dirlinks option does not effect symlinks in the +

    Note that the --keep-dirlinks option does not affect symlinks in the transfer but instead affects how rsync treats a symlink to a directory that already exists on the receiving side. See that option's section for a warning.

    DIAGNOSTICS

    @@ -4333,7 +4575,7 @@
  • 0 -⁠ Success
  • 1 -⁠ Syntax or usage error
  • 2 -⁠ Protocol incompatibility
  • -
  • 3 -⁠ Errors selecting input/output files, dirs
  • +
  • 3 -⁠ Errors selecting input/output files, directories
  • 4 -⁠ Requested action not supported. Either:
    • an attempt was made to manipulate 64-bit files on a platform that cannot support them
    • @@ -4341,18 +4583,20 @@
  • 5 -⁠ Error starting client-server protocol
  • -
  • 6 -⁠ Daemon unable to append to log-file
  • 10 -⁠ Error in socket I/O
  • 11 -⁠ Error in file I/O
  • 12 -⁠ Error in rsync protocol data stream
  • 13 -⁠ Errors with program diagnostics
  • 14 -⁠ Error in IPC code
  • -
  • 20 -⁠ Received SIGUSR1 or SIGINT
  • +
  • 15 -⁠ Sibling process crashed (e.g. core dumped).
  • +
  • 16 -⁠ Sibling process was killed by a signal.
  • +
  • 19 -⁠ Received SIGUSR1.
  • +
  • 20 -⁠ Received SIGINT, SIGTERM, or SIGHUP.
  • 21 -⁠ Some error returned by waitpid()
  • 22 -⁠ Error allocating core memory buffers
  • 23 -⁠ Partial transfer due to error
  • 24 -⁠ Partial transfer due to vanished source files
  • -
  • 25 -⁠ The -⁠-⁠max-delete limit stopped deletions
  • +
  • 25 -⁠ The -⁠-⁠max-⁠delete limit stopped deletions
  • 30 -⁠ Timeout in data send/receive
  • 35 -⁠ Timeout waiting for daemon connection
  • @@ -4366,7 +4610,7 @@
    RSYNC_ICONV

    Specify a default --iconv setting using this environment -variable. First supported in 3.0.0.

    +variable.

    RSYNC_OLD_ARGS
    @@ -4377,7 +4621,7 @@ supersedes the RSYNC_PROTECT_ARGS variable.

    This variable is ignored if --old-args, --no-old-args, or --secluded-args is specified on the command line.

    -

    First supported in 3.2.4.

    +

    First supported in 3.2.4 (April 2022).

    RSYNC_PROTECT_ARGS
    @@ -4386,7 +4630,7 @@ disabled by default.

    This variable is ignored if --secluded-args, --no-secluded-args, or --old-args is specified on the command line.

    -

    First supported in 3.1.0. Starting in 3.2.4, this variable is ignored if +

    Starting in 3.2.4 (April 2022), this variable is ignored if RSYNC_OLD_ARGS is set to a non-zero value.

    @@ -4479,7 +4723,7 @@

    Please report bugs! See the web site at https://rsync.samba.org/.

    VERSION

    -

    This manpage is current for version 3.4.1 of rsync.

    +

    This manpage is current for version 3.5.0 of rsync.

    INTERNAL OPTIONS

    The options --server and --sender are used internally by rsync, and should never be typed by a user under normal circumstances. Some awareness of these @@ -4507,7 +4751,8 @@

    AUTHOR

    Rsync was originally written by Andrew Tridgell and Paul Mackerras. Many people from around the world have helped to maintain and improve it.

    +

    Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024.

    Mailing lists for support and development are available at https://lists.samba.org/.

    -

    15 Jan 2025

    +

    13 Aug 2026

    diff -Nru rsync-3.4.1+ds1/rsync.1.md rsync-3.5.0+ds1/rsync.1.md --- rsync-3.4.1+ds1/rsync.1.md 2024-11-20 05:45:50.000000000 +0000 +++ rsync-3.5.0+ds1/rsync.1.md 2026-08-02 21:10:00.000000000 +0000 @@ -1,6 +1,6 @@ ## NAME -rsync - a fast, versatile, remote (and local) file-copying tool +rsync - a fast, versatile, local/remote file-copying tool ## SYNOPSIS @@ -20,19 +20,19 @@ rsync [OPTION...] rsync://[USER@]HOST[:PORT]/SRC... [DEST] Push: rsync [OPTION...] SRC... [USER@]HOST::DEST - rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST) + rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST ``` -Usages with just one SRC arg and no DEST arg will list the source files instead +Usages with just one SRC argument and no DEST argument will list the source files instead of copying. -The online version of this manpage (that includes cross-linking of topics) +The online version of this manpage (which includes cross-linking of topics) is available at . ## DESCRIPTION Rsync is a fast and extraordinarily versatile file copying tool. It can copy -locally, to/from another host over any remote shell, or to/from a remote rsync +locally, to/from another host over a remote shell, or to/from a remote rsync daemon. It offers a large number of options that control every aspect of its behavior and permit very flexible specification of the set of files to be copied. It is famous for its delta-transfer algorithm, which reduces the @@ -49,7 +49,7 @@ Some of the additional features of rsync are: -- support for copying links, devices, owners, groups, and permissions +- support for copying hard and soft links, devices, owners, groups, and permissions - exclude and exclude-from options similar to GNU tar - a CVS exclude mode for ignoring the same files that CVS would ignore - can use any transparent remote shell, including ssh or rsh @@ -60,46 +60,62 @@ ## GENERAL Rsync copies files either to or from a remote host, or locally on the current -host (it does not support copying files between two remote hosts). +host. It does not support copying files between two different remote hosts. There are two different ways for rsync to contact a remote system: using a -remote-shell program as the transport (such as ssh or rsh) or contacting an +remote-shell program as the transport (such as ssh or rsh), or by contacting an rsync daemon directly via TCP. The remote-shell transport is used whenever the source or destination path contains a single colon (:) separator after a host -specification. Contacting an rsync daemon directly happens when the source or +specification. In this case, the local rsync process uses the remote +shell program to start an rsync process on the remote system. The two +rsync processes then communicate via the remote shell program to +accomplish the desired transfers. In this case, the files that are +accessible on the remote system are those accessible to the user ID +under which the remote shell starts the remote rsync process. + +Contacting an rsync daemon directly happens when the source or destination path contains a double colon (::) separator after a host -specification, OR when an rsync:// URL is specified (see also the [USING -RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION](#) section for an -exception to this latter rule). +specification, OR when an rsync:// URL is specified. +In this case, the remote rsync daemon process will +provide access only to specific sets of files, called modules, +according to its configuration. +It is also possible to contact an rsync daemon via a remote-shell +transport; see the [USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL +CONNECTION](#) section for details. -As a special case, if a single source arg is specified without a destination, +As a special case, if a single source argument is specified without a destination, the files are listed in an output format similar to "`ls -l`". -As expected, if neither the source or destination path specify a remote host, +If neither the source or destination path specify a remote host, the copy occurs locally (see also the [`--list-only`](#opt) option). -Rsync refers to the local side as the client and the remote side as the server. -Don't confuse server with an rsync daemon. A daemon is always a server, but a -server can be either a daemon or a remote-shell spawned process. +Rsync refers to the local side as the client and the remote side as +the server. The server is not to be confused with an rsync daemon. +A daemon is always a server, but a server can be either a daemon or a +remote-shell spawned process. If the transfer is local, then the +rsync subprocess which receives the contents of the files being +transferred is the server. ## SETUP -See the file README.md for installation instructions. +Most Linux distributions include rsync as a package that can be +installed using the package manager. +If for any reason you need to build and install rsync from source, +see the file README.md for installation instructions. Once installed, you can use rsync to any machine that you can access via a -remote shell (as well as some that you can access using the rsync daemon-mode -protocol). For remote transfers, a modern rsync uses ssh for its -communications, but it may have been configured to use a different remote shell -by default, such as rsh or remsh. - -You can also specify any remote shell you like, either by using the [`-e`](#opt) +remote shell, or that has an rsync daemon available. +For remote transfers, rsync normally uses ssh for its +communications. Alternatively, +you can specify any remote shell you like, either by using the [`-e`](#opt) command line option, or by setting the [`RSYNC_RSH`](#) environment variable. Note that rsync must be installed on both the source and destination machines. ## USAGE -You use rsync in the same way you use rcp. You must specify a source and a +You use rsync in a similar way to other file-copying commands such +as cp and rcp. You must specify a source and a destination, one of which may be remote. Perhaps the best way to explain the syntax is with some examples: @@ -111,8 +127,8 @@ exist on the remote system then the rsync remote-update protocol is used to update the file by sending only the differences in the data. Note that the expansion of wildcards on the command-line (`*.c`) into a list of files is -handled by the shell before it runs rsync and not by rsync itself (exactly the -same as all other Posix-style programs). +handled by the shell before it runs rsync and not by rsync itself (as for +other Posix-style programs). > rsync -avz foo:src/bar /data/tmp @@ -160,32 +176,36 @@ > rsync -ai foo/ bar/ -Rsync also has the ability to customize a destination file's name when copying -a single item. The rules for this are: +Rsync also has the ability to copy a single file to a +destination file with a different name. The rules for this are: - The transfer list must consist of a single item (either a file or an empty directory) - The final element of the destination path must not exist as a directory - The destination path must not have been specified with a trailing slash -Under those circumstances, rsync will set the name of the destination's single -item to the last element of the destination path. Keep in mind that it is best -to only use this idiom when copying a file and use the above trailing-slash +Under those circumstances, rsync will use the specified destination +path as the filename of the destination (rather than constructing a +filename using the last element of the source path). It is best +to use this idiom only when copying a file, and to use the above trailing-slash idiom when copying a directory. -The following example copies the `foo.c` file as `bar.c` in the `save` dir +The following example copies the `foo.c` file as `bar.c` in the `save` directory (assuming that `bar.c` isn't a directory): > rsync -ai src/foo.c save/bar.c -The single-item copy rule might accidentally bite you if you unknowingly copy a -single item and specify a destination dir that doesn't exist (without using a -trailing slash). For example, if `src/*.c` matches one file and `save/dir` -doesn't exist, this will confuse you by naming the destination file `save/dir`: +The single-item copy rule can give unexpected results if a wildcard +pattern for the source yields a single item, and the destination, +though specified without a trailing slash, is intended to be a +directory, but that directory does not exist. +For example, if `src/*.c` matches one file and `save/dir` +doesn't exist, this will perhaps confusingly name the destination file +`save/dir`: > rsync -ai src/*.c save/dir -To prevent such an accident, either make sure the destination dir exists or +To prevent such an accident, either make sure the destination directory exists or specify the destination path with a trailing slash: > rsync -ai src/*.c save/dir/ @@ -194,65 +214,127 @@ Rsync always sorts the specified filenames into its internal transfer list. This handles the merging together of the contents of identically named -directories, makes it easy to remove duplicate filenames. It can, however, -confuse someone when the files are transferred in a different order than what -was given on the command-line. - -If you need a particular file to be transferred prior to another, either -separate the files into different rsync calls, or consider using -[`--delay-updates`](#opt) (which doesn't affect the sorted transfer order, but -does make the final file-updating phase happen much more rapidly). - -## MULTI-HOST SECURITY - -Rsync takes steps to ensure that the file requests that are shared in a -transfer are protected against various security issues. Most of the potential -problems arise on the receiving side where rsync takes steps to ensure that the -list of files being transferred remains within the bounds of what was -requested. - -Toward this end, rsync 3.1.2 and later have aborted when a file list contains -an absolute or relative path that tries to escape out of the top of the -transfer. Also, beginning with version 3.2.5, rsync does two more safety -checks of the file list to (1) ensure that no extra source arguments were added -into the transfer other than those that the client requested and (2) ensure -that the file list obeys the exclude rules that were sent to the sender. - -For those that don't yet have a 3.2.5 client rsync (or those that want to be -extra careful), it is safest to do a copy into a dedicated destination -directory for the remote files when you don't trust the remote host. For -example, instead of doing an rsync copy into your home directory: +directories and makes it easy to remove duplicate filenames. It can, however, +result in files being transferred in a different order from that +specified on the command-line. + +If you need a particular file to be transferred prior to another, the +only way to be guaranteed of that is to separate the files into +different rsync calls. If it is sufficient for the destination files to +appear at almost the same time, consider using +[`--delay-updates`](#opt), which doesn't affect the sorted transfer order, but +does make the final file-updating phase happen much more rapidly. + +## SECURITY + +Rsync is frequently run across a network and with elevated privileges, so it is +worth thinking about who you are trusting and with what. This section is a +practical guide for safe use; the project's `SECURITY.md` describes the full +threat model and the per-platform residuals. + +### Use an authenticated, encrypted transport + +A plain `host:path` transfer runs over your remote shell (ssh by default), which +authenticates the peer and encrypts the connection -- this is the safe default. +A direct daemon connection (`host::module` or `rsync://`) is **not encrypted** +and its authentication is comparatively weak, so do not send sensitive data +across an untrusted network. +Instead tunnel it over ssh +(see [USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION](#)) or wrap it in +TLS with [**rsync-ssl**(1)](rsync-ssl.1), setting `RSYNC_SSL_CA_CERT` so that the +server certificate's chain **and** hostname are verified. When you must supply a +daemon password non-interactively, put it in a [`--password-file`](#opt) that is +readable only by you (mode 600) rather than on the command line. + +### Copying from an untrusted sending host + +When receiving from a remote host, rsync takes steps to ensure that a +corrupted or compromised remote rsync process can't cause the local +rsync process to access files beyond the bounds of what was requested. +Rsync ensures that the list of files being +transferred stays within the requested tree, and will abort when a +file list contains an absolute or relative path that tries to escape +the top of the transfer. It also verifies that no extra source arguments +were added to the transfer and that the file list obeys the exclude rules +that were sent to the sender. + +For those who want to be extra careful, +it is safest to copy untrusted remote files into a dedicated +destination directory. For example, instead of copying into your home +directory: > rsync -aiv host1:dir1 ~ -Dedicate a "host1-files" dir to the remote content: +dedicate a "host1-files" directory to the remote content: > rsync -aiv host1:dir1 ~/host1-files See the [`--trust-sender`](#opt) option for additional details. -CAUTION: it is not particularly safe to use rsync to copy files from a -case-preserving filesystem to a case-ignoring filesystem. If you must perform -such a copy, you should either disable symlinks via `--no-links` or enable the -munging of symlinks via [`--munge-links`](#opt) (and make sure you use the -right local or remote option). This will prevent rsync from doing potentially -dangerous things if a symlink name overlaps with a file or directory. It does -not, however, ensure that you get a full copy of all the files (since that may -not be possible when the names overlap). A potentially better solution is to -list all the source files and create a safe list of filenames that you pass to -the [`--files-from`](#opt) option. Any files that conflict in name would need -to be copied to different destination directories using more than one copy. - -While a copy of a case-ignoring filesystem to a case-ignoring filesystem can -work out fairly well, if no `--delete-during` or `--delete-before` option is -active, rsync can potentially update an existing file on the receiving side -without noticing that the upper-/lower-case of the filename should be changed -to match the sender. +### Copying between case-preserving and case-insensitive filesystems + +Copying from case-preserving to case-insensitive filesystems requires +caution, because it is possible for the name of a symbolic link to +overlap with the name of a file or directory once the case +distinctions are removed, which can cause potentially dangerous +results such as files being written outside the destination directory +hierarchy. +If you must perform +such a copy, either disable symlinks via `--no-links` or enable the munging of +symlinks via [`--munge-links`](#opt) (and make sure you use the right local or +remote option). +This does not, however, ensure +that you get a full copy of all the files (since that may not be possible when +the names overlap); a potentially better solution is to build a safe list of +filenames and pass it to [`--files-from`](#opt). + +### Symbolic links + +A malicious sender can include symlinks that point outside the destination tree +(for example at `/etc/passwd`). Use [`--safe-links`](#opt) to ignore any symlink +that points outside the set of files +being transferred, or [`--munge-links`](#opt) to store every symlink in a +form that is unusable on disk but recoverable later; `--no-links` drops symlinks +entirely. See the [SYMBOLIC LINKS](#) section for how these interact. + +Separately, the directory and file paths that *you* supply on the command line -- +[`--backup-dir`](#opt), [`--temp-dir`](#opt), [`--partial-dir`](#opt), the +[`--link-dest`](#opt)/[`--compare-dest`](#opt)/[`--copy-dest`](#opt) basis directories, +[`--log-file`](#opt), [`--files-from`](#opt)/`--include-from`/`--exclude-from`, +[`--filter`](#opt) merge files, [`--write-batch`](#opt)/[`--read-batch`](#opt), +and the destination itself -- are resolved so that a symlink component is followed +only when it is owned by you or by root; an attacker-planted symlink along one of +those paths is refused. [`--insecure-links`](#opt) turns that protection off +(restoring the historical follow-any-symlink behaviour); use it only when every +directory along those paths is trusted, never on a path an unprivileged user can +write. + +### Use strong checksums + +Rsync auto-negotiates the strongest checksum that both ends support, so keeping +both local and remote rsync versions reasonably current is usually all +that is needed. You can +pin the choice with [`--checksum-choice`](#opt) (`--cc`, e.g. `--cc=sha1` or one +of the xxHash variants) or constrain negotiation with the +[`RSYNC_CHECKSUM_LIST`](#) environment variable; only very old peers fall back to +MD4/MD5. This pre-transfer "does this file need updating?" checksum is separate +from the whole-file checksum rsync normally computes to verify each transferred +file afterward (verification is off when `--checksum-choice=none` is forced). + +### Protocol version + +The client and server automatically negotiate the newest protocol they both +support, so a current pair is already using the most recent version; +[`--protocol`](#opt) only *forces an older* version for compatibility and should +not be used to downgrade a connection. Avoiding old protocols is therefore a +matter of running a current rsync on both ends (a protocol below 30 also forces +the weak MD4 authentication digest on a daemon connection -- see the AUTHENTICATION +STRENGTH section of [**rsyncd.conf**(5)](rsyncd.conf.5)). ## ADVANCED USAGE The syntax for requesting multiple files from a remote host is done by -specifying additional remote-host args in the same style as the first, or with +specifying additional remote-host arguments in the same style as the first, or with the hostname omitted. For instance, all these work: > rsync -aiv host:file1 :file2 host:file{3,4} /dest/ @@ -264,20 +346,21 @@ modname of the first path, it is assumed to be a path in the module (such as the extra-file1 & extra-file2 that are grabbed above). -Really old versions of rsync (2.6.9 and before) only allowed specifying one -remote-source arg, so some people have instead relied on the remote-shell -performing space splitting to break up an arg into multiple paths. Such -unintuitive behavior is no longer supported by default (though you can request -it, as described below). - -Starting in 3.2.4, filenames are passed to a remote shell in such a way as to +Because really old versions of rsync (prior to 3.0.0, released +March 2008) only allowed specifying one +remote-source argument, some people have come to rely on the remote shell +performing space splitting to break a single argument into multiple paths. Such +unintuitive behavior is no longer supported by default, though you can request +it, as described in the next paragraph. +Rsync now (since 3.2.4) passes filenames to a remote +shell in such a way as to preserve the characters you give it. Thus, if you ask for a file with spaces in the name, that's what the remote rsync looks for: > rsync -aiv host:'a simple file.pdf' /dest/ If you use scripts that have been written to manually apply extra quoting to -the remote rsync args (or to require remote arg splitting), you can ask rsync +the remote rsync arguments, or to rely on remote argument splitting, you can ask rsync to let your script handle the extra escaping. This is done by either adding the [`--old-args`](#opt) option to the rsync runs in the script (which requires a new rsync) or exporting [RSYNC_OLD_ARGS](#)=1 and [RSYNC_PROTECT_ARGS](#)=0 @@ -285,28 +368,36 @@ ## CONNECTING TO AN RSYNC DAEMON -It is also possible to use rsync without a remote shell as the transport. In -this case you will directly connect to a remote rsync daemon, typically using -TCP port 873. (This obviously requires the daemon to be running on the remote -system, so refer to the [STARTING AN RSYNC DAEMON TO ACCEPT CONNECTIONS](#) -section below for information on that.) +An rsync daemon provides a way to give access to one or more directory +hierarchies on a system in a controlled way, without having to provide +shell access to the system. Each directory hierarchy is called a +"module". The names, directories, access permissions and so on for +each module are defined by the rsync daemon configuration file, +described in [**rsyncd.conf**(5)](rsyncd.conf.5). +Connections to an rsync daemon typically use TCP port 873. The +administrator of the system would normally arrange for the rsync +daemon to be running; +refer to the [STARTING AN RSYNC DAEMON TO ACCEPT CONNECTIONS](#) +section below for information on how to do that. -Using rsync in this way is the same as using it with a remote shell except -that: +From the client point of view, using rsync to access an rsync daemon +is similar to using it with a remote shell except that: - Use either double-colon syntax or rsync:// URL syntax instead of the single-colon (remote shell) syntax. - The first element of the "path" is actually a module name. -- Additional remote source args can use an abbreviated syntax that omits the +- Additional remote source arguments can use an abbreviated syntax that omits the hostname and/or the module name, as discussed in [ADVANCED USAGE](#). - The remote daemon may print a "message of the day" when you connect. - If you specify only the host (with no module or path) then a list of accessible modules on the daemon is output. - If you specify a remote source path but no destination, a listing of the matching files on the remote daemon is output. -- The [`--rsh`](#opt) (`-e`) option must be omitted to avoid changing the - connection style from using a socket connection to [USING RSYNC-DAEMON - FEATURES VIA A REMOTE-SHELL CONNECTION](#). +- To contact an rsync daemon directly, the [`--rsh`](#opt) (`-e`) + option must be omitted. If it is used, rsync will use the specified + remote shell program to start an rsync daemon on the remote system. + See the [USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL + CONNECTION](#) section for information on this mode of operation. An example that copies all the files in a remote module named "src": @@ -318,7 +409,7 @@ want to use or using the [`--password-file`](#opt) option. This may be useful when scripting rsync. -WARNING: On some systems environment variables are visible to all users. On +WARNING: On some systems, environment variables are visible to all users. On those systems using [`--password-file`](#opt) is recommended. You may establish the connection via a web proxy by setting the environment @@ -347,34 +438,45 @@ It is sometimes useful to use various features of an rsync daemon (such as named modules) without actually allowing any new socket connections into a -system (other than what is already required to allow remote-shell access). +system, other than what is already required to allow remote-shell access. Rsync supports connecting to a host using a remote shell and then spawning a -single-use "daemon" server that expects to read its config file in the home dir +single-use "daemon" server that expects to read its config file in the home directory of the remote user. This can be useful if you want to encrypt a daemon-style -transfer's data, but since the daemon is started up fresh by the remote user, -you may not be able to use features such as chroot or change the uid used by -the daemon. (For another way to encrypt a daemon transfer, consider using ssh -to tunnel a local port to a remote machine and configure a normal rsync daemon -on that remote host to only allow connections from "localhost".) +transfer's data. However, since the daemon is started by the remote user, +you may not be able to use features which require root privilege, such +as chroot or setting the user ID used by the daemon. +(For another way to encrypt a daemon transfer, consider using ssh +to tunnel a local port to a remote machine, and configure a normal rsync daemon +on that remote host to allow connections only from "localhost".) From the user's perspective, a daemon transfer via a remote-shell connection uses nearly the same command-line syntax as a normal rsync-daemon transfer, -with the only exception being that you must explicitly set the remote shell +with the only difference being that you must explicitly set the remote shell program on the command-line with the [`--rsh=COMMAND`](#opt) option. (Setting the RSYNC_RSH in the environment will not turn on this functionality.) For example: > rsync -av --rsh=ssh host::module /dest -If you need to specify a different remote-shell user, keep in mind that the -user@ prefix in front of the host is specifying the rsync-user value (for a -module that requires user-based authentication). This means that you must give -the '-l user' option to ssh when specifying the remote-shell, as in this -example that uses the short version of the [`--rsh`](#opt) option: +A "user@" prefix in front of the host serves two purposes at once in this mode: +it is the rsync-user value (used to log in to a module that requires user-based +authentication) and, by default, it is also passed to the remote shell as the +login user. So the simple form + +> rsync -av --rsh=ssh user@host::module /dest + +runs `ssh -l user host` and offers "user" as the rsync-user to the module; the +two are forced to be the same name. + +If you need the remote-shell (ssh) login to use a different name than the +rsync-user, give an explicit '-l' option to ssh in the remote-shell command. +When ssh is already told which user to log in as, rsync does not add its own +'-l', so the "user@" prefix is then used only as the rsync-user. For example, +using the short version of the [`--rsh`](#opt) option: > rsync -av -e "ssh -l ssh-user" rsync-user@host::module /dest -The "ssh-user" will be used at the ssh level; the "rsync-user" will be used to -log-in to the "module". +Here "ssh-user" is used at the ssh level while "rsync-user" is used to log in to +the "module". In this setup, the daemon is started by the ssh command that is accessing the system (which can be forced via the `~/.ssh/authorized_keys` file, if desired). @@ -386,9 +488,9 @@ daemon already running (or it needs to have configured something like inetd to spawn an rsync daemon for incoming connections on a particular port). For full information on how to start a daemon that will handling incoming socket -connections, see the [**rsyncd.conf**(5)](rsyncd.conf.5) manpage -- that is -the config file for the daemon, and it contains the full details for how to run -the daemon (including stand-alone and inetd configurations). +connections, see the manpage for [**rsyncd.conf**(5)](rsyncd.conf.5), +the config file for the daemon. The manpage contains the full details for how to run +the daemon, including stand-alone and inetd configurations. If you're using one of the remote-shell transports for the transfer, there is no need to manually start an rsync daemon. @@ -425,8 +527,11 @@ --archive, -a archive mode is -rlptgoD (no -A,-X,-U,-N,-H) --no-OPTION turn off an implied OPTION (e.g. --no-D) --recursive, -r recurse into directories +--inc-recursive, --i-r enable incremental recursion +--no-inc-recursive disable incremental recursion +--no-i-r same as --no-inc-recursive --relative, -R use relative path names ---no-implied-dirs don't send implied dirs with --relative +--no-implied-dirs don't send implied directories with --relative --backup, -b make backups (see --suffix & --backup-dir) --backup-dir=DIR make backups into hierarchy based in DIR --suffix=SUFFIX backup suffix (default ~ w/o --backup-dir) @@ -435,15 +540,18 @@ --append append data onto shorter files --append-verify --append w/old data in file checksum --dirs, -d transfer directories without recursing ---old-dirs, --old-d works like --dirs when talking to old rsync +--old-dirs works like --dirs when talking to old rsync +--old-d same as --old-dirs --mkpath create destination's missing path components --links, -l copy symlinks as symlinks ---copy-links, -L transform symlink into referent file/dir +--copy-links, -L transform symlink into referent file/directory --copy-unsafe-links only "unsafe" symlinks are transformed --safe-links ignore symlinks that point outside the tree +--insecure-links follow attacker-owned symlinks in operator paths +--confine-root=DIR refuse operator paths resolving outside DIR --munge-links munge symlinks to make them safe & unusable ---copy-dirlinks, -k transform symlink to dir into referent dir ---keep-dirlinks, -K treat symlinked dir on receiver as dir +--copy-dirlinks, -k transform symlink to directory into referent directory +--keep-dirlinks, -K treat symlinked directory on receiver as directory --hard-links, -H preserve hard links --perms, -p preserve permissions --executability, -E preserve executability @@ -456,6 +564,7 @@ --copy-devices copy device contents as a regular file --write-devices write to devices as files (implies --inplace) --specials preserve special files +--drop-D receiver refuses to create devices/specials -D same as --devices --specials --times, -t preserve modification times --atimes, -U preserve access (use) times @@ -469,25 +578,27 @@ --preallocate allocate dest files before writing them --dry-run, -n perform a trial run with no changes made --whole-file, -W copy files whole (w/o delta-xfer algorithm) +--no-whole-file, --no-W use the delta-xfer algorithm --checksum-choice=STR choose the checksum algorithm (aka --cc) --one-file-system, -x don't cross filesystem boundaries --block-size=SIZE, -B force a fixed checksum block-size --rsh=COMMAND, -e specify the remote shell to use --rsync-path=PROGRAM specify the rsync to run on remote machine --existing skip creating new files on receiver +--ignore-non-existing skip creating new files on receiver --ignore-existing skip updating files that exist on receiver ---remove-source-files sender removes synchronized files (non-dir) +--remove-source-files sender removes synchronized files (non-directory) --del an alias for --delete-during ---delete delete extraneous files from dest dirs +--delete delete extraneous files from dest directories --delete-before receiver deletes before xfer, not during --delete-during receiver deletes during the transfer --delete-delay find deletions during, delete after --delete-after receiver deletes after transfer, not during ---delete-excluded also delete excluded files from dest dirs ---ignore-missing-args ignore missing source args without error ---delete-missing-args delete missing source args from destination +--delete-excluded also delete excluded files from dest directories +--ignore-missing-args ignore missing source arguments without error +--delete-missing-args delete missing source arguments from destination --ignore-errors delete even if there are I/O errors ---force force deletion of dirs even if not empty +--force force deletion of directories even if not empty --max-delete=NUM don't delete more than NUM files --max-size=SIZE don't transfer any file larger than SIZE --min-size=SIZE don't transfer any file smaller than SIZE @@ -513,6 +624,7 @@ --compress, -z compress file data during the transfer --compress-choice=STR choose the compression algorithm (aka --zc) --compress-level=NUM explicitly set compression level (aka --zl) +--compress-threads=NUM explicitly set compression threads (aka --zt) --skip-compress=LIST skip compressing files with suffix in LIST --cvs-exclude, -C auto-ignore files in the same way CVS does --filter=RULE, -f add a file-filtering RULE @@ -524,8 +636,8 @@ --include-from=FILE read include patterns from FILE --files-from=FILE read list of source-file names from FILE --from0, -0 all *-from/filter files are delimited by 0s ---old-args disable the modern arg-protection idiom ---secluded-args, -s use the protocol to safely send the args +--old-args disable the modern argument-protection idiom +--secluded-args, -s use the protocol to safely send the arguments --trust-sender trust the remote sender's file list --copy-as=USER[:GROUP] specify user & optional group for the copy --address=ADDRESS bind address for outgoing socket to daemon @@ -600,8 +712,8 @@ The parameter may need to be quoted in some manner for it to survive the shell's command-line parsing. Also keep in mind that a leading tilde (`~`) in a pathname is substituted by your shell, so make sure that you separate the -option name from the pathname using a space if you want the local shell to -expand it. +option name from the pathname using a space (rather than an equal +sign) if you want the local shell to expand it. [comment]: # (Some markup below uses a literal non-breakable space when a backtick string) [comment]: # (needs to contain a space since markdown strips spaces from the start/end) @@ -612,12 +724,12 @@ Print a short help page describing the options available in rsync and exit. You can also use `-h` for `--help` when it is used without any other - options (since it normally means [`--human-readable`](#opt)). + options or arguments (since otherwise it means [`--human-readable`](#opt)). 0. `--version`, `-V` Print the rsync version plus other info and exit. When repeated, the - information is output is a JSON format that is still fairly readable + information is output in a JSON format that is still fairly readable (client side only). The output includes a list of compiled-in capabilities, a list of @@ -646,15 +758,16 @@ human-readable by using the [`--human-readable`](#opt) (or `--no-human-readable`) options. - In a modern rsync, the `-v` option is equivalent to the setting of groups + The `-v` option is equivalent to the setting of groups of [`--info`](#opt) and [`--debug`](#opt) options. You can choose to use these newer options in addition to, or in place of using `--verbose`, as any fine-grained settings override the implied settings of `-v`. Both - [`--info`](#opt) and [`--debug`](#opt) have a way to ask for help that - tells you exactly what flags are set for each increase in verbosity. + [`--info`](#opt) and [`--debug`](#opt) have help texts available that + tells you exactly what flags are set for each increase in verbosity + (use `--info=help` or `--debug=help` to see them). However, do keep in mind that a daemon's "`max verbosity`" setting will limit - how high of a level the various individual flags can be set on the daemon + how high the various individual flags can be set on the daemon side. For instance, if the max is 2, then any info and/or debug flag that is set to a higher value than what would be set by `-vv` will be downgraded to the `-vv` level in the daemon's logging. @@ -676,9 +789,9 @@ and [`--itemize-changes`](#opt) (`-i`) options. See those options for more information on what is output and when. - This option was added to 3.1.0, so an older rsync on the server side might - reject your attempts at fine-grained control (if one or more flags needed - to be send to the server and the server was too old to understand them). + This option was added to 3.1.0 (released September 2013), + so an older rsync on the server side might + reject your attempts at fine-grained control. See also the "`max verbosity`" caveat above when dealing with a daemon. 0. `--debug=FLAGS` @@ -697,9 +810,9 @@ Note that some debug messages will only be output when the [`--stderr=all`](#opt) option is specified, especially those pertaining to I/O and buffer debugging. - Beginning in 3.2.0, this option is no longer auto-forwarded to the server - side in order to allow you to specify different debug values for each side - of the transfer, as well as to specify a new debug option that is only + This option is not forwarded by the client to the server side, so that + you can specify different debug values for each side of the transfer. + This also allows you to specify debug options that are only present in one of the rsync versions. If you want to duplicate the same option on both sides, using brace expansion is an easy way to save you some typing. This works in zsh and bash: @@ -708,20 +821,21 @@ 0. `--stderr=errors|all|client` - This option controls which processes output to stderr and if info messages - are also changed to stderr. The mode strings can be abbreviated, so feel - free to use a single letter value. The 3 possible choices are: + This option controls which processes output to stderr, and whether + info messages are output to stderr rather than stdout. + The mode strings can be abbreviated down as far as a single letter. + The 3 possible choices are: - - `errors` - (the default) causes all the rsync processes to send an - error directly to stderr, even if the process is on the remote side of + - `errors` - (the default) causes all the rsync processes to send + errors directly to stderr, even if the process is on the remote side of the transfer. Info messages are sent to the client side via the protocol stream. If stderr is not available (i.e. when directly connecting with a daemon via a socket) errors fall back to being sent via the protocol stream. - - `all` - causes all rsync messages (info and error) to get written + - `all` - causes all rsync messages (info and error) to be written directly to stderr from all (possible) processes. This causes stderr to - become line-buffered (instead of raw) and eliminates the ability to + become line-buffered (instead of unbuffered) and eliminates the ability to divide up the info and error messages by file handle. For those doing debugging or using several levels of verbosity, this option can help to avoid clogging up the transfer stream (which should prevent any chance of @@ -736,11 +850,10 @@ older rsync, you may want to use `--stderr=all` since that idiom has been around for several releases. - This option was added in rsync 3.2.3. This version also began the - forwarding of a non-default setting to the remote side, though rsync uses - the backward-compatible options `--msgs2stderr` and `--no-msgs2stderr` to - represent the `all` and `client` settings, respectively. A newer rsync - will continue to accept these older option names to maintain compatibility. + Rsync also accepts the old `--msgs2stderr` and `--no-msgs2stderr` + options, equivalent to the `all` and `client` settings, + respectively, and uses them when forwarding non-default settings + to the remote side, in case the remote rsync is an older version. 0. `--quiet`, `-q` @@ -781,7 +894,8 @@ When comparing two timestamps, rsync treats the timestamps as being equal if they differ by no more than the modify-window value. The default is 0, which matches just integer seconds. If you specify a negative value (and - the receiver is at least version 3.1.3) then nanoseconds will also be taken + the receiver is at least version 3.1.3, released January 2018) + then nanoseconds will also be taken into account. Specifying 1 is useful for copies to/from MS Windows FAT filesystems, because FAT represents times with a 2-second resolution (allowing times to differ from the original by up to 1 second). @@ -802,7 +916,7 @@ need of a transfer. Without this option, rsync uses a "quick check" that (by default) checks if each file's size and time of last modification match between the sender and receiver. This option changes this to compare a - 128-bit checksum for each file that has a matching size. Generating the + checksum for each file that has a matching size. Generating the checksums means that both sides will expend a lot of disk I/O reading all the data in the files in the transfer, so this can slow things down significantly (and this is prior to any reading that will be done to @@ -814,7 +928,7 @@ file that has the same size as the corresponding sender's file: files with either a changed size or a changed checksum are selected for transfer. - Note that rsync always verifies that each _transferred_ file was correctly + Note that rsync normally verifies that each _transferred_ file was correctly reconstructed on the receiving side by checking a whole-file checksum that is generated as the file is transferred, but that automatic after-the-transfer verification has nothing to do with this option's @@ -867,9 +981,10 @@ 0. `--inc-recursive`, `--i-r` - This option explicitly enables on incremental recursion when scanning for + This option explicitly enables incremental recursion when scanning for files, which is enabled by default when using the [`--recursive`](#opt) - option and both sides of the transfer are running rsync 3.0.0 or newer. + option and both sides of the transfer are running rsync 3.0.0 + (released March 2008) or newer. Incremental recursion uses much less memory than non-incremental, while also beginning the transfer more quickly (since it doesn't need to scan the @@ -883,13 +998,12 @@ - [`--prune-empty-dirs`](#opt) - [`--delay-updates`](#opt) - In order to make [`--delete`](#opt) compatible with incremental recursion, - rsync 3.0.0 made [`--delete-during`](#opt) the default delete mode (which - was first added in 2.6.4). + In order to be compatible with incremental recursion, + [`--delete-during`](#opt) is the default delete mode for [`--delete`](#opt). One side-effect of incremental recursion is that any missing sub-directories inside a recursively-scanned directory are (by default) - created prior to recursing into the sub-dirs. This earlier creation point + created prior to recursing into the sub-directories. This earlier creation point (compared to a non-incremental recursion) allows rsync to then set the modify time of the finished directory right away (without having to delay that until a bunch of recursive copying has finished). However, these @@ -903,7 +1017,7 @@ 0. `--no-inc-recursive`, `--no-i-r` - Disables the new incremental recursion algorithm of the + Disables the incremental recursion algorithm of the [`--recursive`](#opt) option. This makes rsync scan the full file list before it begins to transfer files. See [`--inc-recursive`](#opt) for more info. @@ -913,8 +1027,7 @@ Use relative paths. This means that the full path names specified on the command line are sent to the server rather than just the last parts of the filenames. This is particularly useful when you want to send several - different directories at the same time. For example, if you used this - command: + different directories at the same time. For example, this command: > rsync -av /foo/bar/baz.c remote:/tmp/ @@ -928,30 +1041,31 @@ "implied directories" (i.e. the "foo" and the "foo/bar" directories in the above example). - Beginning with rsync 3.0.0, rsync always sends these implied directories as + Rsync always sends these implied directories as real directories in the file list, even if a path element is really a symlink on the sending side. This prevents some really unexpected behaviors - when copying the full path of a file that you didn't realize had a symlink + when copying the full path of a file that has a symlink in its path. If you want to duplicate a server-side symlink, include both - the symlink via its path, and referent directory via its real path. If + the symlink via its path, and the referent directory via its real path. If you're dealing with an older rsync on the sending side, you may need to use the [`--no-implied-dirs`](#opt) option. It is also possible to limit the amount of path information that is sent as implied directories for each path you specify. With a modern rsync on the - sending side (beginning with 2.6.7), you can insert a dot and a slash into + sending side (beginning with 2.6.7, released March 2006), + you can insert a dot and a slash into the source path, like this: > rsync -avR /foo/./bar/baz.c remote:/tmp/ - That would create /tmp/bar/baz.c on the remote machine. (Note that the dot + That would create /tmp/bar/baz.c on the receiving machine. (Note that the dot must be followed by a slash, so "/foo/." would not be abbreviated.) For - older rsync versions, you would need to use a chdir to limit the source + older rsync versions, you would need to change directory to limit the source path. For example, when pushing files: > (cd /foo; rsync -avR bar/baz.c remote:/tmp/) - (Note that the parens put the two commands into a sub-shell, so that the + (Note that the parentheses put the two commands into a sub-shell, so that the "cd" command doesn't remain in effect for future commands.) If you're pulling files from an older rsync, use this idiom (but only for a non-daemon transfer): @@ -969,25 +1083,33 @@ This even allows these implied path elements to have big differences, such as being a symlink to a directory on the receiving side. - For instance, if a command-line arg or a files-from entry told rsync to + For instance, if a command-line argument or a files-from entry told rsync to transfer the file "path/foo/file", the directories "path" and "path/foo" are implied when [`--relative`](#opt) is used. If "path/foo" is a symlink to "bar" on the destination system, the receiving rsync would ordinarily delete "path/foo", recreate it as a directory, and receive the file into the new - directory. With `--no-implied-dirs`, the receiving rsync updates - "path/foo/file" using the existing path elements, which means that the file - ends up being created in "path/bar". Another way to accomplish this link - preservation is to use the [`--keep-dirlinks`](#opt) option (which will also affect - symlinks to directories in the rest of the transfer). + directory. With `--no-implied-dirs`, the receiving rsync leaves the + existing "path/foo" symlink in place and follows it, so the file is written + through the symlink and ends up in "path/bar". Note the security + implication: a pre-existing in-tree symlink-to-directory on the receiving + side will redirect where the file is written, so only use + `--no-implied-dirs` when you trust the destination's existing path elements. + (A symlink whose target is absolute or escapes the destination tree is still + refused by the secure path resolver rather than followed.) Another way to + accomplish this link preservation is to use the [`--keep-dirlinks`](#opt) + option (which will also affect symlinks to directories in the rest of the + transfer). - When pulling files from an rsync older than 3.0.0, you may need to use this + When pulling files from an rsync older than 3.0.0 (March 2008), + you may need to use this option if the sending side has a symlink in the path you request and you wish the implied directories to be transferred as normal directories. 0. `--backup`, `-b` With this option, preexisting destination files are renamed as each file is - transferred or deleted. You can control where the backup file goes and + transferred or deleted (that is, each file to be deleted or overwritten + gets renamed instead). You can control where the backup file goes and what (if any) suffix gets appended using the [`--backup-dir`](#opt) and [`--suffix`](#opt) options. @@ -1017,8 +1139,9 @@ Note that if you specify a relative path, the backup directory will be relative to the destination directory, so you probably want to specify either an absolute path or a path that starts with "../". If an rsync - daemon is the receiver, the backup dir cannot go outside the module's path - hierarchy, so take extra care not to delete it or copy into it. + daemon is the receiver, the backup directory cannot go outside the module's + path hierarchy, so take extra care not to delete or copy into the + backup directory inadvertently. 0. `--suffix=SUFFIX` @@ -1033,10 +1156,10 @@ destination file has a modification time equal to the source file's, it will be updated if the sizes are different.) - Note that this does not affect the copying of dirs, symlinks, or other - special files. Also, a difference of file format between the sender and + Note that this does not affect the copying of directories, symlinks, or other + special files. Also, a difference of object type between the sender and receiver is always considered to be important enough for an update, no - matter what date is on the objects. In other words, if the source has a + matter what date is on the objects. For example, if the source has a directory where the destination has a file, the transfer would occur regardless of the timestamps. @@ -1089,13 +1212,12 @@ The option implies [`--partial`](#opt) (since an interrupted transfer does not delete the file), but conflicts with [`--partial-dir`](#opt) and - [`--delay-updates`](#opt). Prior to rsync 2.6.4 `--inplace` was also - incompatible with [`--compare-dest`](#opt) and [`--link-dest`](#opt). + [`--delay-updates`](#opt). 0. `--append` - This special copy mode only works to efficiently update files that are - known to be growing larger where any existing content on the receiving side + This special copy mode is only applicable to update files that are + known to be growing larger, that is, where any existing content on the receiving side is also known to be the same as the content on the sender. The use of `--append` **can be dangerous** if you aren't 100% sure that all the files in the transfer are shared, growing files. You should thus use filter @@ -1104,8 +1226,8 @@ Rsync updates these growing file in-place without verifying any of the existing content in the file (it only verifies the content that it is appending). Rsync skips any files that exist on the receiving side that - are not shorter than the associated file on the sending side (which means - that new files are transferred). It also skips any files whose size on the + are not shorter than the associated file on the sending side. + It also skips any files whose size on the sending side gets shorter during the send negotiations (rsync warns about a "diminished" file when this happens). @@ -1117,12 +1239,12 @@ 0. `--append-verify` This special copy mode works like [`--append`](#opt) except that all the - data in the file is included in the checksum verification (making it less - efficient but also potentially safer). This option **can be dangerous** if + data in the file is included in the checksum verification, making it less + efficient but also potentially safer. This option **can be dangerous** if you aren't 100% sure that all the files in the transfer are shared, growing files. See the [`--append`](#opt) option for more details. - Note: prior to rsync 3.0.0, the [`--append`](#opt) option worked like + Note: prior to rsync 3.0.0 (March 2008), the [`--append`](#opt) option worked like `--append-verify`, so if you are interacting with an older rsync (or the transfer is using a protocol prior to 30), specifying either append option will initiate an `--append-verify` transfer. @@ -1143,17 +1265,22 @@ seen in the listing). Specify `--no-dirs` (or `--no-d`) if you want to turn this off. - There is also a backward-compatibility helper option, `--old-dirs` - (`--old-d`) that tells rsync to use a hack of `-r --exclude='/*/*'` to get - an older rsync to list a single directory without recursing. + See also the backward-compatibility helper option [`--old-dirs`](#opt). + +0. `--old-dirs`, `--old-d` + + This backward-compatibility helper tells rsync to use a hack of + `-r --exclude='/*/*'` to get an older remote rsync to list a single directory + without recursing. 0. `--mkpath` Create all missing path components of the destination path. - By default, rsync allows only the final component of the destination path - to not exist, which is an attempt to help you to validate your destination - path. With this option, rsync creates all the missing destination-path + By default, rsync will create only the final component of the destination path + if it does not exist. If any other component does not exist that is an + error, as this can help to catch mistakes in the destination path specification. + With this option, rsync creates all the missing destination-path components, just as if `mkdir -p $DEST_PATH` had been run on the receiving side. @@ -1163,11 +1290,11 @@ section for full details on how rsync decides if a final destination-path component should be created as a directory or not. - If you would like the newly-created destination dirs to match the dirs on + If you would like the newly-created destination directories to match the directories on the sending side, you should be using [`--relative`](#opt) (`-R`) instead of `--mkpath`. For instance, the following two commands result in the same destination tree, but only the second command ensures that the - "some/extra/path" components match the dirs on the sending side: + "some/extra/path" components match the directories on the sending side: > rsync -ai --mkpath host:some/extra/path/*.c some/extra/path/ > rsync -aiR host:some/extra/path/*.c ./ @@ -1176,7 +1303,7 @@ Add symlinks to the transferred files instead of noisily ignoring them with a "non-regular file" warning for each symlink encountered. You can - alternately silence the warning by specifying [`--info=nonreg0`](#opt). + alternatively silence the warning by specifying [`--info=nonreg0`](#opt). The default handling of symlinks is to recreate each symlink's unchanged value on the receiving side. @@ -1194,12 +1321,7 @@ transfer, since there are no symlinks left in the transfer. This option does not change the handling of existing symlinks on the - receiving side, unlike versions of rsync prior to 2.6.3 which had the - side-effect of telling the receiving side to also follow symlinks. A - modern rsync won't forward this option to a remote receiver (since only the - sender needs to know about it), so this caveat should only affect someone - using an rsync client older than 2.6.7 (which is when `-L` stopped being - forwarded to the receiver). + receiving side. See the [`--keep-dirlinks`](#opt) (`-K`) if you need a symlink to a directory to be treated as a real directory on the receiving side. @@ -1213,14 +1335,17 @@ and so are any symlinks in the source path itself when [`--relative`](#opt) is used. - Note that the cut-off point is the top of the transfer, which is the part - of the path that rsync isn't mentioning in the verbose output. If you copy - "/src/subdir" to "/dest/" then the "subdir" directory is a name inside the - transfer tree, not the top of the transfer (which is /src) so it is legal - for created relative symlinks to refer to other names inside the /src and - /dest directories. If you instead copy "/src/subdir/" (with a trailing - slash) to "/dest/subdir" that would not allow symlinks to any files outside - of "subdir". + A symlink is judged unsafe by a lexical test on its value, without resolving + it on disk. An absolute or empty target is always unsafe. A relative + target is unsafe if its ".." components would climb above the top of the + transfer; a ".." that appears anywhere other than as a leading prefix (an + embedded or trailing "/..") is also treated as unsafe. The top is set by + how the source is specified: a trailing slash on the source (e.g. + "/src/subdir/" copied to "/dest/subdir") makes that directory itself the + top, so a symlink may not point above it; without the trailing slash (e.g. + "/src/subdir" copied to "/dest/") the source's parent ("/src") is the top, + so "subdir" is a name inside the transfer and a relative symlink may point + to any other name within it. Note that safe symlinks are only copied if [`--links`](#opt) was also specified or implied. The `--copy-unsafe-links` option has no extra effect @@ -1240,28 +1365,96 @@ present in the transfer prevents any matching file on the receiver from being deleted when the symlink is deemed to be unsafe and is skipped. - This option must be combined with [`--links`](#opt) (or - [`--archive`](#opt)) to have any symlinks in the transfer to conditionally - ignore. Its effect is superseded by [`--copy-unsafe-links`](#opt). + This option has no effect unless it is combined with [`--links`](#opt) (or + an option that implies [`--links`](#opt) such as [`--archive`](#opt)), + because without [`--links`](#opt) there will not be any symlinks in the + transfer. Its effect is superseded by [`--copy-unsafe-links`](#opt). Using this option in conjunction with [`--relative`](#opt) may give unexpected results. See the [SYMBOLIC LINKS](#) section for multi-option info. +0. `--insecure-links` + + By default rsync resolves the directory and file paths that the **operator** + supplies on the command line with a defensive directory-tree walk that + **follows a symlink component only when it is owned by uid 0 or by the + running user**, and + refuses one owned by any other user. This stops an attacker who can write + inside one of those directories from planting a symlink that + redirects a privileged rsync to a target outside the intended tree, while + still honouring the operator's own symlinks. + The rule is the same for absolute and relative paths. + This applies to the destination directory and to the parameters to the + following options: `--backup-dir`, `--temp-dir`/`-T`, + `--partial-dir`, `--link-dest`, `--compare-dest`, `--copy-dest`, `--log-file`, + `--password-file`, `--files-from`, `--include-from`, `--exclude-from`, + `--filter` merge files, `--write-batch` and `--read-batch`. + + `--insecure-links` turns that defence off, restoring the historical behaviour + of following any symlink in those paths. Use it only when you fully trust + every directory along each operator-supplied path, as it re-exposes the + symlink-redirection attacks the walk prevents. + + The option is **local only**: it is not sent to the remote side of the + transfer. A remote-shell peer that needs the opt-out must set it there, e.g. + via [`--rsync-path`](#opt)), and a daemon never honours it -- a daemon that + receives `--insecure-links` from a client refuses the request. A daemon + administrator who wants the legacy behaviour for a single trusted module sets + "`insecure links = yes`" in that module's **rsyncd.conf**(5) section instead; + a client can never enable it. + + See the [SYMBOLIC LINKS](#) section for multi-option info. + +0. `--confine-root=DIR` + + This bounds where the paths listed under [`--insecure-links`](#opt) are + allowed to resolve: one that ends up outside DIR is refused, even if every + symlink along it was owned by a trusted user. The ownership walk asks who + planted a link; this asks where the path came out. + + DIR must be absolute. Nothing is confined by default, and + `--confine-root=/` is a no-op. + + It exists for a wrapper that serves a restricted directory over a remote + shell, `rrsync` being the one shipped here, which passes it automatically + whenever its restricted dir is not "`/`". Such a wrapper can vet the argv + it is handed, but filter rules travel over the protocol instead: a client + can name a merge file outside the restricted dir in a dir-merge rule and + have the server read it in as filter rules. On a pull that needs neither + `--delete` nor any verbosity, because an exclude-only merge (the "`-`" + modifier) makes every line a pattern, and the client reads the file's + contents off which of its own names went missing. Confining the open is + what closes that; a merge file inside DIR keeps working as before. + + A daemon ignores this option -- its module directory is already the + boundary, and the option arrives in a client-supplied argv, so honouring it + could only widen the module. + + [`--insecure-links`](#opt) is refused alongside it. That opt-out restores + the historical open, which skips the walk that enforces the root, so the two + together would silently mean no confinement at all. + + Like [`--drop-D`](#opt), it is not forwarded to the remote side: it is meant + to be applied to one end of a connection by itself. + 0. `--munge-links` This option affects just one side of the transfer and tells rsync to munge symlink values when it is receiving files or unmunge symlink values when it - is sending files. The munged values make the symlinks unusable on disk but - allows the original contents of the symlinks to be recovered. + is sending files. + "Munging" changes the symlink target so that the symlink cannot be followed + successfully, but allows the original target of the symlink to be + recovered. "Unmunging" reverses this process so that the symlink points to + the original target. - The server-side rsync often enables this option without the client's + This option can be enabled on the server side without the client's knowledge, such as in an rsync daemon's configuration file or by an option given to the rrsync (restricted rsync) script. When specified on the client side, specify the option normally if it is the client side that - has/needs the munged symlinks, or use `-M--munge-links` to give the option - to the server when it has/needs the munged symlinks. Note that on a local + has or needs the munged symlinks, or use `-M--munge-links` to give the option + to the server when it has or needs the munged symlinks. Note that on a local transfer, the client is the sender, so specifying the option directly unmunges symlinks while specifying it as a remote option munges symlinks. @@ -1269,17 +1462,18 @@ because the daemon configures whether it wants munged symlinks via its "`munge symlinks`" parameter. - The symlink value is munged/unmunged once it is in the transfer, so any + The symlink value is munged or unmunged once it is in the transfer, so any option that transforms symlinks into non-symlinks occurs prior to the munging/unmunging **except** for [`--safe-links`](#opt), which is a choice that the receiver makes, so it bases its decision on the munged/unmunged - value. This does mean that if a receiver has munging enabled, that using - [`--safe-links`](#opt) will cause all symlinks to be ignored (since they - are all absolute). - - The method that rsync uses to munge the symlinks is to prefix each one's - value with the string "/rsyncd-munged/". This prevents the links from - being used as long as the directory does not exist. When this option is + value. This does mean that if a receiver has munging enabled, using + [`--safe-links`](#opt) will cause all symlinks to be ignored (since munging + makes them all absolute). + + The method that rsync uses to munge a symlink is to prefix its + value with the string "/rsyncd-munged/". This prevents the link from + being used, provided that the /rsyncd-munged directory does not exist. + When this option is enabled, rsync will refuse to run if that path is a directory or a symlink to a directory (though it only checks at startup). See also the "munge-symlinks" python script in the support directory of the source code @@ -1302,16 +1496,11 @@ `--copy-dirlinks` applies to all symlinks to directories in the source. If you want to follow only a few specified symlinks, a trick you can use is to - pass them as additional source args with a trailing slash, using + pass them as additional source arguments with a trailing slash, using [`--relative`](#opt) to make the paths match up right. For example: > rsync -r --relative src/./ src/./follow-me/ dest/ - This works because rsync calls **lstat**(2) on the source arg as given, and - the trailing slash makes **lstat**(2) follow the symlink, giving rise to a - directory in the file-list which overrides the symlink found during the - scan of "src/./". - See the [SYMBOLIC LINKS](#) section for multi-option info. 0. `--keep-dirlinks`, `-K` @@ -1322,14 +1511,14 @@ deleted and replaced with a real directory. For example, suppose you transfer a directory "foo" that contains a file - "file", but "foo" is a symlink to directory "bar" on the receiver. Without + "file", but on the receiver, "foo" is a symlink to directory "bar". Without `--keep-dirlinks`, the receiver deletes symlink "foo", recreates it as a directory, and receives the file into the new directory. With `--keep-dirlinks`, the receiver keeps the symlink and "file" ends up in "bar". - One note of caution: if you use `--keep-dirlinks`, you must trust all the - symlinks in the copy or enable the [`--munge-links`](#opt) option on the + One note of caution: if you use `--keep-dirlinks`, you must either trust all the + symlinks in the copy, or enable the [`--munge-links`](#opt) option on the receiving side! If it is possible for an untrusted user to create their own symlink to any real directory, the user could then (on a subsequent copy) replace the symlink with a real directory and affect the content of @@ -1412,7 +1601,7 @@ behavior easier to type, you could define a popt alias for it, such as putting this line in the file `~/.popt` (the following defines the `-Z` option, and includes `--no-g` to use the default group of the destination - dir): + directory): > rsync alias -Z --no-p --no-g --chmod=ugo=rwX @@ -1423,20 +1612,14 @@ (Caveat: make sure that `-a` does not follow `-Z`, or it will re-enable the two `--no-*` options mentioned above.) - The preservation of the destination's setgid bit on newly-created - directories when `--perms` is off was added in rsync 2.6.7. Older rsync - versions erroneously preserved the three special permission bits for - newly-created files when `--perms` was off, while overriding the - destination's setgid bit setting on a newly-created directory. Default ACL - observance was added to the ACL patch for rsync 2.6.7, so older (or - non-ACL-enabled) rsyncs use the umask even if default ACLs are present. - (Keep in mind that it is the version of the receiving rsync that affects - these behaviors.) + Note that if the remote rsync is older than 2.6.7 (March 2006) and it is + the receiver, the behavior when `--perms` is off may differ from that + described above. 0. `--executability`, `-E` - This option causes rsync to preserve the executability (or - non-executability) of regular files when [`--perms`](#opt) is not enabled. + This option causes rsync to preserve the executability or + non-executability of regular files when [`--perms`](#opt) is not enabled. A regular file is considered to be executable if at least one 'x' is turned on in its permissions. When an existing destination file's executability differs from that of the corresponding source file, rsync modifies the @@ -1512,6 +1695,16 @@ > --chmod=D2775,F664 + Symbolic permission-copy modes are also allowed, such as `g=u`, `o=g` or + `g-o`. A permission-copy item may copy from one class only (`u`, `g` or + `o`) and cannot be combined with `rwxXst` permission letters in the same + item. Use comma-separated items when you need both behaviours, such as + `g=o,o=`. + + A permission-copy `=` item also clears the special bit for each destination + class it updates (`u` clears setuid, `g` clears setgid, and `o` clears + sticky), matching GNU **chmod** behaviour. + It is also legal to specify multiple `--chmod` options, as each additional option is just appended to the list of changes to make. @@ -1520,8 +1713,9 @@ 0. `--owner`, `-o` - This option causes rsync to set the owner of the destination file to be the - same as the source file, but only if the receiving rsync is being run as + This option causes rsync to set the owner of each destination filesystem + object (file, directory, etc.) to be the + same as the source object, but only if the receiving rsync is being run as the super-user (see also the [`--super`](#opt) and [`--fake-super`](#opt) options). Without this option, the owner of new and/or transferred files are set to the invoking user on the receiving side. @@ -1532,8 +1726,9 @@ 0. `--group`, `-g` - This option causes rsync to set the group of the destination file to be the - same as the source file. If the receiving program is not running as the + This option causes rsync to set the group of each destination filesystem + object (file, directory, etc.) to be the + same as the source object. If the receiving program is not running as the super-user (or if `--no-super` was specified), only groups that the invoking user on the receiving side is a member of will be preserved. Without this option, the group is set to the default group of the invoking @@ -1570,6 +1765,36 @@ The `-D` option is equivalent to "[`--devices`](#opt) [`--specials`](#opt)". +0. `--drop-D` + + This tells the receiving rsync to refuse to create device files and + special files, whatever the transfer requested. Entries for them are + skipped exactly as if [`-D`](#opt) had not been used, with the usual + "non-regular file" warning. + + This differs from `--no-D` in that it changes only what is created, not + how the file list is encoded. `--no-D` also turns off the rdev fields + that devices and special files carry on the wire, so applying it to one + end of a connection alone leaves the two ends disagreeing about the + encoding and the transfer fails. `--drop-D` can therefore be added on the + receiving side by itself, which is what it exists for -- the `rrsync` + wrapper uses it to stop a restricted directory's clients creating device + and special files in it. Nodes already present there are left alone. + + Because it only withholds creation, it has no effect on a sending rsync. + + This option is not forwarded to the remote side, since its whole purpose + is to be applied to one end of a connection by itself. It therefore + affects the rsync process you give it to and no other: on a local copy, or + on the receiving end of a `--server` invocation such as the one rrsync + builds. To set it on a remote receiver from the command line, send it + explicitly with [`--remote-option`](#opt) (`-M`): + + > rsync -av -M--drop-D src/ host:dest/ + + Passing a plain `--drop-D` to a push affects only the local sender, where + it does nothing. + 0. `--copy-devices` This tells rsync to treat a device on the sending side as a regular file, @@ -1592,21 +1817,21 @@ 0. `--times`, `-t` - This tells rsync to transfer modification times along with the files and - update them on the remote system. Note that if this option is not used, + This tells rsync to set the modification times of the destination files + (including directories, symlinks, devices, etc.) to be the same as the + source files. + Note that if this option is not used, the optimization that excludes files that have not been modified cannot be effective; in other words, a missing `-t` (or [`-a`](#opt)) will cause the next transfer to behave as if it used [`--ignore-times`](#opt) (`-I`), - causing all files to be updated (though rsync's delta-transfer algorithm + causing all files to be updated. (Although rsync's delta-transfer algorithm will make the update fairly efficient if the files haven't actually - changed, you're much better off using `-t`). + changed, you're much better off using `-t`.) - A modern rsync that is using transfer protocol 30 or 31 conveys a modify - time using up to 8-bytes. If rsync is forced to speak an older protocol - (perhaps due to the remote rsync being older than 3.0.0) a modify time is - conveyed using 4-bytes. Prior to 3.2.7, these shorter values could convey - a date range of 13-Dec-1901 to 19-Jan-2038. Beginning with 3.2.7, these - 4-byte values now convey a date range of 1-Jan-1970 to 7-Feb-2106. If you + If the negotiated protocol is older than 30 -- usually because the remote + rsync is older than 3.0.0 (March 2008), but also if [`--protocol`](#opt) + forces it -- the range of modification times that can be conveyed is + restricted. If you have files dated older than 1970, make sure your rsync executables are upgraded so that the full range of dates can be conveyed. @@ -1620,10 +1845,6 @@ transferred files without needing to run rsync an extra time after a file is transferred. - Note that some older rsync versions (prior to 3.2.0) may have been built - with a pre-release `--atimes` patch that does not imply - [`--open-noatime`](#opt) when this option is repeated. - 0. `--open-noatime` This tells rsync to open files with the O_NOATIME flag (on systems that @@ -1704,13 +1925,14 @@ 0. `--sparse`, `-S` Try to handle sparse files efficiently so they take up less space on the - destination. If combined with [`--inplace`](#opt) the file created might - not end up with sparse blocks with some combinations of kernel version - and/or filesystem type. If [`--whole-file`](#opt) is in effect (e.g. for a - local copy) then it will always work because rsync truncates the file prior - to writing out the updated version. - - Note that versions of rsync older than 3.1.3 will reject the combination of + destination. This relies on the destination filesystem supporting sparse + files, that is, files where some parts of the file don't have corresponding + disk blocks allocated for them because they contain all zero bytes. + + If combined with [`--inplace`](#opt) the file created may not end up with + sparse blocks (depending on the filesystem type and kernel version), + unless the [`--whole-file`](#opt) option is also in effect. Note that versions + of rsync older than 3.1.3 (January 2018) will reject the combination of `--sparse` and [`--inplace`](#opt). 0. `--preallocate` @@ -1732,14 +1954,15 @@ 0. `--dry-run`, `-n` - This makes rsync perform a trial run that doesn't make any changes (and - produces mostly the same output as a real run). It is most commonly used + This makes rsync perform a trial run that doesn't make any changes, and + produces mostly the same output as a real run. It is most commonly used in combination with the [`--verbose`](#opt) (`-v`) and/or [`--itemize-changes`](#opt) (`-i`) options to see what an rsync command is going to do before one actually runs it. The output of [`--itemize-changes`](#opt) is supposed to be exactly the - same on a dry run and a subsequent real run (barring intentional trickery + same on a dry run and a subsequent real run (barring external changes to + the source or destination and system call failures); if it isn't, that's a bug. Other output should be mostly unchanged, but may differ in some areas. Notably, a dry run does not send the actual data for file transfers, so [`--progress`](#opt) has no @@ -1749,8 +1972,8 @@ 0. `--whole-file`, `-W` - This option disables rsync's delta-transfer algorithm, which causes all - transferred files to be sent whole. The transfer may be faster if this + This option disables rsync's delta-transfer algorithm, causing the whole of + all transferred files to be sent. The transfer may be faster if this option is used when the bandwidth between the source and destination machines is higher than the bandwidth to disk (especially when the "disk" is actually a networked filesystem). This is the default when both the @@ -1769,10 +1992,11 @@ 0. `--checksum-choice=STR`, `--cc=STR` This option overrides the checksum algorithms. If one algorithm name is - specified, it is used for both the transfer checksums and (assuming - [`--checksum`](#opt) is specified) the pre-transfer checksums. If two + specified, it is used for both the transfer checksums and the pre-transfer + checksums (note that the pre-transfer checksum is only performed if + [`--checksum`](#opt) is specified). If two comma-separated names are supplied, the first name affects the transfer - checksums, and the second name affects the pre-transfer checksums (`-c`). + checksums, and the second name affects the pre-transfer checksums. The checksum options that you may be able to use are: @@ -1796,9 +2020,11 @@ The "auto" option is the default, where rsync bases its algorithm choice on a negotiation between the client and the server as follows: - When both sides of the transfer are at least 3.2.0, rsync chooses the first - algorithm in the client's list of choices that is also in the server's list - of choices. If no common checksum choice is found, rsync exits with + When both sides of the transfer are at least 3.2.0 (released June 2020), + each side chooses its own + most-preferred algorithm that also appears in the peer's list. Both sides + order their lists strongest-first, so they converge on the strongest mutual + choice. If no common checksum choice is found, rsync exits with an error. If the remote rsync is too old to support checksum negotiation, a value is chosen based on the protocol version (which chooses between MD5 and various flavors of MD4 based on protocol age). @@ -1886,30 +2112,30 @@ "foo" when it is done, and then use the option [`--exclude='*.new'`](#opt) for the rsync transfer). - Starting with 3.1.0, rsync will skip the sender-side removal (and output an - error) if the file's size or modify time has not stayed unchanged. + Rsync will skip the sender-side removal, and output an + error, if the file's size or modify time has not stayed unchanged. - Starting with 3.2.6, a local rsync copy will ensure that the sender does + If the copy is local, rsync will ensure that the sender does not remove a file the receiver just verified, such as when the user accidentally makes the source and destination directory the same path. 0. `--delete` - This tells rsync to delete extraneous files from the receiving side (ones - that aren't on the sending side), but only for the directories that are + This tells rsync to delete extraneous files from the receiving side (those + that don't exist on the sending side), but only for the directories that are being synchronized. You must have asked rsync to send the whole directory - (e.g. "`dir`" or "`dir/`") without using a wildcard for the directory's - contents (e.g. "`dir/*`") since the wildcard is expanded by the shell and + (e.g. "`dir`" or "`dir/`"), rather than asking for all the files in a + directory via a wildcard (e.g. "`dir/*`"), since the wildcard is + expanded by the shell and rsync thus gets a request to transfer individual files, not the files' parent directory. Files that are excluded from the transfer are also excluded from being deleted unless you use the [`--delete-excluded`](#opt) option or mark the rules as only matching on the sending side (see the include/exclude modifiers in the [FILTER RULES](#) section). - Prior to rsync 2.6.7, this option would have no effect unless - [`--recursive`](#opt) was enabled. Beginning with 2.6.7, deletions will - also occur when [`--dirs`](#opt) (`-d`) is enabled, but only for - directories whose contents are being copied. + This option has no effect unless either [`--recursive`](#opt) or + [`--dirs`](#opt) (`-d`) is enabled. In the latter case, deletions only + occur in directories whose contents are being copied. This option can be dangerous if used incorrectly! It is a very good idea to first try a run using the [`--dry-run`](#opt) (`-n`) option to see what @@ -1924,7 +2150,8 @@ The `--delete` option may be combined with one of the --delete-WHEN options without conflict, as well as [`--delete-excluded`](#opt). However, if none of the `--delete-WHEN` options are specified, rsync will choose the - [`--delete-during`](#opt) algorithm when talking to rsync 3.0.0 or newer, + [`--delete-during`](#opt) algorithm when talking to rsync 3.0.0 (March + 2008) or newer, or the [`--delete-before`](#opt) algorithm when talking to an older rsync. See also [`--delete-delay`](#opt) and [`--delete-after`](#opt). @@ -1948,8 +2175,8 @@ as the transfer happens. The per-directory delete scan is done right before each directory is checked for updates, so it behaves like a more efficient [`--delete-before`](#opt), including doing the deletions prior to - any per-directory filter files being updated. This option was first added - in rsync version 2.6.4. See [`--delete`](#opt) (which is implied) for more + any per-directory filter files being updated. + See [`--delete`](#opt) (which is implied) for more details on file-deletion. 0. `--delete-delay` @@ -1958,9 +2185,9 @@ the transfer (like [`--delete-during`](#opt)), and then removed after the transfer completes. This is useful when combined with [`--delay-updates`](#opt) and/or [`--fuzzy`](#opt), and is more efficient - than using [`--delete-after`](#opt) (but can behave differently, since + than using [`--delete-after`](#opt), but can behave differently, since [`--delete-after`](#opt) computes the deletions in a separate pass after - all updates are done). If the number of removed files overflows an + all updates are done. If the number of removed files overflows an internal buffer, a temporary file will be created on the receiving side to hold the names (it is removed while open, so you shouldn't see it during the transfer). If the creation of the temporary file fails, rsync will try @@ -2016,7 +2243,7 @@ 0. `--delete-missing-args` This option takes the behavior of the (implied) - [`--ignore-missing-args`](#opt) option a step farther: each missing arg + [`--ignore-missing-args`](#opt) option a step farther: each missing argument will become a deletion request of the corresponding destination file on the receiving side (should it exist). If the destination file is a non-empty directory, it will only be successfully deleted if [`--force`](#opt) or @@ -2037,10 +2264,6 @@ replaced by a non-directory. This is only relevant if deletions are not active (see [`--delete`](#opt) for details). - Note for older rsync versions: `--force` used to still be required when - using [`--delete-after`](#opt), and it used to be non-functional unless the - [`--recursive`](#opt) option was also enabled. - 0. `--max-delete=NUM` This tells rsync not to delete more than NUM files or directories. If that @@ -2049,19 +2272,19 @@ skipped deletions) and exits with an error code of 25 (unless some more important error condition also occurred). - Beginning with version 3.0.0, you may specify `--max-delete=0` to be warned + You may specify `--max-delete=0` to be warned about any extraneous files in the destination without removing any of them. - Older clients interpreted this as "unlimited", so if you don't know what - version the client is, you can use the less obvious `--max-delete=-1` as a - backward-compatible way to specify that no deletions be allowed (though - really old versions didn't warn when the limit was exceeded). + CAUTION: a client rsync older than 3.0.0 (March 2008) treats + `--max-delete=0` as unlimited, so use `--max-delete=-1` if the command + might be run by such an old rsync. (A 3.0.0 or newer client protects an + older remote by forwarding the option as `--max-delete=-1`.) 0. `--max-size=SIZE` This tells rsync to avoid transferring any file that is larger than the specified SIZE. A numeric value can be suffixed with a string to indicate - the numeric units or left unqualified to specify bytes. Feel free to use a - fractional value along with the units, such as `--max-size=1.5m`. + the numeric units or left unqualified to specify bytes, and the numeric + value can have a fractional part, such as `--max-size=1.5m`. This option is a [TRANSFER RULE](#TRANSFER_RULES), so don't expect any exclude side effects. @@ -2080,16 +2303,12 @@ Examples: `--max-size=1.5mb-1` is 1499999 bytes, and `--max-size=2g+1` is 2147483649 bytes. - Note that rsync versions prior to 3.1.0 did not allow `--max-size=0`. - 0. `--min-size=SIZE` This tells rsync to avoid transferring any file that is smaller than the specified SIZE, which can help in not transferring small, junk files. See the [`--max-size`](#opt) option for a description of SIZE and other info. - Note that rsync versions prior to 3.1.0 did not allow `--min-size=0`. - 0. `--max-alloc=SIZE` By default rsync limits an individual malloc/realloc to about 1GB in size. @@ -2097,7 +2316,7 @@ causing rsync to request massive amounts of memory. However, if you have many millions of files in a transfer, a large amount of server memory, and you don't want to split up your transfer into multiple parts, you can - increase the per-allocation limit to something larger and rsync will + increase the per-allocation limit to something larger, allowing rsync to consume more memory. Keep in mind that this is not a limit on the total size of allocated @@ -2106,8 +2325,12 @@ See the [`--max-size`](#opt) option for a description of how SIZE can be specified. The default suffix if none is given is bytes. - Beginning in 3.2.7, a value of 0 is an easy way to specify SIZE_MAX (the - largest limit possible). + Beginning in 3.2.7, a value of 0 was an easy way to specify SIZE_MAX (the + largest limit possible). However, beginning with 3.5.0, a value of 0 is + rejected as invalid for security reasons (a 0-byte cap could be used to + disable the allocation limit, which could lead to a denial-of-service via + memory exhaustion). Use an explicit very large value if you want a very + high limit. You can set a default value using the environment variable [`RSYNC_MAX_ALLOC`](#) using the same SIZE values as supported by this @@ -2122,15 +2345,14 @@ fixed value. It is normally selected based on the size of each file being updated. See the technical report for details. - Beginning in 3.2.3 the SIZE can be specified with a suffix as detailed in - the [`--max-size`](#opt) option. Older versions only accepted a byte count. + The SIZE can be specified with a suffix as detailed in + the [`--max-size`](#opt) option. 0. `--rsh=COMMAND`, `-e` This option allows you to choose an alternative remote shell program to use for communication between the local and remote copies of rsync. Typically, - rsync is configured to use ssh by default, but you may prefer to use rsh on - a local network. + rsync is configured to use ssh by default. If this option is used with `[user@]host::module/path`, then the remote shell _COMMAND_ will be used to run an rsync daemon on the remote host, and @@ -2139,18 +2361,19 @@ remote host. See the [USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION](#) section above. - Beginning with rsync 3.2.0, the [`RSYNC_PORT`](#) environment variable will + The [`RSYNC_PORT`](#) environment variable will be set when a daemon connection is being made via a remote-shell connection. It is set to 0 if the default daemon port is being assumed, or it is set to the value of the rsync port that was specified via either the - [`--port`](#opt) option or a non-empty port value in an `rsync://` URL. - This allows the script to discern if a non-default port is being requested, - allowing for things such as an SSL or stunnel helper script to connect to a - default or alternate port. + [`--port`](#opt) option or a non-empty port value in an `rsync://` URL (for + example `rsync://host.example.com:984`). This is useful if the program + being run is not actually rsync but rather something such as an SSL or + stunnel helper script; the script can use `RSYNC_PORT` to determine whether + it should connect to a default or alternate port. Command-line arguments are permitted in COMMAND provided that COMMAND is presented to rsync as a single argument. You must use spaces (not tabs or - other whitespace) to separate the command and args from each other, and you + other whitespace) to separate the command and arguments from each other, and you can use single- and/or double-quotes to preserve spaces in an argument (but not backslashes). Note that doubling a single-quote inside a single-quoted string gives you a single-quote; likewise for double-quotes (though you @@ -2204,14 +2427,14 @@ Note that you should use a separate `-M` option for each remote option you want to pass. On older rsync versions, the presence of any spaces in the - remote-option arg could cause it to be split into separate remote args, but + remote-option argument could cause it to be split into separate remote arguments, but this requires the use of [`--old-args`](#opt) in a modern rsync. When performing a local transfer, the "local" side is the sender and the "remote" side is the receiver. Note some versions of the popt option-parsing library have a bug in them - that prevents you from using an adjacent arg with an equal in it next to a + that prevents you from using an adjacent argument with an equal in it next to a short option letter (e.g. `-M--log-file=/tmp/foo`). If this bug affects your version of popt, you can use the version of popt that is included with rsync. @@ -2295,8 +2518,8 @@ You may use as many `--filter` options on the command line as you like to build up the list of files to exclude. If the filter contains whitespace, be sure to quote it so that the shell gives the rule to rsync as a single - argument. The text below also mentions that you can use an underscore to - replace the space that separates a rule from its arg. + argument. You can use an underscore instead of a space to separate a rule + from its argument. See the [FILTER RULES](#) section for detailed information on this option. @@ -2369,8 +2592,8 @@ 0. `--files-from=FILE` - Using this option allows you to specify the exact list of files to transfer - (as read from the specified FILE or '`-`' for standard input). It also + Using this option tells rsync to read the exact list of files to transfer + from FILE (or standard input if FILE is '`-`'). It also tweaks the default behavior of rsync to make transferring just the specified files and directories easier: @@ -2388,17 +2611,21 @@ `--files-from`, as does `--no-R` and all other options). The filenames that are read from the FILE are all relative to the source - dir -- any leading slashes are removed and no ".." references are allowed - to go higher than the source dir. For example, take this command: + directory: any leading slash is removed, and ".." components are resolved away so + an entry cannot rise above the source directory -- e.g. "../foo" is taken as "foo" + within the source directory. An entry that still contains an active ".." after + that resolution (one that cannot be collapsed) is rejected with an error. + Blank entries are ignored, as are whole-entry comments that start with '`;`' + or '`#`'. For example, take this command: > rsync -a --files-from=/tmp/foo /usr remote:/backup If /tmp/foo contains the string "bin" (or even "/bin"), the /usr/bin directory will be created as /backup/bin on the remote host. If it contains "bin/" (note the trailing slash), the immediate contents of the - directory would also be sent (without needing to be explicitly mentioned in - the file -- this began in version 2.6.4). In both cases, if the - [`-r`](#opt) option was enabled, that dir's entire hierarchy would also be + directory would also be sent, without needing to be explicitly mentioned in + the file. In both cases, if the + [`-r`](#opt) option was enabled, that directory's entire hierarchy would also be transferred (keep in mind that [`-r`](#opt) needs to be specified explicitly with `--files-from`, since it is not implied by [`-a`](#opt). Also note that the effect of the (enabled by default) [`-r`](#opt) option @@ -2430,7 +2657,7 @@ 0. `--from0`, `-0` - This tells rsync that the rules/filenames it reads from a file are + This tells rsync that the rules or filenames it reads from a file are terminated by a null ('\\0') character, not a NL, CR, or CR+LF. This affects [`--exclude-from`](#opt), [`--include-from`](#opt), [`--files-from`](#opt), and any merged files specified in a @@ -2439,18 +2666,18 @@ 0. `--old-args` - This option tells rsync to stop trying to protect the arg values on the + This option tells rsync not to protect the argument values sent to the remote side from unintended word-splitting or other misinterpretation. - It also allows the client to treat an empty arg as a "." instead of + It also allows the client to treat an empty argument as a "." instead of generating an error. The default in a modern rsync is for "shell-active" characters (including - spaces) to be backslash-escaped in the args that are sent to the remote + spaces) to be backslash-escaped in the arguments that are sent to the remote shell. The wildcard characters `*`, `?`, `[`, & `]` are not escaped in - filename args (allowing them to expand into multiple filenames) while being - protected in option args, such as [`--usermap`](#opt). + filename arguments (allowing them to expand into multiple filenames) while being + protected in option arguments, such as [`--usermap`](#opt). - If you have a script that wants to use old-style arg splitting in its + If you have a script that wants to use old-style argument splitting in its filenames, specify this option once. If the remote shell has a problem with any backslash escapes at all, specify this option twice. @@ -2461,35 +2688,35 @@ behavior. The environment is always overridden by manually specified positive or negative options (the negative is `--no-old-args`). - Note that this option also disables the extra safety check added in 3.2.5 + Note that this option also disables the extra safety check that ensures that a remote sender isn't including extra top-level items in the file-list that you didn't request. This side-effect is necessary because we can't know for sure what names to expect when the remote shell - is interpreting the args. + is interpreting the arguments. This option conflicts with the [`--secluded-args`](#opt) option. 0. `--secluded-args`, `-s` This option sends all filenames and most options to the remote rsync via - the protocol (not the remote shell command line) which avoids letting the - remote shell modify them. Wildcards are expanded on the remote host by - rsync instead of a shell. - - This is similar to the default backslash-escaping of args that was added - in 3.2.4 (see [`--old-args`](#opt)) in that it prevents things like space - splitting and unwanted special-character side-effects. However, it has the - drawbacks of being incompatible with older rsync versions (prior to 3.0.0) - and of being refused by restricted shells that want to be able to inspect - all the option values for safety. - + the protocol (rather than via the remote shell command line), which avoids + the possibility of the remote shell modifying them. + Wildcards are expanded on the remote host by rsync instead of a shell. + + Without this option, rsync does backslash-escaping of arguments to prevent + things like space splitting and unwanted special-character side-effects, + which is normally sufficient to avoid unwanted modifications by the shell. This option is useful for those times that you need the argument's character set to be converted for the remote host, if the remote shell is - incompatible with the default backslash-escpaing method, or there is some + incompatible with the default backslash-escaping method, or there is some other reason that you want the majority of the options and arguments to bypass the command-line of the remote shell. - If you combine this option with [`--iconv`](#opt), the args related to the + This option is incompatible with remote rsync versions prior to 3.0.0 + (March 2008). It also has the drawback of being refused by restricted + shells that want to be able to inspect all the option values for safety. + + If you combine this option with [`--iconv`](#opt), the arguments related to the remote side will be translated from the local to the remote character-set. The translation happens before wild-cards are expanded. See also the [`--files-from`](#opt) option. @@ -2500,11 +2727,12 @@ is overridden by a manually specified positive or negative version of this option (note that `--no-s` and `--no-secluded-args` are the negative versions). This environment variable is also superseded by a non-zero - [`RSYNC_OLD_ARGS`](#) export. + [`RSYNC_OLD_ARGS`](#) environment variable. This option conflicts with the [`--old-args`](#opt) option. - This option used to be called `--protect-args` (before 3.2.6) and that + This option used to be called `--protect-args` (before 3.2.6, September + 2022) and that older name can still be used (though specifying it as `-s` is always the easiest and most compatible choice). @@ -2516,10 +2744,10 @@ file list (something that could possibly be done via a modified rsync, a modified shell, or some other similar manipulation). - Normally, the rsync client (as of version 3.2.5) runs two extra validation + Normally, the rsync client runs two extra validation checks when pulling files from a remote rsync: - - It verifies that additional arg items didn't get added at the top of the + - It verifies that additional argument items didn't get added at the top of the transfer. - It verifies that none of the items in the file list are names that should have been excluded (if filter rules were specified). @@ -2530,19 +2758,19 @@ - Using a per-directory filter file reads filter rules that only the server knows about, so the filter checking is disabled. - Using the [`--old-args`](#opt) option allows the sender to manipulate the - requested args, so the arg checking is disabled. + requested arguments, so the argument checking is disabled. - Reading the files-from list from the server side means that the client - doesn't know the arg list, so the arg checking is disabled. + doesn't know the argument list, so the argument checking is disabled. - Using [`--read-batch`](#opt) disables both checks since the batch file's contents will have been verified when it was created. This option may help an under-powered client server if the extra pattern - matching is slowing things down on a huge transfer. It can also be used to - work around a currently-unknown bug in the verification logic for a transfer - from a trusted sender. + matching is slowing things down on a huge transfer. It could also be used + for a transfer from a trusted sender as a workaround if there appeared to + be a bug in the verification logic. When using this option it is a good idea to specify a dedicated destination - directory, as discussed in the [MULTI-HOST SECURITY](#) section. + directory, as discussed in the [SECURITY](#) section. 0. `--copy-as=USER[:GROUP]` @@ -2551,8 +2779,9 @@ is running rsync has the ability to change users. If the group is not specified then the user's default groups are used. - This option can help to reduce the risk of an rsync being run as root into - or out of a directory that might have live changes happening to it and you + This option can help to reduce the risk in the case where rsync is being + run as root, copying into or out of a directory that might have live + changes happening to it, and you want to make sure that root-level read or write actions of system files are not possible. While you could alternatively run all of rsync as the specified user, sometimes you need the root-level host-access credentials @@ -2567,7 +2796,7 @@ "localhost:" or "lh:" host-spec to be specified without needing to setup any remote shells, allowing you to specify remote options that affect the side of the transfer that is using the host-spec (and using hostname "lh" - avoids the overriding of the remote directory to the user's home dir). + avoids the overriding of the remote directory to the user's home directory). For example, the following rsync writes the local files as user "joe": @@ -2578,8 +2807,8 @@ a timed exploit of the path to induce a change to a file that the joe user has no permissions to change. - The following command does a local copy into the "dest/" dir as user "joe" - (assuming you've installed support/lsh into a dir on your $PATH): + The following command does a local copy into the "dest/" directory as user "joe" + (assuming you've installed support/lsh into a directory on your $PATH): > sudo rsync -aive lsh -M--copy-as=joe src/ lh:dest/ @@ -2588,9 +2817,9 @@ This option instructs rsync to use DIR as a scratch directory when creating temporary copies of the files transferred on the receiving side. The default behavior is to create each temporary file in the same directory as - the associated destination file. Beginning with rsync 3.1.1, the temp-file - names inside the specified DIR will not be prefixed with an extra dot - (though they will still have a random suffix added). + the associated destination file. The temp-file + names inside the specified DIR will not be prefixed with an extra dot, + though they will still have a random suffix added. This option is most often used when the receiving disk partition does not have enough free space to hold a copy of the largest file in the transfer. @@ -2599,9 +2828,7 @@ over the top of the associated destination file, but instead must copy it into place. Rsync does this by copying the file over the top of the destination file, which means that the destination file will contain - truncated data during this copy. If this were not done this way (even if - the destination file were first removed, the data locally copied to a - temporary file in the destination directory, and then renamed into place) + truncated data during this copy. If this were not done this way, it would be possible for the old file to continue taking up disk space (if someone had it open), and thus there might not be enough room to fit the new version on the disk at the same time. @@ -2614,7 +2841,7 @@ destination partition, another way to tell rsync that you aren't overly concerned about disk space is to use the [`--partial-dir`](#opt) option with a relative path; because this tells rsync that it is OK to stash off a - copy of a single file in a subdir in the destination hierarchy, rsync will + copy of a single file in a subdirectory in the destination hierarchy, rsync will use the partial-dir as a staging area to bring over the copied file, and then rename it into place from there. (Specifying a [`--partial-dir`](#opt) with an absolute path does not have this side-effect.) @@ -2622,7 +2849,7 @@ 0. `--fuzzy`, `-y` This option tells rsync that it should look for a basis file for any - destination file that is missing. The current algorithm looks in the same + destination file that is missing. With this option, rsync looks in the same directory as the destination file for either a file that has an identical size and modified-time, or a similarly-named file. If found, rsync uses the fuzzy basis file to try to speed up the transfer. @@ -2646,7 +2873,7 @@ option is typically used to copy into an empty (or newly created) directory. - Beginning in version 2.6.4, multiple `--compare-dest` directories may be + Multiple `--compare-dest` directories may be provided, which will cause rsync to search the list in the order specified for an exact match. If a match is found that differs only in attributes, a local copy is made and the attributes updated. If a match is not found, a @@ -2656,10 +2883,10 @@ If _DIR_ is a relative path, it is relative to the destination directory. See also [`--copy-dest`](#opt) and [`--link-dest`](#opt). - NOTE: beginning with version 3.1.0, rsync will remove a file from a + NOTE: rsync will remove a file from a non-empty destination hierarchy if an exact match is found in one of the - compare-dest hierarchies (making the end result more closely match a fresh - copy). + compare-dest hierarchies, making the end result more closely match a fresh + copy. 0. `--copy-dest=DIR` @@ -2692,17 +2919,34 @@ with generic ownership (such as OS X's "Ignore ownership on this volume" option). - Beginning in version 2.6.4, multiple `--link-dest` directories may be + Multiple `--link-dest` directories may be provided, which will cause rsync to search the list in the order specified for an exact match (there is a limit of 20 such directories). If a match is found that differs only in attributes, a local copy is made and the attributes updated. If a match is not found, a basis file from one of the _DIRs_ will be selected to try to speed up the transfer. + Not every filesystem can hard-link a symlink, a device node, a FIFO or a + socket, and the destination need not agree with the one rsync was built on + -- macOS builds on APFS, which can, and may write to HFS+, which cannot. + Where the destination refuses to link such an entry, it is copied instead + and the transfer carries on, so only that entry loses the space saving. + This applies to any refusal, because the error alone does not identify one: + link(2) reports `EPERM` both for a filesystem without hard links and for an + ordinary permission refusal. Regular files have always behaved this way. + + One case is not covered. With [`--hard-links`](#opt) (`-H`), a group of + such entries hard-linked to *each other* in the source needs a second link, + from the first member to the rest, inside the destination itself. Where + that link is refused too -- a destination that cannot hard-link the type at + all -- the members after the first are not created and the transfer fails. + A `--link-dest` on another filesystem is fine: only the link to _DIR_ is + impossible there, and the ones within the destination still succeed. + This option works best when copying into an empty destination hierarchy, as existing files may get their attributes tweaked, and that can affect alternate destination files via hard-links. Also, itemizing of changes can - get a bit muddled. Note that prior to version 3.1.0, an + get a bit muddled. Note that prior to version 3.1.0 (September 2013), an alternate-directory exact match would never be found (nor linked into the destination) when a destination file already exists. @@ -2714,11 +2958,11 @@ If _DIR_ is a relative path, it is relative to the destination directory. See also [`--compare-dest`](#opt) and [`--copy-dest`](#opt). - Note that rsync versions prior to 2.6.1 had a bug that could prevent + Note that rsync versions prior to 2.6.1 (April 2004) had a bug that could prevent `--link-dest` from working properly for a non-super-user when [`--owner`](#opt) (`-o`) was specified (or implied). You can work-around - this bug by avoiding the `-o` option (or using `--no-o`) when sending to an - old rsync. + this bug by avoiding the `-o` option (or using `--no-o`) when sending to a + really old remote rsync. 0. `--compress`, `-z` @@ -2733,9 +2977,11 @@ Run `rsync --version` to see the default compress list compiled into your version. - When both sides of the transfer are at least 3.2.0, rsync chooses the first - algorithm in the client's list of choices that is also in the server's list - of choices. If no common compress choice is found, rsync exits with + When both sides of the transfer are at least 3.2.0 (June 2020), + each side chooses its own + most-preferred algorithm that also appears in the peer's list. Both sides + order their lists strongest-first, so they converge on the strongest mutual + choice. If no common compress choice is found, rsync exits with an error. If the remote rsync is too old to support checksum negotiation, its list is assumed to be "zlib". @@ -2785,7 +3031,7 @@ 0. `--compress-level=NUM`, `--zl=NUM` Explicitly set the compression level to use (see [`--compress`](#opt), - `-z`) instead of letting it default. The [`--compress`](#opt) option is + `-z`) instead of using the default. The [`--compress`](#opt) option is implied as long as the level chosen is not a "don't compress" level for the compression algorithm that is in effect (e.g. zlib compression treats level 0 as "off"). @@ -2817,13 +3063,29 @@ report something like "`Client compress: zstd (level 3)`" (along with the checksum choice in effect). +0. `--compress-threads=NUM`, `--zt=NUM` + + Set the number of threads to spawn when compressing data. Setting this + option to 1 or more will instruct the compression library to spawn 1 or + more threads for compression. Ideally, increasing the number of threads + will increase transfer speed if the transfer is CPU bound on the sender. + + This option does not affect decompression. + + Compression algorithms that allow threading: + + - `zstd` (only when libzstd is compiled with threading support) + + This option is ignored if one of the above alogithms is not selected as the + `--compression-choice` or if compression not enabled. + 0. `--skip-compress=LIST` **NOTE:** no compression method currently supports per-file compression changes, so this option has no effect. Override the list of file suffixes that will be compressed as little as - possible. Rsync sets the compression level on a per-file basis based on + possible. Rsync can set the compression level on a per-file basis based on the file's suffix. If the compression algorithm has an "off" level, then no compression occurs for those files. Other algorithms that support changing the streaming level on-the-fly will have the level minimized to @@ -2999,6 +3261,10 @@ > --usermap=:nobody --groupmap=*:nobody + An empty **FROM** value matches only sender-side IDs that have no name. It + is not a wildcard for named users or groups; use "`*`" when you want to map + every sender-side name. + When the [`--numeric-ids`](#opt) option is used, the sender does not send any names, so all the IDs are treated as having an empty name. This means that you will need to specify numeric **FROM** values if you want to map these @@ -3009,8 +3275,8 @@ options). For the `--groupmap` option to work, the receiver will need to have permissions to set that group. - Starting with rsync 3.2.4, the `--usermap` option implies the - [`--owner`](#opt) (`-o`) option while the `--groupmap` option implies the + The `--usermap` option implies the + [`--owner`](#opt) (`-o`) option, and the `--groupmap` option implies the [`--group`](#opt) (`-g`) option (since rsync needs to have those options enabled for the mapping options to work). @@ -3021,7 +3287,7 @@ This option forces all files to be owned by USER with group GROUP. This is a simpler interface than using [`--usermap`](#opt) & [`--groupmap`](#opt) - directly, but it is implemented using those options internally so they + directly, but it is implemented using those options internally, thus they cannot be mixed. If either the USER or GROUP is empty, no mapping for the omitted user/group will occur. If GROUP is empty, the trailing colon may be omitted, but if USER is empty, a leading colon must be supplied. @@ -3095,8 +3361,8 @@ file, including attribute changes. This is exactly the same as specifying [`--out-format='%i %n%L'`](#opt). If you repeat the option, unchanged files will also be output, but only if the receiving rsync is at least - version 2.6.7 (you can use `-vv` with older versions of rsync, but that - also turns on the output of other verbose messages). + version 2.6.7 (March 2006). You can use `-vv` with older versions of rsync, + but that also turns on the output of other verbose messages. The "%i" escape has a cryptic output that is 11 letters long. The general format is like the string `YXcstpoguax`, where **Y** is replaced by the type @@ -3135,7 +3401,7 @@ - A `c` means either that a regular file has a different checksum (requires [`--checksum`](#opt)) or that a symlink, device, or special file has a changed value. Note that if you are sending files to an rsync prior to - 3.0.1, this change flag will be present only for checksum-differing + 3.0.1 (April 2008), this change flag will be present only for checksum-differing regular files. - A `s` means the size of a regular file is different and will be updated by the file transfer. @@ -3144,9 +3410,7 @@ `T` means that the modification time will be set to the transfer time, which happens when a file/symlink/device is updated without [`--times`](#opt) and when a symlink is changed and the receiver can't - set its time. (Note: when using an rsync 3.0.0 client, you might see the - `s` flag combined with `t` instead of the proper `T` flag for this - time-setting failure.) + set its time. - A `p` means the permissions are different and are being updated to the sender's value (requires [`--perms`](#opt)). - An `o` means the owner is different and is being updated to the sender's @@ -3159,8 +3423,8 @@ - `n` means the create time (newness) is different and is being updated to the sender's value (requires [`--crtimes`](#opt)) - `b` means that both the access and create times are being updated - - The `a` means that the ACL information is being changed. - - The `x` means that the extended attribute information is being changed. + - An `a` means that the ACL information is being changed. + - An `x` means that the extended attribute information is being changed. One other output is possible: when deleting files, the "%i" will output the string "`*deleting`" for each item that is being removed (assuming that you @@ -3179,7 +3443,7 @@ format`](rsyncd.conf.5#log_format) setting in the rsyncd.conf manpage. Specifying the `--out-format` option implies the [`--info=name`](#opt) - option, which will mention each file, dir, etc. that gets updated in a + option, which will mention each file, directory, etc. that gets updated in a significant way (a transferred file, a recreated symlink/device, or a touched directory). In addition, if the itemize-changes escape (%i) is included in the string (e.g. if the [`--itemize-changes`](#opt) option was @@ -3196,7 +3460,7 @@ 0. `--log-file=FILE` - This option causes rsync to log what it is doing to a file. This is + This option causes rsync to write the log of what it is doing to a file. This is similar to the logging that a daemon does, but can be requested for the client side and/or the server side of a non-daemon transfer. If specified as a client option, transfer logging will be enabled with a default format @@ -3243,7 +3507,7 @@ followed by a list of counts by filetype (if the total is non-zero). For example: "(reg: 5, dir: 3, link: 2, dev: 1, special: 1)" lists the totals for regular files, directories, symlinks, devices, and special files. If - any of value is 0, it is completely omitted from the list. + any value is 0, it is completely omitted from the list. - `Number of created files` is the count of how many "files" (generic sense) were created (as opposed to updated). The total count will be followed by a list of counts by filetype (if the total is non-zero). @@ -3251,17 +3515,17 @@ sense) were deleted. The total count will be followed by a list of counts by filetype (if the total is non-zero). Note that this line is only output if deletions are in effect, and only - if protocol 31 is being used (the default for rsync 3.1.x). + if the negotiated protocol is at least 31 (the default when both sides + are 3.1.0, September 2013, or newer). - `Number of regular files transferred` is the count of normal files that were updated via rsync's delta-transfer algorithm, which does not include - dirs, symlinks, etc. Note that rsync 3.1.0 added the word "regular" into - this heading. + directories, symlinks, etc. - `Total file size` is the total sum of all file sizes in the transfer. This does not count any size for directories or special files, but does include the size of symlinks. - `Total transferred file size` is the total sum of all files sizes for just the transferred files. - - `Literal data` is how much unmatched file-update data we had to send to + - `Literal data` is how much unmatched file-update data the sender had to send to the receiver for it to recreate the updated files. - `Matched data` is how much data the receiver got locally when recreating the updated files. @@ -3284,11 +3548,12 @@ 0. `--8-bit-output`, `-8` This tells rsync to leave all high-bit characters unescaped in the output + (to standard output or standard error) instead of trying to test them to see if they're valid in the current locale and escaping the invalid ones. All control characters (but never tabs) are always escaped, regardless of this option's setting. - The escape idiom that started in 2.6.7 is to output a literal backslash + The escape idiom that started in 2.6.7 (March 2006) is to output a literal backslash (`\`) and a hash (`#`), followed by exactly 3 octal digits. For example, a newline would output as "`\#012`". A literal backslash that is in a filename is not escaped unless it is followed by a hash and 3 digits (0-9). @@ -3313,7 +3578,7 @@ file would output as 1.23M in level-2 (assuming that a period is your local decimal point). - Backward compatibility note: versions of rsync prior to 3.1.0 do not + Backward compatibility note: versions of rsync prior to 3.1.0 (September 2013) do not support human-readable level 1, and they default to level 0. Thus, specifying one or two `-h` options will behave in a comparable manner in old and new versions as long as you didn't specify a `--no-h` option prior @@ -3334,7 +3599,7 @@ also implying that it be enabled. This enhanced partial-file method puts any partially transferred files into the specified _DIR_ instead of writing the partial file out to the destination file. On the next transfer, rsync - will use a file found in this dir as data to speed up the resumption of the + will use a file found in this directory as data to speed up the resumption of the transfer and then delete it after it has served its purpose. Note that if [`--whole-file`](#opt) is specified (or implied), any @@ -3342,7 +3607,7 @@ simply be removed (since rsync is sending files without using rsync's delta-transfer algorithm). - Rsync will create the _DIR_ if it is missing, but just the last dir -- not + Rsync will create the _DIR_ if it is missing, but just the last directory -- not the whole path. This makes it easy to use a relative path (such as "`--partial-dir=.rsync-partial`") to have rsync create the partial-directory in the destination file's directory when it is needed, @@ -3365,14 +3630,14 @@ 1. the auto-added rule may be ineffective at the end of your other rules, or 2. you may wish to override rsync's exclude choice. - For instance, if you want to make rsync clean-up any left-over partial-dirs + For instance, if you want to make rsync clean-up any left-over partial-directories that may be lying around, you should specify [`--delete-after`](#opt) and add a "risk" filter rule, e.g. `-f 'R .rsync-partial/'`. Avoid using [`--delete-before`](#opt) or [`--delete-during`](#opt) unless you don't need rsync to use any of the left-over partial-dir data during the current run. - IMPORTANT: the `--partial-dir` should not be writable by other users or it + IMPORTANT: the partial-dir directory should not be writable by other users or it is a security risk! E.g. AVOID "/tmp"! You can also set the partial-dir value the [`RSYNC_PARTIAL_DIR`](#) @@ -3393,7 +3658,7 @@ partial file is now updated in-place instead of creating yet another tmp-file copy (so it maxes out at dest + tmp instead of dest + partial + tmp). This requires both ends of the transfer to be at least version - 3.2.0. + 3.2.0 (June 2020). For the purposes of the daemon-config's "`refuse options`" setting, `--partial-dir` does _not_ imply [`--partial`](#opt). This is so that a @@ -3406,12 +3671,12 @@ This option puts the temporary file from each updated file into a holding directory until the end of the transfer, at which time all the files are renamed into place in rapid succession. This attempts to make the updating - of the files a little more atomic. By default the files are placed into a + of the files a little closer to atomic. By default the files are placed into a directory named `.~tmp~` in each file's destination directory, but if you've specified the [`--partial-dir`](#opt) option, that directory will be used instead. See the comments in the [`--partial-dir`](#opt) section for a discussion of how this `.~tmp~` dir will be excluded from the transfer, - and what you can do if you want rsync to cleanup old `.~tmp~` dirs that + and what you can do if you want rsync to cleanup old `.~tmp~` directories that might be lying around. Conflicts with [`--inplace`](#opt) and [`--append`](#opt). @@ -3429,8 +3694,8 @@ 2. there are no mount points in the hierarchy (since the delayed updates will fail if they can't be renamed into place). - See also the "atomic-rsync" python script in the "support" subdir for an - update algorithm that is even more atomic (it uses [`--link-dest`](#opt) + See also the "atomic-rsync" python script in the "support" subdirectory for an + update algorithm that is even closer to atomic (it uses [`--link-dest`](#opt) and a parallel hierarchy of files). 0. `--prune-empty-dirs`, `-m` @@ -3540,7 +3805,7 @@ don't know which of the 3 rsync processes is the client process, it's OK to signal all of them (since the non-client processes ignore the signal). - CAUTION: sending SIGVTALRM to an older rsync (pre-3.2.0) will kill it. + CAUTION: sending SIGVTALRM to an older rsync (pre-3.2.0, June 2020) will kill it. 0. `--password-file=FILE` @@ -3564,22 +3829,22 @@ secret that can be used to mount an encrypted filesystem (which you should unmount in the the "post-xfer exec" script). - The daemon must be at least version 3.2.1. + The daemon must be at least version 3.2.1 (June 2020). 0. `--list-only` This option will cause the source files to be listed instead of - transferred. This option is inferred if there is a single source arg and + transferred. This option is inferred if there is a single source argument and no destination specified, so its main uses are: - 1. to turn a copy command that includes a destination arg into a + 1. to turn a copy command that includes a destination argument into a file-listing command, or - 2. to be able to specify more than one source arg. Note: be sure to + 2. to be able to specify more than one source argument. Note: be sure to include the destination. - CAUTION: keep in mind that a source arg with a wild-card is expanded by the - shell into multiple args, so it is never safe to try to specify a single - wild-card arg to try to infer this option. A safe example is: + CAUTION: keep in mind that a source argument with a wild-card is expanded by the + shell into multiple arguments, so it is never safe to try to specify a single + wild-card argument to try to infer this option. A safe example is: > rsync -av --list-only foo* dest/ @@ -3588,7 +3853,7 @@ > drwxrwxr-x 4,096 2022/09/30 12:53:11 support > -rw-rw-r-- 80 2005/01/11 10:37:37 support/Makefile - The only option that affects this output style is (as of 3.1.0) the + The only option that affects this output style is the [`--human-readable`](#opt) (`-h`) option. The default is to output sizes as byte counts with digit separators (in a 14-character-width column). Specifying at least one `-h` option makes the sizes output with unit @@ -3596,9 +3861,10 @@ (and an 11-character-width column) use `--no-h`. Compatibility note: when requesting a remote listing of files from an rsync - that is version 2.6.3 or older, you may encounter an error if you ask for a + that is version 2.6.3 or older (i.e., pre-2005), + you may encounter an error if you ask for a non-recursive listing. This is because a file listing implies the - [`--dirs`](#opt) option w/o [`--recursive`](#opt), and older rsyncs don't + [`--dirs`](#opt) option without [`--recursive`](#opt), and older rsyncs don't have that option. To avoid this problem, either specify the `--no-dirs` option (if you don't need to expand a directory's content), or turn on recursion and exclude the content of subdirectories: `-r --exclude='/*/*'`. @@ -3684,9 +3950,9 @@ also the [`--only-write-batch`](#opt) option. This option overrides the negotiated checksum & compress lists and always - negotiates a choice based on old-school md5/md4/zlib choices. If you want - a more modern choice, use the [`--checksum-choice`](#opt) (`--cc`) and/or - [`--compress-choice`](#opt) (`--zc`) options. + negotiates a choice based on old-school md5/md4/zlib choices. This means + batch mode is not compatible with newer compression choices such as zstd or + lz4. 0. `--only-write-batch=FILE` @@ -3775,7 +4041,7 @@ Set the checksum seed to the integer NUM. This 4 byte checksum seed is included in each block and MD4 file checksum calculation (the more modern - MD5 file checksums don't use a seed). By default the checksum seed is + file checksums don't use a seed). By default the checksum seed is generated by the server and defaults to the current **time**(). This option is used to set a specific checksum seed, which is useful for applications that want repeatable block checksums, or in the case where the @@ -3930,9 +4196,9 @@ affect the contents of directories that rsync is "recursing" into, but they can also affect a top-level item in the transfer that was specified as a argument. -The default for any unmatched file/dir is for it to be included in the -transfer, which puts the file/dir into the sender's file list. The use of an -exclude rule causes one or more matching files/dirs to be left out of the +The default for any unmatched file/directory is for it to be included in the +transfer, which puts the file/directory into the sender's file list. The use of an +exclude rule causes one or more matching files/directories to be left out of the sender's file list. An include rule can be used to limit the effect of an exclude rule that is matching too many files. @@ -3996,7 +4262,7 @@ The following command does not need an include of the "x" directory because it is not a part of the transfer (note the trailing slash). Running this command -would copy just "`/tmp/x/file.txt`" because the "y" and "z" dirs get excluded: +would copy just "`/tmp/x/file.txt`" because the "y" and "z" directories get excluded: > rsync -ai -f'+ file.txt' -f'- *' x/ host:/tmp/x/ @@ -4081,7 +4347,7 @@ 0. `risk, 'R'` files that match the pattern are not protected. Equivalent to a receiver-only include, so `-f'R foo'` could also be specified as `-f'+r foo'`. -0. `clear, '!'` clears the current include/exclude list (takes no arg) +0. `clear, '!'` clears the current include/exclude list (takes no argument) When rules are being read from a file (using merge or dir-merge), empty lines are ignored, as are whole-line comments that start with a '`#`' (filename rules @@ -4146,9 +4412,9 @@ - Option `-f'- /foo'` would exclude a file (or directory) named foo in the transfer-root directory - Option `-f'- foo/'` would exclude any directory named foo -- Option `-f'- foo/*/bar'` would exclude any file/dir named bar which is at two +- Option `-f'- foo/*/bar'` would exclude any file/directory named bar which is at two levels below a directory named foo (if foo is in the transfer) -- Option `-f'- /foo/**/bar'` would exclude any file/dir named bar that was two +- Option `-f'- /foo/**/bar'` would exclude any file/directory named bar that was two or more levels below a top-level directory named foo (note that /foo/bar is **not** excluded by this) - Options `-f'+ */' -f'+ *.c' -f'- *'` would include all directories and .c @@ -4165,12 +4431,12 @@ absolute pathname of the current item. For example, `-f'-/ /etc/passwd'` would exclude the passwd file any time the transfer was sending files from the "/etc" directory, and "-/ subdir/foo" would always exclude "foo" when it - is in a dir named "subdir", even if "foo" is at the root of the current + is in a directory named "subdir", even if "foo" is at the root of the current transfer. - A `!` specifies that the include/exclude should take effect if the pattern fails to match. For instance, `-f'-! */'` would exclude all non-directories. - A `C` is used to indicate that all the global CVS-exclude rules should be - inserted as excludes in place of the "-C". No arg should follow. + inserted as excludes in place of the "-C". No argument should follow. - An `s` is used to indicate that the rule applies to the sending side. When a rule affects the sending side, it affects what files are put into the sender's file list. The default is for a rule to affect both sides unless @@ -4188,7 +4454,7 @@ directory that was removed on the source from being deleted on the destination. - An `x` indicates that a rule affects xattr names in xattr copy/delete - operations (and is thus ignored when matching file/dir names). If no + operations (and is thus ignored when matching file/directory names). If no xattr-matching rules are specified, a default xattr filtering rule is used (see the [`--xattrs`](#opt) option). @@ -4276,7 +4542,7 @@ transfer). If a per-directory merge-file is specified with a path that is a parent -directory of the first transfer directory, rsync will scan all the parent dirs +directory of the first transfer directory, rsync will scan all the parent directories from that starting point to the transfer directory for the indicated per-directory file. For instance, here is a common filter (see [`-F`](#opt)): @@ -4296,7 +4562,7 @@ The first two commands above will look for ".rsync-filter" in "/" and "/src" before the normal scan begins looking for the file in "/src/path" and its -subdirectories. The last command avoids the parent-dir scan and only looks for +subdirectories. The last command avoids the parent-directory scan and only looks for the ".rsync-filter" files in each directory that is a part of the transfer. If you want to include the contents of a ".cvsignore" in your patterns, you @@ -4319,7 +4585,7 @@ Both of the above rsync commands are identical. Each one will merge all the per-directory .cvsignore rules in the middle of the list rather than at the -end. This allows their dir-specific rules to supersede the rules that follow +end. This allows their directory-specific rules to supersede the rules that follow the :C instead of being subservient to all your rules. To affect the other CVS exclude rules (i.e. the default list of exclusions, the contents of $HOME/.cvsignore, and the value of $CVSIGNORE) you should omit the `-C` @@ -4491,7 +4757,7 @@ For your convenience, a script file is also created when the write-batch option is used: it will be named the same as the batch file with ".sh" appended. This -script file contains a command-line suitable for updating a destination tree +script file contains a command line suitable for updating a destination tree using the associated batch file. It can be executed using a Bourne (or Bourne-like) shell, optionally passing in an alternate destination tree pathname which is then used instead of the original destination path. This is @@ -4545,8 +4811,8 @@ version in the batch file is too new for the batch-reading rsync to handle. See also the [`--protocol`](#opt) option for a way to have the creating rsync generate a batch file that an older rsync can understand. (Note that batch -files changed format in version 2.6.3, so mixing versions older than that with -newer versions will not work.) +files changed format in version 2.6.3 (September 2004), so mixing versions +older than that with newer versions will not work.) When reading a batch file, rsync will force the value of certain options to match the data in the batch file if you didn't set them to the same as the @@ -4563,9 +4829,6 @@ can ignore this detail and just use the shell script as an easy way to run the appropriate [`--read-batch`](#opt) command for the batched data. -The original batch mode in rsync was based on "rsync+", but the latest -version uses a new implementation. - ## SYMBOLIC LINKS Three basic behaviors are possible when rsync encounters a symbolic @@ -4614,7 +4877,7 @@ For the effect of [`--munge-links`](#opt), see the discussion in that option's section. -Note that the [`--keep-dirlinks`](#opt) option does not effect symlinks in the +Note that the [`--keep-dirlinks`](#opt) option does not affect symlinks in the transfer but instead affects how rsync treats a symlink to a directory that already exists on the receiving side. See that option's section for a warning. @@ -4646,18 +4909,20 @@ - **0** - Success - **1** - Syntax or usage error - **2** - Protocol incompatibility -- **3** - Errors selecting input/output files, dirs +- **3** - Errors selecting input/output files, directories - **4** - Requested action not supported. Either: - an attempt was made to manipulate 64-bit files on a platform that cannot support them - an option was specified that is supported by the client and not by the server - **5** - Error starting client-server protocol -- **6** - Daemon unable to append to log-file - **10** - Error in socket I/O - **11** - Error in file I/O - **12** - Error in rsync protocol data stream - **13** - Errors with program diagnostics - **14** - Error in IPC code -- **20** - Received SIGUSR1 or SIGINT +- **15** - Sibling process crashed (e.g. core dumped). +- **16** - Sibling process was killed by a signal. +- **19** - Received SIGUSR1. +- **20** - Received SIGINT, SIGTERM, or SIGHUP. - **21** - Some error returned by **waitpid()** - **22** - Error allocating core memory buffers - **23** - Partial transfer due to error @@ -4676,7 +4941,7 @@ 0. `RSYNC_ICONV` Specify a default [`--iconv`](#opt) setting using this environment - variable. First supported in 3.0.0. + variable. 0. `RSYNC_OLD_ARGS` @@ -4689,7 +4954,7 @@ This variable is ignored if [`--old-args`](#opt), `--no-old-args`, or [`--secluded-args`](#opt) is specified on the command line. - First supported in 3.2.4. + First supported in 3.2.4 (April 2022). 0. `RSYNC_PROTECT_ARGS` @@ -4700,7 +4965,7 @@ This variable is ignored if [`--secluded-args`](#opt), `--no-secluded-args`, or [`--old-args`](#opt) is specified on the command line. - First supported in 3.1.0. Starting in 3.2.4, this variable is ignored if + Starting in 3.2.4 (April 2022), this variable is ignored if [`RSYNC_OLD_ARGS`](#) is set to a non-zero value. 0. `RSYNC_RSH` @@ -4840,5 +5105,7 @@ Rsync was originally written by Andrew Tridgell and Paul Mackerras. Many people from around the world have helped to maintain and improve it. +Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024. + Mailing lists for support and development are available at . diff -Nru rsync-3.4.1+ds1/rsync.c rsync-3.5.0+ds1/rsync.c --- rsync-3.4.1+ds1/rsync.c 2024-10-30 06:06:34.000000000 +0000 +++ rsync-3.5.0+ds1/rsync.c 2026-07-22 04:50:55.000000000 +0000 @@ -37,6 +37,7 @@ 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; @@ -408,7 +409,21 @@ if ((len = read_vstring(f_in, buf, MAXPATHLEN)) < 0) exit_cleanup(RERR_PROTOCOL); - if (sanitize_paths) { + /* For a basis type (FNAMECMP_FUZZY and the alt-dest FNAMECMP_FUZZY+N) + * the xname is a peer-supplied leaf name that the receiver joins to + * an operator-chosen basedir (--link-dest / --compare-dest / + * --copy-dest, or the fuzzy dir) and opens as the delta basis. It + * must never contain a ".." (or leading "/") that escapes that dir: + * a malicious sender could otherwise walk the receiver's filesystem + * -- e.g. --link-dest=/backup with an xname of "../../etc/shadow" -- + * and read an out-of-tree file as the basis (client-side + * arbitrary-read / FIFO-hang). Sanitize it always, not only in the + * daemon (sanitize_paths) case: the operator basedir may legitimately + * be relative (#915), but the wire-supplied leaf never needs "..". + * Only the basis types use xname as a path; other ITEM_XNAME_FOLLOWS + * uses (the hard-link "=> target" display name, which is often empty) + * are not paths and must be left byte-for-byte. */ + if (fnamecmp_type >= FNAMECMP_FUZZY) { sanitize_path(buf, buf, "", 0, SP_DEFAULT); len = strlen(buf); } @@ -494,11 +509,29 @@ int change_uid, change_gid; mode_t new_mode = file->mode; int inherit; + int dfd = -1; /* held dir fd for the entry's own dir, or -1 */ + const char *leaf = NULL; /* leaf of fname relative to dfd */ + 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 */ +#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 */ +#endif if (!sxp) { + int sret, sdfd; if (dry_run) return 1; - if (link_stat(fname, &sx2.st, 0) < 0) { + /* 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) { + const char *sl = strrchr(fname, '/'); + sret = link_stat_at(sdfd, sl ? sl + 1 : fname, &sx2.st, 0); + } else + sret = link_stat(fname, &sx2.st, 0); + if (sret < 0) { rsyserr(FERROR_XFER, errno, "stat %s failed", full_fname(fname)); return 0; @@ -509,6 +542,102 @@ } else inherit = !preserve_perms && file->flags & FLAG_DIR_CREATED; + /* Resolve the entry's directory once; the chown/chmod/times ops below + * 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); + if (dfd >= 0) { + const char *slash = strrchr(fname, '/'); + leaf = slash ? slash + 1 : fname; + } + +#if defined SUPPORT_XATTRS || defined SUPPORT_ACLS + /* Pin a regular-file/dir/fifo entry via the held dir fd with O_NOFOLLOW so + * the xattr/ACL ops below act on the held inode (sys_f*xattr/fsetxattr), + * not a re-resolved path a parent-symlink race could redirect. O_NONBLOCK + * stops a raced FIFO blocking the open; O_NOFOLLOW refuses a raced symlink + * leaf. A symlink/socket/device leaf or no held dfd leaves held_fd == -1, + * and the ACL code then uses setxattrat(AT_SYMLINK_NOFOLLOW) or falls back. + * Under --fake-super the ACL store stays an l-variant xattr and ignores it. */ + if (dfd >= 0 + && (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 + )) + 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() + && (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); + if (held_fd < 0 && strchr(fname, '/')) + xattr_refuse = 1; + } +#endif + + /* A cross-tree operator path (an absolute --backup-dir/--temp-dir/--*-dest + * leaf) has no held parent dfd, so the chmod/chown below would re-resolve the + * full path and could follow a parent component an attacker flips to a + * symlink mid-operation -- and an lchown through such a component even retags + * the planted symlink as ours (trust laundering that then defeats the + * owner-walk). Pin the leaf inode itself with an O_NOFOLLOW open via the + * 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 + * 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() + && (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); + /* 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 + * O_NOFOLLOW / owner-walk -> ELOOP, or vanished mid-flip -> ENOENT): + * refuse, never redirect via a re-resolvable path. --fake-super + * (am_root < 0) is the same: the daemon owns the freshly-staged leaf it is + * about to set %stat/ACL/xattr metadata on, so any open failure is a race + * -- refuse rather than fall through to a path-based sys_lsetxattr that a + * flipped parent could redirect outside the module. A plain non-root + * operator (am_root == 0) can still hit a legitimate EACCES on an + * owned-but-unreadable leaf; there we only treat the explicit symlink-race + * signal (ELOOP) as a refusal and otherwise fall back to the legacy path + * op rather than spuriously failing a real file. */ + if (op_leaf_fd < 0 && (am_root != 0 || errno == ELOOP)) + op_refuse = 1; +#if defined SUPPORT_XATTRS || defined SUPPORT_ACLS + if (op_leaf_fd >= 0) + held_fd = op_leaf_fd; /* xattr/ACL ops below pin to this leaf fd too */ +#endif + } + if (inherit && S_ISDIR(new_mode) && sxp->st.st_mode & S_ISGID) { /* We just created this directory and its setgid * bit is on, so make sure it stays on. */ @@ -519,8 +648,8 @@ new_mode = tweak_mode(new_mode, daemon_chmod_modes); #ifdef SUPPORT_ACLS - if (preserve_acls && !S_ISLNK(file->mode) && !ACL_READY(*sxp)) - get_acl(fname, sxp); + if (preserve_acls && !S_ISLNK(file->mode) && !ACL_READY(*sxp) && !op_refuse && !xattr_refuse) + get_acl_fdat(held_fd, dfd, leaf, fname, sxp); #endif change_uid = am_root && uid_ndx && sxp->st.st_uid != (uid_t)F_OWNER(file); @@ -547,7 +676,10 @@ 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 (do_lchown(fname, uid, gid) != 0) { + if ((op_leaf_fd >= 0 ? do_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) { /* We shouldn't have attempted to change uid * or gid unless have the privilege. */ rsyserr(FERROR_XFER, errno, "%s %s failed", @@ -563,8 +695,12 @@ * the destination had the setuid or setgid bits set * (due to the side effect of the chown call). */ if (sxp->st.st_mode & (S_ISUID | S_ISGID)) { - link_stat(fname, &sxp->st, - keep_dirlinks && S_ISDIR(sxp->st.st_mode)); + if (dfd >= 0) + link_stat_at(dfd, leaf, &sxp->st, + keep_dirlinks && S_ISDIR(sxp->st.st_mode)); + else + link_stat(fname, &sxp->st, + keep_dirlinks && S_ISDIR(sxp->st.st_mode)); } } if (change_uid) @@ -574,10 +710,10 @@ } #ifdef SUPPORT_XATTRS - if (am_root < 0) - set_stat_xattr(fname, file, new_mode); - if (preserve_xattrs && fnamecmp) - set_xattr(fname, file, fnamecmp, sxp); + if (am_root < 0 && !op_refuse && !xattr_refuse) + set_stat_xattr(fname, file, new_mode, held_fd); + if (preserve_xattrs && fnamecmp && !op_refuse && !xattr_refuse) + set_xattr(fname, file, fnamecmp, sxp, held_fd); #endif if ((omit_dir_times && S_ISDIR(sxp->st.st_mode)) @@ -631,7 +767,19 @@ } #endif if (updated & (UPDATED_MTIME|UPDATED_ATIME)) { - int ret = set_times(fname, &sx2.st); + int ret; +#ifdef HAVE_FUTIMENS + if (op_leaf_fd >= 0) + ret = do_futimens(op_leaf_fd, &sx2.st); + else +#endif + if (op_refuse) + ret = (errno = ELOOP, -1); + else { + ret = dfd >= 0 ? set_times_at(dfd, leaf, &sx2.st) : -2; + if (ret == -2) + ret = set_times(fname, &sx2.st); + } if (ret < 0) { rsyserr(FERROR_XFER, errno, "failed to set times on %s", full_fname(fname)); goto cleanup; @@ -649,15 +797,19 @@ * If set_acl() changes permission bits in the process of setting * an access ACL, it changes sxp->st.st_mode so we know whether we * need to chmod(). */ - if (preserve_acls && !S_ISLNK(new_mode)) { - if (set_acl(fname, file, sxp, new_mode) > 0) + if (preserve_acls && !S_ISLNK(new_mode) && !op_refuse && !xattr_refuse) { + if (set_acl_fdat(held_fd, dfd, leaf, fname, file, sxp, new_mode) > 0) updated |= UPDATED_ACLS; } #endif #ifdef HAVE_CHMOD if (!BITS_EQUAL(sxp->st.st_mode, new_mode, CHMOD_BITS)) { - int ret = am_root < 0 ? 0 : do_chmod(fname, new_mode); + int ret = am_root < 0 ? 0 + : op_leaf_fd >= 0 ? do_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); if (ret < 0) { rsyserr(FERROR_XFER, errno, "failed to set permissions on %s", @@ -676,6 +828,12 @@ rprintf(FCLIENT, "%s is uptodate\n", fname); } cleanup: +#if defined SUPPORT_XATTRS || defined SUPPORT_ACLS + if (held_fd >= 0 && held_fd != op_leaf_fd) /* may alias op_leaf_fd (cross-tree) */ + close(held_fd); +#endif + if (op_leaf_fd >= 0) + close(op_leaf_fd); if (sxp == &sx2) free_stat_x(&sx2); return updated; @@ -744,21 +902,27 @@ fnamecmp = get_backup_name(fname); } - /* Change permissions before putting the file into place. */ + /* Change permissions before putting the file into place. An absolute + * --temp-dir/--partial-dir leaves fnametmp on an operator path with no held + * dirfd, so resolve its metadata through the ownership walk (op_pin); a + * flipped temp-dir parent then can't redirect the chmod/chown/times/etc. + * (in-tree temps keep their held dirfd, so op_pin stays off there). */ + operator_path_resolve = 1; set_file_attrs(fnametmp, file, NULL, fnamecmp, ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME); + operator_path_resolve = 0; /* move tmp file over real file */ if (DEBUG_GTE(RECV, 1)) rprintf(FINFO, "renaming %s to %s\n", fnametmp, fname); - ret = robust_rename(fnametmp, fname, temp_copy_name, file->mode); + ret = robust_rename(fnametmp, fname, temp_copy_name, file->mode, file); if (ret < 0) { rsyserr(FERROR_XFER, errno, "%s %s -> \"%s\"", ret == -2 ? "copy" : "rename", full_fname(fnametmp), fname); if (!partialptr || (ret == -2 && temp_copy_name) - || robust_rename(fnametmp, partialptr, NULL, file->mode) < 0) - do_unlink(fnametmp); + || robust_rename(fnametmp, partialptr, NULL, file->mode, file) < 0) + do_unlink_at(fnametmp); return 0; } if (ret == 0) { @@ -774,7 +938,7 @@ ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME); if (temp_copy_name) { - if (do_rename(fnametmp, fname) < 0) { + if (do_rename_at(fnametmp, fname) < 0) { rsyserr(FERROR_XFER, errno, "rename %s -> \"%s\"", full_fname(fnametmp), fname); return 0; diff -Nru rsync-3.4.1+ds1/rsync.h rsync-3.5.0+ds1/rsync.h --- rsync-3.4.1+ds1/rsync.h 2025-01-15 19:21:54.000000000 +0000 +++ rsync-3.5.0+ds1/rsync.h 2026-08-02 10:32:33.000000000 +0000 @@ -163,12 +163,42 @@ /* For compatibility with older rsyncs */ #define OLD_MAX_BLOCK_SIZE ((int32)1 << 29) +/* Policy ceilings on attacker-controlled wire values. Picked well above any + * legitimate filesystem / protocol traffic but well below sizes that could + * cause integer overflow or DoS-grade allocations. See input_checking.txt. + * + * Note on MAX_WIRE_XATTR_DATALEN: xattr datum size is bounded only by the + * wire-format maximum (signed int32 varint, ~2GB). macOS resource forks + * are transferred as the com.apple.ResourceFork xattr and can legitimately + * be many GB; --max-alloc (default 1GB, configurable) is the real + * allocation cap. read_varint_size() still rejects negative values so a + * hostile peer cannot wrap to ~SIZE_MAX. */ +#define MAX_WIRE_XATTR_COUNT 65536 +#define MAX_WIRE_XATTR_NAMELEN 4096 +#define MAX_WIRE_XATTR_DATALEN ((int32)0x7fffffff) +#define MAX_WIRE_ACL_COUNT 65536 +#define MAX_WIRE_NSEC 999999999 +/* MAX_WIRE_DEL_STAT is the per-category cap for read_del_stats() in main.c, + * which accumulates 5 wire-supplied counts into the int32 stats.deleted_files + * accumulator. Capped at 2^28 so 5 * 2^28 = 1.34 GB stays under INT32_MAX + * (2.15 GB) with margin -- a higher cap (e.g. 2^30) would let a hostile peer + * supplying 3+ max-sized counts overflow the accumulator, which is signed-int + * UB. 2^28 is still well above any plausible real transfer's deletion count. */ +#define MAX_WIRE_DEL_STAT ((int32)1 << 28) + #define ROUND_UP_1024(siz) ((siz) & (1024-1) ? ((siz) | (1024-1)) + 1 : (siz)) #define IOERR_GENERAL (1<<0) /* For backward compatibility, this must == 1 */ #define IOERR_VANISHED (1<<1) #define IOERR_DEL_LIMIT (1<<2) +/* Mask of all currently defined IOERR_* bits. Used to sanitize values + * received from a peer via MSG_IO_ERROR so a malicious peer cannot set + * arbitrary (undefined) bits in the local io_error, which would then be + * stored and re-forwarded upstream. (Undefined bits never reach the exit + * code: cleanup.c maps only these defined bits onto RERR_* values.) */ +#define IOERR_VALID_MASK (IOERR_GENERAL | IOERR_VANISHED | IOERR_DEL_LIMIT) + #define MAX_ARGS 1000 #define MAX_BASIS_DIRS 20 #define MAX_SERVER_ARGS (MAX_BASIS_DIRS*2 + 100) @@ -400,6 +430,16 @@ #endif #include +/* O_NOFOLLOW refuses a final-component symlink with ELOOP on Linux, EMLINK on + * FreeBSD, and EFTYPE on NetBSD/OpenBSD. Treat all three as "hit a symlink". A + * genuine EMLINK (too many hard links) is harmless where this is used: callers + * fall through to a readlink/lstat, which restores the real error. */ +#ifdef EFTYPE +# define NOFOLLOW_HIT_SYMLINK(e) ((e) == ELOOP || (e) == EMLINK || (e) == EFTYPE) +#else +# define NOFOLLOW_HIT_SYMLINK(e) ((e) == ELOOP || (e) == EMLINK) +#endif + #ifdef HAVE_UTIME_H #include #endif @@ -675,6 +715,8 @@ # define SIZEOF_INT64 SIZEOF_OFF_T #endif +#define MAX_INT32 ((int32)0x7fffffff) + #define HT_KEY32 0 #define HT_KEY64 1 @@ -695,7 +737,7 @@ int64 key; }; -#define HT_NODE(tbl, bkts, i) ((void*)((char*)(bkts) + (i)*(tbl)->node_size)) +#define HT_NODE(tbl, bkts, i) ((void*)((char*)(bkts) + (size_t)(i)*(tbl)->node_size)) #define HT_KEY(node, k64) ((k64)? ((struct ht_int64_node*)(node))->key \ : (int64)((struct ht_int32_node*)(node))->key) @@ -1010,6 +1052,7 @@ #define FILTRULE_CLEAR_LIST (1<<18)/* this item is the "!" token */ #define FILTRULE_PERISHABLE (1<<19)/* perishable if parent dir goes away */ #define FILTRULE_XATTR (1<<20)/* rule only applies to xattr names */ +#define FILTRULE_FROM_FILE (1<<21)/* pattern text came from a file's contents */ #define FILTRULES_SIDES (FILTRULE_SENDER_SIDE | FILTRULE_RECEIVER_SIDE) @@ -1133,6 +1176,20 @@ #define NORETURN __attribute__((__noreturn__)) #endif +/* Under --enable-coverage, gcov flushes counters via an atexit handler that + * _exit() bypasses. Forked helpers that terminate via _exit() -- the + * pre/post-xfer-exec children, the become_daemon original process, the + * per-connection accept-loop child on early return -- must dump explicitly + * or their counters are lost, which systematically under-reports daemon-side + * coverage. cleanup.c and main.c already inline this for the two main exit + * paths; this macro covers the rest. No-op when not a coverage build. */ +#ifdef GCOV_COVERAGE +extern void __gcov_dump(void); +#define gcov_flush() __gcov_dump() +#else +#define gcov_flush() ((void)0) +#endif + typedef struct { STRUCT_STAT st; time_t crtime; diff -Nru rsync-3.4.1+ds1/rsync3.txt rsync-3.5.0+ds1/rsync3.txt --- rsync-3.4.1+ds1/rsync3.txt 2020-06-22 21:21:15.000000000 +0000 +++ rsync-3.5.0+ds1/rsync3.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,467 +0,0 @@ --*- indented-text -*- - -Notes towards a new version of rsync -Martin Pool , September 2001. - - -Good things about the current implementation: - - - Widely known and adopted. - - - Fast/efficient, especially for moderately small sets of files over - slow links (transoceanic or modem.) - - - Fairly reliable. - - - The choice of running over a plain TCP socket or tunneling over - ssh. - - - rsync operations are idempotent: you can always run the same - command twice to make sure it worked properly without any fear. - (Are there any exceptions?) - - - Small changes to files cause small deltas. - - - There is a way to evolve the protocol to some extent. - - - rdiff and rsync --write-batch allow generation of standalone patch - sets. rsync+ is pretty cheesy, though. xdelta seems cleaner. - - - Process triangle is creative, but seems to provoke OS bugs. - - - "Morning-after property": you don't need to know anything on the - local machine about the state of the remote machine, or about - transfers that have been done in the past. - - - You can easily push or pull simply by switching the order of - files. - - - The "modules" system has some neat features compared to - e.g. Apache's per-directory configuration. In particular, because - you can set a userid and chroot directory, there is strong - protection between different modules. I haven't seen any calls - for a more flexible system. - - -Bad things about the current implementation: - - - Persistent and hard-to-diagnose hang bugs remain - - - Protocol is sketchily documented, tied to this implementation, and - hard to modify/extend - - - Both the program and the protocol assume a single non-interactive - one-way transfer - - - A list of all files are held in memory for the entire transfer, - which cripples scalability to large file trees - - - Opening a new socket for every operation causes problems, - especially when running over SSH with password authentication. - - - Renamed files are not handled: the old file is removed, and the - new file created from scratch. - - - The versioning approach assumes that future versions of the - program know about all previous versions, and will do the right - thing. - - - People always get confused about ':' vs '::' - - - Error messages can be cryptic. - - - Default behaviour is not intuitive: in too many cases rsync will - happily do nothing. Perhaps -a should be the default? - - - People get confused by trailing slashes, though it's hard to think - of another reasonable way to make this necessary distinction - between a directory and its contents. - - -Protocol philosophy: - - *The* big difference between protocols like HTTP, FTP, and NFS is - that their fundamental operations are "read this file", "delete - this file", and "make this directory", whereas rsync is "make this - directory like this one". - - -Questionable features: - - These are neat, but not necessarily clean or worth preserving. - - - The remote rsync can be wrapped by some other program, such as in - tridge's rsync-mail scripts. The general feature of sending and - retrieving mail over rsync is good, but this is perhaps not the - right way to implement it. - - -Desirable features: - - These don't really require architectural changes; they're just - something to keep in mind. - - - Synchronize ACLs and extended attributes - - - Anonymous servers should be efficient - - - Code should be portable to non-UNIX systems - - - Should be possible to document the protocol in RFC form - - - --dry-run option - - - IPv6 support. Pretty straightforward. - - - Allow the basis and destination files to be different. For - example, you could use this when you have a CD-ROM and want to - download an updated image onto a hard drive. - - - Efficiently interrupt and restart a transfer. We can write a - checkpoint file that says where we're up to in the filesystem. - Alternatively, as long as transfers are idempotent, we can just - restart the whole thing. [NFSv4] - - - Scripting support. - - - Propagate atimes and do not modify them. This is very ugly on - Unix. It might be better to try to add O_NOATIME to kernels, and - call that. - - - Unicode. Probably just use UTF-8 for everything. - - - Open authentication system. Can we use PAM? Is SASL an adequate - mapping of PAM to the network, or useful in some other way? - - - Resume interrupted transfers without the --partial flag. We need - to leave the temporary file behind, and then know to use it. This - leaves a risk of large temporary files accumulating, which is not - good. Perhaps it should be off by default. - - - tcpwrappers support. Should be trivial; can already be done - through tcpd or inetd. - - - Socks support built in. It's not clear this is any better than - just linking against the socks library, though. - - - When run over SSH, invoke with predictable command-line arguments, - so that people can restrict what commands sshd will run. (Is this - really required?) - - - Comparison mode: give a list of which files are new, gone, or - different. Set return code depending on whether anything has - changed. - - - Internationalized messages (gettext?) - - - Optionally use real regexps rather than globs? - - - Show overall progress. Pretty hard to do, especially if we insist - on not scanning the directory tree up front. - - -Regression testing: - - - Support automatic testing. - - - Have hard internal timeouts against hangs. - - - Be deterministic. - - - Measure performance. - - -Hard links: - - At the moment, we can recreate hard links, but it's a bit - inefficient: it depends on holding a list of all files in the tree. - Every time we see a file with a linkcount >1, we need to search for - another known name that has the same (fsid,inum) tuple. We could do - that more efficiently by keeping a list of only files with - linkcount>1, and removing files from that list as all their names - become known. - - -Command-line options: - - We have rather a lot at the moment. We might get more if the tool - becomes more flexible. Do we need a .rc or configuration file? - That wouldn't really fit with its pattern of use: cp and tar don't - have them, though ssh does. - - -Scripting issues: - - - Perhaps support multiple scripting languages: candidates include - Perl, Python, Tcl, Scheme (guile?), sh, ... - - - Simply running a subprocess and looking at its stdout/exit code - might be sufficient, though it could also be pretty slow if it's - called often. - - - There are security issues about running remote code, at least if - it's not running in the users own account. So we can either - disallow it, or use some kind of sandbox system. - - - Python is a good language, but the syntax is not so good for - giving small fragments on the command line. - - - Tcl is broken Lisp. - - - Lots of sysadmins know Perl, though Perl can give some bizarre or - confusing errors. The built in stat operators and regexps might - be useful. - - - Sadly probably not enough people know Scheme. - - - sh is hard to embed. - - -Scripting hooks: - - - Whether to transfer a file - - - What basis file to use - - - Logging - - - Whether to allow transfers (for public servers) - - - Authentication - - - Locking - - - Cache - - - Generating backup path/name. - - - Post-processing of backups, e.g. to do compression. - - - After transfer, before replacement: so that we can spit out a diff - of what was changed, or kick off some kind of reconciliation - process. - - -VFS: - - Rather than talking straight to the filesystem, rsyncd talks through - an internal API. Samba has one. Is it useful? - - - Could be a tidy way to implement cached signatures. - - - Keep files compressed on disk? - - -Interactive interface: - - - Something like ncFTP, or integration into GNOME-vfs. Probably - hold a single socket connection open. - - - Can either call us as a separate process, or as a library. - - - The standalone process needs to produce output in a form easily - digestible by a calling program, like the --emacs feature some - have. Same goes for output: rpm outputs a series of hash symbols, - which are easier for a GUI to handle than "\r30% complete" - strings. - - - Yow! emacs support. (You could probably build that already, of - course.) I'd like to be able to write a simple script on a remote - machine that rsyncs it to my workstation, edits it there, then - pushes it back up. - - -Pie-in-the-sky features: - - These might have a severe impact on the protocol, and are not - clearly in our core requirements. It looks like in many of them - having scripting hooks will allow us - - - Transport over UDP multicast. The hard part is handling multiple - destinations which have different basis files. We can look at - multicast-TFTP for inspiration. - - - Conflict resolution. Possibly general scripting support will be - sufficient. - - - Integrate with locking. It's hard to see a good general solution, - because Unix systems have several locking mechanisms, and grabbing - the lock from programs that don't expect it could cause deadlocks, - timeouts, or other problems. Scripting support might help. - - - Replicate in place, rather than to a temporary file. This is - dangerous in the case of interruption, and it also means that the - delta can't refer to blocks that have already been overwritten. - On the other hand we could semi-trivially do this at first by - simply generating a delta with no copy instructions. - - - Replicate block devices. Most of the difficulties here are to do - with replication in place, though on some systems we will also - have to do I/O on block boundaries. - - - Peer to peer features. Flavour of the year. Can we think about - ways for clients to smoothly and voluntarily become servers for - content they receive? - - - Imagine a situation where the destination has a much faster link - to the cloud than the source. In this case, Mojo Nation downloads - interleaved blocks from several slower servers. The general - situation might be a way for a master rsync process to farm out - tasks to several subjobs. In this particular case they'd need - different sockets. This might be related to multicast. - - -Unlikely features: - - - Allow remote source and destination. If this can be cleanly - designed into the protocol, perhaps with the remote machine acting - as a kind of echo, then it's good. It's uncommon enough that we - don't want to shape the whole protocol around it, though. - - In fact, in a triangle of machines there are two possibilities: - all traffic passes from remote1 to remote2 through local, or local - just sets up the transfer and then remote1 talks to remote2. FTP - supports the second but it's not clearly good. There are some - security problems with being able to instruct one machine to open - a connection to another. - - -In favour of evolving the protocol: - - - Keeping compatibility with existing rsync servers will help with - adoption and testing. - - - We should at the very least be able to fall back to the new - protocol. - - - Error handling is not so good. - - -In favour of using a new protocol: - - - Maintaining compatibility might soak up development time that - would better go into improving a new protocol. - - - If we start from scratch, it can be documented as we go, and we - can avoid design decisions that make the protocol complex or - implementation-bound. - - -Error handling: - - - Errors should come back reliably, and be clearly associated with - the particular file that caused the problem. - - - Some errors ought to cause the whole transfer to abort; some are - just warnings. If any errors have occurred, then rsync ought to - return an error. - - -Concurrency: - - - We want to keep the CPU, filesystem, and network as full as - possible as much of the time as possible. - - - We can do nonblocking network IO, but not so for disk. - - - It makes sense to on the destination be generating signatures and - applying patches at the same time. - - - Can structure this with nonblocking, threads, separate processes, - etc. - - -Uses: - - - Mirroring software distributions: - - - Synchronizing laptop and desktop - - - NFS filesystem migration/replication. See - http://www.ietf.org/proceedings/00jul/00july-133.htm#P24510_1276764 - - - Sync with PDA - - - Network backup systems - - - CVS filemover - - -Conflict resolution: - - - Requires application-specific knowledge. We want to provide - policy, rather than mechanism. - - - Possibly allowing two-way migration across a single connection - would be useful. - - -Moved files: - - - There's no trivial way to detect renamed files, especially if they - move between directories. - - - If we had a picture of the remote directory from last time on - either machine, then the inode numbers might give us a hint about - files which may have been renamed. - - - Files that are renamed and not modified can be detected by - examining the directory listing, looking for files with the same - size/date as the origin. - - -Filesystem migration: - - NFSv4 probably wants to migrate file locks, but that's not really - our problem. - - -Atomic updates: - - The NFSv4 working group wants atomic migration. Most of the - responsibility for this lies on the NFS server or OS. - - If migrating a whole tree, then we could do a nearly-atomic rename - at the end. This ties in to having separate basis and destination - files. - - There's no way in Unix to replace a whole set of files atomically. - However, if we get them all onto the destination machine and then do - the updates quickly it would greatly reduce the window. - - -Scalability: - - We should aim to work well on machines in use in a year or two. - That probably means transfers of many millions of files in one - batch, and gigabytes or terabytes of data. - - For argument's sake: at the low end, we want to sync ten files for a - total of 10kb across a 1kB/s link. At the high end, we want to sync - 1e9 files for 1TB of data across a 1GB/s link. - - On the whole CPU usage is not normally a limiting factor, if only - because running over SSH burns a lot of cycles on encryption. - - Perhaps have resource throttling without relying on rlimit. - - -Streaming: - - A big attraction of rsync is that there are few round-trip delays: - basically only one to get started, and then everything is - pipelined. This is a problem with FTP, and NFS (at least up to - v3). NFSv4 can pipeline operations, but building on that is - probably a bit complicated. - - -Related work: - - - mirror.pl - - - ProFTPd - - - Apache - - - BitTorrent -- p2p mirroring - http://bitconjurer.org/BitTorrent/ diff -Nru rsync-3.4.1+ds1/rsyncd.conf.5 rsync-3.5.0+ds1/rsyncd.conf.5 --- rsync-3.4.1+ds1/rsyncd.conf.5 2025-01-15 20:49:30.000000000 +0000 +++ rsync-3.5.0+ds1/rsyncd.conf.5 2026-08-13 00:05:31.000000000 +0000 @@ -1,4 +1,4 @@ -.TH "rsyncd.conf" "5" "15 Jan 2025" "rsyncd.conf from rsync 3.4.1" "User Commands" +.TH "rsyncd.conf" "5" "13 Aug 2026" "rsyncd.conf from rsync 3.5.0" "User Commands" .\" prefix=/usr .P .SH "NAME" @@ -61,6 +61,13 @@ client via a remote shell. If run as a stand-alone daemon then just run the command "\fBrsync\ \-\-daemon\fP" from a suitable startup script. .P +Systems using systemd can use the example unit files in the source tree's +\fBpackaging/systemd\fP directory. The \fBrsync.service\fP file runs a stand-alone +daemon using \fBrsync\ \-\-daemon\ \-\-no\-detach\fP, while \fBrsync.socket\fP and +\fBrsync@.service\fP show a socket-activated setup for incoming connections. These +files may need local adjustment to match your installed rsync path, packaging +layout, and module policy. +.P When run via inetd you should add a line like this to /etc/services: .RS 4 .P @@ -234,15 +241,16 @@ When it has to limit access to a particular subdir (either due to chroot being disabled or having an inside-chroot path set), rsync will munge symlinks (by default) and sanitize paths. Those that dislike munged -symlinks (and really, really trust their users to not break out of the -subdir) can disable the symlink munging via the "munge symlinks" -parameter. -.IP -When rsync is sanitizing paths, it trims ".." path elements from args that -it believes would escape the module hierarchy. It also substitutes leading -slashes in absolute paths with the module's path (so that options such as -\fB\-\-backup-dir\fP & \fB\-\-compare-dest\fP interpret an absolute path as rooted in -the module's "path" dir). +symlinks can disable the symlink munging via the "munge symlinks" +parameter; incoming symlink values are then sanitized instead (see that +parameter). +.IP +When rsync is sanitizing paths, it resolves away the ".." path elements that +would take an arg above the module hierarchy. For an operator-supplied +option path such as \fB\-\-backup\-dir\fP & \fB\-\-compare\-dest\fP, a leading slash is +also substituted with the module's path (so an absolute path is interpreted +as rooted in the module's "path" dir); for the transfer's own source +args a leading slash is simply dropped. .IP When a chroot is in effect \fIand\fP the "name converter" parameter is \fInot\fP set, the "numeric ids" parameter will default to being enabled @@ -287,6 +295,11 @@ This setting is global. If you need some modules to require this and not others, then you will need to setup multiple rsync daemon processes on different ports. +.IP "\fBproxy\ protocol\ hosts\fP" +This global parameter lists the socket peer IP addresses that are allowed +to supply a \fBproxy\ protocol\fP header. The syntax is the same token format +used by \fBhosts\ allow\fP. When \fBproxy\ protocol\ =\ true\fP, this list must match +the direct peer before rsync trusts the forwarded client address. .IP "\fBname\ converter\fP" This parameter lets you specify a program that will be run by the rsync daemon to do user & group conversions between names & ids. This script @@ -310,7 +323,7 @@ Enabling this parameter disables the mapping of users and groups by name for the current daemon module. This prevents the daemon from trying to load any user/group-related files or libraries. This enabling makes the -transfer behave as if the client had passed the \fB\-\-numeric-ids\fP +transfer behave as if the client had passed the \fB\-\-numeric\-ids\fP command-line option. By default, this parameter is enabled for chroot modules and disabled for non-chroot modules. Also keep in mind that uid/gid preservation requires the module to be running as root (see "uid") @@ -321,42 +334,70 @@ that the module has the necessary resources it needs to translate names and that it is not possible for a user to change those resources. .IP "\fBmunge\ symlinks\fP" -This parameter tells rsync to modify all symlinks in the same way as the -(non-daemon-affecting) \fB\-\-munge-links\fP command-line option (using a method -described below). This should help protect your files from user trickery -when your daemon module is writable. The default is disabled when -"use chroot" is on with an inside-chroot path of "/", OR if "daemon chroot" -is on, otherwise it is enabled. -.IP -If you disable this parameter on a daemon that is not read-only, there are -tricks that a user can play with uploaded symlinks to access -daemon-excluded items (if your module has any), and, if "use chroot" is -off, rsync can even be tricked into showing or changing data that is -outside the module's path (as access-permissions allow). -.IP -The way rsync disables the use of symlinks is to prefix each one with the -string "/rsyncd-munged/". This prevents the links from being used as long -as that directory does not exist. When this parameter is enabled, rsync -will refuse to run if that path is a directory or a symlink to a directory. +This parameter tells rsync to modify all incoming symlinks the same way as +the (non-daemon-affecting) \fB\-\-munge\-links\fP command-line option: it prefixes +each stored symlink's value with the string "/rsyncd-munged/". Because that +directory does not normally exist, a munged symlink cannot be followed, so +an uploaded symlink cannot be used to read or write a file through it. On +the way back out the prefix is stripped, so clients see the original symlink +values. When this parameter is enabled, rsync refuses to run a module if +"/rsyncd-munged/" already exists in it as a directory or a symlink to a +directory. The default depends on chroot. For a plain chrooted module\ \-\- +"use chroot" on and "path" with no "/./" split, so the daemon +chroots straight into the module and serves it as the chroot root\ \-\- munging +is disabled by default, since the chroot itself already stops a symlink from +escaping. In every other case it is enabled: any non-chroot module, or a +chroot module whose "path" uses a "/./" split to serve an inner subdir +below the chroot root. +.IP +When this parameter is disabled on a writable module whose access is limited +to a subdir (i.e. "use chroot" is off, or the inside-chroot path is not +"/"), an incoming symlink is not prefixed but is still sanitized as it is +stored: a leading slash is dropped and any leading ".." components that would +take the value above the module are removed, so a stored symlink's value +cannot name a path outside the module (it can still point to another name +inside the module). +.IP +Munging changes only the symlink VALUES that are stored. Whether the daemon +follows a \fIpre-existing\fP in-module symlink when it resolves a transfer path +is a separate matter, governed by the secure path resolver that is on by +default; see the "insecure links" parameter for the exact rule and how +to opt out of it. +.IP When using the "munge symlinks" parameter in a chroot area that has an inside-chroot path of "/", you should add "/rsyncd-munged/" to the exclude setting for the module so that a user can't try to create it. .IP -Note: rsync makes no attempt to verify that any pre-existing symlinks in -the module's hierarchy are as safe as you want them to be (unless, of -course, it just copied in the whole hierarchy). If you setup an rsync -daemon on a new area or locally add symlinks, you can manually protect your -symlinks from being abused by prefixing "/rsyncd-munged/" to the start of -every symlink's value. There is a perl script in the support directory of -the source code named "munge-symlinks" that can be used to add or remove -this prefix from your symlinks. -.IP -When this parameter is disabled on a writable module and "use chroot" is -off (or the inside-chroot path is not "/"), incoming symlinks will be -modified to drop a leading slash and to remove ".." path elements that -rsync believes will allow a symlink to escape the module's hierarchy. -There are tricky ways to work around this, though, so you had better trust -your users if you choose this combination of parameters. +To add or remove the "/rsyncd-munged/" prefix on symlinks yourself (for +instance on links you create locally inside a module), there is a python +script in the support directory of the source code named "munge-symlinks". +.IP "\fBinsecure\ links\fP" +This parameter (defaulting to "false") controls whether the daemon resolves +its symlink-bearing paths with the normal symlink-race defences or with the +legacy follow-any-symlink behaviour. +.IP +When false (the default), the daemon refuses to follow a symlink it did not +create\ \-\- the directory enumeration and file-content opens of the served +module, and the operator-supplied paths (\fB\-\-backup\-dir\fP, \fB\-\-temp\-dir\fP, +\fB\-\-partial\-dir\fP, the alt-dest basis dirs, and so on) are all resolved with a +walk that follows a symlink component only when it is owned by uid\ 0 or +by the uid the module runs as, refusing one planted by any other user. A +client can never relax this: a client that sends \fB\-\-insecure\-links\fP to the +daemon has its request refused. +.IP +Setting this parameter to "true" restores the pre-hardening behaviour for +\fBthis module only\fP: the daemon follows \fBany\fP symlink in those paths. +.RS 4 +.IP +\fBWARNING:\fP enabling "insecure links" re-opens the symlink-escape / +TOCTOU vulnerabilities that the default closes (including CVE-2026-53797 +and CVE-2026-53801): an attacker who can create a symlink inside the module\ \-\- or who shares the host with the served tree\ \-\- can redirect the daemon's +reads and writes to files \fBoutside\fP the module. Enable it \fBonly\fP on a +single-tenant, fully-isolated, or otherwise trusted host where no untrusted +local user can plant a symlink in the served tree. It is the daemon +analogue of the client's \fB\-\-insecure\-links\fP and exists for the same narrow +"I accept the risk in my isolated environment" case. +.RE .IP "\fBcharset\fP" This specifies the name of the character set in which the module's filenames are stored. If the client uses an \fB\-\-iconv\fP option, the daemon @@ -389,7 +430,7 @@ syslog and output an error about the failure. (Note that the failure to open the specified log file used to be a fatal error.) .IP -This setting can be overridden by using the \fB\-\-log-file=FILE\fP or +This setting can be overridden by using the \fB\-\-log\-file=FILE\fP or \fB\-\-dparam=logfile=FILE\fP command-line options. The former overrides all the log-file parameters of the daemon and all module settings. The latter sets the daemon's log file and the default for all the modules, which still @@ -464,7 +505,7 @@ opened with O_NOATIME. .IP When set to Unset (the default) the user controls the setting via -\fB\-\-open-noatime\fP. +\fB\-\-open\-noatime\fP. .IP "\fBlist\fP" This parameter determines whether this module is listed when the client asks for a listing of available modules. In addition, if this is false, @@ -519,7 +560,7 @@ unchanged. See also the "gid" parameter. .IP "\fBfake\ super\fP" Setting "fake super = yes" for a module causes the daemon side to behave as -if the \fB\-\-fake-super\fP command-line option had been specified. This allows +if the \fB\-\-fake\-super\fP command-line option had been specified. This allows the full attributes of a file to be stored without having to have the daemon actually running as root. .IP "\fBfilter\fP" @@ -540,6 +581,15 @@ exclude everything in the subtree; the easiest way to do this is with a triple-star pattern like "\fB/secret/***\fP". .IP +The filter chain matches the \fBlogical name\fP of each item relative to the +module root, not the physical file it resolves to. It is a visibility and +tamper filter, \fBnot\fP a security boundary against symlinks: a symlink inside +the module whose own name is not excluded can still be followed to an excluded +target (the name the filter sees\ \-\- e.g. "\fBlink\fP"\ \-\- is not the excluded name\ \-\- e.g. "\fBsecret\fP"). What protects a writable module from symlink trickery is +the "munge symlinks" parameter (enabled by default for a writable, +non-chrooted module), \fBnot\fP this filter; do not rely on \fBexclude\fP/\fBfilter\fP to +confine a peer who can introduce or traverse a symlink. +.IP The "filter" parameter takes a space-separated list of daemon filter rules, though it is smart enough to know not to split a token at an internal space in a rule (e.g. "\fB\-\ /foo\ \ \-\ /bar\fP" is parsed as two rules). You may specify @@ -673,16 +723,60 @@ passwords. .IP There is no default for the "secrets file" parameter, you must choose a -name (such as \fB/etc/rsyncd.secrets\fP). The file must normally not be -readable by "other"; see "strict modes". If the file is not found or is -rejected, no logins for an "auth users" module will be possible. +name (such as \fB/etc/rsyncd.secrets\fP). Unless "strict modes" is +disabled, the file must not be readable or writable by "other", and (when +the daemon runs as root) must be owned by root; see "strict modes" for +the exact check. If the file is not found or is rejected, no logins for an +"auth users" module will be possible. +.IP "\fBauth\ digest\fP" +This parameter sets the \fIminimum\fP message digest that the daemon will accept +for the challenge-response authentication of an "auth users" module. +The available digests, strongest first, are \fBsha512\fP, \fBsha256\fP, \fBsha1\fP, +\fBmd5\fP, \fBmd4\fP. The daemon selects its own most-preferred digest that also +appears in the connecting client's advertised list (and falls back to \fBmd5\fP, +or \fBmd4\fP below protocol 30, for a client that advertises none), so a client +that advertises only weak digests can still force a weak negotiated digest. +If it is weaker than the configured +name, the connection is refused before the challenge is sent. The value is a +single digest name, for example: +.RS 4 +.IP +.nf +auth digest = sha256 +.fi +.RE +.IP +which requires \fBsha256\fP or \fBsha512\fP and refuses an \fBmd5\fP or \fBmd4\fP exchange. +.IP +This guards against an \fIauth-digest downgrade\fP. A peer that sends no digest +list (any rsync before 3.2.0, including the openrsync that ships with macOS) +makes the daemon fall back to \fBmd5\fP (or \fBmd4\fP below protocol 30), and an +on-path attacker can rewrite the unauthenticated negotiation to force the same +weak choice. A captured challenge-response is far cheaper to brute-force +offline against a weak digest, so a site whose clients are all modern can +require a strong one. See the rsync \fBSECURITY.md\fP document for the full +threat model. +.IP +There is \fBno default\fP\ \-\- the floor is off and the daemon accepts whatever +digest is negotiated, preserving compatibility with older clients. Enabling a +floor refuses every client that cannot offer at least that digest, notably any +rsync older than 3.2.0 (when the SHA digests were added) and the +macOS-bundled openrsync, which authenticate only with \fBmd4\fP/\fBmd5\fP. The SHA +digests require an rsync built with openssl at both ends; naming a digest this +build does not provide refuses all logins to the module (fail-closed). +.IP +Like the other auth parameters, this may be set per module or, for a default +that applies to every module, in the global part of the config file. .IP "\fBstrict\ modes\fP" This parameter determines whether or not the permissions on the secrets -file will be checked. If "strict modes" is true, then the secrets file -must not be readable by any user ID other than the one that the rsync -daemon is running under. If "strict modes" is false, the check is not -performed. The default is true. This parameter was added to accommodate -rsync running on the Windows operating system. +file will be checked. If "strict modes" is true (the default), the secrets +file is rejected if it is readable or writable by "other" (i.e. if any of +the other-read or other-write permission bits is set), and, when the daemon +is running as root, if the file is not owned by root. Group permissions and +the other-execute bit are not consulted, so modes such as 600 or 640 are +accepted while 644 (or any other-readable/\-writable mode) is rejected. If +"strict modes" is false, no permission check is performed. This parameter +was added to accommodate rsync running on the Windows operating system. .IP "\fBhosts\ allow\fP" This parameter allows you to specify a list of comma- and/or whitespace-separated patterns that are matched against a connecting @@ -793,7 +887,7 @@ "\fB%\-50n\ %8l\ %07p\fP"). In addition, one or more apostrophes may be specified prior to a numerical escape to indicate that the numerical value should be made more human-readable. The 3 supported levels are the same as for the -\fB\-\-human-readable\fP command-line option, though the default is for +\fB\-\-human\-readable\fP command-line option, though the default is for human-readability to be off. Each added apostrophe increases the level (e.g. "\fB%''l\ %'b\ %f\fP"). .IP @@ -820,7 +914,7 @@ value (and is not displayed when that is the case). For the checksum to output for a file, either the \fB\-\-checksum\fP option must be in-effect or the file must have been transferred without a salted checksum being used. -See the \fB\-\-checksum-choice\fP option for a way to choose the algorithm. +See the \fB\-\-checksum\-choice\fP option for a way to choose the algorithm. .IP o %f the filename (long form on sender; no trailing "/") .IP o @@ -856,7 +950,7 @@ .RE .IP For a list of what the characters mean that are output by "%i", see the -\fB\-\-itemize-changes\fP option in the rsync manpage. +\fB\-\-itemize\-changes\fP option in the rsync manpage. .IP Note that some of the logged output changes when talking with older rsync versions. For instance, deleted files were only output as verbose messages @@ -909,7 +1003,7 @@ .RE .IP Don't worry that the "\fB*\fP" will refuse certain vital options such as -\fB\-\-dry-run\fP, \fB\-\-server\fP, \fB\-\-no-iconv\fP, \fB\-\-seclude-args\fP, etc. These +\fB\-\-dry\-run\fP, \fB\-\-server\fP, \fB\-\-no\-iconv\fP, \fB\-\-seclude\-args\fP, etc. These important options are not matched by wild-card, so they must be overridden by their exact name. For instance, if you're forcing iconv transfers you could use something like this: @@ -930,8 +1024,8 @@ As an additional safety feature, the refusal of "delete" also refuses \fBremove-source-files\fP when the daemon is the sender; if you want the latter without the former, instead refuse "\fBdelete-*\fP" as that refuses all the -delete modes without affecting \fB\-\-remove-source-files\fP. (Keep in mind that -the client's \fB\-\-delete\fP option typically results in \fB\-\-delete-during\fP.) +delete modes without affecting \fB\-\-remove\-source\-files\fP. (Keep in mind that +the client's \fB\-\-delete\fP option typically results in \fB\-\-delete\-during\fP.) .IP When un-refusing delete options, you should either specify "\fB!delete*\fP" (to accept all delete options) or specify a limited set that includes "delete", @@ -943,7 +1037,7 @@ .fi .RE .IP -\&... whereas this accepts any delete option except \fB\-\-delete-after\fP: +\&... whereas this accepts any delete option except \fB\-\-delete\-after\fP: .RS 4 .IP .nf @@ -958,7 +1052,7 @@ option. .IP If you are un-refusing the compress option, you may want to match -"\fB!compress*\fP" if you also want to allow the \fB\-\-compress-level\fP option. +"\fB!compress*\fP" if you also want to allow the \fB\-\-compress\-level\fP option. .IP Note that the "copy-devices" & "write-devices" options are refused by default, but they can be explicitly accepted with "\fB!copy-devices\fP" and/or @@ -973,27 +1067,27 @@ .IP o \fB\-\-rsh\fP, \fB\-e\fP: Required to convey compatibility flags to the server. .IP o -\fB\-\-out-format\fP: This is required to convey output behavior to a remote -receiver. While rsync passes the older alias \fB\-\-log-format\fP for +\fB\-\-out\-format\fP: This is required to convey output behavior to a remote +receiver. While rsync passes the older alias \fB\-\-log\-format\fP for compatibility reasons, this options should not be confused with -\fB\-\-log-file-format\fP. +\fB\-\-log\-file\-format\fP. .IP o \fB\-\-sender\fP: Use "write only" parameter instead of refusing this. .IP o -\fB\-\-dry-run\fP, \fB\-n\fP: Who would want to disable this? +\fB\-\-dry\-run\fP, \fB\-n\fP: Who would want to disable this? .IP o -\fB\-\-seclude-args\fP, \fB\-s\fP: Is the oldest arg-protection method. +\fB\-\-seclude\-args\fP, \fB\-s\fP: Is the oldest arg-protection method. .IP o -\fB\-\-from0\fP, \fB\-0\fP: Makes it easier to accept/refuse \fB\-\-files-from\fP without +\fB\-\-from0\fP, \fB\-0\fP: Makes it easier to accept/refuse \fB\-\-files\-from\fP without affecting this helpful modifier. .IP o \fB\-\-iconv\fP: This is auto-disabled based on "charset" parameter. .IP o -\fB\-\-no-iconv\fP: Most transfers use this option. +\fB\-\-no\-iconv\fP: Most transfers use this option. .IP o -\fB\-\-checksum-seed\fP: Is a fairly rare, safe option. +\fB\-\-checksum\-seed\fP: Is a fairly rare, safe option. .IP o -\fB\-\-write-devices\fP: Is non-wild but also auto-disabled. +\fB\-\-write\-devices\fP: Is non-wild but also auto-disabled. .RE .IP "\fBdont\ compress\fP" \fBNOTE:\fP This parameter currently has no effect except in one instance: if @@ -1015,7 +1109,7 @@ mid-stream, it will be minimized to reduce the CPU usage as much as possible. .IP -See the \fB\-\-skip-compress\fP parameter in the \fBrsync\fP(1) manpage for the +See the \fB\-\-skip\-compress\fP parameter in the \fBrsync\fP(1) manpage for the list of file suffixes that are skipped by default if this parameter is not set. .IP "\fBearly\ exec\fP, \fBpre-xfer\ exec\fP, \fBpost-xfer\ exec\fP" @@ -1027,14 +1121,14 @@ \fInot\fP displayed if the script returns success. The other programs cannot send any text to the user. All output except for the \fBpre-xfer\ exec\fP stdout goes to the corresponding daemon's stdout/stderr, which is typically -discarded. See the \fB\-\-no-detach\fP option for a way to see the daemon's +discarded. See the \fB\-\-no\-detach\fP option for a way to see the daemon's output, which can assist with debugging. .IP Note that the \fBearly\ exec\fP command runs before any part of the transfer request is known except for the module name. This helper script can be used to setup a disk mount or decrypt some data into a module dir, but you may need to use \fBlock\ file\fP and \fBmax\ connections\fP to avoid concurrency -issues. If the client rsync specified the \fB\-\-early-input=FILE\fP option, it +issues. If the client rsync specified the \fB\-\-early\-input=FILE\fP option, it can send up to about 5K of data to the stdin of the early script. The stdin will otherwise be empty. .IP @@ -1080,6 +1174,46 @@ \fBwaitpid()\fP. .RE .IP +A \fB%VAR%\fP reference expanded into one of these commands is escaped for the +quoting context it appears in, which is correct for the single shell that +runs the command. It is not correct for a command that starts a SECOND +shell, such as \fBsh\ \-c\ '...\ %RSYNC_USER_NAME%\ ...'\fP: the outer shell consumes +the escaping, and the inner one sees the value bare. A value of \fBtouc?\fP +would then be glob-expanded against \fB/usr/bin/\fP, so the substitution is not +data any more\ \-\- it chooses the command. +.IP +Because rsync cannot escape for an unknown number of shell passes, a value +carrying any character that a shell could act on is refused outright, and +the transfer is aborted with +.RS 4 +.IP +.nf +refusing to run shell hook: %VAR% holds a shell metacharacter +.fi +.RE +.IP +The refused characters are whitespace, the quoting and expansion characters +\fB'\fP \fB"\fP \fB`\fP \fB$\fP \fB\\\fP, the separators \fB;\fP \fB&\fP \fB|\fP, redirections \fB<\fP \fB>\fP, +parentheses, the pattern characters \fB*\fP \fB?\fP \fB[\fP \fB]\fP, \fB#\fP, \fB!\fP, \fB~\fP, \fB{\fP \fB}\fP, +and any control character. Some of those are harmless on their own and are +refused for what a \fIsecond\fP shell would do with them\ \-\- \fB!\fP negates in +command position, so a hook written as an access check can be turned from a +denial into an approval, and \fB~\fP is tilde-expanded. +.IP +This applies to EVERY \fB%RSYNC_*%\fP value, including ones you supplied +yourself. In particular a module whose \fBpath\fP contains any of them cannot +be interpolated into one of these commands: \fBpath\ =\ /srv/My\ Backups\fP with a command mentioning +\fB%RSYNC_MODULE_PATH%\fP will refuse every transfer of that module, not just a +hostile one. The restriction is deliberate\ \-\- rsync cannot tell your space +from an attacker's once both are inside the same string, and \fBpath\fP itself +may be built from a peer value such as \fBpath\ =\ /home/%RSYNC_USER_NAME%\fP. +.IP +If you need such a value in a hook, pass it through the environment instead +of interpolating it: the same names are exported to the command, so +\fB"$RSYNC_MODULE_PATH"\fP inside your script is unrestricted by this check. +Quote it there as shown\ \-\- rsync no longer has any say in how your script +splits the value. +.IP Even though the commands can be associated with a particular module, they are run using the permissions of the user that started the daemon (not the module's uid/gid setting) without any chroot restrictions. @@ -1088,6 +1222,13 @@ shell to use when running the command (which otherwise uses your \fBsystem()\fP call's default shell), and use RSYNC_NO_XFER_EXEC to disable both options completely. +.IP "\fBtemp\ dir\fP" +Specifies a directory that rsync should use for temporary files created +during the transfer of updated files. If that directory is on a different +partition, after transfer file is being copied instead of unlinked. +.IP +This parameter equals with \fB\-\-temp\-dir\fP option, so please consult rsync +manpage for further information. .P .SH "CONFIG DIRECTIVES" .P @@ -1156,13 +1297,62 @@ stay in effect), and then include any \fB/etc/rsyncd.d/*.conf\fP files (defining modules without any global-value cross-talk). .P +.SH "SECURITY" +.P +An rsync daemon exposes part of your filesystem to network clients, and its +master process usually runs as root, so it should be configured defensively. +The following is a practical checklist; the project's \fBSECURITY.md\fP describes the +underlying threat model and the per-platform residuals. +.P +.IP "\fBConfine each module to its path.\fP +Leave "use chroot" enabled (the default attempts a chroot) so the +per-connection worker is jailed inside the module's "path". Where chroot +is unavailable rsync still resolves every transfer path so it cannot escape +the module through a symlinked parent, but a chroot is the stronger boundary\ \-\- keep it on unless you have a specific reason not to. A \fBpath\ =\ /outer/./inner\fP chroots to \fB/outer\fP while serving \fB/inner\fP as the module root." +.IP "\fBDrop privileges.\fP +Give each module a low-privilege "uid" and "gid" so a worker never +serves files as root; only the master process needs root (to chroot and bind +the port). Keep modules "read only" unless they must accept uploads, and +prefer "numeric ids" (or a "name converter") so a malicious file +list cannot map files to an unexpected local owner." +.IP "\fBRestrict who can connect.\fP +Limit reachability with "hosts allow" / "hosts deny", bind the +daemon to a specific "address", and use a host firewall. A module +without "auth users" is reachable by anyone who can reach the port." +.IP "\fBAuthenticate with a strong secret and digest.\fP +Require login with "auth users" plus a "secrets file" that is +readable only by the daemon user (rsync refuses a too-open file unless +"strict modes" is disabled), and use a long, high-entropy secret. To +refuse the weak MD4/MD5 challenge digests that old clients can negotiate, set +a floor with "auth digest" (for example \fBauth\ digest\ =\ sha512\fP); see the +AUTHENTICATION STRENGTH section below." +.IP "\fBEncrypt the connection.\fP +The daemon protocol authenticates but does \fBnot\fP encrypt the data stream. +Do not expose a cleartext daemon to an untrusted network: front it with a TLS +proxy (see the SSL/TLS Daemon Setup section below) or run it over ssh." +.IP "\fBKeep symlink handling safe.\fP +Leave "munge symlinks" enabled on any writable module (it is the default) +so an uploaded symlink cannot redirect a later operation outside the module. +Do \fBnot\fP set "insecure links = yes" on a host shared with untrusted +local users: it disables the daemon's symlink-escape protections (re-opening +the symlink TOCTOU/escape vulnerabilities, including CVE-2026-53797 and +CVE-2026-53801) and is appropriate only on a single-tenant, fully isolated +host." +.IP "\fBRefuse options you do not want.\fP +Use "refuse options" to reject client options a module should not allow." +.P .SH "AUTHENTICATION STRENGTH" .P -The authentication protocol used in rsync is a 128 bit MD4 based challenge -response system. This is fairly weak protection, though (with at least one -brute-force hash-finding algorithm publicly available), so if you want really -top-quality security, then I recommend that you run rsync over ssh. (Yes, a -future version of rsync will switch over to a stronger hashing method.) +Daemon authentication is a challenge-response system in which the client +returns a digest of the shared secret and a server-chosen challenge. Modern +rsync (3.2.7 and later, built with openssl) negotiates the strongest digest both +sides support\ \-\- \fBsha512\fP by default\ \-\- but for backward compatibility it falls +back to \fBmd5\fP (or \fBmd4\fP below protocol 30) for a client that advertises no digest +list, and the digest negotiation is itself unauthenticated. A daemon that wants +to require a strong digest can set "auth digest". For the best protection, +run rsync over ssh or a verified TLS transport (\fBrsync-ssl\fP), which protects the +exchange at the transport layer, and use a high-entropy shared secret (which is +infeasible to brute-force regardless of the digest). .P Also note that the rsync daemon protocol does not currently provide any encryption of the data that is transferred over the connection. Only @@ -1298,7 +1488,7 @@ .P .SH "VERSION" .P -This manpage is current for version 3.4.1 of rsync. +This manpage is current for version 3.5.0 of rsync. .P .SH "CREDITS" .P @@ -1323,6 +1513,8 @@ Rsync was originally written by Andrew Tridgell and Paul Mackerras. Many people from around the world have helped to maintain and improve it. .P +Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024. +.P Mailing lists for support and development are available at .UR https://lists.samba.org/ .UE . diff -Nru rsync-3.4.1+ds1/rsyncd.conf.5.html rsync-3.5.0+ds1/rsyncd.conf.5.html --- rsync-3.4.1+ds1/rsyncd.conf.5.html 2025-01-15 20:49:30.000000000 +0000 +++ rsync-3.5.0+ds1/rsyncd.conf.5.html 2026-08-13 00:05:31.000000000 +0000 @@ -70,6 +70,12 @@

    You can launch it either via inetd, as a stand-alone daemon, or from an rsync client via a remote shell. If run as a stand-alone daemon then just run the command "rsync --daemon" from a suitable startup script.

    +

    Systems using systemd can use the example unit files in the source tree's +packaging/systemd directory. The rsync.service file runs a stand-alone +daemon using rsync --daemon --no-detach, while rsync.socket and +rsync@.service show a socket-activated setup for incoming connections. These +files may need local adjustment to match your installed rsync path, packaging +layout, and module policy.

    When run via inetd you should add a line like this to /etc/services:

    rsync           873/tcp
    @@ -232,14 +238,15 @@
     

    When it has to limit access to a particular subdir (either due to chroot being disabled or having an inside-chroot path set), rsync will munge symlinks (by default) and sanitize paths. Those that dislike munged -symlinks (and really, really trust their users to not break out of the -subdir) can disable the symlink munging via the "munge symlinks" -parameter.

    -

    When rsync is sanitizing paths, it trims ".." path elements from args that -it believes would escape the module hierarchy. It also substitutes leading -slashes in absolute paths with the module's path (so that options such as ---backup-dir & --compare-dest interpret an absolute path as rooted in -the module's "path" dir).

    +symlinks can disable the symlink munging via the "munge symlinks" +parameter; incoming symlink values are then sanitized instead (see that +parameter).

    +

    When rsync is sanitizing paths, it resolves away the ".." path elements that +would take an arg above the module hierarchy. For an operator-supplied +option path such as --backup-dir & --compare-dest, a leading slash is +also substituted with the module's path (so an absolute path is interpreted +as rooted in the module's "path" dir); for the transfer's own source +args a leading slash is simply dropped.

    When a chroot is in effect and the "name converter" parameter is not set, the "numeric ids" parameter will default to being enabled (disabling name lookups). This means that if you manually setup @@ -285,6 +292,13 @@ different ports.

    +
    proxy protocol hosts
    +

    This global parameter lists the socket peer IP addresses that are allowed +to supply a proxy protocol header. The syntax is the same token format +used by hosts allow. When proxy protocol = true, this list must match +the direct peer before rsync trusts the forwarded client address.

    +
    +
    name converter

    This parameter lets you specify a program that will be run by the rsync daemon to do user & group conversions between names & ids. This script @@ -319,38 +333,65 @@

    -

    This parameter tells rsync to modify all symlinks in the same way as the -(non-daemon-affecting) --munge-links command-line option (using a method -described below). This should help protect your files from user trickery -when your daemon module is writable. The default is disabled when -"use chroot" is on with an inside-chroot path of "/", OR if "daemon chroot" -is on, otherwise it is enabled.

    -

    If you disable this parameter on a daemon that is not read-only, there are -tricks that a user can play with uploaded symlinks to access -daemon-excluded items (if your module has any), and, if "use chroot" is -off, rsync can even be tricked into showing or changing data that is -outside the module's path (as access-permissions allow).

    -

    The way rsync disables the use of symlinks is to prefix each one with the -string "/rsyncd-munged/". This prevents the links from being used as long -as that directory does not exist. When this parameter is enabled, rsync -will refuse to run if that path is a directory or a symlink to a directory. -When using the "munge symlinks" parameter in a chroot area that has an +

    This parameter tells rsync to modify all incoming symlinks the same way as +the (non-daemon-affecting) --munge-links command-line option: it prefixes +each stored symlink's value with the string "/rsyncd-munged/". Because that +directory does not normally exist, a munged symlink cannot be followed, so +an uploaded symlink cannot be used to read or write a file through it. On +the way back out the prefix is stripped, so clients see the original symlink +values. When this parameter is enabled, rsync refuses to run a module if +"/rsyncd-munged/" already exists in it as a directory or a symlink to a +directory. The default depends on chroot. For a plain chrooted module -⁠-⁠ +"use chroot" on and "path" with no "/./" split, so the daemon +chroots straight into the module and serves it as the chroot root -⁠-⁠ munging +is disabled by default, since the chroot itself already stops a symlink from +escaping. In every other case it is enabled: any non-chroot module, or a +chroot module whose "path" uses a "/./" split to serve an inner subdir +below the chroot root.

    +

    When this parameter is disabled on a writable module whose access is limited +to a subdir (i.e. "use chroot" is off, or the inside-chroot path is not +"/"), an incoming symlink is not prefixed but is still sanitized as it is +stored: a leading slash is dropped and any leading ".." components that would +take the value above the module are removed, so a stored symlink's value +cannot name a path outside the module (it can still point to another name +inside the module).

    +

    Munging changes only the symlink VALUES that are stored. Whether the daemon +follows a pre-existing in-module symlink when it resolves a transfer path +is a separate matter, governed by the secure path resolver that is on by +default; see the "insecure links" parameter for the exact rule and how +to opt out of it.

    +

    When using the "munge symlinks" parameter in a chroot area that has an inside-chroot path of "/", you should add "/rsyncd-munged/" to the exclude setting for the module so that a user can't try to create it.

    -

    Note: rsync makes no attempt to verify that any pre-existing symlinks in -the module's hierarchy are as safe as you want them to be (unless, of -course, it just copied in the whole hierarchy). If you setup an rsync -daemon on a new area or locally add symlinks, you can manually protect your -symlinks from being abused by prefixing "/rsyncd-munged/" to the start of -every symlink's value. There is a perl script in the support directory of -the source code named "munge-symlinks" that can be used to add or remove -this prefix from your symlinks.

    -

    When this parameter is disabled on a writable module and "use chroot" is -off (or the inside-chroot path is not "/"), incoming symlinks will be -modified to drop a leading slash and to remove ".." path elements that -rsync believes will allow a symlink to escape the module's hierarchy. -There are tricky ways to work around this, though, so you had better trust -your users if you choose this combination of parameters.

    +

    To add or remove the "/rsyncd-munged/" prefix on symlinks yourself (for +instance on links you create locally inside a module), there is a python +script in the support directory of the source code named "munge-symlinks".

    +
    + +
    +

    This parameter (defaulting to "false") controls whether the daemon resolves +its symlink-bearing paths with the normal symlink-race defences or with the +legacy follow-any-symlink behaviour.

    +

    When false (the default), the daemon refuses to follow a symlink it did not +create -⁠-⁠ the directory enumeration and file-content opens of the served +module, and the operator-supplied paths (--backup-dir, --temp-dir, +--partial-dir, the alt-dest basis dirs, and so on) are all resolved with a +walk that follows a symlink component only when it is owned by uid 0 or +by the uid the module runs as, refusing one planted by any other user. A +client can never relax this: a client that sends --insecure-links to the +daemon has its request refused.

    +

    Setting this parameter to "true" restores the pre-hardening behaviour for +this module only: the daemon follows any symlink in those paths.

    +
    +

    WARNING: enabling "insecure links" re-opens the symlink-escape / +TOCTOU vulnerabilities that the default closes (including CVE-2026-53797 +and CVE-2026-53801): an attacker who can create a symlink inside the module -⁠-⁠ or who shares the host with the served tree -⁠-⁠ can redirect the daemon's +reads and writes to files outside the module. Enable it only on a +single-tenant, fully-isolated, or otherwise trusted host where no untrusted +local user can plant a symlink in the served tree. It is the daemon +analogue of the client's --insecure-links and exists for the same narrow +"I accept the risk in my isolated environment" case.

    +
    charset
    @@ -551,6 +592,14 @@ prevent access to an entire subtree, for example, "/secret", you must exclude everything in the subtree; the easiest way to do this is with a triple-star pattern like "/secret/***".

    +

    The filter chain matches the logical name of each item relative to the +module root, not the physical file it resolves to. It is a visibility and +tamper filter, not a security boundary against symlinks: a symlink inside +the module whose own name is not excluded can still be followed to an excluded +target (the name the filter sees -⁠-⁠ e.g. "link" -⁠-⁠ is not the excluded name -⁠-⁠ e.g. "secret"). What protects a writable module from symlink trickery is +the "munge symlinks" parameter (enabled by default for a writable, +non-chrooted module), not this filter; do not rely on exclude/filter to +confine a peer who can introduce or traverse a symlink.

    The "filter" parameter takes a space-separated list of daemon filter rules, though it is smart enough to know not to split a token at an internal space in a rule (e.g. "- /foo - /bar" is parsed as two rules). You may specify @@ -686,18 +735,58 @@ require that you specify a group password if you do not want to use shared passwords.

    There is no default for the "secrets file" parameter, you must choose a -name (such as /etc/rsyncd.secrets). The file must normally not be -readable by "other"; see "strict modes". If the file is not found or is -rejected, no logins for an "auth users" module will be possible.

    +name (such as /etc/rsyncd.secrets). Unless "strict modes" is +disabled, the file must not be readable or writable by "other", and (when +the daemon runs as root) must be owned by root; see "strict modes" for +the exact check. If the file is not found or is rejected, no logins for an +"auth users" module will be possible.

    +
    + +
    auth digest
    +

    This parameter sets the minimum message digest that the daemon will accept +for the challenge-response authentication of an "auth users" module. +The available digests, strongest first, are sha512, sha256, sha1, +md5, md4. The daemon selects its own most-preferred digest that also +appears in the connecting client's advertised list (and falls back to md5, +or md4 below protocol 30, for a client that advertises none), so a client +that advertises only weak digests can still force a weak negotiated digest. +If it is weaker than the configured +name, the connection is refused before the challenge is sent. The value is a +single digest name, for example:

    +
    +
    auth digest = sha256
    +
    +
    +

    which requires sha256 or sha512 and refuses an md5 or md4 exchange.

    +

    This guards against an auth-digest downgrade. A peer that sends no digest +list (any rsync before 3.2.0, including the openrsync that ships with macOS) +makes the daemon fall back to md5 (or md4 below protocol 30), and an +on-path attacker can rewrite the unauthenticated negotiation to force the same +weak choice. A captured challenge-response is far cheaper to brute-force +offline against a weak digest, so a site whose clients are all modern can +require a strong one. See the rsync SECURITY.md document for the full +threat model.

    +

    There is no default -⁠-⁠ the floor is off and the daemon accepts whatever +digest is negotiated, preserving compatibility with older clients. Enabling a +floor refuses every client that cannot offer at least that digest, notably any +rsync older than 3.2.0 (when the SHA digests were added) and the +macOS-bundled openrsync, which authenticate only with md4/md5. The SHA +digests require an rsync built with openssl at both ends; naming a digest this +build does not provide refuses all logins to the module (fail-closed).

    +

    Like the other auth parameters, this may be set per module or, for a default +that applies to every module, in the global part of the config file.

    strict modes

    This parameter determines whether or not the permissions on the secrets -file will be checked. If "strict modes" is true, then the secrets file -must not be readable by any user ID other than the one that the rsync -daemon is running under. If "strict modes" is false, the check is not -performed. The default is true. This parameter was added to accommodate -rsync running on the Windows operating system.

    +file will be checked. If "strict modes" is true (the default), the secrets +file is rejected if it is readable or writable by "other" (i.e. if any of +the other-read or other-write permission bits is set), and, when the daemon +is running as root, if the file is not owned by root. Group permissions and +the other-execute bit are not consulted, so modes such as 600 or 640 are +accepted while 644 (or any other-readable/-⁠writable mode) is rejected. If +"strict modes" is false, no permission check is performed. This parameter +was added to accommodate rsync running on the Windows operating system.

    hosts allow
    @@ -1022,6 +1111,39 @@
  • RSYNC_RAW_STATUS: (post-xfer only) the raw exit value from waitpid().
  • +

    A %VAR% reference expanded into one of these commands is escaped for the +quoting context it appears in, which is correct for the single shell that +runs the command. It is not correct for a command that starts a SECOND +shell, such as sh -c '... %RSYNC_USER_NAME% ...': the outer shell consumes +the escaping, and the inner one sees the value bare. A value of touc? +would then be glob-expanded against /usr/bin/, so the substitution is not +data any more -⁠-⁠ it chooses the command.

    +

    Because rsync cannot escape for an unknown number of shell passes, a value +carrying any character that a shell could act on is refused outright, and +the transfer is aborted with

    +
    +
    refusing to run shell hook: %VAR% holds a shell metacharacter
    +
    +
    +

    The refused characters are whitespace, the quoting and expansion characters +' " ` $ \, the separators ; & |, redirections < >, +parentheses, the pattern characters * ? [ ], #, !, ~, { }, +and any control character. Some of those are harmless on their own and are +refused for what a second shell would do with them -⁠-⁠ ! negates in +command position, so a hook written as an access check can be turned from a +denial into an approval, and ~ is tilde-expanded.

    +

    This applies to EVERY %RSYNC_*% value, including ones you supplied +yourself. In particular a module whose path contains any of them cannot +be interpolated into one of these commands: path = /srv/My Backups with a command mentioning +%RSYNC_MODULE_PATH% will refuse every transfer of that module, not just a +hostile one. The restriction is deliberate -⁠-⁠ rsync cannot tell your space +from an attacker's once both are inside the same string, and path itself +may be built from a peer value such as path = /home/%RSYNC_USER_NAME%.

    +

    If you need such a value in a hook, pass it through the environment instead +of interpolating it: the same names are exported to the command, so +"$RSYNC_MODULE_PATH" inside your script is unrestricted by this check. +Quote it there as shown -⁠-⁠ rsync no longer has any say in how your script +splits the value.

    Even though the commands can be associated with a particular module, they are run using the permissions of the user that started the daemon (not the module's uid/gid setting) without any chroot restrictions.

    @@ -1030,6 +1152,14 @@ system() call's default shell), and use RSYNC_NO_XFER_EXEC to disable both options completely.

    + +
    temp dir
    +

    Specifies a directory that rsync should use for temporary files created +during the transfer of updated files. If that directory is on a different +partition, after transfer file is being copied instead of unlinked.

    +

    This parameter equals with --temp-dir option, so please consult rsync +manpage for further information.

    +

    CONFIG DIRECTIVES

    There are currently two config directives available that allow a config file to @@ -1081,12 +1211,74 @@

    This would merge any /etc/rsyncd.d/*.inc files (for global values that should stay in effect), and then include any /etc/rsyncd.d/*.conf files (defining modules without any global-value cross-talk).

    +

    SECURITY

    +

    An rsync daemon exposes part of your filesystem to network clients, and its +master process usually runs as root, so it should be configured defensively. +The following is a practical checklist; the project's SECURITY.md describes the +underlying threat model and the per-platform residuals.

    +
    + +
    Confine each module to its path. +Leave "use chroot" enabled (the default attempts a chroot) so the +per-connection worker is jailed inside the module's "path". Where chroot +is unavailable rsync still resolves every transfer path so it cannot escape +the module through a symlinked parent, but a chroot is the stronger boundary -⁠-⁠ keep it on unless you have a specific reason not to. A path = /outer/./inner chroots to /outer while serving /inner as the module root.
    +
    + +
    Drop privileges. +Give each module a low-privilege "uid" and "gid" so a worker never +serves files as root; only the master process needs root (to chroot and bind +the port). Keep modules "read only" unless they must accept uploads, and +prefer "numeric ids" (or a "name converter") so a malicious file +list cannot map files to an unexpected local owner.
    +
    + +
    Restrict who can connect. +Limit reachability with "hosts allow" / "hosts deny", bind the +daemon to a specific "address", and use a host firewall. A module +without "auth users" is reachable by anyone who can reach the port.
    +
    + +
    Authenticate with a strong secret and digest. +Require login with "auth users" plus a "secrets file" that is +readable only by the daemon user (rsync refuses a too-open file unless +"strict modes" is disabled), and use a long, high-entropy secret. To +refuse the weak MD4/MD5 challenge digests that old clients can negotiate, set +a floor with "auth digest" (for example auth digest = sha512); see the +AUTHENTICATION STRENGTH section below.
    +
    + +
    Encrypt the connection. +The daemon protocol authenticates but does not encrypt the data stream. +Do not expose a cleartext daemon to an untrusted network: front it with a TLS +proxy (see the SSL/TLS Daemon Setup section below) or run it over ssh.
    +
    + +
    +
    + +
    Refuse options you do not want. +Use "refuse options" to reject client options a module should not allow.
    +
    +

    AUTHENTICATION STRENGTH

    -

    The authentication protocol used in rsync is a 128 bit MD4 based challenge -response system. This is fairly weak protection, though (with at least one -brute-force hash-finding algorithm publicly available), so if you want really -top-quality security, then I recommend that you run rsync over ssh. (Yes, a -future version of rsync will switch over to a stronger hashing method.)

    +

    Daemon authentication is a challenge-response system in which the client +returns a digest of the shared secret and a server-chosen challenge. Modern +rsync (3.2.7 and later, built with openssl) negotiates the strongest digest both +sides support -⁠-⁠ sha512 by default -⁠-⁠ but for backward compatibility it falls +back to md5 (or md4 below protocol 30) for a client that advertises no digest +list, and the digest negotiation is itself unauthenticated. A daemon that wants +to require a strong digest can set "auth digest". For the best protection, +run rsync over ssh or a verified TLS transport (rsync-ssl), which protects the +exchange at the transport layer, and use a high-entropy shared secret (which is +infeasible to brute-force regardless of the digest).

    Also note that the rsync daemon protocol does not currently provide any encryption of the data that is transferred over the connection. Only authentication is provided. Use ssh as the transport if you want encryption.

    @@ -1191,7 +1383,7 @@

    Please report bugs! The rsync bug tracking system is online at https://rsync.samba.org/.

    VERSION

    -

    This manpage is current for version 3.4.1 of rsync.

    +

    This manpage is current for version 3.5.0 of rsync.

    CREDITS

    Rsync is distributed under the GNU General Public License. See the file COPYING for details.

    @@ -1203,7 +1395,8 @@

    AUTHOR

    Rsync was originally written by Andrew Tridgell and Paul Mackerras. Many people from around the world have helped to maintain and improve it.

    +

    Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024.

    Mailing lists for support and development are available at https://lists.samba.org/.

    -

    15 Jan 2025

    +

    13 Aug 2026

    diff -Nru rsync-3.4.1+ds1/rsyncd.conf.5.md rsync-3.5.0+ds1/rsyncd.conf.5.md --- rsync-3.4.1+ds1/rsyncd.conf.5.md 2024-04-08 20:16:12.000000000 +0000 +++ rsync-3.5.0+ds1/rsyncd.conf.5.md 2026-07-31 23:52:27.000000000 +0000 @@ -56,6 +56,13 @@ client via a remote shell. If run as a stand-alone daemon then just run the command "`rsync --daemon`" from a suitable startup script. +Systems using systemd can use the example unit files in the source tree's +`packaging/systemd` directory. The `rsync.service` file runs a stand-alone +daemon using `rsync --daemon --no-detach`, while `rsync.socket` and +`rsync@.service` show a socket-activated setup for incoming connections. These +files may need local adjustment to match your installed rsync path, packaging +layout, and module policy. + When run via inetd you should add a line like this to /etc/services: > rsync 873/tcp @@ -231,15 +238,16 @@ When it has to limit access to a particular subdir (either due to chroot being disabled or having an inside-chroot path set), rsync will munge symlinks (by default) and sanitize paths. Those that dislike munged - symlinks (and really, really trust their users to not break out of the - subdir) can disable the symlink munging via the "[munge symlinks](#)" - parameter. - - When rsync is sanitizing paths, it trims ".." path elements from args that - it believes would escape the module hierarchy. It also substitutes leading - slashes in absolute paths with the module's path (so that options such as - `--backup-dir` & `--compare-dest` interpret an absolute path as rooted in - the module's "[path](#)" dir). + symlinks can disable the symlink munging via the "[munge symlinks](#)" + parameter; incoming symlink values are then sanitized instead (see that + parameter). + + When rsync is sanitizing paths, it resolves away the ".." path elements that + would take an arg above the module hierarchy. For an operator-supplied + option path such as `--backup-dir` & `--compare-dest`, a leading slash is + also substituted with the module's path (so an absolute path is interpreted + as rooted in the module's "[path](#)" dir); for the transfer's own source + args a leading slash is simply dropped. When a chroot is in effect *and* the "[name converter](#)" parameter is *not* set, the "[numeric ids](#)" parameter will default to being enabled @@ -289,6 +297,13 @@ others, then you will need to setup multiple rsync daemon processes on different ports. +0. `proxy protocol hosts` + + This global parameter lists the socket peer IP addresses that are allowed + to supply a `proxy protocol` header. The syntax is the same token format + used by `hosts allow`. When `proxy protocol = true`, this list must match + the direct peer before rsync trusts the forwarded client address. + 0. `name converter` This parameter lets you specify a program that will be run by the rsync @@ -328,42 +343,71 @@ 0. `munge symlinks` - This parameter tells rsync to modify all symlinks in the same way as the - (non-daemon-affecting) `--munge-links` command-line option (using a method - described below). This should help protect your files from user trickery - when your daemon module is writable. The default is disabled when - "[use chroot](#)" is on with an inside-chroot path of "/", OR if "[daemon chroot](#)" - is on, otherwise it is enabled. - - If you disable this parameter on a daemon that is not read-only, there are - tricks that a user can play with uploaded symlinks to access - daemon-excluded items (if your module has any), and, if "[use chroot](#)" is - off, rsync can even be tricked into showing or changing data that is - outside the module's path (as access-permissions allow). - - The way rsync disables the use of symlinks is to prefix each one with the - string "/rsyncd-munged/". This prevents the links from being used as long - as that directory does not exist. When this parameter is enabled, rsync - will refuse to run if that path is a directory or a symlink to a directory. + This parameter tells rsync to modify all incoming symlinks the same way as + the (non-daemon-affecting) `--munge-links` command-line option: it prefixes + each stored symlink's value with the string "/rsyncd-munged/". Because that + directory does not normally exist, a munged symlink cannot be followed, so + an uploaded symlink cannot be used to read or write a file through it. On + the way back out the prefix is stripped, so clients see the original symlink + values. When this parameter is enabled, rsync refuses to run a module if + "/rsyncd-munged/" already exists in it as a directory or a symlink to a + directory. The default depends on chroot. For a plain chrooted module -- + "[use chroot](#)" on and "[path](#)" with no "/./" split, so the daemon + chroots straight into the module and serves it as the chroot root -- munging + is disabled by default, since the chroot itself already stops a symlink from + escaping. In every other case it is enabled: any non-chroot module, or a + chroot module whose "[path](#)" uses a "/./" split to serve an inner subdir + below the chroot root. + + When this parameter is disabled on a writable module whose access is limited + to a subdir (i.e. "[use chroot](#)" is off, or the inside-chroot path is not + "/"), an incoming symlink is not prefixed but is still sanitized as it is + stored: a leading slash is dropped and any leading ".." components that would + take the value above the module are removed, so a stored symlink's value + cannot name a path outside the module (it can still point to another name + inside the module). + + Munging changes only the symlink VALUES that are stored. Whether the daemon + follows a *pre-existing* in-module symlink when it resolves a transfer path + is a separate matter, governed by the secure path resolver that is on by + default; see the "[insecure links](#)" parameter for the exact rule and how + to opt out of it. + When using the "munge symlinks" parameter in a chroot area that has an inside-chroot path of "/", you should add "/rsyncd-munged/" to the exclude setting for the module so that a user can't try to create it. - Note: rsync makes no attempt to verify that any pre-existing symlinks in - the module's hierarchy are as safe as you want them to be (unless, of - course, it just copied in the whole hierarchy). If you setup an rsync - daemon on a new area or locally add symlinks, you can manually protect your - symlinks from being abused by prefixing "/rsyncd-munged/" to the start of - every symlink's value. There is a perl script in the support directory of - the source code named "munge-symlinks" that can be used to add or remove - this prefix from your symlinks. - - When this parameter is disabled on a writable module and "[use chroot](#)" is - off (or the inside-chroot path is not "/"), incoming symlinks will be - modified to drop a leading slash and to remove ".." path elements that - rsync believes will allow a symlink to escape the module's hierarchy. - There are tricky ways to work around this, though, so you had better trust - your users if you choose this combination of parameters. + To add or remove the "/rsyncd-munged/" prefix on symlinks yourself (for + instance on links you create locally inside a module), there is a python + script in the support directory of the source code named "munge-symlinks". + +0. `insecure links` + + This parameter (defaulting to "false") controls whether the daemon resolves + its symlink-bearing paths with the normal symlink-race defences or with the + legacy follow-any-symlink behaviour. + + When false (the default), the daemon refuses to follow a symlink it did not + create -- the directory enumeration and file-content opens of the served + module, and the operator-supplied paths (`--backup-dir`, `--temp-dir`, + `--partial-dir`, the alt-dest basis dirs, and so on) are all resolved with a + walk that follows a symlink component only when it is owned by uid 0 or + by the uid the module runs as, refusing one planted by any other user. A + client can never relax this: a client that sends `--insecure-links` to the + daemon has its request refused. + + Setting this parameter to "true" restores the pre-hardening behaviour for + **this module only**: the daemon follows **any** symlink in those paths. + + > **WARNING:** enabling "insecure links" re-opens the symlink-escape / + > TOCTOU vulnerabilities that the default closes (including CVE-2026-53797 + > and CVE-2026-53801): an attacker who can create a symlink inside the module + > -- or who shares the host with the served tree -- can redirect the daemon's + > reads and writes to files **outside** the module. Enable it **only** on a + > single-tenant, fully-isolated, or otherwise trusted host where no untrusted + > local user can plant a symlink in the served tree. It is the daemon + > analogue of the client's `--insecure-links` and exists for the same narrow + > "I accept the risk in my isolated environment" case. 0. `charset` @@ -573,6 +617,16 @@ exclude everything in the subtree; the easiest way to do this is with a triple-star pattern like "`/secret/***`". + The filter chain matches the **logical name** of each item relative to the + module root, not the physical file it resolves to. It is a visibility and + tamper filter, **not** a security boundary against symlinks: a symlink inside + the module whose own name is not excluded can still be followed to an excluded + target (the name the filter sees -- e.g. "`link`" -- is not the excluded name + -- e.g. "`secret`"). What protects a writable module from symlink trickery is + the "[munge symlinks](#)" parameter (enabled by default for a writable, + non-chrooted module), **not** this filter; do not rely on `exclude`/`filter` to + confine a peer who can introduce or traverse a symlink. + The "filter" parameter takes a space-separated list of daemon filter rules, though it is smart enough to know not to split a token at an internal space in a rule (e.g. "`- /foo - /bar`" is parsed as two rules). You may specify @@ -714,18 +768,60 @@ passwords. There is no default for the "secrets file" parameter, you must choose a - name (such as `/etc/rsyncd.secrets`). The file must normally not be - readable by "other"; see "[strict modes](#)". If the file is not found or is - rejected, no logins for an "[auth users](#)" module will be possible. + name (such as `/etc/rsyncd.secrets`). Unless "[strict modes](#)" is + disabled, the file must not be readable or writable by "other", and (when + the daemon runs as root) must be owned by root; see "[strict modes](#)" for + the exact check. If the file is not found or is rejected, no logins for an + "[auth users](#)" module will be possible. + +0. `auth digest` + + This parameter sets the *minimum* message digest that the daemon will accept + for the challenge-response authentication of an "[auth users](#)" module. + The available digests, strongest first, are `sha512`, `sha256`, `sha1`, + `md5`, `md4`. The daemon selects its own most-preferred digest that also + appears in the connecting client's advertised list (and falls back to `md5`, + or `md4` below protocol 30, for a client that advertises none), so a client + that advertises only weak digests can still force a weak negotiated digest. + If it is weaker than the configured + name, the connection is refused before the challenge is sent. The value is a + single digest name, for example: + + > auth digest = sha256 + + which requires `sha256` or `sha512` and refuses an `md5` or `md4` exchange. + + This guards against an *auth-digest downgrade*. A peer that sends no digest + list (any rsync before 3.2.0, including the openrsync that ships with macOS) + makes the daemon fall back to `md5` (or `md4` below protocol 30), and an + on-path attacker can rewrite the unauthenticated negotiation to force the same + weak choice. A captured challenge-response is far cheaper to brute-force + offline against a weak digest, so a site whose clients are all modern can + require a strong one. See the rsync `SECURITY.md` document for the full + threat model. + + There is **no default** -- the floor is off and the daemon accepts whatever + digest is negotiated, preserving compatibility with older clients. Enabling a + floor refuses every client that cannot offer at least that digest, notably any + rsync older than 3.2.0 (when the SHA digests were added) and the + macOS-bundled openrsync, which authenticate only with `md4`/`md5`. The SHA + digests require an rsync built with openssl at both ends; naming a digest this + build does not provide refuses all logins to the module (fail-closed). + + Like the other auth parameters, this may be set per module or, for a default + that applies to every module, in the global part of the config file. 0. `strict modes` This parameter determines whether or not the permissions on the secrets - file will be checked. If "strict modes" is true, then the secrets file - must not be readable by any user ID other than the one that the rsync - daemon is running under. If "strict modes" is false, the check is not - performed. The default is true. This parameter was added to accommodate - rsync running on the Windows operating system. + file will be checked. If "strict modes" is true (the default), the secrets + file is rejected if it is readable or writable by "other" (i.e. if any of + the other-read or other-write permission bits is set), and, when the daemon + is running as root, if the file is not owned by root. Group permissions and + the other-execute bit are not consulted, so modes such as 600 or 640 are + accepted while 644 (or any other-readable/-writable mode) is rejected. If + "strict modes" is false, no permission check is performed. This parameter + was added to accommodate rsync running on the Windows operating system. 0. `hosts allow` @@ -1064,6 +1160,42 @@ - `RSYNC_RAW_STATUS`: (post-xfer only) the raw exit value from **waitpid()**. + A `%VAR%` reference expanded into one of these commands is escaped for the + quoting context it appears in, which is correct for the single shell that + runs the command. It is not correct for a command that starts a SECOND + shell, such as `sh -c '... %RSYNC_USER_NAME% ...'`: the outer shell consumes + the escaping, and the inner one sees the value bare. A value of `touc?` + would then be glob-expanded against `/usr/bin/`, so the substitution is not + data any more -- it chooses the command. + + Because rsync cannot escape for an unknown number of shell passes, a value + carrying any character that a shell could act on is refused outright, and + the transfer is aborted with + + > refusing to run shell hook: %VAR% holds a shell metacharacter + + The refused characters are whitespace, the quoting and expansion characters + `'` `"` `` ` `` `$` `\`, the separators `;` `&` `|`, redirections `<` `>`, + parentheses, the pattern characters `*` `?` `[` `]`, `#`, `!`, `~`, `{` `}`, + and any control character. Some of those are harmless on their own and are + refused for what a *second* shell would do with them -- `!` negates in + command position, so a hook written as an access check can be turned from a + denial into an approval, and `~` is tilde-expanded. + + This applies to EVERY `%RSYNC_*%` value, including ones you supplied + yourself. In particular a module whose `path` contains any of them cannot + be interpolated into one of these commands: `path = /srv/My Backups` with a command mentioning + `%RSYNC_MODULE_PATH%` will refuse every transfer of that module, not just a + hostile one. The restriction is deliberate -- rsync cannot tell your space + from an attacker's once both are inside the same string, and `path` itself + may be built from a peer value such as `path = /home/%RSYNC_USER_NAME%`. + + If you need such a value in a hook, pass it through the environment instead + of interpolating it: the same names are exported to the command, so + `"$RSYNC_MODULE_PATH"` inside your script is unrestricted by this check. + Quote it there as shown -- rsync no longer has any say in how your script + splits the value. + Even though the commands can be associated with a particular module, they are run using the permissions of the user that started the daemon (not the module's uid/gid setting) without any chroot restrictions. @@ -1073,6 +1205,16 @@ **system()** call's default shell), and use RSYNC_NO_XFER_EXEC to disable both options completely. +0. `temp dir` + + Specifies a directory that rsync should use for temporary files created + during the transfer of updated files. If that directory is on a different + partition, after transfer file is being copied instead of unlinked. + + This parameter equals with `--temp-dir` option, so please consult rsync + manpage for further information. + + ## CONFIG DIRECTIVES There are currently two config directives available that allow a config file to @@ -1128,13 +1270,70 @@ stay in effect), and then include any `/etc/rsyncd.d/*.conf` files (defining modules without any global-value cross-talk). +## SECURITY + +An rsync daemon exposes part of your filesystem to network clients, and its +master process usually runs as root, so it should be configured defensively. +The following is a practical checklist; the project's `SECURITY.md` describes the +underlying threat model and the per-platform residuals. + +0. **Confine each module to its path.** + Leave "[use chroot](#)" enabled (the default attempts a chroot) so the + per-connection worker is jailed inside the module's "[path](#)". Where chroot + is unavailable rsync still resolves every transfer path so it cannot escape + the module through a symlinked parent, but a chroot is the stronger boundary + -- keep it on unless you have a specific reason not to. A `path = + /outer/./inner` chroots to `/outer` while serving `/inner` as the module root. + +0. **Drop privileges.** + Give each module a low-privilege "[uid](#)" and "[gid](#)" so a worker never + serves files as root; only the master process needs root (to chroot and bind + the port). Keep modules "[read only](#)" unless they must accept uploads, and + prefer "[numeric ids](#)" (or a "[name converter](#)") so a malicious file + list cannot map files to an unexpected local owner. + +0. **Restrict who can connect.** + Limit reachability with "[hosts allow](#)" / "[hosts deny](#)", bind the + daemon to a specific "[address](#)", and use a host firewall. A module + without "[auth users](#)" is reachable by anyone who can reach the port. + +0. **Authenticate with a strong secret and digest.** + Require login with "[auth users](#)" plus a "[secrets file](#)" that is + readable only by the daemon user (rsync refuses a too-open file unless + "[strict modes](#)" is disabled), and use a long, high-entropy secret. To + refuse the weak MD4/MD5 challenge digests that old clients can negotiate, set + a floor with "[auth digest](#)" (for example `auth digest = sha512`); see the + AUTHENTICATION STRENGTH section below. + +0. **Encrypt the connection.** + The daemon protocol authenticates but does **not** encrypt the data stream. + Do not expose a cleartext daemon to an untrusted network: front it with a TLS + proxy (see the SSL/TLS Daemon Setup section below) or run it over ssh. + +0. **Keep symlink handling safe.** + Leave "[munge symlinks](#)" enabled on any writable module (it is the default) + so an uploaded symlink cannot redirect a later operation outside the module. + Do **not** set "[insecure links](#) = yes" on a host shared with untrusted + local users: it disables the daemon's symlink-escape protections (re-opening + the symlink TOCTOU/escape vulnerabilities, including CVE-2026-53797 and + CVE-2026-53801) and is appropriate only on a single-tenant, fully isolated + host. + +0. **Refuse options you do not want.** + Use "[refuse options](#)" to reject client options a module should not allow. + ## AUTHENTICATION STRENGTH -The authentication protocol used in rsync is a 128 bit MD4 based challenge -response system. This is fairly weak protection, though (with at least one -brute-force hash-finding algorithm publicly available), so if you want really -top-quality security, then I recommend that you run rsync over ssh. (Yes, a -future version of rsync will switch over to a stronger hashing method.) +Daemon authentication is a challenge-response system in which the client +returns a digest of the shared secret and a server-chosen challenge. Modern +rsync (3.2.7 and later, built with openssl) negotiates the strongest digest both +sides support -- `sha512` by default -- but for backward compatibility it falls +back to `md5` (or `md4` below protocol 30) for a client that advertises no digest +list, and the digest negotiation is itself unauthenticated. A daemon that wants +to require a strong digest can set "[auth digest](#)". For the best protection, +run rsync over ssh or a verified TLS transport (`rsync-ssl`), which protects the +exchange at the transport layer, and use a high-entropy shared secret (which is +infeasible to brute-force regardless of the digest). Also note that the rsync daemon protocol does not currently provide any encryption of the data that is transferred over the connection. Only @@ -1275,5 +1474,7 @@ Rsync was originally written by Andrew Tridgell and Paul Mackerras. Many people from around the world have helped to maintain and improve it. +Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024. + Mailing lists for support and development are available at . diff -Nru rsync-3.4.1+ds1/rsyncsh.txt rsync-3.5.0+ds1/rsyncsh.txt --- rsync-3.4.1+ds1/rsyncsh.txt 2001-08-30 07:11:46.000000000 +0000 +++ rsync-3.5.0+ds1/rsyncsh.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -rsyncsh -Copyright (C) 2001 by Martin Pool - -This is a quick hack to build an interactive shell around rsync, the -same way we have the ftp, lftp and ncftp programs for the FTP -protocol. The key application for this is connecting to a public -rsync server, such as rsync.kernel.org, change down through and list -directories, and finally pull down the file you want. - -rsync is somewhat ill-at-ease as an interactive operation, since every -network connection is used to carry out exactly one operation. rsync -kind of "forks across the network" passing the options and filenames -to operate upon, and the connection is closed when the transfer is -complete. (This might be fixed in the future, either by adapting the -current protocol to allow chained operations over a single socket, or -by writing a new protocol that better supports interactive use.) - -So, rsyncsh runs a new rsync command and opens a new socket for every -(network-based) command you type. - -This has two consequences. Firstly, there is more command latency -than is really desirable. More seriously, if the connection cannot be -done automatically, because for example it uses SSH with a password, -then you will need to enter the password every time. We might even -fix this in the future, though, by having a way to automatically feed -the password to SSH if it's entered once. diff -Nru rsync-3.4.1+ds1/runtests.py rsync-3.5.0+ds1/runtests.py --- rsync-3.4.1+ds1/runtests.py 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/runtests.py 2026-08-06 04:28:03.000000000 +0000 @@ -0,0 +1,1024 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2001, 2002 by Martin Pool +# Copyright (C) 2003-2022 Wayne Davison +# Copyright (C) 2026 Andrew Tridgell +# +# Rewrite of runtests.sh in Python (runtests.sh is now deprecated). +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version +# 2 as published by the Free Software Foundation. + +"""rsync test runner. + +Invokes test scripts from testsuite/ and reports results. +Can be called by 'make check' or directly. + +Usage: + ./runtests.py [options] [TEST ...] + +Each TEST is a test name (e.g. 'delete') or glob pattern (e.g. 'xattr*'). +If no tests are specified, all tests are run. +""" + +import argparse +import concurrent.futures +import fnmatch +import glob +import math +import os +import signal +import subprocess +import sys +import threading +import time + +# Share the test exit-code enum with the test helpers. exitcodes.py lives in +# testsuite/ (next to this script); it has no import-time side effects. +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testsuite')) +from exitcodes import Exit + + +def _race_seconds(text): + """argparse type for --race-timeout: a finite, strictly positive number. + + A race test loops `while monotonic() < deadline`, so a budget of 0 (or a + negative, or a NaN, which fails every comparison) runs the body ZERO times + and the test reports PASS without ever exercising its oracle -- a silently + disarmed security test, which is worse than a slow one. Infinity would run + until the unrelated per-test timeout. The old max(RACE_TIMEOUT, 10.0) floor + used to make this unreachable; validating here restores that guarantee.""" + try: + secs = float(text) + except ValueError: + raise argparse.ArgumentTypeError(f'not a number: {text!r}') + if not math.isfinite(secs) or secs <= 0: + raise argparse.ArgumentTypeError( + f'must be a finite positive number of seconds, got {text!r}; ' + 'a zero/negative/NaN budget would make every race test pass ' + 'without running its race') + return secs + + +def parse_args(): + p = argparse.ArgumentParser(description='Run rsync test suite') + p.add_argument('tests', nargs='*', metavar='TEST', + help='Test names or patterns to run (default: all)') + p.add_argument('--exclude', default=None, metavar='LIST', + help='Comma-separated test names/globs to skip entirely: ' + 'they are not run and not reported as skipped. Useful ' + 'for tests that cannot work in a given build/CI ' + 'environment (e.g. a restricted buildd chroot). ' + 'Falls back to the RSYNC_EXCLUDE environment variable.') + p.add_argument('-j', '--parallel', type=int, default=1, metavar='N', + help='Run up to N tests in parallel (default: 1)') + p.add_argument('--valgrind', action='store_true', + help='Run rsync under valgrind (logs to per-process files)') + p.add_argument('--valgrind-opts', default='', metavar='OPTS', + help='Extra valgrind options (e.g. "--leak-check=full")') + p.add_argument('--preserve-scratch', action='store_true', + help='Keep scratch directories after tests complete') + p.add_argument('--log-level', type=int, default=1, metavar='N', + help='Verbosity level 1-10 (default: 1)') + p.add_argument('--always-log', action='store_true', + help='Show test logs even for passing tests') + p.add_argument('--stop-on-fail', action='store_true', + help='Stop after first test failure') + p.add_argument('--timing', action='store_true', + help='After the run, report each test\'s wall-clock time, ' + 'slowest first. With -j N the report also shows how ' + 'much of the run the slowest test alone accounts for.') + p.add_argument('--timeout', type=int, default=300, metavar='SECS', + help='Per-test timeout in seconds (default: 300)') + p.add_argument('--race-timeout', type=_race_seconds, default=None, metavar='SECS', + help='Budget (seconds) a TOCTOU symlink-race test may spend ' + 'trying to win its race before concluding. Overrides ' + 'every such test\'s own default (5-15s, the suite\'s ' + 'slowest tests: a race test always spends its whole ' + 'budget). Lowering it speeds the suite up but weakens ' + 'the oracle. Unset: each test keeps its default.') + p.add_argument('--rsync-bin', default=None, metavar='PATH', + help='Path to rsync binary (default: ./rsync)') + p.add_argument('--rsync-bin2', default=None, metavar='PATH', + help='Path to a second ("peer") rsync binary used for the ' + 'daemon side and remote-shell --rsync-path. Lets the ' + 'suite mix two rsync versions over the wire. Default: ' + 'same as --rsync-bin (no version mixing).') + p.add_argument('--tooldir', default=None, metavar='DIR', + help='Tool/build directory (default: cwd)') + p.add_argument('--srcdir', default=None, metavar='DIR', + help='Source directory (default: script directory)') + p.add_argument('--protocol', type=int, default=None, metavar='VER', + help='Force protocol version (adds --protocol=VER to rsync)') + p.add_argument('--expect-skipped', default=None, metavar='LIST', + help='Comma-separated list of expected-skipped tests. An ' + '@FILE entry reads a skip list (one test per line, ' + '"#" comments); relative paths resolve against srcdir ' + 'and several may be composed, e.g. ' + '@testsuite/skiplist/linux.txt,@testsuite/skiplist/proto29.txt. ' + 'A -NAME entry removes a name the rest of the spec ' + 'added, for a host that can really run a test its ' + 'platform list expects to skip.') + p.add_argument('--expect-result', default=None, metavar='FILE', + help='Path to an expected-outcome manifest (one ' + '" " per line). When ' + 'set, ONLY the tests listed in FILE are run, and each ' + "test's actual outcome is compared against its " + 'expected one; any mismatch (including an unexpected ' + 'pass) fails the run. Used for version-mixing CI.') + p.add_argument('--daemon-tests-only', action='store_true', + help='Run only the tests that can reach the daemon ' + 'transport. Intended for a --use-tcp pass that follows ' + 'a full default-transport run: the tests this drops ' + 'never call start_test_daemon(), so they cannot observe ' + '--use-tcp and would just repeat themselves. Disables ' + 'the expected-skip oracle (it describes a full run).') + p.add_argument('--use-tcp', action='store_true', + help='Run daemon tests against a real rsyncd bound to ' + '127.0.0.1 (non-default). The default is the secure ' + 'stdio-pipe transport, which opens no listening ' + 'socket; --use-tcp exposes a loopback port for the ' + 'duration of each daemon test.') + return p.parse_args() + + +def find_setfacl_nodef(scratchbase): + """Determine the setfacl command to remove default ACLs.""" + for cmd in [ + ['setacl', '-k', 'u::7,g::5,o:5', scratchbase], + ['setfacl', '-k', scratchbase], + ['setfacl', '-s', 'u::7,g::5,o:5', scratchbase], + ]: + try: + subprocess.run(cmd, capture_output=True, timeout=5) + return cmd[:2] if cmd[0] == 'setacl' else cmd[:2] + except (FileNotFoundError, subprocess.TimeoutExpired): + continue + try: + r = subprocess.run(['setfacl', '--help'], capture_output=True, text=True, timeout=5) + if '-k,' in r.stdout or '-k,' in r.stderr: + return ['setfacl', '-k'] + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + return None + + +def get_tls_args(config_h): + """Determine TLS_ARGS from config.h.""" + args = '' + try: + with open(config_h) as f: + text = f.read() + if '#define HAVE_LUTIMES 1' in text: + args += ' -l' + if '#undef CHOWN_MODIFIES_SYMLINK' in text: + args += ' -L' + except FileNotFoundError: + pass + return args.strip() + + +def read_shconfig(path): + """Read shell config variables from shconfig.""" + env = {} + try: + with open(path) as f: + for line in f: + line = line.strip() + if line.startswith('#') or line.startswith('export') or not line: + continue + if '=' in line: + k, _, v = line.partition('=') + env[k.strip()] = v.strip().strip('"') + except FileNotFoundError: + pass + return env + + +def get_testuser(): + """Determine the current test user.""" + for cmd in ['/usr/bin/whoami', '/usr/ucb/whoami', '/bin/whoami']: + if os.path.isfile(cmd): + try: + return subprocess.check_output([cmd], text=True).strip() + except subprocess.CalledProcessError: + pass + try: + return subprocess.check_output(['id', '-un'], text=True).strip() + except (FileNotFoundError, subprocess.CalledProcessError): + return os.environ.get('LOGNAME', os.environ.get('USER', 'UNKNOWN')) + + +def _move_aside(path): + """Rename an un-removable directory to a unique sibling so its name is free. + + A rename-storm symlink-race test can corrupt a directory on some filesystems + (OpenBSD FFS soft-updates can leave an "empty" dir that still reports + ENOTEMPTY/EPERM and only fsck clears). `rm -rf` then can't remove it, but + renaming the top dir aside succeeds even with a corrupted descendant, freeing + the original name for a clean scratchdir.""" + n = 0 + while os.path.exists(f"{path}.corrupt.{os.getpid()}.{n}"): + n += 1 + try: + os.rename(path, f"{path}.corrupt.{os.getpid()}.{n}") + except OSError: + pass + + +def prep_scratch(scratchdir, srcdir, tooldir, setfacl_nodef): + """Prepare a scratch directory for a test.""" + if os.path.isdir(scratchdir): + subprocess.run(['chmod', '-R', 'u+rwX', scratchdir], capture_output=True) + subprocess.run(['rm', '-rf', scratchdir], capture_output=True) + if os.path.isdir(scratchdir): + _move_aside(scratchdir) # rm -rf left corrupted debris; don't inherit it + os.makedirs(scratchdir, exist_ok=True) + if setfacl_nodef: + subprocess.run(setfacl_nodef + [scratchdir], capture_output=True) + try: + os.chmod(scratchdir, os.stat(scratchdir).st_mode & ~0o2000) # clear setgid + except OSError: + pass + src_link = os.path.join(scratchdir, 'src') + if not os.path.exists(src_link): + if os.path.isabs(srcdir): + os.symlink(srcdir, src_link) + else: + os.symlink(os.path.join(tooldir, srcdir), src_link) + + +# Python tests are identified by a positive "_test.py" suffix so that +# helper modules (e.g. rsyncfns.py) sit in testsuite/ without being mistaken +# for tests. +_PY_TEST_SUFFIX = '_test.py' + + +def _is_test_path(path): + return os.path.basename(path).endswith(_PY_TEST_SUFFIX) + + +def _testbase(path): + """Strip the test extension to get the canonical test name.""" + base = os.path.basename(path) + if base.endswith(_PY_TEST_SUFFIX): + return base[:-len(_PY_TEST_SUFFIX)] + return base + + +def collect_tests(suitedir, patterns): + """Collect test scripts (_test.py) matching the given patterns.""" + if not patterns: + candidates = glob.glob(os.path.join(suitedir, '*' + _PY_TEST_SUFFIX)) + tests = sorted(p for p in candidates if _is_test_path(p)) + else: + seen = set() + tests = [] + for pat in patterns: + # Accept either bare name ("mkpath"), explicit extension, or glob. + if pat.endswith('.py'): + pats = [pat] + else: + pats = [pat + _PY_TEST_SUFFIX] + for p in pats: + for m in sorted(glob.glob(os.path.join(suitedir, p))): + if _is_test_path(m) and m not in seen: + seen.add(m) + tests.append(m) + return tests + + +# Tokens through which a test can reach the daemon transport. --use-tcp works by +# setting RSYNC_TEST_USE_TCP, which is read in exactly one place (rsyncfns +# USE_TCP) and acted on in exactly one function (start_test_daemon): a test whose +# source mentions none of these never gets there, so it behaves identically with +# and without --use-tcp and running it a second time under TCP buys no coverage. +# +# The list is the closure of every rsyncfns helper that reaches USE_TCP, +# start_rsyncd or claim_ports, plus the helper modules that open a daemon +# connection themselves and the bare literals a test might use directly. It is +# deliberately over-broad: a false positive only costs runtime, while a false +# negative would silently drop real coverage. +_DAEMON_API = ( + 'USE_TCP', 'require_tcp', 'start_test_daemon', 'start_rsyncd', + 'claim_ports', 'claim_free_port', 'setup_chroot_inner', + 'stdio_daemon', 'rsync_proto', 'DaemonClient', + 'rsync://', '--daemon', 'rsyncd', +) + + +def select_daemon_tests(tests): + """Split `tests` into (daemon-transport tests, the rest). + + Used by --daemon-tests-only so a TCP pass need not re-run the whole suite. + A test we cannot read is kept, not dropped -- the failure mode of this + filter must always be "ran too much".""" + keep, dropped = [], [] + for path in tests: + try: + with open(path, errors='replace') as f: + text = f.read() + except OSError: + keep.append(path) + continue + (keep if any(tok in text for tok in _DAEMON_API) else dropped).append(path) + return keep, dropped + + +_VALID_OUTCOMES = ('pass', 'skip', 'fail', 'xfail') + + +def parse_expect_result(path): + """Parse an expected-outcome manifest into {testbase: outcome}. + + One " " entry per line; '#' comments and blank lines + are ignored. outcome is one of pass|skip|fail|xfail. The set of listed + tests doubles as the run set (see main()). Exits 2 on a malformed file. + """ + expect = {} + with open(path) as f: + for lineno, raw in enumerate(f, 1): + line = raw.split('#', 1)[0].strip() + if not line: + continue + fields = line.split() + if len(fields) != 2 or fields[1] not in _VALID_OUTCOMES: + sys.stderr.write( + f"{path}:{lineno}: expected ' " + f"<{'|'.join(_VALID_OUTCOMES)}>', got: {raw.rstrip()}\n" + ) + sys.exit(Exit.ERROR) + expect[fields[0]] = fields[1] + return expect + + +def expand_skip_spec(spec, srcdir, suitedir): + """Expand an RSYNC_EXPECT_SKIPPED spec into a normalised csv. + + The spec is a comma-separated list of test names, '@FILE' skip-list + references, and '-name' removals. A skip-list file holds one test name per line ('#' starts a + comment; blank lines are ignored), which is what keeps two branches from + colliding: adding a test edits one line of one file rather than a shared + 3 KB csv. Several may be composed, e.g. + RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/linux.txt,@.../proto29.txt + Relative paths resolve against srcdir (not the cwd) so out-of-tree builds + and `make installcheck` work. A '-name' entry removes a name the rest of + the spec added, for a host that can genuinely run a test its platform list + expects to skip; it is applied last, and must actually remove something. + + Entries must name a real test, and each file must be non-empty, sorted and + free of duplicates: unsorted files defeat the point (everyone appends to + the same last line), and a stale name would otherwise fail as a skip + mismatch far from its cause. Exits 2 on any of those -- nothing malformed + may quietly shrink the expected set, which would disarm the oracle. + """ + def die(msg): + sys.stderr.write(msg + '\n') + sys.exit(Exit.ERROR) + + # An entirely empty spec is the legitimate "expect no skips at all". An + # empty entry *within* a spec is not: it is what an unset shell variable + # expands to, and silently dropping it would quietly shrink the expected + # set. + if not spec.strip(): + return '' + + names = [] + drop = [] + for tok in (t.strip() for t in spec.split(',')): + if not tok: + die('RSYNC_EXPECT_SKIPPED: empty entry (an unset variable?): ' + f'{spec!r}') + if tok.startswith('-'): + # '-name' removes a name a composed list added, for a host that + # really can run a test its platform list expects to skip (e.g. a + # scratch dir on a second filesystem makes a cross-device copy + # work). Subtraction cannot be done by whoever composes the spec, + # because the name lives inside an @FILE that is only expanded + # here. Applied after every addition, so order does not matter. + drop.append((tok[1:], tok)) + continue + if not tok.startswith('@'): + names.append((tok, 'RSYNC_EXPECT_SKIPPED')) + continue + path = tok[1:] + if not os.path.isabs(path): + path = os.path.join(srcdir, path) + try: + with open(path) as f: + lines = f.readlines() + except (OSError, UnicodeDecodeError) as e: + die(f'{tok}: cannot read skip list: {e}') + prev = None + found = 0 + for lineno, raw in enumerate(lines, 1): + name = raw.split('#', 1)[0].strip() + if not name: + continue + where = f'{path}:{lineno}' + if len(name.split()) != 1: + die(f'{where}: expected one test name per line, got: {raw.rstrip()}') + if prev is not None and name <= prev: + die(f'{where}: skip lists must be sorted and duplicate-free ' + f'({name!r} follows {prev!r})') + prev = name + found += 1 + names.append((name, where)) + # A truncated or emptied list must not read as "expect no skips". + if not found: + die(f'{path}: skip list contains no test names') + + seen = {} + for name, where in names: + if name in seen: + continue + seen[name] = where + # A plain test name: no path, and no comma (which the expanded csv, + # and the summary the fleet parses back, use as the separator). + if ',' in name or '/' in name or os.sep in name or name in ('.', '..'): + die(f'{where}: not a test name: {name!r}') + if not os.path.isfile(os.path.join(suitedir, name + '_test.py')): + die(f'{where}: no such test: {name}') + for name, tok in drop: + # Every removal must remove something. A name the spec never added is + # stale (the list stopped expecting that skip, or it is misspelled), and + # a repeated removal is the same no-op written twice. Shrinking the + # expected set is precisely what must not happen quietly, so neither is + # allowed to sit in a config unnoticed. + if name not in seen: + die(f'RSYNC_EXPECT_SKIPPED: {tok!r} removes a name that nothing ' + f'added: {name}') + del seen[name] + return ','.join(sorted(seen)) + + +def read_backport_exclude(tooldir, suitedir): + """Test names from the BUILD tree's testsuite/skiplist/backport.txt. + + A stable-backport branch runs a newer suite than its own code, and this + file names the tests that base cannot run. It lives in the tree being + built, not the suite tree, so it cannot be an @FILE in the expect-skipped + spec -- those resolve against srcdir. + """ + path = os.path.join(tooldir, 'testsuite', 'skiplist', 'backport.txt') + if not os.path.isfile(path): + return set() + names = set() + with open(path) as f: + for lineno, raw in enumerate(f, 1): + name = raw.split('#', 1)[0].strip() + if not name: + continue + where = f'{path}:{lineno}' + if len(name.split()) != 1: + sys.stderr.write(f'{where}: expected one test name per line\n') + sys.exit(Exit.ERROR) + # A stale name here would silently exclude nothing, which is the + # failure this file exists to prevent. + if not os.path.isfile(os.path.join(suitedir, name + '_test.py')): + sys.stderr.write(f'{where}: no such test: {name}\n') + sys.exit(Exit.ERROR) + names.add(name) + return names + + +_TIMING_TOP = 25 + + +def print_timing_report(durations, outcomes, run_wall, parallel): + """Report per-test wall-clock, slowest first (--timing). + + With -j N the run cannot finish sooner than its slowest single test, so the + tail matters as much as the total: a 300s test pins the whole suite to 300s + no matter how many workers there are. The footer gives both bounds -- the + serial sum (what one worker would take) and that floor -- so it is obvious + whether a slow run wants more parallelism or a faster individual test.""" + if not durations: + return + ranked = sorted(durations.items(), key=lambda kv: kv[1], reverse=True) + total = sum(durations.values()) + print(f'----- slowest tests (of {len(ranked)}, wall-clock each):') + for name, secs in ranked[:_TIMING_TOP]: + print(f' {secs:7.1f}s {name:<32} {outcomes.get(name, "?")}') + print(f' serial sum {total:.0f}s over {len(ranked)} tests; ' + f'run took {run_wall:.0f}s with -j{parallel}') + slowest, slowest_secs = ranked[0] + if parallel > 1: + # The floor: even with unlimited workers the suite cannot beat its + # longest single test. + print(f' floor {slowest_secs:.0f}s ({slowest}) = ' + f'{100.0 * slowest_secs / run_wall:.0f}% of this run; ' + f'ideal at -j{parallel} is {total / parallel:.0f}s') + + +def outcome_of(result): + """Map a per-test exit code to an outcome string.""" + if result == Exit.PASS: + return 'pass' + if result == Exit.SKIP: + return 'skip' + if result == Exit.XFAIL: + return 'xfail' + return 'fail' + + +def build_rsync_cmd(rsync_bin, args, scratchbase): + """Build the RSYNC command string for tests.""" + parts = [] + if args.valgrind: + # Logs go in a world-writable+sticky subdir so that rsync children + # which drop privileges (the setpriv cap-drop in partial_nowrite, a + # daemon dropping to the module's uid) can still create their log file + # even when scratchbase itself is root-owned. + vgdir = os.path.join(scratchbase, 'valgrind-logs') + os.makedirs(vgdir, exist_ok=True) + os.chmod(vgdir, 0o1777) + vlog = os.path.join(vgdir, 'valgrind.%p.log') + vopts = f'--log-file={vlog}' + supp = os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'testsuite', 'valgrind.supp') + if os.path.exists(supp): + vopts += f' --suppressions={supp}' + if args.valgrind_opts: + vopts += ' ' + args.valgrind_opts + parts.append(f'valgrind {vopts}') + parts.append(rsync_bin) + if args.protocol is not None: + parts.append(f'--protocol={args.protocol}') + return ' '.join(parts) + + +class TestResult: + """Result of a single test execution.""" + __slots__ = ('testbase', 'result', 'output', 'skipped_reason', 'duration') + + def __init__(self, testbase, result, output='', skipped_reason='', + duration=0.0): + self.testbase = testbase + self.result = result + self.output = output + self.skipped_reason = skipped_reason + self.duration = duration + + +def run_one_test(testscript, testbase, scratchdir, base_env, timeout, + srcdir, tooldir, setfacl_nodef, always_log): + """Run a single test. Returns a TestResult. + + This function is safe to call from multiple threads — it uses only + per-test state (unique scratchdir, copy of env). + """ + started = time.monotonic() + prep_scratch(scratchdir, srcdir, tooldir, setfacl_nodef) + + env = base_env.copy() + env['scratchdir'] = scratchdir + + # Dispatch by extension: shell tests via /bin/sh -e, Python tests via + # the same python3 that's running this runner. + if testscript.endswith('.py'): + cmd = [sys.executable, testscript] + else: + cmd = ['sh', '-e', testscript] + + logfile = os.path.join(scratchdir, 'test.log') + with open(logfile, 'w') as log: + # start_new_session: run the test driver as its own session/group leader + # so the daemon, clients and flipper it spawns inherit that group. A + # timeout then killpg's the whole tree (not just the driver), and the + # lock-file sweep can reap a SIGKILLed run's stranded group the same way. + proc = subprocess.Popen( + cmd, + stdout=log, stderr=subprocess.STDOUT, + env=env, cwd=env.get('TOOLDIR', '.'), + start_new_session=True, + ) + try: + result = proc.wait(timeout=timeout) + except subprocess.TimeoutExpired: + # Reap the whole session group, but only if the driver really is its + # own group leader (start_new_session took) and that group isn't ours + # -- killpg of our own group would take down the runner. + try: + pgid = os.getpgid(proc.pid) + except OSError: + pgid = -1 + if pgid == proc.pid and pgid != os.getpgrp(): + try: + os.killpg(pgid, signal.SIGKILL) + except OSError: + proc.kill() + else: + proc.kill() + proc.wait() + result = 1 + log.write(f"\nTIMEOUT: test took over {timeout} seconds\n") + + # Build output text + output_parts = [] + + show_log = always_log or (result not in (Exit.PASS, Exit.SKIP, Exit.XFAIL)) + if show_log: + output_parts.append(f'----- {testbase} log follows') + try: + with open(logfile) as f: + output_parts.append(f.read().rstrip()) + except FileNotFoundError: + pass + output_parts.append(f'----- {testbase} log ends') + rsyncd_log = os.path.join(scratchdir, 'rsyncd.log') + if os.path.isfile(rsyncd_log): + output_parts.append(f'----- {testbase} rsyncd.log follows') + with open(rsyncd_log) as f: + output_parts.append(f.read().rstrip()) + output_parts.append(f'----- {testbase} rsyncd.log ends') + + skipped_reason = '' + if result == Exit.PASS: + output_parts.append(f'PASS {testbase}') + elif result == Exit.SKIP: + whyfile = os.path.join(scratchdir, 'whyskipped') + try: + with open(whyfile) as f: + skipped_reason = f.read().strip() + except FileNotFoundError: + pass + output_parts.append(f'SKIP {testbase} ({skipped_reason})') + elif result == Exit.XFAIL: + output_parts.append(f'XFAIL {testbase}') + else: + output_parts.append(f'FAIL {testbase}') + + return TestResult(testbase, result, '\n'.join(output_parts), skipped_reason, + time.monotonic() - started) + + +# Lock for serializing output in parallel mode +_print_lock = threading.Lock() + + +def main(): + args = parse_args() + + # Also accept legacy environment variables + if args.preserve_scratch or os.environ.get('preserve_scratch') == 'yes': + args.preserve_scratch = True + if args.log_level == 1: + args.log_level = int(os.environ.get('loglevel', '1')) + if args.expect_skipped is None: + args.expect_skipped = os.environ.get('RSYNC_EXPECT_SKIPPED', 'IGNORE') + if args.exclude is None: + args.exclude = os.environ.get('RSYNC_EXCLUDE', '') + if os.environ.get('whichtests'): + args.tests = [os.environ['whichtests']] + + # Determine directories + tooldir = args.tooldir or os.environ.get('TOOLDIR') or os.getcwd() + script_path = os.path.dirname(os.path.abspath(__file__)) + srcdir = args.srcdir or script_path + if not srcdir or srcdir == '.': + srcdir = tooldir + rsync_bin = args.rsync_bin or os.environ.get('rsync_bin') or os.path.join(tooldir, 'rsync') + # Absolutize: tests run with subprocess(cwd=TOOLDIR) below, so a relative + # argv[0] would re-resolve against TOOLDIR rather than the runner's + # invocation cwd, breaking --rsync-bin=../foo/rsync forms. abspath() + # captures os.getcwd() now, which is what the operator intended. + if rsync_bin and not os.path.isabs(rsync_bin): + rsync_bin = os.path.abspath(rsync_bin) + + # Optional second ("peer") binary for the daemon / remote-shell side, so a + # run can mix two rsync versions. Defaults to rsync_bin -> no mixing. + rsync_bin2 = args.rsync_bin2 or os.environ.get('rsync_bin2') or rsync_bin + if rsync_bin2 and not os.path.isabs(rsync_bin2): + rsync_bin2 = os.path.abspath(rsync_bin2) + + suitedir = os.path.join(srcdir, 'testsuite') + # A backport tree excludes what its base cannot run. Those tests never + # run, so they must also drop out of the expected-skip set -- otherwise the + # oracle demands a skip from a test that was never started. + backport_excl = read_backport_exclude(tooldir, suitedir) + if backport_excl: + args.exclude = ','.join(x for x in (args.exclude, + ','.join(sorted(backport_excl))) if x) + if args.expect_skipped != 'IGNORE': + args.expect_skipped = expand_skip_spec(args.expect_skipped, srcdir, suitedir) + if backport_excl: + args.expect_skipped = ','.join(n for n in args.expect_skipped.split(',') + if n and n not in backport_excl) + scratchbase = os.path.join(os.environ.get('scratchbase', tooldir), 'testtmp') + os.makedirs(scratchbase, exist_ok=True) + + shconfig = read_shconfig(os.path.join(tooldir, 'shconfig')) + tls_args = get_tls_args(os.path.join(tooldir, 'config.h')) + setfacl_nodef = find_setfacl_nodef(scratchbase) + rsync_cmd = build_rsync_cmd(rsync_bin, args, scratchbase) + rsync_peer_cmd = build_rsync_cmd(rsync_bin2, args, scratchbase) + + if not os.path.isfile(rsync_bin): + sys.stderr.write(f"rsync_bin {rsync_bin} is not a file\n") + sys.exit(Exit.ERROR) + if not os.path.isfile(rsync_bin2): + sys.stderr.write(f"rsync_bin2 {rsync_bin2} is not a file\n") + sys.exit(Exit.ERROR) + if not os.path.isdir(srcdir): + sys.stderr.write(f"srcdir {srcdir} is not a directory\n") + sys.exit(Exit.ERROR) + + # Helper programs the test scripts invoke directly. Missing any of these + # would cause many tests to fail with confusing "not found" errors, so + # check up front and point the user at the make target that builds them. + required_helpers = ['tls', 'trimslash', 't_unsafe', 't_chmod_secure', + 't_secure_relpath', + 'wildtest', 'getgroups', 'getfsdev'] + missing = [h for h in required_helpers + if not os.path.isfile(os.path.join(tooldir, h))] + if missing: + sys.stderr.write( + f"runtests.py: missing test helper program(s) in {tooldir}: " + f"{', '.join(missing)}\n" + f"Build them with: make {' '.join(missing)}\n" + f"or run the full test target: make check\n" + ) + sys.exit(Exit.ERROR) + + testuser = get_testuser() + + # Print header + print('=' * 60) + print(f'{sys.argv[0]} running in {tooldir}') + print(f' rsync_bin={rsync_cmd}') + if rsync_peer_cmd != rsync_cmd: + print(f' rsync_peer={rsync_peer_cmd}') + print(f' srcdir={srcdir}') + print(f' TLS_ARGS={tls_args}') + print(f' testuser={testuser}') + print(f' os={subprocess.check_output(["uname", "-a"], text=True).strip()}') + print(f' preserve_scratch={"yes" if args.preserve_scratch else "no"}') + if args.valgrind: + print(f' valgrind=enabled (logs in valgrind-logs/valgrind.*.log)') + if args.parallel > 1: + print(f' parallel={args.parallel}') + print(f' daemon_transport={"tcp (loopback)" if args.use_tcp else "pipe (secure default)"}') + print(f' scratchbase={scratchbase}') + + # Build base environment for test scripts + path = os.environ.get('PATH', '') + if os.path.isdir('/usr/xpg4/bin'): + path = '/usr/xpg4/bin:' + path + + # Make the testsuite/ directory importable so Python tests can `import rsyncfns`. + pythonpath = suitedir + if os.environ.get('PYTHONPATH'): + pythonpath = suitedir + os.pathsep + os.environ['PYTHONPATH'] + + base_env = os.environ.copy() + base_env.update({ + 'PATH': path, + 'POSIXLY_CORRECT': '1', + 'TOOLDIR': tooldir, + 'srcdir': srcdir, + 'RSYNC': rsync_cmd, + 'RSYNC_PEER': rsync_peer_cmd, + 'TLS_ARGS': tls_args, + 'RUNSHFLAGS': '-e', + 'scratchbase': scratchbase, + 'suitedir': suitedir, + 'TESTRUN_TIMEOUT': str(args.timeout), + 'HOME': scratchbase, + 'PYTHONPATH': pythonpath, + }) + if args.use_tcp: + # Opt-in: daemon tests start a real rsyncd on a claimed loopback port. + # Default (unset) keeps the secure stdio-pipe transport. + base_env['RSYNC_TEST_USE_TCP'] = '1' + if args.race_timeout is not None: + # Only exported when the operator actually passed --race-timeout: its + # mere presence is what tells a race test to override its own default. + base_env['race_timeout'] = str(args.race_timeout) + else: + # A stale value inherited from the environment would silently override + # every test's default; the flag is the only way to set this. + base_env.pop('race_timeout', None) + for k, v in shconfig.items(): + if v: + base_env[k] = v + if setfacl_nodef: + base_env['setfacl_nodef'] = ' '.join(setfacl_nodef) + else: + base_env['setfacl_nodef'] = 'true' + if args.log_level > 8: + base_env['RUNSHFLAGS'] = '-e -x' + + # Collect tests + tests = collect_tests(suitedir, args.tests) + full_run = len(args.tests) == 0 + + # Drop excluded tests entirely (matched by basename against name/glob). + excl = [e.strip() for e in args.exclude.split(',') if e.strip()] + if excl: + before = len(tests) + tests = [t for t in tests + if not any(fnmatch.fnmatch(_testbase(t), pat) for pat in excl)] + if before != len(tests): + print(f"Excluding {before - len(tests)} test(s) matching: " + f"{', '.join(excl)}") + + # Narrow to the daemon-transport tests. The dropped count is always printed: + # a pass that silently ran a third of the suite would read in the report as + # if it had run all of it. + if args.daemon_tests_only: + tests, dropped = select_daemon_tests(tests) + print(f"Daemon-transport tests only: running {len(tests)}, skipping " + f"{len(dropped)} test(s) that cannot observe the transport") + # The expected-skip list describes a full run, so it cannot be enforced + # against a subset -- same rule as naming tests explicitly. + full_run = False + + # An expected-result manifest defines BOTH the run set (its keys) and the + # expected per-test outcome (its values). Used for version-mixing runs. + expect = parse_expect_result(args.expect_result) if args.expect_result else None + if expect is not None: + have = {_testbase(t) for t in tests} + unknown = sorted(k for k in expect if k not in have) + if unknown: + sys.stderr.write( + "runtests.py: --expect-result lists test(s) with no matching " + f"test file (ignored): {', '.join(unknown)}\n" + ) + tests = [t for t in tests if _testbase(t) in expect] + full_run = False + + def _cls(outcome): + """Equivalence class for outcome comparison: fail and xfail both just + mean 'broke', so a manifest 'fail' matches an actual fail OR xfail.""" + return 'broken' if outcome in ('fail', 'xfail') else outcome + + def mismatch(testbase, actual): + """True if actual outcome disagrees with the manifest expectation.""" + return expect is not None and _cls(expect[testbase]) != _cls(actual) + + # Record test order for consistent skipped-list output + test_order = {_testbase(t): i for i, t in enumerate(tests)} + + passed = 0 + failed = 0 + skipped = 0 + xfailed = 0 + skipped_list = [] + outcomes = {} # testbase -> actual outcome string ('pass'/'skip'/'fail'/'xfail') + durations = {} # testbase -> wall-clock seconds (for --timing) + + def process_result(tr): + """Process a TestResult and update counters. Returns True if the test + should count as a failure for --stop-on-fail purposes.""" + nonlocal passed, failed, skipped, xfailed + with _print_lock: + if tr.output: + print(tr.output) + scratchdir = os.path.join(scratchbase, tr.testbase) + oc = outcome_of(tr.result) + outcomes[tr.testbase] = oc + durations[tr.testbase] = tr.duration + if tr.result == Exit.PASS: + passed += 1 + elif tr.result == Exit.SKIP: + skipped_list.append(tr.testbase) + skipped += 1 + elif tr.result == Exit.XFAIL: + # XFAIL: an expected failure (a known, documented residual the test + # asserts against). Reported distinctly but does NOT fail the suite; + # when the underlying issue is fixed the test returns 0 instead. + xfailed += 1 + else: + failed += 1 + if tr.result in (Exit.PASS, Exit.SKIP, Exit.XFAIL) and not args.preserve_scratch \ + and os.path.isdir(scratchdir): + subprocess.run(['rm', '-rf', scratchdir], capture_output=True) + # With a manifest, only a mismatch is a "failure" (an expected fail is + # fine); without one, any non-pass/non-skip/non-xfail result is a failure. + if expect is not None: + return mismatch(tr.testbase, oc) + return tr.result not in (Exit.PASS, Exit.SKIP, Exit.XFAIL) + + run_started = time.monotonic() + + if args.parallel > 1: + # Parallel execution + with concurrent.futures.ThreadPoolExecutor(max_workers=args.parallel) as executor: + futures = {} + for testscript in tests: + testbase = _testbase(testscript) + scratchdir = os.path.join(scratchbase, testbase) + timeout = 600 if ('hardlinks' in testbase or testbase == 'variety') else args.timeout + f = executor.submit( + run_one_test, testscript, testbase, scratchdir, + base_env, timeout, srcdir, tooldir, setfacl_nodef, + args.always_log + ) + futures[f] = testbase + + for f in concurrent.futures.as_completed(futures): + tr = f.result() + is_fail = process_result(tr) + if is_fail and args.stop_on_fail: + # Cancel pending futures + for pending in futures: + pending.cancel() + break + else: + # Sequential execution + for testscript in tests: + testbase = _testbase(testscript) + scratchdir = os.path.join(scratchbase, testbase) + timeout = 600 if ('hardlinks' in testbase or testbase == 'variety') else args.timeout + tr = run_one_test( + testscript, testbase, scratchdir, + base_env, timeout, srcdir, tooldir, setfacl_nodef, + args.always_log + ) + is_fail = process_result(tr) + if is_fail and args.stop_on_fail: + break + + run_wall = time.monotonic() - run_started + + # Check valgrind logs for errors + vg_errors = 0 + if args.valgrind: + for vlog in sorted(glob.glob(os.path.join(scratchbase, 'valgrind-logs', 'valgrind.*.log'))): + try: + with open(vlog) as f: + content = f.read() + for line in content.splitlines(): + if 'ERROR SUMMARY:' in line and 'ERROR SUMMARY: 0 errors' not in line: + vg_errors += 1 + print(f'----- valgrind errors in {os.path.basename(vlog)}:') + print(content) + break + except FileNotFoundError: + pass + + # Summary + print('-' * 60) + print('----- overall results:') + print(f' {passed} passed') + if failed > 0: + print(f' {failed} failed') + if xfailed > 0: + print(f' {xfailed} xfailed (expected)') + if skipped > 0: + print(f' {skipped} skipped') + if vg_errors > 0: + print(f' {vg_errors} valgrind error(s) found (see logs in {os.path.join(scratchbase, "valgrind-logs")})') + + if args.timing: + print_timing_report(durations, outcomes, run_wall, args.parallel) + + if expect is not None: + # Version-mixing mode: the run is judged purely on whether each test's + # actual outcome matched its manifest expectation. An expected 'fail' + # is fine; an UNEXPECTED pass (xpass) or any other divergence is not. + mismatches = [] + for tb in sorted(expect, key=lambda x: test_order.get(x, 1 << 30)): + actual = outcomes.get(tb, 'notrun') + if actual == 'notrun' or mismatch(tb, actual): + mismatches.append((tb, expect[tb], actual)) + if mismatches: + print('----- expected-result mismatches:') + for tb, want, got in mismatches: + tag = ' (xpass)' if _cls(want) == 'broken' and got == 'pass' else '' + print(f' {tb}: expected {want}, got {got}{tag}') + print('-' * 60) + exit_code = len(mismatches) + vg_errors + print(f'overall result is {exit_code}') + sys.exit(exit_code) + + skipped_str = ','.join(sorted(skipped_list, key=lambda x: test_order.get(x, 0))) + if full_run and args.expect_skipped != 'IGNORE': + print('----- skipped results:') + print(f' expected: {args.expect_skipped}') + print(f' got: {skipped_str}') + else: + skipped_str = '' + args.expect_skipped = '' + + print('-' * 60) + + exit_code = failed + vg_errors + if exit_code == 0: + # Compare the skipped set order-insensitively: which tests skipped is + # what matters, not the order runtests happened to collect them in + # (that order is just sorted filenames -- an easy thing to get subtly + # wrong when maintaining the per-platform expected lists). + got = set(s for s in skipped_str.split(',') if s) + want = set(s for s in args.expect_skipped.split(',') if s) + if got != want: + exit_code = 1 + + print(f'overall result is {exit_code}') + sys.exit(exit_code) + + +if __name__ == '__main__': + main() diff -Nru rsync-3.4.1+ds1/runtests.sh rsync-3.5.0+ds1/runtests.sh --- rsync-3.4.1+ds1/runtests.sh 2022-01-16 01:21:01.000000000 +0000 +++ rsync-3.5.0+ds1/runtests.sh 1970-01-01 00:00:00.000000000 +0000 @@ -1,360 +0,0 @@ -#! /bin/sh - -# Copyright (C) 2001, 2002 by 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 version -# 2 as published by the Free Software Foundation. -# -# 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -# ------------------------------------------------------------------------- - -# rsync top-level test script -- this invokes all the other more -# detailed tests in order. This script can either be called by `make -# check' or `make installcheck'. `check' runs against the copies of -# the program and other files in the build directory, and -# `installcheck' against the installed copy of the program. - -# It can also be called on a single test file using a run like this: -# -# preserve_scratch=yes whichtests=itemize.test ./runtests.sh - -# In either case we need to also be able to find the source directory, -# since we read test scripts and possibly other information from -# there. - -# Whenever possible, informational messages are written to stdout and -# error messages to stderr. They're separated out by the build farm -# display scripts. - -# According to the GNU autoconf manual, the only valid place to set up -# directory locations is through Make, since users are allowed to (try -# to) change their mind on the Make command line. So, Make has to -# pass in all the values we need. - -# For other configured settings we read ./config.sh, which tells us -# about shell commands on this machine and similar things. - -# rsync_bin gives the location of the rsync binary. This is either -# builddir/rsync if we're testing an uninstalled copy, or -# install_prefix/bin/rsync if we're testing an installed copy. On the -# build farm rsync will be installed, but into a scratch /usr. - -# srcdir gives the location of the source tree, which lets us find the -# build scripts. At the moment we assume we are invoked from the -# source directory. - -# This script must be invoked from the build directory. - -# A scratch directory, 'testtmp', is used in the build directory to -# hold per-test subdirectories. - -# This script also uses the $loglevel environment variable. 1 is the -# default value, and 10 the most verbose. You can set this from the -# Make command line. It's also set by the build farm to give more -# detail for failing builds. - -# ------------------------------------------------------------------------- - -# NOTES FOR TEST CASES: - -# Each test case runs in its own shell. - -# Exit codes from tests: - -# 1 tests failed -# 2 error in starting tests -# 77 this test skipped (random value unlikely to happen by chance, same as -# automake) - -# HOWEVER, the overall exit code to the farm is different: we return -# the *number of tests that failed*, so that it will show up nicely in -# the overall summary. - -# rsync.fns contains some general setup functions and definitions. - -# ------------------------------------------------------------------------- - -# NOTES ON PORTABILITY: - -# Both this script and the Makefile have to be pretty conservative -# about which Unix features they use. - -# We cannot count on Make exporting variables to commands, unless -# they're explicitly given on the command line. - -# Also, we can't count on 'cp -a' or 'mkdir -p', although they're -# pretty handy (see function makepath for the latter). - -# I think some of the GNU documentation suggests that we shouldn't -# rely on shell functions. However, the Bash manual seems to say that -# they're in POSIX 1003.2, and since the build farm relies on them -# they're probably working on most machines we really care about. - -# You cannot use "function foo {" syntax, but must instead say "foo() -# {", or it breaks on FreeBSD. - -# BSD machines tend not to have "head" or "seq". - -# You cannot do "export VAR=VALUE" all on one line; the export must be -# separate from the assignment. (SCO SysV) - -# Don't rely on grep -q, as that doesn't work everywhere -- just redirect -# stdout to /dev/null to keep it quiet. - -# ------------------------------------------------------------------------- - -# STILL TO DO: - -# We need a good protection against tests that hang indefinitely. -# Perhaps some combination of starting them in the background, wait, -# and kill? - -# Perhaps we need a common way to cleanup tests. At the moment just -# clobbering the directory when we're done should be enough. - -# If any of the targets fail, then (GNU?) Make returns 2, instead of -# the return code from the failing command. This is fine, but it -# means that the build farm just shows "2" for failed tests, not the -# number of tests that actually failed. For more details we might -# need to grovel through the log files to find a line saying how many -# failed. - - -set -e - -. "./shconfig" - -RUNSHFLAGS='-e' -export RUNSHFLAGS - -# for Solaris -if [ -d /usr/xpg4/bin ]; then - PATH="/usr/xpg4/bin/:$PATH" - export PATH -fi - -if [ "x$loglevel" != x ] && [ "$loglevel" -gt 8 ]; then - if set -x; then - # If it doesn't work the first time, don't keep trying. - RUNSHFLAGS="$RUNSHFLAGS -x" - fi -fi - -POSIXLY_CORRECT=1 -if test x"$TOOLDIR" = x; then - TOOLDIR=`pwd` -fi -srcdir=`dirname $0` -if test x"$srcdir" = x || test x"$srcdir" = x.; then - srcdir="$TOOLDIR" -fi -if test x"$rsync_bin" = x; then - rsync_bin="$TOOLDIR/rsync" -fi - -# This allows the user to specify extra rsync options -- use carefully! -RSYNC="$rsync_bin $*" -#RSYNC="valgrind $rsync_bin $*" - -TLS_ARGS='' -if grep -E '^#define HAVE_LUTIMES 1' config.h >/dev/null; then - TLS_ARGS="$TLS_ARGS -l" -fi -if grep -E '#undef CHOWN_MODIFIES_SYMLINK' config.h >/dev/null; then - TLS_ARGS="$TLS_ARGS -L" -fi - -export POSIXLY_CORRECT TOOLDIR srcdir RSYNC TLS_ARGS - -echo "============================================================" -echo "$0 running in $TOOLDIR" -echo " rsync_bin=$RSYNC" -echo " srcdir=$srcdir" -echo " TLS_ARGS=$TLS_ARGS" - -if [ -f /usr/bin/whoami ]; then - testuser=`/usr/bin/whoami` -elif [ -f /usr/ucb/whoami ]; then - testuser=`/usr/ucb/whoami` -elif [ -f /bin/whoami ]; then - testuser=`/bin/whoami` -else - testuser=`id -un 2>/dev/null || echo ${LOGNAME:-${USERNAME:-${USER:-'UNKNOWN'}}}` -fi - -echo " testuser=$testuser" -echo " os=`uname -a`" - -# It must be "yes", not just nonnull -if [ "x$preserve_scratch" = xyes ]; then - echo " preserve_scratch=yes" -else - echo " preserve_scratch=no" -fi - -# Check if setacl/setfacl is around and if it supports the -k or -s option. -if setacl -k u::7,g::5,o:5 testsuite 2>/dev/null; then - setfacl_nodef='setacl -k' -elif setfacl --help 2>&1 | grep ' -k,\|\[-[a-z]*k' >/dev/null; then - setfacl_nodef='setfacl -k' -elif setfacl -s u::7,g::5,o:5 testsuite 2>/dev/null; then - setfacl_nodef='setfacl -s u::7,g::5,o:5' -else - # The "true" command runs successfully, but does nothing. - setfacl_nodef=true -fi - -export setfacl_nodef - -if [ ! -f "$rsync_bin" ]; then - echo "rsync_bin $rsync_bin is not a file" >&2 - exit 2 -fi - -if [ ! -d "$srcdir" ]; then - echo "srcdir $srcdir is not a directory" >&2 - exit 2 -fi - -expect_skipped="${RSYNC_EXPECT_SKIPPED-IGNORE}" -skipped_list='' -skipped=0 -missing=0 -passed=0 -failed=0 - -# Directory that holds the other test subdirs. We create separate dirs -# inside for each test case, so that they can be left behind in case of -# failure to aid investigation. We don't remove the testtmp subdir at -# the end so that it can be configured as a symlink to a filesystem that -# has ACLs and xattr support enabled (if desired). -scratchbase="${scratchbase:-$TOOLDIR}"/testtmp -echo " scratchbase=$scratchbase" -[ -d "$scratchbase" ] || mkdir "$scratchbase" - -suitedir="$srcdir/testsuite" -TESTRUN_TIMEOUT=300 - -export scratchdir suitedir TESTRUN_TIMEOUT - -prep_scratch() { - [ -d "$scratchdir" ] && chmod -R u+rwX "$scratchdir" && rm -rf "$scratchdir" - mkdir "$scratchdir" - # Get rid of default ACLs and dir-setgid to avoid confusing some tests. - $setfacl_nodef "$scratchdir" 2>/dev/null || true - chmod g-s "$scratchdir" - case "$srcdir" in - /*) ln -s "$srcdir" "$scratchdir/src" ;; - *) ln -s "$TOOLDIR/$srcdir" "$scratchdir/src" ;; - esac - return 0 -} - -maybe_discard_scratch() { - [ x"$preserve_scratch" != xyes ] && [ -d "$scratchdir" ] && rm -rf "$scratchdir" - return 0 -} - -if [ "x$whichtests" = x ]; then - whichtests="*.test" - full_run=yes -else - full_run=no -fi - -for testscript in $suitedir/$whichtests; do - testbase=`echo $testscript | sed -e 's!.*/!!' -e 's/.test\$//'` - scratchdir="$scratchbase/$testbase" - - prep_scratch - - case "$testscript" in - *hardlinks*) TESTRUN_TIMEOUT=600 ;; - *) TESTRUN_TIMEOUT=300 ;; - esac - - set +e - "$TOOLDIR/"testrun $RUNSHFLAGS "$testscript" >"$scratchdir/test.log" 2>&1 - result=$? - set -e - - if [ "x$always_log" = xyes ] || ( [ $result != 0 ] && [ $result != 77 ] && [ $result != 78 ] ) - then - echo "----- $testbase log follows" - cat "$scratchdir/test.log" - echo "----- $testbase log ends" - if [ -f "$scratchdir/rsyncd.log" ]; then - echo "----- $testbase rsyncd.log follows" - cat "$scratchdir/rsyncd.log" - echo "----- $testbase rsyncd.log ends" - fi - fi - - case $result in - 0) - echo "PASS $testbase" - passed=`expr $passed + 1` - maybe_discard_scratch - ;; - 77) - # backticks will fill the whole file onto one line, which is a feature - whyskipped=`cat "$scratchdir/whyskipped"` - echo "SKIP $testbase ($whyskipped)" - skipped_list="$skipped_list,$testbase" - skipped=`expr $skipped + 1` - maybe_discard_scratch - ;; - 78) - # It failed, but we expected that. don't dump out error logs, - # because most users won't want to see them. But do leave - # the working directory around. - echo "XFAIL $testbase" - failed=`expr $failed + 1` - ;; - *) - echo "FAIL $testbase" - failed=`expr $failed + 1` - if [ "x$nopersist" = xyes ]; then - exit 1 - fi - esac -done - -echo '------------------------------------------------------------' -echo "----- overall results:" -echo " $passed passed" -[ "$failed" -gt 0 ] && echo " $failed failed" -[ "$skipped" -gt 0 ] && echo " $skipped skipped" -[ "$missing" -gt 0 ] && echo " $missing missing" -if [ "$full_run" = yes ] && [ "$expect_skipped" != IGNORE ]; then - skipped_list=`echo "$skipped_list" | sed 's/^,//'` - echo "----- skipped results:" - echo " expected: $expect_skipped" - echo " got: $skipped_list" -else - skipped_list='' - expect_skipped='' -fi -echo '------------------------------------------------------------' - -# OK, so expr exits with 0 if the result is neither null nor zero; and -# 1 if the expression is null or zero. This is the opposite of what -# we want, and if we just call expr then this script will always fail, -# because -e is set. - -result=`expr $failed + $missing || true` -if [ "$result" = 0 ] && [ "$skipped_list" != "$expect_skipped" ]; then - result=1 -fi -echo "overall result is $result" -exit $result diff -Nru rsync-3.4.1+ds1/sender.c rsync-3.5.0+ds1/sender.c --- rsync-3.4.1+ds1/sender.c 2025-01-14 18:30:32.000000000 +0000 +++ rsync-3.5.0+ds1/sender.c 2026-07-26 09:40:12.000000000 +0000 @@ -23,6 +23,7 @@ #include "inums.h" extern int do_xfers; +extern int open_noatime; extern int am_server; extern int am_daemon; extern int local_server; @@ -35,6 +36,9 @@ extern int csum_length; extern int append_mode; extern int copy_links; +extern int copy_unsafe_links; +extern int copy_dirlinks; +extern int insecure_links; extern int io_error; extern int flist_eof; extern int whole_file; @@ -48,6 +52,8 @@ extern int inplace; extern int inplace_partial; extern int batch_fd; +extern char *module_dir; +extern int module_dirfd; extern int write_batch; extern int file_old_total; extern BOOL want_progress_now; @@ -65,6 +71,266 @@ * machine holding the source files. **/ +static int secure_sender_parent_fd(struct file_struct *file, const char *fname, const char **bname_p) +{ +#ifdef AT_FDCWD + const char *path, *slash, *relp, *bslash, *fslash; + char secure_path[MAXPATHLEN]; + int dfd, fl, slen; + + if (!fname || !*fname) { + errno = 0; + return -1; + } + + /* "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()) { + errno = 0; + return -1; + } + + if (!am_daemon || !module_dir || module_dir[0] != '/') { + /* Local (non-daemon) sender: there is no module root to anchor at, but + * still confine the parent via the shared held ancestor-dirfd stack + * (anchor = cwd, the transfer root set by change_pathname) so the + * --remove-source-files re-stat below won't follow an attacker-planted + * parent symlink. Best-effort: an uncacheable (very deep) path declines + * to -1 and the caller falls back to the path-based stat. */ + const char *fslash = strrchr(fname, '/'); + *bname_p = fslash ? fslash + 1 : fname; + if (fslash) { + char dir[MAXPATHLEN]; + size_t dlen = (size_t)(fslash - fname); + if (dlen >= sizeof dir) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dir, fname, dlen); + dir[dlen] = '\0'; + /* An absolute --relative name is still rooted at / after + * change_pathname(). Resolving its parent through the cwd-backed + * dirfd cache would re-anchor cleanup at the sender's working + * directory and can remove a same-named, unrelated entry there. */ + if (*fname == '/') { + const char *rel = dir; +#ifdef __CYGWIN__ + /* clean_fname() keeps exactly two leading slashes here, + * because //server/share is a separate UNC namespace. + * Stripping them and anchoring at "/" would resolve a + * different object entirely, so decline (errno 0) and let + * the caller fall back to the path-based cleanup. */ + if (fname[1] == '/' && fname[2] != '/') { + errno = 0; + return -1; + } +#endif + while (*rel == '/') + rel++; + return secure_relative_open("/", rel, + O_RDONLY | O_DIRECTORY, 0); + } + /* held_dir_path_fd 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); + if (dfd >= 0) + return dup(dfd); + if (errno != 0) + return -1; + return secure_relative_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0); + } + errno = 0; /* top-level file: no parent component to confine */ + return -1; + } + + /* Resolve the file's parent anchored at the absolute module root, never the + * process CWD: on cygwin the CWD is path-based, so a removal that trusted + * openat(".") could be raced (a parent-symlink flip) into unlinking outside + * the module. Reconstruct the module-relative path from F_PATHNAME + f_name + * (as send_files does) and walk it confined beneath module_dir -- the + * per-component O_NOFOLLOW walk refuses the flipped symlink. */ + path = F_PATHNAME(file); + if (!path) + path = ""; + slash = *path ? "/" : ""; + slen = snprintf(secure_path, sizeof secure_path, "%s%s%s", path, slash, fname); + if (slen < 0 || slen >= (int)sizeof secure_path) { + errno = ENAMETOOLONG; + return -1; + } + relp = secure_path; + while (*relp == '/') + relp++; + + bslash = strrchr(relp, '/'); + if (bslash) { + char dir[MAXPATHLEN]; + size_t dlen = (size_t)(bslash - relp); + if (dlen >= sizeof dir) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dir, relp, dlen); + dir[dlen] = '\0'; + dfd = secure_relative_open(module_dir, dir, O_RDONLY | O_DIRECTORY, 0); + } else + dfd = secure_relative_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. */ + fslash = strrchr(fname, '/'); + *bname_p = fslash ? fslash + 1 : fname; + + if (dfd >= 0 && (fl = fcntl(dfd, F_GETFD)) >= 0) + fcntl(dfd, F_SETFD, fl | FD_CLOEXEC); + return dfd; +#else + (void)file; (void)fname; (void)bname_p; + errno = 0; + return -1; +#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. */ +static int secure_remove_source_file(int dfd, const char *bname) +{ + return do_unlink_atfd(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 + * 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. */ +static int sender_open_confined(const char *anchor, const char *relpath, int flags) +{ +#ifdef AT_FDCWD + const char *slash = strrchr(relpath, '/'); + const char *bname; + char dirbuf[MAXPATHLEN]; + const char *dir; + int dfd; + + if (slash) { + size_t dlen = slash - relpath; + if (dlen >= sizeof dirbuf) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(dirbuf, relpath, dlen); + dirbuf[dlen] = '\0'; + dir = dirbuf; + bname = slash + 1; + } else { + dir = ""; /* file directly in the anchor dir */ + bname = relpath; + } + +#ifdef O_NOATIME + if (open_noatime) + flags |= O_NOATIME; +#endif + dfd = held_dir_path_fd(anchor, dir); + if (dfd < 0) + return secure_relative_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, + * so nothing to amortise); use it directly to keep the anchor semantics. */ + return secure_relative_open(anchor, relpath, flags | O_NOFOLLOW, 0); +#endif +} + +/* Open the content of `relpath` for a symlink-following transfer mode (-L / + * --copy-unsafe-links / -k) while staying confined beneath `anchor`. The leaf + * 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 + * 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. */ +static int sender_open_copylinks_confined(const char *anchor, const char *relpath) +{ +#if defined AT_FDCWD && defined O_NOFOLLOW + char cur[MAXPATHLEN]; + int hops = 32; + int extra = 0; +#ifdef O_NOATIME + if (open_noatime) + extra |= O_NOATIME; +#endif + if (strlcpy(cur, relpath, sizeof cur) >= sizeof cur) { + errno = ENAMETOOLONG; + return -1; + } + while (hops-- > 0) { + const char *slash = strrchr(cur, '/'); + const char *bname; + char dir[MAXPATHLEN], tgt[MAXPATHLEN]; + int pdfd, fd, e; + ssize_t n; + if (slash) { + size_t dlen = slash - cur; + if (dlen >= sizeof dir) { errno = ENAMETOOLONG; return -1; } + memcpy(dir, cur, dlen); + dir[dlen] = '\0'; + bname = slash + 1; + } else { + dir[0] = '\0'; + bname = cur; + } + /* anchor is checked explicitly: the resolver treats a NULL anchor as + * "relative to cwd", so it is a legal argument for the else branch -- + * 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, + O_RDONLY | O_DIRECTORY, 0); + else + pdfd = secure_relative_open(anchor, dir, + O_RDONLY | O_DIRECTORY, 0); + if (pdfd < 0) + return -1; + n = do_readlink_atfd(pdfd, bname, tgt, sizeof tgt - 1); + e = errno; + if (n < 0) { + /* EINVAL: not a symlink -> the resolved target file. Open it + * O_NOFOLLOW (a raced symlink flip is still refused). */ + fd = e == EINVAL + ? openat(pdfd, bname, O_RDONLY | O_NOFOLLOW | O_BINARY | extra, 0) + : -1; + close(pdfd); + if (n < 0 && e != EINVAL) + errno = e; + return fd; + } + close(pdfd); + tgt[n] = '\0'; + if (tgt[0] == '/') { /* absolute target escapes the module */ + errno = ELOOP; + return -1; + } + if ((size_t)snprintf(cur, sizeof cur, "%s%s%s", + dir, *dir ? "/" : "", tgt) >= sizeof cur) { + errno = ENAMETOOLONG; + return -1; + } + } + errno = ELOOP; + return -1; +#else + return secure_relative_open(anchor, relpath, O_RDONLY | O_NOFOLLOW, 0); +#endif +} + /** * Receive the checksums for a buffer **/ @@ -130,20 +396,36 @@ { char fname[MAXPATHLEN]; char *failed_op; + const char *bname = NULL; struct file_struct *file; struct file_list *flist; STRUCT_STAT st; + int dfd = -1, secure_errno = 0; if (!remove_source_files) return; flist = flist_for_ndx(ndx, "successful_send"); + if (ndx < flist->ndx_start) + exit_cleanup(RERR_PROTOCOL); file = flist->files[ndx - flist->ndx_start]; if (!change_pathname(file, NULL, 0)) return; f_name(file, fname); - if ((copy_links ? do_stat(fname, &st) : do_lstat(fname, &st)) < 0) { + dfd = secure_sender_parent_fd(file, fname, &bname); + if (dfd < 0) + secure_errno = errno; + + if (dfd < 0 && secure_errno) { + errno = secure_errno; + failed_op = "secure-open-parent"; + goto failed; + } + + 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) { failed_op = "re-lstat"; goto failed; } @@ -152,6 +434,8 @@ && (int64)st.st_dev == IVAL64(num_dev_ino_buf, 4) && (int64)st.st_ino == IVAL64(num_dev_ino_buf, 4 + 8)) { rprintf(FERROR_XFER, "ERROR: Skipping sender remove of destination file: %s\n", fname); + if (dfd >= 0) + close(dfd); return; } @@ -161,10 +445,12 @@ #endif ) { rprintf(FERROR_XFER, "ERROR: Skipping sender remove for changed file: %s\n", fname); + if (dfd >= 0) + close(dfd); return; } - if (do_unlink(fname) < 0) { + if (dfd >= 0 ? secure_remove_source_file(dfd, bname) < 0 : do_unlink(fname) < 0) { failed_op = "remove"; failed: if (errno == ENOENT) @@ -175,6 +461,8 @@ if (INFO_GTE(REMOVE, 1)) rprintf(FINFO, "sender removed %s\n", fname); } + if (dfd >= 0) + close(dfd); } static void write_ndx_and_attrs(int f_out, int ndx, int iflags, @@ -262,8 +550,17 @@ if (ndx - cur_flist->ndx_start >= 0) file = cur_flist->files[ndx - cur_flist->ndx_start]; + else if (cur_flist->parent_ndx < 0 + || cur_flist->parent_ndx >= dir_flist->used) + exit_cleanup(RERR_PROTOCOL); else file = dir_flist->files[cur_flist->parent_ndx]; + if (!F_IS_ACTIVE(file)) { + rprintf(FERROR, + "rsync: refusing transfer of cleared file index %d\n", + ndx); + exit_cleanup(RERR_PROTOCOL); + } if (F_PATHNAME(file)) { path = F_PATHNAME(file); slash = "/"; @@ -350,7 +647,65 @@ exit_cleanup(RERR_PROTOCOL); } - fd = do_open_checklinks(fname); + if (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()) { + /* 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 + * from F_PATHNAME (path) and f_name (fname). */ + char secure_path[MAXPATHLEN]; + const char *relp; + int slen = snprintf(secure_path, sizeof secure_path, "%s%s%s", path, slash, fname); + if (slen >= (int)sizeof secure_path) { + io_error |= IOERR_GENERAL; + rprintf(FERROR_XFER, "path too long: %s%s%s\n", path, slash, fname); + free_sums(s); + if (protocol_version >= 30) + send_msg_int(MSG_NO_SEND, ndx); + continue; + } + /* 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). */ + relp = secure_path; + while (*relp == '/') + relp++; + /* A symlink-following mode must follow an in-tree symlink leaf the + * operator asked for, still confined to the module; the default + * keeps the O_NOFOLLOW leaf so a raced leaf symlink is refused. */ + if (copy_links || copy_unsafe_links || copy_dirlinks || insecure_links) + fd = sender_open_copylinks_confined(module_dir, relp); + else + fd = sender_open_confined(module_dir, relp, O_RDONLY); + } else if (!copy_links && !copy_unsafe_links && !copy_dirlinks && !insecure_links) { + /* Default symlink handling (no dir-link following): the scan + * recorded this as a regular file. Open it confined beneath the + * transfer root: an in-tree symlinked parent (e.g. -R keeps one in + * the path) is followed beneath the root, a parent raced into a + * symlink pointing out of the tree is refused, and O_NOFOLLOW + * governs the leaf so a raced leaf symlink is refused. A + * symlink-following mode (-L/--copy-unsafe-links/-k) or + * --insecure-links keeps the legacy open below. */ + if (fname[0] == '/') { + /* --relative (or a --files-from absolute name) keeps the + * full absolute path as fname; the transfer root is then "/", + * so anchor the confined open there and strip the leading + * slash to the module-relative path the resolver wants -- it + * rejects an absolute relpath outright. */ + const char *relp = fname; + while (*relp == '/') + relp++; + fd = sender_open_confined("/", relp, O_RDONLY); + } else + fd = sender_open_confined(NULL, fname, O_RDONLY); + } else { + fd = do_open_checklinks(fname); + } if (fd == -1) { if (errno == ENOENT) { enum logcode c = am_daemon && protocol_version < 28 ? FERROR : FWARNING; diff -Nru rsync-3.4.1+ds1/simd-checksum-avx2.S rsync-3.5.0+ds1/simd-checksum-avx2.S --- rsync-3.4.1+ds1/simd-checksum-avx2.S 2022-03-04 01:00:57.000000000 +0000 +++ rsync-3.5.0+ds1/simd-checksum-avx2.S 2026-08-02 22:48:33.000000000 +0000 @@ -46,6 +46,14 @@ vpxor xmm1, xmm1, xmm1 # reset both partial sums accumulators. vpxor xmm4, xmm4, xmm4 mov eax, [r8] + # The loop below is software-pipelined: it preloads the 64 bytes AFTER the + # ones it is folding in. Run it one block short and finish that block in + # .last, which does not preload -- otherwise the final iteration reads 64 + # bytes past the end of what the caller gave us. The remainder (len & 63) + # is always under 64, so that read always crossed buf+len; it only faulted + # when the buffer happened to end at a page boundary. + sub esi, 1 + jz .last .p2align 4 # should fit into the LSD allocation queue. .loop: vpmaddubsw ymm0, ymm15, ymm2 # s1 partial sums @@ -70,11 +78,33 @@ vpaddd ymm6, ymm10, ymm6 # 32*CHAR_OFFSET vpaddd ymm1, ymm13, ymm1 # 528*CHAR_OFFSET #endif - vmovdqa ymm2, ymm8 # move the next 64 bytes + vmovdqa ymm2, ymm8 # move the next 64 bytes vmovdqa ymm3, ymm9 # into the right registers sub esi, 1 jnz .loop + # The final 64 bytes, already in ymm2/ymm3. Same arithmetic as the loop + # body with the preload, the prefetch and the loop control dropped. +.last: + vpmaddubsw ymm0, ymm15, ymm2 # s1 partial sums + vpmaddubsw ymm5, ymm15, ymm3 + vpaddd ymm4, ymm4, ymm6 + vpaddw ymm5, ymm5, ymm0 + vpsrld ymm0, ymm5, 16 + vpaddw ymm5, ymm0, ymm5 + vpaddd ymm6, ymm5, ymm6 + vpmaddubsw ymm2, ymm7, ymm2 # s2 partial sums + vpmaddubsw ymm3, ymm12, ymm3 + vpaddw ymm3, ymm2, ymm3 + vpsrldq ymm2, ymm3, 2 + vpaddd ymm3, ymm2, ymm3 + vpaddd ymm1, ymm1, ymm3 + +#if CHAR_OFFSET != 0 + vpaddd ymm6, ymm10, ymm6 # 32*CHAR_OFFSET + vpaddd ymm1, ymm13, ymm1 # 528*CHAR_OFFSET +#endif + # now we reduce the partial sums. vpslld ymm3, ymm4, 6 vpsrldq ymm2, ymm6, 4 diff -Nru rsync-3.4.1+ds1/simd-checksum-x86_64.cpp rsync-3.5.0+ds1/simd-checksum-x86_64.cpp --- rsync-3.4.1+ds1/simd-checksum-x86_64.cpp 2024-11-20 05:28:39.000000000 +0000 +++ rsync-3.5.0+ds1/simd-checksum-x86_64.cpp 2026-08-02 22:48:33.000000000 +0000 @@ -317,6 +317,21 @@ extern "C" __attribute__ ((target("avx2"))) int32 get_checksum1_avx2_asm(schar* buf, int32 len, int32 i, uint32* ps1, uint32* ps2); +/* The asm routine is AVX2-only and, unlike the multi-versioned intrinsic + * paths, has no compiler-generated fallback, so it must not be called on a + * CPU without AVX2 (it would fault with SIGILL). Gate it on a cached runtime + * check; when AVX2 is absent we skip it and the SSSE3/SSE2/scalar steps, + * which are safe everywhere, do all the work. */ +static int roll_asm_have_avx2(void) +{ + static int have = -1; + if (have < 0) { + __builtin_cpu_init(); + have = __builtin_cpu_supports("avx2") ? 1 : 0; + } + return have; +} + #else /* } { */ /* @@ -347,8 +362,7 @@ __m128i tmp = _mm_load_si128((__m128i*) mul_t1_buf); __m256i mul_t1 = _mm256_cvtepu8_epi16(tmp); __m256i mul_const = _mm256_broadcastd_epi32(_mm_cvtsi32_si128(4 | (3 << 8) | (2 << 16) | (1 << 24))); - __m256i mul_one; - mul_one = _mm256_abs_epi8(_mm256_cmpeq_epi16(mul_one,mul_one)); // set all vector elements to 1 + __m256i mul_one = _mm256_set1_epi8(1); for (; i < (len-64); i+=64) { // Load ... 4*[int8*16] @@ -462,7 +476,8 @@ // multiples of 64 bytes using AVX2 (if available) #ifdef USE_ROLL_ASM - i = get_checksum1_avx2_asm((schar*)buf1, len, i, &s1, &s2); + if (roll_asm_have_avx2()) + i = get_checksum1_avx2_asm((schar*)buf1, len, i, &s1, &s2); #else i = get_checksum1_avx2_64((schar*)buf1, len, i, &s1, &s2); #endif @@ -473,8 +488,8 @@ // multiples of 32 bytes using SSE2 (if available) i = get_checksum1_sse2_32((schar*)buf1, len, i, &s1, &s2); - // whatever is left - i = get_checksum1_default_1((schar*)buf1, len, i, &s1, &s2); + // whatever is left (updates s1/s2; the returned offset is unused here) + get_checksum1_default_1((schar*)buf1, len, i, &s1, &s2); return (s1 & 0xffff) + (s2 << 16); } @@ -548,6 +563,187 @@ #pragma clang optimize on #endif /* BENCHMARK_SIMD_CHECKSUM1 */ +#ifdef TEST_SIMD_CHECKSUM1 +#include +#include + +static uint32 checksum_via_default(char *buf, int32 len) +{ + uint32 s1 = 0, s2 = 0; + get_checksum1_default_1((schar*)buf, len, 0, &s1, &s2); + return (s1 & 0xffff) + (s2 << 16); +} + +static uint32 checksum_via_sse2(char *buf, int32 len) +{ + int32 i; + uint32 s1 = 0, s2 = 0; + i = get_checksum1_sse2_32((schar*)buf, len, 0, &s1, &s2); + get_checksum1_default_1((schar*)buf, len, i, &s1, &s2); + return (s1 & 0xffff) + (s2 << 16); +} + +static uint32 checksum_via_ssse3(char *buf, int32 len) +{ + int32 i; + uint32 s1 = 0, s2 = 0; + i = get_checksum1_ssse3_32((schar*)buf, len, 0, &s1, &s2); + get_checksum1_default_1((schar*)buf, len, i, &s1, &s2); + return (s1 & 0xffff) + (s2 << 16); +} + +static uint32 checksum_via_avx2(char *buf, int32 len) +{ + int32 i; + uint32 s1 = 0, s2 = 0; +#ifdef USE_ROLL_ASM + if (roll_asm_have_avx2()) + i = get_checksum1_avx2_asm((schar*)buf, len, 0, &s1, &s2); + else + i = 0; +#else + i = get_checksum1_avx2_64((schar*)buf, len, 0, &s1, &s2); +#endif + get_checksum1_default_1((schar*)buf, len, i, &s1, &s2); + return (s1 & 0xffff) + (s2 << 16); +} + + +/* Run every implementation on a buffer placed flush against an unreadable page, + * so any read past buf+len faults here instead of in a user's transfer. The + * buffers above deliberately carry 64 spare bytes for the unaligned case, which + * is exactly what let a 64-byte over-read in the AVX2 assembly go unnoticed: + * it only crashed when an allocation happened to end at a page boundary. */ +static int test_no_overread(void) +{ +#if defined HAVE_SYS_MMAN_H || defined __linux__ || defined __APPLE__ || defined BSD + /* Setting the guard up is not optional on the platforms this is compiled + * for: if it fails we are not testing what the caller thinks, so say so and + * fail rather than return a pass that looks identical to a real one. */ + long pagesz = sysconf(_SC_PAGESIZE); + if (pagesz <= 0) { + printf("FAIL guard-page: sysconf(_SC_PAGESIZE) gave %ld\n", pagesz); + return 1; + } + size_t region = (size_t)pagesz * 4; + char *base = (char *)mmap(NULL, region, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (base == MAP_FAILED) { + printf("FAIL guard-page: mmap failed\n"); + return 1; + } + if (mprotect(base + region - pagesz, pagesz, PROT_NONE) != 0) { + printf("FAIL guard-page: mprotect failed\n"); + munmap(base, region); + return 1; + } + /* Whether the ASSEMBLY was reached is a separate question from whether the + * checks passed. Without AVX2 the dispatcher falls back and the guard loop + * proves nothing about it, which must not read as coverage. */ + __builtin_cpu_init(); + printf("guard-page: AVX2 %s\n", + __builtin_cpu_supports("avx2") + ? "present, assembly and intrinsics exercised" + : "ABSENT -- fallback only, the AVX2 paths were NOT exercised"); + int failures = 0; + /* 128 is the shortest the AVX2 paths will touch; step by 1 so every + * remainder mod 64, and both alignments, get covered. */ + for (int32 len = 128; len <= 4096; len++) { + char *buf = base + region - pagesz - len; /* last byte abuts the guard */ + for (int32 i = 0; i < len; i++) + buf[i] = (char)((i + (i % 3) + (i % 11)) % 256); + uint32 ref = checksum_via_default(buf, len); + if (checksum_via_sse2(buf, len) != ref + || checksum_via_ssse3(buf, len) != ref + || checksum_via_avx2(buf, len) != ref + || get_checksum1(buf, len) != ref) { + printf("FAIL guard-page size=%5d: mismatch at the page boundary\n", len); + if (++failures > 4) + break; + } + } + munmap(base, region); + return failures; +#else + printf("guard-page: no mmap on this platform, over-read check NOT RUN\n"); + return 0; +#endif +} + +int main() +{ + static const int sizes[] = {1, 4, 31, 32, 33, 63, 64, 65, 128, 129, 256, 700, 1024, 4096, 65536}; + int num_sizes = sizeof(sizes) / sizeof(sizes[0]); + int max_size = sizes[num_sizes - 1]; + int failures = 0; + + /* Allocate with extra bytes for unaligned test */ + unsigned char *raw = (unsigned char *)malloc(max_size + 64 + 1); + if (!raw) { + fprintf(stderr, "malloc failed\n"); + return 1; + } + + /* Fill with deterministic data */ + for (int i = 0; i < max_size + 64 + 1; i++) + raw[i] = (i + (i % 3) + (i % 11)) % 256; + + /* Test with aligned buffer (64-byte aligned) */ + unsigned char *aligned = raw + (64 - ((uintptr_t)raw % 64)); + + /* Test with unaligned buffer (+1 byte offset) */ + unsigned char *unaligned = aligned + 1; + + struct { const char *name; unsigned char *buf; } buffers[] = { + {"aligned", aligned}, + {"unaligned", unaligned}, + }; + + for (int b = 0; b < 2; b++) { + char *buf = (char *)buffers[b].buf; + const char *bname = buffers[b].name; + + for (int s = 0; s < num_sizes; s++) { + int32 len = sizes[s]; + uint32 ref = checksum_via_default(buf, len); + uint32 cs_sse2 = checksum_via_sse2(buf, len); + uint32 cs_ssse3 = checksum_via_ssse3(buf, len); + uint32 cs_avx2 = checksum_via_avx2(buf, len); + uint32 cs_auto = get_checksum1(buf, len); + + if (cs_sse2 != ref) { + printf("FAIL %-9s size=%5d: SSE2=%08x ref=%08x\n", bname, len, cs_sse2, ref); + failures++; + } + if (cs_ssse3 != ref) { + printf("FAIL %-9s size=%5d: SSSE3=%08x ref=%08x\n", bname, len, cs_ssse3, ref); + failures++; + } + if (cs_avx2 != ref) { + printf("FAIL %-9s size=%5d: AVX2=%08x ref=%08x\n", bname, len, cs_avx2, ref); + failures++; + } + if (cs_auto != ref) { + printf("FAIL %-9s size=%5d: auto=%08x ref=%08x\n", bname, len, cs_auto, ref); + failures++; + } + } + } + + free(raw); + + failures += test_no_overread(); + + if (failures) { + printf("%d checksum mismatches!\n", failures); + return 1; + } + printf("All SIMD checksum tests passed.\n"); + return 0; +} + +#endif /* TEST_SIMD_CHECKSUM1 */ + #endif /* } USE_ROLL_SIMD */ #endif /* } __cplusplus */ #endif /* } __x86_64__ */ diff -Nru rsync-3.4.1+ds1/socket.c rsync-3.5.0+ds1/socket.c --- rsync-3.4.1+ds1/socket.c 2020-06-26 03:54:21.000000000 +0000 +++ rsync-3.5.0+ds1/socket.c 2026-08-03 03:02:33.000000000 +0000 @@ -25,6 +25,7 @@ * emulate it using the KAME implementation. */ #include "rsync.h" +#include #include "itypes.h" #include "ifuncs.h" #ifdef HAVE_NETINET_IN_SYSTM_H @@ -46,22 +47,34 @@ #endif static int sock_exec(const char *prog); +static char *shell_quote_connect_host(const char *host); + +#define PROXY_BUF_SIZE 1024 /* Establish a proxy connection on an open socket to a web proxy by using the * CONNECT method. If proxy_user and proxy_pass are not NULL, they are used to * authenticate to the proxy using the "Basic" proxy-authorization protocol. */ static int establish_proxy_connection(int fd, char *host, int port, char *proxy_user, char *proxy_pass) { - char *cp, buffer[1024]; - char *authhdr, authbuf[1024]; + char *cp, buffer[PROXY_BUF_SIZE + 1]; + char *authhdr, authbuf[PROXY_BUF_SIZE + 1]; int len; + /* Reject control bytes in the host so they can't be smuggled into the + * HTTP CONNECT request line (CRLF header/request injection). */ + for (cp = host; *cp; cp++) { + if ((unsigned char)*cp < 0x20 || (unsigned char)*cp == 0x7f) { + rprintf(FERROR, "invalid control character in proxy CONNECT host\n"); + return -1; + } + } + if (proxy_user && proxy_pass) { - stringjoin(buffer, sizeof buffer, + stringjoin(buffer, PROXY_BUF_SIZE, proxy_user, ":", proxy_pass, NULL); len = strlen(buffer); - if ((len*8 + 5) / 6 >= (int)sizeof authbuf - 3) { + if ((len*8 + 5) / 6 >= PROXY_BUF_SIZE - 3) { rprintf(FERROR, "authentication information is too long\n"); return -1; @@ -74,14 +87,17 @@ authhdr = ""; } - len = snprintf(buffer, sizeof buffer, "CONNECT %s:%d HTTP/1.0%s%s\r\n\r\n", host, port, authhdr, authbuf); - assert(len > 0 && len < (int)sizeof buffer); + len = snprintf(buffer, PROXY_BUF_SIZE, "CONNECT %s:%d HTTP/1.0%s%s\r\n\r\n", host, port, authhdr, authbuf); + if (len <= 0 || len >= PROXY_BUF_SIZE) { + rprintf(FERROR, "proxy CONNECT request too long\n"); + return -1; + } if (write(fd, buffer, len) != len) { rsyserr(FERROR, errno, "failed to write to proxy"); return -1; } - for (cp = buffer; cp < &buffer[sizeof buffer - 1]; cp++) { + for (cp = buffer; cp < &buffer[PROXY_BUF_SIZE - 1]; cp++) { if (read(fd, cp, 1) != 1) { rsyserr(FERROR, errno, "failed to read from proxy"); return -1; @@ -90,11 +106,13 @@ break; } - if (*cp != '\n') - cp++; - *cp-- = '\0'; - if (*cp == '\r') - *cp = '\0'; + if (cp == &buffer[PROXY_BUF_SIZE - 1]) { + rprintf(FERROR, "proxy response line too long\n"); + return -1; + } + *cp = '\0'; + if (cp > buffer && cp[-1] == '\r') + cp[-1] = '\0'; if (strncmp(buffer, "HTTP/", 5) != 0) { rprintf(FERROR, "bad response from proxy -- %s\n", buffer); @@ -110,7 +128,7 @@ } /* throw away the rest of the HTTP header */ while (1) { - for (cp = buffer; cp < &buffer[sizeof buffer - 1]; cp++) { + for (cp = buffer; cp < &buffer[PROXY_BUF_SIZE - 1]; cp++) { if (read(fd, cp, 1) != 1) { rsyserr(FERROR, errno, "failed to read from proxy"); @@ -119,6 +137,10 @@ if (*cp == '\n') break; } + if (cp == &buffer[PROXY_BUF_SIZE - 1]) { + rprintf(FERROR, "proxy response header line too long\n"); + return -1; + } if (cp > buffer && *cp == '\n') cp--; if (cp == buffer && (*cp == '\n' || *cp == '\r')) @@ -127,6 +149,73 @@ return 0; } +static char *shell_quote_connect_host(const char *host) +{ + const char *s; + char *quoted, *t; + size_t len = 2; /* surrounding single quotes */ + + for (s = host; *s; s++) + len += *s == '\'' ? 4 : 1; + quoted = new_array(char, len + 1); + t = quoted; + *t++ = '\''; + for (s = host; *s; s++) { + if (*s == '\'') { + memcpy(t, "'\\''", 4); + t += 4; + } else + *t++ = *s; + } + *t++ = '\''; + *t = '\0'; + return quoted; +} + +/* %H is substituted into a free-form command string that a shell then runs, and + * a hook of the form `sh -c '... %H ...'` re-parses the word in a nested shell + * where the quoting we added has already been consumed. Restrict the host to + * characters that stay data through such a pass. ('Any shell' is too strong a + * claim -- see the leading-'%' note below.) + * + * The set is deliberately a little wider than a DNS name: RSYNC_CONNECT_PROG + * exists for custom transports, where %H may be an alias the program resolves + * itself rather than a name the resolver ever sees, so '+' and '~' are allowed + * inside the string. '%' is required for an IPv6 zone id (fe80::1%eth0) and is + * not special to a POSIX shell. + * + * The first character is checked separately, because several of the allowed + * ones change an argument's MEANING rather than its text, which no amount of + * quoting prevents. Mid-word each is literal, which is why the set still + * allows them: + * '-' and '+' both introduce options to plenty of programs -- `sh +x` is as + * real as `sh -x`; + * '~' is tilde-expanded by the nested shell, turning ~root into /root; + * '%' is expanded by a nested fish, where %self becomes its pid (an IPv6 + * zone id has its '%' mid-word, so nothing legitimate is lost). + * An empty host is refused too: it survives a direct exec as an empty argument + * but vanishes entirely when a nested shell re-splits the command, shifting + * every argument after it. None of these can begin a real hostname. + * + * This bounds what a SHELL can do with the value. It cannot bound what the + * named program does with it -- something like "host:-rf" arrives intact, and a + * program that splits on ':' may reinterpret the tail. That boundary belongs + * to whoever writes RSYNC_CONNECT_PROG. */ +static int shell_unsafe_connect_host(const char *host) +{ + const unsigned char *s; + + if (!*host || strchr("-+~%", *host)) + return 1; + + for (s = (const unsigned char *)host; *s; s++) { + if (!((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z') + || (*s >= '0' && *s <= '9') || strchr("._:-%+~", *s))) + return 1; + } + return 0; +} + /* Try to set the local address for a newly-created socket. * Return -1 if this fails. */ @@ -166,6 +255,68 @@ connect_timeout = -1; } +/* How long each poll() pass waits before re-checking a pending connect(). */ +#define CONNECT_POLL_SLICE 100 + +/* connect() to addr, waiting for completion with poll() rather than blocking + * in the kernel. A blocking connect() can sleep forever on a connection that + * is already established (seen on OpenBSD, where the socket shows ESTABLISHED + * at both ends while connect() never returns); with no timeout set that hangs + * rsync for good. Re-checking the socket on each pass costs a loop instead. + * Returns 0 on success, -1 with errno set on failure. */ +static int connect_polled(int s, const struct sockaddr *addr, socklen_t addrlen) +{ + struct pollfd pfd; + int save_errno; + + set_nonblocking(s); + + if (connect(s, addr, addrlen) == 0) + goto connected; + if (errno != EINPROGRESS && errno != EINTR) + goto failed; + + pfd.fd = s; + pfd.events = POLLOUT; + while (1) { + int err = 0; + socklen_t errlen = sizeof err; + + /* the --contimeout alarm fired: let the caller report it */ + if (connect_timeout < 0) { + errno = ETIMEDOUT; + goto failed; + } + pfd.revents = 0; + if (poll(&pfd, 1, CONNECT_POLL_SLICE) < 0) { + if (errno == EINTR) + continue; + goto failed; + } + /* A finished slice is not a failure: loop so that poll() looks + * at the socket again, which is what recovers a missed wakeup. */ + if (!pfd.revents) + continue; + if (getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &errlen) < 0) + goto failed; + if (err) { + errno = err; + goto failed; + } + break; + } + + connected: + set_blocking(s); + return 0; + + failed: + save_errno = errno; + set_blocking(s); + errno = save_errno; + return -1; +} + /* Open a socket to a tcp remote host with the specified port. * * Based on code from Warren. Proxy support by Stephen Rothwell. @@ -273,23 +424,20 @@ } set_socket_options(s, sockopts); - while (connect(s, res->ai_addr, res->ai_addrlen) < 0) { + if (connect_polled(s, res->ai_addr, res->ai_addrlen) < 0) { if (connect_timeout < 0) exit_cleanup(RERR_CONTIMEOUT); - if (errno == EINTR) - continue; + /* stash it before close()/alarm() can overwrite errno */ + errnos[j] = errno; close(s); s = -1; - break; } if (connect_timeout > 0) alarm(0); - if (s < 0) { - errnos[j] = errno; + if (s < 0) continue; - } if (proxied && establish_proxy_connection(s, host, port, proxy_user, proxy_pass) != 0) { close(s); @@ -340,7 +488,12 @@ char *prog = getenv("RSYNC_CONNECT_PROG"); if (prog && strchr(prog, '%')) { - int hlen = strlen(host); + if (shell_unsafe_connect_host(host)) { + rprintf(FERROR, "unsafe host characters for RSYNC_CONNECT_PROG\n"); + return -1; + } + char *qhost = shell_quote_connect_host(host); + int hlen = strlen(qhost); int len = strlen(prog) + 1; char *f, *t; for (f = prog; *f; f++) { @@ -361,7 +514,7 @@ /* Just skips the extra '%'. */ break; case 'H': - memcpy(t, host, hlen); + memcpy(t, qhost, hlen); t += hlen; continue; default: @@ -372,6 +525,7 @@ *t++ = *f; } *t = '\0'; + free(qhost); } if (DEBUG_GTE(CONNECT, 1)) { @@ -532,8 +686,8 @@ void start_accept_loop(int port, int (*fn)(int, int)) { - fd_set deffds; - int *sp, maxfd, i; + struct pollfd *pfds; + int *sp, nsp, i; #ifdef HAVE_SIGACTION sigact.sa_flags = SA_NOCLDSTOP; @@ -545,8 +699,12 @@ exit_cleanup(RERR_SOCKETIO); /* ready to listen */ - FD_ZERO(&deffds); - for (i = 0, maxfd = -1; sp[i] >= 0; i++) { + for (nsp = 0; sp[nsp] >= 0; nsp++) {} + /* poll() rather than select(): a listening fd at or above FD_SETSIZE + * would overflow an fd_set, which is undefined behaviour (issue #231). */ + if (!(pfds = new_array(struct pollfd, nsp ? nsp : 1))) + out_of_memory("start_accept_loop"); + for (i = 0; sp[i] >= 0; i++) { if (listen(sp[i], lp_listen_backlog()) < 0) { rsyserr(FERROR, errno, "listen() on socket failed"); #ifdef INET6 @@ -556,36 +714,32 @@ #endif exit_cleanup(RERR_SOCKETIO); } - FD_SET(sp[i], &deffds); - if (maxfd < sp[i]) - maxfd = sp[i]; + pfds[i].fd = sp[i]; + pfds[i].events = POLLIN; + pfds[i].revents = 0; } /* now accept incoming connections - forking a new process * for each incoming connection */ while (1) { - fd_set fds; pid_t pid; int fd; struct sockaddr_storage addr; socklen_t addrlen = sizeof addr; - /* close log file before the potentially very long select so + /* close log file before the potentially very long wait so the * file can be trimmed by another process instead of growing * forever */ logfile_close(); -#ifdef FD_COPY - FD_COPY(&deffds, &fds); -#else - fds = deffds; -#endif + for (i = 0; i < nsp; i++) + pfds[i].revents = 0; - if (select(maxfd + 1, &fds, NULL, NULL, NULL) < 1) + if (poll(pfds, nsp, -1) < 1) continue; - for (i = 0, fd = -1; sp[i] >= 0; i++) { - if (FD_ISSET(sp[i], &fds)) { + for (i = 0, fd = -1; i < nsp; i++) { + if (pfds[i].revents & (POLLIN | POLLERR | POLLHUP)) { fd = accept(sp[i], (struct sockaddr *)&addr, &addrlen); break; } @@ -607,6 +761,7 @@ logfile_reopen(); ret = fn(fd, fd); close_all(); + gcov_flush(); _exit(ret); } else if (pid < 0) { rsyserr(FERROR, errno, @@ -733,10 +888,19 @@ } +/* How long socketpair_tcp() waits for its own loopback connection (seconds), + * and how long each poll() pass waits before re-checking the listen queue. */ +#define SOCKETPAIR_ACCEPT_TIMEOUT 60 +#define SOCKETPAIR_ACCEPT_SLICE 100 + /* This is like socketpair but uses tcp. The function guarantees that nobody * else can attach to the socket, or if they do that this function fails and - * the socket gets closed. Returns 0 on success, -1 on failure. The resulting - * file descriptors are symmetrical. Currently only for RSYNC_CONNECT_PROG. */ + * the socket gets closed. The anti-hijack guarantee is enforced after the + * accept() below: a local attacker who races a connection in on the loopback + * listener before our own connect() lands would be detected by the peer-vs- + * local address comparison and the function fails. Returns 0 on success, -1 + * on failure. The resulting file descriptors are symmetrical. Currently + * only for RSYNC_CONNECT_PROG. */ static int socketpair_tcp(int fd[2]) { int listener; @@ -744,8 +908,9 @@ struct sockaddr_in sock2; socklen_t socklen = sizeof sock; int connect_done = 0; + int save_errno; - fd[0] = fd[1] = listener = -1; + fd[0] = fd[1] = -1; /* listener is set by socket() below before any use */ memset(&sock, 0, sizeof sock); @@ -775,8 +940,53 @@ } else connect_done = 1; - if ((fd[0] = accept(listener, (struct sockaddr *)&sock2, &socklen)) == -1) - goto failed; + /* Wait for our own connection with a polled, non-blocking accept() + * rather than a blocking one. A blocking accept() here can sleep + * forever on a connection the kernel has already completed (seen on + * OpenBSD: both ends ESTABLISHED and the connection queued on the + * listener, the accept()ing process still asleep), which hangs rsync + * with no timeout to break it. Re-checking the queue on each pass + * turns a missed wakeup into another loop rather than a hang. */ + set_nonblocking(listener); + { + struct pollfd pfd; + time_t deadline = time(NULL) + SOCKETPAIR_ACCEPT_TIMEOUT; + int ready_but_empty = 0; + + pfd.fd = listener; + pfd.events = POLLIN; + while ((fd[0] = accept(listener, (struct sockaddr *)&sock2, &socklen)) == -1) { + int nready; + + if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) + goto failed; + /* Bound the wait by the clock, not by a count of passes: a + * signal on every pass must not extend it, and a poll() that + * returns at once must not consume it. */ + if (time(NULL) >= deadline) { + errno = ETIMEDOUT; + goto failed; + } + if (ready_but_empty) { + /* The listener said ready, yet accept() found nothing + * (the peer can reset first). Pause instead of + * spinning on a poll() that returns immediately. A + * signal only cuts the pause short -- the deadline + * above, rechecked every pass, is the bound. */ + if (poll(NULL, 0, SOCKETPAIR_ACCEPT_SLICE) < 0 && errno != EINTR) + goto failed; + ready_but_empty = 0; + continue; + } + pfd.revents = 0; + nready = poll(&pfd, 1, SOCKETPAIR_ACCEPT_SLICE); + if (nready < 0 && errno != EINTR) + goto failed; + ready_but_empty = nready > 0; + } + } + /* BSD gives the accepted fd the listener's non-blocking flag. */ + set_blocking(fd[0]); close(listener); listener = -1; @@ -788,16 +998,41 @@ goto failed; } + /* Confirm that the connection we accepted is the one we just made, and + * not one a local attacker raced in on the loopback listener before our + * own connect() completed. The peer of the accepted end (fd[0]) must be + * the local address of our connecting end (fd[1]), and both must be + * loopback. If they differ, someone else connected first; fail closed. */ + { + /* {0}: the analyzer doesn't model getpeername/getsockname filling these. */ + struct sockaddr_in accepted_peer = {0}, our_local = {0}; + socklen_t plen = sizeof accepted_peer; + socklen_t llen = sizeof our_local; + + if (getpeername(fd[0], (struct sockaddr *)&accepted_peer, &plen) != 0 + || getsockname(fd[1], (struct sockaddr *)&our_local, &llen) != 0 + || accepted_peer.sin_family != AF_INET + || our_local.sin_family != AF_INET + || accepted_peer.sin_addr.s_addr != htonl(INADDR_LOOPBACK) + || our_local.sin_addr.s_addr != htonl(INADDR_LOOPBACK) + || accepted_peer.sin_port != our_local.sin_port) { + errno = EPERM; + goto failed; + } + } + /* all OK! */ return 0; failed: + save_errno = errno; /* keep it: a failing close() would overwrite it */ if (fd[0] != -1) close(fd[0]); if (fd[1] != -1) close(fd[1]); if (listener != -1) close(listener); + errno = save_errno; return -1; } diff -Nru rsync-3.4.1+ds1/support/git-set-file-times rsync-3.5.0+ds1/support/git-set-file-times --- rsync-3.4.1+ds1/support/git-set-file-times 2022-04-11 15:57:19.000000000 +0000 +++ rsync-3.5.0+ds1/support/git-set-file-times 2026-08-13 00:05:47.000000000 +0000 @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import os, re, argparse, subprocess -from datetime import datetime +from datetime import datetime, timezone NULL_COMMIT_RE = re.compile(r'\0\0commit [a-f0-9]{40}$|\0$') @@ -74,7 +74,7 @@ if args.list > 1: ts = str(commit_time).rjust(10) else: - ts = datetime.utcfromtimestamp(commit_time).strftime("%Y-%m-%d %H:%M:%S") + ts = datetime.fromtimestamp(commit_time, timezone.utc).strftime("%Y-%m-%d %H:%M:%S") chg = '.' if mtime == commit_time else '*' print(chg, ts, fn) diff -Nru rsync-3.4.1+ds1/support/rrsh.sh rsync-3.5.0+ds1/support/rrsh.sh --- rsync-3.4.1+ds1/support/rrsh.sh 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/support/rrsh.sh 2026-06-11 02:32:54.000000000 +0000 @@ -0,0 +1,21 @@ +#!/bin/sh +# abdiff helper: a "remote shell" that emulates an sshd forced-command of +# `rrsync DIR`. rsync invokes a remote shell as: +# [ssh-opts] +# so when used as -e "sh rrsh.sh " rsync calls us as: +# sh rrsh.sh [opts] lh rsync --server ... +# We hand the server command to rrsync via SSH_ORIGINAL_COMMAND (exactly as +# sshd would) and exec the restricted wrapper, so abdiff can A/B the rrsync +# path itself. Only the pretend hosts "lh"/"localhost" are accepted. +RRSYNC="$1"; DIR="$2"; shift 2 +while [ $# -gt 0 ]; do + case "$1" in + -l) shift 2 ;; + lh|localhost) shift; break ;; + -*) shift ;; + *) break ;; + esac +done +SSH_ORIGINAL_COMMAND="$*" +export SSH_ORIGINAL_COMMAND +exec "$RRSYNC" "$DIR" diff -Nru rsync-3.4.1+ds1/support/rrsync rsync-3.5.0+ds1/support/rrsync --- rsync-3.4.1+ds1/support/rrsync 2024-11-20 05:35:49.000000000 +0000 +++ rsync-3.5.0+ds1/support/rrsync 2026-08-02 19:35:03.000000000 +0000 @@ -46,11 +46,12 @@ 'compare-dest': 2, 'compress-choice': 1, 'compress-level': 1, + 'compress-threads': 1, 'copy-dest': 2, 'copy-devices': -1, 'copy-unsafe-links': 0, 'daemon': -1, - 'debug': 1, + 'debug': -1, 'delay-updates': 0, 'delete': 0, 'delete-after': 0, @@ -59,6 +60,7 @@ 'delete-during': 0, 'delete-excluded': 0, 'delete-missing-args': 0, + 'dirs': 0, 'existing': 0, 'fake-super': 0, 'files-from': 3, @@ -130,9 +132,66 @@ ### END of options data produced by the cull-options script. ### -import os, sys, re, argparse, glob, socket, time, subprocess +import os, sys, re, argparse, glob, socket, stat, time, subprocess from argparse import RawTextHelpFormatter +# Held open across exec so rsync inherits them. Each entry pins a path +# validated_arg() approved; the corresponding arg passed to rsync is +# rewritten to /proc/self/fd/N so rsync's path resolution cannot be +# race-flipped after rrsync's realpath check, closing the realpath-vs-exec +# TOCTOU. +pinned_fds = [] + +# Directory pins, keyed by (st_dev, st_ino), so a glob or a multi-arg command +# whose args share a parent inherits one fd rather than one per arg. +pinned_dirs = {} + +# Whether the client asked for --relative/-R, which decides how much of a +# sender arg rsync transmits as the file's name (see sender_pinned_arg). +client_relative = False + +# The inode-pin trick needs /proc/self/fd/N to be a Linux-style magic symlink +# whose readlink yields the open file's real path. macOS/BSD lack the directory +# entirely; Solaris HAS /proc/self/fd but its entries are not such symlinks (its +# readlink does not return the path), so an isdir() check is not enough -- probe +# the actual behaviour once against a known fd. Where it works we pin (and a +# later readlink failure is an anomaly that fails closed); where it does not we +# fall through to the unhardened path. +# +# A correct readlink is NOT sufficient evidence that the fd pins anything, and +# the two platforms that get this wrong fail in opposite directions: +# +# * NetBSD makes the entry a symlink for DIRECTORIES only -- readlink of a +# regular file's entry fails with EINVAL, so a directory-only probe claims +# support that is not there and every pull of a file dies in the post-pin +# check. +# * Cygwin's readlink returns the right path, but opening the magic link +# RE-RESOLVES it: rename the directory out from under a held fd and the +# magic link reaches the replacement. The pin silently protects nothing. +# +# Only the Linux kernel gives the inode-bound magic link this relies on, so +# require that explicitly and keep the runtime probes as a guard for Linux-like +# environments where /proc is absent or restricted (containers, seccomp). +def _probe_proc_self_fd(): + if not sys.platform.startswith(('linux', 'android')): + return False + + def resolves(path): + try: + fd = os.open(path, os.O_RDONLY) + except OSError: + return False + try: + return os.readlink('/proc/self/fd/%d' % fd) == os.path.realpath(path) + except OSError: + return False + finally: + os.close(fd) + + return resolves('/') and resolves(os.path.realpath(__file__)) + +HAVE_PROC_SELF_FD = _probe_proc_self_fd() + try: from braceexpand import braceexpand except: @@ -142,6 +201,275 @@ LONG_OPT_RE = re.compile(r'^--([^=]+)(?:=(.*))?$') DE_BACKSLASH_RE = re.compile(r'\\(.)') +def make_inheritable(fd): + """Clear FD_CLOEXEC so the exec'd rsync inherits `fd`. + + os.set_inheritable() prefers ioctl(FIONCLEX), which an O_PATH descriptor + rejects with EBADF on older kernels; F_SETFD is one of the few operations + O_PATH always allows. + """ + try: + os.set_inheritable(fd, True) + except OSError: + import fcntl + fcntl.fcntl(fd, fcntl.F_SETFD, + fcntl.fcntl(fd, fcntl.F_GETFD) & ~fcntl.FD_CLOEXEC) + +def pin_dir(path, orig_arg): + """Inode-pin a directory and return an fd rsync will inherit. + + The open resolves `path` normally -- including a symlink at its last + component, which is legitimate and which 3.4.4 accepts -- so a component + could be flipped first. The readlink check afterwards is what makes that + safe: it proves the inode we ended up holding is inside the restricted + tree. From then on the fd names that inode, not the path, so nothing above + it can be flipped again. + + O_PATH, not O_RDONLY: reaching a known name beneath a directory needs only + search permission, and a mode 0111 parent is a perfectly ordinary way to + publish a file without letting it be listed. An O_PATH directory fd is + just as firmly pinned when used as a /proc/self/fd/N/... prefix (the O_PATH + caveat in validated_arg() is about reopening the magic link as the file + itself, which is not what happens here). + """ + flags = os.O_DIRECTORY | getattr(os, 'O_PATH', 0) + if not flags & getattr(os, 'O_PATH', 0): + flags |= os.O_RDONLY + try: + fd = os.open(path or '.', flags) + except OSError as e: + die('unable to pin sender path:', orig_arg, e.strerror) + try: + st = os.fstat(fd) + pinned_path = os.readlink('/proc/self/fd/%d' % fd) + except OSError as e: + os.close(fd) + die('post-pin readlink failed (race?):', orig_arg, e.strerror) + if pinned_path != args.dir and not pinned_path.startswith(args.dir_slash): + os.close(fd) + die('post-pin path escaped tree (race?):', orig_arg, pinned_path) + key = (st.st_dev, st.st_ino) + if key in pinned_dirs: + os.close(fd) + return pinned_dirs[key] + make_inheritable(fd) + pinned_fds.append(fd) + pinned_dirs[key] = fd + return fd + +# Checked receiver-side directory options, split by what rsync does with a +# missing one. It creates these itself on demand, so rrsync creates and pins +# them (0700 for the partial dir, which is what rsync uses -- partial files are +# incomplete copies of the peer's data and rsync deliberately does not publish +# them to the rest of the machine). +CREATE_DIR_MODE = {'--backup-dir': 0o777, '--partial-dir': 0o700} +# It requires this one to exist already, so a missing one stays an error. +MUST_EXIST_DIR_OPTS = ('--temp-dir',) +# And it only READS through these: a missing one is the ordinary first-run case +# and must keep working, so stand in an empty directory rather than refusing. +EMPTY_BASIS_OPTS = ('--link-dest', '--compare-dest', '--copy-dest') + +def pinned_empty_dir(orig_arg): + """Pin an empty unlinked directory to stand in for a missing basis dir. + + A basis lookup cannot tell an empty directory from a missing one, so this + preserves "first run has no basis" exactly -- but without leaving a name + the peer can win: the directory is created inside the restricted dir, + opened, then unlinked while we keep the fd, so what rsync is handed has no + path at all for an in-band symlink to take over. + """ + name = '.rrsync-empty-basis.%d' % os.getpid() + rootfd = os.open('.', os.O_RDONLY | os.O_DIRECTORY) + try: + os.mkdir(name, 0o700, dir_fd=rootfd) + except FileExistsError: + pass # our own leftover, or something planted; the open decides + except OSError as e: + os.close(rootfd) + die('unable to create basis placeholder:', orig_arg, e.strerror) + try: + fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY, + dir_fd=rootfd) + except OSError as e: + os.close(rootfd) + die('unable to pin basis placeholder:', orig_arg, e.strerror) + try: + os.rmdir(name, dir_fd=rootfd) + except FileNotFoundError: + pass # already gone; we hold the inode either way + except OSError as e: + # The whole point is that rsync gets an inode with no name. If the + # name survives, the peer can still reach and fill the directory, so + # this is not a placeholder we can safely hand over. + os.close(rootfd) + os.close(fd) + die('unable to detach basis placeholder:', orig_arg, e.strerror) + os.close(rootfd) + if os.listdir(fd): + os.close(fd) + die('basis placeholder is not empty:', orig_arg) + make_inheritable(fd) + pinned_fds.append(fd) + return '/proc/self/fd/%d' % fd + +def create_pinned_dir(path, orig_arg, mode): + """Create a missing receiver-option directory and return a pin of it. + + rsync makes --backup-dir/--partial-dir itself and then works inside it, so + the parent-pinned /proc/self/fd// spelling is not enough: the + same transfer can plant a symlink at first and rsync would create + through it, outside the restricted dir. Creating it here, beneath the + already-pinned parent, means the name is a real directory before rsync ever + looks at it, and the O_NOFOLLOW reopen proves we hold what we made rather + than something that raced in between. + """ + # Walk down from the restricted dir a component at a time, creating what + # is missing. rsync builds a whole missing --backup-dir hierarchy itself + # (backup.c make_bak_dir()), so stopping at "the immediate parent must + # exist" would refuse a first-use dated/nested name that works today. + # + # O_NOFOLLOW on every component costs nothing and rules out a symlink + # anywhere along the way: `path` is a realpath, so none of its components + # is legitimately a symlink, and each open is relative to the fd we are + # already holding rather than to a name that could be reshaped underneath. + fd = os.open('.', os.O_RDONLY | os.O_DIRECTORY) # the chdir'd restricted dir + for comp in os.path.relpath(path, args.dir).split(os.sep): + if comp in ('', '.', '..'): + os.close(fd) + die('bad receiver option path:', orig_arg) + try: + nfd = os.open(comp, os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY, + dir_fd=fd) + except FileNotFoundError: + try: + os.mkdir(comp, mode, dir_fd=fd) + except FileExistsError: + pass # raced in; the open below decides if it is usable + except OSError as e: + os.close(fd) + die('unable to create receiver option dir:', + orig_arg, e.strerror) + try: + nfd = os.open(comp, + os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY, + dir_fd=fd) + except OSError as e: + os.close(fd) + die('receiver option path is not a usable directory:', + orig_arg, e.strerror) + except OSError as e: + os.close(fd) + die('receiver option path is not a usable directory:', + orig_arg, e.strerror) + os.close(fd) + fd = nfd + try: + dpath = os.readlink('/proc/self/fd/%d' % fd) + except OSError as e: + os.close(fd) + die('post-pin readlink failed (race?):', orig_arg, e.strerror) + if dpath != args.dir and not dpath.startswith(args.dir_slash): + os.close(fd) + die('post-pin path escaped tree (race?):', orig_arg, dpath) + make_inheritable(fd) + pinned_fds.append(fd) + return '/proc/self/fd/%d' % fd + +# sender_pinned_arg() verdicts that are not a rewritten argument. +KEEP_LEAF_PIN = 'keep' # hand rsync the leaf's own /proc/self/fd/N +LEAF_PIN_UNUSABLE = 'unusable' # no pin for this shape; keep the plain name + +def sender_pinned_arg(fd, arg, orig_arg, has_slash, has_slash_dot): + """Return a source name rsync will both resolve safely and name correctly. + + The obvious rewrite -- hand rsync /proc/self/fd/N for the leaf itself -- + only works where rsync open()s the argument. A sender lstat()s it first, + and lstat of a procfs magic link is always S_IFLNK, so rsync describes the + argument as a symlink instead of sending the file. Which pin is usable + therefore depends on what rsync does with the argument: + + * a trailing "/" or "/." directory is opened, not lstat()ed, and rsync + DOES follow a symlink there, so it keeps the leaf pin; + * anything else keeps the pin one level up and passes the leaf by name. + rsync will not follow a symlink at that position (it sends the symlink + itself), the follow options that would change that are already disabled + for a restricted dir, and its own leaf open is O_NOFOLLOW. + + Under --relative the transmitted name is the whole argument rather than + its basename, so the pin has to move up to wherever that name starts and + the rest is spelled after a /./ marker, which is how rsync is told where + the transmitted portion begins. + """ + if client_relative: + # Look for the marker in the argument as the client spelled it: the + # caller has already split off a trailing "/" or "/.", which is exactly + # what turns "sub/./" into a terminal marker. + full = arg + ('/' if has_slash else '/.' if has_slash_dot else '') + # rsync honours the FIRST /./, so split there and keep the client's + # marker rather than inserting a second, earlier one. + head, marker, tail = full.partition('/./') + if marker: + anchor, suffix = head, tail.rstrip('/') + else: + anchor, suffix = '', arg + if suffix.startswith('/'): + return LEAF_PIN_UNUSABLE + if suffix in ('', '.'): + # A terminal marker: everything the client wants transmitted starts + # at the argument itself, which is the directory we already pinned. + # The caller re-appends the trailing "/" or "/.". + dfd = pin_dir(anchor, orig_arg) + check = '.' + pinned = '/proc/self/fd/%d/.' % dfd + else: + dfd = pin_dir(anchor, orig_arg) + pinned = '/proc/self/fd/%d/./%s' % (dfd, suffix) + check = suffix + else: + if has_slash or has_slash_dot: + return KEEP_LEAF_PIN + anchor, _, leaf = arg.rpartition('/') + if not leaf or leaf in ('.', '..'): + return LEAF_PIN_UNUSABLE + dfd = pin_dir(anchor, orig_arg) + pinned = '/proc/self/fd/%d/%s' % (dfd, leaf) + check = leaf + + # Tie the pinned directory to the inode realpath() validated: resolving + # `check` beneath the held fd cannot be redirected above the leaf, so if it + # does not reach the same file, something was flipped -- fail closed. + # fd is None for a leaf we deliberately never opened (a symlink, or a + # device/FIFO/socket): there is no inode to compare against, and the leaf + # was never going to be content-opened by the sender either. + if fd is None: + return pinned + try: + st = os.stat(check, dir_fd=dfd) + except OSError as e: + die('post-pin stat failed (race?):', orig_arg, e.strerror) + leaf_st = os.fstat(fd) + if (st.st_dev, st.st_ino) != (leaf_st.st_dev, leaf_st.st_ino): + die('post-pin path changed (race?):', orig_arg, check) + return pinned + +def safe_open_logfile(): + nofollow = getattr(os, 'O_NOFOLLOW', 0) + try: + st = os.lstat(LOGFILE) + except OSError: + return None + if not stat.S_ISREG(st.st_mode): + return None + try: + fd = os.open(LOGFILE, os.O_WRONLY | os.O_APPEND | nofollow) + except OSError: + return None + st2 = os.fstat(fd) + if not stat.S_ISREG(st2.st_mode) or st.st_dev != st2.st_dev or st.st_ino != st2.st_ino: + os.close(fd) + return None + return os.fdopen(fd, 'a') + def main(): if not os.path.isdir(args.dir): die("Restricted directory does not exist!") @@ -183,9 +511,21 @@ if args.ro: long_opts['log-file'] = -1 + global short_disabled + if args.no_overwrite: + # --ignore-existing guards only the live transfer destination. These + # options append to, consume, move or remove other existing objects in + # the restricted dir. Backup mode belongs here too: publishing a + # backup onto a name that already exists deletes what is there + # (backup.c make_backup()), and deletion backs files up as well + # (delete.c), so an unrelated --delete can land on a protected name. + long_opts['log-file'] = long_opts['partial-dir'] = long_opts['delay-updates'] = -1 + long_opts['backup-dir'] = -1 + short_disabled += 'b' # must precede the short_no_arg_re build below + if args.dir != '/': - global short_disabled short_disabled += short_disabled_subdir + long_opts['copy-unsafe-links'] = -1 short_no_arg_re = short_no_arg short_with_num_re = short_with_num @@ -197,13 +537,14 @@ short_no_arg_re = re.compile(r'^-(?=.)[%s]*(e\d*\.\w*)?$' % short_no_arg_re) short_with_num_re = re.compile(r'^-[%s]\d+$' % short_with_num_re) - log_fh = open(LOGFILE, 'a') if os.path.isfile(LOGFILE) else None + log_fh = safe_open_logfile() try: os.chdir(args.dir) except OSError as e: die('unable to chdir to restricted dir:', str(e)) + global client_relative rsync_opts = [ '--server' ] rsync_args = [ ] saw_the_dot_arg = False @@ -226,7 +567,16 @@ saw_the_dot_arg = True continue rsync_opts.append(arg) - if short_no_arg_re.match(arg) or short_with_num_re.match(arg): + sm = short_no_arg_re.match(arg) + if sm or short_with_num_re.match(arg): + if sm: + # Scan the cluster's own letters only: the trailing + # capability blob (-e.iLsfxC) is not a set of options. + letters = arg[1:] + if sm.group(1): + letters = letters[:-len(sm.group(1))] + if 'R' in letters: + client_relative = True continue disabled = False m = LONG_OPT_RE.match(arg) @@ -236,6 +586,11 @@ ct = long_opts.get(opt, None) if ct is None: break # Generate generic failure due to unfinished arg parsing + # Last one wins, matching rsync's own option handling. + if opt == 'relative': + client_relative = True + elif opt == 'no-relative': + client_relative = False if ct == 0: continue opt = '--' + opt @@ -260,6 +615,34 @@ if not saw_the_dot_arg: die("invalid rsync-command syntax or options") + if args.dir != '/' and not am_sender: + # A restricted dir denies device/special CREATION, but `-a` (-rlptgoD) + # bundles -D into the client's short-option string, so rejecting -D + # outright would break every `rsync -a` to a restricted rrsync. + # + # --no-D cannot do this job: preserve_devices/preserve_specials also + # frame the file list's rdev fields, and only this end of the + # connection gets the option, so the client's -D sender writes rdev + # that a --no-D receiver never reads. That desynchronises the list -- + # a FIFO hangs the transfer at protocol 29 and corrupts it at 30, a + # device node breaks EVERY protocol. --drop-D refuses the creation + # while leaving the wire format alone. (Needs rsync 3.5.0+, which is + # what rrsync is installed alongside.) + # + # Only on the receiving side: creation happens where files are + # written, so a sender has nothing to deny -- --drop-D would be a + # no-op there. + rsync_opts.append('--drop-D') + + if args.dir != '/': + # Filter rules travel over the protocol, not in the argv we validate, so + # a client can name a merge file outside the restricted dir and have the + # server read it in as rules -- a pull needs no --delete and no + # verbosity for that. --confine-root bounds the server's own resolution + # of such paths, which is the only end that can see them. Both + # directions: a dir-merge is read by whichever side the rule applies to. + rsync_opts.append('--confine-root=' + os.getcwd()) + if args.munge: rsync_opts.append('--munge-links') @@ -287,7 +670,10 @@ if args.no_lock: os.execlp(RSYNC, *cmd) die("execlp(", RSYNC, *cmd, ') failed') - child = subprocess.run(cmd) + # pass_fds keeps the inode-pinning O_PATH fds open across the spawn so + # /proc/self/fd/N in the cmd resolves correctly in the child. See the + # pinned_fds comment near the top. + child = subprocess.run(cmd, pass_fds=tuple(pinned_fds)) if child.returncode != 0: sys.exit(child.returncode) @@ -296,15 +682,24 @@ if opt != 'arg': # arg values already have their backslashes removed. arg = DE_BACKSLASH_RE.sub(r'\1', arg) + # "-" is rsync's read-the-list-from-stdin sentinel, not a pathname: a pull + # with a local --files-from sends exactly "--files-from=-" to the server. + if opt == '--files-from': + if arg == '-': + return arg + if args.wo: + die('a write-only server cannot read a remote --files-from path') + orig_arg = arg if arg.startswith('./'): arg = arg[1:] arg = arg.replace('//', '/') + is_absolute_arg = args.absolute and opt == 'arg' and args.dir != '/' and (arg == args.dir or arg.startswith(args.dir_slash)) + if not is_absolute_arg: + arg = arg.lstrip('/') if args.dir != '/': if HAS_DOT_DOT_RE.search(arg): die("do not use .. in", opt, "(anchor the path at the root of your restricted dir)") - if arg.startswith('/'): - arg = args.dir + arg if wild: got = glob.glob(arg) @@ -317,6 +712,7 @@ for arg in got: if args.dir != '/' and arg != '.' and (typ == 3 or (typ == 2 and not am_sender)): arg_has_trailing_slash = arg.endswith('/') + arg_has_trailing_slash_dot = False if arg_has_trailing_slash: arg = arg[:-1] else: @@ -325,12 +721,224 @@ arg = arg[:-2] real_arg = os.path.realpath(arg) if arg != real_arg and not real_arg.startswith(args.dir_slash): - die('unsafe arg:', orig_arg, [arg, real_arg]) + if not (is_absolute_arg and real_arg == args.dir): + die('unsafe arg:', orig_arg, [arg, real_arg]) + # Inode-pin the validated path so an attacker cannot flip a + # path component AFTER realpath validates it but BEFORE the + # exec'd rsync resolves it. + # + # CRITICAL: open with O_RDONLY (not O_PATH). An O_PATH fd + # holds a path/dentry reference and /proc/self/fd/N for an + # O_PATH fd re-resolves the path on open -- which means the + # race window stays open across the exec. A regular + # O_RDONLY fd holds an open file (inode-bound), and + # /proc/self/fd/N for a regular fd references the inode + # directly -- exactly the race-closing primitive we need. + # + # O_NOFOLLOW on this open means a symlink that raced into + # place between realpath and this open is refused at the + # leaf. A subsequent fstat() + readlink-of-fd verifies the + # pinned inode is still within the restricted tree (a + # parent-component race that landed on an in-tree symlink + # but outside-tree target would surface here). + # + # /proc/self/fd/N then routes the exec'd rsync's open + # through the kernel's magic link to the SAME pinned inode + # regardless of any subsequent flip; the race is closed. + # + # Linux-only (O_PATH/proc trick is Linux specific); on + # non-Linux fall through to the unhardened path. For paths + # that don't exist yet (receiver-side new dest) os.open + # fails -- we skip pinning there; the new-dest race is a + # separate concern. + # Only a regular file or directory gets its CONTENT opened. A + # sender needs neither for anything else: rsync transmits a symlink + # by its target string and skips a device/FIFO/socket under the + # forced --no-D. Opening them here is also actively wrong -- + # O_RDONLY on a FIFO blocks until a writer appears, so naming an + # in-tree FIFO wedged rrsync before exec, and a dangling symlink + # resolved to a missing target and was reported as a race. 3.4.4 + # transfers both. These shapes take the parent pin, which is what + # confines them anyway. + # NOT for a trailing "/" or "/." argument: rsync opens that one and + # DOES follow a symlink there, so its leaf pin is load-bearing -- + # rrsync-sender-leaf-flip proves a raced flip leaks the outside + # directory's content without it. + sender_leaf_unopened = False + # Not gated on HAVE_PROC_SELF_FD: this is a decision about what + # rsync does with the argument, not about whether we can pin it, so + # it has to hold on the BSDs, macOS, Solaris and Cygwin too -- where + # otherwise a dangling symlink still resolved to nothing and died. + if (am_sender and opt == 'arg' + and not arg_has_trailing_slash + and not arg_has_trailing_slash_dot): + try: + lst = os.lstat(arg) + except OSError: + lst = None + if lst is not None and not (stat.S_ISREG(lst.st_mode) + or stat.S_ISDIR(lst.st_mode)): + sender_leaf_unopened = True + try: + if sender_leaf_unopened: + raise InterruptedError() # jump to the sender-pin branch + try: + # O_NONBLOCK so a special file that raced in after the + # lstat above still cannot block this open. + fd = os.open(real_arg, + os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + except IsADirectoryError: + fd = os.open(real_arg, + os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY) + except InterruptedError: + # No CONTENT fd for this leaf -- but it is still named + # beneath a pinned directory below, not re-resolved from + # the tree root. + fd = None + except FileNotFoundError: + # In --sender mode the path MUST exist (we're reading + # from it) -- ENOENT here means the rename-based race + # caught a transient gap in the flipper's swap. Die. + if am_sender: + die('post-realpath open failed (race detected):', + orig_arg, 'No such file or directory') + if opt in MUST_EXIST_DIR_OPTS: + die('receiver option path does not exist:', orig_arg) + # Receiver-side new destination: the leaf has no inode to pin + # yet, but pin its existing PARENT directory and route the + # exec'd rsync's creation through /proc/self/fd//, + # so a parent-component flip after realpath can't redirect the + # new file/dir out of the tree. Linux-only (the /proc magic + # link); elsewhere, or if the parent itself doesn't exist yet + # (a deeper -R new path), fall through unpinned as before. + fd = None + leaf = os.path.basename(real_arg) + if opt in CREATE_DIR_MODE or opt in EMPTY_BASIS_OPTS: + # An auxiliary directory the peer can supply mid-transfer. + # The parent-pinned spelling below is not enough for these: + # the same transfer can plant a symlink at first, + # and rsync would create or read through it, outside the + # tree. Both answers below hand rsync an inode instead of + # a name, so without that primitive there is no safe way to + # proceed -- refuse rather than pass the name through. + if not HAVE_PROC_SELF_FD: + die('receiver option path does not exist:', orig_arg) + if opt in CREATE_DIR_MODE: + if not leaf or leaf in ('.', '..'): + die('bad receiver option path:', orig_arg) + arg = create_pinned_dir(real_arg, orig_arg, + CREATE_DIR_MODE[opt]) + else: + arg = pinned_empty_dir(orig_arg) + elif HAVE_PROC_SELF_FD and leaf and leaf not in ('.', '..'): + try: + pfd = os.open(os.path.dirname(real_arg) or '/', + os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY) + except OSError: + pfd = -1 + if pfd >= 0: + try: + ppath = os.readlink('/proc/self/fd/%d' % pfd) + except OSError as e: + os.close(pfd) + die('post-pin readlink failed (race?):', + orig_arg, e.strerror) + # The pinned parent must be the tree root or under it. + if ppath != args.dir and not ppath.startswith(args.dir_slash): + os.close(pfd) + die('post-pin path escaped tree (race?):', + orig_arg, ppath) + os.set_inheritable(pfd, True) + pinned_fds.append(pfd) + arg = '/proc/self/fd/%d/%s' % (pfd, leaf) + except OSError as e: + # ELOOP or anything else is a race signal: realpath + # validated the path moments ago, but the open just + # failed -- something flipped between the check and + # the pin (typically a symlink-flip on the leaf). + die('post-realpath open failed (race detected):', + orig_arg, e.strerror) + if am_sender and opt == 'arg': + logical = arg + if is_absolute_arg: + if logical == args.dir: + logical = '' + elif logical.startswith(args.dir_slash): + logical = logical[args.dir_slash_len:] + if fd is None and sender_leaf_unopened: + # A leaf we deliberately never opened is still spelled beneath + # a pinned directory: leaving the bare name for rsync to + # re-resolve puts every component back in play, which is + # CVE-2026-53783 -- measured at 3 leaks in 83 raced pulls with + # a dangling-symlink leaf whose parent was flipped to point + # outside the tree. It costs nothing here: the directory is + # opened O_PATH, so the special file itself is never opened + # and a FIFO cannot block, and whatever the leaf turns into + # afterwards is reached only from beneath the held one. + # + # WHICH directory is sender_pinned_arg()'s decision, and it is + # the immediate parent for every shape EXCEPT a --relative + # argument with no client "/./": there the whole argument is + # the transmitted name, so only the anchor it starts from can + # be pinned and the components below it stay raceable. That + # --relative limit predates this and is stated in NEWS. + if HAVE_PROC_SELF_FD: + pinned = sender_pinned_arg(None, logical, orig_arg, + arg_has_trailing_slash, + arg_has_trailing_slash_dot) + if pinned != LEAF_PIN_UNUSABLE: + arg = pinned + elif fd is not None: + # The inode-pin trick (verify + route the exec'd rsync's open via + # the /proc/self/fd magic link) is Linux-only. Where /proc/self/fd + # does not exist at all (the BSDs, Solaris, macOS, Cygwin, or a + # /proc-less namespace) we cannot pin -- fall through to the + # unhardened path (close the fd, keep the realpath-validated arg) + # per the design note above. But where /proc/self/fd DOES exist + # (Linux), a readlink failure is an anomaly (sandbox/seccomp), not + # a no-proc platform: fail CLOSED rather than silently unharden. + if not HAVE_PROC_SELF_FD: + os.close(fd) # no /proc/self/fd: run unpinned + else: + try: + pinned_path = os.readlink('/proc/self/fd/%d' % fd) + except OSError as e: + os.close(fd) + die('post-pin readlink failed (race?):', + orig_arg, e.strerror) + # The pinned inode must live under args.dir_slash (or BE + # args.dir). Catches a parent-component flip that landed + # inside an in-tree path but pointed outside. + if (not pinned_path.startswith(args.dir_slash) + and pinned_path != args.dir): + os.close(fd) + die('post-pin path escaped tree (race?):', + orig_arg, pinned_path) + if am_sender and opt == 'arg': + pinned = sender_pinned_arg(fd, logical, orig_arg, + arg_has_trailing_slash, + arg_has_trailing_slash_dot) + else: + pinned = KEEP_LEAF_PIN + if pinned == KEEP_LEAF_PIN: + os.set_inheritable(fd, True) + pinned_fds.append(fd) + arg = '/proc/self/fd/%d' % fd + elif pinned == LEAF_PIN_UNUSABLE: + # Nothing to spell beneath a held directory (a bare "." + # or the tree root): keep the realpath-validated name, + # which is what 3.4.4 passes. + os.close(fd) + else: + os.close(fd) # only needed to validate the pin + arg = pinned if arg_has_trailing_slash: arg += '/' elif arg_has_trailing_slash_dot: arg += '/.' - if opt == 'arg' and arg.startswith(args.dir_slash): + if is_absolute_arg and arg == args.dir: + arg = '.' + elif opt == 'arg' and arg.startswith(args.dir_slash): arg = arg[args.dir_slash_len:] if arg == '': arg = '.' @@ -340,13 +948,20 @@ def lock_or_die(dirname): - import fcntl + import fcntl, errno global lock_handle lock_handle = os.open(dirname, os.O_RDONLY) try: fcntl.flock(lock_handle, fcntl.LOCK_EX | fcntl.LOCK_NB) - except: - die('Another instance of rrsync is already accessing this directory.') + except OSError as e: + if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN, errno.EACCES): + die('Another instance of rrsync is already accessing this directory.') + # flock() is unavailable on this fd/platform -- e.g. Solaris returns + # EBADF for flock() on a directory fd. The single-run lock is a + # best-effort convenience (cf. -no-lock), not a security control, so + # proceed without it rather than abort every transfer. + os.close(lock_handle) + lock_handle = None def die(*msg): @@ -369,6 +984,7 @@ only_group.add_argument('-ro', action='store_true', help="Allow only reading from the DIR. Implies -no-del and -no-lock.") only_group.add_argument('-wo', action='store_true', help="Allow only writing to the DIR.") arg_parser.add_argument('-munge', action='store_true', help="Enable rsync's --munge-links on the server side.") + arg_parser.add_argument('-absolute', action='store_true', help="Allow transfer args to use absolute server paths under DIR.") arg_parser.add_argument('-no-del', action='store_true', help="Disable rsync's --delete* and --remove* options.") arg_parser.add_argument('-no-lock', action='store_true', help="Avoid the single-run (per-user) lock check.") arg_parser.add_argument('-no-overwrite', action='store_true', help="Prevent overwriting existing files by enforcing --ignore-existing") diff -Nru rsync-3.4.1+ds1/support/rrsync.1.md rsync-3.5.0+ds1/support/rrsync.1.md --- rsync-3.4.1+ds1/support/rrsync.1.md 2024-11-20 05:32:18.000000000 +0000 +++ rsync-3.5.0+ds1/support/rrsync.1.md 2026-07-31 03:18:39.000000000 +0000 @@ -5,7 +5,7 @@ ## SYNOPSIS ``` -rrsync [-ro|-wo] [-munge] [-no-del] [-no-lock] [-no-overwrite] DIR +rrsync [-ro|-wo] [-munge] [-absolute] [-no-del] [-no-lock] [-no-overwrite] DIR ``` The single non-option argument specifies the restricted _DIR_ to use. It can be @@ -77,6 +77,12 @@ Enable rsync's [`--munge-links`](rsync.1#opt) on the server side. +0. `-absolute` + + Allow file-transfer arguments to name the restricted directory using its + absolute server path. For example, with `rrsync -absolute /path/to/root`, + the transfer arg `/path/to/root/dir1` is accepted as an alias for `dir1`. + 0. `-no-del` Disable rsync's `--delete*` and `--remove*` options. @@ -90,6 +96,14 @@ Enforce `--ignore-existing` on the server. Prevents overwriting existing files when the server is the receiver. + Because `--ignore-existing` protects only the file being transferred, this + also refuses the options that can reach a *different* existing file in the + restricted dir: `--log-file`, `--partial-dir`, `--delay-updates`, and + backup mode (`-b`, `--backup-dir`, whose published backup replaces whatever + already occupies the backup name). Resumable uploads with an explicit + `--partial-dir`, `--delay-updates`, and server-side logging are therefore + unavailable under this option. + 0. `-help`, `-h` Output this help message and exit. diff -Nru rsync-3.4.1+ds1/support/rsync-no-vanished rsync-3.5.0+ds1/support/rsync-no-vanished --- rsync-3.4.1+ds1/support/rsync-no-vanished 2021-11-13 18:39:09.000000000 +0000 +++ rsync-3.5.0+ds1/support/rsync-no-vanished 2025-08-23 07:31:52.000000000 +0000 @@ -2,7 +2,7 @@ REAL_RSYNC=/usr/bin/rsync IGNOREEXIT=24 -IGNOREOUT='^(file has vanished: |rsync warning: some files vanished before they could be transferred)' +IGNOREOUT='^((file|directory) has vanished: |rsync warning: some files vanished before they could be transferred)' # If someone installs this as "rsync", make sure we don't affect a server run. for arg in "${@}"; do diff -Nru rsync-3.4.1+ds1/syscall.c rsync-3.5.0+ds1/syscall.c --- rsync-3.4.1+ds1/syscall.c 2025-01-15 19:43:57.000000000 +0000 +++ rsync-3.5.0+ds1/syscall.c 2026-08-02 19:35:03.000000000 +0000 @@ -22,8 +22,18 @@ #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 +#include /* for the socket+bind() fallback in do_mknod() */ #endif #ifdef HAVE_SYS_ATTR_H #include @@ -33,6 +43,10 @@ #include #endif +#ifdef __linux__ +#include /* SYS_fchmodat2 / SYS_fallocate raw-syscall wrappers */ +#endif + #include "ifuncs.h" extern int dry_run; @@ -42,11 +56,548 @@ 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 @@ -81,6 +632,11 @@ #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; @@ -88,11 +644,86 @@ 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 @@ -112,6 +743,120 @@ 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) { @@ -123,7 +868,7 @@ close(fd); return len; } - if (errno != ELOOP) + 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. */ @@ -135,6 +880,30 @@ 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 @@ -142,28 +911,268 @@ { 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) { @@ -173,11 +1182,20 @@ return 0; } -#if !defined MKNOD_CREATES_FIFOS && defined HAVE_MKFIFO + /* 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 -#if !defined MKNOD_CREATES_SOCKETS && defined HAVE_SYS_UN_H +#ifdef HAVE_SYS_UN_H if (S_ISSOCK(mode)) { int sock; struct sockaddr_un saddr; @@ -204,12 +1222,171 @@ } #endif #ifdef HAVE_MKNOD - return mknod(pathname, mode, dev); + 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; @@ -217,8 +1394,76 @@ 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; @@ -232,6 +1477,91 @@ 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) { @@ -240,6 +1570,7 @@ if (dry_run) return 0; RETURN_ERROR_IF_RO_OR_LO; + RETURN_ERROR_IF_NULL(path); switch (switch_step) { #ifdef HAVE_LCHMOD @@ -276,6 +1607,237 @@ 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) @@ -285,6 +1847,166 @@ 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) { @@ -323,10 +2045,87 @@ { 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) { @@ -360,6 +2159,7 @@ int do_stat(const char *path, STRUCT_STAT *st) { + RETURN_ERROR_IF_NULL(path); #ifdef USE_STAT64_FUNCS return stat64(path, st); #else @@ -369,6 +2169,7 @@ 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); @@ -380,6 +2181,94 @@ #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 @@ -407,6 +2296,19 @@ 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; @@ -429,6 +2331,14 @@ 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; @@ -448,6 +2358,10 @@ 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; @@ -480,7 +2394,7 @@ free(pathw); if (handle == INVALID_HANDLE_VALUE) return -1; - int64 temp_time = Int32x32To64(crtime, 10000000) + 116444736000000000LL; + int64 temp_time = (crtime * 10000000LL) + 116444736000000000LL; FILETIME birth_time; birth_time.dwLowDateTime = (DWORD)temp_time; birth_time.dwHighDateTime = (DWORD)(temp_time >> 32); @@ -498,6 +2412,7 @@ 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 @@ -513,6 +2428,81 @@ #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 @@ -598,7 +2588,13 @@ OFF_T do_fallocate(int fd, OFF_T offset, OFF_T length) { - int opts = inplace || preallocate_files ? DO_FALLOC_OPTIONS : 0; + /* 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; @@ -623,7 +2619,14 @@ return length; return st.st_blocks * S_BLKSIZE; } - return 0; + /* 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 @@ -683,6 +2686,11 @@ #endif } +#ifdef O_NOATIME + if (open_noatime) + flags |= O_NOATIME; +#endif + #ifdef O_NOFOLLOW fd = open(pathname, flags|O_NOFOLLOW); #else @@ -715,82 +2723,726 @@ /* 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, and the relpath must not contain any - elements in the path which follow symlinks (ie. like O_NOFOLLOW, but - applies to all path components, not just the last component) + 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). */ -int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode) -{ - if (!relpath || relpath[0] == '/') { - // must be a relative path - errno = EINVAL; + +/* 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; } - if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../")) { - // no ../ elements allowed in the relpath - errno = EINVAL; + 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; +} -#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); +/* 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; } - char fullpath[MAXPATHLEN]; - pathjoin(fullpath, sizeof fullpath, basedir, relpath); - return open(fullpath, flags, mode); -#else - int dirfd = AT_FDCWD; - if (basedir != NULL) { - dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); - if (dirfd == -1) { + + 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; - char *path_copy = my_strdup(relpath, __FILE__, __LINE__); + 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; } - - for (const char *part = strtok(path_copy, "/"); + + /* 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(NULL, "/")) + part = strtok_r(NULL, "/", &psave)) { - int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); - if (next_fd == -1 && errno == ENOTDIR) { - if (strtok(NULL, "/") != NULL) { - // this is not the last component of the path - errno = ELOOP; + 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; } - // this could be the last component of the path, try as a file - retfd = openat(dirfd, part, flags | O_NOFOLLOW, mode); + 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; } - if (next_fd == -1) { + + /* 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; } - if (dirfd != AT_FDCWD) close(dirfd); - dirfd = next_fd; } - // the path must be a directory - errno = EINVAL; + /* 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); - if (dirfd != AT_FDCWD) { - close(dirfd); + 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 @@ -808,3 +3460,516 @@ } 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 -Nru rsync-3.4.1+ds1/t_acl.c rsync-3.5.0+ds1/t_acl.c --- rsync-3.4.1+ds1/t_acl.c 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/t_acl.c 2026-07-20 04:05:31.000000000 +0000 @@ -0,0 +1,532 @@ +/* + * Unit test for lib/acl.c. + * + * Validates the fd- and at-based POSIX ACL get/set/delete in lib/acl.c against + * the system libacl, used here as an oracle: we set an ACL via one and read it + * back via the other (both directions), round-trip through lib/acl.c, and check + * the default-ACL and delete paths. We deliberately do NOT try to reproduce + * libacl's symlink-following races -- we only compare functional behaviour. + * + * Not linked into rsync itself. Exits 0 if all checks pass, 1 on any failure, + * 77 to skip (built without SUPPORT_ACL_FD, no libacl, or a scratch filesystem + * without ACL support). + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +#include "rsync.h" +#include "lib/acl.h" + +#include + +#ifndef SUPPORT_ACL_FD + +int main(int argc, char *argv[]) +{ + (void)argc; + (void)argv; + fprintf(stderr, "t_acl: built without SUPPORT_ACL_FD -- skipping\n"); + return 77; +} + +#else + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_ACL_LIBACL_H +#include /* acl_get_perm() */ +#endif + +#define MAX_ENT 16 + +static int errs = 0; +static const char *scratch; + +static void ok(int cond, const char *label) +{ + if (cond) + fprintf(stderr, "OK %s\n", label); + else { + fprintf(stderr, "FAIL %s\n", label); + errs++; + } +} + +static void dump_entries(const char *pfx, const rsync_acl_ent *e, int n) +{ + int i; + for (i = 0; i < n; i++) + fprintf(stderr, " %s tag=0x%02x perm=%o id=%u\n", + pfx, e[i].tag, e[i].perm, + e[i].id == RACL_UNDEFINED_ID ? (unsigned)-1 : e[i].id); +} + +static int ent_cmp(const void *a, const void *b) +{ + const rsync_acl_ent *x = a, *y = b; + if (x->tag != y->tag) + return x->tag < y->tag ? -1 : 1; + if (x->id != y->id) + return x->id < y->id ? -1 : 1; + return 0; +} + +static void canon(rsync_acl_ent *e, int n) +{ + if (n > 1) + qsort(e, n, sizeof e[0], ent_cmp); +} + +static int entries_equal(rsync_acl_ent *a, int na, rsync_acl_ent *b, int nb) +{ + int i; + canon(a, na); + canon(b, nb); + if (na != nb) + return 0; + for (i = 0; i < na; i++) { + if (a[i].tag != b[i].tag || a[i].perm != b[i].perm) + return 0; + if ((a[i].tag == RACL_USER || a[i].tag == RACL_GROUP) + && a[i].id != b[i].id) + return 0; + } + return 1; +} + +/* === libacl oracle helpers === */ + +static uint16_t racl_from_tag(acl_tag_t tag) +{ + switch (tag) { + case ACL_USER_OBJ: return RACL_USER_OBJ; + case ACL_USER: return RACL_USER; + case ACL_GROUP_OBJ: return RACL_GROUP_OBJ; + case ACL_GROUP: return RACL_GROUP; + case ACL_MASK: return RACL_MASK; + case ACL_OTHER: return RACL_OTHER; + } + return 0; +} + +static acl_tag_t tag_from_racl(uint16_t tag) +{ + switch (tag) { + case RACL_USER_OBJ: return ACL_USER_OBJ; + case RACL_USER: return ACL_USER; + case RACL_GROUP_OBJ: return ACL_GROUP_OBJ; + case RACL_GROUP: return ACL_GROUP; + case RACL_MASK: return ACL_MASK; + case RACL_OTHER: return ACL_OTHER; + } + return 0; +} + +/* Convert a libacl acl_t to our neutral entry array. Returns count or -1. */ +static int libacl_to_entries(acl_t acl, rsync_acl_ent *out, int max) +{ + acl_entry_t e; + int n = 0, r; + + for (r = acl_get_entry(acl, ACL_FIRST_ENTRY, &e); r == 1; + r = acl_get_entry(acl, ACL_NEXT_ENTRY, &e)) { + acl_tag_t tag; + acl_permset_t ps; + uint16_t perm = 0; + + if (n >= max) + return -1; + if (acl_get_tag_type(e, &tag) != 0 || acl_get_permset(e, &ps) != 0) + return -1; + if (acl_get_perm(ps, ACL_READ) > 0) + perm |= 4; + if (acl_get_perm(ps, ACL_WRITE) > 0) + perm |= 2; + if (acl_get_perm(ps, ACL_EXECUTE) > 0) + perm |= 1; + out[n].tag = racl_from_tag(tag); + out[n].perm = perm; + out[n].id = RACL_UNDEFINED_ID; + if (tag == ACL_USER || tag == ACL_GROUP) { + void *q = acl_get_qualifier(e); + if (q) { + out[n].id = *(id_t *)q; + acl_free(q); + } + } + n++; + } + if (r < 0) + return -1; + canon(out, n); + return n; +} + +/* Build a libacl acl_t from our neutral entries. */ +static acl_t entries_to_libacl(const rsync_acl_ent *ents, int n) +{ + acl_t acl = acl_init(n); + int i; + + if (!acl) + return NULL; + for (i = 0; i < n; i++) { + acl_entry_t e; + acl_permset_t ps; + if (acl_create_entry(&acl, &e) != 0) + goto fail; + if (acl_set_tag_type(e, tag_from_racl(ents[i].tag)) != 0) + goto fail; + if (acl_get_permset(e, &ps) != 0 || acl_clear_perms(ps) != 0) + goto fail; + if ((ents[i].perm & 4) && acl_add_perm(ps, ACL_READ) != 0) + goto fail; + if ((ents[i].perm & 2) && acl_add_perm(ps, ACL_WRITE) != 0) + goto fail; + if ((ents[i].perm & 1) && acl_add_perm(ps, ACL_EXECUTE) != 0) + goto fail; + if (acl_set_permset(e, ps) != 0) + goto fail; + if (ents[i].tag == RACL_USER || ents[i].tag == RACL_GROUP) { + id_t id = ents[i].id; + if (acl_set_qualifier(e, &id) != 0) + goto fail; + } + } + return acl; + fail: + acl_free(acl); + return NULL; +} + +static int libacl_get(const char *path, acl_type_t type, rsync_acl_ent *out, int max) +{ + acl_t acl = acl_get_file(path, type); + int n; + if (!acl) + return -1; + n = libacl_to_entries(acl, out, max); + acl_free(acl); + return n; +} + +static int libacl_set(const char *path, acl_type_t type, const rsync_acl_ent *ents, int n) +{ + acl_t acl = entries_to_libacl(ents, n); + int rc; + if (!acl) + return -1; + rc = acl_set_file(path, type, acl); + acl_free(acl); + return rc; +} + +/* === scratch helpers === */ + +static char *acl_path(const char *name) +{ + static char buf[4096]; + snprintf(buf, sizeof buf, "%s/%s", scratch, name); + return buf; +} + +static int acl_mkfile(const char *name) +{ + char *p = acl_path(name); + int fd = open(p, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd >= 0) + close(fd); + return fd < 0 ? -1 : 0; +} + +static int acl_mkdir(const char *name) +{ + return mkdir(acl_path(name), 0755); +} + +/* Open a held fd the way set_file_attrs does (REG/DIR, NOFOLLOW). */ +static int open_held(const char *name) +{ + return open(acl_path(name), O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC); +} + +/* === comparison tests for one ACL shape === */ + +static void cmp_via_fd(const char *name, int want_default, + const rsync_acl_ent *ents, int n, const char *tag) +{ + char label[256]; + char *path = acl_path(name); + acl_type_t type = want_default ? ACL_TYPE_DEFAULT : ACL_TYPE_ACCESS; + rsync_acl_ent got[MAX_ENT], oracle[MAX_ENT]; + rsync_acl_ent *lib_ents = NULL; + int gc = 0, oc, fd; + + /* 1. set via lib (fd) -> read via libacl */ + fd = open_held(name); + snprintf(label, sizeof label, "%s: open held fd", tag); + ok(fd >= 0, label); + if (fd < 0) + return; + snprintf(label, sizeof label, "%s: xacl_set_fd", tag); + ok(xacl_set_fd(fd, want_default, ents, n) == 0, label); + oc = libacl_get(path, type, oracle, MAX_ENT); + memcpy(got, ents, n * sizeof ents[0]); + snprintf(label, sizeof label, "%s: libacl reads back what xacl_set_fd wrote", tag); + if (!entries_equal(got, n, oracle, oc)) { + dump_entries("set ", got, n); + dump_entries("got ", oracle, oc < 0 ? 0 : oc); + } + ok(oc == n && entries_equal(got, n, oracle, oc), label); + + /* 2. set via libacl -> read via lib (fd) */ + snprintf(label, sizeof label, "%s: libacl_set", tag); + ok(libacl_set(path, type, ents, n) == 0, label); + snprintf(label, sizeof label, "%s: xacl_get_fd reads back what libacl wrote", tag); + if (xacl_get_fd(fd, want_default, &lib_ents, &gc) == 0) { + memcpy(got, ents, n * sizeof ents[0]); + if (!entries_equal(got, n, lib_ents, gc)) { + dump_entries("set ", got, n); + dump_entries("got ", lib_ents, gc); + } + ok(gc == n && entries_equal(got, n, lib_ents, gc), label); + } else + ok(0, label); + if (lib_ents) + free(lib_ents); + lib_ents = NULL; + + /* 3. round-trip lib set -> lib get */ + snprintf(label, sizeof label, "%s: xacl_set_fd/xacl_get_fd round-trip", tag); + if (xacl_set_fd(fd, want_default, ents, n) == 0 + && xacl_get_fd(fd, want_default, &lib_ents, &gc) == 0) { + memcpy(got, ents, n * sizeof ents[0]); + ok(gc == n && entries_equal(got, n, lib_ents, gc), label); + } else + ok(0, label); + if (lib_ents) + free(lib_ents); + + close(fd); +} + +static void cmp_via_at(const char *name, int want_default, + const rsync_acl_ent *ents, int n, const char *tag) +{ + char label[256]; + char *path = acl_path(name); + acl_type_t type = want_default ? ACL_TYPE_DEFAULT : ACL_TYPE_ACCESS; + rsync_acl_ent got[MAX_ENT], oracle[MAX_ENT]; + rsync_acl_ent *lib_ents = NULL; + int gc = 0, oc, dirfd; + + dirfd = open(scratch, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + snprintf(label, sizeof label, "%s: open scratch dirfd", tag); + ok(dirfd >= 0, label); + if (dirfd < 0) + return; + + snprintf(label, sizeof label, "%s: xacl_set_at", tag); + ok(xacl_set_at(dirfd, name, want_default, ents, n) == 0, label); + oc = libacl_get(path, type, oracle, MAX_ENT); + memcpy(got, ents, n * sizeof ents[0]); + snprintf(label, sizeof label, "%s: libacl reads back what xacl_set_at wrote", tag); + ok(oc == n && entries_equal(got, n, oracle, oc), label); + + snprintf(label, sizeof label, "%s: libacl_set", tag); + ok(libacl_set(path, type, ents, n) == 0, label); + snprintf(label, sizeof label, "%s: xacl_get_at reads back what libacl wrote", tag); + if (xacl_get_at(dirfd, name, want_default, &lib_ents, &gc) == 0) { + memcpy(got, ents, n * sizeof ents[0]); + ok(gc == n && entries_equal(got, n, lib_ents, gc), label); + } else + ok(0, label); + if (lib_ents) + free(lib_ents); + + close(dirfd); +} + +/* === scenarios === */ + +#define U(p) { RACL_USER_OBJ, p, RACL_UNDEFINED_ID } +#define G(p) { RACL_GROUP_OBJ, p, RACL_UNDEFINED_ID } +#define M(p) { RACL_MASK, p, RACL_UNDEFINED_ID } +#define O(p) { RACL_OTHER, p, RACL_UNDEFINED_ID } +#define NU(i,p) { RACL_USER, p, i } +#define NG(i,p) { RACL_GROUP, p, i } + +static const rsync_acl_ent A1[] = { U(6), NU(12345,4), G(4), M(6), O(4) }; +static const rsync_acl_ent A2[] = { U(7), NU(1000,6), NU(2000,5), G(5), NG(100,4), NG(200,1), M(7), O(0) }; +static const rsync_acl_ent A3[] = { U(7), NU(4000000000U,5), G(5), M(7), O(4) }; +static const rsync_acl_ent A4[] = { U(7), NU(0,0), G(0), M(4), O(0) }; + +static const rsync_acl_ent D1[] = { U(7), G(5), O(5) }; +static const rsync_acl_ent D2[] = { U(7), NU(1000,6), G(5), NG(100,4), M(6), O(0) }; + +#define NELEM(a) ((int)(sizeof (a) / sizeof (a)[0])) + +static void run_access(const char *base, const rsync_acl_ent *e, int n, const char *tag) +{ + char fdname[128], atname[128]; + char fdtag[160], attag[160]; + + snprintf(fdname, sizeof fdname, "%s_fd", base); + snprintf(atname, sizeof atname, "%s_at", base); + snprintf(fdtag, sizeof fdtag, "access %s [fd]", tag); + snprintf(attag, sizeof attag, "access %s [at]", tag); + + if (acl_mkfile(fdname) == 0) + cmp_via_fd(fdname, 0, e, n, fdtag); + if (xacl_at_available()) { + if (acl_mkfile(atname) == 0) + cmp_via_at(atname, 0, e, n, attag); + } +} + +static void run_default(const char *base, const rsync_acl_ent *e, int n, const char *tag) +{ + char fdname[128], atname[128]; + char fdtag[160], attag[160]; + + snprintf(fdname, sizeof fdname, "%s_fd", base); + snprintf(atname, sizeof atname, "%s_at", base); + snprintf(fdtag, sizeof fdtag, "default %s [fd]", tag); + snprintf(attag, sizeof attag, "default %s [at]", tag); + + if (acl_mkdir(fdname) == 0) + cmp_via_fd(fdname, 1, e, n, fdtag); + if (xacl_at_available()) { + if (acl_mkdir(atname) == 0) + cmp_via_at(atname, 1, e, n, attag); + } +} + +/* delete of a default ACL + the "no explicit ACL" / errno behaviour */ +static void run_misc(void) +{ + rsync_acl_ent *ents = NULL; + int n = 0, fd; + char *p; + + /* default-ACL delete */ + if (acl_mkdir("deldir") == 0) { + fd = open_held("deldir"); + ok(fd >= 0, "misc: open deldir fd"); + if (fd >= 0) { + ok(xacl_set_fd(fd, 1, D2, NELEM(D2)) == 0, "misc: set default to delete"); + ok(xacl_del_default_fd(fd) == 0, "misc: xacl_del_default_fd"); + /* libacl returns an empty (0-entry) default ACL after delete */ + { + acl_t a = acl_get_file(acl_path("deldir"), ACL_TYPE_DEFAULT); + int cnt = -1; + if (a) { + acl_entry_t e; + cnt = acl_get_entry(a, ACL_FIRST_ENTRY, &e); + acl_free(a); + } + ok(cnt == 0, "misc: default ACL is empty after delete"); + } + /* deleting again is still success */ + ok(xacl_del_default_fd(fd) == 0, "misc: xacl_del_default_fd idempotent"); + close(fd); + } + } + + /* a freshly-created file has no explicit access ACL xattr -> count 0 */ + if (acl_mkfile("plainfile") == 0) { + fd = open_held("plainfile"); + ok(fd >= 0, "misc: open plainfile fd"); + if (fd >= 0) { + int rc = xacl_get_fd(fd, 0, &ents, &n); + ok(rc == 0 && n == 0, "misc: xacl_get_fd on mode-only file -> no entries"); + if (ents) + free(ents); + ents = NULL; + /* no default ACL on a regular file -> empty */ + rc = xacl_get_fd(fd, 1, &ents, &n); + ok(rc == 0 && n == 0, "misc: xacl_get_fd default on regular file -> no entries"); + if (ents) + free(ents); + close(fd); + } + } + + /* at-variant: leaf NOFOLLOW must not touch a symlink target */ + if (xacl_at_available()) { + int dirfd; + acl_mkfile("nofollow_target"); + p = acl_path("nofollow_link"); + unlink(p); + if (symlink("nofollow_target", p) == 0 + && (dirfd = open(scratch, O_RDONLY | O_DIRECTORY | O_CLOEXEC)) >= 0) { + rsync_acl_ent before[MAX_ENT], after[MAX_ENT]; + int bc, ac; + /* give the target a distinctive ACL via libacl */ + libacl_set(acl_path("nofollow_target"), ACL_TYPE_ACCESS, A1, NELEM(A1)); + bc = libacl_get(acl_path("nofollow_target"), ACL_TYPE_ACCESS, before, MAX_ENT); + /* attempt to set through the symlink leaf with NOFOLLOW: must fail */ + errno = 0; + ok(xacl_set_at(dirfd, "nofollow_link", 0, A2, NELEM(A2)) != 0, + "misc: xacl_set_at on symlink leaf is refused"); + ac = libacl_get(acl_path("nofollow_target"), ACL_TYPE_ACCESS, after, MAX_ENT); + ok(bc == ac && entries_equal(before, bc, after, ac), + "misc: symlink target ACL unchanged by NOFOLLOW set"); + close(dirfd); + } + } +} + +int main(int argc, char *argv[]) +{ + char *p; + + if (argc > 1) + scratch = argv[1]; + else + scratch = "/tmp"; + + /* Probe filesystem ACL support: set a trivial ACL via libacl. */ + if (acl_mkfile(".acl_probe") != 0) { + fprintf(stderr, "t_acl: cannot create scratch file in %s\n", scratch); + return 77; + } + p = acl_path(".acl_probe"); + if (libacl_set(p, ACL_TYPE_ACCESS, A1, NELEM(A1)) != 0) { + if (errno == EOPNOTSUPP || errno == ENOTSUP || errno == ENOSYS) { + fprintf(stderr, "t_acl: %s has no ACL support -- skipping\n", scratch); + unlink(p); + return 77; + } + fprintf(stderr, "t_acl: probe acl_set_file failed: %s\n", strerror(errno)); + unlink(p); + return 77; + } + unlink(p); + + fprintf(stderr, "t_acl: scratch=%s, setxattrat=%s\n", + scratch, xacl_at_available() ? "yes" : "no"); + + run_access("a1", A1, NELEM(A1), "named-user+mask"); + run_access("a2", A2, NELEM(A2), "multi-named+mask"); + run_access("a3", A3, NELEM(A3), "large-uid"); + run_access("a4", A4, NELEM(A4), "zero-perms"); + + run_default("d1", D1, NELEM(D1), "minimal"); + run_default("d2", D2, NELEM(D2), "named+mask"); + + run_misc(); + + fprintf(stderr, "t_acl: %d failure(s)\n", errs); + return errs ? 1 : 0; +} + +#endif /* SUPPORT_ACL_FD */ diff -Nru rsync-3.4.1+ds1/t_chmod_secure.c rsync-3.5.0+ds1/t_chmod_secure.c --- rsync-3.4.1+ds1/t_chmod_secure.c 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/t_chmod_secure.c 2026-07-20 04:05:31.000000000 +0000 @@ -0,0 +1,156 @@ +/* + * Test harness for do_chmod_at(). 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 + * 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). + * + * Not linked into rsync itself. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +#include "rsync.h" + +#include + +int dry_run = 0; +int am_root = 0; +int am_sender = 0; +int read_only = 0; +int list_only = 0; +int copy_links = 0; +int copy_unsafe_links = 0; +extern int am_daemon, am_chrooted; + +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 + * 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. */ +static int leaf_chmod_nofollow_supported(void) +{ +#if defined AT_SYMLINK_NOFOLLOW + return 1; +#else + return 0; +#endif +} + +static void check(const char *label, int actual_rc, int expect_ok, + const char *path, mode_t expected_mode) +{ + struct stat st; + int got_ok = (actual_rc == 0); + /* expect_ok < 0: rc is platform-dependent, don't assert it (leaf-symlink + * scenario); only the target-mode check below is portable. */ + if (expect_ok >= 0 && got_ok != expect_ok) { + fprintf(stderr, "FAIL [%s]: rc=%d errno=%d (%s), expected %s\n", + label, actual_rc, errno, strerror(errno), + expect_ok ? "success" : "rejection"); + errs++; + return; + } + if (path && stat(path, &st) < 0) { + fprintf(stderr, "FAIL [%s]: stat(%s) failed: %s\n", + label, path, strerror(errno)); + errs++; + return; + } + if (path && (st.st_mode & 07777) != expected_mode) { + fprintf(stderr, + "FAIL [%s]: %s mode is 0%o, expected 0%o\n", + label, path, st.st_mode & 07777, expected_mode); + errs++; + return; + } + fprintf(stderr, "OK [%s]\n", label); +} + +int main(int argc, char **argv) +{ + if (argc != 2) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + if (chdir(argv[1]) < 0) { + perror("chdir"); + return 2; + } + + /* Simulate the daemon-without-chroot deployment that do_chmod_at() + * defends. With am_daemon=0 or am_chrooted=1 the wrapper falls + * through to plain do_chmod() and the symlink-race test would be + * meaningless. */ + am_daemon = 1; + am_chrooted = 0; + + /* Test layout (all inside the directory we just chdir'd to): + * + * ./realdir/sentinel -- regular target file + * ./inside_link -> realdir -- legitimate dir-symlink within the tree + * ./escape_link -> ../trap -- attacker swap, target outside tree + * ../trap/sentinel -- the file the attacker wants to alter + * + * The shell wrapper that calls this helper has set both sentinel + * files to mode 0600 so we have a clean baseline to compare. + */ + + /* Scenario A: legitimate parent dir-symlink within the tree. + * + * The within-tree symlink is followed and the chmod must succeed on + * every platform: the kernel RESOLVE_BENEATH paths (Linux 5.6+ openat2, + * FreeBSD 13+ / macOS 15+ O_RESOLVE_BENEATH) and, since the #715/-K + * fallback fix, the per-component O_NOFOLLOW walk too (OpenBSD, NetBSD, + * 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); + 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); + 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); + 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); + check("D: top-level file, no parent component", + rc, 1, "topfile", 0640); + + /* Scenario E: the LEAF component is an escaping symlink -- the chmod- + * specific TOCTOU, distinct from the parent races A/B. realdir is a real + * directory, isolating the O_NOFOLLOW leaf guard. rc is platform-dependent + * (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); + 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), " + "skipping scenario E\n"); + } + + if (errs) + fprintf(stderr, "%d failure(s)\n", errs); + return errs ? 1 : 0; +} diff -Nru rsync-3.4.1+ds1/t_clean_fname.c rsync-3.5.0+ds1/t_clean_fname.c --- rsync-3.4.1+ds1/t_clean_fname.c 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/t_clean_fname.c 2026-07-20 04:05:32.000000000 +0000 @@ -0,0 +1,46 @@ +/* Unit test for KI-50: clean_fname(name, CFN_COLLAPSE_DOT_DOT_DIRS) must + * collapse ".." components. An off-by-one left the collapse dead for all + * multi-component and absolute paths. Exits 0 if all cases collapse correctly, + * 1 otherwise. */ + +#define main rsync_main +#include "rsync.h" +#undef main + +#include + +/* Globals referenced by syscall.o/util1.o (mirrors t_unsafe.c). */ +int dry_run = 0, am_root = 0, am_sender = 1, read_only = 0, list_only = 0; +int copy_links = 0, copy_unsafe_links = 0; +short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG]; + +static const struct { const char *in, *out; } cases[] = { + { "a/b/../c", "a/c" }, + { "/x/y/../z", "/x/z" }, + { "a/../b", "b" }, + { "p/q/r/../../s","p/s" }, + { "d/e/..", "d" }, +}; + +int main(int argc, char *argv[]) +{ + (void)argc; (void)argv; + int i, fails = 0, n = (int)(sizeof cases / sizeof cases[0]); + for (i = 0; i < n; i++) { + char buf[MAXPATHLEN]; + strlcpy(buf, cases[i].in, sizeof buf); + clean_fname(buf, CFN_COLLAPSE_DOT_DOT_DIRS); + if (strcmp(buf, cases[i].out) != 0) { + printf("FAIL: clean_fname(\"%s\") = \"%s\", expected \"%s\"\n", + cases[i].in, buf, cases[i].out); + fails++; + } + } + if (fails) { + printf("clean_fname: %d case(s) not collapsed -- CFN_COLLAPSE_DOT_DOT_DIRS " + "off-by-one\n", fails); + return 1; + } + printf("clean_fname: '..' collapse correct\n"); + return 0; +} diff -Nru rsync-3.4.1+ds1/t_hashtable_overflow.c rsync-3.5.0+ds1/t_hashtable_overflow.c --- rsync-3.4.1+ds1/t_hashtable_overflow.c 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/t_hashtable_overflow.c 2026-07-20 04:05:32.000000000 +0000 @@ -0,0 +1,50 @@ +/* + * Regression harness for the hashtable size*node_size integer overflow + * (Leonid Bugaev May-2026 re-audit, KI-11/12). + * + * hashtable_create() once computed the slot-array byte count as + * new_array0(char, size * node_size) in 32-bit int arithmetic; for a large + * peer/data-driven size the product wrapped to a tiny value, under-allocating + * the table while tbl->size recorded the huge size -- a later node access then + * ran out of bounds (heap overflow / SEGV). The fix passes size and node_size + * as separate factors so my_alloc's --max-alloc guard rejects the oversized + * request, exiting RERR_MALLOC instead of under-allocating. + * + * This harness sets a realistic max_alloc (t_stub leaves it at SIZE_MAX) and + * asks for an absurd size: the fixed code exits RERR_MALLOC; a regressed build + * under-allocates and crashes on the node access below. Not linked into rsync. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License version 3 as published by the + * Free Software Foundation. + */ + +#include "rsync.h" + +extern size_t max_alloc; /* defined in util2.o/t_stub.o */ +short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG]; /* for DEBUG_GTE in hashtable.o */ + +int main(UNUSED(int argc), UNUSED(char *argv[])) +{ + struct hashtable *tbl; + int i; + + /* A realistic --max-alloc cap (the default is 1 GiB) so my_alloc's guard + * can engage; t_stub.o leaves max_alloc == SIZE_MAX. */ + max_alloc = (size_t)1024 * 1024 * 1024; + + /* 2^28 buckets * 16-byte node = 2^32 bytes: the product wraps int to ~0 in + * the unfixed code. The fix must reject this (exit RERR_MALLOC) rather than + * under-allocate. */ + tbl = hashtable_create(1 << 28, 0); + + /* Unreachable with the fix (hashtable_create exits above). If a regression + * lets it return, touch a node near the claimed end -- an under-allocated + * table faults here -- and report the unexpected survival as a failure. */ + for (i = 0; i < tbl->size; i += tbl->size / 64 + 1) { + struct ht_int32_node *node = HT_NODE(tbl, tbl->nodes, i); + node->key = i; + } + fprintf(stderr, "FAIL: hashtable_create(1<<28) was not rejected\n"); + return 1; +} diff -Nru rsync-3.4.1+ds1/t_iwildmatch.c rsync-3.5.0+ds1/t_iwildmatch.c --- rsync-3.4.1+ds1/t_iwildmatch.c 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/t_iwildmatch.c 2026-07-20 04:05:32.000000000 +0000 @@ -0,0 +1,44 @@ +/* Unit test for KI-53: iwildmatch() must be case-insensitive on BOTH the text + * and the pattern. The old code folded only the text, so an upper-case pattern + * (e.g. a "hosts deny" token *.BADDOMAIN.COM) failed to match a lower-case host + * -> access-control fail-open. Exits 0 if all cases match, 1 otherwise. */ + +#include + +extern int iwildmatch(const char *pattern, const char *text); + +static const struct { const char *p, *t; int exp; } cases[] = { + { "abc", "ABC", 1 }, /* text folded (always worked) */ + { "ABC", "abc", 1 }, /* pattern folded (the fix) */ + { "ABC", "abd", 0 }, + { "*.EXAMPLE.COM", "foo.example.com", 1 }, + { "*.example.com", "FOO.EXAMPLE.COM", 1 }, + { "*.BADDOMAIN.COM", "x.baddomain.com", 1 }, /* the access-control case */ + { "[A-Z]bc", "abc", 1 }, /* upper-case range folds */ + { "[ABC]xy", "bxy", 1 }, /* upper-case class-member folds */ + { "x[A-Z]z", "xyz", 1 }, /* range mid-pattern */ + { "[a-z]BC", "abc", 1 }, /* mixed: class + literal fold */ + { "[\\A]bc", "abc", 1 }, /* escaped class member folds */ + { "[A-Z]bc", "5bc", 0 }, /* negative: digit not in range */ +}; + +int main(int argc, char *argv[]) +{ + (void)argc; (void)argv; + int i, fails = 0, n = (int)(sizeof cases / sizeof cases[0]); + for (i = 0; i < n; i++) { + int r = iwildmatch(cases[i].p, cases[i].t); + if (r != cases[i].exp) { + printf("FAIL: iwildmatch(%s, %s) = %d, expected %d\n", + cases[i].p, cases[i].t, r, cases[i].exp); + fails++; + } + } + if (fails) { + printf("iwildmatch: %d case(s) wrong -- the pattern is not folded " + "(asymmetric case handling)\n", fails); + return 1; + } + printf("iwildmatch: case-insensitive on both text and pattern\n"); + return 0; +} diff -Nru rsync-3.4.1+ds1/t_rename_secure.c rsync-3.5.0+ds1/t_rename_secure.c --- rsync-3.4.1+ds1/t_rename_secure.c 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/t_rename_secure.c 2026-07-20 04:05:31.000000000 +0000 @@ -0,0 +1,206 @@ +/* + * 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 + * fall back to plain rename(). Not linked into rsync. GPL version 2. + */ + +#include "rsync.h" + +#include + +int dry_run = 0; +int am_root = 0; +int am_sender = 0; +int read_only = 0; +int list_only = 0; +int copy_links = 0; +int copy_unsafe_links = 0; +extern int am_daemon, am_chrooted; + +short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG]; + +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(). */ +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); + + old_slash = strrchr(old_path, '/'); + new_slash = strrchr(new_path, '/'); + if (!old_slash || !new_slash) + return do_rename(old_path, new_path); + + return do_rename_at(old_path, new_path); +} +#endif + +static void check_exists(const char *label, const char *path, int expect_exists) +{ + int exists = access(path, F_OK) == 0; + + if (exists != expect_exists) { + fprintf(stderr, "FAIL [%s]: %s %s, expected %s\n", + label, path, exists ? "exists" : "does not exist", + expect_exists ? "exists" : "does not exist"); + errs++; + return; + } + fprintf(stderr, "OK [%s]: %s %s\n", + label, path, exists ? "exists" : "does not exist"); +} + +static void check_rename(const char *label, const char *old_path, + const char *new_path, int expect_ok) +{ + int rc; + int got_ok; + int saved_errno; + + errno = 0; + rc = do_rename_at(old_path, new_path); + saved_errno = errno; + got_ok = rc == 0; + + if (got_ok != expect_ok) { + fprintf(stderr, "FAIL [%s]: rename %s -> %s rc=%d errno=%d (%s), expected %s\n", + label, old_path, new_path, 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) +{ +#ifdef AT_FDCWD + int rc; + int saved_errno; + + errno = 0; + rc = vulnerable_mixed_rename_at(old_path, new_path); + saved_errno = errno; + + if (rc != 0) { + fprintf(stderr, "FAIL [%s]: vulnerable rename %s -> %s rc=%d errno=%d (%s), expected escape\n", + label, old_path, new_path, rc, saved_errno, + strerror(saved_errno)); + errs++; + return; + } + fprintf(stderr, "OK [%s]: vulnerable rename %s -> %s escaped\n", + label, old_path, new_path); +#else + fprintf(stderr, "SKIP [%s]: AT_FDCWD not available\n", label); +#endif +} + +static int run_escape_poc(const char *module_dir) +{ +#ifndef AT_FDCWD + fprintf(stderr, "SKIP: AT_FDCWD not available\n"); + return 77; +#else + if (chdir(module_dir) < 0) { + perror("chdir"); + return 2; + } + + am_daemon = 1; + am_chrooted = 0; + + check_vulnerable_rename("P1: 3.4.3-style top-level source to escaping destination parent", + "poc-top-to-escape", "escape_link/vuln-created"); + check_exists("P1 source consumed", "poc-top-to-escape", 0); + check_exists("P1 outside destination created", "../trap/vuln-created", 1); + + check_vulnerable_rename("P2: 3.4.3-style escaping source parent to top-level destination", + "escape_link/poc-outside-source", "vuln-stolen"); + check_exists("P2 outside source consumed", "../trap/poc-outside-source", 0); + check_exists("P2 destination created in module", "vuln-stolen", 1); + + check_rename("P3: fixed top-level source to escaping destination parent", + "fixed-top-to-escape", "escape_link/fixed-created", 0); + check_exists("P3 source preserved", "fixed-top-to-escape", 1); + check_exists("P3 outside destination absent", "../trap/fixed-created", 0); + + check_rename("P4: fixed escaping source parent to top-level destination", + "escape_link/fixed-outside-source", "fixed-stolen", 0); + check_exists("P4 outside source preserved", "../trap/fixed-outside-source", 1); + check_exists("P4 destination absent", "fixed-stolen", 0); + + if (errs) + fprintf(stderr, "%d failure(s)\n", errs); + return errs ? 1 : 0; +#endif +} + +int main(int argc, char **argv) +{ + if (argc == 3 && strcmp(argv[1], "--poc") == 0) + return run_escape_poc(argv[2]); + + if (argc != 2) { + fprintf(stderr, "usage: %s [--poc] \n", argv[0]); + return 2; + } + +#ifndef AT_FDCWD + fprintf(stderr, "SKIP: AT_FDCWD not available\n"); + return 77; +#else + if (chdir(argv[1]) < 0) { + perror("chdir"); + return 2; + } + + am_daemon = 1; + am_chrooted = 0; + + /* Plain mixed paths must keep working. */ + check_rename("A: top-level source to slashed destination", + "top-to-dir", "realdir/top-to-dir", 1); + check_exists("A source consumed", "top-to-dir", 0); + check_exists("A destination created", "realdir/top-to-dir", 1); + + check_rename("B: slashed source to top-level destination", + "realdir/dir-to-top", "dir-to-top", 1); + check_exists("B source consumed", "realdir/dir-to-top", 0); + check_exists("B destination created", "dir-to-top", 1); + + /* A slashed destination parent that escapes the module must be rejected. */ + check_rename("C: top-level source to escaping destination parent", + "top-to-escape", "escape_link/new-outside", 0); + check_exists("C source preserved", "top-to-escape", 1); + check_exists("C outside destination absent", "../trap/new-outside", 0); + + /* A slashed source parent that escapes the module must be rejected too. */ + check_rename("D: escaping source parent to top-level destination", + "escape_link/outside-source", "stolen-from-outside", 0); + check_exists("D outside source preserved", "../trap/outside-source", 1); + check_exists("D destination absent", "stolen-from-outside", 0); + + check_rename("E: shared slashed parent", + "realdir/same-old", "realdir/same-new", 1); + check_exists("E source consumed", "realdir/same-old", 0); + check_exists("E destination created", "realdir/same-new", 1); + + check_rename("F: top-level source to top-level destination", + "top-old", "top-new", 1); + check_exists("F source consumed", "top-old", 0); + check_exists("F destination created", "top-new", 1); + + if (errs) + fprintf(stderr, "%d failure(s)\n", errs); + return errs ? 1 : 0; +#endif +} diff -Nru rsync-3.4.1+ds1/t_safe_arg.c rsync-3.5.0+ds1/t_safe_arg.c --- rsync-3.4.1+ds1/t_safe_arg.c 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/t_safe_arg.c 2026-07-20 04:05:32.000000000 +0000 @@ -0,0 +1,56 @@ +/* Unit test for KI-54: safe_arg() in filename mode (opt==NULL) must not leak an + * uninitialized heap byte. The escape counter and the writer disagreed on a + * backslash before a wildcard / a trailing backslash, leaving a gap that + * strlen() walks into. We poison the heap first so the leaked byte is a + * deterministic non-NUL; then any extra byte makes the output differ from the + * expected exact quoting. Exits 0 if all outputs are exact, 1 otherwise. */ + +/* We link the real rsync objects (see the Makefile), so safe_arg() and all its + * globals come from options.o/exclude.o/etc; we only declare what we touch. */ +#include "rsync.h" + +#include + +extern char *safe_arg(const char *opt, const char *arg); +extern int protect_args, old_style_args, am_sender, relative_paths; +extern int trust_sender_args; + +static const struct { const char *arg, *exp; } cases[] = { + { "\\*", "\\*" }, /* backslash+wildcard: NOT doubled */ + { "\\?", "\\?" }, + { "\\[", "\\[" }, + { "\\", "\\\\" }, /* trailing backslash: doubled (NUL-footgun case) */ + { "\\*\\?", "\\*\\?" }, /* two suppressed backslashes */ + { "a\\*b", "a\\*b" }, + { "\\a", "\\\\a" }, /* backslash+non-wildcard: doubled */ +}; + +int main(int argc, char *argv[]) +{ + (void)argc; (void)argv; + int i, fails = 0, n = (int)(sizeof cases / sizeof cases[0]); + + protect_args = 0; old_style_args = 0; am_sender = 1; + relative_paths = 0; trust_sender_args = 1; + + /* Poison the heap so an uninitialized byte reads as 0xbe, not a lucky NUL. */ + for (i = 0; i < 256; i++) { + void *p = malloc(64); + if (p) { memset(p, 0xbe, 64); free(p); } + } + + for (i = 0; i < n; i++) { + char *r = safe_arg(NULL, cases[i].arg); + if (!r || strcmp(r, cases[i].exp) != 0) { + printf("FAIL: safe_arg(NULL, \"%s\") = \"%s\" (len %zu), expected \"%s\"\n", + cases[i].arg, r ? r : "(null)", r ? strlen(r) : 0, cases[i].exp); + fails++; + } + } + if (fails) { + printf("safe_arg: %d case(s) wrong -- uninitialized-byte leak / miscount\n", fails); + return 1; + } + printf("safe_arg: filename-mode quoting is exact (no uninit byte)\n"); + return 0; +} diff -Nru rsync-3.4.1+ds1/t_secure_relpath.c rsync-3.5.0+ds1/t_secure_relpath.c --- rsync-3.4.1+ds1/t_secure_relpath.c 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/t_secure_relpath.c 2026-07-25 07:28:54.000000000 +0000 @@ -0,0 +1,231 @@ +/* + * Test harness for secure_relative_open()'s front-door input + * validation. Codex audit Finding 5 noted that the existing check + * + * if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../")) + * + * catches "../foo" and "foo/../bar" but misses bare ".." (an actual + * one-level escape on platforms that fall back to the per-component + * walk), as well as "a/..", "foo/..", and any other form that + * decomposes to a ".." component when split on "/". The kernel- + * enforced RESOLVE_BENEATH (Linux 5.6+) and O_RESOLVE_BENEATH + * (FreeBSD 13+, macOS 15+) reject these in-kernel; the per- + * component fallback used on NetBSD, OpenBSD, Solaris, Cygwin and + * pre-5.6 Linux does not, so the validation must happen at the + * front door. + * + * This helper invokes secure_relative_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); + * post-fix, the front-door check catches every variant up front + * with a consistent EINVAL across platforms. + * + * Not linked into rsync itself. + */ + +#include "rsync.h" + +#include + +int dry_run = 0; +int am_root = 0; +int am_sender = 0; +int read_only = 0; +int list_only = 0; +int copy_links = 0; +int copy_unsafe_links = 0; +extern int am_daemon, am_chrooted; + +short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG]; + +static int errs = 0; + +static void check_relpath(const char *relpath) +{ + int fd; + int saved_errno; + + errno = 0; + fd = secure_relative_open(NULL, relpath, O_RDONLY | O_DIRECTORY, 0); + saved_errno = errno; + + if (fd >= 0) { + fprintf(stderr, + "FAIL [relpath=%-12s]: returned valid fd %d (escape) -- expected -1 EINVAL\n", + relpath, fd); + close(fd); + errs++; + return; + } + + if (saved_errno != EINVAL) { + fprintf(stderr, + "FAIL [relpath=%-12s]: rejected but errno=%d (%s), expected EINVAL\n", + relpath, saved_errno, strerror(saved_errno)); + errs++; + return; + } + + fprintf(stderr, "OK [relpath=%-12s]: rejected with EINVAL\n", relpath); +} + +static void check_basedir(const char *basedir) +{ + int fd; + int saved_errno; + + errno = 0; + fd = secure_relative_open(basedir, "ok", O_RDONLY | O_DIRECTORY, 0); + saved_errno = errno; + + if (fd >= 0) { + fprintf(stderr, + "FAIL [basedir=%-12s]: returned valid fd %d -- expected -1 EINVAL\n", + basedir, fd); + close(fd); + errs++; + return; + } + + if (saved_errno != EINVAL) { + fprintf(stderr, + "FAIL [basedir=%-12s]: rejected but errno=%d (%s), expected EINVAL\n", + basedir, saved_errno, strerror(saved_errno)); + errs++; + return; + } + + fprintf(stderr, "OK [basedir=%-12s]: rejected with EINVAL\n", basedir); +} + +static void check_beneath_dotdot(void) +{ + STRUCT_STAT ast, fst; + int anchor, fd; + + anchor = open(".", O_RDONLY | O_DIRECTORY); + if (anchor < 0 || fstat(anchor, &ast) < 0) { + perror("open/fstat anchor"); + errs++; + return; + } + + fd = secure_relative_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) { + fprintf(stderr, "FAIL [beneath safe]: in-anchor '..' was not resolved\n"); + errs++; + } else + fprintf(stderr, "OK [beneath safe]: in-anchor '..' resolved\n"); + if (fd >= 0) + close(fd); + + /* A bare final ".." must be refused whatever flags the caller passes. The + * leaf fast paths in secure_walk_at() openat() the final component + * directly, so before they learned to defer a literal ".." to ds_descend() + * an O_NOFOLLOW caller got back the anchor's own parent, and a caller + * without O_DIRECTORY opened it transiently. Only the O_DIRECTORY form + * was ever refused. */ + { + static const struct { const char *label; int flags; } dotdot_cases[] = { + { "O_DIRECTORY", O_RDONLY | O_DIRECTORY }, + { "O_DIRECTORY|O_NOFOLLOW", O_RDONLY | O_DIRECTORY | O_NOFOLLOW }, + { "no O_DIRECTORY", O_RDONLY }, + }; + unsigned ci; + for (ci = 0; ci < sizeof dotdot_cases / sizeof *dotdot_cases; ci++) { + int dfd; + errno = 0; + dfd = secure_relative_open_at_beneath(anchor, "..", + dotdot_cases[ci].flags, 0); + if (dfd >= 0) { + STRUCT_STAT dst; + int above = fstat(dfd, &dst) == 0 && dst.st_ino != ast.st_ino; + fprintf(stderr, "FAIL [beneath bare-dotdot %s]: rc=%d%s\n", + dotdot_cases[ci].label, dfd, + above ? " -- resolved ABOVE the anchor" : ""); + errs++; + close(dfd); + } else if (errno != ELOOP) { + fprintf(stderr, "FAIL [beneath bare-dotdot %s]: errno=%d, expected ELOOP\n", + dotdot_cases[ci].label, errno); + errs++; + } else + fprintf(stderr, "OK [beneath bare-dotdot %s]: refused with ELOOP\n", + dotdot_cases[ci].label); + } + } + + errno = 0; + fd = secure_relative_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", + fd, errno); + if (fd >= 0) + close(fd); + errs++; + } else + fprintf(stderr, "OK [beneath escape]: climb above anchor refused\n"); + + close(anchor); +} + +int main(int argc, char **argv) +{ + if (argc != 2) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + if (chdir(argv[1]) < 0) { + perror("chdir"); + return 2; + } + + /* secure_relative_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 + * helper exercises the same code shape the receiver does. */ + am_daemon = 1; + am_chrooted = 0; + + mkdir("subdir", 0755); + symlink("subdir", "alias"); + + /* Each of these relpaths must be rejected with EINVAL at the + * secure_relative_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 + * is suspicious and the caller should normalise before passing + * it in. The "../foo" / "foo/../bar" / "/foo" / "/" cases are + * regression checks for the existing checks. */ + check_relpath(".."); + check_relpath("../foo"); + check_relpath("subdir/.."); + check_relpath("subdir/../subdir"); + check_relpath("foo/../bar"); + check_relpath("/foo"); + check_relpath("/"); + + /* Same checks against basedir (which the codex Finding 2 fix + * routes through the same RESOLVE_BENEATH-equivalent). Absolute + * basedirs are operator-trusted and intentionally not validated + * here. */ + check_basedir(".."); + check_basedir("../subdir"); + check_basedir("subdir/.."); + check_basedir("foo/../bar"); + + /* A followed symlink target is the one caller that legitimately carries + * literal '..'. Its dedicated fd-anchored entry point must preserve an + * in-tree climb while refusing to pop above the anchor. */ + check_beneath_dotdot(); + + if (errs) + fprintf(stderr, "\n%d failure(s)\n", errs); + return errs ? 1 : 0; +} diff -Nru rsync-3.4.1+ds1/t_stub.c rsync-3.5.0+ds1/t_stub.c --- rsync-3.4.1+ds1/t_stub.c 2024-11-14 19:42:24.000000000 +0000 +++ rsync-3.5.0+ds1/t_stub.c 2026-08-02 19:35:03.000000000 +0000 @@ -23,8 +23,12 @@ int do_fsync = 0; int inplace = 0; +int am_daemon = 0; +int am_chrooted = 0; +int insecure_links = 0; int modify_window = 0; int preallocate_files = 0; +int sparse_files = 0; int protect_args = 0; int module_id = -1; int relative_paths = 0; @@ -34,9 +38,19 @@ int preserve_executability = 0; int omit_link_times = 0; int open_noatime = 0; -size_t max_alloc = 0; /* max_alloc is needed when combined with util2.o */ +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() + * 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 + * syscall.c, which every helper links -- no stub needed here. */ filter_rule_list daemon_filter_list; void rprintf(UNUSED(enum logcode code), const char *format, ...) @@ -72,7 +86,7 @@ return 0; } - int copy_xattrs(UNUSED(const char *source), UNUSED(const char *dest)) + int copy_xattrs(UNUSED(const char *source), UNUSED(int source_fd), UNUSED(const char *dest), UNUSED(int dest_fd)) { return -1; } @@ -96,6 +110,11 @@ { return 0; } + + BOOL lp_insecure_links(UNUSED(int mod)) +{ + return 0; +} const char *who_am_i(void) { diff -Nru rsync-3.4.1+ds1/t_symlink_secure.c rsync-3.5.0+ds1/t_symlink_secure.c --- rsync-3.4.1+ds1/t_symlink_secure.c 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/t_symlink_secure.c 2026-08-01 09:19:42.000000000 +0000 @@ -0,0 +1,160 @@ +/* + * Test harness for the fake-super branches of do_symlink_at()/do_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 + * symlink and escaped the module. Checks the fixed wrappers refuse it; + * --poc shows the old fallback escaping. Not linked into rsync. GPL version 2. + */ + +#include "rsync.h" + +#include + +/* The symlink placeholder (and thus this escape) exists only where symlink + * xattrs are unavailable -- the same guard do_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 +#endif + +int dry_run = 0; +int am_root = -1; /* --fake-super */ +int am_sender = 0; +int read_only = 0; +int list_only = 0; +int copy_links = 0; +int copy_unsafe_links = 0; +extern int am_daemon, am_chrooted; + +short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG]; + +static int errs = 0; + +static void check_preserved(const char *label, const char *victim, const char *want) +{ + char buf[256]; + int fd = open(victim, O_RDONLY); + ssize_t n = fd >= 0 ? read(fd, buf, sizeof buf - 1) : -1; + + if (fd >= 0) + close(fd); + if (n < 0) + n = 0; + buf[n] = '\0'; + if (n > 0 && buf[n-1] == '\n') + buf[n-1] = '\0'; + + if (strcmp(buf, want) != 0) { + fprintf(stderr, "FAIL [%s]: victim %s = \"%s\", expected \"%s\" " + "(basename symlink was followed -> module escape)\n", + label, victim, buf, want); + errs++; + return; + } + fprintf(stderr, "OK [%s]: victim %s preserved\n", label, victim); +} + +static void check_clobbered(const char *label, const char *victim, const char *unwanted) +{ + char buf[256]; + int fd = open(victim, O_RDONLY); + ssize_t n = fd >= 0 ? read(fd, buf, sizeof buf - 1) : -1; + + if (fd >= 0) + close(fd); + if (n < 0) + n = 0; + buf[n] = '\0'; + if (n > 0 && buf[n-1] == '\n') + buf[n-1] = '\0'; + + if (strcmp(buf, unwanted) != 0) { + fprintf(stderr, "FAIL [%s]: victim %s = \"%s\", expected the escape to write \"%s\"\n", + label, victim, buf, unwanted); + errs++; + return; + } + fprintf(stderr, "OK [%s]: victim %s clobbered as expected (escape demonstrated)\n", + label, victim); +} + +int main(int argc, char **argv) +{ +#ifndef AT_FDCWD + fprintf(stderr, "SKIP: AT_FDCWD not available\n"); + return 77; +#else + int poc = 0; + 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 + * pass vacuously. */ + (void)argc; (void)argv; + fprintf(stderr, "SKIP: no mknodat() and no symlink placeholders -- " + "nothing this helper asserts applies to this build\n"); + return 77; +# endif + + if (argc == 3 && strcmp(argv[1], "--poc") == 0) { + poc = 1; + moddir = argv[2]; + } else if (argc == 2) { + moddir = argv[1]; + } else { + fprintf(stderr, "usage: %s [--poc] \n", argv[0]); + return 2; + } + + if (chdir(moddir) < 0) { + perror("chdir"); + return 2; + } + + am_daemon = 1; + am_chrooted = 0; + 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(), + * 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", + "VULN_SYM_PAYLOAD"); +#endif + do_mknod("nodpath", S_IFCHR | 0600, 0); + check_clobbered("poc do_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"); + + /* 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"); +#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"); +# endif + + if (errs) + fprintf(stderr, "%d failure(s)\n", errs); + return errs ? 1 : 0; +#endif +} diff -Nru rsync-3.4.1+ds1/testhelp/maketree.py rsync-3.5.0+ds1/testhelp/maketree.py --- rsync-3.4.1+ds1/testhelp/maketree.py 2020-06-17 01:27:48.000000000 +0000 +++ rsync-3.5.0+ds1/testhelp/maketree.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,133 +0,0 @@ -#!/usr/bin/env python2 - -# Copyright (C) 2002 by Martin Pool - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version -# 2 as published by the Free Software Foundation. -# -# 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program; if not, write to the Free Software -# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -# Populate a tree with pseudo-randomly distributed files to test -# rsync. - -from __future__ import generators -import random, string, os, os.path - -nfiles = 10000 -depth = 5 -n_children = 20 -n_files = 20 -n_symlinks = 10 - -name_chars = string.digits + string.letters - -abuffer = 'a' * 1024 - -def random_name_chars(): - a = "" - for i in range(10): - a = a + random.choice(name_chars) - return a - - -def generate_names(): - n = 0 - while 1: - yield "%05d_%s" % (n, random_name_chars()) - n += 1 - - -class TreeBuilder: - def __init__(self): - self.n_children = 20 - self.n_files = 100 - self.total_entries = 100000 # long(1e8) - self.actual_size = 0 - self.name_gen = generate_names() - self.all_files = [] - self.all_dirs = [] - self.all_symlinks = [] - - - def random_size(self): - return random.lognormvariate(4, 4) - - - def random_symlink_target(self): - what = random.choice(['directory', 'file', 'symlink', 'none']) - try: - if what == 'directory': - return random.choice(self.all_dirs) - elif what == 'file': - return random.choice(self.all_files) - elif what == 'symlink': - return random.choice(self.all_symlinks) - elif what == 'none': - return self.name_gen.next() - except IndexError: - return self.name_gen.next() - - - def can_continue(self): - self.total_entries -= 1 - return self.total_entries > 0 - - - def build_tree(self, prefix, depth): - """Generate a breadth-first tree""" - for count, function in [[n_files, self.make_file], - [n_children, self.make_child_recurse], - [n_symlinks, self.make_symlink]]: - for i in range(count): - if not self.can_continue(): - return - name = os.path.join(prefix, self.name_gen.next()) - function(name, depth) - - - def print_summary(self): - print "total bytes: %d" % self.actual_size - - - def make_child_recurse(self, dname, depth): - if depth > 1: - self.make_dir(dname) - self.build_tree(dname, depth-1) - - - def make_dir(self, dname, depth='ignore'): - print "%s/" % (dname) - os.mkdir(dname) - self.all_dirs.append(dname) - - - def make_symlink(self, lname, depth='ignore'): - print "%s -> %s" % (lname, self.random_symlink_target()) - - - def make_file(self, fname, depth='ignore'): - size = long(self.random_size()) - print "%-70s %d" % (fname, size) - f = open(fname, 'w') - f.truncate(size) - self.fill_file(f, size) - self.all_files.append(fname) - self.actual_size += size - - def fill_file(self, f, size): - while size > 0: - f.write(abuffer[:size]) - size -= len(abuffer) - - -tb = TreeBuilder() -tb.build_tree('/tmp/foo', 3) -tb.print_summary() diff -Nru rsync-3.4.1+ds1/testsuite/00-hello.test rsync-3.5.0+ds1/testsuite/00-hello.test --- rsync-3.4.1+ds1/testsuite/00-hello.test 2022-01-20 18:51:13.000000000 +0000 +++ rsync-3.5.0+ds1/testsuite/00-hello.test 1970-01-01 00:00:00.000000000 +0000 @@ -1,61 +0,0 @@ -#!/bin/sh - -# Test some foundational things. - -. "$suitedir/rsync.fns" - -RSYNC_RSH="$scratchdir/src/support/lsh.sh" -export RSYNC_RSH - -echo $0 running - -$RSYNC --version || test_fail '--version output failed' - -$RSYNC --info=help || test_fail '--info=help output failed' - -$RSYNC --debug=help || test_fail '--debug=help output failed' - -weird_name="A weird)name" - -mkdir "$fromdir" -mkdir "$fromdir/$weird_name" - -append_line() { - echo "$1" - echo "$1" >>"$fromdir/$weird_name/file" -} - -append_line test1 -checkit "$RSYNC -ai '$fromdir/' '$todir/'" "$fromdir" "$todir" - -copy_weird() { - checkit "$RSYNC $1 --rsync-path='$RSYNC' '$2$fromdir/$weird_name/' '$3$todir/$weird_name'" "$fromdir" "$todir" -} - -append_line test2 -copy_weird '-ai' 'lh:' '' - -append_line test3 -copy_weird '-ai' '' 'lh:' - -append_line test4 -copy_weird '-ais' 'lh:' '' - -append_line test5 -copy_weird '-ais' '' 'lh:' - -echo test6 - -touch "$fromdir/one" "$fromdir/two" -(cd "$fromdir" && $RSYNC -ai --old-args --rsync-path="$RSYNC" lh:'one two' "$todir/") -if [ ! -f "$todir/one" ] || [ ! -f "$todir/two" ]; then - test_fail "old-args copy of 'one two' failed" -fi - -echo test7 - -rm "$todir/one" "$todir/two" -(cd "$fromdir" && RSYNC_OLD_ARGS=1 $RSYNC -ai --rsync-path="$RSYNC" lh:'one two' "$todir/") - -# The script would have aborted on error, so getting here means we've won. -exit 0 diff -Nru rsync-3.4.1+ds1/testsuite/00-hello_test.py rsync-3.5.0+ds1/testsuite/00-hello_test.py --- rsync-3.4.1+ds1/testsuite/00-hello_test.py 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/testsuite/00-hello_test.py 2026-08-01 22:13:25.000000000 +0000 @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# Python rewrite of testsuite/00-hello.test. +# +# Foundational smoke test: --version / --info=help / --debug=help all +# work, plus a round-trip transfer of a directory whose name contains +# shell-special characters via the lsh.sh remote-shell stand-in. + +import os + +from rsyncfns import ( + FROMDIR, RSYNC, RSYNC_PEER, SRCDIR, TODIR, + checkit, run_rsync, test_fail, rsync_path_arg, rsh_cmd, +) + + +# Set RSYNC_RSH so rsync picks up lsh.sh for the "lh:" hosts below. +os.environ['RSYNC_RSH'] = rsh_cmd() + +# Basic help dumps must not crash. +if run_rsync('--version', check=False).returncode != 0: + test_fail('--version output failed') +if run_rsync('--info=help', check=False).returncode != 0: + test_fail('--info=help output failed') +if run_rsync('--debug=help', check=False).returncode != 0: + test_fail('--debug=help output failed') + +weird_name = "A weird)name" + +FROMDIR.mkdir(parents=True, exist_ok=True) +weird_dir = FROMDIR / weird_name +weird_dir.mkdir() + + +def append_line(line: str) -> None: + print(line) + with open(weird_dir / 'file', 'a') as f: + f.write(line + '\n') + + +def copy_weird(args: list, src_host: str, dst_host: str) -> None: + checkit( + [*args, f'--rsync-path={rsync_path_arg()}', + f'{src_host}{weird_dir}/', + f'{dst_host}{TODIR / weird_name}'], + FROMDIR, TODIR, + ) + + +append_line('test1') +checkit(['-ai', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR) + +append_line('test2') +copy_weird(['-ai'], 'lh:', '') + +append_line('test3') +copy_weird(['-ai'], '', 'lh:') + +append_line('test4') +copy_weird(['-ais'], 'lh:', '') + +append_line('test5') +copy_weird(['-ais'], '', 'lh:') + +# test6: --old-args lets two whitespace-separated names go through as a +# single "one two" remote argument to be re-split by the remote shell. +print('test6') +(FROMDIR / 'one').touch() +(FROMDIR / 'two').touch() + +saved = os.getcwd() +os.chdir(FROMDIR) +try: + run_rsync('-ai', '--old-args', f'--rsync-path={rsync_path_arg()}', + 'lh:one two', f'{TODIR}/') +finally: + os.chdir(saved) + +if not (TODIR / 'one').is_file() or not (TODIR / 'two').is_file(): + test_fail("old-args copy of 'one two' failed") + +# test7: the RSYNC_OLD_ARGS=1 env var should be equivalent to --old-args. +print('test7') +(TODIR / 'one').unlink() +(TODIR / 'two').unlink() + +env = os.environ.copy() +env['RSYNC_OLD_ARGS'] = '1' +import subprocess +from rsyncfns import rsync_argv + +os.chdir(FROMDIR) +try: + subprocess.run( + rsync_argv('-ai', f'--rsync-path={rsync_path_arg()}', + 'lh:one two', f'{TODIR}/'), + env=env, check=True, + ) +finally: + os.chdir(saved) + +# check=True only proves a zero exit; confirm the env-var path actually copied +# both files (as the explicit --old-args case above does). +if not (TODIR / 'one').is_file() or not (TODIR / 'two').is_file(): + test_fail("RSYNC_OLD_ARGS=1 copy of 'one two' failed") diff -Nru rsync-3.4.1+ds1/testsuite/COVERAGE.md rsync-3.5.0+ds1/testsuite/COVERAGE.md --- rsync-3.4.1+ds1/testsuite/COVERAGE.md 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/testsuite/COVERAGE.md 2026-07-20 04:05:31.000000000 +0000 @@ -0,0 +1,204 @@ +# rsync option / daemon-parameter test coverage matrix + +Living checklist for the test-coverage effort that precedes the path-handling +restructure of rsync's path resolution. The restructure rewrites parent-directory +resolution for essentially every option, so the goal here is a regression net +that exercises each option **at directory depth** (≥3 levels) and, where the +option spans trees, **across directory boundaries**, asserting the *specific +property* the option controls — not just `dest == src`. + +How to read the columns: + +* **test(s)** — the `testsuite/*_test.py` that exercise the option. Tests added + by this effort are marked `*new*`. +* **depth** — Y = asserted on entries ≥3 levels deep; `~` = exercised only at/near + the tree root; `n/a` = not a path-resolution option. +* **x-dir** — Y = exercised with the relevant aux tree (temp/backup/dest/partial) + **outside** the main tree; `—` = not a cross-directory option. +* **gap** — what is still missing. + +Status legend: ✓ property asserted · `~` shallow / by an existing ported test · +✗ no coverage. + +--- + +## Command-line options + +### Recursion / structure +| option | test(s) | depth | x-dir | notes / gap | +|---|---|---|---|---| +| -a, --archive | (all) | Y | — | ✓ ubiquitous | +| -r, --recursive | hands, delete-deep*new* | Y | — | ✓ | +| -R, --relative | relative, relative-implied*new* | Y | — | ✓ implied-dir attrs at depth | +| --no-implied-dirs | relative-implied*new* | Y | — | ✓ (proto 30+; proto 29 rejects multi-component path) | +| --inc-recursive / --no-inc-recursive | hardlinks | Y | — | `~` exercised, not isolated | +| -d, --dirs | dirs*new* | Y | — | ✓ no-recurse top layer | +| --old-dirs / --old-d | — | — | — | ✗ | +| -m, --prune-empty-dirs | prune-empty-dirs*new* | Y | — | ✓ incl. filter-emptied chains | + +### Links +| option | test(s) | depth | x-dir | notes / gap | +|---|---|---|---|---| +| -l, --links | links*new*, symlink-ignore | Y | — | ✓ | +| -L, --copy-links | links*new* | Y | — | ✓ deref file+dir | +| -k, --copy-dirlinks | links*new* | Y | — | ✓ follow dir-symlink | +| -K, --keep-dirlinks | symlink-dirlink-basis | Y | — | ✓ #715; skips on no-RESOLVE_BENEATH / --disable-openat2 | +| -H, --hard-links | hardlinks, hardlinks-deep*new* | Y | Y | ✓ cross-directory hardlink | +| --copy-unsafe-links | unsafe-links | `~` | — | `~` | +| --safe-links | safe-links | `~` | — | `~` | +| --munge-links | (daemon-munge*new* covers the daemon param) | — | — | `~` client option not isolated; local mode is a near no-op | + +### Metadata / permissions / ownership +| option | test(s) | depth | x-dir | notes / gap | +|---|---|---|---|---| +| -p, --perms | metadata-depth*new* | Y | — | ✓ exact modes per entry | +| -E, --executability | executability | `~` | — | `~` | +| --chmod | metadata-depth*new*, chmod-option | Y | — | ✓ | +| -A, --acls | acls, acls-depth*new* | Y | — | ✓ | +| -X, --xattrs | xattrs, xattrs-depth*new* | Y | — | ✓ | +| -t, --times | metadata-depth*new* | Y | — | ✓ | +| -U, --atimes | atimes | `~` | — | `~` (same set path as -t, covered deep) | +| --open-noatime | open-noatime | `~` | — | `~` | +| -N, --crtimes | crtimes | `~` | — | `~` (skips without crtimes support) | +| -O, --omit-dir-times | omit-times*new* | Y | — | ✓ | +| -J, --omit-link-times | omit-times*new* | Y | — | ✓ | +| -o, --owner | chown, ownership-depth*new* | Y | — | ✓ uid map root-gated | +| -g, --group | chgrp, ownership-depth*new* | Y | — | ✓ group remap non-root | +| --super / --fake-super | chown, chown-fake | `~` | — | `~` | +| --numeric-ids | — | — | — | ✗ client; daemon `numeric ids` also ✗ | +| --usermap / --groupmap | ownership-depth*new* | Y | — | ✓ groupmap non-root; usermap root-gated | +| --chown | ownership-depth*new* | Y | — | ✓ group half | +| -D / --devices / --specials | devices, devices-fake | `~` | — | `~` root/device-gated | +| --copy-devices / --write-devices | — | — | — | ✗ device-gated | +| -S, --sparse | sparse*new* | Y | — | ✓ hole preserved at depth | + +### Delta / temp / backup / dest (highest restructure risk) +| option | test(s) | depth | x-dir | notes / gap | +|---|---|---|---|---| +| -T, --temp-dir | temp-dir*new*, chmod-temp-dir | Y | Y | ✓ cross-dir rename | +| --partial | partial*new* | Y | — | ✓ partial kept in dest file | +| --partial-dir | partial*new*, symlink-dirlink-basis | Y | Y | ✓ relative (in-tree) + absolute (outside), incl. delta resume from an absolute outside-tree partial | +| --delay-updates | delay-updates, delay-updates-deep*new* | Y | — | ✓ per-dir staging | +| --inplace | inplace*new*, alt-dest | Y | — | ✓ inode preserved | +| --append / --append-verify | append*new* | Y | — | ✓ verify split is proto 30+ | +| -b, --backup / --backup-dir / --suffix | backup, backup-deep*new* | Y | Y | ✓ | +| --compare-dest / --copy-dest / --link-dest | alt-dest, alt-dest-deep*new* | Y | Y | ✓ link=hardlink, copy=copy, compare=skip | +| -y, --fuzzy | fuzzy | `~` | — | `~` | +| -u, --update | update*new* | Y | — | ✓ keeps newer dest, updates older | +| -W, --whole-file | (used widely; --no-whole-file ubiquitous) | n/a | — | `~` | +| --mkpath | mkpath | `~` | — | `~` | +| -x, --one-file-system | — | — | — | ✗ (needs a mount boundary) | +| --preallocate / --fsync | — | — | — | ✗ | +| -B, --block-size | — | — | — | ✗ | +| --max-alloc | max-alloc-zero-rejected*new*, daemon-max-alloc-zero*new* | — | — | ✓ zero rejected locally and when forwarded to the daemon (needs an old client) | + +### Filtering +| option | test(s) | depth | x-dir | notes / gap | +|---|---|---|---|---| +| -f, --filter / -F | filter-depth*new*, merge | Y | — | ✓ deep per-dir merge | +| --exclude / --include | filter-depth*new*, exclude, exclude-lsh | Y | — | ✓ | +| --exclude-from / --include-from | files-from-depth*new* | Y | — | ✓ | +| -C, --cvs-exclude | cvs-exclude*new* | Y | — | ✓ incl. deep .cvsignore | +| --files-from | files-from-depth*new* | Y | — | ✓ | +| -0, --from0 | files-from-depth*new* | Y | — | ✓ | +| --max-size / --min-size | size-filter*new* | Y | — | ✓ | +| --existing / --ignore-existing | delete-deep*new* | Y | — | ✓ | +| --ignore-missing-args / --delete-missing-args | — | — | — | ✗ | + +### Deletion +| option | test(s) | depth | x-dir | notes / gap | +|---|---|---|---|---| +| --delete / --del | delete, delete-deep*new* | Y | — | ✓ deep subtree | +| --delete-before/during/delay/after | delete-deep*new* | Y | — | ✓ all four agree | +| --delete-excluded | delete | `~` | — | `~` | +| --max-delete | delete-deep*new* | Y | — | ✓ caps deletions | +| --remove-source-files | delete | `~` | — | `~` | +| --force | update*new* | Y | — | ✓ replaces a non-empty dir with a file | +| --ignore-errors | — | — | — | ✗ (client; daemon `ignore errors` also ✗) | + +### Comparison / checksum / compression +| option | test(s) | depth | x-dir | notes / gap | +|---|---|---|---|---| +| -c, --checksum | compare*new* | Y | — | ✓ catches stealth change | +| -I, --ignore-times | compare*new* | Y | — | ✓ | +| --size-only | compare*new* | Y | — | ✓ | +| -@, --modify-window | compare*new* | Y | — | ✓ | +| --checksum-choice / --checksum-seed | compress-options*new* | Y | — | ✓ every advertised algo | +| -z, --compress | daemon-gzip-{up,down}load, daemon-refuse-compress | `~` | — | `~` | +| --compress-choice / --compress-level / --skip-compress | compress-options*new* | Y | — | ✓ | + +### Output / reporting (path-irrelevant — checked for output shape) +| option | test(s) | notes / gap | +|---|---|---| +| -i, --itemize-changes | output-options*new*, itemize | ✓ | +| -n, --dry-run | output-options*new* | ✓ | +| --stats | output-options*new* | ✓ | +| --out-format | output-options*new* | ✓ | +| --list-only | output-options*new* | ✓ | +| -q, --quiet | output-options*new* | ✓ | +| --progress / -P | output-options*new* | ✓ (--progress) | +| -h, --human-readable / -8, --8-bit-output | output-options*new* | ✓ smoke | +| --version / --help | output-options*new* | ✓ | +| --info / --debug / --stderr / --no-motd / --outbuf | — | ✗ | +| -M, --remote-option / --log-file / --log-file-format | — | ✗ (daemon `log file` covered) | + +### Batch / connection / misc +| option | test(s) | notes / gap | +|---|---|---| +| --write-batch / --only-write-batch / --read-batch | batch-mode | `~` | +| -e, --rsh / --rsync-path | ssh-basic, many | `~` | +| --protocol | check29 / check30 (whole suite) | ✓ | +| --address / --port | daemon tests under --use-tcp | `~` | +| --password-file | daemon-auth*new* | ✓ | +| --early-input / daemon `early exec` | — | ✗ | +| --sockopts / --blocking-io / --timeout / --contimeout | — | ✗ | +| -4/-6, --ipv4/--ipv6 | — | ✗ | +| --stop-after / --stop-at | — | ✗ | +| --bwlimit | partial*new* (used, not asserted) | `~` | +| --copy-as | — | ✗ root-gated | +| --iconv | — | ✗ | +| -s/--secluded-args, --old-args, --trust-sender | (default arg-protection exercised) | `~` | + +--- + +## Daemon (rsyncd.conf) parameters + +| parameter | test(s) | notes / gap | +|---|---|---| +| path | daemon-access*new*, all daemon tests | ✓ incl. deep sub-path | +| read only | daemon-access*new*, daemon | ✓ | +| write only | daemon-access*new* | ✓ | +| list | daemon-access*new*, daemon | ✓ hidden-but-usable | +| use chroot | sender-flist-symlink-leak, daemon-chroot-acl | `~` (no=most tests; yes needs root) | +| munge symlinks | daemon-munge*new* | ✓ /rsyncd-munged/ add+strip | +| exclude / include | daemon-filter*new*, daemon | ✓ exclude | +| filter / exclude from / include from | — | ✗ (exclude covers the mechanism) | +| incoming chmod | daemon-filter*new*, chmod-option | ✓ | +| outgoing chmod | daemon-filter*new* | ✓ | +| auth users / secrets file | daemon-auth*new* | ✓ accept/reject/unauth | +| strict modes | daemon-auth*new* | ✓ rejects world-readable secrets | +| refuse options | daemon-refuse*new*, daemon-refuse-compress | ✓ named/wildcard/allow-list | +| pre-xfer exec / post-xfer exec | daemon-exec*new* | ✓ env + abort | +| early exec | — | ✗ (needs --early-input) | +| hosts allow / hosts deny | daemon (allow), daemon-chroot-acl (deny) | `~` (needs --use-tcp for real peer) | +| reverse lookup / forward lookup | daemon-chroot-acl | `~` reverse only | +| log file / transfer logging / log format | daemon | `~` set, not asserted | +| max verbosity | daemon | `~` | +| comment | daemon, daemon-access*new* | ✓ | +| numeric ids | — | ✗ (hard to observe non-root) | +| fake super | chown-fake (client side) | ✗ as daemon param | +| timeout / max connections / lock file | — | ✗ (need --use-tcp + concurrency) | +| temp dir / open noatime / ignore errors / ignore nonreadable | — | ✗ | +| charset / name converter / dont compress | — | ✗ | +| uid / gid / daemon uid / daemon gid / daemon chroot | build_rsyncd_conf (uid/gid when root), daemon-chroot-acl | `~` root-gated | +| motd file / pid file / port / address / socket options / listen backlog / proxy protocol / syslog facility / syslog tag | — | ✗ (server-startup/connection params) | + +--- + +## Known gaps worth a future pass +* Connection/timeout params (`--timeout`, `--contimeout`, daemon `timeout`, + `max connections`) need a real socket + concurrency (run under `--use-tcp`). +* Root-only behaviours (`-o`/`--usermap` uid remap, real devices, `use chroot + = yes`, daemon uid/gid) skip as non-root; run the suite as root to cover. +* `--ignore-errors`, `-x/--one-file-system`, `--numeric-ids` have no dedicated + test yet (lower restructure risk). diff -Nru rsync-3.4.1+ds1/testsuite/README.md rsync-3.5.0+ds1/testsuite/README.md --- rsync-3.4.1+ds1/testsuite/README.md 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.5.0+ds1/testsuite/README.md 2026-08-02 06:05:24.000000000 +0000 @@ -0,0 +1,304 @@ +# rsync testsuite + +This directory holds rsync's automated regression tests. Ideally every code +change or bug fix comes with a test that would have caught the problem. + +The tests are Python scripts named `testsuite/*_test.py`, driven by the +`runtests.py` harness at the top of the tree (the old shell-based `runtests.sh` +is gone). Shared helpers live in `testsuite/rsyncfns.py`. A handful of C helper +programs (`tls`, `getgroups`, `trimslash`, …) are built alongside `rsync` and +used by some tests. Coverage notes are in [COVERAGE.md](COVERAGE.md). + +## Writing tests + +Favour readability — a test is also documentation of the behaviour it pins, so +prefer clarity over cleverness: + +* When a test writes an `rsyncd.conf`, write it as a triple-quoted f-string so + the actual config is readable top-to-bottom, with module parameters indented + with plain spaces. Don't build it from adjacent string literals full of `\n` + (and `\t`) escapes. The daemon's parser accepts space-indented parameters. +* Better still, use the structured helpers in `rsyncfns.py` when a stock config + will do: `write_daemon_conf(modules, globals)` (per-test modules/params) or + `build_rsyncd_conf()` (the four standard modules). They also handle the + root-only `uid`/`gid` lines for you (needed so a `use chroot = no` daemon run + as root can read a root-owned module). +* For config that varies (e.g. those root-only `uid`/`gid` lines), interpolate a + single optional block that expands when needed and is an empty string + otherwise, rather than splicing pieces together: + + ```python + root = get_testuid() == get_rootuid() + ids = f"uid = {get_rootuid()}\ngid = {get_rootgid()}" if root else "" + conf.write_text(f"""\ + pid file = {base}/rsyncd.pid + use chroot = no + {ids} + log file = {base}/rsyncd.log + + [m] + path = {mod} + read only = yes + """) + ``` + +## Running the tests + +### Via make + +Run from the build directory: + +- **`make check`** — build the helper programs and run the whole suite in + parallel (`CHECK_J`, default 8) against the just-built `./rsync`. You do **not** + need `make install` first; indeed you generally should not install before + testing. Use `make check CHECK_J=1` to run serially. +- **`make check29`** / **`make check30`** — the same, forcing protocol version 29 + or 30. +- **`make installcheck`** — run the suite against the *installed* binary (e.g. + `/usr/local/bin/rsync`). Per the GNU standards this does not search `$PATH`. + Handy for testing a distribution build. +- **`make check-progs`** — (re)build just the C helper programs the tests need, + without running anything. +- **`make coverage`** / **`coverage-tcp`** / **`coverage-all`** — generate an HTML + coverage report (needs `./configure --enable-coverage` and `gcovr`); + `coverage-all` merges runs across protocol versions and the tcp transport. + +### Via runtests.py directly + +`make check` just drives `runtests.py`; run it directly for finer control. It +defaults `--rsync-bin` to `./rsync`, so run it from the build directory (or pass +`--rsync-bin` / `--tooldir`): + +```sh +./runtests.py # all tests +./runtests.py chmod-temp-dir # a single test by name +./runtests.py 'xattr*' # a glob of test names +``` + +Useful options: + +- `-j N`, `--parallel N` — run up to N tests at once +- `--use-tcp` — run daemon tests against a real `rsyncd` on `127.0.0.1` (the + default runs them over a stdio pipe). **Read the security warning below before + using this on a shared machine.** +- `--protocol VER` — force a protocol version +- `--preserve-scratch` — keep each test's scratch dir afterwards +- `--log-level N`, `--always-log` — more verbose output / show logs for passing tests too +- `--stop-on-fail` — stop after the first failure +- `--timeout SECS` — per-test timeout (default 300) +- `--timing` — after the run, list the tests by wall-clock, slowest first, with + the serial sum and the floor set by the single longest test +- `--race-timeout SECS` — budget a TOCTOU race test may spend trying to win its + race. These are the suite's slowest tests: a race test is a negative oracle, + so it passes by spending its *whole* budget (5–15s each by default). Lowering + this speeds the suite up and weakens the oracle in equal measure. +- `--valgrind`, `--valgrind-opts OPTS` — run rsync under valgrind +- `--rsync-bin PATH`, `--tooldir DIR`, `--srcdir DIR` — locate the binary / build / source dirs +- `--expect-skipped LIST` — see skip enforcement below + +### Security warning: `--use-tcp` + +> **⚠️ Do not use `--use-tcp` on a machine with untrusted local users.** +> +> `--use-tcp` starts a real `rsync` daemon listening on a loopback TCP port +> (`127.0.0.1` / `::1`) and **deliberately configures insecure test scenarios** +> (daemon modules without authentication, unsafe options enabled, etc.). Loopback +> addresses are reachable by *every* local user, so for as long as the tests run, +> any other user on the machine can connect to that daemon and exploit those +> deliberately-insecure modules — potentially reading or writing files with the +> privileges of the user running the tests (which is **root** if you run the suite +> as root). +> +> Only run `--use-tcp` where there are **no possible local users who might try to +> exploit it** — a single-user workstation or a dedicated, isolated CI machine. +> The default stdio-pipe transport carries no such risk: it talks to the daemon +> over a private pipe with nothing listening on the network, so prefer it on any +> shared or multi-user host. + +### Results and exit codes + +Each test prints one result line — `PASS`, `FAIL`, `ERROR`, `SKIP` (with a +reason), or `XFAIL` (an expected failure) — and the run ends with a +`passed / failed / skipped` summary. Per-test exit-code convention: + +| code | meaning | +|------|---------| +| 0 | pass | +| 1 | fail | +| 2 | error | +| 77 | skip | +| 78 | xfail | + +`runtests.py` exits non-zero if any test fails. Some tests need root or another +precondition and otherwise `SKIP` — read the individual test scripts for details. + +**Skip enforcement:** on a full run, set `RSYNC_EXPECT_SKIPPED=a,b,c` (or +`--expect-skipped a,b,c`) and the run fails if the set of skipped tests does not +match. This is how the CI workflows pin each platform's expected skip set. An +`@FILE` entry reads a skip list (one test per line) instead, and several may be +composed: the workflows use +`@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt`. Keeping the +lists one-name-per-line is what stops two branches that each add a skipping test +from conflicting -- see `testsuite/skiplist/README.md`. + +### Scratch dirs and debugging + +Each test runs in `testtmp//`. On failure the scratch directory is left in +place (also `--preserve-scratch`); including its logs in a bug report is helpful. + +### Preconditions + +You need `python3`, `/bin/sh`, and the normal build toolchain. The ACL/xattr +tests need the `acl` and `attr` tools (`getfacl`/`setfacl`, +`getfattr`/`setfattr`) and skip if they are absent. Some tests need root. + +These tests also run in CI via GitHub Actions (see `.github/workflows/`). + +## Fleet testing (fleettest.py) + +`testsuite/fleettest.py` builds the committed HEAD of an rsync checkout on a +fleet of remote machines over ssh and runs the suite under both transports +(stdio-pipe and `--use-tcp`) in parallel, reporting only the *unexpected* +results. It is a fast local pre-flight for the GitHub CI matrix: each target +mirrors a `.github/workflows/*.yml` job — its configure flags, and the +`RSYNC_EXPECT_SKIPPED` list parsed straight from the workflow. + +Because every run includes a `--use-tcp` pass, the fleet stands up the insecure +loopback test daemon on each target — so only point it at machines with **no +untrusted local users** (see the [security warning](#security-warning---use-tcp) +above). + +The fleet — which machines, and how to reach and build on each — is described in +a JSON file. Copy the bundled example (it is git-ignored) and edit it for your +hosts: + +```sh +cp testsuite/fleettest.json.example testsuite/fleettest.json # then edit +# (or symlink it, or point elsewhere with --fleet PATH) +``` + +The config is looked up in order: `~/.fleettest.json` first, then +`testsuite/fleettest.json`, unless overridden with `--fleet PATH`. + +Each entry names an ssh host (`null` to run locally), the workflow it mirrors, +and its configure flags, plus optional per-target settings (`make`, `privilege`, +`env_prefix`, …). See the comments in `fleettest.json.example`. + +A target with `"nonroot": true` does an extra pass, after the main (root) run, +that reruns the privilege-sensitive tests as the unprivileged ssh user. Which +tests those are is **not** listed in the fleet config — a test opts in by +setting a module-level `fleet_nonroot = True`, so the set is maintained in the +test files and new privilege-sensitive tests join automatically with no +fleet-config change. + +A target with `"protocols": [30, 29]` runs one extra stdio-pipe pass per listed +version, each forcing that older wire version with `runtests --protocol=N` — the +fleet analogue of a workflow's `check30`/`check29` steps. Each pass takes the +`RSYNC_EXPECT_SKIPPED` spec from the workflow's own `check30`/`check29` step, so +a lane with extra protocol-gated skips (`check29` adds +`@testsuite/skiplist/proto29.txt`) is enforced correctly. They show up as +`protoNN` columns in the report (and `--timing` breakdown); targets that don't +set `protocols` show `-` there. + +Run it from inside a checkout (it builds the current directory's HEAD; use +`--repo PATH` for another tree): + +```sh +python3 testsuite/fleettest.py # whole fleet, both transports +python3 testsuite/fleettest.py --list # list configured targets +python3 testsuite/fleettest.py --targets NAME[,NAME] +python3 testsuite/fleettest.py --fleet other.json --transport pipe +python3 testsuite/fleettest.py --timing # per-target wall-clock breakdown +python3 testsuite/fleettest.py --keep-on-fail # keep logs + tree where it broke +python3 testsuite/fleettest.py --full-tcp # whole suite in the tcp pass too +``` + +`--timing` adds a per-target breakdown after the report — total wall-clock plus +the push / build / pipe / tcp / protoNN / nonroot phases, sorted slowest-first. Targets +run in parallel, so the whole run is gated by the slowest one; the phase columns +show whether that target's hold-up is the push, the build, or a test pass. It +also passes `--timing` down to each target's `runtests.py`, so the captured +output attributes a slow pass to individual tests. + +The `tcp` pass runs **only the tests that can reach the daemon transport**, since +it follows a full pipe pass over the very same build. `--use-tcp` is observable +through exactly one path — `RSYNC_TEST_USE_TCP` is read once in `rsyncfns` +(`USE_TCP`) and acted on once, in `start_test_daemon()` — so a test that never +gets there produces an identical result twice. That drops 186 of the 340 tests +and roughly a third of the pass's work; the count skipped is always printed. +Pass `--full-tcp` to sweep the whole suite there anyway. The narrowing applies +only when both transports run: under `--transport tcp` that pass is the only +one, so it runs the whole suite regardless. + +`--keep-on-fail [DIR]` makes a failure inspectable without repeating the run. +For every target that came back with anything unexpected it writes the full +build and per-transport output to `DIR///` (default +`./fleettest-logs`) and keeps that target's remote run dir, with the scratch +trees its failing tests left behind. Targets that came back clean are swept as +usual. This matters most for the race tests, which may not fail the same way +twice — and because a re-run costs a full configure + build on every machine. + +Each run gets its own randomly-named build dir on every target +(`-`), so two or three runs can share the same fleet without +interfering. The dir is removed when the run ends — on success or failure, and +best-effort on Ctrl-C/kill; pass `--keep` to retain it for inspection. A hard +kill (`SIGKILL`), or a signal arriving mid-push, can leave a stray +`-` behind; sweep leftovers with +`python3 testsuite/fleettest.py --cleanup` (scope it with `--targets`, and only +run it when no other fleet runs are active, since it removes *all* matching run +dirs on the selected targets). + +Each target must be provisioned with the build toolchain its workflow installs +(autoconf, automake, a C compiler, perl, a python3 markdown module such as +cmarkgfm or commonmark unless the flags pass `--disable-md2man`, and the dev +libraries its configure flags enable). A missing piece shows up as `BUILD-FAIL`. + +## Differential regression hunting (abdiff.py) + +`testsuite/abdiff.py` is a developer tool — **not** a `*_test.py`, so `runtests.py` +ignores it. It hunts *regressions* by running the **same benign transfer** with +two rsync binaries (`A` = the build under test, `B` = a baseline) and comparing +the OUTCOME. The oracle is: for a benign input, a correctness/behaviour change +between the builds must be **invisible**, so A and B must produce an identical +result. Any divergence is a regression candidate to investigate and, if real, +minimize into a `*_test.py`. + +It compares exit code, stderr (error markers + normalised text), `--stats` +"Literal data", the destination tree (content + full metadata: mode/uid/gid/ +mtime/size/symlink target/xattrs/ACLs/hardlink grouping), the `--itemize` list, +and — with `--cost` — peak process-group RSS (a resource-regression oracle that +functional comparison misses). A **stability gate** runs each binary several +times and escalates on a candidate diff; nondeterministic scenarios are +quarantined `FLAKY`, never reported as regressions. + +Run it from the build directory (so `./rsync` and `old_versions/` resolve): + +```sh +testsuite/abdiff.py # default: ./rsync vs old_versions/rsync_3.4.1 +testsuite/abdiff.py --sweep all -j5 # broad single pass, 5-way parallel +testsuite/abdiff.py --loop --timelimit 3600 --cost # hunt for an hour, resource oracle on +testsuite/abdiff.py --list --sweep all # list scenarios without running +``` + +Each finding is classed `DIFF` (regression candidate), `ALLOW` (an intentional, +documented behaviour change listed in the tool's allowlist), `BETTER` (A succeeds +where B fails), `FLAKY`, or `TIMEOUT`. Findings are printed and appended to a +per-run `abdiff-log_