From 6d85812e1214c6a93323ee9a7862f5d849ab5cd6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 11 Mar 2026 10:23:11 +0100 Subject: [PATCH] fix spurious sparse test fail on win32, fixes #7616 Python's `os.truncate()` on Windows relies on `SetEndOfFile()`, which does not initialize the extended disk space with zeroes. This means that trailing sparse holes simply leave uninitialized garbage data at the end of the file. During sparse file extraction, when the very last chunk is a sparse hole, the VDL (Valid Data Length) is not properly advanced by `os.truncate()`. As a result, reading from the end of the file fetches random disk garbage instead of zeroes, causing spurious test failures at boundaries (like 2MB or 8MB) depending on what was in the uninitialized disk sectors. Fix this by tracking trailing holes and manually writing a single `b"\0"` byte at the end of the file before truncating on Windows. Writing explicit data forces NTFS to officially advance the VDL and securely zero-fill the preceding hole space. Re-enable `test_sparse_file` on Windows. --- src/borg/archive.py | 9 +++++++++ src/borg/testsuite/archiver/extract_cmd_test.py | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 61f1f0bfa..c0ae8978a 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -865,6 +865,7 @@ Duration: {0.duration} with backup_io("open"): fd = open(path, "wb") with fd: + trailing_hole = False for data in self.pipeline.fetch_many(item.chunks, is_preloaded=True, ro_type=ROBJ_FILE_STREAM): if pi: pi.show(increase=len(data), info=[remove_surrogates(item.path)]) @@ -872,10 +873,18 @@ Duration: {0.duration} if sparse and zeros.startswith(data): # all-zero chunk: create a hole in a sparse file fd.seek(len(data), 1) + trailing_hole = True else: fd.write(data) + trailing_hole = False with backup_io("truncate_and_attrs"): pos = item_chunks_size = fd.tell() + if is_win32 and trailing_hole and pos > 0: + # Windows: truncate() does not zero-fill properly (no VDL update). + # Writing a single zero at the end forces NTFS to zero-fill the hole + # and update valid data length. + fd.seek(pos - 1) + fd.write(b"\0") fd.truncate(pos) fd.flush() self.restore_attrs(path, item, fd=fd.fileno()) diff --git a/src/borg/testsuite/archiver/extract_cmd_test.py b/src/borg/testsuite/archiver/extract_cmd_test.py index 2b89db1cb..c65f385f1 100644 --- a/src/borg/testsuite/archiver/extract_cmd_test.py +++ b/src/borg/testsuite/archiver/extract_cmd_test.py @@ -182,7 +182,6 @@ def test_birthtime(archivers, request): assert same_ts_ns(sto.st_mtime_ns, mtime * 10**9) -@pytest.mark.skipif(is_win32, reason="frequent test failures on github CI on win32") def test_sparse_file(archivers, request): archiver = request.getfixturevalue(archivers)