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.

Watch on YouTube · Slides

Visual summary for Exploiting the Undefined: PWNing Firefox by Settling its Promises by Tao Yan, Edouard Bochin
Visual summary for Exploiting the Undefined: PWNing Firefox by Settling its Promises by Tao Yan, Edouard Bochin

Key moments

  1. 0:00 Introduction, speakers, and talk agenda
  2. 1:00 From Callback Hell to JavaScript Promises
  3. 2:00 Promises as a new attack surface and common vulnerability patterns
  4. 2:50 Revealing the Firefox Promise.allSettled out-of-bounds write bug
  5. 4:00 Deep dive into Promise.allSettled internals with custom promises
  6. 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:

  1. Setup: The exploit calls Promise.allSettled with 12 promises. For the first 11 promises, their fulfill functions are called immediately. For the 12th promise, its fulfill and reject functions are captured and stored in JavaScript variables but not called.
  2. Premature Resolution & Callback: After the 11th promise resolves, the remaining count becomes zero (because the 12th promise's resolution was deferred). This triggers a new code path, leading to the execution of the custom resolve function (stored in the dataHolder), which is a JavaScript callback. This callback is passed the values array.
  3. Heap Manipulation: Inside this custom resolve JavaScript function, the exploit uses the shift() operator repeatedly to empty the values array. This action causes the underlying memory buffer of the values array to be trimmed and its size set to zero.
  4. Stale Index & Out-of-Bounds: The exploit then calls the previously stored reject function for the 12th promise. This function still holds the original index (11, for the 12th promise) which is now stale, as the values array has been resized. The promiseAllSettledElementFunction is invoked, attempting to access values[11]. This results in an out-of-bound read.
  5. 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 into values[11], resulting in a highly restricted out-of-bounds write. The value written is an internal object with a status and value property, 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 JSClass objects (referred to as object C0) as suitable victims. These objects reside on the nursery heap, can have their properties set to undefined, and their size can be controlled by adjusting the number of properties (propertyCountEstimate).
  • Achieving Adjacency: After emptying the values array, object C0 is allocated as the first element of the now-empty values array. A major GC is then triggered. Crucially, if the values array is small, the GC can combine its internal value and value_element into an inline_value array. Due to the GC's depth-first allocation algorithm and object C0 sharing the same "kind" and having a controllable size, object C0 is allocated immediately after the inline_values array on the tenure heap, achieving adjacency.
  • OOB Write to UAF: The out-of-bounds write primitive is then triggered, overwriting a property of object C0 with the address of the internal outcome object (object_val). Next, a minor GC is triggered. Since the reference to object_val within object C0 (which is now an out-of-bounds write) is not recognized by the garbage collector as a valid reference, object_val is freed. This leaves object C0 with a dangling pointer to freed memory, creating a use-after-free (UAF) condition.
  • UAF to Type Confusion: The freed object_val memory is then reclaimed by allocating a new JSArray. Now, object C0's corrupted property points to the data section of this newly allocated JSArray, leading to a type confusion.
  • Arbitrary Read/Write Primitives:
  • Nursery Address Leak: By triggering the OOB write again, but this time with a BigUint64Array groomed to be adjacent, the address of an object_val (which is a nursery address) is written into BigUint64Array[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 shape object, which resides on the tenure heap and is hard to locate. The researchers discovered that SpiderMonkey's getElement function has checks that can be bypassed. By making a fake shape point to the vtable of a Uint32Array (which resides in known memory regions) and ensuring certain conditions (like ops property being zero), they could read array metadata without needing the actual shape address.
  • This led to a fake object primitive, where a fake_array and a BigUint64Array could 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.

→ Top-rated talks at Hexacon 2025

All talks from Hexacon 2025