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