fix: correct subpiece slice bounds for non-power-of-2 piece lengths#124
Conversation
| 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() |
There was a problem hiding this comment.
nice, I missed this in my previous pr
| Bytes::copy_from_slice(&buffer.raw_slice()[start_idx..end_idx]), | ||
| ); | ||
| let buffer_slice = buffer.raw_slice(); | ||
| if end_idx <= buffer_slice.len() { |
There was a problem hiding this comment.
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
|
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. |
|
#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. |
baebe40 to
9669d82
Compare
|
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>
9669d82 to
ae9dbbe
Compare
|
The piece length in the torrent metadata is 1,986,560 bytes (not a power of 2). That's A peer legitimately requests The bug is in the disk read completion handler. The 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
I've updated this PR to fix the root cause — 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 |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When
piece_lengthfrom torrent metadata is not a multiple ofSUBPIECE_SIZE(16384), the last subpiece of every non-last piece is shorter than 16384 bytes. The disk read completion handler computedend_idxaspiece_offset + piece_len.min(SUBPIECE_SIZE), which always resolved topiece_offset + 16384— overflowing the buffer for that final subpiece.Example from monsoon crash (piece_length = 1,986,560 = 121 × 16384 + 4096):
(begin=1982464, length=4096)— valid peris_valid_piece_reqend_idx= 1,982,464 + 16,384 = 1,998,848 > buffer length 1,986,560event_loop.rs:1039, then double-panic inBuffer::drop→ process abortChanges:
event_loop.rs — Fix
end_idxto(start_idx + SUBPIECE_SIZE).min(piece_len), capping the slice at the actual piece boundary.buf_pool.rs — Guard
Buffer::dropwith!std::thread::panicking()to prevent secondary panics during unwind. Matches the existingBufferRing::droppattern from PR fix: Avoid double panic in buf_ring destructor #95.