From dc4124acc082fc35bc8b5f8a9968cef1416f5cb8 Mon Sep 17 00:00:00 2001 From: Yi LIU Date: Thu, 12 Feb 2026 19:56:02 +0800 Subject: [PATCH] Fix TypeGeneralizing stack corruption and crash Two bugs in the experimental TypeGeneralizing pass: 1. visitStructSet unconditionally pushed the field type onto the backward analysis stack, even for non-reference fields (i32, f64). Since non-ref producers (visitConst, etc.) are no-ops that don't pop, this corrupted the stack alignment, causing subsequent local.get visitors to pop wrong type requirements. Add an isRef() guard to match the pattern used by visitArraySet, visitStructNew, and handleCall. 2. visitRefAs called type.getHeapType() on Type::none (returned by pop() when the stack is empty), triggering an assertion failure. This happens when ref.as_non_null's result is dropped. Add a Type::none check to propagate "no requirement" through RefAs. --- src/passes/TypeGeneralizing.cpp | 10 ++- test/lit/passes/type-generalizing-fixes.wast | 83 ++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 test/lit/passes/type-generalizing-fixes.wast diff --git a/src/passes/TypeGeneralizing.cpp b/src/passes/TypeGeneralizing.cpp index 7ed65c602c7..c0607ef51be 100644 --- a/src/passes/TypeGeneralizing.cpp +++ b/src/passes/TypeGeneralizing.cpp @@ -687,7 +687,10 @@ struct TransferFn : OverriddenVisitor { } auto generalized = generalizeStructType(type, curr->index); push(Type(generalized, Nullable)); - push(generalized.getStruct().fields[curr->index].type); + auto fieldType = generalized.getStruct().fields[curr->index].type; + if (fieldType.isRef()) { + push(fieldType); + } } void visitStructRMW(StructRMW* curr) { WASM_UNREACHABLE("TODO"); } @@ -861,6 +864,11 @@ struct TransferFn : OverriddenVisitor { void visitRefAs(RefAs* curr) { auto type = pop(); + if (type == Type::none) { + // No downstream requirement, so no requirement for the input either. + push(Type::none); + return; + } switch (curr->op) { case RefAsNonNull: push(Type(type.getHeapType(), Nullable)); diff --git a/test/lit/passes/type-generalizing-fixes.wast b/test/lit/passes/type-generalizing-fixes.wast new file mode 100644 index 00000000000..2ca85ae5722 --- /dev/null +++ b/test/lit/passes/type-generalizing-fixes.wast @@ -0,0 +1,83 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: foreach %s %t wasm-opt --experimental-type-generalizing -all -S -o - | filecheck %s + +;; Test that visitStructSet correctly handles structs with non-ref fields +;; without corrupting the backward analysis stack. +(module + ;; CHECK: (type $s (struct (field $x (mut i32)) (field $r (mut anyref)))) + (type $s (struct (field $x (mut i32)) (field $r (mut anyref)))) + + ;; CHECK: (type $1 (func (param (ref null $s) anyref))) + + ;; CHECK: (func $struct-set-nonref-field (type $1) (param $obj (ref null $s)) (param $r anyref) + ;; CHECK-NEXT: (struct.set $s $x + ;; CHECK-NEXT: (local.get $obj) + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.set $s $r + ;; CHECK-NEXT: (local.get $obj) + ;; CHECK-NEXT: (local.get $r) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $struct-set-nonref-field (param $obj (ref null $s)) (param $r anyref) + ;; Setting a non-ref field (i32) should not push i32 onto the type + ;; requirement stack, which would corrupt subsequent ref type tracking. + (struct.set $s $x + (local.get $obj) + (i32.const 10) + ) + (struct.set $s $r + (local.get $obj) + (local.get $r) + ) + ) +) + +;; Test that visitRefAs handles the case where the stack is empty (no +;; downstream type requirement) without crashing. +(module + ;; CHECK: (type $0 (func (param anyref))) + + ;; CHECK: (func $ref-as-non-null-dropped (type $0) (param $x anyref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref-as-non-null-dropped (param $x anyref) + ;; The result of ref.as_non_null is dropped, so no downstream type + ;; requirement exists. This should not crash. + (drop + (ref.as_non_null + (local.get $x) + ) + ) + ) +) + +;; Test extern.convert_any with dropped result. +(module + ;; CHECK: (type $0 (func (param anyref))) + + ;; CHECK: (func $any-convert-extern-dropped (type $0) (param $x anyref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (any.convert_extern + ;; CHECK-NEXT: (extern.convert_any + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $any-convert-extern-dropped (param $x anyref) + ;; Dropped extern.convert_any / any.convert_extern chain should not crash. + (drop + (any.convert_extern + (extern.convert_any + (local.get $x) + ) + ) + ) + ) +)