mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-09-14 14:18:23 -04:00
Adds six coverage tests from the code-scanner run, each closing a measured
gap in a daemon or metadata code path the suite never reached:
- daemon-include-maxconn: rsyncd.conf &include/&merge directives +
`max connections`/`lock file` (params.c include_config, connection.c
claim_connection, util1.c lock_range).
- fake-super-acl-xattr: --fake-super -A stores ACLs as user.rsync.%aacl/
%dacl xattrs (acls.c am_root<0 IVAL/SIVAL pack, xattrs.c get/set/
del_def_xattr_acl); Linux-only (the user.rsync.* namespace).
- backup-crossdev-copy: make_backup() EXDEV copy-fallback for non-regular
files (do_symlink_at/do_mknod_at/copy_file); skips without a cross-dev
tmpfs.
- daemon-http-proxy: RSYNC_PROXY HTTP CONNECT (socket.c
establish_proxy_connection + base64 Proxy-Authorization + 503 branch).
- daemon-module-options: motd file, socket options, incoming/outgoing
chmod, dont compress, list=no, comment, --sockopts.
- daemon-chroot: `use chroot = yes` incl. the /outer/./inner split and
`temp dir`; probes CAP_SYS_CHROOT and skips cleanly without it.
clientserver.c flushes gcov counters just before chroot() in rsync_module()
so the per-connection child's pre-chroot lines reach disk (the build-tree
.gcda paths are unreachable post-chroot); no-op without --enable-coverage.
CI: the require_tcp-gated tests (daemon-chroot/-http-proxy/-module-options)
plus the Linux-only fake-super-acl-xattr and the cross-dev backup-crossdev-copy
are listed in the per-platform RSYNC_EXPECT_SKIPPED sets where they skip on the
pipe-transport make-check jobs.
Co-authored-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
120 lines
3.9 KiB
Python
120 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Daemon coverage: `use chroot = yes` and the `path = /outer/./inner`
|
|
chroot-with-inner-subdir syntax.
|
|
|
|
Every other daemon test in the suite forces `use chroot = no` (via
|
|
write_daemon_conf()'s globals) so the chroot setup in clientserver.c
|
|
rsync_module() -- the use_chroot path-split (~866-887), the actual
|
|
chroot(2) call (~1003-1012), and the `temp dir` module option -- was
|
|
never reached. Those lines are also where the per-connection child
|
|
loses the ability to write its .gcda counters (the build-tree paths
|
|
are outside the chroot), so a companion gcov_flush() lands just before
|
|
chroot() in clientserver.c; this test exercises that flush point and
|
|
everything before it.
|
|
|
|
Three modules:
|
|
chr use chroot = yes, plain path -> 880-887, 1003-1012
|
|
chrinner use chroot = yes, path = /outer/./in -> 868-877 split
|
|
chrtmp use chroot = yes, temp dir = /tmpd -> 1093-1095 lp_temp_dir
|
|
|
|
Requires CAP_SYS_CHROOT (root in the test container); skips cleanly
|
|
without it.
|
|
"""
|
|
|
|
import ctypes
|
|
import os
|
|
import subprocess
|
|
|
|
from rsyncfns import (
|
|
SCRATCHDIR, FROMDIR,
|
|
claim_ports, get_rootuid, get_testuid, make_tree, makepath, require_tcp,
|
|
rmtree, rsync_argv, start_test_daemon, test_fail, test_skipped,
|
|
write_daemon_conf,
|
|
)
|
|
|
|
if get_testuid() != get_rootuid():
|
|
test_skipped("use chroot = yes needs root (CAP_SYS_CHROOT)")
|
|
|
|
# Probe chroot() directly so a rootless/seccomp'd container that maps uid 0
|
|
# but withholds CAP_SYS_CHROOT skips cleanly instead of failing.
|
|
pid = os.fork()
|
|
if pid == 0:
|
|
try:
|
|
ctypes.CDLL(None).chroot(b"/")
|
|
os._exit(0)
|
|
except Exception:
|
|
os._exit(1)
|
|
_, st = os.waitpid(pid, 0)
|
|
if not (os.WIFEXITED(st) and os.WEXITSTATUS(st) == 0):
|
|
test_skipped("chroot(2) not permitted in this environment")
|
|
|
|
require_tcp("daemon chroot path needs the real start_daemon socket flow")
|
|
|
|
PORT = 19886
|
|
claim_ports(PORT)
|
|
|
|
src = FROMDIR
|
|
rmtree(src)
|
|
make_tree(src, depth=1)
|
|
|
|
# Module roots. For the /./ form the OUTER path is what gets chroot()'d
|
|
# and the INNER path is chdir'd inside it.
|
|
root_chr = SCRATCHDIR / 'chroot-plain'
|
|
root_outer = SCRATCHDIR / 'chroot-outer'
|
|
root_inner = root_outer / 'inner'
|
|
root_tmp = SCRATCHDIR / 'chroot-tmp'
|
|
tmpd = root_tmp / 'tmpd' # `temp dir` is interpreted INSIDE the chroot
|
|
for d in (root_chr, root_inner, root_tmp, tmpd):
|
|
rmtree(d)
|
|
makepath(root_chr, root_inner, root_tmp, tmpd)
|
|
|
|
mods = [
|
|
('chr', {
|
|
'path': str(root_chr),
|
|
'use chroot': 'yes',
|
|
'read only': 'no',
|
|
}),
|
|
('chrinner', {
|
|
# /outer/./inner: chroot to /outer, chdir to /inner inside.
|
|
'path': f'{root_outer}/./inner',
|
|
'use chroot': 'yes',
|
|
'read only': 'no',
|
|
}),
|
|
('chrtmp', {
|
|
'path': str(root_tmp),
|
|
'use chroot': 'yes',
|
|
'read only': 'no',
|
|
# Relative to the chroot root (= module path).
|
|
'temp dir': '/tmpd',
|
|
}),
|
|
]
|
|
conf = write_daemon_conf(mods, name='chroot.conf')
|
|
url = start_test_daemon(conf, PORT)
|
|
|
|
|
|
def push(module, dest):
|
|
r = subprocess.run(rsync_argv('-r', f'{src}/', f'{url}{module}/'),
|
|
capture_output=True, text=True)
|
|
if r.returncode != 0:
|
|
test_fail(f"push to [{module}] (use chroot=yes) failed "
|
|
f"(rc={r.returncode}):\n{r.stderr}")
|
|
if not any(dest.iterdir()):
|
|
test_fail(f"push to [{module}] wrote nothing into {dest}")
|
|
|
|
|
|
push('chr', root_chr)
|
|
push('chrinner', root_inner)
|
|
push('chrtmp', root_tmp)
|
|
|
|
# Pull from a chrooted module too (sender side under chroot).
|
|
pull_dst = SCRATCHDIR / 'pull-chr'
|
|
rmtree(pull_dst); makepath(pull_dst)
|
|
r = subprocess.run(rsync_argv('-r', f'{url}chr/', f'{pull_dst}/'),
|
|
capture_output=True, text=True)
|
|
if r.returncode != 0:
|
|
test_fail(f"pull from [chr] failed (rc={r.returncode}):\n{r.stderr}")
|
|
if not any(pull_dst.iterdir()):
|
|
test_fail("pull from [chr] produced nothing")
|
|
|
|
print("daemon-chroot: use chroot=yes (plain + /./inner + temp dir) push+pull ok")
|