Repository: bytecodealliance/wasmtime
Pull request #8541 (2024-05-03 18:21:26)

Call-indirect caching: protect against out-of-bounds table index during prescan.
Call-indirect caching requires a "prescan" of a module's code section during compilation in order to know which tables are possibly written, and to count call-indirect callsites so that each separate function compilation can enable caching if possible and use the right slot range.

This prescan is not integrated with the usual validation logic (nor should it be, probably, for performance), so it's possible that an out-of-bounds table index or other illegal instruction could be present. We previously indexed into an internal data structure (`table_plans`) with this index, allowing for a compilation panic on certain invalid modules before validation would have caught it. This PR fixes that with a one-off check at the single point that we interpret a parameter (the table index) from an instruction during the prescan.

Fixes https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=68614.

No test case because this requires compilation with non-default parameters, so the usual WAST `assert_invalid` form can't be used in a WAST test, and requires an invalid module, so a compilation disas test can't be used. Could build out either if desired but that seems like a much larger change.


Patch:
diff --git a/crates/bench-api/src/lib.rs b/crates/bench-api/src/lib.rs
index ebcef9ac6..166db3dc2 100644
--- a/crates/bench-api/src/lib.rs
+++ b/crates/bench-api/src/lib.rs
@@ -435,7 +435,7 @@ impl BenchState {
         execution_end: extern "C" fn(*mut u8),
         make_wasi_cx: impl FnMut() -> Result<WasiCtx> + 'static,
     ) -> Result<Self> {
-        let mut config = options.config(Some(&Triple::host().to_string()))?;
+        let mut config = options.config(Some(&Triple::host().to_string()), None)?;
         // NB: always disable the compilation cache.
         config.disable_cache();
         let engine = Engine::new(&config)?;
diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs
index 985993e4c..50ddc9441 100644
--- a/crates/cli-flags/src/lib.rs
+++ b/crates/cli-flags/src/lib.rs
@@ -432,7 +432,11 @@ impl CommonOptions {
         Ok(())
     }
 
-    pub fn config(&mut self, target: Option<&str>) -> Result<Config> {
+    pub fn config(
+        &mut self,
+        target: Option<&str>,
+        pooling_allocator_default: Option<bool>,
+    ) -> Result<Config> {
         self.configure();
         let mut config = Config::new();
 
@@ -563,7 +567,7 @@ impl CommonOptions {
         }
 
         match_feature! {
-            ["pooling-allocator" : self.opts.pooling_allocator]
+            ["pooling-allocator" : self.opts.pooling_allocator.or(pooling_allocator_default)]
             enable => {
                 if enable {
                     let mut cfg = wasmtime::PoolingAllocationConfig::default();
diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs
index e98b412b7..c24e274a1 100644
--- a/crates/cranelift/src/func_environ.rs
+++ b/crates/cranelift/src/func_environ.rs
@@ -1467,7 +1467,9 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> {
             WasmHeapType::ConcreteFunc(EngineOrModuleTypeIndex::Engine(_))
             | WasmHeapType::ConcreteFunc(EngineOrModuleTypeIndex::RecGroup(_))
             | WasmHeapType::Extern
+            | WasmHeapType::NoExtern
             | WasmHeapType::Any
+            | WasmHeapType::Eq
             | WasmHeapType::I31
             | WasmHeapType::Array
             | WasmHeapType::ConcreteArray(_)
diff --git a/crates/environ/src/compile/module_environ.rs b/crates/environ/src/compile/module_environ.rs
index 3bf194489..af289cab1 100644
--- a/crates/environ/src/compile/module_environ.rs
+++ b/crates/environ/src/compile/module_environ.rs
@@ -11,6 +11,7 @@ use crate::{
 };
 use anyhow::{bail, Result};
 use cranelift_entity::packed_option::ReservedValue;
+use cranelift_entity::EntityRef;
 use std::borrow::Cow;
 use std::collections::HashMap;
 use std::mem;
@@ -737,7 +738,14 @@ and for re-adding support for interface types you can see this issue:
                     | Operator::TableCopy {
                         dst_table: table, ..
                     } => {
-                        self.flag_written_table(TableIndex::from_u32(table));
+                        // We haven't yet validated the body during
+                        // this pre-scan, so we need to check that
+                        // `dst_table` is in bounds. Ignore if not:
+                        // we'll catch the error later.
+                        let table = TableIndex::from_u32(table);
+                        if table.index() < self.result.module.table_plans.len() {
+                            self.flag_written_table(table);
+                        }
                     }
                     // Count the `call_indirect` sites so we can
                     // assign them unique slots.
diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs
index cb3d83a43..51488bfbe 100644
--- a/crates/types/src/lib.rs
+++ b/crates/types/src/lib.rs
@@ -390,6 +390,7 @@ impl EngineOrModuleTypeIndex {
 pub enum WasmHeapType {
     // External types.
     Extern,
+    NoExtern,
 
     // Function types.
     Func,
@@ -398,6 +399,7 @@ pub enum WasmHeapType {
 
     // Internal types.
     Any,
+    Eq,
     I31,
     Array,
     ConcreteArray(EngineOrModuleTypeIndex),
@@ -421,10 +423,12 @@ impl fmt::Display for WasmHeapType {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
             Self::Extern => write!(f, "extern"),
+            Self::NoExtern => write!(f, "noextern"),
             Self::Func => write!(f, "func"),
             Self::ConcreteFunc(i) => write!(f, "func {i}"),
             Self::NoFunc => write!(f, "nofunc"),
             Self::Any => write!(f, "any"),
+            Self::Eq => write!(f, "eq"),
             Self::I31 => write!(f, "i31"),
             Self::Array => write!(f, "array"),
             Self::ConcreteArray(i) => write!(f, "array {i}"),
@@ -489,13 +493,14 @@ impl WasmHeapType {
     #[inline]
     pub fn top(&self) -> WasmHeapTopType {
         match self {
-            WasmHeapType::Extern => WasmHeapTopType::Extern,
+            WasmHeapType::Extern | WasmHeapType::NoExtern => WasmHeapTopType::Extern,
 
             WasmHeapType::Func | WasmHeapType::ConcreteFunc(_) | WasmHeapType::NoFunc => {
                 WasmHeapTopType::Func
             }
 
             WasmHeapType::Any
+            | WasmHeapType::Eq
             | WasmHeapType::I31
             | WasmHeapType::Array
             | WasmHeapType::ConcreteArray(_)
@@ -1541,19 +1546,18 @@ pub trait TypeConvert {
     fn convert_heap_type(&self, ty: wasmparser::HeapType) -> WasmHeapType {
         match ty {
             wasmparser::HeapType::Extern => WasmHeapType::Extern,
+            wasmparser::HeapType::NoExtern => WasmHeapType::NoExtern,
             wasmparser::HeapType::Func => WasmHeapType::Func,
             wasmparser::HeapType::NoFunc => WasmHeapType::NoFunc,
             wasmparser::HeapType::Concrete(i) => self.lookup_heap_type(i),
             wasmparser::HeapType::Any => WasmHeapType::Any,
+            wasmparser::HeapType::Eq => WasmHeapType::Eq,
             wasmparser::HeapType::I31 => WasmHeapType::I31,
             wasmparser::HeapType::Array => WasmHeapType::Array,
             wasmparser::HeapType::Struct => WasmHeapType::Struct,
             wasmparser::HeapType::None => WasmHeapType::None,
 
-            wasmparser::HeapType::Exn
-            | wasmparser::HeapType::NoExn
-            | wasmparser::HeapType::NoExtern
-            | wasmparser::HeapType::Eq => {
+            wasmparser::HeapType::Exn | wasmparser::HeapType::NoExn => {
                 unimplemented!("unsupported heap type {ty:?}");
             }
         }
diff --git a/crates/wasmtime/src/runtime/externals/global.rs b/crates/wasmtime/src/runtime/externals/global.rs
index 0cc329576..c11c7d2a7 100644
--- a/crates/wasmtime/src/runtime/externals/global.rs
+++ b/crates/wasmtime/src/runtime/externals/global.rs
@@ -133,7 +133,10 @@ impl Global {
                                 .into(),
                         ),
 
+                        HeapType::NoExtern => Ref::Extern(None),
+
                         HeapType::Any
+                        | HeapType::Eq
                         | HeapType::I31
                         | HeapType::Struct
                         | HeapType::ConcreteStruct(_)
diff --git a/crates/wasmtime/src/runtime/types.rs b/crates/wasmtime/src/runtime/types.rs
index 822a0986f..2d17fe865 100644
--- a/crates/wasmtime/src/runtime/types.rs
+++ b/crates/wasmtime/src/runtime/types.rs
@@ -431,6 +431,132 @@ impl RefType {
 
 /// The heap types that can Wasm can have references to.
 ///
+/// # Subtyping Hierarchy
+///
+/// Wasm has three different heap type hierarchies:
+///
+/// 1. Function types
+/// 2. External types
+/// 3. Internal types
+///
+/// Each hierarchy has a top type (the common supertype of which everything else
+/// in its hierarchy is a subtype of) and a bottom type (the common subtype of
+/// which everything else in its hierarchy is supertype of).
+///
+/// ## Function Types Hierarchy
+///
+/// The top of the function types hierarchy is `func`; the bottom is
+/// `nofunc`. In between are all the concrete function types.
+///
+/// ```text
+///                          func
+///                       /  /  \  \
+///      ,----------------  /    \  -------------------------.
+///     /                  /      \                           \
+///    |              ,----        -----------.                |
+///    |              |                       |                |
+///    |              |                       |                |
+/// (func)    (func (param i32))    (func (param i32 i32))    ...
+///    |              |                       |                |
+///    |              |                       |                |
+///    |              `---.        ,----------'                |
+///     \                  \      /                           /
+///      `---------------.  \    /  ,------------------------'
+///                       \  \  /  /
+///                         nofunc
+/// ```
+///
+/// Additionally, some concrete function types are sub- or supertypes of other
+/// concrete function types. For simplicity, this isn't depicted in the diagram
+/// above. Specifically, this is based on their parameter and result types, and
+/// whether those types are in a subtyping relationship:
+///
+/// * Parameters are contravariant: `(func (param $a) (result $c))` is a subtype
+///   of `(func (param $b) (result $c))` when `$b` is a subtype of `$a`.
+///
+///   For example, we can substitute `(func (param $cat))` with `(func (param
+///   $animal))` because `$cat` is a subtype of `$animal` and so the new
+///   function is still prepared to accept all `$cat` arguments that any caller
+///   might pass in.
+///
+///   We can't do the opposite and replace `(func (param $animal))` with `(func
+///   (param $cat))`. What would the new function do when given a `$dog`? It is
+///   invalid.
+///
+/// * Results are covariant: `(func (result $a))` is a subtype of `(func (result
+///   $b))` when `$a` is a subtype of `$b`.
+///
+///   For example, we can substitute a `(func (result $animal))` with a
+///   `(func (result $cat))` because callers expect to be returned an
+///   `$animal` and all `$cat`s are `$animal`s.
+///
+///   We cannot do the opposite and substitute a `(func (result $cat))` with a
+///   `(func (result $animal))`, since callers expect a `$cat` but the new
+///   function could return a `$dog`.
+///
+/// As always, Wikipedia is also helpful:
+/// https:///en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)
+///
+/// ## External
+///
+/// The top of the external types hierarchy is `extern`; the bottom is
+/// `noextern`. There are no concrete types in this hierarchy.
+///
+/// ```text
+///  extern
+///    |
+/// noextern
+/// ```
+///
+/// ## Internal
+///
+/// The top of the internal types hierarchy is `any`; the bottom is `none`. The
+/// `eq` type is the common supertype of all types that can be compared for
+/// equality. The `struct` and `array` types are the common supertypes of all
+/// concrete struct and array types respectively. The `i31` type represents
+/// unboxed 31-bit integers.
+///
+/// ```text
+///                                   any
+///                                  / | \
+///    ,----------------------------'  |  `--------------------------.
+///   /                                |                              \
+///  |                        .--------'                               |
+///  |                        |                                        |
+///  |                      struct                                   array
+///  |                     /  |   \                                 /  |   \
+/// i31             ,-----'   |    '-----.                   ,-----'   |    `-----.
+///  |             /          |           \                 /          |           \
+///  |            |           |            |               |           |            |
+///  |        (struct)    (struct i32)    ...        (array i32)    (array i64)    ...
+///  |            |           |            |               |           |            |
+///  |             \          |           /                 \          |           /
+///   \             `-----.   |    ,-----'                   `-----.   |    ,-----'
+///    \                   \  |   /                                 \  |   /
+///     \                   \ |  /                                   \ |  /
+///      \                   \| /                                     \| /
+///       \                   |/                                       |/
+///        \                  |                                        |
+///         \                 |                                       /
+///          \                '--------.                             /
+///           \                        |                            /
+///            `--------------------.  |   ,-----------------------'
+///                                  \ |  /
+///                                   none
+/// ```
+///
+/// Additionally, concrete struct and array types can be subtypes of other
+/// concrete struct and array types respectively. Once again, this is omitted
+/// from the above diagram for simplicity. Specifically:
+///
+/// * Array types are covariant with their element type: `(array $a)` is a
+///   subtype of `(array $b)` if `$a` is a subtype of `$b`. For example, `(array
+///   $cat)` is a subtype of `(array $animal)`.
+///
+/// * Struct types are covariant with their field types, and subtypes may
+///   additionally have appended fields that do not appear in the supertype. For
+///   example, `(struct $cat $dog)` is a subtype of `(struct $animal)`.
+///
 /// # Subtyping and Equality
 ///
 /// `HeapType` does not implement `Eq`, because heap types have a subtyping
@@ -442,8 +568,18 @@ impl RefType {
 #[derive(Debug, Clone, Hash)]
 pub enum HeapType {
     /// The abstract `extern` heap type represents external host data.
+    ///
+    /// This is the top type for the external type hierarchy, and therefore is
+    /// the common supertype of all external reference types.
     Extern,
 
+    /// The abstract `noextern` heap type represents the null external
+    /// reference.
+    ///
+    /// This is the bottom type for the external type hierarchy, and therefore
+    /// is the common subtype of all external reference types.
+    NoExtern,
+
     /// The abstract `func` heap type represents a reference to any kind of
     /// function.
     ///
@@ -465,47 +601,46 @@ pub enum HeapType {
     /// The abstract `any` heap type represents all internal Wasm data.
     ///
     /// This is the top type of the internal type hierarchy, and is therefore a
-    /// supertype of all internal types (such as `i31`, `struct`s, and
+    /// supertype of all internal types (such as `eq`, `i31`, `struct`s, and
     /// `array`s).
     Any,
 
+    /// The abstract `eq` heap type represenets all internal Wasm references
+    /// that can be compared for equality.
+    ///
+    /// This is a subtype of `any` and a supertype of `i31`, `array`, `struct`,
+    /// and `none` heap types.
+    Eq,
+
     /// The `i31` heap type represents unboxed 31-bit integers.
     ///
-    /// This is a subtype of `any` and a supertype of `none`.
+    /// This is a subtype of `any` and `eq`, and a supertype of `none`.
     I31,
 
     /// The abstract `array` heap type represents a reference to any kind of
     /// array.
     ///
-    /// This is a subtype of `any` and a supertype of all concrete array types,
-    /// as well as a supertype of the abstract `none` heap type.
-    //
-    // TODO: add docs for subtype of `eq` once we add that heap type
+    /// This is a subtype of `any` and `eq`, and a supertype of all concrete
+    /// array types, as well as a supertype of the abstract `none` heap type.
     Array,
 
     /// A reference to an array of a specific, concrete type.
     ///
     /// These are subtypes of the `array` heap type (therefore also a subtype of
-    /// `any`) and supertypes of the `none` heap type.
-    //
-    // TODO: add docs for subtype of `eq` once we add that heap type
+    /// `any` and `eq`) and supertypes of the `none` heap type.
     ConcreteArray(ArrayType),
 
     /// The abstract `struct` heap type represents a reference to any kind of
     /// struct.
     ///
-    /// This is a subtype of `any` and a supertype of all concrete struct types,
-    /// as well as a supertype of the abstract `none` heap type.
-    //
-    // TODO: add docs for subtype of `eq` once we add that heap type
+    /// This is a subtype of `any` and `eq`, and a supertype of all concrete
+    /// struct types, as well as a supertype of the abstract `none` heap type.
     Struct,
 
     /// A reference to an struct of a specific, concrete type.
     ///
     /// These are subtypes of the `struct` heap type (therefore also a subtype
-    /// of `any`) and supertypes of the `none` heap type.
-    //
-    // TODO: add docs for subtype of `eq` once we add that heap type
+    /// of `any` and `eq`) and supertypes of the `none` heap type.
     ConcreteStruct(StructType),
 
     /// The abstract `none` heap type represents the null internal reference.
@@ -519,9 +654,11 @@ impl Display for HeapType {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
             HeapType::Extern => write!(f, "extern"),
+            HeapType::NoExtern => write!(f, "noextern"),
             HeapType::Func => write!(f, "func"),
             HeapType::NoFunc => write!(f, "nofunc"),
             HeapType::Any => write!(f, "any"),
+            HeapType::Eq => write!(f, "eq"),
             HeapType::I31 => write!(f, "i31"),
             HeapType::Array => write!(f, "array"),
             HeapType::Struct => write!(f, "struct"),
@@ -644,9 +781,10 @@ impl HeapType {
         match self {
             HeapType::Func | HeapType::ConcreteFunc(_) | HeapType::NoFunc => HeapType::Func,
 
-            HeapType::Extern => HeapType::Extern,
+            HeapType::Extern | HeapType::NoExtern => HeapType::Extern,
 
             HeapType::Any
+            | HeapType::Eq
             | HeapType::I31
             | HeapType::Array
             | HeapType::ConcreteArray(_)
@@ -657,6 +795,7 @@ impl HeapType {
     }
 
     /// Is this the top type within its type hierarchy?
+    #[inline]
     pub fn is_top(&self) -> bool {
         match self {
             HeapType::Any | HeapType::Extern | HeapType::Func => true,
@@ -664,6 +803,37 @@ impl HeapType {
         }
     }
 
+    /// Get the bottom type of this heap type's type hierarchy.
+    ///
+    /// The returned heap type is a subtype of all types in this heap type's
+    /// type hierarchy.
+    #[inline]
+    pub fn bottom(&self) -> HeapType {
+        match self {
+            HeapType::Extern | HeapType::NoExtern => HeapType::NoExtern,
+
+            HeapType::Func | HeapType::ConcreteFunc(_) | HeapType::NoFunc => HeapType::NoFunc,
+
+            HeapType::Any
+            | HeapType::Eq
+            | HeapType::I31
+            | HeapType::Array
+            | HeapType::ConcreteArray(_)
+            | HeapType::Struct
+            | HeapType::ConcreteStruct(_)
+            | HeapType::None => HeapType::None,
+        }
+    }
+
+    /// Is this the bottom type within its type hierarchy?
+    #[inline]
+    pub fn is_bottom(&self) -> bool {
+        match self {
+            HeapType::None | HeapType::NoExtern | HeapType::NoFunc => true,
+            _ => false,
+        }
+    }
+
     /// Does this heap type match the other heap type?
     ///
     /// That is, is this heap type a subtype of the other?
@@ -677,6 +847,9 @@ impl HeapType {
             (HeapType::Extern, HeapType::Extern) => true,
             (HeapType::Extern, _) => false,
 
+            (HeapType::NoExtern, HeapType::NoExtern | HeapType::Extern) => true,
+            (HeapType::NoExtern, _) => false,
+
             (HeapType::NoFunc, HeapType::NoFunc | HeapType::ConcreteFunc(_) | HeapType::Func) => {
                 true
             }
@@ -697,27 +870,31 @@ impl HeapType {
                 | HeapType::ConcreteStruct(_)
                 | HeapType::Struct
                 | HeapType::I31
+                | HeapType::Eq
                 | HeapType::Any,
             ) => true,
             (HeapType::None, _) => false,
 
-            (HeapType::ConcreteArray(_), HeapType::Array | HeapType::Any) => true,
+            (HeapType::ConcreteArray(_), HeapType::Array | HeapType::Eq | HeapType::Any) => true,
             (HeapType::ConcreteArray(a), HeapType::ConcreteArray(b)) => a.matches(b),
             (HeapType::ConcreteArray(_), _) => false,
 
-            (HeapType::Array, HeapType::Array | HeapType::Any) => true,
+            (HeapType::Array, HeapType::Array | HeapType::Eq | HeapType::Any) => true,
             (HeapType::Array, _) => false,
 
-            (HeapType::ConcreteStruct(_), HeapType::Struct | HeapType::Any) => true,
+            (HeapType::ConcreteStruct(_), HeapType::Struct | HeapType::Eq | HeapType::Any) => true,
             (HeapType::ConcreteStruct(a), HeapType::ConcreteStruct(b)) => a.matches(b),
             (HeapType::ConcreteStruct(_), _) => false,
 
-            (HeapType::Struct, HeapType::Struct | HeapType::Any) => true,
+            (HeapType::Struct, HeapType::Struct | HeapType::Eq | HeapType::Any) => true,
             (HeapType::Struct, _) => false,
 
-            (HeapType::I31, HeapType::I31 | HeapType::Any) => true,
+            (HeapType::I31, HeapType::I31 | HeapType::Eq | HeapType::Any) => true,
             (HeapType::I31, _) => false,
 
+            (HeapType::Eq, HeapType::Eq | HeapType::Any) => true,
+            (HeapType::Eq, _) => false,
+
             (HeapType::Any, HeapType::Any) => true,
             (HeapType::Any, _) => false,
         }
@@ -750,9 +927,11 @@ impl HeapType {
     pub(crate) fn comes_from_same_engine(&self, engine: &Engine) -> bool {
         match self {
             HeapType::Extern
+            | HeapType::NoExtern
             | HeapType::Func
             | HeapType::NoFunc
             | HeapType::Any
+            | HeapType::Eq
             | HeapType::I31
             | HeapType::Array
             | HeapType::Struct
@@ -766,9 +945,11 @@ impl HeapType {
     pub(crate) fn to_wasm_type(&self) -> WasmHeapType {
         match self {
             HeapType::Extern => WasmHeapType::Extern,
+            HeapType::NoExtern => WasmHeapType::NoExtern,
             HeapType::Func => WasmHeapType::Func,
             HeapType::NoFunc => WasmHeapType::NoFunc,
             HeapType::Any => WasmHeapType::Any,
+            HeapType::Eq => WasmHeapType::Eq,
             HeapType::I31 => WasmHeapType::I31,
             HeapType::Array => WasmHeapType::Array,
             HeapType::Struct => WasmHeapType::Struct,
@@ -788,9 +969,11 @@ impl HeapType {
     pub(crate) fn from_wasm_type(engine: &Engine, ty: &WasmHeapType) -> HeapType {
         match ty {
             WasmHeapType::Extern => HeapType::Extern,
+            WasmHeapType::NoExtern => HeapType::NoExtern,
             WasmHeapType::Func => HeapType::Func,
             WasmHeapType::NoFunc => HeapType::NoFunc,
             WasmHeapType::Any => HeapType::Any,
+            WasmHeapType::Eq => HeapType::Eq,
             WasmHeapType::I31 => HeapType::I31,
             WasmHeapType::Array => HeapType::Array,
             WasmHeapType::Struct => HeapType::Struct,
@@ -823,9 +1006,11 @@ impl HeapType {
             HeapType::ConcreteStruct(a) => Some(&a.registered_type),
 
             HeapType::Extern
+            | HeapType::NoExtern
             | HeapType::Func
             | HeapType::NoFunc
             | HeapType::Any
+            | HeapType::Eq
             | HeapType::I31
             | HeapType::Array
             | HeapType::Struct
diff --git a/crates/wasmtime/src/runtime/types/matching.rs b/crates/wasmtime/src/runtime/types/matching.rs
index f7ed606ef..35e60fcad 100644
--- a/crates/wasmtime/src/runtime/types/matching.rs
+++ b/crates/wasmtime/src/runtime/types/matching.rs
@@ -248,11 +248,15 @@ fn match_heap(expected: WasmHeapType, actual: WasmHeapType, desc: &str) -> Resul
         (H::NoFunc | H::ConcreteFunc(_) | H::Func, H::Func) => true,
         (_, H::Func) => false,
 
-        (H::Extern, H::Extern) => true,
+        (H::Extern | H::NoExtern, H::Extern) => true,
         (_, H::Extern) => false,
 
+        (H::NoExtern, H::NoExtern) => true,
+        (_, H::NoExtern) => false,
+
         (
             H::Any
+            | H::Eq
             | H::I31
             | H::Array
             | H::ConcreteArray(_)
@@ -263,6 +267,18 @@ fn match_heap(expected: WasmHeapType, actual: WasmHeapType, desc: &str) -> Resul
         ) => true,
         (_, H::Any) => false,
 
+        (
+            H::Eq
+            | H::I31
+            | H::Array
+            | H::ConcreteArray(_)
+            | H::Struct
+            | H::ConcreteStruct(_)
+            | H::None,
+            H::Eq,
+        ) => true,
+        (_, H::Eq) => false,
+
         (H::I31 | H::None, H::I31) => true,
         (_, H::I31) => false,
 
diff --git a/crates/wasmtime/src/runtime/values.rs b/crates/wasmtime/src/runtime/values.rs
index 740e2c7bf..63cdd6ce1 100644
--- a/crates/wasmtime/src/runtime/values.rs
+++ b/crates/wasmtime/src/runtime/values.rs
@@ -248,7 +248,10 @@ impl Val {
 
                     HeapType::Extern => ExternRef::from_raw(store, raw.get_externref()).into(),
 
+                    HeapType::NoExtern => Ref::Extern(None),
+
                     HeapType::Any
+                    | HeapType::Eq
                     | HeapType::I31
                     | HeapType::Array
                     | HeapType::ConcreteArray(_)
diff --git a/src/commands/compile.rs b/src/commands/compile.rs
index d1e6ef03d..e31610094 100644
--- a/src/commands/compile.rs
+++ b/src/commands/compile.rs
@@ -61,7 +61,7 @@ impl CompileCommand {
     pub fn execute(mut self) -> Result<()> {
         self.common.init_logging()?;
 
-        let mut config = self.common.config(self.target.as_deref())?;
+        let mut config = self.common.config(self.target.as_deref(), None)?;
 
         if let Some(path) = self.emit_clif {
             if !path.exists() {
diff --git a/src/commands/explore.rs b/src/commands/explore.rs
index 6aaa004e3..b2a87b5c6 100644
--- a/src/commands/explore.rs
+++ b/src/commands/explore.rs
@@ -30,7 +30,7 @@ impl ExploreCommand {
     pub fn execute(mut self) -> Result<()> {
         self.common.init_logging()?;
 
-        let config = self.common.config(self.target.as_deref())?;
+        let config = self.common.config(self.target.as_deref(), None)?;
 
         let bytes =
             Cow::Owned(std::fs::read(&self.module).with_context(|| {
diff --git a/src/commands/run.rs b/src/commands/run.rs
index d5e1d6242..6f3c843a3 100644
--- a/src/commands/run.rs
+++ b/src/commands/run.rs
@@ -74,7 +74,7 @@ impl RunCommand {
     pub fn execute(mut self) -> Result<()> {
         self.run.common.init_logging()?;
 
-        let mut config = self.run.common.config(None)?;
+        let mut config = self.run.common.config(None, None)?;
 
         if self.run.common.wasm.timeout.is_some() {
             config.epoch_interruption(true);
diff --git a/src/commands/serve.rs b/src/commands/serve.rs
index 90cb660d5..e50e5228b 100644
--- a/src/commands/serve.rs
+++ b/src/commands/serve.rs
@@ -247,23 +247,12 @@ impl ServeCommand {
     async fn serve(mut self) -> Result<()> {
         use hyper::server::conn::http1;
 
-        let mut config = self.run.common.config(None)?;
-        match self.run.common.opts.pooling_allocator {
-            // If explicitly enabled on the CLI then the pooling allocator was
-            // already configured in the `config` method above. If the allocator
-            // is explicitly disabled, then we don't want it. In both cases do
-            // nothing.
-            Some(true) | Some(false) => {}
-
-            // Otherwise though if not explicitly specified then always enable
-            // the pooling allocator. The `wasmtime serve` use case is
-            // tailor-made for pooling allocation and there's no downside to
-            // enabling it.
-            None => {
-                let cfg = wasmtime::PoolingAllocationConfig::default();
-                config.allocation_strategy(wasmtime::InstanceAllocationStrategy::Pooling(cfg));
-            }
-        }
+        // If not explicitly specified then always enable the pooling allocator.
+        // The `wasmtime serve` use case is tailor-made for pooling allocation
+        // and there's not much downside to enabling it.
+        let pooling_allocator_default = Some(true);
+
+        let mut config = self.run.common.config(None, pooling_allocator_default)?;
         config.wasm_component_model(true);
         config.async_support(true);
 
diff --git a/src/commands/wast.rs b/src/commands/wast.rs
index 50f8d15ba..4d4cf5000 100644
--- a/src/commands/wast.rs
+++ b/src/commands/wast.rs
@@ -23,7 +23,7 @@ impl WastCommand {
     pub fn execute(mut self) -> Result<()> {
         self.common.init_logging()?;
 
-        let config = self.common.config(None)?;
+        let config = self.common.config(None, None)?;
         let store = Store::new(&Engine::new(&config)?, ());
         let mut wast_context = WastContext::new(store);
 


Test patch:
diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs
index 930e709b4..89fb70bd4 100644
--- a/tests/all/cli_tests.rs
+++ b/tests/all/cli_tests.rs
@@ -1640,36 +1640,51 @@ mod test_programs {
             // all remaining output is left to be captured by future requests
             // send to the server.
             let mut line = String::new();
-            BufReader::new(child.stderr.as_mut().unwrap()).read_line(&mut line)?;
-            let addr_start = line.find("127.0.0.1").unwrap();
-            let addr = &line[addr_start..];
-            let addr_end = addr.find("/").unwrap();
-            let addr = &addr[..addr_end];
-            Ok(WasmtimeServe {
-                child: Some(child),
-                addr: addr.parse().unwrap(),
-            })
+            let mut reader = BufReader::new(child.stderr.take().unwrap());
+            reader.read_line(&mut line)?;
+
+            match line.find("127.0.0.1").and_then(|addr_start| {
+                let addr = &line[addr_start..];
+                let addr_end = addr.find("/")?;
+                addr[..addr_end].parse().ok()
+            }) {
+                Some(addr) => {
+                    assert!(reader.buffer().is_empty());
+                    child.stderr = Some(reader.into_inner());
+                    Ok(WasmtimeServe {
+                        child: Some(child),
+                        addr,
+                    })
+                }
+                None => {
+                    child.kill()?;
+                    child.wait()?;
+                    reader.read_to_string(&mut line)?;
+                    bail!("failed to start child: {line}")
+                }
+            }
         }
 
         /// Completes this server gracefully by printing the output on failure.
-        fn finish(mut self) -> Result<()> {
+        fn finish(mut self) -> Result<String> {
             let mut child = self.child.take().unwrap();
 
             // If the child process has already exited then collect the output
             // and test if it succeeded. Otherwise it's still running so kill it
             // and then reap it. Assume that if it's still running then the test
             // has otherwise passed so no need to print the output.
-            if child.try_wait()?.is_some() {
-                let output = child.wait_with_output()?;
-                if output.status.success() {
-                    return Ok(());
-                }
-                bail!("child failed {output:?}");
+            let known_failure = if child.try_wait()?.is_some() {
+                false
             } else {
                 child.kill()?;
-                child.wait_with_output()?;
+                true
+            };
+            let output = child.wait_with_output()?;
+            if !known_failure && !output.status.success() {
+                bail!("child failed {output:?}");
             }
-            Ok(())
+
+            Ok(String::from_utf8_lossy(&output.stderr).into_owned())
         }
 
         /// Send a request to this server and wait for the response.
@@ -1775,6 +1790,34 @@ mod test_programs {
         server.finish()?;
         Ok(())
     }
+
+    #[tokio::test]
+    #[ignore] // TODO: printing stderr in the child and killing the child at the
+              // end of this test race so the stderr may be present or not. Need
+              // to implement a more graceful shutdown routine for `wasmtime
+              // serve`.
+    async fn cli_serve_respect_pooling_options() -> Result<()> {
+        let server = WasmtimeServe::new(CLI_SERVE_ECHO_ENV_COMPONENT, |cmd| {
+            cmd.arg("-Opooling-total-memories=0").arg("-Scli");
+        })?;
+
+        let result = server
+            .send_request(
+                hyper::Request::builder()
+                    .uri("http://localhost/")
+                    .header("env", "FOO")
+                    .body(String::new())
+                    .context("failed to make request")?,
+            )
+            .await;
+        assert!(result.is_err());
+        let stderr = server.finish()?;
+        assert!(
+            stderr.contains("maximum concurrent memory limit of 0 reached"),
+            "bad stderr: {stderr}",
+        );
+        Ok(())
+    }
 }
 
 #[test]
diff --git a/tests/all/module.rs b/tests/all/module.rs
index 153be7b34..20c5f23d3 100644
--- a/tests/all/module.rs
+++ b/tests/all/module.rs
@@ -269,3 +269,31 @@ fn call_indirect_caching_and_memory64() -> Result<()> {
     )?;
     Ok(())
 }
+
+#[test]
+fn call_indirect_caching_out_of_bounds_table_index() -> Result<()> {
+    let mut config = Config::new();
+    config.cache_call_indirects(true);
+    let engine = Engine::new(&config)?;
+    // Test an out-of-bounds table index: this is exposed to the prescan
+    // that call-indirect caching must perform during compilation, so we
+    // need to make sure the error is properly handled by the validation
+    // that comes later.
+    let err = Module::new(
+        &engine,
+        "(module
+            (func (param i32)
+                ref.null func
+                local.get 0
+                table.set 32  ;; out-of-bounds table index
+            )
+        )",
+    )
+    .unwrap_err();
+    let err = format!("{err:?}");
+    assert!(
+        err.contains("table index out of bounds"),
+        "bad error: {err}"
+    );
+    Ok(())
+}
diff --git a/tests/disas.rs b/tests/disas.rs
index dc1c150ef..fa38dbb0f 100644
--- a/tests/disas.rs
+++ b/tests/disas.rs
@@ -190,7 +190,7 @@ impl Test {
         // Use wasmtime::Config with its `emit_clif` option to get Wasmtime's
         // code generator to jettison CLIF out the back.
         let tempdir = TempDir::new().context("failed to make a tempdir")?;
-        let mut config = self.opts.config(Some(&self.config.target))?;
+        let mut config = self.opts.config(Some(&self.config.target), None)?;
         match self.config.test {
             TestKind::Clif | TestKind::Optimize => {
                 config.emit_clif(tempdir.path());