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
@@ -250,6 +250,10 @@ Generic Functions
gets a copy of what they point to. With `LITE_FOPT_RSYNC_LINKS` a
symlink to a file, or a dangling symlink, is recreated in the
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
`delete`, it is now called `opt`. The APIs are 100% compatible if the
+44 -10
View File
@@ -36,7 +36,7 @@
#include "lite.h"
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 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
* 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
* what files to include from the source directory when backing up. If
* 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))
return 1;
if (mdir(dest, sizeof(dest), dst, ptr, &st))
if (mdir(dest, sizeof(dest), dst, ptr, &st, opt))
return 1;
dst = dest;
do_mtim = keep_mtim;
@@ -133,7 +137,7 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file))
continue;
}
if (mdir(dst2, sizeof(dst2), dst, files[i], &sb)) {
if (mdir(dst2, sizeof(dst2), dst, files[i], &sb, opt)) {
result++;
continue;
}
@@ -166,20 +170,30 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file))
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)
{
char *dest = NULL, *temp = NULL, *file = NULL;
int result = 1, saved_errno, made_temp = 0;
int copt = opt & LITE_FOPT_KEEP_MTIME;
if (opt & LITE_FOPT_RSYNC_LINKS)
copt |= LITE_FOPT_COPYFILE_SYM;
copyfile(src, dst, 0, copt);
if (errno != EEXIST)
if (!(opt & LITE_FOPT_RSYNC_LINKS)) {
copyfile(src, dst, 0, copt);
return errno ? 1 : 0;
}
copt |= LITE_FOPT_COPYFILE_SYM;
/* Replace an existing destination only after the new link is ready. */
if (fisdir(dst)) {
char *base = strrchr(src, '/');
@@ -190,6 +204,21 @@ static int copy(char *src, char *dst, int opt)
if (!dest)
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)
goto exit;
if (!mkdtemp(temp))
@@ -218,9 +247,14 @@ exit:
}
/* 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);
/* 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 (EEXIST != errno)
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);
}
/* 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)
{
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("copied") == 0 && unlink("link") == 0);
rsync_stale_links();
errno = 0;
fail_unless(copyfile("dir", "copied", 0, LITE_FOPT_COPYFILE_SYM) == 0 && errno == EISDIR);