Closed Bug 1299686 (CVE-2016-9066) Opened 4 years ago Closed 4 years ago

Integer overflow leading to a buffer overflow in nsScriptLoadHandler


(Core :: DOM: Core & HTML, defect)

45 Branch
Not set



Tracking Status
firefox49 --- wontfix
firefox-esr45 50+ verified
firefox50 --- verified
firefox51 --- verified
firefox52 --- verified


(Reporter: mozilla, Assigned: baku)


(Keywords: csectype-intoverflow, regression, sec-high, Whiteboard: [post-critsmash-triage][adv-main50+][adv-esr45.5+])


(3 files, 1 obsolete file)

Attached file
While loading external script data, the following code is executed in nsScriptLoadHandler (nsScriptLoader.cpp):

nsScriptLoadHandler::TryDecodeRawData(const uint8_t* aData,
                                      uint32_t aDataLength,
                                      bool aEndOfStream)
  int32_t srcLen = aDataLength;
  const char* src = reinterpret_cast<const char *>(aData);
  int32_t dstLen;
  nsresult rv =
    mDecoder->GetMaxLength(src, srcLen, &dstLen);


  uint32_t haveRead = mBuffer.length();
  uint32_t capacity = haveRead + dstLen;        // [[ 1 ]]
  if (!mBuffer.reserve(capacity)) {

  rv = mDecoder->Convert(src,
                      mBuffer.begin() + haveRead,


  haveRead += dstLen;
  MOZ_ASSERT(haveRead <= capacity, "mDecoder produced more data than expected");

  return NS_OK;

The calculation of the new capacity at [[ 1 ]] can overflow if enough data is sent by the server (>4GB). In that case, the following call to mBuffer.reserve() won't change the size of mBuffer. Afterwards, mDecoder->Convert() will write data past the end of the 8GB buffer (mBuffer stores char16_t) [1]. (requires python 3.5) in the attached archive will trigger this condition and cause an access violation while writing data into an unmapped region following the buffer (tested on Firefox 48.0.1 and Aurora):

    Process 97720 stopped
    * thread #1: tid = 0x19afd5, 0x0000000101c66c68 XUL`nsOneByteDecoderSupport::Convert(char const*, int*, char16_t*, int*) [inlined] nsUnicodeDecodeHelper::ConvertByFastTable(aSrcLength=<unavailable>, aDestLength=<unavailable>) + 9 at nsUnicodeDecodeHelper.cpp:206, queue = '', stop reason = EXC_BAD_ACCESS (code=1, address=0x577000000)
    frame #0: 0x0000000101c66c68 XUL`nsOneByteDecoderSupport::Convert(char const*, int*, char16_t*, int*) [inlined] nsUnicodeDecodeHelper::ConvertByFastTable(aSrcLength=<unavailable>, aDestLength=<unavailable>) + 9 at nsUnicodeDecodeHelper.cpp:206
       203    }
       205    for (; src<srcEnd;) {
    -> 206      *dest = aFastTable[*src];
       207      if (*dest == 0xfffd && aErrorSignal) {
       208        res = NS_ERROR_ILLEGAL_INPUT;
       209        break;
    (lldb) bt
    frame #0: 0x0000000101c66c68 XUL`nsOneByteDecoderSupport::Convert(char const*, int*, char16_t*, int*) [inlined] nsUnicodeDecodeHelper::ConvertByFastTable(aSrcLength=<unavailable>, aDestLength=<unavailable>) + 9 at nsUnicodeDecodeHelper.cpp:206
    frame #1: 0x0000000101c66c5f XUL`nsOneByteDecoderSupport::Convert(this=0x0000000112a0e800, aSrc="", aSrcLength=0x00007fff5fbfcc6c, aDest=u"", aDestLength=0x00007fff5fbfcc68) + 207 at nsUCSupport.cpp:273
    frame #2: 0x0000000102a5c3f2 XUL`nsScriptLoadHandler::TryDecodeRawData(this=0x00000001290eb740, aData="", aDataLength=<unavailable>, aEndOfStream=<unavailable>) + 146 at nsScriptLoader.cpp:2752
    frame #3: 0x0000000102a5be73 XUL`nsScriptLoadHandler::OnIncrementalData(this=0x00000001290eb740, aLoader=<unavailable>, aContext=<unavailable>, aDataLength=101, aData="", aConsumedLength=0x00007fff5fbfccf4) + 83 at nsScriptLoader.cpp:2720

Some notes regarding exploitability:

    - It is possible to use compression to reduce the network load. The attached PoC code does that and thus only needs to send about 18MB of data.

    - It should be possible to place some objects behind the buffer, as mmap will usually allocate adjacent memory regions. This could be done as follows:
        - The server sends the first 0xffffffff bytes of the payload, causing an allocation of an 8GB memory region
        - JavaScript code executes and allocates lots of target objects. At some point new memory is requested via mmap, which should be placed behind the 8GB buffer
        - The server sends the remaining data, overflowing into the previously allocated objects

    - Due to page deduplication and/or compression beeing performed by the kernel, it should be possible to trigger the bug while having less than 8GB of available RAM. At least on my Mac this seems to be the case as memory usage stays relatively low even during the allocations.

    - A 64-bit build of Firefox is required so the bug can be triggered.

    - The data that is being written is not 100% controlled as it must be valid UTF-16

[1] There is a special case when the capacity wraps around to zero. In that case the process will (likely) not crash as the overflow only happens inside the 8GB chunk.
I haven't tried to repro, but from code inspection this seems like a valid issue. Moving to a better component. mrbkap / baku, can either of you take a look? (CC'ing people because of our security groups...)
Group: firefox-core-security → core-security
Component: Security → DOM
Flags: needinfo?(mrbkap)
Flags: needinfo?(amarchesini)
Product: Firefox → Core
Assignee: nobody → amarchesini
Flags: needinfo?(amarchesini)
Attached file Another Proof-of-Concept (obsolete) —
I've created a second PoC that allocates a Uint8Array behind mBuffer, which is then being overflown into. I've tested this on OS X 10.11. It's still a bit hackish since I haven't fully figured out what jemalloc and the OS X kernel are doing during the realloc calls.

If there are no mitigations in place which add guard pages in front of important memory regions (or something similar) it should be possible (at least on OS X) to place something more "interesting" behind mBuffer, which is then overwritten.

Things look a bit different on Linux though. For one, the mmap region on Linux grows towards lower addresses, while it grows towards higher addresses on OS X. I haven't checked Windows. It should still be possible to allocate something in the memory region following the buffer, but the new PoC will likely not work on Linux out of the box.
Flags: needinfo?(mrbkap)
Attached patch crash.patchSplinter Review
Attachment #8787402 - Flags: review?(bugs)
Group: core-security → dom-core-security
Attachment #8787402 - Flags: review?(bugs) → review+
Comment on attachment 8787402 [details] [diff] [review]

[Security approval request comment]
How easily could an exploit be constructed based on the patch?

See comment 1.

Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?


Which older supported branches are affected by this flaw?

All. 2015. Bug 1218029

Do you have backports for the affected branches? If not, how different, hard to create, and risky will they be?

Easy to write.

How likely is this patch to cause regressions; how much testing does it need?

Attachment #8787402 - Flags: sec-approval?
Flags: sec-bounty?
Too late for 49. Sec-approval to land on September 20 (two weeks into next cycle).
Attachment #8787402 - Flags: sec-approval? → sec-approval+
checkin-needed, but see comment 5!
Keywords: checkin-needed
Keywords: checkin-needed
Whiteboard: [checkin on 9/20]
Ever confirmed: true
Attached file PoC Exploit
I've created another PoC. This one spawns (most of the time) on the latest Firefox (48.0.1) for OS X (10.11.6).

The large comment in code.js gives a decent overview over how the exploit works. The very short version is that it forces an allocation of an Arena (filled with ArrayBuffers) behind the buffer that is overflown. It then frees (roughly) one ArrayBuffer in every Arena and triggers GC. Finally it overflows into the |firstFreeSpan| field of the Arena and makes it look like the free slot is inside the inline data of one of the ArrayBuffer objects. When more ArrayBuffer objects are then allocated, one of them will be placed inside the data of another ArrayBuffer, making it accessible from JavaScript. From there, crafting an arbitrary read/write primitive becomes fairly easy.

The PoC currently is version dependent (due to the offsets in code.js) and only works on OS X since it assumes that the mmap region grows upwards.

For a generic mitigation, either randomizing mmap regions or inserting guard pages before/after mmap chunks should work, although I think this type of bug is best mitigated by the kernel...
Attachment #8787399 - Attachment is obsolete: true
Flags: in-testsuite?
Keywords: regression
Whiteboard: [checkin on 9/20]
Version: unspecified → 45 Branch
Closed: 4 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla52
Flags: sec-bounty? → sec-bounty+
Group: dom-core-security → core-security-release
Hi Baku, should we uplift this fix to Beta50, Aurora51 and ESR45.5?
Flags: needinfo?(amarchesini)
Comment on attachment 8787402 [details] [diff] [review]

[Approval Request Comment]
If this is not a sec:{high,crit} bug, please state case for ESR consideration:
User impact if declined: 
Fix Landed on Version: 51... ?
Risk to taking this patch (and alternatives if risky): zero. patch is very easy.
String or UUID changes made by this patch: none

See for more info.

Approval Request Comment
[Feature/regressing bug #]: Script Loader 
[User impact if declined]: a crash could happen if the capacity variable overflows.
[Describe test coverage new/current, TreeHerder]: green, but we don't have a test for this: it's not trivial to write it.
[Risks and why]: zero.
[String/UUID change made/needed]: none.
Flags: needinfo?(amarchesini)
Attachment #8787402 - Flags: approval-mozilla-esr45?
Attachment #8787402 - Flags: approval-mozilla-beta?
Attachment #8787402 - Flags: approval-mozilla-aurora?
Comment on attachment 8787402 [details] [diff] [review]

Sec-high, Aurora51+, Beta50+, ESR45+
Attachment #8787402 - Flags: approval-mozilla-esr45?
Attachment #8787402 - Flags: approval-mozilla-esr45+
Attachment #8787402 - Flags: approval-mozilla-beta?
Attachment #8787402 - Flags: approval-mozilla-beta+
Attachment #8787402 - Flags: approval-mozilla-aurora?
Attachment #8787402 - Flags: approval-mozilla-aurora+
Flags: qe-verify+
Whiteboard: [post-critsmash-triage]
I didn't managed to reproduce this issue on Mac OS X 10.11.6. I've used both and PoC Exploit and tried to run index.html by making a http server with python 3.5. This was done on firefox 48.0.1 in 64 bit as Samuel said in comment 0 in order to trigger the bug. If I understand right this needs to crash (this is what I see: or maybe I am doing something wrong here.

Andrea, can you please provide me some additional info about how to reproduce this bug? What I'm missing?
Flags: needinfo?(amarchesini)
Flags: needinfo?(amarchesini) → needinfo?(mozilla)
You'll have to use the script (a custom webserver) from the zip archive since some server side logic is required for the code to work. Are you doing that?
Thank you Samuel, now I'm able to repoduce the crash on 49.0.2 with your info.

This issue is verified fixed on 45.5.0 (20161031153904), 50.0-build2 (20161104212021), 51.0a2 (20161108004019) and 52.0a1 (20161109030210) running Mac OS X 10.11.6. 

I will mark here accordengelly.
Flags: qe-verify+
Flags: needinfo?(mozilla)
Whiteboard: [post-critsmash-triage] → [post-critsmash-triage][adv-main50+][adv-esr45.5+]
Alias: CVE-2016-9066
Group: core-security-release
Component: DOM → DOM: Core & HTML
You need to log in before you can comment on or make changes to this bug.