CVE-2025-43432
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Hossein Lotfi (@hosselot) of Trend Micro Zero Day Initiative
Key insight. The wasm FunctionParser aliased its signature TypeDefinition by bare reference instead of retaining it, so the object could be freed mid-parse; holding a Ref<const TypeDefinition> restores lifetime safety.
Patch Details
diff --git a/Source/JavaScriptCore/wasm/WasmFunctionParser.h b/Source/JavaScriptCore/wasm/WasmFunctionParser.h
index 574b54ef4bfe..2d228035afb0 100644
--- a/Source/JavaScriptCore/wasm/WasmFunctionParser.h
+++ b/Source/JavaScriptCore/wasm/WasmFunctionParser.h
@@ -377,7 +377,7 @@ private:
Stack m_expressionStack;
ControlStack m_controlStack;
Vector<Type, 16> m_locals;
- const TypeDefinition& m_signature;
+ Ref<const TypeDefinition> m_signature;
const ModuleInformation& m_info;
Vector<uint32_t> m_localInitStack;
@@ -432,8 +432,8 @@ auto FunctionParser<Context>::parse() -> Result
{
uint32_t localGroupsCount;
- WASM_PARSER_FAIL_IF(!m_signature.is<FunctionSignature>(), "type signature was not a function signature"_s);
- const auto& signature = *m_signature.as<FunctionSignature>();
+ WASM_PARSER_FAIL_IF(!m_signature->template is<FunctionSignature>(), "type signature was not a function signature"_s);
+ const auto& signature = *m_signature->template as<FunctionSignature>();
if (signature.numVectors() || signature.numReturnVectors())
m_context.notifyFunctionUsesSIMD();
@@ -485,8 +485,8 @@ auto FunctionParser<Context>::parse() -> Result
template<typename Context>
auto FunctionParser<Context>::parseConstantExpression() -> Result
{
- WASM_PARSER_FAIL_IF(!m_signature.is<FunctionSignature>(), "type signature was not a function signature"_s);
- const auto& signature = *m_signature.as<FunctionSignature>();
+ WASM_PARSER_FAIL_IF(!m_signature->template is<FunctionSignature>(), "type signature was not a function signature"_s);
+ const auto& signature = *m_signature->template as<FunctionSignature>();
if (signature.numVectors() || signature.numReturnVectors())
m_context.notifyFunctionUsesSIMD();
@@ -500,7 +500,7 @@ auto FunctionParser<Context>::parseConstantExpression() -> Result
template<typename Context>
auto FunctionParser<Context>::parseBody() -> PartialResult
{
- m_controlStack.append({ { }, { }, 0, m_context.addTopLevel({ m_signature.as<FunctionSignature>(), nullptr }) });
+ m_controlStack.append({ { }, { }, 0, m_context.addTopLevel({ m_signature->template as<FunctionSignature>(), nullptr }) });
uint8_t op = 0;
while (m_controlStack.size()) {
m_currentOpcodeStartingOffset = m_offset;
@@ -540,7 +540,7 @@ auto FunctionParser<Context>::parseBody() -> PartialResult
WASM_FAIL_IF_HELPER_FAILS(parseExpression());
m_context.didParseOpcode();
}
- WASM_FAIL_IF_HELPER_FAILS(m_context.endTopLevel({ m_signature.as<FunctionSignature>(), nullptr }, m_expressionStack));
+ WASM_FAIL_IF_HELPER_FAILS(m_context.endTopLevel({ m_signature->template as<FunctionSignature>(), nullptr }, m_expressionStack));
if (Context::validateFunctionBodySize)
WASM_PARSER_FAIL_IF(m_offset != source().size(), "function body size doesn't match the expected size");
@@ -3067,7 +3067,7 @@ FOR_EACH_WASM_MEMORY_STORE_OP(CREATE_CASE)
if (m_currentOpcode == TailCall) {
- const auto& callerSignature = *m_signature.as<FunctionSignature>();
+ const auto& callerSignature = *m_signature->template as<FunctionSignature>();
WASM_PARSER_FAIL_IF(calleeSignature.returnCount() != callerSignature.returnCount(), "tail call function index "_s, functionIndex, " with return count "_s, calleeSignature.returnCount(), ", but the caller's signature has "_s, callerSignature.returnCount(), " return values"_s);
@@ -3135,7 +3135,7 @@ FOR_EACH_WASM_MEMORY_STORE_OP(CREATE_CASE)
if (m_currentOpcode == TailCallIndirect) {
- const auto& callerSignature = *m_signature.as<FunctionSignature>();
+ const auto& callerSignature = *m_signature->template as<FunctionSignature>();
WASM_PARSER_FAIL_IF(calleeSignature.returnCount() != callerSignature.returnCount(), "tail call indirect function with return count "_s, calleeSignature.returnCount(), "_s, but the caller's signature has "_s, callerSignature.returnCount(), " return values"_s);
@@ -3200,7 +3200,7 @@ FOR_EACH_WASM_MEMORY_STORE_OP(CREATE_CASE)
ResultList results;
if (m_currentOpcode == TailCallRef) {
- const auto& callerSignature = *m_signature.as<FunctionSignature>();
+ const auto& callerSignature = *m_signature->template as<FunctionSignature>();
WASM_PARSER_FAIL_IF(calleeSignature.returnCount() != callerSignature.returnCount(), "tail call indirect function with return count "_s, calleeSignature.returnCount(), "_s, but the caller's signature has "_s, callerSignature.returnCount(), " return values"_s);
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-43432 |
| Bugzilla | 299313 |
| Component | JSC |
| Bug Class | UAF |
| Severity | medium |
| Commit | d329e095fc8c1874… |
| Advisory | Apple Advisory |
Terminology
- Wasm FunctionParser — The JSC component that validates/parses a WebAssembly function body against its declared signature.
- TypeDefinition / FunctionSignature — The interned description of a wasm type; a function’s signature is a FunctionSignature view of a TypeDefinition.
- Bare reference vs Ref<> — A C++
const T&member aliases without owning, so it dangles if the referent is freed; Ref<> holds a counted strong reference. - Object lifetime / retention — Holding a strong reference guarantees the object stays alive for the holder’s lifetime.
Root Cause Analysis
JavaScriptCore’s WebAssembly FunctionParser stored the function’s type as const TypeDefinition& m_signature — a bare C++ reference that does not retain the referenced object. The parser uses m_signature throughout function/constant-expression parsing (m_signature.as<FunctionSignature>() in parse, parseConstantExpression, parseBody, and the TailCall/TailCallIndirect/TailCallRef paths). The invariant that made the bare reference ‘safe’ was that the TypeDefinition outlives the parser, but that does not hold in all cases: the TypeDefinition can be released while the parser is still running (e.g. during recursive/streaming parsing where the owning type registry or module information drops the last other reference), leaving m_signature dangling. Any later dereference is a use-after-free. The fix changes the member to Ref<const TypeDefinition> m_signature, so the parser holds its own strong reference for its entire lifetime, and updates every use to m_signature->template is/as<FunctionSignature>(). The restored invariant is that the parser owns a reference to the signature TypeDefinition it parses against, so the object cannot be freed underneath it. This is the same ‘retain the wasm TypeDefinition, don’t alias it’ class as the GC type-retention fixes, but here on the parser’s input signature.
Attack Path
1. Deliver a crafted module
Serve a WebAssembly module whose function signatures reference type definitions arranged so a signature’s other references are dropped during parsing.
2. Parse the function body
FunctionParser holds m_signature as a bare reference to the function’s TypeDefinition while it parses the body / constant expressions.
3. Free the signature
During parsing the TypeDefinition is released (last owning reference gone), leaving m_signature dangling.
4. Use-after-free
A subsequent m_signature.as<FunctionSignature>() (e.g. on a tail-call opcode) dereferences freed memory — crash or memory corruption in WebContent.
Impact Assessment
A use-after-free of the wasm function-signature TypeDefinition reachable while parsing a crafted module in the WebContent process. Triggering it requires arranging for the signature to be released mid-parse, and the immediate effect is a crash, though a reclaimed TypeDefinition could enable type confusion in the parser. Confined to WebContent; rated medium.
Audit Directions
Other bare TypeDefinition references
grep JSC wasm/ for const TypeDefinition& / TypeDefinition* members and long-lived locals that don’t retain via Ref/RefPtr.
Parser-held aliases
Audit FunctionParser and related wasm parsers/validators for other members aliasing module-owned data (ModuleInformation, signatures) across parsing.
Ref-vs-reference members
Look for & members initialized from getRef()/registry lookups elsewhere that assume the referent outlives the holder.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
FunctionParser<Context>::m_signature |
Source/JavaScriptCore/wasm/WasmFunctionParser.h |
modified | Type changed from const TypeDefinition& to Ref<const TypeDefinition> so the parser retains its signature for its whole lifetime. |
FunctionParser<Context>::parse / parseConstantExpression / parseBody / tail-call handlers |
Source/JavaScriptCore/wasm/WasmFunctionParser.h |
modified | Updated all uses to m_signature->template is/as<FunctionSignature>() to match the Ref member. |
Files Changed
Source Files
Source/JavaScriptCore/wasm/WasmFunctionParser.h