Skip to content

Design PDF resource containment and cancellation #10

Description

@FHoffarth

Successor to #8, which established the problem by measurement and is now closed. This issue
is for design, not implementation.
#8 proved what the problem is; it did not prove what the
right architecture is, and nothing here should be read as a decided solution.

What #8 established

A 64 KiB PDF makes FilePass inflate 64 MiB of metadata, at roughly 1024:1, linear to at least
256 MiB with no refusal from either library. Four reachable paths, in two libraries:

# path triggered by phase
1 catalog /Metadata XMP getMetadata() inspection + verification, 2× on the source
2 content streams getTextContent() verification, conditional
3 ObjStm PDFDocument.load / getDocument parsing itself
4 XRef stream PDFDocument.load / getDocument parsing itself

The only limit FilePass owns today is the 50 MB compressed input cap in sniff.ts, which
does not bound the decompressed size at all.

Equally established, and it constrains the design: the UI does not freeze, the tab does
not crash, and the file is still cleaned and verified correctly. This is CPU and memory on
the user's own device in a local-only tool, not a server-side DoS.

Three separate design questions

They are listed separately because they have different answers and may have different
verdicts. Any of them may reasonably end in "accept as documented limit".

1. Containment of parser decompression

FilePass does not perform the decompression, so it cannot bound it the way it bounds PNG.
Paths 3 and 4 happen during parsing, before FilePass knows anything about the file.

Open, not decided: whether to intercept bytes before any parser sees them, whether to
bound only what is cheaply identifiable, whether a limit can be pushed into the libraries,
whether to bound observationally (time/memory) instead of structurally, or whether to
document the limit and do nothing. Pre-inflating every flate stream ourselves is one
candidate among several and is explicitly not the assumed answer — it is close to
reimplementing part of a PDF parser, which carries its own trust cost.

2. Cancellation and run ownership

Measured: the abandoned run keeps working for 4.6 s after the user has their next answer.
runRef filters results, it does not cancel; there is no AbortController anywhere and
loadingTask.destroy() is never used to interrupt.

Open: what "the user moved on" should mean, whether the run token should own a cancellation
scope, what a partially cancelled verification means for the trust model, and whether
cancellation can be made safe given that a half-finished verification must never produce a
verdict.

3. Concurrency control

Measured: 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.

Open: whether PDF work should be serialised, whether a newer run should displace an older
one, and how that interacts with question 2.

Constraints carried over

  • No replacement of pdf-lib or pdfjs-dist merely because they are dependencies.
  • No global resource framework built by default; PNG's budget is not to be reused out of
    convenience — it works because FilePass holds that reader itself, which is not the case here.
  • No change to verification semantics as a side effect.
  • A design that concludes "document the limit, change nothing" is a legitimate outcome.

Done when

Each of the three questions has a written verdict — designed, deferred, or accepted as a
documented limit — with the reasoning recorded. Implementation is a separate decision after
that.

Severity

P2. Real and measured, but on the user's own device, with no incorrect sanitisation result
and no fail-open.

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions