Skip to content

Commit f85e3f8

Browse files
authored
wasi: avoid buffer underflow with shared memory (#5543)
* wasi: avoid buffer underflow with shared memory This change fixes an issue identified when using wasi-threads to perform file reads. In order to maintain Rust safety guarantees in the presence of WebAssembly shared memory, which can be modified concurrently by any of the running threads, the WASI implementations of `fd_read` and `fd_pread` were given special code paths when shared memory is detected: in these cases, the data is first read into a host-limited buffer and then subsequently copied into linear memory. The problem was that the rather-complex logic for doing this "buffer then copy" idea for multiple IO vectors could fail due to buffer underflow. If, e.g., a read was limited by the host to 64K (or even if the read returned less than the total buffer size) the `UnsafeGuestSlice::copy_from_slice` logic would fail, complaining that the sizes of both buffers were unequal. This change both simplifies and fixes the logic: - only the first IO vector is filled; this could represent a performance penalty for threaded programs, but the "buffer then copy" idea already imposes a non-trivial overhead. This simplifies the logic, allowing us to... - resize the shared memory buffer to the exact number of bytes read * review: early return when no IO vectors passed to shared memory * fix: add empty RoFlags on early exit
1 parent f845ebb commit f85e3f8

File tree

2 files changed

+81
-101
lines changed

2 files changed

+81
-101
lines changed

crates/wasi-common/src/snapshots/preview_0.rs

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -533,12 +533,12 @@ impl wasi_unstable::WasiUnstable for WasiCtx {
533533
.get_file_mut(u32::from(fd))?
534534
.get_cap_mut(FileCaps::READ)?;
535535

536-
let iovs: Vec<wiggle::UnsafeGuestSlice<u8>> = iovs
536+
let iovs: Vec<wiggle::GuestPtr<[u8]>> = iovs
537537
.iter()
538538
.map(|iov_ptr| {
539539
let iov_ptr = iov_ptr?;
540540
let iov: types::Iovec = iov_ptr.read()?;
541-
Ok(iov.buf.as_array(iov.buf_len).as_unsafe_slice_mut()?)
541+
Ok(iov.buf.as_array(iov.buf_len))
542542
})
543543
.collect::<Result<_, Error>>()?;
544544

@@ -560,24 +560,20 @@ impl wasi_unstable::WasiUnstable for WasiCtx {
560560
.and_then(|s| Some(s.is_shared_memory()))
561561
.unwrap_or(false);
562562
let bytes_read: u64 = if is_shared_memory {
563-
// Read into an intermediate buffer.
564-
let total_available_size = iovs.iter().fold(0, |a, s| a + s.len());
565-
let mut buffer = vec![0; total_available_size.min(MAX_SHARED_BUFFER_SIZE)];
566-
let bytes_read = f.read_vectored(&mut [IoSliceMut::new(&mut buffer)]).await?;
567-
568-
// Copy the intermediate buffer into the Wasm shared memory--`iov`
569-
// by `iov`.
570-
let mut data_to_write = &buffer[0..];
571-
for iov in iovs.into_iter() {
572-
let len = data_to_write.len().min(iov.len());
573-
iov.copy_from_slice(&data_to_write[0..len])?;
574-
data_to_write = &data_to_write[len..];
575-
if data_to_write.is_empty() {
576-
break;
577-
}
563+
// For shared memory, read into an intermediate buffer. Only the
564+
// first iov will be filled and even then the read is capped by the
565+
// `MAX_SHARED_BUFFER_SIZE`, so users are expected to re-call.
566+
let iov = iovs.into_iter().next();
567+
if let Some(iov) = iov {
568+
let mut buffer = vec![0; (iov.len() as usize).min(MAX_SHARED_BUFFER_SIZE)];
569+
let bytes_read = f.read_vectored(&mut [IoSliceMut::new(&mut buffer)]).await?;
570+
iov.get_range(0..bytes_read.try_into()?)
571+
.expect("it should always be possible to slice the iov smaller")
572+
.copy_from_slice(&buffer[0..bytes_read.try_into()?])?;
573+
bytes_read
574+
} else {
575+
return Ok(0);
578576
}
579-
580-
bytes_read
581577
} else {
582578
// Convert all of the unsafe guest slices to safe ones--this uses
583579
// Wiggle's internal borrow checker to ensure no overlaps. We assume
@@ -610,12 +606,12 @@ impl wasi_unstable::WasiUnstable for WasiCtx {
610606
.get_file_mut(u32::from(fd))?
611607
.get_cap_mut(FileCaps::READ | FileCaps::SEEK)?;
612608

613-
let iovs: Vec<wiggle::UnsafeGuestSlice<u8>> = iovs
609+
let iovs: Vec<wiggle::GuestPtr<[u8]>> = iovs
614610
.iter()
615611
.map(|iov_ptr| {
616612
let iov_ptr = iov_ptr?;
617613
let iov: types::Iovec = iov_ptr.read()?;
618-
Ok(iov.buf.as_array(iov.buf_len).as_unsafe_slice_mut()?)
614+
Ok(iov.buf.as_array(iov.buf_len))
619615
})
620616
.collect::<Result<_, Error>>()?;
621617

@@ -637,26 +633,22 @@ impl wasi_unstable::WasiUnstable for WasiCtx {
637633
.and_then(|s| Some(s.is_shared_memory()))
638634
.unwrap_or(false);
639635
let bytes_read: u64 = if is_shared_memory {
640-
// Read into an intermediate buffer.
641-
let total_available_size = iovs.iter().fold(0, |a, s| a + s.len());
642-
let mut buffer = vec![0; total_available_size.min(MAX_SHARED_BUFFER_SIZE)];
643-
let bytes_read = f
644-
.read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset)
645-
.await?;
646-
647-
// Copy the intermediate buffer into the Wasm shared memory--`iov`
648-
// by `iov`.
649-
let mut data_to_write = &buffer[0..];
650-
for iov in iovs.into_iter() {
651-
let len = data_to_write.len().min(iov.len());
652-
iov.copy_from_slice(&data_to_write[0..len])?;
653-
data_to_write = &data_to_write[len..];
654-
if data_to_write.is_empty() {
655-
break;
656-
}
636+
// For shared memory, read into an intermediate buffer. Only the
637+
// first iov will be filled and even then the read is capped by the
638+
// `MAX_SHARED_BUFFER_SIZE`, so users are expected to re-call.
639+
let iov = iovs.into_iter().next();
640+
if let Some(iov) = iov {
641+
let mut buffer = vec![0; (iov.len() as usize).min(MAX_SHARED_BUFFER_SIZE)];
642+
let bytes_read = f
643+
.read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset)
644+
.await?;
645+
iov.get_range(0..bytes_read.try_into()?)
646+
.expect("it should always be possible to slice the iov smaller")
647+
.copy_from_slice(&buffer[0..bytes_read.try_into()?])?;
648+
bytes_read
649+
} else {
650+
return Ok(0);
657651
}
658-
659-
bytes_read
660652
} else {
661653
// Convert all of the unsafe guest slices to safe ones--this uses
662654
// Wiggle's internal borrow checker to ensure no overlaps. We assume

crates/wasi-common/src/snapshots/preview_1.rs

Lines changed: 49 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,12 @@ impl wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx {
299299
.get_file_mut(u32::from(fd))?
300300
.get_cap_mut(FileCaps::READ)?;
301301

302-
let iovs: Vec<wiggle::UnsafeGuestSlice<u8>> = iovs
302+
let iovs: Vec<wiggle::GuestPtr<[u8]>> = iovs
303303
.iter()
304304
.map(|iov_ptr| {
305305
let iov_ptr = iov_ptr?;
306306
let iov: types::Iovec = iov_ptr.read()?;
307-
Ok(iov.buf.as_array(iov.buf_len).as_unsafe_slice_mut()?)
307+
Ok(iov.buf.as_array(iov.buf_len))
308308
})
309309
.collect::<Result<_, Error>>()?;
310310

@@ -326,24 +326,20 @@ impl wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx {
326326
.and_then(|s| Some(s.is_shared_memory()))
327327
.unwrap_or(false);
328328
let bytes_read: u64 = if is_shared_memory {
329-
// Read into an intermediate buffer.
330-
let total_available_size = iovs.iter().fold(0, |a, s| a + s.len());
331-
let mut buffer = vec![0; total_available_size.min(MAX_SHARED_BUFFER_SIZE)];
332-
let bytes_read = f.read_vectored(&mut [IoSliceMut::new(&mut buffer)]).await?;
333-
334-
// Copy the intermediate buffer into the Wasm shared memory--`iov`
335-
// by `iov`.
336-
let mut data_to_write = &buffer[0..];
337-
for iov in iovs.into_iter() {
338-
let len = data_to_write.len().min(iov.len());
339-
iov.copy_from_slice(&data_to_write[0..len])?;
340-
data_to_write = &data_to_write[len..];
341-
if data_to_write.is_empty() {
342-
break;
343-
}
329+
// For shared memory, read into an intermediate buffer. Only the
330+
// first iov will be filled and even then the read is capped by the
331+
// `MAX_SHARED_BUFFER_SIZE`, so users are expected to re-call.
332+
let iov = iovs.into_iter().next();
333+
if let Some(iov) = iov {
334+
let mut buffer = vec![0; (iov.len() as usize).min(MAX_SHARED_BUFFER_SIZE)];
335+
let bytes_read = f.read_vectored(&mut [IoSliceMut::new(&mut buffer)]).await?;
336+
iov.get_range(0..bytes_read.try_into()?)
337+
.expect("it should always be possible to slice the iov smaller")
338+
.copy_from_slice(&buffer[0..bytes_read.try_into()?])?;
339+
bytes_read
340+
} else {
341+
return Ok(0);
344342
}
345-
346-
bytes_read
347343
} else {
348344
// Convert all of the unsafe guest slices to safe ones--this uses
349345
// Wiggle's internal borrow checker to ensure no overlaps. We assume
@@ -376,12 +372,12 @@ impl wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx {
376372
.get_file_mut(u32::from(fd))?
377373
.get_cap_mut(FileCaps::READ | FileCaps::SEEK)?;
378374

379-
let iovs: Vec<wiggle::UnsafeGuestSlice<u8>> = iovs
375+
let iovs: Vec<wiggle::GuestPtr<[u8]>> = iovs
380376
.iter()
381377
.map(|iov_ptr| {
382378
let iov_ptr = iov_ptr?;
383379
let iov: types::Iovec = iov_ptr.read()?;
384-
Ok(iov.buf.as_array(iov.buf_len).as_unsafe_slice_mut()?)
380+
Ok(iov.buf.as_array(iov.buf_len))
385381
})
386382
.collect::<Result<_, Error>>()?;
387383

@@ -403,26 +399,22 @@ impl wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx {
403399
.and_then(|s| Some(s.is_shared_memory()))
404400
.unwrap_or(false);
405401
let bytes_read: u64 = if is_shared_memory {
406-
// Read into an intermediate buffer.
407-
let total_available_size = iovs.iter().fold(0, |a, s| a + s.len());
408-
let mut buffer = vec![0; total_available_size.min(MAX_SHARED_BUFFER_SIZE)];
409-
let bytes_read = f
410-
.read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset)
411-
.await?;
412-
413-
// Copy the intermediate buffer into the Wasm shared memory--`iov`
414-
// by `iov`.
415-
let mut data_to_write = &buffer[0..];
416-
for iov in iovs.into_iter() {
417-
let len = data_to_write.len().min(iov.len());
418-
iov.copy_from_slice(&data_to_write[0..len])?;
419-
data_to_write = &data_to_write[len..];
420-
if data_to_write.is_empty() {
421-
break;
422-
}
402+
// For shared memory, read into an intermediate buffer. Only the
403+
// first iov will be filled and even then the read is capped by the
404+
// `MAX_SHARED_BUFFER_SIZE`, so users are expected to re-call.
405+
let iov = iovs.into_iter().next();
406+
if let Some(iov) = iov {
407+
let mut buffer = vec![0; (iov.len() as usize).min(MAX_SHARED_BUFFER_SIZE)];
408+
let bytes_read = f
409+
.read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset)
410+
.await?;
411+
iov.get_range(0..bytes_read.try_into()?)
412+
.expect("it should always be possible to slice the iov smaller")
413+
.copy_from_slice(&buffer[0..bytes_read.try_into()?])?;
414+
bytes_read
415+
} else {
416+
return Ok(0);
423417
}
424-
425-
bytes_read
426418
} else {
427419
// Convert all of the unsafe guest slices to safe ones--this uses
428420
// Wiggle's internal borrow checker to ensure no overlaps. We assume
@@ -1169,12 +1161,12 @@ impl wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx {
11691161
.get_file_mut(u32::from(fd))?
11701162
.get_cap_mut(FileCaps::READ)?;
11711163

1172-
let iovs: Vec<wiggle::UnsafeGuestSlice<u8>> = ri_data
1164+
let iovs: Vec<wiggle::GuestPtr<[u8]>> = ri_data
11731165
.iter()
11741166
.map(|iov_ptr| {
11751167
let iov_ptr = iov_ptr?;
11761168
let iov: types::Iovec = iov_ptr.read()?;
1177-
Ok(iov.buf.as_array(iov.buf_len).as_unsafe_slice_mut()?)
1169+
Ok(iov.buf.as_array(iov.buf_len))
11781170
})
11791171
.collect::<Result<_, Error>>()?;
11801172

@@ -1196,26 +1188,22 @@ impl wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx {
11961188
.and_then(|s| Some(s.is_shared_memory()))
11971189
.unwrap_or(false);
11981190
let (bytes_read, ro_flags) = if is_shared_memory {
1199-
// Read into an intermediate buffer.
1200-
let total_available_size = iovs.iter().fold(0, |a, s| a + s.len());
1201-
let mut buffer = vec![0; total_available_size.min(MAX_SHARED_BUFFER_SIZE)];
1202-
let (bytes_read, ro_flags) = f
1203-
.sock_recv(&mut [IoSliceMut::new(&mut buffer)], RiFlags::from(ri_flags))
1204-
.await?;
1205-
1206-
// Copy the intermediate buffer into the Wasm shared memory--`iov`
1207-
// by `iov`.
1208-
let mut data_to_write = &buffer[0..];
1209-
for iov in iovs.into_iter() {
1210-
let len = data_to_write.len().min(iov.len());
1211-
iov.copy_from_slice(&data_to_write[0..len])?;
1212-
data_to_write = &data_to_write[len..];
1213-
if data_to_write.is_empty() {
1214-
break;
1215-
}
1191+
// For shared memory, read into an intermediate buffer. Only the
1192+
// first iov will be filled and even then the read is capped by the
1193+
// `MAX_SHARED_BUFFER_SIZE`, so users are expected to re-call.
1194+
let iov = iovs.into_iter().next();
1195+
if let Some(iov) = iov {
1196+
let mut buffer = vec![0; (iov.len() as usize).min(MAX_SHARED_BUFFER_SIZE)];
1197+
let (bytes_read, ro_flags) = f
1198+
.sock_recv(&mut [IoSliceMut::new(&mut buffer)], RiFlags::from(ri_flags))
1199+
.await?;
1200+
iov.get_range(0..bytes_read.try_into()?)
1201+
.expect("it should always be possible to slice the iov smaller")
1202+
.copy_from_slice(&buffer[0..bytes_read.try_into()?])?;
1203+
(bytes_read, ro_flags)
1204+
} else {
1205+
return Ok((0, RoFlags::empty().into()));
12161206
}
1217-
1218-
(bytes_read, ro_flags)
12191207
} else {
12201208
// Convert all of the unsafe guest slices to safe ones--this uses
12211209
// Wiggle's internal borrow checker to ensure no overlaps. We assume

0 commit comments

Comments
 (0)