#287: - --safe-links: keep safe in-tree links AS symlinks and skip unsafe (absolute or ".."-escaping) ones, mirroring rsync's unsafe_symlink(). Skipped links are recorded as delete-protected so --delete does not remove their destination mirror (no silent data loss). - --copy-unsafe-links: preserve safe links as symlinks and dereference only unsafe ones. - --munge-links: receiver-side rewrite storing /rsyncd-munged/-prefixed targets (rsync parity), replacing the no-op #SYMLINK sender prefix. - -l: store the target verbatim, including absolute and ".." targets (rsync -l parity); the old receiver containment silently dropped them. #288: - --specials: recreate unix-domain sockets via mknod(S_IFSOCK), which Linux permits unprivileged; keep EEXIST/EPERM skip behavior. - --copy-devices: copy a device's content into a regular file when requested; skip unrequested non-regular entries like rsync's default.
This commit is contained in:
+161
-92
@@ -98,17 +98,51 @@ static DirEntry* dir_entry_create(const char* path, int depth, FilterNode* conte
|
||||
return de;
|
||||
}
|
||||
|
||||
static bool safe_relative_link(const char* source_root, const char* containing_dir,
|
||||
const char* link_target) {
|
||||
char root[PATH_MAX];
|
||||
if (!realpath(source_root, root))
|
||||
return false;
|
||||
char* joined = path_cat(containing_dir, link_target);
|
||||
char resolved[PATH_MAX];
|
||||
bool safe = joined && realpath(joined, resolved) && strncmp(root, resolved, strlen(root)) == 0 &&
|
||||
(resolved[strlen(root)] == '\0' || resolved[strlen(root)] == '/');
|
||||
free(joined);
|
||||
return safe;
|
||||
/* How rsync's readlink_stat()/generator resolves one source symlink. */
|
||||
typedef enum {
|
||||
LINK_ACTION_SKIP, /* not transferred (no link option) */
|
||||
LINK_ACTION_SKIP_PROTECTED, /* ignored as unsafe by --safe-links; rsync keeps
|
||||
it in the transfer, so its destination mirror
|
||||
must be protected from --delete */
|
||||
LINK_ACTION_DEREF, /* follow the referent (--copy-links, an unsafe
|
||||
target under --copy-unsafe-links, or -k dir) */
|
||||
LINK_ACTION_CARRY, /* transmit the link itself (-l) */
|
||||
} LinkAction;
|
||||
|
||||
/* Apply rsync's symlink-resolution precedence to one S_ISLNK entry:
|
||||
* --copy-links dereferences every symlink;
|
||||
* --copy-unsafe-links dereferences only targets unsafe_symlink() flags;
|
||||
* -k/--copy-dirlinks dereferences only a symlink whose referent is a dir;
|
||||
* --safe-links (receiver-side in rsync; modelled here) ignores an unsafe
|
||||
* target that would otherwise be carried; with --munge-links
|
||||
* every stored target becomes absolute, so --safe-links then
|
||||
* ignores every symlink, exactly as rsync documents;
|
||||
* -l/--links carries the link.
|
||||
* `link_rel` is the symlink's transfer-relative path (incl. name) and is used
|
||||
* only for the lexical unsafe test. `target` receives the raw link value. */
|
||||
static LinkAction scanner_link_action(const ScannerOptions* options, const char* path,
|
||||
const char* link_rel, char* target, size_t target_size) {
|
||||
if (!options->follow_symlinks && !options->copy_links && !options->safe_links &&
|
||||
!options->copy_unsafe_links && !options->copy_dirlinks)
|
||||
return LINK_ACTION_SKIP;
|
||||
ssize_t length = readlink(path, target, target_size - 1);
|
||||
if (length < 0)
|
||||
return LINK_ACTION_SKIP;
|
||||
target[length] = '\0';
|
||||
|
||||
bool unsafe = file_symlink_unsafe(target, link_rel);
|
||||
if (options->copy_links || (options->copy_unsafe_links && unsafe))
|
||||
return LINK_ACTION_DEREF;
|
||||
if (options->copy_dirlinks) {
|
||||
struct stat ref;
|
||||
if (stat(path, &ref) == 0 && S_ISDIR(ref.st_mode))
|
||||
return LINK_ACTION_DEREF;
|
||||
}
|
||||
if (options->safe_links && (unsafe || options->munge_links))
|
||||
return LINK_ACTION_SKIP_PROTECTED;
|
||||
if (!options->follow_symlinks || target[0] == '\0')
|
||||
return LINK_ACTION_SKIP;
|
||||
return LINK_ACTION_CARRY;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
@@ -210,24 +244,35 @@ static void scanner_assign_hardlink(DirectoryScanner* scanner, HardLinkTable* ta
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 4 special/devices: detect a device (char/block), FIFO or socket entry
|
||||
and, when the matching --devices/--specials flag asks it be preserved,
|
||||
convert the File into a node to recreate (is_special, empty payload) with its
|
||||
device rdev captured from the source stat. When the entry is not preserved
|
||||
(or --copy-devices instead copies its content as an ordinary regular file)
|
||||
the File is left as a normal data file. Returns true when converted. */
|
||||
static bool scanner_prepare_special(bool preserve_devices, bool preserve_specials, File* file,
|
||||
const struct stat* stats) {
|
||||
/* Phase 4 special/devices decision for one non-regular entry, matching rsync:
|
||||
- a char/block device is RECREATED as a node under -D/--devices, unless
|
||||
--copy-devices asks for its content to be copied into a regular file;
|
||||
- a FIFO/socket is RECREATED under --specials;
|
||||
- when the matching flag is absent the entry is SKIPPED ("skipping
|
||||
non-regular file"), exactly like rsync's default, instead of being
|
||||
silently copied as a zero-length regular file;
|
||||
- anything else (regular/directory) is left to the normal data path. */
|
||||
typedef enum {
|
||||
SCANNER_SPECIAL_REGULAR, /* ordinary file: transfer content */
|
||||
SCANNER_SPECIAL_RECREATE, /* is_special node to recreate on the receiver */
|
||||
SCANNER_SPECIAL_SKIP, /* non-regular entry not requested: skip */
|
||||
} ScannerSpecial;
|
||||
|
||||
static ScannerSpecial scanner_prepare_special(bool preserve_devices, bool preserve_specials,
|
||||
bool copy_devices, File* file,
|
||||
const struct stat* stats) {
|
||||
if (!file || !stats)
|
||||
return false;
|
||||
return SCANNER_SPECIAL_REGULAR;
|
||||
bool is_device = S_ISCHR(stats->st_mode) || S_ISBLK(stats->st_mode);
|
||||
bool is_fifo = S_ISFIFO(stats->st_mode);
|
||||
bool is_socket = S_ISSOCK(stats->st_mode);
|
||||
if (!is_device && !is_fifo && !is_socket)
|
||||
return false;
|
||||
return SCANNER_SPECIAL_REGULAR;
|
||||
if (is_device && copy_devices)
|
||||
return SCANNER_SPECIAL_REGULAR; /* copy device content as a regular file */
|
||||
bool preserve = is_device ? preserve_devices : preserve_specials;
|
||||
if (!preserve)
|
||||
return false;
|
||||
return SCANNER_SPECIAL_SKIP;
|
||||
file->is_special = true;
|
||||
file->data->size = 0;
|
||||
file->data->data = NULL;
|
||||
@@ -235,7 +280,7 @@ static bool scanner_prepare_special(bool preserve_devices, bool preserve_special
|
||||
file->rdev_major = (int32_t)major(stats->st_rdev);
|
||||
file->rdev_minor = (int32_t)minor(stats->st_rdev);
|
||||
}
|
||||
return true;
|
||||
return SCANNER_SPECIAL_RECREATE;
|
||||
}
|
||||
|
||||
/* Append `rel` to the caller's exclusion sink, taking `mtx` when shared across
|
||||
@@ -306,10 +351,11 @@ static int open_directory_filter_context(DirectoryScanner* scanner, const Filter
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Inspect symlinks, resolve the entry type, and apply file filters once for both scanners. */
|
||||
static int scanner_inspect_entry(const ScannerOptions* options, const char* source_root,
|
||||
const char* containing_dir, const char* name,
|
||||
ScannerEntry* entry) {
|
||||
/* Inspect symlinks, resolve the entry type, and apply file filters once for both scanners.
|
||||
* `link_rel` is the entry's path relative to the transfer root (including its
|
||||
* name), used for the lexical rsync unsafe-symlink test. */
|
||||
static int scanner_inspect_entry(const ScannerOptions* options, const char* containing_dir,
|
||||
const char* link_rel, const char* name, ScannerEntry* entry) {
|
||||
entry->excluded = false;
|
||||
entry->is_symlink = false;
|
||||
entry->link_target = NULL;
|
||||
@@ -322,72 +368,49 @@ static int scanner_inspect_entry(const ScannerOptions* options, const char* sour
|
||||
free(entry->path);
|
||||
return 0;
|
||||
}
|
||||
bool is_symlink = S_ISLNK(link_stats.st_mode);
|
||||
if (!is_symlink)
|
||||
if (!S_ISLNK(link_stats.st_mode))
|
||||
goto regular;
|
||||
|
||||
/* Symlink: choose between dereferencing (---copy-links / --safe-links /
|
||||
--copy-unsafe-links, plus -k for symlinks-to-directories) and carrying the
|
||||
link through as a symlink (-l, and -k for symlinks-to-files). No link
|
||||
option means the symlink is skipped entirely (pre-existing behavior). */
|
||||
const bool any_link_option = options->follow_symlinks || options->copy_links ||
|
||||
options->safe_links || options->copy_unsafe_links ||
|
||||
options->copy_dirlinks;
|
||||
if (!any_link_option)
|
||||
goto skip;
|
||||
|
||||
char link_target[4096];
|
||||
ssize_t length = readlink(entry->path, link_target, sizeof(link_target) - 1);
|
||||
if (length < 0)
|
||||
switch (scanner_link_action(options, entry->path, link_rel, link_target, sizeof(link_target))) {
|
||||
case LINK_ACTION_SKIP:
|
||||
goto skip;
|
||||
link_target[length] = '\0';
|
||||
|
||||
if (options->safe_links) {
|
||||
if (link_target[0] == '/' || !safe_relative_link(source_root, containing_dir, link_target))
|
||||
goto skip;
|
||||
}
|
||||
if (options->copy_unsafe_links && !options->copy_links) {
|
||||
if (link_target[0] != '/')
|
||||
goto skip;
|
||||
}
|
||||
|
||||
bool emit_symlink = false;
|
||||
if (options->copy_links) {
|
||||
emit_symlink = false; /* --copy-links dereferences every referent */
|
||||
} else if (options->safe_links || options->copy_unsafe_links) {
|
||||
emit_symlink = false; /* preserve pre-existing dereference behavior */
|
||||
} else if (options->copy_dirlinks) {
|
||||
struct stat ref;
|
||||
if (stat(entry->path, &ref) == 0 && S_ISDIR(ref.st_mode))
|
||||
emit_symlink = false; /* -k: symlink to a directory recurses as a dir */
|
||||
else
|
||||
emit_symlink = true; /* -k: symlink to a file stays a symlink */
|
||||
} else if (options->follow_symlinks) {
|
||||
emit_symlink = true; /* -l: copy symlink as symlink */
|
||||
}
|
||||
|
||||
if (!emit_symlink) {
|
||||
if (stat(entry->path, &entry->stats) != 0)
|
||||
case LINK_ACTION_SKIP_PROTECTED:
|
||||
/* --safe-links ignored the link, but rsync still counts it as present in
|
||||
the transfer, so its destination mirror survives --delete. Record it as
|
||||
an excluded path (the same delete-protection channel as a filter prune). */
|
||||
entry->excluded = true;
|
||||
goto skip;
|
||||
case LINK_ACTION_DEREF:
|
||||
if (stat(entry->path, &entry->stats) != 0) {
|
||||
/* rsync reports "symlink has no referent" and continues (exit 23); we
|
||||
surface the same condition rather than silently dropping the entry. */
|
||||
char* escaped = output_escape(entry->path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "symlink has no referent: %s",
|
||||
escaped ? escaped : "<allocation failed>");
|
||||
free(escaped);
|
||||
goto skip;
|
||||
}
|
||||
entry->is_directory = S_ISDIR(entry->stats.st_mode);
|
||||
if (entry->is_directory)
|
||||
return 1;
|
||||
goto apply_filters;
|
||||
case LINK_ACTION_CARRY:
|
||||
break;
|
||||
}
|
||||
|
||||
/* Carry the link as a symlink. --munge-links containment: a target that
|
||||
could escape the receive root (absolute or containing "..") is never
|
||||
transmitted -- the entry is merely skipped ("contained"). */
|
||||
if (link_target[0] == '\0' ||
|
||||
(options->munge_links && !file_symlink_target_contained(link_target)))
|
||||
goto skip;
|
||||
/* Carry the link as a symlink. --munge-links is applied by the RECEIVER (it
|
||||
prefixes every stored target with /rsyncd-munged/); when the SOURCE already
|
||||
holds a munged value the sender strips it so the receiver re-munges a clean
|
||||
target, round-tripping a munged tree exactly like rsync. */
|
||||
entry->is_symlink = true;
|
||||
entry->stats = link_stats;
|
||||
entry->is_directory = false;
|
||||
entry->link_target =
|
||||
options->munge_links ? file_symlink_munge(link_target) : str_dup(link_target);
|
||||
entry->link_target = str_dup(link_target);
|
||||
if (!entry->link_target)
|
||||
goto skip;
|
||||
if (options->munge_links)
|
||||
file_symlink_unmunge(entry->link_target);
|
||||
goto apply_filters;
|
||||
|
||||
regular:
|
||||
@@ -798,30 +821,57 @@ static File* dirs_file_for_entry(DirectoryScanner* scanner, const char* entry) {
|
||||
return NULL;
|
||||
}
|
||||
struct stat effective = link_stats;
|
||||
bool emit_symlink = false;
|
||||
char* symlink_target = NULL;
|
||||
if (S_ISLNK(link_stats.st_mode)) {
|
||||
/* A symlink is transferred (following its referent) only when a link
|
||||
resolution option is active, mirroring the regular scanner. */
|
||||
bool resolve = scanner->options.follow_symlinks || scanner->options.copy_links ||
|
||||
scanner->options.safe_links || scanner->options.copy_unsafe_links;
|
||||
if (!resolve || stat(abs_path, &effective) != 0) {
|
||||
/* Resolve the listed symlink with the same precedence as the recursive
|
||||
scanner: dereference or carry the link. */
|
||||
char link_target[4096];
|
||||
LinkAction action =
|
||||
scanner_link_action(&scanner->options, abs_path, entry, link_target, sizeof(link_target));
|
||||
if (action == LINK_ACTION_SKIP || action == LINK_ACTION_SKIP_PROTECTED) {
|
||||
free(abs_path);
|
||||
return NULL;
|
||||
}
|
||||
if (action == LINK_ACTION_DEREF) {
|
||||
if (stat(abs_path, &effective) != 0) {
|
||||
free(abs_path);
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
emit_symlink = true;
|
||||
symlink_target = str_dup(link_target);
|
||||
if (!symlink_target) {
|
||||
free(abs_path);
|
||||
scanner->failed = true;
|
||||
return NULL;
|
||||
}
|
||||
if (scanner->options.munge_links)
|
||||
file_symlink_unmunge(symlink_target);
|
||||
}
|
||||
}
|
||||
bool is_dir = S_ISDIR(effective.st_mode);
|
||||
bool is_file = S_ISREG(effective.st_mode);
|
||||
if (!is_dir && !is_file) {
|
||||
if (!emit_symlink && !is_dir && !is_file) {
|
||||
free(symlink_target);
|
||||
free(abs_path);
|
||||
return NULL;
|
||||
}
|
||||
File* file = file_create(abs_path);
|
||||
free(abs_path);
|
||||
if (!file) {
|
||||
free(symlink_target);
|
||||
scanner->failed = true;
|
||||
return NULL;
|
||||
}
|
||||
file->is_dir = is_dir;
|
||||
file->data->size = is_file ? (unsigned long long)effective.st_size : 0;
|
||||
if (emit_symlink) {
|
||||
file->is_symlink = true;
|
||||
file->symlink_target = symlink_target;
|
||||
symlink_target = NULL;
|
||||
} else {
|
||||
file->is_dir = is_dir;
|
||||
file->data->size = is_file ? (unsigned long long)effective.st_size : 0;
|
||||
}
|
||||
if (scanner->relative_mode) {
|
||||
file->send_path = str_dup(entry);
|
||||
if (!file->send_path) {
|
||||
@@ -978,8 +1028,14 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
||||
continue;
|
||||
|
||||
ScannerEntry inspected;
|
||||
int inspection = scanner_inspect_entry(&scanner->options, scanner->current_path,
|
||||
scanner->current_path, entry->d_name, &inspected);
|
||||
char* link_rel = child_rel_path(scanner->current_rel, entry->d_name);
|
||||
if (!link_rel) {
|
||||
scanner->failed = true;
|
||||
break;
|
||||
}
|
||||
int inspection = scanner_inspect_entry(&scanner->options, scanner->current_path, link_rel,
|
||||
entry->d_name, &inspected);
|
||||
free(link_rel);
|
||||
if (inspection < 0) {
|
||||
scanner->failed = true;
|
||||
break;
|
||||
@@ -1084,9 +1140,16 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
||||
rel_copy = NULL;
|
||||
}
|
||||
/* --devices/--specials: a device/FIFO/socket entry marked for preservation
|
||||
becomes a node to recreate (is_special, no data, rdev captured). */
|
||||
scanner_prepare_special(scanner->options.preserve_devices, scanner->options.preserve_specials,
|
||||
file, &stats);
|
||||
becomes a node to recreate (is_special, no data, rdev captured); an
|
||||
unrequested non-regular entry is skipped (rsync default). */
|
||||
ScannerSpecial special = scanner_prepare_special(scanner->options.preserve_devices,
|
||||
scanner->options.preserve_specials,
|
||||
scanner->options.copy_devices, file, &stats);
|
||||
if (special == SCANNER_SPECIAL_SKIP) {
|
||||
free(rel_copy);
|
||||
file_destroy(file);
|
||||
continue;
|
||||
}
|
||||
if (scanner->options.hardlinks && S_ISREG(stats.st_mode))
|
||||
scanner_assign_hardlink(scanner, scanner->options.hardlinks, file, &stats);
|
||||
if (scanner->options.use_metadata)
|
||||
@@ -1335,7 +1398,7 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
|
||||
ParallelScanner* ps) {
|
||||
ScannerEntry inspected;
|
||||
int inspection =
|
||||
scanner_inspect_entry(options, root_directory, root_directory, entry->d_name, &inspected);
|
||||
scanner_inspect_entry(options, root_directory, entry->d_name, entry->d_name, &inspected);
|
||||
if (inspection < 0) {
|
||||
ps->failed = true;
|
||||
return;
|
||||
@@ -1415,7 +1478,13 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
|
||||
file->send_path = rel;
|
||||
rel = NULL;
|
||||
}
|
||||
scanner_prepare_special(options->preserve_devices, options->preserve_specials, file, &st);
|
||||
ScannerSpecial special = scanner_prepare_special(
|
||||
options->preserve_devices, options->preserve_specials, options->copy_devices, file, &st);
|
||||
if (special == SCANNER_SPECIAL_SKIP) {
|
||||
free(rel);
|
||||
file_destroy(file);
|
||||
return;
|
||||
}
|
||||
if (options->hardlinks && S_ISREG(st.st_mode)) {
|
||||
int gid;
|
||||
bool is_first;
|
||||
|
||||
+4
-5
@@ -277,11 +277,11 @@ void print_usage(void) {
|
||||
printf(" Local receiver policy: never sent to the peer, off by default\n");
|
||||
printf(" -l, --links Copy symlinks as symlinks\n");
|
||||
printf(" -L, --copy-links Transform symlinks into referent files\n");
|
||||
printf(" --safe-links Skip symlinks that point outside transfer tree\n");
|
||||
printf(" --copy-unsafe-links Only transform unsafe symlinks into referent files\n");
|
||||
printf(" --safe-links Skip symlinks whose target points outside the tree\n");
|
||||
printf(" --copy-unsafe-links Copy unsafe symlinks (outside tree) as referent files\n");
|
||||
printf(" -k, --copy-dirlinks Transform symlinks to directories into real dirs\n");
|
||||
printf(" -K, --keep-dirlinks Keep an existing symlink-to-dir as that dir\n");
|
||||
printf(" --munge-links Munge symlink targets on the wire (sender)\n");
|
||||
printf(" --munge-links Munge stored symlink targets (/rsyncd-munged/) on the receiver\n");
|
||||
printf(" -H, --hard-links Preserve hard-link relationships across the transfer\n");
|
||||
printf(" -S, --sparse Handle sparse files efficiently\n");
|
||||
printf(
|
||||
@@ -289,8 +289,7 @@ void print_usage(void) {
|
||||
printf(
|
||||
" --devices Recreate device nodes on the destination (privileged; skipped when\n");
|
||||
printf(" the receiver lacks CAP_MKNOD)\n");
|
||||
printf(" --specials Recreate special files (FIFOs) on the destination (sockets "
|
||||
"skipped)\n");
|
||||
printf(" --specials Recreate special files (FIFOs, sockets) on the destination\n");
|
||||
printf(" --copy-devices Copy a source device's content as a regular file instead\n");
|
||||
printf(" --write-devices Write received data into an existing destination device node\n");
|
||||
printf(" --inplace Update files in-place (no temp+rename)\n");
|
||||
|
||||
+71
-20
@@ -415,10 +415,69 @@ bool file_get_trust_sender(void) {
|
||||
return file_trust_sender;
|
||||
}
|
||||
|
||||
/* True when `target` is a lexical symlink target that can never escape the
|
||||
* receive root once created beneath it: relative (not absolute) and containing
|
||||
* no ".." path component. Used by --munge-links' sender-side containment: an
|
||||
* escaping target is never transmitted (the entry is skipped/contained). */
|
||||
/* rsync 3.4.1 unsafe_symlink(): true when `target` (the link's destination
|
||||
* string) points outside the transfer tree rooted at the symlink's own
|
||||
* location. `link_path` is the symlink's path relative to the top of the
|
||||
* transfer (including its name). This is a purely lexical test matching
|
||||
* rsync's util1.c: absolute/empty targets are always unsafe; leading "../"
|
||||
* components are counted against the symlink's own directory depth; a ".."
|
||||
* that would climb above the transfer root is unsafe. rsync 3.4.1 additionally
|
||||
* rejects any INTERNAL "/../" component and a trailing "/..". */
|
||||
bool file_symlink_unsafe(const char* target, const char* link_path) {
|
||||
if (!target || target[0] == '\0' || target[0] == '/')
|
||||
return true;
|
||||
const char* rest = target;
|
||||
while (strncmp(rest, "../", 3) == 0) {
|
||||
rest += 3;
|
||||
while (*rest == '/')
|
||||
rest++;
|
||||
}
|
||||
if (strstr(rest, "/../") != NULL)
|
||||
return true;
|
||||
size_t target_len = strlen(target);
|
||||
if (target_len > 3 && strcmp(&target[target_len - 3], "/..") == 0)
|
||||
return true;
|
||||
|
||||
int depth = 0;
|
||||
const char* name;
|
||||
const char* slash;
|
||||
const char* src = link_path ? link_path : "";
|
||||
for (name = src; (slash = strchr(name, '/')) != NULL; name = slash + 1) {
|
||||
if (*name == '.' && (name[1] == '/' || (name[1] == '.' && name[2] == '/'))) {
|
||||
if (name[1] == '.')
|
||||
depth = 0;
|
||||
} else {
|
||||
depth++;
|
||||
}
|
||||
while (slash[1] == '/')
|
||||
slash++;
|
||||
}
|
||||
if (*name == '.' && name[1] == '.' && name[2] == '\0')
|
||||
depth = 0;
|
||||
|
||||
for (name = target; (slash = strchr(name, '/')) != NULL; name = slash + 1) {
|
||||
if (*name == '.' && (name[1] == '/' || (name[1] == '.' && name[2] == '/'))) {
|
||||
if (name[1] == '.') {
|
||||
if (--depth < 0)
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
depth++;
|
||||
}
|
||||
while (slash[1] == '/')
|
||||
slash++;
|
||||
}
|
||||
if (*name == '.' && name[1] == '.' && name[2] == '\0')
|
||||
depth--;
|
||||
return depth < 0;
|
||||
}
|
||||
|
||||
/* Strict lexical helper: true when `target` is relative (not absolute) and
|
||||
* contains no ".." component at all, so it can never escape the directory it
|
||||
* is created in. This is stricter than rsync's unsafe_symlink() (which allows
|
||||
* an in-tree ".."); the scanner/receiver use file_symlink_unsafe()/--safe-links
|
||||
* for rsync parity, and this helper is retained for callers that want the
|
||||
* ".."-free guarantee. */
|
||||
bool file_symlink_target_contained(const char* target) {
|
||||
if (!target || target[0] == '\0' || target[0] == '/')
|
||||
return false;
|
||||
@@ -449,8 +508,9 @@ bool file_symlink_unmunge(char* target) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Owned copy of `target` prefixed with SYMLINK_MUNGE_PREFIX (the sender-side
|
||||
* --munge-links rewriting). Returns NULL on allocation failure. */
|
||||
/* Owned copy of `target` prefixed with SYMLINK_MUNGE_PREFIX (the receiver-side
|
||||
* --munge-links rewriting, matching rsync's receiver). Returns NULL on
|
||||
* allocation failure. */
|
||||
char* file_symlink_munge(const char* target) {
|
||||
if (!target)
|
||||
return NULL;
|
||||
@@ -471,23 +531,14 @@ char* file_symlink_munge(const char* target) {
|
||||
* the target is ever followed. The final component is never dereferenced: an
|
||||
* existing non-directory entry at `path` is unlinked by name before the link is
|
||||
* placed; an existing directory there is left untouched (returns false, so a
|
||||
* caller can treat it as a collision). As a receiver-side trust-boundary
|
||||
* invariant, `target` must be file_symlink_target_contained() (relative and
|
||||
* ".."-free): an absolute or escaping target is rejected outright (returns
|
||||
* false) so a malicious sender can never materialize a symlink that points
|
||||
* outside the receive root. */
|
||||
* caller can treat it as a collision). The link VALUE `target` is copied
|
||||
* verbatim, matching rsync -l (which stores absolute and ".."-bearing targets
|
||||
* as-is); target policy is the caller's job -- the scanner applies
|
||||
* --safe-links/--copy-unsafe-links, and the receiver applies --munge-links.
|
||||
* The PLACEMENT path is always confined below the authorized root. */
|
||||
bool file_symlink_at_secure(const char* path, const char* target) {
|
||||
/* The link itself (`path`) is always kept below the authorized root. The
|
||||
TARGET may point anywhere: normally only a contained (relative, ".."-free)
|
||||
target is permitted so a malicious sender can never plant a symlink that
|
||||
later dereferences outside the root. Under --trust-sender that target
|
||||
containment check is relaxed (the receiver trusts the sender and copies the
|
||||
link verbatim, matching rsync -l), but path/leaf confinement is never
|
||||
disabled, so the link still cannot be placed outside the tree. */
|
||||
if (!path || !target || has_path_traversal(path))
|
||||
return false;
|
||||
if (!file_trust_sender && !file_symlink_target_contained(target))
|
||||
return false;
|
||||
char* leaf = NULL;
|
||||
int parent_fd = file_open_secure_parent(path, &leaf, true);
|
||||
if (parent_fd < 0)
|
||||
|
||||
+15
-6
@@ -44,12 +44,20 @@ int file_open_for_read(const char* path);
|
||||
bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size,
|
||||
bool inplace, bool sparse);
|
||||
|
||||
/* Symlink trust-boundary helpers (Phase 4, symlink wave). --munge-links
|
||||
* sender-side marker: every transmitted symlink target is prefixed with this
|
||||
* while the flag is on; the receiver strips it to restore the real target. */
|
||||
#define SYMLINK_MUNGE_PREFIX "#SYMLINK/"
|
||||
/* Symlink trust-boundary helpers (Phase 4, symlink wave; rsync parity).
|
||||
* --munge-links is a RECEIVER-side rewrite: rsync prefixes every stored symlink
|
||||
* target with this marker, making the link unusable while the referenced
|
||||
* directory does not exist. A SENDER receiving a munged source strips it back
|
||||
* off before transmitting (so a munged tree round-trips through the receiver's
|
||||
* re-munging). */
|
||||
#define SYMLINK_MUNGE_PREFIX "/rsyncd-munged/"
|
||||
|
||||
char* file_symlink_munge(const char* target);
|
||||
/* rsync 3.4.1 unsafe_symlink(): true when `target` escapes the transfer tree
|
||||
* rooted at `link_path` (the symlink's transfer-relative path incl. its name).
|
||||
* Absolute/empty targets and targets climbing above the transfer root (via
|
||||
* "..") are unsafe, as are internal "/../" components and trailing "/..". */
|
||||
bool file_symlink_unsafe(const char* target, const char* link_path);
|
||||
/* True when a lexical target is relative and contains no ".." component, so it
|
||||
* can never escape the receive root once created beneath it. */
|
||||
bool file_symlink_target_contained(const char* target);
|
||||
@@ -57,8 +65,9 @@ bool file_symlink_target_contained(const char* target);
|
||||
* returns true when a marker was removed. */
|
||||
bool file_symlink_unmunge(char* target);
|
||||
/* Create a symlink at `path` -> `target`, confined below the authorized root
|
||||
* (O_NOFOLLOW parent walk, symlinkat; the target is never followed). Returns
|
||||
* false when a directory already occupies `path`. */
|
||||
* (O_NOFOLLOW parent walk, symlinkat; the target is never followed). The link
|
||||
* value is copied verbatim (rsync -l); only the placement path is confined.
|
||||
* Returns false when a directory already occupies `path`. */
|
||||
bool file_symlink_at_secure(const char* path, const char* target);
|
||||
/* --keep-dirlinks (-K) receiver process-wide policy: allow an in-root existing
|
||||
* symlink-to-directory to be followed as a directory. */
|
||||
|
||||
+26
-33
@@ -358,14 +358,6 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
log_message(LOG_LEVEL_ERROR, "Special node has no device/FIFO/socket mode");
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
if (is_sock) {
|
||||
/* No standard filesystem call recreates a socket; best-effort unsupported. */
|
||||
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "socket not recreated: %s (unsupported; skipped)",
|
||||
escaped_path ? escaped_path : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
if (is_char || is_blk) {
|
||||
if (!config || !config->preserve_devices)
|
||||
return FILE_SAVE_SKIPPED;
|
||||
@@ -383,7 +375,10 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
free(escaped_path);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
} else if (is_fifo) {
|
||||
} else if (is_fifo || is_sock) {
|
||||
/* FIFOs and unix sockets are recreated by --specials. mknod(S_IFSOCK)
|
||||
works unprivileged on Linux (the node carries no live socket), so unlike
|
||||
a socket bound to a live fd it can be materialized. */
|
||||
if (!config || !config->preserve_specials)
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
@@ -433,9 +428,12 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
} else if (is_blk) {
|
||||
create_mode = S_IFBLK;
|
||||
rdev = makedev((unsigned)file->rdev_major, (unsigned)file->rdev_minor);
|
||||
} else if (is_sock) {
|
||||
create_mode = S_IFSOCK;
|
||||
} else {
|
||||
create_mode = S_IFIFO;
|
||||
}
|
||||
const char* node_kind = (is_char || is_blk) ? "device" : (is_fifo ? "FIFO" : "socket");
|
||||
/* The creation permission bits come from the source only under -p/--perms;
|
||||
* otherwise a safe default (0644, group/other write never granted) keeps an
|
||||
* unprivileged no--p run from materializing a world-writable node. */
|
||||
@@ -450,7 +448,7 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
struct stat st;
|
||||
if (fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) == 0 &&
|
||||
((is_char && S_ISCHR(st.st_mode)) || (is_blk && S_ISBLK(st.st_mode)) ||
|
||||
(is_fifo && S_ISFIFO(st.st_mode)))) {
|
||||
(is_fifo && S_ISFIFO(st.st_mode)) || (is_sock && S_ISSOCK(st.st_mode)))) {
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
free(destination);
|
||||
@@ -458,7 +456,7 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
}
|
||||
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "refusing to replace existing entry with %s: %s (skipped)",
|
||||
is_fifo ? "FIFO" : "device", escaped_path ? escaped_path : "<allocation failed>");
|
||||
node_kind, escaped_path ? escaped_path : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
} else if (errno == EPERM || errno == EACCES) {
|
||||
/* Missing CAP_MKNOD / parent write permission: the environment cannot
|
||||
@@ -467,14 +465,12 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"skipping %s: cannot create %s node (%s)\n"
|
||||
" --devices/--specials node creation needs privilege (CAP_MKNOD)",
|
||||
escaped_path ? escaped_path : "<allocation failed>", is_fifo ? "FIFO" : "device",
|
||||
strerror(errno));
|
||||
escaped_path ? escaped_path : "<allocation failed>", node_kind, strerror(errno));
|
||||
free(escaped_path);
|
||||
} else {
|
||||
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "failed to create %s %s: %s (skipped)",
|
||||
is_fifo ? "FIFO" : "device", escaped_path ? escaped_path : "<allocation failed>",
|
||||
strerror(errno));
|
||||
log_message(LOG_LEVEL_WARNING, "failed to create %s %s: %s (skipped)", node_kind,
|
||||
escaped_path ? escaped_path : "<allocation failed>", strerror(errno));
|
||||
free(escaped_path);
|
||||
}
|
||||
close(parent_fd);
|
||||
@@ -710,26 +706,23 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
char* link_path = path_cat(root_directory, file->path);
|
||||
if (!link_path)
|
||||
return FILE_SAVE_ERROR;
|
||||
/* Restore the real target by stripping the sender's --munge-links marker.
|
||||
Only unmunge when the policy was negotiated: a plain -l run must preserve
|
||||
a source symlink whose target genuinely begins with the marker verbatim. */
|
||||
/* The link value is stored verbatim (rsync -l parity: absolute and
|
||||
".."-bearing targets are preserved; the scanner's --safe-links /
|
||||
--copy-unsafe-links decide which links are sent at all). --munge-links
|
||||
is a RECEIVER-side rewrite: the stored target is prefixed with
|
||||
/rsyncd-munged/, making the link unusable while the referenced directory
|
||||
does not exist -- exactly as rsync's receiver munges. Only the link's
|
||||
own placement path is confined below the receive root. */
|
||||
bool munge = config && config->munge_links;
|
||||
char* target = str_dup(file->symlink_target);
|
||||
bool ok = target != NULL;
|
||||
if (ok && config && config->munge_links)
|
||||
file_symlink_unmunge(target);
|
||||
/* Receiver-side trust boundary (independent of the sender): a target that
|
||||
could escape the receive root (absolute, or relative-with-"..") is never
|
||||
materialized. It is contained (the entry is skipped) rather than failing
|
||||
the whole transfer, so a hostile sender can inject a broken symlink but
|
||||
can never redirect it outside the root. --trust-sender deliberately
|
||||
relaxes this receiver-side re-validation: a trusted sender's escaping
|
||||
symlink target is copied verbatim (rsync -l parity). The low-level
|
||||
leaf/destination confinement in file_symlink_at_secure still ensures the
|
||||
link itself is placed inside the authorized root. */
|
||||
if (ok && !file_get_trust_sender() && !file_symlink_target_contained(target))
|
||||
ok = false;
|
||||
if (ok && munge) {
|
||||
char* munged = file_symlink_munge(target);
|
||||
free(target);
|
||||
target = munged;
|
||||
ok = target != NULL;
|
||||
}
|
||||
if (!ok) {
|
||||
/* Skip the escaping/empty target (contained) rather than abort. */
|
||||
free(target);
|
||||
free(link_path);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
|
||||
Reference in New Issue
Block a user