Blame SOURCES/rsync-3.2.3-cve-2022-29154.patch

78df90
diff --git a/exclude.c b/exclude.c
78df90
index e095744..7906caa 100644
78df90
--- a/exclude.c
78df90
+++ b/exclude.c
78df90
@@ -25,18 +25,26 @@
78df90
 
78df90
 extern int am_server;
78df90
 extern int am_sender;
78df90
+extern int am_generator;
78df90
 extern int eol_nulls;
78df90
 extern int io_error;
78df90
+extern int xfer_dirs;
78df90
+extern int recurse;
78df90
 extern int local_server;
78df90
 extern int prune_empty_dirs;
78df90
 extern int ignore_perishable;
78df90
+extern int old_style_args;
78df90
+extern int relative_paths;
78df90
 extern int delete_mode;
78df90
 extern int delete_excluded;
78df90
 extern int cvs_exclude;
78df90
 extern int sanitize_paths;
78df90
 extern int protocol_version;
78df90
+extern int read_batch;
78df90
+extern int list_only;
78df90
 extern int module_id;
78df90
 
78df90
+extern char *filesfrom_host;
78df90
 extern char curr_dir[MAXPATHLEN];
78df90
 extern unsigned int curr_dir_len;
78df90
 extern unsigned int module_dirlen;
78df90
@@ -44,8 +51,10 @@ extern unsigned int module_dirlen;
78df90
 filter_rule_list filter_list = { .debug_type = "" };
78df90
 filter_rule_list cvs_filter_list = { .debug_type = " [global CVS]" };
78df90
 filter_rule_list daemon_filter_list = { .debug_type = " [daemon]" };
78df90
+filter_rule_list implied_filter_list = { .debug_type = " [implied]" };
78df90
 
78df90
 int saw_xattr_filter = 0;
78df90
+int trust_sender_filter = 0;
78df90
 
78df90
 /* Need room enough for ":MODS " prefix plus some room to grow. */
78df90
 #define MAX_RULE_PREFIX (16)
78df90
@@ -288,6 +297,233 @@ static void add_rule(filter_rule_list *listp, const char *pat, unsigned int pat_
78df90
 	}
78df90
 }
78df90
 
78df90
+/* If the wildcards failed, the remote shell might give us a file matching the literal
78df90
+ * wildcards.  Since "*" & "?" already match themselves, this just needs to deal with
78df90
+ * failed "[foo]" idioms.
78df90
+ */
78df90
+static void maybe_add_literal_brackets_rule(filter_rule const *based_on, int arg_len)
78df90
+{
78df90
+	filter_rule *rule;
78df90
+	const char *arg = based_on->pattern, *cp;
78df90
+	char *p;
78df90
+	int cnt = 0;
78df90
+
78df90
+	if (arg_len < 0)
78df90
+		arg_len = strlen(arg);
78df90
+
78df90
+	for (cp = arg; *cp; cp++) {
78df90
+		if (*cp == '\\' && cp[1]) {
78df90
+			cp++;
78df90
+		} else if (*cp == '[')
78df90
+			cnt++;
78df90
+	}
78df90
+	if (!cnt)
78df90
+		return;
78df90
+
78df90
+	rule = new0(filter_rule);
78df90
+	rule->rflags = based_on->rflags;
78df90
+	rule->u.slash_cnt = based_on->u.slash_cnt;
78df90
+	p = rule->pattern = new_array(char, arg_len + cnt + 1);
78df90
+	for (cp = arg; *cp; ) {
78df90
+		if (*cp == '\\' && cp[1]) {
78df90
+			*p++ = *cp++;
78df90
+		} else if (*cp == '[')
78df90
+			*p++ = '\\';
78df90
+		*p++ = *cp++;
78df90
+	}
78df90
+	*p++ = '\0';
78df90
+
78df90
+	rule->next = implied_filter_list.head;
78df90
+	implied_filter_list.head = rule;
78df90
+	if (DEBUG_GTE(FILTER, 3)) {
78df90
+		rprintf(FINFO, "[%s] add_implied_include(%s%s)\n", who_am_i(), rule->pattern,
78df90
+			rule->rflags & FILTRULE_DIRECTORY ? "/" : "");
78df90
+	}
78df90
+}
78df90
+
78df90
+static char *partial_string_buf = NULL;
78df90
+static int partial_string_len = 0;
78df90
+void implied_include_partial_string(const char *s_start, const char *s_end)
78df90
+{
78df90
+	partial_string_len = s_end - s_start;
78df90
+	if (partial_string_len <= 0 || partial_string_len >= MAXPATHLEN) { /* too-large should be impossible... */
78df90
+		partial_string_len = 0;
78df90
+		return;
78df90
+	}
78df90
+	if (!partial_string_buf)
78df90
+		partial_string_buf = new_array(char, MAXPATHLEN);
78df90
+	memcpy(partial_string_buf, s_start, partial_string_len);
78df90
+}
78df90
+
78df90
+void free_implied_include_partial_string()
78df90
+{
78df90
+	if (partial_string_buf) {
78df90
+		free(partial_string_buf);
78df90
+		partial_string_buf = NULL;
78df90
+	}
78df90
+	partial_string_len = 0; /* paranoia */
78df90
+}
78df90
+
78df90
+/* Each arg the client sends to the remote sender turns into an implied include
78df90
+ * that the receiver uses to validate the file list from the sender. */
78df90
+void add_implied_include(const char *arg, int skip_daemon_module)
78df90
+{
78df90
+	filter_rule *rule;
78df90
+	int arg_len, saw_wild = 0, saw_live_open_brkt = 0, backslash_cnt = 0;
78df90
+	int slash_cnt = 1; /* We know we're adding a leading slash. */
78df90
+	const char *cp;
78df90
+	char *p;
78df90
+	if (am_server || old_style_args || list_only || read_batch || filesfrom_host != NULL)
78df90
+		return;
78df90
+	if (partial_string_len) {
78df90
+		arg_len = strlen(arg);
78df90
+		if (partial_string_len + arg_len >= MAXPATHLEN) {
78df90
+			partial_string_len = 0;
78df90
+			return; /* Should be impossible... */
78df90
+		}
78df90
+		memcpy(partial_string_buf + partial_string_len, arg, arg_len + 1);
78df90
+		partial_string_len = 0;
78df90
+		arg = partial_string_buf;
78df90
+	}
78df90
+	if (skip_daemon_module) {
78df90
+		if ((cp = strchr(arg, '/')) != NULL)
78df90
+			arg = cp + 1;
78df90
+		else
78df90
+			arg = "";
78df90
+	}
78df90
+	if (relative_paths) {
78df90
+		if ((cp = strstr(arg, "/./")) != NULL)
78df90
+			arg = cp + 3;
78df90
+	} else if ((cp = strrchr(arg, '/')) != NULL) {
78df90
+		arg = cp + 1;
78df90
+		if (*arg == '.' && arg[1] == '\0')
78df90
+			arg++;
78df90
+	}
78df90
+	arg_len = strlen(arg);
78df90
+	if (arg_len) {
78df90
+		if (strpbrk(arg, "*[?")) {
78df90
+			/* We need to add room to escape backslashes if wildcard chars are present. */
78df90
+			for (cp = arg; (cp = strchr(cp, '\\')) != NULL; cp++)
78df90
+				arg_len++;
78df90
+			saw_wild = 1;
78df90
+		}
78df90
+		arg_len++; /* Leave room for the prefixed slash */
78df90
+		rule = new0(filter_rule);
78df90
+		if (!implied_filter_list.head)
78df90
+			implied_filter_list.head = implied_filter_list.tail = rule;
78df90
+		else {
78df90
+			rule->next = implied_filter_list.head;
78df90
+			implied_filter_list.head = rule;
78df90
+		}
78df90
+		rule->rflags = FILTRULE_INCLUDE + (saw_wild ? FILTRULE_WILD : 0);
78df90
+		p = rule->pattern = new_array(char, arg_len + 1);
78df90
+		*p++ = '/';
78df90
+		for (cp = arg; *cp; ) {
78df90
+			switch (*cp) {
78df90
+			  case '\\':
78df90
+				if (cp[1] == ']') {
78df90
+					if (!saw_wild)
78df90
+						cp++; /* A \] in a non-wild filter causes a problem, so drop the \ . */
78df90
+				} else if (!strchr("*[?", cp[1])) {
78df90
+					backslash_cnt++;
78df90
+					if (saw_wild)
78df90
+						*p++ = '\\';
78df90
+				}
78df90
+				*p++ = *cp++;
78df90
+				break;
78df90
+			  case '/':
78df90
+				if (p[-1] == '/') { /* This is safe because of the initial slash. */
78df90
+					cp++;
78df90
+					break;
78df90
+				}
78df90
+				if (relative_paths) {
78df90
+					filter_rule const *ent;
78df90
+					int found = 0;
78df90
+					*p = '\0';
78df90
+					for (ent = implied_filter_list.head; ent; ent = ent->next) {
78df90
+						if (ent != rule && strcmp(ent->pattern, rule->pattern) == 0) {
78df90
+							found = 1;
78df90
+							break;
78df90
+						}
78df90
+					}
78df90
+					if (!found) {
78df90
+						filter_rule *R_rule = new0(filter_rule);
78df90
+						R_rule->rflags = FILTRULE_INCLUDE | FILTRULE_DIRECTORY;
78df90
+						/* Check if our sub-path has wildcards or escaped backslashes */
78df90
+						if (saw_wild && strpbrk(rule->pattern, "*[?\\"))
78df90
+							R_rule->rflags |= FILTRULE_WILD;
78df90
+						R_rule->pattern = strdup(rule->pattern);
78df90
+						R_rule->u.slash_cnt = slash_cnt;
78df90
+						R_rule->next = implied_filter_list.head;
78df90
+						implied_filter_list.head = R_rule;
78df90
+						if (DEBUG_GTE(FILTER, 3)) {
78df90
+							rprintf(FINFO, "[%s] add_implied_include(%s/)\n",
78df90
+								who_am_i(), R_rule->pattern);
78df90
+						}
78df90
+						if (saw_live_open_brkt)
78df90
+							maybe_add_literal_brackets_rule(R_rule, -1);
78df90
+					}
78df90
+				}
78df90
+				slash_cnt++;
78df90
+				*p++ = *cp++;
78df90
+				break;
78df90
+			  case '[':
78df90
+				saw_live_open_brkt = 1;
78df90
+				*p++ = *cp++;
78df90
+				break;
78df90
+			  default:
78df90
+				*p++ = *cp++;
78df90
+				break;
78df90
+			}
78df90
+		}
78df90
+		*p = '\0';
78df90
+		rule->u.slash_cnt = slash_cnt;
78df90
+		arg = rule->pattern;
78df90
+		arg_len = p - arg; /* We recompute it due to backslash weirdness. */
78df90
+		if (DEBUG_GTE(FILTER, 3))
78df90
+			rprintf(FINFO, "[%s] add_implied_include(%s)\n", who_am_i(), rule->pattern);
78df90
+		if (saw_live_open_brkt)
78df90
+			maybe_add_literal_brackets_rule(rule, arg_len);
78df90
+	}
78df90
+
78df90
+	if (recurse || xfer_dirs) {
78df90
+		/* Now create a rule with an added "/" & "**" or "*" at the end */
78df90
+		rule = new0(filter_rule);
78df90
+		rule->rflags = FILTRULE_INCLUDE | FILTRULE_WILD;
78df90
+		if (recurse)
78df90
+			rule->rflags |= FILTRULE_WILD2;
78df90
+		/* We must leave enough room for / * * \0. */
78df90
+		if (!saw_wild && backslash_cnt) {
78df90
+			/* We are appending a wildcard, so now the backslashes need to be escaped. */
78df90
+			p = rule->pattern = new_array(char, arg_len + backslash_cnt + 3 + 1);
78df90
+			for (cp = arg; *cp; ) {
78df90
+				if (*cp == '\\')
78df90
+					*p++ = '\\';
78df90
+				*p++ = *cp++;
78df90
+			}
78df90
+		} else {
78df90
+			p = rule->pattern = new_array(char, arg_len + 3 + 1);
78df90
+			if (arg_len) {
78df90
+				memcpy(p, arg, arg_len);
78df90
+				p += arg_len;
78df90
+			}
78df90
+		}
78df90
+		if (p[-1] != '/')
78df90
+			*p++ = '/';
78df90
+		*p++ = '*';
78df90
+		if (recurse)
78df90
+			*p++ = '*';
78df90
+		*p = '\0';
78df90
+		rule->u.slash_cnt = slash_cnt + 1;
78df90
+		rule->next = implied_filter_list.head;
78df90
+		implied_filter_list.head = rule;
78df90
+		if (DEBUG_GTE(FILTER, 3))
78df90
+			rprintf(FINFO, "[%s] add_implied_include(%s)\n", who_am_i(), rule->pattern);
78df90
+		if (saw_live_open_brkt)
78df90
+			maybe_add_literal_brackets_rule(rule, p - rule->pattern);
78df90
+	}
78df90
+}
78df90
+
78df90
 /* This frees any non-inherited items, leaving just inherited items on the list. */
78df90
 static void pop_filter_list(filter_rule_list *listp)
78df90
 {
78df90
@@ -702,11 +938,12 @@ static void report_filter_result(enum logcode code, char const *name,
78df90
 				 filter_rule const *ent,
78df90
 				 int name_flags, const char *type)
78df90
 {
78df90
+	int log_level = am_sender || am_generator ? 1 : 3;
78df90
+
78df90
 	/* If a trailing slash is present to match only directories,
78df90
 	 * then it is stripped out by add_rule().  So as a special
78df90
-	 * case we add it back in here. */
78df90
-
78df90
-	if (DEBUG_GTE(FILTER, 1)) {
78df90
+	 * case we add it back in the log output. */
78df90
+	if (DEBUG_GTE(FILTER, log_level)) {
78df90
 		static char *actions[2][2]
78df90
 		    = { {"show", "hid"}, {"risk", "protect"} };
78df90
 		const char *w = who_am_i();
78df90
@@ -714,7 +951,7 @@ static void report_filter_result(enum logcode code, char const *name,
78df90
 			      : name_flags & NAME_IS_DIR ? "directory"
78df90
 			      : "file";
78df90
 		rprintf(code, "[%s] %sing %s %s because of pattern %s%s%s\n",
78df90
-		    w, actions[*w!='s'][!(ent->rflags & FILTRULE_INCLUDE)],
78df90
+		    w, actions[*w=='g'][!(ent->rflags & FILTRULE_INCLUDE)],
78df90
 		    t, name, ent->pattern,
78df90
 		    ent->rflags & FILTRULE_DIRECTORY ? "/" : "", type);
78df90
 	}
78df90
@@ -886,6 +1123,7 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
78df90
 		}
78df90
 		switch (ch) {
78df90
 		case ':':
78df90
+			trust_sender_filter = 1;
78df90
 			rule->rflags |= FILTRULE_PERDIR_MERGE
78df90
 				      | FILTRULE_FINISH_SETUP;
78df90
 			/* FALL THROUGH */
78df90
diff --git a/flist.c b/flist.c
78df90
index 5a1e424..4e9dd10 100644
78df90
--- a/flist.c
78df90
+++ b/flist.c
78df90
@@ -72,6 +72,7 @@ extern int need_unsorted_flist;
78df90
 extern int sender_symlink_iconv;
78df90
 extern int output_needs_newline;
78df90
 extern int sender_keeps_checksum;
78df90
+extern int trust_sender_filter;
78df90
 extern int unsort_ndx;
78df90
 extern uid_t our_uid;
78df90
 extern struct stats stats;
78df90
@@ -82,8 +83,7 @@ extern char curr_dir[MAXPATHLEN];
78df90
 
78df90
 extern struct chmod_mode_struct *chmod_modes;
78df90
 
78df90
-extern filter_rule_list filter_list;
78df90
-extern filter_rule_list daemon_filter_list;
78df90
+extern filter_rule_list filter_list, implied_filter_list, daemon_filter_list;
78df90
 
78df90
 #ifdef ICONV_OPTION
78df90
 extern int filesfrom_convert;
78df90
@@ -971,6 +971,19 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
78df90
 		exit_cleanup(RERR_UNSUPPORTED);
78df90
 	}
78df90
 
78df90
+	if (*thisname != '.' || thisname[1] != '\0') {
78df90
+		int filt_flags = S_ISDIR(mode) ? NAME_IS_DIR : NAME_IS_FILE;
78df90
+		if (!trust_sender_filter /* a per-dir filter rule means we must trust the sender's filtering */
78df90
+		 && filter_list.head && check_filter(&filter_list, FINFO, thisname, filt_flags) < 0) {
78df90
+			rprintf(FERROR, "ERROR: rejecting excluded file-list name: %s\n", thisname);
78df90
+			exit_cleanup(RERR_PROTOCOL);
78df90
+		}
78df90
+		if (implied_filter_list.head && check_filter(&implied_filter_list, FINFO, thisname, filt_flags) <= 0) {
78df90
+			rprintf(FERROR, "ERROR: rejecting unrequested file-list name: %s\n", thisname);
78df90
+			exit_cleanup(RERR_PROTOCOL);
78df90
+		}
78df90
+	}
78df90
+
78df90
 	if (inc_recurse && S_ISDIR(mode)) {
78df90
 		if (one_file_system) {
78df90
 			/* Room to save the dir's device for -x */
78df90
diff --git a/io.c b/io.c
78df90
index b50a066..6d0a389 100644
78df90
--- a/io.c
78df90
+++ b/io.c
78df90
@@ -373,6 +373,7 @@ static void forward_filesfrom_data(void)
78df90
 			free_xbuf(&ff_xb);
78df90
 			if (ff_reenable_multiplex >= 0)
78df90
 				io_start_multiplex_out(ff_reenable_multiplex);
78df90
+			free_implied_include_partial_string();
78df90
 		}
78df90
 		return;
78df90
 	}
78df90
@@ -414,6 +415,7 @@ static void forward_filesfrom_data(void)
78df90
 		while (s != eob) {
78df90
 			if (*s++ == '\0') {
78df90
 				ff_xb.len = s - sob - 1;
78df90
+				add_implied_include(sob, 0);
78df90
 				if (iconvbufs(ic_send, &ff_xb, &iobuf.out, flags) < 0)
78df90
 					exit_cleanup(RERR_PROTOCOL); /* impossible? */
78df90
 				write_buf(iobuf.out_fd, s-1, 1); /* Send the '\0'. */
78df90
@@ -429,6 +431,7 @@ static void forward_filesfrom_data(void)
78df90
 			ff_lastchar = '\0';
78df90
 		else {
78df90
 			/* Handle a partial string specially, saving any incomplete chars. */
78df90
+			implied_include_partial_string(sob, s);
78df90
 			flags &= ~ICB_INCLUDE_INCOMPLETE;
78df90
 			if (iconvbufs(ic_send, &ff_xb, &iobuf.out, flags) < 0) {
78df90
 				if (errno == E2BIG)
78df90
@@ -445,13 +448,17 @@ static void forward_filesfrom_data(void)
78df90
 		char *f = ff_xb.buf + ff_xb.pos;
78df90
 		char *t = ff_xb.buf;
78df90
 		char *eob = f + len;
78df90
+		char *cur = t;
78df90
 		/* Eliminate any multi-'\0' runs. */
78df90
 		while (f != eob) {
78df90
 			if (!(*t++ = *f++)) {
78df90
+				add_implied_include(cur, 0);
78df90
+				cur = t;
78df90
 				while (f != eob && *f == '\0')
78df90
 					f++;
78df90
 			}
78df90
 		}
78df90
+		implied_include_partial_string(cur, t);
78df90
 		ff_lastchar = f[-1];
78df90
 		if ((len = t - ff_xb.buf) != 0) {
78df90
 			/* This will not circle back to perform_io() because we only get
78df90
diff --git a/main.c b/main.c
78df90
index 46b97b5..f124a2d 100644
78df90
--- a/main.c
78df90
+++ b/main.c
78df90
@@ -48,6 +48,7 @@ extern int called_from_signal_handler;
78df90
 extern int need_messages_from_generator;
78df90
 extern int kluge_around_eof;
78df90
 extern int got_xfer_error;
78df90
+extern int old_style_args;
78df90
 extern int msgs2stderr;
78df90
 extern int module_id;
78df90
 extern int read_only;
78df90
@@ -87,6 +88,7 @@ extern BOOL shutting_down;
78df90
 extern int backup_dir_len;
78df90
 extern int basis_dir_cnt;
78df90
 extern int default_af_hint;
78df90
+extern int trust_sender_filter;
78df90
 extern struct stats stats;
78df90
 extern char *stdout_format;
78df90
 extern char *logfile_format;
78df90
@@ -102,7 +104,7 @@ extern char curr_dir[MAXPATHLEN];
78df90
 extern char backup_dir_buf[MAXPATHLEN];
78df90
 extern char *basis_dir[MAX_BASIS_DIRS+1];
78df90
 extern struct file_list *first_flist;
78df90
-extern filter_rule_list daemon_filter_list;
78df90
+extern filter_rule_list daemon_filter_list, implied_filter_list;
78df90
 
78df90
 uid_t our_uid;
78df90
 gid_t our_gid;
78df90
@@ -611,11 +613,7 @@ static pid_t do_cmd(char *cmd, char *machine, char *user, char **remote_argv, in
78df90
 				rprintf(FERROR, "internal: args[] overflowed in do_cmd()\n");
78df90
 				exit_cleanup(RERR_SYNTAX);
78df90
 			}
78df90
-			if (**remote_argv == '-') {
78df90
-				if (asprintf(args + argc++, "./%s", *remote_argv++) < 0)
78df90
-					out_of_memory("do_cmd");
78df90
-			} else
78df90
-				args[argc++] = *remote_argv++;
78df90
+			args[argc++] = safe_arg(NULL, *remote_argv++);
78df90
 			remote_argc--;
78df90
 		}
78df90
 	}
78df90
@@ -642,6 +640,7 @@ static pid_t do_cmd(char *cmd, char *machine, char *user, char **remote_argv, in
78df90
 #ifdef ICONV_CONST
78df90
 		setup_iconv();
78df90
 #endif
78df90
+		trust_sender_filter = 1;
78df90
 	} else if (local_server) {
78df90
 		/* If the user didn't request --[no-]whole-file, force
78df90
 		 * it on, but only if we're not batch processing. */
78df90
@@ -1080,6 +1079,7 @@ static int do_recv(int f_in, int f_out, char *local_name)
78df90
 	}
78df90
 
78df90
 	am_generator = 1;
78df90
+	implied_filter_list.head = implied_filter_list.tail = NULL;
78df90
 	flist_receiving_enabled = True;
78df90
 
78df90
 	io_end_multiplex_in(MPLX_SWITCHING);
78df90
@@ -1475,6 +1475,10 @@ static int start_client(int argc, char *argv[])
78df90
 		rsync_port = 0;
78df90
 	}
78df90
 
78df90
+	/* A local transfer doesn't unbackslash anything, so leave the args alone. */
78df90
+	if (local_server)
78df90
+		old_style_args = 2;
78df90
+
78df90
 	if (!rsync_port && remote_argc && !**remote_argv) /* Turn an empty arg into a dot dir. */
78df90
 		*remote_argv = ".";
78df90
 
78df90
@@ -1500,6 +1504,8 @@ static int start_client(int argc, char *argv[])
78df90
 		char *dummy_host;
78df90
 		int dummy_port = rsync_port;
78df90
 		int i;
78df90
+		if (filesfrom_fd < 0)
78df90
+			add_implied_include(remote_argv[0], daemon_connection);
78df90
 		/* For remote source, any extra source args must have either
78df90
 		 * the same hostname or an empty hostname. */
78df90
 		for (i = 1; i < remote_argc; i++) {
78df90
@@ -1523,6 +1529,7 @@ static int start_client(int argc, char *argv[])
78df90
 			if (!rsync_port && !*arg) /* Turn an empty arg into a dot dir. */
78df90
 				arg = ".";
78df90
 			remote_argv[i] = arg;
78df90
+			add_implied_include(arg, daemon_connection);
78df90
 		}
78df90
 	}
78df90
 
78df90
diff --git a/receiver.c b/receiver.c
78df90
index 9df603f..3182e2d 100644
78df90
--- a/receiver.c
78df90
+++ b/receiver.c
78df90
@@ -584,10 +584,13 @@ int recv_files(int f_in, int f_out, char *local_name)
78df90
 		if (DEBUG_GTE(RECV, 1))
78df90
 			rprintf(FINFO, "recv_files(%s)\n", fname);
78df90
 
78df90
-		if (daemon_filter_list.head && (*fname != '.' || fname[1] != '\0')
78df90
-		 && check_filter(&daemon_filter_list, FLOG, fname, 0) < 0) {
78df90
-			rprintf(FERROR, "attempt to hack rsync failed.\n");
78df90
-			exit_cleanup(RERR_PROTOCOL);
78df90
+		if (daemon_filter_list.head && (*fname != '.' || fname[1] != '\0')) {
78df90
+			int filt_flags = S_ISDIR(file->mode) ? NAME_IS_DIR : NAME_IS_FILE;
78df90
+			if (check_filter(&daemon_filter_list, FLOG, fname, filt_flags) < 0) {
78df90
+				rprintf(FERROR, "ERROR: rejecting file transfer request for daemon excluded file: %s\n",
78df90
+					fname);
78df90
+				exit_cleanup(RERR_PROTOCOL);
78df90
+			}
78df90
 		}
78df90
 
78df90
 #ifdef SUPPORT_XATTRS
78df90
diff --git a/options.c b/options.c
78df90
index 3e530c2..7582236 100644
78df90
--- a/options.c
78df90
+++ b/options.c
78df90
@@ -99,6 +99,7 @@ int filesfrom_fd = -1;
78df90
 char *filesfrom_host = NULL;
78df90
 int eol_nulls = 0;
78df90
 int protect_args = -1;
78df90
+int old_style_args = -1;
78df90
 int human_readable = 1;
78df90
 int recurse = 0;
78df90
 int mkpath_dest_arg = 0;
78df90
@@ -287,7 +288,7 @@ static struct output_struct debug_words[COUNT_DEBUG+1] = {
78df90
 	DEBUG_WORD(DELTASUM, W_SND|W_REC, "Debug delta-transfer checksumming (levels 1-4)"),
78df90
 	DEBUG_WORD(DUP, W_REC, "Debug weeding of duplicate names"),
78df90
 	DEBUG_WORD(EXIT, W_CLI|W_SRV, "Debug exit events (levels 1-3)"),
78df90
-	DEBUG_WORD(FILTER, W_SND|W_REC, "Debug filter actions (levels 1-2)"),
78df90
+	DEBUG_WORD(FILTER, W_SND|W_REC, "Debug filter actions (levels 1-3)"),
78df90
 	DEBUG_WORD(FLIST, W_SND|W_REC, "Debug file-list operations (levels 1-4)"),
78df90
 	DEBUG_WORD(FUZZY, W_REC, "Debug fuzzy scoring (levels 1-2)"),
78df90
 	DEBUG_WORD(GENR, W_REC, "Debug generator functions"),
78df90
@@ -575,7 +576,7 @@ enum {OPT_SERVER = 1000, OPT_DAEMON, OPT_SENDER, OPT_EXCLUDE, OPT_EXCLUDE_FROM,
78df90
       OPT_READ_BATCH, OPT_WRITE_BATCH, OPT_ONLY_WRITE_BATCH, OPT_MAX_SIZE,
78df90
       OPT_NO_D, OPT_APPEND, OPT_NO_ICONV, OPT_INFO, OPT_DEBUG, OPT_BLOCK_SIZE,
78df90
       OPT_USERMAP, OPT_GROUPMAP, OPT_CHOWN, OPT_BWLIMIT, OPT_STDERR,
78df90
-      OPT_OLD_COMPRESS, OPT_NEW_COMPRESS, OPT_NO_COMPRESS,
78df90
+      OPT_OLD_COMPRESS, OPT_NEW_COMPRESS, OPT_NO_COMPRESS, OPT_OLD_ARGS,
78df90
       OPT_STOP_AFTER, OPT_STOP_AT,
78df90
       OPT_REFUSED_BASE = 9000};
78df90
 
78df90
@@ -779,6 +780,8 @@ static struct poptOption long_options[] = {
78df90
   {"files-from",       0,  POPT_ARG_STRING, &files_from, 0, 0, 0 },
78df90
   {"from0",           '0', POPT_ARG_VAL,    &eol_nulls, 1, 0, 0},
78df90
   {"no-from0",         0,  POPT_ARG_VAL,    &eol_nulls, 0, 0, 0},
78df90
+  {"old-args",         0,  POPT_ARG_NONE,   0, OPT_OLD_ARGS, 0, 0},
78df90
+  {"no-old-args",      0,  POPT_ARG_VAL,    &old_style_args, 0, 0, 0},
78df90
   {"protect-args",    's', POPT_ARG_VAL,    &protect_args, 1, 0, 0},
78df90
   {"no-protect-args",  0,  POPT_ARG_VAL,    &protect_args, 0, 0, 0},
78df90
   {"no-s",             0,  POPT_ARG_VAL,    &protect_args, 0, 0, 0},
78df90
@@ -1605,6 +1608,13 @@ int parse_arguments(int *argc_p, const char ***argv_p)
78df90
 			compress_choice = NULL;
78df90
 			break;
78df90
 
78df90
+		case OPT_OLD_ARGS:
78df90
+			if (old_style_args <= 0)
78df90
+				old_style_args = 1;
78df90
+			else
78df90
+				old_style_args++;
78df90
+			break;
78df90
+
78df90
 		case 'M':
78df90
 			arg = poptGetOptArg(pc);
78df90
 			if (*arg != '-') {
78df90
@@ -1914,6 +1924,21 @@ int parse_arguments(int *argc_p, const char ***argv_p)
78df90
 		max_alloc = size;
78df90
 	}
78df90
 
78df90
+	if (old_style_args < 0) {
78df90
+		if (!am_server && protect_args <= 0 && (arg = getenv("RSYNC_OLD_ARGS")) != NULL && *arg) {
78df90
+			protect_args = 0;
78df90
+			old_style_args = atoi(arg);
78df90
+		} else
78df90
+			old_style_args = 0;
78df90
+	} else if (old_style_args) {
78df90
+		if (protect_args > 0) {
78df90
+			snprintf(err_buf, sizeof err_buf,
78df90
+				 "--protect-args conflicts with --old-args.\n");
78df90
+			return 0;
78df90
+		}
78df90
+		protect_args = 0;
78df90
+	}
78df90
+
78df90
 	if (protect_args < 0) {
78df90
 		if (am_server)
78df90
 			protect_args = 0;
78df90
@@ -2451,6 +2476,71 @@ int parse_arguments(int *argc_p, const char ***argv_p)
78df90
 }
78df90
 
78df90
 
78df90
+static char SPLIT_ARG_WHEN_OLD[1];
78df90
+
78df90
+/**
78df90
+ * Do backslash quoting of any weird chars in "arg", append the resulting
78df90
+ * string to the end of the "opt" (which gets a "=" appended if it is not
78df90
+ * an empty or NULL string), and return the (perhaps malloced) result.
78df90
+ * If opt is NULL, arg is considered a filename arg that allows wildcards.
78df90
+ * If it is "" or any other value, it is considered an option.
78df90
+ **/
78df90
+char *safe_arg(const char *opt, const char *arg)
78df90
+{
78df90
+#define SHELL_CHARS "!#$&;|<>(){}\"' \t\\"
78df90
+#define WILD_CHARS  "*?[]" /* We don't allow remote brace expansion */
78df90
+	BOOL is_filename_arg = !opt;
78df90
+	char *escapes = is_filename_arg ? SHELL_CHARS : WILD_CHARS SHELL_CHARS;
78df90
+	BOOL escape_leading_dash = is_filename_arg && *arg == '-';
78df90
+	BOOL escape_leading_tilde = 0;
78df90
+	int len1 = opt && *opt ? strlen(opt) + 1 : 0;
78df90
+	int len2 = strlen(arg);
78df90
+	int extras = escape_leading_dash ? 2 : 0;
78df90
+	char *ret;
78df90
+	if (!protect_args && old_style_args < 2 && (!old_style_args || (!is_filename_arg && opt != SPLIT_ARG_WHEN_OLD))) {
78df90
+		const char *f;
78df90
+		if (!old_style_args && *arg == '~' && (relative_paths || !strchr(arg, '/'))) {
78df90
+			extras++;
78df90
+			escape_leading_tilde = 1;
78df90
+		}
78df90
+		for (f = arg; *f; f++) {
78df90
+			if (strchr(escapes, *f))
78df90
+				extras++;
78df90
+		}
78df90
+	}
78df90
+	if (!len1 && !extras)
78df90
+		return (char*)arg;
78df90
+	ret = new_array(char, len1 + len2 + extras + 1);
78df90
+	if (len1) {
78df90
+		memcpy(ret, opt, len1-1);
78df90
+		ret[len1-1] = '=';
78df90
+	}
78df90
+	if (escape_leading_dash) {
78df90
+		ret[len1++] = '.';
78df90
+		ret[len1++] = '/';
78df90
+		extras -= 2;
78df90
+	}
78df90
+	if (!extras)
78df90
+		memcpy(ret + len1, arg, len2);
78df90
+	else {
78df90
+		const char *f = arg;
78df90
+		char *t = ret + len1;
78df90
+		if (escape_leading_tilde)
78df90
+			*t++ = '\\';
78df90
+		while (*f) {
78df90
+                        if (*f == '\\') {
78df90
+				if (!is_filename_arg || !strchr(WILD_CHARS, f[1]))
78df90
+					*t++ = '\\';
78df90
+			} else if (strchr(escapes, *f))
78df90
+				*t++ = '\\';
78df90
+			*t++ = *f++;
78df90
+		}
78df90
+	}
78df90
+	ret[len1+len2+extras] = '\0';
78df90
+	return ret;
78df90
+}
78df90
+
78df90
+
78df90
 /**
78df90
  * Construct a filtered list of options to pass through from the
78df90
  * client to the server.
78df90
@@ -2633,9 +2723,7 @@ void server_options(char **args, int *argc_p)
78df90
 			set++;
78df90
 		else
78df90
 			set = iconv_opt;
78df90
-		if (asprintf(&arg, "--iconv=%s", set) < 0)
78df90
-			goto oom;
78df90
-		args[ac++] = arg;
78df90
+		args[ac++] = safe_arg("--iconv", set);
78df90
 	}
78df90
 #endif
78df90
 
78df90
@@ -2701,33 +2789,24 @@ void server_options(char **args, int *argc_p)
78df90
 	}
78df90
 
78df90
 	if (backup_dir) {
78df90
+		/* This split idiom allows for ~/path expansion via the shell. */
78df90
 		args[ac++] = "--backup-dir";
78df90
-		args[ac++] = backup_dir;
78df90
+		args[ac++] = safe_arg("", backup_dir);
78df90
 	}
78df90
 
78df90
 	/* Only send --suffix if it specifies a non-default value. */
78df90
-	if (strcmp(backup_suffix, backup_dir ? "" : BACKUP_SUFFIX) != 0) {
78df90
-		/* We use the following syntax to avoid weirdness with '~'. */
78df90
-		if (asprintf(&arg, "--suffix=%s", backup_suffix) < 0)
78df90
-			goto oom;
78df90
-		args[ac++] = arg;
78df90
-	}
78df90
+	if (strcmp(backup_suffix, backup_dir ? "" : BACKUP_SUFFIX) != 0)
78df90
+		args[ac++] = safe_arg("--suffix", backup_suffix);
78df90
 
78df90
-	if (checksum_choice) {
78df90
-		if (asprintf(&arg, "--checksum-choice=%s", checksum_choice) < 0)
78df90
-			goto oom;
78df90
-		args[ac++] = arg;
78df90
-	}
78df90
+	if (checksum_choice)
78df90
+		args[ac++] = safe_arg("--checksum-choice", checksum_choice);
78df90
 
78df90
 	if (do_compression == CPRES_ZLIBX)
78df90
 		args[ac++] = "--new-compress";
78df90
 	else if (compress_choice && do_compression == CPRES_ZLIB)
78df90
 		args[ac++] = "--old-compress";
78df90
-	else if (compress_choice) {
78df90
-		if (asprintf(&arg, "--compress-choice=%s", compress_choice) < 0)
78df90
-			goto oom;
78df90
-		args[ac++] = arg;
78df90
-	}
78df90
+	else if (compress_choice)
78df90
+		args[ac++] = safe_arg("--compress-choice", compress_choice);
78df90
 
78df90
 	if (am_sender) {
78df90
 		if (max_delete > 0) {
78df90
@@ -2736,14 +2815,10 @@ void server_options(char **args, int *argc_p)
78df90
 			args[ac++] = arg;
78df90
 		} else if (max_delete == 0)
78df90
 			args[ac++] = "--max-delete=-1";
78df90
-		if (min_size >= 0) {
78df90
-			args[ac++] = "--min-size";
78df90
-			args[ac++] = min_size_arg;
78df90
-		}
78df90
-		if (max_size >= 0) {
78df90
-			args[ac++] = "--max-size";
78df90
-			args[ac++] = max_size_arg;
78df90
-		}
78df90
+		if (min_size >= 0)
78df90
+			args[ac++] = safe_arg("--min-size", min_size_arg);
78df90
+		if (max_size >= 0)
78df90
+			args[ac++] = safe_arg("--max-size", max_size_arg);
78df90
 		if (delete_before)
78df90
 			args[ac++] = "--delete-before";
78df90
 		else if (delete_during == 2)
78df90
@@ -2767,17 +2842,12 @@ void server_options(char **args, int *argc_p)
78df90
 		if (do_stats)
78df90
 			args[ac++] = "--stats";
78df90
 	} else {
78df90
-		if (skip_compress) {
78df90
-			if (asprintf(&arg, "--skip-compress=%s", skip_compress) < 0)
78df90
-				goto oom;
78df90
-			args[ac++] = arg;
78df90
-		}
78df90
+		if (skip_compress)
78df90
+			args[ac++] = safe_arg("--skip-compress", skip_compress);
78df90
 	}
78df90
 
78df90
-	if (max_alloc_arg && max_alloc != DEFAULT_MAX_ALLOC) {
78df90
-		args[ac++] = "--max-alloc";
78df90
-		args[ac++] = max_alloc_arg;
78df90
-	}
78df90
+	if (max_alloc_arg && max_alloc != DEFAULT_MAX_ALLOC)
78df90
+		args[ac++] = safe_arg("--max-alloc", max_alloc_arg);
78df90
 
78df90
 	/* --delete-missing-args needs the cooperation of both sides, but
78df90
 	 * the sender can handle --ignore-missing-args by itself. */
78df90
@@ -2802,7 +2872,7 @@ void server_options(char **args, int *argc_p)
78df90
 	if (partial_dir && am_sender) {
78df90
 		if (partial_dir != tmp_partialdir) {
78df90
 			args[ac++] = "--partial-dir";
78df90
-			args[ac++] = partial_dir;
78df90
+			args[ac++] = safe_arg("", partial_dir);
78df90
 		}
78df90
 		if (delay_updates)
78df90
 			args[ac++] = "--delay-updates";
78df90
@@ -2825,17 +2895,11 @@ void server_options(char **args, int *argc_p)
78df90
 		args[ac++] = "--use-qsort";
78df90
 
78df90
 	if (am_sender) {
78df90
-		if (usermap) {
78df90
-			if (asprintf(&arg, "--usermap=%s", usermap) < 0)
78df90
-				goto oom;
78df90
-			args[ac++] = arg;
78df90
-		}
78df90
+		if (usermap)
78df90
+			args[ac++] = safe_arg("--usermap", usermap);
78df90
 
78df90
-		if (groupmap) {
78df90
-			if (asprintf(&arg, "--groupmap=%s", groupmap) < 0)
78df90
-				goto oom;
78df90
-			args[ac++] = arg;
78df90
-		}
78df90
+		if (groupmap)
78df90
+			args[ac++] = safe_arg("--groupmap", groupmap);
78df90
 
78df90
 		if (ignore_existing)
78df90
 			args[ac++] = "--ignore-existing";
78df90
@@ -2846,7 +2910,7 @@ void server_options(char **args, int *argc_p)
78df90
 
78df90
 		if (tmpdir) {
78df90
 			args[ac++] = "--temp-dir";
78df90
-			args[ac++] = tmpdir;
78df90
+			args[ac++] = safe_arg("", tmpdir);
78df90
 		}
78df90
 
78df90
 		if (basis_dir[0]) {
78df90
@@ -2856,7 +2920,7 @@ void server_options(char **args, int *argc_p)
78df90
 			 */
78df90
 			for (i = 0; i < basis_dir_cnt; i++) {
78df90
 				args[ac++] = alt_dest_opt(0);
78df90
-				args[ac++] = basis_dir[i];
78df90
+				args[ac++] = safe_arg("", basis_dir[i]);
78df90
 			}
78df90
 		}
78df90
 	}
78df90
@@ -2877,7 +2941,7 @@ void server_options(char **args, int *argc_p)
78df90
 	if (files_from && (!am_sender || filesfrom_host)) {
78df90
 		if (filesfrom_host) {
78df90
 			args[ac++] = "--files-from";
78df90
-			args[ac++] = files_from;
78df90
+			args[ac++] = safe_arg("", files_from);
78df90
 			if (eol_nulls)
78df90
 				args[ac++] = "--from0";
78df90
 		} else {
78df90
@@ -2923,7 +2987,7 @@ void server_options(char **args, int *argc_p)
78df90
 			exit_cleanup(RERR_SYNTAX);
78df90
 		}
78df90
 		for (j = 1; j <= remote_option_cnt; j++)
78df90
-			args[ac++] = (char*)remote_options[j];
78df90
+			args[ac++] = safe_arg(SPLIT_ARG_WHEN_OLD, remote_options[j]);
78df90
 	}
78df90
 
78df90
 	*argc_p = ac;
78df90
diff --git a/clientserver.c b/clientserver.c
78df90
index 48c15a6..feca9c8 100644
78df90
--- a/clientserver.c
78df90
+++ b/clientserver.c
78df90
@@ -47,6 +47,7 @@ extern int protocol_version;
78df90
 extern int io_timeout;
78df90
 extern int no_detach;
78df90
 extern int write_batch;
78df90
+extern int old_style_args;
78df90
 extern int default_af_hint;
78df90
 extern int logfile_format_has_i;
78df90
 extern int logfile_format_has_o_or_i;
78df90
@@ -288,20 +289,45 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char
78df90
 
78df90
 	sargs[sargc++] = ".";
78df90
 
78df90
+	if (!old_style_args)
78df90
+		snprintf(line, sizeof line, " %.*s/", modlen, modname);
78df90
+
78df90
 	while (argc > 0) {
78df90
 		if (sargc >= MAX_ARGS - 1) {
78df90
 		  arg_overflow:
78df90
 			rprintf(FERROR, "internal: args[] overflowed in do_cmd()\n");
78df90
 			exit_cleanup(RERR_SYNTAX);
78df90
 		}
78df90
-		if (strncmp(*argv, modname, modlen) == 0
78df90
-		 && argv[0][modlen] == '\0')
78df90
+		if (strncmp(*argv, modname, modlen) == 0 && argv[0][modlen] == '\0')
78df90
 			sargs[sargc++] = modname; /* we send "modname/" */
78df90
-		else if (**argv == '-') {
78df90
-			if (asprintf(sargs + sargc++, "./%s", *argv) < 0)
78df90
-				out_of_memory("start_inband_exchange");
78df90
-		} else
78df90
-			sargs[sargc++] = *argv;
78df90
+		else {
78df90
+			char *arg = *argv;
78df90
+			int extra_chars = *arg == '-' ? 2 : 0; /* a leading dash needs a "./" prefix. */
78df90
+			/* If --old-args was not specified, make sure that the arg won't split at a mod name! */
78df90
+			if (!old_style_args && (p = strstr(arg, line)) != NULL) {
78df90
+				do {
78df90
+					extra_chars += 2;
78df90
+				} while ((p = strstr(p+1, line)) != NULL);
78df90
+			}
78df90
+			if (extra_chars) {
78df90
+				char *f = arg;
78df90
+				char *t = arg = new_array(char, strlen(arg) + extra_chars + 1);
78df90
+				if (*f == '-') {
78df90
+					*t++ = '.';
78df90
+					*t++ = '/';
78df90
+				}
78df90
+				while (*f) {
78df90
+					if (*f == ' ' && strncmp(f, line, modlen+2) == 0) {
78df90
+						*t++ = '[';
78df90
+						*t++ = *f++;
78df90
+						*t++ = ']';
78df90
+					} else
78df90
+						*t++ = *f++;
78df90
+				}
78df90
+				*t = '\0';
78df90
+			}
78df90
+			sargs[sargc++] = arg;
78df90
+		}
78df90
 		argv++;
78df90
 		argc--;
78df90
 	}
78df90
diff --git a/rsync.1 b/rsync.1
78df90
index e0e13cf..e363827 100644
78df90
--- a/rsync.1
78df90
+++ b/rsync.1
78df90
@@ -194,32 +194,27 @@ the hostname omitted.  For instance, all these work:
78df90
 .nf
78df90
 rsync -av host:file1 :file2 host:file{3,4} /dest/
78df90
 rsync -av host::modname/file{1,2} host::modname/file3 /dest/
78df90
-rsync -av host::modname/file1 ::modname/file{3,4}
78df90
+rsync -av host::modname/file1 ::modname/file{3,4} /dest/
78df90
 .fi
78df90
 .RE
78df90
 .P
78df90
-Older versions of rsync required using quoted spaces in the SRC, like these
78df90
-examples:
78df90
+Starting this version of rsync, filenames are passed to a remote shell
78df90
+in such a way as to preserve the characters you give it.
78df90
+Thus, if you ask for a file with spaces in the name, that's what the
78df90
+remote rsync looks for:
78df90
 .RS 4
78df90
 .P
78df90
 .nf
78df90
-rsync -av host:'dir1/file1 dir2/file2' /dest
78df90
-rsync host::'modname/dir1/file1 modname/dir2/file2' /dest
78df90
+rsync -aiv host:'a simple file.pdf' /dest/
78df90
 .fi
78df90
 .RE
78df90
 .P
78df90
-This word-splitting still works (by default) in the latest rsync, but is not as
78df90
-easy to use as the first method.
78df90
-.P
78df90
-If you need to transfer a filename that contains whitespace, you can either
78df90
-specify the \fB\-\-protect-args\fP (\fB\-s\fP) option, or you'll need to escape the
78df90
-whitespace in a way that the remote shell will understand.  For instance:
78df90
-.RS 4
78df90
-.P
78df90
-.nf
78df90
-rsync -av host:'file\\ name\\ with\\ spaces' /dest
78df90
-.fi
78df90
-.RE
78df90
+If you use scripts that have been written to manually apply extra quoting to
78df90
+the remote rsync args (or to require remote arg splitting), you can ask rsync
78df90
+to let your script handle the extra escaping.  This is done by either adding
78df90
+the \fB\-\-old\-args\fP option to the rsync runs in the script (which requires
78df90
+a new rsync) or exporting \fBRSYNC_OLD_ARGS\fP=1 and \fBRSYNC_PROTECT_ARGS\fP=0
78df90
+(which works with old or new rsync versions).
78df90
 .P
78df90
 .SH "CONNECTING TO AN RSYNC DAEMON"
78df90
 .P
78df90
@@ -427,6 +422,7 @@ detailed description below for a complete description.
78df90
 --append                 append data onto shorter files
78df90
 --append-verify          --append w/old data in file checksum
78df90
 --dirs, -d               transfer directories without recursing
78df90
+--old-dirs, --old-d      works like --dirs when talking to old rsync
78df90
 --mkpath                 create the destination's path component
78df90
 --links, -l              copy symlinks as symlinks
78df90
 --copy-links, -L         transform symlink into referent file/dir
78df90
@@ -515,6 +511,7 @@ detailed description below for a complete description.
78df90
 --include-from=FILE      read include patterns from FILE
78df90
 --files-from=FILE        read list of source-file names from FILE
78df90
 --from0, -0              all *-from/filter files are delimited by 0s
78df90
+--old-args               disable the modern arg-protection idiom
78df90
 --protect-args, -s       no space-splitting; wildcard chars only
78df90
 --copy-as=USER[:GROUP]   specify user & optional group for the copy
78df90
 --address=ADDRESS        bind address for outgoing socket to daemon
78df90
@@ -1950,11 +1947,10 @@ Be cautious using this, as it is possible to toggle an option that will
78df90
 cause rsync to have a different idea about what data to expect next over
78df90
 the socket, and that will make it fail in a cryptic fashion.
78df90
 .IP
78df90
-Note that it is best to use a separate \fB\-\-remote-option\fP for each option
78df90
-you want to pass.  This makes your usage compatible with the
78df90
-\fB\-\-protect-args\fP option.  If that option is off, any spaces in your remote
78df90
-options will be split by the remote shell unless you take steps to protect
78df90
-them.
78df90
+Note that you should use a separate \fB\-M\fP for each remote option you
78df90
+want to pass. On older rsync versions, the presence of any spaces in the
78df90
+remote-option arg could cause it to be split into separate remote args, but
78df90
+this requires the use of \fB\-\-old\-args\fP in this version of rsync.
78df90
 .IP
78df90
 When performing a local transfer, the "local" side is the sender and the
78df90
 "remote" side is the receiver.
78df90
@@ -2169,26 +2165,64 @@ terminated by a null ('\\0') character, not a NL, CR, or CR+LF.  This
78df90
 affects \fB\-\-exclude-from\fP, \fB\-\-include-from\fP, \fB\-\-files-from\fP, and any merged
78df90
 files specified in a \fB\-\-filter\fP rule.  It does not affect \fB\-\-cvs-exclude\fP
78df90
 (since all names read from a .cvsignore file are split on whitespace).
78df90
+.IP "\fB\-\-old\-args\fP"
78df90
+This option tells rsync to stop trying to protect the arg values from
78df90
+unintended word-splitting or other misinterpretation by using its new
78df90
+backslash-escape idiom.  The newest default is for remote filenames to only
78df90
+allow wildcards characters to be interpretated by the shell while
78df90
+protecting other shell-interpreted characters (and the args of options get
78df90
+even wildcards escaped).  The only active wildcard characters on the remote
78df90
+side are: `*`, `?`, `[`, & `]`.
78df90
+.IP
78df90
+If you have a script that wants to use old-style arg splitting in the
78df90
+filenames, specify this option once.  If the remote shell has a problem
78df90
+with any backslash escapes, specify the option twice.
78df90
+.IP
78df90
+You may also control this setting via the RSYNC_OLD_ARGS environment
78df90
+variable.  If it has the value "1", rsync will default to a single-option
78df90
+setting.  If it has the value "2" (or more), rsync will default to a
78df90
+repeated-option setting.  If it is "0", you'll get the default escaping
78df90
+behavior.  The environment is always overridden by manually specified
78df90
+positive or negative options (the negative is \fB\-\-no\-old\-args\fP).
78df90
+.IP
78df90
+Note that this option also disables the extra safety check added in this
78df90
+version of rsync,
78df90
+that ensures that a remote sender isn't including extra top-level items in
78df90
+the file-list that you didn't request.  This side-effect is necessary
78df90
+because we can't know for sure what names to expect when the remote shell
78df90
+is interpreting the args.
78df90
+.IP
78df90
+This option conflicts with the \fB\-\-protect\-args\fP option.
78df90
+.IP
78df90
 .IP "\fB\-\-protect-args\fP, \fB\-s\fP"
78df90
 This option sends all filenames and most options to the remote rsync
78df90
-without allowing the remote shell to interpret them.  This means that
78df90
-spaces are not split in names, and any non-wildcard special characters are
78df90
-not translated (such as \fB~\fP, \fB$\fP, \fB;\fP, \fB&\fP, etc.).  Wildcards are expanded
78df90
-on the remote host by rsync (instead of the shell doing it).
78df90
+without allowing the remote shell to interpret them.  Wildcards are
78df90
+expanded on the remote host by rsync instead of the shell doing it.
78df90
+.IP
78df90
+This is similar to the new-style backslash-escaping of args that was added
78df90
+in this version of rsync, but supports some extra features and doesn't
78df90
+rely on backslash escaping in the remote shell.
78df90
 .IP
78df90
 If you use this option with \fB\-\-iconv\fP, the args related to the remote side
78df90
 will also be translated from the local to the remote character-set.  The
78df90
 translation happens before wild-cards are expanded.  See also the
78df90
 \fB\-\-files-from\fP option.
78df90
 .IP
78df90
-You may also control this option via the RSYNC_PROTECT_ARGS environment
78df90
-variable.  If this variable has a non-zero value, this option will be
78df90
-enabled by default, otherwise it will be disabled by default.  Either state
78df90
+You may also control this setting via the RSYNC_PROTECT_ARGS environment
78df90
+variable.  If it has a non-zero value, this setting will be enabled
78df90
+by default, otherwise it will be disabled by default.  Either state
78df90
 is overridden by a manually specified positive or negative version of this
78df90
 option (note that \fB\-\-no-s\fP and \fB\-\-no-protect-args\fP are the negative
78df90
-versions).  Since this option was first introduced in 3.0.0, you'll need to
78df90
-make sure it's disabled if you ever need to interact with a remote rsync
78df90
-that is older than that.
78df90
+versions). This environment variable is also superseded by a non-zero
78df90
+\fBRSYNC_OLD_ARGS\fP export.
78df90
+.IP
78df90
+You may need to disable this option when interacting with an older rsync
78df90
+(one prior to 3.0.0).
78df90
+.IP
78df90
+This option conflicts with the \fB\-\-old\-args\fP option.
78df90
+.IP
78df90
+Note that this option is incompatible with the use of the restricted rsync
78df90
+script (`rrsync`) since it hides options from the script's inspection.
78df90
 .IP
78df90
 Rsync can also be configured (at build time) to have this option enabled by
78df90
 default (with is overridden by both the environment and the command-line).
78df90
@@ -2675,7 +2708,10 @@ super-user (see also the \fB\-\-fake-super\fP option).  For the \fB\-\-groupmap\
78df90
 option to have any effect, the \fB\-g\fP (\fB\-\-groups\fP) option must be used (or
78df90
 implied), and the receiver will need to have permissions to set that group.
78df90
 .IP
78df90
-If your shell complains about the wildcards, use \fB\-\-protect-args\fP (\fB\-s\fP).
78df90
+An older rsync client may need to use \fB\-\-protect\-args\fP (\fB\-s\fP)
78df90
+to avoid a complaint about wildcard characters, but a modern rsync handles
78df90
+this automatically.
78df90
+.IP
78df90
 .IP "\fB\-\-chown=USER:GROUP\fP"
78df90
 This option forces all files to be owned by USER with group GROUP.  This is
78df90
 a simpler interface than using \fB\-\-usermap\fP and \fB\-\-groupmap\fP directly, but
78df90
@@ -2685,8 +2721,11 @@ will occur.  If GROUP is empty, the trailing colon may be omitted, but if
78df90
 USER is empty, a leading colon must be supplied.
78df90
 .IP
78df90
 If you specify "\fB\-\-chown=foo:bar\fP", this is exactly the same as specifying
78df90
-"\fB\-\-usermap=*:foo\ \-\-groupmap=*:bar\fP", only easier.  If your shell complains
78df90
-about the wildcards, use \fB\-\-protect-args\fP (\fB\-s\fP).
78df90
+"\fB\-\-usermap=*:foo\ \-\-groupmap=*:bar\fP", only easier.
78df90
+.IP
78df90
+An older rsync client may need to use \fB\-\-protect\-args\fP (\fB\-s\fP) to avoid a
78df90
+complaint about wildcard characters, but a modern rsync handles this
78df90
+automatically.
78df90
 .IP "\fB\-\-timeout=SECONDS\fP"
78df90
 This option allows you to set a maximum I/O timeout in seconds.  If no data
78df90
 is transferred for the specified time then rsync will exit.  The default is
78df90
@@ -4233,10 +4272,24 @@ The CVSIGNORE environment variable supplements any ignore patterns in
78df90
 .IP "\fBRSYNC_ICONV\fP"
78df90
 Specify a default \fB\-\-iconv\fP setting using this environment variable. (First
78df90
 supported in 3.0.0.)
78df90
+.IP "\fBRSYNC_OLD_ARGS\fP"
78df90
+Specify a "1" if you want the \fB\-\-old\-args\fP option to be enabled by default,
78df90
+a "2" (or more) if you want it to be enabled in the option-repeated state,
78df90
+or a "0" to make sure that it is disabled by default. When this environment
78df90
+variable is set to a non-zero value, it supersedes the \fBRSYNC_PROTECT_ARGS\fP
78df90
+variable.
78df90
+.IP
78df90
+This variable is ignored if \fB\-\-old\-args\fP, \fB\-\-no\-old\-args\fP, or
78df90
+\fB\-\-protect\-args\fP is specified on the command line.
78df90
 .IP "\fBRSYNC_PROTECT_ARGS\fP"
78df90
 Specify a non-zero numeric value if you want the \fB\-\-protect-args\fP option to
78df90
 be enabled by default, or a zero value to make sure that it is disabled by
78df90
 default. (First supported in 3.1.0.)
78df90
+.IP
78df90
+This variable is ignored if \fB\-\-protect\-args\fP, \fB\-\-no\-protect\-args\fP,
78df90
+or \fB\-\-old\-args\fP is specified on the command line.
78df90
+.IP
78df90
+This variable is ignored if \fBRSYNC_OLD_ARGS\fP is set to a non-zero value.
78df90
 .IP "\fBRSYNC_RSH\fP"
78df90
 The RSYNC_RSH environment variable allows you to override the default shell
78df90
 used as the transport for rsync.  Command line options are permitted after