#14381·graal

[GR-79472] `struct.new_default` leaves `v128` fields uninitialized (NullPointerException on first read)

Author: khagankhanCreated Sep 4, 2026Updated Sep 8, 2026
Labelsbuggraalwasm

Describe the issue

struct.new_default does not materialize the default value for v128 fields. The field is left as a Java null instead of the zero vector, and the first struct.get of that field throws:

java.lang.NullPointerException: Cannot invoke "org.graalvm.wasm.vector.Vector128.getBytes()" because "vector128" is null

This reaches the embedder as an internal error rather than as a guest value.

The module is valid and GraalWasm validates it. v128 is a defaultable type, so struct.new_default on a struct with a v128 field is legal and must produce a struct whose field reads back as i32x4 0 0 0 0.

Two observations suggest a single missed code path rather than a general gap:

  • array.new_default is not affected
  • The null is latent. It faults only when the field is actually read, so a struct that is default-created and then overwritten, or whose v128 field is never touched, behaves correctly. An invalid-by-construction struct can therefore be carried arbitrarily far from its allocation site before anything surfaces.

The module under test:

wat
(module
  (type $s (struct (field (mut v128))))
  (func (export "run") (result i32)
    struct.new_default $s
    struct.get $s 0
    i32x4.extract_lane 0))

Expected: run() returns 0. Actual: NullPointerException.

For comparison, Wasmtime 49.0.0-dev (wasmtime run --invoke run -W gc,function-references,simd) returns 0 on the same bytes, and wasm-tools validate --features all accepts the module.

Steps to reproduce the issue

GraalWasm ships no standalone wasm launcher, so the reproducer drives the polyglot API with the module embedded as base64. It needs no wat2wasm/wasm-tools, no build system, and only three jars. Requires a JDK >= 21.

  1. Fetch the jars:
    bash
    mkdir repro && cd repro && mkdir lib
    V=25.3.4.1 && B=https://repo1.maven.org/maven2/org/graalvm
    curl -sSL -o lib/polyglot.jar      "$B/polyglot/polyglot/$V/polyglot-$V.jar"
    curl -sSL -o lib/truffle-api.jar   "$B/truffle/truffle-api/$V/truffle-api-$V.jar"
    curl -sSL -o lib/wasm-language.jar "$B/wasm/wasm-language/$V/wasm-language-$V.jar"
  2. Save Repro.java (below) into that directory.
  3. Run it — no separate compile step is needed:
    bash
    java -cp 'lib/*' Repro.java

truffle-runtime/truffle-compiler are deliberately absent, so this runs on the plain Truffle interpreter with no optimizing runtime. The bug reproduces there, so it is not a compiler or partial-evaluation issue.

Repro.java
java
import java.util.Base64;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.io.ByteSequence;

/**
 * GraalWasm: struct.new_default leaves a v128 field null.
 *
 * The embedded module is the assembled form of:
 *
 *   (module
 *     (type $s (struct (field (mut v128))))
 *     (func (export "run") (result i32)
 *       struct.new_default $s
 *       struct.get $s 0
 *       i32x4.extract_lane 0))
 *
 * Per the Wasm GC spec the default value of a v128 field is 0, so run()
 * must return 0. GraalWasm throws a NullPointerException instead.
 *
 * Run with the three jars from Maven Central (JDK >= 21):
 *   java -cp 'lib/*' Repro.java
 */
public final class Repro {

    // wat2wasm / `wasm-tools parse` output for the module above.
    private static final String MODULE_BASE64 =
            "AGFzbQEAAAABCQJfAXsBYAABfwMCAQEHBwEDcnVuAAAKDgEMAPsBAPsCAAD9GwALAAsEbmFtZQQEAQABcw==";

    public static void main(String[] args) {
        byte[] wasm = Base64.getDecoder().decode(MODULE_BASE64);

        try (Context ctx = Context.newBuilder("wasm")
                .allowExperimentalOptions(true)
                .option("wasm.GC", "true")
                .option("wasm.TypedFunctionReferences", "true")
                .build()) {

            Source src = Source.newBuilder("wasm", ByteSequence.create(wasm), "repro").build();
            Value exports = ctx.eval(src).newInstance().getMember("exports");

            int result = exports.invokeMember("run").asInt();
            System.out.println("run() returned " + result + " (expected 0)");
        } catch (java.io.IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Describe GraalVM and your environment:

  • GraalVM version: CE 25.3.4.1 — also reproduced on CE 25.2.4 and CE 25.1.3.
  • JDK major version: 25
  • OS: Ubuntu 22.04.5 LTS
  • Architecture: AMD64

Output of java -Xinternalversion:

OpenJDK 64-Bit Server VM (25.0.4.1+1-jvmci-25.3-b22) for linux-amd64 JRE (25.0.4.1+1-jvmci-25.3-b22), built on 2026-08-19T00:46:28Z with gcc 14.2.0

More details

Exception in thread "main" org.graalvm.polyglot.PolyglotException: java.lang.NullPointerException: Cannot invoke "org.graalvm.wasm.vector.Vector128.getBytes()" because "vector128" is null
        at org.graalvm.wasm.vector.Vector128OpsFallback.fromVector128(Vector128OpsFallback.java:1501)
        at org.graalvm.wasm.vector.Vector128OpsFallback.fromVector128(Vector128OpsFallback.java:66)
        at org.graalvm.wasm.nodes.WasmFunctionNode.executeAggregate(WasmFunctionNode.java:4943)
        at org.graalvm.wasm.nodes.WasmFunctionNode.aggregateHandler(WasmFunctionNode.java:4519)
        at org.graalvm.wasm.nodes.WasmFunctionNode.executeBodyFromOffset(WasmFunctionNode.java:1411)
        at org.graalvm.wasm.nodes.WasmFunctionNode.execute(WasmFunctionNode.java:294)
        at org.graalvm.wasm.nodes.WasmInstrumentableFunctionNode.execute(WasmInstrumentableFunctionNode.java:138)
        at org.graalvm.wasm.nodes.WasmFunctionBaseNode.execute(WasmFunctionBaseNode.java:61)
        at org.graalvm.wasm.nodes.WasmFixedMemoryImplFunctionNode.doDispatched(WasmFixedMemoryImplFunctionNode.java:104)
        at org.graalvm.wasm.nodes.WasmFunctionRootNode.executeWithInstance(WasmFunctionRootNode.java:152)
        at org.graalvm.wasm.nodes.WasmRootNode.execute(WasmRootNode.java:145)

Additional cases tried, all on 25.3.4.1:

case result
struct.new_default, (field (mut v128)) NPE
struct.new_default, (field v128) (immutable) NPE
struct.new_default, v128 as 2nd field NPE
struct.new_default, mixed struct, read only the i32 field ok
struct.new_default then struct.set before any read ok
struct.new with an explicit v128 operand ok
array.new_default + array.get ok, reads back as zero
array.new with an explicit v128 operand ok
global (mut v128) ok

ACK:

Found by differential fuzzing of Wasm GC modules across engines, reduced to the three-instruction case above. This section will be updated