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
121 changes: 119 additions & 2 deletions flist.c
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,117 @@ 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;
}

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(best->path, O_RDONLY | O_DIRECTORY);
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 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 Down Expand Up @@ -2028,10 +2139,14 @@ static void interpret_stat_error(const char *fname, int is_dir)
* 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 (!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 @@ -2724,6 +2839,8 @@ 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 && S_ISDIR(st.st_mode))
remember_sender_source_root(fbuf, &st);

if (inc_recurse && relative_paths && *fbuf) {
if ((p = strchr(fbuf+1, '/')) != NULL) {
Expand Down
7 changes: 6 additions & 1 deletion sender.c
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,7 @@ 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) {
int matched;
/* 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
Expand All @@ -688,7 +689,10 @@ void send_files(int f_in, int f_out)
* 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] == '/') {
fd = open_sender_source_path(fname, O_RDONLY | O_NOFOLLOW, &matched);
if (matched) {
/* The explicit source directory is the trust root. */
} else 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
Expand Down Expand Up @@ -809,6 +813,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
1 change: 0 additions & 1 deletion t_secure_relpath.c
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,6 @@ int main(int argc, char **argv)
* literal '..'. Its dedicated fd-anchored entry point must preserve an
* in-tree climb while refusing to pop above the anchor. */
check_beneath_dotdot();

if (errs)
fprintf(stderr, "\n%d failure(s)\n", errs);
return errs ? 1 : 0;
Expand Down
105 changes: 105 additions & 0 deletions testsuite/relative-source-ancestor_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Explicit source directory symlinks remain transfer roots."""

import os
import pwd
import subprocess

from rsyncfns import SCRATCHDIR, rsync_argv, test_fail

real = SCRATCHDIR / 'real'
source = real / 'My_Documents'
source.mkdir(parents=True)
(source / 'marker').write_text('source contents\n')
absolute_link = SCRATCHDIR / 'home'
relative_link = SCRATCHDIR / 'relative-home'
os.symlink(str(real), absolute_link)
os.symlink(str(real), relative_link)

cases = (
(('-r',), False),
(('-rR',), True),
(('-rR', '--no-inc-recursive'), True),
)

for link_name, link, cwd in (
('absolute', absolute_link, None),
('relative', relative_link, SCRATCHDIR),
):
source_arg = (str(SCRATCHDIR) + '/./home/My_Documents/' if cwd is None
else 'relative-home/My_Documents/')
for index, (options, relative) in enumerate(cases):
dest = SCRATCHDIR / f'dest-{link_name}-descendant-{index}'
dest.mkdir()
proc = subprocess.run(
rsync_argv(*options, source_arg, str(dest) + '/'),
cwd=cwd, capture_output=True, text=True,
)
if proc.returncode:
test_fail(f'{link_name} descendant source transfer with {options} '
f'failed: {proc.stdout}{proc.stderr}')
expected = (dest / link.name / 'My_Documents' / 'marker' if relative
else dest / 'marker')
if not expected.is_file() or expected.read_text() != 'source contents\n':
test_fail('relative source layout or contents changed')

root_real = SCRATCHDIR / 'root-real'
root_real.mkdir()
(root_real / 'marker').write_text('source contents\n')
absolute_root_link = SCRATCHDIR / 'root-home'
relative_root_link = SCRATCHDIR / 'relative-root-home'
os.symlink(str(root_real), absolute_root_link)
os.symlink(str(root_real), relative_root_link)

for link_name, link, cwd in (
('absolute', absolute_root_link, None),
('relative', relative_root_link, SCRATCHDIR),
):
source_arg = (str(SCRATCHDIR) + '/./root-home/' if cwd is None
else 'relative-root-home/')
for index, (options, relative) in enumerate(cases):
dest = SCRATCHDIR / f'dest-{link_name}-root-{index}'
dest.mkdir()
proc = subprocess.run(
rsync_argv(*options, source_arg, str(dest) + '/'),
cwd=cwd, capture_output=True, text=True,
)
if proc.returncode:
test_fail(f'{link_name} root source transfer with {options} failed: '
f'{proc.stdout}{proc.stderr}')
expected = dest / link.name / 'marker' if relative else dest / 'marker'
if not expected.is_file() or expected.read_text() != 'source contents\n':
test_fail('explicit source-root layout or contents changed')

if os.geteuid() == 0:
untrusted_uid = next((entry.pw_uid for entry in pwd.getpwall()
if entry.pw_uid != 0), None)
if untrusted_uid is not None:
untrusted_link = SCRATCHDIR / 'untrusted-home'
os.symlink(str(real), untrusted_link)
os.lchown(untrusted_link, untrusted_uid, -1)
source_arg = str(SCRATCHDIR) + '/./untrusted-home/'
for index, (options, relative) in enumerate(cases[:2]):
dest = SCRATCHDIR / f'untrusted-dest-{index}'
dest.mkdir()
proc = subprocess.run(
rsync_argv(*options, source_arg, str(dest) + '/'),
capture_output=True, text=True,
)
expected = dest / 'My_Documents' / 'marker'
if relative:
expected = dest / 'untrusted-home' / 'My_Documents' / 'marker'
if proc.returncode or not expected.is_file():
test_fail(f'explicit untrusted-owned source link with {options} '
f'failed: {proc.stdout}{proc.stderr}')

dest = SCRATCHDIR / 'remove-dest'
dest.mkdir()
proc = subprocess.run(
rsync_argv('-r', '--remove-source-files', str(absolute_link / 'My_Documents') + '/', str(dest) + '/'),
capture_output=True, text=True,
)
expected = dest / 'marker'
if proc.returncode or (source / 'marker').exists() or not expected.is_file():
test_fail(f'remove-source-files failed: {proc.stdout}{proc.stderr}')
(source / 'marker').write_text('source contents\n')
Loading