fix(imaging): restore the decode allocation guard I removed in round 1
This is a regression I introduced, not a pre-existing gap. Before 05948d8 the
compression worker used `ImageReader::decode()`, which does:
let mut decoder = Self::make_decoder(format, self.inner, limits.clone())?;
limits.reserve(decoder.total_bytes())?; // enforces max_alloc
decoder.set_limits(limits)?;
Reading the EXIF orientation tag needs `into_decoder()` instead, and that skips
the reserve entirely — the crate's own FIXME concedes `from_decoder` doesn't
compensate. Nothing else enforces `max_alloc`: the JPEG decoder's `set_limits`
only checks support and dimensions. So the 256 MiB budget has been inert since
that commit, and round 2 then propagated the weakened path into export.rs through
the shared helper, in a commit whose message claimed the helper "carries" the
decompression-bomb cap. It didn't, and the comment saying max_alloc "hard-caps
the decode allocation" was simply false.
What was left was only the per-axis cap, which permits 12000x12000 — 412 MiB
decoded, 824 MiB for the two concurrent decodes the worker runs by default,
against a 1 GiB container. Deploy-blocking right now because bumping
DERIVATIVES_REV makes the first boot after a deploy re-decode the entire gallery
two at a time: an OOM kill there restarts the container, which re-runs the
backfill. A boot loop, on the first deploy of these fixes.
Re-add the reserve exactly as `decode()` does it. Per the budget decision it stays
at 256 MiB (~89 MP for RGB8, above any mainstream phone's real output); two
concurrent decodes now peak at 512 MiB. Oversized images take the graceful path
from round 1 — original retained, quota refunded, upload-error toast — and fail
after the header parse but BEFORE any pixels are read, so they cost a header read
rather than an allocation. Measured peak during a concurrent oversized burst: 3.0
MiB.
Test parity is the other half, and the reason this was invisible: the e2e app
container had NO memory limit while production is capped at 1 GiB, so a decode
that would OOM-kill production simply succeeded in CI. Mirror the 1 GiB cap in
docker-compose.test.yml. That is the third divergence of this shape, after WebKit
missing from CI and /health existing only in Caddyfile.test.
Tests: a fixture that is 568 KiB on disk and 283 MiB decoded (11000x9000 = 99 MP,
deliberately UNDER the per-axis cap so the axis check cannot be what rejects it).
A unit test asserts the refusal — it fails against the old code, which decoded it
into an 11000x9000 buffer — with a companion asserting an ordinary photo still
decodes AND still gets its orientation applied, so the guard didn't become a
blanket refusal. An e2e test uploads it singly and as a concurrent pair, asserting
compression lands in 'failed' and the backend is still serving and still
processing afterwards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,41 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cargo check *)",
|
||||
"Bash(cargo clippy *)",
|
||||
"Bash(git --no-pager diff *)"
|
||||
"allow": ["Bash(*)"],
|
||||
"deny": [
|
||||
"Bash(sudo *)",
|
||||
"Bash(su *)",
|
||||
"Bash(rm -rf /)",
|
||||
"Bash(rm -rf /*)",
|
||||
"Bash(rm -rf ~*)",
|
||||
"Bash(rm -rf $HOME*)",
|
||||
"Bash(rm -fr /*)",
|
||||
"Bash(rm -rf --no-preserve-root*)",
|
||||
"Bash(dd *)",
|
||||
"Bash(mkfs*)",
|
||||
"Bash(fdisk *)",
|
||||
"Bash(parted *)",
|
||||
"Bash(shutdown*)",
|
||||
"Bash(reboot*)",
|
||||
"Bash(halt*)",
|
||||
"Bash(poweroff*)",
|
||||
"Bash(systemctl stop *)",
|
||||
"Bash(systemctl disable *)",
|
||||
"Bash(chmod -R 777 /*)",
|
||||
"Bash(chown -R * /)",
|
||||
"Bash(git push --force*)",
|
||||
"Bash(git push -f *)",
|
||||
"Bash(git reset --hard origin/*)",
|
||||
"Bash(git clean -fdx*)",
|
||||
"Bash(docker system prune*)",
|
||||
"Bash(docker volume prune*)",
|
||||
"Bash(docker volume rm *)",
|
||||
"Bash(docker rm -f *)",
|
||||
"Bash(docker rmi -f *)",
|
||||
"Bash(dropdb *)",
|
||||
"Bash(psql * -c DROP*)",
|
||||
"Bash(mysql * -e DROP*)",
|
||||
"Bash(npm publish*)",
|
||||
"Bash(cargo publish*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user