Repository: bytecodealliance/wasm-tools
Pull request #2114 (2025-03-26 21:18:30)

Fix/improve validation of components with tags
This commit fixes an issue where the `Tag` kind of core item was allowed to "leak through" validation of components without the exceptions proposal active. Additionally creating a core instance with a tag was consulting the wrong index space and needed fixing too. The end result is that the `exceptions` proposal is now required for tags to appear within components.

This so far has been glossed over because any valid component would require a core module with a tag and those are properly gated, but it's possible to construct an invalid component with a tag "kind" within it which doesn't actually have any tags. By forgetting to gate on the exception-handling proposal it was leaking this "kind" to callers which weren't necessarily prepared for it.

Finally this also fixes a panic in the text format for components where named tags still had an `unimplemented!()` for when they were being registered.

alexcrichton: Thanks to @0xalpharush for identifying this in Wasmtime which led to this bugfix here

Patch:
diff --git a/crates/wasm-encoder/src/component/names.rs b/crates/wasm-encoder/src/component/names.rs
index bd2df42c..ee57d91c 100644
--- a/crates/wasm-encoder/src/component/names.rs
+++ b/crates/wasm-encoder/src/component/names.rs
@@ -57,6 +57,12 @@ impl ComponentNameSection {
         self.core_decls(ExportKind::Global as u8, names)
     }
 
+    /// Appends a decls name subsection to name core tags within the
+    /// component.
+    pub fn core_tags(&mut self, names: &NameMap) {
+        self.core_decls(ExportKind::Tag as u8, names)
+    }
+
     /// Appends a decls name subsection to name core types within the
     /// component.
     pub fn core_types(&mut self, names: &NameMap) {
diff --git a/crates/wasmparser/src/validator.rs b/crates/wasmparser/src/validator.rs
index e61095a7..db9feb6c 100644
--- a/crates/wasmparser/src/validator.rs
+++ b/crates/wasmparser/src/validator.rs
@@ -1117,11 +1117,11 @@ impl Validator {
                 current.core_instances.reserve(count as usize);
                 Ok(())
             },
-            |components, types, _, instance, offset| {
+            |components, types, features, instance, offset| {
                 components
                     .last_mut()
                     .unwrap()
-                    .add_core_instance(instance, types, offset)
+                    .add_core_instance(instance, features, types, offset)
             },
         )
     }
diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs
index 13488d0f..f326b833 100644
--- a/crates/wasmparser/src/validator/component.rs
+++ b/crates/wasmparser/src/validator/component.rs
@@ -309,6 +309,7 @@ impl ComponentState {
     pub fn add_core_instance(
         &mut self,
         instance: crate::Instance,
+        features: &WasmFeatures,
         types: &mut TypeAlloc,
         offset: usize,
     ) -> Result<()> {
@@ -317,7 +318,7 @@ impl ComponentState {
                 self.instantiate_core_module(module_index, args.into_vec(), types, offset)?
             }
             crate::Instance::FromExports(exports) => {
-                self.instantiate_core_exports(exports.into_vec(), types, offset)?
+                self.instantiate_core_exports(exports.into_vec(), features, types, offset)?
             }
         };
 
@@ -2126,6 +2127,7 @@ impl ComponentState {
                 instance_index,
                 kind,
                 name,
+                features,
                 types,
                 offset,
             ),
@@ -3215,6 +3217,7 @@ impl ComponentState {
     fn instantiate_core_exports(
         &mut self,
         exports: Vec<crate::Export>,
+        features: &WasmFeatures,
         types: &mut TypeAlloc,
         offset: usize,
     ) -> Result<ComponentCoreInstanceTypeId> {
@@ -3278,14 +3281,19 @@ impl ComponentState {
                         offset,
                     )?;
                 }
-                ExternalKind::Tag => insert_export(
-                    types,
-                    export.name,
-                    EntityType::Tag(self.core_function_at(export.index, offset)?),
-                    &mut inst_exports,
-                    &mut info,
-                    offset,
-                )?,
+                ExternalKind::Tag => {
+                    if !features.exceptions() {
+                        bail!(offset, "exceptions proposal not enabled");
+                    }
+                    insert_export(
+                        types,
+                        export.name,
+                        EntityType::Tag(self.tag_at(export.index, offset)?),
+                        &mut inst_exports,
+                        &mut info,
+                        offset,
+                    )?
+                }
             }
         }
 
@@ -3300,6 +3308,7 @@ impl ComponentState {
         instance_index: u32,
         kind: ExternalKind,
         name: &str,
+        features: &WasmFeatures,
         types: &TypeList,
         offset: usize,
     ) -> Result<()> {
@@ -3362,6 +3371,9 @@ impl ComponentState {
                 push_module_export!(EntityType::Global, core_globals, "global");
             }
             ExternalKind::Tag => {
+                if !features.exceptions() {
+                    bail!(offset, "exceptions proposal not enabled");
+                }
                 check_max(
                     self.core_tags.len(),
                     1,
@@ -3958,6 +3970,13 @@ impl ComponentState {
         }
     }
 
+    fn tag_at(&self, idx: u32, offset: usize) -> Result<CoreTypeId> {
+        match self.core_tags.get(idx as usize) {
+            Some(t) => Ok(*t),
+            None => bail!(offset, "unknown tag {idx}: tag index out of bounds"),
+        }
+    }
+
     /// Validates that the linear memory at `idx` is valid to use as a canonical
     /// ABI memory.
     ///
diff --git a/crates/wast/src/component/binary.rs b/crates/wast/src/component/binary.rs
index 513e0eea..b981ee39 100644
--- a/crates/wast/src/component/binary.rs
+++ b/crates/wast/src/component/binary.rs
@@ -160,6 +160,7 @@ struct Encoder<'a> {
     core_type_names: Vec<Option<&'a str>>,
     core_module_names: Vec<Option<&'a str>>,
     core_instance_names: Vec<Option<&'a str>>,
+    core_tag_names: Vec<Option<&'a str>>,
     func_names: Vec<Option<&'a str>>,
     value_names: Vec<Option<&'a str>>,
     type_names: Vec<Option<&'a str>>,
@@ -609,6 +610,7 @@ impl<'a> Encoder<'a> {
         funcs(&self.core_table_names, ComponentNameSection::core_tables);
         funcs(&self.core_memory_names, ComponentNameSection::core_memories);
         funcs(&self.core_global_names, ComponentNameSection::core_globals);
+        funcs(&self.core_tag_names, ComponentNameSection::core_tags);
         funcs(&self.core_type_names, ComponentNameSection::core_types);
         funcs(&self.core_module_names, ComponentNameSection::core_modules);
         funcs(
@@ -658,7 +660,7 @@ impl<'a> Encoder<'a> {
             core::ExportKind::Global => &mut self.core_global_names,
             core::ExportKind::Table => &mut self.core_table_names,
             core::ExportKind::Memory => &mut self.core_memory_names,
-            core::ExportKind::Tag => unimplemented!(),
+            core::ExportKind::Tag => &mut self.core_tag_names,
         }
     }
 


Test patch:
diff --git a/tests/cli/component-model/gated-tags.wast b/tests/cli/component-model/gated-tags.wast
new file mode 100644
index 00000000..1ff92fa2
--- /dev/null
+++ b/tests/cli/component-model/gated-tags.wast
@@ -0,0 +1,18 @@
+;; RUN: wast --features=-exceptions %
+
+(assert_invalid
+  (component
+    (core module $m (func (export "")))
+    (core instance $i (instantiate $m))
+    (alias core export $i "" (core tag $t))
+  )
+  "exceptions proposal not enabled")
+
+(assert_invalid
+  (component
+    (core module $m (func (export "")))
+    (core instance $i (instantiate $m))
+    (core instance
+      (export "" (tag 0)))
+  )
+  "exceptions proposal not enabled")
diff --git a/tests/cli/component-model/tags.wast b/tests/cli/component-model/tags.wast
new file mode 100644
index 00000000..9f8d754b
--- /dev/null
+++ b/tests/cli/component-model/tags.wast
@@ -0,0 +1,30 @@
+;; RUN: wast %
+
+(assert_invalid
+  (component
+    (core module $m (func (export "")))
+    (core instance $i (instantiate $m))
+    (alias core export $i "" (core tag $t))
+  )
+  "export `` for core instance 0 is not a tag")
+
+(component
+  (core module $m (tag (export "")))
+  (core instance $i (instantiate $m))
+  (alias core export $i "" (core tag $t))
+)
+
+(component
+  (core module $m (tag (export "")))
+  (core instance $i (instantiate $m))
+  (core instance
+    (export "" (tag $i ""))))
+
+(assert_invalid
+  (component
+    (core module $m (func (export "")))
+    (core instance $i (instantiate $m))
+    (core instance
+      (export "" (tag 0)))
+  )
+  "unknown tag 0")