From f47dce87e987c2965a6b77389f7ef60d942e12bf Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Mon, 21 Sep 2026 19:44:21 +1000 Subject: [PATCH] Backport 3.5.1 fixes --- batch.c | 8 +- flist.c | 259 +++++++++++++++++++++++++++++++- generator.c | 2 +- options.c | 24 ++- receiver.c | 20 ++- sender.c | 37 +++-- support/rrsync | 7 +- syscall.c | 389 ++++++++++++++++++++++++++++++++++++------------- t_stub.c | 2 + util1.c | 10 +- 10 files changed, 611 insertions(+), 147 deletions(-) diff --git a/batch.c b/batch.c index a45fe833..bab10233 100644 --- a/batch.c +++ b/batch.c @@ -271,12 +271,12 @@ void open_batch_files(void) exit_cleanup(RERR_FILEIO); } - /* --read-batch: the file's bytes drive the protocol parser, so refuse - * non-regular files (FIFO, device, socket) at the batch path. */ + /* --read-batch: the file's bytes drive the protocol parser, + * allow FIFOs used by shell process substitution while continuing to reject other non-regular inputs. */ if (!write_batch && batch_fd != STDIN_FILENO) { STRUCT_STAT st; - if (do_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode)) { - rprintf(FERROR, "Batch file %s is not a regular file\n", + if (do_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode) && !S_ISFIFO(st.st_mode)) { + rprintf(FERROR, "Batch file %s is neither a regular file nor a FIFO\n", full_fname(batch_name)); exit_cleanup(RERR_FILEIO); } diff --git a/flist.c b/flist.c index 5f0868d7..5f7ecd67 100644 --- a/flist.c +++ b/flist.c @@ -46,6 +46,7 @@ extern int recurse; extern int use_qsort; extern int xfer_dirs; extern int filesfrom_fd; +extern char *files_from; extern int one_file_system; extern int copy_devices; extern int copy_dirlinks; @@ -70,6 +71,7 @@ extern int prune_empty_dirs; extern int copy_links; extern int copy_unsafe_links; extern int insecure_links; +extern int filesfrom_owner_walk_override; extern int protocol_version; extern int sanitize_paths; extern int munge_symlinks; @@ -230,27 +232,250 @@ static int scan_dirfd = -1; static const char *scan_dir_prefix; static int scan_dir_prefix_len; +struct sender_source_root { + struct sender_source_root *next; + dev_t dev; + ino_t ino; + char path[1]; +}; + +static struct sender_source_root *sender_source_roots; +static struct sender_source_root *sender_source_root_fd_owner; +static int sender_source_root_fd = -1; + +static int sender_source_full_path(const char *path, char *full, size_t full_size) +{ + size_t len; + + if (*path == '/') + len = strlcpy(full, path, full_size); + else + len = pathjoin(full, full_size, curr_dir, path); + if (len >= full_size) { + errno = ENAMETOOLONG; + return -1; + } + clean_fname(full, CFN_COLLAPSE_DOT_DOT_DIRS | CFN_DROP_TRAILING_DOT_DIR); + return 0; +} + +static void remember_sender_source_root(const char *path, const STRUCT_STAT *st) +{ + struct sender_source_root *root; + char full[MAXPATHLEN]; + size_t len; + + if (sender_source_full_path(path, full, sizeof full) < 0) + overflow_exit("remember_sender_source_root"); + len = strlen(full); + + for (root = sender_source_roots; root; root = root->next) { + if (strcmp(root->path, full) == 0) + return; + } + root = (struct sender_source_root *)new_array(char, sizeof *root + len); + root->next = sender_source_roots; + root->dev = st->st_dev; + root->ino = st->st_ino; + memcpy(root->path, full, len + 1); + sender_source_roots = root; +} + +static void remember_sender_source_arg(const char *path, const STRUCT_STAT *st) +{ + STRUCT_STAT parent_st; + char full[MAXPATHLEN], *slash; + + if (S_ISDIR(st->st_mode)) { + remember_sender_source_root(path, st); + return; + } + if (sender_source_full_path(path, full, sizeof full) < 0) + overflow_exit("remember_sender_source_arg"); + slash = strrchr(full, '/'); + if (!slash) + return; + if (slash == full) + slash[1] = '\0'; + else + *slash = '\0'; + /* The operator selected this parent as part of the source argument. Pin + * its resolved identity while the file leaf remains O_NOFOLLOW later. */ + if (do_stat(full, &parent_st) == 0 && S_ISDIR(parent_st.st_mode)) + remember_sender_source_root(full, &parent_st); +} + +#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY +/* Keep one validated source root open at a time. A descriptor per explicit + * argument would make a large argument list exhaust the process limit. */ +static int sender_source_root_fd_for(struct sender_source_root *root) +{ + STRUCT_STAT st; + int fd, fl, saved_errno; + + if (sender_source_root_fd_owner == root && sender_source_root_fd >= 0) + return sender_source_root_fd; + if (sender_source_root_fd >= 0) + close(sender_source_root_fd); + sender_source_root_fd = -1; + sender_source_root_fd_owner = NULL; + + fd = open_anchor_dirfd(root->path); + if (fd < 0) + return -1; + if (do_fstat(fd, &st) < 0) + saved_errno = errno; + else if (st.st_dev != root->dev || st.st_ino != root->ino) + saved_errno = ELOOP; + else + saved_errno = 0; + if (saved_errno) { + close(fd); + errno = saved_errno; + return -1; + } + if ((fl = fcntl(fd, F_GETFD)) >= 0) + fcntl(fd, F_SETFD, fl | FD_CLOEXEC); + sender_source_root_fd_owner = root; + sender_source_root_fd = fd; + return fd; +} +#endif + +int open_sender_source_path(const char *path, int flags, int *matched) +{ +#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY + struct sender_source_root *root, *best = NULL; + char full[MAXPATHLEN], *rel; + size_t best_len = 0; + int rootfd, fd, saved_errno; + + *matched = 0; + if (!sender_source_roots) + return -1; + if (sender_source_full_path(path, full, sizeof full) < 0) + return -1; + for (root = sender_source_roots; root; root = root->next) { + size_t len = strlen(root->path); + if (len > best_len && strncmp(full, root->path, len) == 0 + && (root->path[len-1] == '/' || full[len] == '\0' || full[len] == '/')) { + best = root; + best_len = len; + } + } + if (!best) + return -1; + + *matched = 1; + rootfd = sender_source_root_fd_for(best); + if (rootfd < 0) + return -1; + rel = full + best_len; + while (*rel == '/') + rel++; + fd = secure_relative_open_at(rootfd, *rel ? rel : ".", flags, 0); + saved_errno = errno; + errno = saved_errno; + return fd; +#else + *matched = 0; + errno = ENOSYS; + return -1; +#endif +} + +void clear_sender_source_roots(void) +{ + if (sender_source_root_fd >= 0) + close(sender_source_root_fd); + sender_source_root_fd = -1; + sender_source_root_fd_owner = NULL; + while (sender_source_roots) { + struct sender_source_root *root = sender_source_roots; + sender_source_roots = root->next; + free(root); + } +} + +static int filesfrom_link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks) +{ +#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY + const char *bname; + int dfd, ret, save_errno; + + dfd = owner_walk_parent(path, &bname); + if (dfd < 0) + return -1; + if (am_root < 0) { + close(dfd); + return link_stat(path, stp, follow_dirlinks); + } + ret = do_lstat_atfd(dfd, bname, stp); + /* A list-selected directory symlink is a path component too. Resolve it + * through the ownership and confinement walk before following it. */ + if (ret == 0 && S_ISLNK(stp->st_mode) + && (follow_dirlinks || copy_links)) { + int targetfd = open_no_attacker_symlinks_dirfd(path); + if (targetfd >= 0) { + ret = do_fstat(targetfd, stp); + close(targetfd); + } else if (errno != ENOENT && errno != ENOTDIR && errno != EACCES) { + ret = -1; + } + } + save_errno = ret < 0 ? errno : 0; + close(dfd); + errno = save_errno; + return ret; +#else + return link_stat(path, stp, follow_dirlinks); +#endif +} + +static int filesfrom_readlink(const char *path, char *linkbuf, size_t bufsiz) +{ +#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY + const char *bname; + int dfd, ret, save_errno; + + dfd = owner_walk_parent(path, &bname); + if (dfd < 0) + return -1; + ret = do_readlink_atfd(dfd, bname, linkbuf, bufsiz); + save_errno = ret < 0 ? errno : 0; + close(dfd); + errno = save_errno; + return ret; +#else + return do_readlink(path, linkbuf, bufsiz); +#endif +} + static int scan_link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks) { /* Use the held scan fd only for a single component directly inside the * scanned dir, and only when am_root >= 0 (link_stat_at folds in no * fake-super %stat xattr; link_stat does so via get_stat_xattr, a no-op * once am_root >= 0). */ - if (scan_dirfd >= 0 && am_root >= 0 + if (scan_dirfd >= 0 && am_root >= 0 && !filesfrom_owner_walk_active() && strncmp(path, scan_dir_prefix, scan_dir_prefix_len) == 0 && path[scan_dir_prefix_len] == '/' && strchr(path + scan_dir_prefix_len + 1, '/') == NULL) return link_stat_at(scan_dirfd, path + scan_dir_prefix_len + 1, stp, follow_dirlinks); + if (filesfrom_owner_walk_active()) + return filesfrom_link_stat(path, stp, follow_dirlinks); return link_stat(path, stp, follow_dirlinks); } static int scan_readlink(const char *path, char *linkbuf, size_t bufsiz) { - if (scan_dirfd >= 0 && am_root >= 0 + if (scan_dirfd >= 0 && am_root >= 0 && !filesfrom_owner_walk_active() && strncmp(path, scan_dir_prefix, scan_dir_prefix_len) == 0 && path[scan_dir_prefix_len] == '/' && strchr(path + scan_dir_prefix_len + 1, '/') == NULL) return do_readlink_atfd(scan_dirfd, path + scan_dir_prefix_len + 1, linkbuf, bufsiz); + if (filesfrom_owner_walk_active()) + return filesfrom_readlink(path, linkbuf, bufsiz); return do_readlink(path, linkbuf, bufsiz); } @@ -2014,7 +2239,7 @@ static void interpret_stat_error(const char *fname, int is_dir) } #if defined HAVE_FDOPENDIR && defined HAVE_DIRFD -/* Open a source directory for scanning confined beneath the transfer root. +/* Open a source directory for scanning under the applicable source authority. * secure_relative_open() does a per-component O_NOFOLLOW walk that refuses a * parent component raced into a symlink pointing out of the tree; fdopendir() * then turns the held fd into the DIR* the scan reads. This mirrors the @@ -2025,13 +2250,24 @@ static void interpret_stat_error(const char *fname, int is_dir) * O_NOFOLLOW makes secure_relative_open() follow in-tree directory symlinks * beneath the anchor and refuse escapes, so this serves both the default * no-follow scan and a daemon's symlink-following scan (see the caller). + * Files-from entries instead use the ownership walk: their source base is + * operator-selected, but each list entry may not be, so only trusted-owned + * symlinks are followed and a trusted link may retain its legacy target. * Returns NULL with errno set on failure, like opendir(). */ static DIR *secure_opendir(const char *fbuf) { - int dfd, fl; + int dfd, fl, matched; DIR *d; - if (am_daemon && (!am_chrooted || module_dirlen) + if (filesfrom_owner_walk_active()) { + /* The source base is operator-selected, while each list entry may not + * be. Follow only trusted-owned symlinks while opening the directory. */ + dfd = open_no_attacker_symlinks(fbuf, O_RDONLY | O_DIRECTORY, 0); + } else if (!am_daemon && am_sender + && (dfd = open_sender_source_path(fbuf, O_RDONLY | O_DIRECTORY, &matched), matched)) { + /* The command-line directory is the operator-selected transfer root. + * Follow that root, then keep every recursive scan beneath its held fd. */ + } else if (am_daemon && (!am_chrooted || module_dirlen) && module_dir && module_dir[0] == '/' && *fbuf != '/' && module_dirfd >= 0 && curr_dir_len >= module_dirlen && strncmp(curr_dir, module_dir, module_dirlen) == 0 @@ -2246,8 +2482,11 @@ static void send_implied_dirs(int f, struct file_list *flist, char *fname, if (need_new_dir) { int save_copy_links = copy_links; int save_xfer_dirs = xfer_dirs; + int save_filesfrom_owner_walk = filesfrom_owner_walk_override; char *slash; + if (filesfrom_owner_walk_active()) + filesfrom_owner_walk_override = 1; copy_links = xfer_dirs = 1; *limit = '\0'; @@ -2276,6 +2515,7 @@ static void send_implied_dirs(int f, struct file_list *flist, char *fname, copy_links = save_copy_links; xfer_dirs = save_xfer_dirs; + filesfrom_owner_walk_override = save_filesfrom_owner_walk; if (!inc_recurse) goto done; @@ -2327,7 +2567,7 @@ static void send1extra(int f, struct file_struct *file, struct file_list *flist) if (file->flags & FLAG_CONTENT_DIR) { if (one_file_system) { STRUCT_STAT st; - if (link_stat(fbuf, &st, copy_dirlinks) != 0) { + if (scan_link_stat(fbuf, &st, copy_dirlinks) != 0) { interpret_stat_error(fbuf, True); return; } @@ -2363,7 +2603,7 @@ static void send1extra(int f, struct file_struct *file, struct file_list *flist) if (name_type != NORMAL_NAME) { STRUCT_STAT st = {0}; - if (name_type != MISSING_NAME && link_stat(fbuf, &st, 1) != 0) { + if (name_type != MISSING_NAME && scan_link_stat(fbuf, &st, 1) != 0) { interpret_stat_error(fbuf, True); continue; } @@ -2688,7 +2928,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[]) if (fn != fbuf) memmove(fbuf, fn, len + 1); - if (link_stat(fbuf, &st, copy_dirlinks || name_type != NORMAL_NAME) != 0 + if (scan_link_stat(fbuf, &st, copy_dirlinks || name_type != NORMAL_NAME) != 0 || (name_type != DOTDIR_NAME && is_excluded(fbuf, S_ISDIR(st.st_mode) != 0, SERVER_FILTERS)) || (relative_paths && path_is_daemon_excluded(fbuf, 1))) { if (errno != ENOENT || missing_args == 0) { @@ -2718,6 +2958,9 @@ struct file_list *send_file_list(int f, int argc, char *argv[]) rprintf(FINFO, "skipping directory %s\n", fbuf); continue; } + if (!am_daemon && !use_ff_fd && st.st_mode != 0 + && (relative_paths || S_ISDIR(st.st_mode))) + remember_sender_source_arg(fbuf, &st); if (inc_recurse && relative_paths && *fbuf) { if ((p = strchr(fbuf+1, '/')) != NULL) { diff --git a/generator.c b/generator.c index 399f30a8..ceac3520 100644 --- a/generator.c +++ b/generator.c @@ -1053,7 +1053,7 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp) if (dlen >= sizeof dir) { errno = ENAMETOOLONG; return -1; } memcpy(dir, path, dlen); dir[dlen] = '\0'; - if ((dfd = secure_relative_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0)) < 0) + if ((dfd = secure_relative_dirfd(NULL, dir)) < 0) return -1; r = link_stat_at(dfd, slash + 1, stp, 0); e = errno; diff --git a/options.c b/options.c index 0d2f80e5..98c0f032 100644 --- a/options.c +++ b/options.c @@ -1153,6 +1153,12 @@ static int count_args(const char **argv) return i; } +/* The largest value parse_size_arg() will accept when no explicit max_value is + * given. It is SIZE_MAX/2 rather than SIZE_MAX because the parser computes and + * returns the size as a signed ssize_t (with a negative return meaning error), + * so this keeps every accepted size representable as a positive ssize_t. */ +#define SIZE_ARG_MAX ((ssize_t)(SIZE_MAX / 2)) + /* If the size_arg is an invalid string or the value is < min_value, an error * is put into err_buf & the return is -1. Note that this parser does NOT * support negative numbers, so a min_value < 0 doesn't make any sense. */ @@ -1162,7 +1168,7 @@ static ssize_t parse_size_arg(const char *size_arg, char def_suf, const char *op int reps, mult, len; const char *arg, *err = "invalid", *min_max = NULL; ssize_t limit = -1, size = 1; - ssize_t size_max = max_value >= 0 ? max_value : (ssize_t)(SIZE_MAX / 2); + ssize_t size_max = max_value >= 0 ? max_value : SIZE_ARG_MAX; double dsize; for (arg = size_arg; isDigit(arg); arg++) {} @@ -2064,14 +2070,17 @@ int parse_arguments(int *argc_p, const char ***argv_p) ssize_t size = parse_size_arg(max_alloc_arg, 'B', "max-alloc", 1024*1024, -1, True); if (size < 0) goto cleanup; - if (size == 0) { - snprintf(err_buf, sizeof err_buf, "max-alloc must be greater than zero\n"); - goto cleanup; - } max_alloc = size; } + /* A 0 value means "as large as this build allows". We resolve it to the + * same ceiling parse_size_arg() enforces, so that --max-alloc=0 is exactly + * the largest value a user could also have typed, and never a limit that + * only the 0 spelling can reach. Note that max_alloc_arg is forwarded to + * the peer un-normalized (see server_options()), which is what lets each + * side resolve 0 against its own SIZE_MAX -- a 64-bit client and a 32-bit + * daemon each get their own ceiling from the one portable spelling. */ if (!max_alloc) - max_alloc = SIZE_MAX; + max_alloc = SIZE_ARG_MAX; if (old_style_args < 0) { if (!am_server && protect_args <= 0 && (arg = getenv("RSYNC_OLD_ARGS")) != NULL && *arg) { @@ -2639,7 +2648,8 @@ int parse_arguments(int *argc_p, const char ***argv_p) * module root -- e.g. a root-owned backup symlink. No-op off a * daemon (the module-root check only fires when am_daemon). */ int save_opr = operator_path_resolve; - operator_path_resolve = 1; + /* The daemon process is the only case that the files-from path comes from an untrusted argument */ + operator_path_resolve = am_daemon ? 1 : 0; filesfrom_fd = open_no_attacker_symlinks(files_from, O_RDONLY|O_BINARY, 0); operator_path_resolve = save_opr; if (filesfrom_fd < 0) { diff --git a/receiver.c b/receiver.c index 4d10eae1..0c11b759 100644 --- a/receiver.c +++ b/receiver.c @@ -124,10 +124,15 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags * and traverse a symlink the secure_relative_open path can't confine: resolve * it with the ownership walk, which follows a uid0/euid-owned symlink but * refuses a foreign one AND (via abspath_excluded_by_module) refuses a target - * the module's exclude hides -- closing the partial-dir exclude bypass. */ + * the module's exclude hides -- closing the partial-dir exclude bypass. The + * final component is opened with O_NOFOLLOW so an operator-path leaf symlink + * cannot be selected as an alternate basis. */ +#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY if (operator_path_resolve) { char fullpath[MAXPATHLEN]; const char *p = relpath; + const char *leaf; + int dfd, fd, saved_errno; if (basedir) { if (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) { errno = ENAMETOOLONG; @@ -135,8 +140,16 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags } p = fullpath; } - return open_no_attacker_symlinks(p, flags, mode); + dfd = owner_walk_parent(p, &leaf); + if (dfd < 0) + return -1; + fd = do_open_atfd(dfd, leaf, flags, mode); + saved_errno = fd < 0 ? errno : 0; + close(dfd); + errno = saved_errno; + return fd; } +#endif /* The confined resolver is needed for the sanitizing daemon * (am_daemon && !am_chrooted) and for a /./ inner-module chroot @@ -1044,7 +1057,8 @@ int recv_files(int f_in, int f_out, char *local_name) /* A peer's basis selector cannot enable direct output through a path * that the confined basis open did not validate. */ - one_inplace = inplace_partial && fnamecmp_type == FNAMECMP_PARTIAL_DIR + one_inplace = inplace_partial && partial_dir + && fnamecmp_type == FNAMECMP_PARTIAL_DIR && fd1 != -1; updating_basis_or_equiv = one_inplace || (inplace && (fnamecmp == fname || fnamecmp_type == FNAMECMP_BACKUP)); diff --git a/sender.c b/sender.c index 38488c0a..79e945b5 100644 --- a/sender.c +++ b/sender.c @@ -57,6 +57,7 @@ extern char *module_dir; extern int module_dirfd; extern int write_batch; extern int file_old_total; +extern char *files_from; extern BOOL want_progress_now; extern struct stats stats; extern struct file_list *cur_flist, *first_flist, *dir_flist; @@ -248,11 +249,9 @@ static int sender_open_copylinks_confined(const char *anchor, const char *relpat * only this branch would hand it to strcmp(). */ if (am_daemon && module_dirfd >= 0 && module_dir && anchor && strcmp(anchor, module_dir) == 0) - pdfd = secure_relative_open_at_beneath(module_dirfd, dir, - O_RDONLY | O_DIRECTORY, 0); + pdfd = secure_relative_dirfd_at_beneath(module_dirfd, dir); else - pdfd = secure_relative_open(anchor, dir, - O_RDONLY | O_DIRECTORY, 0); + pdfd = secure_relative_dirfd(anchor, dir); if (pdfd < 0) return -1; n = readlinkat(pdfd, bname, tgt, sizeof tgt - 1); @@ -480,15 +479,26 @@ void send_files(int f_in, int f_out) fd = sender_open_copylinks_confined(module_dir, relp); else fd = secure_relative_open(module_dir, relp, O_RDONLY | O_NOFOLLOW, 0); - } else if (fname[0] != '/' && !copy_links && !copy_unsafe_links && !copy_dirlinks && !insecure_links) { - /* Default symlink handling (no -L/--copy-unsafe-links/-k follow): - * the scan recorded this as a regular file under a real-directory - * parent. Open it confined beneath the transfer root that - * change_pathname() chdir'd into, so a parent raced into a symlink - * after the scan isn't followed out of the tree, and O_NOFOLLOW - * refuses a raced leaf symlink. A symlink-following mode keeps the - * legacy open below. */ - fd = secure_relative_open(NULL, fname, O_RDONLY | O_NOFOLLOW, 0); + } else if (!copy_links && !copy_unsafe_links && !copy_dirlinks && !insecure_links) { + int matched; + /* A files-from entry follows only trusted-owned ancestors because + * its source base is operator-selected but the entry itself may not + * be. Other paths stay confined beneath their explicit transfer root. + * Every file leaf remains O_NOFOLLOW. */ + if (files_from) { + fd = do_open_checklinks(fname); + } else { + fd = open_sender_source_path(fname, O_RDONLY | O_NOFOLLOW, &matched); + if (!matched) { + if (fname[0] == '/') { + const char *relp = fname; + while (*relp == '/') + relp++; + fd = secure_relative_open("/", relp, O_RDONLY | O_NOFOLLOW, 0); + } else + fd = secure_relative_open(NULL, fname, O_RDONLY | O_NOFOLLOW, 0); + } + } } else { fd = do_open_checklinks(fname); } @@ -598,6 +608,7 @@ void send_files(int f_in, int f_out) if (DEBUG_GTE(SEND, 1)) rprintf(FINFO, "send files finished\n"); + clear_sender_source_roots(); match_report(); write_ndx(f_out, NDX_DONE); diff --git a/support/rrsync b/support/rrsync index e1145700..060c8f08 100755 --- a/support/rrsync +++ b/support/rrsync @@ -694,8 +694,13 @@ def validated_arg(opt, arg, typ=3, wild=False): if arg.startswith('./'): arg = arg[1:] arg = arg.replace('//', '/') + # One leading slash means the root of the restricted tree. + arg = re.sub(r'^/+', '/', arg) is_absolute_arg = args.absolute and opt == 'arg' and args.dir != '/' and (arg == args.dir or arg.startswith(args.dir_slash)) - if not is_absolute_arg: + is_root_relative_arg = arg.startswith('/') and not is_absolute_arg + if is_root_relative_arg and args.dir != '/': + arg = args.dir + arg + elif not is_absolute_arg: arg = arg.lstrip('/') if args.dir != '/': if HAS_DOT_DOT_RE.search(arg): diff --git a/syscall.c b/syscall.c index 04c6b1f5..12609ea2 100644 --- a/syscall.c +++ b/syscall.c @@ -56,13 +56,30 @@ extern int preserve_executability; extern int open_noatime; extern int copy_links; extern int copy_unsafe_links; +extern int copy_dirlinks; extern int operator_path_resolve; /* defined below; fwd-declared for the exclude check */ extern unsigned int module_dirlen; extern char *module_dir; extern int module_dirfd; /* daemon: served module root pinned by identity, or -1 */ +extern char *files_from; extern char *confine_root; /* --confine-root, or NULL; see confinement_root() */ extern unsigned int confine_rootlen; extern char curr_dir[MAXPATHLEN]; /* defined below; fwd-declared for the seed */ +extern int filesfrom_owner_walk_override; /* defined below; used by files-from implied dirs */ + +/* A directory fd used only for pathname traversal, fchdir(), or as *at() + * authority does not need read permission on Linux. Keep the portable + * O_RDONLY fallback for systems without O_PATH. */ +static int directory_traverse_flags(void) +{ +#if defined O_PATH && defined O_DIRECTORY + return O_PATH | O_DIRECTORY; +#elif defined O_DIRECTORY + return O_RDONLY | O_DIRECTORY; +#else + return O_RDONLY; +#endif +} int secure_relpath_active(void) { @@ -93,6 +110,16 @@ int symlink_optout_allowed(void) return insecure_links; } +/* A files-from entry is peer-selected, even when its source base was supplied + * by the operator. Keep its ownership walk inside --confine-root when one is + * active. */ +int filesfrom_owner_walk_active(void) +{ + return !am_daemon && am_sender && files_from + && !copy_unsafe_links && !copy_dirlinks && !insecure_links + && (!copy_links || filesfrom_owner_walk_override); +} + /* The root an operator/peer-supplied path must stay under, or NULL when nothing * is confined. A daemon has the served module; a server launched by a wrapper * with its own restricted directory (rrsync) gets one from --confine-root. @@ -110,33 +137,52 @@ static const char *confinement_root(unsigned int *lenp) return confine_root; } -/* Split the "/proc//fd" prefix off `p`, returning the tail -- "" for - * the pin directory itself, otherwise a string starting with '/'. NULL when `p` - * is not in the fd-pin namespace at all. */ +/* Split a recognised fd-pin prefix off `p`, returning the tail -- "" for the + * pin directory itself, otherwise a string starting with '/'. NULL when `p` + * is not in an fd-pin namespace. */ static const char *fd_pin_tail(const char *p) { - const char *s; + const char *s; - if (strncmp(p, "/proc/", 6) != 0) - return NULL; - s = p + 6; - if (strncmp(s, "self/", 5) == 0) /* "/proc/self/..." */ - s += 4; - else { /* "/proc//..." */ - const char *d = s; - while (*s >= '0' && *s <= '9') - s++; - if (s == d || *s != '/') - return NULL; - } - if (strncmp(s, "/fd", 3) != 0) - return NULL; - s += 3; - return (*s == '\0' || *s == '/') ? s : NULL; + /* Group all /dev/ checks under a single prefix comparison */ + if (strncmp(p, "/dev/", 5) == 0) { + s = p + 5; + if (strncmp(s, "fd", 2) == 0) { + s += 2; + return (*s == '\0' || *s == '/') ? s : NULL; + } + if (strncmp(s, "std", 3) == 0) { + s += 3; + if (strncmp(s, "in", 3) == 0) + return "/0"; + if (strncmp(s, "out", 4) == 0) + return "/1"; + if (strncmp(s, "err", 4) == 0) + return "/2"; + } + return NULL; /* Instantly reject any other /dev/ path */ + } + + if (strncmp(p, "/proc/", 6) != 0) + return NULL; + s = p + 6; + if (strncmp(s, "self/", 5) == 0) /* "/proc/self/..." */ + s += 4; + else { /* "/proc//..." */ + const char *d = s; + while (*s >= '0' && *s <= '9') + s++; + if (s == d || *s != '/') + return NULL; + } + if (strncmp(s, "/fd", 3) != 0) + return NULL; + s += 3; + return (*s == '\0' || *s == '/') ? s : NULL; } -/* An EXACT pin entry, "/proc/self/fd/7" -- the one spelling whose target is what - * confinement must judge. rrsync also writes a pinned parent as +/* An EXACT pin entry, such as "/proc/self/fd/7" or "/dev/fd/7", whose target is + * what confinement must judge. rrsync also writes a pinned parent as * ".../fd/7/", but the walk resolves the magic link itself and checks the * components past it, so only the bare entry is resolved here. Requiring all * digits keeps a planted name like ".../fd/outside-secret" out. */ @@ -152,25 +198,29 @@ static int is_exact_fd_pin(const char *p) /* Refuse (return 1) when the ABSOLUTE resolved path `abspath` lands OUTSIDE the * confinement root, for an operator/peer-supplied path that must stay inside it - * (--partial-dir/--backup-dir/alt-basis/merge files: operator_path_resolve). An + * (--partial-dir/--backup-dir/alt-basis/merge files or files-from entries). An * in-tree symlink owned by uid 0 / the euid is followed by design, so it can * redirect the resolved target outside the root; this catches that escape. + * `final` distinguishes a completed target from an ancestor crossed on the way + * to it. * * This is ROOT confinement only. The daemon exclude/filter list is a name-based * visibility filter, NOT a physical-path boundary: a symlink whose own name is * not excluded may still resolve into an excluded IN-tree subtree, exactly as in * stock rsync. The defense for a writable module is `munge symlinks` (see * rsyncd.conf(5)), not this walk. */ -static int abspath_outside_confinement(const char *abspath) +static int abspath_outside_confinement(const char *abspath, int final) { unsigned int rootlen; const char *root = confinement_root(&rootlen); char pinned[MAXPATHLEN]; + int enforce; if (!root || !abspath) return 0; if (rootlen <= 1) /* root is "/": nothing is outside */ return 0; + enforce = operator_path_resolve || filesfrom_owner_walk_active(); /* An fd pin (rrsync rewrites a validated option path to /proc/self/fd/N so * no later symlink can redirect it) is spelled outside the root by * construction. Judge it by what it points AT rather than by its spelling, @@ -184,7 +234,7 @@ static int abspath_outside_confinement(const char *abspath) if (is_exact_fd_pin(abspath)) { ssize_t n = readlink(abspath, pinned, sizeof pinned - 1); if (n <= 0 || pinned[0] != '/') - return operator_path_resolve ? 1 : 0; + return enforce ? 1 : 0; pinned[n] = '\0'; abspath = pinned; } @@ -196,14 +246,43 @@ static int abspath_outside_confinement(const char *abspath) * ("/", "/home", ...) on the way down -- those are not "outside", just * not-yet-arrived, so allow them. A path that has truly DIVERGED is * outside: refuse it for an operator/peer path that must stay in the tree - * (operator_path_resolve); other opens (--log-file, --*-from, lock/motd) + * (operator_path_resolve or filesfrom_owner_walk_active()); other opens + * (--log-file, --*-from, lock/motd) * may legitimately live elsewhere. The --insecure-links / "insecure links * = yes" opt-out short-circuits before we get here. */ size_t alen = strlen(abspath); if (alen == 0 || (strncmp(abspath, root, alen) == 0 && root[alen] == '/')) - return 0; /* ancestor of the root: still descending */ - return operator_path_resolve ? 1 : 0; + return enforce && final ? 1 : 0; /* an ancestor is valid only while descending */ + return enforce ? 1 : 0; +} + +/* Check a completed path made from a tracked directory and one leaf. */ +static int check_abspath_leaf(const char *base, const char *leaf) +{ + char leafabs[MAXPATHLEN]; + size_t baselen; + int n; + + if (!base || !*base) { + if (abspath_outside_confinement(base, 1)) { + errno = ELOOP; + return -1; + } + return 0; + } + baselen = strlen(base); + n = snprintf(leafabs, sizeof leafabs, "%s%s%s", base, + base[baselen - 1] == '/' ? "" : "/", leaf); + if (n < 0 || (size_t)n >= sizeof leafabs) { + errno = ENAMETOOLONG; + return -1; + } + if (abspath_outside_confinement(leafabs, 1)) { + errno = ELOOP; + return -1; + } + return 0; } /* Advance the tracked absolute path `abspath` by one resolved component, @@ -252,12 +331,13 @@ static int abspath_step(char *abspath, size_t cap, const char *comp, size_t comp * uses it to filter-check the (otherwise unchecked) leaf basename. */ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, size_t out_cap) { -#if defined AT_FDCWD && defined O_NOFOLLOW +#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY /* O_CLOEXEC predates some still-supported targets; mirror rand_bytes()'s * fallback in syscall.c so a build without it still compiles. */ #ifndef O_CLOEXEC #define O_CLOEXEC 0 #endif + const int dir_traverse_flags = directory_traverse_flags() | O_CLOEXEC; if (!path || !*path) { errno = EINVAL; return -1; @@ -276,8 +356,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz * (abspath_outside_confinement). A relative operator path starts at the * daemon's cwd == the module root; an absolute one (or a followed absolute * symlink target) restarts at "/". */ - char abspath[MAXPATHLEN]; - abspath[0] = '\0'; + char abspath[MAXPATHLEN] = {0}; if (am_daemon && module_dir && module_dir[0] == '/') strlcpy(abspath, module_dir, sizeof abspath); /* "/" for a path=/ module */ else if (confine_root) { @@ -295,13 +374,18 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz return -1; } + /* Tracker: 1 if we are genuinely walking from the system root, + * 0 if we are walking a relative path where abspath_step will fake a '/' */ + int is_anchored = (abspath[0] != '\0'); + /* An fd pin (rrsync rewrites an option path to /proc/self/fd/N so no * later symlink can redirect it) is spelled outside the root by * construction, so the walk has to be allowed through /proc/self/fd to * reach the magic link. This only suspends the check for that prefix: * following the link restarts the walk at its absolute target, and every * component of THAT is checked, so a pin aimed outside is still refused. */ - int pin_transit = !am_daemon && confine_root && fd_pin_tail(path) != NULL; + const char *ptail = fd_pin_tail(path); + int pin_transit = !am_daemon && confine_root && ptail != NULL; /* Path-walk state. `remaining` is the unconsumed tail; we splice * symlink targets back into it as we go. Sized 2x MAXPATHLEN so a @@ -315,11 +399,12 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz /* Absolute path: pin "/" as the starting dfd. */ if (remaining[0] == '/') { - dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + dfd = open("/", dir_traverse_flags); if (dfd < 0) return -1; dfd_owns = 1; abspath[0] = '\0'; /* now resolving from "/" */ + is_anchored = 1; char *p = remaining; while (*p == '/') p++; memmove(remaining, p, strlen(p) + 1); @@ -355,7 +440,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz saved_errno = errno; goto out; } - if (!pin_transit && abspath_outside_confinement(abspath)) { + if (!pin_transit && abspath_outside_confinement(abspath, 1)) { saved_errno = ELOOP; goto out; } @@ -368,9 +453,21 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz } if (S_ISLNK(lst.st_mode)) { - /* Symlink: untrusted owner is refused; trusted owner - * is followed via readlinkat + splice. */ - if (lst.st_uid != 0 && lst.st_uid != trusted_uid) { + /* Symlink: untrusted owner is refused; trusted owner is followed + * via readlinkat + splice. In a user namespace the /proc/self, + * /dev/fd and /dev/std* symlinks may report the overflow uid, so + * allow those exact components while traversing a recognised pin. */ + int namespace_pin = is_anchored + && ((strcmp(abspath, "/proc") == 0 && strcmp(comp, "self") == 0) + || (strcmp(abspath, "/dev") == 0 && (strcmp(comp, "fd") == 0 + || strcmp(comp, "stdin") == 0 + || strcmp(comp, "stdout") == 0 + || strcmp(comp, "stderr") == 0))); + if (!namespace_pin && lst.st_uid != 0 && lst.st_uid != trusted_uid) { + rprintf(FERROR, + "refusing to follow a symlink owned by an untrusted user; " + "use --insecure-links locally or \"insecure links = yes\" in a " + "daemon module ONLY if every path component is trusted\n"); saved_errno = ELOOP; goto out; } @@ -386,6 +483,41 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz } target[n] = '\0'; + /* Detect Linux kernel pseudo-paths (pipes, sockets, anon_inodes). + * These are not real paths on disk and never contain slashes. */ + const char *abstail = fd_pin_tail(abspath); + int is_fd_dir = (abstail != NULL && *abstail == '\0' && is_anchored); + if (is_fd_dir && (strncmp(target, "pipe:[", 6) == 0 + || strncmp(target, "socket:[", 8) == 0 + || strncmp(target, "anon_inode:", 11) == 0)) { + if (!is_last) { + saved_errno = ENOTDIR; + goto out; + } + if (confine_root) { + /* Anonymous objects cannot be proven to reside beneath + * the confinement root. */ + saved_errno = ENOENT; + goto out; + } + /* Process substitution exposes /dev/fd/X as a symlink to a + * kernel object. Reopen the validated leaf without O_NOFOLLOW + * so the kernel applies the caller's requested open flags. */ + retfd = openat(dfd, comp, (flags & ~O_NOFOLLOW) | O_CLOEXEC, mode); + /* Refuse a descriptor that changed to a filesystem object + * between validation and openat(). */ + if (retfd >= 0) { + STRUCT_STAT pst; + if (fstat(retfd, &pst) < 0 || S_ISREG(pst.st_mode) || S_ISDIR(pst.st_mode)) { + close(retfd); + retfd = -1; + errno = ELOOP; + } + } + saved_errno = retfd < 0 ? errno : 0; + goto out; + } + /* Splice: new `remaining` = + . * Absolute target restarts the walk from "/". */ char tail[MAXPATHLEN]; @@ -402,7 +534,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz if (target[0] == '/') { if (dfd_owns) close(dfd); - dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + dfd = open("/", dir_traverse_flags); if (dfd < 0) { saved_errno = errno; dfd_owns = 0; @@ -413,6 +545,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz /* "self" resolves to "", still inside the pin; * the magic link itself lands elsewhere and ends the * exemption. Never turns back on. */ + is_anchored = 1; pin_transit = pin_transit && fd_pin_tail(rebuilt) != NULL; char *p = rebuilt; while (*p == '/') p++; @@ -429,7 +562,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz saved_errno = errno; goto out; } - if (!pin_transit && abspath_outside_confinement(abspath)) { + if (!pin_transit && abspath_outside_confinement(abspath, 1)) { saved_errno = ELOOP; goto out; } @@ -440,7 +573,7 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz if (retfd >= 0 && out_abs && out_cap) /* Root-resolved (".." popped abspath empty) tracked daemon walk: * hand back "/" so owner_walk_parent still leaf-checks (path=/ bypass). */ - strlcpy(out_abs, (am_daemon && !abspath[0]) ? "/" : abspath, out_cap); + strlcpy(out_abs, !abspath[0] ? "/" : abspath, out_cap); goto out; } @@ -453,11 +586,11 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz saved_errno = errno; goto out; } - if (!pin_transit && abspath_outside_confinement(abspath)) { + if (!pin_transit && abspath_outside_confinement(abspath, 0)) { saved_errno = ELOOP; goto out; } - int next = openat(dfd, comp, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + int next = openat(dfd, comp, dir_traverse_flags | O_NOFOLLOW); if (next < 0) { saved_errno = errno; goto out; @@ -477,16 +610,20 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz } /* Path resolved entirely to a directory (no leaf component left). - * If the caller wanted O_DIRECTORY we already hold the dirfd we - * built up; otherwise it's an EISDIR. */ + * Reopen the held traversal fd with the caller's requested access mode; + * an O_PATH fd is sufficient for traversal and fchdir but not operations + * such as fchmod. */ if (flags & O_DIRECTORY) { - retfd = dfd; - dfd_owns = 0; /* caller now owns it */ - saved_errno = 0; + if (!pin_transit && abspath_outside_confinement(abspath, 1)) { + saved_errno = ELOOP; + goto out; + } + retfd = openat(dfd, ".", flags | O_NOFOLLOW, mode); + saved_errno = retfd < 0 ? errno : 0; if (out_abs && out_cap) /* Root-resolved (".." popped abspath empty) tracked daemon walk: * hand back "/" so owner_walk_parent still leaf-checks (path=/ bypass). */ - strlcpy(out_abs, (am_daemon && !abspath[0]) ? "/" : abspath, out_cap); + strlcpy(out_abs, !abspath[0] ? "/" : abspath, out_cap); } else { saved_errno = EISDIR; } @@ -507,6 +644,14 @@ int open_no_attacker_symlinks(const char *path, int flags, mode_t mode) return ona_open(path, flags, mode, NULL, 0); } +/* Open a directory for traversal or as *at()/fchdir() authority. Unlike an + * O_RDONLY directory endpoint, this accepts a searchable but unreadable + * directory on Linux. */ +int open_no_attacker_symlinks_dirfd(const char *path) +{ + return ona_open(path, directory_traverse_flags(), 0, NULL, 0); +} + /* When set, the do_*_at() wrappers resolve their path as an OPERATOR-supplied * directory path (an absolute or relative --backup-dir/--temp-dir/--*-dest) * using the ownership walk -- follow a symlink owned by uid 0 or our euid, @@ -517,6 +662,7 @@ int open_no_attacker_symlinks(const char *path, int flags, mode_t mode) * relevant ops by backup.c et al.; the opt-out (--insecure-links / "insecure * links =") restores legacy following. Default 0 (transfer-path resolver). */ int operator_path_resolve = 0; +int filesfrom_owner_walk_override = 0; #if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY /* For an operator-supplied path: open its parent directory via the ownership @@ -532,7 +678,7 @@ int owner_walk_parent(const char *path, const char **bname) *bname = slash ? slash + 1 : path; pabs[0] = '\0'; if (!slash) - dfd = ona_open(".", O_RDONLY | O_DIRECTORY, 0, pabs, sizeof pabs); + dfd = ona_open(".", directory_traverse_flags(), 0, pabs, sizeof pabs); else { dlen = slash == path ? 1 : (size_t)(slash - path); /* "/x" -> parent "/" */ if (dlen >= sizeof dir) { @@ -541,7 +687,7 @@ int owner_walk_parent(const char *path, const char **bname) } memcpy(dir, path, dlen); dir[dlen] = '\0'; - dfd = ona_open(dir, O_RDONLY | O_DIRECTORY, 0, pabs, sizeof pabs); + dfd = ona_open(dir, directory_traverse_flags(), 0, pabs, sizeof pabs); } if (dfd < 0) return -1; @@ -550,15 +696,8 @@ int owner_walk_parent(const char *path, const char **bname) * module in an otherwise-served dir. (The module exclude/filter is name- * based and not enforced here -- see abspath_outside_confinement.) */ if (pabs[0]) { - char leafabs[MAXPATHLEN]; - if (snprintf(leafabs, sizeof leafabs, "%s/%s", pabs, *bname) >= (int)sizeof leafabs) { + if (check_abspath_leaf(pabs, *bname) < 0) { close(dfd); - errno = ENAMETOOLONG; /* fail closed, never skip the check */ - return -1; - } - if (abspath_outside_confinement(leafabs)) { - close(dfd); - errno = ELOOP; return -1; } } @@ -665,7 +804,7 @@ int do_unlink_at(const char *path) dirpath[dlen] = '\0'; bname = slash + 1; - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + dfd = secure_relative_dirfd(NULL, dirpath); if (dfd < 0) return -1; @@ -770,7 +909,7 @@ int do_symlink_at(const char *lnk, const char *path) memcpy(dirpath, path, dlen); dirpath[dlen] = '\0'; bname = slash + 1; - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + dfd = secure_relative_dirfd(NULL, dirpath); if (dfd < 0) return -1; owns = True; @@ -968,7 +1107,7 @@ int do_link_at(const char *old_path, const char *new_path) memcpy(old_dirpath, old_path, old_dlen); old_dirpath[old_dlen] = '\0'; old_bname = old_slash + 1; - old_dfd = secure_relative_open(NULL, old_dirpath, O_RDONLY | O_DIRECTORY, 0); + old_dfd = secure_relative_dirfd(NULL, old_dirpath); if (old_dfd < 0) return -1; old_owns = True; @@ -1007,7 +1146,7 @@ int do_link_at(const char *old_path, const char *new_path) && memcmp(old_dirpath, new_dirpath, old_dlen) == 0) { new_dfd = old_dfd; } else { - new_dfd = secure_relative_open(NULL, new_dirpath, O_RDONLY | O_DIRECTORY, 0); + new_dfd = secure_relative_dirfd(NULL, new_dirpath); if (new_dfd < 0) { e = errno; if (old_owns) close(old_dfd); @@ -1109,7 +1248,7 @@ int do_lchown_at(const char *fname, uid_t owner, gid_t group) dirpath[dlen] = '\0'; bname = slash + 1; - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + dfd = secure_relative_dirfd(NULL, dirpath); if (dfd < 0) return -1; @@ -1274,7 +1413,7 @@ int do_mknod_at(const char *pathname, mode_t mode, dev_t dev) memcpy(dirpath, pathname, dlen); dirpath[dlen] = '\0'; bname = slash + 1; - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + dfd = secure_relative_dirfd(NULL, dirpath); if (dfd < 0) return -1; owns = True; @@ -1379,7 +1518,7 @@ int do_rmdir_at(const char *pathname) dirpath[dlen] = '\0'; bname = slash + 1; - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + dfd = secure_relative_dirfd(NULL, dirpath); if (dfd < 0) return -1; @@ -1474,7 +1613,7 @@ int do_open_at(const char *pathname, int flags, mode_t mode) dirpath[dlen] = '\0'; bname = slash + 1; - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + dfd = secure_relative_dirfd(NULL, dirpath); if (dfd < 0) return -1; @@ -1756,7 +1895,7 @@ int do_chmod_at(const char *fname, mode_t mode) dirpath[dlen] = '\0'; bname = slash + 1; - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + dfd = secure_relative_dirfd(NULL, dirpath); if (dfd < 0) return -1; @@ -1873,7 +2012,7 @@ int do_rename_at(const char *old_path, const char *new_path) memcpy(old_dirpath, old_path, old_dlen); old_dirpath[old_dlen] = '\0'; old_bname = old_slash + 1; - old_dfd = secure_relative_open(NULL, old_dirpath, O_RDONLY | O_DIRECTORY, 0); + old_dfd = secure_relative_dirfd(NULL, old_dirpath); if (old_dfd < 0) return -1; old_owns = True; @@ -1912,7 +2051,7 @@ int do_rename_at(const char *old_path, const char *new_path) && memcmp(old_dirpath, new_dirpath, old_dlen) == 0) { new_dfd = old_dfd; } else { - new_dfd = secure_relative_open(NULL, new_dirpath, O_RDONLY | O_DIRECTORY, 0); + new_dfd = secure_relative_dirfd(NULL, new_dirpath); if (new_dfd < 0) { e = errno; if (old_owns) close(old_dfd); @@ -2041,7 +2180,7 @@ int do_mkdir_at(char *path, mode_t mode) dirpath[dlen] = '\0'; bname = slash + 1; - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + dfd = secure_relative_dirfd(NULL, dirpath); if (dfd < 0) return -1; @@ -2165,7 +2304,7 @@ static int do_xstat_at(const char *path, STRUCT_STAT *st, int at_flags, int (*fa dirpath[dlen] = '\0'; bname = slash + 1; - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + dfd = secure_relative_dirfd(NULL, dirpath); if (dfd < 0) return -1; @@ -2424,7 +2563,7 @@ int do_utimensat_at(const char *path, STRUCT_STAT *stp) t[1].tv_nsec = 0; #endif - dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); + dfd = secure_relative_dirfd(NULL, dirpath); if (dfd < 0) return -1; @@ -2701,11 +2840,11 @@ extern int module_dirfd; /* daemon: served module root pinned by identity, or -1 * which re-traverses the module's ancestors as the dropped-privilege module uid * and EACCESes when the module sits under a non-traversable parent (a 0700 home). * Functionally identical (same inode), just privilege-drop-safe. */ -static int open_anchor_dirfd(const char *path) +int open_anchor_dirfd(const char *path) { if (module_dirfd >= 0 && am_daemon && module_dir && strcmp(path, module_dir) == 0) return dup(module_dirfd); - return openat(AT_FDCWD, path, O_RDONLY | O_DIRECTORY); + return openat(AT_FDCWD, path, directory_traverse_flags()); } #endif @@ -2758,12 +2897,14 @@ static int ds_path_push(struct dirstack *ds, const char *comp) if (al == 0) return 0; /* unseeded: tracking disabled for this walk */ size_t cl = strlen(comp); - if (al + 1 + cl >= sizeof ds->abspath) { + size_t off = (al > 0 && ds->abspath[al - 1] == '/') ? al : al + 1; + if (off + cl >= sizeof ds->abspath) { errno = ENAMETOOLONG; return -1; } - ds->abspath[al] = '/'; - memcpy(ds->abspath + al + 1, comp, cl + 1); + if (off != al) + ds->abspath[al] = '/'; + memcpy(ds->abspath + off, comp, cl + 1); return 0; } @@ -2811,13 +2952,13 @@ static int ds_push(struct dirstack *ds, int fd) return 0; } -/* Detach the current dir as an owned fd the caller must close. At the anchor - * (top 0) the anchor is borrowed, so return a fresh dup of it instead. */ +/* Detach the current traversal dirfd as an owned fd the caller must close. At + * the anchor (top 0) the anchor is borrowed, so open a fresh traversal fd. */ static int ds_take(struct dirstack *ds) { if (ds->top > 0) return ds->fds[ds->top--]; - return openat(ds->fds[0], ".", O_RDONLY | O_DIRECTORY); + return openat(ds->fds[0], ".", directory_traverse_flags()); } static int ds_walk_path(struct dirstack *ds, char *path, int *hops); @@ -2842,7 +2983,7 @@ static int ds_descend(struct dirstack *ds, const char *part, int *hops) return 0; } - int fd = openat(ds_cur(ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); + int fd = openat(ds_cur(ds), part, directory_traverse_flags() | O_NOFOLLOW); if (fd != -1) { /* a real subdirectory */ if (ds_push(ds, fd) < 0) return -1; @@ -2850,7 +2991,7 @@ static int ds_descend(struct dirstack *ds, const char *part, int *hops) return -1; /* exclude-aware: refuse descending into a module-hidden dir (catches a * symlink that redirected the walk into an excluded subtree). */ - if (abspath_outside_confinement(ds->abspath)) { + if (abspath_outside_confinement(ds->abspath, 0)) { errno = ELOOP; return -1; } @@ -2964,8 +3105,11 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath, if (ds_descend(&ds, part, hops) < 0) goto cleanup; if (is_last) { - if (flags & O_DIRECTORY) - retfd = ds_take(&ds); + if ((flags & O_DIRECTORY) + && abspath_outside_confinement(ds.abspath, 1)) + errno = ELOOP; + else if (flags & O_DIRECTORY) + retfd = openat(ds_cur(&ds), ".", flags | O_NOFOLLOW, mode); else errno = EISDIR; goto cleanup; @@ -2976,16 +3120,10 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath, /* File leaf (final component, caller did not ask for O_DIRECTORY): * never follow a symlink leaf. */ if (is_last && !(flags & O_DIRECTORY)) { - if (ds.abspath[0]) { - char leafabs[MAXPATHLEN]; - if (snprintf(leafabs, sizeof leafabs, "%s/%s", ds.abspath, part) - < (int)sizeof leafabs - && abspath_outside_confinement(leafabs)) { - errno = ELOOP; - goto cleanup; - } - } - int next_fd = openat(ds_cur(&ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); + if (check_abspath_leaf(ds.abspath, part) < 0) + goto cleanup; + int next_fd = openat(ds_cur(&ds), part, + directory_traverse_flags() | O_NOFOLLOW); if (next_fd == -1 && (errno == ENOTDIR || errno == ENOENT)) { retfd = openat(ds_cur(&ds), part, flags | O_NOFOLLOW, mode); goto cleanup; @@ -2999,7 +3137,9 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath, /* O_DIRECTORY|O_NOFOLLOW leaf: the caller's O_NOFOLLOW governs the leaf. */ if (is_last && (flags & O_NOFOLLOW)) { - retfd = openat(ds_cur(&ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); + if (check_abspath_leaf(ds.abspath, part) < 0) + goto cleanup; + retfd = openat(ds_cur(&ds), part, flags | O_NOFOLLOW, mode); goto cleanup; } @@ -3011,17 +3151,24 @@ static int secure_walk_at(int anchor_fd, const char *anchor_abspath, goto cleanup; } if (is_last) { - retfd = ds_take(&ds); + if (abspath_outside_confinement(ds.abspath, 1)) { + errno = ELOOP; + goto cleanup; + } + retfd = openat(ds_cur(&ds), ".", flags | O_NOFOLLOW, mode); goto cleanup; } } - /* Empty relpath: hand back a real anchor for an O_DIRECTORY caller (ds_take - * dups the borrowed anchor), else EISDIR. An AT_FDCWD anchor is not a - * resolvable target, so it fails rather than silently returning the cwd. */ + /* Empty relpath: reopen the anchor with the caller's requested directory + * access, else EISDIR. An AT_FDCWD anchor is not a resolvable target, so it + * fails rather than silently returning the cwd. */ if (!saw_component) { - if ((flags & O_DIRECTORY) && anchor_fd != AT_FDCWD) - retfd = ds_take(&ds); + if ((flags & O_DIRECTORY) + && abspath_outside_confinement(ds.abspath, 1)) + errno = ELOOP; + else if ((flags & O_DIRECTORY) && anchor_fd != AT_FDCWD) + retfd = openat(anchor_fd, ".", flags | O_NOFOLLOW, mode); else errno = EISDIR; } @@ -3174,6 +3321,14 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo #endif // O_NOFOLLOW, O_DIRECTORY } +/* Resolve a directory for traversal or as *at()/fchdir() authority. Callers + * that read directory entries or need a read-capable fd must continue to use + * secure_relative_open(..., O_RDONLY | O_DIRECTORY, ...). */ +int secure_relative_dirfd(const char *basedir, const char *relpath) +{ + return secure_relative_open(basedir, relpath, directory_traverse_flags(), 0); +} + /* Common fd-anchored resolver. A caller may explicitly allow literal ".." * components when the fd itself is the confinement boundary: secure_walk_at() * resolves each one by popping its held-dirfd stack and refuses a pop above the @@ -3228,6 +3383,12 @@ int secure_relative_open_at_beneath(int anchor_fd, const char *relpath, return secure_relative_open_at_internal(anchor_fd, relpath, flags, mode, 1); } +int secure_relative_dirfd_at_beneath(int anchor_fd, const char *relpath) +{ + return secure_relative_open_at_internal(anchor_fd, relpath, + directory_traverse_flags(), 0, 1); +} + /* 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). */ @@ -3358,7 +3519,7 @@ int secure_mkstemp(char *template, mode_t perms, int operator_path) dirbuf[dlen] = '\0'; dir = dirbuf; } - dirfd = open_no_attacker_symlinks(dir, O_RDONLY | O_DIRECTORY, 0); + dirfd = open_no_attacker_symlinks_dirfd(dir); if (dirfd < 0) return -1; } @@ -3468,6 +3629,26 @@ int do_open_checklinks(const char *pathname) if (copy_links || copy_unsafe_links) { return do_open(pathname, O_RDONLY, 0); } +#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY + if (am_sender && !am_daemon && files_from + && !copy_dirlinks && !symlink_optout_allowed()) { + const char *bname; + int dfd, fd, save_errno, open_flags = O_RDONLY | O_NOFOLLOW; + + dfd = owner_walk_parent(pathname, &bname); + if (dfd < 0) + return -1; +#ifdef O_NOATIME + if (open_noatime) + open_flags |= O_NOATIME; +#endif + fd = openat(dfd, bname, open_flags, 0); + save_errno = fd < 0 ? errno : 0; + close(dfd); + errno = save_errno; + return fd; + } +#endif return do_open_nofollow(pathname, O_RDONLY); } @@ -3504,14 +3685,14 @@ int open_dir_secure(const char *dirname) if (!dirname || !*dirname) { /* The transfer root itself (file->dirname == NULL): the cwd. */ - dfd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY); + dfd = openat(AT_FDCWD, ".", directory_traverse_flags()); } else if (dirname[0] == '/') { /* An absolute dirname is not expected for an in-transfer entry; * leave it to the legacy path. */ errno = 0; return -1; } else { - dfd = secure_relative_open(NULL, dirname, O_RDONLY | O_DIRECTORY, 0); + dfd = secure_relative_dirfd(NULL, dirname); } if (dfd >= 0) { diff --git a/t_stub.c b/t_stub.c index 8e192255..4a4a10ff 100644 --- a/t_stub.c +++ b/t_stub.c @@ -26,6 +26,7 @@ int inplace = 0; int am_daemon = 0; int am_chrooted = 0; int insecure_links = 0; +int copy_dirlinks = 0; int modify_window = 0; int preallocate_files = 0; int protect_args = 0; @@ -42,6 +43,7 @@ size_t max_alloc = (size_t)-1; /* unlimited: helpers link util2.o, where 0 makes * "exceeded --max-alloc=0" (hit on the * secure_relative_open() fallback path). */ char *partial_dir; +char *files_from; char *module_dir; int module_dirfd = -1; char *confine_root; diff --git a/util1.c b/util1.c index 5b02d713..6ca51728 100644 --- a/util1.c +++ b/util1.c @@ -1236,7 +1236,7 @@ int change_dir(const char *dir, int set_path_only) * transfer begins. For daemon receivers, refuse symlinks not * owned by uid 0 or our euid in the path walk. */ if (am_daemon && (!am_chrooted || module_dirlen)) { - int dfd = open_no_attacker_symlinks(dir, O_RDONLY | O_DIRECTORY, 0); + int dfd = open_no_attacker_symlinks_dirfd(dir); if (dfd < 0) return 0; if (fchdir(dfd) != 0) { @@ -1264,7 +1264,7 @@ int change_dir(const char *dir, int set_path_only) memcpy(nf, dir, nl + 1); while (nl > 1 && nf[nl-1] == '/') nf[--nl] = '\0'; - dfd = open_no_attacker_symlinks(nf, O_RDONLY | O_DIRECTORY, 0); + dfd = open_no_attacker_symlinks_dirfd(nf); if (dfd < 0) return 0; if (fchdir(dfd) != 0) { @@ -1325,8 +1325,7 @@ int change_dir(const char *dir, int set_path_only) prefix[save_dir_len] = '\0'; basedir = prefix; } - dfd = secure_relative_open(basedir, dir, - O_RDONLY | O_DIRECTORY, 0); + dfd = secure_relative_dirfd(basedir, dir); if (dfd < 0) { chdir_failed = 1; } else { @@ -1343,8 +1342,7 @@ int change_dir(const char *dir, int set_path_only) * destination like the absolute case -- refuse a component * symlink not owned by uid 0 or our euid, while still following * the operator's own symlinks. */ - int dfd = open_no_attacker_symlinks(curr_dir, - O_RDONLY | O_DIRECTORY, 0); + int dfd = open_no_attacker_symlinks_dirfd(curr_dir); if (dfd < 0) chdir_failed = 1; else {