java-topology/defects/libtiff-0001/unit/LibtiffDirDedupTest.java
russell@unturf.com bbc12a3ca6 libtiff+libpng: 5-MOAD scan; 1 defect / libpng CLEAN
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).
2026-03-31 21:41:06 -04:00

106 lines
3.9 KiB
Java

/**
* LibtiffDirDedupTest — MOAD-0001 model for libtiff-0001
*
* Models the O(D^2) duplicate-tag detection loop in TIFFReadDirectory /
* TIFFReadCustomDirectory (libtiff/tif_dirread.c, bugzilla 1994 dedup block).
*
* A TIFF IFD can contain up to 65535 directory entries (dircount is uint16_t).
* For each entry ma the original code scans all subsequent entries na looking
* for a matching tdir_tag, producing O(D*(D-1)/2) comparisons.
*
* Fix: replace with a HashSet membership test — O(D) total.
*
* Severity: HIGH — a crafted adversarial TIFF with 65535 entries bearing
* unique tags causes ~2 billion tag comparisons on each TIFFReadDirectory call.
* At ~1 ns per comparison that is ~2 seconds per IFD open on modern hardware.
* Multi-page TIFFs (each page is an IFD) multiply the impact linearly.
*/
import java.util.*;
public class LibtiffDirDedupTest {
// --- O(D^2) original: nested loop duplicate detection ---
static boolean[] markDuplicatesQuadratic(int[] tags) {
boolean[] ignore = new boolean[tags.length];
for (int ma = 0; ma < tags.length; ma++) {
for (int na = ma + 1; na < tags.length; na++) {
if (tags[ma] == tags[na]) {
ignore[na] = true;
}
}
}
return ignore;
}
// --- O(D) fixed: HashSet membership dedup ---
static boolean[] markDuplicatesLinear(int[] tags) {
boolean[] ignore = new boolean[tags.length];
Set<Integer> seen = new HashSet<>();
for (int i = 0; i < tags.length; i++) {
if (!seen.add(tags[i])) {
ignore[i] = true;
}
}
return ignore;
}
static int[] buildTags(int count, boolean withDups) {
int[] tags = new int[count];
// Fill with unique ascending tag IDs (TIFF spec: tags should be sorted)
for (int i = 0; i < count; i++) {
tags[i] = i;
}
if (withDups) {
// Insert duplicates at regular intervals
for (int i = count / 4; i < count; i += count / 4) {
tags[i] = tags[i - 1];
}
}
return tags;
}
static void assertResultsMatch(int[] tags) {
boolean[] q = markDuplicatesQuadratic(tags);
boolean[] l = markDuplicatesLinear(tags);
for (int i = 0; i < tags.length; i++) {
if (q[i] != l[i]) {
throw new AssertionError(
"Mismatch at index " + i + ": quadratic=" + q[i] +
" linear=" + l[i] + " tag=" + tags[i]);
}
}
}
public static void main(String[] args) {
// Correctness tests
assertResultsMatch(new int[]{});
assertResultsMatch(new int[]{1});
assertResultsMatch(new int[]{1, 2, 3});
assertResultsMatch(new int[]{1, 1, 2, 3, 3, 3, 4});
assertResultsMatch(buildTags(64, true));
assertResultsMatch(buildTags(256, true));
assertResultsMatch(buildTags(1000, false));
assertResultsMatch(buildTags(1000, true));
System.out.println("PASS: correctness checks done");
// Performance benchmark
int[] sizes = {500, 1000, 2000, 4000, 8000};
System.out.printf("%-8s %-12s %-12s %-8s%n",
"D", "quadratic(ms)", "linear(ms)", "ratio");
for (int sz : sizes) {
int[] tags = buildTags(sz, false);
long t0 = System.nanoTime();
for (int r = 0; r < 200; r++) markDuplicatesQuadratic(tags);
long quadMs = (System.nanoTime() - t0) / 1_000_000;
long t1 = System.nanoTime();
for (int r = 0; r < 200; r++) markDuplicatesLinear(tags);
long linMs = (System.nanoTime() - t1) / 1_000_000;
double ratio = linMs > 0 ? (double) quadMs / linMs : Double.NaN;
System.out.printf("%-8d %-12d %-12d %-8.1f%n",
sz, quadMs, linMs, ratio);
}
}
}