]> git.ipfire.org Git - thirdparty/rsync.git/commitdiff
testsuite: regression for the #829 daemon --chown/--groupmap wildcard
authorAndrew Tridgell <andrew@tridgell.net>
Thu, 4 Jun 2026 06:19:31 +0000 (16:19 +1000)
committerAndrew Tridgell <andrew@tridgell.net>
Thu, 4 Jun 2026 20:35:12 +0000 (06:35 +1000)
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.

testsuite/daemon-groupmap-wild_test.py [new file with mode: 0644]

diff --git a/testsuite/daemon-groupmap-wild_test.py b/testsuite/daemon-groupmap-wild_test.py
new file mode 100644 (file)
index 0000000..1f67846
--- /dev/null
@@ -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')