Files
erofs-freebsd-out-tree/docs/pre15-stage0/phase0-B-ZSTD-001.md
T
2026-08-18 09:20:44 +02:00

4.6 KiB

Phase 0: B-ZSTD-001 Remediation

Status: FIXED_STATICALLY; runtime corruption-tail verification remains NOT_TESTED in this remediation pass.

Problem

Strict audit item B-ZSTD-001 / BUG-ZSTD-035 found that ordinary ZSTD extents advertised subextent support. A prefix or middle read could therefore decode only the requested output and accept z_erofs_zstd_finish() without checking the remaining compressed stream. Corruption after the requested range could be skipped.

P15-086 already authorizes partial ordinary reads only for LZ4, LZMA, and Deflate. Its ZSTD result is FULL_FALLBACK because the partial candidate used more temporary memory than full decode.

Call Graph

For an ordinary mapped compressed read:

  1. z_erofs_do_read() calls z_erofs_decode_length() with mapoff and want.
  2. z_erofs_decode_length() sets partial=true and decoded_len=mapoff+want when the descriptor reports supports_subextent and the request ends before the extent.
  3. z_erofs_decode_extent() passes that length and mode to z_erofs_decompress().
  4. z_erofs_zstd_decompress() fills the shortened output and calls z_erofs_zstd_finish().
  5. z_erofs_zstd_finish() intentionally permits partial decoding without requiring stream end. That behavior remains necessary for the separately defined EROFS_MAP_PARTIAL_REF bounded-prefix path.

The smallest correct policy fix is to stop ordinary ZSTD reads at step 2: ZSTD now advertises .supports_subextent = 0. z_erofs_decode_length() then selects the full extent for ordinary prefix and middle reads. The explicit EROFS_MAP_PARTIAL_REF branch still sets partial=true and remains unchanged.

Changes

  • Set the ZSTD descriptor capability to false in src/decompressor_zstd.c; no finish validation was weakened or bypassed.
  • Updated B27 assertions to preserve ZSTD partial-reference completion while requiring ordinary ZSTD full fallback.
  • Updated B28 source and extracted-policy assertions, host report, and QEMU corruption decision so ZSTD is FULL_FALLBACK; LZ4/LZMA/Deflate policy is unchanged.

Verification

Commands and actual results:

  • git diff --check: PASS.
  • sh -n repo-pre-15/tests/pre15/cases/B27-stream-tail.sh and sh -n repo-pre-15/tests/pre15/cases/B28-partial.sh: PASS.
  • PRE15_EVIDENCE_ROOT=/tmp/erofs-phase0-b28-20260818-r2 timeout -k 10 600 repo-pre-15/tests/pre15/run-host.sh B28-partial: PASS; cleanup PASS, target reached. Evidence: /tmp/erofs-phase0-b28-20260818-r2/20260818T070848Z-host-B28-partial-1780769-0/. The source audit reports zstd=false, ZSTD in current_full_fallback, current_partial=[deflate,lz4,lzma], and partial-reference preserved.
  • PRE15_EVIDENCE_ROOT=/tmp/erofs-phase0-b27-20260818 timeout -k 10 600 repo-pre-15/tests/pre15/run-host.sh B27-stream-tail: RUNNER_FAIL, cleanup PASS; the pre-existing B29 Deflate context-pool delta fails B27's frozen exact-transform audit before the completion harness. This is not claimed as ZSTD runtime evidence.
  • P15-086 frozen gate result and policy were inspected, not rerun; its existing recorded decision remains ZSTD FULL_FALLBACK.

The B28 host case generated and compared real EROFS fixtures and ran the descriptor/decode-length source audit, but did not load a FreeBSD KLD or read an image through the kernel. No full feature suite was run.

Not Tested

NOT_TESTED: a real FreeBSD runtime read of an EROFS ZSTD image whose stream is corrupted after an ordinary prefix or middle request. The required runtime threshold is that the short ordinary read returns positive EINTEGRITY (97), matching full fallback, while a valid ordinary prefix/middle/tail read matches the source bytes.

NOT_TESTED: runtime confirmation that EROFS_MAP_PARTIAL_REF retains its bounded-prefix semantics on a real image after this policy change.

Risk

Ordinary ZSTD prefix and middle reads now decode the complete extent. This may increase CPU or temporary output work relative to the previously incorrect partial path, but it restores corruption visibility and matches P15-086 policy. The changed tests do not alter test fixtures, image bytes, test environment, or finish-error behavior.

Runtime Gate

Before claiming Phase 0 runtime closure, run the existing focused B28 runtime case with ZSTD enabled and verify valid prefix, middle, and tail reads, an after-request-range corruption case returning EINTEGRITY, full corruption returning EINTEGRITY, and the real partial-reference case. Record separate PASS, INFRA_BLOCKED, or NOT_TESTED results; do not infer runtime behavior from the extracted C harness.