Merge remote-tracking branch 'origin/fix/bugs-v2' into integration/all-fixes-v2

This commit is contained in:
2026-07-30 19:04:12 +02:00
15 changed files with 411 additions and 92 deletions
+48
View File
@@ -89,6 +89,18 @@ Config* config_create(void) {
config->compare_dest = NULL;
config->copy_dest = NULL;
config->link_dest = NULL;
config->partial_dir = NULL;
config->suffix = NULL;
config->delete_before = false;
config->address = NULL;
config->bind_address = NULL;
config->ipv6 = false;
config->ipv4 = false;
config->daemon = false;
config->daemon_config = NULL;
config->server_mode = false;
config->checksum = false;
config->compress_choice = NULL;
return config;
}
@@ -140,9 +152,18 @@ void config_delete(Config* config) {
free(config->rsh_command);
free(config->rsync_path);
free(config->temp_dir);
free(config->partial_dir);
free(config->suffix);
free(config->compress_choice);
free(config->compare_dest);
free(config->copy_dest);
free(config->link_dest);
free(config->partial_dir);
free(config->suffix);
free(config->address);
free(config->bind_address);
free(config->daemon_config);
free(config->compress_choice);
if (config->filters) {
array_list_delete(config->filters);
}
@@ -225,6 +246,18 @@ bool config_send(int file_descriptor, const Config* config) {
if (!send_str(file_descriptor, config->temp_dir ? config->temp_dir : ""))
return false;
Status status;
if (!send_int(file_descriptor, config->partial))
return false;
if (!send_str(file_descriptor, config->partial_dir ? config->partial_dir : ""))
return false;
if (!send_str(file_descriptor, config->suffix ? config->suffix : ""))
return false;
if (!send_int(file_descriptor, config->delete_before))
return false;
if (!send_int(file_descriptor, config->checksum))
return false;
if (!send_str(file_descriptor, config->compress_choice ? config->compress_choice : ""))
return false;
if (!receive_status(file_descriptor, &status))
return false;
if (status != STATUS_OK) {
@@ -359,6 +392,18 @@ Config* config_receive(int file_descriptor) {
config->compare_dest = NULL;
config->copy_dest = NULL;
config->link_dest = NULL;
config->partial_dir = NULL;
config->suffix = NULL;
config->delete_before = false;
config->address = NULL;
config->bind_address = NULL;
config->ipv6 = false;
config->ipv4 = false;
config->daemon = false;
config->daemon_config = NULL;
config->server_mode = false;
config->checksum = false;
config->compress_choice = NULL;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->backup = tmp;
@@ -434,6 +479,9 @@ error:
free(config->server_host);
free(config->backup_dir);
free(config->temp_dir);
free(config->partial_dir);
free(config->suffix);
free(config->compress_choice);
free(config);
return NULL;
}
+26
View File
@@ -100,6 +100,32 @@ typedef struct Config {
char* compare_dest;
char* copy_dest;
char* link_dest;
// PR #174: Partial transfer resumption
char* partial_dir;
// PR #178: Backup versioning
char* suffix;
// PR #179: Delete policies
bool delete_before;
// PR #181: IPv6 and bind address
char* address;
char* bind_address;
bool ipv6;
bool ipv4;
// PR #182: Daemon/server mode
bool daemon;
char* daemon_config;
bool server_mode;
// PR #183: Checksum comparison
bool checksum;
// PR #184: Compression algorithm negotiation
char* compress_choice;
} Config;
#define PROTOCOL_VERSION "1.3.0"
+102 -21
View File
@@ -129,22 +129,28 @@ bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
}
bool file_save_to_disk(const char* root_directory, File* file, const Config* config) {
(void)config;
bool backup_enabled = config && config->backup;
bool inplace = config && config->inplace;
bool sparse = config && config->preserve_sparse;
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;
if (has_path_traversal(file->path)) {
log_message(LOG_LEVEL_ERROR, "Path traversal detected in file path: %s", file->path);
return false;
}
// Resolve the destination root to its real path, preventing symlink-based escapes.
// If the root does not yet exist, try to create it so realpath can succeed.
char* resolved_root = realpath(root_directory, NULL);
char* resolved_root = NULL;
const char* actual_root = (partial_dir && config && config->partial) ? partial_dir : root_directory;
resolved_root = realpath(actual_root, NULL);
if (resolved_root == NULL) {
if (mkdir_r(root_directory)) {
resolved_root = realpath(root_directory, 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", root_directory);
log_message(LOG_LEVEL_ERROR, "Failed to resolve destination root: %s", actual_root);
return false;
}
@@ -154,7 +160,43 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
return false;
}
// Ensure the target directory exists so the parent can be resolved for path safety.
if (backup_enabled) {
struct stat backup_stat;
if (stat(disk_path, &backup_stat) == 0) {
char* backup_path = NULL;
if (backup_dir) {
char* resolved_backup_dir = realpath(backup_dir, NULL);
if (!resolved_backup_dir) {
mkdir_r(backup_dir);
resolved_backup_dir = realpath(backup_dir, NULL);
}
if (resolved_backup_dir) {
backup_path = path_cat(resolved_backup_dir, file->path);
free(resolved_backup_dir);
}
}
if (!backup_path) {
size_t path_len = strlen(disk_path);
size_t suffix_len = strlen(backup_suffix);
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) {
char* bdir = dirname(backup_dir_path);
mkdir_r(bdir);
free(backup_dir_path);
}
rename(disk_path, backup_path);
free(backup_path);
}
}
}
char* dir_dup = str_dup(disk_path);
if (!dir_dup) {
free(resolved_root);
@@ -162,7 +204,6 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
return false;
}
char* dir_str = dirname(dir_dup);
// Create the directory if needed (no-op if it already exists) so realpath can resolve it.
if (!mkdir_r(dir_str)) {
free(dir_dup);
free(resolved_root);
@@ -178,13 +219,11 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
return false;
}
// Verify that the resolved directory is inside the resolved root.
// Both are canonical absolute paths — this prevents symlink-based escapes.
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,
root_directory);
actual_root);
free(resolved_dir);
free(resolved_root);
free(disk_path);
@@ -193,7 +232,7 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
free(resolved_dir);
free(resolved_root);
bool ok = to_disk(disk_path, file->data->data, file->data->size);
bool ok = to_disk(disk_path, file->data->data, file->data->size, inplace, sparse);
if (ok)
file_restore_metadata(disk_path, file->metadata);
free(disk_path);
@@ -478,7 +517,8 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
return file;
}
bool to_disk(const char* path, const void* data, unsigned long long data_size) {
bool to_disk(const char* path, const void* data, unsigned long long data_size,
bool inplace, bool sparse) {
char* tmp_path = NULL;
char* directory = NULL;
@@ -495,6 +535,39 @@ bool to_disk(const char* path, const void* data, unsigned long long data_size) {
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) {
@@ -510,6 +583,21 @@ bool to_disk(const char* path, const void* data, unsigned long long data_size) {
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);
@@ -534,13 +622,6 @@ done:
bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int compression_level,
bool send_path) {
// sendfile is incompatible with compression (kernel zero-copy).
// If compression is requested, fall back to the regular send path.
// NOTE: This is a safety net only — callers must ensure compression_level == 0
// before calling file_send_sendfile. The fallback to file_send_single_calls
// preserves the send_path contract, but callers should not rely on it for
// correctness (the --sendfile flag is validated to be mutually exclusive with
// -c/--compress at the CLI layer).
if (compression_level > 0)
return file_send_single_calls(file, file_descriptor, use_metadata, compression_level,
send_path);
+2 -1
View File
@@ -34,7 +34,8 @@ bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int
size_t file_content_to_buffer(File* file);
FileMetadata* file_metadata_create(const struct stat* stats);
void file_metadata_destroy(void* metadata);
bool to_disk(const char* path, const void* data, unsigned long long data_size);
bool to_disk(const char* path, const void* data, unsigned long long data_size,
bool inplace, bool sparse);
bool file_save_to_disk(const char* root_directory, File* file, const Config* config);
File* receive_incremental_check(int fd, const Config* config, bool* skipped);
int receive_manifest(int fd, const Config* config, int* next_status);
+6 -2
View File
@@ -14,7 +14,7 @@
#include <threads.h>
PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner,
Queue* queue_loader) {
Queue* queue_loader) {
PipelineContextSender* context = malloc(sizeof(PipelineContextSender));
if (context == NULL)
return NULL;
@@ -24,11 +24,14 @@ PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* que
context->scanner_done = false;
context->loader_done = false;
context->manifest = NULL;
context->progress_bytes = 0;
context->sender_done = false;
if (mtx_init(&context->mutex_scanner, mtx_plain) != thrd_success ||
cnd_init(&context->condition_not_full_scanner) != thrd_success ||
cnd_init(&context->condition_not_empty_scanner) != thrd_success ||
mtx_init(&context->mutex_loader, mtx_plain) != thrd_success ||
cnd_init(&context->condition_not_full_loader) != thrd_success ||
mtx_init(&context->mutex_progress, mtx_plain) != thrd_success ||
cnd_init(&context->condition_not_empty_loader) != thrd_success) {
perror("Error initializing synchronization objects");
free(context);
@@ -50,11 +53,12 @@ void pipeline_context_sender_destroy(PipelineContextSender* context) {
mtx_destroy(&context->mutex_loader);
cnd_destroy(&context->condition_not_full_loader);
cnd_destroy(&context->condition_not_empty_loader);
mtx_destroy(&context->mutex_progress);
free(context);
}
PipelineContextReceiver* pipeline_context_receiver_create(Config* config, Queue* queue,
int file_descriptor, SSL* ssl) {
int file_descriptor, SSL* ssl) {
PipelineContextReceiver* context = malloc(sizeof(PipelineContextReceiver));
if (context == NULL)
return NULL;
+5 -2
View File
@@ -23,6 +23,9 @@ typedef struct {
cnd_t condition_not_empty_loader;
bool loader_done;
ArrayList* manifest;
mtx_t mutex_progress;
unsigned long long progress_bytes;
bool sender_done;
} PipelineContextSender;
typedef struct PipelineContextReceiver {
@@ -37,10 +40,10 @@ typedef struct PipelineContextReceiver {
} PipelineContextReceiver;
PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner,
Queue* queue_loader);
Queue* queue_loader);
void pipeline_context_sender_destroy(PipelineContextSender* context);
PipelineContextReceiver* pipeline_context_receiver_create(Config* config, Queue* queue_receiver,
int file_descriptor, SSL* ssl);
int file_descriptor, SSL* ssl);
void pipeline_context_receiver_destroy(PipelineContextReceiver* context);
int receive_thread(void* pipeline_context);
int write_thread(void* pipeline_context);
+1 -1
View File
@@ -182,7 +182,7 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
return NULL;
}
client->file_descriptor = sv[0];
client->address.sin_family = AF_UNIX;
client->address.ss_family = AF_UNIX;
client->address_length = 0;
client->ssh_child_pid = pid;
client->ssl = NULL;
+50 -22
View File
@@ -3,6 +3,7 @@
#include "protocol.h"
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <openssl/ssl.h>
#include <signal.h>
#include <stdio.h>
@@ -143,6 +144,10 @@ void tcp_set_timeouts(int timeout_sec, int contimeout_sec) {
g_contimeout_sec = contimeout_sec;
}
int tcp_get_contimeout_sec(void) {
return g_contimeout_sec;
}
static void tcp_apply_socket_timeout(int fd) {
struct timeval tv;
tv.tv_sec = g_timeout_sec;
@@ -152,19 +157,12 @@ static void tcp_apply_socket_timeout(int fd) {
}
Client* client_create() {
int file_descriptor = socket(AF_INET, SOCK_STREAM, 0);
if (file_descriptor < 0) {
perror("Could not create Socket!");
return NULL;
}
Client* client = (Client*)malloc(sizeof(Client));
if (client == NULL) {
close(file_descriptor);
return NULL;
}
client->file_descriptor = file_descriptor;
client->address.sin_family = AF_INET;
client->file_descriptor = -1;
memset(&client->address, 0, sizeof(client->address));
client->address_length = sizeof(client->address);
client->ssh_child_pid = -1;
client->ssl = NULL;
@@ -173,23 +171,50 @@ Client* client_create() {
}
bool client_connect(Client* client, char* host, int port) {
client->address.sin_port = htons(port);
client->address.sin_family = AF_INET;
client->address_length = sizeof(client->address);
struct addrinfo hints;
struct addrinfo* result;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
if (inet_pton(AF_INET, host, &client->address.sin_addr) <= 0) {
perror("Could not convert host address!");
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", port);
int err = getaddrinfo(host, port_str, &hints, &result);
if (err != 0 || result == NULL) {
fprintf(stderr, "Could not resolve host: %s (%s)\n", host, gai_strerror(err));
return false;
}
struct timeval ct;
ct.tv_sec = g_contimeout_sec;
ct.tv_usec = 0;
setsockopt(client->file_descriptor, SOL_SOCKET, SO_RCVTIMEO, &ct, sizeof(ct));
setsockopt(client->file_descriptor, SOL_SOCKET, SO_SNDTIMEO, &ct, sizeof(ct));
struct addrinfo* rp;
bool connected = false;
for (rp = result; rp != NULL; rp = rp->ai_next) {
if (client->file_descriptor >= 0)
close(client->file_descriptor);
if (connect(client->file_descriptor, (struct sockaddr*)&client->address, client->address_length) <
0) {
client->file_descriptor = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (client->file_descriptor < 0)
continue;
struct timeval ct;
ct.tv_sec = g_contimeout_sec;
ct.tv_usec = 0;
setsockopt(client->file_descriptor, SOL_SOCKET, SO_RCVTIMEO, &ct, sizeof(ct));
setsockopt(client->file_descriptor, SOL_SOCKET, SO_SNDTIMEO, &ct, sizeof(ct));
memcpy(&client->address, rp->ai_addr, rp->ai_addrlen);
client->address_length = rp->ai_addrlen;
if (connect(client->file_descriptor, (struct sockaddr*)&client->address,
client->address_length) == 0) {
connected = true;
break;
}
}
freeaddrinfo(result);
if (!connected) {
perror("Could not connect to Server!");
return false;
}
@@ -205,7 +230,10 @@ void client_disconnect(Client* client) {
client->ssl = NULL;
io_set_ssl(NULL);
}
close(client->file_descriptor);
if (client->file_descriptor >= 0) {
close(client->file_descriptor);
client->file_descriptor = -1;
}
if (client->ssh_child_pid > 0) {
int status;
waitpid(client->ssh_child_pid, &status, 0);
+2 -1
View File
@@ -15,7 +15,7 @@ typedef struct Server {
} Server;
typedef struct Client {
struct sockaddr_in address;
struct sockaddr_storage address;
unsigned int address_length;
int file_descriptor;
pid_t ssh_child_pid;
@@ -33,5 +33,6 @@ bool client_connect(Client* client, char* host, int port);
void client_disconnect(Client* client);
void client_delete(Client* client);
void tcp_set_timeouts(int timeout_sec, int contimeout_sec);
int tcp_get_contimeout_sec(void);
#endif
+43 -5
View File
@@ -3,6 +3,7 @@
#include "protocol.h"
#include "transport_tcp.h"
#include <arpa/inet.h>
#include <netdb.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <signal.h>
@@ -148,13 +149,50 @@ bool server_listen_tls(Server* server, void (*handler)(int file_descriptor)) {
bool client_connect_tls(Client* client, char* host, int port, const char* cert_path,
const char* key_path, const char* ca_path) {
client->address.sin_port = htons(port);
if (inet_pton(AF_INET, host, &client->address.sin_addr) <= 0) {
perror("Could not convert host address!");
struct addrinfo hints;
struct addrinfo* result;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", port);
int err = getaddrinfo(host, port_str, &hints, &result);
if (err != 0 || result == NULL) {
fprintf(stderr, "Could not resolve host: %s (%s)\n", host, gai_strerror(err));
return false;
}
if (connect(client->file_descriptor, (struct sockaddr*)&client->address, client->address_length) <
0) {
struct addrinfo* rp;
bool connected = false;
for (rp = result; rp != NULL; rp = rp->ai_next) {
if (client->file_descriptor >= 0)
close(client->file_descriptor);
client->file_descriptor = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (client->file_descriptor < 0)
continue;
struct timeval ct;
ct.tv_sec = tcp_get_contimeout_sec();
ct.tv_usec = 0;
setsockopt(client->file_descriptor, SOL_SOCKET, SO_RCVTIMEO, &ct, sizeof(ct));
setsockopt(client->file_descriptor, SOL_SOCKET, SO_SNDTIMEO, &ct, sizeof(ct));
memcpy(&client->address, rp->ai_addr, rp->ai_addrlen);
client->address_length = rp->ai_addrlen;
if (connect(client->file_descriptor, (struct sockaddr*)&client->address,
client->address_length) == 0) {
connected = true;
break;
}
}
freeaddrinfo(result);
if (!connected) {
perror("Could not connect to Server!");
return false;
}