CVE-2026-0889 is a Firefox remote crash triggered by registering an oversized service worker. This research was a joint effort with Elysee Franchuk, who discovered the condition while we both explored potential attack paths. This post will describe my process performing the root cause analysis, as well as the patch analysis.


Recording the Crash

After verifying the crash, the first addition to my lab was the Record and Replay Framework (rr). This tool allowed me to record the application crash in the GNU debugger and replay it as many times as I needed. I stepped forwards and backwards through key events in the debugger to better understand the conditions and the code.

Starting RR to record the crash: Link to ./pcores explanation here.

The first area of the code that I investigated was the cause of the SIGSEV error. (SIGSEV is the code for a segmentation fault in linux.) I found a null pointer dereference, which was hardcoded in the source code. (Image below) This was confusing to me, and emphasized the need for a debugging build, so I could compare the information I am seeing in the debugger with the source code.

The assembly code clearing the A register, then copying a value to the location stored in the register:

This code turned out to be part of a defensive function, causing a crash when unstable conditions are discovered during runtime. Causing an immediate crash may save key information about the cause in the crash logs, and prevents further exploitation of the condition.

At this point I had added a tool to my home lab and learned how to use it, but I had not found the root cause of the crash. To fill some gaps I cloned the Firefox git repository which contains all the source code. I customized the compiler options and built Firefox locally. This build:

  • Printed additional error information to my terminal when running.
  • Allowed the debugger to show the file and line of the source code for every instruction I stopped at.

I ran my new Firefox build with rr and recreated the crash. An assertion error printed to my terminal:

[803288] Assertion failure: aLength <= kMax (string is too large), at /home/caleb/Documents/SecRe/firefox/firefox/xpcom/string/nsTStringRepr.h:89

The Firefox source code is by far the largest repo I have tried to understand. Starting at the file nsTStringRepr.h and working backwards to the input source, as well as comparing what I was reading to real instructions recorded in my debugger helped me to sort out the cause of the fault.


Finding the Root Cause

nsTStringRepr.h shows that the "size" attribute of a string is stored in a uint32 variable type. (Max size of 0xFFFFFFFF) There is a check to confirm the size is small enough to be cast to an int32 (Max size 0x7FFFFFFF) which may happen later in the program. The assertion error occurs when the actual string size is larger than an int32 and smaller than an uint32.

The memory map of the program, showing the relevant heap allocation with size 0x80000000:


Analyzing the Code Path

Throughout this project I recorded several instances of the crash, I frequently took breaks and then restarted. I picked up a little trick with rr to jump into the relevant code sections quickly:

  • Search the dump file for a the fault with a command like: rr dump | grep -E "SIG(SEGV|ABRT|BUS|ILL|FPE)"
  • Note the global time of the fault.
  • Use the goto option in rr to replay the program until it opens the debugger at that time.
This allowed me to stop the debugger close to the right location without knowing what function to add a breakpoint to.

The replay command I would use for this output is rr replay -g 421344:

Pausing just before the crash is good for taking a stack trace and understanding the program flow that leads to the assertion error. This was a significant help in my efforts to find the input source during my analysis of the source code.

  1. dom/serviceworkers/ServiceWorkerScriptCache.cpp handles the initial input of the service worker into memory, and returns a successful status code when it is complete.
  2. multiple functions within xpcom/string/nsTSubstring.cpp perform parsing operations on the service worker, such as removing white-space from the file.
  3. After the white space is removed, the total size is checked, found to be too large, and causes the error.
  4. dist/include/mozilla/Assertions.h is called after the assertion error and causes an unrecoverable crash in the faulty thread. In this case it is the parent thread of Firefox and the program crashes. This is the hard-coded null pointer dereference I had seen earlier.

Selecting different stack frames in GDB allowed me to inspect the memory and locate the malicious input when it was first written. Creating break points and moving back and forth between functions with rr allowed me to see the modifications written by the string parsing functions. I tracked the *tainting* of local variables which included a variable called aLength being too large and triggering the assertion fault in nsTStringRepr.h.

The phrase *tainted* refers to variables that are derived from attacker-controlled input. The malicious service worker was our input, saved with the name aData. The aLength value was derived from the length of aData. By controlling aData we have *tainted* aLength, which ultimately caused the crash.

When examining variables GDB prints the contents of a memory location rather than the pointer address, except in this case the contents of the service worker were too large and it would hang. The command (gdb) set print elements <VALUE> is necessary to restrict the length of output.

I wrote a small script to remove the addresses and decode the UTF-16 output given by GDB when reading the raw memory values. The following image shows the original service worker, in readable form, taken from the heap using GDB and rr:

Before deriving the length of aData, the whitespace was stripped by string functions in Firefox. The following image shows the memory after the program made these changes:

In reality, aData was actually tainted before aLength was derived. The above image is not the exact attacker-controlled input. This vulnerability shows an example of *taint propagation*, or a tainted variable tainting another variable.

This taint propagation is the fundamental reason that a defensive crash is the best option at this point in the program flow. The boundary enforcement prevents the string functions from passing a value that is too large to be handled by other areas of the code, but it does not know why this value is corrupt. The string functions do not know what input source caused this corruption, or how many other variables are tainted.

Further exploitation is prevented, but this crash is an example of a denial of service attack. It has a security impact and CVSS score because it impacts the availability of Firefox.


Patch Analysis

The patch addresses these issues by moving the boundary enforcement to the input source. This is fundamentally different because when the boundary enforcement triggers an import failure, the program knows the cause, the scope of damage, and how to safely proceed.

When attempting this exploit against a patched build of Firefox it shows 2 errors:


      [Parent 123573, Main Thread] WARNING: 'NS_FAILED(aStatus)', file /home/caleb/Documents/SecRe/firefox/firefox/dom/serviceworkers/ServiceWorkerScriptCache.cpp:287/n 
      [Parent 123573, Main Thread] WARNING: 'NS_FAILED(aStatus)', file /home/caleb/Documents/SecRe/firefox/firefox/dom/serviceworkers/ServiceWorkerUpdateJob.cpp:330
      
The program then recovers and continues. The malicious service worker is not imported to the browser and does not result in a crash

To accomplish this, a function was added in the file dom/workers/WorkerCommon.h which returns the maximum string length as the maximum size a worker script could be.

Source code of the function:


    inline size_t GetWorkerScriptMaxSizeInBytes() {
        // This is the max size that any of our worker scripts can be.

        return nsString::LengthStorage::kMax;
    }
      

A check was added to ServiceWorkerScriptCache.cpp before returning a success code after importing a service worker. This check ensures the size of the service worker is less than the maximum.

Source code of the function:


    if (aLen > GetWorkerScriptMaxSizeInBytes()) {
        rv = NS_ERROR_DOM_ABORT_ERR;  // This will make sure an exception gets
                                      // thrown to the global.
    return NS_OK;
    }
    


Lesson

During this project I learned a significant amount about browser internals. At one point I took a stack trace of over 100 threads to better understand the jobs that were being done internally to the browser. I added new tools to my homelab and I ran into and solved unique challenges with tools I thought I already knew.

I read more of Firefox's source code, and debugged more of the program than was necessary for this analysis but I don't feel like it was a loss. Developing a better understanding of browser internals was a key motivation for this project all along. Learning about source to sink analysis and taint propagation really helped me to organize my efforts.

Firefox is most recently estimated to have 150 million users. I am one of them and I am proud to have contributed to the project.


Links