Skip to content

Commit 7bd61b8

Browse files
authored
Add OOM test for component Func::call_async (#13017)
* Add OOM test for component Func::call_async Add NameMap::get_by_str to avoid infallible String allocation in component export name lookups. The NameMapNoIntern implementation allocated a String on every lookup; the new method uses borrowed &str keys directly on the underlying IndexMap instead. * Address review feedback * Remove some unnnecessary Result<_, OutOfMemory> * fix clippy warning * address review feedback
1 parent aeaedf3 commit 7bd61b8

8 files changed

Lines changed: 203 additions & 41 deletions

File tree

crates/core/src/alloc/string.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,31 @@ impl Borrow<str> for TryString {
6060
}
6161
}
6262

63+
impl TryFrom<&str> for TryString {
64+
type Error = OutOfMemory;
65+
66+
#[inline]
67+
fn try_from(value: &str) -> Result<Self, Self::Error> {
68+
let mut s = TryString::new();
69+
s.push_str(value)?;
70+
Ok(s)
71+
}
72+
}
73+
6374
impl From<inner::String> for TryString {
6475
#[inline]
6576
fn from(inner: inner::String) -> Self {
6677
Self { inner }
6778
}
6879
}
6980

81+
impl From<TryString> for inner::String {
82+
#[inline]
83+
fn from(s: TryString) -> Self {
84+
s.inner
85+
}
86+
}
87+
7088
#[cfg(feature = "serde")]
7189
impl serde::ser::Serialize for TryString {
7290
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>

crates/environ/src/component/info.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ pub struct Component {
107107
pub imports: PrimaryMap<RuntimeImportIndex, (ImportIndex, Vec<String>)>,
108108

109109
/// This component's own root exports from the component itself.
110-
pub exports: NameMap<String, ExportIndex>,
110+
pub exports: NameMap<TryString, ExportIndex>,
111111

112112
/// All exports of this component and exported instances of this component.
113113
///
@@ -459,7 +459,7 @@ pub enum ExportItem<T> {
459459
}
460460

461461
/// Possible exports from a component.
462-
#[derive(Debug, Clone, Serialize, Deserialize)]
462+
#[derive(Debug, Serialize, Deserialize)]
463463
pub enum Export {
464464
/// A lifted function being exported which is an adaptation of a core wasm
465465
/// function.
@@ -491,7 +491,7 @@ pub enum Export {
491491
/// Instance type index, if such is assigned
492492
ty: TypeComponentInstanceIndex,
493493
/// Instance export map
494-
exports: NameMap<String, ExportIndex>,
494+
exports: NameMap<TryString, ExportIndex>,
495495
},
496496
/// An exported type from a component or instance, currently only
497497
/// informational.

crates/environ/src/component/names.rs

Lines changed: 109 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
use crate::collections::TryCow;
12
use crate::error::{Result, bail};
23
use crate::prelude::*;
4+
use alloc::sync::Arc;
5+
use core::borrow::Borrow;
36
use core::hash::Hash;
47
use semver::Version;
58
use serde_derive::{Deserialize, Serialize};
@@ -12,16 +15,19 @@ use serde_derive::{Deserialize, Serialize};
1215
/// which is currently considered a key feature of WASI's compatibility story.
1316
///
1417
/// On the outside this looks like a map of `K` to `V`.
15-
#[derive(Clone, Serialize, Deserialize, Debug)]
16-
pub struct NameMap<K: Clone + Hash + Eq + Ord, V> {
18+
#[derive(Serialize, Deserialize, Debug)]
19+
pub struct NameMap<K, V>
20+
where
21+
K: TryClone + Hash + Eq + Ord,
22+
{
1723
/// A map of keys to the value that they define.
1824
///
1925
/// Note that this map is "exact" where the name here is the exact name that
2026
/// was specified when the `insert` was called. This doesn't have any
2127
/// semver-mangling or anything like that.
2228
///
2329
/// This map is always consulted first during lookups.
24-
definitions: IndexMap<K, V>,
30+
definitions: TryIndexMap<K, V>,
2531

2632
/// An auxiliary map tracking semver-compatible names. This is a map from
2733
/// "semver compatible alternate name" to a name present in `definitions`
@@ -43,12 +49,81 @@ pub struct NameMap<K: Clone + Hash + Eq + Ord, V> {
4349
///
4450
/// The `Version` here is tracked to ensure that when multiple versions on
4551
/// one track are defined that only the maximal version here is retained.
46-
alternate_lookups: IndexMap<K, (K, Version)>,
52+
alternate_lookups: TryIndexMap<K, (K, TryVersion)>,
53+
}
54+
55+
/// A wrapper around `Version` that implements `TryClone`.
56+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
57+
struct TryVersion(Arc<Version>);
58+
59+
impl TryFrom<Version> for TryVersion {
60+
type Error = OutOfMemory;
61+
62+
fn try_from(value: Version) -> Result<Self, Self::Error> {
63+
Ok(Self(try_new::<Arc<_>>(value)?))
64+
}
65+
}
66+
67+
impl TryClone for TryVersion {
68+
#[inline]
69+
fn try_clone(&self) -> Result<Self, OutOfMemory> {
70+
Ok(Self(self.0.clone()))
71+
}
72+
}
73+
74+
impl core::ops::Deref for TryVersion {
75+
type Target = Version;
76+
77+
#[inline]
78+
fn deref(&self) -> &Self::Target {
79+
&self.0
80+
}
81+
}
82+
83+
impl Borrow<Version> for TryVersion {
84+
#[inline]
85+
fn borrow(&self) -> &Version {
86+
&self.0
87+
}
88+
}
89+
90+
impl serde::Serialize for TryVersion {
91+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
92+
where
93+
S: serde::Serializer,
94+
{
95+
self.0.serialize(serializer)
96+
}
97+
}
98+
99+
impl<'de> serde::Deserialize<'de> for TryVersion {
100+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
101+
where
102+
D: serde::Deserializer<'de>,
103+
{
104+
use serde::de::Error;
105+
let v = Version::deserialize(deserializer)?;
106+
let v = try_new::<Arc<_>>(v).map_err(|oom| D::Error::custom(oom))?;
107+
Ok(Self(v))
108+
}
109+
}
110+
111+
impl<K, V> TryClone for NameMap<K, V>
112+
where
113+
K: TryClone + Hash + Eq + Ord,
114+
V: TryClone,
115+
{
116+
fn try_clone(&self) -> Result<Self, OutOfMemory> {
117+
Ok(Self {
118+
definitions: self.definitions.try_clone()?,
119+
alternate_lookups: self.alternate_lookups.try_clone()?,
120+
})
121+
}
47122
}
48123

49124
impl<K, V> NameMap<K, V>
50125
where
51-
K: Clone + Hash + Eq + Ord,
126+
K: TryClone + Hash + Eq + Ord,
52127
{
53128
/// Inserts the `name` specified into this map.
54129
///
@@ -64,32 +139,32 @@ where
64139
pub fn insert<I>(&mut self, name: &str, cx: &mut I, allow_shadowing: bool, item: V) -> Result<K>
65140
where
66141
I: NameMapIntern<Key = K>,
142+
I::BorrowedKey: Hash + Eq,
67143
{
68144
// Always insert `name` and `item` as an exact definition.
69-
let key = cx.intern(name);
70-
if let Some(prev) = self.definitions.insert(key.clone(), item) {
71-
if !allow_shadowing {
72-
self.definitions.insert(key, prev);
73-
bail!("map entry `{name}` defined twice")
74-
}
145+
let key = cx.intern(name)?;
146+
if !allow_shadowing && self.definitions.contains_key(&key) {
147+
bail!("map entry `{name}` defined twice")
75148
}
149+
self.definitions.insert(key.try_to_owned()?, item)?;
76150

77151
// If `name` is a semver-looking thing, like `a:b/c@1.0.0`, then also
78152
// insert an entry in the semver-compatible map under a key such as
79153
// `a:b/c@1`.
80154
//
81155
// This key is used during `get` later on.
82156
if let Some((alternate_key, version)) = alternate_lookup_key(name) {
83-
let alternate_key = cx.intern(alternate_key);
84-
if let Some((prev_key, prev_version)) = self
85-
.alternate_lookups
86-
.insert(alternate_key.clone(), (key.clone(), version.clone()))
87-
{
157+
let alternate_key = cx.intern(alternate_key)?;
158+
let version = TryVersion::try_from(version)?;
159+
if let Some((prev_key, prev_version)) = self.alternate_lookups.insert(
160+
alternate_key.try_clone()?,
161+
(key.try_clone()?, version.clone()),
162+
)? {
88163
// Prefer the latest version, so only do this if we're
89164
// greater than the prior version.
90165
if version < prev_version {
91166
self.alternate_lookups
92-
.insert(alternate_key, (prev_key, prev_version));
167+
.insert(alternate_key, (prev_key, prev_version))?;
93168
}
94169
}
95170
}
@@ -105,11 +180,13 @@ where
105180
pub fn get<I>(&self, name: &str, cx: &I) -> Option<&V>
106181
where
107182
I: NameMapIntern<Key = K>,
183+
I::Key: Borrow<I::BorrowedKey>,
184+
I::BorrowedKey: Hash + Eq,
108185
{
109186
// First look up an exact match and if that's found return that. This
110187
// enables defining multiple versions in the map and the requested
111188
// version is returned if it matches exactly.
112-
let candidate = cx.lookup(name).and_then(|k| self.definitions.get(&k));
189+
let candidate = cx.lookup(name).and_then(|k| self.definitions.get(&*k));
113190
if let Some(def) = candidate {
114191
return Some(def);
115192
}
@@ -122,7 +199,7 @@ where
122199
let (alternate_name, _version) = alternate_lookup_key(name)?;
123200
let alternate_key = cx.lookup(alternate_name)?;
124201
let (exact_key, _version) = self.alternate_lookups.get(&alternate_key)?;
125-
self.definitions.get(exact_key)
202+
self.definitions.get(exact_key.borrow())
126203
}
127204

128205
/// Returns an iterator over inserted values in this map.
@@ -142,7 +219,7 @@ where
142219

143220
impl<K, V> Default for NameMap<K, V>
144221
where
145-
K: Clone + Hash + Eq + Ord,
222+
K: TryClone + Hash + Eq + Ord,
146223
{
147224
fn default() -> NameMap<K, V> {
148225
NameMap {
@@ -156,29 +233,33 @@ where
156233
/// keys to non-strings.
157234
pub trait NameMapIntern {
158235
/// The key that this interning context generates.
159-
type Key;
236+
type Key: Borrow<Self::BorrowedKey>;
237+
238+
/// The borrowed version of the key type.
239+
type BorrowedKey: ?Sized + TryToOwned<Owned = Self::Key>;
160240

161241
/// Inserts `s` into `self` and returns the intern'd key `Self::Key`.
162-
fn intern(&mut self, s: &str) -> Self::Key;
242+
fn intern(&mut self, s: &str) -> Result<Self::Key, OutOfMemory>;
163243

164244
/// Looks up `s` in `self` returning `Some` if it was found or `None` if
165245
/// it's not present.
166-
fn lookup(&self, s: &str) -> Option<Self::Key>;
246+
fn lookup<'a>(&'a self, s: &'a str) -> Option<TryCow<'a, Self::BorrowedKey>>;
167247
}
168248

169249
/// For use with [`NameMap`] when no interning should happen and instead string
170250
/// keys are copied as-is.
171251
pub struct NameMapNoIntern;
172252

173253
impl NameMapIntern for NameMapNoIntern {
174-
type Key = String;
254+
type Key = TryString;
255+
type BorrowedKey = str;
175256

176-
fn intern(&mut self, s: &str) -> String {
177-
s.to_string()
257+
fn intern(&mut self, s: &str) -> Result<Self::Key, OutOfMemory> {
258+
TryString::try_from(s)
178259
}
179260

180-
fn lookup(&self, s: &str) -> Option<String> {
181-
Some(s.to_string())
261+
fn lookup<'a>(&'a self, s: &'a str) -> Option<TryCow<'a, Self::BorrowedKey>> {
262+
Some(TryCow::Borrowed(s))
182263
}
183264
}
184265

crates/environ/src/component/types.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ macro_rules! indices {
2222
#[repr(transparent)]
2323
pub struct $name(u32);
2424
cranelift_entity::entity_impl!($name);
25+
impl TryClone for $name {
26+
#[inline]
27+
fn try_clone(&self) -> Result<Self, OutOfMemory> {
28+
Ok(*self)
29+
}
30+
}
2531
)*);
2632
}
2733

crates/environ/src/component/types_builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ impl ComponentTypesBuilder {
150150
}
151151
for (name, ty) in component.exports.raw_iter() {
152152
component_ty.exports.insert(
153-
name.clone(),
153+
name.clone_panic_on_oom().into(),
154154
self.export_type_def(&component.export_items, *ty),
155155
);
156156
}

crates/fuzzing/tests/oom/component_func.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#![cfg(arc_try_new)]
22

3-
use wasmtime::component::{Component, Linker};
3+
use wasmtime::component::{Component, Linker, Val};
44
use wasmtime::{Config, Engine, Result, Store};
55
use wasmtime_fuzzing::oom::OomTest;
66

@@ -42,3 +42,47 @@ async fn component_instance_pre_instantiate_async() -> Result<()> {
4242
})
4343
.await
4444
}
45+
46+
#[tokio::test]
47+
async fn component_func_call_async() -> Result<()> {
48+
let component_bytes = {
49+
let mut config = Config::new();
50+
config.concurrency_support(false);
51+
let engine = Engine::new(&config)?;
52+
Component::new(
53+
&engine,
54+
r#"
55+
(component
56+
(core module $m
57+
(func (export "id") (param i32) (result i32) (local.get 0))
58+
)
59+
(core instance $i (instantiate $m))
60+
(func (export "id") (param "x" s32) (result s32)
61+
(canon lift (core func $i "id"))
62+
)
63+
)
64+
"#,
65+
)?
66+
.serialize()?
67+
};
68+
let mut config = Config::new();
69+
config.enable_compiler(false);
70+
config.concurrency_support(false);
71+
let engine = Engine::new(&config)?;
72+
let component = unsafe { Component::deserialize(&engine, &component_bytes)? };
73+
let linker = Linker::<()>::new(&engine);
74+
let instance_pre = linker.instantiate_pre(&component)?;
75+
76+
OomTest::new()
77+
.test_async(|| async {
78+
let mut store = Store::try_new(&engine, ())?;
79+
let instance = instance_pre.instantiate_async(&mut store).await?;
80+
let func = instance.get_func(&mut store, "id").unwrap();
81+
let mut results = [Val::S32(0)];
82+
func.call_async(&mut store, &[Val::S32(42)], &mut results)
83+
.await?;
84+
assert_eq!(results[0], Val::S32(42));
85+
Ok(())
86+
})
87+
.await
88+
}

0 commit comments

Comments
 (0)