Closed Bug 1355639 (CVE-2026-8949) Opened 9 years ago Closed 5 months ago

Write beyond bounds in nsDataObjCollection::GetText()

Categories

(Core :: Widget: Win32, enhancement, P3)

52 Branch
enhancement

Tracking

()

RESOLVED FIXED
152 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 151+ fixed
firefox150 --- wontfix
firefox151 --- fixed
firefox152 --- fixed

People

(Reporter: q1, Assigned: jstutte)

References

Details

(Keywords: csectype-intoverflow, reporter-external, sec-moderate, Whiteboard: tpi:+, widget-next[adv-main151+][adv-esr140.11+])

Attachments

(2 files, 1 obsolete file)

nsDataObjCollection::GetText() (widget\windows\nsDataObjCollection.cpp) can cause an integer overflow. If it does, it will write beyond the bounds of its |buffer| array. The bug is that it its |mDataObjects| array, where it accumulates the length of each string into |bufferSize|. But |mDataObjects| can be filled with an arbitrary number of strings (containing arbitrary binary data) by JS code that calls dataTransfer.mozSetDataAt(), with each string being <= 2**28 bytes long (js::MaxStringLength). At present, I do not know how to cause FF to call nsDataObjCollection::GetText(), though I have attached a POC that populates |mDataObjects| with enough large strings to cause an overflow and write beyond bounds if GetText() _were_ called with the containing nsDataObjCollection. The POC populates |mDataObjects| with 17 strings. The first 15 are exactly 256MB, the next is 256MB - 16KB, and the last is 32KB. If GetText() were invoked with these strings, the last string would cause the addition on line 265 to compute the quantity 0xffffc001 + 0x8000 -> overflows to 0x4001 and to allocate 0x4001 bytes of memory. Lines 269-72 would then point |buffer| 0xffffc001-0x4001-1 bytes beyond the allocated buffer's end, and line 273 would copy |0x8000| bytes of data there, overrunning |buffer| by 0x3ffe bytes. Thus, the attacker could control the amount of data written, as well as the data itself (JS strings can contain arbitrary binary characters, using \uXXXX, where X is a hex digit). There are 2 overflow bugs in each main section of GetText(): lines 265 and 275, and lines 305 and 315: 232: HRESULT nsDataObjCollection::GetText(LPFORMATETC pFE, LPSTGMEDIUM pSTM) 233: { 234: STGMEDIUM workingmedium; 235: FORMATETC fe = *pFE; 236: HGLOBAL hGlobalMemory; 237: HRESULT hr; 238: uint32_t buffersize = 1; 239: uint32_t alloclen = 0; 240: 241: hGlobalMemory = GlobalAlloc(GHND, buffersize); 242: 243: if (pFE->cfFormat == CF_TEXT) { 244: nsAutoCString text; 245: for (uint32_t i = 0; i < mDataObjects.Length(); ++i) { 246: nsDataObj* dataObj = mDataObjects.ElementAt(i); 247: hr = dataObj->GetData(&fe, &workingmedium); ... 256: // Now we need to pull out the text 257: char* buffer = (char*)GlobalLock(workingmedium.hGlobal); 258: if (buffer == nullptr) 259: return E_FAIL; 260: text = buffer; 261: GlobalUnlock(workingmedium.hGlobal); 262: ReleaseStgMedium(&workingmedium); 263: // Now put the text into our buffer 264: alloclen = text.Length(); 265: hGlobalMemory = ::GlobalReAlloc(hGlobalMemory, buffersize + alloclen, 266: GHND); 267: if (hGlobalMemory == nullptr) 268: return E_FAIL; 269: buffer = ((char*)GlobalLock(hGlobalMemory) + buffersize); 270: if (!buffer) 271: return E_FAIL; 272: buffer--; // Overwrite the preceding null 273: memcpy(buffer, text.get(), alloclen); 274: GlobalUnlock(hGlobalMemory); 275: buffersize += alloclen; 276: } 277: pSTM->tymed = TYMED_HGLOBAL; 278: pSTM->pUnkForRelease = nullptr; // Caller gets to free the data 279: pSTM->hGlobal = hGlobalMemory; 280: return S_OK; 281: } 282: if (pFE->cfFormat == CF_UNICODETEXT) { 283: buffersize = sizeof(char16_t); 284: nsAutoString text; 285: for (uint32_t i = 0; i < mDataObjects.Length(); ++i) { 286: nsDataObj* dataObj = mDataObjects.ElementAt(i); 287: hr = dataObj->GetData(&fe, &workingmedium); ... 296: // Now we need to pull out the text 297: char16_t* buffer = (char16_t*)GlobalLock(workingmedium.hGlobal); 298: if (buffer == nullptr) 299: return E_FAIL; 300: text = buffer; 301: GlobalUnlock(workingmedium.hGlobal); 302: ReleaseStgMedium(&workingmedium); 303: // Now put the text into our buffer 304: alloclen = text.Length() * sizeof(char16_t); 305: hGlobalMemory = ::GlobalReAlloc(hGlobalMemory, buffersize + alloclen, 306: GHND); 307: if (hGlobalMemory == nullptr) 308: return E_FAIL; 309: buffer = (char16_t*)((char*)GlobalLock(hGlobalMemory) + buffersize); 310: if (!buffer) 311: return E_FAIL; 312: buffer--; // Overwrite the preceding null 313: memcpy(buffer, text.get(), alloclen); 314: GlobalUnlock(hGlobalMemory); 315: buffersize += alloclen; 316: } 317: pSTM->tymed = TYMED_HGLOBAL; 318: pSTM->pUnkForRelease = nullptr; // Caller gets to free the data 319: pSTM->hGlobal = hGlobalMemory; 320: return S_OK; 321: } 322: 323: return E_FAIL; 324: } Use the POC by loading it into FF, attaching a debugger to FF, and setting a BP on nsNativeDragTarget::Drop(). Select the text "select and drag this text..." and drag and drop it onto the text "...to here!". Wait for the BP to fire, then examine |pData| to verify that it contains 17 objects totalling 0x100004000 bytes. Note that the first 16 objects are represented by clipboard cache files, because they are too large to be cached in memory; those items have |DataStruct::mDataLen| == 0. You can examine them (and their actual sizes) in the running account's temp folder (e.g., c:\users\browser\AppData\Local\temp\clipboardcache-xxxxx). The last object is cached in memory, and has |DataStruct::mDataLen| == 0x8000.
Summary: Potential write beyond bounds in nsDataObjCollection::GetText() → Latent (?) write beyond bounds in nsDataObjCollection::GetText()
Flags: sec-bounty?
Ack! > The bug is that it its |mDataObjects| array, where it accumulates the length of each string into |bufferSize|. isn't English. It should read: "The bug is that it accumulates the length of each string into |bufferSize| while iterating its |mDataObjects| array, without checking for overflow." Maybe I need the 2 aspirin and the call in the morning.
Jim, can you take a look at this to start maybe?
Flags: needinfo?(jmathies)
Trying to find a resource to look these over, thanks. This one isn't a drive-by fwiw.
Flags: needinfo?(jmathies)
Oh yes, the vulnerability exists -- and the POC works -- only on 64-bit builds.
Group: core-security → dom-core-security
Flags: sec-bounty? → sec-bounty+
Priority: -- → P4
Whiteboard: tpi:+
I have created a POC that demonstrates the overflow and consequent write beyond bounds. It is attached. You will need a machine with >= 16GB of RAM, and a 64-bit OS and 64-bit build of FF. Use the POC by putting it on a webserver and starting FF (I used 58.0.2 debug). Load the POC in FF, attach a debugger to FF, and set a BP on the GlobalRealloc() in the CF_UNICODETEXT section of nsDataObjCollection::GetText() (line 305 in FF 58.0.2). Now start Windows Write/Wordpad under the same Windows account as you started FF. Select the text in the FF window and drag it into Write/Wordpad. The BP will fire several times, with |buffersize| 0x08000000 bytes larger each time. When |buffersize| reaches 0xf8000002, go into disassembly mode and watch the expression |buffersize + alloclen| overflow to |2|. Now trace the rest of the code and watch the memcpy() on line 313 scribble binary data from the JS object |s| onto the heap.
Attached file bug_804_poc_5.htm —
Summary: Latent (?) write beyond bounds in nsDataObjCollection::GetText() → Write beyond bounds in nsDataObjCollection::GetText()
BTW, while fixing this bug, you might also want to look into similar potential overflows and writes beyond bounds in nsDataObjCollection::GetFile() and nsDataObjCollection::GetFileDescriptors() (same module). It's not clear whether those overflows practically can occur, but it should be easy to prevent them.
Dan, A PoC has been added to this bug. It sounds like there is still user interaction required, and I expect we took this into account when we looked at this a year ago. Given the PoC do we want to re-rate this (maybe not?) or at least try to raise the priority a little so that gets a fix? Need-info'ing myself to investigate the suggested functions in comment 7.
Flags: needinfo?(ptheriault)
Flags: needinfo?(dveditz)
I didn't step through the debugger (I need to try windbg; visual studio is hanging trying to deal with the huge amounts of memory) but I did crash in Mac and Windows in Pickle::BeginWrite on a release assertion that indicates an integer overflow bp-27489966-7d84-4629-8c24-2f2c60180319 https://hg.mozilla.org/mozilla-central/annotate/9cd8e03d9e472f07041be3cd80cc95a8194f91a5/ipc/chromium/src/base/pickle.cc#l492 It's getting stopped safely there, but if I'm reading the bug correctly an overwrite may have already happened. Or is nsDataObjCollection over in the parent? If I disable e10s then I crash so hard our own crash reporter doesn't show up, just the windows built-in one. Re-nominating for bounty to consider topping this one up based on the new PoC. It still requires dragging text that doesn't appear draggable though (because tab hangs before any animation can happen).
Flags: sec-bounty?
Flags: sec-bounty+
Flags: needinfo?(dveditz)
(In reply to Daniel Veditz [:dveditz] from comment #9) > I didn't step through the debugger (I need to try windbg; visual studio is > hanging trying to deal with the huge amounts of memory) but I did crash in > Mac and Windows in Pickle::BeginWrite on a release assertion that indicates > an integer overflow > bp-27489966-7d84-4629-8c24-2f2c60180319 > https://hg.mozilla.org/mozilla-central/annotate/ > 9cd8e03d9e472f07041be3cd80cc95a8194f91a5/ipc/chromium/src/base/pickle.cc#l492 > > It's getting stopped safely there, but if I'm reading the bug correctly an > overwrite may have already happened. Or is nsDataObjCollection over in the > parent? > > If I disable e10s then I crash so hard our own crash reporter doesn't show > up, just the windows built-in one. Yes, you do need to disable e10s. It's odd that the crash reporter doesn't show, but maybe the POC overwrote something that it uses. Attaching a debugger is the best way to verify what's happening.
BTW, if the system is thrashing badly, you might need more RAM. I used a system with 16GB, and the thrashing was tolerable even with the VS debugger.
Jim, could you find somebody to investigate this bug? Thx!
Flags: needinfo?(jmathies)
Group: dom-core-security → layout-core-security
Flags: needinfo?(jmathies)
Priority: P4 → P2
Flags: needinfo?(agashlin)
The required user interaction keeps this in the sec-moderate range and the original bounty award stands.
Flags: sec-bounty? → sec-bounty+
Keywords: sec-high → sec-moderate
Flags: needinfo?(agashlin)
Flags: needinfo?(ptheriault)
Priority: P2 → P3
Whiteboard: tpi:+ → tpi:+, widget-next
Group: layout-core-security → core-security-release
Severity: normal → S3

Re-checked this almost 9 years later: the C++ arithmetic is unchanged, but the
web-reachable exploitation path is most likely closed since 2020.

Vulnerable code is still present. nsDataObjCollection::GetText still
sums uint32_t buffersize + alloclen without overflow checking in both the
CF_TEXT and CF_UNICODETEXT loops, and the same pattern appears in GetFile
and GetFileDescriptors:
https://searchfox.org/firefox-main/rev/ab269cb0e28f247ee5ed83cbc3323c2ba166d508/widget/windows/nsDataObjCollection.cpp#209-294

The class is still wired in via nsDragService::InvokeDragSession for drags
with more than one transferable item, so the code is reachable in principle.

What changed since the original PoC:

  1. mozSetDataAt — the API the comment-0 PoC used to populate
    mDataObjects with multi-hundred-MB strings of arbitrary binary content
    — is now [ChromeOnly]:
    https://searchfox.org/firefox-main/rev/ab269cb0e28f247ee5ed83cbc3323c2ba166d508/dom/webidl/DataTransfer.webidl#114
    This was done in bug 1666287, which landed on 2020-09-22 and shipped in
    Firefox 83. Web content can no longer reach that path; only chrome JS
    can.

  2. The 2018 PoC (comment 5) required disabling e10s, because with e10s the
    IPC pickle release-asserts on integer overflow before the data reaches
    the parent (see dveditz, comment 9). e10s is now mandatory, so the
    content-driven multi-GB drag aborts safely upstream of this code.

Conclusion: the bug seems no longer exploitable from web content.
It is though still potentially usable as sandbox escape, see bug 2034754.

Attached file (secure) (obsolete) —
Assignee: nobody → jstutte
Status: NEW → ASSIGNED
Attachment #9575605 - Attachment is obsolete: true

This seems to have been fixed by bug 2034754.

Status: ASSIGNED → RESOLVED
Closed: 5 months ago
Depends on: CVE-2026-8959
Resolution: --- → FIXED
Target Milestone: --- → 152 Branch
QA Whiteboard: [sec] [uplift] [qa-triage-done-c152/b151]
Whiteboard: tpi:+, widget-next → tpi:+, widget-next[adv-main151+][adv-main151+][adv-esr115.36+][adv-esr115.36+][adv-esr140.11+][adv-esr140.11+]
Whiteboard: tpi:+, widget-next[adv-main151+][adv-main151+][adv-esr115.36+][adv-esr115.36+][adv-esr140.11+][adv-esr140.11+] → tpi:+, widget-next[adv-main151+][adv-esr140.11+]
Alias: CVE-2026-8949
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: