diff --git a/README.md b/README.md index 3aaf949..fdbd9f3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# FastSync +#FastSync FastSync is a high-performance file synchronization tool designed to become a drop-in replacement for common `rsync` workflows. It keeps the familiar @@ -79,10 +79,166 @@ partial, alternate, and planned behavior. ### Build -Requirements: C11 compiler, CMake 3.22 or newer, xxHash, zstd, OpenSSL, -pthreads, and an SSH client for SSH transport. The first CMake configure fetches -xxHash from GitHub, so network access is required unless the dependency is -already cached. +### Client + +| Argument | Description | +|----------|-------------| +| Positional | ` ` — automatic SSH detection if dest contains `:` | +| `-c [level]` | Compression with optional level (1–22, default 5) | +| `-z [level]` | Alias for `-c` | +| `-a, --archive` | Archive mode: enables `-c -m -M` (no `-s`) | +| `-m` | Multithreading mode | +| `-s` | Chunk serialization (batch all files per chunk) | +| `-f, --sendfile` | Sendfile zero-copy. Incompatible with `-c` / `-s`. TCP only. | +| `-M, --preserve` | Preserve supported file metadata (mode and mtime; ownership and atime are unsupported) | +| `-n, --dry-run` | Scan and print what would be transferred | +| `-p ` | SSH port (default: 22) | +| `-v, --verbose` | Enable debug logging | +| `--progress` | Show real-time transfer speed | +| `--delete` | Delete files on receiver not present in source | +| `--exclude ` | Exclude files matching glob pattern (repeatable) | +| `--exclude-from ` | Read exclude patterns from a file (one per line) | +| `--include ` | Only transfer files matching glob pattern (repeatable, whitelist) | +| `--max-size ` | Skip files larger than n bytes | +| `--min-size ` | Skip files smaller than n bytes | +| `--incremental` | Skip files unchanged since last transfer (size + mtime). Auto-enables `--preserve`. Incompatible with `-s`. | +| `--bwlimit ` | Bandwidth limit in kilobytes per second | +| `--chunk-size ` | Chunk size in bytes (default: 10485760) | +| `--timeout ` | I/O timeout in seconds (default: 30) | +| `--contimeout ` | Connection timeout in seconds (default: 10) | +| `--backup` | Backup existing destination files before overwriting | +| `--backup-dir ` | Target directory for backups (requires `--backup`) | +| `--stats` | Print transfer statistics at end (bytes, files, timing) | +| `--max-depth ` | Maximum directory depth to recurse (0 = unlimited, default: 0) | +| `--log-file ` | Write log messages to file instead of stderr | +| `--source-dir ` | Source directory (overrides `FASTSYNC_SOURCE_DIR`) | +| `--dest-dir ` | Server destination directory (overrides `FASTSYNC_DEST_DIR`) | +| `--save-to-disk` | Write received files to disk | +| `--server-host ` | Server IP address (default: `127.0.0.1`) | +| `--server-port ` | Server port (default: `8080`) | +| `--tls` | Enable TLS encryption | +| `--cert ` | TLS certificate file (PEM) | +| `--key ` | TLS private key file (PEM) | +| `--ca ` | TLS CA certificate file for verification (PEM) | +| `--client-cn ` | Required TLS client certificate common name | + +### Server + +| Argument | Description | +|----------|-------------| +| `--stdio` | Run in stdio mode (for SSH transport; single connection then exits) | +| `-p ` | TCP listen port (default: 8080, range: 1–65535) | +| `--tls` | Enable TLS encryption | +| `--cert ` | TLS certificate file (PEM) | +| `--key ` | TLS private key file (PEM) | +| `--ca ` | TLS CA certificate file for verification (PEM) | +| `--destination-root ` | Authorized destination root (default: `.`) | +| `--allow-delete` | Permit manifest deletion | +| `--allow-unauthenticated` | Permit plaintext TCP clients | +| `-v, --verbose` | Enable debug logging | +| `--help` | Show help | + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `FASTSYNC_SOURCE_DIR` | — | Source directory fallback | +| `FASTSYNC_DEST_DIR` | — | Destination directory fallback | +| `FASTSYNC_SAVE_TO_DISK` | `false` | Disk persistence fallback | +| `FASTSYNC_SSH_PORT` | `22` | Default SSH port | +| `FASTSYNC_SERVER_HOST` | `127.0.0.1` | Default server host | +| `FASTSYNC_SERVER_PORT` | `8080` | Default server port | +| `FASTSYNC_TLS_CERT` | — | Default TLS certificate path | +| `FASTSYNC_TLS_KEY` | — | Default TLS private key path | +| `FASTSYNC_TLS_CA` | — | Default TLS CA certificate path | + +## Implementation Details + +### Data Structures +1. **Chunk** — collection of files (~10 MB total by default) +2. **File** — path, content (`Data`), optional `FileMetadata` pointer +3. **FileMetadata** — `mode`, `uid`, `gid`, `mtime_sec`, `mtime_nsec`; +uid / gid are advisory wire fields and are never applied by the receiver; +atime is unsupported +4. **Config** — runtime parameters (transported over wire, TLS settings excluded). Includes `timeout`, `contimeout`, `quiet`, `backup`, `backup_dir`, `stats`, `max_depth`, `log_file`, `queue_size`. +5. **Queue** — thread-safe bounded queue with condition variables +6. **DirectoryScanner** — recursive BFS traversal with exclude and include pattern support, max-depth enforcement + +### Key Algorithms +1. **File scanning** — BFS directory traversal; +entries matched against exclude and include patterns, + max - depth enforced 2. * *Chunking ** — files accumulated until `chunk_size` threshold, + then flushed 3. * + *Compression ** — streaming zstd + via `ZSTD_compressStream2` / `ZSTD_decompressStream` 4. * + *Network protocol ** — status - + code - driven exchange with metadata packing, + keep - alive, + and abort support 5. * *Incremental check ** — client sends `STATUS_CHECK` + path + size + + mtime and, + with `--checksum`, XXH64 content checksum; server compares against destination. Can be batched via `STATUS_CHECK_BATCH` for reduced round-trips. +6. **Bandwidth limiting** — token-bucket algorithm with `nanosleep` throttling on 64 KB write chunks +7. **Metadata restoration** — `chmod()`, `chown()`, `utimensat()` on the receiving side +8. **`--delete`** — sender tracks all sent paths; +receiver walks destination tree and removes unlisted files / directories 9. * + *SSH transport * + * — `socketpair()` + `fork()` + `execvp("ssh", + ...)` with `ControlMaster` and port support + 10. * + *TLS transport ** — OpenSSL `SSL_CTX` with TLS + 1.2 minimum, + mutual CA verification, + transparent `SSL_read`/`SSL_write` via `io_set_ssl()` 11. * + *Path traversal protection ** — `has_path_traversal()` rejects any file path + containing `..` components, + preventing directory escape attacks 12. * + *Connection limiting ** — server tracks active connections and rejects + new ones beyond `max_connections` (default 100)13. * + *Keep + - alive ** — idle connections receive periodic `STATUS_KEEPALIVE` to detect half + - open TCP connections 14. * *Abort handling ** — `SIGINT` sets an abort flag; the next protocol operation sends `STATUS_ABORT` for clean server cleanup +15. **Atomic writes** — files are written to a `.tmp` suffix then atomically renamed via `rename()`, preventing partial files +16. **Backup** — before overwriting, existing files are moved to `--backup-dir` (or same directory with `~` suffix) preserving the original + +## Security Features + +### Path Traversal Protection +All received file paths are validated by `has_path_traversal()` before any disk operation. Any path containing `..` components is rejected with `STATUS_ERROR`, preventing directory escape attacks. + +### TLS Certificate Verification +TLS requires `--ca` and performs mutual TLS verification (`SSL_VERIFY_PEER` with depth 4). Connections without certificate verification are rejected. + +### Connection Limits +The server enforces a maximum of 100 concurrent connections (configurable via `max_connections` in `Server`). When the limit is reached, new connections are immediately rejected and closed. + +### Abort Handling +If the client receives `SIGINT` (Ctrl+C) during a transfer, it sends `STATUS_ABORT` to the server. The server then cleans up temporary files and exits the child process, preventing incomplete files from remaining on disk. + +### Atomic Writes +Received files are written to a temporary path (suffixed with `.tmp`) and then atomically renamed to the final filename via `rename()`. This prevents partial or corrupted files from appearing at the destination if the transfer is interrupted. + +## Build Requirements + +- C11 compiler +- CMake >= 3.22 +- zstd library +- OpenSSL (development headers and libraries) +- pthreads +- SSH client (for SSH transport mode only) + +### Installing Dependencies + +**Ubuntu/Debian:** +```bash +sudo apt install cmake build-essential libzstd-dev libssl-dev openssh-client +``` + +**Nix:** +```bash +nix-shell # provides zstd, openssl, cmake, gcc +``` + +## Building ```bash cmake -B build -S . @@ -125,8 +281,10 @@ Then run the client: --save-to-disk ``` -### TLS transfer +Plain TCP requires the explicit `--allow-unauthenticated` server option. Use TLS for +authenticated network connections. +### TLS transfer ```bash ./build/server --destination-root /path/to --tls --cert server.pem --key server-key.pem -p 8443 ./build/client --tls --cert client.pem --key client-key.pem --ca ca.pem \ @@ -141,32 +299,32 @@ These examples show the intended rsync-style workflow. Options marked as FastSync-native are optional performance or transport extensions. ```bash -# Basic synchronization +#Basic synchronization ./build/client /source/ /destination/ -# Archive-style synchronization (current FastSync archive behavior) +#Archive - style synchronization(current FastSync archive behavior) ./build/client -a /source/ user@host:destination/ -# Preview a transfer without changing the destination +#Preview a transfer without changing the destination ./build/client -n /source/ /destination/ -# Exclude temporary and object files +#Exclude temporary and object files ./build/client --exclude '*.tmp' --exclude '*.o' \ /source/ user@host:destination/ -# Remove destination entries not present in the source +#Remove destination entries not present in the source ./build/client --delete /source/ user@host:destination/ -# Skip unchanged files using size and modification time +#Skip unchanged files using size and modification time ./build/client --incremental /source/ user@host:destination/ -# Verify content when size and time are not sufficient +#Verify content when size and time are not sufficient ./build/client --incremental --checksum /source/ user@host:destination/ -# Preserve supported mode and timestamp metadata +#Preserve supported mode and timestamp metadata ./build/client -M /source/ user@host:destination/ -# Keep backups of overwritten destination files +#Keep backups of overwritten destination files ./build/client --backup --backup-dir backups \ /source/ user@host:destination/ ``` @@ -216,14 +374,16 @@ before FastSync can claim full rsync CLI compatibility. | `--include-from ` | Read include patterns from a file. | | `--max-size ` | Skip files larger than the limit. | | `--min-size ` | Skip files smaller than the limit. | -| `--max-depth ` | Limit recursive scanning depth; zero means unlimited. | -| `--incremental` | Skip files matching destination size and mtime. | -| `--checksum` | Include xxHash64 content checks in incremental comparisons. | -| `--backup` | Back up overwritten files. | -| `--backup-dir ` | Store backups under a separate directory. | -| `--suffix ` | Set the backup filename suffix. | -| `--partial` | Select partial-transfer handling. With `--partial-dir`, completed files are written there; resumable transfers are not implemented. | -| `--partial-dir ` | Set a relative partial-transfer directory below the server destination root; use with `--partial`. | +| `--max-depth ` | Limit recursive scanning depth; +zero means unlimited.| | `--incremental` | Skip files matching destination size and mtime.| + | `--checksum` | Include xxHash64 content checks in incremental comparisons.| | `--backup` | + Back up overwritten files.| | `--backup - dir` | Store backups under a separate directory.| + | `--suffix` | Set the backup filename suffix.| | `--partial` | + Select partial - transfer handling.With `--partial - dir`, + completed files are written there; +resumable transfers are not implemented.| | `--partial - dir` | + Set a relative partial - transfer directory below the server destination root; +use with `--partial`. | | `--inplace` | Write directly to the destination instead of using a temporary file. | ### Metadata and links @@ -231,7 +391,8 @@ before FastSync can claim full rsync CLI compatibility. | Option | Description | |---|---| | `-M`, `--preserve` | Preserve supported file metadata, currently mode and modification time. | -| `-l`, `--links` | Request symlink preservation; link-target transfer remains incomplete. | +| `-l`, `--links` | Request symlink preservation; +link-target transfer remains incomplete. | | `--copy-links` | Copy symlink referents. | | `--safe-links` | Skip symlinks that point outside the transfer tree. | | `--copy-unsafe-links` | Copy unsafe symlink referents. | @@ -274,7 +435,8 @@ before FastSync can claim full rsync CLI compatibility. | `--cert ` | TLS certificate file. | | `--key ` | TLS private key file. | | `--ca ` | CA file for peer verification. | -| `--destination-root ` | Confine received files to this server-side root; defaults to the current directory. | +| `--destination-root ` | Confine received files to this server-side root; +defaults to the current directory. | | `--allow-delete` | Permit client delete manifests. Deletion is refused by default. | | `-v`, `--verbose` | Enable debug logging. | | `--help` | Print server usage. | diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 33f7c29..191cc07 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -417,7 +417,6 @@ int main(int argc, char* argv[]) { parse_environment(&env_source, &env_dest, &save_to_disk); int exit_code = 0; - bool config_owned_by_pipeline = false; Config* config = config_create(); if (!config) { log_message(LOG_LEVEL_ERROR, "failed to allocate config"); @@ -500,18 +499,14 @@ int main(int argc, char* argv[]) { /* Execute transfer */ if (config->use_multithreading) { - config_owned_by_pipeline = true; - exit_code = send_files_multithreaded(config); + exit_code = send_files_multithreaded(&config); } else { exit_code = send_files(config); } cleanup: if (config) { - if (config->log_file) - fclose(config->log_file); - if (!config_owned_by_pipeline) - config_delete(config); + config_delete(config); } return exit_code; } diff --git a/src/client/client_send.c b/src/client/client_send.c index f8fb31e..0b4ca5c 100644 --- a/src/client/client_send.c +++ b/src/client/client_send.c @@ -150,6 +150,8 @@ static int send_dry_run_manifest(const Config* config) { /* Send the delete manifest (list of files) to the server. Returns 0 on success, -1 on failure. */ static int send_delete_manifest(int fd, ArrayList* manifest) { + if (!manifest) + return -1; if (!send_status(fd, STATUS_MANIFEST)) return -1; if (!send_int(fd, manifest->size)) @@ -191,17 +193,22 @@ static int incremental_check(Client* client, File* file, const Config* config, return 1; if (s == STATUS_DELTA_SIGNATURE) { Data* sig_data = receive_data(client->file_descriptor); - if (!sig_data) + if (!sig_data) { + send_status(client->file_descriptor, STATUS_ERROR); return -1; + } DeltaSignature* sig = delta_signature_deserialize(sig_data); data_destroy(sig_data); - if (!sig) + if (!sig) { + send_status(client->file_descriptor, STATUS_ERROR); return -1; + } *out_sig = sig; return 2; } if (s != STATUS_NEXT) { log_message(LOG_LEVEL_ERROR, "Unexpected server status"); + send_status(client->file_descriptor, STATUS_ERROR); return -1; } return 0; @@ -209,11 +216,10 @@ static int incremental_check(Client* client, File* file, const Config* config, static int send_delta(Client* client, File* file, DeltaSignature* sig, Config* config) { Delta* delta = delta_compute(file->data->data, file->data->size, sig, config->delta_block_size); - if (!delta) { - if (!send_status(client->file_descriptor, STATUS_NEXT)) - return -1; - return 1; - } + /* The receiver is blocked after sending the signature. Every local + fallback therefore needs the explicit NEXT response before full data. */ + if (!delta) + return send_status(client->file_descriptor, STATUS_NEXT) ? 1 : -1; if (!delta_is_worthwhile(delta, file->data->size)) { delta_destroy(delta); @@ -225,14 +231,14 @@ static int send_delta(Client* client, File* file, DeltaSignature* sig, Config* c Data* delta_data = delta_serialize(delta); delta_destroy(delta); if (!delta_data) - return -1; + return send_status(client->file_descriptor, STATUS_NEXT) ? 1 : -1; Data* to_send = delta_data; if (config->use_compression) { to_send = data_compress(delta_data, config->compression_level); data_destroy(delta_data); if (!to_send) - return -1; + return send_status(client->file_descriptor, STATUS_NEXT) ? 1 : -1; } bool ok = send_status(client->file_descriptor, STATUS_DELTA_DATA) && @@ -362,7 +368,8 @@ int send_chunk(Client* client, Chunk* chunk, Config* config) { if (f == NULL) continue; bool stream = f->data->data == NULL && f->data->size > 0; - bool use_sendfile = (config->use_sendfile && !config->use_compression) || stream; + bool use_sendfile = + (config->use_sendfile && !config->use_compression) || (stream && !config->use_compression); int rc = send_single_file(client, f, config, config->use_incremental, use_sendfile); if (rc == 1) continue; @@ -516,7 +523,7 @@ static int load_files_multithreaded(void* pipeline_context) { if (!context->config->use_sendfile) { for (int i = 0; i < chunk->element_count; i++) { File* f = chunk->items[i]; - if (f->data->size > STREAM_THRESHOLD) + if (f->data->size > STREAM_THRESHOLD && !context->config->use_compression) continue; if (!file_load_data(f)) { log_message(LOG_LEVEL_ERROR, "Failed to load file data"); @@ -622,7 +629,7 @@ int send_files(Config* config) { bool load_ok = true; for (int i = 0; i < current_chunk->element_count; i++) { File* f = current_chunk->items[i]; - if (f->data->size > STREAM_THRESHOLD) + if (f->data->size > STREAM_THRESHOLD && !config->use_compression) continue; if (!file_load_data(f)) { log_message(LOG_LEVEL_ERROR, "Failed to load file data"); @@ -687,7 +694,10 @@ send_fail: return ret; } -int send_files_multithreaded(Config* config) { +int send_files_multithreaded(Config** config_ptr) { + if (!config_ptr || !*config_ptr) + return 1; + Config* config = *config_ptr; if (config->dry_run) return send_dry_run_manifest(config); @@ -718,8 +728,9 @@ int send_files_multithreaded(Config* config) { queue_destroy(q2); return 1; } + *config_ptr = NULL; /* context now owns config through all remaining paths */ if (config->use_delete) - context->manifest = create_transfer_manifest(config); + context->manifest = array_list_create(free); if (config->use_delete && !context->manifest) { pipeline_context_sender_destroy(context); return 1; diff --git a/src/client/client_send.h b/src/client/client_send.h index e3c484f..664b060 100644 --- a/src/client/client_send.h +++ b/src/client/client_send.h @@ -7,6 +7,7 @@ int send_chunk(Client* client, Chunk* chunk, Config* config); int send_files(Config* config); -int send_files_multithreaded(Config* config); +/* Takes ownership only when *config is set to NULL on return. */ +int send_files_multithreaded(Config** config); #endif diff --git a/src/client/client_validation.c b/src/client/client_validation.c index eb890fd..106580f 100644 --- a/src/client/client_validation.c +++ b/src/client/client_validation.c @@ -42,8 +42,8 @@ bool validate_config(const Config* config) { return false; } if (config->use_tls) { - if (!config->tls_cert || !config->tls_key) { - log_message(LOG_LEVEL_ERROR, "--tls requires --cert and --key"); + if (!config->tls_cert || !config->tls_key || !config->tls_ca) { + log_message(LOG_LEVEL_ERROR, "--tls requires --cert, --key, and --ca"); return false; } } diff --git a/src/server/receiver.c b/src/server/receiver.c index cd5f3a8..2235cf9 100644 --- a/src/server/receiver.c +++ b/src/server/receiver.c @@ -8,8 +8,14 @@ #include static bool receiver_process_chunk(Chunk* chunk, const ReceiverSink* sink) { + if (!chunk || !sink || !sink->store_file) + return false; for (int i = 0; i < chunk->element_count; i++) { File* file = chunk->items[i]; + if (!file) { + chunk_destroy(chunk); + return false; + } chunk->items[i] = NULL; if (!sink->store_file(file, sink->context)) { chunk_destroy(chunk); @@ -41,9 +47,19 @@ static bool receiver_process_batch(Config* config, int file_descriptor) { send_status(file_descriptor, STATUS_ERROR); return false; } + if (check_size > MAX_RECEIVE_FILE_SIZE) { + free(check_path); + send_status(file_descriptor, STATUS_ERROR); + return false; + } char* full_path = path_cat(config->receive_root_directory, check_path); + if (!full_path) { + free(check_path); + send_status(file_descriptor, STATUS_ERROR); + return false; + } struct stat st; - bool has_old = full_path && lstat(full_path, &st) == 0; + bool has_old = file_stat_secure(full_path, &st); bool match = has_old && (unsigned long long)st.st_size == check_size && (long long)st.st_mtime == check_mtime; bool sent = send_status(file_descriptor, match ? STATUS_OK : STATUS_NEXT); diff --git a/src/server/server.c b/src/server/server.c index 510d0a3..af5cd43 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -1,4 +1,5 @@ #include "config.h" +#include "chunk.h" #include "file.h" #include "log.h" #include "multiprocessing.h" @@ -15,10 +16,31 @@ #include #include #include +#include +#include static char* authorized_root; static int authorized_root_fd = -1; static bool allow_delete; +static bool allow_unauthenticated; +static const char* required_client_cn; + +static bool tls_client_identity_allowed(SSL* ssl) { + if (!ssl || !required_client_cn) + return false; + X509* certificate = SSL_get1_peer_certificate(ssl); + if (!certificate) + return false; + char common_name[256]; + int length = X509_NAME_get_text_by_NID(X509_get_subject_name(certificate), NID_commonName, + common_name, sizeof(common_name)); + size_t required_length = strlen(required_client_cn); + bool allowed = length >= 0 && (size_t)length == required_length && + required_length < sizeof(common_name) && + memcmp(common_name, required_client_cn, required_length) == 0; + X509_free(certificate); + return allowed; +} static void release_authorization(void) { file_set_authorized_root(-1, NULL); @@ -37,28 +59,166 @@ static bool path_is_within(const char* root, const char* path) { static bool __attribute__((unused)) configure_authorization(const char* root) { char resolved[PATH_MAX]; - if (!root || !realpath(root, resolved)) - return false; - authorized_root = str_dup(resolved); - if (!authorized_root) - return false; - authorized_root_fd = open(resolved, O_RDONLY | O_DIRECTORY | O_CLOEXEC); - if (authorized_root_fd < 0) { - free(authorized_root); - authorized_root = NULL; + if (!root) { + file_set_authorized_root(-1, NULL); + utils_set_authorized_root(-1, NULL); return false; } - if (!file_set_authorized_root(authorized_root_fd, authorized_root)) { + int root_fd = open(root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (root_fd < 0) { + file_set_authorized_root(-1, NULL); + utils_set_authorized_root(-1, NULL); + return false; + } + char fd_path[64]; + int fd_path_length = snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", root_fd); + if (fd_path_length < 0 || (size_t)fd_path_length >= sizeof(fd_path) || + !realpath(fd_path, resolved)) { + close(root_fd); + file_set_authorized_root(-1, NULL); + utils_set_authorized_root(-1, NULL); + return false; + } + authorized_root = str_dup(resolved); + if (!authorized_root) { + close(root_fd); + file_set_authorized_root(-1, NULL); + utils_set_authorized_root(-1, NULL); + return false; + } + authorized_root_fd = root_fd; + if (!file_set_authorized_root(authorized_root_fd, authorized_root) || + !utils_set_authorized_root(authorized_root_fd, authorized_root)) { + file_set_authorized_root(-1, NULL); + utils_set_authorized_root(-1, NULL); close(authorized_root_fd); authorized_root_fd = -1; free(authorized_root); authorized_root = NULL; return false; } - utils_set_authorized_root_fd(authorized_root_fd); return true; } +int receive_files(Config* config, int fd) { + Status status; + if (!receive_status(fd, &status)) + return -1; + + while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK || + status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH) { + if (status == STATUS_KEEPALIVE) { + send_status(fd, STATUS_KEEPALIVE); + goto next; + } + if (status == STATUS_ABORT) { + log_message(LOG_LEVEL_INFO, "Received abort from client, cleaning up"); + return -1; + } + if (status == STATUS_CHECK) { + bool skipped; + File* file = receive_incremental_check(fd, config, &skipped); + if (skipped) + goto next; + if (file == NULL && !skipped) + return -1; + if (config->save_to_disk && + !file_save_to_disk(config->receive_root_directory, file, config)) { + file_destroy(file); + send_status(fd, STATUS_ERROR); + return -1; + } + file_destroy(file); + } else if (status == STATUS_CHUNK) { + Chunk* chunk = receive_chunk_data(fd, config); + if (chunk == NULL) { + send_status(fd, STATUS_ERROR); + return -1; + } + for (int i = 0; i < chunk->element_count; i++) { + if (config->save_to_disk && + !file_save_to_disk(config->receive_root_directory, chunk->items[i], config)) { + chunk_destroy(chunk); + send_status(fd, STATUS_ERROR); + return -1; + } + } + chunk_destroy(chunk); + } else if (status == STATUS_CHECK_BATCH) { + int count; + /* Batch framing has no checksum field yet; never silently downgrade a + checksum-enabled transfer into mtime-only matching. */ + if (config->checksum || !receive_int(fd, &count) || count < 0 || count > MAX_MANIFEST_ENTRIES) + return -1; + for (int i = 0; i < count; i++) { + char* check_path = receive_str(fd); + if (!check_path) + return -1; + unsigned long long check_size; + long long check_mtime; + if (!receive_n_data(fd, &check_size, sizeof(check_size)) || + !receive_n_data(fd, &check_mtime, sizeof(check_mtime))) { + free(check_path); + return -1; + } + if (!utils_valid_batch_path(check_path)) { + free(check_path); + send_status(fd, STATUS_ERROR); + return -1; + } + struct stat st; + char* full_path = path_cat(config->receive_root_directory, check_path); + if (!full_path) { + free(check_path); + send_status(fd, STATUS_ERROR); + return -1; + } + bool has_old = full_path && file_stat_secure(full_path, &st); + bool match = has_old && (unsigned long long)st.st_size == check_size && + (long long)st.st_mtime == check_mtime; + bool sent = send_status(fd, match ? STATUS_OK : STATUS_NEXT); + free(full_path); + free(check_path); + if (!sent) + return -1; + } + goto next; + } else { + File* file = file_receive(config, fd); + if (file == NULL) { + log_message(LOG_LEVEL_ERROR, "Failed to receive file"); + send_status(fd, STATUS_ERROR); + return -1; + } + if (config->save_to_disk && + !file_save_to_disk(config->receive_root_directory, file, config)) { + file_destroy(file); + send_status(fd, STATUS_ERROR); + return -1; + } + file_destroy(file); + } + next: + if (!receive_status(fd, &status)) { + send_status(fd, STATUS_ERROR); + return -1; + } + } + + if (status == STATUS_MANIFEST) { + if (receive_manifest(fd, config, &status) != 0) { + return -1; + } + } + if (status != STATUS_FINISHED) { + log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED Status"); + send_status(fd, STATUS_ERROR); + return -1; + } + send_status(fd, STATUS_OK); + return 0; +} + void handler(int file_descriptor) { SSL* ssl = io_get_ssl(); ProtocolSession session; @@ -79,25 +239,37 @@ void handler(int file_descriptor) { protocol_session_unbind(); return; } - char resolved_destination[PATH_MAX]; - char* canonical_destination = realpath(config->receive_root_directory, NULL); - const char* destination = - canonical_destination ? canonical_destination : config->receive_root_directory; - if (has_path_traversal(destination) || !path_is_within(authorized_root, destination)) { - log_message(LOG_LEVEL_ERROR, "Rejected destination outside authorized root"); - free(canonical_destination); + if (!allow_unauthenticated && ssl == NULL) { + log_message(LOG_LEVEL_ERROR, "Rejected unauthenticated plaintext connection"); config_delete(config); close(file_descriptor); protocol_session_unbind(); return; } - if (canonical_destination) - snprintf(resolved_destination, sizeof(resolved_destination), "%s", canonical_destination); - else - snprintf(resolved_destination, sizeof(resolved_destination), "%s", destination); - free(canonical_destination); - free(config->receive_root_directory); - config->receive_root_directory = str_dup(resolved_destination); + if (ssl && required_client_cn && !tls_client_identity_allowed(ssl)) { + log_message(LOG_LEVEL_ERROR, "Rejected TLS client with unauthorized identity"); + config_delete(config); + close(file_descriptor); + return; + } + char* destination = config->receive_root_directory; + char* joined_destination = NULL; + if (destination && destination[0] != '/') + joined_destination = path_cat(authorized_root, destination); + if (joined_destination) + destination = joined_destination; + if (!destination || has_path_traversal(destination) || + !path_is_within(authorized_root, destination)) { + log_message(LOG_LEVEL_ERROR, "Rejected destination outside authorized root"); + free(joined_destination); + config_delete(config); + close(file_descriptor); + return; + } + if (joined_destination) { + free(config->receive_root_directory); + config->receive_root_directory = joined_destination; + } if (!config->receive_root_directory) { config_delete(config); close(file_descriptor); @@ -184,8 +356,10 @@ static void print_server_usage(void) { printf(" --cert TLS certificate file (PEM)\n"); printf(" --key TLS private key file (PEM)\n"); printf(" --ca TLS CA certificate file (PEM)\n"); + printf(" --client-cn Required TLS client certificate CN\n"); printf(" --destination-root Authorized destination root (default: .)\n"); printf(" --allow-delete Permit manifest deletion\n"); + printf(" --allow-unauthenticated Allow plaintext/anonymous network clients\n"); printf(" -v, --verbose Enable debug logging\n"); printf(" --help Show this help\n"); } @@ -214,10 +388,14 @@ int main(int argc, char* argv[]) { tls_key = argv[++i]; } else if (strcmp(argv[i], "--ca") == 0 && i + 1 < argc) { tls_ca = argv[++i]; + } else if (strcmp(argv[i], "--client-cn") == 0 && i + 1 < argc) { + required_client_cn = argv[++i]; } else if (strcmp(argv[i], "--destination-root") == 0 && i + 1 < argc) { destination_root = argv[++i]; } else if (strcmp(argv[i], "--allow-delete") == 0) { allow_delete = true; + } else if (strcmp(argv[i], "--allow-unauthenticated") == 0) { + allow_unauthenticated = true; } else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) { char* end; long p = strtol(argv[++i], &end, 10); @@ -241,6 +419,8 @@ int main(int argc, char* argv[]) { return 1; } if (stdio_mode) { + /* SSH authenticates the stdio transport outside of FastSync. */ + allow_unauthenticated = true; io_set_fds(STDIN_FILENO, STDOUT_FILENO); handler(STDIN_FILENO); release_authorization(); @@ -253,8 +433,8 @@ int main(int argc, char* argv[]) { return 1; } if (use_tls) { - if (!tls_cert || !tls_key) { - fprintf(stderr, "Error: --tls requires --cert and --key\n"); + if (!tls_cert || !tls_key || !tls_ca || !required_client_cn) { + fprintf(stderr, "Error: --tls requires --cert, --key, --ca, and --client-cn\n"); server_delete(&g_server); release_authorization(); return 1; diff --git a/src/shared/chunk.c b/src/shared/chunk.c index 158503c..3af1c63 100644 --- a/src/shared/chunk.c +++ b/src/shared/chunk.c @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -12,22 +13,33 @@ #include "log.h" #include "metadata.h" #include "protocol.h" +#include "utils.h" /* Maximum individual file data size within a chunk (64 MB) */ #define MAX_FILE_DATA_SIZE (64ULL * 1024 * 1024) -#define MAX_CHUNK_FILES (1024 * 1024) +#define MAX_FILES_PER_CHUNK 65536U Chunk* chunk_create(File** items, int element_count) { + if (element_count < 0 || (element_count > 0 && items == NULL)) + return NULL; Chunk* chunk = (Chunk*)malloc(sizeof(Chunk)); if (chunk == NULL) { log_perror("ERROR: Could not allocate memory for chunk structure"); return NULL; } - chunk->items = (File**)malloc(element_count * sizeof(File*)); - if (chunk->items == NULL) { - free(chunk); - return NULL; + if (element_count == 0) { + chunk->items = NULL; + } else { + if ((size_t)element_count > SIZE_MAX / sizeof(File*)) { + free(chunk); + return NULL; + } + chunk->items = (File**)malloc((size_t)element_count * sizeof(File*)); + if (chunk->items == NULL) { + free(chunk); + return NULL; + } } for (int i = 0; i < element_count; i++) { @@ -52,15 +64,37 @@ void chunk_destroy(void* item) { } static unsigned long long per_file_serialize_size(File* file, bool use_metadata) { - return sizeof(size_t) + strlen(file->path) + - (use_metadata ? sizeof(int) + (file->metadata ? FILE_METADATA_WIRE_SIZE : 0) : 0) + - sizeof(size_t) + file->data->size; + unsigned long long size = sizeof(size_t); + size_t path_len = strlen(file->path); + unsigned long long metadata_size = + use_metadata ? sizeof(int) + (file->metadata ? FILE_METADATA_WIRE_SIZE : 0) : 0; + if ((unsigned long long)path_len > ULLONG_MAX - size) + return 0; + size += path_len; + if (metadata_size > ULLONG_MAX - size) + return 0; + size += metadata_size; + if (sizeof(size_t) > ULLONG_MAX - size) + return 0; + size += sizeof(size_t); + if ((unsigned long long)file->data->size > ULLONG_MAX - size) + return 0; + return size + file->data->size; } Data* chunk_serialize(Chunk* chunk, bool use_metadata) { + if (!chunk || chunk->element_count < 0 || (chunk->element_count > 0 && chunk->items == NULL)) + return NULL; unsigned long long data_size = 0; for (int i = 0; i < chunk->element_count; i++) { - data_size += per_file_serialize_size(chunk->items[i], use_metadata); + if (!chunk->items[i] || !chunk->items[i]->path || !chunk->items[i]->data || + (chunk->items[i]->data->size > 0 && !chunk->items[i]->data->data) || + chunk->items[i]->path[0] == '\0' || has_path_traversal(chunk->items[i]->path)) + return NULL; + unsigned long long file_size = per_file_serialize_size(chunk->items[i], use_metadata); + if (file_size == 0 || file_size > ULLONG_MAX - data_size || data_size + file_size > SIZE_MAX) + return NULL; + data_size += file_size; } Data* data = data_create_empty(data_size); if (data == NULL) { @@ -89,12 +123,16 @@ Data* chunk_serialize(Chunk* chunk, bool use_metadata) { } Chunk* chunk_deserialize(Data* data, bool use_metadata) { + if (!data || (!data->data && data->size != 0)) + return NULL; ArrayList* files = array_list_create(file_destroy); + if (files == NULL) + return NULL; char* data_pointer = data->data; size_t remaining_size = data->size; while (remaining_size > 0) { - if (files->size >= MAX_CHUNK_FILES) { + if ((unsigned int)files->size >= MAX_FILES_PER_CHUNK) { log_message(LOG_LEVEL_ERROR, "Chunk contains too many files"); array_list_delete(files); return NULL; @@ -116,6 +154,10 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) { return NULL; } + if (path_len == SIZE_MAX) { + array_list_delete(files); + return NULL; + } char* path = malloc(path_len + 1); if (path == NULL) { log_perror("Could not allocate memory for file path"); @@ -124,9 +166,20 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) { } memcpy(path, data_pointer, path_len); path[path_len] = '\0'; + if (memchr(path, '\0', path_len) != NULL) { + free(path); + array_list_delete(files); + return NULL; + } data_pointer += path_len; remaining_size -= path_len; + if (path_len == 0 || has_path_traversal(path)) { + free(path); + array_list_delete(files); + return NULL; + } + File* file = file_create(path); free(path); if (file == NULL) { @@ -144,7 +197,8 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) { // Peek at present flag to determine total size needed before reading int present_flag; memcpy(&present_flag, data_pointer, sizeof(int)); - if (present_flag && remaining_size < sizeof(int) + FILE_METADATA_WIRE_SIZE) { + if ((present_flag != 0 && present_flag != 1) || + (present_flag == 1 && remaining_size < sizeof(int) + FILE_METADATA_WIRE_SIZE)) { log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for metadata body"); file_destroy(file); array_list_delete(files); @@ -152,12 +206,13 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) { } file->metadata = metadata_from_buf(&data_pointer); remaining_size -= sizeof(int); - if (file->metadata) + if (present_flag == 1) { + if (file->metadata == NULL) { + file_destroy(file); + array_list_delete(files); + return NULL; + } remaining_size -= FILE_METADATA_WIRE_SIZE; - else if (present_flag) { - file_destroy(file); - array_list_delete(files); - return NULL; } } @@ -189,7 +244,8 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) { return NULL; } - void* file_data = malloc(file_data_size > 0 ? file_data_size : 1); + size_t allocation_size = file_data_size > 0 ? file_data_size : 1; + void* file_data = malloc(allocation_size); if (file_data == NULL) { log_perror("Could not allocate memory for file data"); file_destroy(file); @@ -197,13 +253,14 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) { return NULL; } memcpy(file_data, data_pointer, file_data_size); - data_destroy(file->data); - file->data = data_create(file_data, file_data_size); - if (file->data == NULL) { + Data* replacement = data_create(file_data, file_data_size); + if (replacement == NULL) { file_destroy(file); array_list_delete(files); return NULL; } + data_destroy(file->data); + file->data = replacement; data_pointer += file_data_size; remaining_size -= file_data_size; @@ -215,7 +272,7 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) { } File** file_array = (File**)array_list_to_array(files); - if (file_array == NULL) { + if (files->size > 0 && file_array == NULL) { array_list_delete(files); return NULL; } @@ -246,14 +303,14 @@ Data* chunk_compress(Chunk* chunk, int compression_level, bool use_metadata) { } Chunk* receive_chunk_data(int fd, const Config* config) { - Data* chunk_data = receive_data(fd); + Data* chunk_data = receive_data_limited(fd, MAX_CHUNK_SIZE); if (chunk_data == NULL) { log_message(LOG_LEVEL_ERROR, "Failed to receive chunk data"); return NULL; } Data* data_to_process = chunk_data; if (config->use_compression) { - data_to_process = data_decompress(chunk_data); + data_to_process = data_decompress_limited(chunk_data, MAX_CHUNK_SIZE); data_destroy(chunk_data); if (data_to_process == NULL) { log_message(LOG_LEVEL_ERROR, "Failed to decompress chunk"); diff --git a/src/shared/compression.c b/src/shared/compression.c index 27b6da4..1a42ba5 100644 --- a/src/shared/compression.c +++ b/src/shared/compression.c @@ -2,6 +2,8 @@ #include "data.h" #include "log.h" #include +#include +#include #include #include #include @@ -69,7 +71,10 @@ Data* data_compress(Data* data_to_compress, int compression_level) { return compressed_data; } -Data* data_decompress(Data* compressed_data) { +Data* data_decompress_limited(Data* compressed_data, size_t maximum_size) { + if (!compressed_data || (!compressed_data->data && compressed_data->size != 0) || + maximum_size == 0) + return NULL; log_message(LOG_LEVEL_DEBUG, "Start to decompress data"); unsigned long long dst_size = ZSTD_getFrameContentSize(compressed_data->data, compressed_data->size); @@ -82,15 +87,16 @@ Data* data_decompress(Data* compressed_data) { // ZSTD_CONTENTSIZE_UNKNOWN (~2^64) can cause massive allocation; // fall back to a conservative estimate (3x compressed size) when unknown. if (dst_size == ZSTD_CONTENTSIZE_UNKNOWN) { + if (compressed_data->size > ULLONG_MAX / 3) + return NULL; dst_size = compressed_data->size * 3; if (dst_size < INITIAL_DECOMPRESS_BUF_SIZE) dst_size = INITIAL_DECOMPRESS_BUF_SIZE; - if (dst_size > MAX_DECOMPRESSED_SIZE) - dst_size = MAX_DECOMPRESSED_SIZE; } - if (dst_size > MAX_DECOMPRESSED_SIZE) { - log_message(LOG_LEVEL_ERROR, "Declared decompressed size exceeds %llu bytes", - (unsigned long long)MAX_DECOMPRESSED_SIZE); + unsigned long long hard_limit = + maximum_size < MAX_DECOMPRESSED_SIZE ? maximum_size : MAX_DECOMPRESSED_SIZE; + if (dst_size > hard_limit) { + log_message(LOG_LEVEL_ERROR, "Declared decompressed size exceeds %llu bytes", hard_limit); return NULL; } @@ -101,6 +107,8 @@ Data* data_decompress(Data* compressed_data) { } size_t buf_size = (dst_size > 0) ? (size_t)dst_size : INITIAL_DECOMPRESS_BUF_SIZE; + if (buf_size > maximum_size) + buf_size = maximum_size; Data* uncompressed_data = data_create_empty(buf_size); if (!uncompressed_data) { log_message(LOG_LEVEL_ERROR, "Failed to allocate decompression buffer"); @@ -121,7 +129,7 @@ Data* data_decompress(Data* compressed_data) { return NULL; } if (ret > 0 && output.pos == output.size) { - if (buf_size >= MAX_DECOMPRESSED_SIZE) { + if (buf_size >= hard_limit || buf_size > SIZE_MAX / 2) { log_message(LOG_LEVEL_ERROR, "Decompressed data exceeds %llu bytes", (unsigned long long)MAX_DECOMPRESSED_SIZE); ZSTD_freeDCtx(dctx); @@ -129,8 +137,8 @@ Data* data_decompress(Data* compressed_data) { return NULL; } buf_size *= 2; - if (buf_size > MAX_DECOMPRESSED_SIZE) - buf_size = MAX_DECOMPRESSED_SIZE; + if (buf_size > hard_limit) + buf_size = (size_t)hard_limit; void* new_data = realloc(uncompressed_data->data, buf_size); if (!new_data) { log_message(LOG_LEVEL_ERROR, "Failed to grow decompression buffer"); @@ -150,3 +158,7 @@ Data* data_decompress(Data* compressed_data) { log_message(LOG_LEVEL_DEBUG, "Decompressed data successfully"); return uncompressed_data; } + +Data* data_decompress(Data* compressed_data) { + return data_decompress_limited(compressed_data, MAX_DECOMPRESSED_SIZE); +} diff --git a/src/shared/compression.h b/src/shared/compression.h index d392f25..5322abe 100644 --- a/src/shared/compression.h +++ b/src/shared/compression.h @@ -6,6 +6,7 @@ Data* data_compress(Data* data_to_compress, int compression_level); Data* data_decompress(Data* compressed_data); +Data* data_decompress_limited(Data* compressed_data, size_t maximum_size); bool compression_should_skip(const char* path); #endif diff --git a/src/shared/config.c b/src/shared/config.c index 57c8eef..efc2708 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -100,6 +100,43 @@ static void config_set_defaults(Config* config) { config->compress_choice = NULL; } +static bool valid_wire_bool(int value) { + return value == 0 || value == 1; +} + +static bool receive_wire_bool(int fd, bool* value) { + int wire_value; + if (!receive_int(fd, &wire_value) || !valid_wire_bool(wire_value)) + return false; + *value = wire_value != 0; + return true; +} + +static bool validate_received_config(const Config* config) { + return valid_wire_bool(config->save_to_disk) && valid_wire_bool(config->use_multithreading) && + valid_wire_bool(config->use_chunk_serialization) && + valid_wire_bool(config->use_compression) && valid_wire_bool(config->use_metadata) && + valid_wire_bool(config->use_sendfile) && valid_wire_bool(config->use_delete) && + valid_wire_bool(config->use_incremental) && valid_wire_bool(config->use_delta) && + valid_wire_bool(config->backup) && valid_wire_bool(config->follow_symlinks) && + valid_wire_bool(config->copy_links) && valid_wire_bool(config->safe_links) && + valid_wire_bool(config->copy_unsafe_links) && + valid_wire_bool(config->preserve_hard_links) && valid_wire_bool(config->preserve_acls) && + valid_wire_bool(config->preserve_xattrs) && valid_wire_bool(config->preserve_devices) && + valid_wire_bool(config->preserve_sparse) && valid_wire_bool(config->update) && + valid_wire_bool(config->inplace) && valid_wire_bool(config->append) && + valid_wire_bool(config->append_verify) && valid_wire_bool(config->delete_excluded) && + valid_wire_bool(config->delete_after) && valid_wire_bool(config->relative) && + valid_wire_bool(config->prune_empty_dirs) && valid_wire_bool(config->partial) && + valid_wire_bool(config->delete_before) && valid_wire_bool(config->checksum) && + (!config->use_compression || + (config->compression_level >= 1 && config->compression_level <= 22)) && + config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE && + config->delta_block_size >= DELTA_BLOCK_SIZE_MIN && + config->delta_block_size <= DELTA_BLOCK_SIZE_MAX && + config->delta_max_file_size <= DELTA_MAX_FILE_SIZE && config->max_delete >= 0; +} + Config* config_create(void) { Config* config = malloc(sizeof(Config)); if (!config) @@ -137,6 +174,10 @@ void config_parse_ssh_dest(Config* config) { void config_delete(Config* config) { if (config == NULL) return; + if (config->log_file) { + fclose(config->log_file); + config->log_file = NULL; + } free(config->version); free(config->send_directory); free(config->receive_root_directory); @@ -221,52 +262,34 @@ static bool receive_core_fields(int fd, Config* c) { c->receive_root_directory = receive_str(fd); if (!c->send_directory || !c->receive_root_directory) return false; - if (!receive_int(fd, &value)) + if (!receive_wire_bool(fd, &c->save_to_disk) || !receive_wire_bool(fd, &c->use_multithreading) || + !receive_wire_bool(fd, &c->use_chunk_serialization) || + !receive_wire_bool(fd, &c->use_compression) || !receive_wire_bool(fd, &c->use_metadata)) return false; - c->save_to_disk = value; - if (!receive_int(fd, &value)) - return false; - c->use_multithreading = value; - if (!receive_int(fd, &value)) - return false; - c->use_chunk_serialization = value; - if (!receive_int(fd, &value)) - return false; - c->use_compression = value; - if (!receive_int(fd, &value)) - return false; - c->use_metadata = value; if (!receive_int(fd, &value)) return false; c->compression_level = value; if (!receive_n_data(fd, &c->chunk_size, sizeof(c->chunk_size))) return false; - if (!receive_int(fd, &value)) + if (!receive_wire_bool(fd, &c->use_sendfile)) return false; - c->use_sendfile = value; return true; } static bool receive_delta_fields(int fd, Config* c) { - int value; - if (!receive_int(fd, &value)) + if (!receive_wire_bool(fd, &c->use_delete)) return false; - c->use_delete = value; - if (!receive_int(fd, &value)) + if (!receive_wire_bool(fd, &c->use_incremental)) return false; - c->use_incremental = value; - if (!receive_int(fd, &value)) + if (!receive_wire_bool(fd, &c->use_delta)) return false; - c->use_delta = value; return receive_n_data(fd, &c->delta_block_size, sizeof(c->delta_block_size)) && receive_n_data(fd, &c->delta_max_file_size, sizeof(unsigned long long)); } static bool receive_file_options(int fd, Config* c) { - int value; - if (!receive_int(fd, &value)) + if (!receive_wire_bool(fd, &c->backup)) return false; - c->backup = value; c->backup_dir = receive_str(fd); if (!c->backup_dir) return false; @@ -274,47 +297,38 @@ static bool receive_file_options(int fd, Config* c) { &c->copy_unsafe_links, &c->preserve_hard_links, &c->preserve_acls, &c->preserve_xattrs, &c->preserve_devices, &c->preserve_sparse}; for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { - if (!receive_int(fd, &value)) + if (!receive_wire_bool(fd, flags[i])) return false; - *flags[i] = value; } return true; } static bool receive_selection_options(int fd, Config* c) { - int value; bool* flags[] = {&c->update, &c->inplace, &c->append, &c->append_verify, &c->delete_excluded, &c->delete_after}; for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { - if (!receive_int(fd, &value)) + if (!receive_wire_bool(fd, flags[i])) return false; - *flags[i] = value; } if (!receive_n_data(fd, &c->max_delete, sizeof(c->max_delete))) return false; - if (!receive_int(fd, &value)) + if (!receive_wire_bool(fd, &c->relative)) return false; - c->relative = value; - if (!receive_int(fd, &value)) + if (!receive_wire_bool(fd, &c->prune_empty_dirs)) return false; - c->prune_empty_dirs = value; return true; } static bool receive_resume_options(int fd, Config* c) { - int value; c->temp_dir = receive_str(fd); - if (!c->temp_dir || !receive_int(fd, &value)) + if (!c->temp_dir || !receive_wire_bool(fd, &c->partial)) return false; - c->partial = value; c->partial_dir = receive_str(fd); c->suffix = c->partial_dir ? receive_str(fd) : NULL; - if (!c->partial_dir || !c->suffix || !receive_int(fd, &value)) + if (!c->partial_dir || !c->suffix || !receive_wire_bool(fd, &c->delete_before)) return false; - c->delete_before = value; - if (!receive_int(fd, &value)) + if (!receive_wire_bool(fd, &c->checksum)) return false; - c->checksum = value; c->compress_choice = receive_str(fd); return c->compress_choice != NULL; } @@ -361,6 +375,11 @@ Config* config_receive(int file_descriptor) { send_status(file_descriptor, STATUS_ERROR); goto error; } + if (!validate_received_config(config)) { + fprintf(stderr, "Invalid configuration received from client\n"); + send_status(file_descriptor, STATUS_ERROR); + goto error; + } if (!send_status(file_descriptor, STATUS_OK)) goto error; return config; diff --git a/src/shared/data.c b/src/shared/data.c index 82e6198..dd43e15 100644 --- a/src/shared/data.c +++ b/src/shared/data.c @@ -21,6 +21,7 @@ Data* data_create_reserve(size_t size) { } d->data = NULL; d->size = size; + d->protocol_charge = 0; return d; } @@ -33,12 +34,15 @@ Data* data_create(void* data, size_t data_size) { } new_data->data = data; new_data->size = data_size; + new_data->protocol_charge = 0; return new_data; } void data_destroy(Data* data) { if (data == NULL) return; + if (data->protocol_charge != 0) + protocol_release_memory(data->protocol_charge); free(data->data); free(data); } diff --git a/src/shared/data.h b/src/shared/data.h index f03fabf..b65ae29 100644 --- a/src/shared/data.h +++ b/src/shared/data.h @@ -6,11 +6,14 @@ typedef struct { void* data; size_t size; + /* Non-zero only for a buffer charged to the protocol connection budget. */ + size_t protocol_charge; } Data; Data* data_create_empty(size_t data_size); Data* data_create_reserve(size_t size); Data* data_create(void* data, size_t data_size); void data_destroy(Data* data); +void protocol_release_memory(size_t charge); #endif diff --git a/src/shared/delta.c b/src/shared/delta.c index 26685db..fc941fb 100644 --- a/src/shared/delta.c +++ b/src/shared/delta.c @@ -1,6 +1,7 @@ #include "delta.h" #include "log.h" #include +#include #include #include @@ -36,6 +37,10 @@ DeltaSignature* delta_signature_create(const void* old_file_data, uint64_t old_f if (old_file_data == NULL || old_file_size == 0 || block_size == 0) return NULL; + if (old_file_size > DELTA_MAX_FILE_SIZE || block_size > DELTA_BLOCK_SIZE_MAX || + old_file_size > UINT32_MAX * (uint64_t)block_size) + return NULL; + uint32_t block_count = (uint32_t)((old_file_size + block_size - 1) / block_size); DeltaSignature* sig = malloc(sizeof(DeltaSignature)); @@ -45,7 +50,11 @@ DeltaSignature* delta_signature_create(const void* old_file_data, uint64_t old_f sig->file_size = old_file_size; sig->block_size = block_size; sig->block_count = block_count; - sig->blocks = malloc(block_count * sizeof(DeltaBlockSig)); + if (block_count == 0) { + free(sig); + return NULL; + } + sig->blocks = malloc((size_t)block_count * sizeof(DeltaBlockSig)); if (!sig->blocks) { free(sig); return NULL; @@ -67,8 +76,11 @@ Data* delta_signature_serialize(const DeltaSignature* sig) { if (!sig) return NULL; - uint64_t total = sizeof(uint64_t) + sizeof(uint32_t) + sizeof(uint32_t) + - (uint64_t)sig->block_count * (sizeof(uint32_t) + sizeof(uint32_t)); + uint64_t block_bytes = (uint64_t)sig->block_count * (sizeof(uint32_t) + sizeof(uint32_t)); + uint64_t total = sizeof(uint64_t) + sizeof(uint32_t) + sizeof(uint32_t) + block_bytes; + if (block_bytes > UINT64_MAX - (sizeof(uint64_t) + sizeof(uint32_t) + sizeof(uint32_t)) || + total > SIZE_MAX) + return NULL; uint8_t* buf = malloc((size_t)total); if (!buf) @@ -118,6 +130,13 @@ DeltaSignature* delta_signature_deserialize(const Data* data) { return NULL; } + if (sig->block_size == 0 || sig->block_size > DELTA_BLOCK_SIZE_MAX || + sig->file_size > DELTA_MAX_FILE_SIZE || sig->file_size == 0 || + (sig->file_size + sig->block_size - 1) / sig->block_size != sig->block_count) { + free(sig); + return NULL; + } + uint64_t expected = sizeof(uint64_t) + sizeof(uint32_t) + sizeof(uint32_t) + (uint64_t)sig->block_count * (sizeof(uint32_t) + sizeof(uint32_t)); if (data->size < expected) { @@ -156,8 +175,10 @@ void delta_signature_destroy(DeltaSignature* sig) { static bool ensure_capacity(DeltaInstruction** instrs, uint32_t* capacity, uint32_t count) { if (count < *capacity) return true; + if (*capacity > MAX_DELTA_INSTRUCTIONS / 2) + return false; uint32_t new_cap = *capacity * 2; - DeltaInstruction* tmp = realloc(*instrs, new_cap * sizeof(DeltaInstruction)); + DeltaInstruction* tmp = realloc(*instrs, (size_t)new_cap * sizeof(DeltaInstruction)); if (!tmp) return false; *instrs = tmp; @@ -169,6 +190,8 @@ static bool flush_literal(DeltaInstruction** instrs, uint32_t* capacity, uint32_ const uint8_t* data, uint64_t start, uint64_t end) { if (start >= end) return true; + if (end - start > UINT32_MAX || *count >= MAX_DELTA_INSTRUCTIONS) + return false; uint32_t lit_len = (uint32_t)(end - start); if (!ensure_capacity(instrs, capacity, *count)) return false; @@ -183,16 +206,26 @@ static bool flush_literal(DeltaInstruction** instrs, uint32_t* capacity, uint32_ return true; } +static void free_instructions(DeltaInstruction* instrs, uint32_t count) { + if (!instrs) + return; + for (uint32_t i = 0; i < count; i++) + if (instrs[i].type == DELTA_INSTR_LITERAL) + free(instrs[i].literal.data); + free(instrs); +} + Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const DeltaSignature* sig, uint32_t block_size) { - if (!new_file_data || !sig || new_file_size == 0 || block_size == 0) + if (!new_file_data || !sig || !sig->blocks || new_file_size == 0 || block_size == 0 || + block_size > DELTA_BLOCK_SIZE_MAX || sig->block_size != block_size) return NULL; const uint8_t* new_data = (const uint8_t*)new_file_data; uint32_t capacity = 64; uint32_t count = 0; - DeltaInstruction* instrs = malloc(capacity * sizeof(DeltaInstruction)); + DeltaInstruction* instrs = malloc((size_t)capacity * sizeof(DeltaInstruction)); if (!instrs) return NULL; @@ -236,14 +269,14 @@ Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const De if (xxh == sig->blocks[j].xxhash) { if (has_literal) { if (!flush_literal(&instrs, &capacity, &count, new_data, literal_start, i)) { - free(instrs); + free_instructions(instrs, count); return NULL; } has_literal = false; } if (!ensure_capacity(&instrs, &capacity, count)) { - free(instrs); + free_instructions(instrs, count); return NULL; } instrs[count].type = DELTA_INSTR_BLOCK_MATCH; @@ -271,18 +304,14 @@ Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const De if (has_literal) { if (!flush_literal(&instrs, &capacity, &count, new_data, literal_start, new_file_size)) { - free(instrs); + free_instructions(instrs, count); return NULL; } } Delta* delta = malloc(sizeof(Delta)); if (!delta) { - for (uint32_t k = 0; k < count; k++) { - if (instrs[k].type == DELTA_INSTR_LITERAL) - free(instrs[k].literal.data); - } - free(instrs); + free_instructions(instrs, count); return NULL; } @@ -292,11 +321,24 @@ Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const De delta->delta_size = 0; for (uint32_t k = 0; k < count; k++) { + if (delta->delta_size == UINT64_MAX) { + delta_destroy(delta); + return NULL; + } delta->delta_size += 1; if (instrs[k].type == DELTA_INSTR_BLOCK_MATCH) { + if (delta->delta_size > UINT64_MAX - sizeof(uint32_t) * 3) { + delta_destroy(delta); + return NULL; + } delta->delta_size += sizeof(uint32_t) * 3; } else { - delta->delta_size += sizeof(uint32_t) + instrs[k].literal.length; + uint64_t extra = sizeof(uint32_t) + instrs[k].literal.length; + if (delta->delta_size > UINT64_MAX - extra) { + delta_destroy(delta); + return NULL; + } + delta->delta_size += extra; } } @@ -307,7 +349,12 @@ Data* delta_serialize(const Delta* delta) { if (!delta) return NULL; - uint64_t total = sizeof(uint64_t) + sizeof(uint32_t) + delta->delta_size; + if (delta->instruction_count > 0 && !delta->instructions) + return NULL; + uint64_t header_size = sizeof(uint64_t) + sizeof(uint32_t); + if (delta->delta_size > UINT64_MAX - header_size || header_size + delta->delta_size > SIZE_MAX) + return NULL; + uint64_t total = header_size + delta->delta_size; uint8_t* buf = malloc((size_t)total); if (!buf) return NULL; @@ -365,8 +412,10 @@ Delta* delta_deserialize(const Data* data) { return NULL; } - delta->instructions = malloc(delta->instruction_count * sizeof(DeltaInstruction)); - if (!delta->instructions) { + delta->instructions = delta->instruction_count == 0 + ? NULL + : malloc((size_t)delta->instruction_count * sizeof(DeltaInstruction)); + if (delta->instruction_count > 0 && !delta->instructions) { free(delta); return NULL; } @@ -375,11 +424,7 @@ Delta* delta_deserialize(const Data* data) { for (uint32_t i = 0; i < delta->instruction_count; i++) { if (pos >= data->size) { - for (uint32_t k = 0; k < i; k++) { - if (delta->instructions[k].type == DELTA_INSTR_LITERAL) - free(delta->instructions[k].literal.data); - } - free(delta->instructions); + free_instructions(delta->instructions, i); free(delta); return NULL; } @@ -391,12 +436,8 @@ Delta* delta_deserialize(const Data* data) { delta->delta_size += 1; if (type == DELTA_OP_BLOCK_MATCH) { - if (pos + sizeof(uint32_t) * 3 > data->size) { - for (uint32_t k = 0; k < i; k++) { - if (delta->instructions[k].type == DELTA_INSTR_LITERAL) - free(delta->instructions[k].literal.data); - } - free(delta->instructions); + if (data->size - pos < sizeof(uint32_t) * 3) { + free_instructions(delta->instructions, i); free(delta); return NULL; } @@ -409,12 +450,8 @@ Delta* delta_deserialize(const Data* data) { pos += sizeof(uint32_t); delta->delta_size += sizeof(uint32_t) * 3; } else if (type == DELTA_OP_LITERAL) { - if (pos + sizeof(uint32_t) > data->size) { - for (uint32_t k = 0; k < i; k++) { - if (delta->instructions[k].type == DELTA_INSTR_LITERAL) - free(delta->instructions[k].literal.data); - } - free(delta->instructions); + if (data->size - pos < sizeof(uint32_t)) { + free_instructions(delta->instructions, i); free(delta); return NULL; } @@ -423,23 +460,15 @@ Delta* delta_deserialize(const Data* data) { pos += sizeof(uint32_t); uint32_t lit_len = delta->instructions[i].literal.length; - if (pos + lit_len > data->size) { - for (uint32_t k = 0; k < i; k++) { - if (delta->instructions[k].type == DELTA_INSTR_LITERAL) - free(delta->instructions[k].literal.data); - } - free(delta->instructions); + if (lit_len > data->size - pos) { + free_instructions(delta->instructions, i); free(delta); return NULL; } - delta->instructions[i].literal.data = malloc(lit_len); + delta->instructions[i].literal.data = malloc(lit_len ? lit_len : 1); if (!delta->instructions[i].literal.data) { log_message(LOG_LEVEL_ERROR, "Failed to allocate %u bytes for literal data", lit_len); - for (uint32_t k = 0; k < i; k++) { - if (delta->instructions[k].type == DELTA_INSTR_LITERAL) - free(delta->instructions[k].literal.data); - } - free(delta->instructions); + free_instructions(delta->instructions, i); free(delta); return NULL; } @@ -447,11 +476,7 @@ Delta* delta_deserialize(const Data* data) { pos += lit_len; delta->delta_size += sizeof(uint32_t) + lit_len; } else { - for (uint32_t k = 0; k < i; k++) { - if (delta->instructions[k].type == DELTA_INSTR_LITERAL) - free(delta->instructions[k].literal.data); - } - free(delta->instructions); + free_instructions(delta->instructions, i); free(delta); return NULL; } @@ -463,10 +488,11 @@ Delta* delta_deserialize(const Data* data) { void* delta_apply(const void* old_data, uint64_t old_size, const Delta* delta, uint32_t block_size) { if (!old_data || !delta || (delta->new_file_size > 0 && delta->instructions == NULL) || - (delta->instruction_count > 0 && block_size == 0)) + (delta->instruction_count > 0 && block_size == 0) || + delta->new_file_size > DELTA_MAX_FILE_SIZE || delta->new_file_size > SIZE_MAX) return NULL; - void* output = malloc((size_t)delta->new_file_size); + void* output = malloc(delta->new_file_size ? (size_t)delta->new_file_size : 1); if (!output) return NULL; @@ -491,7 +517,7 @@ void* delta_apply(const void* old_data, uint64_t old_size, const Delta* delta, } memcpy(out + out_pos, old + src_offset, len); out_pos += len; - } else { + } else if (delta->instructions[i].type == DELTA_INSTR_LITERAL) { uint32_t len = delta->instructions[i].literal.length; if (out_pos > delta->new_file_size || (uint64_t)len > delta->new_file_size - out_pos) { free(output); @@ -499,6 +525,9 @@ void* delta_apply(const void* old_data, uint64_t old_size, const Delta* delta, } memcpy(out + out_pos, delta->instructions[i].literal.data, len); out_pos += len; + } else { + free(output); + return NULL; } } @@ -534,7 +563,7 @@ bool delta_should_attempt(uint64_t old_size, uint64_t new_size, uint64_t max_fil } bool delta_is_worthwhile(const Delta* delta, uint64_t new_file_size) { - if (!delta || delta->instruction_count == 0) + if (!delta || delta->instruction_count == 0 || new_file_size == 0) return false; bool has_match = false; diff --git a/src/shared/file.c b/src/shared/file.c index 8dc17e5..9514e87 100644 --- a/src/shared/file.c +++ b/src/shared/file.c @@ -1,3 +1,7 @@ +#include +#include +#include +#include #include #include #include @@ -7,10 +11,24 @@ #include "data.h" #include "delta.h" #include "file.h" -#include "file_store.h" #include "log.h" +#include "metadata.h" #include "utils.h" +static bool write_all(int fd, const void* data, unsigned long long size) { + const unsigned char* p = data; + unsigned long long done = 0; + while (done < size) { + ssize_t n = write(fd, p + done, (size_t)(size - done)); + if (n < 0 && errno == EINTR) + continue; + if (n <= 0) + return false; + done += (unsigned long long)n; + } + return true; +} + bool file_checksum(File* file, uint64_t* checksum) { if (!file || !checksum || !file->data) return false; @@ -33,7 +51,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); @@ -89,7 +107,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) @@ -111,18 +129,9 @@ bool file_load_data(File* file) { return true; } -bool file_set_authorized_root(int fd, const char* canonical_path) { - return file_store_set_authorized_root(fd, canonical_path); -} - -bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size, - bool inplace, bool sparse) { - if (!path || (!data && data_size != 0) || has_path_traversal(path)) - return false; - return file_store_write_secure(path, data, data_size, inplace, sparse, NULL); -} - 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) { log_perror("Could not open the file!"); @@ -137,3 +146,210 @@ size_t file_content_to_buffer(File* file) { fclose(file_pointer); return bytes_read; } + +/* ---- Secure filesystem primitives ---- */ + +static int authorized_root_fd = -1; +static char* authorized_root_path; + +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] == '/'); +} + +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_path_exists_secure(const char* path) { + struct stat st; + return file_stat_secure(path, &st); +} + +bool file_stat_secure(const char* path, struct stat* st) { + if (!path || !st) + return false; + char* leaf = NULL; + int parent_fd = file_open_secure_parent(path, &leaf, false); + if (parent_fd < 0) + return false; + int fd = openat(parent_fd, leaf, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW); + bool exists = fd >= 0 && fstat(fd, st) == 0 && S_ISREG(st->st_mode); + if (fd >= 0) + close(fd); + close(parent_fd); + free(leaf); + return exists; +} + +int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs) { + char* copy = str_dup(path); + if (!copy) + return -1; + char* parent = dirname(copy); + const char* slash = strrchr(path, '/'); + char* leaf = str_dup(slash ? slash + 1 : path); + if (!leaf) { + free(copy); + return -1; + } + int fd; + 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) { + free(copy); + free(leaf); + close(fd); + return -1; + } + free(copy); + copy = relative; + parent = dirname(copy); + } else { + fd = (parent[0] == '/') ? open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC) + : open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + } + if (fd < 0) { + free(copy); + free(leaf); + return -1; + } + char* save = NULL; + char* component = strtok_r(parent, "/", &save); + while (component) { + 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) { + 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); + free(leaf); + return -1; + } + close(fd); + fd = next; + } + component = strtok_r(NULL, "/", &save); + } + free(copy); + *leaf_out = leaf; + return fd; +} + +bool file_ensure_directory_secure(const char* path) { + char* leaf = NULL; + int parent_fd = file_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; +} + +bool file_rename_secure(const char* old_path, const char* new_path) { + char *old_leaf = NULL, *new_leaf = NULL; + int old_parent = file_open_secure_parent(old_path, &old_leaf, false); + int new_parent = file_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; + if (old_parent >= 0) + close(old_parent); + if (new_parent >= 0) + close(new_parent); + free(old_leaf); + free(new_leaf); + return ok; +} + +bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size, + bool inplace, bool sparse, const FileMetadata* metadata) { + char* leaf = NULL; + int dirfd = file_open_secure_parent(path, &leaf, true); + if (dirfd < 0) + return false; + int fd = -1; + bool ok = false; + if (inplace) { + fd = openat(dirfd, leaf, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0644); + if (fd >= 0) { + if (!sparse || data_size == 0 || ftruncate(fd, (off_t)data_size) == 0) + ok = write_all(fd, data, data_size); + if (ok && metadata) + ok = file_restore_metadata_fd(fd, metadata); + } + } else { + char tmp[NAME_MAX]; + for (unsigned int i = 0; i < 100 && !ok; ++i) { + snprintf(tmp, sizeof(tmp), ".%s.tmp.%ld.%u", leaf, (long)getpid(), i); + fd = openat(dirfd, tmp, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600); + if (fd < 0) + continue; + if (sparse && data_size > 0) + ok = ftruncate(fd, (off_t)data_size) == 0; + if (ok || (!sparse || data_size == 0)) + ok = write_all(fd, data, data_size); + if (ok && metadata) + ok = file_restore_metadata_fd(fd, metadata); + if (close(fd) != 0) + ok = false; + fd = -1; + if (ok && renameat(dirfd, tmp, dirfd, leaf) != 0) + ok = false; + if (!ok) + unlinkat(dirfd, tmp, 0); + } + } + if (fd >= 0) + close(fd); + close(dirfd); + free(leaf); + return ok; +} + +bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size, + bool inplace, bool sparse) { + if (!path || (!data && data_size != 0) || has_path_traversal(path)) + return false; + return file_to_disk_secure(path, data, data_size, inplace, sparse, NULL); +} diff --git a/src/shared/file.h b/src/shared/file.h index 7f72116..23dac26 100644 --- a/src/shared/file.h +++ b/src/shared/file.h @@ -6,8 +6,10 @@ #include "file_types.h" #include #include +#include -/* File/FileMetadata lifecycle and local disk helpers. */ +/* File/FileMetadata lifecycle, local disk helpers, and secure filesystem + primitives shared by the send/receive pipelines. */ File* file_create(const char* path); void file_destroy(void* item); @@ -18,6 +20,17 @@ FileMetadata* file_metadata_create(const struct stat* stats); void file_metadata_destroy(void* metadata); bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size, bool inplace, bool sparse); + +/* A configured fd without a canonical identity deliberately rejects paths. */ bool file_set_authorized_root(int fd, const char* canonical_path); +/* Secure path/filesystem primitives (symlink-safe, O_NOFOLLOW, root-confined). */ +bool file_path_exists_secure(const char* path); +bool file_stat_secure(const char* path, struct stat* st); +int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs); +bool file_ensure_directory_secure(const char* path); +bool file_rename_secure(const char* old_path, const char* new_path); +bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size, + bool inplace, bool sparse, const FileMetadata* metadata); + #endif diff --git a/src/shared/file_receive.c b/src/shared/file_receive.c index f809b7c..a610d67 100644 --- a/src/shared/file_receive.c +++ b/src/shared/file_receive.c @@ -13,16 +13,13 @@ #include "data.h" #include "delta.h" #include "file.h" -#include "file_store.h" #include "log.h" #include "metadata.h" #include "protocol.h" #include "utils.h" -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] == '/'); -} +#define MAX_SERVER_DELETE_COUNT 100000U +#define MAX_FILE_DATA_SIZE MAX_RECEIVE_FILE_SIZE bool file_save_to_disk(const char* root_directory, const File* file, const Config* config) { bool backup_enabled = config && config->backup; @@ -31,9 +28,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))) { @@ -53,45 +52,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); @@ -101,99 +75,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 (!file_store_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 || !file_ensure_directory_secure(dirname(parent_copy))) + goto fail; + free(parent_copy); + parent_copy = NULL; + if (!file_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 = file_store_write_secure(disk_path, file->data->data, file->data->size, inplace, sparse, - file->metadata); + bool ok = file_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, @@ -235,7 +160,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); @@ -245,7 +170,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); @@ -264,8 +189,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) { @@ -297,8 +229,16 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_ } } + Data* replacement = data_create(new_data, (size_t)new_size); + if (replacement == NULL) { + file_destroy(file); + free(old_data); + delta_signature_destroy(sig); + send_status(fd, STATUS_ERROR); + return NULL; + } data_destroy(file->data); - file->data = data_create(new_data, (size_t)new_size); + file->data = replacement; free(old_data); delta_signature_destroy(sig); @@ -325,7 +265,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); *failed = true; @@ -333,13 +273,19 @@ 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); *failed = true; return NULL; } + if (uncompressed->size > MAX_FILE_DATA_SIZE) { + data_destroy(uncompressed); + file_destroy(file); + send_status(fd, STATUS_ERROR); + return NULL; + } file_data = uncompressed; } @@ -350,11 +296,16 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_ delta_signature_destroy(sig); free(old_data); + send_status(fd, STATUS_ERROR); *failed = true; return NULL; } 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) { @@ -374,6 +325,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); @@ -381,22 +338,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 = file_store_open_secure_parent(full_path, &leaf); - 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 = file_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) { + 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; @@ -440,7 +400,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) { @@ -487,19 +447,25 @@ 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); return NULL; } 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); return NULL; } + if (uncompressed->size > MAX_FILE_DATA_SIZE) { + data_destroy(uncompressed); + file_destroy(file); + send_status(fd, STATUS_ERROR); + return NULL; + } file_data = uncompressed; } @@ -529,18 +495,23 @@ 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); return NULL; } + if (file_data_uncompressed->size > MAX_FILE_DATA_SIZE) { + data_destroy(file_data_uncompressed); + file_destroy(file); + return NULL; + } file_data = file_data_uncompressed; } data_destroy(file->data); @@ -549,16 +520,26 @@ File* file_receive(const Config* config, int file_descriptor) { } 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); @@ -568,21 +549,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(config->receive_root_directory, manifest); + 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; } diff --git a/src/shared/file_send.c b/src/shared/file_send.c index 95fcaeb..5757ce1 100644 --- a/src/shared/file_send.c +++ b/src/shared/file_send.c @@ -19,7 +19,7 @@ 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; diff --git a/src/shared/file_store.c b/src/shared/file_store.c index 73aa2c6..57f23bb 100644 --- a/src/shared/file_store.c +++ b/src/shared/file_store.c @@ -22,8 +22,12 @@ static bool path_is_within_root(const char* root, const char* path) { bool file_store_set_authorized_root(int fd, const char* canonical_path) { char* new_path = canonical_path ? str_dup(canonical_path) : NULL; - if (canonical_path && !new_path) + if (canonical_path && !new_path) { + authorized_root_fd = -1; + free(authorized_root_path); + authorized_root_path = NULL; return false; + } free(authorized_root_path); authorized_root_path = new_path; authorized_root_fd = fd; @@ -42,9 +46,19 @@ int file_store_open_secure_parent(const char* path, char** leaf_out) { 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_length = strlen(authorized_root_path); char* relative = str_dup(path + root_length); if (!relative) { @@ -68,10 +82,18 @@ int file_store_open_secure_parent(const char* path, char** leaf_out) { 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 (next < 0 && errno == ENOENT && mkdirat(fd, component, 0755) == 0) - next = openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (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); diff --git a/src/shared/metadata.c b/src/shared/metadata.c index 49052ca..0c37309 100644 --- a/src/shared/metadata.c +++ b/src/shared/metadata.c @@ -51,6 +51,8 @@ FileMetadata* metadata_from_buf(char** buf) { int32_t present; memcpy(&present, *buf, sizeof(present)); *buf += sizeof(present); + if (present != 0 && present != 1) + return NULL; if (!present) return NULL; FileMetadata* m = malloc(sizeof(FileMetadata)); @@ -76,10 +78,15 @@ FileMetadata* metadata_from_buf(char** buf) { memcpy(&mtime_nsec, *buf, sizeof(mtime_nsec)); *buf += sizeof(mtime_nsec); m->mtime_nsec = (long)mtime_nsec; + if (present != 1 || mtime_nsec < 0 || mtime_nsec >= 1000000000LL || mode < 0 || uid < 0 || + gid < 0) { + free(m); + return NULL; + } return m; } -bool metadata_send(int file_descriptor, FileMetadata* m) { +bool metadata_send(int file_descriptor, const FileMetadata* m) { if (m == NULL) { int32_t zero = 0; return send_n_data(file_descriptor, &zero, sizeof(zero)); @@ -175,7 +182,8 @@ FileMetadata* metadata_receive(int file_descriptor, int* ok) { void file_restore_metadata(const char* path, const FileMetadata* metadata) { if (metadata == NULL) return; - if (chmod(path, metadata->mode & 07777 & ~(S_ISUID | S_ISGID)) != 0) + mode_t safe_mode = metadata->mode & 0777 & ~(S_IWGRP | S_IWOTH); + if (chmod(path, safe_mode) != 0) log_message(LOG_LEVEL_WARNING, "Failed to chmod %s: %s", path, strerror(errno)); /* Never apply client-supplied ownership. The descriptor API below is the receiver write path; retain this legacy API only for compatibility. */ @@ -192,7 +200,8 @@ bool file_restore_metadata_fd(int fd, const FileMetadata* metadata) { if (fd < 0 || metadata == NULL) return metadata == NULL; bool ok = true; - if (fchmod(fd, metadata->mode & 07777 & ~(S_ISUID | S_ISGID)) != 0) + mode_t safe_mode = metadata->mode & 0777 & ~(S_IWGRP | S_IWOTH); + if (fchmod(fd, safe_mode) != 0) ok = false; /* Client uid/gid values are deliberately not authoritative. */ struct timespec times[2] = {{.tv_sec = 0, .tv_nsec = UTIME_OMIT}, diff --git a/src/shared/metadata.h b/src/shared/metadata.h index f6f90aa..b8d7ba9 100644 --- a/src/shared/metadata.h +++ b/src/shared/metadata.h @@ -27,7 +27,7 @@ void metadata_to_buf(char** buf, const FileMetadata* m); FileMetadata* metadata_from_buf(char** buf); -bool metadata_send(int file_descriptor, FileMetadata* m); +bool metadata_send(int file_descriptor, const FileMetadata* m); FileMetadata* metadata_receive(int file_descriptor, int* ok); void file_restore_metadata(const char* path, const FileMetadata* metadata); bool file_restore_metadata_fd(int fd, const FileMetadata* metadata); diff --git a/src/shared/protocol.c b/src/shared/protocol.c index d7d7b5f..d3e4aa3 100644 --- a/src/shared/protocol.c +++ b/src/shared/protocol.c @@ -13,7 +13,7 @@ #define RECEIVE_TIMEOUT_SEC 60 /* 60 second per-message timeout */ #define SEND_TIMEOUT_SEC 60 -#define MAX_CONNECTION_MEMORY (1024ULL * 1024 * 1024) /* 1 GB total per connection */ +#define MAX_CONNECTION_MEMORY (256ULL * 1024 * 1024) /* bounded cumulative receive budget */ static __thread int io_read_fd = -1; static __thread int io_write_fd = -1; @@ -22,11 +22,18 @@ static __thread ProtocolSession* bound_session; static __thread ProtocolSession legacy_io_session = {.read_fd = -1, .write_fd = -1}; static unsigned long long io_bwlimit = 0; -static long long bw_tokens = 0; -static struct timespec bw_last_refill = {0, 0}; static mtx_t bw_mutex; static once_flag bw_mutex_once = ONCE_FLAG_INIT; +static unsigned long long global_bwlimit(void); + +void protocol_release_memory(size_t charge) { + ProtocolSession* session = bound_session ? bound_session : &legacy_io_session; + if ((unsigned long long)charge >= session->total_allocated_bytes) + session->total_allocated_bytes = 0; + else + session->total_allocated_bytes -= charge; +} void io_set_fds(int read_fd, int write_fd) { bound_session = NULL; io_read_fd = read_fd; @@ -38,7 +45,7 @@ void io_set_fds(int read_fd, int write_fd) { legacy_io_session.write_fd = write_fd; legacy_io_session.ssl = NULL; legacy_io_session.total_allocated_bytes = 0; - protocol_session_set_bwlimit(&legacy_io_session, io_bwlimit); + protocol_session_set_bwlimit(&legacy_io_session, global_bwlimit()); } void protocol_session_init(ProtocolSession* session, int read_fd, int write_fd) { @@ -47,8 +54,7 @@ void protocol_session_init(ProtocolSession* session, int read_fd, int write_fd) memset(session, 0, sizeof(*session)); session->read_fd = read_fd; session->write_fd = write_fd; - if (io_bwlimit) - protocol_session_set_bwlimit(session, io_bwlimit); + protocol_session_set_bwlimit(session, global_bwlimit()); } void protocol_session_bind(ProtocolSession* session) { @@ -68,20 +74,29 @@ static void bw_mutex_init(void) { mtx_init(&bw_mutex, mtx_plain); } +static unsigned long long global_bwlimit(void) { + unsigned long long limit; + call_once(&bw_mutex_once, bw_mutex_init); + mtx_lock(&bw_mutex); + limit = io_bwlimit; + mtx_unlock(&bw_mutex); + return limit; +} + void io_set_bwlimit(unsigned long long bytes_per_sec) { call_once(&bw_mutex_once, bw_mutex_init); mtx_lock(&bw_mutex); - io_bwlimit = bytes_per_sec; - bw_tokens = (long long)io_bwlimit; - clock_gettime(CLOCK_MONOTONIC, &bw_last_refill); + io_bwlimit = + bytes_per_sec > (unsigned long long)LLONG_MAX ? (unsigned long long)LLONG_MAX : bytes_per_sec; mtx_unlock(&bw_mutex); } void protocol_session_set_bwlimit(ProtocolSession* session, unsigned long long bytes_per_sec) { if (!session) return; - session->bwlimit = bytes_per_sec; - session->bw_tokens = (long long)bytes_per_sec; + session->bwlimit = + bytes_per_sec > (unsigned long long)LLONG_MAX ? (unsigned long long)LLONG_MAX : bytes_per_sec; + session->bw_tokens = (long long)session->bwlimit; struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); session->bw_last_refill_sec = now.tv_sec; @@ -139,9 +154,9 @@ static ProtocolSession* legacy_session(int read_fd, int write_fd) { legacy_io_session.read_fd = target_read_fd; legacy_io_session.write_fd = target_write_fd; legacy_io_session.total_allocated_bytes = 0; - protocol_session_set_bwlimit(&legacy_io_session, io_bwlimit); - } else if (legacy_io_session.bwlimit != io_bwlimit) { - protocol_session_set_bwlimit(&legacy_io_session, io_bwlimit); + protocol_session_set_bwlimit(&legacy_io_session, global_bwlimit()); + } else if (legacy_io_session.bwlimit != global_bwlimit()) { + protocol_session_set_bwlimit(&legacy_io_session, global_bwlimit()); } legacy_io_session.ssl = io_ssl; return &legacy_io_session; @@ -167,6 +182,8 @@ static int deadline_remaining_ms(const struct timespec* deadline) { } bool protocol_send_n_data(ProtocolSession* session, const void* data, size_t data_size) { + if (!data && data_size != 0) + return false; log_message(LOG_LEVEL_DEBUG, " Sending n Data: %zu", data_size); if (!session) return false; @@ -208,6 +225,8 @@ bool protocol_send_n_data(ProtocolSession* session, const void* data, size_t dat } bw_throttle_session(session, (size_t)bytes_send); total_bytes_send += bytes_send; + if (session->ssl) + wait_events = POLLOUT; } log_message(LOG_LEVEL_DEBUG, " Send n Data: %zu", total_bytes_send); return true; @@ -226,20 +245,22 @@ bool protocol_receive_n_data(ProtocolSession* session, void* data, size_t data_s size_t total_bytes_received = 0; short wait_events = POLLIN; while (total_bytes_received < data_size) { - struct pollfd pfd = {.fd = fd, .events = wait_events}; - int poll_result = poll(&pfd, 1, deadline_remaining_ms(&deadline)); - if (poll_result == 0) { - log_message(LOG_LEVEL_ERROR, "Receive timeout after %ds", RECEIVE_TIMEOUT_SEC); - return false; + if (!session->ssl || SSL_pending(session->ssl) == 0) { + struct pollfd pfd = {.fd = fd, .events = wait_events}; + int poll_result = poll(&pfd, 1, deadline_remaining_ms(&deadline)); + if (poll_result == 0) { + log_message(LOG_LEVEL_ERROR, "Receive timeout after %ds", RECEIVE_TIMEOUT_SEC); + return false; + } + if (poll_result < 0) { + if (errno == EINTR) + continue; + return false; + } + /* POLLHUP may accompany the final readable bytes on pipes/sockets. */ + if (pfd.revents & (POLLERR | POLLNVAL)) + return false; } - if (poll_result < 0) { - if (errno == EINTR) - continue; - return false; - } - /* POLLHUP may accompany the final readable bytes on pipes/sockets. */ - if (pfd.revents & (POLLERR | POLLNVAL)) - return false; ssize_t bytes_received; if (session->ssl) @@ -262,7 +283,9 @@ bool protocol_receive_n_data(ProtocolSession* session, void* data, size_t data_s log_message(LOG_LEVEL_ERROR, "Could not receive bytes"); return false; } - total_bytes_received += bytes_received; + total_bytes_received += (size_t)bytes_received; + if (session->ssl) + wait_events = POLLIN; } log_message(LOG_LEVEL_DEBUG, " Received n Data: %zu", total_bytes_received); return true; @@ -326,6 +349,11 @@ char* protocol_receive_str(ProtocolSession* session) { free(data); return NULL; } + if (memchr(data, '\0', size) != NULL) { + free(data); + log_message(LOG_LEVEL_ERROR, "Received string contains an embedded NUL"); + return NULL; + } data[size] = '\0'; session->total_allocated_bytes += size + 1; log_message(LOG_LEVEL_DEBUG, "Received String: %s", data); @@ -333,6 +361,10 @@ char* protocol_receive_str(ProtocolSession* session) { } bool protocol_send_data(ProtocolSession* session, const Data* data) { + if (!data || (!data->data && data->size != 0)) + return false; + if (!session) + return false; unsigned long long data_size = data->size; if (!protocol_send_n_data(session, &data_size, sizeof(unsigned long long))) return false; @@ -342,11 +374,13 @@ bool protocol_send_data(ProtocolSession* session, const Data* data) { return true; } -Data* protocol_receive_data(ProtocolSession* session) { +Data* protocol_receive_data_limited(ProtocolSession* session, unsigned long long maximum_size) { + if (!session) + return NULL; unsigned long long size = 0; if (!protocol_receive_n_data(session, &size, sizeof(unsigned long long))) return NULL; - if (size > MAX_DATA_PAYLOAD_SIZE) { + if (size > MAX_DATA_PAYLOAD_SIZE || size > maximum_size) { log_message(LOG_LEVEL_ERROR, "Data size %llu exceeds maximum %llu", size, (unsigned long long)MAX_DATA_PAYLOAD_SIZE); return NULL; @@ -369,12 +403,17 @@ Data* protocol_receive_data(ProtocolSession* session) { log_message(LOG_LEVEL_DEBUG, "Received %lld data", size); Data* result = data_create(data, (size_t)size); if (!result) { - free(data); session->total_allocated_bytes -= allocation_size; + return NULL; } + result->protocol_charge = allocation_size; return result; } +Data* protocol_receive_data(ProtocolSession* session) { + return protocol_receive_data_limited(session, MAX_DATA_PAYLOAD_SIZE); +} + bool protocol_send_int(ProtocolSession* session, int data) { if (!protocol_send_n_data(session, &data, sizeof(int))) return false; @@ -413,7 +452,10 @@ bool send_data(int fd, const Data* data) { return protocol_send_data(legacy_session(-1, fd), data); } Data* receive_data(int fd) { - return protocol_receive_data(legacy_session(fd, -1)); + return protocol_receive_data_limited(legacy_session(fd, -1), MAX_DATA_PAYLOAD_SIZE); +} +Data* receive_data_limited(int fd, unsigned long long maximum_size) { + return protocol_receive_data_limited(legacy_session(fd, -1), maximum_size); } bool send_int(int fd, int data) { return protocol_send_int(legacy_session(-1, fd), data); diff --git a/src/shared/protocol.h b/src/shared/protocol.h index b5811db..8db196a 100644 --- a/src/shared/protocol.h +++ b/src/shared/protocol.h @@ -10,6 +10,8 @@ /* Maximum allowed data payload size for receive_data (100 MB) */ #define MAX_DATA_PAYLOAD_SIZE (100ULL * 1024 * 1024) +/* Maximum uncompressed file payload accepted by the receiver. */ +#define MAX_RECEIVE_FILE_SIZE (64ULL * 1024 * 1024) /* Maximum chunk size (64 MB) — prevents unbounded allocation from the wire */ #define MAX_CHUNK_SIZE (64ULL * 1024 * 1024) @@ -69,6 +71,7 @@ bool protocol_send_str(ProtocolSession* session, const char* data); char* protocol_receive_str(ProtocolSession* session); bool protocol_send_data(ProtocolSession* session, const Data* data); Data* protocol_receive_data(ProtocolSession* session); +Data* protocol_receive_data_limited(ProtocolSession* session, unsigned long long maximum_size); bool protocol_send_int(ProtocolSession* session, int data); bool protocol_receive_int(ProtocolSession* session, int* data); bool protocol_send_status(ProtocolSession* session, Status status); @@ -80,6 +83,7 @@ bool send_str(int file_descriptor, const char* data); char* receive_str(int file_descriptor); bool send_data(int file_descriptor, const Data* data); Data* receive_data(int file_descriptor); +Data* receive_data_limited(int file_descriptor, unsigned long long maximum_size); bool send_int(int file_descriptor, int data); bool receive_int(int file_descriptor, int* data); bool send_status(int file_descriptor, Status status); diff --git a/src/shared/transport_tls.c b/src/shared/transport_tls.c index 2c5a4ff..ec89f4e 100644 --- a/src/shared/transport_tls.c +++ b/src/shared/transport_tls.c @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include #include @@ -32,6 +35,10 @@ static void log_ssl_errors(void) { static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key, const char* ca_path) { + if (!is_server && !ca_path) { + log_message(LOG_LEVEL_ERROR, "TLS clients require a CA certificate path"); + return NULL; + } const SSL_METHOD* method = is_server ? TLS_server_method() : TLS_client_method(); SSL_CTX* ctx = SSL_CTX_new(method); if (!ctx) { @@ -40,9 +47,23 @@ static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key return NULL; } - SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); + if (SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) != 1) { + SSL_CTX_free(ctx); + return NULL; + } + if (SSL_CTX_set_cipher_list(ctx, "HIGH:!aNULL:!eNULL:!MD5:!RC4:!3DES") != 1) { + SSL_CTX_free(ctx); + return NULL; + } if (cert && key) { + struct stat key_stat; + if (stat(key, &key_stat) != 0 || !S_ISREG(key_stat.st_mode) || key_stat.st_uid != geteuid() || + (key_stat.st_mode & (S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH))) { + log_message(LOG_LEVEL_ERROR, "TLS private key must be owned by the current user and private"); + SSL_CTX_free(ctx); + return NULL; + } if (SSL_CTX_use_certificate_file(ctx, cert, SSL_FILETYPE_PEM) <= 0) { log_message(LOG_LEVEL_ERROR, "Failed to load certificate: %s", cert); log_ssl_errors(); @@ -84,12 +105,18 @@ static SSL* wrap_fd_with_ssl(int fd, SSL_CTX* ctx, bool is_server, const char* h log_message(LOG_LEVEL_ERROR, "Failed to create SSL object"); return NULL; } - SSL_set_fd(ssl, fd); + if (SSL_set_fd(ssl, fd) != 1) { + SSL_free(ssl); + return NULL; + } // Enable hostname verification for client connections when a hostname is provided. // Must be done before SSL_connect to take effect during the handshake. if (!is_server && hostname) { - SSL_set1_host(ssl, hostname); + if (SSL_set1_host(ssl, hostname) != 1) { + SSL_free(ssl); + return NULL; + } } // Retry SSL_accept/SSL_connect on WANT_READ/WANT_WRITE (non-blocking handshake) @@ -132,8 +159,10 @@ struct tls_child_ctx { static void tls_child_fn(int fd, void* arg) { struct tls_child_ctx* ctx = (struct tls_child_ctx*)arg; SSL* ssl = wrap_fd_with_ssl(fd, ctx->ssl_ctx, true, NULL); - if (!ssl) + if (!ssl) { + io_set_ssl(NULL); return; + } io_set_ssl(ssl); ctx->handler(fd); SSL_shutdown(ssl); @@ -149,12 +178,19 @@ 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) { - if (!tcp_connect_socket(client, host, port)) + if (!tcp_connect_socket(client, host, port)) { + if (client->file_descriptor >= 0) + close(client->file_descriptor); + client->file_descriptor = -1; return false; + } SSL_CTX* ctx = create_ssl_ctx(false, cert_path, key_path, ca_path); - if (!ctx) + if (!ctx) { + close(client->file_descriptor); + client->file_descriptor = -1; return false; + } client->ssl_ctx = ctx; // Pass the server hostname for TLS hostname verification (SSL_set1_host @@ -164,6 +200,8 @@ bool client_connect_tls(Client* client, char* host, int port, const char* cert_p if (!ssl) { SSL_CTX_free(ctx); client->ssl_ctx = NULL; + close(client->file_descriptor); + client->file_descriptor = -1; return false; } diff --git a/src/shared/utils.c b/src/shared/utils.c index 26a6213..49c1367 100644 --- a/src/shared/utils.c +++ b/src/shared/utils.c @@ -1,4 +1,3 @@ -#include "log.h" #include "utils.h" #include "array_list.h" #include "libgen.h" @@ -8,62 +7,117 @@ #include #include #include +#include #include #include static int authorized_root_fd = -1; +static char* authorized_root_path; + +bool utils_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; +} void utils_set_authorized_root_fd(int fd) { - authorized_root_fd = fd; + (void)utils_set_authorized_root(fd, 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 int open_authorized_destination(const char* dest_root) { + if (authorized_root_fd < 0 || !authorized_root_path || !dest_root || + !path_is_within_root(authorized_root_path, dest_root)) + return -1; + + int dirfd = dup(authorized_root_fd); + if (dirfd < 0) + return -1; + + const char* relative_path = dest_root + strlen(authorized_root_path); + while (*relative_path == '/') + relative_path++; + char* relative = str_dup(*relative_path ? relative_path : "."); + if (!relative) { + close(dirfd); + return -1; + } + + char* saveptr = NULL; + char* component = strtok_r(relative, "/", &saveptr); + while (component) { + if (strcmp(component, "..") == 0) { + free(relative); + close(dirfd); + return -1; + } + if (strcmp(component, ".") == 0) { + component = strtok_r(NULL, "/", &saveptr); + continue; + } + int next = openat(dirfd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (next < 0) { + free(relative); + close(dirfd); + return -1; + } + close(dirfd); + dirfd = next; + component = strtok_r(NULL, "/", &saveptr); + } + + free(relative); + return dirfd; } bool mkdir_r(const char* path) { - size_t path_len = strlen(path); - char* path_duplicate = malloc(path_len + 1); - if (!path_duplicate) + if (!path || *path == '\0') return false; - memcpy(path_duplicate, path, path_len + 1); - size_t capacity = path_len + 2; - char* path_current = (char*)malloc(capacity * sizeof(char)); - if (!path_current) { - free(path_duplicate); + char* duplicate = str_dup(path); + if (!duplicate) + return false; + int dirfd = open(path[0] == '/' ? "/" : ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); + if (dirfd < 0) { + free(duplicate); return false; } - char* path_current_position = path_current; - if (path[0] == '/') { - path_current[0] = '/'; - path_current[1] = '\0'; - path_current_position += 1; - } else { - path_current[0] = '\0'; - } - const char* delimiter = "/"; - char* saveptr; - const char* part = strtok_r(path_duplicate, delimiter, &saveptr); bool ok = true; - while (part != NULL) { - size_t part_len = strlen(part); - if ((size_t)(path_current_position - path_current) + part_len + 2 > capacity) { + char* saveptr = NULL; + char* component = strtok_r(duplicate, "/", &saveptr); + while (component) { + if (strcmp(component, "..") == 0) { ok = false; break; } - memcpy(path_current_position, part, part_len); - path_current_position += part_len; - path_current_position[0] = '/'; - path_current_position[1] = '\0'; - path_current_position++; - struct stat st; - if (stat(path_current, &st) != 0) { - if (mkdir(path_current, 0755) != 0) { - log_perror("Could not create directory"); + if (strcmp(component, ".") != 0) { + int next = openat(dirfd, component, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); + if (next < 0 && errno == ENOENT) { + if (mkdirat(dirfd, component, 0755) == 0 || errno == EEXIST) + next = openat(dirfd, component, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); + } + if (next < 0) { ok = false; break; } + close(dirfd); + dirfd = next; } - part = strtok_r(NULL, delimiter, &saveptr); + component = strtok_r(NULL, "/", &saveptr); } - free(path_duplicate); - free(path_current); + close(dirfd); + free(duplicate); return ok; } @@ -144,7 +198,8 @@ static bool is_dir_in_manifest(const char* rel_path, ArrayList* manifest) { return false; } -static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifest) { +static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifest, + size_t max_delete, size_t* deleted_count) { int scanfd = dup(dirfd); if (scanfd < 0) return false; @@ -153,15 +208,20 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes close(scanfd); return false; } - bool all_removed = true; bool operation_ok = true; const struct dirent* entry; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; char* child_rel = path_cat((char*)rel_path, entry->d_name); + if (!child_rel) { + operation_ok = false; + continue; + } struct stat st; if (fstatat(dirfd, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0) { + if (errno != ENOENT) + operation_ok = false; free(child_rel); continue; } @@ -174,14 +234,24 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes int childfd = openat(dirfd, entry->d_name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); bool child_removed = false; if (childfd >= 0) { - child_removed = delete_extras_fd(childfd, child_rel, manifest); + child_removed = delete_extras_fd(childfd, child_rel, manifest, max_delete, deleted_count); + if (!child_removed) + operation_ok = false; close(childfd); - } - if (child_removed && !is_dir_in_manifest(child_rel, manifest) && - unlinkat(dirfd, entry->d_name, AT_REMOVEDIR) != 0 && errno != ENOENT) { + } else if (errno != ENOENT) { operation_ok = false; - } else if (!child_removed) { - all_removed = false; + } + if (child_removed && !is_dir_in_manifest(child_rel, manifest)) { + if (*deleted_count >= max_delete) { + operation_ok = false; + } else { + if (unlinkat(dirfd, entry->d_name, AT_REMOVEDIR) != 0) { + if (errno != ENOENT) + operation_ok = false; + } else { + (*deleted_count)++; + } + } } } else { // Check if relative path is in manifest @@ -193,38 +263,59 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes } } if (!found) { - if (unlinkat(dirfd, entry->d_name, 0) != 0 && errno != ENOENT) + if (*deleted_count >= max_delete) { operation_ok = false; + free(child_rel); + continue; + } + if (unlinkat(dirfd, entry->d_name, 0) != 0) { + if (errno != ENOENT) + operation_ok = false; + } else { + (*deleted_count)++; + } fprintf(stderr, " Deleted: %s\n", child_rel); - } else { - all_removed = false; } } free(child_rel); } closedir(dir); - (void)all_removed; return operation_ok; } -bool delete_extras(const char* dest_root, ArrayList* manifest) { - int rootfd = authorized_root_fd >= 0 - ? dup(authorized_root_fd) - : open(dest_root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); +bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t max_delete) { + if (!manifest) + return false; + int rootfd; + if (authorized_root_fd >= 0) { + if (authorized_root_path) + rootfd = open_authorized_destination(dest_root); + else if (dest_root == NULL) + rootfd = dup(authorized_root_fd); + else + rootfd = -1; + } else { + rootfd = open(dest_root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + } if (rootfd < 0) return false; - bool ok = delete_extras_fd(rootfd, "", manifest); + size_t deleted_count = 0; + bool ok = delete_extras_fd(rootfd, "", manifest, max_delete, &deleted_count); if (close(rootfd) != 0) ok = false; return ok; } +bool delete_extras(const char* dest_root, ArrayList* manifest) { + return delete_extras_limited(dest_root, manifest, SIZE_MAX); +} + bool has_path_traversal(const char* path) { if (!path) - return false; + return true; char* dup = str_dup(path); if (!dup) - return false; + return true; char* saveptr; const char* part = strtok_r(dup, "/", &saveptr); while (part) { @@ -256,6 +347,8 @@ char* path_cat(const char* path1, const char* path2) { offset = 1; path2_len -= 1; } + if (path1_len > SIZE_MAX - path2_len - 2) + return NULL; char* new_path = malloc(path1_len + path2_len + 2); if (new_path == NULL) return NULL; diff --git a/src/shared/utils.h b/src/shared/utils.h index 8ab4a64..757d26f 100644 --- a/src/shared/utils.h +++ b/src/shared/utils.h @@ -2,6 +2,7 @@ #define UTILS_H #include "array_list.h" +#include #include bool mkdir_r(const char* path); @@ -9,6 +10,10 @@ char* str_dup(const char* string); char* path_cat(const char* path1, const char* path2); bool glob_match(const char* pattern, const char* str); bool delete_extras(const char* dest_root, ArrayList* manifest); +bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t max_delete); +bool utils_set_authorized_root(int fd, const char* canonical_path); +/* The fd-only compatibility form is fail-closed for path-based operations; + * callers should use utils_set_authorized_root with the canonical identity. */ void utils_set_authorized_root_fd(int fd); bool has_path_traversal(const char* path); bool utils_valid_batch_path(const char* path); diff --git a/tests/integration/common.py b/tests/integration/common.py index 08cc9c9..2e9653a 100644 --- a/tests/integration/common.py +++ b/tests/integration/common.py @@ -25,7 +25,9 @@ class ServerManager: def start(self, extra_args=None): self.stop() self._port = _find_free_port() - cmd = SERVER_CMD + ["-p", str(self._port)] + # Plain TCP is intentionally explicit in the server; integration tests + # exercise that opt-in mode rather than relying on the secure default. + cmd = SERVER_CMD + ["-p", str(self._port), "--allow-unauthenticated"] if extra_args: cmd += extra_args self._proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) diff --git a/tests/integration/test_tls.py b/tests/integration/test_tls.py index 06e8caa..29074b2 100644 --- a/tests/integration/test_tls.py +++ b/tests/integration/test_tls.py @@ -102,6 +102,7 @@ class TestTLSBasic: with ServerManager() as server: server.start(extra_args=[ "--tls", "--cert", certs["server_cert"], "--key", certs["server_key"], + "--ca", certs["ca"], "--client-cn", "fastsync-client", ]) result, dur = run_client( SOURCE_DIR, DEST_DIR, @@ -125,6 +126,7 @@ class TestTLSBasic: with ServerManager() as server: server.start(extra_args=[ "--tls", "--cert", certs["server_cert"], "--key", certs["server_key"], + "--ca", certs["ca"], "--client-cn", "fastsync-client", ]) result, dur = run_client( SOURCE_DIR, DEST_DIR, @@ -149,6 +151,7 @@ class TestTLSBasic: with ServerManager() as server: server.start(extra_args=[ "--tls", "--cert", certs["server_cert"], "--key", certs["server_key"], + "--ca", certs["ca"], "--client-cn", "fastsync-client", ]) result, dur = run_client( SOURCE_DIR, DEST_DIR, diff --git a/tests/test_array_list.c b/tests/test_array_list.c index 1bf05d9..964458d 100644 --- a/tests/test_array_list.c +++ b/tests/test_array_list.c @@ -17,6 +17,8 @@ void test_array_list() { // Test adding int* val1 = malloc(sizeof(int)); + if (!val1) + return; *val1 = 42; array_list_add(list, val1); EXPECT_EQ_INT(list->size, 1); @@ -26,6 +28,8 @@ void test_array_list() { // Initial capacity is 100. Let's add 105 elements. for (int i = 0; i < 105; i++) { int* val = malloc(sizeof(int)); + if (!val) + return; *val = i; array_list_add(list, val); } diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 014c6d3..656fd40 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -46,6 +46,8 @@ static void test_validate_config_tls_requirements() { cfg->tls_cert = str_dup("cert.pem"); EXPECT_FALSE(validate_config(cfg)); cfg->tls_key = str_dup("key.pem"); + EXPECT_FALSE(validate_config(cfg)); + cfg->tls_ca = str_dup("ca.pem"); EXPECT_TRUE(validate_config(cfg)); config_delete(cfg); } diff --git a/tests/test_compression.c b/tests/test_compression.c index a859e6a..a234e00 100644 --- a/tests/test_compression.c +++ b/tests/test_compression.c @@ -13,6 +13,8 @@ static void test_data_compress_decompress_roundtrip() { size_t len = strlen(original); char* buf = malloc(len); + if (!buf) + return; memcpy(buf, original, len); Data* original_data = data_create(buf, len); EXPECT_NOT_NULL(original_data); diff --git a/tests/test_property.c b/tests/test_property.c index 6327bd6..2a88dd5 100644 --- a/tests/test_property.c +++ b/tests/test_property.c @@ -14,6 +14,8 @@ static Data* random_data(int min_size, int max_size) { int size = min_size + rand() % (max_size - min_size + 1); char* buf = malloc(size); + if (!buf) + return NULL; for (int i = 0; i < size; i++) buf[i] = (char)(rand() % 256); return data_create(buf, size); @@ -81,6 +83,8 @@ static void test_property_chunk_roundtrip() { int content_len = 1 + rand() % 4096; char* content = malloc(content_len); + if (!content) + return; for (int i = 0; i < content_len; i++) content[i] = (char)(rand() % 256); diff --git a/tests/test_queue.c b/tests/test_queue.c index 3ab6d3c..4c90153 100644 --- a/tests/test_queue.c +++ b/tests/test_queue.c @@ -122,6 +122,8 @@ static void test_queue_destroyer() { for (int i = 0; i < 3; i++) { int* val = malloc(sizeof(int)); + if (!val) + break; *val = i; queue_enqueue(q, val); } @@ -181,6 +183,8 @@ static void test_queue_multithreaded() { for (int i = 1; i <= 100; i++) { int* val = malloc(sizeof(int)); + if (!val) + break; *val = i; queue_enqueue_multithreaded(q, val, &mutex, &cnd_empty, &cnd_full); } diff --git a/tests/test_stress.c b/tests/test_stress.c index c73f46e..bf0e3f3 100644 --- a/tests/test_stress.c +++ b/tests/test_stress.c @@ -32,6 +32,8 @@ static int mpmc_producer_func(void* arg) { ProducerCtx* ctx = (ProducerCtx*)arg; for (int i = 1; i <= ITEMS_PER_PRODUCER; i++) { int* val = malloc(sizeof(int)); + if (!val) + return thrd_error; *val = ctx->producer_id * ITEMS_PER_PRODUCER + i; queue_enqueue_multithreaded(ctx->q, val, ctx->mutex, ctx->cnd_empty, ctx->cnd_full); } @@ -121,6 +123,8 @@ static int bp_producer_func(void* arg) { BackpressureCtx* ctx = (BackpressureCtx*)arg; for (int i = 0; i < 5; i++) { int* val = malloc(sizeof(int)); + if (!val) + return thrd_error; *val = i + 1; queue_enqueue_multithreaded(ctx->q, val, ctx->mutex, ctx->cnd_empty, ctx->cnd_full); ctx->items_sent++; @@ -194,6 +198,8 @@ static void test_queue_rapid_create_destroy() { for (int j = 0; j < 3; j++) { int* val = malloc(sizeof(int)); + if (!val) + break; *val = j; queue_enqueue(q, val); }