Repository: bytecodealliance/wasm-tools
Pull request #1558 (2024-05-14 18:30:30)

`wasmparser`: Enforce the implementation limits for subtyping depth
Originally forgot about this one.

Limit is defined here: https://webassembly.github.io/gc/js-api/index.html#limits


Patch:
diff --git a/crates/wasmparser/src/limits.rs b/crates/wasmparser/src/limits.rs
index 0a6fa6f4..6e453b6f 100644
--- a/crates/wasmparser/src/limits.rs
+++ b/crates/wasmparser/src/limits.rs
@@ -37,6 +37,7 @@ pub const MAX_WASM_TAGS: usize = 1_000_000;
 pub const MAX_WASM_BR_TABLE_SIZE: usize = MAX_WASM_FUNCTION_SIZE;
 pub const MAX_WASM_STRUCT_FIELDS: usize = 10_000;
 pub const MAX_WASM_CATCHES: usize = 10_000;
+pub const MAX_WASM_SUBTYPING_DEPTH: usize = 63;
 
 pub const DEFAULT_WASM_PAGE_SIZE: u64 = 1 << 16;
 
diff --git a/crates/wasmparser/src/validator/core.rs b/crates/wasmparser/src/validator/core.rs
index a36d958a..4d728dc5 100644
--- a/crates/wasmparser/src/validator/core.rs
+++ b/crates/wasmparser/src/validator/core.rs
@@ -610,7 +610,7 @@ impl Module {
 
         self.check_composite_type(&ty.composite_type, features, offset)?;
 
-        if let Some(supertype_index) = ty.supertype_idx {
+        let depth = if let Some(supertype_index) = ty.supertype_idx {
             debug_assert!(supertype_index.is_canonical());
             let sup_id = self.at_packed_index(types, rec_group, supertype_index, offset)?;
             if types[sup_id].is_final {
@@ -619,7 +619,20 @@ impl Module {
             if !types.matches(id, sup_id) {
                 bail!(offset, "sub type must match super type");
             }
-        }
+            let depth = types.get_subtyping_depth(sup_id) + 1;
+            if usize::from(depth) > crate::limits::MAX_WASM_SUBTYPING_DEPTH {
+                bail!(
+                    offset,
+                    "sub type hierarchy too deep: found depth {}, cannot exceed depth {}",
+                    depth,
+                    crate::limits::MAX_WASM_SUBTYPING_DEPTH,
+                );
+            }
+            depth
+        } else {
+            0
+        };
+        types.set_subtyping_depth(id, depth);
 
         Ok(())
     }
diff --git a/crates/wasmparser/src/validator/types.rs b/crates/wasmparser/src/validator/types.rs
index 5a9892d7..8da034d7 100644
--- a/crates/wasmparser/src/validator/types.rs
+++ b/crates/wasmparser/src/validator/types.rs
@@ -2458,8 +2458,13 @@ pub struct TypeList {
     core_type_to_rec_group: SnapshotList<RecGroupId>,
     // The supertype of each core type.
     //
-    // A secondary map from `coreTypeId` to `Option<CoreTypeId>`.
+    // A secondary map from `CoreTypeId` to `Option<CoreTypeId>`.
     core_type_to_supertype: SnapshotList<Option<CoreTypeId>>,
+    // The subtyping depth of each core type. We use `u8::MAX` as a sentinel for
+    // an uninitialized entry.
+    //
+    // A secondary map from `CoreTypeId` to `u8`.
+    core_type_to_depth: Option<IndexMap<CoreTypeId, u8>>,
     // A primary map from `RecGroupId` to the range of the rec group's elements
     // within `core_types`.
     rec_group_elements: SnapshotList<Range<CoreTypeId>>,
@@ -2499,6 +2504,7 @@ struct TypeListCheckpoint {
     core_instances: usize,
     core_type_to_rec_group: usize,
     core_type_to_supertype: usize,
+    core_type_to_depth: usize,
     rec_group_elements: usize,
     canonical_rec_groups: usize,
 }
@@ -2614,6 +2620,29 @@ impl TypeList {
         self.core_type_to_supertype[id.index()]
     }
 
+    /// Get the subtyping depth of the given type. A type without any supertype
+    /// has depth 0.
+    pub fn get_subtyping_depth(&self, id: CoreTypeId) -> u8 {
+        let depth = self
+            .core_type_to_depth
+            .as_ref()
+            .expect("cannot get subtype depth from a committed list")[id.index()];
+        debug_assert!(usize::from(depth) <= crate::limits::MAX_WASM_SUBTYPING_DEPTH);
+        depth
+    }
+
+    /// Set the subtyping depth of the given type. This may only be done once
+    /// per type.
+    pub fn set_subtyping_depth(&mut self, id: CoreTypeId, depth: u8) {
+        debug_assert!(usize::from(depth) <= crate::limits::MAX_WASM_SUBTYPING_DEPTH);
+        let map = self
+            .core_type_to_depth
+            .as_mut()
+            .expect("cannot set a subtype depth in a committed list");
+        debug_assert!(!map.contains_key(&id));
+        map.insert(id, depth);
+    }
+
     /// Get the `CoreTypeId` for a canonicalized `PackedIndex`.
     ///
     /// Panics when given a non-canonicalized `PackedIndex`.
@@ -2827,6 +2856,7 @@ impl TypeList {
             core_instances,
             core_type_to_rec_group,
             core_type_to_supertype,
+            core_type_to_depth,
             rec_group_elements,
             canonical_rec_groups,
         } = self;
@@ -2842,6 +2872,7 @@ impl TypeList {
             core_instances: core_instances.len(),
             core_type_to_rec_group: core_type_to_rec_group.len(),
             core_type_to_supertype: core_type_to_supertype.len(),
+            core_type_to_depth: core_type_to_depth.as_ref().map(|m| m.len()).unwrap_or(0),
             rec_group_elements: rec_group_elements.len(),
             canonical_rec_groups: canonical_rec_groups.as_ref().map(|m| m.len()).unwrap_or(0),
         }
@@ -2862,6 +2893,7 @@ impl TypeList {
             core_instances,
             core_type_to_rec_group,
             core_type_to_supertype,
+            core_type_to_depth,
             rec_group_elements,
             canonical_rec_groups,
         } = self;
@@ -2878,6 +2910,14 @@ impl TypeList {
         core_type_to_supertype.truncate(checkpoint.core_type_to_supertype);
         rec_group_elements.truncate(checkpoint.rec_group_elements);
 
+        if let Some(core_type_to_depth) = core_type_to_depth {
+            assert_eq!(
+                core_type_to_depth.len(),
+                checkpoint.core_type_to_depth,
+                "checkpointing does not support resetting `core_type_to_depth` (it would require a \
+                 proper immutable and persistent hash map) so adding new groups is disallowed"
+            );
+        }
         if let Some(canonical_rec_groups) = canonical_rec_groups {
             assert_eq!(
                 canonical_rec_groups.len(),
@@ -2914,6 +2954,7 @@ impl TypeList {
             core_instances: self.core_instances.commit(),
             core_type_to_rec_group: self.core_type_to_rec_group.commit(),
             core_type_to_supertype: self.core_type_to_supertype.commit(),
+            core_type_to_depth: None,
             rec_group_elements: self.rec_group_elements.commit(),
             canonical_rec_groups: None,
         }
@@ -3006,6 +3047,7 @@ impl Default for TypeAlloc {
             },
             next_resource_id: 0,
         };
+        ret.list.core_type_to_depth = Some(Default::default());
         ret.list.canonical_rec_groups = Some(Default::default());
         ret
     }


Test patch:
diff --git a/tests/local/gc/gc-subtyping-too-deep.wast b/tests/local/gc/gc-subtyping-too-deep.wast
new file mode 100644
index 00000000..7eb1b1c4
--- /dev/null
+++ b/tests/local/gc/gc-subtyping-too-deep.wast
@@ -0,0 +1,70 @@
+(assert_invalid
+  (module
+    (type (;0;) (sub (struct)))
+    (type (;1;) (sub 0 (struct)))
+    (type (;2;) (sub 1 (struct)))
+    (type (;3;) (sub 2 (struct)))
+    (type (;4;) (sub 3 (struct)))
+    (type (;5;) (sub 4 (struct)))
+    (type (;6;) (sub 5 (struct)))
+    (type (;7;) (sub 6 (struct)))
+    (type (;8;) (sub 7 (struct)))
+    (type (;9;) (sub 8 (struct)))
+    (type (;10;) (sub 9 (struct)))
+    (type (;11;) (sub 10 (struct)))
+    (type (;12;) (sub 11 (struct)))
+    (type (;13;) (sub 12 (struct)))
+    (type (;14;) (sub 13 (struct)))
+    (type (;15;) (sub 14 (struct)))
+    (type (;16;) (sub 15 (struct)))
+    (type (;17;) (sub 16 (struct)))
+    (type (;18;) (sub 17 (struct)))
+    (type (;19;) (sub 18 (struct)))
+    (type (;20;) (sub 19 (struct)))
+    (type (;21;) (sub 20 (struct)))
+    (type (;22;) (sub 21 (struct)))
+    (type (;23;) (sub 22 (struct)))
+    (type (;24;) (sub 23 (struct)))
+    (type (;25;) (sub 24 (struct)))
+    (type (;26;) (sub 25 (struct)))
+    (type (;27;) (sub 26 (struct)))
+    (type (;28;) (sub 27 (struct)))
+    (type (;29;) (sub 28 (struct)))
+    (type (;30;) (sub 29 (struct)))
+    (type (;31;) (sub 30 (struct)))
+    (type (;32;) (sub 31 (struct)))
+    (type (;33;) (sub 32 (struct)))
+    (type (;34;) (sub 33 (struct)))
+    (type (;35;) (sub 34 (struct)))
+    (type (;36;) (sub 35 (struct)))
+    (type (;37;) (sub 36 (struct)))
+    (type (;38;) (sub 37 (struct)))
+    (type (;39;) (sub 38 (struct)))
+    (type (;40;) (sub 39 (struct)))
+    (type (;41;) (sub 40 (struct)))
+    (type (;42;) (sub 41 (struct)))
+    (type (;43;) (sub 42 (struct)))
+    (type (;44;) (sub 43 (struct)))
+    (type (;45;) (sub 44 (struct)))
+    (type (;46;) (sub 45 (struct)))
+    (type (;47;) (sub 46 (struct)))
+    (type (;48;) (sub 47 (struct)))
+    (type (;49;) (sub 48 (struct)))
+    (type (;50;) (sub 49 (struct)))
+    (type (;51;) (sub 50 (struct)))
+    (type (;52;) (sub 51 (struct)))
+    (type (;53;) (sub 52 (struct)))
+    (type (;54;) (sub 53 (struct)))
+    (type (;55;) (sub 54 (struct)))
+    (type (;56;) (sub 55 (struct)))
+    (type (;57;) (sub 56 (struct)))
+    (type (;58;) (sub 57 (struct)))
+    (type (;59;) (sub 58 (struct)))
+    (type (;60;) (sub 59 (struct)))
+    (type (;61;) (sub 60 (struct)))
+    (type (;62;) (sub 61 (struct)))
+    (type (;63;) (sub 62 (struct)))
+    (type (;64;) (sub 63 (struct)))
+  )
+  "sub type hierarchy too deep: found depth 64, cannot exceed depth 63"
+)
diff --git a/tests/snapshots/local/gc/gc-subtyping-too-deep.wast.json b/tests/snapshots/local/gc/gc-subtyping-too-deep.wast.json
new file mode 100644
index 00000000..8e81da85
--- /dev/null
+++ b/tests/snapshots/local/gc/gc-subtyping-too-deep.wast.json
@@ -0,0 +1,12 @@
+{
+  "source_filename": "tests/local/gc/gc-subtyping-too-deep.wast",
+  "commands": [
+    {
+      "type": "assert_invalid",
+      "line": 2,
+      "filename": "gc-subtyping-too-deep.0.wasm",
+      "text": "sub type hierarchy too deep: found depth 64, cannot exceed depth 63",
+      "module_type": "binary"
+    }
+  ]
+}
\ No newline at end of file