From 3f86e4477be697e9849a2fd8259beff6bd5656a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Fri, 17 Apr 2026 05:22:23 -0400 Subject: [PATCH] Add fuzz targets for package and project coverage Add cargo-fuzz coverage for package semantic deserialization and Miden project manifests, loading, and assembly. The package semantic target starts from binary Package deserialization, then exercises debug-section decoding, runtime dependency checks, embedded kernel package decoding, and program/kernel conversion paths. The TOML target exercises MidenProject::parse and direct AST TOML parsing. The loader target writes fuzz input into temporary project trees and exercises Package::load, Project::load, and load_project_reference. The assembly target builds tiny temporary projects and exercises ProjectAssembler for library, executable, kernel, and kernel-linked executable targets. Add CI matrix entries and seed corpora for the new targets. --- .github/workflows/fuzz.yml | 41 ++- CHANGELOG.md | 1 + Makefile | 4 + crates/mast-package/src/package/seed_gen.rs | 14 +- miden-core-fuzz/Cargo.toml | 44 +++ .../corpus/project_assemble/debug_profile | 2 + .../corpus/project_assemble/empty_snippet | 1 + .../project_assemble/release_trim_paths | 2 + .../corpus/project_load/inherited_workspace | 10 + .../corpus/project_load/kernel_package | 11 + .../corpus/project_load/package_with_bins | 11 + .../corpus/project_load/package_with_deps | 12 + .../corpus/project_load/package_with_lib | 8 + .../corpus/project_load/package_with_metadata | 14 + .../corpus/project_load/package_with_profile | 14 + .../corpus/project_load/simple_package | 3 + .../corpus/project_load/workspace_manifest | 10 + .../project_toml_parse/inherited_workspace | 10 + .../corpus/project_toml_parse/kernel_package | 11 + .../project_toml_parse/package_with_bins | 11 + .../project_toml_parse/package_with_deps | 12 + .../project_toml_parse/package_with_lib | 8 + .../project_toml_parse/package_with_metadata | 14 + .../project_toml_parse/package_with_profile | 14 + .../corpus/project_toml_parse/simple_package | 3 + .../project_toml_parse/workspace_manifest | 10 + .../package_semantic_deserialize.rs | 54 +++ .../fuzz_targets/project_assemble.rs | 317 ++++++++++++++++++ miden-core-fuzz/fuzz_targets/project_load.rs | 221 ++++++++++++ .../fuzz_targets/project_toml_parse.rs | 41 +++ 30 files changed, 923 insertions(+), 5 deletions(-) create mode 100644 miden-core-fuzz/corpus/project_assemble/debug_profile create mode 100644 miden-core-fuzz/corpus/project_assemble/empty_snippet create mode 100644 miden-core-fuzz/corpus/project_assemble/release_trim_paths create mode 100644 miden-core-fuzz/corpus/project_load/inherited_workspace create mode 100644 miden-core-fuzz/corpus/project_load/kernel_package create mode 100644 miden-core-fuzz/corpus/project_load/package_with_bins create mode 100644 miden-core-fuzz/corpus/project_load/package_with_deps create mode 100644 miden-core-fuzz/corpus/project_load/package_with_lib create mode 100644 miden-core-fuzz/corpus/project_load/package_with_metadata create mode 100644 miden-core-fuzz/corpus/project_load/package_with_profile create mode 100644 miden-core-fuzz/corpus/project_load/simple_package create mode 100644 miden-core-fuzz/corpus/project_load/workspace_manifest create mode 100644 miden-core-fuzz/corpus/project_toml_parse/inherited_workspace create mode 100644 miden-core-fuzz/corpus/project_toml_parse/kernel_package create mode 100644 miden-core-fuzz/corpus/project_toml_parse/package_with_bins create mode 100644 miden-core-fuzz/corpus/project_toml_parse/package_with_deps create mode 100644 miden-core-fuzz/corpus/project_toml_parse/package_with_lib create mode 100644 miden-core-fuzz/corpus/project_toml_parse/package_with_metadata create mode 100644 miden-core-fuzz/corpus/project_toml_parse/package_with_profile create mode 100644 miden-core-fuzz/corpus/project_toml_parse/simple_package create mode 100644 miden-core-fuzz/corpus/project_toml_parse/workspace_manifest create mode 100644 miden-core-fuzz/fuzz_targets/package_semantic_deserialize.rs create mode 100644 miden-core-fuzz/fuzz_targets/project_assemble.rs create mode 100644 miden-core-fuzz/fuzz_targets/project_load.rs create mode 100644 miden-core-fuzz/fuzz_targets/project_toml_parse.rs diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 6d31b905d4..c562a03722 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -39,6 +39,10 @@ jobs: - precompile_request_deserialize - library_deserialize - package_deserialize + - package_semantic_deserialize + - project_toml_parse + - project_load + - project_assemble timeout-minutes: 15 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 @@ -51,9 +55,42 @@ jobs: - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Install cargo-fuzz run: cargo install cargo-fuzz --locked + - name: Generate fuzz seed corpus + env: + TARGET: ${{ matrix.target }} + GENERATED_CORPUS_TARGETS: >- + mast_forest_deserialize program_deserialize kernel_deserialize + stack_io_deserialize advice_inputs_deserialize operation_deserialize + execution_proof_deserialize precompile_request_deserialize + library_deserialize package_deserialize package_semantic_deserialize + run: | + if [[ " $GENERATED_CORPUS_TARGETS " == *" $TARGET "* ]]; then + make fuzz-seeds + else + echo "No generated seed corpus for $TARGET" + fi - name: Run fuzz target (smoke test) + env: + TARGET: ${{ matrix.target }} run: | + FUZZ_TARGET="$(cargo +nightly fuzz build --help | awk ' + $0 ~ /^ --target / { in_target = 1; next } + in_target && /\[default:/ { + sub(/^.*\[default: /, "") + sub(/\].*$/, "") + print + exit + } + ')" + test -n "$FUZZ_TARGET" # Build the fuzz target first - cargo +nightly fuzz build --fuzz-dir miden-core-fuzz ${{ matrix.target }} + cargo +nightly fuzz build --target "$FUZZ_TARGET" --fuzz-dir miden-core-fuzz "$TARGET" # Run directly to avoid cargo-fuzz wrapper SIGPIPE issue - miden-core-fuzz/target/x86_64-unknown-linux-gnu/release/${{ matrix.target }} -max_total_time=60 -runs=10000 + FUZZ_BIN="miden-core-fuzz/target/$FUZZ_TARGET/release/$TARGET" + test -x "$FUZZ_BIN" + CORPUS_DIR="miden-core-fuzz/corpus/$TARGET" + if [ -d "$CORPUS_DIR" ]; then + "$FUZZ_BIN" "$CORPUS_DIR" -max_total_time=60 -runs=10000 + else + "$FUZZ_BIN" -max_total_time=60 -runs=10000 + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 826433283c..ee30b9a8c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - Refactor trace generation to row-major format ([#2937](https://github.com/0xMiden/miden-vm/pull/2937)). - Documented non-overlap requirement for `memcopy_words`, `memcopy_elements`, and AEAD encrypt/decrypt procedures ([#2941](https://github.com/0xMiden/miden-vm/pull/2941)). - Added chainable `Test` builders for common test setup in `miden-utils-testing` ([#2957](https://github.com/0xMiden/miden-vm/pull/2957)). +- Added fuzz coverage for package semantic deserialization and project parsing, loading, and assembly ([#3015](https://github.com/0xMiden/miden-vm/pull/3015)). - Speed-up AUX range check trace generation by changing divisors to a flat Vec layout ([#2966](https://github.com/0xMiden/miden-vm/pull/2966)). - Removed AIR constraint tagging instrumentation, applied a uniform constraint description style across components, and optimized constraint evaluation ([#2856](https://github.com/0xMiden/miden-vm/pull/2856)). diff --git a/Makefile b/Makefile index 9832fae841..2f0b2d510a 100644 --- a/Makefile +++ b/Makefile @@ -297,7 +297,11 @@ fuzz-all: fuzz-seeds ## Run all fuzz targets (in sequence) -@cargo +nightly fuzz run library_deserialize --release --fuzz-dir miden-core-fuzz -- -max_total_time=300 -@cargo +nightly fuzz run library_serde_deserialize --release --fuzz-dir miden-core-fuzz -- -max_total_time=300 -@cargo +nightly fuzz run package_deserialize --release --fuzz-dir miden-core-fuzz -- -max_total_time=300 + -@cargo +nightly fuzz run package_semantic_deserialize --release --fuzz-dir miden-core-fuzz -- -max_total_time=300 -@cargo +nightly fuzz run package_serde_deserialize --release --fuzz-dir miden-core-fuzz -- -max_total_time=300 + -@cargo +nightly fuzz run project_toml_parse --release --fuzz-dir miden-core-fuzz -- -max_total_time=300 + -@cargo +nightly fuzz run project_load --release --fuzz-dir miden-core-fuzz -- -max_total_time=300 + -@cargo +nightly fuzz run project_assemble --release --fuzz-dir miden-core-fuzz -- -max_total_time=300 .PHONY: fuzz-list fuzz-list: ## List available fuzz targets diff --git a/crates/mast-package/src/package/seed_gen.rs b/crates/mast-package/src/package/seed_gen.rs index 379878c5bd..fbf7b8fd90 100644 --- a/crates/mast-package/src/package/seed_gen.rs +++ b/crates/mast-package/src/package/seed_gen.rs @@ -48,7 +48,7 @@ fn build_library(signature: Option) -> Arc { Arc::new(Library::new(Arc::new(forest), exports).expect("failed to build library")) } -fn build_package(library: Arc, signature: FunctionType) -> Package { +fn build_package(library: Arc, signature: Option) -> Package { let path = absolute_path("test::proc"); let node_id = library.get_export_node_id(path.as_ref()); let digest = library.mast_forest()[node_id].digest(); @@ -56,7 +56,7 @@ fn build_package(library: Arc, signature: FunctionType) -> Package { let export = PackageExport::Procedure(PackageProcedureExport { path: Arc::clone(&path), digest, - signature: Some(signature), + signature, attributes: AttributeSet::default(), }); @@ -96,8 +96,16 @@ fn generate_fuzz_seeds() { &library_with_signature.to_bytes(), ); - let package = build_package(library_with_signature, signature); + let package = build_package(Arc::clone(&library), None); write_seed("package_deserialize", "minimal_package.bin", &package.to_bytes()); + write_seed("package_semantic_deserialize", "minimal_package.bin", &package.to_bytes()); + + let package_with_signature = build_package(library_with_signature, Some(signature)); + write_seed( + "package_deserialize", + "package_with_signature.bin", + &package_with_signature.to_bytes(), + ); println!("\nSeed corpus generated in ../../miden-core-fuzz/corpus"); } diff --git a/miden-core-fuzz/Cargo.toml b/miden-core-fuzz/Cargo.toml index 9d05e66626..c3b2756c11 100644 --- a/miden-core-fuzz/Cargo.toml +++ b/miden-core-fuzz/Cargo.toml @@ -23,10 +23,26 @@ features = ["std", "serde"] path = "../crates/assembly-syntax" features = ["std", "serde"] +[dependencies.miden-assembly] +path = "../crates/assembly" +features = ["std"] + [dependencies.miden-mast-package] path = "../crates/mast-package" features = ["std"] +[dependencies.miden-package-registry] +path = "../crates/package-registry" +features = ["std", "serde", "resolver"] + +[dependencies.miden-project] +path = "../crates/project" +features = ["std", "serde"] + +[dependencies.toml] +version = "1.0" +features = ["parse", "display", "serde"] + # Fuzz targets - each is a separate binary [[bin]] name = "mast_forest_deserialize" @@ -175,6 +191,13 @@ test = false doc = false bench = false +[[bin]] +name = "package_semantic_deserialize" +path = "fuzz_targets/package_semantic_deserialize.rs" +test = false +doc = false +bench = false + [[bin]] name = "package_serde_deserialize" path = "fuzz_targets/package_serde_deserialize.rs" @@ -188,3 +211,24 @@ path = "fuzz_targets/mast_forest_serde_deserialize.rs" test = false doc = false bench = false + +[[bin]] +name = "project_toml_parse" +path = "fuzz_targets/project_toml_parse.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "project_load" +path = "fuzz_targets/project_load.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "project_assemble" +path = "fuzz_targets/project_assemble.rs" +test = false +doc = false +bench = false diff --git a/miden-core-fuzz/corpus/project_assemble/debug_profile b/miden-core-fuzz/corpus/project_assemble/debug_profile new file mode 100644 index 0000000000..1d566547a2 --- /dev/null +++ b/miden-core-fuzz/corpus/project_assemble/debug_profile @@ -0,0 +1,2 @@ +[metadata] +purpose = "exercise debug-enabled project assembly" diff --git a/miden-core-fuzz/corpus/project_assemble/empty_snippet b/miden-core-fuzz/corpus/project_assemble/empty_snippet new file mode 100644 index 0000000000..47e8704ac8 --- /dev/null +++ b/miden-core-fuzz/corpus/project_assemble/empty_snippet @@ -0,0 +1 @@ +# empty snippet diff --git a/miden-core-fuzz/corpus/project_assemble/release_trim_paths b/miden-core-fuzz/corpus/project_assemble/release_trim_paths new file mode 100644 index 0000000000..e04f0f506d --- /dev/null +++ b/miden-core-fuzz/corpus/project_assemble/release_trim_paths @@ -0,0 +1,2 @@ +[lints] +unused-imports = "allow" diff --git a/miden-core-fuzz/corpus/project_load/inherited_workspace b/miden-core-fuzz/corpus/project_load/inherited_workspace new file mode 100644 index 0000000000..5fd5d0d468 --- /dev/null +++ b/miden-core-fuzz/corpus/project_load/inherited_workspace @@ -0,0 +1,10 @@ +[package] +name = "workspace-member" +version.workspace = true +description.workspace = true + +[lib] +path = "lib.masm" + +[dependencies] +shared-dep.workspace = true diff --git a/miden-core-fuzz/corpus/project_load/kernel_package b/miden-core-fuzz/corpus/project_load/kernel_package new file mode 100644 index 0000000000..4f36c6b4e4 --- /dev/null +++ b/miden-core-fuzz/corpus/project_load/kernel_package @@ -0,0 +1,11 @@ +[package] +name = "mykernel" +version = "2.0.0" + +[lib] +kind = "kernel" +path = "kernel/mod.masm" + +[[bin]] +name = "entry" +path = "bin/entry.masm" diff --git a/miden-core-fuzz/corpus/project_load/package_with_bins b/miden-core-fuzz/corpus/project_load/package_with_bins new file mode 100644 index 0000000000..52fe67b1da --- /dev/null +++ b/miden-core-fuzz/corpus/project_load/package_with_bins @@ -0,0 +1,11 @@ +[package] +name = "myapp" +version = "0.1.0" + +[[bin]] +name = "main" +path = "bin/main.masm" + +[[bin]] +name = "helper" +path = "bin/helper.masm" diff --git a/miden-core-fuzz/corpus/project_load/package_with_deps b/miden-core-fuzz/corpus/project_load/package_with_deps new file mode 100644 index 0000000000..9ef860bd50 --- /dev/null +++ b/miden-core-fuzz/corpus/project_load/package_with_deps @@ -0,0 +1,12 @@ +[package] +name = "myproject" +version = "0.5.0" + +[lib] +path = "lib.masm" + +[dependencies] +stdlib = { version = "1.0.0" } +local-dep = { path = "../local-dep", linkage = "static" } +remote-dep = { git = "https://github.com/example/repo", branch = "main" } +workspace-dep.workspace = true diff --git a/miden-core-fuzz/corpus/project_load/package_with_lib b/miden-core-fuzz/corpus/project_load/package_with_lib new file mode 100644 index 0000000000..39254d2bec --- /dev/null +++ b/miden-core-fuzz/corpus/project_load/package_with_lib @@ -0,0 +1,8 @@ +[package] +name = "mylib" +version = "1.0.0" +description = "A simple library" + +[lib] +namespace = "mylib" +path = "lib.masm" diff --git a/miden-core-fuzz/corpus/project_load/package_with_metadata b/miden-core-fuzz/corpus/project_load/package_with_metadata new file mode 100644 index 0000000000..75dc0c2566 --- /dev/null +++ b/miden-core-fuzz/corpus/project_load/package_with_metadata @@ -0,0 +1,14 @@ +[package] +name = "metadata-example" +version = "0.1.0" +description = "Example with metadata" + +[lib] +path = "lib.masm" + +[package.metadata.custom] +key = "value" +number = 42 + +[lints.miden] +unused = "warn" diff --git a/miden-core-fuzz/corpus/project_load/package_with_profile b/miden-core-fuzz/corpus/project_load/package_with_profile new file mode 100644 index 0000000000..984ef24e3b --- /dev/null +++ b/miden-core-fuzz/corpus/project_load/package_with_profile @@ -0,0 +1,14 @@ +[package] +name = "profiled" +version = "0.1.0" + +[lib] +path = "lib.masm" + +[profile.release] +debug = false +trim-paths = true + +[profile.custom] +inherits = "dev" +debug = true diff --git a/miden-core-fuzz/corpus/project_load/simple_package b/miden-core-fuzz/corpus/project_load/simple_package new file mode 100644 index 0000000000..517a262fe8 --- /dev/null +++ b/miden-core-fuzz/corpus/project_load/simple_package @@ -0,0 +1,3 @@ +[package] +name = "simple" +version = "0.1.0" diff --git a/miden-core-fuzz/corpus/project_load/workspace_manifest b/miden-core-fuzz/corpus/project_load/workspace_manifest new file mode 100644 index 0000000000..67a73e0486 --- /dev/null +++ b/miden-core-fuzz/corpus/project_load/workspace_manifest @@ -0,0 +1,10 @@ +[workspace] +members = ["crate-a", "crate-b", "crate-c"] + +[workspace.package] +version = "0.1.0" +description = "A workspace example" + +[workspace.dependencies] +foo = { path = "crate-a" } +bar = { version = "1.0.0", linkage = "static" } diff --git a/miden-core-fuzz/corpus/project_toml_parse/inherited_workspace b/miden-core-fuzz/corpus/project_toml_parse/inherited_workspace new file mode 100644 index 0000000000..5fd5d0d468 --- /dev/null +++ b/miden-core-fuzz/corpus/project_toml_parse/inherited_workspace @@ -0,0 +1,10 @@ +[package] +name = "workspace-member" +version.workspace = true +description.workspace = true + +[lib] +path = "lib.masm" + +[dependencies] +shared-dep.workspace = true diff --git a/miden-core-fuzz/corpus/project_toml_parse/kernel_package b/miden-core-fuzz/corpus/project_toml_parse/kernel_package new file mode 100644 index 0000000000..4f36c6b4e4 --- /dev/null +++ b/miden-core-fuzz/corpus/project_toml_parse/kernel_package @@ -0,0 +1,11 @@ +[package] +name = "mykernel" +version = "2.0.0" + +[lib] +kind = "kernel" +path = "kernel/mod.masm" + +[[bin]] +name = "entry" +path = "bin/entry.masm" diff --git a/miden-core-fuzz/corpus/project_toml_parse/package_with_bins b/miden-core-fuzz/corpus/project_toml_parse/package_with_bins new file mode 100644 index 0000000000..52fe67b1da --- /dev/null +++ b/miden-core-fuzz/corpus/project_toml_parse/package_with_bins @@ -0,0 +1,11 @@ +[package] +name = "myapp" +version = "0.1.0" + +[[bin]] +name = "main" +path = "bin/main.masm" + +[[bin]] +name = "helper" +path = "bin/helper.masm" diff --git a/miden-core-fuzz/corpus/project_toml_parse/package_with_deps b/miden-core-fuzz/corpus/project_toml_parse/package_with_deps new file mode 100644 index 0000000000..9ef860bd50 --- /dev/null +++ b/miden-core-fuzz/corpus/project_toml_parse/package_with_deps @@ -0,0 +1,12 @@ +[package] +name = "myproject" +version = "0.5.0" + +[lib] +path = "lib.masm" + +[dependencies] +stdlib = { version = "1.0.0" } +local-dep = { path = "../local-dep", linkage = "static" } +remote-dep = { git = "https://github.com/example/repo", branch = "main" } +workspace-dep.workspace = true diff --git a/miden-core-fuzz/corpus/project_toml_parse/package_with_lib b/miden-core-fuzz/corpus/project_toml_parse/package_with_lib new file mode 100644 index 0000000000..39254d2bec --- /dev/null +++ b/miden-core-fuzz/corpus/project_toml_parse/package_with_lib @@ -0,0 +1,8 @@ +[package] +name = "mylib" +version = "1.0.0" +description = "A simple library" + +[lib] +namespace = "mylib" +path = "lib.masm" diff --git a/miden-core-fuzz/corpus/project_toml_parse/package_with_metadata b/miden-core-fuzz/corpus/project_toml_parse/package_with_metadata new file mode 100644 index 0000000000..75dc0c2566 --- /dev/null +++ b/miden-core-fuzz/corpus/project_toml_parse/package_with_metadata @@ -0,0 +1,14 @@ +[package] +name = "metadata-example" +version = "0.1.0" +description = "Example with metadata" + +[lib] +path = "lib.masm" + +[package.metadata.custom] +key = "value" +number = 42 + +[lints.miden] +unused = "warn" diff --git a/miden-core-fuzz/corpus/project_toml_parse/package_with_profile b/miden-core-fuzz/corpus/project_toml_parse/package_with_profile new file mode 100644 index 0000000000..984ef24e3b --- /dev/null +++ b/miden-core-fuzz/corpus/project_toml_parse/package_with_profile @@ -0,0 +1,14 @@ +[package] +name = "profiled" +version = "0.1.0" + +[lib] +path = "lib.masm" + +[profile.release] +debug = false +trim-paths = true + +[profile.custom] +inherits = "dev" +debug = true diff --git a/miden-core-fuzz/corpus/project_toml_parse/simple_package b/miden-core-fuzz/corpus/project_toml_parse/simple_package new file mode 100644 index 0000000000..517a262fe8 --- /dev/null +++ b/miden-core-fuzz/corpus/project_toml_parse/simple_package @@ -0,0 +1,3 @@ +[package] +name = "simple" +version = "0.1.0" diff --git a/miden-core-fuzz/corpus/project_toml_parse/workspace_manifest b/miden-core-fuzz/corpus/project_toml_parse/workspace_manifest new file mode 100644 index 0000000000..67a73e0486 --- /dev/null +++ b/miden-core-fuzz/corpus/project_toml_parse/workspace_manifest @@ -0,0 +1,10 @@ +[workspace] +members = ["crate-a", "crate-b", "crate-c"] + +[workspace.package] +version = "0.1.0" +description = "A workspace example" + +[workspace.dependencies] +foo = { path = "crate-a" } +bar = { version = "1.0.0", linkage = "static" } diff --git a/miden-core-fuzz/fuzz_targets/package_semantic_deserialize.rs b/miden-core-fuzz/fuzz_targets/package_semantic_deserialize.rs new file mode 100644 index 0000000000..4c830d79f2 --- /dev/null +++ b/miden-core-fuzz/fuzz_targets/package_semantic_deserialize.rs @@ -0,0 +1,54 @@ +//! Fuzz target for semantic Package deserialization checks. +//! +//! This target starts from binary `Package` deserialization, then exercises package APIs that +//! interpret decoded sections, runtime dependencies, and package kind. +//! +//! Run with: cargo +nightly fuzz run package_semantic_deserialize --fuzz-dir miden-core-fuzz + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use miden_core::serde::{Deserializable, SliceReader}; +use miden_mast_package::{ + Package, SectionId, TargetType, + debug_info::{DebugFunctionsSection, DebugSourcesSection, DebugTypesSection}, +}; + +fuzz_target!(|data: &[u8]| { + let Ok(package) = Package::read_from_bytes(data) else { + return; + }; + + validate_debug_sections(&package); + + let _ = package.kernel_runtime_dependency(); + let _ = package.try_embedded_kernel_package(); + + // These conversion helpers borrow the package, despite the `try_into_*` names. + match package.kind { + TargetType::Executable => { + let _ = package.try_into_program(); + }, + TargetType::Kernel => { + let _ = package.kernel_module_info(); + let _ = package.to_kernel(); + let _ = package.try_into_kernel_library(); + }, + _ => (), + } +}); + +fn validate_debug_sections(package: &Package) { + for section in &package.sections { + if section.id == SectionId::DEBUG_SOURCES { + let mut reader = SliceReader::new(section.data.as_ref()); + let _ = DebugSourcesSection::read_from(&mut reader); + } else if section.id == SectionId::DEBUG_FUNCTIONS { + let mut reader = SliceReader::new(section.data.as_ref()); + let _ = DebugFunctionsSection::read_from(&mut reader); + } else if section.id == SectionId::DEBUG_TYPES { + let mut reader = SliceReader::new(section.data.as_ref()); + let _ = DebugTypesSection::read_from(&mut reader); + } + } +} diff --git a/miden-core-fuzz/fuzz_targets/project_assemble.rs b/miden-core-fuzz/fuzz_targets/project_assemble.rs new file mode 100644 index 0000000000..907c27de2b --- /dev/null +++ b/miden-core-fuzz/fuzz_targets/project_assemble.rs @@ -0,0 +1,317 @@ +//! Fuzz target for assembling Miden projects from a filesystem tree. +//! +//! This target exercises `ProjectAssembler::assemble` on controlled library, executable, and +//! kernel project layouts. It also reads emitted debug sections and converts assembled executable +//! and kernel packages through the public package APIs. +//! +//! Run with: cargo +nightly fuzz run project_assemble --fuzz-dir miden-core-fuzz + +#![no_main] + +use std::{ + fs, + path::{Path, PathBuf}, + sync::atomic::{AtomicU64, Ordering}, +}; + +use libfuzzer_sys::fuzz_target; +use miden_assembly::{Assembler, ProjectTargetSelector}; +use miden_core::serde::{Deserializable, SliceReader}; +use miden_mast_package::{ + Package as MastPackage, SectionId, TargetType, + debug_info::{DebugFunctionsSection, DebugSourcesSection, DebugTypesSection}, +}; +use miden_package_registry::InMemoryPackageRegistry; + +const MAX_MANIFEST_SNIPPET_LEN: usize = 40 * 1024; +const EXECUTABLE_TARGET: &str = "main"; + +static NEXT_DIR_ID: AtomicU64 = AtomicU64::new(0); + +struct TempProjectTree { + root: PathBuf, +} + +impl TempProjectTree { + fn new() -> std::io::Result { + let id = NEXT_DIR_ID.fetch_add(1, Ordering::Relaxed); + let root = std::env::temp_dir() + .join(format!("miden-project-assemble-fuzz-{}-{id}", std::process::id())); + + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(&root)?; + + Ok(Self { root }) + } + + fn path(&self, path: impl AsRef) -> PathBuf { + self.root.join(path) + } +} + +impl Drop for TempProjectTree { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } +} + +fuzz_target!(|data: &[u8]| { + if data.len() > MAX_MANIFEST_SNIPPET_LEN { + return; + } + let Ok(snippet) = core::str::from_utf8(data) else { + return; + }; + let Ok(tree) = TempProjectTree::new() else { + return; + }; + let metadata_value = toml::Value::String(snippet.to_owned()).to_string(); + + match scenario_index(data, 8) { + 0 => assemble_library_project(&tree, &metadata_value, "dev"), + 1 => assemble_library_project(&tree, &metadata_value, "release"), + 2 => assemble_executable_project(&tree, &metadata_value, "dev"), + 3 => assemble_executable_project(&tree, &metadata_value, "release"), + 4 => assemble_kernel_project(&tree, &metadata_value, ProjectTargetSelector::Library, "dev"), + 5 => assemble_kernel_project( + &tree, + &metadata_value, + ProjectTargetSelector::Library, + "release", + ), + 6 => assemble_kernel_project( + &tree, + &metadata_value, + ProjectTargetSelector::Executable(EXECUTABLE_TARGET), + "dev", + ), + _ => assemble_kernel_project( + &tree, + &metadata_value, + ProjectTargetSelector::Executable(EXECUTABLE_TARGET), + "release", + ), + } +}); + +fn assemble_library_project(tree: &TempProjectTree, metadata_value: &str, profile: &str) { + let manifest_path = tree.path("library/miden-project.toml"); + if write_file( + &manifest_path, + &format!( + r#"[package] +name = "libpkg" +version = "1.0.0" +description = "library fuzz package" + +[package.metadata.fuzz] +input = {metadata_value} + +[lib] +path = "lib.masm" + +[profile.dev] +debug = true + +[profile.release] +debug = true +trim-paths = true +"#, + ), + ) + .is_err() + || write_file( + &tree.path("library/lib.masm"), + r#"pub proc helper + push.1 + push.2 + add +end +"#, + ) + .is_err() + { + return; + } + + assemble_project(&manifest_path, ProjectTargetSelector::Library, profile); +} + +fn assemble_executable_project(tree: &TempProjectTree, metadata_value: &str, profile: &str) { + let manifest_path = tree.path("executable/miden-project.toml"); + if write_file( + &manifest_path, + &format!( + r#"[package] +name = "apppkg" +version = "1.0.0" +description = "executable fuzz package" + +[package.metadata.fuzz] +input = {metadata_value} + +[lib] +path = "lib.masm" + +[[bin]] +name = "main" +path = "main.masm" + +[profile.dev] +debug = true + +[profile.release] +debug = true +trim-paths = true +"#, + ), + ) + .is_err() + || write_file( + &tree.path("executable/lib.masm"), + r#"pub proc helper + push.3 +end +"#, + ) + .is_err() + || write_file( + &tree.path("executable/main.masm"), + r#"use $exec::lib + +begin + exec.lib::helper + drop +end +"#, + ) + .is_err() + { + return; + } + + assemble_project(&manifest_path, ProjectTargetSelector::Executable(EXECUTABLE_TARGET), profile); +} + +fn assemble_kernel_project( + tree: &TempProjectTree, + metadata_value: &str, + target: ProjectTargetSelector<'_>, + profile: &str, +) { + let manifest_path = tree.path("kernel/miden-project.toml"); + if write_file( + &manifest_path, + &format!( + r#"[package] +name = "kernelpkg" +version = "1.0.0" +description = "kernel fuzz package" + +[package.metadata.fuzz] +input = {metadata_value} + +[lib] +kind = "kernel" +path = "kernel.masm" + +[[bin]] +name = "main" +path = "main.masm" + +[profile.dev] +debug = true + +[profile.release] +debug = true +trim-paths = true +"#, + ), + ) + .is_err() + || write_file( + &tree.path("kernel/kernel.masm"), + r#"pub proc foo + caller +end +"#, + ) + .is_err() + || write_file( + &tree.path("kernel/main.masm"), + r#"begin + syscall.foo +end +"#, + ) + .is_err() + { + return; + } + + assemble_project(&manifest_path, target, profile); +} + +fn assemble_project(manifest_path: &Path, target: ProjectTargetSelector<'_>, profile: &str) { + let assembler = Assembler::default(); + let mut registry = InMemoryPackageRegistry::default(); + + let Ok(mut project_assembler) = assembler.for_project_at_path(manifest_path, &mut registry) + else { + return; + }; + let Ok(package) = project_assembler.assemble(target, profile) else { + return; + }; + + validate_package(&package); +} + +fn validate_package(package: &MastPackage) { + validate_debug_sections(package); + + // These conversion helpers borrow the package, despite the `try_into_*` names. + match package.kind { + TargetType::Executable => { + let _ = package.try_into_program(); + let _ = package.try_embedded_kernel_package(); + }, + TargetType::Kernel => { + let _ = package.try_into_kernel_library(); + let _ = package.to_kernel(); + let _ = package.kernel_module_info(); + }, + _ if package.is_library() => { + let _ = package.kernel_runtime_dependency(); + }, + _ => (), + } +} + +fn validate_debug_sections(package: &MastPackage) { + for section in &package.sections { + if section.id == SectionId::DEBUG_SOURCES { + let mut reader = SliceReader::new(section.data.as_ref()); + let _ = DebugSourcesSection::read_from(&mut reader); + } else if section.id == SectionId::DEBUG_FUNCTIONS { + let mut reader = SliceReader::new(section.data.as_ref()); + let _ = DebugFunctionsSection::read_from(&mut reader); + } else if section.id == SectionId::DEBUG_TYPES { + let mut reader = SliceReader::new(section.data.as_ref()); + let _ = DebugTypesSection::read_from(&mut reader); + } + } +} + +fn write_file(path: &Path, contents: &str) -> std::io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, contents) +} + +fn scenario_index(data: &[u8], count: usize) -> usize { + data.iter() + .fold(0usize, |acc, byte| acc.wrapping_mul(31).wrapping_add(*byte as usize)) + % count +} diff --git a/miden-core-fuzz/fuzz_targets/project_load.rs b/miden-core-fuzz/fuzz_targets/project_load.rs new file mode 100644 index 0000000000..0d1ccbfa97 --- /dev/null +++ b/miden-core-fuzz/fuzz_targets/project_load.rs @@ -0,0 +1,221 @@ +//! Fuzz target for loading Miden project manifests from a filesystem tree. +//! +//! This target exercises `Package::load` and `Project::load`, including workspace-member loading +//! and workspace inheritance. It does not assemble MASM sources. +//! +//! Run with: cargo +nightly fuzz run project_load --fuzz-dir miden-core-fuzz + +#![no_main] + +use std::{ + fs, + path::{Path, PathBuf}, + sync::atomic::{AtomicU64, Ordering}, +}; + +use libfuzzer_sys::fuzz_target; +use miden_assembly_syntax::debuginfo::{DefaultSourceManager, SourceManagerExt}; +use miden_project::{Package, Project}; + +const MAX_MANIFEST_LEN: usize = 40 * 1024; + +static NEXT_DIR_ID: AtomicU64 = AtomicU64::new(0); + +struct TempProjectTree { + root: PathBuf, +} + +impl TempProjectTree { + fn new() -> std::io::Result { + let id = NEXT_DIR_ID.fetch_add(1, Ordering::Relaxed); + let root = std::env::temp_dir() + .join(format!("miden-project-load-fuzz-{}-{id}", std::process::id())); + + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(&root)?; + + Ok(Self { root }) + } + + fn path(&self, path: impl AsRef) -> PathBuf { + self.root.join(path) + } +} + +impl Drop for TempProjectTree { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } +} + +fuzz_target!(|data: &[u8]| { + if data.len() > MAX_MANIFEST_LEN { + return; + } + let Ok(manifest) = core::str::from_utf8(data) else { + return; + }; + let Ok(tree) = TempProjectTree::new() else { + return; + }; + + let source_manager = DefaultSourceManager::default(); + + match scenario_index(data, 5) { + 0 => load_standalone_package(&tree, &source_manager, manifest), + 1 => load_standalone_project(&tree, &source_manager, manifest), + 2 => load_fuzzed_workspace(&tree, &source_manager, manifest), + 3 => load_inherited_workspace_member(&tree, &source_manager, manifest), + _ => load_project_reference(&tree, &source_manager, manifest), + } +}); + +fn load_standalone_package( + tree: &TempProjectTree, + source_manager: &DefaultSourceManager, + manifest: &str, +) { + let manifest_path = tree.path("standalone/miden-project.toml"); + if write_file(&manifest_path, manifest).is_err() { + return; + } + + if let Ok(source) = source_manager.load_file(&manifest_path) { + let _ = Package::load(source); + } +} + +fn load_standalone_project( + tree: &TempProjectTree, + source_manager: &DefaultSourceManager, + manifest: &str, +) { + let manifest_path = tree.path("standalone/miden-project.toml"); + if write_file(&manifest_path, manifest).is_err() { + return; + } + + if scenario_index(manifest.as_bytes(), 2) == 0 { + let _ = Project::load(tree.path("standalone"), source_manager); + } else { + let _ = Project::load(&manifest_path, source_manager); + } +} + +fn load_fuzzed_workspace( + tree: &TempProjectTree, + source_manager: &DefaultSourceManager, + manifest: &str, +) { + let workspace_manifest = tree.path("fuzzed-workspace/miden-project.toml"); + let workspace_member = tree.path("fuzzed-workspace/app/miden-project.toml"); + + if write_file(&workspace_manifest, manifest).is_err() + || write_file( + &workspace_member, + r#"[package] +name = "app" +version = "0.1.0" + +[lib] +path = "lib.masm" +"#, + ) + .is_err() + || write_file( + &tree.path("fuzzed-workspace/app/lib.masm"), + r#"pub proc helper + push.1 +end +"#, + ) + .is_err() + { + return; + } + + if scenario_index(manifest.as_bytes(), 2) == 0 { + let _ = Project::load(tree.path("fuzzed-workspace"), source_manager); + } else { + let _ = Project::load(&workspace_manifest, source_manager); + } +} + +fn load_inherited_workspace_member( + tree: &TempProjectTree, + source_manager: &DefaultSourceManager, + manifest: &str, +) { + let member_manifest = write_inherited_workspace(tree, manifest); + let Ok(member_manifest) = member_manifest else { + return; + }; + + if scenario_index(manifest.as_bytes(), 2) == 0 { + let _ = Project::load(tree.path("inherited-workspace/app"), source_manager); + } else { + let _ = Project::load(&member_manifest, source_manager); + } +} + +fn load_project_reference( + tree: &TempProjectTree, + source_manager: &DefaultSourceManager, + manifest: &str, +) { + if write_inherited_workspace(tree, manifest).is_err() { + return; + } + let workspace_manifest = tree.path("inherited-workspace/miden-project.toml"); + + if scenario_index(manifest.as_bytes(), 2) == 0 { + let _ = Project::load_project_reference( + "app", + tree.path("inherited-workspace"), + source_manager, + ); + } else { + let _ = Project::load_project_reference("app", &workspace_manifest, source_manager); + } +} + +fn write_inherited_workspace(tree: &TempProjectTree, manifest: &str) -> std::io::Result { + let workspace_manifest = tree.path("inherited-workspace/miden-project.toml"); + let member_manifest = tree.path("inherited-workspace/app/miden-project.toml"); + + write_file( + &workspace_manifest, + r#"[workspace] +members = ["app"] + +[workspace.package] +version = "1.0.0" +description = "workspace defaults" + +[workspace.dependencies] +shared = { version = "1.0.0" } +"#, + )?; + write_file(&member_manifest, manifest)?; + write_file( + &tree.path("inherited-workspace/app/lib.masm"), + r#"pub proc helper + push.1 +end +"#, + )?; + Ok(member_manifest) +} + +fn scenario_index(data: &[u8], count: usize) -> usize { + data.iter() + .fold(0usize, |acc, byte| acc.wrapping_mul(31).wrapping_add(*byte as usize)) + % count +} + +fn write_file(path: &Path, contents: &str) -> std::io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, contents) +} diff --git a/miden-core-fuzz/fuzz_targets/project_toml_parse.rs b/miden-core-fuzz/fuzz_targets/project_toml_parse.rs new file mode 100644 index 0000000000..347b569833 --- /dev/null +++ b/miden-core-fuzz/fuzz_targets/project_toml_parse.rs @@ -0,0 +1,41 @@ +//! Fuzz target for Project TOML manifest parsing. +//! +//! This target fuzzes the `miden_project::ast::MidenProject` TOML parsing, +//! which is used to parse `miden-project.toml` manifest files. +//! +//! Run with: cargo +nightly fuzz run project_toml_parse --fuzz-dir miden-core-fuzz + +#![no_main] + +use std::sync::Arc; + +use libfuzzer_sys::fuzz_target; +use miden_assembly_syntax::debuginfo::{SourceFile, SourceId, SourceLanguage}; +use miden_project::{ + Uri, + ast::{MidenProject, PackageConfig, PackageTable, ProjectFile, WorkspaceFile}, +}; + +fuzz_target!(|data: &[u8]| { + // Try to parse the data as a TOML string + if let Ok(toml_str) = core::str::from_utf8(data) { + let source_file = Arc::new(SourceFile::new( + SourceId::default(), + SourceLanguage::Other("toml"), + Uri::new("fuzz://miden-project.toml"), + toml_str, + )); + + // Exercise the production manifest parser, including validation and source-span setup. + let _ = MidenProject::parse(source_file); + + // Attempt to parse as MidenProject AST (workspace or package manifest) + let _ = toml::from_str::(toml_str); + + // Also try parsing individual components + let _ = toml::from_str::(toml_str); + let _ = toml::from_str::(toml_str); + let _ = toml::from_str::(toml_str); + let _ = toml::from_str::(toml_str); + } +});