fix(parity): review-wave fixes (basis stats over-report, filter-rule bounds)

- receiver stats: exclude basis-dir materializations (--link-dest/--copy-dest)
  from created/literal tallies; rsync reports 0 for a basis hit, so a fresh
  --link-dest --stats run now matches (differential test_link_dest_stats_matches_rsync)
- filter wire block: reject a pattern above the glob evaluation bound and lower
  MAX_FILTER_RULES to 1024, so a crafted rule list cannot amplify delete-walk
  glob work or install a rule that silently never protects
- negative tests for over-cap count and over-long pattern
- README: correct --delete default, --stats/--progress description, add
  --delete-commit; RSYNC_COMPAT stale version labels/overclaim fixed
This commit is contained in:
2026-09-19 17:04:12 +02:00
parent 4b09213b88
commit 38d304103c
21 changed files with 130 additions and 8 deletions
@@ -382,7 +382,9 @@ _STANDALONE_REFS = {
"compare_dest": "--compare-dest",
"copy_dest": "--copy-dest",
"link_dest": "--link-dest",
"link_dest_stats": "--link-dest + --stats",
"verify_basis": "--verify-basis (FastSync-only)",
"verify_basis_default": "--verify-basis (default quick-check vs rsync)",
"added_and_deleted": "--delete across two runs",
"added_and_deleted_seed": "--delete across two runs",
"one_file_system": "-x/--one-file-system",
@@ -510,6 +512,34 @@ def test_link_dest_hardlinks_basis(parity_server_factory):
_run_and_check(case_id, result)
@requires_rsync
@parity
def test_link_dest_stats_matches_rsync(parity_server_factory):
"""A basis hit must not be counted as created or literal data: rsync reports
zero for both, so FastSync's receiver tallies must too (regression for the
basis materialization over-report)."""
case_id = "link_dest_stats"
src = os.path.join(TEST_DATA_DIR, "parity_linkds_src")
rdst = os.path.join(TEST_DATA_DIR, "parity_linkds_rdst")
fdst = os.path.join(TEST_DATA_DIR, "parity_linkds_fdst")
clean_dir(src)
_mk(os.path.join(src, "f.txt"), b"link-basis-content\n")
_pin(os.path.join(src, "f.txt"), _OLD_MTIME)
server = parity_server_factory(SUPER)
rel = os.path.abspath(src).lstrip(os.sep)
def seed(_src, rroot, froot):
_mk(os.path.join(rroot, "basis", "f.txt"), b"link-basis-content\n", _OLD_MTIME)
_mk(os.path.join(fdst, "basis", rel, "f.txt"), b"link-basis-content\n", _OLD_MTIME)
result = H.run_differential(
src, rdst, fdst,
["-a", "--link-dest=basis", "--stats"],
["-a", f"--link-dest={os.path.join(fdst, 'basis')}", "--incremental", "--stats"],
server, seed=seed, ignore_paths=("basis",), stdout=H.STDOUT_STATS)
_run_and_check(case_id, result, ref="--link-dest + --stats")
@requires_rsync
@parity
def test_copy_dest_copies_basis(parity_server_factory):
+43
View File
@@ -2120,6 +2120,48 @@ static void test_config_receive_rejects_oversized_string_budget() {
config_delete(over_bytes);
}
/* Pre-auth bounds for the receiver-side filter rule block (protocol 2.28.0).
A peer may send `protect`/`risk` rules; the receiver must reject an over-cap
count or an over-long pattern before evaluating anything, so a crafted config
cannot drive unbounded glob work or install a rule that silently never
matches. */
static void test_config_receive_rejects_bad_protect_rules() {
if (is_running_under_valgrind())
return;
/* Over-cap rule count: one more than MAX_FILTER_RULES rules. */
Config* c = config_create();
EXPECT_NOT_NULL(c);
c->send_directory = str_dup("/src");
c->receive_root_directory = str_dup("/dst");
c->filters = array_list_create(free);
EXPECT_NOT_NULL(c->filters);
for (int i = 0; i <= MAX_FILTER_RULES; i++)
EXPECT_TRUE(array_list_add(c->filters, str_dup("- *.tmp")));
EXPECT_TRUE(roundtrip_config_rejected(c));
config_delete(c);
/* A pattern longer than the receiver's evaluation bound is rejected by the
sender (mirroring the receiver's guard) instead of being sent as an inert
rule. */
c = config_create();
EXPECT_NOT_NULL(c);
c->send_directory = str_dup("/src");
c->receive_root_directory = str_dup("/dst");
c->filters = array_list_create(free);
EXPECT_NOT_NULL(c->filters);
size_t big = MAX_PROTECT_PATTERN_LEN + 1;
char* long_rule = malloc(big + 3);
EXPECT_NOT_NULL(long_rule);
long_rule[0] = '-';
long_rule[1] = ' ';
memset(long_rule + 2, 'x', big);
long_rule[big + 2] = '\0';
EXPECT_TRUE(array_list_add(c->filters, long_rule));
EXPECT_TRUE(roundtrip_config_rejected(c));
config_delete(c);
}
/* identity_copy_as_refused() is the pure, pre-snapshot refusal predicate: a
--copy-as is refused when the receiver is not root OR the effective super
mode is OFF (an operator veto), and never when --copy-as is unset. */
@@ -3304,6 +3346,7 @@ void test_config() {
test_config_wire_golden_receive();
test_config_wire_receive_bounds();
test_config_receive_rejects_overcap_counts();
test_config_receive_rejects_bad_protect_rules();
test_config_wire_roundtrip_all_fields();
test_config_preserve_attribute_wire_roundtrip();
}