Skip to content

Validate chunk size in IndexPos read path to prevent panic/hang#350

Merged
folbricht merged 1 commit into
masterfrom
fix-readseeker-chunk-size-mismatch
May 19, 2026
Merged

Validate chunk size in IndexPos read path to prevent panic/hang#350
folbricht merged 1 commit into
masterfrom
fix-readseeker-chunk-size-mismatch

Conversation

@folbricht
Copy link
Copy Markdown
Owner

Problem

IndexPos (used by desync cat and the desync mount-index FUSE read handler) assumes the invariant len(curChunk) == Index.Chunks[curChunkIdx].Size, but nothing enforced it:

  • findOffset bounds curChunkOffset against the declared size from the index, not the actual chunk data.
  • loadChunk stored the chunk bytes from the store without comparing their length to the declared size.
  • The index parser only rejects Size > ChunkSizeMax, so a declared size like 1000 against a real 4‑byte chunk (default max 256 KiB) passes validation.

A .caibx that references a real chunk ID present in the store but over-declares that chunk's Size therefore triggers:

  • PanicSeek(200) sets curChunkOffset = 200 (within the declared 1000), then Read loads the real 4‑byte chunk and evaluates curChunk[200:4]slice bounds out of range at readseeker.go:148. With mount-index this crashes the daemon and orphans the mount.
  • Infinite loop — when the offset reaches the real end of a non-last chunk but is still below the declared size, Read copies 0 bytes and Seek(0, SeekCurrent) makes no progress, hanging the caller forever.

Fix

After loading a chunk, verify its actual length matches the size the index declares for it and return an error on mismatch — the same check AssembleFile already performs (assemble.go:73). The null-chunk shortcut path is covered too (a legitimate null chunk always declares Size == ChunkSizeMax). This restores the invariant the rest of the read path relies on, eliminating both the panic and the hang; callers receive an "unexpected size for chunk" error through the normal io.Reader path instead.

Tests

Added readseeker_test.go:

  • TestIndexReadSeekerSizeMismatchPanic — over-declared chunk + seek/read returns an error instead of panicking.
  • TestIndexReadSeekerSizeMismatchNoLoop — short non-last chunk returns an error promptly instead of spinning (goroutine + 5s timeout guard).
  • TestIndexReadSeekerValid — a well-formed multi-chunk index (including a memory-served null chunk) still reads back correctly, with and without seeking.

Reverting the fix makes the first test panic at readseeker.go:148 and the second time out; the valid-path test passes either way. Full library suite and cmd/desync build pass.

IndexPos.Read assumes len(curChunk) equals the size the index declares
for the chunk, but loadChunk never verified it. An index that declares a
chunk larger than the data actually stored for that chunk ID could drive
curChunkOffset past len(curChunk), making the slice in Read panic with
"slice bounds out of range". When the declared size is larger but the
offset lands exactly at the real end of a non-last chunk, Read instead
copies zero bytes and Seek(0) makes no progress, spinning forever.

Both are reachable with an untrusted .caibx via 'desync cat' and the
'desync mount-index' FUSE read handler.

Verify after loading that the actual chunk length matches the declared
index size and return an error otherwise, the same check AssembleFile
already performs. Covers the null-chunk shortcut too.
@folbricht folbricht merged commit 43f08c2 into master May 19, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant