From d4ae6f064c09be1e9bc6fc327854f21e745c88d8 Mon Sep 17 00:00:00 2001 From: "Tomasz (Tom) Kramkowski" Date: Fri, 28 Aug 2026 12:21:34 +0100 Subject: [PATCH] reject file lengths that do not fit in usize Previously on 32 bit platforms we'd end up potentially accepting the size initially, even if it was wrong, and then either returning errors or panicking. Now we just return the same consistent error that the upstream mmap crate gives when the requested size is too large. The problem of always relying on mmap is that it's limited to 2GiB for 32 bit platforms anyway. So there should be a better fallback here. But for now this just makes the errors more consistent, since for any size under 4GiB but above 2GiB on 32 bit we'd give this error, with any truncated size which fell outside this range getting incorrectly accepted with no error. --- src/lib.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d25ffbc..3b64870 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -238,9 +238,14 @@ impl AsyncHttpRangeReader { }; // Allocate a memory map to hold the data - let memory_map = memmap2::MmapOptions::new() - .len(complete_length as usize) - .map_anon() + let memory_map = usize::try_from(complete_length) + .map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("file length of {complete_length} bytes cannot be represented on this platform"), + ) + }) + .and_then(MmapMut::map_anon) .map_err(Arc::new) .map_err(AsyncHttpRangeReaderError::MemoryMapError)?; @@ -339,9 +344,14 @@ impl AsyncHttpRangeReader { .map_err(|_err| AsyncHttpRangeReaderError::ContentLengthMissing)?; // Allocate a memory map to hold the data - let memory_map = memmap2::MmapOptions::new() - .len(content_length as _) - .map_anon() + let memory_map = usize::try_from(content_length) + .map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("file length of {content_length} bytes cannot be represented on this platform"), + ) + }) + .and_then(MmapMut::map_anon) .map_err(Arc::new) .map_err(AsyncHttpRangeReaderError::MemoryMapError)?; @@ -585,7 +595,7 @@ async fn stream_response( ) -> bool { // Enforce request channel contract assert!( - (end_exclusive as usize) <= memory_map.len(), + end_exclusive <= memory_map.len() as u64, "end is outside of memory map {} > {}", end_exclusive, memory_map.len() @@ -952,6 +962,50 @@ mod test { ); } + #[rstest] + #[case(CheckSupportMethod::Head)] + #[case(CheckSupportMethod::NegativeRangeRequest(1))] + #[tokio::test] + async fn test_file_length_too_large( + #[case] check_method: CheckSupportMethod, + #[values(1_u64 << 63, (1_u64 << 63) + 1)] length: u64, + ) { + // Neither length can be mapped on any platform. + let response = axum::http::Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header(header::ACCEPT_RANGES, "bytes") + .header(header::CONTENT_LENGTH, length) + .header(header::CONTENT_RANGE, format!("bytes 0-0/{length}")) + .body("") + .unwrap() + .into(); + let client = Client::new(); + let url = Url::parse("http://localhost/file").unwrap(); + let err = match check_method { + CheckSupportMethod::Head => { + AsyncHttpRangeReader::from_head_response( + client, + response, + url, + HeaderMap::default(), + ) + .await + } + CheckSupportMethod::NegativeRangeRequest(_) => { + AsyncHttpRangeReader::from_range_response( + client, + response, + url, + HeaderMap::default(), + ) + .await + } + } + .unwrap_err(); + + assert_matches!(err, AsyncHttpRangeReaderError::MemoryMapError(_)); + } + /// Spawn a server where the HEAD response reports `head_size` bytes, and range requests always /// claim to be `pretend_size` bytes, while actually serving `actual_size`. async fn spawn_mismatch_server(