https://bugs.gentoo.org/948106

Backport provided by Red Hat on the VINCE case.
diff --git a/receiver.c b/receiver.c
index 6b4b369..8031b8f 100644
--- a/receiver.c
+++ b/receiver.c
@@ -66,6 +66,7 @@ extern char sender_file_sum[MAX_DIGEST_LEN];
 extern struct file_list *cur_flist, *first_flist, *dir_flist;
 extern filter_rule_list daemon_filter_list;
 extern OFF_T preallocated_len;
+extern int fuzzy_basis;
 
 extern struct name_num_item *xfer_sum_nni;
 extern int xfer_sum_len;
@@ -551,6 +552,8 @@ int recv_files(int f_in, int f_out, char *local_name)
 	progress_init();
 
 	while (1) {
+		const char *basedir = NULL;
+
 		cleanup_disable();
 
 		/* This call also sets cur_flist. */
@@ -716,28 +719,34 @@ int recv_files(int f_in, int f_out, char *local_name)
 				fnamecmp = get_backup_name(fname);
 				break;
 			case FNAMECMP_FUZZY:
+				if (fuzzy_basis == 0) {
+					rprintf(FERROR_XFER, "rsync: refusing malicious fuzzy operation for %s\n", xname);
+					exit_cleanup(RERR_PROTOCOL);
+				}
 				if (file->dirname) {
-					pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, file->dirname, xname);
-					fnamecmp = fnamecmpbuf;
-				} else
-					fnamecmp = xname;
+					basedir = file->dirname;
+				}
+				fnamecmp = xname;
 				break;
 			default:
 				if (fnamecmp_type > FNAMECMP_FUZZY && fnamecmp_type-FNAMECMP_FUZZY <= basis_dir_cnt) {
 					fnamecmp_type -= FNAMECMP_FUZZY + 1;
 					if (file->dirname) {
-						stringjoin(fnamecmpbuf, sizeof fnamecmpbuf,
-							   basis_dir[fnamecmp_type], "/", file->dirname, "/", xname, NULL);
-					} else
-						pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], xname);
+						pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], file->dirname);
+						basedir = fnamecmpbuf;
+					} else {
+						basedir = basis_dir[fnamecmp_type];
+					}
+					fnamecmp = xname;
 				} else if (fnamecmp_type >= basis_dir_cnt) {
 					rprintf(FERROR,
 						"invalid basis_dir index: %d.\n",
 						fnamecmp_type);
 					exit_cleanup(RERR_PROTOCOL);
-				} else
-					pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], fname);
-				fnamecmp = fnamecmpbuf;
+				} else {
+					basedir = basis_dir[fnamecmp_type];
+					fnamecmp = fname;
+				}
 				break;
 			}
 			if (!fnamecmp || (daemon_filter_list.head
@@ -760,7 +769,7 @@ int recv_files(int f_in, int f_out, char *local_name)
 		}
 
 		/* open the file */
-		fd1 = do_open(fnamecmp, O_RDONLY, 0);
+		fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);
 
 		if (fd1 == -1 && protocol_version < 29) {
 			if (fnamecmp != fname) {
@@ -771,14 +780,20 @@ int recv_files(int f_in, int f_out, char *local_name)
 
 			if (fd1 == -1 && basis_dir[0]) {
 				/* pre-29 allowed only one alternate basis */
-				pathjoin(fnamecmpbuf, sizeof fnamecmpbuf,
-					 basis_dir[0], fname);
-				fnamecmp = fnamecmpbuf;
+				basedir = basis_dir[0];
+				fnamecmp = fname;
 				fnamecmp_type = FNAMECMP_BASIS_DIR_LOW;
-				fd1 = do_open(fnamecmp, O_RDONLY, 0);
+				fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);
 			}
 		}
 
+		if (basedir) {
+			// for the following code we need the full
+			// path name as a single string
+			pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basedir, fnamecmp);
+			fnamecmp = fnamecmpbuf;
+		}
+
 		one_inplace = inplace_partial && fnamecmp_type == FNAMECMP_PARTIAL_DIR;
 		updating_basis_or_equiv = one_inplace
 		    || (inplace && (fnamecmp == fname || fnamecmp_type == FNAMECMP_BACKUP));
diff --git a/syscall.c b/syscall.c
index d92074a..47c5ea5 100644
--- a/syscall.c
+++ b/syscall.c
@@ -33,6 +33,8 @@
 #include <sys/syscall.h>
 #endif
 
+#include "ifuncs.h"
+
 extern int dry_run;
 extern int am_root;
 extern int am_sender;
@@ -712,3 +714,82 @@ int do_open_nofollow(const char *pathname, int flags)
 
 	return fd;
 }
+
+/*
+  open a file relative to a base directory. The basedir can be NULL,
+  in which case the current working directory is used. The relpath
+  must be a relative path, and the relpath must not contain any
+  elements in the path which follow symlinks (ie. like O_NOFOLLOW, but
+  applies to all path components, not just the last component)
+
+  The relpath must also not contain any ../ elements in the path
+*/
+int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode)
+{
+	if (!relpath || relpath[0] == '/') {
+		// must be a relative path
+		errno = EINVAL;
+		return -1;
+	}
+	if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../")) {
+		// no ../ elements allowed in the relpath
+		errno = EINVAL;
+		return -1;
+	}
+
+#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY)
+	// really old system, all we can do is live with the risks
+	if (!basedir) {
+		return open(relpath, flags, mode);
+	}
+	char fullpath[MAXPATHLEN];
+	pathjoin(fullpath, sizeof fullpath, basedir, relpath);
+	return open(fullpath, flags, mode);
+#else
+	int dirfd = AT_FDCWD;
+	if (basedir != NULL) {
+		dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY);
+		if (dirfd == -1) {
+			return -1;
+		}
+	}
+	int retfd = -1;
+
+	char *path_copy = my_strdup(relpath, __FILE__, __LINE__);
+	if (!path_copy) {
+		return -1;
+	}
+	
+	for (const char *part = strtok(path_copy, "/");
+	     part != NULL;
+	     part = strtok(NULL, "/"))
+	{
+		int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
+		if (next_fd == -1 && errno == ENOTDIR) {
+			if (strtok(NULL, "/") != NULL) {
+				// this is not the last component of the path
+				errno = ELOOP;
+				goto cleanup;
+			}
+			// this could be the last component of the path, try as a file
+			retfd = openat(dirfd, part, flags | O_NOFOLLOW, mode);
+			goto cleanup;
+		}
+		if (next_fd == -1) {
+			goto cleanup;
+		}
+		if (dirfd != AT_FDCWD) close(dirfd);
+		dirfd = next_fd;
+	}
+
+	// the path must be a directory
+	errno = EINVAL;
+
+cleanup:
+	free(path_copy);
+	if (dirfd != AT_FDCWD) {
+		close(dirfd);
+	}
+	return retfd;
+#endif // O_NOFOLLOW, O_DIRECTORY
+}
