From 7565b410d3cad3f3a9b29cfb513955b9d5d86f9a 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 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/borg/archive.py b/src/borg/archive.py index ef020948d..943cd1afc 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -832,6 +832,7 @@ Utilization of max. archive size: {csize_max:.0%} with backup_io('open'): fd = open(path, 'wb') with fd: + trailing_hole = False ids = [c.id for c in item.chunks] for data in self.pipeline.fetch_many(ids, is_preloaded=True): if pi: @@ -840,10 +841,18 @@ Utilization of max. archive size: {csize_max:.0%} 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())