From 33385aefe4773e7a3982d41995681eb079c92d12 Mon Sep 17 00:00:00 2001
From: Andrew Tridgell <andrew@tridgell.net>
Date: Sat, 23 Nov 2024 12:26:10 +1100
Subject: [PATCH 2/4] added secure_relative_open()

this is an open that enforces no symlink following for all path
components in a relative path
---
 syscall.c | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 74 insertions(+)

Index: rsync-3.1.3/syscall.c
===================================================================
--- rsync-3.1.3.orig/syscall.c
+++ rsync-3.1.3/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;
@@ -578,3 +580,75 @@ int do_open_nofollow(const char *pathnam
 
 	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)
+*/
+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 !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
+}
Index: rsync-3.1.3/ifuncs.h
===================================================================
--- rsync-3.1.3.orig/ifuncs.h
+++ rsync-3.1.3/ifuncs.h
@@ -104,3 +104,11 @@ free_stat_x(stat_x *sx_p)
     }
 #endif
 }
+
+static inline char *my_strdup(const char *str, const char *file, int line)
+{
+    int len = strlen(str)+1;
+    char *buf = my_alloc(NULL, len, 1, file, line);
+    memcpy(buf, str, len);
+    return buf;
+}
\ No newline at end of file
Index: rsync-3.1.3/util2.c
===================================================================
--- rsync-3.1.3.orig/util2.c
+++ rsync-3.1.3/util2.c
@@ -25,6 +25,9 @@
 #include "itypes.h"
 #include "inums.h"
 
+extern size_t max_alloc;
+
+char *do_calloc = "42";
 /**
  * Sleep for a specified number of milliseconds.
  *
@@ -77,6 +80,26 @@ void *_realloc_array(void *ptr, unsigned
 	return realloc(ptr, size * num);
 }
 
+void *my_alloc(void *ptr, size_t num, size_t size, const char *file, int line)
+{
+	if (max_alloc && num >= max_alloc/size) {
+		if (!file)
+			return NULL;
+		rprintf(FERROR, "[%s] exceeded --max-alloc=%s setting (file=%s, line=%d)\n",
+			who_am_i(), do_big_num(max_alloc, 0, NULL), src_file(file), line);
+		exit_cleanup(RERR_MALLOC);
+	}
+	if (!ptr)
+		ptr = malloc(num * size);
+	else if (ptr == do_calloc)
+		ptr = calloc(num, size);
+	else
+		ptr = realloc(ptr, num * size);
+	if (!ptr && file)
+		_out_of_memory("my_alloc caller", file, line);
+	return ptr;
+}
+
 const char *sum_as_hex(int csum_type, const char *sum, int flist_csum)
 {
 	static char buf[MAX_DIGEST_LEN*2+1];
@@ -99,6 +122,27 @@ const char *sum_as_hex(int csum_type, co
 	return buf;
 }
 
+NORETURN void _out_of_memory(const char *msg, const char *file, int line)
+{
+	rprintf(FERROR, "[%s] out of memory: %s (file=%s, line=%d)\n", who_am_i(), msg, src_file(file), line);
+	exit_cleanup(RERR_MALLOC);
+}
+
+const char *src_file(const char *file)
+{
+	static const char *util2 = __FILE__;
+	static int prefix = -1;
+
+	if (prefix < 0) {
+		const char *cp = strrchr(util2, '/');
+		prefix = cp ? cp - util2 + 1 : 0;
+	}
+
+	if (prefix && strncmp(file, util2, prefix) == 0)
+		return file + prefix;
+	return file;
+}
+
 NORETURN void out_of_memory(const char *str)
 {
 	rprintf(FERROR, "ERROR: out of memory in %s [%s]\n", str, who_am_i());
Index: rsync-3.1.3/options.c
===================================================================
--- rsync-3.1.3.orig/options.c
+++ rsync-3.1.3/options.c
@@ -185,6 +185,9 @@ int link_dest = 0;
 int basis_dir_cnt = 0;
 char *dest_option = NULL;
 
+#define DEFAULT_MAX_ALLOC (1024L * 1024 * 1024)
+size_t max_alloc = DEFAULT_MAX_ALLOC;
+
 static int remote_option_alloc = 0;
 int remote_option_cnt = 0;
 const char **remote_options = NULL;
Index: rsync-3.1.3/Makefile.in
===================================================================
--- rsync-3.1.3.orig/Makefile.in
+++ rsync-3.1.3/Makefile.in
@@ -46,7 +46,7 @@ popt_OBJS=popt/findme.o  popt/popt.o  po
 	popt/popthelp.o popt/poptparse.o
 OBJS=$(OBJS1) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@
 
-TLS_OBJ = tls.o syscall.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@
+TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@
 
 # Programs we must have to run the test cases
 CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \
@@ -128,7 +128,7 @@ getgroups$(EXEEXT): getgroups.o
 getfsdev$(EXEEXT): getfsdev.o
 	$(CC) $(CFLAGS) $(LDFLAGS) -o $@ getfsdev.o $(LIBS)
 
-TRIMSLASH_OBJ = trimslash.o syscall.o lib/compat.o lib/snprintf.o
+TRIMSLASH_OBJ = trimslash.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o
 trimslash$(EXEEXT): $(TRIMSLASH_OBJ)
 	$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(TRIMSLASH_OBJ) $(LIBS)
 
Index: rsync-3.1.3/t_stub.c
===================================================================
--- rsync-3.1.3.orig/t_stub.c
+++ rsync-3.1.3/t_stub.c
@@ -31,6 +31,7 @@ int module_dirlen = 0;
 int preserve_acls = 0;
 int preserve_times = 0;
 int preserve_xattrs = 0;
+size_t max_alloc = 0; /* max_alloc is needed when combined with util2.o */
 char *partial_dir;
 char *module_dir;
 filter_rule_list daemon_filter_list;
Index: rsync-3.1.3/tls.c
===================================================================
--- rsync-3.1.3.orig/tls.c
+++ rsync-3.1.3/tls.c
@@ -51,8 +51,8 @@ int link_owner = 0;
 int nsec_times = 0;
 int preserve_perms = 0;
 int preserve_executability = 0;
-int preallocate_files = 0;
-int inplace = 0;
+// int preallocate_files = 0;
+// int inplace = 0;
 int noatime = 0;
 
 #ifdef SUPPORT_XATTRS
Index: rsync-3.1.3/trimslash.c
===================================================================
--- rsync-3.1.3.orig/trimslash.c
+++ rsync-3.1.3/trimslash.c
@@ -28,8 +28,8 @@ int read_only = 1;
 int list_only = 0;
 int preserve_perms = 0;
 int preserve_executability = 0;
-int preallocate_files = 0;
-int inplace = 0;
+// int preallocate_files = 0;
+// int inplace = 0;
 int noatime = 0;
 
 int
