You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #10, which decided B) document/accept for v0.1, harden later. #8 established
the measurements; #10 chose the trade. This issue carries the hardening that was deferred.
Two workstreams, deliberately kept apart. They differ in risk by an order of magnitude, and
the whole point of splitting them is that the trust-sensitive one must not ride along in the
simple one. If a PR touches both, it is the wrong PR.
Workstream 1 — Concurrency guard (low risk)
What it fixes. Measured in #8: three attack files at once cost 2.91× a single one, fully
additive. In the browser a harmless PDF took 1.4× longer while an abandoned run was still
active. This is the only multiplicative element in the problem.
Shape. At most one active PDF analysis per tab. Serialise before inspectFile / cleanAndVerify. No worker changes, no library changes.
Why it is the safe half. It does not touch verifyClean, the verdict predicate, or any
pdfjs call site. The one rule it must keep: a displaced or queued run must never produce a
verdict — the existing runRef result filter already establishes that intent and this must
not weaken it.
Done when. Concurrent PDF work is bounded, the accumulation measurement no longer
reproduces, and no displaced run can reach a verdict.
What it fixes. Measured in #8: an abandoned run keeps working for 4.6 s after the user
already has their next answer. runRef filters results, it does not cancel. There is no AbortController anywhere, and loadingTask.destroy() is never used to interrupt.
Available surface, checked rather than assumed: pdfjs loadingTask.destroy() = true, PDFWorker exported with destroy() = true, pdf-lib abort API = false. So cancellation
can cover roughly 100 % of the XMP path and ~55 % of the ObjStm path; pdf-lib's share stays
structurally uncancellable without moving it off the main thread, which is out of scope here.
Why it is the dangerous half.verifyClean has several await points. A destroyed task
makes them reject, and today that lands on unresolved → unverified. That is fail-closed
by accident, not by design. Do not build on it. A cancelled run must be incapable of
producing "verified", and equally must not manufacture a false refusal that a user would read
as a statement about their file.
Done when. A superseded run stops real work; a cancelled verification can produce no
verdict of any kind; and the distinction between "refused because of the file" and "stopped
because you moved on" is visible to the user and to the tests.
Explicitly not in scope
Watchdog / timeout refusal (option 4 in Design PDF resource containment and cancellation #10). Only if device-dependent refusal is
deliberately accepted — a time limit makes "FilePass refuses this file" stop being a
property of the file. That needs its own decision, not a side effect of this work.
Replacing pdf-lib or pdfjs-dist.
A global resource framework, or reuse of the PNG budget — that one works because FilePass
holds the reader itself, which is not the case here.
Moving pdf-lib into a worker.
Severity
P2. Real and measured, on the user's own device, with no incorrect sanitisation and no
fail-open. Sequenced after v0.1 by the decision in #10.
Follow-up to #10, which decided
B) document/accept for v0.1, harden later. #8 establishedthe measurements; #10 chose the trade. This issue carries the hardening that was deferred.
Two workstreams, deliberately kept apart. They differ in risk by an order of magnitude, and
the whole point of splitting them is that the trust-sensitive one must not ride along in the
simple one. If a PR touches both, it is the wrong PR.
Workstream 1 — Concurrency guard (low risk)
What it fixes. Measured in #8: three attack files at once cost 2.91× a single one, fully
additive. In the browser a harmless PDF took 1.4× longer while an abandoned run was still
active. This is the only multiplicative element in the problem.
Shape. At most one active PDF analysis per tab. Serialise before
inspectFile/cleanAndVerify. No worker changes, no library changes.Why it is the safe half. It does not touch
verifyClean, the verdict predicate, or anypdfjs call site. The one rule it must keep: a displaced or queued run must never produce a
verdict — the existing
runRefresult filter already establishes that intent and this mustnot weaken it.
Done when. Concurrent PDF work is bounded, the accumulation measurement no longer
reproduces, and no displaced run can reach a verdict.
Workstream 2 — Cancellation architecture (trust-sensitive)
What it fixes. Measured in #8: an abandoned run keeps working for 4.6 s after the user
already has their next answer.
runReffilters results, it does not cancel. There is noAbortControlleranywhere, andloadingTask.destroy()is never used to interrupt.Available surface, checked rather than assumed:
pdfjs loadingTask.destroy() = true,PDFWorkerexported withdestroy() = true,pdf-libabort API = false. So cancellationcan cover roughly 100 % of the XMP path and ~55 % of the ObjStm path; pdf-lib's share stays
structurally uncancellable without moving it off the main thread, which is out of scope here.
Why it is the dangerous half.
verifyCleanhas several await points. A destroyed taskmakes them reject, and today that lands on
unresolved→unverified. That is fail-closedby accident, not by design. Do not build on it. A cancelled run must be incapable of
producing "verified", and equally must not manufacture a false refusal that a user would read
as a statement about their file.
Done when. A superseded run stops real work; a cancelled verification can produce no
verdict of any kind; and the distinction between "refused because of the file" and "stopped
because you moved on" is visible to the user and to the tests.
Explicitly not in scope
deliberately accepted — a time limit makes "FilePass refuses this file" stop being a
property of the file. That needs its own decision, not a side effect of this work.
pdf-liborpdfjs-dist.holds the reader itself, which is not the case here.
pdf-libinto a worker.Severity
P2. Real and measured, on the user's own device, with no incorrect sanitisation and no
fail-open. Sequenced after v0.1 by the decision in #10.
🤖 Generated with Claude Code