Do not let rsync() write through symlinks in the destination

With LITE_FOPT_RSYNC_LINKS, rsync() leaves symlinks in the destination.
If the source later has a regular file or a directory by the same name,
the next run follows the old link and writes to whatever it points at,
which can be outside the destination tree.

Remove such a link before copying.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg committed 2026-11-14 12:47:14 +01:00
1 parent 4609c53ddd
commit ef8b17f438
3 files changed
+82 -10

No files matched your search

+4
View File
@@ -251,6 +251,10 @@ Generic Functions
symlink to a file, or a dangling symlink, is recreated in the symlink to a file, or a dangling symlink, is recreated in the
destination instead. Symlinks to directories are always followed. destination instead. Symlinks to directories are always followed.
With `LITE_FOPT_RSYNC_LINKS` a symlink in the destination is never
followed. If one sits where a file or directory from the source
goes, it is removed first.
In releases prior to v2.0 the argument controlling pruning was called In releases prior to v2.0 the argument controlling pruning was called
`delete`, it is now called `opt`. The APIs are 100% compatible if the `delete`, it is now called `opt`. The APIs are 100% compatible if the
value `1` was used. value `1` was used.
+44 -10
View File
@@ -36,7 +36,7 @@
#include "lite.h" #include "lite.h"
static int copy(char *src, char *dst, int opt); static int copy(char *src, char *dst, int opt);
static int mdir(char *buf, size_t buf_len, char *dir, char *name, struct stat *st); static int mdir(char *buf, size_t buf_len, char *dir, char *name, struct stat *st, int opt);
static int prune(char *dst, char **new_files, int new_num); static int prune(char *dst, char **new_files, int new_num);
static int set_mtime(char *fn, struct stat *st); static int set_mtime(char *fn, struct stat *st);
@@ -69,6 +69,10 @@ static int set_mtime(char *fn, struct stat *st);
* text, replacing any file or symlink already there. Symlinks to * text, replacing any file or symlink already there. Symlinks to
* directories in @p src are always followed. * directories in @p src are always followed.
* *
* With ::LITE_FOPT_RSYNC_LINKS a symlink in @p dst is never followed.
* If one sits where a file or directory from @p src goes, it is removed
* first.
*
* The filter callback, @p filter, if provided, is used to determine * The filter callback, @p filter, if provided, is used to determine
* what files to include from the source directory when backing up. If * what files to include from the source directory when backing up. If
* a file is to be skipped the callback should simply return zero. * a file is to be skipped the callback should simply return zero.
@@ -113,7 +117,7 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file))
if (stat(src, &st)) if (stat(src, &st))
return 1; return 1;
if (mdir(dest, sizeof(dest), dst, ptr, &st)) if (mdir(dest, sizeof(dest), dst, ptr, &st, opt))
return 1; return 1;
dst = dest; dst = dest;
do_mtim = keep_mtim; do_mtim = keep_mtim;
@@ -133,7 +137,7 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file))
continue; continue;
} }
if (mdir(dst2, sizeof(dst2), dst, files[i], &sb)) { if (mdir(dst2, sizeof(dst2), dst, files[i], &sb, opt)) {
result++; result++;
continue; continue;
} }
@@ -166,20 +170,30 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file))
return result; return result;
} }
/* Leaves errno alone, a missing @path is not an error to rsync() */
static int islink(const char *path)
{
int rc, saved_errno = errno;
struct stat st;
rc = !lstat(path, &st) && S_ISLNK(st.st_mode);
errno = saved_errno;
return rc;
}
static int copy(char *src, char *dst, int opt) static int copy(char *src, char *dst, int opt)
{ {
char *dest = NULL, *temp = NULL, *file = NULL; char *dest = NULL, *temp = NULL, *file = NULL;
int result = 1, saved_errno, made_temp = 0; int result = 1, saved_errno, made_temp = 0;
int copt = opt & LITE_FOPT_KEEP_MTIME; int copt = opt & LITE_FOPT_KEEP_MTIME;
if (opt & LITE_FOPT_RSYNC_LINKS) if (!(opt & LITE_FOPT_RSYNC_LINKS)) {
copt |= LITE_FOPT_COPYFILE_SYM; copyfile(src, dst, 0, copt);
copyfile(src, dst, 0, copt);
if (errno != EEXIST)
return errno ? 1 : 0; return errno ? 1 : 0;
}
copt |= LITE_FOPT_COPYFILE_SYM;
/* Replace an existing destination only after the new link is ready. */
if (fisdir(dst)) { if (fisdir(dst)) {
char *base = strrchr(src, '/'); char *base = strrchr(src, '/');
@@ -190,6 +204,21 @@ static int copy(char *src, char *dst, int opt)
if (!dest) if (!dest)
return 1; return 1;
} }
/*
* A link in the destination, e.g. from a previous run, may point
* anywhere. Remove it so copyfile() cannot write through it.
*/
if (islink(dest) && !islink(src))
unlink(dest);
copyfile(src, dst, 0, copt);
if (errno != EEXIST) {
result = errno ? 1 : 0;
goto exit;
}
/* Replace an existing destination only after the new link is ready. */
if (asprintf(&temp, "%s/.rsync-XXXXXX", dirname(strdupa(dest))) < 0) if (asprintf(&temp, "%s/.rsync-XXXXXX", dirname(strdupa(dest))) < 0)
goto exit; goto exit;
if (!mkdtemp(temp)) if (!mkdtemp(temp))
@@ -218,9 +247,14 @@ exit:
} }
/* Creates dir/name @mode ... skipping / if dir already ends so. */ /* Creates dir/name @mode ... skipping / if dir already ends so. */
static int mdir(char *buf, size_t buf_len, char *dir, char *name, struct stat *st) static int mdir(char *buf, size_t buf_len, char *dir, char *name, struct stat *st, int opt)
{ {
snprintf(buf, buf_len, "%s%s%s", dir, fisslashdir(dir) ? "" : "/", name); snprintf(buf, buf_len, "%s%s%s", dir, fisslashdir(dir) ? "" : "/", name);
/* Same as in copy(), do not descend into a stale link. */
if ((opt & LITE_FOPT_RSYNC_LINKS) && islink(buf))
unlink(buf);
if (mkdir(buf, st->st_mode)) { if (mkdir(buf, st->st_mode)) {
if (EEXIST != errno) if (EEXIST != errno)
return 1; return 1;
+34
View File
@@ -15,6 +15,39 @@ static void check_link(const char *path, const char *target)
fail_unless(memcmp(buf, target, len) == 0); fail_unless(memcmp(buf, target, len) == 0);
} }
/* A link left in the destination by an earlier run must never be written through. */
static void rsync_stale_links(void)
{
struct stat st;
FILE *fp;
fail_unless(mkdir("src", 0700) == 0 && mkdir("dst", 0700) == 0);
fail_unless(touch("victim") == 0);
fail_unless(symlink("../victim", "src/a") == 0);
fail_unless(symlink("../victims", "src/d") == 0);
fail_unless(rsync("src/", "dst/", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
check_link("dst/a", "../victim");
check_link("dst/d", "../victims");
/* The source links become a file and a directory, and dst/d stops dangling. */
fail_unless(unlink("src/a") == 0 && unlink("src/d") == 0);
fp = fopen("src/a", "w");
fail_unless(fp && fputs("data", fp) >= 0 && fclose(fp) == 0);
fail_unless(mkdir("src/d", 0700) == 0 && touch("src/d/f") == 0);
fail_unless(mkdir("victims", 0700) == 0);
fail_unless(rsync("src/", "dst/", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
fail_unless(lstat("dst/a", &st) == 0 && S_ISREG(st.st_mode) && st.st_size == 4);
fail_unless(stat("victim", &st) == 0 && st.st_size == 0);
fail_unless(lstat("dst/d", &st) == 0 && S_ISDIR(st.st_mode));
fail_unless(fexist("dst/d/f") && !fexist("victims/f"));
fail_unless(unlink("src/a") == 0 && unlink("src/d/f") == 0 && rmdir("src/d") == 0);
fail_unless(unlink("dst/a") == 0 && unlink("dst/d/f") == 0 && rmdir("dst/d") == 0);
fail_unless(rmdir("src") == 0 && rmdir("dst") == 0);
fail_unless(unlink("victim") == 0 && rmdir("victims") == 0);
}
int main(void) int main(void)
{ {
char tmp[] = "/tmp/libite-copylink-XXXXXX"; char tmp[] = "/tmp/libite-copylink-XXXXXX";
@@ -106,6 +139,7 @@ int main(void)
fail_unless(unlink("protected/link") == 0 && rmdir("protected") == 0); fail_unless(unlink("protected/link") == 0 && rmdir("protected") == 0);
} }
fail_unless(unlink("copied") == 0 && unlink("link") == 0); fail_unless(unlink("copied") == 0 && unlink("link") == 0);
rsync_stale_links();
errno = 0; errno = 0;
fail_unless(copyfile("dir", "copied", 0, LITE_FOPT_COPYFILE_SYM) == 0 && errno == EISDIR); fail_unless(copyfile("dir", "copied", 0, LITE_FOPT_COPYFILE_SYM) == 0 && errno == EISDIR);