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
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ modtime.

### BUG FIXES:

- Added support for a per-directory relative form of `--compare-dest`,
`--copy-dest`, and `--link-dest`: prefix a relative DIR with `: ` to make it
resolve from each destination file's containing directory.

- Fixed a regression introduced by the 3.4.0 secure_relative_open()
CVE fix where legitimate directory symlinks on the receiver side
(e.g. when using `-K` / `--copy-dirlinks`) caused "failed
Expand Down
10 changes: 5 additions & 5 deletions generator.c
Original file line number Diff line number Diff line change
Expand Up @@ -960,7 +960,7 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,
int j = 0;

do {
pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);
pathjoin_altdest(cmpbuf, MAXPATHLEN, j, fname);
if (link_stat(cmpbuf, &sxp->st, 0) < 0 || !S_ISREG(sxp->st.st_mode))
continue;
if (match_level == 0) {
Expand All @@ -986,7 +986,7 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,

if (j != best_match) {
j = best_match;
pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);
pathjoin_altdest(cmpbuf, MAXPATHLEN, j, fname);
if (link_stat(cmpbuf, &sxp->st, 0) < 0)
goto got_nothing_for_ya;
}
Expand Down Expand Up @@ -1081,7 +1081,7 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx,
}

do {
pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);
pathjoin_altdest(cmpbuf, MAXPATHLEN, j, fname);
if (link_stat(cmpbuf, &sxp->st, 0) < 0)
continue;
if (ftype != get_file_type(sxp->st.st_mode))
Expand All @@ -1108,7 +1108,7 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx,

if (j != best_match) {
j = best_match;
pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);
pathjoin_altdest(cmpbuf, MAXPATHLEN, j, fname);
if (link_stat(cmpbuf, &sxp->st, 0) < 0)
return -1;
}
Expand Down Expand Up @@ -1773,7 +1773,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
int i;
strlcpy(fnamecmpbuf, dn, sizeof fnamecmpbuf);
for (i = 0; i < fuzzy_basis; i++) {
if (i && pathjoin(fnamecmpbuf, MAXPATHLEN, basis_dir[i-1], dn) >= MAXPATHLEN)
if (i && pathjoin_altdest_dir(fnamecmpbuf, MAXPATHLEN, i-1, dn) >= MAXPATHLEN)
continue;
fuzzy_dirlist[i] = get_dirlist(fnamecmpbuf, -1, GDL_IGNORE_FILTER_RULES | GDL_PERHAPS_DIR);
if (fuzzy_dirlist[i] && fuzzy_dirlist[i]->used == 0) {
Expand Down
10 changes: 5 additions & 5 deletions hlink.c
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,8 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,
}

if (alt_dest >= 0 && dry_run) {
pathjoin(namebuf, MAXPATHLEN, basis_dir[alt_dest],
f_name(prev_file, NULL));
pathjoin_altdest(namebuf, MAXPATHLEN, alt_dest,
f_name(prev_file, NULL));
prev_name = namebuf;
realname = f_name(prev_file, altbuf);
} else {
Expand Down Expand Up @@ -389,7 +389,7 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,
int j = 0;
init_stat_x(&alt_sx);
do {
pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);
pathjoin_altdest(cmpbuf, MAXPATHLEN, j, fname);
if (link_stat(cmpbuf, &alt_sx.st, 0) < 0)
continue;
if (alt_dest_type == LINK_DEST) {
Expand Down Expand Up @@ -496,8 +496,8 @@ void finish_hard_link(struct file_struct *file, const char *fname, int fin_ndx,
file->flags |= FLAG_HLINK_FIRST | FLAG_HLINK_DONE;
F_HL_PREV(file) = alt_dest;
if (alt_dest >= 0 && dry_run) {
pathjoin(alt_name, MAXPATHLEN, basis_dir[alt_dest],
f_name(file, NULL));
pathjoin_altdest(alt_name, MAXPATHLEN, alt_dest,
f_name(file, NULL));
our_name = alt_name;
} else
our_name = fname;
Expand Down
5 changes: 4 additions & 1 deletion main.c
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ extern BOOL want_progress_now;
extern BOOL shutting_down;
extern int backup_dir_len;
extern int basis_dir_cnt;
extern int basis_dir_perdir[MAX_BASIS_DIRS+1];
extern int default_af_hint;
extern int stdout_format_has_i;
extern int trust_sender_filter;
Expand Down Expand Up @@ -867,7 +868,7 @@ static void check_alt_basis_dirs(void)
int bd_len = strlen(bdir);
if (bd_len > 1 && bdir[bd_len-1] == '/')
bdir[--bd_len] = '\0';
if (dry_run > 1 && *bdir != '/') {
if (!basis_dir_perdir[j] && dry_run > 1 && *bdir != '/') {
int len = curr_dir_len + 1 + bd_len + 1;
char *new = new_array(char, len);
if (slash && strncmp(bdir, "../", 3) == 0) {
Expand All @@ -882,6 +883,8 @@ static void check_alt_basis_dirs(void)
pathjoin(new, len, curr_dir, bdir);
basis_dir[j] = bdir = new;
}
if (basis_dir_perdir[j] && *bdir != '/')
continue;
if (do_stat(bdir, &st) < 0)
rprintf(FWARNING, "%s arg does not exist: %s\n", alt_dest_opt(0), bdir);
else if (!S_ISDIR(st.st_mode))
Expand Down
17 changes: 15 additions & 2 deletions options.c
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ char *backup_suffix = NULL;
char *tmpdir = NULL;
char *partial_dir = NULL;
char *basis_dir[MAX_BASIS_DIRS+1];
int basis_dir_perdir[MAX_BASIS_DIRS+1];
char *config_file = NULL;
char *shell_cmd = NULL;
char *logfile_name = NULL;
Expand Down Expand Up @@ -1737,6 +1738,7 @@ int parse_arguments(int *argc_p, const char ***argv_p)
want_dest_type = COMPARE_DEST;

set_dest_dir:
arg = poptGetOptArg(pc);
if (alt_dest_type && alt_dest_type != want_dest_type) {
snprintf(err_buf, sizeof err_buf,
"ERROR: the %s option conflicts with the %s option\n",
Expand All @@ -1751,9 +1753,13 @@ int parse_arguments(int *argc_p, const char ***argv_p)
MAX_BASIS_DIRS, alt_dest_opt(0));
goto cleanup;
}
if (arg[0] == ':' && arg[1] == ' ') {
basis_dir_perdir[basis_dir_cnt] = 1;
arg += 2;
}
/* We defer sanitizing this arg until we know what
* our destination directory is going to be. */
basis_dir[basis_dir_cnt++] = (char *)poptGetOptArg(pc);
basis_dir[basis_dir_cnt++] = (char *)arg;
break;

case OPT_CHMOD:
Expand Down Expand Up @@ -2935,8 +2941,15 @@ void server_options(char **args, int *argc_p)
* option, so don't send it if client is the sender.
*/
for (i = 0; i < basis_dir_cnt; i++) {
char *basis_arg = basis_dir[i];
args[ac++] = alt_dest_opt(0);
args[ac++] = safe_arg("", basis_dir[i]);
if (basis_dir_perdir[i]) {
int len = strlen(basis_arg) + 3;
char *new = new_array(char, len);
snprintf(new, len, ": %s", basis_arg);
basis_arg = new;
}
args[ac++] = safe_arg("", basis_arg);
}
}
}
Expand Down
23 changes: 18 additions & 5 deletions receiver.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ extern int write_devices;
extern int preserve_xattrs;
extern int do_fsync;
extern int basis_dir_cnt;
extern int basis_dir_perdir[MAX_BASIS_DIRS+1];
extern int make_backups;
extern int cleanup_got_literal;
extern int remove_source_files;
Expand Down Expand Up @@ -844,7 +845,8 @@ int recv_files(int f_in, int f_out, char *local_name)
if (fnamecmp_type > FNAMECMP_FUZZY && fnamecmp_type-FNAMECMP_FUZZY <= basis_dir_cnt) {
fnamecmp_type -= FNAMECMP_FUZZY + 1;
if (file->dirname) {
pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], file->dirname);
pathjoin_altdest_dir(fnamecmpbuf, sizeof fnamecmpbuf,
fnamecmp_type, file->dirname);
basedir = fnamecmpbuf;
} else {
basedir = basis_dir[fnamecmp_type];
Expand All @@ -856,8 +858,13 @@ int recv_files(int f_in, int f_out, char *local_name)
fnamecmp_type);
exit_cleanup(RERR_PROTOCOL);
} else {
basedir = basis_dir[fnamecmp_type];
fnamecmp = fname;
if (basis_dir_perdir[fnamecmp_type] && *basis_dir[fnamecmp_type] != '/') {
pathjoin_altdest(fnamecmpbuf, sizeof fnamecmpbuf, fnamecmp_type, fname);
fnamecmp = fnamecmpbuf;
} else {
basedir = basis_dir[fnamecmp_type];
fnamecmp = fname;
}
}
break;
}
Expand Down Expand Up @@ -893,8 +900,14 @@ int recv_files(int f_in, int f_out, char *local_name)

if (fd1 == -1 && basis_dir[0]) {
/* pre-29 allowed only one alternate basis */
basedir = basis_dir[0];
fnamecmp = fname;
if (basis_dir_perdir[0] && *basis_dir[0] != '/') {
pathjoin_altdest(fnamecmpbuf, sizeof fnamecmpbuf, 0, fname);
basedir = NULL;
fnamecmp = fnamecmpbuf;
} else {
basedir = basis_dir[0];
fnamecmp = fname;
}
fnamecmp_type = FNAMECMP_BASIS_DIR_LOW;
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
}
Expand Down
6 changes: 6 additions & 0 deletions rsync.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -2677,6 +2677,8 @@ expand it.
transfer.

If _DIR_ is a relative path, it is relative to the destination directory.
If _DIR_ begins with `: `, the rest of the path is instead relative to
each destination file's containing directory.
See also [`--copy-dest`](#opt) and [`--link-dest`](#opt).

NOTE: beginning with version 3.1.0, rsync will remove a file from a
Expand All @@ -2698,6 +2700,8 @@ expand it.
try to speed up the transfer.

If _DIR_ is a relative path, it is relative to the destination directory.
If _DIR_ begins with `: `, the rest of the path is instead relative to
each destination file's containing directory.
See also [`--compare-dest`](#opt) and [`--link-dest`](#opt).

0. `--link-dest=DIR`
Expand Down Expand Up @@ -2735,6 +2739,8 @@ expand it.
the file is updated.

If _DIR_ is a relative path, it is relative to the destination directory.
If _DIR_ begins with `: `, the rest of the path is instead relative to
each destination file's containing directory.
See also [`--compare-dest`](#opt) and [`--copy-dest`](#opt).

Note that rsync versions prior to 2.6.1 had a bug that could prevent
Expand Down
2 changes: 2 additions & 0 deletions t_stub.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ size_t max_alloc = (size_t)-1; /* test helpers are not memory-constrained;
* per-component fallback of secure_relative_open()
* hits at its first my_strdup() call. */
char *partial_dir;
char *basis_dir[MAX_BASIS_DIRS+1];
int basis_dir_perdir[MAX_BASIS_DIRS+1];
char *module_dir;
/* curr_dir[]/curr_dir_len (read by secure_relative_open) are defined in
* syscall.c, which every helper links -- no stub needed here. */
Expand Down
2 changes: 1 addition & 1 deletion testsuite/COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Status legend: ✓ property asserted · `~` shallow / by an existing ported test
| --inplace | inplace*new*, alt-dest | Y | — | ✓ inode preserved |
| --append / --append-verify | append*new* | Y | — | ✓ verify split is proto 30+ |
| -b, --backup / --backup-dir / --suffix | backup, backup-deep*new* | Y | Y | ✓ |
| --compare-dest / --copy-dest / --link-dest | alt-dest, alt-dest-deep*new* | Y | Y | ✓ link=hardlink, copy=copy, compare=skip |
| --compare-dest / --copy-dest / --link-dest | alt-dest, alt-dest-deep*new*, alt-dest-per-dir*new* | Y | Y | ✓ link=hardlink, copy=copy, compare=skip, per-dir `: REL` |
| -y, --fuzzy | fuzzy | `~` | — | `~` |
| -u, --update | update*new* | Y | — | ✓ keeps newer dest, updates older |
| -W, --whole-file | (used widely; --no-whole-file ubiquitous) | n/a | — | `~` |
Expand Down
62 changes: 62 additions & 0 deletions testsuite/alt-dest-per-dir_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Per-directory relative --compare/copy/link-dest paths.

An alt-dest arg prefixed with ": " is resolved from each destination file's
containing directory. The fixture mirrors a restructure where an old flat file
already exists in the destination and the source now places it one level deeper.
"""

import os

from rsyncfns import (
FROMDIR, TODIR,
assert_exists, assert_hardlinked, assert_not_exists, assert_not_hardlinked,
assert_same, rmtree, run_rsync, test_fail,
)

old_rel = os.path.join('show', 'episode.mkv')
new_rel = os.path.join('show', 'season1', 'episode.mkv')


def seed_trees(old_name='episode.mkv'):
rmtree(FROMDIR)
rmtree(TODIR)
(FROMDIR / 'show' / 'season1').mkdir(parents=True)
(TODIR / 'show').mkdir(parents=True)
data = b'episode payload\n'
(FROMDIR / new_rel).write_bytes(data)
(TODIR / 'show' / old_name).write_bytes(data)
os.utime(FROMDIR / new_rel, (1000000000, 1000000000))
os.utime(TODIR / 'show' / old_name, (1000000000, 1000000000))


def run_to(opt, *extra):
seed_trees()
run_rsync('-a', *extra, f'--{opt}=: ..', f'{FROMDIR}/', f'{TODIR}/')


run_to('link-dest')
assert_exists(TODIR / new_rel, label='link-dest per-dir result')
assert_hardlinked(TODIR / new_rel, TODIR / old_rel, label='link-dest : ..')

run_to('copy-dest')
assert_exists(TODIR / new_rel, label='copy-dest per-dir result')
assert_same(TODIR / new_rel, FROMDIR / new_rel, label='copy-dest : ..')
assert_not_hardlinked(TODIR / new_rel, TODIR / old_rel, label='copy-dest : ..')

run_to('compare-dest')
assert_not_exists(TODIR / new_rel, label='compare-dest : ..')

seed_trees(old_name='episode-old.mkv')
proc = run_rsync('-a', '--debug=FUZZY1', '--fuzzy', '--fuzzy',
'--link-dest=: ..', f'{FROMDIR}/', f'{TODIR}/',
capture_output=True)
out = proc.stdout + proc.stderr
if 'fuzzy basis selected for show/season1/episode.mkv: show/season1/../episode-old.mkv' not in out:
test_fail(f"--fuzzy did not scan per-dir --link-dest=: .. basis:\n{out}")
assert_exists(TODIR / new_rel, label='fuzzy per-dir basis result')
assert_same(TODIR / new_rel, FROMDIR / new_rel, label='fuzzy : ..')
assert_not_hardlinked(TODIR / new_rel, TODIR / 'show' / 'episode-old.mkv',
label='fuzzy transfers delta')

print("alt-dest-per-dir: per-directory --compare/copy/link-dest basis paths verified")
45 changes: 45 additions & 0 deletions util1.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ extern int preallocate_files;
extern char *module_dir;
extern unsigned int module_dirlen;
extern char *partial_dir;
extern char *basis_dir[MAX_BASIS_DIRS+1];
extern int basis_dir_perdir[MAX_BASIS_DIRS+1];
extern filter_rule_list daemon_filter_list;

int sanitize_paths = 0;
Expand Down Expand Up @@ -905,6 +907,49 @@ size_t pathjoin(char *dest, size_t destsize, const char *p1, const char *p2)
return len;
}

/* Join an alternate-basis dir and a destination filename. A per-directory
* basis dir is relative to the destination file's containing directory. */
size_t pathjoin_altdest(char *dest, size_t destsize, int basis_ndx, const char *fname)
{
const char *bdir = basis_dir[basis_ndx];
const char *slash;
size_t bdir_len;
int len;

if (!basis_dir_perdir[basis_ndx] || *bdir == '/')
return pathjoin(dest, destsize, bdir, fname);

slash = strrchr(fname, '/');
if (!slash) {
if (!*bdir)
return strlcpy(dest, fname, destsize);
return pathjoin(dest, destsize, bdir, fname);
}

if (!*bdir)
len = snprintf(dest, destsize, "%.*s/%s", (int)(slash - fname), fname, slash + 1);
else {
bdir_len = strlen(bdir);
len = snprintf(dest, destsize, "%.*s/%s%s%s",
(int)(slash - fname), fname, bdir,
bdir[bdir_len-1] == '/' ? "" : "/", slash + 1);
}

return len < 0 ? destsize : (size_t)len;
}

/* Join an alternate-basis dir and a destination directory. */
size_t pathjoin_altdest_dir(char *dest, size_t destsize, int basis_ndx, const char *dir)
{
const char *bdir = basis_dir[basis_ndx];

if (!basis_dir_perdir[basis_ndx] || *bdir == '/')
return pathjoin(dest, destsize, bdir, dir);
if (!*bdir)
return strlcpy(dest, dir, destsize);
return pathjoin(dest, destsize, dir, bdir);
}

/* Join any number of strings together, putting them in "dest". The return
* value is the length of all the strings, regardless of whether the null-
* terminated whole fits in destsize. Your list of string pointers must end
Expand Down