Compare commits

..

11 Commits

Author SHA1 Message Date
TapTap 0b68974b42 docs: add 🔀 Alt Arg status for features with different flag semantics
CI / lint (pull_request) Successful in 2m19s
CI / sanitizers (address) (pull_request) Successful in 36s
CI / sanitizers (undefined) (pull_request) Successful in 35s
CI / fuzz-build (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 32s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 33s
2026-08-02 09:01:11 +02:00
TapTap df41e8bd06 docs: add rsync feature compatibility matrix 2026-08-02 09:01:11 +02:00
TapTap 9d67fcbf92 Merge pull request 'Batch fix: security, bugs, quality, and features (v2)' (#198) from integration/all-fixes-v2 into dev 2026-07-30 19:38:26 +02:00
TapTap 261683aaff fix: null check in str_dup, const in file.c
CI / lint (pull_request) Successful in 2m16s
CI / sanitizers (address) (pull_request) Successful in 36s
CI / sanitizers (undefined) (pull_request) Successful in 37s
CI / fuzz-build (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 30s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 33s
2026-07-30 19:34:17 +02:00
TapTap 15a861747a style: apply clang-format
CI / lint (pull_request) Failing after 2m18s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-07-30 19:16:36 +02:00
TapTap 5f1b27c8ff fix: security #192 #187, bugs #194 #193, quality #195, features #174-#186
CI / lint (pull_request) Failing after 2s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-07-30 19:05:19 +02:00
TapTap 732cd67735 Merge remote-tracking branch 'origin/fix/features-v2' into integration/all-fixes-v2 2026-07-30 19:04:12 +02:00
TapTap 50fb7185a8 Merge remote-tracking branch 'origin/fix/quality-v2' into integration/all-fixes-v2 2026-07-30 19:04:12 +02:00
TapTap 3062927e07 Merge remote-tracking branch 'origin/fix/bugs-v2' into integration/all-fixes-v2 2026-07-30 19:04:12 +02:00
TapTap 88746c3396 fix: replace strcpy with bounded memory operations (#195)
Replace all uses of strcpy() with memcpy() + explicit NUL termination
or direct assignment for safety and consistency. No behavioral changes.

src/shared/file.c:
  - file_create(): strcpy -> memcpy + explicit NUL (buffer size known)

src/shared/utils.c:
  - mkdir_r(): strcpy -> memcpy for path_duplicate
  - mkdir_r(): strcpy(path_current, "/") -> direct assignment
  - mkdir_r(): strcpy loop -> memcpy + direct assignment
  - str_dup(): strcpy -> memcpy (buffer size known)

PR #196 (dry-run manifest refactoring) was already applied in a previous
commit - send_dry_run_manifest() and send_delete_manifest() helpers
already exist and are used by both send_files() and
send_files_multithreaded().
2026-07-30 18:49:52 +02:00
TapTap 80768d64d4 fix: security issues #192 #191 #190 #189 #188 #187 2026-07-30 18:49:25 +02:00
11 changed files with 400 additions and 42 deletions
+276
View File
@@ -0,0 +1,276 @@
# Rsync Feature Compatibility
This document maps rsync's full feature set to FastSync's current implementation status.
## Summary
| Status | Count | Description |
|--------|-------|-------------|
| ✅ Implemented | 42 | Feature works end-to-end |
| 🔀 Alt Arg | 3 | Functionality exists but under different flag/semantics |
| ⚠️ Partial | 31 | Flag parsed/stored but behavior incomplete |
| ❌ Not Implemented | 66 | Flag not recognized or no behavior |
| **Total** | **142** | |
---
## 1. General Options
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `-a`, `--archive` | Archive mode is -rlptgoD | 🔀 Alt Arg | Maps to -c -m -M (compression + multithread + metadata) |
| `-v`, `--verbose` | Increase verbosity | ✅ Implemented | Sets `log_level=DEBUG` |
| `-q`, `--quiet` | Suppress non-error messages | ✅ Implemented | `quiet` config field |
| `-h`, `--help` | Show help | ✅ Implemented | Prints usage and exits |
| `-V`, `--version` | Print version | ❌ Not Implemented | |
| `--info=FLAGS` | Fine-grained info verbosity | ⚠️ Partial | `info_level` stored, not wired |
| `--debug=FLAGS` | Fine-grained debug verbosity | ⚠️ Partial | `debug_level` stored, not wired |
| `--stderr=MODE` | Change stderr output mode | ❌ Not Implemented | |
| `--no-motd` | Suppress daemon MOTD | ❌ Not Implemented | |
| `--exclude=PATTERN` | Exclude files matching pattern | ✅ Implemented | Glob matching in scanner |
| `--include=PATTERN` | Include files matching pattern | ✅ Implemented | Glob matching in scanner |
| `-C`, `--cvs-exclude` | Auto-ignore CVS files | ⚠️ Partial | `cvs_exclude` stored, not wired |
## 2. Modifying Output
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `--stats` | Give transfer stats | ✅ Implemented | Prints file/byte counts |
| `-h`, `--human-readable` | Human-readable numbers | ⚠️ Partial | `human_readable` stored, not wired |
| `-i`, `--itemize-changes` | Per-file change summary | ⚠️ Partial | `itemize_changes` stored, not wired |
| `--progress` | Show progress | ✅ Implemented | Progress callback in sender |
| `-P` | Same as --partial --progress | ❌ Not Implemented | |
| `--out-format=FORMAT` | Custom output format | ⚠️ Partial | `out_format` stored, not wired |
| `--log-file=FILE` | Log to file | ✅ Implemented | `log_file` config field |
| `--log-file-format=FMT` | Log format | ❌ Not Implemented | |
| `--8-bit-output` | Leave high-bit chars unescaped | ❌ Not Implemented | |
| `--list-only` | List files instead of copying | ⚠️ Partial | `list_only` stored, not wired |
## 3. File Selection
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `--exclude-from=FILE` | Read exclude patterns from file | ✅ Implemented | Reads patterns from file |
| `--include-from=FILE` | Read include patterns from file | ✅ Implemented | Reads patterns from file |
| `--filter=RULE` | Add file-filtering rule | ⚠️ Partial | `filters` ArrayList stored, not wired |
| `--files-from=FILE` | Read source file list from file | ⚠️ Partial | `files_from` stored, not wired |
| `-0`, `--from0` | Delimit *-from files with NULs | ❌ Not Implemented | |
| `--max-size=SIZE` | Skip files larger than SIZE | ✅ Implemented | `max_size` in scanner |
| `--min-size=SIZE` | Skip files smaller than SIZE | ✅ Implemented | `min_size` in scanner |
| `-I`, `--ignore-times` | Don't skip files matching size+time | ❌ Not Implemented | |
| `--size-only` | Skip based on size only | ❌ Not Implemented | |
| `-@`, `--modify-window=NUM` | Mod-time comparison accuracy | ❌ Not Implemented | |
| `--existing` | Skip creating new files on receiver | ❌ Not Implemented | |
| `--ignore-existing` | Skip updating existing files | ❌ Not Implemented | |
| `--remove-source-files` | Sender removes synced files | ❌ Not Implemented | |
## 4. Directory Options
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `-r`, `--recursive` | Recurse into directories | ✅ Implemented | Default behavior |
| `-R`, `--relative` | Use relative path names | ⚠️ Partial | `relative` stored, not wired |
| `--no-implied-dirs` | Don't send implied dirs with -R | ❌ Not Implemented | |
| `-d`, `--dirs` | Transfer dirs without recursing | ❌ Not Implemented | |
| `--mkpath` | Create missing path components | ❌ Not Implemented | |
## 5. Transfer Modifications
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `-u`, `--update` | Skip files newer on receiver | ⚠️ Partial | `update` stored, not wired |
| `--inplace` | Update files in-place | ✅ Implemented | Direct write mode |
| `--append` | Append data to shorter files | ⚠️ Partial | `append` stored, not wired |
| `--append-verify` | Append with old-data checksum | ⚠️ Partial | `append_verify` stored, not wired |
| `-W`, `--whole-file` | Copy whole file (no delta) | ❌ Not Implemented | |
| `--block-size=SIZE` | Force checksum block-size | ⚠️ Partial | `delta_block_size` configurable |
## 6. Destination Handling
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `-n`, `--dry-run` | Trial run with no changes | ✅ Implemented | `dry_run` config field |
| `-b`, `--backup` | Make backups of overwritten files | ✅ Implemented | Backup before overwrite |
| `--backup-dir=DIR` | Backup directory hierarchy | ✅ Implemented | `backup_dir` config field |
| `--suffix=SUFFIX` | Backup suffix (default ~) | ✅ Implemented | `suffix` config field |
| `--delay-updates` | Put updated files in place at end | ❌ Not Implemented | |
## 7. Deletion
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `--delete` | Delete extraneous files from dest | ✅ Implemented | `use_delete` config field |
| `--delete-before` | Delete before transfer | ⚠️ Partial | `delete_before` stored, not wired |
| `--delete-during` | Delete during transfer | ❌ Not Implemented | |
| `--delete-delay` | Find deletions during, delete after | ❌ Not Implemented | |
| `--delete-after` | Delete after transfer | ⚠️ Partial | `delete_after` stored, not wired |
| `--delete-excluded` | Also delete excluded files | ⚠️ Partial | `delete_excluded` stored, not wired |
| `--max-delete=NUM` | Max files to delete | ⚠️ Partial | `max_delete` stored, not wired |
| `--ignore-errors` | Delete even with I/O errors | ❌ Not Implemented | |
| `--force` | Force deletion of non-empty dirs | ❌ Not Implemented | |
| `--prune-empty-dirs` | Prune empty dir chains | ⚠️ Partial | `prune_empty_dirs` stored, not wired |
## 8. Metadata Preservation
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `-M`, `--preserve` | Preserve file metadata | ✅ Implemented | Mode, uid, gid, mtime |
| `-p`, `--perms` | Preserve permissions | ✅ Implemented | Part of -M |
| `-o`, `--owner` | Preserve owner | ✅ Implemented | Part of -M |
| `-g`, `--group` | Preserve group | ✅ Implemented | Part of -M |
| `-t`, `--times` | Preserve modification times | ✅ Implemented | Part of -M |
| `-E`, `--executability` | Preserve executability | ❌ Not Implemented | |
| `--chmod=CHMOD` | Affect file permissions | ❌ Not Implemented | |
| `-A`, `--acls` | Preserve ACLs | ⚠️ Partial | `preserve_acls` stored, not wired |
| `-X`, `--xattrs` | Preserve extended attributes | ⚠️ Partial | `preserve_xattrs` stored, not wired |
| `-H`, `--hard-links` | Preserve hard links | ⚠️ Partial | `preserve_hard_links` stored, not wired |
| `-D` | Same as --devices --specials | 🔀 Alt Arg | Maps to --devices only (no --specials) |
| `--devices` | Preserve device files | ⚠️ Partial | `preserve_devices` stored, not wired |
| `--specials` | Preserve special files | ❌ Not Implemented | |
| `--copy-devices` | Copy device contents as file | ❌ Not Implemented | |
| `--write-devices` | Write to devices as files | ❌ Not Implemented | |
| `-U`, `--atimes` | Preserve access times | ❌ Not Implemented | |
| `-N`, `--crtimes` | Preserve create times | ❌ Not Implemented | |
| `-O`, `--omit-dir-times` | Omit dirs from --times | ❌ Not Implemented | |
| `-J`, `--omit-link-times` | Omit symlinks from --times | ❌ Not Implemented | |
| `--super` | Receiver attempts super-user activities | ❌ Not Implemented | |
| `--fake-super` | Store/recover privileged attrs via xattrs | ❌ Not Implemented | |
## 9. Symlink Handling
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `-l`, `--links` | Copy symlinks as symlinks | ✅ Implemented | Scanner symlink handling |
| `-L`, `--copy-links` | Transform symlink to referent | ✅ Implemented | `copy_links` config field |
| `--copy-unsafe-links` | Transform unsafe symlinks | ✅ Implemented | `copy_unsafe_links` config field |
| `--safe-links` | Ignore symlinks outside tree | ✅ Implemented | `safe_links` config field |
| `--munge-links` | Munge symlinks for safety | ❌ Not Implemented | |
| `-k`, `--copy-dirlinks` | Transform symlink to dir | ❌ Not Implemented | |
| `-K`, `--keep-dirlinks` | Treat symlinked dir as dir | ❌ Not Implemented | |
## 10. Sparse & Device
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `-S`, `--sparse` | Sparse block handling | ✅ Implemented | `preserve_sparse` config field |
| `--preallocate` | Allocate dest files before writing | ❌ Not Implemented | |
## 11. Checksum & Comparison
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `-c`, `--checksum` | Skip based on checksum | ⚠️ Partial | `checksum` stored, forwarded to scanner |
| `--checksum-choice=STR` | Choose checksum algorithm | ❌ Not Implemented | xxHash used internally |
| `--whole-file` | Disable delta-xfer algorithm | ❌ Not Implemented | |
| `--compare-dest=DIR` | Compare dest files relative to DIR | ⚠️ Partial | `compare_dest` stored, not wired |
| `--copy-dest=DIR` | Include copies of unchanged files | ⚠️ Partial | `copy_dest` stored, not wired |
| `--link-dest=DIR` | Hardlink to files when unchanged | ⚠️ Partial | `link_dest` stored, not wired |
| `--fuzzy`, `--no-fuzzy` | Find similar file for basis | ❌ Not Implemented | |
## 12. Compression
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `-z`, `--compress` | Compress file data | 🔀 Alt Arg | Always uses zstd (rsync supports multiple algorithms) |
| `--compress-choice=STR` | Choose compression algorithm | ⚠️ Partial | `compress_choice` stored, always zstd |
| `--compress-level=NUM` | Set compression level | ✅ Implemented | 1-22, default 5 |
| `--compress-threads=NUM` | Set compression threads | ❌ Not Implemented | |
| `--skip-compress=LIST` | Skip compress for suffixes | ❌ Not Implemented | Smart skip for known types |
## 13. Connectivity
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `-e`, `--rsh=COMMAND` | Remote shell to use | ✅ Implemented | SSH transport support |
| `--rsync-path=PROGRAM` | rsync binary on remote | ✅ Implemented | `rsync_path` config field |
| `--port=PORT` | Alternate daemon port | ✅ Implemented | `server_port` config field |
| `--sockopts=OPTIONS` | Custom TCP options | ❌ Not Implemented | |
| `--blocking-io` | Use blocking I/O for remote shell | ❌ Not Implemented | |
| `--outbuf=N\|L\|B` | Set output buffering | ❌ Not Implemented | |
| `--address=ADDRESS` | Bind address for outgoing socket | ✅ Implemented | `bind_address` config field |
| `-4`, `--ipv4` | Prefer IPv4 | ✅ Implemented | `ipv4` config field |
| `-6`, `--ipv6` | Prefer IPv6 | ✅ Implemented | `ipv6` config field |
## 14. Daemon Mode
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `--daemon` | Run as rsync daemon | ⚠️ Partial | `daemon` stored, not wired |
| `--config=FILE` | Alternate rsyncd.conf file | ⚠️ Partial | `daemon_config` stored, not wired |
| `--dparam=OVERRIDE` | Override global daemon config | ❌ Not Implemented | |
| `--no-detach` | Don't detach from parent | ❌ Not Implemented | |
| `--password-file=FILE` | Read daemon password from file | ❌ Not Implemented | |
| `--early-input=FILE` | Use FILE for daemon early exec | ❌ Not Implemented | |
## 15. Safety & Security
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| Path escape detection | Ensure files stay within root | ✅ Implemented | `has_path_traversal()` + realpath |
| Symlink-safe delete | Skip symlinks in delete walk | ✅ Implemented | `delete_extras_walk()` |
| Protocol version check | Verify compatible versions | ✅ Implemented | `config_receive()` |
| Max data/string/chunk sizes | Prevent OOM attacks | ✅ Implemented | Per-message limits |
| Per-connection memory limit | 1GB per connection | ✅ Implemented | `MAX_CONNECTION_MEMORY` |
| `--trust-sender` | Trust remote sender's file list | ❌ Not Implemented | |
| `--secluded-args` | Send args via protocol | ❌ Not Implemented | |
| `--old-args` | Disable modern arg protection | ❌ Not Implemented | |
| `--ignore-missing-args` | Ignore missing source args | ❌ Not Implemented | |
| `--delete-missing-args` | Delete missing source args | ❌ Not Implemented | |
## 16. Batch Operations
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `--write-batch=FILE` | Write batched update to file | ❌ Not Implemented | |
| `--only-write-batch=FILE` | Write batch without updating dest | ❌ Not Implemented | |
| `--read-batch=FILE` | Read batched update from file | ❌ Not Implemented | |
## 17. Advanced
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `--stop-after=MINS` | Stop after N minutes | ❌ Not Implemented | |
| `--stop-at=TIME` | Stop at specified time | ❌ Not Implemented | |
| `--fsync` | Fsync every written file | ❌ Not Implemented | |
| `--protocol=NUM` | Force older protocol version | ❌ Not Implemented | |
| `--iconv=CONVERT_SPEC` | Charset conversion | ❌ Not Implemented | |
| `--checksum-seed=NUM` | Set checksum seed | ❌ Not Implemented | |
| `-s`, `--secluded-args` | Use protocol to send args | ❌ Not Implemented | |
| `--no-OPTION` | Turn off implied option | ❌ Not Implemented | |
---
## Recommendations: Top Features to Implement Next
Ranked by user demand, implementation complexity, and interoperability impact:
| Priority | Feature | Effort | Impact |
|----------|---------|--------|--------|
| 1 | `--whole-file` / `-W` | Low | High — users expect opt-out of delta |
| 2 | `--ignore-times` / `-I` | Low | Medium — useful for forcing re-transfer |
| 3 | `--size-only` | Low | Medium — common migration scenario |
| 4 | `--existing` / `--ignore-existing` | Low | Medium — common sync patterns |
| 5 | `--remove-source-files` | Low | High — common for moves/backup |
| 6 | `--delete-during` | Medium | High — performance improvement |
| 7 | `--delay-updates` | Medium | High — atomic updates |
| 8 | `--chmod` | Low | Medium — permission flexibility |
| 9 | `--executability` / `-E` | Low | Low — simple flag |
| 10 | `--skip-compress` | Low | Medium — performance tuning |
---
## FastSync-Specific Features (Not in rsync)
| Feature | Description |
|---------|-------------|
| `-m` | Multithreaded pipeline (scanner/loader/sender) |
| `-s` | Chunk serialization mode |
| `-f` / `--sendfile` | Zero-copy sendfile() syscall (TCP only) |
| `-c [level]` | zstd compression level (1-22) |
| `--chunk-size` | Configurable chunk size |
| `--queue-size` | Pipeline queue capacity |
| `--tls` | TLS encryption (mutual auth) |
| `--fastsync-server-path` | Path to fastsync-server binary |
| `--server-host` / `--server-port` | Direct TCP connection |
| Incremental sync | Skip unchanged files (size+mtime) |
| Delta transfer | Block-level delta for changed files |
+8 -7
View File
@@ -166,7 +166,7 @@ static int send_single_file(Client* client, File* file, Config* config, bool use
if (!use_incremental) { if (!use_incremental) {
if (use_sendfile) { if (use_sendfile) {
return send_file_direct_sendfile(file, client->file_descriptor, config->use_metadata) ? 0 return send_file_direct_sendfile(file, client->file_descriptor, config->use_metadata) ? 0
: -1; : -1;
} }
return send_file_direct(file, client->file_descriptor, config->use_metadata, compression_level) return send_file_direct(file, client->file_descriptor, config->use_metadata, compression_level)
? 0 ? 0
@@ -284,8 +284,8 @@ static int send_chunks_multithreaded(void* pipeline_context) {
} else if (context->config->use_tls) { } else if (context->config->use_tls) {
client = client_create(); client = client_create();
if (!client || !client_connect_tls(client, context->config->server_host, if (!client || !client_connect_tls(client, context->config->server_host,
context->config->server_port, context->config->tls_cert, context->config->server_port, context->config->tls_cert,
context->config->tls_key, context->config->tls_ca)) { context->config->tls_key, context->config->tls_ca)) {
if (client) if (client)
client_delete(client); client_delete(client);
fprintf(stderr, "Error: could not connect to server via TLS\n"); fprintf(stderr, "Error: could not connect to server via TLS\n");
@@ -374,7 +374,8 @@ static int scan_directory_multithreaded(void* pipeline_context) {
context->config->exclude_patterns, context->config->exclude_count, context->config->exclude_patterns, context->config->exclude_count,
context->config->include_patterns, context->config->include_count, context->config->max_size, context->config->include_patterns, context->config->include_count, context->config->max_size,
context->config->min_size, context->config->max_depth, 4, context->config->follow_symlinks, context->config->min_size, context->config->max_depth, 4, context->config->follow_symlinks,
context->config->copy_links, context->config->safe_links, context->config->copy_unsafe_links, context->config->checksum); context->config->copy_links, context->config->safe_links, context->config->copy_unsafe_links,
context->config->checksum);
Chunk* current_chunk; Chunk* current_chunk;
while ((current_chunk = parallel_scanner_next(scanner)) != NULL) { while ((current_chunk = parallel_scanner_next(scanner)) != NULL) {
@@ -485,7 +486,7 @@ int send_files(Config* config) {
} else if (config->use_tls) { } else if (config->use_tls) {
client = client_create(); client = client_create();
if (!client || !client_connect_tls(client, config->server_host, config->server_port, if (!client || !client_connect_tls(client, config->server_host, config->server_port,
config->tls_cert, config->tls_key, config->tls_ca)) { config->tls_cert, config->tls_key, config->tls_ca)) {
if (client) if (client)
client_delete(client); client_delete(client);
fprintf(stderr, "Error: could not connect to server via TLS\n"); fprintf(stderr, "Error: could not connect to server via TLS\n");
@@ -575,8 +576,8 @@ int send_files(Config* config) {
} }
if (config->stats) { if (config->stats) {
double rate = elapsed_total > 0 ? total_bytes / (1048576.0 * elapsed_total) : 0; double rate = elapsed_total > 0 ? total_bytes / (1048576.0 * elapsed_total) : 0;
fprintf(stderr, "Stats: %d files, %.1f MB, %.1f MB/s\n", total_files, fprintf(stderr, "Stats: %d files, %.1f MB, %.1f MB/s\n", total_files, total_bytes / 1048576.0,
total_bytes / 1048576.0, rate); rate);
} }
directory_scanner_destroy(scanner); directory_scanner_destroy(scanner);
client_disconnect(client); client_disconnect(client);
+59 -8
View File
@@ -309,12 +309,12 @@ static int parallel_worker_thread(void* arg) {
} }
ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata, ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata,
unsigned long long chunk_size, char** exclude_patterns, unsigned long long chunk_size, char** exclude_patterns,
int exclude_count, char** include_patterns, int exclude_count, char** include_patterns,
int include_count, unsigned long long max_size, int include_count, unsigned long long max_size,
unsigned long long min_size, int max_depth, unsigned long long min_size, int max_depth,
int num_threads, bool follow_symlinks, bool copy_links, int num_threads, bool follow_symlinks, bool copy_links,
bool safe_links, bool copy_unsafe_links, bool checksum) { bool safe_links, bool copy_unsafe_links, bool checksum) {
ParallelScanner* ps = calloc(1, sizeof(ParallelScanner)); ParallelScanner* ps = calloc(1, sizeof(ParallelScanner));
if (!ps) if (!ps)
return NULL; return NULL;
@@ -347,11 +347,62 @@ ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata
char* cur_path = path_cat(root_directory, entry->d_name); char* cur_path = path_cat(root_directory, entry->d_name);
if (!cur_path) if (!cur_path)
continue; continue;
struct stat st; struct stat lstats;
if (stat(cur_path, &st) != 0) { if (lstat(cur_path, &lstats) != 0) {
free(cur_path); free(cur_path);
continue; continue;
} }
bool is_symlink = S_ISLNK(lstats.st_mode);
// Skip symlinks unless the user explicitly enabled following/copying them.
if (is_symlink && !follow_symlinks && !copy_links && !safe_links && !copy_unsafe_links) {
free(cur_path);
continue;
}
// --safe-links: reject symlinks pointing outside the source tree.
if (is_symlink && safe_links) {
char link_target[4096];
ssize_t len = readlink(cur_path, link_target, sizeof(link_target) - 1);
if (len < 0) {
free(cur_path);
continue;
}
link_target[len] = 0;
if (link_target[0] == '/') {
free(cur_path);
continue;
}
}
// --copy-unsafe-links (without --copy-links): only copy absolute symlinks.
if (is_symlink && copy_unsafe_links && !copy_links) {
char link_target[4096];
ssize_t len = readlink(cur_path, link_target, sizeof(link_target) - 1);
if (len < 0) {
free(cur_path);
continue;
}
link_target[len] = 0;
bool unsafe = (link_target[0] == '/');
if (!unsafe) {
free(cur_path);
continue;
}
}
// Determine whether to use lstat or stat results for the entry.
struct stat st;
bool use_lstat_res = is_symlink && follow_symlinks && !copy_links;
if (use_lstat_res) {
st = lstats;
} else {
if (stat(cur_path, &st) != 0) {
free(cur_path);
continue;
}
}
if (S_ISDIR(st.st_mode)) { if (S_ISDIR(st.st_mode)) {
array_list_add(subdirs, cur_path); array_list_add(subdirs, cur_path);
} else { } else {
+6 -6
View File
@@ -51,12 +51,12 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner);
void directory_scanner_destroy(DirectoryScanner* scanner); void directory_scanner_destroy(DirectoryScanner* scanner);
ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata, ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata,
unsigned long long chunk_size, char** exclude_patterns, unsigned long long chunk_size, char** exclude_patterns,
int exclude_count, char** include_patterns, int exclude_count, char** include_patterns,
int include_count, unsigned long long max_size, int include_count, unsigned long long max_size,
unsigned long long min_size, int max_depth, unsigned long long min_size, int max_depth,
int num_threads, bool follow_symlinks, bool copy_links, int num_threads, bool follow_symlinks, bool copy_links,
bool safe_links, bool copy_unsafe_links, bool checksum); bool safe_links, bool copy_unsafe_links, bool checksum);
Chunk* parallel_scanner_next(ParallelScanner* scanner); Chunk* parallel_scanner_next(ParallelScanner* scanner);
void parallel_scanner_destroy(ParallelScanner* scanner); void parallel_scanner_destroy(ParallelScanner* scanner);
+20
View File
@@ -12,6 +12,9 @@
#include "metadata.h" #include "metadata.h"
#include "protocol.h" #include "protocol.h"
/* Maximum individual file data size within a chunk (64 MB) */
#define MAX_FILE_DATA_SIZE (64ULL * 1024 * 1024)
Chunk* chunk_create(File** items, int element_count) { Chunk* chunk_create(File** items, int element_count) {
Chunk* chunk = (Chunk*)malloc(sizeof(Chunk)); Chunk* chunk = (Chunk*)malloc(sizeof(Chunk));
if (chunk == NULL) { if (chunk == NULL) {
@@ -157,6 +160,14 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
return NULL; return NULL;
} }
// Reject individual file data larger than the maximum allowed size.
if (file_data_size > MAX_FILE_DATA_SIZE) {
log_message(LOG_LEVEL_ERROR, "File data size %zu exceeds maximum %llu", file_data_size,
(unsigned long long)MAX_FILE_DATA_SIZE);
array_list_delete(files);
return NULL;
}
void* file_data = malloc(file_data_size); void* file_data = malloc(file_data_size);
if (file_data == NULL) { if (file_data == NULL) {
perror("Could not allocate memory for file data"); perror("Could not allocate memory for file data");
@@ -210,6 +221,15 @@ Chunk* receive_chunk_data(int fd, const Config* config) {
return NULL; return NULL;
} }
} }
// Reject chunks larger than the maximum allowed size to prevent OOM.
if (data_to_process->size > MAX_CHUNK_SIZE) {
log_message(LOG_LEVEL_ERROR, "Chunk size %zu exceeds maximum %llu", data_to_process->size,
(unsigned long long)MAX_CHUNK_SIZE);
data_destroy(data_to_process);
return NULL;
}
Chunk* chunk = chunk_deserialize(data_to_process, config->use_metadata); Chunk* chunk = chunk_deserialize(data_to_process, config->use_metadata);
data_destroy(data_to_process); data_destroy(data_to_process);
if (chunk == NULL) if (chunk == NULL)
+8 -7
View File
@@ -35,7 +35,8 @@ File* file_create(const char* path) {
return NULL; return NULL;
} }
strcpy(file->path, path); memcpy(file->path, path, path_len);
file->path[path_len] = '\0';
file->data = data_create_reserve(0); file->data = data_create_reserve(0);
if (file->data == NULL) { if (file->data == NULL) {
free(file->path); free(file->path);
@@ -142,7 +143,8 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
} }
char* resolved_root = NULL; char* resolved_root = NULL;
const char* actual_root = (partial_dir && config && config->partial) ? partial_dir : root_directory; const char* actual_root =
(partial_dir && config && config->partial) ? partial_dir : root_directory;
resolved_root = realpath(actual_root, NULL); resolved_root = realpath(actual_root, NULL);
if (resolved_root == NULL) { if (resolved_root == NULL) {
if (mkdir_r(actual_root)) { if (mkdir_r(actual_root)) {
@@ -187,7 +189,7 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
if (backup_path) { if (backup_path) {
char* backup_dir_path = str_dup(backup_path); char* backup_dir_path = str_dup(backup_path);
if (backup_dir_path) { if (backup_dir_path) {
char* bdir = dirname(backup_dir_path); const char* bdir = dirname(backup_dir_path);
mkdir_r(bdir); mkdir_r(bdir);
free(backup_dir_path); free(backup_dir_path);
} }
@@ -222,8 +224,7 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
size_t root_len = strlen(resolved_root); size_t root_len = strlen(resolved_root);
if (strncmp(resolved_dir, resolved_root, root_len) != 0 || if (strncmp(resolved_dir, resolved_root, root_len) != 0 ||
(resolved_dir[root_len] != '\0' && resolved_dir[root_len] != '/')) { (resolved_dir[root_len] != '\0' && resolved_dir[root_len] != '/')) {
log_message(LOG_LEVEL_ERROR, "Path escape detected: %s is outside %s", disk_path, log_message(LOG_LEVEL_ERROR, "Path escape detected: %s is outside %s", disk_path, actual_root);
actual_root);
free(resolved_dir); free(resolved_dir);
free(resolved_root); free(resolved_root);
free(disk_path); free(disk_path);
@@ -517,8 +518,8 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
return file; return file;
} }
bool to_disk(const char* path, const void* data, unsigned long long data_size, bool to_disk(const char* path, const void* data, unsigned long long data_size, bool inplace,
bool inplace, bool sparse) { bool sparse) {
char* tmp_path = NULL; char* tmp_path = NULL;
char* directory = NULL; char* directory = NULL;
+2 -2
View File
@@ -34,8 +34,8 @@ bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int
size_t file_content_to_buffer(File* file); size_t file_content_to_buffer(File* file);
FileMetadata* file_metadata_create(const struct stat* stats); FileMetadata* file_metadata_create(const struct stat* stats);
void file_metadata_destroy(void* metadata); void file_metadata_destroy(void* metadata);
bool to_disk(const char* path, const void* data, unsigned long long data_size, bool to_disk(const char* path, const void* data, unsigned long long data_size, bool inplace,
bool inplace, bool sparse); bool sparse);
bool file_save_to_disk(const char* root_directory, File* file, const Config* config); bool file_save_to_disk(const char* root_directory, File* file, const Config* config);
File* receive_incremental_check(int fd, const Config* config, bool* skipped); File* receive_incremental_check(int fd, const Config* config, bool* skipped);
int receive_manifest(int fd, const Config* config, int* next_status); int receive_manifest(int fd, const Config* config, int* next_status);
+2 -2
View File
@@ -14,7 +14,7 @@
#include <threads.h> #include <threads.h>
PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner, PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner,
Queue* queue_loader) { Queue* queue_loader) {
PipelineContextSender* context = malloc(sizeof(PipelineContextSender)); PipelineContextSender* context = malloc(sizeof(PipelineContextSender));
if (context == NULL) if (context == NULL)
return NULL; return NULL;
@@ -58,7 +58,7 @@ void pipeline_context_sender_destroy(PipelineContextSender* context) {
} }
PipelineContextReceiver* pipeline_context_receiver_create(Config* config, Queue* queue, PipelineContextReceiver* pipeline_context_receiver_create(Config* config, Queue* queue,
int file_descriptor, SSL* ssl) { int file_descriptor, SSL* ssl) {
PipelineContextReceiver* context = malloc(sizeof(PipelineContextReceiver)); PipelineContextReceiver* context = malloc(sizeof(PipelineContextReceiver));
if (context == NULL) if (context == NULL)
return NULL; return NULL;
+2 -2
View File
@@ -40,10 +40,10 @@ typedef struct PipelineContextReceiver {
} PipelineContextReceiver; } PipelineContextReceiver;
PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner, PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner,
Queue* queue_loader); Queue* queue_loader);
void pipeline_context_sender_destroy(PipelineContextSender* context); void pipeline_context_sender_destroy(PipelineContextSender* context);
PipelineContextReceiver* pipeline_context_receiver_create(Config* config, Queue* queue_receiver, PipelineContextReceiver* pipeline_context_receiver_create(Config* config, Queue* queue_receiver,
int file_descriptor, SSL* ssl); int file_descriptor, SSL* ssl);
void pipeline_context_receiver_destroy(PipelineContextReceiver* context); void pipeline_context_receiver_destroy(PipelineContextReceiver* context);
int receive_thread(void* pipeline_context); int receive_thread(void* pipeline_context);
int write_thread(void* pipeline_context); int write_thread(void* pipeline_context);
+3
View File
@@ -11,6 +11,9 @@
/* Maximum allowed data payload size for receive_data (100 MB) */ /* Maximum allowed data payload size for receive_data (100 MB) */
#define MAX_DATA_PAYLOAD_SIZE (100ULL * 1024 * 1024) #define MAX_DATA_PAYLOAD_SIZE (100ULL * 1024 * 1024)
/* Maximum chunk size (64 MB) — prevents unbounded allocation from the wire */
#define MAX_CHUNK_SIZE (64ULL * 1024 * 1024)
typedef struct ssl_st SSL; typedef struct ssl_st SSL;
typedef int Status; typedef int Status;
+14 -8
View File
@@ -13,7 +13,7 @@ bool mkdir_r(const char* path) {
char* path_duplicate = malloc(strlen(path) + 1); char* path_duplicate = malloc(strlen(path) + 1);
if (!path_duplicate) if (!path_duplicate)
return false; return false;
strcpy(path_duplicate, path); memcpy(path_duplicate, path, strlen(path) + 1);
char* path_current = (char*)malloc((strlen(path) + 2) * sizeof(char)); char* path_current = (char*)malloc((strlen(path) + 2) * sizeof(char));
if (!path_current) { if (!path_current) {
free(path_duplicate); free(path_duplicate);
@@ -21,7 +21,8 @@ bool mkdir_r(const char* path) {
} }
char* path_current_position = path_current; char* path_current_position = path_current;
if (path[0] == '/') { if (path[0] == '/') {
strcpy(path_current, "/"); path_current[0] = '/';
path_current[1] = '\0';
path_current_position += 1; path_current_position += 1;
} else { } else {
path_current[0] = '\0'; path_current[0] = '\0';
@@ -31,10 +32,12 @@ bool mkdir_r(const char* path) {
const char* part = strtok_r(path_duplicate, delimiter, &saveptr); const char* part = strtok_r(path_duplicate, delimiter, &saveptr);
bool ok = true; bool ok = true;
while (part != NULL) { while (part != NULL) {
strcpy(path_current_position, part); size_t part_len = strlen(part);
path_current_position += strlen(part) * sizeof(char); memcpy(path_current_position, part, part_len);
strcpy(path_current_position, "/"); path_current_position += part_len;
path_current_position += sizeof(char); path_current_position[0] = '/';
path_current_position[1] = '\0';
path_current_position++;
struct stat st; struct stat st;
if (stat(path_current, &st) != 0) { if (stat(path_current, &st) != 0) {
if (mkdir(path_current, 0755) != 0) { if (mkdir(path_current, 0755) != 0) {
@@ -53,8 +56,11 @@ bool mkdir_r(const char* path) {
char* str_dup(const char* string) { char* str_dup(const char* string) {
if (string == NULL) if (string == NULL)
return NULL; return NULL;
char* new_string = (char*)malloc(strlen(string) + 1); size_t str_len = strlen(string);
strcpy(new_string, string); char* new_string = (char*)malloc(str_len + 1);
if (new_string == NULL)
return NULL;
memcpy(new_string, string, str_len + 1);
return new_string; return new_string;
} }