From c3343a3f24d618ba01744ddad07970a4e51240c2 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Tue, 30 Sep 2025 17:03:12 -0700
Subject: [PATCH 1/2] first pass at resource-sanitizer
---
crates/wasmtime/Cargo.toml | 6 +
.../src/runtime/component/resource_table.rs | 119 ++++++++++++++++--
.../component/resource_table/sanitizer.rs | 56 +++++++++
3 files changed, 170 insertions(+), 11 deletions(-)
create mode 100644 crates/wasmtime/src/runtime/component/resource_table/sanitizer.rs
diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml
index 81ccc0e5083a..f8958382d365 100644
--- a/crates/wasmtime/Cargo.toml
+++ b/crates/wasmtime/Cargo.toml
@@ -408,3 +408,9 @@ component-model-async-bytes = [
"component-model-async",
"dep:bytes",
]
+
+# Enables support for `component::ResourceSanitizer`
+resource-sanitizer = [
+ "component-model",
+ "std",
+]
diff --git a/crates/wasmtime/src/runtime/component/resource_table.rs b/crates/wasmtime/src/runtime/component/resource_table.rs
index e427518bb2e7..7bac9ee6b7a2 100644
--- a/crates/wasmtime/src/runtime/component/resource_table.rs
+++ b/crates/wasmtime/src/runtime/component/resource_table.rs
@@ -5,6 +5,13 @@ use core::any::Any;
use core::fmt;
use core::mem;
+#[cfg(feature = "resource-sanitizer")]
+mod sanitizer;
+#[cfg(feature = "resource-sanitizer")]
+use sanitizer::{ResourceSanitizer, SanInfo};
+#[cfg(feature = "resource-sanitizer")]
+use std::backtrace::Backtrace;
+
#[derive(Debug)]
/// Errors returned by operations on `ResourceTable`
pub enum ResourceTableError {
@@ -36,6 +43,40 @@ impl core::error::Error for ResourceTableError {}
pub struct ResourceTable {
entries: Vec,
free_head: Option,
+ #[cfg(feature = "resource-sanitizer")]
+ sanitizer: Option,
+}
+
+/// Builder for `ResourceTable`
+pub struct ResourceTableBuilder {
+ entries: Option>,
+ #[cfg(feature = "resource-sanitizer")]
+ sanitizer: Option,
+}
+
+impl ResourceTableBuilder {
+ /// Allocate at least the specified capacity for table entries
+ pub fn capacity(mut self, capacity: usize) -> Self {
+ self.entries = Some(Vec::with_capacity(capacity));
+ self
+ }
+
+ #[cfg(feature = "resource-sanitizer")]
+ /// Use a `ResourceSanitizer` with the ResourceTable
+ pub fn sanitizer(mut self, sanitizer: ResourceSanitizer) -> Self {
+ self.sanitizer = Some(sanitizer);
+ self
+ }
+
+ /// Construct a `ResourceTable`
+ pub fn build(self) -> ResourceTable {
+ ResourceTable {
+ entries: self.entries.unwrap_or_default(),
+ free_head: None,
+ #[cfg(feature = "resource-sanitizer")]
+ sanitizer: self.sanitizer,
+ }
+ }
}
#[derive(Debug)]
@@ -101,9 +142,15 @@ impl TableEntry {
impl ResourceTable {
/// Create an empty table
pub fn new() -> Self {
- ResourceTable {
- entries: Vec::new(),
- free_head: None,
+ Self::builder().build()
+ }
+
+ /// Create a builder
+ pub fn builder() -> ResourceTableBuilder {
+ ResourceTableBuilder {
+ entries: None,
+ #[cfg(feature = "resource-sanitizer")]
+ sanitizer: None,
}
}
@@ -118,14 +165,6 @@ impl ResourceTable {
})
}
- /// Create an empty table with at least the specified capacity.
- pub fn with_capacity(capacity: usize) -> Self {
- ResourceTable {
- entries: Vec::with_capacity(capacity),
- free_head: None,
- }
- }
-
/// Inserts a new value `T` into this table, returning a corresponding
/// `Resource` which can be used to refer to it after it was inserted.
pub fn push(&mut self, entry: T) -> Result, ResourceTableError>
@@ -133,6 +172,13 @@ impl ResourceTable {
T: Send + 'static,
{
let idx = self.push_(TableEntry::new(Box::new(entry), None))?;
+ #[cfg(feature = "resource-sanitizer")]
+ if let Some(san) = &self.sanitizer {
+ san.log_construction(
+ idx,
+ SanInfo::new(std::any::type_name::(), Backtrace::force_capture()),
+ )
+ }
Ok(Resource::new_own(idx))
}
@@ -245,6 +291,14 @@ impl ResourceTable {
self.occupied(parent)?;
let child = self.push_(TableEntry::new(Box::new(entry), Some(parent)))?;
self.occupied_mut(parent)?.add_child(child);
+
+ #[cfg(feature = "resource-sanitizer")]
+ if let Some(san) = &self.sanitizer {
+ san.log_construction(
+ child,
+ SanInfo::new(std::any::type_name::(), Backtrace::force_capture()),
+ )
+ }
Ok(Resource::new_own(child))
}
@@ -279,6 +333,10 @@ impl ResourceTable {
///
/// Multiple shared references can be borrowed at any given time.
pub fn get(&self, key: &Resource) -> Result<&T, ResourceTableError> {
+ #[cfg(feature = "resource-sanitizer")]
+ if let Some(san) = &self.sanitizer {
+ san.log_usage(key.rep(), Backtrace::force_capture())
+ }
self.get_(key.rep())?
.downcast_ref()
.ok_or(ResourceTableError::WrongType)
@@ -295,6 +353,10 @@ impl ResourceTable {
&mut self,
key: &Resource,
) -> Result<&mut T, ResourceTableError> {
+ #[cfg(feature = "resource-sanitizer")]
+ if let Some(san) = &self.sanitizer {
+ san.log_usage(key.rep(), Backtrace::force_capture())
+ }
self.get_any_mut(key.rep())?
.downcast_mut()
.ok_or(ResourceTableError::WrongType)
@@ -302,6 +364,10 @@ impl ResourceTable {
/// Returns the raw `Any` at the `key` index provided.
pub fn get_any_mut(&mut self, key: u32) -> Result<&mut dyn Any, ResourceTableError> {
+ #[cfg(feature = "resource-sanitizer")]
+ if let Some(san) = &self.sanitizer {
+ san.log_usage(key, Backtrace::force_capture())
+ }
let r = self.occupied_mut(key)?;
Ok(&mut *r.entry)
}
@@ -311,6 +377,10 @@ impl ResourceTable {
where
T: Any,
{
+ #[cfg(feature = "resource-sanitizer")]
+ if let Some(san) = &self.sanitizer {
+ san.log_delete(resource.rep(), Backtrace::force_capture())
+ }
self.delete_maybe_debug(resource, cfg!(debug_assertions))
}
@@ -353,6 +423,12 @@ impl ResourceTable {
&'a mut self,
map: BTreeMap,
) -> impl Iterator- , T)> {
+ #[cfg(feature = "resource-sanitizer")]
+ if let Some(san) = &self.sanitizer {
+ for (k, _) in map.iter() {
+ san.log_usage(*k, Backtrace::force_capture())
+ }
+ }
map.into_iter().map(move |(k, v)| {
let item = self
.occupied_mut(k)
@@ -373,6 +449,11 @@ impl ResourceTable {
{
let parent_entry = self.occupied(parent.rep())?;
Ok(parent_entry.children.iter().map(|child_index| {
+ #[cfg(feature = "resource-sanitizer")]
+ if let Some(san) = &self.sanitizer {
+ san.log_usage(*child_index, Backtrace::force_capture())
+ }
+
let child = self.occupied(*child_index).expect("missing child");
child.entry.as_ref()
}))
@@ -380,11 +461,27 @@ impl ResourceTable {
/// Iterate over all the entries in this table.
pub fn iter_mut(&mut self) -> impl Iterator
- {
+ #[cfg(feature = "resource-sanitizer")]
+ if let Some(san) = &self.sanitizer {
+ for (ix, entry) in self.entries.iter().enumerate() {
+ if matches!(entry, Entry::Occupied { .. }) {
+ san.log_usage(ix.try_into().unwrap(), Backtrace::force_capture())
+ }
+ }
+ }
+
self.entries.iter_mut().filter_map(|entry| match entry {
Entry::Occupied { entry } => Some(&mut *entry.entry),
Entry::Free { .. } => None,
})
}
+
+ /// Get the table's `ResourceSanitizer` if one exists. Some iff
+ /// `ResourceTableBuilder::sanitizer` was used at creation.
+ #[cfg(feature = "resource-sanitizer")]
+ pub fn get_sanitizer(&self) -> Option<&ResourceSanitizer> {
+ self.sanitizer.as_ref()
+ }
}
impl Default for ResourceTable {
diff --git a/crates/wasmtime/src/runtime/component/resource_table/sanitizer.rs b/crates/wasmtime/src/runtime/component/resource_table/sanitizer.rs
new file mode 100644
index 000000000000..9020b979d200
--- /dev/null
+++ b/crates/wasmtime/src/runtime/component/resource_table/sanitizer.rs
@@ -0,0 +1,56 @@
+use alloc::vec::Vec;
+use std::backtrace::Backtrace;
+use std::cell::RefCell;
+
+#[derive(Debug)]
+pub struct ResourceSanitizer {
+ uses: RefCell>,
+}
+
+impl ResourceSanitizer {
+ pub fn new() -> Self {
+ Self {
+ uses: RefCell::new(Vec::new()),
+ }
+ }
+ pub(super) fn log_construction(&self, index: u32, info: SanInfo) {
+ self.uses.borrow_mut().push((index, info));
+ }
+ pub(super) fn log_usage(&self, index: u32, backtrace: Backtrace) {
+ let mut uses = self.uses.borrow_mut();
+ let (_, info) = uses
+ .iter_mut()
+ .rev()
+ .find(|(ix, _)| index == *ix)
+ .expect("used resource present in sanitizer log");
+ info.last_used = Some(backtrace);
+ }
+ pub(super) fn log_delete(&self, index: u32, backtrace: Backtrace) {
+ let mut uses = self.uses.borrow_mut();
+ let (_, info) = uses
+ .iter_mut()
+ .rev()
+ .find(|(ix, _)| index == *ix)
+ .expect("deleted resource present in sanitizer log");
+ info.deleted = Some(backtrace);
+ }
+}
+
+#[derive(Debug)]
+pub struct SanInfo {
+ type_name: &'static str,
+ allocated: std::backtrace::Backtrace,
+ last_used: Option,
+ deleted: Option,
+}
+
+impl SanInfo {
+ pub fn new(type_name: &'static str, allocated: Backtrace) -> Self {
+ SanInfo {
+ type_name,
+ allocated,
+ last_used: None,
+ deleted: None,
+ }
+ }
+}
From 394d7ba937b2a7d7f81bb4a2f777d94b0f2b9619 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Wed, 1 Oct 2025 16:00:52 -0700
Subject: [PATCH 2/2] wasmtime run always uses resource sanitizer and prints
leaks at end of execution
---
Cargo.toml | 2 +-
crates/wasi/src/p1.rs | 4 +++-
crates/wasmtime/src/runtime/component/mod.rs | 3 +++
.../src/runtime/component/resource_table.rs | 2 +-
.../component/resource_table/sanitizer.rs | 23 ++++++++++++++++++-
src/commands/run.rs | 3 +++
src/commands/serve.rs | 4 +++-
7 files changed, 36 insertions(+), 5 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 8b0a18df76ec..447c23f7f1d2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -43,7 +43,7 @@ path = "src/bin/wasmtime.rs"
doc = false
[dependencies]
-wasmtime = { workspace = true, features = ['std'] }
+wasmtime = { workspace = true, features = ['std', 'resource-sanitizer'] }
wasmtime-cache = { workspace = true, optional = true }
wasmtime-cli-flags = { workspace = true }
wasmtime-cranelift = { workspace = true, optional = true }
diff --git a/crates/wasi/src/p1.rs b/crates/wasi/src/p1.rs
index c63331f5ded5..a3b674f46cb6 100644
--- a/crates/wasi/src/p1.rs
+++ b/crates/wasi/src/p1.rs
@@ -150,7 +150,9 @@ pub struct WasiP1Ctx {
impl WasiP1Ctx {
pub(crate) fn new(wasi: WasiCtx) -> Self {
Self {
- table: ResourceTable::new(),
+ table: ResourceTable::builder()
+ .sanitizer(wasmtime::component::ResourceSanitizer::new())
+ .build(),
wasi,
adapter: WasiP1Adapter::new(),
}
diff --git a/crates/wasmtime/src/runtime/component/mod.rs b/crates/wasmtime/src/runtime/component/mod.rs
index 3497eaf5bb8b..98563c50ef31 100644
--- a/crates/wasmtime/src/runtime/component/mod.rs
+++ b/crates/wasmtime/src/runtime/component/mod.rs
@@ -137,6 +137,9 @@ pub use self::resources::{Resource, ResourceAny};
pub use self::types::{ResourceType, Type};
pub use self::values::Val;
+#[cfg(feature = "resource-sanitizer")]
+pub use self::resource_table::{ResourceSanitizer, SanInfo};
+
pub(crate) use self::instance::RuntimeImport;
pub(crate) use self::resources::HostResourceData;
pub(crate) use self::store::ComponentInstanceId;
diff --git a/crates/wasmtime/src/runtime/component/resource_table.rs b/crates/wasmtime/src/runtime/component/resource_table.rs
index 7bac9ee6b7a2..48a57d517d88 100644
--- a/crates/wasmtime/src/runtime/component/resource_table.rs
+++ b/crates/wasmtime/src/runtime/component/resource_table.rs
@@ -8,7 +8,7 @@ use core::mem;
#[cfg(feature = "resource-sanitizer")]
mod sanitizer;
#[cfg(feature = "resource-sanitizer")]
-use sanitizer::{ResourceSanitizer, SanInfo};
+pub use sanitizer::{ResourceSanitizer, SanInfo};
#[cfg(feature = "resource-sanitizer")]
use std::backtrace::Backtrace;
diff --git a/crates/wasmtime/src/runtime/component/resource_table/sanitizer.rs b/crates/wasmtime/src/runtime/component/resource_table/sanitizer.rs
index 9020b979d200..9dd8347d5af3 100644
--- a/crates/wasmtime/src/runtime/component/resource_table/sanitizer.rs
+++ b/crates/wasmtime/src/runtime/component/resource_table/sanitizer.rs
@@ -1,13 +1,17 @@
-use alloc::vec::Vec;
use std::backtrace::Backtrace;
use std::cell::RefCell;
+use std::io::Write;
+use std::vec::Vec;
+use std::write;
+/// Tracks allocation, usage, and deletion of all resources
#[derive(Debug)]
pub struct ResourceSanitizer {
uses: RefCell>,
}
impl ResourceSanitizer {
+ /// XXX
pub fn new() -> Self {
Self {
uses: RefCell::new(Vec::new()),
@@ -34,8 +38,24 @@ impl ResourceSanitizer {
.expect("deleted resource present in sanitizer log");
info.deleted = Some(backtrace);
}
+
+ /// XXX
+ pub fn report_live_set(&self, w: &mut impl Write) -> Result<(), std::io::Error> {
+ let uses = self.uses.borrow();
+ for (ix, info) in uses.iter() {
+ if info.deleted.is_none() {
+ write!(
+ w,
+ "LEAK resource {ix}: {}\nLEAK allocated at {:#?}\nLEAK last used at {:#?}\n",
+ info.type_name, info.allocated, info.last_used
+ )?;
+ }
+ }
+ Ok(())
+ }
}
+/// sanitizer information for a given resource
#[derive(Debug)]
pub struct SanInfo {
type_name: &'static str,
@@ -45,6 +65,7 @@ pub struct SanInfo {
}
impl SanInfo {
+ /// XXX
pub fn new(type_name: &'static str, allocated: Backtrace) -> Self {
SanInfo {
type_name,
diff --git a/src/commands/run.rs b/src/commands/run.rs
index 2e658b725f48..54e02454731d 100644
--- a/src/commands/run.rs
+++ b/src/commands/run.rs
@@ -231,6 +231,9 @@ impl RunCommand {
.await
});
+ if let Some(san) = WasiView::ctx(store.data_mut()).table.get_sanitizer() {
+ san.report_live_set(&mut std::io::stderr())?;
+ }
// Load the main wasm module.
match result.unwrap_or_else(|elapsed| {
Err(anyhow::Error::from(wasmtime::Trap::Interrupt))
diff --git a/src/commands/serve.rs b/src/commands/serve.rs
index 907a83eb6034..da5e294e599e 100644
--- a/src/commands/serve.rs
+++ b/src/commands/serve.rs
@@ -190,7 +190,9 @@ impl ServeCommand {
builder.stderr(LogStream::new(stderr_prefix, Output::Stderr));
let mut host = Host {
- table: wasmtime::component::ResourceTable::new(),
+ table: wasmtime::component::ResourceTable::builder()
+ .sanitizer(wasmtime::component::ResourceSanitizer::new())
+ .build(),
ctx: builder.build(),
http: WasiHttpCtx::new(),
http_outgoing_body_buffer_chunks: self.run.common.wasi.http_outgoing_body_buffer_chunks,