Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/cli-flags/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,10 @@ wasmtime_option_group! {
pub max_resources: Option<usize>,
/// Fuel to use for all hostcalls to limit guest<->host data transfer.
pub hostcall_fuel: Option<usize>,
/// Maximum size, in bytes, of a single WASI filesystem or socket read
/// or write, which bounds the host buffer allocated per I/O operation.
/// (default: 2MiB)
pub max_io_size: Option<NonZeroUsize>,
/// Maximum number of component-model handle-table slots (e.g. for
/// resources, streams, futures) per store; allocating past it traps.
/// (default: 100000)
Expand Down
20 changes: 20 additions & 0 deletions crates/wasi/src/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use rand::Rng;
use std::future::Future;
use std::mem;
use std::net::SocketAddr;
use std::num::NonZeroUsize;
use std::path::Path;
use std::pin::Pin;
use tokio::io::{stderr, stdin, stdout};
Expand Down Expand Up @@ -163,6 +164,25 @@ impl WasiCtxBuilder {
self
}

/// Configures the maximum number of bytes that a single read or write of a
/// file or socket may transfer.
///
/// This bounds the size of the host-side buffer allocated for each
/// individual filesystem or socket I/O operation, which is how much data a
/// guest can cause the host to allocate at once. For example file
/// reads/writes, UDP bytes received, and TCP reads/writes will use this
/// value when limiting a buffering being allocated is appropriate.
///
/// Larger values may improve throughput of large transfers at the cost of
/// larger per-operation allocations.
///
/// The default is 64 KiB.
pub fn max_io_size(&mut self, size: NonZeroUsize) -> &mut Self {
self.filesystem.max_io_size = size;
self.sockets.max_io_size = size;
self
}

/// Appends multiple environment variables at once for this builder.
///
/// All environment variables are appended to the list of environment
Expand Down
14 changes: 13 additions & 1 deletion crates/wasi/src/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::runtime::{AbortOnDropJoinHandle, spawn_blocking};
use crate::{NamedId, WasiCtxNamedView};
use std::collections::hash_map;
use std::marker;
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::SystemTime;
use tracing::debug;
Expand Down Expand Up @@ -65,10 +66,21 @@ impl HasData for WasiFilesystem {
type Data<'a> = WasiFilesystemCtxView<'a>;
}

#[derive(Clone, Default)]
#[derive(Clone)]
pub struct WasiFilesystemCtx {
pub(crate) allow_blocking_current_thread: bool,
pub(crate) preopens: Vec<(Dir, String)>,
pub(crate) max_io_size: NonZeroUsize,
}

impl Default for WasiFilesystemCtx {
fn default() -> Self {
Self {
allow_blocking_current_thread: false,
preopens: Vec::new(),
max_io_size: crate::DEFAULT_MAX_IO_SIZE,
}
}
}

pub struct WasiFilesystemCtxView<'a> {
Expand Down
22 changes: 7 additions & 15 deletions crates/wasi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,15 @@
//!
//! For WASIp3, see [`p3`]. WASIp3 support is experimental, unstable and incomplete.

/// The maximum size, in bytes, that this crate will allocate on the host on a
/// per-read basis.
/// The default maximum size, in bytes, that this crate will allocate on the
/// host on a per-read basis.
///
/// This is used to limit the size of `wasi:io/streams.input-stream#read`, for
/// example, along with a variety of other read-style APIs. All of these APIs
/// have the shape where the guest asks the host to read some data for it, and
/// then it's returned. The data is allocated on the host, however, meaning that
/// when the guest asks for an extremely large read that could cause a very
/// large allocations on the host. For now this constant serves as a hard limit
/// on the size of the allocation a host will make.
///
/// TODO: make this configurable per-I/O? Per-WASI? It's not great that this
/// just a hard and unconfigurable limit. There are probably situations where
/// performance will be improved if this limit were higher or removed. Doing so
/// without exposing hosts to guest-controlled resource exhaustion is the
/// important part, though.
/// This is used throughout this crate as the default limiting factor for
/// buffers allocated on the host for I/O reads, writes, etc. This can be
/// overridden through `WasiCtxBuilder` in some situations, too.
const MAX_READ_SIZE_ALLOC: usize = 64 * 1024;
const DEFAULT_MAX_IO_SIZE: std::num::NonZeroUsize =
std::num::NonZeroUsize::new(MAX_READ_SIZE_ALLOC).unwrap();

pub mod cli;
pub mod clocks;
Expand Down
22 changes: 13 additions & 9 deletions crates/wasi/src/p2/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub struct FileInputStream {
file: File,
position: u64,
state: ReadState,
max_read_size: usize,
}
enum ReadState {
Idle,
Expand All @@ -76,16 +77,17 @@ enum ReadState {
Closed,
}
impl FileInputStream {
pub fn new(file: &File, position: u64) -> Self {
pub fn new(file: &File, position: u64, max_read_size: usize) -> Self {
Self {
file: file.clone(),
position,
state: ReadState::Idle,
max_read_size,
}
}

fn blocking_read(file: &std::fs::File, offset: u64, size: usize) -> ReadState {
let mut buf = BytesMut::zeroed(size.min(crate::MAX_READ_SIZE_ALLOC));
let mut buf = BytesMut::zeroed(size);
loop {
match sys::read_at_cursor_unspecified(file, &mut buf, offset) {
Ok(0) => return ReadState::Closed,
Expand Down Expand Up @@ -121,6 +123,7 @@ impl InputStream for FileInputStream {
}

let p = self.position;
let size = size.min(self.max_read_size);
self.state = ReadState::Waiting(
self.file
.spawn_blocking(move |f| Self::blocking_read(f, p, size)),
Expand Down Expand Up @@ -152,6 +155,7 @@ impl InputStream for FileInputStream {
// Before we defer to the regular `read`, make sure it has data ready to go:
if let ReadState::Idle = self.state {
let p = self.position;
let size = size.min(self.max_read_size);
self.state = self
.file
.run_blocking(move |f| Self::blocking_read(f, p, size))
Expand Down Expand Up @@ -208,6 +212,7 @@ pub(crate) struct FileOutputStream {
file: File,
mode: FileOutputMode,
state: OutputState,
max_write_size: usize,
}

enum OutputState {
Expand All @@ -221,19 +226,21 @@ enum OutputState {
}

impl FileOutputStream {
pub fn write_at(file: &File, position: u64) -> Self {
pub fn write_at(file: &File, position: u64, max_write_size: usize) -> Self {
Self {
file: file.clone(),
mode: FileOutputMode::Position(position),
state: OutputState::Ready,
max_write_size,
}
}

pub fn append(file: &File) -> Self {
pub fn append(file: &File, max_write_size: usize) -> Self {
Self {
file: file.clone(),
mode: FileOutputMode::Append,
state: OutputState::Ready,
max_write_size,
}
}

Expand Down Expand Up @@ -273,14 +280,11 @@ impl FileOutputStream {
}
}

// FIXME: configurable? determine from how much space left in file?
const FILE_WRITE_CAPACITY: usize = crate::MAX_READ_SIZE_ALLOC;

#[async_trait::async_trait]
impl OutputStream for FileOutputStream {
fn write(&mut self, buf: Bytes) -> Result<(), StreamError> {
match self.state {
OutputState::Ready if buf.len() <= FILE_WRITE_CAPACITY => {}
OutputState::Ready if buf.len() <= self.max_write_size => {}
OutputState::Ready => {
return Err(StreamError::Trap(format_err!("write exceeded budget")));
}
Expand Down Expand Up @@ -350,7 +354,7 @@ impl OutputStream for FileOutputStream {
}
fn check_write(&mut self) -> Result<usize, StreamError> {
match self.state {
OutputState::Ready => Ok(FILE_WRITE_CAPACITY),
OutputState::Ready => Ok(self.max_write_size),
OutputState::Closed => Err(StreamError::Closed),
OutputState::Error(_) => match mem::replace(&mut self.state, OutputState::Closed) {
OutputState::Error(e) => Err(StreamError::LastOperationFailed(e.into())),
Expand Down
15 changes: 6 additions & 9 deletions crates/wasi/src/p2/host/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,11 @@ impl HostDescriptor for WasiFilesystemCtxView<'_> {
offset: types::Filesize,
) -> FsResult<(Vec<u8>, bool)> {
let f = self.table.get(&fd)?.file()?;
let max = self.ctx.max_io_size.get();

let (mut buffer, r) = f
.run_blocking(move |f| {
let mut buffer = vec![
0;
len.try_into()
.unwrap_or(usize::MAX)
.min(crate::MAX_READ_SIZE_ALLOC)
];
let mut buffer = vec![0; len.try_into().unwrap_or(usize::MAX).min(max)];
let r = sys::read_at_cursor_unspecified(f, &mut buffer, offset);
(buffer, r)
})
Expand Down Expand Up @@ -372,7 +368,8 @@ impl HostDescriptor for WasiFilesystemCtxView<'_> {
};

// Create a stream view for it.
let reader: DynInputStream = Box::new(FileInputStream::new(f, offset));
let reader: DynInputStream =
Box::new(FileInputStream::new(f, offset, self.ctx.max_io_size.get()));

// Insert the stream view into the table. Trap if the table is full.
let index = self.table.push(reader)?;
Expand All @@ -393,7 +390,7 @@ impl HostDescriptor for WasiFilesystemCtxView<'_> {
}

// Create a stream view for it.
let writer = FileOutputStream::write_at(f, offset);
let writer = FileOutputStream::write_at(f, offset, self.ctx.max_io_size.get());
let writer: DynOutputStream = Box::new(writer);

// Insert the stream view into the table. Trap if the table is full.
Expand All @@ -414,7 +411,7 @@ impl HostDescriptor for WasiFilesystemCtxView<'_> {
}

// Create a stream view for it.
let appender = FileOutputStream::append(f);
let appender = FileOutputStream::append(f, self.ctx.max_io_size.get());
let appender: DynOutputStream = Box::new(appender);

// Insert the stream view into the table. Trap if the table is full.
Expand Down
4 changes: 2 additions & 2 deletions crates/wasi/src/p2/host/tcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ impl crate::p2::host::tcp::tcp::HostTcpSocket for WasiSocketsCtxView<'_> {
return Err(e.into());
}

let (input, output) = socket.take_streams()?;
let (input, output) = socket.take_streams(self.ctx.max_io_size.get())?;
let input = self.table.push_child(input, &this)?;
let output = self.table.push_child(output, &this)?;
Ok((input, output))
Expand Down Expand Up @@ -139,7 +139,7 @@ impl crate::p2::host::tcp::tcp::HostTcpSocket for WasiSocketsCtxView<'_> {
None => return Err(ErrorCode::WouldBlock.into()),
};
let mut tcp_socket = TcpSocket::new(accepted);
let (input, output) = tcp_socket.take_streams()?;
let (input, output) = tcp_socket.take_streams(self.ctx.max_io_size.get())?;

let tcp_socket = self.table.push(tcp_socket)?;
let input_stream = self.table.push_child(input, &tcp_socket)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/wasi/src/p2/host/udp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ impl udp::HostIncomingDatagramStream for WasiSocketsCtxView<'_> {
let mut datagrams = vec![];
let mut sum = 0;

while datagrams.len() < max_results && sum < crate::MAX_READ_SIZE_ALLOC {
while datagrams.len() < max_results && sum < self.ctx.max_io_size.get() {
match stream.try_recv() {
Err(ErrorCode::WouldBlock) => break,
Ok((data, remote_addr)) => {
Expand Down
Loading
Loading