Exploiting the Undefined: PWNing Firefox by Settling its Promises
Tao Yan (Palo Alto Networks), Edouard Bochin (Palo Alto Networks)
Hexacon 2025 · Day 1 · Main Stage
Overview
This talk, presented by Tao Yan and Edouard Bochin from Palo Alto Networks, delves into a sophisticated exploitation chain targeting a long-standing vulnerability in the Firefox JavaScript engine, SpiderMonkey. The researchers detail an out-of-bounds write bug present in the Promise.allSettled implementation since its inception in 2019, which they successfully leveraged to achieve arbitrary code execution at Pwn2Own Berlin. Their work highlights the often-overlooked complexity introduced by modern asynchronous programming constructs in JavaScript, turning what appears to be developer-friendly abstractions into ripe attack surface within browser engines.

Key moments
- 0:00 Introduction, speakers, and talk agenda
- 1:00 From Callback Hell to JavaScript Promises
- 2:00 Promises as a new attack surface and common vulnerability patterns
- 2:50 Revealing the Firefox Promise.allSettled out-of-bounds write bug
- 4:00 Deep dive into Promise.allSettled internals with custom promises
- 6:40 Detailed breakdown of internal promise resolution function initialization
Exploiting the Undefined: PWNing Firefox by Settling its Promises
Speakers: Tao Yan, Security Researcher, Palo Alto Networks; Edouard Bochin, Security Researcher, Palo Alto Networks
Conference: Hexacon
YouTube: https://www.youtube.com/watch?v=6RL3akTLHuQ
Overview
This talk, presented by Tao Yan and Edouard Bochin from Palo Alto Networks, delves into a sophisticated exploitation chain targeting a long-standing vulnerability in the Firefox JavaScript engine, SpiderMonkey. The researchers detail an out-of-bounds write bug present in the Promise.allSettled implementation since its inception in 2019, which they successfully leveraged to achieve arbitrary code execution at Pwn2Own Berlin. Their work highlights the often-overlooked complexity introduced by modern asynchronous programming constructs in JavaScript, turning what appears to be developer-friendly abstractions into ripe attack surface within browser engines.
The presentation provides a deep dive into the internal workings of SpiderMonkey's Promise handling, illustrating how a seemingly restrictive bug can be escalated through meticulous heap grooming, garbage collector manipulation, and type confusion. Yan and Bochin meticulously unpack the journey from a limited out-of-bounds write to a full renderer compromise, culminating in a demonstration of calculator execution on a vulnerable Firefox instance. This research serves as a critical reminder for browser developers about the challenges of implementing intricate web specifications and offers valuable insights for security researchers into the art of exploiting complex software vulnerabilities.
Background
▶ Watch: Introduction, speakers, and talk agenda (0:00)
The evolution of JavaScript asynchronous programming has seen a significant shift from traditional callbacks, which often led to unmanageable "callback hell," to more structured abstractions like Promises. Promises, introduced to mitigate the readability and maintainability issues of deeply nested callback code, offer a cleaner way to handle asynchronous operations. While beneficial for developers, this new abstraction introduced substantial complexity within JavaScript engines, inadvertently creating new attack surfaces for security researchers.
Previous vulnerabilities related to JavaScript Promises often shared common characteristics: they involved defining custom promise implementations, providing custom resolve and reject functions within their executor logic, and redefining the static resolve method. These custom implementations would then interact with a vulnerable API, such as Promise.any or Promise.allSettled, leading to exploitable conditions. The inherent challenge lies in the intricate interplay between user-defined JavaScript code and the low-level internal machinery of the browser's JavaScript engine. The Promise.allSettled API, in particular, takes an iterable of Promises and returns a Promise that resolves with an array of objects describing the outcome (fulfilled or rejected) of each input Promise. Its internal implementation in SpiderMonkey, Firefox's JavaScript engine, proved to be a fertile ground for the vulnerability discussed in this talk.
Key Findings
▶ Watch: Promises as a new attack surface and common vulnerability patterns (2:00)
The researchers' journey began by analyzing similar implementation issues across various browsers related to the JavaScript Promise specification. They identified recurring patterns in how these complex asynchronous constructs were mishandled, leading to vulnerabilities. These patterns typically fell into three categories: issues with the element resolution function (specifically, when and where fulfill and reject functions were called), problems with the remaining element count (incorrect incrementing or decrementing), and general difficulties in handling user-defined JavaScript as control transitioned between internal engine APIs and the JavaScript runtime.
Applying this analytical framework, Yan and Bochin uncovered a critical out-of-bounds write vulnerability within Firefox's Promise.allSettled implementation. The bug, a six-year-old flaw present since the API's initial integration into SpiderMonkey, stemmed from a race condition and a stale index issue. The core finding was that by carefully orchestrating the resolution of promises and manipulating the internal values array from JavaScript, they could trick the engine into performing a write operation outside the allocated bounds of the array.
Initial analysis revealed that the discovered out-of-bounds write was highly restrictive: it could only overwrite an undefined value, offered limited control over the object being written, and targeted memory on the nursery heap, making reliable exploitation challenging. However, through a deep understanding of SpiderMonkey's memory management, garbage collection mechanisms, and object allocation strategies, they demonstrated that these restrictions could be overcome. The ability to transform such a constrained primitive into a powerful arbitrary read/write and ultimately code execution was the paramount finding of their research.
Technical Deep Dive
▶ Watch: Revealing the Firefox Promise.allSettled out-of-bounds write bug (2:50)
The vulnerability lies within the intricate internal process of Promise.allSettled. When this API is invoked, SpiderMonkey initializes two key internal structures: the values array (a JavaScript array that will store the outcomes of the input promises) and a dataHolder object. The dataHolder stores crucial context, including a reference to the custom promise object, the remaining count (tracking unresolved promises), the values array itself, and a custom resolve function defined by the user.
For each promise in the iterable, the engine calls commonPerformPromiseCombinator, which in turn calls a custom Promise.resolve (if overridden) to obtain a denable object. This object contains a den function, which, when executed, will eventually invoke either the resolution function or rejection function for that promise. Critically, these resolution/rejection functions are internal JS functions that capture the dataHolder and the promise's index. The remaining count is incremented, and an undefined placeholder is initially pushed to the values array at the current index.
The core bug is triggered with a specific sequence:
- Setup: The exploit calls
Promise.allSettledwith 12 promises. For the first 11 promises, theirfulfillfunctions are called immediately. For the 12th promise, itsfulfillandrejectfunctions are captured and stored in JavaScript variables but not called. - Premature Resolution & Callback: After the 11th promise resolves, the
remaining countbecomes zero (because the 12th promise's resolution was deferred). This triggers a new code path, leading to the execution of thecustom resolvefunction (stored in thedataHolder), which is a JavaScript callback. This callback is passed thevaluesarray. - Heap Manipulation: Inside this
custom resolveJavaScript function, the exploit uses theshift()operator repeatedly to empty thevaluesarray. This action causes the underlying memory buffer of thevaluesarray to be trimmed and its size set to zero. - Stale Index & Out-of-Bounds: The exploit then calls the previously stored
rejectfunction for the 12th promise. This function still holds the originalindex(11, for the 12th promise) which is now stale, as thevaluesarray has been resized. ThepromiseAllSettledElementFunctionis invoked, attempting to accessvalues[11]. This results in an out-of-bound read. - Out-of-Bounds Write Primitive: If the value at this out-of-bounds location happens to be
undefined(which can be groomed for), the internal check passes. The engine then proceeds to write a newly created outcome object intovalues[11], resulting in a highly restricted out-of-bounds write. The value written is an internal object with astatusandvalueproperty, not directly controllable by JavaScript.
To escalate this limited primitive, Yan and Bochin leveraged deep knowledge of SpiderMonkey's memory management:
- Heap Layout & GC: SpiderMonkey uses a nursery heap for new, short-lived objects (managed by minor GC) and a tenure heap for longer-lived objects (managed by major GC). Objects on the nursery heap are allocated linearly.
- GC Triggering: Major GC can be triggered by allocating a large
ArrayBuffer(over 128MB). Minor GC is triggered by allocating many small JS objects (e.g.,JSArrays) to fill the nursery. - Victim Object & Grooming: They identified
JSClassobjects (referred to asobject C0) as suitable victims. These objects reside on the nursery heap, can have their properties set toundefined, and their size can be controlled by adjusting the number of properties (propertyCountEstimate). - Achieving Adjacency: After emptying the
valuesarray,object C0is allocated as the first element of the now-emptyvaluesarray. A major GC is then triggered. Crucially, if thevaluesarray is small, the GC can combine its internalvalueandvalue_elementinto aninline_valuearray. Due to the GC's depth-first allocation algorithm andobject C0sharing the same "kind" and having a controllable size,object C0is allocated immediately after theinline_valuesarray on the tenure heap, achieving adjacency. - OOB Write to UAF: The out-of-bounds write primitive is then triggered, overwriting a property of
object C0with the address of the internal outcome object (object_val). Next, a minor GC is triggered. Since the reference toobject_valwithinobject C0(which is now an out-of-bounds write) is not recognized by the garbage collector as a valid reference,object_valis freed. This leavesobject C0with a dangling pointer to freed memory, creating a use-after-free (UAF) condition. - UAF to Type Confusion: The freed
object_valmemory is then reclaimed by allocating a newJSArray. Now,object C0's corrupted property points to the data section of this newly allocatedJSArray, leading to a type confusion. - Arbitrary Read/Write Primitives:
- Nursery Address Leak: By triggering the OOB write again, but this time with a
BigUint64Arraygroomed to be adjacent, the address of anobject_val(which is a nursery address) is written intoBigUint64Array[0], allowing the leak of the nursery base address. - Shape Faking Bypass: To create a fake array for arbitrary read/write, one typically needs to fake its
shapeobject, which resides on the tenure heap and is hard to locate. The researchers discovered that SpiderMonkey'sgetElementfunction has checks that can be bypassed. By making a fake shape point to thevtableof aUint32Array(which resides in known memory regions) and ensuring certain conditions (likeopsproperty being zero), they could read array metadata without needing the actual shape address. - This led to a fake object primitive, where a
fake_arrayand aBigUint64Arraycould be made to point to the same memory region. This provides robust address-of and arbitrary read/write capabilities.
Finally, for code execution, they targeted WebAssembly (Wasm) RWX memory. Wasm functions are compiled into executable code in RWX memory. By encoding their shellcode as 64-bit floating-point numbers within a Wasm module, the Wasm compiler directly writes the shellcode into executable memory. Using the arbitrary read/write primitive, they could then locate the base address of the Wasm module's compiled code block (via Wasm instance -> Wasm code -> Wasm compiler tier code block) and corrupt it to point to their shellcode. When a Wasm function was subsequently called, the engine would jump to the corrupted function pointer, executing the shellcode.
Demo / Proof of Concept
▶ Watch: Deep dive into Promise.allSettled internals with custom promises (4:00)
The talk concluded with a live demonstration of the exploit, which was initially used at Pwn2Own Berlin. The target was Firefox version 138. The speakers noted that for the demo, they had to disable the Firefox sandbox, as the exploit was a renderer-only compromise.
During the demonstration, the researchers faced a common live demo challenge, with the exploit failing on the first few attempts. After clearing the browser cache and history, they successfully executed the exploit. The proof of concept involved navigating to a malicious webpage, which then triggered the complex chain of vulnerabilities. Upon successful exploitation, a calculator application (calc.exe on Windows) was launched, demonstrating arbitrary code execution within the context of the Firefox renderer process. The successful pop-up of the calculator confirmed the full compromise of the browser.
Defensive Implications
▶ Watch: Detailed breakdown of internal promise resolution function initialization (6:40)
This research carries several crucial defensive implications for browser developers and security professionals:
- Complexity of Specifications: Modern JavaScript specifications, particularly those involving asynchronous programming and intricate state management like Promises, introduce immense complexity. Developers must exercise extreme caution when implementing these features, especially where user-defined JavaScript can influence internal engine state or memory layouts.
- Robustness against GC Interaction: The exploit heavily relies on triggering and manipulating the garbage collector at precise moments. Browser engines need more robust mechanisms to ensure that internal pointers and array metadata are consistently updated, even when JavaScript-driven memory manipulations or GC events occur, preventing use-after-free and type confusion scenarios.
- Array Boundary Checks: The initial out-of-bounds write highlights the need for rigorous and pervasive boundary checks for array accesses, especially in code paths that interact with dynamically sized or user-controlled arrays. Stale indices must be a particular focus.
- Memory Layout Randomization: While not explicitly discussed as a defense, improved memory layout randomization and stricter memory segmentation could make heap grooming techniques more challenging, hindering the ability to reliably place victim objects adjacent to vulnerable structures.
- Hardening against Information Leaks: The ability to leak the nursery base address was critical for further exploitation. Defenses should focus on preventing such information leaks that can bypass ASLR and facilitate arbitrary read/write primitives.
- WebAssembly Security: Although WebAssembly provides a sandboxed environment, its RWX memory remains a potent target for code execution once an arbitrary write primitive is achieved. Continued hardening of Wasm compilation and execution environments is essential.
- Continuous Vulnerability Research: The fact that this was a six-year-old bug underscores the importance of continuous, deep-dive vulnerability research, even into seemingly mature or low-impact components. Developers are prone to making similar mistakes across different implementations of complex specifications.
Key Takeaways
- Asynchronous JavaScript features, while beneficial for developers, introduce significant attack surface due to their intricate internal implementations within browser engines.
- Analyzing similar implementation issues across different applications or specifications can reveal common developer mistakes and aid in discovering new vulnerabilities.
- Seemingly "unexploitable" bugs with highly restrictive primitives can often be escalated to full compromise with a deep understanding of engine internals, heap layout, and garbage collector behavior.
- SpiderMonkey's unique memory management features (e.g., nursery/tenure heaps, specific object allocation, GC behavior) can be leveraged for powerful exploitation primitives like UAF and type confusion.
- The exploitation chain from a limited out-of-bounds write to use-after-free, then type confusion, and finally arbitrary read/write, is a common and effective strategy in browser exploitation.
- WebAssembly's RWX memory provides a reliable and accessible target for code execution once arbitrary write primitives are established, often by corrupting internal pointers to Wasm code blocks.
About the Speaker(s)
Tao Yan and Edouard Bochin are security researchers at Palo Alto Networks. Their work primarily focuses on vulnerability research, encompassing both offensive and defensive aspects of cybersecurity. They are active participants in prominent hacking events such as Pwn2Own, where they frequently showcase their expertise by exploiting cutting-edge vulnerabilities in widely used software. Both researchers also regularly speak at security conferences, sharing their findings and contributing to the broader security community's understanding of complex exploitation techniques.
Reviews
Dr. Zero (Offensive Security Researcher) — STRONG ACCEPT
Yan and Bochin present a legitimate, well-executed browser exploitation chain rooted in original Pwn2Own work. The research demonstrates real technical depth — taking a six-year-old, highly constrained OOB write in SpiderMonkey's Promise.allSettled implementation and escalating it through GC manipulation, UAF, type confusion, and finally Wasm RWX code execution. This is the kind of talk that separates people who actually exploit browsers from people who write blog posts about people who exploit browsers. Minor reservations around presentation polish and the demo hiccups, but the underlying work is solid and the content will meaningfully advance the audience's understanding of browser…
Heather Calloway (CISO) — PASS
Technically sophisticated Pwn2Own exploit research demonstrating a full browser compromise chain in Firefox's SpiderMonkey engine. This is credible, serious work — but it operates entirely within the domain of offensive exploit construction. There is no governance angle, no institutional accountability framing, no defender decision path, and no operator relevance. The 'defensive implications' section is boilerplate that any competent browser security team already knows. This is not a failure of the research — it is a scope judgment. Route to browser engine researchers and Pwn2Own competitors. Not my audience.