From 27401aa02cc7910597fd06becb47ab21e6c54f46 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 13 Jun 2026 23:02:10 +0200 Subject: [PATCH] testsuite: fix flaky byte-corruption in check_cmd tests Several check_cmd tests corrupted a repo object by overwriting a byte at a fixed position with a fixed value, e.g.: manifest[:250] + b"x" + manifest[251:] Manifests/chunks are stored as AEAD-encrypted repo objects, so their bytes are ~random. When the target byte already happened to hold the overwrite value (~1/256), the "corruption" was a no-op: the object stayed valid, "check" returned 0 instead of 1, and the test failed intermittently (observed in test_spoofed_archive). Introduce a corrupt(data, position) helper that flips the byte (XOR 0xFF), so the result is guaranteed to differ, and use it in all the byte-overwrite corruption sites: test_corrupted_manifest, test_spoofed_manifest, test_manifest_rebuild_corrupted_chunk, test_spoofed_archive, test_verify_data and test_corrupted_file_chunk. Co-Authored-By: Claude Opus 4.8 --- src/borg/testsuite/archiver/check_cmd_test.py | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/borg/testsuite/archiver/check_cmd_test.py b/src/borg/testsuite/archiver/check_cmd_test.py index 929b96f17c..17804d7990 100644 --- a/src/borg/testsuite/archiver/check_cmd_test.py +++ b/src/borg/testsuite/archiver/check_cmd_test.py @@ -16,6 +16,19 @@ pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA +def corrupt(data, position): + """Return data with the byte at position flipped, so the result is guaranteed to differ. + + Overwriting a byte with a fixed value is not reliable: if the original byte already happens + to have that value, nothing changes and the "corruption" is a no-op. For encrypted/MACed + objects the bytes are ~random, so a fixed overwrite is a no-op ~1/256 of the time, which made + tests relying on it intermittently fail. Flipping all bits always changes the byte. + """ + if position < 0: + position += len(data) + return data[:position] + bytes([data[position] ^ 0xFF]) + data[position + 1 :] + + def check_cmd_setup(archiver): with patch.object(ChunkBuffer, "BUFFER_SIZE", 10): cmd(archiver, "repo-create", RK_ENCRYPTION) @@ -224,7 +237,7 @@ def test_corrupted_manifest(archivers, request): archive, repository = open_archive(archiver.repository_path, "archive1") with repository: manifest = repository.get_manifest() - corrupted_manifest = manifest[:250] + b"x" + manifest[251:] + corrupted_manifest = corrupt(manifest, 250) repository.put_manifest(corrupted_manifest) cmd(archiver, "check", exit_code=1) output = cmd(archiver, "check", "-v", "--repair", exit_code=0) @@ -272,10 +285,10 @@ def test_manifest_rebuild_corrupted_chunk(archivers, request): archive, repository = open_archive(archiver.repository_path, "archive1") with repository: manifest = repository.get_manifest() - corrupted_manifest = manifest[:250] + b"x" + manifest[251:] + corrupted_manifest = corrupt(manifest, 250) repository.put_manifest(corrupted_manifest) chunk = repository.get(archive.id) - corrupted_chunk = chunk[:-1] + b"x" + corrupted_chunk = corrupt(chunk, -1) repository.put(archive.id, corrupted_chunk) cmd(archiver, "check", exit_code=1) output = cmd(archiver, "check", "-v", "--repair", exit_code=0) @@ -311,7 +324,7 @@ def test_spoofed_archive(archivers, request): with repository: # attacker would corrupt or delete the manifest to trigger a rebuild of it: manifest = repository.get_manifest() - corrupted_manifest = manifest[:250] + b"x" + manifest[251:] + corrupted_manifest = corrupt(manifest, 250) repository.put_manifest(corrupted_manifest) archive_dict = { "command_line": "", @@ -372,7 +385,7 @@ def test_verify_data(archivers, request, init_args): if item.path.endswith(src_file): chunk = item.chunks[-1] data = repository.get(chunk.id) - data = data[0:123] + b"x" + data[123:] + data = corrupt(data, 123) repository.put(chunk.id, data) break @@ -407,7 +420,7 @@ def test_corrupted_file_chunk(archivers, request, init_args): if item.path.endswith(src_file): chunk = item.chunks[-1] data = repository.get(chunk.id) - data = data[0:123] + b"x" + data[123:] + data = corrupt(data, 123) repository.put(chunk.id, data) break