Skip to content

Make WASM Function Signature Support ref concrete heap type #525

Description

@chennbnbnb

Hello,

I recently attempted to add support for custom concrete heap types in WebAssembly function signatures but discovered that Fuzzilli currently does not support this, leading to a Use-After-Free (UAF) crash. Below is a test case that reproduces the issue:

func testWasmOperationWithWasmSignatureUAFBug() throws {

        let runner = try GetJavaScriptExecutorOrSkipTest()
        let liveTestConfig = Configuration(logLevel: .error, enableInspection: true)
        let fuzzer = makeMockFuzzer(config: liveTestConfig, environment: JavaScriptEnvironment())
        let b = fuzzer.makeBuilder()

        let types = b.wasmDefineTypeGroup {
            let structType = b.wasmDefineStructType(fields: [WasmStructTypeDescription.Field(type: .wasmi32, mutability: true)], indexTypes: [])
            let structType2 = b.wasmDefineStructType(fields: [WasmStructTypeDescription.Field(type: ILType.wasmRef(.Index(), nullability: true), mutability: true)], indexTypes: [structType])
            return [structType, structType2]
        }
        let structType2 = types[1]

        let refStrcutType = b.type(of: structType2).wasmTypeDefinition!.getReferenceTypeTo(nullability: true)

        b.buildWasmModule { m in
            m.addWasmFunction(with: [] => [refStrcutType]) { f, _, _ in
                let structInstance = f.wasmStructNewDefault(structType: structType2)
                return [structInstance]
            }
        }


        let prog = b.finalize()
        let jsProg = fuzler.lifter.lift(prog)
        let result = try runner.execute(jsProg)
    }

Running this test causes Fuzzilli to crash with a UAF error:

Fatal error: Attempted to read an unowned reference but object 0x55555764efe0 was already deallocated

Root Cause Analysis

The issue occurs because for values of type ref concrete heap type, the WasmReferenceType does not directly hold a strong reference. Instead, it holds an unowned weak reference via UnownedWasmTypeDescription, while the JSTyper is the actual owner of the WasmStructTypeDescription reference.

struct UnownedWasmTypeDescription : Hashable {
    private unowned var description: WasmTypeDescription?

    init(_ description: WasmTypeDescription? = nil) {
        self.description = description
    }

    func get() -> WasmTypeDescription? {
        return description
    }
}

The getReferenceTypeTo method creates an ILType object that also holds a weak reference to the WasmStructTypeDescription object. Subsequently, addWasmFunction() places this ILType into a WasmSignature object.

public func addWasmFunction(with signature: WasmSignature, _ body: (WasmFunction, Variable, [Variable]) -> [Variable]) -> Variable {
            let instr = b.emit(BeginWasmFunction(signature: signature))
            let results = body(currentWasmFunction, instr.innerOutput(0), Array(instr.innerOutputs(1...)))
            return b.emit(EndWasmFunction(signature: signature), withInputs: results).output
        }

The WasmSignature is itself a property of various WasmOperation classes (like BeginWasmFunction).

final class BeginWasmFunction: WasmOperation {
    override var opcode: Opcode { .beginWasmFunction(self) }
    public let signature: WasmSignature

    init(signature: WasmSignature) {
        self.signature = signature
        super.init(numInnerOutputs: 1 + signature.parameterTypes.count, attributes: [.isBlockStart], requiredContext: [.wasm], contextOpened: [.wasmFunction])
    }
}

Consequently, while the WasmStructTypeDescription object is logically owned by the JSTyper, the BeginWasmFunction operation also maintains a weak reference to it. When the ProgramBuilder finishes and the JSTyper is deallocated, the WasmStructTypeDescription object is also freed. However, during WasmLifting, the BeginWasmFunction operation attempts to access the ILType objects within its WasmSignature, triggering the UAF crash.

Summary: The UAF occurs because the WasmStructTypeDescription object is leaked into WasmOperation objects which have a longer lifecycle than the JSTyper.

This bug prevents function signatures from supporting concrete heap types and blocks support for additional instructions like ref.func.

Proposed Solution & Design Discussion

I believe the core issue is that WasmOperation objects directly reference ILType objects. This violates the intended FuzzIL design, where ILType should be the result of the JSTyper's analysis of the FuzzIL code, not a property stored within a WasmOperation. Therefore, WasmOperation should not hold references to ILType.

Proposed Improvement:

The solution is to support defining function signatures within a Rec Group, similar to how WasmStructTypeDescription and WasmArrayTypeDescription currently work. This would involve introducing a new WasmFuncSigDescription object to describe function signatures.

Benefits of this approach:

  1. Better aligns with the WASM GC Proposal, where struct, array, and func should be parallel types. The func type shouldn't be treated as a special case.
  2. Allows representing a WASM signature via a Variable.
  3. Paves the way for future support of instructions like ref.func and call_ref.
  4. Removes the need for WasmSignature fields in WasmOperation classes. Instead, a new Input Variable can be used to represent the function signature.

Example refactor for BeginWasmFunction:

final class BeginWasmFunction: WasmOperation {
    override var opcode: Opcode { .beginWasmFunction(self) }

    init() {
        // The signature is now an input Variable, not a stored property
        super.init(numInputs: 1, numInnerOutputs: 1 + signature.parameterTypes.count, attributes: [.isBlockStart], requiredContext: [.wasm], contextOpened: [.wasmFunction])
    }
}

The primary role of WasmSignature during lifting (generating the correct typeidx) can be fulfilled by querying the JSTyper for the type information associated with the input Variable, as all necessary type information is already embedded within the FuzzIL code.

Questions for the Maintainers

Before proceeding with the changes, I would like to understand your opinions on this matter.

  1. What are your thoughts on this proposed change?
  2. Are you already working on this issue?
  3. If I proceed with implementing this change, would you be open to accepting a Pull Request (PR) for it?

I look forward to your feedback and am willing to contribute the necessary implementation work.

Activity

  1. Liedtke commented on Sep 8, 2025

    @Liedtke
    Collaborator

    Hi,

    Thanks for opening this issue and looking into this.
    Let me start with answering your main questions:

    1.What are your thoughts on this proposed change?
    2.Are you already working on this issue?
    3. If I proceed with implementing this change, would you be open to accepting a Pull Request (PR) for it?

    1. This is very close to my views on this. The signature ILType should move out of the BeginWasmFunction, WasmBeginBlock, WasmBeginIf and all the other blocks that have an input signature and similarly for the operations closing the block. On top of that, there are more instructions that would need to be updated, like WasmDefineTag, WasmDefineGlobal, ... Instead, we'll have a WasmDefineSignatureType operation to define signatures inside recursive type groups which would then be passed as an input into the listed instructions.
    2. Yes, kind of. This is the next thing I'm going to look into once I have time to continue working on the wasm-gc implementation. (Until a few weeks ago (38b76d7) we didn't even have abstract heap types in signatures, yet.)
    3. Due to the size of this feature, it being very intrusive and having large implications, this might not be very suitable for an external contribution. There are some design constraints that we'll need to be careful about and we're currently performing some large-scale refactoring as Fuzzilli's current code generation doesn't work well for wasm-gc in general. 12

    A few more in-depth responses to your issue description:

    I believe the core issue is that WasmOperation objects directly reference ILType objects. This violates the intended FuzzIL design, where ILType should be the result of the JSTyper's analysis of the FuzzIL code, not a property stored within a WasmOperation. Therefore, WasmOperation should not hold references to ILType.

    This isn't a simple yes/no question. There are things that just fundamentally need some static type annotations, e.g. the WasmDefineGlobal needs to be able to annotate that the global should be an i32 or an f64. If we didn't use ILType for that, we'd need a separate enum or we'd need a WasmDefineGlobalI32 etc., which wouldn't scale. The separate enum would be a "cleaner" solution but it would require extra conversions. So instead we have some ILTypes flowing into wasm operations however, only for "built-in types", i.e. anything that is not "user"-defined / an index type in wasm-gc. Index-types (as you point out as well), require additional inputs, they depend on states in the IL program.

    Better aligns with the WASM GC Proposal, where struct, array, and func should be parallel types. The func type shouldn't be treated as a special case.

    Yes, we'll need a WasmDefineSignatureType operation and the corresponding extensions to the ILType mechanism. The main reason why this is inconsistent right now is that array and struct types are simpler because they didn't interfere with the existing operations for functions and blocks and the functions and blocks simply weren't built with wasm-gc in mind (as the initial goal was to cover wasm MVP and add other proposals later on.)

    So unfortunately, Fuzzilli is still in an intermediate stage where a lot of wasm-gc support is missing and a lot of that is blocked on having the proper signature support. Similarly, we don't have inheritance of any wasm-gc-type-definitions and we are still missing a large amount of operations (e.g. casts). On top of that, there are also other proposals that depend on wasm-gc like string-builtins, custom descriptors, ..., that therefore are also not covered by Fuzzilli, yet.

    I look forward to your feedback and am willing to contribute the necessary implementation work.

    I hope this answers most of the raised questions. As described, due to the scope and relevance for our progress on wasm-gc, currently this doesn't seem to be a great issue for an external contribution. If you're in general interested in contributing, I can see if I can (a) document a few more of the outstanding tasks in our issue-tracker and (b) see if we can make these issues publicly visible (right now most of the Fuzzilli project work tracking issues are marked as non-public.)

    Footnotes

    1. The main issue being input constraints: If the BeginWasmFunction simply requires a signature type definition, we can't create this ad-hoc: When we are in the .wasm context, we can't emit a recursive type group definition (which we embed into the .javascript context for better sharing between modules.) I'm currently leaning towards having a signature type definition statement in the .wasm context that is then not emedded into a recursive type group. But we'll have the same issue then in the .wasmfunction context, so we'd prefer having that operation available in both of these contexts which also isn't a supported feature for operations as of now as far as I'm aware of. ↩

    2. If you run Fuzzilli with default weights (or increased weights only for e.g. WasmStructSetGenerator, it gets obvious: We will almost never emit a struct.set successfully because it is very unlikely to have both a struct and a matching value to store, the struct being the more difficult part as it requires to have (1) a typegroup, (2) a WasmDefineStructType inside and (3) an operation that instantiates a struct (right now only WasmStructNewDefault). As said, this is hopefully going to be addressed soon but it's still WIP. ↩

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions