Merge feat/p4-devices
# Conflicts: # src/client/client_send.c # src/server/receiver.c # src/shared/chunk.c # src/shared/file.c # src/shared/file_receive.c # src/shared/file_receive.h # src/shared/file_types.h # src/shared/protocol.h # tests/test_chunk.c
This commit is contained in:
+55
-5
@@ -247,11 +247,11 @@ why plain `--append` works on the normal atomic path, not only with `--inplace`.
|
|||||||
| `-A`, `--acls` | Preserve ACLs | ❌ Not Implemented | Removed because it had no effect |
|
| `-A`, `--acls` | Preserve ACLs | ❌ Not Implemented | Removed because it had no effect |
|
||||||
| `-X`, `--xattrs` | Preserve extended attributes | ❌ Not Implemented | Removed because it had no effect |
|
| `-X`, `--xattrs` | Preserve extended attributes | ❌ Not Implemented | Removed because it had no effect |
|
||||||
| `-H`, `--hard-links` | Preserve hard links | ✅ Implemented | Files on the source that share an inode (`st_dev`+`st_ino`, e.g. a `cp -al` tree) are re-created as hard links to one another on the destination, so duplicate links stay deduplicated and only the first member's data is sent (later members are transmitted as payload-less `STATUS_HARDLINK` frames). The receiver links each sibling to the first member's installed file with an atomic link + rename; on `link()` failure it falls back to a byte-identical local copy of the first member, never a partial/corrupt file. Requires the sequential scan for ordering (the first member is always emitted and installed before any sibling is linked). Works single-threaded and under `-m`, `--inplace`, `--delay-updates` (links staged and published by rename) and `--partial`. Crosses the wire (`preserve_hard_links` bool; `PROTOCOL_VERSION` bumped **2.11.0 → 2.12.0**, peers must match). Incompatible with `-s` (chunk serialization) and `--append`/`--append-verify`, rejected up front with a distinct error. See the Phase-4 hard-links notes below |
|
| `-H`, `--hard-links` | Preserve hard links | ✅ Implemented | Files on the source that share an inode (`st_dev`+`st_ino`, e.g. a `cp -al` tree) are re-created as hard links to one another on the destination, so duplicate links stay deduplicated and only the first member's data is sent (later members are transmitted as payload-less `STATUS_HARDLINK` frames). The receiver links each sibling to the first member's installed file with an atomic link + rename; on `link()` failure it falls back to a byte-identical local copy of the first member, never a partial/corrupt file. Requires the sequential scan for ordering (the first member is always emitted and installed before any sibling is linked). Works single-threaded and under `-m`, `--inplace`, `--delay-updates` (links staged and published by rename) and `--partial`. Crosses the wire (`preserve_hard_links` bool; `PROTOCOL_VERSION` bumped **2.11.0 → 2.12.0**, peers must match). Incompatible with `-s` (chunk serialization) and `--append`/`--append-verify`, rejected up front with a distinct error. See the Phase-4 hard-links notes below |
|
||||||
| `-D` | Same as --devices --specials | ❌ Not Implemented | Removed because device-file handling is not implemented |
|
| `-D` | Same as --devices --specials | ✅ Implemented | Implies `--devices --specials`. `-D` was unassigned in FastSync (verified: no collision), so it is free to imply both device-node and special-file preservation. See the `--devices`/`--specials` rows and the Phase-4 devices notes below |
|
||||||
| `--devices` | Preserve device files | ❌ Not Implemented | Removed because it had no effect |
|
| `--devices` | Preserve device files | ⚠️ Partial | Recreates char/block device nodes on the destination via `mknod` instead of transferring content. Type + rdev are validated strictly (S_IFMT from the transmitted mode; major/minor range-checked, non-negative), and creation is **privilege-gated**: `mknod` needs `CAP_MKNOD`, so a non-root receiver (CI runs via setpriv as non-root) logs a warning and **skips the device entry safely** — the whole transfer never aborts just because the node could not be made. The node is created fd-relative below the receive root (`mknodat` on the confined secure parent), so it can never be placed outside the authorized root, never follows a symlink, and never replaces an existing directory. Only a char/block mode is honored. Crosses the wire (a new `STATUS_SPECIAL` frame carries the path + metadata mode + rdev; `PROTOCOL_VERSION` bumped **2.12.0 → 2.13.0**). Divergence: per-entry skip (not a hard error) when the receiver lacks `CAP_MKNOD`, documented in the Phase-4 devices notes |
|
||||||
| `--specials` | Preserve special files | ❌ Not Implemented | |
|
| `--specials` | Preserve special files | ⚠️ Partial | Recreates **FIFOs** on the destination via `mkfifo` (unprivileged, so this is a real, assertable behavior under CI). Sockets cannot be recreated by any standard filesystem call and are skipped with an explicit note (best-effort / unsupported, matching the plan). FIFO creation is privileged-gated only in the sense of graceful skip on any permission failure. Node creation is confined below the receive root (`mkfifoat` on the secure fd-relative parent; no `..`, no symlink follow). Crosses the wire like `--devices` (the `STATUS_SPECIAL` frame; `PROTOCOL_VERSION` bumped **2.12.0 → 2.13.0**). See the Phase-4 devices notes |
|
||||||
| `--copy-devices` | Copy device contents as file | ❌ Not Implemented | |
|
| `--copy-devices` | Copy device contents as file | ⚠️ Partial | Copy a device's CONTENT into an ordinary regular file on the destination instead of recreating the node — non-privileged and safe. FastSync scans a device/FIFO as a regular file: its reported size (`st_size`, typically 0 for char devices and FIFOs) is copied, so a FIFO or a non-readable device becomes an empty (or size-bounded) regular file without ever blocking or reading unbounded pseudo-device streams. The run always succeeds and never crashes on such input. **Deliberate, safe divergence from rsync's dd-like unbounded device read.** See the Phase-4 devices notes |
|
||||||
| `--write-devices` | Write to devices as files | ❌ Not Implemented | |
|
| `--write-devices` | Write to devices as files | ⚠️ Partial | Write the received data directly into an **existing** device node on the destination instead of creating a regular file. Restricted and best-effort: the destination must already exist and be a char/block device (opened only under the confined receive root, with `O_NOFOLLOW` + `O_NONBLOCK`); a missing, symlinked, FIFO-with-no-reader (`ENXIO`), non-device destination, or any write failure is **skipped with a warning** rather than allowed, so a run can never clobber the system, never blocks on a special-file target, and never aborts on an unusable target. See the Phase-4 devices notes |
|
||||||
| `-U`, `--atimes` | Preserve access times | ✅ Implemented | Captures the source access time (from the scanner's pre-read stat, so it is not clobbered by reading the file for transfer) and transmits it over the wire; the receiver restores it together with the mtime via `futimens`/`utimensat`. Implies metadata transmission (the times travel inside the `-M` metadata payload), but does not enable ownership application (that stays opt-in via the identity flags). Wire: new `atime` fields on the metadata frame + a `preserve_atimes` config boolean; `PROTOCOL_VERSION` bumped **2.11.0 → 2.12.0** |
|
| `-U`, `--atimes` | Preserve access times | ✅ Implemented | Captures the source access time (from the scanner's pre-read stat, so it is not clobbered by reading the file for transfer) and transmits it over the wire; the receiver restores it together with the mtime via `futimens`/`utimensat`. Implies metadata transmission (the times travel inside the `-M` metadata payload), but does not enable ownership application (that stays opt-in via the identity flags). Wire: new `atime` fields on the metadata frame + a `preserve_atimes` config boolean; `PROTOCOL_VERSION` bumped **2.11.0 → 2.12.0** |
|
||||||
| `-N`, `--crtimes` | Preserve create times | ⚠️ Partial | Captures the source birth time via `statx(STATX_BTIME)` on Linux and transmits it (recorded as a wire field), but there is **no portable way to set a birth time** (`utimensat` can only set atime/mtime), so the receiver explicitly does NOT apply it: it logs a debug note and continues — never failing the transfer and never pretending it worked. On platforms without `statx` it parses as a documented no-op (flag accepted; nothing is captured). Implies metadata transmission. Wire: new `crtime` fields + a `preserve_crtimes` config boolean; `PROTOCOL_VERSION` bumped **2.11.0 → 2.12.0** (see the Phase-4 metadata-time notes) |
|
| `-N`, `--crtimes` | Preserve create times | ⚠️ Partial | Captures the source birth time via `statx(STATX_BTIME)` on Linux and transmits it (recorded as a wire field), but there is **no portable way to set a birth time** (`utimensat` can only set atime/mtime), so the receiver explicitly does NOT apply it: it logs a debug note and continues — never failing the transfer and never pretending it worked. On platforms without `statx` it parses as a documented no-op (flag accepted; nothing is captured). Implies metadata transmission. Wire: new `crtime` fields + a `preserve_crtimes` config boolean; `PROTOCOL_VERSION` bumped **2.11.0 → 2.12.0** (see the Phase-4 metadata-time notes) |
|
||||||
| `-O`, `--omit-dir-times` | Omit dirs from --times | 🔄 Compatibility No-op | Accepted and parsed for CLI compatibility, and the config boolean crosses the wire, but it has **no effect**: FastSync never preserves directory mtimes in the first place (directories are created via `mkdir` with no metadata, a documented divergence under `-d`/recursive), so there is nothing for an "omit" to suppress. It never breaks a normal run |
|
| `-O`, `--omit-dir-times` | Omit dirs from --times | 🔄 Compatibility No-op | Accepted and parsed for CLI compatibility, and the config boolean crosses the wire, but it has **no effect**: FastSync never preserves directory mtimes in the first place (directories are created via `mkdir` with no metadata, a documented divergence under `-d`/recursive), so there is nothing for an "omit" to suppress. It never breaks a normal run |
|
||||||
@@ -392,6 +392,56 @@ front with a distinct error on the client, and re-checked on receive): `-H` with
|
|||||||
`-s` chunk serialization (the chunk wire has no per-file hard-link info) and `-H`
|
`-s` chunk serialization (the chunk wire has no per-file hard-link info) and `-H`
|
||||||
with `--append`/`--append-verify` (a payload-less sibling cannot be tail-resumed).
|
with `--append`/`--append-verify` (a payload-less sibling cannot be tail-resumed).
|
||||||
|
|
||||||
|
**Phase-4 devices notes:** `--devices`, `--specials`, `-D`, `--copy-devices`,
|
||||||
|
and `--write-devices` are new. They change the wire: the config frame grows three
|
||||||
|
booleans — `preserve_specials`, `copy_devices`, `write_devices` — that CROSS the
|
||||||
|
wire (`preserve_devices` already existed), and a new `STATUS_SPECIAL` frame (used
|
||||||
|
by `--devices`/`--specials`/`-D`) carries a special/device entry: the destination
|
||||||
|
path, the metadata frame (whose mode's S_IFMT bits carry the node kind, requiring
|
||||||
|
the flags to imply metadata transmission), and two int32 `rdev` major/minor
|
||||||
|
fields. The chunk-serialized wire (`-s`) grows a matching per-file special
|
||||||
|
marker + rdev so `--devices/--specials` also work under `-s`. `PROTOCOL_VERSION`
|
||||||
|
was bumped **2.12.0 → 2.13.0** (peers must match, exactly as prior phases did).
|
||||||
|
|
||||||
|
**Privilege gating (the crux):** making a device node requires `CAP_MKNOD` (root).
|
||||||
|
CI runs the integration suite as a NON-ROOT user (via setpriv), so `mknod` fails
|
||||||
|
with `EPERM`. The receiver treats this as a graceful, logged *skip of the entry*
|
||||||
|
returned as a success/skip outcome — the whole transfer NEVER aborts just because
|
||||||
|
the environment cannot create the node. `mkfifo` (FIFOs) is unprivileged, so
|
||||||
|
`--specials` FIFO creation is a real, assertable behavior under CI; sockets cannot
|
||||||
|
be recreated by any standard filesystem call and are skipped with an explicit
|
||||||
|
note. The "device actually created" integration assertions are guarded to run
|
||||||
|
only as root. User-facing expectation: point `--devices` at devices and a
|
||||||
|
non-root receiver will faithfully skip them while transferring everything else.
|
||||||
|
|
||||||
|
**Confinement & validation:** a special/device node is created with
|
||||||
|
`mknodat`/`mkfifoat` on the parent directory opened fd-relative below the receive
|
||||||
|
root (`file_open_secure_parent`: `O_NOFOLLOW`, no `..` components, root-checked),
|
||||||
|
so a node can never be created outside the authorized destination root and never
|
||||||
|
through a symlinked parent. The transmitted type is derived ONLY from the
|
||||||
|
validated S_IFMT bits of the metadata mode (char/block/FIFO honored, socket
|
||||||
|
skipped, regular/dir rejected as an invalid special), and the transmitted rdev is
|
||||||
|
validated both on the wire (`file_receive_special`, `chunk_deserialize`) and at
|
||||||
|
the creation site (`file_special_rdev_valid`): a negative, oversize, or
|
||||||
|
non-device-carrying rdev is rejected outright (receiver aborts the frame), and a
|
||||||
|
node is never replaced over an existing directory or unrelated entry (a matching
|
||||||
|
existing node is left in place). `--write-devices` is the deliberately restricted
|
||||||
|
danger path: it only ever opens an existing char/block node under the confined
|
||||||
|
root, and every failure mode (missing, non-device, write error, EPERM) is a
|
||||||
|
warning + skip, never a system-clobbering write or an abort.
|
||||||
|
|
||||||
|
**Documented divergences (honest subset):**
|
||||||
|
- A device entry the receiver cannot create (missing `CAP_MKNOD`) is *skipped*,
|
||||||
|
not a transfer failure — rsync under the same conditions would error.
|
||||||
|
- `--copy-devices` copies the device's *reported size* (typically 0 for char
|
||||||
|
devices/FIFOs) into a regular file and never reads an unbounded pseudo-device;
|
||||||
|
this is the safe, non-hanging alternative to rsync's dd-like read.
|
||||||
|
- `--write-devices` requires the device to already exist at the destination and
|
||||||
|
never creates it; unsupported/inaccessible targets are skipped, not written.
|
||||||
|
- Ownership is not applied to recreated nodes (identity `fchown` needs an fd and
|
||||||
|
would require opening the node); permissions and mtime are applied at
|
||||||
|
creation / via `utimensat`.
|
||||||
|
|
||||||
## 9. Symlink Handling
|
## 9. Symlink Handling
|
||||||
|
|
||||||
| Flag | Rsync Description | FastSync Status | Notes |
|
| Flag | Rsync Description | FastSync Status | Notes |
|
||||||
|
|||||||
@@ -540,6 +540,11 @@ static const OptionEntry OPTION_TABLE[] = {
|
|||||||
{"--numeric-ids", NULL, OPT_FLAG, offsetof(Config, numeric_ids)},
|
{"--numeric-ids", NULL, OPT_FLAG, offsetof(Config, numeric_ids)},
|
||||||
{"--atimes", "-U", OPT_FLAG, offsetof(Config, preserve_atimes)},
|
{"--atimes", "-U", OPT_FLAG, offsetof(Config, preserve_atimes)},
|
||||||
{"--crtimes", "-N", OPT_FLAG, offsetof(Config, preserve_crtimes)},
|
{"--crtimes", "-N", OPT_FLAG, offsetof(Config, preserve_crtimes)},
|
||||||
|
/* -D is handled separately (it implies both --devices and --specials). */
|
||||||
|
{"--devices", NULL, OPT_FLAG, offsetof(Config, preserve_devices)},
|
||||||
|
{"--specials", NULL, OPT_FLAG, offsetof(Config, preserve_specials)},
|
||||||
|
{"--copy-devices", NULL, OPT_FLAG, offsetof(Config, copy_devices)},
|
||||||
|
{"--write-devices", NULL, OPT_FLAG, offsetof(Config, write_devices)},
|
||||||
{"--omit-dir-times", "-O", OPT_FLAG, offsetof(Config, omit_dir_times)},
|
{"--omit-dir-times", "-O", OPT_FLAG, offsetof(Config, omit_dir_times)},
|
||||||
{"--omit-link-times", "-J", OPT_FLAG, offsetof(Config, omit_link_times)},
|
{"--omit-link-times", "-J", OPT_FLAG, offsetof(Config, omit_link_times)},
|
||||||
{"--open-noatime", NULL, OPT_FLAG, offsetof(Config, open_noatime)},
|
{"--open-noatime", NULL, OPT_FLAG, offsetof(Config, open_noatime)},
|
||||||
@@ -834,6 +839,12 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
|||||||
} else if (opt_is(argv[i], "-V", "--version")) {
|
} else if (opt_is(argv[i], "-V", "--version")) {
|
||||||
printf("fastsync version %s\n", PROTOCOL_VERSION);
|
printf("fastsync version %s\n", PROTOCOL_VERSION);
|
||||||
return 1;
|
return 1;
|
||||||
|
} else if (opt_is(argv[i], "-D", NULL)) {
|
||||||
|
/* rsync -D == --devices --specials. -D is otherwise unassigned in
|
||||||
|
FastSync (verified: no collision), so it is free to imply both. */
|
||||||
|
config->preserve_devices = true;
|
||||||
|
config->preserve_specials = true;
|
||||||
|
log_info_message(LOG_INFO_MISC, "Enabled preservation of device and special files (-D)");
|
||||||
} else if (opt_is(argv[i], "-a", "--archive")) {
|
} else if (opt_is(argv[i], "-a", "--archive")) {
|
||||||
config->use_compression =
|
config->use_compression =
|
||||||
!config->compress_choice || strcmp(config->compress_choice, "zstd") == 0;
|
!config->compress_choice || strcmp(config->compress_choice, "zstd") == 0;
|
||||||
@@ -1203,6 +1214,15 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
|||||||
config->files_from_set = set;
|
config->files_from_set = set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Device/special preservation recreates a node from its metadata mode (whose
|
||||||
|
S_IFMT bits carry the node kind), so --devices/--specials/-D imply metadata
|
||||||
|
transmission. --copy-devices/--write-devices treat the entry as data but a
|
||||||
|
mtime/mode-preserving transfer still benefits from metadata, so all four
|
||||||
|
imply it (FastSync's broad -M bundle; ownership stays opt-in). */
|
||||||
|
if (config->preserve_devices || config->preserve_specials || config->copy_devices ||
|
||||||
|
config->write_devices)
|
||||||
|
config->use_metadata = true;
|
||||||
|
|
||||||
/* The "unchanged" decision for --compare-dest/--copy-dest/--link-dest must
|
/* The "unchanged" decision for --compare-dest/--copy-dest/--link-dest must
|
||||||
* be made on the receiver against the basis directories, which requires the
|
* be made on the receiver against the basis directories, which requires the
|
||||||
* per-file STATUS_CHECK handshake: basis-dir options therefore imply
|
* per-file STATUS_CHECK handshake: basis-dir options therefore imply
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ static bool prepare_scanner(const Config* config, int num_threads, PreparedScann
|
|||||||
options->munge_links = config->munge_links;
|
options->munge_links = config->munge_links;
|
||||||
options->checksum = config->checksum;
|
options->checksum = config->checksum;
|
||||||
options->one_file_system = config->one_file_system;
|
options->one_file_system = config->one_file_system;
|
||||||
|
options->preserve_devices = config->preserve_devices;
|
||||||
|
options->preserve_specials = config->preserve_specials;
|
||||||
|
options->copy_devices = config->copy_devices;
|
||||||
options->file_list = (const FileListSet*)config->files_from_set;
|
options->file_list = (const FileListSet*)config->files_from_set;
|
||||||
options->base_filters = out->base_filters;
|
options->base_filters = out->base_filters;
|
||||||
options->per_dir_filters = config->per_dir_filter;
|
options->per_dir_filters = config->per_dir_filter;
|
||||||
@@ -1271,6 +1274,14 @@ static int send_chunk_with_removal(Client* client, Chunk* chunk, Config* config,
|
|||||||
change_emit_file_sent(config, f);
|
change_emit_file_sent(config, f);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
/* --devices/--specials: a device/special node is recreated on the receiver,
|
||||||
|
not transferred as content. Send the dedicated STATUS_SPECIAL frame. */
|
||||||
|
if (f->is_special) {
|
||||||
|
if (!file_send_special(f, client->file_descriptor, config->use_metadata))
|
||||||
|
return -1;
|
||||||
|
change_emit_file_sent(config, f);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
bool stream = f->data->data == NULL && f->data->size > 0;
|
bool stream = f->data->data == NULL && f->data->size > 0;
|
||||||
bool use_sendfile =
|
bool use_sendfile =
|
||||||
(config->use_sendfile && !config->use_compression) || (stream && !config->use_compression);
|
(config->use_sendfile && !config->use_compression) || (stream && !config->use_compression);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
#include <sys/sysmacros.h>
|
||||||
#include <threads.h>
|
#include <threads.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
@@ -199,6 +200,34 @@ static void scanner_assign_hardlink(DirectoryScanner* scanner, HardLinkTable* ta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Phase 4 special/devices: detect a device (char/block), FIFO or socket entry
|
||||||
|
and, when the matching --devices/--specials flag asks it be preserved,
|
||||||
|
convert the File into a node to recreate (is_special, empty payload) with its
|
||||||
|
device rdev captured from the source stat. When the entry is not preserved
|
||||||
|
(or --copy-devices instead copies its content as an ordinary regular file)
|
||||||
|
the File is left as a normal data file. Returns true when converted. */
|
||||||
|
static bool scanner_prepare_special(bool preserve_devices, bool preserve_specials, File* file,
|
||||||
|
const struct stat* stats) {
|
||||||
|
if (!file || !stats)
|
||||||
|
return false;
|
||||||
|
bool is_device = S_ISCHR(stats->st_mode) || S_ISBLK(stats->st_mode);
|
||||||
|
bool is_fifo = S_ISFIFO(stats->st_mode);
|
||||||
|
bool is_socket = S_ISSOCK(stats->st_mode);
|
||||||
|
if (!is_device && !is_fifo && !is_socket)
|
||||||
|
return false;
|
||||||
|
bool preserve = is_device ? preserve_devices : preserve_specials;
|
||||||
|
if (!preserve)
|
||||||
|
return false;
|
||||||
|
file->is_special = true;
|
||||||
|
file->data->size = 0;
|
||||||
|
file->data->data = NULL;
|
||||||
|
if (is_device) {
|
||||||
|
file->rdev_major = (int32_t)major(stats->st_rdev);
|
||||||
|
file->rdev_minor = (int32_t)minor(stats->st_rdev);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/* Append `rel` to the caller's exclusion sink, taking `mtx` when shared across
|
/* Append `rel` to the caller's exclusion sink, taking `mtx` when shared across
|
||||||
parallel worker threads. Returns false on allocation failure (list left
|
parallel worker threads. Returns false on allocation failure (list left
|
||||||
unchanged). */
|
unchanged). */
|
||||||
@@ -420,6 +449,9 @@ DirectoryScanner* directory_scanner_create_with_options(const char* root_directo
|
|||||||
scanner->munge_links = options->munge_links;
|
scanner->munge_links = options->munge_links;
|
||||||
scanner->checksum = options->checksum;
|
scanner->checksum = options->checksum;
|
||||||
scanner->one_file_system = options->one_file_system;
|
scanner->one_file_system = options->one_file_system;
|
||||||
|
scanner->preserve_devices = options->preserve_devices;
|
||||||
|
scanner->preserve_specials = options->preserve_specials;
|
||||||
|
scanner->copy_devices = options->copy_devices;
|
||||||
scanner->failed = false;
|
scanner->failed = false;
|
||||||
scanner->root_path = str_dup(root_directory);
|
scanner->root_path = str_dup(root_directory);
|
||||||
if (!scanner->root_path) {
|
if (!scanner->root_path) {
|
||||||
@@ -990,6 +1022,9 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
|||||||
file->send_path = rel_copy;
|
file->send_path = rel_copy;
|
||||||
rel_copy = NULL;
|
rel_copy = NULL;
|
||||||
}
|
}
|
||||||
|
/* --devices/--specials: a device/FIFO/socket entry marked for preservation
|
||||||
|
becomes a node to recreate (is_special, no data, rdev captured). */
|
||||||
|
scanner_prepare_special(scanner->preserve_devices, scanner->preserve_specials, file, &stats);
|
||||||
if (scanner->hardlinks && S_ISREG(stats.st_mode))
|
if (scanner->hardlinks && S_ISREG(stats.st_mode))
|
||||||
scanner_assign_hardlink(scanner, scanner->hardlinks, file, &stats);
|
scanner_assign_hardlink(scanner, scanner->hardlinks, file, &stats);
|
||||||
if (scanner->use_metadata)
|
if (scanner->use_metadata)
|
||||||
@@ -1316,6 +1351,7 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
|
|||||||
file->send_path = rel;
|
file->send_path = rel;
|
||||||
rel = NULL;
|
rel = NULL;
|
||||||
}
|
}
|
||||||
|
scanner_prepare_special(options->preserve_devices, options->preserve_specials, file, &st);
|
||||||
if (options->hardlinks && S_ISREG(st.st_mode)) {
|
if (options->hardlinks && S_ISREG(st.st_mode)) {
|
||||||
int gid;
|
int gid;
|
||||||
bool is_first;
|
bool is_first;
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ typedef struct {
|
|||||||
bool munge_links;
|
bool munge_links;
|
||||||
bool checksum;
|
bool checksum;
|
||||||
bool one_file_system;
|
bool one_file_system;
|
||||||
|
/* Phase 4 special/devices: whether device nodes (--devices) and special files
|
||||||
|
* (--specials) are preserved via recreation, and whether --copy-devices
|
||||||
|
* copies a device's content as an ordinary regular file. */
|
||||||
|
bool preserve_devices;
|
||||||
|
bool preserve_specials;
|
||||||
|
bool copy_devices;
|
||||||
/* Phase 2 (files-from / filter layer). All pointers are shared read-only
|
/* Phase 2 (files-from / filter layer). All pointers are shared read-only
|
||||||
* across scanner instances and worker threads; ownership stays with the
|
* across scanner instances and worker threads; ownership stays with the
|
||||||
* caller (client_send). */
|
* caller (client_send). */
|
||||||
@@ -114,6 +120,10 @@ typedef struct {
|
|||||||
bool one_file_system;
|
bool one_file_system;
|
||||||
dev_t root_dev;
|
dev_t root_dev;
|
||||||
bool failed;
|
bool failed;
|
||||||
|
/* Phase 4 special/devices (see ScannerOptions). */
|
||||||
|
bool preserve_devices;
|
||||||
|
bool preserve_specials;
|
||||||
|
bool copy_devices;
|
||||||
/* Phase 2 (files-from / filter layer). */
|
/* Phase 2 (files-from / filter layer). */
|
||||||
char* root_path; /* transfer root (fs path) for rel computation */
|
char* root_path; /* transfer root (fs path) for rel computation */
|
||||||
char* current_rel; /* rel path of the open directory ("" == root) */
|
char* current_rel; /* rel path of the open directory ("" == root) */
|
||||||
|
|||||||
@@ -185,6 +185,15 @@ void print_usage(void) {
|
|||||||
printf(" --munge-links Munge symlink targets on the wire (sender)\n");
|
printf(" --munge-links Munge symlink targets on the wire (sender)\n");
|
||||||
printf(" -H, --hard-links Preserve hard-link relationships across the transfer\n");
|
printf(" -H, --hard-links Preserve hard-link relationships across the transfer\n");
|
||||||
printf(" -S, --sparse Handle sparse files efficiently\n");
|
printf(" -S, --sparse Handle sparse files efficiently\n");
|
||||||
|
printf(
|
||||||
|
" -D Preserve device and special files (implies --devices --specials)\n");
|
||||||
|
printf(
|
||||||
|
" --devices Recreate device nodes on the destination (privileged; skipped when\n");
|
||||||
|
printf(" the receiver lacks CAP_MKNOD)\n");
|
||||||
|
printf(" --specials Recreate special files (FIFOs) on the destination (sockets "
|
||||||
|
"skipped)\n");
|
||||||
|
printf(" --copy-devices Copy a source device's content as a regular file instead\n");
|
||||||
|
printf(" --write-devices Write received data into an existing destination device node\n");
|
||||||
printf(" --inplace Update files in-place (no temp+rename)\n");
|
printf(" --inplace Update files in-place (no temp+rename)\n");
|
||||||
printf(
|
printf(
|
||||||
" --preallocate Allocate destination file space up front (fail-fast on full disk)\n");
|
" --preallocate Allocate destination file space up front (fail-fast on full disk)\n");
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK ||
|
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK ||
|
||||||
status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH ||
|
status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH ||
|
||||||
status == STATUS_MKDIR || status == STATUS_MANIFEST || status == STATUS_HARDLINK ||
|
status == STATUS_MKDIR || status == STATUS_MANIFEST || status == STATUS_HARDLINK ||
|
||||||
status == STATUS_SYMLINK) {
|
status == STATUS_SYMLINK || status == STATUS_SPECIAL) {
|
||||||
if (status == STATUS_KEEPALIVE) {
|
if (status == STATUS_KEEPALIVE) {
|
||||||
if (!send_status(file_descriptor, STATUS_KEEPALIVE))
|
if (!send_status(file_descriptor, STATUS_KEEPALIVE))
|
||||||
goto fail;
|
goto fail;
|
||||||
@@ -190,6 +190,10 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
File* sym = file_receive_symlink(file_descriptor, config);
|
File* sym = file_receive_symlink(file_descriptor, config);
|
||||||
if (!sym || !sink->store_file(sym, sink->context))
|
if (!sym || !sink->store_file(sym, sink->context))
|
||||||
goto receive_error;
|
goto receive_error;
|
||||||
|
} else if (status == STATUS_SPECIAL) {
|
||||||
|
File* file = file_receive_special(file_descriptor);
|
||||||
|
if (!file || !sink->store_file(file, sink->context))
|
||||||
|
goto receive_error;
|
||||||
} else if (status == STATUS_MANIFEST) {
|
} else if (status == STATUS_MANIFEST) {
|
||||||
DeleteManifest* manifest = receive_manifest_entries(file_descriptor);
|
DeleteManifest* manifest = receive_manifest_entries(file_descriptor);
|
||||||
if (!manifest)
|
if (!manifest)
|
||||||
@@ -314,7 +318,8 @@ static bool receiver_save_file(File* file, void* context_pointer) {
|
|||||||
result = file_save_to_disk_full(context->config->receive_root_directory, file, context->config);
|
result = file_save_to_disk_full(context->config->receive_root_directory, file, context->config);
|
||||||
}
|
}
|
||||||
if (result != FILE_SAVE_ERROR && context->config->remove_source_files && !file->is_dir &&
|
if (result != FILE_SAVE_ERROR && context->config->remove_source_files && !file->is_dir &&
|
||||||
!file->skip && !receiver_outcomes_append(&context->outcomes, (unsigned char)result)) {
|
!file->is_special && !file->skip &&
|
||||||
|
!receiver_outcomes_append(&context->outcomes, (unsigned char)result)) {
|
||||||
file_destroy(file);
|
file_destroy(file);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-3
@@ -75,10 +75,17 @@ static unsigned long long per_file_serialize_size(File* file, bool use_metadata)
|
|||||||
return 0;
|
return 0;
|
||||||
size += metadata_size;
|
size += metadata_size;
|
||||||
/* Entry type marker: 0 = regular file, 1 = explicit directory entry,
|
/* Entry type marker: 0 = regular file, 1 = explicit directory entry,
|
||||||
2 = symlink entry (carries its target string). */
|
2 = symlink entry (carries its target string), 3 = special/device node
|
||||||
|
(recreated by the receiver). */
|
||||||
if (sizeof(int) > ULLONG_MAX - size)
|
if (sizeof(int) > ULLONG_MAX - size)
|
||||||
return 0;
|
return 0;
|
||||||
size += sizeof(int);
|
size += sizeof(int);
|
||||||
|
/* A special node also carries its rdev major/minor. */
|
||||||
|
if (file->is_special) {
|
||||||
|
if (2 * sizeof(int32_t) > ULLONG_MAX - size)
|
||||||
|
return 0;
|
||||||
|
size += 2 * sizeof(int32_t);
|
||||||
|
}
|
||||||
if (sizeof(size_t) > ULLONG_MAX - size)
|
if (sizeof(size_t) > ULLONG_MAX - size)
|
||||||
return 0;
|
return 0;
|
||||||
size += sizeof(size_t);
|
size += sizeof(size_t);
|
||||||
@@ -128,10 +135,19 @@ Data* chunk_serialize(Chunk* chunk, bool use_metadata) {
|
|||||||
memcpy(data_pointer, wire_path, path_len);
|
memcpy(data_pointer, wire_path, path_len);
|
||||||
data_pointer += path_len;
|
data_pointer += path_len;
|
||||||
|
|
||||||
int entry_type = file->is_symlink ? 2 : (file->is_dir ? 1 : 0);
|
int entry_type = file->is_dir ? 1 : (file->is_symlink ? 2 : (file->is_special ? 3 : 0));
|
||||||
memcpy(data_pointer, &entry_type, sizeof(int));
|
memcpy(data_pointer, &entry_type, sizeof(int));
|
||||||
data_pointer += sizeof(int);
|
data_pointer += sizeof(int);
|
||||||
|
|
||||||
|
if (file->is_special) {
|
||||||
|
int32_t special_major = file->rdev_major;
|
||||||
|
int32_t special_minor = file->rdev_minor;
|
||||||
|
memcpy(data_pointer, &special_major, sizeof(special_major));
|
||||||
|
data_pointer += sizeof(special_major);
|
||||||
|
memcpy(data_pointer, &special_minor, sizeof(special_minor));
|
||||||
|
data_pointer += sizeof(special_minor);
|
||||||
|
}
|
||||||
|
|
||||||
if (use_metadata)
|
if (use_metadata)
|
||||||
metadata_to_buf(&data_pointer, file->metadata);
|
metadata_to_buf(&data_pointer, file->metadata);
|
||||||
|
|
||||||
@@ -227,7 +243,7 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
|||||||
}
|
}
|
||||||
int entry_type;
|
int entry_type;
|
||||||
memcpy(&entry_type, data_pointer, sizeof(int));
|
memcpy(&entry_type, data_pointer, sizeof(int));
|
||||||
if (entry_type != 0 && entry_type != 1 && entry_type != 2) {
|
if (entry_type != 0 && entry_type != 1 && entry_type != 2 && entry_type != 3) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: bad entry type");
|
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: bad entry type");
|
||||||
file_destroy(file);
|
file_destroy(file);
|
||||||
array_list_delete(files);
|
array_list_delete(files);
|
||||||
@@ -235,9 +251,38 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
|||||||
}
|
}
|
||||||
file->is_dir = entry_type == 1;
|
file->is_dir = entry_type == 1;
|
||||||
file->is_symlink = entry_type == 2;
|
file->is_symlink = entry_type == 2;
|
||||||
|
file->is_special = entry_type == 3;
|
||||||
data_pointer += sizeof(int);
|
data_pointer += sizeof(int);
|
||||||
remaining_size -= sizeof(int);
|
remaining_size -= sizeof(int);
|
||||||
|
|
||||||
|
if (file->is_special) {
|
||||||
|
if (remaining_size < 2 * (int32_t)sizeof(int32_t)) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for special rdev");
|
||||||
|
file_destroy(file);
|
||||||
|
array_list_delete(files);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
int32_t special_major, special_minor;
|
||||||
|
memcpy(&special_major, data_pointer, sizeof(special_major));
|
||||||
|
data_pointer += sizeof(special_major);
|
||||||
|
memcpy(&special_minor, data_pointer, sizeof(special_minor));
|
||||||
|
data_pointer += sizeof(special_minor);
|
||||||
|
remaining_size -= 2 * sizeof(int32_t);
|
||||||
|
/* Reject an out-of-range/negative rdev here as a malformed chunk (the
|
||||||
|
same 0xffff / 0x00ffffff bounds file_special_rdev_valid uses), so a
|
||||||
|
bogus large-but-positive rdev is refused cleanly instead of being
|
||||||
|
deferred to the creation site where it would abort after the frame. */
|
||||||
|
if (special_major < 0 || special_minor < 0 || special_major > 0xffff ||
|
||||||
|
special_minor > 0x00ffffff) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: out-of-range special rdev");
|
||||||
|
file_destroy(file);
|
||||||
|
array_list_delete(files);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
file->rdev_major = special_major;
|
||||||
|
file->rdev_minor = special_minor;
|
||||||
|
}
|
||||||
|
|
||||||
if (use_metadata) {
|
if (use_metadata) {
|
||||||
if (remaining_size < sizeof(int)) {
|
if (remaining_size < sizeof(int)) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for metadata");
|
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for metadata");
|
||||||
|
|||||||
+22
-12
@@ -79,6 +79,9 @@ static void config_set_defaults(Config* config) {
|
|||||||
config->preserve_xattrs = false;
|
config->preserve_xattrs = false;
|
||||||
config->preserve_devices = false;
|
config->preserve_devices = false;
|
||||||
config->preserve_sparse = false;
|
config->preserve_sparse = false;
|
||||||
|
config->preserve_specials = false;
|
||||||
|
config->copy_devices = false;
|
||||||
|
config->write_devices = false;
|
||||||
config->itemize_changes = false;
|
config->itemize_changes = false;
|
||||||
config->out_format = NULL;
|
config->out_format = NULL;
|
||||||
config->log_file_format = NULL;
|
config->log_file_format = NULL;
|
||||||
@@ -183,16 +186,18 @@ static bool validate_received_config(const Config* config) {
|
|||||||
valid_wire_bool(config->safe_links) && valid_wire_bool(config->copy_unsafe_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_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_xattrs) && valid_wire_bool(config->preserve_devices) &&
|
||||||
valid_wire_bool(config->preserve_sparse) && valid_wire_bool(config->ignore_existing) &&
|
valid_wire_bool(config->preserve_sparse) && valid_wire_bool(config->preserve_specials) &&
|
||||||
valid_wire_bool(config->existing) && valid_wire_bool(config->update) &&
|
valid_wire_bool(config->copy_devices) && valid_wire_bool(config->write_devices) &&
|
||||||
valid_wire_bool(config->inplace) && valid_wire_bool(config->append) &&
|
valid_wire_bool(config->ignore_existing) && valid_wire_bool(config->existing) &&
|
||||||
valid_wire_bool(config->use_fsync) && valid_wire_bool(config->append_verify) &&
|
valid_wire_bool(config->update) && valid_wire_bool(config->inplace) &&
|
||||||
valid_wire_bool(config->delete_excluded) && valid_wire_bool(config->force_delete) &&
|
valid_wire_bool(config->append) && valid_wire_bool(config->use_fsync) &&
|
||||||
valid_wire_bool(config->delete_missing_args) && valid_wire_bool(config->delete_after) &&
|
valid_wire_bool(config->append_verify) && valid_wire_bool(config->delete_excluded) &&
|
||||||
valid_wire_bool(config->preallocate) && valid_wire_bool(config->delete_delay) &&
|
valid_wire_bool(config->force_delete) && valid_wire_bool(config->delete_missing_args) &&
|
||||||
valid_wire_bool(config->delete_during) && valid_wire_bool(config->relative) &&
|
valid_wire_bool(config->delete_after) && valid_wire_bool(config->preallocate) &&
|
||||||
valid_wire_bool(config->prune_empty_dirs) && valid_wire_bool(config->delay_updates) &&
|
valid_wire_bool(config->delete_delay) && valid_wire_bool(config->delete_during) &&
|
||||||
valid_wire_bool(config->mkpath) && !(config->delay_updates && config->inplace) &&
|
valid_wire_bool(config->relative) && valid_wire_bool(config->prune_empty_dirs) &&
|
||||||
|
valid_wire_bool(config->delay_updates) && valid_wire_bool(config->mkpath) &&
|
||||||
|
!(config->delay_updates && config->inplace) &&
|
||||||
!(config->delay_updates && delay_updates_staging_name_conflict(config->backup_dir)) &&
|
!(config->delay_updates && delay_updates_staging_name_conflict(config->backup_dir)) &&
|
||||||
valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) &&
|
valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) &&
|
||||||
valid_wire_bool(config->checksum) && valid_wire_bool(config->eight_bit_output) &&
|
valid_wire_bool(config->checksum) && valid_wire_bool(config->eight_bit_output) &&
|
||||||
@@ -448,12 +453,16 @@ static bool send_delta_fields(int fd, const Config* c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool send_file_options(int fd, const Config* c) {
|
static bool send_file_options(int fd, const Config* c) {
|
||||||
|
/* Device/special preservation flags cross the wire so the receiver knows a
|
||||||
|
* special/device entry must be recreated. Trailing fields; protocol 2.13.0. */
|
||||||
return send_int(fd, c->backup) && send_str(fd, c->backup_dir ? c->backup_dir : "") &&
|
return send_int(fd, c->backup) && send_str(fd, c->backup_dir ? c->backup_dir : "") &&
|
||||||
send_int(fd, c->remove_source_files) && send_int(fd, c->follow_symlinks) &&
|
send_int(fd, c->remove_source_files) && send_int(fd, c->follow_symlinks) &&
|
||||||
send_int(fd, c->copy_links) && send_int(fd, c->safe_links) &&
|
send_int(fd, c->copy_links) && send_int(fd, c->safe_links) &&
|
||||||
send_int(fd, c->copy_unsafe_links) && send_int(fd, c->preserve_hard_links) &&
|
send_int(fd, c->copy_unsafe_links) && send_int(fd, c->preserve_hard_links) &&
|
||||||
send_int(fd, c->preserve_acls) && send_int(fd, c->preserve_xattrs) &&
|
send_int(fd, c->preserve_acls) && send_int(fd, c->preserve_xattrs) &&
|
||||||
send_int(fd, c->preserve_devices) && send_int(fd, c->preserve_sparse);
|
send_int(fd, c->preserve_devices) && send_int(fd, c->preserve_sparse) &&
|
||||||
|
send_int(fd, c->preserve_specials) && send_int(fd, c->copy_devices) &&
|
||||||
|
send_int(fd, c->write_devices);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool send_selection_options(int fd, const Config* c) {
|
static bool send_selection_options(int fd, const Config* c) {
|
||||||
@@ -573,7 +582,8 @@ static bool receive_file_options(int fd, Config* c) {
|
|||||||
return false;
|
return false;
|
||||||
bool* flags[] = {&c->follow_symlinks, &c->copy_links, &c->safe_links,
|
bool* flags[] = {&c->follow_symlinks, &c->copy_links, &c->safe_links,
|
||||||
&c->copy_unsafe_links, &c->preserve_hard_links, &c->preserve_acls,
|
&c->copy_unsafe_links, &c->preserve_hard_links, &c->preserve_acls,
|
||||||
&c->preserve_xattrs, &c->preserve_devices, &c->preserve_sparse};
|
&c->preserve_xattrs, &c->preserve_devices, &c->preserve_sparse,
|
||||||
|
&c->preserve_specials, &c->copy_devices, &c->write_devices};
|
||||||
for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) {
|
for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) {
|
||||||
if (!receive_wire_bool(fd, flags[i]))
|
if (!receive_wire_bool(fd, flags[i]))
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -124,6 +124,22 @@ typedef struct Config {
|
|||||||
bool preserve_xattrs;
|
bool preserve_xattrs;
|
||||||
bool preserve_devices;
|
bool preserve_devices;
|
||||||
bool preserve_sparse;
|
bool preserve_sparse;
|
||||||
|
/* Phase 4 special/devices: preserve special files (FIFOs, sockets) and device
|
||||||
|
* nodes on the destination by recreating them (mknod/mkfifo) instead of
|
||||||
|
* transferring content. preserve_specials mirrors rsync --specials (the
|
||||||
|
* special-file half of -D); preserve_devices mirrors --devices (the device
|
||||||
|
* half of -D); both CROSS the wire so the receiver knows a special/device
|
||||||
|
* entry must be recreated rather than written as a regular file. */
|
||||||
|
bool preserve_specials;
|
||||||
|
/* --copy-devices: copy the CONTENT of a source device as an ordinary regular
|
||||||
|
* file on the destination (rsync's non-privileged safe mode), instead of
|
||||||
|
* recreating the device node. CROSSES the wire (receiver treats the entry as
|
||||||
|
* a regular file, which is the default, so this is belt-and-braces). */
|
||||||
|
bool copy_devices;
|
||||||
|
/* --write-devices: write the received data directly INTO an existing device
|
||||||
|
* node on the destination instead of creating a regular file. Dangeroud;
|
||||||
|
* see RSYNC_COMPAT.md for the tight gating. CROSSES the wire. */
|
||||||
|
bool write_devices;
|
||||||
|
|
||||||
// Issue #122: Output/logging options
|
// Issue #122: Output/logging options
|
||||||
bool itemize_changes;
|
bool itemize_changes;
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ File* file_create(const char* path) {
|
|||||||
file->hardlink_target = NULL;
|
file->hardlink_target = NULL;
|
||||||
file->is_symlink = false;
|
file->is_symlink = false;
|
||||||
file->symlink_target = NULL;
|
file->symlink_target = NULL;
|
||||||
|
file->is_special = false;
|
||||||
|
file->rdev_major = 0;
|
||||||
|
file->rdev_minor = 0;
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
#include <sys/sysmacros.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "array_list.h"
|
#include "array_list.h"
|
||||||
@@ -288,6 +289,231 @@ static FileSaveResult file_save_hardlink_sibling(const char* root_directory, con
|
|||||||
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_ERROR;
|
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Validate a transmitted special rdev against the node kind implied by `mode`'s
|
||||||
|
* S_IFMT bits. Char/block devices require a legal major/minor pair (non-negative,
|
||||||
|
* range-checked); a non-device special (FIFO/socket) must carry an empty rdev.
|
||||||
|
* Used identically on the wire path and at the secure recreation site so a
|
||||||
|
* malicious/bogus rdev can never drive a dangerous node. */
|
||||||
|
bool file_special_rdev_valid(int32_t major, int32_t minor, mode_t mode) {
|
||||||
|
bool is_device = S_ISCHR(mode) || S_ISBLK(mode);
|
||||||
|
if (is_device)
|
||||||
|
return major >= 0 && minor >= 0 && major <= 0xffff && minor <= 0x00ffffff;
|
||||||
|
/* A non-device entry must actually be a special (FIFO/socket) and carry no
|
||||||
|
rdev; a regular/dir mode is never a valid special node. */
|
||||||
|
return (S_ISFIFO(mode) || S_ISSOCK(mode)) && major == 0 && minor == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Device/special node RECREATION (--devices/--specials), receiver side ----
|
||||||
|
*
|
||||||
|
* Privilege gating: making a real device node requires CAP_MKNOD (root); making
|
||||||
|
* a FIFO works unprivileged (mkfifo). When the receiver lacks the capability,
|
||||||
|
* mknodat() fails with EPERM and the entry is SKIPPED with a warning -- the
|
||||||
|
* whole transfer must NOT abort just because the environment cannot make the
|
||||||
|
* node. CI runs non-root, so device creation is expected to skip there and
|
||||||
|
* only a FIFO is honestly assertable unprivileged.
|
||||||
|
*
|
||||||
|
* Confinement: the parent directory is opened fd-relative below the receive
|
||||||
|
* root (file_open_secure_parent: O_NOFOLLOW, no "..", root-checked) and the
|
||||||
|
* node is created with mknodat()/mkfifoat(), so it can never be placed outside
|
||||||
|
* the confined root and never follows a symlink.
|
||||||
|
*
|
||||||
|
* rdev validation: a malicious/bogus rdev (negative, out-of-range) is rejected
|
||||||
|
* here as well as on the wire (file_receive_special / chunk_deserialize), and a
|
||||||
|
* non-device entry must carry an empty rdev.
|
||||||
|
*/
|
||||||
|
static FileSaveResult file_save_special_to_disk(const char* root_directory, const File* file,
|
||||||
|
const Config* config) {
|
||||||
|
if (!root_directory || !file || !file->path || file->path[0] == '\0' ||
|
||||||
|
has_path_traversal(file->path) || !file->metadata)
|
||||||
|
return FILE_SAVE_ERROR;
|
||||||
|
|
||||||
|
mode_t mode = file->metadata->mode;
|
||||||
|
bool is_char = S_ISCHR(mode);
|
||||||
|
bool is_blk = S_ISBLK(mode);
|
||||||
|
bool is_fifo = S_ISFIFO(mode);
|
||||||
|
bool is_sock = S_ISSOCK(mode);
|
||||||
|
if (!is_char && !is_blk && !is_fifo && !is_sock) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "Special node has no device/FIFO/socket mode");
|
||||||
|
return FILE_SAVE_ERROR;
|
||||||
|
}
|
||||||
|
if (is_sock) {
|
||||||
|
/* No standard filesystem call recreates a socket; best-effort unsupported. */
|
||||||
|
log_message(LOG_LEVEL_WARNING, "socket not recreated: %s (unsupported; skipped)", file->path);
|
||||||
|
return FILE_SAVE_SKIPPED;
|
||||||
|
}
|
||||||
|
if (is_char || is_blk) {
|
||||||
|
if (!config || !config->preserve_devices)
|
||||||
|
return FILE_SAVE_SKIPPED;
|
||||||
|
} else if (is_fifo) {
|
||||||
|
if (!config || !config->preserve_specials)
|
||||||
|
return FILE_SAVE_SKIPPED;
|
||||||
|
}
|
||||||
|
/* Defense-in-depth rdev/type validation (also done on the wire path). */
|
||||||
|
if (!file_special_rdev_valid(file->rdev_major, file->rdev_minor, mode)) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "Rejected out-of-range device rdev %d:%d", file->rdev_major,
|
||||||
|
file->rdev_minor);
|
||||||
|
return FILE_SAVE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* destination = path_cat(root_directory, file->path);
|
||||||
|
if (!destination)
|
||||||
|
return FILE_SAVE_ERROR;
|
||||||
|
char* leaf = NULL;
|
||||||
|
int parent_fd = file_open_secure_parent(destination, &leaf, true);
|
||||||
|
if (parent_fd < 0) {
|
||||||
|
free(destination);
|
||||||
|
return FILE_SAVE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --existing / --ignore-existing / --update decide against the node that
|
||||||
|
would be replaced, mirroring the regular-file path. */
|
||||||
|
if (config->existing && !file_path_exists_secure(destination)) {
|
||||||
|
close(parent_fd);
|
||||||
|
free(leaf);
|
||||||
|
free(destination);
|
||||||
|
return FILE_SAVE_SKIPPED;
|
||||||
|
}
|
||||||
|
if (config->ignore_existing && file_path_exists_secure(destination)) {
|
||||||
|
close(parent_fd);
|
||||||
|
free(leaf);
|
||||||
|
free(destination);
|
||||||
|
return FILE_SAVE_SKIPPED;
|
||||||
|
}
|
||||||
|
if (config->update && file_destination_is_newer_secure(destination, file->metadata)) {
|
||||||
|
close(parent_fd);
|
||||||
|
free(leaf);
|
||||||
|
free(destination);
|
||||||
|
return FILE_SAVE_SKIPPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
dev_t rdev = 0;
|
||||||
|
mode_t create_mode;
|
||||||
|
if (is_char) {
|
||||||
|
create_mode = S_IFCHR;
|
||||||
|
rdev = makedev((unsigned)file->rdev_major, (unsigned)file->rdev_minor);
|
||||||
|
} else if (is_blk) {
|
||||||
|
create_mode = S_IFBLK;
|
||||||
|
rdev = makedev((unsigned)file->rdev_major, (unsigned)file->rdev_minor);
|
||||||
|
} else {
|
||||||
|
create_mode = S_IFIFO;
|
||||||
|
}
|
||||||
|
mode_t perms = mode & 0777;
|
||||||
|
|
||||||
|
int rc = is_fifo ? mkfifoat(parent_fd, leaf, perms)
|
||||||
|
: mknodat(parent_fd, leaf, create_mode | perms, rdev);
|
||||||
|
if (rc != 0) {
|
||||||
|
if (errno == EEXIST) {
|
||||||
|
/* An entry already exists: only skip when it already is a matching node;
|
||||||
|
never replace an existing directory or unrelated entry with the node. */
|
||||||
|
struct stat st;
|
||||||
|
if (fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) == 0 &&
|
||||||
|
((is_char && S_ISCHR(st.st_mode)) || (is_blk && S_ISBLK(st.st_mode)) ||
|
||||||
|
(is_fifo && S_ISFIFO(st.st_mode)))) {
|
||||||
|
close(parent_fd);
|
||||||
|
free(leaf);
|
||||||
|
free(destination);
|
||||||
|
return FILE_SAVE_SKIPPED;
|
||||||
|
}
|
||||||
|
log_message(LOG_LEVEL_WARNING, "refusing to replace existing entry with %s: %s (skipped)",
|
||||||
|
is_fifo ? "FIFO" : "device", file->path);
|
||||||
|
} else if (errno == EPERM || errno == EACCES) {
|
||||||
|
/* Missing CAP_MKNOD / parent write permission: the environment cannot
|
||||||
|
create the node, so skip instead of failing the whole run. */
|
||||||
|
log_message(LOG_LEVEL_WARNING,
|
||||||
|
"skipping %s: cannot create %s node (%s)\n"
|
||||||
|
" --devices/--specials node creation needs privilege (CAP_MKNOD)",
|
||||||
|
file->path, is_fifo ? "FIFO" : "device", strerror(errno));
|
||||||
|
} else {
|
||||||
|
log_message(LOG_LEVEL_WARNING, "failed to create %s %s: %s (skipped)",
|
||||||
|
is_fifo ? "FIFO" : "device", file->path, strerror(errno));
|
||||||
|
}
|
||||||
|
close(parent_fd);
|
||||||
|
free(leaf);
|
||||||
|
free(destination);
|
||||||
|
return FILE_SAVE_SKIPPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply mtime on the fresh node (utimensat, no-follow). Ownership is not
|
||||||
|
applied -- identity fchown needs an fd and would require opening the node. */
|
||||||
|
struct timespec times[2] = {
|
||||||
|
{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
|
||||||
|
{.tv_sec = file->metadata->mtime_sec, .tv_nsec = file->metadata->mtime_nsec}};
|
||||||
|
utimensat(parent_fd, leaf, times, AT_SYMLINK_NOFOLLOW);
|
||||||
|
close(parent_fd);
|
||||||
|
free(leaf);
|
||||||
|
free(destination);
|
||||||
|
return FILE_SAVE_WRITTEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --write-devices (receiver): write the received data directly into an EXISTING
|
||||||
|
* device node on the destination instead of creating a regular file. The node
|
||||||
|
* must already exist and be a char/block device (the device itself is opened and
|
||||||
|
* followed); it is confined to the receive root via file_open_secure_parent.
|
||||||
|
* Dangerous by nature, so deliberately restricted: a missing/non-device
|
||||||
|
* destination, or a write failure, is SKIPPED with a warning rather than
|
||||||
|
* allowed. On environments without device access the run still succeeds (the
|
||||||
|
* entry is skipped), never aborts. */
|
||||||
|
static FileSaveResult file_save_write_device(const char* root_directory, const File* file) {
|
||||||
|
if (!root_directory || !file || !file->path || file->path[0] == '\0' ||
|
||||||
|
has_path_traversal(file->path))
|
||||||
|
return FILE_SAVE_ERROR;
|
||||||
|
if (!file->data)
|
||||||
|
return FILE_SAVE_ERROR;
|
||||||
|
char* destination = path_cat(root_directory, file->path);
|
||||||
|
if (!destination)
|
||||||
|
return FILE_SAVE_ERROR;
|
||||||
|
char* leaf = NULL;
|
||||||
|
int parent_fd = file_open_secure_parent(destination, &leaf, false);
|
||||||
|
if (parent_fd < 0) {
|
||||||
|
free(destination);
|
||||||
|
return FILE_SAVE_SKIPPED;
|
||||||
|
}
|
||||||
|
/* O_NONBLOCK: a pre-existing FIFO at the target would otherwise block the
|
||||||
|
receive thread forever on open(2). With it the open only succeeds for a
|
||||||
|
readerless FIFO with O_RDWR (which the device fstat gate rejects anyway)
|
||||||
|
or fails with ENXIO/EAGAIN, both treated as a normal skip below. */
|
||||||
|
int fd = openat(parent_fd, leaf, O_WRONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
|
||||||
|
int saved_errno = errno;
|
||||||
|
free(leaf);
|
||||||
|
close(parent_fd);
|
||||||
|
if (fd < 0) {
|
||||||
|
free(destination);
|
||||||
|
if (saved_errno == ENXIO || saved_errno == EAGAIN) {
|
||||||
|
/* A FIFO with no reader / an unreadable special: skip like every other
|
||||||
|
unusable write-devices target instead of blocking or failing. */
|
||||||
|
log_message(LOG_LEVEL_WARNING, "write-devices: %s not writable (%s); skipped", file->path,
|
||||||
|
strerror(saved_errno));
|
||||||
|
} else {
|
||||||
|
log_message(LOG_LEVEL_WARNING, "write-devices: cannot open %s (%s); skipped", file->path,
|
||||||
|
strerror(saved_errno));
|
||||||
|
}
|
||||||
|
return FILE_SAVE_SKIPPED;
|
||||||
|
}
|
||||||
|
struct stat st;
|
||||||
|
if (fstat(fd, &st) != 0 || !(S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode))) {
|
||||||
|
close(fd);
|
||||||
|
free(destination);
|
||||||
|
log_message(LOG_LEVEL_WARNING, "write-devices: %s is not a device node; skipped", file->path);
|
||||||
|
return FILE_SAVE_SKIPPED;
|
||||||
|
}
|
||||||
|
bool ok = true;
|
||||||
|
if (file->data->size > 0) {
|
||||||
|
size_t total = (size_t)file->data->size;
|
||||||
|
size_t written = 0;
|
||||||
|
while (written < total) {
|
||||||
|
ssize_t n = write(fd, (char*)file->data->data + written, total - written);
|
||||||
|
if (n <= 0) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
written += (size_t)n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
free(destination);
|
||||||
|
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_SKIPPED;
|
||||||
|
}
|
||||||
|
|
||||||
FileSaveResult file_save_to_disk_full(const char* root_directory, const File* file,
|
FileSaveResult file_save_to_disk_full(const char* root_directory, const File* file,
|
||||||
const Config* config) {
|
const Config* config) {
|
||||||
/* Backups are incompatible with ignore-existing: moving the entry first
|
/* Backups are incompatible with ignore-existing: moving the entry first
|
||||||
@@ -314,6 +540,14 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
|||||||
return FILE_SAVE_ERROR;
|
return FILE_SAVE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Device/special node (--devices/--specials): recreate the node instead of
|
||||||
|
writing content (privilege-gated, confined, rdev-validated). */
|
||||||
|
if (file->is_special)
|
||||||
|
return file_save_special_to_disk(root_directory, file, config);
|
||||||
|
/* --write-devices: write straight into an existing device node. */
|
||||||
|
if (config && config->write_devices)
|
||||||
|
return file_save_write_device(root_directory, file);
|
||||||
|
|
||||||
/* Explicit directory entries (--dirs) carry an empty payload; the entry is
|
/* Explicit directory entries (--dirs) carry an empty payload; the entry is
|
||||||
created as a directory under the receive root, applying the same secure
|
created as a directory under the receive root, applying the same secure
|
||||||
mkdir-parent semantics as regular writes. Directories are created
|
mkdir-parent semantics as regular writes. Directories are created
|
||||||
@@ -1964,6 +2198,70 @@ File* file_receive_symlink(int file_descriptor, const Config* config) {
|
|||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Receive a device/special node frame (--devices/--specials): the leading
|
||||||
|
* STATUS_SPECIAL code has already been consumed. Payload: the destination path,
|
||||||
|
* the metadata frame (whose mode's S_IFMT bits carry the node kind), and two
|
||||||
|
* int32 rdev major/minor fields. The created File carries no payload and is
|
||||||
|
* recreated by file_save_to_disk_full (mknod/mkfifo, privilege-gated and
|
||||||
|
* confined). rdev is validated here (non-negative, range-checked) so a bogus
|
||||||
|
* value cannot drive a dangerous node on the receiver. */
|
||||||
|
File* file_receive_special(int file_descriptor) {
|
||||||
|
char* path = receive_str(file_descriptor);
|
||||||
|
if (path == NULL)
|
||||||
|
return NULL;
|
||||||
|
if (path[0] == '\0' || has_path_traversal(path)) {
|
||||||
|
char* escaped_path = output_escape(path, log_get_8_bit_output());
|
||||||
|
log_message(LOG_LEVEL_ERROR, "Invalid received special path: %s",
|
||||||
|
escaped_path ? escaped_path : "<allocation failed>");
|
||||||
|
free(escaped_path);
|
||||||
|
free(path);
|
||||||
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
int meta_ok = 1;
|
||||||
|
FileMetadata* metadata = metadata_receive(file_descriptor, &meta_ok);
|
||||||
|
if (!meta_ok) {
|
||||||
|
free(path);
|
||||||
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
int32_t major = 0;
|
||||||
|
int32_t minor = 0;
|
||||||
|
if (!receive_n_data(file_descriptor, &major, sizeof(major)) ||
|
||||||
|
!receive_n_data(file_descriptor, &minor, sizeof(minor))) {
|
||||||
|
free(path);
|
||||||
|
file_metadata_destroy(metadata);
|
||||||
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
/* A node kind must be present; without metadata mode there is no S_IFMT to
|
||||||
|
recreate from. */
|
||||||
|
if (!metadata) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "Special node sent without metadata (mode)");
|
||||||
|
free(path);
|
||||||
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (!file_special_rdev_valid(major, minor, metadata->mode)) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "Invalid special rdev received (%d:%d)", (int)major, (int)minor);
|
||||||
|
free(path);
|
||||||
|
file_metadata_destroy(metadata);
|
||||||
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
File* file = file_create(path);
|
||||||
|
free(path);
|
||||||
|
if (file == NULL) {
|
||||||
|
file_metadata_destroy(metadata);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
file->metadata = metadata;
|
||||||
|
file->is_special = true;
|
||||||
|
file->rdev_major = major;
|
||||||
|
file->rdev_minor = minor;
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
/* Read a delete-manifest frame (the STATUS_MANIFEST leading code has already
|
/* Read a delete-manifest frame (the STATUS_MANIFEST leading code has already
|
||||||
been consumed): a keep-set entry count followed by that many
|
been consumed): a keep-set entry count followed by that many
|
||||||
destination-relative paths, then a protected-prefix count followed by that
|
destination-relative paths, then a protected-prefix count followed by that
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ File* file_receive(const Config* config, int file_descriptor);
|
|||||||
File* file_receive_directory(int file_descriptor);
|
File* file_receive_directory(int file_descriptor);
|
||||||
File* file_receive_hardlink(int file_descriptor);
|
File* file_receive_hardlink(int file_descriptor);
|
||||||
File* file_receive_symlink(int file_descriptor, const Config* config);
|
File* file_receive_symlink(int file_descriptor, const Config* config);
|
||||||
|
File* file_receive_special(int file_descriptor);
|
||||||
|
bool file_special_rdev_valid(int32_t major, int32_t minor, mode_t mode);
|
||||||
File* receive_incremental_check(int fd, const Config* config, bool* skipped);
|
File* receive_incremental_check(int fd, const Config* config, bool* skipped);
|
||||||
|
|
||||||
/* A received delete-manifest frame: the keep-set (`keeps`, destination-relative
|
/* A received delete-manifest frame: the keep-set (`keeps`, destination-relative
|
||||||
|
|||||||
@@ -17,6 +17,25 @@
|
|||||||
#include "metadata.h"
|
#include "metadata.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
|
|
||||||
|
/* Transmit a device/special node (--devices / --specials) as a STATUS_SPECIAL
|
||||||
|
* frame: the destination path, the metadata frame (whose mode's S_IFMT bits
|
||||||
|
* carry the node kind) and the device rdev major/minor. The receiver validates
|
||||||
|
* the kind and rdev and recreates the node (privilege-gating the mknod). */
|
||||||
|
bool file_send_special(File* file, int file_descriptor, bool use_metadata) {
|
||||||
|
if (!file || !file_wire_path(file))
|
||||||
|
return false;
|
||||||
|
if (!send_status(file_descriptor, STATUS_SPECIAL))
|
||||||
|
return false;
|
||||||
|
if (!send_str(file_descriptor, file_wire_path(file)))
|
||||||
|
return false;
|
||||||
|
if (use_metadata && !metadata_send(file_descriptor, file->metadata))
|
||||||
|
return false;
|
||||||
|
int32_t major = file->rdev_major;
|
||||||
|
int32_t minor = file->rdev_minor;
|
||||||
|
return send_n_data(file_descriptor, &major, sizeof(major)) &&
|
||||||
|
send_n_data(file_descriptor, &minor, sizeof(minor));
|
||||||
|
}
|
||||||
|
|
||||||
bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
|
bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
|
||||||
int compression_level, bool send_path) {
|
int compression_level, bool send_path) {
|
||||||
return file_send_single_calls_with_skip(file, file_descriptor, use_metadata, compression_level,
|
return file_send_single_calls_with_skip(file, file_descriptor, use_metadata, compression_level,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
/* Client-side file send path. */
|
/* Client-side file send path. */
|
||||||
|
|
||||||
|
bool file_send_special(File* file, int file_descriptor, bool use_metadata);
|
||||||
bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
|
bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
|
||||||
int compression_level, bool send_path);
|
int compression_level, bool send_path);
|
||||||
bool file_send_single_calls_with_skip(File* file, int file_descriptor, bool use_metadata,
|
bool file_send_single_calls_with_skip(File* file, int file_descriptor, bool use_metadata,
|
||||||
|
|||||||
@@ -63,6 +63,15 @@ typedef struct {
|
|||||||
* data. `data` is empty for a symlink entry. Sender + receiver state. */
|
* data. `data` is empty for a symlink entry. Sender + receiver state. */
|
||||||
bool is_symlink;
|
bool is_symlink;
|
||||||
char* symlink_target;
|
char* symlink_target;
|
||||||
|
/* Phase 4 special/devices: when `is_special` is true this entry is a device
|
||||||
|
* or special node to be RECREATED on the destination (mknod/mkfifo) rather
|
||||||
|
* than written from `data`. The concrete node kind is derived from the
|
||||||
|
* metadata mode's S_IFMT bits (receiver-validated), and rdev_major/minor
|
||||||
|
* carry the device major/minor numbers for char/block devices. CROSSES the
|
||||||
|
* wire (protocol 2.13.0). */
|
||||||
|
bool is_special;
|
||||||
|
int32_t rdev_major;
|
||||||
|
int32_t rdev_minor;
|
||||||
} File;
|
} File;
|
||||||
|
|
||||||
/* The path that should be sent on the wire and used for the receiver-side
|
/* The path that should be sent on the wire and used for the receiver-side
|
||||||
|
|||||||
@@ -308,8 +308,9 @@ int write_thread(void* pipeline_context) {
|
|||||||
}
|
}
|
||||||
/* Record the per-file outcome so a --remove-source-files sender learns
|
/* Record the per-file outcome so a --remove-source-files sender learns
|
||||||
which sources were actually written versus skipped on the receiver.
|
which sources were actually written versus skipped on the receiver.
|
||||||
Explicit directory entries have no source and are never acknowledged. */
|
Explicit directory entries and recreated device/special nodes have no
|
||||||
if (context->config->remove_source_files && !file->is_dir && !file->skip &&
|
source and are never acknowledged (mirrors receiver.c). */
|
||||||
|
if (context->config->remove_source_files && !file->is_dir && !file->is_special && !file->skip &&
|
||||||
!receiver_outcomes_append(&context->outcomes, (unsigned char)result)) {
|
!receiver_outcomes_append(&context->outcomes, (unsigned char)result)) {
|
||||||
file_destroy(file);
|
file_destroy(file);
|
||||||
pipeline_context_receiver_note_bytes_released(context, file_bytes);
|
pipeline_context_receiver_note_bytes_released(context, file_bytes);
|
||||||
|
|||||||
@@ -96,7 +96,14 @@ enum NET_STATUS {
|
|||||||
* if --munge-links) symlink target, and optional metadata; the receiver
|
* if --munge-links) symlink target, and optional metadata; the receiver
|
||||||
* creates a symlink to the unmunged target beneath the receive root (see
|
* creates a symlink to the unmunged target beneath the receive root (see
|
||||||
* file_receive_symlink). Protocol 2.13.0. */
|
* file_receive_symlink). Protocol 2.13.0. */
|
||||||
STATUS_SYMLINK
|
STATUS_SYMLINK,
|
||||||
|
/* --devices / --specials (-D): a device or special node the sender wants
|
||||||
|
* recreated (not written from content). Payload: destination path, the
|
||||||
|
* metadata frame (whose mode's S_IFMT bits carry the node kind), and two
|
||||||
|
* int32 rdev major/minor fields. The receiver validates the kind and rdev,
|
||||||
|
* confines the node below the receive root, and recreates it (mknod/mkfifo),
|
||||||
|
* privilege-gating the mknod. Protocol 2.13.0. */
|
||||||
|
STATUS_SPECIAL
|
||||||
};
|
};
|
||||||
|
|
||||||
void io_set_fds(int read_fd, int write_fd);
|
void io_set_fds(int read_fd, int write_fd);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import filecmp
|
|||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -18,6 +19,159 @@ from common import (
|
|||||||
|
|
||||||
SOURCE_DIR = os.path.join(TEST_DATA_DIR, "feature_source")
|
SOURCE_DIR = os.path.join(TEST_DATA_DIR, "feature_source")
|
||||||
DEST_DIR = os.path.join(TEST_DATA_DIR, "feature_dest")
|
DEST_DIR = os.path.join(TEST_DATA_DIR, "feature_dest")
|
||||||
|
DEVICE_SOURCE = os.path.join(TEST_DATA_DIR, "device_source")
|
||||||
|
DEVICE_DEST = os.path.join(TEST_DATA_DIR, "device_dest")
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeviceSpecial:
|
||||||
|
"""Phase 4: --devices / --specials / -D / --copy-devices / --write-devices.
|
||||||
|
|
||||||
|
Device node CREATION (mknod) is privileged (CAP_MKNOD); CI runs non-root, so
|
||||||
|
only the FIFO path (mkfifo, unprivileged) is asserted unconditionally. The
|
||||||
|
real-device-created assertions are guarded to run only as root. Everything
|
||||||
|
else must simply succeed / skip without aborting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _setup(self):
|
||||||
|
clean_dir(DEVICE_SOURCE)
|
||||||
|
clean_dir(DEVICE_DEST)
|
||||||
|
with open(os.path.join(DEVICE_SOURCE, "plain.txt"), "wb") as f:
|
||||||
|
f.write(b"regular content\n")
|
||||||
|
|
||||||
|
def test_specials_recreates_fifo(self, shared_server):
|
||||||
|
self._setup()
|
||||||
|
os.mkfifo(os.path.join(DEVICE_SOURCE, "pipe.fifo"))
|
||||||
|
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||||
|
flags=["--specials"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:200]}"
|
||||||
|
received = get_dest_received_dir(DEVICE_DEST, DEVICE_SOURCE)
|
||||||
|
fifo = os.path.join(received, "pipe.fifo")
|
||||||
|
assert os.path.exists(fifo) and stat.S_ISFIFO(os.stat(fifo).st_mode), (
|
||||||
|
"source FIFO was not recreated as a FIFO on the destination"
|
||||||
|
)
|
||||||
|
# The regular file alongside it still transferred normally.
|
||||||
|
with open(os.path.join(received, "plain.txt")) as f:
|
||||||
|
assert f.read() == "regular content\n"
|
||||||
|
|
||||||
|
def test_D_implies_devices_and_specials_fifo(self, shared_server):
|
||||||
|
"""-D implies --devices --specials; a FIFO is preserved without a crash
|
||||||
|
even though no device mknod is attempted on the (non-root) receiver."""
|
||||||
|
self._setup()
|
||||||
|
os.mkfifo(os.path.join(DEVICE_SOURCE, "pipe.fifo"))
|
||||||
|
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||||
|
flags=["-D"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:200]}"
|
||||||
|
received = get_dest_received_dir(DEVICE_DEST, DEVICE_SOURCE)
|
||||||
|
assert stat.S_ISFIFO(os.stat(os.path.join(received, "pipe.fifo")).st_mode)
|
||||||
|
|
||||||
|
def test_copy_devices_non_crash(self, shared_server):
|
||||||
|
"""--copy-devices treats a special/device source as a regular-file copy;
|
||||||
|
a FIFO (st_size 0) must transfer without hanging or crashing."""
|
||||||
|
self._setup()
|
||||||
|
os.mkfifo(os.path.join(DEVICE_SOURCE, "device_copy.fifo"))
|
||||||
|
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||||
|
flags=["--copy-devices"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:200]}"
|
||||||
|
|
||||||
|
def test_write_devices_non_crash(self, shared_server):
|
||||||
|
"""--write-devices writes into an existing device only; when the
|
||||||
|
destination holds no device node the entry is skipped safely and the
|
||||||
|
run still succeeds (never aborts)."""
|
||||||
|
self._setup()
|
||||||
|
# Destination already holds a regular file at the source FIFO's path:
|
||||||
|
# the receiver must not clobber it and must not crash.
|
||||||
|
os.mkfifo(os.path.join(DEVICE_SOURCE, "target.fifo"))
|
||||||
|
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||||
|
flags=["--write-devices"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:200]}"
|
||||||
|
|
||||||
|
@pytest.mark.skipif(os.geteuid() != 0, reason="requires root to create device nodes")
|
||||||
|
def test_devices_recreates_real_char_device(self, shared_server):
|
||||||
|
"""Root-only: a source char device node is recreated on the destination
|
||||||
|
with the same type and rdev (privilege-gated mknod path)."""
|
||||||
|
self._setup()
|
||||||
|
src_dev = os.path.join(DEVICE_SOURCE, "realdev")
|
||||||
|
os.mknod(src_dev, stat.S_IFCHR | 0o666, os.makedev(1, 3))
|
||||||
|
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||||
|
flags=["--devices"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:200]}"
|
||||||
|
received = get_dest_received_dir(DEVICE_DEST, DEVICE_SOURCE)
|
||||||
|
st = os.lstat(os.path.join(received, "realdev"))
|
||||||
|
assert stat.S_ISCHR(st.st_mode)
|
||||||
|
assert os.major(st.st_rdev) == 1 and os.minor(st.st_rdev) == 3
|
||||||
|
|
||||||
|
def test_m_remove_source_files_keeps_recreated_fifo(self, shared_server):
|
||||||
|
"""-m --remove-source-files --specials: a recreated FIFO must NOT be
|
||||||
|
acknowledged as a removable source (its outcome must not shift the
|
||||||
|
per-file status stream, which would break the run and mis-remove the
|
||||||
|
adjacent regular file). The regular file is removed; the FIFO stays."""
|
||||||
|
self._setup()
|
||||||
|
os.mkfifo(os.path.join(DEVICE_SOURCE, "pipe.fifo"))
|
||||||
|
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||||
|
flags=["-m", "--remove-source-files", "--specials"],
|
||||||
|
port=shared_server.port)
|
||||||
|
assert result.returncode == 0, (
|
||||||
|
f"Exit {result.returncode}: {result.stderr[:300]}"
|
||||||
|
)
|
||||||
|
assert not os.path.exists(os.path.join(DEVICE_SOURCE, "plain.txt")), (
|
||||||
|
"regular source file should have been removed"
|
||||||
|
)
|
||||||
|
assert os.path.exists(os.path.join(DEVICE_SOURCE, "pipe.fifo")), (
|
||||||
|
"recreated FIFO source must never be removed"
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.skipif(os.geteuid() != 0, reason="requires root to create device nodes")
|
||||||
|
def test_m_remove_source_files_keeps_recreated_device(self, shared_server):
|
||||||
|
"""Root-only: -m --remove-source-files --devices must not remove a
|
||||||
|
source device node the receiver recreated (mirrors the single-threaded
|
||||||
|
behavior; the special is never acknowledged as a removable source)."""
|
||||||
|
self._setup()
|
||||||
|
src_dev = os.path.join(DEVICE_SOURCE, "realdev")
|
||||||
|
os.mknod(src_dev, stat.S_IFCHR | 0o666, os.makedev(1, 3))
|
||||||
|
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||||
|
flags=["-m", "--remove-source-files", "--devices"],
|
||||||
|
port=shared_server.port)
|
||||||
|
assert result.returncode == 0, (
|
||||||
|
f"Exit {result.returncode}: {result.stderr[:300]}"
|
||||||
|
)
|
||||||
|
assert not os.path.exists(os.path.join(DEVICE_SOURCE, "plain.txt")), (
|
||||||
|
"regular source file should have been removed"
|
||||||
|
)
|
||||||
|
assert os.path.exists(src_dev) and stat.S_ISCHR(os.lstat(src_dev).st_mode), (
|
||||||
|
"recreated device source must never be removed"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_write_devices_fifo_target_skips_not_hangs(self, shared_server):
|
||||||
|
"""--write-devices must never block on a pre-existing FIFO at the
|
||||||
|
destination mirror: opening with O_NONBLOCK fails with ENXIO and the
|
||||||
|
entry is skipped (the FIFO is left untouched and the run succeeds)."""
|
||||||
|
self._setup()
|
||||||
|
# Pre-plant a FIFO at the destination mirror of the source file's path.
|
||||||
|
received = get_dest_received_dir(DEVICE_DEST, DEVICE_SOURCE)
|
||||||
|
os.makedirs(received, exist_ok=True)
|
||||||
|
target = os.path.join(received, "plain.txt")
|
||||||
|
os.mkfifo(target)
|
||||||
|
result, dur = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||||
|
flags=["--write-devices"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:200]}"
|
||||||
|
assert stat.S_ISFIFO(os.lstat(target).st_mode), "FIFO target was clobbered"
|
||||||
|
assert dur < 60, "write-devices hung on a FIFO target"
|
||||||
|
|
||||||
|
def test_special_confined_to_receive_root(self, shared_server):
|
||||||
|
"""A special node is created only under the receive root; nothing is
|
||||||
|
ever materialized outside it (the receiver is confined to its
|
||||||
|
authorized root)."""
|
||||||
|
self._setup()
|
||||||
|
os.mkfifo(os.path.join(DEVICE_SOURCE, "confined.fifo"))
|
||||||
|
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||||
|
flags=["--specials"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:200]}"
|
||||||
|
# The only new FIFO is under the receive tree; its sibling watchers
|
||||||
|
# confirm the confined dest layout (no stray node at the source root).
|
||||||
|
source_fifo_escaped = os.path.join(DEVICE_DEST, "confined.fifo")
|
||||||
|
assert not os.path.lexists(source_fifo_escaped), "special escaped the receive root"
|
||||||
|
received = get_dest_received_dir(DEVICE_DEST, DEVICE_SOURCE)
|
||||||
|
assert stat.S_ISFIFO(os.stat(os.path.join(received, "confined.fifo")).st_mode)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module", autouse=True)
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
|
|||||||
@@ -216,9 +216,75 @@ static void test_chunk_symlink_roundtrip() {
|
|||||||
rmdir(link_path);
|
rmdir(link_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A --devices/--specials special entry (is_special + rdev) must round-trip
|
||||||
|
* through the chunk wire with a legal rdev. */
|
||||||
|
static void test_chunk_special_rdev_roundtrip() {
|
||||||
|
const char* path = "temp_chunk_special_node";
|
||||||
|
unlink(path);
|
||||||
|
File* special = file_create(path);
|
||||||
|
EXPECT_NOT_NULL(special);
|
||||||
|
special->is_special = true;
|
||||||
|
special->rdev_major = 1;
|
||||||
|
special->rdev_minor = 3;
|
||||||
|
struct stat st;
|
||||||
|
EXPECT_EQ_INT(stat("/dev/null", &st), 0);
|
||||||
|
special->metadata = file_metadata_create(path, &st, false, false);
|
||||||
|
EXPECT_NOT_NULL(special->metadata);
|
||||||
|
|
||||||
|
File* files[1] = {special};
|
||||||
|
Chunk* chunk = chunk_create(files, 1);
|
||||||
|
EXPECT_NOT_NULL(chunk);
|
||||||
|
Data* serialized = chunk_serialize(chunk, true);
|
||||||
|
EXPECT_NOT_NULL(serialized);
|
||||||
|
Chunk* deserialized = chunk_deserialize(serialized, true);
|
||||||
|
EXPECT_NOT_NULL(deserialized);
|
||||||
|
EXPECT_EQ_INT(deserialized->element_count, 1);
|
||||||
|
EXPECT_TRUE(deserialized->items[0]->is_special);
|
||||||
|
EXPECT_FALSE(deserialized->items[0]->is_dir);
|
||||||
|
EXPECT_EQ_INT((int)deserialized->items[0]->data->size, 0);
|
||||||
|
EXPECT_EQ_INT(deserialized->items[0]->rdev_major, 1);
|
||||||
|
EXPECT_EQ_INT(deserialized->items[0]->rdev_minor, 3);
|
||||||
|
EXPECT_NOT_NULL(deserialized->items[0]->metadata);
|
||||||
|
|
||||||
|
data_destroy(serialized);
|
||||||
|
chunk_destroy(deserialized);
|
||||||
|
chunk_destroy(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A special entry carrying an out-of-range rdev is a malformed chunk and must be
|
||||||
|
* rejected at deserialize (bounded by the same 0xffff / 0x00ffffff limits
|
||||||
|
* file_special_rdev_valid uses on the per-file wire), not deferred to the
|
||||||
|
* creation site. */
|
||||||
|
static void test_chunk_special_rdev_out_of_range_rejected() {
|
||||||
|
const char* path = "temp_chunk_special_bad_rdev";
|
||||||
|
unlink(path);
|
||||||
|
File* special = file_create(path);
|
||||||
|
EXPECT_NOT_NULL(special);
|
||||||
|
special->is_special = true;
|
||||||
|
special->rdev_major = 0x10000; /* > 0xffff */
|
||||||
|
special->rdev_minor = 3;
|
||||||
|
struct stat st;
|
||||||
|
EXPECT_EQ_INT(stat("/dev/null", &st), 0);
|
||||||
|
special->metadata = file_metadata_create(path, &st, false, false);
|
||||||
|
EXPECT_NOT_NULL(special->metadata);
|
||||||
|
|
||||||
|
File* files[1] = {special};
|
||||||
|
Chunk* chunk = chunk_create(files, 1);
|
||||||
|
EXPECT_NOT_NULL(chunk);
|
||||||
|
Data* serialized = chunk_serialize(chunk, true);
|
||||||
|
EXPECT_NOT_NULL(serialized);
|
||||||
|
Chunk* deserialized = chunk_deserialize(serialized, true);
|
||||||
|
EXPECT_NULL(deserialized);
|
||||||
|
|
||||||
|
data_destroy(serialized);
|
||||||
|
chunk_destroy(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
void test_chunk() {
|
void test_chunk() {
|
||||||
test_file_operations();
|
test_file_operations();
|
||||||
test_chunk_operations();
|
test_chunk_operations();
|
||||||
test_chunk_dir_entry_roundtrip();
|
test_chunk_dir_entry_roundtrip();
|
||||||
test_chunk_symlink_roundtrip();
|
test_chunk_symlink_roundtrip();
|
||||||
|
test_chunk_special_rdev_roundtrip();
|
||||||
|
test_chunk_special_rdev_out_of_range_rejected();
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-2
@@ -772,8 +772,6 @@ static void test_parse_args_rejects_unimplemented_options() {
|
|||||||
"--acls",
|
"--acls",
|
||||||
"-X",
|
"-X",
|
||||||
"--xattrs",
|
"--xattrs",
|
||||||
"-D",
|
|
||||||
"--devices",
|
|
||||||
"--delete-excluded",
|
"--delete-excluded",
|
||||||
"--max-delete",
|
"--max-delete",
|
||||||
"--prune-empty-dirs",
|
"--prune-empty-dirs",
|
||||||
@@ -2380,6 +2378,52 @@ static void test_parse_args_omit_link_times_long() {
|
|||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --devices / --specials / -D / --copy-devices / --write-devices parse into the
|
||||||
|
config, and the preserved flags imply metadata transmission. */
|
||||||
|
static void test_parse_args_devices_specials() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
char* argv[] = {"fastsync", "--devices", "/src", "/dst"};
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_TRUE(cfg->preserve_devices);
|
||||||
|
EXPECT_FALSE(cfg->preserve_specials);
|
||||||
|
EXPECT_TRUE(cfg->use_metadata);
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
cfg = config_create();
|
||||||
|
char* argv2[] = {"fastsync", "--specials", "/src", "/dst"};
|
||||||
|
positional_count = 0;
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv2, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_TRUE(cfg->preserve_specials);
|
||||||
|
EXPECT_FALSE(cfg->preserve_devices);
|
||||||
|
EXPECT_TRUE(cfg->use_metadata);
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
cfg = config_create();
|
||||||
|
char* argv3[] = {"fastsync", "-D", "/src", "/dst"};
|
||||||
|
positional_count = 0;
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv3, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_TRUE(cfg->preserve_devices);
|
||||||
|
EXPECT_TRUE(cfg->preserve_specials);
|
||||||
|
EXPECT_TRUE(cfg->use_metadata);
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
cfg = config_create();
|
||||||
|
char* argv4[] = {"fastsync", "--copy-devices", "/src", "/dst"};
|
||||||
|
positional_count = 0;
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv4, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_TRUE(cfg->copy_devices);
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
cfg = config_create();
|
||||||
|
char* argv5[] = {"fastsync", "--write-devices", "/src", "/dst"};
|
||||||
|
positional_count = 0;
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv5, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_TRUE(cfg->write_devices);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
void test_client_cli() {
|
void test_client_cli() {
|
||||||
test_validate_config_required_paths();
|
test_validate_config_required_paths();
|
||||||
test_parse_args_numeric_ids();
|
test_parse_args_numeric_ids();
|
||||||
@@ -2390,6 +2434,7 @@ void test_client_cli() {
|
|||||||
test_parse_args_rejects_malformed_identity();
|
test_parse_args_rejects_malformed_identity();
|
||||||
test_parse_args_preallocate();
|
test_parse_args_preallocate();
|
||||||
test_parse_args_metadata_times();
|
test_parse_args_metadata_times();
|
||||||
|
test_parse_args_devices_specials();
|
||||||
test_parse_args_atimes_long_and_short();
|
test_parse_args_atimes_long_and_short();
|
||||||
test_parse_args_omit_link_times_long();
|
test_parse_args_omit_link_times_long();
|
||||||
test_parse_args_append();
|
test_parse_args_append();
|
||||||
|
|||||||
@@ -1141,6 +1141,48 @@ static void test_config_preallocate_wire_roundtrip() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
static void test_config_devices_wire_roundtrip() {
|
||||||
|
if (is_running_under_valgrind())
|
||||||
|
return;
|
||||||
|
Config* send_cfg = config_create();
|
||||||
|
EXPECT_NOT_NULL(send_cfg);
|
||||||
|
send_cfg->send_directory = str_dup("/send/src");
|
||||||
|
send_cfg->receive_root_directory = str_dup("/send/dst");
|
||||||
|
send_cfg->preserve_devices = true;
|
||||||
|
send_cfg->preserve_specials = true;
|
||||||
|
send_cfg->copy_devices = true;
|
||||||
|
send_cfg->write_devices = true;
|
||||||
|
|
||||||
|
int p[2];
|
||||||
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
|
||||||
|
io_set_fds(p[0], p[1]);
|
||||||
|
io_set_bwlimit(0);
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
close(p[1]);
|
||||||
|
io_set_fds(p[0], p[0]);
|
||||||
|
Config* recv = config_receive(p[0]);
|
||||||
|
bool ok = recv != NULL;
|
||||||
|
if (ok) {
|
||||||
|
ok = recv->preserve_devices && recv->preserve_specials && recv->copy_devices &&
|
||||||
|
recv->write_devices;
|
||||||
|
}
|
||||||
|
config_delete(recv);
|
||||||
|
close(p[0]);
|
||||||
|
_exit(ok ? 0 : 1);
|
||||||
|
} else {
|
||||||
|
close(p[0]);
|
||||||
|
io_set_fds(p[1], p[1]);
|
||||||
|
bool sent = config_send(p[1], send_cfg);
|
||||||
|
int status;
|
||||||
|
waitpid(pid, &status, 0);
|
||||||
|
close(p[1]);
|
||||||
|
config_delete(send_cfg);
|
||||||
|
EXPECT_TRUE(sent);
|
||||||
|
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void test_config() {
|
void test_config() {
|
||||||
test_config_lifecycle();
|
test_config_lifecycle();
|
||||||
test_config_ssh_dest();
|
test_config_ssh_dest();
|
||||||
@@ -1169,6 +1211,7 @@ void test_config() {
|
|||||||
test_config_identity_wire_roundtrip();
|
test_config_identity_wire_roundtrip();
|
||||||
test_config_receive_rejects_invalid_identity();
|
test_config_receive_rejects_invalid_identity();
|
||||||
test_config_metadata_times_wire_roundtrip();
|
test_config_metadata_times_wire_roundtrip();
|
||||||
|
test_config_devices_wire_roundtrip();
|
||||||
test_config_preallocate_wire_roundtrip();
|
test_config_preallocate_wire_roundtrip();
|
||||||
}
|
}
|
||||||
test_config_delete_timing_early_helper();
|
test_config_delete_timing_early_helper();
|
||||||
|
|||||||
@@ -25,6 +25,26 @@ static void test_file_create() {
|
|||||||
file_destroy(f);
|
file_destroy(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* rdev/type validation shared by the wire path and the secure recreation site:
|
||||||
|
* a legal char/block major/minor pair is accepted, out-of-range / negative
|
||||||
|
* values and non-device entries carrying an rdev are rejected. */
|
||||||
|
static void test_file_special_rdev_valid() {
|
||||||
|
mode_t fake_char = S_IFCHR | 0600;
|
||||||
|
mode_t fake_blk = S_IFBLK | 0600;
|
||||||
|
mode_t fake_fifo = S_IFIFO | 0600;
|
||||||
|
/* char/block devices: accept a legal pair, reject negative / oversized. */
|
||||||
|
EXPECT_TRUE(file_special_rdev_valid(1, 3, fake_char));
|
||||||
|
EXPECT_TRUE(file_special_rdev_valid(0xffff, 0x00ffffff, fake_blk));
|
||||||
|
EXPECT_FALSE(file_special_rdev_valid(-1, 3, fake_char));
|
||||||
|
EXPECT_FALSE(file_special_rdev_valid(1, -1, fake_char));
|
||||||
|
EXPECT_FALSE(file_special_rdev_valid(0x10000, 3, fake_char));
|
||||||
|
EXPECT_FALSE(file_special_rdev_valid(1, 0x1000000, fake_char));
|
||||||
|
/* FIFOs/sockets must carry an empty rdev. */
|
||||||
|
EXPECT_TRUE(file_special_rdev_valid(0, 0, fake_fifo));
|
||||||
|
EXPECT_FALSE(file_special_rdev_valid(1, 0, fake_fifo));
|
||||||
|
EXPECT_FALSE(file_special_rdev_valid(0, 0, (mode_t)(S_IFREG | 0600)));
|
||||||
|
}
|
||||||
|
|
||||||
static void test_file_destroy_null() {
|
static void test_file_destroy_null() {
|
||||||
file_destroy(NULL);
|
file_destroy(NULL);
|
||||||
}
|
}
|
||||||
@@ -1004,6 +1024,7 @@ static void test_dir_entry_save_to_disk() {
|
|||||||
|
|
||||||
void test_file() {
|
void test_file() {
|
||||||
test_file_create();
|
test_file_create();
|
||||||
|
test_file_special_rdev_valid();
|
||||||
test_file_destroy_null();
|
test_file_destroy_null();
|
||||||
test_file_destroy_normal();
|
test_file_destroy_normal();
|
||||||
test_file_load_data();
|
test_file_load_data();
|
||||||
|
|||||||
Reference in New Issue
Block a user