Backport 3.5.1 fixes

This commit is contained in:
Zen Dodd committed 2026-09-21 19:44:21 +10:00
1 parent c549abd02f
commit f47dce87e9
10 files changed
+611 -147

No files matched your search

+4 -4
View File
@@ -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);
}
+251 -8
View File
@@ -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) {
+1 -1
View File
@@ -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;
+17 -7
View File
@@ -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) {
+17 -3
View File
@@ -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));
+24 -13
View File
@@ -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);
+6 -1
View File
@@ -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):
+285 -104
View File
@@ -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/<self|pid>/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/<pid>/..." */
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/<pid>/..." */
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/<leaf>", 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` = <target> + <tail-after-comp>.
* 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 "<pid>", 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) {
+2
View File
@@ -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;
+4 -6
View File
@@ -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 {