fix: close remaining PR review gaps
CI / lint (pull_request) Failing after 31s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped

This commit is contained in:
2026-08-16 09:35:26 +02:00
parent 78876b110e
commit d102622e28
27 changed files with 609 additions and 538 deletions
+157 -245
View File
@@ -24,7 +24,7 @@
#include "utils.h"
#define MAX_SERVER_DELETE_COUNT 100000U
#define MAX_FILE_DATA_SIZE (64ULL * 1024 * 1024)
#define MAX_FILE_DATA_SIZE MAX_RECEIVE_FILE_SIZE
bool file_checksum(File* file, uint64_t* checksum) {
if (!file || !checksum || !file->data)
@@ -48,7 +48,7 @@ File* file_create(const char* path) {
return NULL;
}
int path_len = strlen(path);
size_t path_len = strlen(path);
file->path = (char*)malloc(path_len + 1);
if (file->path == NULL) {
free(file);
@@ -104,7 +104,7 @@ void file_metadata_destroy(void* metadata) {
}
bool file_load_data(File* file) {
if (file == NULL)
if (file == NULL || !file->data)
return false;
if (file->data->data == NULL) {
if (file->data->size == 0)
@@ -128,7 +128,7 @@ bool file_load_data(File* file) {
bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
int compression_level, bool send_path) {
if (!file || !file->path || !file->data)
if (!file || !file->path || !file->data || (file->data->size != 0 && !file->data->data))
return false;
const Data* data_to_send = file->data;
Data* compressed_data = NULL;
@@ -159,6 +159,7 @@ bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
static bool to_disk_secure(const char* path, const void* data, unsigned long long data_size,
bool inplace, bool sparse, const FileMetadata* metadata);
static int open_secure_parent(const char* path, char** leaf_out, bool create_dirs);
static bool ensure_directory_secure(const char* path);
bool file_path_exists_secure(const char* path) {
struct stat st;
@@ -184,15 +185,23 @@ static bool rename_secure(const char* old_path, const char* new_path);
static int authorized_root_fd = -1;
static char* authorized_root_path;
void file_set_authorized_root(int fd, const char* canonical_path) {
authorized_root_fd = fd;
free(authorized_root_path);
authorized_root_path = canonical_path ? str_dup(canonical_path) : NULL;
static bool path_is_within_root(const char* root, const char* path) {
size_t root_len = strlen(root);
return strncmp(root, path, root_len) == 0 && (path[root_len] == '\0' || path[root_len] == '/');
}
static bool path_is_within_root(const char* root, const char* path) {
size_t n = strlen(root);
return strncmp(root, path, n) == 0 && (path[n] == '\0' || path[n] == '/');
bool file_set_authorized_root(int fd, const char* canonical_path) {
char* path_copy = canonical_path ? str_dup(canonical_path) : NULL;
if (canonical_path && !path_copy) {
authorized_root_fd = -1;
free(authorized_root_path);
authorized_root_path = NULL;
return false;
}
authorized_root_fd = fd;
free(authorized_root_path);
authorized_root_path = path_copy;
return true;
}
bool file_save_to_disk(const char* root_directory, const File* file, const Config* config) {
@@ -202,9 +211,11 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi
const char* backup_suffix = (config && config->suffix) ? config->suffix : "~";
const char* backup_dir = (config && config->backup_dir) ? config->backup_dir : NULL;
const char* partial_dir = (config && config->partial_dir) ? config->partial_dir : NULL;
char *confined_backup = NULL, *confined_partial = NULL;
char *confined_backup = NULL, *confined_partial = NULL, *disk_path = NULL;
char *backup_path = NULL, *parent_copy = NULL;
if (!file || !file->path || !file->data || has_path_traversal(file->path) ||
if (!file || !file->path || !file->data || (file->data->size != 0 && !file->data->data) ||
has_path_traversal(file->path) ||
(backup_enabled &&
(!backup_suffix || backup_suffix[0] == '\0' || strchr(backup_suffix, '/') != NULL ||
strcmp(backup_suffix, ".") == 0 || strcmp(backup_suffix, "..") == 0))) {
@@ -224,45 +235,20 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi
return false;
}
char* resolved_root = NULL;
const char* actual_root =
(partial_dir && config && config->partial) ? confined_partial : root_directory;
resolved_root = realpath(actual_root, NULL);
if (resolved_root == NULL) {
if (mkdir_r(actual_root)) {
resolved_root = realpath(actual_root, NULL);
}
}
if (resolved_root == NULL) {
log_message(LOG_LEVEL_ERROR, "Failed to resolve destination root: %s", actual_root);
free(confined_backup);
free(confined_partial);
return false;
}
char* resolved_base = realpath(root_directory, NULL);
if (resolved_base == NULL || !path_is_within_root(resolved_base, resolved_root)) {
free(resolved_base);
free(confined_backup);
free(confined_partial);
free(resolved_root);
return false;
}
free(resolved_base);
char* disk_path = path_cat(resolved_root, file->path);
disk_path = path_cat(actual_root, file->path);
if (disk_path == NULL) {
free(confined_backup);
free(confined_partial);
free(resolved_root);
return false;
}
/* --update is receiver-side policy: never replace a newer destination. */
if (config && config->update) {
struct stat destination_stat;
if (stat(disk_path, &destination_stat) == 0 && file->metadata &&
if (file_stat_secure(disk_path, &destination_stat) && file->metadata &&
destination_stat.st_mtime > file->metadata->mtime_sec) {
free(resolved_root);
free(confined_backup);
free(confined_partial);
free(disk_path);
@@ -272,99 +258,50 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi
if (backup_enabled) {
struct stat backup_stat;
if (stat(disk_path, &backup_stat) == 0) {
char* backup_path = NULL;
if (file_stat_secure(disk_path, &backup_stat)) {
if (backup_dir) {
char* resolved_backup_dir = realpath(confined_backup, NULL);
if (!resolved_backup_dir) {
mkdir_r(confined_backup);
resolved_backup_dir = realpath(confined_backup, NULL);
}
if (resolved_backup_dir) {
char* backup_base = realpath(root_directory, NULL);
if (backup_base && path_is_within_root(backup_base, resolved_backup_dir))
backup_path = path_cat(resolved_backup_dir, file->path);
free(backup_base);
free(resolved_backup_dir);
}
}
if (!backup_path) {
backup_path = path_cat(confined_backup, file->path);
} else {
size_t path_len = strlen(disk_path);
size_t suffix_len = strlen(backup_suffix);
if (path_len > SIZE_MAX - suffix_len - 1)
goto fail;
backup_path = malloc(path_len + suffix_len + 1);
if (backup_path) {
memcpy(backup_path, disk_path, path_len);
memcpy(backup_path + path_len, backup_suffix, suffix_len + 1);
}
}
if (backup_path) {
char* backup_dir_path = str_dup(backup_path);
if (backup_dir_path) {
const char* bdir = dirname(backup_dir_path);
mkdir_r(bdir);
free(backup_dir_path);
}
if (!rename_secure(disk_path, backup_path)) {
free(backup_path);
free(resolved_root);
free(confined_backup);
free(confined_partial);
free(disk_path);
return false;
}
free(backup_path);
}
if (!backup_path)
goto fail;
parent_copy = str_dup(backup_path);
if (!parent_copy || !ensure_directory_secure(dirname(parent_copy)))
goto fail;
free(parent_copy);
parent_copy = NULL;
if (!rename_secure(disk_path, backup_path))
goto fail;
free(backup_path);
backup_path = NULL;
}
}
char* dir_dup = str_dup(disk_path);
if (!dir_dup) {
free(confined_backup);
free(confined_partial);
free(resolved_root);
free(disk_path);
return false;
}
char* dir_str = dirname(dir_dup);
if (!mkdir_r(dir_str)) {
free(dir_dup);
free(confined_backup);
free(confined_partial);
free(resolved_root);
free(disk_path);
return false;
}
char* resolved_dir = realpath(dir_str, NULL);
free(dir_dup);
if (resolved_dir == NULL) {
log_message(LOG_LEVEL_ERROR, "Failed to resolve directory for: %s", disk_path);
free(confined_backup);
free(confined_partial);
free(resolved_root);
free(disk_path);
return false;
}
size_t root_len = strlen(resolved_root);
if (strncmp(resolved_dir, resolved_root, root_len) != 0 ||
(resolved_dir[root_len] != '\0' && resolved_dir[root_len] != '/')) {
log_message(LOG_LEVEL_ERROR, "Path escape detected: %s is outside %s", disk_path, actual_root);
free(resolved_dir);
free(confined_backup);
free(confined_partial);
free(resolved_root);
free(disk_path);
return false;
}
free(resolved_dir);
free(resolved_root);
bool ok = to_disk_secure(disk_path, file->data->data, file->data->size, inplace, sparse,
file->metadata);
free(parent_copy);
free(backup_path);
free(confined_backup);
free(confined_partial);
free(disk_path);
return ok;
fail:
free(parent_copy);
free(backup_path);
free(confined_backup);
free(confined_partial);
free(disk_path);
return false;
}
static File* receive_delta_file(int fd, const Config* config, const char* check_path,
@@ -402,7 +339,7 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
}
if (resp == STATUS_DELTA_DATA) {
Data* delta_data = receive_data(fd);
Data* delta_data = receive_data_limited(fd, MAX_RECEIVE_FILE_SIZE);
if (!delta_data) {
delta_signature_destroy(sig);
free(old_data);
@@ -412,7 +349,7 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
Data* raw_delta = delta_data;
if (config->use_compression) {
raw_delta = data_decompress(delta_data);
raw_delta = data_decompress_limited(delta_data, MAX_RECEIVE_FILE_SIZE);
data_destroy(delta_data);
if (!raw_delta) {
free(old_data);
@@ -431,8 +368,15 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
return NULL;
}
void* new_data = delta_apply(old_data, old_size, delta, config->delta_block_size);
uint64_t new_size = delta->new_file_size;
if (new_size > MAX_RECEIVE_FILE_SIZE || new_size > SIZE_MAX) {
delta_destroy(delta);
free(old_data);
delta_signature_destroy(sig);
send_status(fd, STATUS_ERROR);
return NULL;
}
void* new_data = delta_apply(old_data, old_size, delta, config->delta_block_size);
delta_destroy(delta);
if (!new_data) {
@@ -500,7 +444,7 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
}
}
Data* file_data = receive_data(fd);
Data* file_data = receive_data_limited(fd, MAX_RECEIVE_FILE_SIZE);
if (file_data == NULL) {
file_destroy(file);
send_status(fd, STATUS_ERROR);
@@ -508,7 +452,7 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
}
if (config->use_compression) {
Data* uncompressed = data_decompress(file_data);
Data* uncompressed = data_decompress_limited(file_data, MAX_RECEIVE_FILE_SIZE);
data_destroy(file_data);
if (uncompressed == NULL) {
file_destroy(file);
@@ -536,6 +480,10 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
}
File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
if (!config || !skipped) {
send_status(fd, STATUS_ERROR);
return NULL;
}
*skipped = false;
char* check_path = receive_str(fd);
if (check_path == NULL) {
@@ -558,6 +506,12 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
return NULL;
}
if (check_size > MAX_RECEIVE_FILE_SIZE) {
free(check_path);
send_status(fd, STATUS_ERROR);
return NULL;
}
if (has_path_traversal(check_path)) {
log_message(LOG_LEVEL_ERROR, "Path traversal detected: %s", check_path);
free(check_path);
@@ -566,22 +520,25 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
}
char* full_path = path_cat(config->receive_root_directory, check_path);
if (!full_path) {
free(check_path);
send_status(fd, STATUS_ERROR);
return NULL;
}
struct stat st;
bool has_old_file = false;
int old_fd = -1;
if (full_path) {
char* leaf = NULL;
int parent_fd = open_secure_parent(full_path, &leaf, false);
if (parent_fd >= 0) {
old_fd = openat(parent_fd, leaf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
free(leaf);
close(parent_fd);
has_old_file = old_fd >= 0 && fstat(old_fd, &st) == 0 && S_ISREG(st.st_mode);
}
char* leaf = NULL;
int parent_fd = open_secure_parent(full_path, &leaf, false);
if (parent_fd >= 0) {
old_fd = openat(parent_fd, leaf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
free(leaf);
close(parent_fd);
has_old_file = old_fd >= 0 && fstat(old_fd, &st) == 0 && S_ISREG(st.st_mode);
}
unsigned long long old_size = has_old_file ? (unsigned long long)st.st_size : 0;
void* old_data = NULL;
if (has_old_file && old_size > 0 && old_size <= DELTA_MAX_FILE_SIZE && old_size <= SIZE_MAX) {
if (has_old_file && old_size > 0 && old_size <= MAX_RECEIVE_FILE_SIZE && old_size <= SIZE_MAX) {
old_data = malloc((size_t)old_size);
if (old_data) {
size_t got = 0;
@@ -625,7 +582,7 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
return NULL;
}
bool try_delta = config->use_delta && has_old_file &&
bool try_delta = config->use_delta && has_old_file && old_data != NULL &&
delta_should_attempt(old_size, check_size, config->delta_max_file_size);
if (try_delta) {
@@ -667,7 +624,7 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
}
}
Data* file_data = receive_data(fd);
Data* file_data = receive_data_limited(fd, MAX_RECEIVE_FILE_SIZE);
if (file_data == NULL) {
file_destroy(file);
send_status(fd, STATUS_ERROR);
@@ -675,7 +632,7 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
}
if (config->use_compression) {
Data* uncompressed = data_decompress(file_data);
Data* uncompressed = data_decompress_limited(file_data, MAX_RECEIVE_FILE_SIZE);
data_destroy(file_data);
if (uncompressed == NULL) {
file_destroy(file);
@@ -708,9 +665,19 @@ static int open_secure_parent(const char* path, char** leaf_out, bool create_dir
return -1;
}
int fd;
if (authorized_root_fd >= 0 && authorized_root_path && path[0] == '/' &&
path_is_within_root(authorized_root_path, path)) {
if (authorized_root_fd >= 0) {
if (!authorized_root_path || path[0] != '/' ||
!path_is_within_root(authorized_root_path, path)) {
free(copy);
free(leaf);
return -1;
}
fd = dup(authorized_root_fd);
if (fd < 0) {
free(copy);
free(leaf);
return -1;
}
size_t root_len = strlen(authorized_root_path);
char* relative = str_dup(path + root_len);
if (!relative) {
@@ -734,10 +701,18 @@ static int open_secure_parent(const char* path, char** leaf_out, bool create_dir
char* save = NULL;
char* component = strtok_r(parent, "/", &save);
while (component) {
if (strcmp(component, ".") != 0 && strcmp(component, "..") != 0) {
if (strcmp(component, "..") == 0) {
close(fd);
free(copy);
free(leaf);
return -1;
}
if (strcmp(component, ".") != 0) {
int next = openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (create_dirs && next < 0 && errno == ENOENT && mkdirat(fd, component, 0755) == 0)
next = openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (create_dirs && next < 0 && errno == ENOENT) {
if (mkdirat(fd, component, 0755) == 0 || errno == EEXIST)
next = openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
}
if (next < 0) {
close(fd);
free(copy);
@@ -754,9 +729,28 @@ static int open_secure_parent(const char* path, char** leaf_out, bool create_dir
return fd;
}
static bool ensure_directory_secure(const char* path) {
char* leaf = NULL;
int parent_fd = open_secure_parent(path, &leaf, true);
if (parent_fd < 0)
return false;
int dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (dir_fd < 0 && errno == ENOENT) {
if (mkdirat(parent_fd, leaf, 0755) == 0 || errno == EEXIST)
dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
}
bool ok = dir_fd >= 0;
if (dir_fd >= 0)
close(dir_fd);
close(parent_fd);
free(leaf);
return ok;
}
static bool rename_secure(const char* old_path, const char* new_path) {
char *old_leaf = NULL, *new_leaf = NULL;
int old_parent = open_secure_parent(old_path, &old_leaf, true);
int old_parent = open_secure_parent(old_path, &old_leaf, false);
int new_parent = open_secure_parent(new_path, &new_leaf, true);
bool ok = old_parent >= 0 && new_parent >= 0 &&
renameat(old_parent, old_leaf, new_parent, new_leaf) == 0;
@@ -833,106 +827,6 @@ bool to_disk(const char* path, const void* data, unsigned long long data_size, b
if (!path || (!data && data_size != 0) || has_path_traversal(path))
return false;
return to_disk_secure(path, data, data_size, inplace, sparse, NULL);
/* Kept below only as historical context; all writes use descriptor-relative operations. */
char* tmp_path = NULL;
char* directory = NULL;
char* path_dup = str_dup(path);
if (!path_dup)
return false;
const char* dir_result = dirname(path_dup);
directory = str_dup(dir_result);
free(path_dup);
if (!directory)
return false;
bool ok = true;
if (!mkdir_r(directory))
goto done;
if (inplace) {
FILE* file_pointer = fopen(path, "wb");
if (file_pointer == NULL) {
perror("Could not open file for inplace write");
ok = false;
goto done;
}
if (sparse && data_size > 0) {
if (fseek(file_pointer, data_size - 1, SEEK_SET) != 0) {
perror("Failed to seek for sparse file");
fclose(file_pointer);
ok = false;
goto done;
}
if (fwrite("", 1, 1, file_pointer) != 1) {
perror("Failed to write sparse file");
fclose(file_pointer);
ok = false;
goto done;
}
rewind(file_pointer);
}
if (data_size > 0 && fwrite(data, 1, data_size, file_pointer) != data_size) {
perror("Failed to write all data to file");
fclose(file_pointer);
ok = false;
goto done;
}
fclose(file_pointer);
free(directory);
return true;
}
size_t path_len = strlen(path);
tmp_path = malloc(path_len + 5);
if (!tmp_path) {
ok = false;
goto done;
}
memcpy(tmp_path, path, path_len);
memcpy(tmp_path + path_len, ".tmp", 5);
FILE* file_pointer = fopen(tmp_path, "wb");
if (file_pointer == NULL) {
perror("Could not open temporary file");
ok = false;
goto done;
}
if (sparse && data_size > 0) {
if (fseek(file_pointer, data_size - 1, SEEK_SET) != 0) {
perror("Failed to seek for sparse file");
fclose(file_pointer);
ok = false;
goto done;
}
if (fwrite("", 1, 1, file_pointer) != 1) {
perror("Failed to write sparse file");
fclose(file_pointer);
ok = false;
goto done;
}
rewind(file_pointer);
}
if (fwrite(data, 1, data_size, file_pointer) != data_size) {
perror("Failed to write all data to temporary file");
fclose(file_pointer);
unlink(tmp_path);
ok = false;
goto done;
}
fclose(file_pointer);
if (rename(tmp_path, path) != 0) {
perror("Failed to atomically rename temporary file");
unlink(tmp_path);
ok = false;
goto done;
}
done:
free(tmp_path);
free(directory);
return ok;
}
bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int compression_level,
@@ -1044,13 +938,13 @@ File* file_receive(const Config* config, int file_descriptor) {
return NULL;
}
}
Data* file_data = receive_data(file_descriptor);
Data* file_data = receive_data_limited(file_descriptor, MAX_RECEIVE_FILE_SIZE);
if (file_data == NULL) {
file_destroy(file);
return NULL;
}
if (config->use_compression) {
Data* file_data_uncompressed = data_decompress(file_data);
if (config->use_compression && !compression_should_skip(file->path)) {
Data* file_data_uncompressed = data_decompress_limited(file_data, MAX_RECEIVE_FILE_SIZE);
data_destroy(file_data);
if (file_data_uncompressed == NULL) {
file_destroy(file);
@@ -1069,6 +963,8 @@ File* file_receive(const Config* config, int file_descriptor) {
}
size_t file_content_to_buffer(File* file) {
if (!file || !file->path || !file->data || (!file->data->data && file->data->size != 0))
return 0;
FILE* file_pointer = fopen(file->path, "rb");
if (file_pointer == NULL) {
perror("Could not open the file!");
@@ -1085,16 +981,26 @@ size_t file_content_to_buffer(File* file) {
}
int receive_manifest(int fd, const Config* config, int* next_status) {
if (!config) {
send_status(fd, STATUS_ERROR);
return -1;
}
int received_status = STATUS_ERROR;
int* status_out = next_status ? next_status : &received_status;
int count;
if (!receive_int(fd, &count))
if (!receive_int(fd, &count)) {
send_status(fd, STATUS_ERROR);
return -1;
if (count < 0 || count > MAX_MANIFEST_ENTRIES)
}
if (count < 0 || count > MAX_MANIFEST_ENTRIES) {
send_status(fd, STATUS_ERROR);
return -1;
}
ArrayList* manifest = array_list_create(free);
if (!manifest)
if (!manifest) {
send_status(fd, STATUS_ERROR);
return -1;
}
size_t manifest_bytes = 0;
for (int i = 0; i < count; i++) {
char* s = receive_str(fd);
@@ -1104,22 +1010,28 @@ int receive_manifest(int fd, const Config* config, int* next_status) {
(manifest_bytes += entry_size) > MAX_MANIFEST_BYTES || !array_list_add(manifest, s)) {
free(s);
array_list_delete(manifest);
send_status(fd, STATUS_ERROR);
return -1;
}
}
if (!receive_status(fd, status_out)) {
array_list_delete(manifest);
send_status(fd, STATUS_ERROR);
return -1;
}
/* Deletion is a commit operation: never perform it until the sender has
completed the manifest frame successfully. */
if (*status_out != STATUS_FINISHED || !config->use_delete) {
array_list_delete(manifest);
if (*status_out != STATUS_FINISHED)
send_status(fd, STATUS_ERROR);
return *status_out == STATUS_FINISHED ? 0 : -1;
}
fprintf(stderr, "Deleting files not in manifest...\n");
bool deletion_ok =
delete_extras_limited(config->receive_root_directory, manifest, MAX_SERVER_DELETE_COUNT);
array_list_delete(manifest);
if (!deletion_ok)
send_status(fd, STATUS_ERROR);
return deletion_ok ? 0 : -1;
}