Honor the symbolic link option when copying files

Inspect source links without following them when COPYFILE_SYM is requested, before rejecting directories. This makes the documented option recreate link text instead of copying the referent, including dangling and directory links. Preserve requested link timestamps and report link-creation errors without overwriting the destination. Cover direct calls, destination directories, normal dereferencing and the existing rsync file caller.
This commit is contained in:
omrzk authored and Joachim Wiberg committed 2026-11-14 12:41:18 +01:00
1 parent dff88a622d
commit 666a8ef7cd
5 files changed
+174 -14

No files matched your search

+17 -9
View File
@@ -157,7 +157,13 @@ ssize_t copyfile(const char *src, const char *dst, int len, int opt)
if (!buffer) if (!buffer)
return 0; return 0;
if (fisdir(src)) { if (sym ? lstat(src, &st) : stat(src, &st)) {
saved_errno = errno;
size = -1;
goto exit;
}
if (S_ISDIR(st.st_mode)) {
saved_errno = EISDIR; /* Error: source is a directory */ saved_errno = EISDIR; /* Error: source is a directory */
goto exit; goto exit;
} }
@@ -165,13 +171,7 @@ ssize_t copyfile(const char *src, const char *dst, int len, int opt)
/* Check if target is a directory, then append src filename. */ /* Check if target is a directory, then append src filename. */
isdir = adjust_target(src, &dest); isdir = adjust_target(src, &dest);
/* Check if the source file is a symlink ... */ /* Only try readlink() if symlink and @sym is set. */
if (stat(src, &st)) {
size = -1;
goto exit;
}
/* ... only try readlink() if symlink and @sym is set. */
if (sym && (st.st_mode & S_IFMT) == S_IFLNK) { if (sym && (st.st_mode & S_IFMT) == S_IFLNK) {
size = readlink(src, buffer, BUFSIZ); size = readlink(src, buffer, BUFSIZ);
if (size > 0) { if (size > 0) {
@@ -181,8 +181,16 @@ ssize_t copyfile(const char *src, const char *dst, int len, int opt)
} else { } else {
buffer[size] = 0; buffer[size] = 0;
size = !symlink(buffer, dest); size = !symlink(buffer, dest);
if (!size)
saved_errno = errno;
else if (keep_mtim) {
struct timespec tv[2] = { st.st_atim, st.st_mtim };
utimensat(AT_FDCWD, dest, tv, AT_SYMLINK_NOFOLLOW);
}
} }
} } else
saved_errno = errno;
/* Don't fall back to copy, that is a potentially /* Don't fall back to copy, that is a potentially
* dangerous race condition, see CWE-367. */ * dangerous race condition, see CWE-367. */
+40 -5
View File
@@ -24,6 +24,7 @@
#include <errno.h> #include <errno.h>
#include <fcntl.h> /* AT_* macros */ #include <fcntl.h> /* AT_* macros */
#include <libgen.h> /* dirname() */
#include <stdlib.h> /* NULL, free() */ #include <stdlib.h> /* NULL, free() */
#include <string.h> /* strlen() */ #include <string.h> /* strlen() */
#include <strings.h> /* rindex() */ #include <strings.h> /* rindex() */
@@ -159,15 +160,49 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file))
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;
int result = 1, saved_errno, made_temp = 0;
copyfile(src, dst, 0, opt | LITE_FOPT_COPYFILE_SYM); copyfile(src, dst, 0, opt | LITE_FOPT_COPYFILE_SYM);
if (errno) { if (errno != EEXIST)
if (errno != EEXIST) return errno ? 1 : 0;
/* Replace an existing destination only after the new link is ready. */
if (fisdir(dst)) {
char *base = strrchr(src, '/');
if (asprintf(&dest, "%s%s%s", dst, fisslashdir(dst) ? "" : "/", base ? base + 1 : src) < 0)
return 1;
} else {
dest = strdup(dst);
if (!dest)
return 1; return 1;
errno = 0;
} }
if (asprintf(&temp, "%s/.rsync-XXXXXX", dirname(strdupa(dest))) < 0)
goto exit;
if (!mkdtemp(temp))
goto exit;
made_temp = 1;
if (asprintf(&file, "%s/link", temp) < 0)
goto exit;
return 0; copyfile(src, file, 0, opt | LITE_FOPT_COPYFILE_SYM);
if (errno)
goto exit;
result = rename(file, dest) != 0;
exit:
saved_errno = errno;
if (file)
unlink(file);
if (made_temp)
rmdir(temp);
free(file);
free(temp);
free(dest);
errno = saved_errno;
return result;
} }
/* Creates dir/name @mode ... skipping / if dir already ends so. */ /* Creates dir/name @mode ... skipping / if dir already ends so. */
+1
View File
@@ -5,6 +5,7 @@
/chomp /chomp
/copyfile /copyfile
/fcopyfile /fcopyfile
/copyfile-symlink
/dir /dir
/fexist /fexist
/fisdir /fisdir
+2
View File
@@ -20,6 +20,7 @@ TESTS =
TESTS += chomp TESTS += chomp
TESTS += copyfile TESTS += copyfile
TESTS += fcopyfile TESTS += fcopyfile
TESTS += copyfile-symlink
TESTS += dir TESTS += dir
TESTS += fexist TESTS += fexist
TESTS += fisdir TESTS += fisdir
@@ -41,3 +42,4 @@ TESTS += which
TESTS += yorn TESTS += yorn
check_PROGRAMS = $(TESTS) check_PROGRAMS = $(TESTS)
copyfile_symlink_SOURCES = copyfile-symlink.c
+114
View File
@@ -0,0 +1,114 @@
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include "check.h"
static void check_link(const char *path, const char *target)
{
char buf[256];
struct stat st;
ssize_t len;
fail_unless(lstat(path, &st) == 0 && S_ISLNK(st.st_mode));
len = readlink(path, buf, sizeof(buf));
fail_unless(len == (ssize_t)strlen(target));
fail_unless(memcmp(buf, target, len) == 0);
}
int main(void)
{
char tmp[] = "/tmp/libite-copylink-XXXXXX";
const char payload[] = "target contents";
char buf[sizeof(payload)];
const char *targets[] = { "file", "dir", "missing", ".", "link" };
struct timespec times[2] = { { 1234567800, 0 }, { 1234567801, 0 } };
struct stat src_st, dst_st;
char cwd[4096];
size_t i;
FILE *fp;
fail_unless(getcwd(cwd, sizeof(cwd)) && mkdtemp(tmp));
fail_unless(chdir(tmp) == 0);
fail_unless(mkdir("dir", 0700) == 0);
fail_unless(mkdir("out", 0700) == 0);
fp = fopen("file", "w");
fail_unless(fp && fwrite(payload, 1, sizeof(payload), fp) == sizeof(payload));
fail_unless(fclose(fp) == 0);
for (i = 0; i < NELEMS(targets); i++) {
fail_unless(symlink(targets[i], "link") == 0);
fail_unless(copyfile("link", "copied", 0, LITE_FOPT_COPYFILE_SYM) == 1);
check_link("copied", targets[i]);
fail_unless(copyfile("link", "out", 0, LITE_FOPT_COPYFILE_SYM) == 1);
check_link("out/link", targets[i]);
/* Existing targets must survive a failed symlink creation. */
errno = 0;
fail_unless(copyfile("link", "file", 0, LITE_FOPT_COPYFILE_SYM) == 0);
fail_unless(errno == EEXIST);
check_link("copied", targets[i]);
fail_unless(unlink("copied") == 0 && unlink("out/link") == 0);
fail_unless(unlink("link") == 0);
}
fail_unless(symlink("file", "link") == 0);
/* Without SYM, continue to copy target data and preserve regular-file metadata. */
fail_unless(copyfile("link", "copied", 0, LITE_FOPT_KEEP_MTIME) == sizeof(payload));
fail_unless(lstat("copied", &dst_st) == 0 && S_ISREG(dst_st.st_mode));
fail_unless(stat("file", &src_st) == 0 && src_st.st_mtime == dst_st.st_mtime);
fp = fopen("copied", "r");
fail_unless(fp && fread(buf, 1, sizeof(buf), fp) == sizeof(payload));
fail_unless(memcmp(buf, payload, sizeof(payload)) == 0 && fclose(fp) == 0);
fail_unless(unlink("copied") == 0);
fail_unless(utimensat(AT_FDCWD, "link", times, AT_SYMLINK_NOFOLLOW) == 0);
fail_unless(copyfile("link", "copied", 0, LITE_FOPT_COPYFILE_SYM | LITE_FOPT_KEEP_MTIME) == 1);
check_link("copied", "file");
fail_unless(lstat("copied", &dst_st) == 0 && dst_st.st_mtime == times[1].tv_sec);
fail_unless(unlink("copied") == 0);
/* Exercise the existing rsync file caller, which requests SYM. */
fail_unless(rsync("link", "copied", 0, NULL) == 0);
check_link("copied", "file");
/* Reuse the destination after changing the source link. */
fail_unless(unlink("link") == 0 && symlink("file", "link") == 0);
fail_unless(rsync("link", "out", 0, NULL) == 0);
check_link("out/link", "file");
fail_unless(unlink("link") == 0 && symlink("copied", "link") == 0);
fail_unless(rsync("link", "out", 0, NULL) == 0);
check_link("out/link", "copied");
/* A same-path copy must not unlink its source. */
fail_unless(rsync("link", "link", 0, NULL) == 0);
check_link("link", "copied");
fail_unless(unlink("out/link") == 0);
/* Replacing a regular destination must also retain the source link. */
fail_unless(touch("out/link") == 0);
fail_unless(rsync("link", "out", 0, NULL) == 0);
check_link("out/link", "copied");
fail_unless(unlink("out/link") == 0);
/* A failed replacement must keep both the destination and source. */
fail_unless(mkdir("out/link", 0700) == 0);
fail_unless(touch("out/link/keep") == 0);
fail_unless(rsync("link", "out", 0, NULL) != 0);
fail_unless(fexist("out/link/keep"));
check_link("link", "copied");
fail_unless(unlink("out/link/keep") == 0 && rmdir("out/link") == 0);
if (getuid() != 0) {
fail_unless(mkdir("protected", 0700) == 0);
fail_unless(symlink("original", "protected/link") == 0);
fail_unless(chmod("protected", 0500) == 0);
fail_unless(rsync("link", "protected", 0, NULL) != 0);
check_link("protected/link", "original");
check_link("link", "copied");
fail_unless(chmod("protected", 0700) == 0);
fail_unless(unlink("protected/link") == 0 && rmdir("protected") == 0);
}
fail_unless(unlink("copied") == 0 && unlink("link") == 0);
errno = 0;
fail_unless(copyfile("dir", "copied", 0, LITE_FOPT_COPYFILE_SYM) == 0 && errno == EISDIR);
errno = 0;
fail_unless(copyfile("missing", "copied", 0, LITE_FOPT_COPYFILE_SYM) == -1 && errno == ENOENT);
fail_unless(unlink("file") == 0 && rmdir("dir") == 0 && rmdir("out") == 0);
fail_unless(chdir(cwd) == 0 && rmdir(tmp) == 0);
return 0;
}