Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 208 additions & 6 deletions flist.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -230,6 +231,189 @@ 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 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);
}

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;
STRUCT_STAT st;
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 = open_anchor_dirfd(best->path);
if (rootfd < 0)
return -1;
if (do_fstat(rootfd, &st) < 0)
saved_errno = errno;
else if (st.st_dev != best->dev || st.st_ino != best->ino)
saved_errno = ELOOP;
else
saved_errno = 0;
if (saved_errno) {
close(rootfd);
errno = saved_errno;
return -1;
}
rel = full + best_len;
while (*rel == '/')
rel++;
fd = secure_relative_open_at(rootfd, *rel ? rel : ".", flags, 0);
saved_errno = errno;
close(rootfd);
errno = saved_errno;
return fd;
#else
*matched = 0;
errno = ENOSYS;
return -1;
#endif
}

void clear_sender_source_roots(void)
{
while (sender_source_roots) {
struct sender_source_root *root = sender_source_roots;
sender_source_roots = root->next;
free(root);
}
}

static int filesfrom_owner_walk_active(void)
{
return !am_daemon && am_sender && files_from
&& !copy_links && !copy_unsafe_links && !copy_dirlinks && !insecure_links;
}

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 = link_stat_at(dfd, bname, stp, follow_dirlinks);
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
Expand All @@ -241,6 +425,8 @@ static int scan_link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlink
&& 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);
}

Expand All @@ -251,6 +437,8 @@ static int scan_readlink(const char *path, char *linkbuf, size_t bufsiz)
&& 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);
}

Expand Down Expand Up @@ -2014,7 +2202,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
Expand All @@ -2025,13 +2213,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
Expand Down Expand Up @@ -2333,7 +2532,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;
}
Expand Down Expand Up @@ -2369,7 +2568,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;
}
Expand Down Expand Up @@ -2694,7 +2893,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) {
Expand Down Expand Up @@ -2724,6 +2923,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) {
Expand Down
43 changes: 23 additions & 20 deletions sender.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,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;
Expand Down Expand Up @@ -680,26 +681,27 @@ void send_files(int f_in, int f_out)
else
fd = sender_open_confined(module_dir, relp, O_RDONLY);
} else if (!copy_links && !copy_unsafe_links && !copy_dirlinks && !insecure_links) {
/* Default symlink handling (no dir-link following): the scan
* recorded this as a regular file. Open it confined beneath the
* transfer root: an in-tree symlinked parent (e.g. -R keeps one in
* the path) is followed beneath the root, a parent raced into a
* symlink pointing out of the tree is refused, and O_NOFOLLOW
* governs the leaf so a raced leaf symlink is refused. A
* symlink-following mode (-L/--copy-unsafe-links/-k) or
* --insecure-links keeps the legacy open below. */
if (fname[0] == '/') {
/* --relative (or a --files-from absolute name) keeps the
* full absolute path as fname; the transfer root is then "/",
* so anchor the confined open there and strip the leading
* slash to the module-relative path the resolver wants -- it
* rejects an absolute relpath outright. */
const char *relp = fname;
while (*relp == '/')
relp++;
fd = sender_open_confined("/", relp, O_RDONLY);
} else
fd = sender_open_confined(NULL, fname, O_RDONLY);
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] == '/') {
/* --relative keeps the full absolute path as fname;
* anchor at "/" and pass the resolver a relative path. */
const char *relp = fname;
while (*relp == '/')
relp++;
fd = sender_open_confined("/", relp, O_RDONLY);
} else
fd = sender_open_confined(NULL, fname, O_RDONLY);
}
}
} else {
fd = do_open_checklinks(fname);
}
Expand Down Expand Up @@ -809,6 +811,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);
Expand Down
Loading
Loading