From e57fd03284bbb2c1feaaf7fa1e71ba7a6e1c16ce Mon Sep 17 00:00:00 2001
From: Andrew Tridgell <andrew@tridgell.net>
Date: Sun, 1 Mar 2026 09:28:40 +1100
Subject: [PATCH 24/43] sender: fix read-path TOCTOU by opening from module
 root (CVE-2026-29518)

The sender's file open was vulnerable to the same TOCTOU symlink
race as the receiver-side basis-file open. change_pathname() calls
chdir() into subdirectories, which follows symlinks; an attacker
could race to swap a directory for a symlink between the chdir and
the file open, allowing reads of privileged files through the
daemon.

Reconstruct the full relative path (F_PATHNAME + fname) and open
via secure_relative_open() from the trusted module_dir, which
walks each path component without following symlinks. This is
independent of CWD, so the chdir race is neutralised.

CVE-2026-29518.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 sender.c | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/sender.c b/sender.c
index b1588b70..99f431fe 100644
--- a/sender.c
+++ b/sender.c
@@ -48,6 +48,8 @@ extern int make_backups;
 extern int inplace;
 extern int inplace_partial;
 extern int batch_fd;
+extern int use_secure_symlinks;
+extern char *module_dir;
 extern int write_batch;
 extern int file_old_total;
 extern BOOL want_progress_now;
@@ -352,7 +354,25 @@ void send_files(int f_in, int f_out)
 			exit_cleanup(RERR_PROTOCOL);
 		}
 
-		fd = do_open_checklinks(fname);
+		if (use_secure_symlinks) {
+			/* Open from module root to prevent TOCTOU race where
+			 * change_pathname's chdir follows a directory symlink.
+			 * Reconstruct the full path relative to module_dir
+			 * from F_PATHNAME (path) and f_name (fname). */
+			char secure_path[MAXPATHLEN];
+			int slen = snprintf(secure_path, sizeof secure_path, "%s%s%s", path, slash, fname);
+			if (slen >= (int)sizeof secure_path) {
+				io_error |= IOERR_GENERAL;
+				rprintf(FERROR_XFER, "path too long: %s%s%s\n", path, slash, fname);
+				free_sums(s);
+				if (protocol_version >= 30)
+					send_msg_int(MSG_NO_SEND, ndx);
+				continue;
+			}
+			fd = secure_relative_open(module_dir, secure_path, O_RDONLY, 0);
+		} else {
+			fd = do_open_checklinks(fname);
+		}
 		if (fd == -1) {
 			if (errno == ENOENT) {
 				enum logcode c = am_daemon && protocol_version < 28 ? FERROR : FWARNING;
-- 
2.51.0

