docs: all-or-nothing TOCTOU caveat and two-section manifest budget
CI / lint (pull_request) Failing after 32s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
CI / lint (pull_request) Failing after 32s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
The walker's all-or-nothing guarantee holds only while the destination is not concurrently modified (rehearsal and delete are separate walks). The protected-prefix list shares the 16 MB MAX_MANIFEST_BYTES budget with the keep-set and each section is capped at MAX_MANIFEST_ENTRIES; an over-budget frame is rejected on the receiver with STATUS_ERROR rather than truncated.
This commit is contained in:
+13
-6
@@ -154,17 +154,24 @@ then reported an error (a truncated deletion); it now removes nothing and fails
|
|||||||
with an error naming the bound. Directories count toward the bound. A directory
|
with an error naming the bound. Directories count toward the bound. A directory
|
||||||
that still holds entries the walker leaves in place (a protected excluded file,
|
that still holds entries the walker leaves in place (a protected excluded file,
|
||||||
a kept manifest entry, a symlink) is left behind rather than failing the run —
|
a kept manifest entry, a symlink) is left behind rather than failing the run —
|
||||||
matching rsync's "cannot delete non-empty directory" behaviour.
|
matching rsync's "cannot delete non-empty directory" behaviour. The
|
||||||
|
all-or-nothing guarantee holds only while the destination is not concurrently
|
||||||
|
modified: rehearsal and delete are two separate walks, so a concurrent change
|
||||||
|
between them (another process adding or removing destination entries) can make
|
||||||
|
the actual deletion diverge from the counted set.
|
||||||
|
|
||||||
Manifest size: the sender's keep-set and protected-prefix collections (streaming
|
Manifest size: the sender's keep-set and protected-prefix collections (streaming
|
||||||
or early pre-scan) are unbounded, but the receiver rejects a manifest beyond
|
or early pre-scan) are unbounded, but the receiver rejects a manifest beyond
|
||||||
`MAX_MANIFEST_ENTRIES` (1 048 576 entries) / `MAX_MANIFEST_BYTES` (16 MB of
|
`MAX_MANIFEST_ENTRIES` (1 048 576 entries, applied to EACH section — a frame can
|
||||||
paths, counted across both sections) as a hard protocol error. In the commit
|
therefore total up to 2 097 152 entries) / `MAX_MANIFEST_BYTES` (16 MB of paths,
|
||||||
|
counted across BOTH sections) as a hard protocol error. A heavily filtered
|
||||||
|
source whose exclusion list grows large thus fails the run cleanly on the
|
||||||
|
receiver (STATUS_ERROR) instead of being silently truncated. In the commit
|
||||||
modes this only means the deletion is refused after the data already arrived; in
|
modes this only means the deletion is refused after the data already arrived; in
|
||||||
the early modes (`--delete-before`/`--delete-during`) the manifest is the first
|
the early modes (`--delete-before`/`--delete-during`) the manifest is the first
|
||||||
frame, so an oversized keep-set aborts the whole transfer BEFORE any data is
|
frame, so an oversized keep-set or protected list aborts the whole transfer
|
||||||
sent. Keep the source tree small enough for the receiver's manifest caps when
|
BEFORE any data is sent. Keep the source tree small enough for the receiver's
|
||||||
using the early timing.
|
manifest caps when using the early timing.
|
||||||
|
|
||||||
Early-delete ACK wait: after committing a large deletion (up to
|
Early-delete ACK wait: after committing a large deletion (up to
|
||||||
`MAX_SERVER_DELETE_COUNT` removals) the receiver's `STATUS_OK`/`STATUS_ERROR`
|
`MAX_SERVER_DELETE_COUNT` removals) the receiver's `STATUS_OK`/`STATUS_ERROR`
|
||||||
|
|||||||
+5
-1
@@ -37,7 +37,11 @@ typedef struct {
|
|||||||
max_delete is not SIZE_MAX the run is all-or-nothing: extras are counted
|
max_delete is not SIZE_MAX the run is all-or-nothing: extras are counted
|
||||||
first and DELETE_WALK_LIMIT_EXCEEDED is returned (with nothing removed) when
|
first and DELETE_WALK_LIMIT_EXCEEDED is returned (with nothing removed) when
|
||||||
the count would exceed the cap. `deleted_out` optionally receives the number
|
the count would exceed the cap. `deleted_out` optionally receives the number
|
||||||
of entries actually removed. */
|
of entries actually removed. The all-or-nothing guarantee holds only while
|
||||||
|
the destination tree is not being concurrently modified: the rehearsal pass
|
||||||
|
and the delete pass are two separate walks, so a concurrent change between
|
||||||
|
them (another process adding/removing entries) can make the second pass
|
||||||
|
delete a different set than the first one counted. */
|
||||||
DeleteWalkResult delete_extras_limited(const char* dest_root, ArrayList* manifest,
|
DeleteWalkResult delete_extras_limited(const char* dest_root, ArrayList* manifest,
|
||||||
size_t max_delete, const DeleteSkipEntry* skips,
|
size_t max_delete, const DeleteSkipEntry* skips,
|
||||||
int skip_count, size_t* deleted_out);
|
int skip_count, size_t* deleted_out);
|
||||||
|
|||||||
Reference in New Issue
Block a user