Skip to content

Bound the judge, and name what it would not finish - #22

Merged
tannevaled merged 2 commits into
mainfrom
poppler-timeout
Aug 31, 2026
Merged

Bound the judge, and name what it would not finish#22
tannevaled merged 2 commits into
mainfrom
poppler-timeout

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

pdfimages -list does not return on pdfforms/gh-qpdf/qpdf_qtest_qpdf_shared-unnamed-field.pdf, a document of 2496 bytes. Neither does pdfimages, and neither does pdfinfo — all three confirmed here under a 25-second bound, rc=124 each. Two sweeps stalled on it and had to be killed by hand.

The hang is a property of the judge. What makes it dangerous is that a hang and a long job are indistinguishable from outside: a sweep that stops dead at document 900 of 2268 is waited on rather than investigated, and the wait has no end.

What changed

Every invocation of a poppler tool in this repository now goes through internal/poppler, under one bound, and a document that exceeds it is recorded by name with the tool that hungMissing.Hung and Result.Tool, Summary.Hung in the baseline record, a named line in both commands' reports. It is never dropped and never retried.

Three consequences are decisions rather than fallout, and each has a test:

  • The deadline is read off the context, not off the error. A killed process reports a signal and a tool that merely failed reports a status, so the error alone cannot tell a hang from a refusal.
  • A pdfinfo that hangs is not an unopenable document. blame asks poppler whether it would open what ours refused; counting no answer as "neither would open it" would credit the document as unopenable, which is subtracted from a population's real size — so it would shrink the denominator every rate is quoted over.
  • A listing that hangs does not make a page's pictures colour-converted. The listing decides which bucket every picture on the page lands in, so a page whose listing never came back would be tallied as wholly converted: a real number in a real column, indistinguishable from a page of CMYK.

The bound is not calibrated from timings, and says so in the code: the machine is shared, so a duration measured on it measures the other job as much as this one, and a bound read off a loaded machine would fire on documents that are merely large. It is two minutes — far above any plausible handling of one page — -timeout on both commands, and the value a run used is recorded beside the gate, because a run under a shorter bound names as hung what a longer one measures.

The bound is exercised on something that genuinely does not return (sleep) rather than on poppler, which is absent from the machine that runs the tests and would fail instantly — that would leave the branch that matters untested while looking covered.

Gate

gofmt, go vet, -race, exact 100% statement coverage and the nine cross-compile targets, all under GOTOOLCHAIN=go1.27.0, GOWORK=off, CGO_ENABLED=0.

Closes #21

pdfimages does not return on pdfforms/gh-qpdf/qpdf_qtest_qpdf_shared-unnamed-field.pdf,
2496 bytes, and neither does pdfimages -list nor pdfinfo. Two sweeps stalled on
it and were killed by hand. The hang is a property of the judge; what makes it
dangerous is that a hang and a long job are indistinguishable from outside, so
a sweep that stops dead at document 900 of 2268 is waited on rather than
investigated.

Every invocation of a poppler tool now goes through internal/poppler under one
bound, and a document that exceeds it is recorded BY NAME with the tool that
hung — Missing.Hung and Result.Tool, Summary.Hung in the record, a named line
in both reports. It is never dropped and never retried.

Three consequences are decisions rather than fallout.

The deadline is read off the context, not off the error: a killed process
reports a signal and a tool that merely failed reports a status, so the error
cannot tell a hang from a refusal.

A pdfinfo that hangs is not an unopenable document. Counting no answer as
"neither would open it" would credit the document as unopenable, which is
subtracted from a population's real size — so it would shrink the denominator
every rate is quoted over.

A listing that hangs does not make a page's pictures colour-converted. The
listing decides which bucket every picture on the page lands in, so a page
whose listing never came back would be tallied as wholly converted: a real
number in a real column, indistinguishable from a page of CMYK.

The bound is not calibrated from timings and says so. The machine is shared, so
a duration measured on it measures the other job too, and a bound read off a
loaded machine would fire on documents that are merely large. It is two
minutes, far above any plausible handling of one page, -timeout on both
commands, and the value a run used is recorded beside the gate because a run
under a shorter bound names as hung what a longer one measures.

Closes #21
pdfimages prints its version on STDERR, so reading only stdout recorded an
empty judge in every baseline record — and the judge is half the measurement: a
filter whose agreement falls because poppler changed has not regressed, and
telling those apart afterwards is impossible if nobody wrote down which poppler
it was.

poppler.Combined sits beside poppler.Run for that one call, and for that one
only: a listing read column by column must not have the tool's warnings folded
into the table, where a line of complaint is a row.
@tannevaled

Copy link
Copy Markdown
Contributor Author

The bound fires on the real document, end to end. With the document that hangs as its own one-document population and -timeout 20s:

$ compare -dir /Users/Shared/calrgb-check -only check -timeout 20s
check	0 compared	1 not
	    1  hung
	slowest page 0s, 0 over the threshold
	hung  pdftoppm  /Users/Shared/calrgb-check/check/qpdf_qtest_qpdf_shared-unnamed-field.pdf page 1

And one honesty about images, which is where the two stalled sweeps were. That document opens for us and its first page draws no picture, so render.Images returns nothing and judgePage never asks poppler about it at all — images runs the whole gh-qpdf population without touching the hang. The bound is therefore unexercised by that command on this corpus, and the re-measurement will say so rather than let a reader infer that nothing hangs. What the bound protects against is the next document, and compare above is the one that meets this one.

@tannevaled
tannevaled merged commit d6ff5ba into main Aug 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A sweep that shells out to poppler needs a timeout: pdfimages -list hangs on a real corpus document

1 participant