lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit d03abe2a9ef46a5fb5568916bd0b4f215ec3dff7
parent 70e808bfa685cc205034bc0feb947bac6a68162d
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Jul 2026 00:39:57 +0000

blossom: fix raster fuzz corpus

- execute all thirty conformance cases from their final governed bytes
- assign each case to one exact raw format-specific fuzz seed
- validate the target-to-public-operation call chain structurally
- record deterministic smoke and bounded sanitizer campaign evidence

Diffstat:
MCargo.toml | 1+
Mbuild/nix/apps.nix | 7+++++++
Mbuild/nix/checks.nix | 9+++++++++
Mbuild/nix/common.nix | 102++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mbuild/nix/devshells.nix | 9+++++++++
Mbuild/nix/toolchains.nix | 2++
Acontracts/conformance/vectors/blossom/raster_decoder_security.v1.json | 410+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/blossom/tests/decoder_security.rs | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Acrates/blossom/tests/fixtures/raster_decoder_security.v1.json | 410+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/blossom-raster-decoder-security.md | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzz/.gitignore | 2++
Afuzz/Cargo.lock | 748+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzz/Cargo.toml | 36++++++++++++++++++++++++++++++++++++
Afuzz/corpus/publication_jpeg/jpeg_baseline_cmyk_8bit.bin | 0
Afuzz/corpus/publication_jpeg/jpeg_baseline_gray_8bit.bin | 0
Afuzz/corpus/publication_jpeg/jpeg_baseline_rgb_8bit.bin | 0
Afuzz/corpus/publication_jpeg/jpeg_entropy_truncated.bin | 0
Afuzz/corpus/publication_jpeg/jpeg_extended_sequential_sof1.bin | 0
Afuzz/corpus/publication_jpeg/jpeg_huffman_overfull.bin | 0
Afuzz/corpus/publication_jpeg/jpeg_progressive_forbidden.bin | 0
Afuzz/corpus/publication_jpeg/jpeg_truncated.bin | 0
Afuzz/corpus/publication_jpeg/jpeg_twelve_bit_forbidden.bin | 0
Afuzz/corpus/publication_jpeg/jpeg_wrong_restart_marker.bin | 0
Afuzz/corpus/publication_png/png_adam7_rgba_8bit.bin | 0
Afuzz/corpus/publication_png/png_animation_forbidden.bin | 0
Afuzz/corpus/publication_png/png_corrupt_crc.bin | 0
Afuzz/corpus/publication_png/png_corrupt_deflate.bin | 0
Afuzz/corpus/publication_png/png_dimension_over_limit.bin | 0
Afuzz/corpus/publication_png/png_gray_8bit.bin | 0
Afuzz/corpus/publication_png/png_gray_alpha_8bit.bin | 0
Afuzz/corpus/publication_png/png_indexed_8bit.bin | 0
Afuzz/corpus/publication_png/png_pixel_limit.bin | 0
Afuzz/corpus/publication_png/png_rgb_8bit.bin | 0
Afuzz/corpus/publication_png/png_rgba_8bit.bin | 0
Afuzz/corpus/publication_png/png_sixteen_bit_forbidden.bin | 0
Afuzz/corpus/publication_png/png_truncated.bin | 0
Afuzz/corpus/publication_webp/webp_animation_forbidden.bin | 0
Afuzz/corpus/publication_webp/webp_duplicate_primary.bin | 0
Afuzz/corpus/publication_webp/webp_lossless_8bit.bin | 0
Afuzz/corpus/publication_webp/webp_lossless_alpha_8bit.bin | 0
Afuzz/corpus/publication_webp/webp_lossy_8bit.bin | 0
Afuzz/corpus/publication_webp/webp_lossy_alpha_8bit.bin | 0
Afuzz/corpus/publication_webp/webp_truncated.bin | 0
Afuzz/fuzz_targets/common.rs | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzz/fuzz_targets/publication_jpeg.rs | 9+++++++++
Afuzz/fuzz_targets/publication_png.rs | 9+++++++++
Afuzz/fuzz_targets/publication_webp.rs | 9+++++++++
Arust-toolchain-fuzz.toml | 4++++
Mtools/xtask/src/contract.rs | 2++
Atools/xtask/src/contract/blossom_raster_decoder_security.rs | 1768+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/contract/nip09_reconciliation.rs | 17+++++++++++++----
Mtools/xtask/src/contract/phase1_publication_media_readiness.rs | 32+++++++++++++++++++-------------
52 files changed, 3940 insertions(+), 36 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +exclude = ["fuzz"] members = [ "crates/core", "crates/event", diff --git a/build/nix/apps.nix b/build/nix/apps.nix @@ -68,6 +68,13 @@ in command = common.contractCommand; }; + decoder-security = mkRepoApp { + name = "decoder-security"; + description = "Run Blossom decoder corpus, differential, RSS, and fuzz gates"; + runtimeInputs = common.runtimeInputs.decoderSecurity; + command = common.decoderSecurityCommand; + }; + coverage-report = mkRepoApp { name = "coverage-report"; description = "Generate coverage reports and blocking gate artifacts"; diff --git a/build/nix/checks.nix b/build/nix/checks.nix @@ -98,6 +98,15 @@ in cargo-test = cargoTest; blossom-no-default-check = blossomNoDefaultCheck; blossom-raster-decode-test = blossomRasterDecodeTest; + blossom-decoder-fuzz-smoke = common.mkRepoCheck { + name = "radroots-blossom-decoder-fuzz-smoke"; + runtimeInputs = common.runtimeInputs.decoderSecurityFuzz; + cargoDeps = common.fuzzCargoDeps; + command = '' + export CARGO_NET_OFFLINE=true + ${common.decoderSecurityFuzzCommand} + ''; + }; replica-sync-default-check = replicaSyncDefaultCheck; replica-sync-default-test = replicaSyncDefaultTest; replica-sync-legacy-ingest-check = replicaSyncLegacyCheck; diff --git a/build/nix/common.nix b/build/nix/common.nix @@ -98,9 +98,22 @@ let pkgs.imagemagick pkgs.time ]; + decoderSecurityFuzzRuntimeInputs = stableRuntimeInputs ++ [ + toolchains.fuzz + pkgs.cargo-fuzz + ]; decoderSecurityIosRuntimeInputs = stableRuntimeInputs ++ [ toolchains.ios ]; + decoderSecurityRuntimeInputs = stableRuntimeInputs ++ [ + toolchains.fuzz + pkgs.cargo-fuzz + pkgs.imagemagick + pkgs.time + ]; + fuzzCargoDeps = pkgs.rustPlatform.importCargoLock { + lockFile = ../../fuzz/Cargo.lock; + }; releaseRuntimeInputs = coverageRuntimeInputs; coreContractCrates = [ "xtask" @@ -165,26 +178,39 @@ let env ? sharedEnv, initGit ? false, linuxOnly ? false, + cargoDeps ? null, }: if linuxOnly && !pkgs.stdenv.isLinux then null else - pkgs.runCommand name { nativeBuildInputs = runtimeInputs; } '' - export HOME="$TMPDIR/home" - mkdir -p "$HOME" - - cp -R ${repoSource} "$TMPDIR/repo" - chmod -R u+w "$TMPDIR/repo" - cd "$TMPDIR/repo" - export RADROOTS_WORKSPACE_ROOT="$PWD" - - ${exportEnv env} - ${lib.optionalString initGit initGitRepo} - - ${command} - - touch "$out" - ''; + pkgs.runCommand name + ( + { + nativeBuildInputs = + runtimeInputs + ++ lib.optionals (cargoDeps != null) [ + pkgs.rustPlatform.cargoSetupHook + ]; + } + // lib.optionalAttrs (cargoDeps != null) { inherit cargoDeps; } + ) + '' + export HOME="$TMPDIR/home" + mkdir -p "$HOME" + + cp -R ${repoSource} "$TMPDIR/repo" + chmod -R u+w "$TMPDIR/repo" + cd "$TMPDIR/repo" + export RADROOTS_WORKSPACE_ROOT="$PWD" + + ${exportEnv env} + ${lib.optionalString (cargoDeps != null) "cargoSetupPostUnpackHook"} + ${lib.optionalString initGit initGitRepo} + + ${command} + + touch "$out" + ''; ensureRepoRoot = '' if [ ! -f Cargo.toml ] || [ ! -f flake.nix ]; then echo "run this command from the radroots workspace checkout" >&2 @@ -205,6 +231,20 @@ let stable_cargo=${toolchains.stable}/bin/cargo magick=${pkgs.imagemagick}/bin/magick + "$stable_cargo" test -p radroots_blossom \ + --no-default-features \ + --features raster-decode,serde \ + --test decoder_security \ + decoder_regression_corpus_executes_every_case + + RADROOTS_INDEPENDENT_RASTER_DECODER=${pkgs.imagemagick}/bin/magick \ + "$stable_cargo" test -p radroots_blossom \ + --no-default-features \ + --features raster-decode,serde \ + --test decoder_security \ + decoder_differential_matches_independent_backend \ + -- --ignored --exact + test_executable="$($stable_cargo test -p radroots_blossom \ --no-default-features \ --features raster-decode,serde \ @@ -350,6 +390,31 @@ let printf '%s\n' "$archive_members" | grep -F -- '-vp8l_dec.o' >/dev/null echo "aarch64-apple-ios static link verified: $ios_archive" ''; + decoderSecurityFuzzCommand = '' + fuzz_cargo=${toolchains.fuzz}/bin/cargo + fuzz_runner=${pkgs.cargo-fuzz}/bin/cargo-fuzz + + cargo_target_root="''${CARGO_TARGET_DIR:-$TMPDIR/cargo-target}" + mkdir -p "$cargo_target_root" + smoke_root="$(mktemp -d "$cargo_target_root/decoder-security-fuzz-smoke.XXXXXX")" + mkdir -p "$smoke_root/corpus" "$smoke_root/artifacts" + cp -R fuzz/corpus/. "$smoke_root/corpus/" + + export PATH=${toolchains.fuzz}/bin:${pkgs.cargo-fuzz}/bin:$PATH + export CARGO="$fuzz_cargo" + export CARGO_TARGET_DIR="$cargo_target_root" + for target in publication_jpeg publication_png publication_webp; do + mkdir -p "$smoke_root/artifacts/$target" + "$fuzz_runner" run --fuzz-dir fuzz "$target" "$smoke_root/corpus/$target" -- \ + -runs=256 \ + -seed=424242 \ + -max_len=65536 \ + -timeout=5 \ + -rss_limit_mb=2048 \ + -artifact_prefix="$smoke_root/artifacts/$target/" + done + ''; + decoderSecurityCommand = decoderSecurityStableCommand + decoderSecurityFuzzCommand; releasePreflightCommand = '' cargo check -q cargo test -q -p xtask @@ -501,8 +566,11 @@ in coverageReportCommand craneLib ensureRepoRoot + decoderSecurityFuzzCommand decoderSecurityIosCommand + decoderSecurityCommand decoderSecurityStableCommand + fuzzCargoDeps mkRepoCheck releasePreflightCommand coreContractCargoArgs @@ -517,6 +585,8 @@ in runtimeInputs = { stable = stableRuntimeInputs; coverage = coverageRuntimeInputs; + decoderSecurity = decoderSecurityRuntimeInputs; + decoderSecurityFuzz = decoderSecurityFuzzRuntimeInputs; decoderSecurityIos = decoderSecurityIosRuntimeInputs; decoderSecurityStable = decoderSecurityStableRuntimeInputs; release = releaseRuntimeInputs; diff --git a/build/nix/devshells.nix b/build/nix/devshells.nix @@ -12,6 +12,10 @@ let ${common.exportCoverageEnv} export PATH=${toolchains.stable}/bin:${toolchains.coverage}/bin:$PATH ''; + decoderSecurityHook = '' + ${common.exportSharedEnv} + export PATH=${toolchains.fuzz}/bin:${toolchains.stable}/bin:$PATH + ''; in { default = pkgs.mkShell { @@ -26,6 +30,11 @@ in shellHook = coverageHook; }; + decoder-security = pkgs.mkShell { + packages = common.runtimeInputs.decoderSecurity; + shellHook = decoderSecurityHook; + }; + release = pkgs.mkShell { packages = common.runtimeInputs.release; shellHook = coverageHook; diff --git a/build/nix/toolchains.nix b/build/nix/toolchains.nix @@ -4,5 +4,7 @@ coverage = pkgs.rust-bin.fromRustupToolchainFile ../../rust-toolchain-coverage.toml; + fuzz = pkgs.rust-bin.fromRustupToolchainFile ../../rust-toolchain-fuzz.toml; + ios = pkgs.rust-bin.fromRustupToolchainFile ../../rust-toolchain-ios.toml; } diff --git a/contracts/conformance/vectors/blossom/raster_decoder_security.v1.json b/contracts/conformance/vectors/blossom/raster_decoder_security.v1.json @@ -0,0 +1,410 @@ +{ + "suite": "blossom_raster_decoder_security", + "contract_version": "1.0.0", + "vectors": [ + { + "id": "jpeg_baseline_gray_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffc0000b080001000201011100ffc40014000100000000000000000000000000000006ffc40014100100000000000000000000000000000000ffda0008010100003f0066ffd9", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "jpeg_baseline_rgb_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc00011080001000203011100021101031101ffc40014000100000000000000000000000000000006ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000506ffc40014110100000000000000000000000000000000ffda000c03010002110311003f00569a1effd9", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "jpeg_baseline_cmyk_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffee000e41646f626500640000000002ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc00014080001000204011100021101031101041100ffc4001500010100000000000000000000000000000709ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000507ffc40014110100000000000000000000000000000000ffda000e040100021103110400003f009a4a01821bffd9", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "jpeg_extended_sequential_sof1", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc10011080001000203011100021101031101ffc40014000100000000000000000000000000000006ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000506ffc40014110100000000000000000000000000000000ffda000c03010002110311003f00569a1effd9", + "mutation": "jpeg_sof1" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "png_gray_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d4948445200000002000000010800000000d14920560000000b4944415408d76360f80f0001020100dac612cc0000000049454e44ae426082", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "png_rgb_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000020000000108020000007b40e8dd0000000f4944415408d763f8cfc0c0c0f01f00070001ff76d5a7600000000049454e44ae426082", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "png_indexed_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d4948445200000002000000010803000000c3fc8fb800000006504c5445ff00000000ff6ca1fd8e0000000b4944415408d7636060040000040002270291ee0000000049454e44ae426082", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "png_gray_alpha_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000020000000108040000005e2bb7010000000d4944415408d7636468f8cf000003880181aebdb1c70000000049454e44ae426082", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "png_rgba_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d4948445200000002000000010806000000f4227f8a000000114944415408d763fccfc0d0c0c8f09f01000c8c0281d7581d390000000049454e44ae426082", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "png_adam7_rgba_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d4948445200000008000000080806000001b3088e1d000000494944415418d3958dc9090020100367c1526cc6626cc662b4188b890f513c40705e61d84d1008812c920460821ea6c904b1609eba8bf1337085c053dc1d15bf8993abe2fbc040cf89065fac1a04c2d2db980000000049454e44ae426082", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 8, + "height": 8 + } + }, + { + "id": "webp_lossless_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "webp", + "bytes_hex": "524946461e000000574542505650384c110000002f0100000007d0cc22f6aeff8188e87f0000", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "webp_lossy_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "webp", + "bytes_hex": "524946462e0000005745425056503820220000005001009d012a0200010001402625004e80280000fee246baf5a5b9b84c4619d60000", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "webp_lossless_alpha_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "webp", + "bytes_hex": "524946461e000000574542505650384c110000002f010000100750a08214ac808188e87f0000", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "webp_lossy_alpha_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "webp", + "bytes_hex": "524946465000000057454250565038580a00000010000000010000000000414c5048030000000080800056503820260000009001009d012a0200010002003425880274ba00039800fef5275fb6b86decd3e6fb1ecc700000", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "jpeg_progressive_forbidden", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc20011080001000203011100021101031101ffc40014000100000000000000000000000000000006ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000506ffc40014110100000000000000000000000000000000ffda000c03010002110311003f00569a1effd9", + "mutation": "jpeg_progressive" + }, + "expected": { + "accepted": false, + "error": "publication_jpeg_process_forbidden" + } + }, + { + "id": "jpeg_twelve_bit_forbidden", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc000110c0001000203011100021101031101ffc40014000100000000000000000000000000000006ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000506ffc40014110100000000000000000000000000000000ffda000c03010002110311003f00569a1effd9", + "mutation": "jpeg_precision_12" + }, + "expected": { + "accepted": false, + "error": "publication_jpeg_process_forbidden" + } + }, + { + "id": "jpeg_entropy_truncated", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc00011080001000203011100021101031101ffc40014000100000000000000000000000000000006ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000506ffc40014110100000000000000000000000000000000ffda000c03010002110311003f0056ffd9", + "mutation": "jpeg_entropy_truncated" + }, + "expected": { + "accepted": false, + "error": "publication_raster_decode_failed" + } + }, + { + "id": "jpeg_huffman_overfull", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc00011080001000203011100021101031101ffc40014000300000000000000000000000000000006ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000506ffc40014110100000000000000000000000000000000ffda000c03010002110311003f00569a1effd9", + "mutation": "jpeg_huffman_overfull" + }, + "expected": { + "accepted": false, + "error": "publication_raster_decode_failed" + } + }, + { + "id": "jpeg_wrong_restart_marker", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffc0000b080001000901011100ffc40026000100000000000000000000000000000000100100000000000000000000000000000000ffdd00040001ffda0008010100003f003fffd13fffd9", + "mutation": "jpeg_wrong_restart" + }, + "expected": { + "accepted": false, + "error": "publication_raster_decode_failed" + } + }, + { + "id": "png_sixteen_bit_forbidden", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000110060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082", + "mutation": "png_16_bit" + }, + "expected": { + "accepted": false, + "error": "publication_raster_process_forbidden" + } + }, + { + "id": "png_animation_forbidden", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489000000086163544c0000000200000000f38d93700000001a6663544c00000000000000010000000100000000000000000001000a00005a7f30d00000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082", + "mutation": "png_animation" + }, + "expected": { + "accepted": false, + "error": "publication_raster_animation_forbidden" + } + }, + { + "id": "png_corrupt_crc", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4880000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082", + "mutation": "png_corrupt_crc" + }, + "expected": { + "accepted": false, + "error": "publication_raster_decode_failed" + } + }, + { + "id": "png_corrupt_deflate", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154879c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082", + "mutation": "png_corrupt_deflate" + }, + "expected": { + "accepted": false, + "error": "publication_raster_decode_failed" + } + }, + { + "id": "png_dimension_over_limit", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000040010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082", + "mutation": "png_dimension_over" + }, + "expected": { + "accepted": false, + "error": "publication_raster_dimensions_out_of_range" + } + }, + { + "id": "png_pixel_limit", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000013880000138808060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082", + "mutation": "png_pixel_over" + }, + "expected": { + "accepted": false, + "error": "publication_raster_pixel_limit_exceeded" + } + }, + { + "id": "png_truncated", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000", + "mutation": "truncate_half" + }, + "expected": { + "accepted": false, + "error": "invalid_publication_raster" + } + }, + { + "id": "webp_animation_forbidden", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "webp", + "bytes_hex": "524946465600000057454250565038580a00000002000000000000000000414e494d06000000000000000000414e4d462a000000000000000000000000000000010000005650384c110000002f0100000007d0cc22f6aeff8188e87f0000", + "mutation": "webp_animation" + }, + "expected": { + "accepted": false, + "error": "publication_raster_animation_forbidden" + } + }, + { + "id": "webp_duplicate_primary", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "webp", + "bytes_hex": "5249464638000000574542505650384c110000002f0100000007d0cc22f6aeff8188e87f00005650384c110000002f0100000007d0cc22f6aeff8188e87f0000", + "mutation": "webp_duplicate_primary" + }, + "expected": { + "accepted": false, + "error": "invalid_publication_raster" + } + }, + { + "id": "webp_truncated", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "webp", + "bytes_hex": "524946461e000000574542505650384c110000", + "mutation": "truncate_half" + }, + "expected": { + "accepted": false, + "error": "invalid_publication_raster" + } + }, + { + "id": "jpeg_truncated", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c", + "mutation": "truncate_half" + }, + "expected": { + "accepted": false, + "error": "invalid_publication_raster" + } + } + ] +} diff --git a/crates/blossom/tests/decoder_security.rs b/crates/blossom/tests/decoder_security.rs @@ -8,8 +8,11 @@ use radroots_blossom::{ RadrootsBlossomError, RadrootsBlossomMediaType, RadrootsBlossomSha256, verify_publication_readiness, }; -use std::{env, fs, path::PathBuf}; +use serde::Deserialize; +use std::{env, fs, io::Write, path::PathBuf, process::Command}; +use tempfile::Builder; +const FIXTURE: &str = include_str!("fixtures/raster_decoder_security.v1.json"); const RESOURCE_CASE_ENV: &str = "RADROOTS_DECODER_RESOURCE_CASE"; const RESOURCE_FIXTURE_ROOT_ENV: &str = "RADROOTS_DECODER_RESOURCE_FIXTURE_ROOT"; const RESOURCE_AXIS_CASE_ENV: &str = "RADROOTS_DECODER_RESOURCE_AXIS_CASE"; @@ -161,6 +164,44 @@ impl AxisProbeCase { } } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Suite { + suite: String, + contract_version: String, + vectors: Vec<Vector>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Vector { + id: String, + kind: String, + input: VectorInput, + expected: VectorExpected, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct VectorInput { + format: String, + bytes_hex: String, + mutation: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct VectorExpected { + accepted: bool, + width: Option<u32>, + height: Option<u32>, + error: Option<String>, +} + +fn suite() -> Suite { + serde_json::from_str(FIXTURE).expect("decoder-security fixture must parse") +} + fn media(format: &str) -> (&'static str, &'static str) { match format { "jpeg" => ("image/jpeg", "jpg"), @@ -226,6 +267,105 @@ fn verify(bytes: &[u8], format: &str) -> Result<(u32, u32), RadrootsBlossomError } #[test] +fn decoder_regression_corpus_executes_every_case() { + let suite = suite(); + assert_eq!(suite.suite, "blossom_raster_decoder_security"); + assert_eq!(suite.contract_version, "1.0.0"); + assert_eq!(suite.vectors.len(), 30); + for vector in suite.vectors { + assert!(!vector.input.mutation.is_empty()); + let bytes = hex::decode(&vector.input.bytes_hex).unwrap(); + let result = verify(&bytes, &vector.input.format); + if vector.expected.accepted { + let (width, height) = result.unwrap_or_else(|error| { + panic!("{} unexpectedly failed with {}", vector.id, error.code()) + }); + assert_eq!( + vector.kind, + "blossom.verify_publication_readiness.decoder_security.accepted" + ); + assert_eq!(Some(width), vector.expected.width, "{} width", vector.id); + assert_eq!(Some(height), vector.expected.height, "{} height", vector.id); + assert!(vector.expected.error.is_none(), "{} error", vector.id); + } else { + let error = result.expect_err(&format!("{} unexpectedly passed", vector.id)); + assert_eq!( + vector.kind, + "blossom.verify_publication_readiness.decoder_security.rejected" + ); + assert_eq!( + Some(error.code()), + vector.expected.error.as_deref(), + "{} error", + vector.id + ); + assert!(vector.expected.width.is_none(), "{} width", vector.id); + assert!(vector.expected.height.is_none(), "{} height", vector.id); + } + } +} + +#[test] +#[ignore = "requires the Nix-pinned independent ImageMagick decoder"] +fn decoder_differential_matches_independent_backend() { + let executable = env::var("RADROOTS_INDEPENDENT_RASTER_DECODER") + .expect("RADROOTS_INDEPENDENT_RASTER_DECODER must name the pinned magick executable"); + for vector in suite().vectors { + if !vector.expected.accepted { + continue; + } + let bytes = hex::decode(&vector.input.bytes_hex).unwrap(); + let (width, height) = verify(&bytes, &vector.input.format).unwrap(); + let (_, extension) = media(&vector.input.format); + let mut file = Builder::new() + .suffix(&format!(".{extension}")) + .tempfile() + .unwrap(); + file.write_all(&bytes).unwrap(); + file.flush().unwrap(); + + let identify = Command::new(&executable) + .args([ + "identify", + "-format", + "%m %w %h %n\\n", + file.path().to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!(identify.status.success(), "{} identify failed", vector.id); + let output = String::from_utf8(identify.stdout).unwrap(); + let lines = output.lines().collect::<Vec<_>>(); + assert_eq!(lines.len(), 1, "{} frame count", vector.id); + let fields = lines[0].split_ascii_whitespace().collect::<Vec<_>>(); + assert_eq!(fields.len(), 4, "{} identify fields", vector.id); + assert_eq!(fields[0].to_ascii_lowercase(), vector.input.format); + assert_eq!(fields[1].parse::<u32>().unwrap(), width); + assert_eq!(fields[2].parse::<u32>().unwrap(), height); + assert_eq!(fields[3], "1"); + + let decoded = Command::new(&executable) + .args([ + file.path().to_str().unwrap(), + "-alpha", + "on", + "-depth", + "8", + "rgba:-", + ]) + .output() + .unwrap(); + assert!(decoded.status.success(), "{} decode failed", vector.id); + assert_eq!( + decoded.stdout.len(), + usize::try_from(u64::from(width) * u64::from(height) * 4).unwrap(), + "{} decoded byte count", + vector.id + ); + } +} + +#[test] #[ignore = "executed in isolation by the governed peak-RSS lane"] fn maximum_resource_probe() { let case_id = env::var(RESOURCE_CASE_ENV).expect("resource case must be selected"); @@ -291,15 +431,24 @@ fn resource_probe_inventory_is_closed() { #[test] fn encoded_byte_boundary_executes_the_public_operation() { + let base = suite() + .vectors + .into_iter() + .find(|vector| vector.id == "png_rgb_8bit") + .expect("PNG RGB vector must exist"); + assert_eq!(base.input.mutation, "none"); let exact = padded_png( - &hex::decode("89504e470d0a1a0a0000000d49484452000000020000000108020000007b40e8dd0000000f4944415408d763f8cfc0c0c0f01f00070001ff76d5a7600000000049454e44ae426082").unwrap(), + &hex::decode(base.input.bytes_hex).unwrap(), RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES as usize, ); assert_eq!( exact.len() as u64, RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES ); - assert_eq!(verify(&exact, "png").unwrap(), (2, 1)); + assert_eq!( + verify(&exact, "png").unwrap(), + (base.expected.width.unwrap(), base.expected.height.unwrap()) + ); let mut one_over = exact; one_over.push(0); diff --git a/crates/blossom/tests/fixtures/raster_decoder_security.v1.json b/crates/blossom/tests/fixtures/raster_decoder_security.v1.json @@ -0,0 +1,410 @@ +{ + "suite": "blossom_raster_decoder_security", + "contract_version": "1.0.0", + "vectors": [ + { + "id": "jpeg_baseline_gray_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffc0000b080001000201011100ffc40014000100000000000000000000000000000006ffc40014100100000000000000000000000000000000ffda0008010100003f0066ffd9", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "jpeg_baseline_rgb_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc00011080001000203011100021101031101ffc40014000100000000000000000000000000000006ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000506ffc40014110100000000000000000000000000000000ffda000c03010002110311003f00569a1effd9", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "jpeg_baseline_cmyk_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffee000e41646f626500640000000002ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc00014080001000204011100021101031101041100ffc4001500010100000000000000000000000000000709ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000507ffc40014110100000000000000000000000000000000ffda000e040100021103110400003f009a4a01821bffd9", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "jpeg_extended_sequential_sof1", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc10011080001000203011100021101031101ffc40014000100000000000000000000000000000006ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000506ffc40014110100000000000000000000000000000000ffda000c03010002110311003f00569a1effd9", + "mutation": "jpeg_sof1" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "png_gray_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d4948445200000002000000010800000000d14920560000000b4944415408d76360f80f0001020100dac612cc0000000049454e44ae426082", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "png_rgb_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000020000000108020000007b40e8dd0000000f4944415408d763f8cfc0c0c0f01f00070001ff76d5a7600000000049454e44ae426082", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "png_indexed_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d4948445200000002000000010803000000c3fc8fb800000006504c5445ff00000000ff6ca1fd8e0000000b4944415408d7636060040000040002270291ee0000000049454e44ae426082", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "png_gray_alpha_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000020000000108040000005e2bb7010000000d4944415408d7636468f8cf000003880181aebdb1c70000000049454e44ae426082", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "png_rgba_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d4948445200000002000000010806000000f4227f8a000000114944415408d763fccfc0d0c0c8f09f01000c8c0281d7581d390000000049454e44ae426082", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "png_adam7_rgba_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d4948445200000008000000080806000001b3088e1d000000494944415418d3958dc9090020100367c1526cc6626cc662b4188b890f513c40705e61d84d1008812c920460821ea6c904b1609eba8bf1337085c053dc1d15bf8993abe2fbc040cf89065fac1a04c2d2db980000000049454e44ae426082", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 8, + "height": 8 + } + }, + { + "id": "webp_lossless_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "webp", + "bytes_hex": "524946461e000000574542505650384c110000002f0100000007d0cc22f6aeff8188e87f0000", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "webp_lossy_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "webp", + "bytes_hex": "524946462e0000005745425056503820220000005001009d012a0200010001402625004e80280000fee246baf5a5b9b84c4619d60000", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "webp_lossless_alpha_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "webp", + "bytes_hex": "524946461e000000574542505650384c110000002f010000100750a08214ac808188e87f0000", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "webp_lossy_alpha_8bit", + "kind": "blossom.verify_publication_readiness.decoder_security.accepted", + "input": { + "format": "webp", + "bytes_hex": "524946465000000057454250565038580a00000010000000010000000000414c5048030000000080800056503820260000009001009d012a0200010002003425880274ba00039800fef5275fb6b86decd3e6fb1ecc700000", + "mutation": "none" + }, + "expected": { + "accepted": true, + "width": 2, + "height": 1 + } + }, + { + "id": "jpeg_progressive_forbidden", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc20011080001000203011100021101031101ffc40014000100000000000000000000000000000006ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000506ffc40014110100000000000000000000000000000000ffda000c03010002110311003f00569a1effd9", + "mutation": "jpeg_progressive" + }, + "expected": { + "accepted": false, + "error": "publication_jpeg_process_forbidden" + } + }, + { + "id": "jpeg_twelve_bit_forbidden", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc000110c0001000203011100021101031101ffc40014000100000000000000000000000000000006ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000506ffc40014110100000000000000000000000000000000ffda000c03010002110311003f00569a1effd9", + "mutation": "jpeg_precision_12" + }, + "expected": { + "accepted": false, + "error": "publication_jpeg_process_forbidden" + } + }, + { + "id": "jpeg_entropy_truncated", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc00011080001000203011100021101031101ffc40014000100000000000000000000000000000006ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000506ffc40014110100000000000000000000000000000000ffda000c03010002110311003f0056ffd9", + "mutation": "jpeg_entropy_truncated" + }, + "expected": { + "accepted": false, + "error": "publication_raster_decode_failed" + } + }, + { + "id": "jpeg_huffman_overfull", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc00011080001000203011100021101031101ffc40014000300000000000000000000000000000006ffc40014100100000000000000000000000000000000ffc4001501010100000000000000000000000000000506ffc40014110100000000000000000000000000000000ffda000c03010002110311003f00569a1effd9", + "mutation": "jpeg_huffman_overfull" + }, + "expected": { + "accepted": false, + "error": "publication_raster_decode_failed" + } + }, + { + "id": "jpeg_wrong_restart_marker", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffc0000b080001000901011100ffc40026000100000000000000000000000000000000100100000000000000000000000000000000ffdd00040001ffda0008010100003f003fffd13fffd9", + "mutation": "jpeg_wrong_restart" + }, + "expected": { + "accepted": false, + "error": "publication_raster_decode_failed" + } + }, + { + "id": "png_sixteen_bit_forbidden", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000110060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082", + "mutation": "png_16_bit" + }, + "expected": { + "accepted": false, + "error": "publication_raster_process_forbidden" + } + }, + { + "id": "png_animation_forbidden", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489000000086163544c0000000200000000f38d93700000001a6663544c00000000000000010000000100000000000000000001000a00005a7f30d00000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082", + "mutation": "png_animation" + }, + "expected": { + "accepted": false, + "error": "publication_raster_animation_forbidden" + } + }, + { + "id": "png_corrupt_crc", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4880000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082", + "mutation": "png_corrupt_crc" + }, + "expected": { + "accepted": false, + "error": "publication_raster_decode_failed" + } + }, + { + "id": "png_corrupt_deflate", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154879c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082", + "mutation": "png_corrupt_deflate" + }, + "expected": { + "accepted": false, + "error": "publication_raster_decode_failed" + } + }, + { + "id": "png_dimension_over_limit", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000040010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082", + "mutation": "png_dimension_over" + }, + "expected": { + "accepted": false, + "error": "publication_raster_dimensions_out_of_range" + } + }, + { + "id": "png_pixel_limit", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000013880000138808060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082", + "mutation": "png_pixel_over" + }, + "expected": { + "accepted": false, + "error": "publication_raster_pixel_limit_exceeded" + } + }, + { + "id": "png_truncated", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "png", + "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000", + "mutation": "truncate_half" + }, + "expected": { + "accepted": false, + "error": "invalid_publication_raster" + } + }, + { + "id": "webp_animation_forbidden", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "webp", + "bytes_hex": "524946465600000057454250565038580a00000002000000000000000000414e494d06000000000000000000414e4d462a000000000000000000000000000000010000005650384c110000002f0100000007d0cc22f6aeff8188e87f0000", + "mutation": "webp_animation" + }, + "expected": { + "accepted": false, + "error": "publication_raster_animation_forbidden" + } + }, + { + "id": "webp_duplicate_primary", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "webp", + "bytes_hex": "5249464638000000574542505650384c110000002f0100000007d0cc22f6aeff8188e87f00005650384c110000002f0100000007d0cc22f6aeff8188e87f0000", + "mutation": "webp_duplicate_primary" + }, + "expected": { + "accepted": false, + "error": "invalid_publication_raster" + } + }, + { + "id": "webp_truncated", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "webp", + "bytes_hex": "524946461e000000574542505650384c110000", + "mutation": "truncate_half" + }, + "expected": { + "accepted": false, + "error": "invalid_publication_raster" + } + }, + { + "id": "jpeg_truncated", + "kind": "blossom.verify_publication_readiness.decoder_security.rejected", + "input": { + "format": "jpeg", + "bytes_hex": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c", + "mutation": "truncate_half" + }, + "expected": { + "accepted": false, + "error": "invalid_publication_raster" + } + } + ] +} diff --git a/docs/blossom-raster-decoder-security.md b/docs/blossom-raster-decoder-security.md @@ -0,0 +1,168 @@ +# Blossom Raster Decoder Security + +This document records the defensive security and cost boundary for the +publication raster decoder in `radroots_blossom`. The decoder parses +attacker-controlled JPEG, PNG, and WebP bytes at the publication-readiness +boundary, so the crate treats container inspection and full decoding as a +hardened parser surface: no input may panic, overflow, loop without a work +budget, allocate beyond its declared ceiling, or produce animated or +multiframe output. + +## Normative limits + +| Bound | Value | Enforcement | +| --- | --- | --- | +| Encoded raster bytes | `10,485,760` | public constant, checked before decode | +| Width/height per axis | `16,384` | dimension gate before allocation | +| Decoded pixels | `20,000,000` | pixel gate before allocation | +| Decoded output bytes | `80,000,000` | public constant, checked before allocation | +| Container records per format | `65,536` | checked record counter in each container walk | +| Sequential JPEG scans | `4` | work budget | +| Sequential JPEG blocks | `3,200,000` | work budget | +| Sequential JPEG coefficient steps | `204,800,000` | work budget | +| JPEG entropy bit reads | `8 x input bytes` | entropy reader budget | +| Decoder lane peak RSS | `131,072` KiB | governed lane hard failure | + +Accepted processes are static 8-bit sequential JPEG (SOF0/SOF1), static +8-bit PNG with approved color types, one ordered palette, and no unknown +critical chunks, and static WebP whose extended header carries no reserved +flags and exactly one primary chunk. Animation, multiframe, higher bit +depths, and every other process fail before product authority is created; +unsupported PNG and WebP processes fail with the stable +`publication_raster_process_forbidden` error. + +## Native WebP boundary + +Production WebP decoding uses the safe `libwebp` `0.1.2` wrapper with features +`1_1` and `static` over `libwebp-sys2` `0.1.11`. Both crates and the vendored +libwebp source are BSD-3-Clause. The sys crate compiles its pinned C sources +with `cc`; static mode bypasses `pkg-config`, vcpkg, system-dylib discovery, and +all production subprocess or network behavior. Public `radroots_blossom` Rust +remains under `#![forbid(unsafe_code)]`; native and unsafe implementation is +confined to the pinned dependency boundary. The production operation obtains +the checked dimensions, reserves one bounded RGBA buffer fallibly, and decodes +directly into it. The governed Darwin lane also emits an +`aarch64-apple-ios` static archive and proves that the vendored decoder symbol +is linked into that archive. + +## Ordinary verification gates + +The regression corpus, independent-decoder differential, and peak-RSS probe +execute through the real public `verify_publication_readiness` API: + +- `crates/blossom/tests/fixtures/raster_decoder_security.v1.json` mirrors the + canonical `contracts/conformance/vectors/blossom/raster_decoder_security.v1.json` + corpus of 30 cases covering every supported format/process variant, including + lossy and lossless WebP alpha, plus + malformed, truncated, animation, precision, Huffman, restart, CRC, + deflate, dimension, pixel, and duplicate-primary failures. Each `bytes_hex` + value is the final byte sequence passed to the operation; mutation labels are + provenance only and the executor performs no runtime transformation. +- `decoder_differential_matches_independent_backend` compares accepted + dimensions, format, frame count, and decoded byte count against the + Nix-pinned ImageMagick `7.1.2-27` backend, which shares no decoder code + with the production path. Disagreements fail and become corpus cases. +- `maximum_resource_probe` selects exactly one already-prepared 5,000 x 4,000 + fixture in a fresh child. The closed matrix covers JPEG grayscale, RGB, + CMYK, and SOF1; PNG RGB, palette, RGBA, and Adam7; and WebP VP8 RGB, VP8 + alpha, VP8L RGB, and VP8L alpha. ImageMagick prepares the compact fixtures + outside every measured child, while the child authenticates the actual + process and executes `verify_publication_readiness` exactly once. Every case + runs three times; any observation above `131,072` KiB fails, and the highest + observation per case is retained in a TSV under the extbuild-owned Cargo + target root. Separate prepared PNGs execute the exact `16,384 x 1` and + `1 x 16,384` axis boundaries. Wall time remains informational only. + +Run the whole gate surface through the governed app: + +```bash +nix run .#decoder-security +``` + +On Darwin, compile and statically link the same production feature profile for +the device target with: + +```bash +nix run .#blossom-raster-ios-compile-link +``` + +## Deterministic fuzz smoke + +The workspace-excluded, non-publishable `fuzz` crate pins +`nightly-2026-07-15` and drives the same public API through three +`cargo-fuzz` targets: `publication_jpeg`, `publication_png`, and +`publication_webp`. Checked-in seed corpora live under +`fuzz/corpus/<target>/` as exactly one raw binary file per conformance case, +under its single format target. The common harness passes those bytes directly +to `verify_publication_readiness`; it has no textual-hex decoder or ASCII +fallback. Generated corpora and crash artifacts stay under the extbuild-owned +Cargo target root (or the isolated Nix build sandbox) and are disposable +evidence only. + +The offline sandboxed smoke check vendors `fuzz/Cargo.lock` and runs each +target for 256 executions with seed `424242`, a 65,536-byte maximum input, +a 5-second per-input timeout, and a 2,048 MiB libFuzzer engine limit (the +engine limit is fuzzer headroom and is distinct from the 128 MiB decoder +lane gate above): + +```bash +cargo extbuild run -- nix build \ + .#checks.aarch64-darwin.blossom-decoder-fuzz-smoke +``` + +## Extended campaign record + +A longer AddressSanitizer campaign completed on 2026-07-27 with zero crash +artifacts: + +- engine: libFuzzer via `cargo-fuzz`, nightly `2026-07-15`, `-fsanitize=address` +- seed: `20260726` +- duration: 60 seconds of fuzzing per format (informational, not contractual) +- maximum input: `10,485,760` bytes +- resulting corpora: 482 JPEG, 657 PNG, and 96 WebP inputs under an isolated + `decoder-security-extended-20260726.*` directory in the extbuild Cargo target + root +- crash artifacts: none in any format loop + +Reproduce the extended campaign from the governed shell (the corpus and +artifact paths are disposable): + +```bash +cargo extbuild run -- nix develop .#decoder-security --command sh -c ' +set -eu +root="$(mktemp -d "${CARGO_TARGET_DIR:?}/decoder-security-extended.XXXXXX")" +mkdir -p "$root/corpus" "$root/artifacts" +cp -R fuzz/corpus/. "$root/corpus/" +for target in publication_jpeg publication_png publication_webp; do + mkdir -p "$root/artifacts/$target" + cargo fuzz run --fuzz-dir fuzz --sanitizer address "$target" \ + "$root/corpus/$target" -- \ + -max_total_time=60 \ + -seed=20260726 \ + -max_len=10485760 \ + -timeout=5 \ + -rss_limit_mb=2048 \ + -artifact_prefix="$root/artifacts/$target/" +done +test -z "$(find "$root/artifacts" -type f -print -quit)" +' +``` + +Any crash artifact must be reproduced and minimized with the same pinned +`decoder-security` Nix shell. Its minimized final bytes must then be promoted +to the canonical vector and regenerated raw corpus before the fix ships; an +unresolved artifact fails the campaign. + +## Contract authority + +The machine-readable successor contract +`radroots_blossom.raster_decoder_security_v1` +(`crates/blossom/contracts/raster_decoder_security_v1.manifest.json`) +authenticates the limits above, the decoder profile, the fuzz toolchain and +targets, the governed Nix lanes, the vector corpus and its executors, and +the immutable `radroots_blossom.publication_readiness_v1` predecessor. +Validate it with: + +```bash +cargo xtask contract blossom-raster-decoder-security-manifest +``` diff --git a/fuzz/.gitignore b/fuzz/.gitignore @@ -0,0 +1,2 @@ +artifacts/ +target/ diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock @@ -0,0 +1,748 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libwebp" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7792a82f95b5b2528d9fe1642231f972d2cdd73b91ccbcb6ab214c2cf1a74f4" +dependencies = [ + "libwebp-sys2", +] + +[[package]] +name = "libwebp-sys2" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4790186411a6843ecc0a141c8948c8e623a0bb5e886834b1b6c90f3dfa85bb99" +dependencies = [ + "cc", + "cfg-if", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "mediatype" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120fa187be19d9962f0926633453784691731018a2bf936ddb4e29101b79c4a7" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radroots-fuzz" +version = "0.0.0" +dependencies = [ + "libfuzzer-sys", + "radroots_blossom", +] + +[[package]] +name = "radroots_blossom" +version = "1.0.0-alpha.1" +dependencies = [ + "image", + "libwebp", + "mediatype", + "serde", + "serde_json", + "sha2", + "unicode-general-category", + "url", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "radroots-fuzz" +version = "0.0.0" +publish = false +edition = "2024" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +radroots_blossom = { path = "../crates/blossom", default-features = false, features = [ + "raster-decode", + "serde", +] } + +[[bin]] +name = "publication_jpeg" +path = "fuzz_targets/publication_jpeg.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "publication_png" +path = "fuzz_targets/publication_png.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "publication_webp" +path = "fuzz_targets/publication_webp.rs" +test = false +doc = false +bench = false diff --git a/fuzz/corpus/publication_jpeg/jpeg_baseline_cmyk_8bit.bin b/fuzz/corpus/publication_jpeg/jpeg_baseline_cmyk_8bit.bin Binary files differ. diff --git a/fuzz/corpus/publication_jpeg/jpeg_baseline_gray_8bit.bin b/fuzz/corpus/publication_jpeg/jpeg_baseline_gray_8bit.bin Binary files differ. diff --git a/fuzz/corpus/publication_jpeg/jpeg_baseline_rgb_8bit.bin b/fuzz/corpus/publication_jpeg/jpeg_baseline_rgb_8bit.bin Binary files differ. diff --git a/fuzz/corpus/publication_jpeg/jpeg_entropy_truncated.bin b/fuzz/corpus/publication_jpeg/jpeg_entropy_truncated.bin Binary files differ. diff --git a/fuzz/corpus/publication_jpeg/jpeg_extended_sequential_sof1.bin b/fuzz/corpus/publication_jpeg/jpeg_extended_sequential_sof1.bin Binary files differ. diff --git a/fuzz/corpus/publication_jpeg/jpeg_huffman_overfull.bin b/fuzz/corpus/publication_jpeg/jpeg_huffman_overfull.bin Binary files differ. diff --git a/fuzz/corpus/publication_jpeg/jpeg_progressive_forbidden.bin b/fuzz/corpus/publication_jpeg/jpeg_progressive_forbidden.bin Binary files differ. diff --git a/fuzz/corpus/publication_jpeg/jpeg_truncated.bin b/fuzz/corpus/publication_jpeg/jpeg_truncated.bin Binary files differ. diff --git a/fuzz/corpus/publication_jpeg/jpeg_twelve_bit_forbidden.bin b/fuzz/corpus/publication_jpeg/jpeg_twelve_bit_forbidden.bin Binary files differ. diff --git a/fuzz/corpus/publication_jpeg/jpeg_wrong_restart_marker.bin b/fuzz/corpus/publication_jpeg/jpeg_wrong_restart_marker.bin Binary files differ. diff --git a/fuzz/corpus/publication_png/png_adam7_rgba_8bit.bin b/fuzz/corpus/publication_png/png_adam7_rgba_8bit.bin Binary files differ. diff --git a/fuzz/corpus/publication_png/png_animation_forbidden.bin b/fuzz/corpus/publication_png/png_animation_forbidden.bin Binary files differ. diff --git a/fuzz/corpus/publication_png/png_corrupt_crc.bin b/fuzz/corpus/publication_png/png_corrupt_crc.bin Binary files differ. diff --git a/fuzz/corpus/publication_png/png_corrupt_deflate.bin b/fuzz/corpus/publication_png/png_corrupt_deflate.bin Binary files differ. diff --git a/fuzz/corpus/publication_png/png_dimension_over_limit.bin b/fuzz/corpus/publication_png/png_dimension_over_limit.bin Binary files differ. diff --git a/fuzz/corpus/publication_png/png_gray_8bit.bin b/fuzz/corpus/publication_png/png_gray_8bit.bin Binary files differ. diff --git a/fuzz/corpus/publication_png/png_gray_alpha_8bit.bin b/fuzz/corpus/publication_png/png_gray_alpha_8bit.bin Binary files differ. diff --git a/fuzz/corpus/publication_png/png_indexed_8bit.bin b/fuzz/corpus/publication_png/png_indexed_8bit.bin Binary files differ. diff --git a/fuzz/corpus/publication_png/png_pixel_limit.bin b/fuzz/corpus/publication_png/png_pixel_limit.bin Binary files differ. diff --git a/fuzz/corpus/publication_png/png_rgb_8bit.bin b/fuzz/corpus/publication_png/png_rgb_8bit.bin Binary files differ. diff --git a/fuzz/corpus/publication_png/png_rgba_8bit.bin b/fuzz/corpus/publication_png/png_rgba_8bit.bin Binary files differ. diff --git a/fuzz/corpus/publication_png/png_sixteen_bit_forbidden.bin b/fuzz/corpus/publication_png/png_sixteen_bit_forbidden.bin Binary files differ. diff --git a/fuzz/corpus/publication_png/png_truncated.bin b/fuzz/corpus/publication_png/png_truncated.bin Binary files differ. diff --git a/fuzz/corpus/publication_webp/webp_animation_forbidden.bin b/fuzz/corpus/publication_webp/webp_animation_forbidden.bin Binary files differ. diff --git a/fuzz/corpus/publication_webp/webp_duplicate_primary.bin b/fuzz/corpus/publication_webp/webp_duplicate_primary.bin Binary files differ. diff --git a/fuzz/corpus/publication_webp/webp_lossless_8bit.bin b/fuzz/corpus/publication_webp/webp_lossless_8bit.bin Binary files differ. diff --git a/fuzz/corpus/publication_webp/webp_lossless_alpha_8bit.bin b/fuzz/corpus/publication_webp/webp_lossless_alpha_8bit.bin Binary files differ. diff --git a/fuzz/corpus/publication_webp/webp_lossy_8bit.bin b/fuzz/corpus/publication_webp/webp_lossy_8bit.bin Binary files differ. diff --git a/fuzz/corpus/publication_webp/webp_lossy_alpha_8bit.bin b/fuzz/corpus/publication_webp/webp_lossy_alpha_8bit.bin Binary files differ. diff --git a/fuzz/corpus/publication_webp/webp_truncated.bin b/fuzz/corpus/publication_webp/webp_truncated.bin Binary files differ. diff --git a/fuzz/fuzz_targets/common.rs b/fuzz/fuzz_targets/common.rs @@ -0,0 +1,67 @@ +use radroots_blossom::{ + RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES, RadrootsBlossomAuthoredRasterDimensions, + RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomBud01GetObservation, + RadrootsBlossomBud01HeadObservation, RadrootsBlossomBud02UploadObservation, + RadrootsBlossomMediaType, RadrootsBlossomSha256, verify_publication_readiness, +}; +pub(crate) fn exercise(input: &[u8], media_type: &str, extension: &str) { + if input.is_empty() || input.len() as u64 > RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES { + return; + } + + let hash = RadrootsBlossomSha256::digest(input); + let url = format!("https://cdn.example/{hash}.{extension}"); + let Ok(media_type) = RadrootsBlossomMediaType::parse(media_type) else { + return; + }; + let Ok(url) = RadrootsBlossomBlobUrl::parse(&url) else { + return; + }; + let Ok(descriptor) = RadrootsBlossomBlobDescriptor::new( + url.clone(), + hash, + input.len() as u64, + media_type.clone(), + 1_800_000_000, + ) else { + return; + }; + let Ok(authored) = descriptor + .clone() + .approve_reference() + .and_then(|descriptor| descriptor.verify_bytes(input, &media_type)) + else { + return; + }; + let Ok(upload) = RadrootsBlossomBud02UploadObservation::new(201, descriptor) else { + return; + }; + let Ok(approved_url) = url.approve() else { + return; + }; + let Ok(head) = RadrootsBlossomBud01HeadObservation::new( + 200, + approved_url.clone(), + input.len() as u64, + media_type, + ) else { + return; + }; + let Ok(get) = RadrootsBlossomBud01GetObservation::from_complete_body( + 200, + approved_url, + input.len() as u64, + input, + ) else { + return; + }; + + let _ = verify_publication_readiness( + &authored, + input, + RadrootsBlossomAuthoredRasterDimensions::Unspecified, + &upload, + &head, + &get, + ); +} diff --git a/fuzz/fuzz_targets/publication_jpeg.rs b/fuzz/fuzz_targets/publication_jpeg.rs @@ -0,0 +1,9 @@ +#![no_main] + +mod common; + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + common::exercise(data, "image/jpeg", "jpg"); +}); diff --git a/fuzz/fuzz_targets/publication_png.rs b/fuzz/fuzz_targets/publication_png.rs @@ -0,0 +1,9 @@ +#![no_main] + +mod common; + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + common::exercise(data, "image/png", "png"); +}); diff --git a/fuzz/fuzz_targets/publication_webp.rs b/fuzz/fuzz_targets/publication_webp.rs @@ -0,0 +1,9 @@ +#![no_main] + +mod common; + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + common::exercise(data, "image/webp", "webp"); +}); diff --git a/rust-toolchain-fuzz.toml b/rust-toolchain-fuzz.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly-2026-07-15" +components = ["rust-src"] +profile = "minimal" diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs @@ -4,6 +4,8 @@ mod admission_authority; mod artifact_bundle; #[allow(dead_code)] mod blossom_publication_readiness; +#[allow(dead_code)] +mod blossom_raster_decoder_security; mod comment_authority; mod deletion_authority; mod food_availability_projection; diff --git a/tools/xtask/src/contract/blossom_raster_decoder_security.rs b/tools/xtask/src/contract/blossom_raster_decoder_security.rs @@ -0,0 +1,1768 @@ +use super::{ + artifact_bundle::{GeneratedArtifact, read_regular_file, with_artifact_bundle_transaction}, + phase1_publication_media_readiness::{ + BLOSSOM_PREDECESSOR_ARTIFACTS, FileDescriptor, PredecessorDescriptor, ProtocolSourcePin, + descriptor_for_bytes, descriptor_for_file, predecessor_descriptor, + validate_immutable_predecessor, validate_source_supersessions, + }, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + path::Path, +}; +use syn::{BinOp, Expr, FnArg, Item, Lit, Pat, Stmt, Type, UseTree, Visibility, visit::Visit}; + +const SCHEMA_VERSION: u32 = 1; +const CONTRACT_ID: &str = "radroots_blossom.raster_decoder_security_v1"; +const AUTHORITY_ID: &str = "blossom_raster_decoder_security_v1"; +const HASH_ALGORITHM: &str = "sha256_bytes_v1"; +const WRITE_COMMAND: &str = "cargo xtask contract blossom-raster-decoder-security-manifest --write"; + +const MANIFEST_RELATIVE: &str = "crates/blossom/contracts/raster_decoder_security_v1.manifest.json"; +const MANIFEST_SCHEMA_RELATIVE: &str = + "crates/blossom/contracts/raster_decoder_security_v1.manifest.schema.json"; +const MANIFEST_SHA256_RELATIVE: &str = + "crates/blossom/contracts/raster_decoder_security_v1.manifest.sha256"; +const GENERATED_DESCRIPTOR_RELATIVE: &str = + "crates/blossom/contracts/raster_decoder_security_v1.descriptor.json"; +const VECTOR_RELATIVE: &str = + "contracts/conformance/vectors/blossom/raster_decoder_security.v1.json"; +const VECTOR_MIRROR_RELATIVE: &str = + "crates/blossom/tests/fixtures/raster_decoder_security.v1.json"; +const VECTOR_EXECUTOR_RELATIVE: &str = "crates/blossom/tests/decoder_security.rs"; +const REGRESSION_TEST: &str = "decoder_regression_corpus_executes_every_case"; +const DIFFERENTIAL_TEST: &str = "decoder_differential_matches_independent_backend"; +const RESOURCE_TEST: &str = "maximum_resource_probe"; +const READINESS_SOURCE_RELATIVE: &str = "crates/blossom/src/publication_readiness.rs"; +const JPEG_SOURCE_RELATIVE: &str = "crates/blossom/src/publication_readiness/sequential_jpeg.rs"; +const ERROR_SOURCE_RELATIVE: &str = "crates/blossom/src/error.rs"; +const WORKSPACE_MANIFEST_RELATIVE: &str = "Cargo.toml"; +const BLOSSOM_MANIFEST_RELATIVE: &str = "crates/blossom/Cargo.toml"; +const FUZZ_MANIFEST_RELATIVE: &str = "fuzz/Cargo.toml"; +const FUZZ_LOCKFILE_RELATIVE: &str = "fuzz/Cargo.lock"; +const FUZZ_TOOLCHAIN_RELATIVE: &str = "rust-toolchain-fuzz.toml"; +const FUZZ_COMMON_RELATIVE: &str = "fuzz/fuzz_targets/common.rs"; +const FUZZ_CORPUS_RELATIVE: &str = "fuzz/corpus"; +const NIX_APPS_RELATIVE: &str = "build/nix/apps.nix"; +const NIX_CHECKS_RELATIVE: &str = "build/nix/checks.nix"; +const NIX_COMMON_RELATIVE: &str = "build/nix/common.nix"; +const NIX_DEVSHELLS_RELATIVE: &str = "build/nix/devshells.nix"; +const NIX_TOOLCHAINS_RELATIVE: &str = "build/nix/toolchains.nix"; +const SECURITY_DOCUMENT_RELATIVE: &str = "docs/blossom-raster-decoder-security.md"; +const OPERATIONS_RELATIVE: &str = "contracts/operations.toml"; +const RELEASE_RELATIVE: &str = "contracts/releases/1.0.0-alpha.1.toml"; +const CHANGELOG_RELATIVE: &str = "CHANGELOG.md"; +const RELEASE_CHANGE_ID: &str = "blossom-raster-decoder-security"; +const CHANGELOG_MARKER: &str = "<!-- release-change: blossom-raster-decoder-security -->"; + +const OPERATION_KEY: &str = "blossom_verify_raster_decoder_security"; +const OPERATION_ID: &str = "blossom.verify_raster_decoder_security"; +const ACCEPTED_KIND: &str = "blossom.verify_publication_readiness.decoder_security.accepted"; +const REJECTED_KIND: &str = "blossom.verify_publication_readiness.decoder_security.rejected"; + +const MAX_RASTER_BYTES: u64 = 10_485_760; +const MAX_DECODED_BYTES: u64 = 80_000_000; +const MAX_DIMENSION: u64 = 16_384; +const MAX_PIXELS: u64 = 20_000_000; +const MAX_CONTAINER_RECORDS: u64 = 65_536; +const JPEG_MAX_SCANS: u64 = 4; +const JPEG_MAX_BLOCKS: u64 = 3_200_000; +const JPEG_MAX_COEFFICIENT_STEPS: u64 = 204_800_000; +const JPEG_ENTROPY_BIT_READS_PER_BYTE: u64 = 8; +const PEAK_RSS_KIB_LIMIT: u64 = 131_072; + +const FUZZ_TOOLCHAIN_CHANNEL: &str = "nightly-2026-07-15"; +const FUZZ_SMOKE_RUNS: u64 = 256; +const FUZZ_SMOKE_SEED: u64 = 424242; +const FUZZ_SMOKE_MAX_INPUT_BYTES: u64 = 65_536; +const FUZZ_SMOKE_TIMEOUT_SECONDS: u64 = 5; +const FUZZ_ENGINE_RSS_LIMIT_MB: u64 = 2048; + +const FUZZ_TARGETS: &[&str] = &["publication_jpeg", "publication_png", "publication_webp"]; +const FUZZ_TARGET_SPECS: &[(&str, &str, &str, &str)] = &[ + ("publication_jpeg", "jpeg", "image/jpeg", "jpg"), + ("publication_png", "png", "image/png", "png"), + ("publication_webp", "webp", "image/webp", "webp"), +]; + +const READINESS_LIMIT_CONSTANTS: &[(&str, u64)] = &[ + ( + "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES", + MAX_RASTER_BYTES, + ), + ( + "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES", + MAX_DECODED_BYTES, + ), + ( + "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION", + MAX_DIMENSION, + ), + ("RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS", MAX_PIXELS), + ( + "PUBLICATION_RASTER_MAX_CONTAINER_RECORDS", + MAX_CONTAINER_RECORDS, + ), +]; +const JPEG_LIMIT_CONSTANTS: &[(&str, u64)] = &[ + ("MAX_SEQUENTIAL_JPEG_SCANS", JPEG_MAX_SCANS), + ("MAX_SEQUENTIAL_JPEG_BLOCKS", JPEG_MAX_BLOCKS), + ( + "MAX_SEQUENTIAL_JPEG_COEFFICIENT_STEPS", + JPEG_MAX_COEFFICIENT_STEPS, + ), +]; + +const REJECTED_ERROR_CODES: &[&str] = &[ + "invalid_publication_raster", + "publication_jpeg_process_forbidden", + "publication_raster_animation_forbidden", + "publication_raster_decode_failed", + "publication_raster_dimensions_out_of_range", + "publication_raster_pixel_limit_exceeded", + "publication_raster_process_forbidden", +]; +const REQUIRED_DEDICATED_ERROR_CODES: &[&str] = &[ + "publication_jpeg_process_forbidden", + "publication_raster_animation_forbidden", + "publication_raster_process_forbidden", +]; +const REQUIRED_MUTATIONS: &[&str] = &[ + "none", + "truncate_half", + "jpeg_sof1", + "jpeg_progressive", + "jpeg_precision_12", + "jpeg_entropy_truncated", + "jpeg_huffman_overfull", + "jpeg_wrong_restart", + "png_16_bit", + "png_animation", + "png_corrupt_crc", + "png_corrupt_deflate", + "png_dimension_over", + "png_pixel_over", + "webp_animation", + "webp_duplicate_primary", +]; +const VECTOR_CASE_IDS: &[&str] = &[ + "jpeg_baseline_gray_8bit", + "jpeg_baseline_rgb_8bit", + "jpeg_baseline_cmyk_8bit", + "jpeg_extended_sequential_sof1", + "png_gray_8bit", + "png_rgb_8bit", + "png_indexed_8bit", + "png_gray_alpha_8bit", + "png_rgba_8bit", + "png_adam7_rgba_8bit", + "webp_lossless_8bit", + "webp_lossy_8bit", + "webp_lossless_alpha_8bit", + "webp_lossy_alpha_8bit", + "jpeg_progressive_forbidden", + "jpeg_twelve_bit_forbidden", + "jpeg_entropy_truncated", + "jpeg_huffman_overfull", + "jpeg_wrong_restart_marker", + "png_sixteen_bit_forbidden", + "png_animation_forbidden", + "png_corrupt_crc", + "png_corrupt_deflate", + "png_dimension_over_limit", + "png_pixel_limit", + "png_truncated", + "webp_animation_forbidden", + "webp_duplicate_primary", + "webp_truncated", + "jpeg_truncated", +]; +const RELEASE_SEMVER_IMPACTS: &[&str] = &[ + "add_conformance_vector", + "add_enum_variant", + "change_exported_algorithm_behavior", + "change_exported_constant_value", +]; +const OPERATION_INPUTS: &[&str] = &[ + "RadrootsBlossomByteVerifiedDescriptor", + "Bytes", + "RadrootsBlossomAuthoredRasterDimensions", + "RadrootsBlossomBud02UploadObservation", + "RadrootsBlossomBud01HeadObservation", + "RadrootsBlossomBud01GetObservation", +]; +const OPERATION_OUTPUTS: &[&str] = &["RadrootsBlossomPublicationReadinessEvidence"]; +const OPERATION_MODULES: &[&str] = &[READINESS_SOURCE_RELATIVE, JPEG_SOURCE_RELATIVE]; +const OPERATION_RUST_TYPES: &[&str] = &[ + "radroots_blossom::RadrootsBlossomAuthoredRasterDimensions", + "radroots_blossom::RadrootsBlossomBud01GetObservation", + "radroots_blossom::RadrootsBlossomBud01HeadObservation", + "radroots_blossom::RadrootsBlossomBud02UploadObservation", + "radroots_blossom::RadrootsBlossomByteVerifiedDescriptor", + "radroots_blossom::RadrootsBlossomError", + "radroots_blossom::RadrootsBlossomPublicationReadinessEvidence", +]; +const SOURCE_SUPERSESSIONS: &[&str] = &[ + "CHANGELOG.md", + "contracts/operations.toml", + "contracts/releases/1.0.0-alpha.1.toml", + "crates/blossom/Cargo.toml", + "crates/blossom/src/error.rs", + "crates/blossom/src/publication_readiness.rs", + "crates/blossom/src/publication_readiness/sequential_jpeg.rs", + "tools/xtask/src/contract.rs", + "tools/xtask/src/main.rs", +]; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct ResourceLimitsDescriptor { + max_raster_bytes: u64, + max_decoded_bytes: u64, + max_dimension: u64, + max_pixels: u64, + max_container_records: u64, + jpeg_max_scans: u64, + jpeg_max_blocks: u64, + jpeg_max_coefficient_steps: u64, + jpeg_entropy_bit_reads_per_byte: u64, + peak_rss_kib_limit: u64, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct DecoderProfileDescriptor { + accepted_formats: Vec<String>, + accepted_jpeg_processes: Vec<String>, + accepted_png_color_types: Vec<u8>, + stable_error_codes: Vec<String>, + required_rejection_mutations: Vec<String>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct FuzzCampaignDescriptor { + toolchain_channel: String, + toolchain_components: Vec<String>, + toolchain_profile: String, + engine: String, + sanitizer: String, + targets: Vec<String>, + corpus_seeds: Vec<String>, + smoke_runs: u64, + smoke_seed: u64, + smoke_max_input_bytes: u64, + smoke_timeout_seconds: u64, + engine_rss_limit_mb: u64, + extended_campaign_document: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct NixLanesDescriptor { + app: String, + devshell: String, + stable_check: String, + fuzz_check: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct OperationDescriptor { + key: String, + id: String, + case_kinds: Vec<String>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct ResultVectorDescriptor { + canonical_path: String, + mirror_path: String, + byte_length: u64, + sha256: String, + hash_algorithm: String, + executor: FileDescriptor, + executor_tests: Vec<String>, + case_ids: Vec<String>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct RasterDecoderSecurityManifest { + schema_version: u32, + contract_id: String, + authority_id: String, + manifest_schema: FileDescriptor, + predecessors: Vec<PredecessorDescriptor>, + protocol_sources: Vec<ProtocolSourcePin>, + resource_limits: ResourceLimitsDescriptor, + decoder_profile: DecoderProfileDescriptor, + fuzz_campaign: FuzzCampaignDescriptor, + nix_lanes: NixLanesDescriptor, + operation: OperationDescriptor, + release_change_id: String, + result_vector: ResultVectorDescriptor, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct VectorSuite { + suite: String, + contract_version: String, + vectors: Vec<VectorCase>, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct VectorCase { + id: String, + kind: String, + input: VectorInput, + expected: VectorExpected, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct VectorInput { + format: String, + bytes_hex: String, + mutation: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct VectorExpected { + accepted: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + width: Option<u32>, + #[serde(default, skip_serializing_if = "Option::is_none")] + height: Option<u32>, + #[serde(default, skip_serializing_if = "Option::is_none")] + error: Option<String>, +} + +struct ValidatedVector { + bytes: Vec<u8>, + case_ids: Vec<String>, +} + +pub(crate) fn write_blossom_raster_decoder_security_manifest( + workspace_root: &Path, +) -> Result<(), String> { + validate_predecessors(workspace_root)?; + with_artifact_bundle_transaction(workspace_root, |transaction| { + transaction.write(expected_artifacts(workspace_root)?)?; + validate_manifest_under_lock(workspace_root) + }) +} + +pub(crate) fn validate_blossom_raster_decoder_security_manifest( + workspace_root: &Path, +) -> Result<(), String> { + validate_predecessors(workspace_root)?; + with_artifact_bundle_transaction(workspace_root, |_| { + validate_manifest_under_lock(workspace_root) + }) +} + +fn validate_predecessors(workspace_root: &Path) -> Result<(), String> { + validate_source_supersessions(workspace_root, SOURCE_SUPERSESSIONS)?; + validate_immutable_predecessor( + workspace_root, + "Blossom publication readiness", + BLOSSOM_PREDECESSOR_ARTIFACTS, + ) +} + +fn validate_manifest_under_lock(workspace_root: &Path) -> Result<(), String> { + for artifact in expected_artifacts(workspace_root)? { + let actual = read_regular_file(workspace_root, artifact.relative)?; + if actual != artifact.contents { + return Err(format!( + "generated Blossom raster decoder security contract {} is stale; run {WRITE_COMMAND}", + artifact.relative + )); + } + } + let bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?; + let manifest: RasterDecoderSecurityManifest = serde_json::from_slice(&bytes) + .map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?; + validate_canonical_json(MANIFEST_RELATIVE, &bytes, &manifest)?; + let schema_bytes = read_regular_file(workspace_root, MANIFEST_SCHEMA_RELATIVE)?; + let schema: Value = serde_json::from_slice(&schema_bytes) + .map_err(|error| format!("parse {MANIFEST_SCHEMA_RELATIVE}: {error}"))?; + validate_canonical_json(MANIFEST_SCHEMA_RELATIVE, &schema_bytes, &schema)?; + validate_json_schema( + &schema, + &serde_json::to_value(&manifest) + .map_err(|error| format!("serialize {MANIFEST_RELATIVE}: {error}"))?, + )?; + let sidecar = read_regular_file(workspace_root, MANIFEST_SHA256_RELATIVE)?; + if sidecar != format!("{}\n", sha256_hex(&bytes)).as_bytes() { + return Err(format!( + "{MANIFEST_SHA256_RELATIVE} must authenticate the exact manifest bytes" + )); + } + Ok(()) +} + +fn expected_artifacts(workspace_root: &Path) -> Result<Vec<GeneratedArtifact>, String> { + validate_source_contract(workspace_root)?; + let manifest_schema_bytes = canonical_json_bytes(&manifest_schema())?; + let manifest = describe_manifest(workspace_root, &manifest_schema_bytes)?; + let manifest_bytes = canonical_json_bytes(&manifest)?; + let sidecar_bytes = format!("{}\n", sha256_hex(&manifest_bytes)).into_bytes(); + let descriptor_bytes = canonical_json_bytes(&json!({ + "schema_version": SCHEMA_VERSION, + "contract_id": CONTRACT_ID, + "manifest": descriptor_for_bytes(MANIFEST_RELATIVE, &manifest_bytes), + "manifest_schema": descriptor_for_bytes(MANIFEST_SCHEMA_RELATIVE, &manifest_schema_bytes), + "manifest_sidecar": descriptor_for_bytes(MANIFEST_SHA256_RELATIVE, &sidecar_bytes), + "predecessor_contract_ids": ["radroots_blossom.publication_readiness_v1"] + }))?; + let vector_bytes = read_regular_file(workspace_root, VECTOR_RELATIVE)?; + Ok(vec![ + GeneratedArtifact { + relative: MANIFEST_RELATIVE, + contents: manifest_bytes, + }, + GeneratedArtifact { + relative: MANIFEST_SCHEMA_RELATIVE, + contents: manifest_schema_bytes, + }, + GeneratedArtifact { + relative: MANIFEST_SHA256_RELATIVE, + contents: sidecar_bytes, + }, + GeneratedArtifact { + relative: GENERATED_DESCRIPTOR_RELATIVE, + contents: descriptor_bytes, + }, + GeneratedArtifact { + relative: VECTOR_MIRROR_RELATIVE, + contents: vector_bytes, + }, + ]) +} + +fn describe_manifest( + workspace_root: &Path, + manifest_schema_bytes: &[u8], +) -> Result<RasterDecoderSecurityManifest, String> { + let vector = validate_vector(workspace_root)?; + Ok(RasterDecoderSecurityManifest { + schema_version: SCHEMA_VERSION, + contract_id: CONTRACT_ID.to_owned(), + authority_id: AUTHORITY_ID.to_owned(), + manifest_schema: descriptor_for_bytes(MANIFEST_SCHEMA_RELATIVE, manifest_schema_bytes), + predecessors: vec![predecessor_descriptor( + "radroots_blossom.publication_readiness_v1", + BLOSSOM_PREDECESSOR_ARTIFACTS, + SOURCE_SUPERSESSIONS, + )], + protocol_sources: vec![ + ProtocolSourcePin { + id: "nostr_nips".to_owned(), + repository: "https://github.com/nostr-protocol/nips".to_owned(), + revision: "bdfa7e62ef87fcfcb992b1a27aee49d36b0b4f91".to_owned(), + }, + ProtocolSourcePin { + id: "blossom".to_owned(), + repository: "https://github.com/hzrd149/blossom".to_owned(), + revision: "b5bd2801d1763aa635fc8fea7a76597e0eb18990".to_owned(), + }, + ], + resource_limits: ResourceLimitsDescriptor { + max_raster_bytes: MAX_RASTER_BYTES, + max_decoded_bytes: MAX_DECODED_BYTES, + max_dimension: MAX_DIMENSION, + max_pixels: MAX_PIXELS, + max_container_records: MAX_CONTAINER_RECORDS, + jpeg_max_scans: JPEG_MAX_SCANS, + jpeg_max_blocks: JPEG_MAX_BLOCKS, + jpeg_max_coefficient_steps: JPEG_MAX_COEFFICIENT_STEPS, + jpeg_entropy_bit_reads_per_byte: JPEG_ENTROPY_BIT_READS_PER_BYTE, + peak_rss_kib_limit: PEAK_RSS_KIB_LIMIT, + }, + decoder_profile: DecoderProfileDescriptor { + accepted_formats: owned(&["image/jpeg", "image/png", "image/webp"]), + accepted_jpeg_processes: owned(&[ + "sof0_baseline_8bit", + "sof1_extended_sequential_8bit", + ]), + accepted_png_color_types: vec![0, 2, 3, 4, 6], + stable_error_codes: owned(REJECTED_ERROR_CODES), + required_rejection_mutations: owned(REQUIRED_MUTATIONS), + }, + fuzz_campaign: FuzzCampaignDescriptor { + toolchain_channel: FUZZ_TOOLCHAIN_CHANNEL.to_owned(), + toolchain_components: owned(&["rust-src"]), + toolchain_profile: "minimal".to_owned(), + engine: "libfuzzer".to_owned(), + sanitizer: "address".to_owned(), + targets: owned(FUZZ_TARGETS), + corpus_seeds: corpus_seed_paths(workspace_root)?, + smoke_runs: FUZZ_SMOKE_RUNS, + smoke_seed: FUZZ_SMOKE_SEED, + smoke_max_input_bytes: FUZZ_SMOKE_MAX_INPUT_BYTES, + smoke_timeout_seconds: FUZZ_SMOKE_TIMEOUT_SECONDS, + engine_rss_limit_mb: FUZZ_ENGINE_RSS_LIMIT_MB, + extended_campaign_document: SECURITY_DOCUMENT_RELATIVE.to_owned(), + }, + nix_lanes: NixLanesDescriptor { + app: "decoder-security".to_owned(), + devshell: "decoder-security".to_owned(), + stable_check: "blossom-raster-decode-test".to_owned(), + fuzz_check: "blossom-decoder-fuzz-smoke".to_owned(), + }, + operation: OperationDescriptor { + key: OPERATION_KEY.to_owned(), + id: OPERATION_ID.to_owned(), + case_kinds: owned(&[ACCEPTED_KIND, REJECTED_KIND]), + }, + release_change_id: RELEASE_CHANGE_ID.to_owned(), + result_vector: ResultVectorDescriptor { + canonical_path: VECTOR_RELATIVE.to_owned(), + mirror_path: VECTOR_MIRROR_RELATIVE.to_owned(), + byte_length: vector.bytes.len() as u64, + sha256: sha256_hex(&vector.bytes), + hash_algorithm: HASH_ALGORITHM.to_owned(), + executor: descriptor_for_file(workspace_root, VECTOR_EXECUTOR_RELATIVE)?, + executor_tests: owned(&[REGRESSION_TEST, DIFFERENTIAL_TEST, RESOURCE_TEST]), + case_ids: vector.case_ids, + }, + }) +} + +fn corpus_seed_inventory(workspace_root: &Path) -> Result<Vec<(String, Vec<u8>)>, String> { + let vector_bytes = read_regular_file(workspace_root, VECTOR_RELATIVE)?; + let suite: VectorSuite = serde_json::from_slice(&vector_bytes) + .map_err(|error| format!("parse {VECTOR_RELATIVE}: {error}"))?; + if suite.vectors.len() != 30 { + return Err(format!( + "{VECTOR_RELATIVE} must contain exactly 30 cases, found {}", + suite.vectors.len() + )); + } + + suite + .vectors + .into_iter() + .map(|case| { + let target = FUZZ_TARGET_SPECS + .iter() + .find_map(|(target, format, _, _)| { + (*format == case.input.format).then_some(*target) + }) + .ok_or_else(|| { + format!( + "{} has unsupported fuzz seed format {}", + case.id, case.input.format + ) + })?; + let bytes = hex::decode(&case.input.bytes_hex) + .map_err(|error| format!("decode vector {} bytes_hex: {error}", case.id))?; + if bytes.is_empty() { + return Err(format!( + "vector {} cannot produce an empty fuzz seed", + case.id + )); + } + Ok(( + format!("{FUZZ_CORPUS_RELATIVE}/{target}/{}.bin", case.id), + bytes, + )) + }) + .collect() +} + +fn corpus_seed_paths(workspace_root: &Path) -> Result<Vec<String>, String> { + corpus_seed_inventory(workspace_root) + .map(|seeds| seeds.into_iter().map(|(relative, _)| relative).collect()) +} + +fn pat_ident_and_type(argument: &FnArg) -> Option<(String, &Type)> { + let FnArg::Typed(argument) = argument else { + return None; + }; + let Pat::Ident(ident) = argument.pat.as_ref() else { + return None; + }; + Some((ident.ident.to_string(), argument.ty.as_ref())) +} + +fn type_is_reference_to_str(ty: &Type) -> bool { + let Type::Reference(reference) = ty else { + return false; + }; + matches!( + reference.elem.as_ref(), + Type::Path(path) if path.path.is_ident("str") + ) +} + +fn type_is_reference_to_u8_slice(ty: &Type) -> bool { + let Type::Reference(reference) = ty else { + return false; + }; + let Type::Slice(slice) = reference.elem.as_ref() else { + return false; + }; + matches!( + slice.elem.as_ref(), + Type::Path(path) if path.path.is_ident("u8") + ) +} + +fn expression_is_ident(expression: &Expr, expected: &str) -> bool { + matches!( + expression, + Expr::Path(path) if path.path.is_ident(expected) + ) +} + +fn expression_string_literal(expression: &Expr) -> Option<String> { + match expression { + Expr::Lit(literal) => match &literal.lit { + Lit::Str(value) => Some(value.value()), + _ => None, + }, + _ => None, + } +} + +fn use_tree_imports_name(tree: &UseTree, expected: &str) -> bool { + match tree { + UseTree::Name(name) => name.ident == expected, + UseTree::Path(path) => use_tree_imports_name(&path.tree, expected), + UseTree::Group(group) => group + .items + .iter() + .any(|item| use_tree_imports_name(item, expected)), + UseTree::Rename(_) | UseTree::Glob(_) => false, + } +} + +fn validate_fuzz_common_harness(workspace_root: &Path) -> Result<(), String> { + let bytes = read_regular_file(workspace_root, FUZZ_COMMON_RELATIVE)?; + let source = std::str::from_utf8(&bytes) + .map_err(|error| format!("{FUZZ_COMMON_RELATIVE} must be UTF-8: {error}"))?; + validate_fuzz_common_source(source) +} + +fn validate_fuzz_common_source(source: &str) -> Result<(), String> { + let file = syn::parse_file(source) + .map_err(|error| format!("parse {FUZZ_COMMON_RELATIVE}: {error}"))?; + + let operation_imports = file + .items + .iter() + .filter_map(|item| match item { + Item::Use(item) => Some(item), + _ => None, + }) + .filter(|item| { + matches!( + &item.tree, + UseTree::Path(path) + if path.ident == "radroots_blossom" + && use_tree_imports_name(&path.tree, "verify_publication_readiness") + ) + }) + .count(); + if operation_imports != 1 { + return Err(format!( + "{FUZZ_COMMON_RELATIVE} must import verify_publication_readiness from radroots_blossom exactly once" + )); + } + if file.items.iter().any(|item| { + matches!( + item, + Item::Fn(function) if function.sig.ident == "verify_publication_readiness" + ) + }) { + return Err(format!( + "{FUZZ_COMMON_RELATIVE} must not shadow verify_publication_readiness" + )); + } + + let exercises = file + .items + .iter() + .filter_map(|item| match item { + Item::Fn(function) if function.sig.ident == "exercise" => Some(function), + _ => None, + }) + .collect::<Vec<_>>(); + if exercises.len() != 1 { + return Err(format!( + "{FUZZ_COMMON_RELATIVE} must define the common exercise harness exactly once" + )); + } + let exercise = exercises[0]; + let arguments = exercise.sig.inputs.iter().collect::<Vec<_>>(); + let signature_matches = arguments.len() == 3 + && pat_ident_and_type(arguments[0]) + .is_some_and(|(name, ty)| name == "input" && type_is_reference_to_u8_slice(ty)) + && pat_ident_and_type(arguments[1]) + .is_some_and(|(name, ty)| name == "media_type" && type_is_reference_to_str(ty)) + && pat_ident_and_type(arguments[2]) + .is_some_and(|(name, ty)| name == "extension" && type_is_reference_to_str(ty)) + && exercise.sig.constness.is_none() + && exercise.sig.asyncness.is_none() + && exercise.sig.unsafety.is_none() + && exercise.sig.abi.is_none() + && exercise.sig.generics.params.is_empty() + && matches!(exercise.sig.output, syn::ReturnType::Default); + if !signature_matches { + return Err(format!( + "{FUZZ_COMMON_RELATIVE}::exercise must retain the governed raw-byte harness signature" + )); + } + + #[derive(Default)] + struct ReadinessCalls { + total: usize, + passes_input: bool, + } + + impl<'ast> Visit<'ast> for ReadinessCalls { + fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) { + if let Expr::Path(path) = call.func.as_ref() + && path.path.is_ident("verify_publication_readiness") + { + self.total += 1; + self.passes_input = call + .args + .iter() + .nth(1) + .is_some_and(|argument| expression_is_ident(argument, "input")); + } + syn::visit::visit_expr_call(self, call); + } + } + + let mut readiness_calls = ReadinessCalls::default(); + readiness_calls.visit_block(&exercise.block); + if readiness_calls.total != 1 || !readiness_calls.passes_input { + return Err(format!( + "{FUZZ_COMMON_RELATIVE}::exercise must pass its raw input to exactly one verify_publication_readiness call" + )); + } + Ok(()) +} + +fn validate_fuzz_target( + workspace_root: &Path, + target: &str, + expected_media_type: &str, + expected_extension: &str, +) -> Result<(), String> { + let relative = format!("fuzz/fuzz_targets/{target}.rs"); + let bytes = read_regular_file(workspace_root, &relative)?; + let source = std::str::from_utf8(&bytes) + .map_err(|error| format!("{relative} must be UTF-8: {error}"))?; + validate_fuzz_target_source(&relative, source, expected_media_type, expected_extension) +} + +fn validate_fuzz_target_source( + relative: &str, + source: &str, + expected_media_type: &str, + expected_extension: &str, +) -> Result<(), String> { + let file = syn::parse_file(source).map_err(|error| format!("parse {relative}: {error}"))?; + + struct FuzzTargetMacros<'ast>(Vec<&'ast syn::Macro>); + + impl<'ast> Visit<'ast> for FuzzTargetMacros<'ast> { + fn visit_macro(&mut self, item: &'ast syn::Macro) { + if item.path.is_ident("fuzz_target") { + self.0.push(item); + } + syn::visit::visit_macro(self, item); + } + } + + let mut macros = FuzzTargetMacros(Vec::new()); + macros.visit_file(&file); + if macros.0.len() != 1 { + return Err(format!( + "{relative} must declare exactly one libFuzzer target" + )); + } + let closure: syn::ExprClosure = syn::parse2(macros.0[0].tokens.clone()) + .map_err(|error| format!("parse {relative} fuzz_target closure: {error}"))?; + if closure.inputs.len() != 1 + || closure.asyncness.is_some() + || closure.movability.is_some() + || closure.capture.is_some() + || !matches!(closure.output, syn::ReturnType::Default) + { + return Err(format!( + "{relative} must accept exactly one borrowed libFuzzer byte slice" + )); + } + let input = closure.inputs.first().expect("length checked"); + let Pat::Type(input) = input else { + return Err(format!("{relative} fuzz input must be explicitly typed")); + }; + let Pat::Ident(input_ident) = input.pat.as_ref() else { + return Err(format!("{relative} fuzz input must be a simple identifier")); + }; + if !type_is_reference_to_u8_slice(&input.ty) { + return Err(format!("{relative} fuzz input must have type &[u8]")); + } + + let Expr::Block(body) = closure.body.as_ref() else { + return Err(format!( + "{relative} fuzz body must call the governed common harness" + )); + }; + let [Stmt::Expr(Expr::Call(call), _)] = body.block.stmts.as_slice() else { + return Err(format!( + "{relative} fuzz body must contain exactly one common harness call" + )); + }; + let Expr::Path(function) = call.func.as_ref() else { + return Err(format!("{relative} must call common::exercise directly")); + }; + let function_segments = function + .path + .segments + .iter() + .map(|segment| segment.ident.to_string()) + .collect::<Vec<_>>(); + let arguments = call.args.iter().collect::<Vec<_>>(); + if function_segments != ["common", "exercise"] + || arguments.len() != 3 + || !expression_is_ident(arguments[0], &input_ident.ident.to_string()) + || expression_string_literal(arguments[1]).as_deref() != Some(expected_media_type) + || expression_string_literal(arguments[2]).as_deref() != Some(expected_extension) + { + return Err(format!( + "{relative} must pass its libFuzzer bytes and the governed MIME/extension to common::exercise" + )); + } + Ok(()) +} + +fn validate_fuzz_corpus(workspace_root: &Path) -> Result<(), String> { + let mut expected = BTreeMap::new(); + for (relative, bytes) in corpus_seed_inventory(workspace_root)? { + if expected.insert(relative.clone(), bytes).is_some() { + return Err(format!("duplicate fuzz seed authority for {relative}")); + } + } + if expected.len() != 30 { + return Err(format!( + "fuzz corpus must contain exactly 30 governed seeds, found {}", + expected.len() + )); + } + + let corpus_root = workspace_root.join(FUZZ_CORPUS_RELATIVE); + let root_entries = fs::read_dir(&corpus_root) + .map_err(|error| format!("read {FUZZ_CORPUS_RELATIVE}: {error}"))?; + let expected_targets = FUZZ_TARGETS + .iter() + .map(|target| (*target).to_owned()) + .collect::<BTreeSet<_>>(); + let mut actual_targets = BTreeSet::new(); + let mut actual_files = BTreeSet::new(); + for target_entry in root_entries { + let target_entry = target_entry + .map_err(|error| format!("read entry under {FUZZ_CORPUS_RELATIVE}: {error}"))?; + let target_name = target_entry + .file_name() + .into_string() + .map_err(|_| "fuzz corpus target directories must be UTF-8".to_owned())?; + let target_type = target_entry + .file_type() + .map_err(|error| format!("inspect fuzz corpus target {target_name}: {error}"))?; + if !target_type.is_dir() || !expected_targets.contains(target_name.as_str()) { + return Err(format!( + "unexpected fuzz corpus target entry {FUZZ_CORPUS_RELATIVE}/{target_name}" + )); + } + if !actual_targets.insert(target_name.clone()) { + return Err(format!( + "duplicate fuzz corpus target directory {target_name}" + )); + } + for seed_entry in fs::read_dir(target_entry.path()) + .map_err(|error| format!("read fuzz corpus target {target_name}: {error}"))? + { + let seed_entry = seed_entry + .map_err(|error| format!("read fuzz corpus seed under {target_name}: {error}"))?; + let seed_name = seed_entry + .file_name() + .into_string() + .map_err(|_| "fuzz corpus seed names must be UTF-8".to_owned())?; + let seed_type = seed_entry.file_type().map_err(|error| { + format!("inspect fuzz corpus seed {target_name}/{seed_name}: {error}") + })?; + if !seed_type.is_file() { + return Err(format!( + "fuzz corpus seed {target_name}/{seed_name} must be a regular file" + )); + } + let relative = format!("{FUZZ_CORPUS_RELATIVE}/{target_name}/{seed_name}"); + if !actual_files.insert(relative.clone()) { + return Err(format!("duplicate fuzz corpus seed {relative}")); + } + let expected_bytes = expected + .get(&relative) + .ok_or_else(|| format!("unexpected or retargeted fuzz corpus seed {relative}"))?; + let actual_bytes = read_regular_file(workspace_root, &relative)?; + if &actual_bytes != expected_bytes { + return Err(format!( + "fuzz corpus seed {relative} differs from its exact vector bytes" + )); + } + } + } + if actual_targets != expected_targets || actual_files != expected.keys().cloned().collect() { + return Err("fuzz corpus is missing a governed target or exact vector seed".to_owned()); + } + Ok(()) +} + +fn validate_source_contract(workspace_root: &Path) -> Result<(), String> { + validate_resource_limits(workspace_root)?; + validate_error_authority(workspace_root)?; + validate_vector_executor(workspace_root)?; + validate_fuzz_authority(workspace_root)?; + validate_nix_lanes(workspace_root)?; + validate_operations_authority(workspace_root)?; + validate_release_authority(workspace_root)?; + validate_vector(workspace_root)?; + Ok(()) +} + +fn collect_const_values( + source_relative: &str, + source: &str, +) -> Result<BTreeMap<String, u64>, String> { + let file = + syn::parse_file(source).map_err(|error| format!("parse {source_relative}: {error}"))?; + let mut values = BTreeMap::new(); + for _ in 0..4 { + for item in &file.items { + if let Item::Const(item) = item { + let name = item.ident.to_string(); + if values.contains_key(&name) { + continue; + } + if let Some(value) = evaluate_u64_expr(&item.expr, &values) { + values.insert(name, value); + } + } + } + } + Ok(values) +} + +fn evaluate_u64_expr(expr: &Expr, known: &BTreeMap<String, u64>) -> Option<u64> { + match expr { + Expr::Lit(lit) => match &lit.lit { + Lit::Int(value) => value.base10_digits().parse().ok(), + _ => None, + }, + Expr::Path(path) => path + .path + .get_ident() + .and_then(|ident| known.get(&ident.to_string()).copied()), + Expr::Binary(binary) => match binary.op { + BinOp::Mul(_) => evaluate_u64_expr(&binary.left, known)? + .checked_mul(evaluate_u64_expr(&binary.right, known)?), + _ => None, + }, + Expr::Paren(paren) => evaluate_u64_expr(&paren.expr, known), + Expr::Group(group) => evaluate_u64_expr(&group.expr, known), + _ => None, + } +} + +fn validate_limit_constants( + workspace_root: &Path, + source_relative: &str, + expected: &[(&str, u64)], +) -> Result<(), String> { + let bytes = read_regular_file(workspace_root, source_relative)?; + let source = std::str::from_utf8(&bytes) + .map_err(|error| format!("{source_relative} must be UTF-8: {error}"))?; + let values = collect_const_values(source_relative, source)?; + for (name, expected_value) in expected { + match values.get(*name) { + Some(actual) if actual == expected_value => {} + Some(actual) => { + return Err(format!( + "{source_relative} constant {name} drifted: expected {expected_value}, found {actual}" + )); + } + None => { + return Err(format!( + "{source_relative} must define an evaluatable u64 constant {name}" + )); + } + } + } + Ok(()) +} + +fn validate_resource_limits(workspace_root: &Path) -> Result<(), String> { + validate_limit_constants( + workspace_root, + READINESS_SOURCE_RELATIVE, + READINESS_LIMIT_CONSTANTS, + )?; + validate_limit_constants(workspace_root, JPEG_SOURCE_RELATIVE, JPEG_LIMIT_CONSTANTS) +} + +fn validate_error_authority(workspace_root: &Path) -> Result<(), String> { + let bytes = read_regular_file(workspace_root, ERROR_SOURCE_RELATIVE)?; + let source = std::str::from_utf8(&bytes) + .map_err(|error| format!("{ERROR_SOURCE_RELATIVE} must be UTF-8: {error}"))?; + let file = syn::parse_file(source) + .map_err(|error| format!("parse {ERROR_SOURCE_RELATIVE}: {error}"))?; + let mut variants = BTreeSet::new(); + for item in &file.items { + if let Item::Enum(item) = item + && item.ident == "RadrootsBlossomError" + && is_public(&item.vis) + { + for variant in &item.variants { + variants.insert(variant.ident.to_string()); + } + } + } + if !variants.contains("PublicationRasterProcessForbidden") { + return Err(format!( + "{ERROR_SOURCE_RELATIVE} must expose the PublicationRasterProcessForbidden variant" + )); + } + + #[derive(Default)] + struct StringLiterals(BTreeSet<String>); + + impl<'ast> Visit<'ast> for StringLiterals { + fn visit_expr(&mut self, expression: &'ast Expr) { + if let Expr::Lit(lit) = expression + && let Lit::Str(value) = &lit.lit + { + self.0.insert(value.value()); + } + syn::visit::visit_expr(self, expression); + } + } + + let mut literals = StringLiterals::default(); + literals.visit_file(&file); + if !literals.0.contains("publication_raster_process_forbidden") { + return Err(format!( + "{ERROR_SOURCE_RELATIVE} must map the stable publication_raster_process_forbidden code" + )); + } + Ok(()) +} + +fn validate_vector_executor(workspace_root: &Path) -> Result<(), String> { + let bytes = read_regular_file(workspace_root, VECTOR_EXECUTOR_RELATIVE)?; + let source = std::str::from_utf8(&bytes) + .map_err(|error| format!("{VECTOR_EXECUTOR_RELATIVE} must be UTF-8: {error}"))?; + let file = syn::parse_file(source) + .map_err(|error| format!("parse {VECTOR_EXECUTOR_RELATIVE}: {error}"))?; + let cfg_gates = file + .attrs + .iter() + .filter(|attribute| attribute.path().is_ident("cfg")) + .count(); + if cfg_gates != 1 { + return Err(format!( + "{VECTOR_EXECUTOR_RELATIVE} must be gated by exactly one crate-level cfg attribute" + )); + } + for (test, ignored) in [ + (REGRESSION_TEST, false), + (DIFFERENTIAL_TEST, true), + (RESOURCE_TEST, true), + ] { + let tests = file + .items + .iter() + .filter_map(|item| match item { + Item::Fn(function) if function.sig.ident == test => Some(function), + _ => None, + }) + .collect::<Vec<_>>(); + if tests.len() != 1 { + return Err(format!( + "{VECTOR_EXECUTOR_RELATIVE} must define {test} exactly once" + )); + } + let function = tests[0]; + let has_test = function + .attrs + .iter() + .any(|attribute| attribute.path().is_ident("test")); + let has_ignore = function + .attrs + .iter() + .any(|attribute| attribute.path().is_ident("ignore")); + if !has_test + || has_ignore != ignored + || function.sig.constness.is_some() + || function.sig.asyncness.is_some() + || function.sig.unsafety.is_some() + || function.sig.abi.is_some() + || !function.sig.inputs.is_empty() + || !matches!(function.sig.output, syn::ReturnType::Default) + { + return Err(format!( + "{VECTOR_EXECUTOR_RELATIVE}::{test} has an invalid test signature or gating" + )); + } + } + + #[derive(Default)] + struct OperationCalls(BTreeSet<String>); + + impl<'ast> Visit<'ast> for OperationCalls { + fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) { + if let Expr::Path(path) = call.func.as_ref() + && let Some(segment) = path.path.segments.last() + { + self.0.insert(segment.ident.to_string()); + } + syn::visit::visit_expr_call(self, call); + } + + fn visit_expr_method_call(&mut self, call: &'ast syn::ExprMethodCall) { + self.0.insert(call.method.to_string()); + syn::visit::visit_expr_method_call(self, call); + } + + fn visit_item_fn(&mut self, function: &'ast syn::ItemFn) { + self.0.insert(function.sig.ident.to_string()); + syn::visit::visit_item_fn(self, function); + } + } + + let mut calls = OperationCalls::default(); + calls.visit_file(&file); + if !calls.0.contains("verify_publication_readiness") { + return Err(format!( + "{VECTOR_EXECUTOR_RELATIVE} must execute the public verify_publication_readiness operation" + )); + } + Ok(()) +} + +fn insert_fuzz_bin_name(bin_names: &mut BTreeSet<String>, name: &str) -> Result<(), String> { + if !bin_names.insert(name.to_owned()) { + return Err(format!("fuzz bin target {name} is declared more than once")); + } + Ok(()) +} + +fn validate_fuzz_authority(workspace_root: &Path) -> Result<(), String> { + let workspace = parse_toml(workspace_root, WORKSPACE_MANIFEST_RELATIVE)?; + let excludes = toml_string_array( + "workspace exclude", + workspace + .get("workspace") + .and_then(|value| value.get("exclude")), + )?; + if excludes + .iter() + .filter(|value| value.as_str() == "fuzz") + .count() + != 1 + { + return Err("workspace manifest must exclude the fuzz project exactly once".to_owned()); + } + + let fuzz = parse_toml(workspace_root, FUZZ_MANIFEST_RELATIVE)?; + let package = fuzz + .get("package") + .and_then(toml::Value::as_table) + .ok_or_else(|| "fuzz package table is missing".to_owned())?; + if package.get("name").and_then(toml::Value::as_str) != Some("radroots-fuzz") + || package.get("publish").and_then(toml::Value::as_bool) != Some(false) + || package + .get("metadata") + .and_then(|value| value.get("cargo-fuzz")) + .and_then(toml::Value::as_bool) + != Some(true) + { + return Err( + "fuzz package must be the non-publishable radroots-fuzz cargo-fuzz project".to_owned(), + ); + } + let dependencies = fuzz + .get("dependencies") + .and_then(toml::Value::as_table) + .ok_or_else(|| "fuzz dependencies table is missing".to_owned())?; + if dependencies.contains_key("hex") { + return Err("fuzz project must not decode textual hex corpus wrappers".to_owned()); + } + for required in ["libfuzzer-sys"] { + if !dependencies.contains_key(required) { + return Err(format!("fuzz dependency {required} is missing")); + } + } + let blossom = dependencies + .get("radroots_blossom") + .and_then(toml::Value::as_table) + .ok_or_else(|| "fuzz radroots_blossom dependency is missing".to_owned())?; + let blossom_features = + toml_string_array("fuzz radroots_blossom features", blossom.get("features"))?; + if blossom.get("path").and_then(toml::Value::as_str) != Some("../crates/blossom") + || blossom + .get("default-features") + .and_then(toml::Value::as_bool) + != Some(false) + || blossom_features.into_iter().collect::<BTreeSet<_>>() + != ["raster-decode", "serde"] + .into_iter() + .map(str::to_owned) + .collect() + { + return Err("fuzz radroots_blossom dependency profile drifted".to_owned()); + } + let bins = fuzz + .get("bin") + .and_then(toml::Value::as_array) + .ok_or_else(|| "fuzz bin targets are missing".to_owned())?; + let mut bin_names = BTreeSet::new(); + for bin in bins { + let bin = bin + .as_table() + .ok_or_else(|| "fuzz bin targets must be tables".to_owned())?; + let name = bin + .get("name") + .and_then(toml::Value::as_str) + .ok_or_else(|| "fuzz bin target names must be strings".to_owned())?; + let path = bin + .get("path") + .and_then(toml::Value::as_str) + .ok_or_else(|| "fuzz bin target paths must be strings".to_owned())?; + if path != format!("fuzz_targets/{name}.rs") + || bin.get("test").and_then(toml::Value::as_bool) != Some(false) + || bin.get("doc").and_then(toml::Value::as_bool) != Some(false) + || bin.get("bench").and_then(toml::Value::as_bool) != Some(false) + { + return Err(format!("fuzz bin target {name} drifted")); + } + insert_fuzz_bin_name(&mut bin_names, name)?; + } + if bin_names + != FUZZ_TARGETS + .iter() + .map(|value| (*value).to_owned()) + .collect() + { + return Err(format!( + "fuzz targets drifted: expected {FUZZ_TARGETS:?}, found {bin_names:?}" + )); + } + + let toolchain = parse_toml(workspace_root, FUZZ_TOOLCHAIN_RELATIVE)?; + let toolchain = toolchain + .get("toolchain") + .and_then(toml::Value::as_table) + .ok_or_else(|| "fuzz toolchain table is missing".to_owned())?; + let components = toml_string_array("fuzz toolchain components", toolchain.get("components"))?; + if toolchain.get("channel").and_then(toml::Value::as_str) != Some(FUZZ_TOOLCHAIN_CHANNEL) + || toolchain.get("profile").and_then(toml::Value::as_str) != Some("minimal") + || components != ["rust-src".to_owned()] + { + return Err("fuzz toolchain authority drifted".to_owned()); + } + + let lockfile = parse_toml(workspace_root, FUZZ_LOCKFILE_RELATIVE)?; + let packages = lockfile + .get("package") + .and_then(toml::Value::as_array) + .ok_or_else(|| "fuzz lockfile packages are missing".to_owned())?; + let package_names = packages + .iter() + .filter_map(|package| package.get("name").and_then(toml::Value::as_str)) + .collect::<BTreeSet<_>>(); + for required in ["radroots-fuzz", "radroots_blossom", "libfuzzer-sys"] { + if !package_names.contains(required) { + return Err(format!("fuzz lockfile is missing package {required}")); + } + } + let fuzz_lock_package = packages + .iter() + .find(|package| package.get("name").and_then(toml::Value::as_str) == Some("radroots-fuzz")) + .ok_or_else(|| "fuzz lockfile is missing package radroots-fuzz".to_owned())?; + let fuzz_lock_dependencies = toml_string_array( + "fuzz lockfile radroots-fuzz dependencies", + fuzz_lock_package.get("dependencies"), + )? + .into_iter() + .collect::<BTreeSet<_>>(); + if fuzz_lock_dependencies + != ["libfuzzer-sys", "radroots_blossom"] + .into_iter() + .map(str::to_owned) + .collect() + { + return Err("fuzz lockfile root dependency authority drifted".to_owned()); + } + + validate_fuzz_common_harness(workspace_root)?; + for (target, _, media_type, extension) in FUZZ_TARGET_SPECS { + validate_fuzz_target(workspace_root, target, media_type, extension)?; + } + validate_fuzz_corpus(workspace_root)?; + read_regular_file(workspace_root, SECURITY_DOCUMENT_RELATIVE)?; + read_regular_file(workspace_root, BLOSSOM_MANIFEST_RELATIVE)?; + Ok(()) +} + +fn validate_nix_lanes(workspace_root: &Path) -> Result<(), String> { + for (relative, needles) in [ + (NIX_APPS_RELATIVE, vec!["decoder-security"]), + (NIX_DEVSHELLS_RELATIVE, vec!["decoder-security"]), + ( + NIX_CHECKS_RELATIVE, + vec!["blossom-raster-decode-test", "blossom-decoder-fuzz-smoke"], + ), + (NIX_TOOLCHAINS_RELATIVE, vec!["rust-toolchain-fuzz.toml"]), + ( + NIX_COMMON_RELATIVE, + vec![ + "decoderSecurityCommand", + "decoderSecurityStableCommand", + "decoderSecurityFuzzCommand", + "fuzz/Cargo.lock", + "131072", + "-runs=256", + "-seed=424242", + "-max_len=65536", + ], + ), + ] { + let bytes = read_regular_file(workspace_root, relative)?; + let source = std::str::from_utf8(&bytes) + .map_err(|error| format!("{relative} must be UTF-8: {error}"))?; + for needle in needles { + if !source.contains(needle) { + return Err(format!( + "{relative} must declare the governed decoder-security lane fragment {needle}" + )); + } + } + } + Ok(()) +} + +fn validate_operations_authority(workspace_root: &Path) -> Result<(), String> { + let manifest = parse_toml(workspace_root, OPERATIONS_RELATIVE)?; + let operations = manifest + .get("operations") + .and_then(toml::Value::as_table) + .ok_or_else(|| "operations.toml has no operations table".to_owned())?; + let operation = operations + .get(OPERATION_KEY) + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("operations.toml is missing {OPERATION_KEY}"))?; + for (field, expected) in [ + ("domain", "blossom"), + ("id", OPERATION_ID), + ("stability", "beta"), + ("error_class", "validation_error"), + ("signing", "none"), + ("transport", "none"), + ] { + if operation.get(field).and_then(toml::Value::as_str) != Some(expected) { + return Err(format!("{OPERATION_KEY} {field} must be {expected}")); + } + } + if operation + .get("deterministic") + .and_then(toml::Value::as_bool) + != Some(true) + || toml_string_array("operation inputs", operation.get("inputs"))? + != owned(OPERATION_INPUTS) + || toml_string_array("operation outputs", operation.get("outputs"))? + != owned(OPERATION_OUTPUTS) + { + return Err(format!("{OPERATION_KEY} signature or determinism drifted")); + } + let implementation = operation + .get("implementation") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{OPERATION_KEY} implementation is missing"))?; + if toml_string_array("operation modules", implementation.get("rust_modules"))? + != owned(OPERATION_MODULES) + || toml_string_array("operation rust types", implementation.get("rust_types"))? + != owned(OPERATION_RUST_TYPES) + { + return Err(format!("{OPERATION_KEY} implementation drifted")); + } + let conformance = operation + .get("conformance") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{OPERATION_KEY} conformance is missing"))?; + if conformance.get("vector").and_then(toml::Value::as_str) != Some(VECTOR_RELATIVE) + || toml_string_array("operation case kinds", conformance.get("case_kinds"))? + != owned(&[ACCEPTED_KIND, REJECTED_KIND]) + { + return Err(format!("{OPERATION_KEY} conformance drifted")); + } + Ok(()) +} + +fn validate_release_authority(workspace_root: &Path) -> Result<(), String> { + let release = parse_toml(workspace_root, RELEASE_RELATIVE)?; + let changes = release + .get("changes") + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("{RELEASE_RELATIVE} must declare changes"))?; + let matching = changes + .iter() + .filter(|change| change.get("id").and_then(toml::Value::as_str) == Some(RELEASE_CHANGE_ID)) + .collect::<Vec<_>>(); + if matching.len() != 1 + || matching[0] + .get("classification") + .and_then(toml::Value::as_str) + != Some("breaking") + || matching[0] + .get("semver_impacts") + .and_then(toml::Value::as_array) + .map(|impacts| { + impacts + .iter() + .filter_map(toml::Value::as_str) + .collect::<BTreeSet<_>>() + }) + != Some(RELEASE_SEMVER_IMPACTS.iter().copied().collect()) + { + return Err(format!( + "{RELEASE_RELATIVE} must contain exactly one breaking change {RELEASE_CHANGE_ID} with its governed semver impacts" + )); + } + let changelog = read_regular_file(workspace_root, CHANGELOG_RELATIVE)?; + let changelog = std::str::from_utf8(&changelog) + .map_err(|error| format!("{CHANGELOG_RELATIVE} must be UTF-8: {error}"))?; + if changelog.matches(CHANGELOG_MARKER).count() != 1 { + return Err(format!( + "{CHANGELOG_RELATIVE} must contain exactly one {CHANGELOG_MARKER}" + )); + } + Ok(()) +} + +fn validate_vector(workspace_root: &Path) -> Result<ValidatedVector, String> { + let bytes = read_regular_file(workspace_root, VECTOR_RELATIVE)?; + let suite: VectorSuite = serde_json::from_slice(&bytes) + .map_err(|error| format!("parse {VECTOR_RELATIVE}: {error}"))?; + if suite.suite != "blossom_raster_decoder_security" || suite.contract_version != "1.0.0" { + return Err("Blossom raster decoder security vector identity drifted".to_owned()); + } + validate_canonical_json(VECTOR_RELATIVE, &bytes, &suite)?; + let ordered_ids = suite + .vectors + .iter() + .map(|case| case.id.clone()) + .collect::<Vec<_>>(); + if ordered_ids + != VECTOR_CASE_IDS + .iter() + .map(|id| (*id).to_owned()) + .collect::<Vec<_>>() + { + return Err("Blossom raster decoder security vector case inventory drifted".to_owned()); + } + let mut ids = BTreeSet::new(); + let mut accepted_formats = BTreeSet::new(); + let mut mutations = BTreeSet::new(); + let mut rejected_codes = BTreeSet::new(); + for case in &suite.vectors { + if !ids.insert(case.id.clone()) + || case.input.format.is_empty() + || case.input.mutation.is_empty() + || case.input.bytes_hex.is_empty() + || case.input.bytes_hex.len() % 2 != 0 + || !case + .input + .bytes_hex + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err( + "Blossom raster decoder security vector ids and inputs must be unique/nonempty hex" + .to_owned(), + ); + } + mutations.insert(case.input.mutation.clone()); + match case.kind.as_str() { + ACCEPTED_KIND => { + if !case.expected.accepted + || case.expected.error.is_some() + || !case.expected.width.is_some_and(|width| width >= 1) + || !case.expected.height.is_some_and(|height| height >= 1) + { + return Err(format!("{} has invalid acceptance expectation", case.id)); + } + accepted_formats.insert(case.input.format.clone()); + } + REJECTED_KIND => { + if case.expected.accepted + || case.expected.width.is_some() + || case.expected.height.is_some() + || !case + .expected + .error + .as_deref() + .is_some_and(|error| REJECTED_ERROR_CODES.contains(&error)) + { + return Err(format!("{} has invalid rejection expectation", case.id)); + } + rejected_codes.insert(case.expected.error.clone().unwrap_or_default()); + } + kind => return Err(format!("{} uses unsupported kind {kind}", case.id)), + } + } + if accepted_formats + != ["jpeg", "png", "webp"] + .into_iter() + .map(str::to_owned) + .collect() + { + return Err( + "Blossom raster decoder security vector must accept all three raster formats" + .to_owned(), + ); + } + if mutations + != REQUIRED_MUTATIONS + .iter() + .map(|mutation| (*mutation).to_owned()) + .collect() + { + return Err("Blossom raster decoder security mutation inventory drifted".to_owned()); + } + for required in REQUIRED_DEDICATED_ERROR_CODES { + if !rejected_codes.contains(*required) { + return Err(format!( + "Blossom raster decoder security vector is missing rejection {required}" + )); + } + } + Ok(ValidatedVector { + bytes, + case_ids: ordered_ids, + }) +} + +fn manifest_schema() -> Value { + json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://radroots.org/schemas/blossom/raster-decoder-security-manifest-v1.json", + "title": "Radroots Blossom raster decoder security semantic contract", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "contract_id", "authority_id", "manifest_schema", "predecessors", "protocol_sources", "resource_limits", "decoder_profile", "fuzz_campaign", "nix_lanes", "operation", "release_change_id", "result_vector"], + "properties": { + "schema_version": {"const": SCHEMA_VERSION}, + "contract_id": {"const": CONTRACT_ID}, + "authority_id": {"const": AUTHORITY_ID}, + "manifest_schema": {"$ref": "#/$defs/file"}, + "predecessors": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["contract_id", "immutable_artifacts", "source_supersessions"], + "properties": { + "contract_id": {"type": "string", "minLength": 1}, + "immutable_artifacts": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/file"}}, + "source_supersessions": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}, "uniqueItems": true} + } + } + }, + "protocol_sources": {"type": "array", "minItems": 2, "maxItems": 2, "items": {"type": "object"}}, + "resource_limits": {"type": "object"}, + "decoder_profile": {"type": "object"}, + "fuzz_campaign": {"type": "object"}, + "nix_lanes": {"type": "object"}, + "operation": {"type": "object"}, + "release_change_id": {"const": RELEASE_CHANGE_ID}, + "result_vector": {"type": "object"} + }, + "$defs": { + "file": { + "type": "object", + "additionalProperties": false, + "required": ["path", "byte_length", "sha256", "hash_algorithm"], + "properties": { + "path": {"type": "string", "minLength": 1}, + "byte_length": {"type": "integer", "minimum": 1}, + "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "hash_algorithm": {"const": HASH_ALGORITHM} + } + } + } + }) +} + +fn is_public(visibility: &Visibility) -> bool { + matches!(visibility, Visibility::Public(_)) +} + +fn owned(values: &[&str]) -> Vec<String> { + values.iter().map(|value| (*value).to_owned()).collect() +} + +fn parse_toml(workspace_root: &Path, relative: &str) -> Result<toml::Value, String> { + let bytes = read_regular_file(workspace_root, relative)?; + let source = std::str::from_utf8(&bytes) + .map_err(|error| format!("{relative} must be UTF-8: {error}"))?; + toml::from_str(source).map_err(|error| format!("parse {relative}: {error}")) +} + +fn toml_string_array(label: &str, value: Option<&toml::Value>) -> Result<Vec<String>, String> { + value + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("{label} must be an array"))? + .iter() + .map(|value| { + value + .as_str() + .map(str::to_owned) + .ok_or_else(|| format!("{label} values must be strings")) + }) + .collect() +} + +fn canonical_json_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, String> { + let mut bytes = + serde_json::to_vec_pretty(value).map_err(|error| format!("serialize JSON: {error}"))?; + bytes.push(b'\n'); + Ok(bytes) +} + +fn validate_canonical_json<T: Serialize>( + relative: &str, + bytes: &[u8], + value: &T, +) -> Result<(), String> { + if canonical_json_bytes(value)? != bytes { + return Err(format!("{relative} is not canonical pretty JSON")); + } + Ok(()) +} + +fn validate_json_schema(schema: &Value, instance: &Value) -> Result<(), String> { + let validator = jsonschema::validator_for(schema) + .map_err(|error| format!("compile {MANIFEST_SCHEMA_RELATIVE}: {error}"))?; + let errors = validator + .iter_errors(instance) + .map(|error| error.to_string()) + .collect::<Vec<_>>(); + if errors.is_empty() { + Ok(()) + } else { + Err(format!( + "{MANIFEST_RELATIVE} violates its schema: {}", + errors.join("; ") + )) + } +} + +fn sha256_hex(bytes: &[u8]) -> String { + hex::encode(Sha256::digest(bytes)) +} + +#[cfg(test)] +mod tests { + use super::{ + insert_fuzz_bin_name, validate_fuzz_authority, validate_fuzz_common_source, + validate_fuzz_target_source, validate_vector, + }; + use std::{collections::BTreeSet, path::Path}; + + const COMMON_SOURCE: &str = include_str!("../../../../fuzz/fuzz_targets/common.rs"); + const JPEG_TARGET_SOURCE: &str = + include_str!("../../../../fuzz/fuzz_targets/publication_jpeg.rs"); + + #[test] + fn checked_in_fuzz_corpus_matches_exact_vector() { + let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + validate_vector(&workspace_root).expect("exact 30-case vector authority"); + validate_fuzz_authority(&workspace_root) + .expect("raw seed inventory and structural fuzz authority"); + } + + #[test] + fn fuzz_call_chain_rejects_structural_drift() { + validate_fuzz_common_source(COMMON_SOURCE).expect("governed common fuzz harness"); + validate_fuzz_target_source( + "fuzz/fuzz_targets/publication_jpeg.rs", + JPEG_TARGET_SOURCE, + "image/jpeg", + "jpg", + ) + .expect("governed JPEG fuzz target"); + + let target_mutations = [ + ( + "no-op target", + JPEG_TARGET_SOURCE + .replace(" common::exercise(data, \"image/jpeg\", \"jpg\");\n", ""), + ), + ( + "wrong MIME", + JPEG_TARGET_SOURCE.replacen("image/jpeg", "image/png", 1), + ), + ( + "wrong extension", + JPEG_TARGET_SOURCE.replacen("\"jpg\"", "\"png\"", 1), + ), + ( + "alternate harness", + JPEG_TARGET_SOURCE.replacen("common::exercise", "alternate::exercise", 1), + ), + ( + "bytes not passed", + JPEG_TARGET_SOURCE.replacen("common::exercise(data", "common::exercise(&[]", 1), + ), + ]; + for (label, source) in target_mutations { + validate_fuzz_target_source( + "fuzz/fuzz_targets/publication_jpeg.rs", + &source, + "image/jpeg", + "jpg", + ) + .expect_err(label); + } + + let omitted_public_call = COMMON_SOURCE.replacen( + "let _ = verify_publication_readiness(", + "let _ = omitted_public_operation(", + 1, + ); + validate_fuzz_common_source(&omitted_public_call) + .expect_err("omitted public operation must fail closed"); + + let mut bin_names = BTreeSet::new(); + insert_fuzz_bin_name(&mut bin_names, "publication_jpeg").expect("first target"); + insert_fuzz_bin_name(&mut bin_names, "publication_jpeg") + .expect_err("duplicate target must fail closed"); + } +} diff --git a/tools/xtask/src/contract/nip09_reconciliation.rs b/tools/xtask/src/contract/nip09_reconciliation.rs @@ -14778,9 +14778,18 @@ xtask = "run -q -p xtask --" )); } } - if workspace.contains_key("exclude") || workspace.contains_key("default-members") { + let excluded = match workspace.get("exclude") { + None => Vec::new(), + value => toml_string_array( + WORKSPACE_CARGO_MANIFEST_RELATIVE, + value, + "workspace.exclude", + )?, + }; + validate_unique_owned("workspace exclusions", &excluded)?; + if excluded != ["fuzz".to_owned()] || workspace.contains_key("default-members") { return Err(format!( - "{WORKSPACE_CARGO_MANIFEST_RELATIVE} must not exclude or narrow the default governed workspace member graph" + "{WORKSPACE_CARGO_MANIFEST_RELATIVE} must not exclude or narrow the default governed workspace member graph beyond the governed fuzz exclusion" )); } if workspace.get("resolver").and_then(toml::Value::as_str) != Some("2") @@ -21729,8 +21738,8 @@ async fn nip09_reconciliation_v1_result_vector() { ( "workspace exclusion", workspace_manifest.replacen( - "[workspace]\n", - "[workspace]\nexclude = [\"crates/event_store\"]\n", + "exclude = [\"fuzz\"]\n", + "exclude = [\"crates/event_store\"]\n", 1, ), "must not exclude or narrow", diff --git a/tools/xtask/src/contract/phase1_publication_media_readiness.rs b/tools/xtask/src/contract/phase1_publication_media_readiness.rs @@ -205,7 +205,7 @@ const ALLOWLIST_PREDECESSOR_ARTIFACTS: &[ImmutableArtifactSpec] = &[ "342d3d3d1100cb5d31aca94b0540b1e03761b034de23475bdb5142baabeed8fd", ), ]; -const BLOSSOM_PREDECESSOR_ARTIFACTS: &[ImmutableArtifactSpec] = &[ +pub(super) const BLOSSOM_PREDECESSOR_ARTIFACTS: &[ImmutableArtifactSpec] = &[ ImmutableArtifactSpec::new( "crates/blossom/contracts/publication_readiness_v1.manifest.json", 13_038, @@ -286,7 +286,7 @@ const BLOSSOM_PREDECESSOR_SOURCE_SUPERSESSIONS: &[&str] = &[ ]; #[derive(Clone, Copy)] -struct ImmutableArtifactSpec { +pub(super) struct ImmutableArtifactSpec { relative: &'static str, byte_length: usize, sha256: &'static str, @@ -304,7 +304,7 @@ impl ImmutableArtifactSpec { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] -struct FileDescriptor { +pub(super) struct FileDescriptor { path: String, byte_length: u64, sha256: String, @@ -313,7 +313,7 @@ struct FileDescriptor { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] -struct PredecessorDescriptor { +pub(super) struct PredecessorDescriptor { contract_id: String, immutable_artifacts: Vec<FileDescriptor>, source_supersessions: Vec<String>, @@ -321,10 +321,10 @@ struct PredecessorDescriptor { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] -struct ProtocolSourcePin { - id: String, - repository: String, - revision: String, +pub(super) struct ProtocolSourcePin { + pub(super) id: String, + pub(super) repository: String, + pub(super) revision: String, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -509,7 +509,10 @@ fn validate_predecessors(workspace_root: &Path) -> Result<(), String> { ) } -fn validate_source_supersessions(workspace_root: &Path, paths: &[&str]) -> Result<(), String> { +pub(super) fn validate_source_supersessions( + workspace_root: &Path, + paths: &[&str], +) -> Result<(), String> { if paths.windows(2).any(|pair| pair[0] >= pair[1]) { return Err("predecessor source supersessions must be sorted and unique".to_owned()); } @@ -523,7 +526,7 @@ fn validate_source_supersessions(workspace_root: &Path, paths: &[&str]) -> Resul Ok(()) } -fn validate_immutable_predecessor( +pub(super) fn validate_immutable_predecessor( workspace_root: &Path, label: &str, artifacts: &[ImmutableArtifactSpec], @@ -716,7 +719,7 @@ fn describe_manifest( }) } -fn predecessor_descriptor( +pub(super) fn predecessor_descriptor( contract_id: &str, artifacts: &[ImmutableArtifactSpec], source_supersessions: &[&str], @@ -1462,14 +1465,17 @@ fn owned(values: &[&str]) -> Vec<String> { values.iter().map(|value| (*value).to_owned()).collect() } -fn descriptor_for_file(workspace_root: &Path, path: &str) -> Result<FileDescriptor, String> { +pub(super) fn descriptor_for_file( + workspace_root: &Path, + path: &str, +) -> Result<FileDescriptor, String> { Ok(descriptor_for_bytes( path, &read_regular_file(workspace_root, path)?, )) } -fn descriptor_for_bytes(path: &str, bytes: &[u8]) -> FileDescriptor { +pub(super) fn descriptor_for_bytes(path: &str, bytes: &[u8]) -> FileDescriptor { FileDescriptor { path: path.to_owned(), byte_length: bytes.len() as u64,