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).
77 lines
3 KiB
Markdown
77 lines
3 KiB
Markdown
# 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):
|
|
|
|
```c
|
|
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 `TIFFReadDirectory` is 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:** `registeredCODECS` global 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.
|