Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions receiver.c
Original file line number Diff line number Diff line change
Expand Up @@ -963,11 +963,40 @@ int recv_files(int f_in, int f_out, char *local_name)
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));
Expand Down
70 changes: 70 additions & 0 deletions testsuite/partial_nowrite_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/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)
Loading