Repository: WasmEdge/WasmEdge (2026-04-10T07:15:14Z)

Issue:
bug: [GC] WasmEdge crash: `ref.test`/`ref.cast` on default-initialized local with concrete ref type
### Summary

# WasmEdge SIGSEGV: `ref.test`/`ref.cast` on default-initialized local with concrete ref type

## Version

- WasmEdge `77c5238980b3c9cd8fc7e7dc69b20127d9fd68ca` (includes fix from #4764)
- Linux 5.15.0-168-generic x86\_64

## Relation to #4757 / #4764

This is the **same crash site** as #4757 (`runRefTestOp` null-pointer dereference in `refInstr.cpp:339`), but a **different source** of the concrete-typed null. PR #4764 fixed the table-initialization path; this bug is in the **function-local initialization** path and remains unfixed on current `master`.

## Minimal Reproducer

```wat
(module
  (type $s (struct))
  (func (export "run")
    (local (ref null $s))
    local.get 0
    ref.test (ref null $s)
    drop
  )
)
```

```bash
wasm-tools parse crash.wat -o crash.wasm
wasm-tools validate crash.wasm   # OK
wasmedge crash.wasm run          # Segmentation fault
wasmtime run --wasm gc --invoke run crash.wasm  # OK
```

## (Potential) Root Cause

`ValueFromType` ([include/common/types.h:680](https://github.com/WasmEdge/WasmEdge/blob/77c5238980b3c9cd8fc7e7dc69b20127d9fd68ca/include/common/types.h#L680)) initializes reference-typed locals with `RefVariant(Type)`, passing the **raw concrete type** (e.g. `(ref null $s)`). This creates a null `RefVariant` whose `ValType` has `HTCode = TypeIndex`, violating the invariant that null references always carry abstract heap types.

When `ref.test` or `ref.cast` later calls `runRefTestOp` ([lib/executor/engine/refInstr.cpp:334-339](https://github.com/WasmEdge/WasmEdge/blob/77c5238980b3c9cd8fc7e7dc69b20127d9fd68ca/lib/executor/engine/refInstr.cpp#L334-L339)):

```cpp
if (!VT.isAbsHeapType()) {                       // true — TypeIndex is not abstract
    auto *Inst = Val.get<RefVariant>()
                    .getPtr<Runtime::Instance::CompositeBase>();
    // "Reference must not be nullptr here because the null references
    //  are typed with the least abstract heap type."
    if (Inst->getModule()) {                      // CRASH — Inst is nullptr
```

This is the same invariant violation fixed for tables in #4764, but through a different entry point.

Either normalize the type in `ValueFromType` (requires access to the module to call `toBottomType`), or normalize in `enterFunction` ([lib/executor/helper.cpp:212-216](https://github.com/WasmEdge/WasmEdge/blob/77c5238980b3c9cd8fc7e7dc69b20127d9fd68ca/lib/executor/helper.cpp#L212-L216)) where locals are pushed onto the stack — mirroring the approach used for tables in #4764.

## Acknowledgement

This bug was found during the development of GCsmith.

_MORE INFO WILL BE ADDED ABOUT ACKNOWLEDGEMENT_

### Current State

_No response_

### Expected State

_No response_

### Reproduction steps

1. Build with options '...'
2. Execute with flags '....'
3. Execute with inputs '....'
4. Get error


### Screenshots

![DESCRIPTION](LINK.png)


### Any logs you want to share for showing the specific issue

_No response_

### Components

CLI

### WasmEdge Version or Commit you used

77c5238980b3c9cd8fc7e7dc69b20127d9fd68ca 

### Operating system information

Ubuntu 22.04.5 LTS

### Hardware Architecture

Linux 5.15.0-168-generic x86_64

### Compiler flags and options

_No response_


Discussion:
Comment: besides the uninitialized referece of table instance, we should check the all possible ref.null values should be configured to bottom types.



Patch:
diff --git a/include/runtime/instance/table.h b/include/runtime/instance/table.h
index f003cf1c72ee..c0d0419bdc35 100644
--- a/include/runtime/instance/table.h
+++ b/include/runtime/instance/table.h
@@ -34,8 +34,12 @@ class TableInstance {
       : TabType(TType),
         Refs(TType.getLimit().getMin(), RefVariant(TType.getRefType())),
         InitValue(RefVariant(TType.getRefType())) {
-    // The reftype should a nullable reference because of no init ref.
+    // The reftype should be a nullable reference because of no init ref.
+    // This constructor only handles abstract heap types correctly for null
+    // refs. For concrete type indices, the caller should use the two-arg
+    // constructor with a properly initialized RefVariant.
     assuming(TType.getRefType().isNullableRefType());
+    assuming(TType.getRefType().isAbsHeapType());
   }
   TableInstance(const AST::TableType &TType, const RefVariant &InitVal) noexcept
       : TabType(TType), Refs(TType.getLimit().getMin(), InitVal),
diff --git a/lib/executor/engine/refInstr.cpp b/lib/executor/engine/refInstr.cpp
index a51a785d43f4..851b32c912df 100644
--- a/lib/executor/engine/refInstr.cpp
+++ b/lib/executor/engine/refInstr.cpp
@@ -336,6 +336,7 @@ Executor::runRefTestOp(const Runtime::Instance::ModuleInstance *ModInst,
         Val.get<RefVariant>().getPtr<Runtime::Instance::CompositeBase>();
     // Reference must not be nullptr here because the null references are typed
     // with the least abstract heap type.
+    assuming(Inst);
     if (Inst->getModule()) {
       GotTypeList = Inst->getModule()->getTypeList();
     }
diff --git a/lib/executor/helper.cpp b/lib/executor/helper.cpp
index 67f4f9f94897..d5f832bad978 100644
--- a/lib/executor/helper.cpp
+++ b/lib/executor/helper.cpp
@@ -210,8 +210,23 @@ Executor::enterFunction(Runtime::StackManager &StackMgr,
 
     // Push local variables into the stack.
     for (auto &Def : Func.getLocals()) {
-      for (uint32_t I = 0; I < Def.first; I++) {
-        StackMgr.push(ValueFromType(Def.second));
+      if (Def.second.isRefType() && !Def.second.isAbsHeapType()) {
+        // For non-abstract heap types (concrete type indices), convert the
+        // null ref to the abstract heap type so that ref.cast/ref.test won't
+        // dereference a null pointer when checking the type.
+        const auto &CompType =
+            (*Func.getModule()->getType(Def.second.getTypeIndex()))
+                ->getCompositeType();
+        auto BotTypeCode =
+            CompType.isFunc() ? TypeCode::NullFuncRef : TypeCode::NullRef;
+        RefVariant InitVal(ValType(TypeCode::RefNull, BotTypeCode));
+        for (uint32_t I = 0; I < Def.first; I++) {
+          StackMgr.push(InitVal);
+        }
+      } else {
+        for (uint32_t I = 0; I < Def.first; I++) {
+          StackMgr.push(ValueFromType(Def.second));
+        }
       }
     }
 
diff --git a/lib/llvm/compiler.cpp b/lib/llvm/compiler.cpp
index 6a0014c90b42..2c731a1b0c5c 100644
--- a/lib/llvm/compiler.cpp
+++ b/lib/llvm/compiler.cpp
@@ -42,8 +42,9 @@ static LLVM::Type
 toLLVMType(LLVM::Context LLContext, LLVM::Type ExecCtxPtrTy,
            const WasmEdge::AST::FunctionType &FuncType) noexcept;
 static LLVM::Value
-toLLVMConstantZero(LLVM::Context LLContext,
-                   const WasmEdge::ValType &ValType) noexcept;
+toLLVMConstantZero(LLVM::Context LLContext, const WasmEdge::ValType &ValType,
+                   WasmEdge::Span<const WasmEdge::AST::CompositeType *const>
+                       CompositeTypes) noexcept;
 static std::vector<LLVM::Value> unpackStruct(LLVM::Builder &Builder,
                                              LLVM::Value Struct) noexcept;
 class FunctionCompiler;
@@ -496,8 +497,9 @@ static LLVM::Type toLLVMType(LLVM::Context LLContext, LLVM::Type ExecCtxPtrTy,
   return LLVM::Type::getFunctionType(RetTy, ArgsTy);
 }
 
-static LLVM::Value toLLVMConstantZero(LLVM::Context LLContext,
-                                      const ValType &ValType) noexcept {
+static LLVM::Value toLLVMConstantZero(
+    LLVM::Context LLContext, const ValType &ValType,
+    Span<const AST::CompositeType *const> CompositeTypes) noexcept {
   switch (ValType.getCode()) {
   case TypeCode::I32:
     return LLVM::Value::getConstNull(LLContext.getInt32Ty());
@@ -506,8 +508,21 @@ static LLVM::Value toLLVMConstantZero(LLVM::Context LLContext,
   case TypeCode::Ref:
   case TypeCode::RefNull: {
     std::array<uint8_t, 16> Data{};
-    const auto Raw = ValType.getRawData();
-    std::copy(Raw.begin(), Raw.end(), Data.begin());
+    if (ValType.isAbsHeapType()) {
+      // Abstract heap types are already fine for null refs.
+      const auto Raw = ValType.getRawData();
+      std::copy(Raw.begin(), Raw.end(), Data.begin());
+    } else {
+      // For non-abstract heap types (concrete type indices), convert to the
+      // abstract heap type so that ref.cast/ref.test won't dereference a null
+      // pointer when checking the type.
+      assuming(ValType.getTypeIndex() < CompositeTypes.size());
+      const auto *CompType = CompositeTypes[ValType.getTypeIndex()];
+      assuming(CompType != nullptr);
+      WasmEdge::ValType VType =
+          CompType->isFunc() ? TypeCode::NullFuncRef : TypeCode::NullRef;
+      std::copy_n(VType.getRawData().cbegin(), 8, Data.begin());
+    }
     return LLVM::Value::getConstVector8(LLContext, Data);
   }
   case TypeCode::V128:
@@ -557,7 +572,9 @@ class FunctionCompiler {
       for (const auto &Type : Locals) {
         LLVM::Type Ty = toLLVMType(LLContext, Type);
         LLVM::Value ArgPtr = Builder.createAlloca(Ty);
-        Builder.createStore(toLLVMConstantZero(LLContext, Type), ArgPtr);
+        Builder.createStore(
+            toLLVMConstantZero(LLContext, Type, Context.CompositeTypes),
+            ArgPtr);
         Local.emplace_back(Ty, ArgPtr);
       }
     }


Test patch:
diff --git a/test/executor/ExecutorRegressionTest.cpp b/test/executor/ExecutorRegressionTest.cpp
index 38bbd629bbcf..6a1f13c0ab0f 100644
--- a/test/executor/ExecutorRegressionTest.cpp
+++ b/test/executor/ExecutorRegressionTest.cpp
@@ -275,6 +275,93 @@ std::array<WasmEdge::Byte, 156> NullBrOnCastWasm{
     0x05, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x03, 0x16, 0x01, 0x01, 0x02, 0x00,
     0x05, 0x74, 0x61, 0x6b, 0x65, 0x6e, 0x01, 0x0a, 0x66, 0x61, 0x69, 0x6c,
     0x5f, 0x74, 0x61, 0x6b, 0x65, 0x6e, 0x04, 0x04, 0x01, 0x00, 0x01, 0x73};
+
+/// Binary Wasm module: ref.test on default-initialized local with concrete
+/// struct ref type (issue #4768).
+///
+/// (module
+///   (type $s (struct))
+///   (import "gc" "check" (func $check (param i32)))
+///   (func (export "test")
+///     (local (ref null $s))
+///     local.get 0  ref.test (ref null $s)  call $check
+///     local.get 0  ref.test eqref          call $check
+///     local.get 0  ref.test (ref $s)       call $check
+///   ))
+std::array<WasmEdge::Byte, 101> NullLocalStructWasm{
+    0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0a, 0x03, 0x5f,
+    0x00, 0x60, 0x01, 0x7f, 0x00, 0x60, 0x00, 0x00, 0x02, 0x0c, 0x01, 0x02,
+    0x67, 0x63, 0x05, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x00, 0x01, 0x03, 0x02,
+    0x01, 0x02, 0x07, 0x08, 0x01, 0x04, 0x74, 0x65, 0x73, 0x74, 0x00, 0x01,
+    0x0a, 0x1c, 0x01, 0x1a, 0x01, 0x01, 0x63, 0x00, 0x20, 0x00, 0xfb, 0x15,
+    0x00, 0x10, 0x00, 0x20, 0x00, 0xfb, 0x15, 0x6d, 0x10, 0x00, 0x20, 0x00,
+    0xfb, 0x14, 0x00, 0x10, 0x00, 0x0b, 0x00, 0x15, 0x04, 0x6e, 0x61, 0x6d,
+    0x65, 0x01, 0x08, 0x01, 0x00, 0x05, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x04,
+    0x04, 0x01, 0x00, 0x01, 0x73};
+
+/// Binary Wasm module: ref.test on default-initialized local with concrete
+/// func ref type (issue #4768).
+///
+/// (module
+///   (type $f (func))
+///   (import "gc" "check" (func $check (param i32)))
+///   (func (export "test")
+///     (local (ref null $f))
+///     local.get 0  ref.test (ref null func)  call $check
+///     local.get 0  ref.test (ref $f)         call $check
+///   ))
+std::array<WasmEdge::Byte, 92> NullLocalFuncWasm{
+    0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x60,
+    0x00, 0x00, 0x60, 0x01, 0x7f, 0x00, 0x02, 0x0c, 0x01, 0x02, 0x67, 0x63,
+    0x05, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x00, 0x01, 0x03, 0x02, 0x01, 0x00,
+    0x07, 0x08, 0x01, 0x04, 0x74, 0x65, 0x73, 0x74, 0x00, 0x01, 0x0a, 0x15,
+    0x01, 0x13, 0x01, 0x01, 0x63, 0x00, 0x20, 0x00, 0xfb, 0x15, 0x70, 0x10,
+    0x00, 0x20, 0x00, 0xfb, 0x14, 0x00, 0x10, 0x00, 0x0b, 0x00, 0x15, 0x04,
+    0x6e, 0x61, 0x6d, 0x65, 0x01, 0x08, 0x01, 0x00, 0x05, 0x63, 0x68, 0x65,
+    0x63, 0x6b, 0x04, 0x04, 0x01, 0x00, 0x01, 0x66};
+
+/// Binary Wasm module: ref.test on default-initialized local with concrete
+/// array ref type (issue #4768).
+///
+/// (module
+///   (type $a (array i32))
+///   (import "gc" "check" (func $check (param i32)))
+///   (func (export "test")
+///     (local (ref null $a))
+///     local.get 0  ref.test eqref    call $check
+///     local.get 0  ref.test (ref $a) call $check
+///   ))
+std::array<WasmEdge::Byte, 95> NullLocalArrayWasm{
+    0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0b, 0x03, 0x5e,
+    0x7f, 0x00, 0x60, 0x01, 0x7f, 0x00, 0x60, 0x00, 0x00, 0x02, 0x0c, 0x01,
+    0x02, 0x67, 0x63, 0x05, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x00, 0x01, 0x03,
+    0x02, 0x01, 0x02, 0x07, 0x08, 0x01, 0x04, 0x74, 0x65, 0x73, 0x74, 0x00,
+    0x01, 0x0a, 0x15, 0x01, 0x13, 0x01, 0x01, 0x63, 0x00, 0x20, 0x00, 0xfb,
+    0x15, 0x6d, 0x10, 0x00, 0x20, 0x00, 0xfb, 0x14, 0x00, 0x10, 0x00, 0x0b,
+    0x00, 0x15, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x01, 0x08, 0x01, 0x00, 0x05,
+    0x63, 0x68, 0x65, 0x63, 0x6b, 0x04, 0x04, 0x01, 0x00, 0x01, 0x61};
+
+/// Binary Wasm module: ref.test on null global with concrete struct ref type
+/// (issue #4768 — verifies globals are safe via ref.null init expression).
+///
+/// (module
+///   (type $s (struct))
+///   (import "gc" "check" (func $check (param i32)))
+///   (global $g (ref null $s) ref.null $s)
+///   (func (export "test")
+///     global.get $g  ref.test eqref    call $check
+///     global.get $g  ref.test (ref $s) call $check
+///   ))
+std::array<WasmEdge::Byte, 106> NullGlobalStructWasm{
+    0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0a, 0x03, 0x5f,
+    0x00, 0x60, 0x01, 0x7f, 0x00, 0x60, 0x00, 0x00, 0x02, 0x0c, 0x01, 0x02,
+    0x67, 0x63, 0x05, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x00, 0x01, 0x03, 0x02,
+    0x01, 0x02, 0x06, 0x07, 0x01, 0x63, 0x00, 0x00, 0xd0, 0x00, 0x0b, 0x07,
+    0x08, 0x01, 0x04, 0x74, 0x65, 0x73, 0x74, 0x00, 0x01, 0x0a, 0x12, 0x01,
+    0x10, 0x00, 0x23, 0x00, 0xfb, 0x15, 0x6d, 0x10, 0x00, 0x23, 0x00, 0xfb,
+    0x14, 0x00, 0x10, 0x00, 0x0b, 0x00, 0x1b, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+    0x01, 0x08, 0x01, 0x00, 0x05, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x04, 0x04,
+    0x01, 0x00, 0x01, 0x73, 0x07, 0x04, 0x01, 0x00, 0x01, 0x67};
 // clang-format on
 
 /// Regression test for ref.test on externalized nullable references.
@@ -616,6 +703,88 @@ TEST(ExecutorRegression, BrOnCastNullAbility) {
   EXPECT_EQ(Values[1], 1); // br_on_cast_fail taken for non-nullable mismatch
 }
 
+/// Regression test for ref.test on default-initialized locals with concrete
+/// ref types (issue #4768).
+///
+/// The bug: ValueFromType initialized reference-typed locals with
+/// RefVariant(Type), passing the raw concrete type (e.g. (ref null $s)).
+/// This created a null RefVariant whose ValType had HTCode = TypeIndex,
+/// violating the invariant that null references always carry abstract heap
+/// types. When ref.test later checked the type, it entered the
+/// !VT.isAbsHeapType() branch and dereferenced the null object pointer.
+///
+/// The fix normalizes concrete type indices to their abstract bottom types
+/// (NullFuncRef for func types, NullRef for struct/array types) when
+/// initializing locals in enterFunction.
+TEST(ExecutorRegression, NullLocalConcreteType) {
+  // --- Part 1: local (ref null $s) where $s is a struct type ---
+  {
+    Configure Conf;
+    VM::VM VM(Conf);
+    GCCheckModule GCMod;
+    VM.registerModule(GCMod);
+    ASSERT_TRUE(VM.loadWasm(NullLocalStructWasm));
+    ASSERT_TRUE(VM.validate());
+    ASSERT_TRUE(VM.instantiate());
+    VM.execute("test");
+    auto Values = GCMod.getValues();
+    ASSERT_EQ(Values.size(), 3);
+    EXPECT_EQ(Values[0], 1); // null matches nullable (ref null $s)
+    EXPECT_EQ(Values[1], 1); // null matches nullable eqref
+    EXPECT_EQ(Values[2], 0); // null doesn't match non-nullable (ref $s)
+  }
+
+  // --- Part 2: local (ref null $f) where $f is a func type ---
+  {
+    Configure Conf;
+    VM::VM VM(Conf);
+    GCCheckModule GCMod;
+    VM.registerModule(GCMod);
+    ASSERT_TRUE(VM.loadWasm(NullLocalFuncWasm));
+    ASSERT_TRUE(VM.validate());
+    ASSERT_TRUE(VM.instantiate());
+    VM.execute("test");
+    auto Values = GCMod.getValues();
+    ASSERT_EQ(Values.size(), 2);
+    EXPECT_EQ(Values[0], 1); // null matches nullable (ref null func)
+    EXPECT_EQ(Values[1], 0); // null doesn't match non-nullable (ref $f)
+  }
+
+  // --- Part 3: local (ref null $a) where $a is an array type ---
+  {
+    Configure Conf;
+    VM::VM VM(Conf);
+    GCCheckModule GCMod;
+    VM.registerModule(GCMod);
+    ASSERT_TRUE(VM.loadWasm(NullLocalArrayWasm));
+    ASSERT_TRUE(VM.validate());
+    ASSERT_TRUE(VM.instantiate());
+    VM.execute("test");
+    auto Values = GCMod.getValues();
+    ASSERT_EQ(Values.size(), 2);
+    EXPECT_EQ(Values[0], 1); // null matches nullable eqref
+    EXPECT_EQ(Values[1], 0); // null doesn't match non-nullable (ref $a)
+  }
+
+  // --- Part 4: global (ref null $s) initialized with ref.null $s ---
+  // Verifies the global init path is safe (ref.null already calls
+  // toBottomType).
+  {
+    Configure Conf;
+    VM::VM VM(Conf);
+    GCCheckModule GCMod;
+    VM.registerModule(GCMod);
+    ASSERT_TRUE(VM.loadWasm(NullGlobalStructWasm));
+    ASSERT_TRUE(VM.validate());
+    ASSERT_TRUE(VM.instantiate());
+    VM.execute("test");
+    auto Values = GCMod.getValues();
+    ASSERT_EQ(Values.size(), 2);
+    EXPECT_EQ(Values[0], 1); // null matches nullable eqref
+    EXPECT_EQ(Values[1], 0); // null doesn't match non-nullable (ref $s)
+  }
+}
+
 } // namespace
 
 GTEST_API_ int main(int argc, char **argv) {