SECURITY: HEIF Heist — untrusted HEIF/HEIC/AVIF decoding via libheif 1.19.8 / libde265 1.0.15 (https://heif-heist.com/) #151

Closed
opened 2026-09-26 07:21:29 +00:00 by kayg · 25 comments
Owner

HEIF Heist (https://heif-heist.com/): memory corruption / RCE in native HEIF/AVIF parsers (libheif, libde265), reachable through libvips, ImageMagick, sharp, distro packages, when a server processes attacker-controlled images. Affected release families include libheif 1.19.x-1.23.x; fixed: libheif >= 1.23.2 and the latest libde265. Attackers first fingerprint the libheif version with probe images, then send version-matched payloads.

Our exposure (runtime image deploy/Containerfile.runtime, Debian 13): libheif1 1.19.8-1+deb13u1, libheif-plugin-libde265/-dav1d 1.19.8, libde265-0 1.0.15-1+deb13u2, libvips 8.16.1 (thumbnails via 'vips thumbnail_source' in crates/plugins/files/src/thumbnails.rs), imagemagick-7 (why is it installed? which loader uses it?), ffmpeg 7.1.5 (video thumbnails/HLS; ffmpeg has its own HEIF/AVIF demux). Any user or share recipient with upload rights (incl. public file-drop links!) can make the server decode an attacker file. The server process holds every user's data, so RCE = full compromise.

Required, in order:

  1. Remove the attack surface we don't need: set VIPS_BLOCK_UNTRUSTED=1 (libvips >= 8.13) for every vips invocation so magickload and other untrusted loaders are refused; drop imagemagick from the image unless something truly needs it; restrict ffmpeg to the demuxers/decoders we need via -f/-c:v allowlists and -protocol_whitelist file,pipe.
  2. Upgrade: build libheif >= 1.23.2 and the latest libde265 (and check dav1d) from pinned, checksum-verified source tarballs in the Containerfile (or a newer Debian package if one with the fixes exists — verify the actual upstream fix commits are present, not just the version string), and link libvips against them. Record versions in the image (a /usr/share/calternal/codecs.txt) and in the admin System page.
  3. Sandbox every native decode as defence in depth: run vips/ffmpeg in a separate process with no network (unshare/landlock/seccomp as available to a rootless container), a read-only view of only the input (already stdin for vips; do the same for ffmpeg), write-only output, RLIMIT_AS/CPU/NOFILE, a wall-clock timeout, a dedicated low-privilege uid if possible, killed on timeout. The server must survive a decoder crash/timeout (thumbnail marked failed, no retry storm).
  4. Magic-byte sniffing: route files to decoders by content, not extension; refuse polyglots; size/dimension caps before decoding (header-only probe).
  5. Tests: fixture HEIC/AVIF corpus incl. malformed files (fuzz-generated), assert the server stays up, no decoder runs without the sandbox, timeouts work; adversarial probe uploads crafted HEIF/AVIF through normal upload AND public file-drop links; confirm version fingerprinting via error differences is not possible (uniform failure responses).
    Blocks merges (security).
HEIF Heist (https://heif-heist.com/): memory corruption / RCE in native HEIF/AVIF parsers (libheif, libde265), reachable through libvips, ImageMagick, sharp, distro packages, when a server processes attacker-controlled images. Affected release families include libheif 1.19.x-1.23.x; fixed: libheif >= 1.23.2 and the latest libde265. Attackers first fingerprint the libheif version with probe images, then send version-matched payloads. Our exposure (runtime image deploy/Containerfile.runtime, Debian 13): libheif1 1.19.8-1+deb13u1, libheif-plugin-libde265/-dav1d 1.19.8, libde265-0 1.0.15-1+deb13u2, libvips 8.16.1 (thumbnails via 'vips thumbnail_source' in crates/plugins/files/src/thumbnails.rs), imagemagick-7 (why is it installed? which loader uses it?), ffmpeg 7.1.5 (video thumbnails/HLS; ffmpeg has its own HEIF/AVIF demux). Any user or share recipient with upload rights (incl. public file-drop links!) can make the server decode an attacker file. The server process holds every user's data, so RCE = full compromise. Required, in order: 1. Remove the attack surface we don't need: set VIPS_BLOCK_UNTRUSTED=1 (libvips >= 8.13) for every vips invocation so magickload and other untrusted loaders are refused; drop imagemagick from the image unless something truly needs it; restrict ffmpeg to the demuxers/decoders we need via -f/-c:v allowlists and -protocol_whitelist file,pipe. 2. Upgrade: build libheif >= 1.23.2 and the latest libde265 (and check dav1d) from pinned, checksum-verified source tarballs in the Containerfile (or a newer Debian package if one with the fixes exists — verify the actual upstream fix commits are present, not just the version string), and link libvips against them. Record versions in the image (a /usr/share/calternal/codecs.txt) and in the admin System page. 3. Sandbox every native decode as defence in depth: run vips/ffmpeg in a separate process with no network (unshare/landlock/seccomp as available to a rootless container), a read-only view of only the input (already stdin for vips; do the same for ffmpeg), write-only output, RLIMIT_AS/CPU/NOFILE, a wall-clock timeout, a dedicated low-privilege uid if possible, killed on timeout. The server must survive a decoder crash/timeout (thumbnail marked failed, no retry storm). 4. Magic-byte sniffing: route files to decoders by content, not extension; refuse polyglots; size/dimension caps before decoding (header-only probe). 5. Tests: fixture HEIC/AVIF corpus incl. malformed files (fuzz-generated), assert the server stays up, no decoder runs without the sandbox, timeouts work; adversarial probe uploads crafted HEIF/AVIF through normal upload AND public file-drop links; confirm version fingerprinting via error differences is not possible (uniform failure responses). Blocks merges (security).
Author
Owner

Starting #151 on branch job/heif-hardening, based on dev at 7c1d6c82ea. Initial worktree is clean. I will verify upstream and Debian codec fixes, map all native decoder paths, and implement the required hardening steps in order.

Starting #151 on branch job/heif-hardening, based on dev at 7c1d6c82ea98ac3772117a534fbb923825a60bab. Initial worktree is clean. I will verify upstream and Debian codec fixes, map all native decoder paths, and implement the required hardening steps in order.
Author
Owner

Verified upstream and Debian package status for #151 (2026-09-26):

  • Upstream libheif v1.23.5 is the current release (tag commit 413e2a87e6a70b3eccc3a3adc5801179dd2d9e00), newer than the issue's minimum v1.23.2. v1.23.5 adds a pre-decode check of coded bitstream dimensions, including the AV1/libaom allocation issue. Its official release tarball SHA-256 is fd9036064c4432f0550d15072ddf34956a248279ee9aeaff0fba3fa0f77d8f1a.
  • Upstream libde265 v1.1.3 is current (tag commit ba62bf4cfb3242f3bf0a45617ff09e35236e4d82); it includes the security fixes released in v1.1.1 and v1.1.2. Its official release tarball SHA-256 is 554228bd17788c99a7e63b37ab5634722190e6e2bf60c1dcb01cef328e133905.
  • Debian trixie still has libheif 1.19.8-1+deb13u1; Debian sources show 1.23.4 only in sid and 1.19.7 in bookworm-backports. The trixie security build predates the Aug 25 upstream critical RCE fix, so it does not meet this issue's fixed version floor.
  • Debian trixie security has libde265 1.0.15-1+deb13u2 (security updates include CVE-2026-54240/-54241; u2 adds CVE-2026-33164). Debian testing has 1.1.2 and unstable has 1.1.3. I will build upstream 1.1.3 to satisfy the latest-upstream requirement. Trixie dav1d is 1.5.1; Debian's source package has no open security-tracker issue, so the AV1 backend can remain on that package.
  • Baseline package installation also pulls imagemagick-7-common and libmagickcore-7.q16-10 through libvips. The Containerfile does not explicitly request the ImageMagick CLI. I am inspecting the exact vips module dependency before removing those packages.

Sources: https://github.com/strukturag/libheif/releases/tag/v1.23.5 ; https://github.com/strukturag/libde265/releases/tag/v1.1.3 ; https://tracker.debian.org/pkg/libheif/news/ ; https://tracker.debian.org/pkg/libde265/news/ ; https://security-tracker.debian.org/tracker/source-package/dav1d

Verified upstream and Debian package status for #151 (2026-09-26): - Upstream `libheif` v1.23.5 is the current release (tag commit `413e2a87e6a70b3eccc3a3adc5801179dd2d9e00`), newer than the issue's minimum v1.23.2. v1.23.5 adds a pre-decode check of coded bitstream dimensions, including the AV1/libaom allocation issue. Its official release tarball SHA-256 is `fd9036064c4432f0550d15072ddf34956a248279ee9aeaff0fba3fa0f77d8f1a`. - Upstream `libde265` v1.1.3 is current (tag commit `ba62bf4cfb3242f3bf0a45617ff09e35236e4d82`); it includes the security fixes released in v1.1.1 and v1.1.2. Its official release tarball SHA-256 is `554228bd17788c99a7e63b37ab5634722190e6e2bf60c1dcb01cef328e133905`. - Debian trixie still has `libheif 1.19.8-1+deb13u1`; Debian sources show 1.23.4 only in sid and 1.19.7 in bookworm-backports. The trixie security build predates the Aug 25 upstream critical RCE fix, so it does not meet this issue's fixed version floor. - Debian trixie security has `libde265 1.0.15-1+deb13u2` (security updates include CVE-2026-54240/-54241; u2 adds CVE-2026-33164). Debian testing has 1.1.2 and unstable has 1.1.3. I will build upstream 1.1.3 to satisfy the latest-upstream requirement. Trixie `dav1d` is 1.5.1; Debian's source package has no open security-tracker issue, so the AV1 backend can remain on that package. - Baseline package installation also pulls `imagemagick-7-common` and `libmagickcore-7.q16-10` through libvips. The Containerfile does not explicitly request the ImageMagick CLI. I am inspecting the exact vips module dependency before removing those packages. Sources: https://github.com/strukturag/libheif/releases/tag/v1.23.5 ; https://github.com/strukturag/libde265/releases/tag/v1.1.3 ; https://tracker.debian.org/pkg/libheif/news/ ; https://tracker.debian.org/pkg/libde265/news/ ; https://security-tracker.debian.org/tracker/source-package/dav1d
Author
Owner

Progress on job/heif-hardening (current head 54164e309d): the bounded media signature probe is in place and its crate tests passed (6 passed, 0 failed). The baseline amd64 runtime image builds. Inspection confirms it contains libheif 1.19.8-1+deb13u1 and libde265 1.0.15-1+deb13u2, while libvips enables its Magick loader and depends on libmagickcore 7.1.1.43; magick and convert executables are absent. The final image will remove that unneeded loader and its dependency chain.

Progress on job/heif-hardening (current head 54164e309d06e2c0e4dcf3acd5ab80a64bed378f): the bounded media signature probe is in place and its crate tests passed (6 passed, 0 failed). The baseline amd64 runtime image builds. Inspection confirms it contains libheif 1.19.8-1+deb13u1 and libde265 1.0.15-1+deb13u2, while libvips enables its Magick loader and depends on libmagickcore 7.1.1.43; `magick` and `convert` executables are absent. The final image will remove that unneeded loader and its dependency chain.
Author
Owner

The real MP4 fixture (crates/plugins/files/tests/fixtures/media/source-generated.mp4) confirms the current thumbnail gap in the baseline runtime: vips thumbnail_source "[descriptor=0]" ... returns VipsForeignLoad: source is not in a known format. I am adding an FFmpeg poster path under the media sandbox so video thumbnails remain available after removing the ImageMagick loader.

The real MP4 fixture (`crates/plugins/files/tests/fixtures/media/source-generated.mp4`) confirms the current thumbnail gap in the baseline runtime: `vips thumbnail_source "[descriptor=0]" ...` returns `VipsForeignLoad: source is not in a known format`. I am adding an FFmpeg poster path under the media sandbox so video thumbnails remain available after removing the ImageMagick loader.
Author
Owner

A regression test found that the first timeout implementation killed the direct wrapper but left a sleep descendant running (/proc/<pid>/stat remained in state R after the one-second deadline). I am fixing timeout cleanup to terminate the whole decoder process group and am retesting this exact case before integrating the real decoders.

A regression test found that the first timeout implementation killed the direct wrapper but left a `sleep` descendant running (`/proc/<pid>/stat` remained in state `R` after the one-second deadline). I am fixing timeout cleanup to terminate the whole decoder process group and am retesting this exact case before integrating the real decoders.
Author
Owner

The runtime smoke test found a namespace access failure: with the deployment user mapping and proc unmask, bubblewrap still rejected /proc/<parent>/fd/N as a bind source (bwrap: Can't find source path /proc/1/fd/3: Permission denied). I am replacing parent-proc paths with inherited read-only stdin and a held output descriptor bound from the sandbox process itself. This keeps the existing directory-handle confinement and removes the cross-user-namespace procfs lookup.

The runtime smoke test found a namespace access failure: with the deployment user mapping and proc unmask, bubblewrap still rejected `/proc/<parent>/fd/N` as a bind source (`bwrap: Can't find source path /proc/1/fd/3: Permission denied`). I am replacing parent-proc paths with inherited read-only stdin and a held output descriptor bound from the sandbox process itself. This keeps the existing directory-handle confinement and removes the cross-user-namespace procfs lookup.
Author
Owner

Runtime smoke found that vips refuses to write a thumbnail when the destination file already exists (File '/calternal-output.webp' already exists. Exiting.). The held output FD approach therefore could not produce thumbnails. Image and video thumbnail jobs now write to the sandbox's private tmpfs; Rust captures at most 16 MiB, validates the WebP signature, then writes to the held cache file. I am rerunning real fixture checks against this flow.

Runtime smoke found that vips refuses to write a thumbnail when the destination file already exists (`File '/calternal-output.webp' already exists. Exiting.`). The held output FD approach therefore could not produce thumbnails. Image and video thumbnail jobs now write to the sandbox's private tmpfs; Rust captures at most 16 MiB, validates the WebP signature, then writes to the held cache file. I am rerunning real fixture checks against this flow.
Author
Owner

Resuming after the planned VM restart on branch job/heif-hardening. Base is dev at 7c1d6c82ea; current checkpoint is 230bfee7. The worktree is clean. I reviewed the earlier progress comments and am continuing with runtime, sandbox and regression checks.

Resuming after the planned VM restart on branch job/heif-hardening. Base is dev at 7c1d6c82ea98ac3772117a534fbb923825a60bab; current checkpoint is 230bfee7. The worktree is clean. I reviewed the earlier progress comments and am continuing with runtime, sandbox and regression checks.
Author
Owner

Focused verification found one regression-test defect: timeout_kills_the_whole_media_process_group writes both the shell PID and sleep PID (echo "$$ $!") but parses the full line as one u32, so it fails with ParseIntError { kind: InvalidDigit } at crates/plugins/files/src/media.rs:657 before checking child cleanup. I am correcting the parser, then I will rerun that test and the Files suite.

Focused verification found one regression-test defect: `timeout_kills_the_whole_media_process_group` writes both the shell PID and sleep PID (`echo "$$ $!"`) but parses the full line as one `u32`, so it fails with `ParseIntError { kind: InvalidDigit }` at `crates/plugins/files/src/media.rs:657` before checking child cleanup. I am correcting the parser, then I will rerun that test and the Files suite.
Author
Owner

The timeout test harness now selects the recorded sleep-child PID. Verification: focused timeout test passed; cargo test -p calternal-plugin-files passed (95 passed, 0 failed, 0 ignored; doc-tests 0). Fix committed as 65ef40abf184a78b28f5efbef49a92c8c1c78265.

The timeout test harness now selects the recorded sleep-child PID. Verification: focused timeout test passed; `cargo test -p calternal-plugin-files` passed (95 passed, 0 failed, 0 ignored; doc-tests 0). Fix committed as 65ef40abf184a78b28f5efbef49a92c8c1c78265.
Author
Owner

Merge review found the new admin codec route is absent from contracts/openapi.json: the route and its utoipa operation are present in crates/calternal-server/src/wire.rs, and main.rs tests assert the generated path, but the committed contract has no /api/v1/admin/system/codecs entry. I am refreshing the generated contract and checking the typed client so the API change stays in sync.

Merge review found the new admin codec route is absent from `contracts/openapi.json`: the route and its `utoipa` operation are present in `crates/calternal-server/src/wire.rs`, and `main.rs` tests assert the generated path, but the committed contract has no `/api/v1/admin/system/codecs` entry. I am refreshing the generated contract and checking the typed client so the API change stays in sync.
Author
Owner

The OpenAPI route and typed client are synchronized in commit 30a36821. bun run --cwd packages/api-client test output: 4 pass, 0 fail, 10 expect() calls. I will compare the committed artifacts with the server generator after the current direct compiler build completes.

The OpenAPI route and typed client are synchronized in commit 30a36821. `bun run --cwd packages/api-client test` output: 4 pass, 0 fail, 10 expect() calls. I will compare the committed artifacts with the server generator after the current direct compiler build completes.
Author
Owner

Server OpenAPI generation and TypeScript regeneration now match the committed artifacts: bash packages/api-client/check-generated.sh completed, and git diff --exit-code -- contracts/openapi.json packages/api-client/src/generated.ts returned exit 0 with no output. The contract/client commit is 6644ad7d. bun run --cwd packages/api-client test: 4 pass, 0 fail, 10 expect() calls.

Server OpenAPI generation and TypeScript regeneration now match the committed artifacts: `bash packages/api-client/check-generated.sh` completed, and `git diff --exit-code -- contracts/openapi.json packages/api-client/src/generated.ts` returned exit 0 with no output. The contract/client commit is 6644ad7d. `bun run --cwd packages/api-client test`: 4 pass, 0 fail, 10 expect() calls.
Author
Owner

Runtime verification after the planned restart: podman build --format docker --file deploy/Containerfile.runtime --build-arg BINARY=calternal-server-placeholder --build-arg AGENT_IMAGE_ARCHIVE=agent-image-empty.tar --tag localhost/calternal-heif-smoke:151 . completed with exit 0. The image manifest reports libheif 1.23.5, libde265 1.1.3, libvips 8.16.1, dav1d 1.5.1-1, and ffmpeg 7.1.5-0+deb13u1. vips -l foreign includes heifload; magickload and the magick executable are absent.

With the o2 Quadlet's rootless UID map, SYS_ADMIN, and /proc unmask settings, the sandbox decoded the repository HEIC and AVIF fixtures to WebP (256x170 and 256x171), and the MP4 fixture to WebP (256x192). A 16-byte HEIF ftyp header failed with decoder exit 1 and a zero-byte output. A first local invocation omitted the Quadlet capabilities and got bubblewrap's expected proc-mount denial; rerunning with the production settings succeeded, so no code change was needed for that result.

Runtime verification after the planned restart: `podman build --format docker --file deploy/Containerfile.runtime --build-arg BINARY=calternal-server-placeholder --build-arg AGENT_IMAGE_ARCHIVE=agent-image-empty.tar --tag localhost/calternal-heif-smoke:151 .` completed with exit 0. The image manifest reports libheif 1.23.5, libde265 1.1.3, libvips 8.16.1, dav1d 1.5.1-1, and ffmpeg 7.1.5-0+deb13u1. `vips -l foreign` includes `heifload`; `magickload` and the `magick` executable are absent. With the o2 Quadlet's rootless UID map, `SYS_ADMIN`, and `/proc` unmask settings, the sandbox decoded the repository HEIC and AVIF fixtures to WebP (256x170 and 256x171), and the MP4 fixture to WebP (256x192). A 16-byte HEIF `ftyp` header failed with decoder exit 1 and a zero-byte output. A first local invocation omitted the Quadlet capabilities and got bubblewrap's expected proc-mount denial; rerunning with the production settings succeeded, so no code change was needed for that result.
Author
Owner

Merging the current dev tip exposed a Files index regression in its unchanged-row optimization. The first build failed because the new early return in record_once returned () from a Result<bool, _> function. A regression test, index_record_of_an_unchanged_file_is_idempotent, then reproduced the behavioral failure when that return was typed as the existing retry signal: a repeated stable index write returned 409 Conflict after eight attempts. The stable-row branch now returns true to stop the retry loop; the green rerun is in progress.

Merging the current `dev` tip exposed a Files index regression in its unchanged-row optimization. The first build failed because the new early return in `record_once` returned `()` from a `Result<bool, _>` function. A regression test, `index_record_of_an_unchanged_file_is_idempotent`, then reproduced the behavioral failure when that return was typed as the existing retry signal: a repeated stable index write returned 409 Conflict after eight attempts. The stable-row branch now returns `true` to stop the retry loop; the green rerun is in progress.
Author
Owner

The duplicate-index regression is fixed and committed in 260007c7f8f808581d9a1dd01d2bf061650028e9. Red/green evidence: the new test first reproduced Failure(409, Conflict, "file kept changing while it was indexed; try again"); after the stable-row branch returned true, it passed. cargo test -p calternal-plugin-files: 99 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; doc-tests: 0 passed; 0 failed. The related calternal-fs formatting cleanup is in 8e6adf34; its unit and storage suites passed (33 and 35 tests).

The duplicate-index regression is fixed and committed in `260007c7f8f808581d9a1dd01d2bf061650028e9`. Red/green evidence: the new test first reproduced `Failure(409, Conflict, "file kept changing while it was indexed; try again")`; after the stable-row branch returned `true`, it passed. `cargo test -p calternal-plugin-files`: `99 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out`; doc-tests: `0 passed; 0 failed`. The related `calternal-fs` formatting cleanup is in `8e6adf34`; its unit and storage suites passed (`33` and `35` tests).
Author
Owner

The scoped live media probe exposed a test bug: Tus creation succeeded, but media_uploads.py used a case-sensitive Location header lookup, so it skipped both PATCH requests and reported the files missing. The probe now reads the header case-insensitively and prints all findings. Rerun against a real local server with the rootless runtime media sandbox wrapper passed: HEIF/AVIF upload probe passed. Committed as d32f0a679a1c752807a718d6753fe3b7eb8ffa60.

The scoped live media probe exposed a test bug: Tus creation succeeded, but `media_uploads.py` used a case-sensitive `Location` header lookup, so it skipped both PATCH requests and reported the files missing. The probe now reads the header case-insensitively and prints all findings. Rerun against a real local server with the rootless runtime media sandbox wrapper passed: `HEIF/AVIF upload probe passed`. Committed as `d32f0a679a1c752807a718d6753fe3b7eb8ffa60`.
Author
Owner

The first full Clippy attempt stopped in the vendored openssl-sys build with this compiler output: crypto/cmac/cmac.c:19:10: fatal error: internal/cryptlib.h: No such file or directory. The header was present after the failure, and compiling that source with the same -I. -Iinclude search paths in syntax-only mode succeeded. A Clippy retry with no source changes passed the OpenSSL build and is now checking project crates; I will report its final result with the gates.

The first full Clippy attempt stopped in the vendored `openssl-sys` build with this compiler output: `crypto/cmac/cmac.c:19:10: fatal error: internal/cryptlib.h: No such file or directory`. The header was present after the failure, and compiling that source with the same `-I. -Iinclude` search paths in syntax-only mode succeeded. A Clippy retry with no source changes passed the OpenSSL build and is now checking project crates; I will report its final result with the gates.
Author
Owner

The final Files Clippy pass found four style lints in media.rs: a manual divisibility check, collapsible duplicate-dimension parsing, and two assertions on constants. I rewrote them without changing behavior and committed 07f72e6b79b0a4dee86a866e3daca34b19f94fb4 (style(files): satisfy media parser clippy). Evidence: cargo test -p calternal-plugin-files passed with 99 tests; cargo clippy -p calternal-plugin-files --all-targets -- -D warnings exited 0. The earlier full Clippy retry had passed the vendored OpenSSL build after a transient missing-header failure, then surfaced these Files lints.

The final Files Clippy pass found four style lints in `media.rs`: a manual divisibility check, collapsible duplicate-dimension parsing, and two assertions on constants. I rewrote them without changing behavior and committed `07f72e6b79b0a4dee86a866e3daca34b19f94fb4` (`style(files): satisfy media parser clippy`). Evidence: `cargo test -p calternal-plugin-files` passed with 99 tests; `cargo clippy -p calternal-plugin-files --all-targets -- -D warnings` exited 0. The earlier full Clippy retry had passed the vendored OpenSSL build after a transient missing-header failure, then surfaced these Files lints.
Author
Owner

The final Files Clippy pass found four style lints in media.rs: a manual divisibility check, collapsible duplicate-dimension parsing, and two assertions on constants. I rewrote them without changing behavior and committed 07f72e6b79b0a4dee86a866e3daca34b19f94fb4 (style(files): satisfy media parser clippy). Evidence: cargo test -p calternal-plugin-files passed with 99 tests; cargo clippy -p calternal-plugin-files --all-targets -- -D warnings exited 0. The earlier full Clippy retry had passed the vendored OpenSSL build after a transient missing-header failure, then surfaced these Files lints.

The final Files Clippy pass found four style lints in `media.rs`: a manual divisibility check, collapsible duplicate-dimension parsing, and two assertions on constants. I rewrote them without changing behavior and committed `07f72e6b79b0a4dee86a866e3daca34b19f94fb4` (`style(files): satisfy media parser clippy`). Evidence: `cargo test -p calternal-plugin-files` passed with 99 tests; `cargo clippy -p calternal-plugin-files --all-targets -- -D warnings` exited 0. The earlier full Clippy retry had passed the vendored OpenSSL build after a transient missing-header failure, then surfaced these Files lints.
Author
Owner

The first full cargo test run reached calternal-plugin-files and reported 98 passed, 1 failed. media::tests::timeout_kills_the_whole_media_process_group observed the descendant in /proc state R after its two-second poll window. The same focused test immediately passed in isolation (1 passed, finished in 1.50s), and no matching sleep 60 process remained on the host after the suite stopped. The host was running several other Cargo builds/tests at the same time. I found no reproducible decoder process-group leak; I am rerunning the full Cargo gate under the current host conditions.

The first full `cargo test` run reached `calternal-plugin-files` and reported 98 passed, 1 failed. `media::tests::timeout_kills_the_whole_media_process_group` observed the descendant in `/proc` state `R` after its two-second poll window. The same focused test immediately passed in isolation (1 passed, finished in 1.50s), and no matching `sleep 60` process remained on the host after the suite stopped. The host was running several other Cargo builds/tests at the same time. I found no reproducible decoder process-group leak; I am rerunning the full Cargo gate under the current host conditions.
Author
Owner

A second full cargo test run (with four test threads) passed Yjs but continuous_typing_is_saved_within_the_maximum_wait missed its five-second window; the other 9 hostile_clients tests passed. The first full run had passed all 10 hostile-client tests. The focused timing test passed immediately afterward (1 passed, finished in 3.79s) with RUST_TEST_THREADS=1. Several other worktrees were running Cargo/test processes concurrently. I am doing one full workspace retry with serial Rust test execution to reduce host contention.

A second full `cargo test` run (with four test threads) passed Yjs but `continuous_typing_is_saved_within_the_maximum_wait` missed its five-second window; the other 9 `hostile_clients` tests passed. The first full run had passed all 10 hostile-client tests. The focused timing test passed immediately afterward (1 passed, finished in 3.79s) with `RUST_TEST_THREADS=1`. Several other worktrees were running Cargo/test processes concurrently. I am doing one full workspace retry with serial Rust test execution to reduce host contention.
Author
Owner

Repeated full workspace runs showed the Files timeout test polling for only two seconds after a group SIGKILL; it observed the descendant in /proc state R under shared-host build load, while the focused test passed. A direct process-group reproduction confirmed the shell and descendant share the group and both die on group kill. I extended the bounded post-kill observation to ten seconds so scheduled signal delivery has time to complete and a real survivor still fails. Commit: a6780bf110f6e9ce392da550f8ecb42de010463a (test(files): allow loaded-host signal delivery). Evidence: focused timeout test passed; Files all-targets Clippy, cargo fmt --check, and git diff --check exited 0. The full Cargo suite is being rerun.

Repeated full workspace runs showed the Files timeout test polling for only two seconds after a group `SIGKILL`; it observed the descendant in `/proc` state `R` under shared-host build load, while the focused test passed. A direct process-group reproduction confirmed the shell and descendant share the group and both die on group kill. I extended the bounded post-kill observation to ten seconds so scheduled signal delivery has time to complete and a real survivor still fails. Commit: `a6780bf110f6e9ce392da550f8ecb42de010463a` (`test(files): allow loaded-host signal delivery`). Evidence: focused timeout test passed; Files all-targets Clippy, `cargo fmt --check`, and `git diff --check` exited 0. The full Cargo suite is being rerun.
Author
Owner

Forgejo #151 — heif-hardening final report

Built

  • Pinned the runtime HEIF stack to libheif 1.23.5, libde265 1.1.3 and libvips 8.16.1. The image has no ImageMagick loader or executable.
  • Added the rootless, network-isolated media sandbox and bounded image/video signature, MIME, byte, dimension, output and runtime checks. Timeout handling terminates the process group.
  • Added the admin codec manifest API and System settings view.
  • Added HEIC, AVIF and video fixtures and a live Tus/public file-drop adversarial probe.
  • Removed two unreferenced VM restart artifacts (empty tar and placeholder ELF).

Files

  • Runtime: deploy/Containerfile.runtime, deploy/media-sandbox, deploy/media-sandbox-dropcaps.c.
  • Media and storage: crates/plugins/files/{Cargo.toml,src/{index,lib,listing,media,thumbnails}.rs,tests/fixtures/media/*}, crates/calternal-fs/src/{hls,lib,thumbnails}.rs, crates/plugins/video/src/transcode.rs.
  • Admin contract and UI: crates/calternal-server/src/{main,wire}.rs, contracts/openapi.json, packages/api-client/src/generated.ts, apps/web/src/routes/settings/admin/{AdminSection,SystemGroup}.svelte, apps/web/src/routes/settings/sections.ts.
  • Probe and lockfile: tests/adversarial/{media_uploads.py,run.sh}, Cargo.lock.

Verification

The final cargo test used RUST_TEST_THREADS=1 because this host ran concurrent Cargo jobs. All final commands exited 0.

cargo fmt --check: exit 0 (no output)

Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 38s

Finished `test` profile [unoptimized + debuginfo] target(s) in 8m 32s
test result: ok. 99 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 238.48s
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 31.50s
test result: ok. 39 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 44.20s

Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/heif-hardening/apps/web
Getting Svelte diagnostics...
svelte-check found 0 errors and 0 warnings

 Test Files  53 passed (53)
      Tests  418 passed (418)
   Duration  103.87s (transform 76%, import 10%, environment 9%, tests 5%)

cargo test also passed all doc tests. The media upload probe passed: owner registration 200, installation login 200, and HEIF/AVIF upload passed. The System page loaded the five codec rows; the Copy link action worked; browser console errors, warnings and page errors were all zero. Screenshot for visual review: /home/kayg/Developer/heif-hardening-system.png.

Known gaps

  • Model-download tests and performance benchmarks remain ignored because their external model assets are not installed or they are manual measurements.
  • No deployment or o2 verification was performed in this job; that remains an orchestrator release gate.
  • Vitest prints the existing Not implemented: Window's scrollTo() method environment warning; all 53 files and 418 tests passed.

Decisions not specified in DESIGN.md

  • Use source pins libheif 1.23.5, libde265 1.1.3 and libvips 8.16.1; disable ImageMagick loading.
  • Set image limits to 512 MiB compressed, 16,384 pixels per edge and 100 million pixels; set video limits to 1 GiB, 8,192 pixels per edge and 33,554,432 pixels.
  • Use a rootless bubblewrap sandbox with no network, dropped capabilities, a low-privilege uid, resource limits and process-group termination.
  • Let the timeout regression poll for up to ten seconds after SIGKILL so a loaded build host does not report delayed scheduling as a surviving decoder; a live process still fails the test.

Head: 9045a165d57c71077dade967dca7ff42a948291f (chore: remove VM restart artifacts). The issue remains open.

# Forgejo #151 — heif-hardening final report ## Built - Pinned the runtime HEIF stack to libheif 1.23.5, libde265 1.1.3 and libvips 8.16.1. The image has no ImageMagick loader or executable. - Added the rootless, network-isolated media sandbox and bounded image/video signature, MIME, byte, dimension, output and runtime checks. Timeout handling terminates the process group. - Added the admin codec manifest API and System settings view. - Added HEIC, AVIF and video fixtures and a live Tus/public file-drop adversarial probe. - Removed two unreferenced VM restart artifacts (empty tar and placeholder ELF). ## Files - Runtime: `deploy/Containerfile.runtime`, `deploy/media-sandbox`, `deploy/media-sandbox-dropcaps.c`. - Media and storage: `crates/plugins/files/{Cargo.toml,src/{index,lib,listing,media,thumbnails}.rs,tests/fixtures/media/*}`, `crates/calternal-fs/src/{hls,lib,thumbnails}.rs`, `crates/plugins/video/src/transcode.rs`. - Admin contract and UI: `crates/calternal-server/src/{main,wire}.rs`, `contracts/openapi.json`, `packages/api-client/src/generated.ts`, `apps/web/src/routes/settings/admin/{AdminSection,SystemGroup}.svelte`, `apps/web/src/routes/settings/sections.ts`. - Probe and lockfile: `tests/adversarial/{media_uploads.py,run.sh}`, `Cargo.lock`. ## Verification The final `cargo test` used `RUST_TEST_THREADS=1` because this host ran concurrent Cargo jobs. All final commands exited 0. ```text cargo fmt --check: exit 0 (no output) Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 38s Finished `test` profile [unoptimized + debuginfo] target(s) in 8m 32s test result: ok. 99 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 238.48s test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 31.50s test result: ok. 39 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 44.20s Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/heif-hardening/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings Test Files 53 passed (53) Tests 418 passed (418) Duration 103.87s (transform 76%, import 10%, environment 9%, tests 5%) ``` `cargo test` also passed all doc tests. The media upload probe passed: owner registration 200, installation login 200, and HEIF/AVIF upload passed. The System page loaded the five codec rows; the Copy link action worked; browser console errors, warnings and page errors were all zero. Screenshot for visual review: `/home/kayg/Developer/heif-hardening-system.png`. ## Known gaps - Model-download tests and performance benchmarks remain ignored because their external model assets are not installed or they are manual measurements. - No deployment or o2 verification was performed in this job; that remains an orchestrator release gate. - Vitest prints the existing `Not implemented: Window's scrollTo() method` environment warning; all 53 files and 418 tests passed. ## Decisions not specified in DESIGN.md - Use source pins libheif 1.23.5, libde265 1.1.3 and libvips 8.16.1; disable ImageMagick loading. - Set image limits to 512 MiB compressed, 16,384 pixels per edge and 100 million pixels; set video limits to 1 GiB, 8,192 pixels per edge and 33,554,432 pixels. - Use a rootless bubblewrap sandbox with no network, dropped capabilities, a low-privilege uid, resource limits and process-group termination. - Let the timeout regression poll for up to ten seconds after SIGKILL so a loaded build host does not report delayed scheduling as a surviving decoder; a live process still fails the test. Head: `9045a165d57c71077dade967dca7ff42a948291f` (`chore: remove VM restart artifacts`). The issue remains open.
Author
Owner

Merged into dev at 2527f0d0. Deploy (image rebuild with the pinned codecs and the sandbox) tracked on #203; Claude verifies the sandbox works in the rootless o2 container at deploy.

Merged into dev at 2527f0d0. Deploy (image rebuild with the pinned codecs and the sandbox) tracked on #203; Claude verifies the sandbox works in the rootless o2 container at deploy.
kayg closed this issue 2026-09-26 18:10:37 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#151
No description provided.