Skip to content

Commit dfdd653

Browse files
authored
Introduce an OOM-handling Error type for Wasmtime (#12163)
* Introduce an OOM-handling `Error` type for Wasmtime This new `Error` has an API that is 99% identical to `anyhow::Error`'s API, but additionally handles memory exhaustion. This commit only introduces the `wasmtime_internal_error` crate into our workspace, along with its regular tests and OOM tests. This commit does not, however, migrate Wasmtime's internals or public-facing API over to the new error type yet. That is left for follow up work. In order to continue fitting `Result<(), Error>` in one word, there is quite a bit of unsafe code in `Error`'s implementation, mostly surrounding the manual creation of our own moral equivalent of `Box<dyn Error>` with explicit vtables and type erasure so that we get a thin pointer to a trait object rather than `Box<dyn Error>`'s fat pointer. To alleviate the associated risks, I've been testing this code under MIRI throughout its whole development, as well as thoroughly testing the API so that MIRI can dynamically exercise all the code paths. Furthermore, I've enabled testing this crate under MIRI in CI. * Fill out more cargo.toml info * Add cargo vet entries for new crate * Use single quotes for string in Cargo.toml to work around publish script bug See #12164 * Add the `wasmtime_error::Ok` function For compatibility with `anyhow`'s API. This unfortunately means we have to move the actual error implementation out into a submodule so that this function isn't visible, or else every `match .. { Ok(_) => ... }` stops type checking. * fix some stuff that was broken in the move to a module * maybe fix cargo vet? * Add comments about backtraces and OOM * Debug assert `ConcreteError<E>` and `DynError` layouts are compatible for all `ConcreteError<E>`s that we actually allocate * Add reference to layout test and assertions to docs about compatible layout requirements * Use `#[track_caller]` to hide internal frame from error's backtrace * Fix typo * Pull various internal `ErrorExt` implementations out to top level * Switch to `0x1` as the representation of OOM in `OomOrDynError` packing * add audit-as-crates-io for wasmtime-internal-error
1 parent bb0ebb7 commit dfdd653

File tree

19 files changed

+3977
-8
lines changed

19 files changed

+3977
-8
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,8 @@ jobs:
11441144
- crate: "wasmtime-cli"
11451145
- crate: "wasmtime-environ --all-features"
11461146
- crate: "pulley-interpreter --all-features"
1147+
- crate: "wasmtime-internal-error"
1148+
- crate: "wasmtime-internal-error --all-features"
11471149
- script: ./ci/miri-provenance-test.sh
11481150
- script: ./ci/miri-wast.sh ./tests/spec_testsuite/table.wast
11491151
needs: determine

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ members = [
158158
"crates/bench-api",
159159
"crates/c-api/artifact",
160160
"crates/environ/fuzz",
161+
"crates/error",
161162
"crates/misc/component-async-tests",
162163
"crates/test-programs",
163164
"crates/wasi-preview1-component-adapter",
@@ -258,6 +259,7 @@ wasmtime-wast = { path = "crates/wast", version = "=41.0.0" }
258259
# that these are internal unsupported crates for external use. These exist as
259260
# part of the project organization of other public crates in Wasmtime and are
260261
# otherwise not supported in terms of CVEs for example.
262+
wasmtime-error = { path = "crates/error", version = "=41.0.0", package = 'wasmtime-internal-error' }
261263
wasmtime-wmemcheck = { path = "crates/wmemcheck", version = "=41.0.0", package = 'wasmtime-internal-wmemcheck' }
262264
wasmtime-c-api-macros = { path = "crates/c-api-macros", version = "=41.0.0", package = 'wasmtime-internal-c-api-macros' }
263265
wasmtime-cache = { path = "crates/cache", version = "=41.0.0", package = 'wasmtime-internal-cache' }

crates/error/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
authors.workspace = true
3+
description = "INTERNAL: Wasmtime's universal error type's implementation"
4+
edition.workspace = true
5+
license = "Apache-2.0 WITH LLVM-exception"
6+
name = "wasmtime-internal-error"
7+
rust-version.workspace = true
8+
version.workspace = true
9+
10+
[dependencies]
11+
anyhow = { workspace = true, optional = true }
12+
13+
[lints]
14+
workspace = true
15+
16+
[features]
17+
# Enable the use of the `std` crate.
18+
std = []
19+
# Enable backtraces.
20+
backtrace = ["std"]
21+
# Enable the `From<Error> for anyhow::Error` implementation and
22+
# `Error::from_anyhow` constructor.
23+
anyhow = ["dep:anyhow"]

crates/error/src/backtrace.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use std::backtrace::Backtrace;
2+
use std::sync::atomic::{AtomicBool, Ordering};
3+
4+
static ENABLED: AtomicBool = AtomicBool::new(true);
5+
6+
fn enabled() -> bool {
7+
ENABLED.load(Ordering::Acquire)
8+
}
9+
10+
/// Forcibly disable capturing backtraces dynamically.
11+
///
12+
/// XXX: This is only exposed for internal testing, to work around cargo
13+
/// workspaces and feature resolution. This method may disappear or change
14+
/// at any time. Instead of using this method, you should disable the
15+
/// `backtrace` cargo feature.
16+
#[doc(hidden)]
17+
pub fn disable_backtrace() {
18+
ENABLED.store(false, Ordering::Release)
19+
}
20+
21+
#[track_caller]
22+
pub fn capture() -> Backtrace {
23+
if enabled() {
24+
Backtrace::capture()
25+
} else {
26+
Backtrace::disabled()
27+
}
28+
}

crates/error/src/boxed.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use super::{OutOfMemory, Result};
2+
use alloc::boxed::Box;
3+
use core::alloc::Layout;
4+
use core::ptr::NonNull;
5+
6+
/// Try to allocate a block of memory that fits the given layout, or return an
7+
/// `OutOfMemory` error.
8+
///
9+
/// # Safety
10+
///
11+
/// Same as `alloc::alloc::alloc`: layout must have non-zero size.
12+
#[inline]
13+
pub(crate) unsafe fn try_alloc(layout: Layout) -> Result<NonNull<u8>, OutOfMemory> {
14+
// Safety: same as our safety conditions.
15+
debug_assert!(layout.size() > 0);
16+
let ptr = unsafe { alloc::alloc::alloc(layout) };
17+
18+
if let Some(ptr) = NonNull::new(ptr) {
19+
Ok(ptr)
20+
} else {
21+
out_of_line_slow_path!(Err(OutOfMemory::new()))
22+
}
23+
}
24+
25+
/// Create a `Box<T>`, or return an `OutOfMemory` error.
26+
#[inline]
27+
pub(crate) fn try_box<T>(value: T) -> Result<Box<T>, OutOfMemory> {
28+
let layout = alloc::alloc::Layout::new::<T>();
29+
30+
if layout.size() == 0 {
31+
// Safety: `Box` explicitly allows construction from dangling pointers
32+
// (which are guaranteed non-null and aligned) for zero-sized types.
33+
return Ok(unsafe { Box::from_raw(core::ptr::dangling::<T>().cast_mut()) });
34+
}
35+
36+
// Safety: layout size is non-zero.
37+
let ptr = unsafe { try_alloc(layout)? };
38+
39+
let ptr = ptr.cast::<T>();
40+
41+
// Safety: The allocation succeeded, and it has `T`'s layout, so the pointer
42+
// is valid for writing a `T`.
43+
unsafe {
44+
ptr.write(value);
45+
}
46+
47+
// Safety: The pointer's memory block was allocated by the global allocator,
48+
// is valid for `T`, and is initialized.
49+
Ok(unsafe { Box::from_raw(ptr.as_ptr()) })
50+
}

0 commit comments

Comments
 (0)