extern int am_sender;
extern int am_server;
extern int am_daemon;
+extern int am_chrooted;
extern int am_root;
extern int msgs2stderr;
extern int rsync_port;
extern int preserve_xattrs;
extern int kluge_around_eof;
extern int munge_symlinks;
+extern int use_secure_symlinks;
extern int open_noatime;
extern int sanitize_paths;
extern int numeric_ids;
io_printf(f_out, "@ERROR: chroot failed\n");
return -1;
}
+ am_chrooted = 1;
module_chdir = module_dir;
}
}
}
+ /* Enable secure symlink handling for any non-chrooted daemon module.
+ * This prevents TOCTOU race attacks where an attacker could switch a
+ * directory to a symlink between path validation and file open.
+ * Match the gate used by the do_*_at() wrappers in syscall.c
+ * (am_daemon && !am_chrooted) -- the protection has nothing to do
+ * with symlink munging, so a module configured with
+ * "munge symlinks = false" must still get the secure-open path. */
+ use_secure_symlinks = am_daemon && !am_chrooted;
+
if (gid_list.count) {
gid_t *gid_array = gid_list.items;
if (setgid(gid_array[0])) {
rsyserr(FLOG, errno, "daemon chroot(\"%s\") failed", p);
return -1;
}
+ /* Deliberately do NOT set am_chrooted here. am_chrooted
+ * gates the per-module symlink-race defenses
+ * (secure_relative_open() and the do_*_at() wrappers in
+ * syscall.c) and means "the kernel is enforcing path
+ * confinement at the module boundary". The daemon chroot
+ * confines path resolution to the daemon-chroot directory,
+ * not to any individual module path -- modules sharing the
+ * daemon chroot are still distinguishable filesystem
+ * subtrees and a sender-controlled symlink in module A
+ * could redirect a syscall to module B (or to other files
+ * inside the daemon chroot) without the per-module
+ * defenses. Leave am_chrooted=0 here so secure_relative_open()
+ * still fires for "use chroot = no" modules. */
if (chdir("/") < 0) {
rsyserr(FLOG, errno, "daemon chdir(\"/\") failed");
return -1;
int allow_inc_recurse = 1;
int xfer_dirs = -1;
int am_daemon = 0;
+/* Set after a successful per-module chroot ("use chroot = yes") in
+ * clientserver.c. NOT set for the daemon-level "daemon chroot = /X"
+ * chroot: that confines path resolution to /X, but module paths
+ * /X/modA, /X/modB, etc. are not chroot boundaries, so the per-module
+ * symlink-race defenses (secure_relative_open() / do_*_at() in
+ * syscall.c, gated by `am_daemon && !am_chrooted`) must still fire
+ * even when the daemon is inside a daemon chroot. */
+int am_chrooted = 0;
int connect_timeout = 0;
int keep_partial = 0;
int safe_symlinks = 0;
int copy_unsafe_links = 0;
int munge_symlinks = 0;
+int use_secure_symlinks = 0;
int size_only = 0;
int daemon_bwlimit = 0;
int bwlimit = 0;
extern struct name_num_item *xfer_sum_nni;
extern int xfer_sum_len;
+extern int use_secure_symlinks;
static struct bitbag *delayed_bits = NULL;
static int phase = 0, redoing = 0;
* access to ensure that there is no race condition. They will be
* correctly updated after the right owner and group info is set.
* (Thanks to snabb@epipe.fi for pointing this out.) */
- fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
+ /* When use_secure_symlinks is on (non-chroot daemon with munge_symlinks),
+ * use secure_mkstemp to prevent symlink race attacks on parent directories. */
+ if (use_secure_symlinks)
+ fd = secure_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
+ else
+ fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
#if 0
/* In most cases parent directories will already exist because their
/* We now check to see if we are writing the file "inplace" */
if (inplace || one_inplace) {
fnametmp = one_inplace ? partialptr : fname;
- fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600);
+ /* When use_secure_symlinks is on (non-chroot daemon),
+ * use secure open to prevent symlink race attacks where an
+ * attacker could switch a directory to a symlink between
+ * path validation and file open. */
+ if (use_secure_symlinks)
+ fd2 = secure_relative_open(NULL, fnametmp, O_WRONLY|O_CREAT, 0600);
+ else
+ fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600);
#ifdef linux
if (fd2 == -1 && errno == EACCES) {
/* Maybe the error was due to protected_regular setting? */
- fd2 = do_open(fname, O_WRONLY, 0600);
+ if (use_secure_symlinks)
+ fd2 = secure_relative_open(NULL, fname, O_WRONLY, 0600);
+ else
+ fd2 = do_open(fname, O_WRONLY, 0600);
}
#endif
if (fd2 == -1) {
#endif // O_NOFOLLOW, O_DIRECTORY
}
+/* Fill buf with len random bytes. Prefers /dev/urandom for cryptographic
+ * quality; falls back to rand() if /dev/urandom cannot be opened or read
+ * (e.g. inside a chroot or container without /dev populated). */
+static void rand_bytes(unsigned char *buf, size_t len)
+{
+#ifndef O_CLOEXEC
+#define O_CLOEXEC 0
+#endif
+ int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ ssize_t n = read(fd, buf, len);
+ close(fd);
+ if (n == (ssize_t)len) {
+ return;
+ }
+ }
+ for (size_t i = 0; i < len; i++) {
+ buf[i] = (unsigned char)rand();
+ }
+}
+
+/*
+ Secure version of mkstemp that prevents symlink attacks on parent directories.
+ Like secure_relative_open(), this walks the path checking each component
+ with O_NOFOLLOW to prevent TOCTOU race conditions.
+
+ The template may be relative or absolute, but must not contain ../ components.
+ Returns fd on success, -1 on error.
+*/
+int secure_mkstemp(char *template, mode_t perms)
+{
+#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)
+ /* Fall back to regular mkstemp on old systems */
+ return do_mkstemp(template, perms);
+#else
+ char *lastslash;
+ int dirfd = AT_FDCWD;
+ int fd = -1;
+
+ if (!template) {
+ errno = EINVAL;
+ return -1;
+ }
+ if (strncmp(template, "../", 3) == 0 || strstr(template, "/../")) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ /* For absolute paths, start the secure walk from "/" rather than CWD. */
+ if (template[0] == '/') {
+ dirfd = open("/", O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
+ if (dirfd < 0)
+ return -1;
+ }
+
+ /* Find the last slash to separate directory from filename */
+ lastslash = strrchr(template, '/');
+ if (lastslash) {
+ char *path_copy = my_strdup(template, __FILE__, __LINE__);
+ if (!path_copy)
+ return -1;
+
+ /* Null-terminate at the last slash to get directory part */
+ path_copy[lastslash - template] = '\0';
+
+ /* Walk the directory path securely */
+ 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) {
+ int save_errno = errno;
+ free(path_copy);
+ if (dirfd != AT_FDCWD) close(dirfd);
+ errno = (save_errno == ELOOP) ? ELOOP : save_errno;
+ return -1;
+ }
+ if (dirfd != AT_FDCWD) close(dirfd);
+ dirfd = next_fd;
+ }
+ free(path_copy);
+ }
+
+ /* Now create the temp file in the securely-opened directory */
+ perms |= S_IWUSR;
+
+ /* Generate unique filename - we need to modify the template in place */
+ char *filename = lastslash ? lastslash + 1 : template;
+ size_t filename_len = strlen(filename);
+
+ if (filename_len < 6) {
+ if (dirfd != AT_FDCWD) close(dirfd);
+ errno = EINVAL;
+ return -1;
+ }
+ char *suffix = filename + filename_len - 6; /* Points to XXXXXX */
+ if (strcmp(suffix, "XXXXXX") != 0) {
+ if (dirfd != AT_FDCWD) close(dirfd);
+ errno = EINVAL;
+ return -1;
+ }
+
+ /* Try random suffixes until we find one that works */
+ static const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ for (int tries = 0; tries < 100; tries++) {
+ unsigned char rbytes[6];
+ rand_bytes(rbytes, sizeof(rbytes));
+ for (int i = 0; i < 6; i++)
+ suffix[i] = letters[rbytes[i] % (sizeof(letters) - 1)];
+
+ fd = openat(dirfd, filename, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, perms);
+ if (fd >= 0)
+ break;
+ if (errno != EEXIST) {
+ if (dirfd != AT_FDCWD) close(dirfd);
+ return -1;
+ }
+ }
+
+ if (fd >= 0) {
+ if (fchmod(fd, perms) != 0 && preserve_perms) {
+ int errno_save = errno;
+ close(fd);
+ unlinkat(dirfd, filename, 0);
+ if (dirfd != AT_FDCWD) close(dirfd);
+ errno = errno_save;
+ return -1;
+ }
+#if defined HAVE_SETMODE && O_BINARY
+ setmode(fd, O_BINARY);
+#endif
+ }
+
+ if (dirfd != AT_FDCWD) close(dirfd);
+ return fd;
+#endif
+}
+
/*
varient of do_open/do_open_nofollow which does do_open() if the
copy_links or copy_unsafe_links options are set and does