fix: address c-review for -R/--dirs/--mkpath row
B1: destination-root existence/creation mishandled two legitimate forms. file_directory_exists_secure/file_ensure_directory_secure now normalize a trailing-slash destination (so the last component is never an empty leaf) and treat a destination equal to the already-open authorized root as present (no spurious <root>/<basename> nested dir with --mkpath). Confinement and O_NOFOLLOW probing are unchanged. Regression integration tests: existing dest with trailing slash works with and without --mkpath; dest == authorized root works and creates no stray nested dir. W1: chunk serialize/deserialize round-trip unit test for a directory entry mixed with a regular file, with and without metadata; --dirs coverage under -s and -s -m. W2: --list-only and --dry-run now print file_wire_path() so all outputs show the -R transformed relative name, matching -i/--out-format. W3: RSYNC_COMPAT -d/--dirs note: dirs are created immediately under --delay-updates (only regular files are staged). W4: --dirs generator flushes chunks by element count too, so a long --files-from list of empty directories cannot exceed the per-chunk file cap. N1: STATUS_MKDIR added to status_to_string. N2: removed dead TestMkpath._transfer. N3: removed redundant --no-implied-dirs OPTION_TABLE row. N4: integration tests: --dirs --delete keeps the just-created empty dir (and deletes extras); a listed dir colliding with a regular file at the dest fails cleanly.
This commit is contained in:
@@ -429,7 +429,6 @@ static const OptionEntry OPTION_TABLE[] = {
|
||||
{"--old-dirs", NULL, OPT_FLAG, offsetof(Config, dirs)},
|
||||
{"--old-d", NULL, OPT_FLAG, offsetof(Config, dirs)},
|
||||
{"--relative", "-R", OPT_FLAG, offsetof(Config, relative)},
|
||||
{"--no-implied-dirs", NULL, OPT_FLAG, offsetof(Config, no_implied_dirs)},
|
||||
{"--mkpath", NULL, OPT_FLAG, offsetof(Config, mkpath)},
|
||||
{"--delete-during", "--del", OPT_UNSUPPORTED, 0},
|
||||
|
||||
|
||||
@@ -435,7 +435,8 @@ static int send_dry_run_manifest(const Config* config) {
|
||||
while ((chunk = directory_scanner_next(scanner)) != NULL) {
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
if (!config->quiet) {
|
||||
char* escaped_path = output_escape(chunk->items[i]->path, config->eight_bit_output);
|
||||
char* escaped_path =
|
||||
output_escape(file_wire_path(chunk->items[i]), config->eight_bit_output);
|
||||
if (!escaped_path) {
|
||||
chunk_destroy(chunk);
|
||||
directory_scanner_destroy(scanner);
|
||||
@@ -529,7 +530,7 @@ static int send_list_only(const Config* config) {
|
||||
entries = grown;
|
||||
capacity = new_capacity;
|
||||
}
|
||||
char* path = str_dup(f->path);
|
||||
char* path = str_dup(file_wire_path(f));
|
||||
if (!path) {
|
||||
oom = true;
|
||||
break;
|
||||
|
||||
@@ -465,6 +465,11 @@ static int open_next_directory(DirectoryScanner* scanner) {
|
||||
listed regular files are transferred as files; nothing else is scanned, so
|
||||
no descent into a listed directory can happen. */
|
||||
|
||||
/* Directory entries carry no payload, so the dirs generator also bounds every
|
||||
chunk by element count; chunk_deserialize refuses more than this many files
|
||||
per chunk (see MAX_FILES_PER_CHUNK in chunk.c). */
|
||||
#define DIRS_CHUNK_MAX_FILES 65536U
|
||||
|
||||
/* Build the File for the transfer root directory itself (the `-d <dir>` and
|
||||
* "." cases). */
|
||||
static File* dirs_root_dir_file(DirectoryScanner* scanner) {
|
||||
@@ -618,6 +623,11 @@ static Chunk* directory_scanner_next_dirs(DirectoryScanner* scanner) {
|
||||
return NULL;
|
||||
}
|
||||
scanner->dirs_batch_size += file->data ? file->data->size : 0;
|
||||
/* Empty directory entries carry no bytes, so a large --dirs --files-from
|
||||
list must also be bounded by element count (the chunk deserializer caps
|
||||
the number of files per chunk). */
|
||||
if (scanner->dirs_batch->size >= (int)DIRS_CHUNK_MAX_FILES)
|
||||
return dirs_flush_batch(scanner);
|
||||
}
|
||||
return dirs_flush_batch(scanner);
|
||||
}
|
||||
|
||||
+47
-3
@@ -308,9 +308,41 @@ int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs)
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* Normalized copy of a directory path: leading '/' kept, trailing '/' removed
|
||||
* ("/" and "//" both collapse to "/"). A trailing slash otherwise makes the
|
||||
* last path component empty, so probing that empty leaf below its parent
|
||||
* always fails. */
|
||||
static char* normalize_directory_path(const char* path) {
|
||||
if (!path)
|
||||
return NULL;
|
||||
size_t len = strlen(path);
|
||||
while (len > 1 && path[len - 1] == '/')
|
||||
len--;
|
||||
char* norm = malloc(len + 1);
|
||||
if (!norm)
|
||||
return NULL;
|
||||
memcpy(norm, path, len);
|
||||
norm[len] = '\0';
|
||||
return norm;
|
||||
}
|
||||
|
||||
bool file_ensure_directory_secure(const char* path) {
|
||||
if (!path)
|
||||
return false;
|
||||
char* norm = normalize_directory_path(path);
|
||||
if (!norm)
|
||||
return false;
|
||||
/* The authorized root is already an open directory, and the filesystem root
|
||||
is always present: there is no final component left to create for them. */
|
||||
bool root_is_open =
|
||||
authorized_root_fd >= 0 && authorized_root_path && strcmp(norm, authorized_root_path) == 0;
|
||||
if (root_is_open || strcmp(norm, "/") == 0) {
|
||||
free(norm);
|
||||
return true;
|
||||
}
|
||||
char* leaf = NULL;
|
||||
int parent_fd = file_open_secure_parent(path, &leaf, true);
|
||||
int parent_fd = file_open_secure_parent(norm, &leaf, true);
|
||||
free(norm);
|
||||
if (parent_fd < 0)
|
||||
return false;
|
||||
|
||||
@@ -329,12 +361,24 @@ bool file_ensure_directory_secure(const char* path) {
|
||||
|
||||
/* True when `path` resolves to an existing directory below the authorized root
|
||||
* (never creating anything). Used by the server to decide whether a client's
|
||||
* destination root already exists. */
|
||||
* destination root already exists. A trailing slash on `path` and a destination
|
||||
* equal to the authorized root itself are normalized/handled here so both
|
||||
* previously-working destination forms keep working. */
|
||||
bool file_directory_exists_secure(const char* path) {
|
||||
if (!path)
|
||||
return false;
|
||||
char* norm = normalize_directory_path(path);
|
||||
if (!norm)
|
||||
return false;
|
||||
bool root_is_open =
|
||||
authorized_root_fd >= 0 && authorized_root_path && strcmp(norm, authorized_root_path) == 0;
|
||||
if (root_is_open || strcmp(norm, "/") == 0) {
|
||||
free(norm);
|
||||
return true;
|
||||
}
|
||||
char* leaf = NULL;
|
||||
int parent_fd = file_open_secure_parent(path, &leaf, false);
|
||||
int parent_fd = file_open_secure_parent(norm, &leaf, false);
|
||||
free(norm);
|
||||
if (parent_fd < 0)
|
||||
return false;
|
||||
int dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
|
||||
@@ -385,6 +385,8 @@ static const char* status_to_string(Status status) {
|
||||
return "ABORT";
|
||||
case STATUS_CHECK_BATCH:
|
||||
return "CHECK_BATCH";
|
||||
case STATUS_MKDIR:
|
||||
return "MKDIR";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user