Skip to content

fix(transfer): read the basis via windowed I/O to survive concurrent truncation - #7052

Merged
oferchen merged 1 commit into
masterfrom
fix/basis-windowed-read-truncation
Jul 29, 2026
Merged

fix(transfer): read the basis via windowed I/O to survive concurrent truncation#7052
oferchen merged 1 commit into
masterfrom
fix/basis-windowed-read-truncation

Conversation

@oferchen

Copy link
Copy Markdown
Owner

What

Fixes a receiver crash: when the basis file was truncated by another process mid-read, oc-rsync dereferenced mmap'd basis pages and died by SIGBUS (exit 135 Linux / 138 Darwin) with no signal-safe recovery. Upstream deliberately uses windowed read(2) on the basis for exactly this reason (fileio.c:214-217, map_ptr()): a short read degrades gracefully instead of faulting.

Root cause

Two receiver paths dereferenced the mmap:

  • delta application (delta_apply/applicator.rs) — Standard writer kind, basis >= 1 MiB
  • signature hashing (receiver/basis.rs) — basis >= 64 KiB, regardless of writer kind

The io_uring delta path already dodged this via open_adaptive_buffered; the default Standard path and the signature path were still exposed.

Fix

Route both basis reads through the existing windowed reader (no unsafe), mirroring upstream:

  • applicator.rs: basis is now always opened via MapFile::open_adaptive_buffered (the is_io_uring() special-case is gone); a truncated file yields UnexpectedEof, which delta-apply reports.
  • basis.rs: generate_basis_signature reads via BufReader; a truncation yields the whole-file fallback (BasisFileResult::EMPTY). Dead MMAP_BASIS_THRESHOLD_BYTES + reopen_basis removed.

Non-truncated output is byte-identical (same bytes, same per-block sums, same order).

Tests

Two regression tests that SIGBUS pre-fix and pass now: basis_apply_survives_concurrent_truncation, generate_basis_signature_survives_concurrent_truncation. Existing mmap-parity tests rewritten to the windowed reader.

Perf

Direct A/B (512 MiB basis, warm cache, every-byte scan): windowed vs mmap = 1.03-1.06x (3-6% slower in the microbench worst case), throughput ~170-190 MB/s for both - dominated by per-byte work, not the read strategy. Not meaningful end-to-end (strong-checksum + network + disk-write dwarf it). A follow-up fast_io-hosted SIGBUS-guarded mmap could recover the microbench delta if ever warranted.

…truncation

The receiver dereferenced mmap'd basis pages on two paths - signature
hashing (basis >= 64 KiB) and delta application (basis >= 1 MiB with the
standard writer). If another process truncated the basis mid-read, the
kernel raised SIGBUS while faulting the now-unmapped tail on our behalf,
killing the process (exit 135 Linux / 138 Darwin) with no signal-safe
recovery.

Route both reads through the existing windowed read() reader that the
io_uring path already used: delta application always opens the basis via
BufferedMap, and signature hashing streams it through a BufReader sized
to MAX_MAP_SIZE. A read(2) past a shrunk EOF returns short and surfaces
as an ordinary io::Error - delta application reports it, and signature
generation drops the basis and falls back to a whole-file transfer -
exactly matching upstream, which uses read(2) for basis files for this
reason (fileio.c:214-217 comment + map_ptr()). Byte output is unchanged
for the non-truncated case; the bytes and per-block sums are identical.

Adds regression tests that truncate the basis after it is opened and
assert a graceful io::Error / whole-file fallback instead of a signal
death, on both the signature and delta-apply paths.
@github-actions github-actions Bot added the bug Something isn't working label Jul 29, 2026
@oferchen
oferchen merged commit 023288b into master Jul 29, 2026
65 checks passed
@oferchen
oferchen deleted the fix/basis-windowed-read-truncation branch July 29, 2026 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant