Skip to content
Open
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
31 changes: 31 additions & 0 deletions crates/c-api/include/wasmtime/_store_class.hh
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,37 @@ public:
}
#endif // WASMTIME_FEATURE_WASI

#ifdef WASMTIME_FEATURE_WASI_HTTP
/// Initializes the WASI HTTP state used by this store.
void set_wasi_http() { wasmtime_context_set_wasi_http(ptr); }

/// Sets the maximum size, in bytes, of each WASI HTTP `fields` resource
/// (headers and trailers).
///
/// Fails if `set_wasi_http` has not been called on this store.
Result<std::monostate> set_wasi_http_field_size_limit(size_t limit) {
auto *error = wasmtime_context_set_wasi_http_field_size_limit(ptr, limit);
if (error != nullptr) {
return Error(error);
}
return std::monostate();
}

/// Sets the maximum combined size, in bytes, of a WASI HTTP request's
/// method, scheme, authority, and path-with-query strings.
///
/// Fails if `set_wasi_http` has not been called on this store.
Result<std::monostate>
set_wasi_http_request_strings_size_limit(size_t limit) {
auto *error =
wasmtime_context_set_wasi_http_request_strings_size_limit(ptr, limit);
if (error != nullptr) {
return Error(error);
}
return std::monostate();
}
#endif // WASMTIME_FEATURE_WASI_HTTP

/// Configures this store's epoch deadline to be the specified number of
/// ticks beyond the engine's current epoch.
///
Expand Down
33 changes: 33 additions & 0 deletions crates/c-api/include/wasmtime/store.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,39 @@ wasmtime_context_set_wasi(wasmtime_context_t *context, wasi_config_t *wasi);
WASM_API_EXTERN void
wasmtime_context_set_wasi_http(wasmtime_context_t *context);

/**
* \brief Sets the maximum size, in bytes, of each WASI HTTP `fields` resource
* (headers and trailers).
*
* This is roughly a limit on the in-memory representation of the fields, so it
* needs to be larger than their size on the wire. Operations that would
* exceed it fail. Defaults to 128 KiB.
*
* Returns an error if #wasmtime_context_set_wasi_http has not been called on
* this store. Calling #wasmtime_context_set_wasi_http again resets the limit to
* the default.
*/
WASM_API_EXTERN wasmtime_error_t *
wasmtime_context_set_wasi_http_field_size_limit(wasmtime_context_t *context,
size_t limit);

/**
* \brief Sets the maximum combined size, in bytes, of a WASI HTTP request's
* method, scheme, authority, and path-with-query strings.
*
* Built-in methods (`GET`, `POST`, ...) and the `http`/`https` schemes don't
* count toward the limit. Guest setters that would exceed it return an error,
* and incoming requests that exceed it are rejected before reaching the guest.
* Defaults to 16 KiB.
*
* Returns an error if #wasmtime_context_set_wasi_http has not been called on
* this store. Calling #wasmtime_context_set_wasi_http again resets the limit to
* the default.
*/
WASM_API_EXTERN wasmtime_error_t *
wasmtime_context_set_wasi_http_request_strings_size_limit(
wasmtime_context_t *context, size_t limit);

#endif // WASMTIME_FEATURE_WASI_HTTP

/**
Expand Down
34 changes: 34 additions & 0 deletions crates/c-api/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,40 @@ pub extern "C" fn wasmtime_context_set_wasi_http(mut context: WasmtimeStoreConte
context.data_mut().wasi_http = Some(wasmtime_wasi_http::WasiHttpCtx::new());
}

#[cfg(feature = "wasi-http")]
fn with_wasi_http(
mut context: WasmtimeStoreContextMut<'_>,
f: impl FnOnce(&mut wasmtime_wasi_http::WasiHttpCtx),
) -> Option<Box<wasmtime_error_t>> {
match context.data_mut().wasi_http.as_mut() {
Some(http) => {
f(http);
None
}
None => Some(Box::new(wasmtime_error_t::from(wasmtime::format_err!(
"wasi-http context not set; call `wasmtime_context_set_wasi_http` first"
)))),
}
}

#[cfg(feature = "wasi-http")]
#[unsafe(no_mangle)]
pub extern "C" fn wasmtime_context_set_wasi_http_field_size_limit(
context: WasmtimeStoreContextMut<'_>,
limit: usize,
) -> Option<Box<wasmtime_error_t>> {
with_wasi_http(context, |http| http.set_field_size_limit(limit))
}

#[cfg(feature = "wasi-http")]
#[unsafe(no_mangle)]
pub extern "C" fn wasmtime_context_set_wasi_http_request_strings_size_limit(
context: WasmtimeStoreContextMut<'_>,
limit: usize,
) -> Option<Box<wasmtime_error_t>> {
with_wasi_http(context, |http| http.set_request_strings_size_limit(limit))
}

#[unsafe(no_mangle)]
#[cfg(feature = "gc")]
pub extern "C" fn wasmtime_context_gc(
Expand Down
23 changes: 23 additions & 0 deletions crates/c-api/tests/wasip2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,26 @@ TEST(wasip2, smoke) {
linker.add_wasip2().unwrap();
linker.instantiate(context, component).unwrap();
}

#ifdef WASMTIME_FEATURE_WASI_HTTP
TEST(wasip2, http_limits) {
wasmtime::Engine engine;
wasmtime::Store store(engine);
auto context = store.context();

// Setting limits before the WASI HTTP context exists is an error.
auto err = context.set_wasi_http_field_size_limit(1024);
EXPECT_FALSE(err);
EXPECT_NE(err.err().message().find("wasmtime_context_set_wasi_http"),
std::string::npos);
err = context.set_wasi_http_request_strings_size_limit(1024);
EXPECT_FALSE(err);
EXPECT_NE(err.err().message().find("wasmtime_context_set_wasi_http"),
std::string::npos);

context.set_wasi(wasmtime::WasiConfig()).unwrap();
context.set_wasi_http();
context.set_wasi_http_field_size_limit(1024).unwrap();
context.set_wasi_http_request_strings_size_limit(1024).unwrap();
}
#endif // WASMTIME_FEATURE_WASI_HTTP
6 changes: 6 additions & 0 deletions crates/cli-flags/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,12 @@ wasmtime_option_group! {
/// `fields` resource (aka `headers` and `trailers`). `fields` methods
/// which cause the contents to exceed this size limit will trap.
pub max_http_fields_size: Option<usize>,
/// Maximum combined size, in bytes, of a wasi-http request's method,
/// scheme, authority, and path-with-query strings. Built-in methods
/// and the `http`/`https` schemes don't count. Guest setters that
/// would exceed this return an error, and `wasmtime serve` answers
/// incoming requests that exceed it with `400 Bad Request`.
pub max_http_request_strings_size: Option<usize>,
}

enum Wasi {
Expand Down
34 changes: 33 additions & 1 deletion crates/test-programs/src/bin/p2_api_proxy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::{Context, Result};
use test_programs::wasi::http::types::{
Headers, IncomingRequest, Method, OutgoingBody, OutgoingResponse, ResponseOutparam,
Fields, Headers, IncomingRequest, Method, OutgoingBody, OutgoingRequest, OutgoingResponse,
ResponseOutparam, Scheme,
};

struct T;
Expand Down Expand Up @@ -40,6 +41,11 @@ impl test_programs::proxy::exports::wasi::http::incoming_handler::Guest for T {
response_for(r, outparam);
return;
}
(Method::Get, Some("/rs")) => {
let r = request_strings_handler(&request);
response_for(r, outparam);
return;
}

_ => {}
}
Expand Down Expand Up @@ -141,3 +147,29 @@ fn new_fields_handler(request: IncomingRequest) -> Result<()> {

Ok(())
}

/// `/rs` with header `sets: <field>=<len>,...`: on a single fresh outgoing
/// request, set each `field` in order to a valid string of exactly `len` bytes.
///
/// The path is kept short, and the sets are passed in a header, so that the
/// incoming request itself stays well within a small request strings limit.
fn request_strings_handler(request: &IncomingRequest) -> Result<()> {
let sets = request.headers().get("sets");
let sets = std::str::from_utf8(sets.first().context("expect a `sets` header")?)?;
let req = OutgoingRequest::new(Fields::new());
for set in sets.split(',') {
let (field, len) = set
.split_once('=')
.context("expect sets: <field>=<len>,...")?;
let len: usize = len.parse().context("expect len to parse as number")?;
let result = match field {
"method" => req.set_method(&Method::Other("X".repeat(len))),
"path" => req.set_path_with_query(Some(&format!("/{}", "a".repeat(len - 1)))),
"scheme" => req.set_scheme(Some(&Scheme::Other("x".repeat(len)))),
"authority" => req.set_authority(Some(&"a".repeat(len))),
other => anyhow::bail!("unknown field {other:?}"),
};
result.map_err(|()| anyhow::anyhow!("failed to set {field} of length {len}"))?;
}
Ok(())
}
25 changes: 25 additions & 0 deletions crates/test-programs/src/bin/p2_cli_http_request_strings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use wasip2::http::types::{Fields, Method, OutgoingRequest, Scheme};

/// Usage: `<field>=<len>...` where field is one of `method`, `path`, `scheme`,
/// or `authority`.
///
/// Sets each field, in order, on a single request to a valid string of exactly
/// `len` bytes, printing `ok` or `error received` for each.
fn main() {
let req = OutgoingRequest::new(Fields::new());
for arg in std::env::args().skip(1) {
let (field, len) = arg.split_once('=').expect("expected <field>=<len>");
let len: usize = len.parse().expect("len must be a number");
let result = match field {
"method" => req.set_method(&Method::Other("X".repeat(len))),
"path" => req.set_path_with_query(Some(&format!("/{}", "a".repeat(len - 1)))),
"scheme" => req.set_scheme(Some(&Scheme::Other("x".repeat(len)))),
"authority" => req.set_authority(Some(&"a".repeat(len))),
other => panic!("unknown field {other:?}"),
};
match result {
Ok(()) => println!("ok"),
Err(()) => println!("error received"),
}
}
}
36 changes: 36 additions & 0 deletions crates/test-programs/src/bin/p3_cli_http_request_strings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use test_programs::p3::wasi::http::types::{Fields, Method, Request, Scheme};
use test_programs::p3::wit_future;

struct Component;

test_programs::p3::export!(Component);

/// Usage: `<field>=<len>...` where field is one of `method`, `path`, `scheme`,
/// or `authority`.
///
/// Sets each field, in order, on a single request to a valid string of exactly
/// `len` bytes, printing `ok` or `error received` for each.
impl test_programs::p3::exports::wasi::cli::run::Guest for Component {
async fn run() -> Result<(), ()> {
let (_trailers_tx, trailers_rx) = wit_future::new(|| Ok(None));
let (req, _transmit) = Request::new(Fields::new(), None, trailers_rx, None);
for arg in std::env::args().skip(1) {
let (field, len) = arg.split_once('=').expect("expected <field>=<len>");
let len: usize = len.parse().expect("len must be a number");
let result = match field {
"method" => req.set_method(&Method::Other("X".repeat(len))),
"path" => req.set_path_with_query(Some(&format!("/{}", "a".repeat(len - 1)))),
"scheme" => req.set_scheme(Some(&Scheme::Other("x".repeat(len)))),
"authority" => req.set_authority(Some(&"a".repeat(len))),
other => panic!("unknown field {other:?}"),
};
match result {
Ok(()) => println!("ok"),
Err(()) => println!("error received"),
}
}
Ok(())
}
}

fn main() {}
16 changes: 16 additions & 0 deletions crates/wasi-http/src/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,18 +120,23 @@ pub struct WasiHttpCtxView<'a> {
/// completely full `HeaderMap` doesn't break the bank in terms of memory
/// consumption.
const DEFAULT_FIELD_SIZE_LIMIT: usize = 128 * 1024;
/// Default limit on the combined size of a request's method, scheme,
/// authority, and path-with-query strings, to limit host memory use.
const DEFAULT_REQUEST_STRINGS_SIZE_LIMIT: usize = 16 * 1024;

/// Capture the state necessary for use in the wasi-http API implementation.
#[derive(Debug, Clone)]
pub struct WasiHttpCtx {
pub(crate) field_size_limit: usize,
pub(crate) request_strings_size_limit: usize,
}

impl WasiHttpCtx {
/// Create a new context.
pub fn new() -> Self {
Self {
field_size_limit: DEFAULT_FIELD_SIZE_LIMIT,
request_strings_size_limit: DEFAULT_REQUEST_STRINGS_SIZE_LIMIT,
}
}

Expand All @@ -145,6 +150,17 @@ impl WasiHttpCtx {
pub fn set_field_size_limit(&mut self, limit: usize) {
self.field_size_limit = limit;
}

/// Set the maximum combined size, in bytes, of a request's method,
/// scheme, authority, and path-with-query strings.
///
/// Built-in methods (`GET`, `POST`, ...) and schemes (`http`, `https`)
/// don't count toward this limit. Guest setters which would exceed the
/// limit return an error, and incoming requests which exceed it are
/// rejected with a `400 Bad Request` before reaching the guest.
pub fn set_request_strings_size_limit(&mut self, limit: usize) {
self.request_strings_size_limit = limit;
}
}

impl Default for WasiHttpCtx {
Expand Down
19 changes: 16 additions & 3 deletions crates/wasi-http/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,9 +1000,22 @@ impl<'a, T: Send> Prepared<'a, T> {
Proxy::P3(guest) => {
let (request, body) = request.into_parts();
let request = http::Request::from_parts(request, body);
let hooks = view(store.data_mut()).hooks;
let (request, request_io_result) = p3::Request::from_http(hooks, request);
let request = view(store.data_mut()).table.push(request)?;
let cx = view(store.data_mut());
let (request, request_io_result) = match p3::Request::from_http(
cx.ctx, cx.hooks, request,
) {
Ok(pair) => pair,
Err(e) => {
// As in the p2 case below, the request never
// reaches the guest, so report the failure through
// `tx`.
_ = tx.send(Err(e));
wasmtime::bail!(
"request was rejected before it could be turned into a guest request"
);
}
};
let request = cx.table.push(request)?;

Ok(Prepared::P3 {
tx,
Expand Down
2 changes: 2 additions & 0 deletions crates/wasi-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub mod p2;
#[cfg(feature = "p3")]
pub mod p3;
mod request_options;
#[cfg(any(feature = "p2", feature = "p3"))]
mod request_strings;

pub use ctx::*;
#[cfg(feature = "default-send-request")]
Expand Down
Loading
Loading