Matteo Panzeri/ ← index

Case study · 2026-07-12

Population-scale content scanning, measured

Two numbers decide whether a population-scale detector helps or harms: how often it flags the innocent, and how easily a sender slips past. Both are usually asserted, not measured. I measured them on the CSAM perceptual-hash example, on a deployed hash plus open ones, and had four adversarial passes try to break my own numbers first. Three of them succeeded.

Written with AI assistance and human primary-source verification; the adversarial audits referenced were independent adversarial LLM passes, not third-party review. Every legislative and technical claim carries a primary source; all code, per-image data (CSV), dependency versions and fixed seeds are published. Perceptual-hash experiments were run on benign standard test images only.

The claim

When a machine-learning classifier or perceptual hash is deployed to scan an entire population’s private messages for child-sexual-abuse material (CSAM), policy-violating content, or jailbreaks, two questions decide whether it helps or harms: how often it flags the innocent, and how easily a motivated sender slips past. As the recent EU “chat control” debate (dated note at the end) shows, both are loudly mis-stated from both directions. This piece models the first and measures its binding input, then measures the second, on the CSAM-scanning example, with open code. The honest answer turns on two axes people leave unstated: the flag-rate scales with prevalence and population scale, and evasion scales with attacker effort and the specific hash. Neither “it works” nor “it’s trivially broken” survives contact with them.

1 · Base-rate collapse: even a good detector floods the innocent

Detection accuracy is not what matters at scale; the base rate is. Let PPV be the probability that a flagged message is genuinely target material. By Bayes, PPV = (π·TPR) / (π·TPR + (1−π)·FPR), where π is prevalence and FPR the per-message false-positive rate. With a generous true-positive rate (TPR = 0.9), the model prints where PPV lives and where it reverses:

prevalence πFPR 1e−3FPR 1e−4FPR 1e−6
1e−48.3%47.4%98.9%
1e−50.9%8.3%90.0%
1e−60.09%0.9%47.4%

PPV = share of flags that are genuinely target material, TPR = 0.9. Output of base_rate_model.py.

The binding premise is the per-message FPR. In the believed regime (FPR of roughly 1e−3 to 1e−4) PPV stays low: at π = 1e−5 and FPR 1e−3, PPV ≈ 0.9%, i.e. roughly 99 in 100 flags are innocent people. The result reverses only at FPR ≤ 1e−6 and higher prevalence, so it depends on the (π, FPR) pair and is shown, not asserted.

Is that FPR regime real, or convenient? To stop assuming it, I measured a per-comparison false-match rate directly: over all 124,750 pairs of 500 benign natural images (BSDS500), how often do two different images collide within a hash’s deployed match threshold? The pairs are not independent (each image is in 499 of them, which a naive interval ignores), so the confidence intervals below are image-level bootstraps:

hashmatch thrfalse-match rateboot 95%
dHash (open, 64-bit)161.4e−3[8.4e−4, 2.1e−3]
pHash (open, 64-bit)161.8e−4[8.0e−5, 3.3e−4]
PDQ (deployed, 256-bit)310 / 124750< 3e−5

Per-comparison false-match rate on benign pairs. Output of perceptual_hash_boundary_v2.py. Open-hash intervals are image-level bootstraps; PDQ’s zero-count bound is a rule-of-3 estimate (~3e−5) that assumes independence and so under-states the true upper bound.

This empirically lower-bounds the model’s FPR input for the open hashes: dHash sits solidly in the base-rate-collapse regime (≥ 1e−4 even at its CI floor); pHash’s interval straddles 1e−4, so I can’t claim it is squarely inside. Two honest caveats keep this from being oversold. First, this is a per-comparison rate; the model’s per-message FPR is roughly this times the size of the known-bad database (a benign image is checked against every entry), so the measurement is a floor, not the deployed rate. Second, the one deployed hash tested, PDQ, produced zero false matches, so its per-comparison rate is below what 124,750 pairs can resolve. The flooding argument is therefore strongest for the open 64-bit class and, for PDQ, rests on database-scale compounding I did not measure. (For scale: Farid, PhotoDNA’s co-creator, has cited a ~1-in-50-billion false-match rate in 2019, a figure whose derivation was never published and never independently verified.)

Absolute harm, unlike PPV, is not scale-invariant: at, conservatively, ten billion private messages a day (an order-of-magnitude assumption; WhatsApp alone reports ~100B), even a 1e−4 rate flags on the order of a million innocent messages daily. An independent real-world anchor, precisely labeled: in Ireland (2020), of 4,192 NCMEC referrals, 852 (20.3%) were marked as CAM, 409 (9.7%) actionable, and 265 (6.3%) completed cases, all from a pipeline already enriched by hash-matching plus human triage, so a later stage than the model’s raw PPV and not a validation of it (“completed” is the Garda’s own case-marking, not prosecutions, which per the ICCL were not retained).

2 · The evasion boundary: a fair test, on a deployed hash

The academic record looks contradictory. Jain, Crețu & de Montjoye (USENIX Security 2022) report black-box attacks evading perceptual-hash detection on >99.9% of images. Madden, Bhavsar, Dorje & Li (NeurIPS 2024 workshop, arXiv:2406.00918) find, “contrary to existing literature … these [algorithms] demonstrate significant robustness,” attributing high evasion figures to “the random hash variations characteristic of” perceptual hashes. Both are verified to primary source, and both can hold, because “evasion rate” is under-specified.

I measured it on two open hashes (pHash, dHash; 64-bit) and, crucially, on PDQ, Meta’s 256-bit hash actually deployed for content matching (open-source; match threshold ≤31). The essential control is a query-budget-matched random baseline: an attacker making N guided queries is compared against N random ones. Over 100 benign images, five seeds, one-sided paired tests with Holm correction, the result is hash-dependent:

What about the “hash is naturally unstable” explanation? Decomposing benign transforms (JPEG-50, 0.9× resize, ×1.1 brightness, a 2° rotation), the instability is almost entirely the rotation: JPEG/resize/brightness move every hash by under a bit or two, while a 2° rotation moves PDQ ~36 bits (past its threshold for 74 of 100 images). An adversarial pass flagged that this could be an artifact of black corners from the rotation fill; re-running with edge-extension instead cut it (from 43 to 36 bits) but the effect persisted, so PDQ’s small-angle rotation sensitivity is real, and it is not covered by the dihedral (90°) database augmentation deployments use. That is a genuine residual gap, and it means less of the “evasion” at invisible budgets can be waved away as natural noise than the naive story assumes.

Limits, stated as the study’s spine

3 · What the data do and do not show

A “working” detector still floods the innocent at population scale, and the sensational evasion figures require an unstated strong-attacker, high-query, specific-hash setting. The data do not show “detectors are broken,” and do not crown Jain over Madden: the verdict is genuinely split by hash. The base-rate structure generalizes to any population-scale content-scanning classifier (moderation, jailbreak and safety filters share the low-prevalence, high-volume shape); the evasion result is perceptual-hash-specific and does not. The contribution is methodological: a reproducible way to force the question into its real variables (prevalence, scale, query budget, hash architecture, perturbation visibility) and watch the answer move.

How I red-teamed my own numbers

Most analyses show you their conclusions. This one shows the process, because on this topic the process caught me being wrong repeatedly before anything was published, and which errors a method catches is more informative than which conclusions it reaches. Every claim was verified against a primary source in both directions, and each artifact was handed to independent adversarial passes whose job was to break it. Four such passes (statistics, overclaim, code-correctness, factual) ran on the measurement itself; here are the load-bearing catches.

None of these would have been caught by a reader; several contradicted my own files. Publishing the corrected numbers is table stakes. Publishing the record of how the corrections happened is the actual demonstration of the habit a security or AI-safety role runs on: red-team your own preferred conclusion, and report the result that weakens it.

Note · the EU record, as of 9 July 2026 (dated; not settled law)

Prompted by the “chat control” debate, and because it is being argued on shaky facts, the record from the European Parliament press release (ref 20260706IPR46318): on Thursday (9 July 2026) Parliament, at second reading on the voluntary ePrivacy derogation for detecting abuse, not the mandatory client-side-scanning CSA Regulation, adopted amendments excluding end-to-end-encrypted communications from scope.

A motion to reject the amended text failed (276 in favour, 286 against, 30 abstentions), so the amended, encryption-excluding text stood and closed the second reading; the anti-scanning side lost the decisive threshold vote and the derogation advances. The mandatory permanent CSA Regulation remains in Council negotiation and is the live risk. This is a dated snapshot, not a prediction.

Reproducibility

Code, per-image data (CSV), dependency versions and fixed seeds are published: base_rate_model.py, perceptual_hash_boundary_v2.py (PDQ via the open-source pdqhash; corpus BSDS500), attack_budget_sweep_v2.py, strong_attack_check.py (with CSVs). Every legislative and technical claim is footnoted to a primary source.

Sources