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
9 changes: 7 additions & 2 deletions .github/workflows/almalinux-8-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,13 @@ jobs:
run: ./rsync --version
- name: check
# In the container we already run as root, so no sudo. The
# crtimes-not-supported skip matches the other Linux jobs.
run: RSYNC_EXPECT_SKIPPED=crtimes make check
# crtimes-not-supported skip matches the other Linux jobs;
# daemon-chroot-acl and proxy-response-line-too-long skip because
# the default (secure) transport opens no listening socket.
run: RSYNC_EXPECT_SKIPPED=crtimes,daemon-chroot-acl,proxy-response-line-too-long make check
- name: check (TCP daemon transport)
# Second run exercising the real loopback-TCP daemon path.
run: ./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
- name: ssl file list
run: ./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/cygwin-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,16 @@ jobs:
- name: info
run: bash -c '/usr/local/bin/rsync --version'
- name: check
run: bash -c 'RSYNC_EXPECT_SKIPPED=acls-default,acls,bare-do-open-symlink-race,chdir-symlink-race,chown,daemon-chroot-acl,devices,dir-sgid,open-noatime,protected-regular,sender-flist-symlink-leak,simd-checksum,symlink-dirlink-basis make check'
# chown-fake / devices-fake / xattrs / xattrs-hlink now RUN on Cygwin
# (rsyncfns.py drives xattrs via getfattr/setfattr from the `attr`
# package installed above), verified on a real Cygwin host. The real
# chown/devices tests still skip (need root/mknod), as do the
# RESOLVE_BENEATH symlink-race tests.
run: bash -c 'RSYNC_EXPECT_SKIPPED=acls-default,acls,bare-do-open-symlink-race,chdir-symlink-race,chown,daemon-chroot-acl,devices,dir-sgid,open-noatime,protected-regular,proxy-response-line-too-long,sender-flist-symlink-leak,simd-checksum,symlink-dirlink-basis make check'
- name: check (TCP daemon transport)
# Second run with daemon tests over a real loopback rsyncd; the default
# 'make check' above uses the secure stdio-pipe transport.
run: bash -c './runtests.py --rsync-bin=`pwd`/rsync.exe --use-tcp -j 8'
- name: ssl file list
run: bash -c 'PATH="/usr/local/bin:$PATH" rsync-ssl --no-motd download.samba.org::rsyncftp/ || true'
- name: save artifact
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/freebsd-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
make
./rsync --version
make check
./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
uses: actions/upload-artifact@v4
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/macos-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,14 @@ jobs:
- name: info
run: rsync --version
- name: check
run: sudo RSYNC_EXPECT_SKIPPED=acls-default,chmod-temp-dir,chown-fake,daemon-chroot-acl,devices-fake,dir-sgid,open-noatime,protected-regular,simd-checksum,xattrs-hlink,xattrs make check
# chown-fake / devices-fake / xattrs / xattrs-hlink now RUN on macOS
# (rsyncfns.py drives xattrs via the `xattr` command), verified on a
# real macOS host, so they're no longer in the skip set.
run: sudo RSYNC_EXPECT_SKIPPED=acls-default,chmod-temp-dir,daemon-chroot-acl,dir-sgid,open-noatime,protected-regular,proxy-response-line-too-long,simd-checksum make check
- name: check (TCP daemon transport)
# Second run with daemon tests over a real loopback rsyncd; the default
# 'make check' above uses the secure stdio-pipe transport.
run: sudo ./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
- name: ssl file list
run: rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/netbsd-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
make
./rsync --version
make check
./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/openbsd-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
make
./rsync --version
make check
./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/solaris-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
make
./rsync --version
make check
./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
uses: actions/upload-artifact@v4
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/ubuntu-22.04-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ jobs:
- name: info
run: rsync --version
- name: check
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-chroot-acl,proxy-response-line-too-long make check
- name: check30
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check30
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-chroot-acl,proxy-response-line-too-long make check30
- name: check29
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check29
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-chroot-acl,proxy-response-line-too-long make check29
- name: check (TCP daemon transport)
# Second run with daemon tests over a real loopback rsyncd; the default
# 'make check' above uses the secure stdio-pipe transport.
run: sudo ./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
- name: ssl file list
run: rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
Expand Down
12 changes: 9 additions & 3 deletions .github/workflows/ubuntu-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,17 @@ jobs:
- name: info
run: rsync --version
- name: check
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-chroot-acl,proxy-response-line-too-long make check
- name: check30
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check30
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-chroot-acl,proxy-response-line-too-long make check30
- name: check29
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check29
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-chroot-acl,proxy-response-line-too-long make check29
- name: check (TCP daemon transport)
# Second run with daemon tests over a real loopback rsyncd. The default
# 'make check' above uses the secure stdio-pipe transport (no listening
# sockets); this run exercises the real TCP accept/auth path. Skip-set
# is env-dependent here (chroot-acl), so leave RSYNC_EXPECT_SKIPPED unset.
run: sudo ./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
- name: ssl file list
run: rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
Expand Down
30 changes: 19 additions & 11 deletions Makefile.in
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \
testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) t_chmod_secure$(EXEEXT) \
t_secure_relpath$(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 t_chmod_secure.o t_secure_relpath.o trimslash.o wildtest.o
Expand Down Expand Up @@ -319,17 +320,21 @@ test: check
# 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

.PHONY: check
check: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
$(srcdir)/runtests.py --rsync-bin=`pwd`/rsync$(EXEEXT)
$(srcdir)/runtests.py --rsync-bin=`pwd`/rsync$(EXEEXT) -j $(CHECK_J)

.PHONY: check29
check29: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
$(srcdir)/runtests.py --rsync-bin=`pwd`/rsync$(EXEEXT) --protocol=29
$(srcdir)/runtests.py --rsync-bin=`pwd`/rsync$(EXEEXT) -j $(CHECK_J) --protocol=29

.PHONY: check30
check30: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
$(srcdir)/runtests.py --rsync-bin=`pwd`/rsync$(EXEEXT) --protocol=30
$(srcdir)/runtests.py --rsync-bin=`pwd`/rsync$(EXEEXT) -j $(CHECK_J) --protocol=30

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@
Expand All @@ -343,22 +348,25 @@ simdtest$(EXEEXT): simd-checksum-x86_64.cpp $(HEADERS)
touch $@; \
fi

testsuite/chown-fake.test:
ln -s chown.test $(srcdir)/testsuite/chown-fake.test
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,
# if you want.

.PHONY: installcheck
installcheck: $(CHECK_PROGS) $(CHECK_SYMLINKS)
$(srcdir)/runtests.py --rsync-bin="$(bindir)/rsync$(EXEEXT)" --srcdir="$(srcdir)" --tooldir=`pwd`
$(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

Expand Down
74 changes: 64 additions & 10 deletions runtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ def parse_args():
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')
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()


Expand Down Expand Up @@ -151,17 +157,47 @@ def prep_scratch(scratchdir, srcdir, tooldir, setfacl_nodef):
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):
base = os.path.basename(path)
return base.endswith('.test') or base.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('.test'):
return base[:-len('.test')]
if base.endswith(_PY_TEST_SUFFIX):
return base[:-len(_PY_TEST_SUFFIX)]
return base


def collect_tests(suitedir, patterns):
"""Collect test scripts matching the given patterns."""
"""Collect test scripts (.test or _test.py) matching the given patterns."""
if not patterns:
tests = sorted(glob.glob(os.path.join(suitedir, '*.test')))
candidates = (glob.glob(os.path.join(suitedir, '*.test'))
+ 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:
if not pat.endswith('.test'):
pat = pat + '.test'
matches = sorted(glob.glob(os.path.join(suitedir, pat)))
tests.extend(matches)
# Accept either bare name ("mkpath"), explicit extension, or glob.
if pat.endswith('.test') or pat.endswith('.py'):
pats = [pat]
else:
pats = [pat + '.test', 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


Expand Down Expand Up @@ -203,11 +239,18 @@ def run_one_test(testscript, testbase, scratchdir, base_env, timeout,
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')
try:
with open(logfile, 'w') as log:
proc = subprocess.run(
['sh', '-e', testscript],
cmd,
stdout=log, stderr=subprocess.STDOUT,
env=env, timeout=timeout,
cwd=env.get('TOOLDIR', '.')
Expand Down Expand Up @@ -329,13 +372,19 @@ def main():
print(f' valgrind=enabled (logs in 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,
Expand All @@ -349,7 +398,12 @@ def main():
'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'
for k, v in shconfig.items():
if v:
base_env[k] = v
Expand All @@ -365,7 +419,7 @@ def main():
full_run = len(args.tests) == 0

# Record test order for consistent skipped-list output
test_order = {os.path.basename(t).replace('.test', ''): i for i, t in enumerate(tests)}
test_order = {_testbase(t): i for i, t in enumerate(tests)}

passed = 0
failed = 0
Expand Down Expand Up @@ -402,7 +456,7 @@ def process_result(tr):
with concurrent.futures.ThreadPoolExecutor(max_workers=args.parallel) as executor:
futures = {}
for testscript in tests:
testbase = os.path.basename(testscript).replace('.test', '')
testbase = _testbase(testscript)
scratchdir = os.path.join(scratchbase, testbase)
timeout = 600 if 'hardlinks' in testbase else args.timeout
f = executor.submit(
Expand All @@ -423,7 +477,7 @@ def process_result(tr):
else:
# Sequential execution
for testscript in tests:
testbase = os.path.basename(testscript).replace('.test', '')
testbase = _testbase(testscript)
scratchdir = os.path.join(scratchbase, testbase)
timeout = 600 if 'hardlinks' in testbase else args.timeout
tr = run_one_test(
Expand Down
30 changes: 28 additions & 2 deletions socket.c
Original file line number Diff line number Diff line change
Expand Up @@ -739,8 +739,12 @@ void set_socket_options(int fd, char *options)

/* 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;
Expand Down Expand Up @@ -792,6 +796,28 @@ static int socketpair_tcp(int fd[2])
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. */
{
struct sockaddr_in accepted_peer, our_local;
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;

Expand Down
Loading
Loading