`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.
Bug 1874489 Comment 0 Edit History
Note: The actual edited comment in the bug view page will always show the original commenter’s name and original timestamp.
`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.