Skip to content

fix: correct subpiece slice bounds for non-power-of-2 piece lengths#124

Merged
Nehliin merged 2 commits into
Nehliin:masterfrom
grenade:fix-double-panic
Apr 14, 2026
Merged

fix: correct subpiece slice bounds for non-power-of-2 piece lengths#124
Nehliin merged 2 commits into
Nehliin:masterfrom
grenade:fix-double-panic

Conversation

@grenade
Copy link
Copy Markdown
Contributor

@grenade grenade commented Apr 5, 2026

When piece_length from torrent metadata is not a multiple of SUBPIECE_SIZE (16384), the last subpiece of every non-last piece is shorter than 16384 bytes. The disk read completion handler computed end_idx as piece_offset + piece_len.min(SUBPIECE_SIZE), which always resolved to piece_offset + 16384 — overflowing the buffer for that final subpiece.

Example from monsoon crash (piece_length = 1,986,560 = 121 × 16384 + 4096):

  • Peer requests (begin=1982464, length=4096) — valid per is_valid_piece_req
  • end_idx = 1,982,464 + 16,384 = 1,998,848 > buffer length 1,986,560
  • Panic at event_loop.rs:1039, then double-panic in Buffer::drop → process abort

Changes:

  1. event_loop.rs — Fix end_idx to (start_idx + SUBPIECE_SIZE).min(piece_len), capping the slice at the actual piece boundary.

  2. buf_pool.rs — Guard Buffer::drop with !std::thread::panicking() to prevent secondary panics during unwind. Matches the existing BufferRing::drop pattern from PR fix: Avoid double panic in buf_ring destructor #95.

if self.inner.is_some() && self.pool_alive.load(std::sync::atomic::Ordering::Acquire) {
if self.inner.is_some()
&& self.pool_alive.load(std::sync::atomic::Ordering::Acquire)
&& !std::thread::panicking()
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, I missed this in my previous pr

Comment thread bittorrent/src/event_loop.rs Outdated
Bytes::copy_from_slice(&buffer.raw_slice()[start_idx..end_idx]),
);
let buffer_slice = buffer.raw_slice();
if end_idx <= buffer_slice.len() {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure we want this validation check here since we should be able to catch this much earlier and avoid unnecessary I/O. If you can consistently reproduce this I would be very much interested in what the value of the piece length is inside of the torrent metadata and see what is actually queued here https://github.com/Nehliin/vortex/blob/master/bittorrent/src/peer_comm/peer_connection.rs#L945

There must be some bug elsewhere because the buffers should match the size of the of the pieces and the request validation should prevent any malicious packets from triggering disk reads.

I do see that the is_valid_piece_req only checks that "begin" is divisible with SUBPIECE_SIZE but not if it's resonable compared to the piece length. Could you confirm if it's a weird begin value instead? If so we should update the piece validation logic instead

@Nehliin
Copy link
Copy Markdown
Owner

Nehliin commented Apr 8, 2026

I merged #129 which hopefully fixes the crash you've seen. Let me know if it's still a problem post patch. I'd still merge the double panic fix though if you keep that in the PR.

@grenade
Copy link
Copy Markdown
Contributor Author

grenade commented Apr 9, 2026

#129 tested switched monsoon back to vortex upstream. however this reintroduced panic crashes in monsoon. so for monsoon the only option is to remain dependent on a fork or await a merge on this pr or a new patch.

@grenade grenade force-pushed the fix-double-panic branch from baebe40 to 9669d82 Compare April 9, 2026 16:11
@Nehliin
Copy link
Copy Markdown
Owner

Nehliin commented Apr 12, 2026

hmm okay, could you see the value of the piece length vs the piece size reported in the torrent metadata? I have a hard time myself reproducing this

The end_idx computation used piece_len.min(SUBPIECE_SIZE) which always
resolved to SUBPIECE_SIZE (16384) for normal pieces. When piece_length
is not a multiple of SUBPIECE_SIZE (e.g. 1986560 = 121*16384 + 4096),
the last subpiece starts at offset 1982464 and is only 4096 bytes, but
end_idx was computed as 1982464 + 16384 = 1998848, overflowing the
buffer of 1986560 bytes.

Fix: cap end_idx at piece_len instead of adding a fixed SUBPIECE_SIZE.

The buf_pool.rs panicking() guard (matching the existing BufferRing::drop
pattern from PR Nehliin#95) is retained as defense-in-depth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@grenade
Copy link
Copy Markdown
Contributor Author

grenade commented Apr 13, 2026

The piece length in the torrent metadata is 1,986,560 bytes (not a power of 2). That's 121 × 16384 + 4096, so the last subpiece of every non-last piece is only 4,096 bytes.

A peer legitimately requests (begin=1982464, length=4096) — this passes is_valid_piece_req correctly since 1982464 + 4096 ≤ 1986560. The validation in #129 can't reject this request, nor should it.

The bug is in the disk read completion handler. The end_idx computation:

let end_idx = start_idx
    + state.piece_selector.piece_len(piece_idx)
        .min(SUBPIECE_SIZE as u32) as usize;

always adds 16,384 bytes from the offset because piece_len.min(SUBPIECE_SIZE) resolves to SUBPIECE_SIZE for any normal-sized piece. When piece_offset + 16384 > piece_length (last subpiece of a piece whose length isn't a multiple of 16384), it overflows the buffer:

  • start_idx = 1,982,464
  • end_idx = 1,982,464 + 16,384 = 1,998,848
  • buffer.len() = 1,986,560
  • overflow = 12,288 bytes

I've updated this PR to fix the root cause — end_idx is now capped at piece_len instead of blindly adding SUBPIECE_SIZE:

let piece_len = state.piece_selector.piece_len(piece_idx) as usize;
let end_idx = (start_idx + SUBPIECE_SIZE as usize).min(piece_len);

This is why you can't reproduce it easily — most torrents use power-of-2 piece lengths (256 KiB, 512 KiB, 1 MiB…) which are always exact multiples of 16,384. It only triggers with non-standard piece lengths.

The buf_pool.rs panicking() guard is kept as defense-in-depth (matching the existing BufferRing::drop pattern from #95).

@grenade grenade changed the title fix: prevent double-panic on buffer slice bounds violation fix: correct subpiece slice bounds for non-power-of-2 piece lengths Apr 13, 2026
Copy link
Copy Markdown
Owner

@Nehliin Nehliin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice find!

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Nehliin Nehliin merged commit 1547372 into Nehliin:master Apr 14, 2026
2 of 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.

2 participants