From: Andrew Tridgell Date: Thu, 4 Jun 2026 06:19:31 +0000 (+1000) Subject: testsuite: regression for the #829 daemon --chown/--groupmap wildcard X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=ac282725cdc4b78275c2061b92cdca0fc00fa0df;p=thirdparty%2Frsync.git testsuite: regression for the #829 daemon --chown/--groupmap wildcard Maps every source group to a second group the test user belongs to via a daemon upload (--groupmap='*:GID') and checks the wildcard took effect. Runs both arg modes: the default path (the '*' is safe_arg-escaped and the daemon must un-backslash it -- the regression) and --secluded-args (the '*' is sent raw over the protected channel, a guard that the fix left that path alone). Needs no root -- a non-root receiver can chgrp to a member group -- and was verified RED on a pre-fix binary (the escaped '\*' is ignored, gid unchanged) and GREEN after the fix. --- diff --git a/testsuite/daemon-groupmap-wild_test.py b/testsuite/daemon-groupmap-wild_test.py new file mode 100644 index 00000000..1f67846f --- /dev/null +++ b/testsuite/daemon-groupmap-wild_test.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# Regression test for issue #829. +# +# Without --secluded-args the client's safe_arg() backslash-escapes wildcard +# chars in option values, so --chown / --groupmap=*:GROUP is sent to a daemon +# as --groupmap=\*:GROUP. A daemon has no shell to strip the backslash, and +# read_args() used to store option args verbatim, so the receiver saw the +# literal "\*", the wildcard never matched, and the map was ignored (the +# module's configured gid won instead). The fix un-backslashes daemon option +# args. +# +# We run it both ways: +# * default args -- the '*' is safe_arg-escaped and the daemon must +# un-backslash it (the path the fix repairs); +# * --secluded-args -- the '*' is sent raw over the protected channel and +# read with unescape=0, so it must keep working too +# (a guard that the fix didn't disturb that path). +# +# No root needed: a non-root receiver can chgrp(2) to a group the test user +# belongs to, so we map every source group to a second such group and check +# the wildcard took effect. + +import os +import subprocess + +from rsyncfns import ( + SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon, + test_fail, test_skipped, write_daemon_conf, +) + +DAEMON_PORT = 12923 + +# Two distinct groups to map between. As root (the usual CI case) we can +# chgrp(2) to any gid, so take two distinct named groups from the group +# database; a non-root user can only chgrp to groups it belongs to, so use those +# (skip if it is in fewer than two). +if os.geteuid() == 0: + import grp + usable = [] + for gr in grp.getgrall(): + if gr.gr_gid not in usable: + usable.append(gr.gr_gid) + if len(usable) < 2: + test_skipped("need >=2 groups defined on the system") +else: + usable = [] + for g in [os.getgid()] + list(os.getgroups()): + if g not in usable: + usable.append(g) + if len(usable) < 2: + test_skipped("need >=2 groups the test user belongs to") +src_gid, dst_gid = usable[0], usable[1] + +moddir = SCRATCHDIR / 'gmod' +srcdir = SCRATCHDIR / 'gsrc' +makepath(moddir) + +conf = write_daemon_conf([('gmod', {'path': str(moddir), 'read only': 'no'})]) +url = start_test_daemon(conf, DAEMON_PORT) + 'gmod/' + + +def check(label, *extra_opts): + rmtree(moddir) + rmtree(srcdir) + makepath(moddir) + makepath(srcdir) + f = srcdir / 'f.dat' + f.write_text("hi\n") + os.chown(f, -1, src_gid) # source group differs from the map target + + # A --chown-style wildcard map sent to a daemon: the '*' must survive as a + # wildcard so every source group is remapped to dst_gid. + proc = subprocess.run( + rsync_argv('-rg', *extra_opts, f'--groupmap=*:{dst_gid}', str(f), url), + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + if proc.returncode != 0: + print(proc.stdout) + test_fail(f"[{label}] groupmap upload failed (rc={proc.returncode})") + + got = os.stat(moddir / 'f.dat').st_gid + if got != dst_gid: + test_fail(f"[{label}] --groupmap='*:{dst_gid}' wildcard ignored over " + f"daemon: got gid {got}, expected {dst_gid} (regression of #829)") + + +check('default-args') +check('secluded-args', '--secluded-args')