Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extra checking to prevent loop counter from wrapping around #1766

Merged

Conversation

kevinbackhouse
Copy link
Collaborator

Fixes: GHSA-hqjh-hpv8-8r9p

The bug is this loop condition in CrwMap::decodeArray. The loop counter, c, is a uint16_t, so if the component size is greater than 2 * 0xffff then c will wrap around and the loop won't terminate.

There are 3 commits:

  1. Regression test
  2. Fix for GHSA-hqjh-hpv8-8r9p
  3. Defensive fixes for similar looking loop conditions

So the 3rd commit is optional. I'll add comments to the code review explaining those changes.

@kevinbackhouse kevinbackhouse added the forward-to-main Forward changes in a 0.28.x PR to main with Mergify label Jul 7, 2021
@kevinbackhouse kevinbackhouse added this to the v0.27.5 milestone Jul 7, 2021
Comment on lines -1820 to -1821
blockIndex--;
blockSize = (long)p_->blocksMap_[blockIndex].getSize();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, this code doesn't seem to be covered by any of our tests. But I am pretty sure that if this loop doesn't terminate early then, on the final iteration of the loop, this array access will attempt to access blocksMap_[-1]. Since all this code is doing is preloading blockSize for the next iteration, it is better to just load it at the beginning of the loop.

for (int i = 0; i < pos->count(); ++i) {
for (long i = 0; i < pos->count(); ++i) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type count is long and the parameter type of toString is long, so long is a better choice for the type of i.

for (unsigned i = 0; i < value.length(); ++i) {
for (size_t i = 0; i < value.length(); ++i) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value is a std::string, so size_t is a better choice for the type of i.

Comment on lines +891 to +896
const uint32_t component_size = ciffComponent.size();
enforce(component_size % 2 == 0, kerCorruptedMetadata);
enforce(component_size/2 <= static_cast<uint32_t>(std::numeric_limits<uint16_t>::max()), kerCorruptedMetadata);
const uint16_t num_components = static_cast<uint16_t>(component_size/2);
uint16_t c = 1;
while (uint32_t(c)*2 < ciffComponent.size()) {
while (c < num_components) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the fix for GHSA-hqjh-hpv8-8r9p
The value of ciffComponent.size() is bounds-checked before the start of the loop and the correct number of iterations is precomputed.

Comment on lines -896 to +900
if (ifdId == canonCsId && c == 23 && ciffComponent.size() > 50) n = 3;
if (ifdId == canonCsId && c == 23 && component_size >= 52) n = 3;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The number 50 looks wrong to me. If c == 23 and n == 3 then the read below will read the 6 bytes starting at offset 46. Therefore the size needs to be at least 52.

for (unsigned int i = 0; i < input.length(); ++i) {
for (size_t i = 0; i < input.length(); ++i) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input is a std::string, so size_t is a better type for i.

if (input.length() - 4 > i) {
if (input.length() >= 4 && input.length() - 4 > i) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure that input.length() - 4 cannot overflow.

src/iptc.cpp Outdated
Comment on lines 348 to 351
uint32_t i = 0;
while (i < bytes.size() - 3 && bytes.at(i) != 0x1c)
size_t i = 0;
while (i + 3 < bytes.size() && bytes.at(i) != 0x1c)
i++;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subtraction bytes.size() - 3 could overflow. i + 3 cannot, because i is initially 0 and only incremented by 1 on each loop iteration.

src/iptc.cpp Outdated
Comment on lines 353 to 354
while (i < bytes.size() - 3) {
while (i + 3 < bytes.size()) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I just realized that my logic that i + 3 cannot overflow doesn't work here because i is incremented with a += in this loop. I'll add another commit to fix it.

Comment on lines -2557 to +2560
for (uint16_t i = 0; i < value.count(); i++ ) { // for each element in value array
long count = value.count();
enforce(0 <= count && count <= std::numeric_limits<uint16_t>::max(), kerCorruptedMetadata);
for (uint16_t i = 0; i < count; i++ ) { // for each element in value array
Copy link
Collaborator Author

@kevinbackhouse kevinbackhouse Jul 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure that value.count() doesn't exceed the range of a uint16_t.

Comment on lines -3220 to +3223
for (unsigned int i = 0; i < stringValue.length(); ++i) {
for (size_t i = 0; i < stringValue.length(); ++i) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stringValue is a std::string, so size_t is a better choice for i.

for ( int16_t k = 0 ; k < records[i].size ; k++ ) s << " " << ints.at(nStart++);
for ( uint16_t k = 0 ; k < records[i].size ; k++ ) s << " " << ints.at(nStart++);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type of records[i].size is uint16_t, so uint16_t is a better choice for k.

Comment on lines +352 to 353
size_t i = 0;
while (i < bytes.size() - 3 && bytes.at(i) != 0x1c)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type of bytes.size() is size_t, so size_t is a better choice for i.

Comment on lines +349 to +351
if (bytes.size() < 3) {
return;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prevent integer overflow in bytes.size() - 3 below.

@kevinbackhouse kevinbackhouse changed the title Fix ghsa hqjh hpv8 8r9p Extra checking to prevent loop counter from wrapping around Jul 7, 2021
@kevinbackhouse
Copy link
Collaborator Author

I used two CodeQL queries to find loop conditions similar to the original bug. Both queries have quite a few false positives, so I ignored some of the results.

The first query looks for loop conditions of the form a < b where the value of b could be larger than the type of a is capable of representing. For example: a is a uint16_t and b is a size_t.

import cpp
import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis
import semmle.code.cpp.rangeanalysis.RangeAnalysisUtils

from Loop loop, RelationalOperation cmp, Expr a, Expr b, LocalVariable v
where loop.getCondition().getAChild*() = cmp
and a = cmp.getLesserOperand()
and b = cmp.getGreaterOperand()
and exprMaxVal(a) < upperBound(b.getFullyConverted())
and a.getAChild*() = v.getAnAccess()
select cmp, a, b

The second query looks for loop conditions that do arithmetic in the comparison. For example: a < size-10.

import cpp

// Find loop conditions like this: `while (a < size-10)`.
// Conditions like that are often an overflow risk.
from Loop loop, RelationalOperation cmp, BinaryArithmeticOperation binop
where
  loop.getCondition().getAChild*() = cmp and
  binop = cmp.getAnOperand() and
  not binop.isConstant() and
  // Ignore results in the standard libraries and in the xmpsdk subdirectory.
  exists (string path |
    path = cmp.getLocation().getFile().getRelativePath() and
    not path.matches("xmpsdk/%"))
select binop, "Arithmetic in loop condition."

@kevinbackhouse kevinbackhouse merged commit c4861fe into Exiv2:0.27-maintenance Jul 17, 2021
kevinbackhouse added a commit that referenced this pull request Jul 25, 2021
Extra checking to prevent loop counter from wrapping around (backport #1766)
@kevinbackhouse kevinbackhouse deleted the Fix-GHSA-hqjh-hpv8-8r9p branch July 26, 2021 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug forward-to-main Forward changes in a 0.28.x PR to main with Mergify
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants