if (fd2 == -1 && errno == EACCES) {
/* Maybe the error was due to protected_regular setting? */
if (use_secure_symlinks)
- fd2 = secure_relative_open(NULL, fname, O_WRONLY, 0600);
+ fd2 = secure_relative_open(NULL, fnametmp, O_WRONLY, 0600);
else
- fd2 = do_open(fname, O_WRONLY, 0600);
+ fd2 = do_open(fnametmp, O_WRONLY, 0600);
}
#endif
+ if (fd2 == -1 && errno == EACCES) {
+ /* A read-only existing file: make it writable, then retry
+ * (its mode is restored after the transfer). On a
+ * non-chroot daemon fchmod() a no-follow fd rather than
+ * chmod the path, so a symlink raced into fnametmp can't
+ * redirect the chmod (do_chmod_at follows the final link). */
+ int errno_save = errno, chmod_ok;
+ if (use_secure_symlinks) {
+#ifdef O_NOFOLLOW
+ int cfd = secure_relative_open(NULL, fnametmp, O_RDONLY|O_NOFOLLOW, 0);
+ chmod_ok = cfd != -1 && fchmod(cfd, 0600) == 0;
+ if (cfd != -1)
+ close(cfd);
+#else
+ /* Without O_NOFOLLOW the resolver's oldest fallback would
+ * follow a raced symlink, so fail closed rather than
+ * chmod through it. */
+ chmod_ok = 0;
+#endif
+ } else
+ chmod_ok = do_chmod_at(fnametmp, 0600) == 0;
+ if (chmod_ok) {
+ if (use_secure_symlinks)
+ fd2 = secure_relative_open(NULL, fnametmp, O_WRONLY, 0600);
+ else
+ fd2 = do_open(fnametmp, O_WRONLY, 0600);
+ } else
+ errno = errno_save;
+ }
if (fd2 == -1) {
rsyserr(FERROR_XFER, errno, "open %s failed",
full_fname(fnametmp));
--- /dev/null
+#!/usr/bin/env python3
+#
+# Test that --partial and --delay-updates work as expected when then
+# permissions of the destination file prevent writing to it.
+
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import sys
+import tempfile
+
+from rsyncfns import make_data_file, cp_p, makepath, checkit, RSYNC, TMPDIR, get_testuid, get_rootuid
+
+BASEDIR = TMPDIR
+
+FROMDIR = BASEDIR / 'from'
+TODIR = BASEDIR / 'to'
+
+makepath(FROMDIR)
+makepath(TODIR)
+
+makepath(FROMDIR)
+make_data_file(FROMDIR / 'some_file', 1 * 1024 * 1024)
+os.chmod(FROMDIR / 'some_file', 0o444)
+
+makepath(TODIR / '.~tmp~')
+os.chmod(TODIR / '.~tmp~', 0o700)
+cp_p(FROMDIR / 'some_file', TODIR / '.~tmp~' / 'some_file')
+
+is_root = get_testuid() == get_rootuid()
+
+# As root the read-only dest temp wouldn't deny the write (root bypasses DAC),
+# so the EACCES path under test never fires. On Linux we can drop
+# CAP_DAC_OVERRIDE with setpriv inside a private mount namespace to force it;
+# where that isn't possible -- non-Linux, Python < 3.12, no mount privilege, or
+# a build dir the cap-dropped root can't even traverse (owned by an
+# unprivileged user with restrictive perms, e.g. a CI tree owned by the ssh
+# user at 0700) -- just run as root: the transfer still succeeds, it merely
+# doesn't exercise the chmod-retry path here (non-root runs do).
+_cwd_st = os.stat(os.getcwd())
+_cwd_traversable = ((_cwd_st.st_uid == 0 and _cwd_st.st_mode & 0o100)
+ or _cwd_st.st_mode & 0o001)
+if (is_root and sys.platform == 'linux' and hasattr(os, 'unshare')
+ and shutil.which('setpriv') and _cwd_traversable):
+ try:
+ cwd = Path(os.getcwd())
+ chown_target = None
+ for p in reversed(cwd.parents):
+ st = p.stat()
+ if not (st.st_uid == 0 or st.st_mode & 0o005):
+ chown_target = p
+ break
+ if chown_target is not None:
+ os.unshare(os.CLONE_NEWNS)
+ subprocess.run(['mount', '--make-rprivate', '/'], check=True)
+ tempdir = tempfile.mkdtemp()
+ subprocess.run(['mount', '--bind', cwd, tempdir], check=True)
+ subprocess.run(['mount', '-t', 'tmpfs', '-o', 'mode=0755', 'tmpfs', chown_target], check=True)
+ makepath(cwd)
+ subprocess.run(['mount', '--bind', tempdir, cwd], check=True)
+ subprocess.run(['umount', tempdir], check=True)
+ os.rmdir(tempdir)
+ import rsyncfns
+ rsyncfns.RSYNC = "setpriv --inh-caps -all --bounding-set -all " + RSYNC
+ except (OSError, subprocess.CalledProcessError):
+ pass # mount namespace denied (unprivileged container) -- run as root
+
+
+checkit(['-avv', '--partial', '--delay-updates', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)