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,