Closed Bug 1874489 (CVE-2024-3859) Opened 1 year ago Closed 11 months ago

Read beyond bounds in OpenTypeSTAT::Parse()

Categories

(Core :: Graphics: Text, defect)

Firefox 121
Other
Unspecified
defect

Tracking

()

RESOLVED FIXED
126 Branch
Tracking Status
firefox-esr115 125+ fixed
firefox124 --- wontfix
firefox125 + fixed
firefox126 + fixed

People

(Reporter: mozillabugs, Assigned: jfkthame)

References

Details

(4 keywords, Whiteboard: [adv-main125+][adv-esr115.10+])

Attachments

(12 files)

491 bytes, text/html
Details
384.90 KB, application/octet-stream
Details
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
233 bytes, text/plain
Details

OpenTypeSTAT::Parse() (gfx\ots\src\stat.cc) can read beyond bounds if a font header contains specially-crafted values for designAxisSize, designAxisCount, and designAxisOffset. The bug can be reached only on 32-bit platforms, and it appears to be latent -- Searchfox indicates that OpenTypeSTAT::Parse() is not called anywhere, though it is built into the Firefox executable.

The bug is on lines 62-68 (FIREFOX_121_0_RELEASE):

 31: bool OpenTypeSTAT::Parse(const uint8_t* data, size_t length) {
 32:   Buffer table(data, length);
 33:   if (!table.ReadU16(&this->majorVersion) ||
 34:       !table.ReadU16(&this->minorVersion) ||
 35:       !table.ReadU16(&this->designAxisSize) ||
 36:       !table.ReadU16(&this->designAxisCount) ||
 37:       !table.ReadU32(&this->designAxesOffset) ||
 38:       !table.ReadU16(&this->axisValueCount) ||
 39:       !table.ReadU32(&this->offsetToAxisValueOffsets) ||
 40:       !(this->minorVersion < 1 || table.ReadU16(&this->elidedFallbackNameID))) {
 41:     return Drop("Failed to read table header");
 42:   }
 ...
 51:   if (this->designAxisSize < sizeof(AxisRecord)) {
 52:     return Drop("Invalid designAxisSize");
 53:   }
 54: 
 55:   size_t headerEnd = table.offset();
 56: 
 57:   if (this->designAxisCount == 0) {
 58:     if (this->designAxesOffset != 0) {
 59:       Warning("Unexpected non-zero designAxesOffset");
 60:       this->designAxesOffset = 0;
 61:     }
 62:   } else {
 63:     if (this->designAxesOffset < headerEnd ||
 64:         size_t(this->designAxesOffset) +
 65:           size_t(this->designAxisCount) * size_t(this->designAxisSize) > length) {
 66:       return Drop("Invalid designAxesOffset");
 67:     }
 68:   }

The issue is that the calculation on lines 64-5 can overflow on 32-bit builds, because in those builds, size_t is 32 bits. So, for example, if designAxesOffset is 0xffffffff, designAxisCount is 16, and designAxisSize is 8, the calculation yields 0x7f in 32 bits. This would then pass the length check on line 65. Once that occurs, line 71 sets table.offset_ to 0xffffffff. The table.Readxxx() functions on lines 74-6 then read from buffer_ + 0xffffffff, then wrap back to 0...:

 69: 
 70:   for (size_t i = 0; i < this->designAxisCount; i++) {
 71:     table.set_offset(this->designAxesOffset + i * this->designAxisSize);
 72:     this->designAxes.emplace_back();
 73:     auto& axis = this->designAxes[i];
 74:     if (!table.ReadU32(&axis.axisTag) ||
 75:         !table.ReadU16(&axis.axisNameID) ||
 76:         !table.ReadU16(&axis.axisOrdering)) {
 77:       return Drop("Failed to read design axis");
 78:     }
 79:     if (!CheckTag(axis.axisTag)) {
 80:       return Drop("Bad design axis tag");
 81:     }
 82:     if (!ValidateNameId(axis.axisNameID)) {
 83:       return true;
 84:     }
 85:   }

because the ReadUxxx() functions' checks also overflow, e.g., ReadU32() (gfx/ots/src/ots.h) does:

 135: bool ReadU32(uint32_t *value) {
 136:   if (offset_ + 4 > length_) {
 137:     return OTS_FAILURE();
 138:   }
 139:   std::memcpy(value, buffer_ + offset_, sizeof(uint32_t));
 140:   *value = ots_ntohl(*value);
 141:   offset_ += 4;
 142:   return true;
 143: }

where offset_ isa size_t, so lines 136 and 141 overflow from 0xffffffff to 3 on the first call to ReadU32(), passing the check on lines 136-38 and corrupting the value of offset on line 141.

There appears also to be a similar bug involving offsetToAxisValueOffsets in the same code.

Flags: sec-bounty?
Group: core-security → gfx-core-security

FWIW, code coverage data on SearchFox says we are running at least some of the code in OpenTypeSTAT::Parse.

(In reply to Andrew McCreight [:mccr8] from comment #1)

FWIW, code coverage data on SearchFox says we are running at least some of the code in OpenTypeSTAT::Parse.

This is interesting, as SearchFox search says it's not called, yet it gets built into the executable, so something must reference it (the linker should remove unreferenced code). Aha! It gets called when visiting...wait for it....google.com . So this isn't latent.

Has this been reported to Chrome?

Flags: needinfo?(mozillabugs)

(In reply to Jeff Muizelaar [:jrmuizel] from comment #5)

Has this been reported to Chrome?

No. Does the Chrome team own OTS? Would Mozilla like me to report it there while this bug is pending here?

Flags: needinfo?(mozillabugs)

I'm not sure it's exact ownership, but it's originally from Google. I think reporting it there probably makes sense.

(In reply to Jeff Muizelaar [:jrmuizel] from comment #7)

I'm not sure it's exact ownership, but it's originally from Google. I think reporting it there probably makes sense.

I'll report it to the Chromium team. It seems to be widely-used, but I'm not sure who (if anyone) is actually the official maintainer at this point. https://bugzilla.mozilla.org/show_bug.cgi?id=1341088 seems to indicate that https://github.com/khaledhosny/ots was/is(?) the official repository for it, but the "security" tab there has no content, so reporting it there doesn't seem safe.

Khaled is (and has always been, IIRC) the primary maintainer, though others (including myself) sometimes contribute. But I'm not sure what his preference would be for reporting potentially sensitive issues.

I do have an email address for him, if you'd like me to reach out directly. Though in this case, I'd be inclined to simply open a PR to use 64-bit arithmetic in the problematic calculation, and it'd probably get merged pretty quickly.

If (as seems likely) this bug turns out to be exploitable, I'd like to minimize the number of projects that get 0-dayed. Does Mozilla have a policy on this issue?

The data that can be forced to be read using this mechanism could be unowned heap if the STAT table in the font file is compressed. If the STAT table is uncompressed, the data is from a preceding part of the font file, (See GetTableData() in gfx/ots/src/ots.cc for details)

Here's a POC that demonstrates the read beyond bounds. Use it thusly:

  1. Build FF for a 32-bit architecture (I used Win32).
  2. Copy ffbug_2218.htm to a webserver in <root>/ffbug_2218/ffbug_2218.htm .
  3. Copy Montserrat-VariableFont_wght-hacked.ttf to <root>/resources/fonts/Montserrat/Montserrat-VariableFont_wght-hacked.ttf (or put it somewhere else and modify the path in ffbug_2218.htm appropriately.
  4. Start FF and attach a debugger to it.
  5. Set a BP on OpenTypeSTAT::Parse().
  6. Load ffbug_2218.htm .
  7. When the BP fires, step lines 33-42.
  8. Examine this->designAxesOffset and this->offsetToAxisValueOffsets . Notice that they're 0xfffffffc and 0xfffffffe, respectively.
  9. Step lines 62-8 and notice that the check doesn't detect the invalid value for this->designAxesOffset .
  10. Step into line 71 and notice that it sets table.offset_ to 0xfffffffc.
  11. Step into line 74 and notice that ReadU32()'s check overflows, and that the memcpy() reads from buffer+0xfffffffc , which wraps to buffer-4 (where buffer is the lowest valid address here).
  12. Set execution to line 91 to demonstrate the second, similar read beyond bounds.
  13. Notice that the check on lines 96-102 doesn't detect the broken value of this->offsetToAxisValueOffsets.
  14. Step the first iteration of the loop beginning on line 104 and notice that line 107 reads from buffer-2 instead of from buffer.

There are also some other overflow-vulnerable computations in this file, such as line 110. Really this code should be using CheckedInt or something like it.

Attached file ffbug_2218.htm
Summary: Latent(?) read beyond bounds in OpenTypeSTAT::Parse() → Read beyond bounds in OpenTypeSTAT::Parse()

cc'ing Dominik, who has received the corresponding report to chromium.

Assignee: nobody → jfkthame

Jonathan, could you please assign a Severity to this?

Also, are we dependent on movement upstream for this one, or is there anything we can do to mitigate it?

Flags: needinfo?(jfkthame)

The potential consequences of the out-of-bounds reads would be either (a) crash (so this could be a DoS vector), or (b) allowing an invalid STAT table to pass validation, so we pass it through to the rasterizer. In theory that should be harmless, as the rasterizer should handle it safely, but we prefer not to rely on that (especially as we can't audit/fix issues there might be in Core Text or DirectWrite) which is why we validate the resources before using them.

So while this is clearly a valid issue, I don't think it's higher than S3 at this point, though if there were actually an exploitable STAT-based bug in CT or DW, that could make it more significant.

I'd be fine with patching this locally, if we want to fix it in gecko ahead of upstream.

Severity: -- → S3
Flags: needinfo?(jfkthame)

The severity field for this bug is set to S3. However, the bug is flagged with the sec-high keyword.
:jfkthame, could you consider increasing the severity of this security bug?

For more information, please visit BugBot documentation.

Flags: needinfo?(jfkthame)
Flags: needinfo?(jfkthame)
Keywords: sec-highsec-moderate

This is just https://github.com/khaledhosny/ots/pull/277 from upstream, not really part
of this issue but touching the same code, so simplest to include it here.

Once we take these patches locally, I'll also open a PR to land them upstream in OTS.

Pushed by jkew@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/dfd86e34e53f patch 1 - Don't check STAT designAxisSize if the designAxisCount is zero. r=gfx-reviewers,lsalzman https://hg.mozilla.org/integration/autoland/rev/eed1acdcdfbd patch 2 - Avoid potential arithmetic overflow during Buffer read operations. r=gfx-reviewers,lsalzman https://hg.mozilla.org/integration/autoland/rev/df5fdbb9e3ec patch 3 - More careful range checks in STAT parsing. r=gfx-reviewers,lsalzman
Group: gfx-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 11 months ago
Resolution: --- → FIXED
Target Milestone: --- → 126 Branch

The patch landed in nightly and beta is affected.
:jfkthame, is this bug important enough to require an uplift?

  • If yes, please nominate the patch for beta approval.
  • If no, please set status-firefox125 to wontfix.

For more information, please visit BugBot documentation.

Flags: needinfo?(jfkthame)

This is just https://github.com/khaledhosny/ots/pull/277 from upstream, not really part
of this issue but touching the same code, so simplest to include it here.

Original Revision: https://phabricator.services.mozilla.com/D204916

Attachment #9393097 - Flags: approval-mozilla-beta?
Attachment #9393098 - Flags: approval-mozilla-beta?
Attachment #9393099 - Flags: approval-mozilla-beta?

Uplift Approval Request

  • Explanation of risk level: just makes bounds-checking arithmetic more careful to avoid potential overflow
  • User impact if declined: potential for out-of-bounds accesses during font validation; could result in crash, unclear whether more serious consequences
  • Code covered by automated testing: no
  • Is Android affected?: yes
  • Needs manual QE test: no
  • String changes made/needed: none
  • Risk associated with taking this patch: low risk
  • Fix verified in Nightly: no
  • Steps to reproduce for manual QE testing: n/a
Attachment #9393097 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9393098 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9393099 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [post-critsmash-triage]
Flags: qe-verify-
Flags: sec-bounty? → sec-bounty+
Keywords: sec-vector
Flags: needinfo?(jfkthame)

Is this something we're going to want on ESR also?

I'd suggest we take it, yes. I'll do the moz-phab uplift thing...

This is just https://github.com/khaledhosny/ots/pull/277 from upstream, not really part
of this issue but touching the same code, so simplest to include it here.

Original Revision: https://phabricator.services.mozilla.com/D204916

Attachment #9393941 - Flags: approval-mozilla-esr115?

Huh, looks like that uplift only did the first patch, which doesn't actually address the issue.... I'll try again.

Attachment #9394082 - Flags: approval-mozilla-esr115?
Attachment #9394083 - Flags: approval-mozilla-esr115?

esr115 Uplift Approval Request

  • User impact if declined: potential for out-of-bounds accesses during font validation
  • Code covered by automated testing: no
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing: n/a
  • Risk associated with taking this patch: low
  • Explanation of risk level: just makes bounds-checking arithmetic more careful to avoid potential overflow
  • String changes made/needed: none
  • Is Android affected?: yes
Attachment #9393941 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Attachment #9394082 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Attachment #9394083 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Whiteboard: [adv-main125+][adv-esr115.10+]
Attached file advisory.txt
Alias: CVE-2024-3859
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: