From 41b10c4fb4c064fca39a414ac14008945c196e46 Mon Sep 17 00:00:00 2001 From: Steven Malis Date: Tue, 25 Aug 2026 13:49:38 -0400 Subject: [PATCH 1/7] Add fuzzing --- Cargo.lock | 1 + Cargo.toml | 1 + README.md | 24 ++ build.rs | 197 +++++++++ fuzz/.gitignore | 4 + fuzz/Cargo.lock | 360 ++++++++++++++++ fuzz/Cargo.toml | 65 +++ fuzz/README.md | 151 +++++++ fuzz/fuzz_targets/fuzz_nvmem.rs | 63 +++ fuzz/fuzz_targets/fuzz_restore_state.rs | 68 +++ fuzz/fuzz_targets/fuzz_tpm.rs | 35 ++ fuzz/fuzz_targets/fuzz_tpm_session.rs | 117 +++++ fuzz/seed_corpus/fuzz_tpm/get_capability.bin | Bin 0 -> 22 bytes fuzz/seed_corpus/fuzz_tpm/get_random.bin | Bin 0 -> 12 bytes fuzz/seed_corpus/fuzz_tpm/get_test_result.bin | Bin 0 -> 10 bytes .../fuzz_tpm/hierarchy_control.bin | Bin 0 -> 32 bytes .../fuzz_tpm/incremental_self_test.bin | Bin 0 -> 16 bytes fuzz/seed_corpus/fuzz_tpm/nv_read_public.bin | Bin 0 -> 14 bytes fuzz/seed_corpus/fuzz_tpm/pcr_extend.bin | Bin 0 -> 65 bytes fuzz/seed_corpus/fuzz_tpm/pcr_read.bin | Bin 0 -> 20 bytes fuzz/seed_corpus/fuzz_tpm/read_clock.bin | Bin 0 -> 10 bytes fuzz/seed_corpus/fuzz_tpm/read_public.bin | Bin 0 -> 14 bytes fuzz/seed_corpus/fuzz_tpm/self_test_full.bin | Bin 0 -> 11 bytes fuzz/seed_corpus/fuzz_tpm/shutdown_clear.bin | Bin 0 -> 12 bytes fuzz/seed_corpus/fuzz_tpm/startup_clear.bin | Bin 0 -> 12 bytes fuzz/seed_corpus/fuzz_tpm/startup_state.bin | Bin 0 -> 12 bytes .../fuzz_tpm/stream_startup_getrandom.bin | Bin 0 -> 44 bytes .../fuzz_tpm/truncated_command.bin | Bin 0 -> 10 bytes fuzz/src/lib.rs | 405 ++++++++++++++++++ fuzz/tpm.dict | 159 +++++++ 30 files changed, 1650 insertions(+) create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.lock create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/README.md create mode 100644 fuzz/fuzz_targets/fuzz_nvmem.rs create mode 100644 fuzz/fuzz_targets/fuzz_restore_state.rs create mode 100644 fuzz/fuzz_targets/fuzz_tpm.rs create mode 100644 fuzz/fuzz_targets/fuzz_tpm_session.rs create mode 100644 fuzz/seed_corpus/fuzz_tpm/get_capability.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/get_random.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/get_test_result.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/hierarchy_control.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/incremental_self_test.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/nv_read_public.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/pcr_extend.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/pcr_read.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/read_clock.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/read_public.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/self_test_full.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/shutdown_clear.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/startup_clear.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/startup_state.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/stream_startup_getrandom.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/truncated_command.bin create mode 100644 fuzz/src/lib.rs create mode 100644 fuzz/tpm.dict diff --git a/Cargo.lock b/Cargo.lock index c337fc9..1b13406 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,6 +138,7 @@ name = "ms-tcg-tpm-sys" version = "0.0.0" dependencies = [ "bitfield-struct", + "cc", "cmake", "fs-err", "openssl-sys", diff --git a/Cargo.toml b/Cargo.toml index 357fb3e..a3567fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ postcard = { version = "1.1", default-features = false, features = ["use-std"] } serde = { version = "1.0", features = ["derive"] } [build-dependencies] +cc = "1" cmake = "0.1" fs-err = "3.3" diff --git a/README.md b/README.md index 17dab61..c688c65 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,30 @@ cargo run -p test-harness -- ./tpm.nvmem feature. - `test-harness/` - A small sample binary that initializes the TPM, sends a few commands, and persists state to an on-disk `.nvmem` blob. +- `fuzz/` - `cargo-fuzz` targets covering the crate's untrusted-input + boundaries. See [`fuzz/README.md`](fuzz/README.md). + +## Fuzzing + +The `fuzz/` directory holds [`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz) +targets for the inputs a vTPM doesn't control: guest-issued commands, saved-state +blobs, and persisted nvmem blobs. + +```sh +cargo install cargo-fuzz +rustup toolchain install nightly + +cargo +nightly fuzz run fuzz_tpm +``` + +Fuzzing needs the TPM's C code instrumented, not just the Rust wrapper around +it, so `build.rs` builds it with clang and the matching sanitizer and coverage +flags whenever Cargo reports that this crate is being built for a fuzzer. That +requires clang to be installed, but nothing needs to be configured by hand, and +it doesn't affect normal builds. + +See [`fuzz/README.md`](fuzz/README.md) for the list of targets and for details +on corpus layout, reproducing crashes, and adjusting the instrumentation. ## Relationship to `tpm-rs` diff --git a/build.rs b/build.rs index 5fe6b77..6b2efdb 100644 --- a/build.rs +++ b/build.rs @@ -19,6 +19,7 @@ fn main() -> Result<(), Box> { for archive in &source_archives { println!("cargo:rerun-if-changed={}", archive.display()); } + fuzzing::warn_if_prebuilt(); source_archives } // Archives built in-tree live in `OUT_DIR`, and watching those would @@ -128,6 +129,8 @@ mod tpm { cmake_config.define("CMAKE_MSVC_RUNTIME_LIBRARY", "MultiThreadedDLL"); } + crate::fuzzing::configure(&mut cmake_config)?; + match backend { Backend::OpenSsl => { crate::openssl::configure(&mut cmake_config, &tpm_src_dir, &out_dir)?; @@ -427,6 +430,200 @@ mod symbols { } } +/// Instrumenting the TPM's C code when this crate is built for a fuzzer. +mod fuzzing { + use crate::util; + use std::ffi::OsStr; + use std::ffi::OsString; + use std::path::Path; + use std::path::PathBuf; + + /// Whether Cargo is building this crate for a fuzzer. + /// + /// `cargo fuzz` puts `--cfg fuzzing` in `RUSTFLAGS`, which Cargo surfaces + /// to build scripts as this variable. + fn enabled() -> bool { + println!("cargo:rerun-if-env-changed=CARGO_CFG_FUZZING"); + std::env::var_os("CARGO_CFG_FUZZING").is_some() + } + + /// Instrument the TPM build to match how Cargo is building the Rust side. + /// + /// Rust's `-Zsanitizer` and libFuzzer's coverage instrumentation only cover + /// Rust code, which for this crate is a thin wrapper around the C library + /// that does the actual work. Left uninstrumented, the fuzzer would be + /// driving the code that parses commands blind, and AddressSanitizer would + /// only see what its allocator interceptors catch rather than the memory + /// errors inside that code. + pub(crate) fn configure( + cmake_config: &mut cmake::Config, + ) -> Result<(), Box> { + if !enabled() { + return Ok(()); + } + + // An explicitly empty value opts out of instrumenting the C code. + let flags = match util::env("TCG_TPM_FUZZ_CFLAGS") { + Some(flags) => flags, + None => default_flags(), + }; + let flags: Vec<&OsStr> = flags + .to_str() + .ok_or("TCG_TPM_FUZZ_CFLAGS is not valid UTF-8")? + .split_whitespace() + .map(OsStr::new) + .collect(); + if flags.is_empty() { + return Ok(()); + } + + if let Some(compiler) = compiler()? { + drop_stale_cmake_cache(&compiler)?; + cmake_config.define("CMAKE_C_COMPILER", &compiler); + } + + for flag in flags { + cmake_config.cflag(flag); + } + + Ok(()) + } + + /// CMake refuses to reconfigure an existing build tree with a different + /// compiler, which would turn something as ordinary as installing a newer + /// clang into a confusing build failure. Drop the cache so that CMake + /// configures from scratch instead. + fn drop_stale_cmake_cache(compiler: &Path) -> Result<(), Box> { + // Where `cmake::Config` puts the build tree, given it inherits `OUT_DIR`. + let cache = PathBuf::from(std::env::var("OUT_DIR")?).join("build/CMakeCache.txt"); + let Ok(contents) = fs_err::read_to_string(&cache) else { + return Ok(()); + }; + + let cached = contents.lines().find_map(|line| { + line.strip_prefix("CMAKE_C_COMPILER:")? + .split_once('=') + .map(|(_, value)| Path::new(value)) + }); + + if cached.is_some_and(|cached| cached != compiler) { + fs_err::remove_file(&cache)?; + } + + Ok(()) + } + + /// Warn that `TCG_TPM_LIB_DIR` libraries are used as-is, since whoever + /// built them is the one who decides whether they're instrumented. + pub(crate) fn warn_if_prebuilt() { + if enabled() { + println!( + "cargo:warning=fuzzing against the pre-built TPM libraries in TCG_TPM_LIB_DIR; \ + unless they were built with sanitizer and coverage instrumentation, the fuzzer \ + will not see inside them" + ); + } + } + + /// The instrumentation to build the TPM with, mirroring what `cargo fuzz` + /// asks `rustc` for. + fn default_flags() -> OsString { + // Gives the C code the SanitizerCoverage instrumentation libFuzzer + // needs, without letting clang link in a `main` of its own. + let mut flags = String::from("-fsanitize=fuzzer-no-link"); + + // `-Zsanitizer=...` reaches build scripts as this variable, already + // comma-separated the way clang wants it. + println!("cargo:rerun-if-env-changed=CARGO_CFG_SANITIZE"); + if let Some(sanitizers) = std::env::var_os("CARGO_CFG_SANITIZE") + && !sanitizers.is_empty() + { + flags.push_str(" -fsanitize="); + flags.push_str(&sanitizers.to_string_lossy()); + } + + flags.into() + } + + /// The compiler to build the instrumented TPM with, or `None` to keep the + /// one the build is already configured to use. + /// + /// The flags above are clang-only: GCC has no `-fsanitize=fuzzer-no-link`, + /// and the `-fsanitize-coverage=trace-pc` scheme it does support was + /// removed from libFuzzer. + fn compiler() -> Result, Box> { + if let Some(compiler) = util::env("TCG_TPM_FUZZ_CC") { + return Ok(Some(PathBuf::from(compiler))); + } + + // Leave an already-clang compiler (and anything the caller pointed `CC` + // at) alone, so cross-compilation setups keep working. + if cc::Build::new().try_get_compiler()?.is_like_clang() { + return Ok(None); + } + + let clang = find_clang().ok_or( + "fuzzing needs clang to instrument the TPM's C code, but none was found on PATH. \ + Install clang, or point TCG_TPM_FUZZ_CC at one. To fuzz without instrumenting the \ + C code - which leaves the fuzzer blind to the code that parses TPM commands - set \ + TCG_TPM_FUZZ_CFLAGS to an empty value.", + )?; + + Ok(Some(clang)) + } + + /// Look for clang on `PATH`. + fn find_clang() -> Option { + // clang-cl is the driver that understands MSVC's command line. + let stem = if util::is_windows_msvc().ok()? { + "clang-cl" + } else { + "clang" + }; + + let path = std::env::var_os("PATH")?; + let mut newest: Option<(u32, PathBuf)> = None; + + for dir in std::env::split_paths(&path) { + let unversioned = dir.join(format!("{stem}{}", std::env::consts::EXE_SUFFIX)); + if unversioned.is_file() { + return Some(unversioned); + } + + // Debian and its derivatives only ship versioned binaries unless + // the `clang` metapackage is installed, so fall back to the newest + // version that is installed. + let Ok(entries) = fs_err::read_dir(&dir) else { + continue; + }; + + for entry in entries.flatten() { + let name = entry.file_name(); + let Some(name) = name.to_str() else { + continue; + }; + let name = name + .strip_suffix(std::env::consts::EXE_SUFFIX) + .unwrap_or(name); + + let Some(version) = name + .strip_prefix(stem) + .and_then(|version| version.strip_prefix('-')) + .and_then(|version| version.parse::().ok()) + else { + continue; + }; + + if newest.as_ref().is_none_or(|(newest, _)| version > *newest) { + newest = Some((version, entry.path())); + } + } + } + + newest.map(|(_, path)| path) + } +} + /// Environment and filesystem odds and ends shared by the rest of the script. mod util { use std::ffi::OsString; diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..c86f687 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,360 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitfield-struct" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca6739863c590881f038d033a146c51ddae239186a4327014839fd864f44ed5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +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 = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "fs-err" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +dependencies = [ + "autocfg", +] + +[[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 = "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 = "ms-tcg-tpm-sys" +version = "0.0.0" +dependencies = [ + "bitfield-struct", + "cc", + "cmake", + "fs-err", + "openssl-sys", + "postcard", + "serde", + "tracing", +] + +[[package]] +name = "ms-tcg-tpm-sys-fuzz" +version = "0.0.0" +dependencies = [ + "arbitrary", + "libfuzzer-sys", + "ms-tcg-tpm-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-src" +version = "300.6.1+3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[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 = "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 = "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 = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[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 = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..c9dcc06 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,65 @@ +# Copyright (C) Microsoft Corporation. All rights reserved. + +[package] +name = "ms-tcg-tpm-sys-fuzz" +version = "0.0.0" +publish = false +edition = "2024" + +[package.metadata] +cargo-fuzz = true + +[features] +# The TPM library requires OpenSSL 3.5 or newer, which is newer than what most +# distros ship, so build it from source by default. +default = ["openssl", "vendored"] + +# `openssl` and `symcrypt` are mutually exclusive; see the root crate's manifest. +openssl = ["ms-tcg-tpm-sys/openssl"] +symcrypt = ["ms-tcg-tpm-sys/symcrypt"] +vendored = ["ms-tcg-tpm-sys/vendored"] + +[dependencies] +arbitrary = { version = "1", features = ["derive"] } +libfuzzer-sys = "0.4" +ms-tcg-tpm-sys = { path = "..", default-features = false } + +[[bin]] +name = "fuzz_tpm" +path = "fuzz_targets/fuzz_tpm.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "fuzz_tpm_session" +path = "fuzz_targets/fuzz_tpm_session.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "fuzz_restore_state" +path = "fuzz_targets/fuzz_restore_state.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "fuzz_nvmem" +path = "fuzz_targets/fuzz_nvmem.rs" +test = false +doc = false +bench = false + +[lints.rust] +missing_docs = "warn" +unused_qualifications = "warn" + +[lints.clippy] +undocumented_unsafe_blocks = "warn" + +# Keep the fuzz crate out of the root workspace, so that the instrumented build +# doesn't interfere with regular `cargo build` invocations. +[workspace] +members = ["."] diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000..e7cd123 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,151 @@ +# Fuzzing `ms-tcg-tpm-sys` + +[`cargo-fuzz`](https://rust-fuzz.github.io/book/cargo-fuzz.html) targets for the +crate's untrusted-input boundaries: the TPM command stream, saved-state blobs, +and persisted nvmem blobs. + +## Running + +```sh +cargo install cargo-fuzz +rustup toolchain install nightly + +# Fuzz until interrupted. +cargo +nightly fuzz run fuzz_tpm + +# Anything after `--` goes to libFuzzer. +cargo +nightly fuzz run fuzz_tpm -- -max_total_time=600 -dict=fuzz/tpm.dict +cargo +nightly fuzz run fuzz_tpm_session -- -runs=100000 +``` + +The `fuzz_tpm` seed corpus is checked in, and `cargo fuzz` doesn't pick it up on +its own, so pass it as a second, read-only corpus to start from real commands +rather than random bytes. libFuzzer writes new inputs to the first directory it +is given and requires it to already exist: + +```sh +mkdir -p fuzz/corpus/fuzz_tpm +cargo +nightly fuzz run fuzz_tpm fuzz/corpus/fuzz_tpm fuzz/seed_corpus/fuzz_tpm \ + -- -dict=fuzz/tpm.dict +``` + +The other three targets take `arbitrary`-encoded structures rather than raw +bytes, so there's nothing meaningful to hand-write a seed for; they build their +corpus from scratch. + +Reproducing and minimizing a crash works as usual: + +```sh +cargo +nightly fuzz run fuzz_tpm fuzz/artifacts/fuzz_tpm/crash- +cargo +nightly fuzz tmin fuzz_tpm fuzz/artifacts/fuzz_tpm/crash- +``` + +The `symcrypt` backend can be fuzzed with `--no-default-features --features +symcrypt` (after `./scripts/fetch-symcrypt.sh`). + +## Targets + +| Target | Input | What it covers | +| --- | --- | --- | +| `fuzz_tpm` | A raw TPM command stream | `execute_command`: header validation, command unmarshaling, and dispatch - the bytes a guest controls | +| `fuzz_tpm_session` | A sequence of platform operations | Commands interleaved with power cycles, live save / restore, locality changes, and cancellation | +| `fuzz_restore_state` | A saved-state blob | `restore_state`, plus running the TPM on whatever the blob restored | +| `fuzz_nvmem` | A persisted nvmem blob | Booting on a corrupted, truncated, or hostile nvmem blob | + +`fuzz_tpm` takes plain bytes: the input is split into commands along the +boundaries declared by each command's own `commandSize` field, so a corpus +entry can be a capture of a real command stream, and +[`seed_corpus/fuzz_tpm/`](seed_corpus/fuzz_tpm) holds hand-built commands to +start from. + +The other three take +[`arbitrary`](https://docs.rs/arbitrary)-derived structures. `fuzz_restore_state` +and `fuzz_nvmem` mostly work by splicing fuzzer controlled bytes into a blob the +TPM itself produced, since random bytes never survive a blob's framing and +header validation. + +## Oracles + +Beyond the crashes, hangs, and leaks that libFuzzer and the sanitizers find on +their own, the targets assert that: + +- a reported response length fits in the response buffer that was handed to the + TPM, +- a non-empty response is at least a header long, and its `responseSize` field + matches the number of bytes actually returned, +- a save / restore / save round-trip reproduces the blob it started from - + otherwise state is being dropped or invented in transit, and a migrated TPM + wouldn't match the one it was migrated from, +- restoring a blob the TPM just saved always succeeds, and +- an `initialize` that fails leaves the platform singleton free to be claimed + again. + +## Determinism + +Replaying a crash has to reproduce it, so every input the TPM sees other than +the fuzzer's bytes is fixed: entropy comes from a fixed-seed PRNG, and the +monotonic timer advances by a fixed step per call. Both are rewound at the start +of every iteration. + +Manufacturing a TPM is far too slow to do per iteration, so every target +manufactures one per process and rolls it back to a pristine snapshot before +each iteration. State that a command leaves behind but that isn't part of a +saved-state blob will therefore bleed into subsequent iterations, which can make +a crash depend on the inputs that ran before it. That's worth chasing rather +than papering over: the same bleed-through would break a live migration. + +This matters in practice. `fuzz_nvmem` originally booted a fresh TPM per +iteration via `InitKind::ColdInitWithPersistentState` instead of rolling one +back, and 50 of the 61 artifacts a campaign produced could not be replayed on +their own. Installing the blob with `reset` on a rolled-back TPM fixed that. +Note that residual nondeterminism in a replay is itself a signal: the TPM +library reads uninitialized stack memory when an NV read fails, so what a +corrupt blob does can genuinely depend on what ran before it. + +## Instrumenting the C library + +Rust's `-Zsanitizer=address` and libFuzzer's coverage instrumentation only apply +to Rust code. Since essentially all of the interesting code here is the vendored +C TPM library, the C build has to be instrumented as well, which requires clang. + +[`build.rs`](../build.rs) handles this: `cargo fuzz` puts `--cfg fuzzing` in +`RUSTFLAGS`, which Cargo passes to build scripts as `CARGO_CFG_FUZZING`, so the +build script knows to build the TPM with clang, `-fsanitize=fuzzer-no-link`, and +whatever `-Zsanitizer` Cargo reported in `CARGO_CFG_SANITIZE`. Nothing needs to +be set by hand, and normal (non-fuzzing) builds are untouched. + +It's worth it: on a short run of `fuzz_tpm`, instrumenting the C code took +coverage from ~550 edges (the Rust wrapper alone) to ~2300, and it's what lets +ASan catch memory errors inside the TPM library - such as a read past the end of +a global - rather than only the ones its allocator interceptors see. + +Two env-vars adjust this, both accepting the target-prefixed forms the crate's +other env-vars do: + +- `TCG_TPM_FUZZ_CC` - the compiler to build the instrumented TPM with. Set this + when cross-compiling, or when the clang to use isn't the newest one on `PATH`. + If the build is already configured to use a clang (via `CC`, say), that one is + kept and this isn't needed. +- `TCG_TPM_FUZZ_CFLAGS` - replaces the instrumentation flags entirely. Setting + it to an empty value fuzzes without instrumenting the C code, which leaves the + fuzzer blind to the code that parses TPM commands. + +The build script fails rather than silently producing an uninstrumented fuzzer +if it can't find a clang. + +GCC can't be used for this - it has no `-fsanitize=fuzzer-no-link`, and the +`-fsanitize-coverage=trace-pc` scheme it does support was removed from libFuzzer. + +OpenSSL is deliberately left uninstrumented: it's a dependency of the code under +test rather than the target, and instrumenting it slows the build down and +spends the fuzzer's energy inside crypto primitives it can't steer. ASan's +allocator interceptors still cover heap errors there. + +## Corpus layout + +- `corpus//` - the working corpus libFuzzer grows as it runs + (git-ignored). +- `seed_corpus/fuzz_tpm/` - checked-in starting inputs. +- `artifacts//` - crashing inputs (git-ignored). +- `tpm.dict` - TPM wire-format constants (tags, command codes, handles, + algorithm ids) for libFuzzer's `-dict`. diff --git a/fuzz/fuzz_targets/fuzz_nvmem.rs b/fuzz/fuzz_targets/fuzz_nvmem.rs new file mode 100644 index 0000000..c1366b8 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_nvmem.rs @@ -0,0 +1,63 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +//! Fuzzes booting the TPM on a persisted nvmem blob. +//! +//! A vTPM's nvmem blob lives outside the TPM, in whatever the host uses for +//! persistent storage, so the TPM has to survive being handed one that has been +//! rolled back or tampered with. Each iteration power-cycles onto a +//! corrupted blob and then drives commands against whatever comes up. +//! +//! The blob is installed with `reset`, on a TPM that [`with_tpm`] has just +//! rolled back, rather than with `InitKind::ColdInitWithPersistentState` on a +//! freshly built one. Both take the same path through the platform's nvmem +//! layer and into `_TPM_Init`, but the rollback means an iteration depends only +//! on its own input: leaving the previous iteration's globals in place made +//! most of this target's crashes impossible to replay on their own. +//! +//! The blob always stays the full `NV_MEMORY_SIZE`. The platform now rejects +//! any other size up front - a shorter region let the TPM library address NV +//! memory that wasn't there - so mutating the length here would just bounce off +//! that check and waste the iteration. `tests/nvmem_size.rs` covers it instead. + +#![no_main] + +use arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; +use ms_tcg_tpm_sys_fuzz::Patch; +use ms_tcg_tpm_sys_fuzz::TPM2_STARTUP_CLEAR; +use ms_tcg_tpm_sys_fuzz::baseline_nvmem; +use ms_tcg_tpm_sys_fuzz::split_commands; +use ms_tcg_tpm_sys_fuzz::with_tpm; + +/// Caps how much work a single input can ask for, keeping the fuzzer's +/// executions-per-second up. +const MAX_COMMANDS: usize = 8; + +#[derive(Arbitrary, Debug)] +struct Input { + /// Corruption to apply to the nvmem blob. + patches: Vec, + /// Commands to run against the TPM that comes up on the corrupted blob. + commands: Vec, +} + +fuzz_target!(|input: Input| { + let mut nvmem = baseline_nvmem().to_vec(); + Patch::apply_all(&mut nvmem, &input.patches); + + with_tpm(|tpm| { + // Rejecting a blob outright is a perfectly good outcome. + if tpm.reset(Some(&nvmem)).is_err() { + return; + } + + // Start the TPM up before anything else; that's where the bulk of the + // nvmem is parsed. + let mut commands = vec![TPM2_STARTUP_CLEAR.to_vec()]; + commands.append(&mut split_commands(&input.commands, MAX_COMMANDS)); + + for command in &mut commands { + let _ = tpm.execute_command(command); + } + }); +}); diff --git a/fuzz/fuzz_targets/fuzz_restore_state.rs b/fuzz/fuzz_targets/fuzz_restore_state.rs new file mode 100644 index 0000000..571dd96 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_restore_state.rs @@ -0,0 +1,68 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +//! Fuzzes saved-state restore. +//! +//! `MsTpm185Platform::restore_state` parses a blob that, for a vTPM, comes off +//! a save file or a migration stream: it can be truncated, stale, corrupted, or +//! outright hostile. Restoring one must fail cleanly rather than crash, and a +//! blob that does restore must leave the TPM in a state that can keep running. +//! +//! Random bytes never get past the blob's postcard framing, so the interesting +//! mode here is `Patched`, which splices fuzzer controlled bytes into a blob +//! the TPM itself produced. + +#![no_main] + +use arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; +use ms_tcg_tpm_sys_fuzz::Patch; +use ms_tcg_tpm_sys_fuzz::split_commands; +use ms_tcg_tpm_sys_fuzz::with_tpm; + +/// Caps how much work a single input can ask for, keeping the fuzzer's +/// executions-per-second up. +const MAX_COMMANDS: usize = 4; + +#[derive(Arbitrary, Debug)] +enum Input { + /// Restore an arbitrary blob. + Raw { + /// The blob to restore. + blob: Vec, + /// Commands to run afterwards, if the restore succeeded. + commands: Vec, + }, + /// Restore a corrupted version of a blob the TPM actually saved. + Patched { + /// Corruption to apply to the saved state. + patches: Vec, + /// Commands to run afterwards, if the restore succeeded. + commands: Vec, + }, +} + +fuzz_target!(|input: Input| { + with_tpm(|tpm| { + let (blob, commands) = match &input { + Input::Raw { blob, commands } => (blob.clone(), commands), + Input::Patched { patches, commands } => { + let mut blob = tpm.snapshot().to_vec(); + Patch::apply_all(&mut blob, patches); + (blob, commands) + } + }; + + // A rejected blob is the expected outcome for most inputs. + if tpm.restore_state(blob).is_err() { + return; + } + + // The restore claimed the state was good, so the TPM has to be able to + // keep running on it, and to save it back out. + for command in &mut split_commands(commands, MAX_COMMANDS) { + let _ = tpm.execute_command(command); + } + + let _ = tpm.save_state(); + }); +}); diff --git a/fuzz/fuzz_targets/fuzz_tpm.rs b/fuzz/fuzz_targets/fuzz_tpm.rs new file mode 100644 index 0000000..d11831e --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_tpm.rs @@ -0,0 +1,35 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +//! Fuzzes TPM command dispatch. +//! +//! The input is a raw TPM command stream: it is split into individual commands +//! along the boundaries declared by each command's `commandSize` field, and +//! each one is dispatched through `MsTpm185Platform::execute_command`, which is +//! how a transport (say, a vTPM's MMIO/CRB interface) would hand guest +//! controlled bytes to this crate. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use ms_tcg_tpm_sys_fuzz::split_commands; +use ms_tcg_tpm_sys_fuzz::with_tpm; + +/// Caps how much work a single input can ask for, keeping the fuzzer's +/// executions-per-second up. +const MAX_COMMANDS: usize = 16; + +fuzz_target!(|data: &[u8]| { + if data.is_empty() { + return; + } + + let mut commands = split_commands(data, MAX_COMMANDS); + + with_tpm(|tpm| { + for command in &mut commands { + // Errors are the crate correctly rejecting a malformed request; + // it's crashes, hangs, and leaks that this target is looking for. + let _ = tpm.execute_command(command); + } + }); +}); diff --git a/fuzz/fuzz_targets/fuzz_tpm_session.rs b/fuzz/fuzz_targets/fuzz_tpm_session.rs new file mode 100644 index 0000000..ad0a4a3 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_tpm_session.rs @@ -0,0 +1,117 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +//! Fuzzes whole TPM sessions. +//! +//! Where `fuzz_tpm` hammers on a single entry point, this target drives the +//! rest of the crate's surface - power cycles, live save / restore, locality +//! changes and command cancellation - interleaved with commands, looking for +//! bugs that only show up in a particular ordering of platform events. +//! +//! Commands are (mostly) generated with a well formed header, so that the +//! fuzzer spends its time on per-command unmarshaling rather than on the header +//! parsing that `fuzz_tpm` already covers. + +#![no_main] + +use arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; +use ms_tcg_tpm_sys::Locality; +use ms_tcg_tpm_sys_fuzz::TPM_CC_FIRST; +use ms_tcg_tpm_sys_fuzz::TPM_ST_NO_SESSIONS; +use ms_tcg_tpm_sys_fuzz::TPM_ST_SESSIONS; +use ms_tcg_tpm_sys_fuzz::build_command; +use ms_tcg_tpm_sys_fuzz::with_tpm; + +/// Caps how much work a single input can ask for, keeping the fuzzer's +/// executions-per-second up. +const MAX_OPS: usize = 24; + +#[derive(Arbitrary, Debug)] +enum Op { + /// Dispatch a command with a well formed header and a fuzzer controlled + /// body (handles, authorization area, and parameters). + Command { + /// Selects between `TPM_ST_SESSIONS` and `TPM_ST_NO_SESSIONS`. + sessions: bool, + /// Offset from `TPM_CC_FIRST`, which covers every implemented command + /// code, plus a margin of unimplemented ones. + code_offset: u8, + /// Everything after the command header. + body: Vec, + }, + /// Dispatch raw bytes, header and all. + Raw(Vec), + /// Dispatch raw bytes through the unchecked entry point, skipping the + /// wrapper's request size validation. + RawUnchecked(Vec), + /// Round-trip the live state through a save / restore, the way a live + /// migration would. + SaveRestore, + /// Simulate a power cycle. + Reset, + /// Assign a locality to subsequent commands. Values that aren't valid + /// localities are skipped. + SetLocality(u8), + /// Set or clear the cancel flag. + SetCancelFlag(bool), +} + +fuzz_target!(|ops: Vec| { + if ops.is_empty() { + return; + } + + with_tpm(|tpm| { + for op in ops.iter().take(MAX_OPS) { + match op { + Op::Command { + sessions, + code_offset, + body, + } => { + let tag = if *sessions { + TPM_ST_SESSIONS + } else { + TPM_ST_NO_SESSIONS + }; + let code = TPM_CC_FIRST + *code_offset as u32; + let mut command = build_command(tag, code, body); + let _ = tpm.execute_command(&mut command); + } + Op::Raw(bytes) => { + let mut command = bytes.clone(); + let _ = tpm.execute_command(&mut command); + } + Op::RawUnchecked(bytes) => { + let mut command = bytes.clone(); + tpm.execute_command_unchecked(&mut command); + } + Op::SaveRestore => { + let saved = tpm.save_state(); + tpm.restore_state(saved.clone()) + .expect("state the TPM just saved should restore"); + + // Restoring a blob and saving it right back out has to + // produce that same blob, otherwise state is being dropped + // (or invented) on the way through, and a migrated TPM + // wouldn't match the one it was migrated from. + assert!( + tpm.save_state() == saved, + "save / restore / save round-trip changed the saved state" + ); + } + Op::Reset => { + tpm.reset(None).expect("power cycling should succeed"); + } + Op::SetLocality(locality) => { + if let Ok(locality) = Locality::try_from(*locality) { + tpm.set_locality(locality); + } + } + Op::SetCancelFlag(enabled) => { + tpm.set_cancel_flag(*enabled); + } + } + } + }); +}); diff --git a/fuzz/seed_corpus/fuzz_tpm/get_capability.bin b/fuzz/seed_corpus/fuzz_tpm/get_capability.bin new file mode 100644 index 0000000000000000000000000000000000000000..61886a51c37910e5fe5fd260cc14a856da9a2ac1 GIT binary patch literal 22 ZcmZo*WME(rV_;yc0@7?i8b~oX000wR0a^e6 literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/get_random.bin b/fuzz/seed_corpus/fuzz_tpm/get_random.bin new file mode 100644 index 0000000000000000000000000000000000000000..5de8ad69d72a746ad6e7c7f2403f04b350422705 GIT binary patch literal 12 TcmZo*WME+6VPIgaW>5eC2mk>p literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/get_test_result.bin b/fuzz/seed_corpus/fuzz_tpm/get_test_result.bin new file mode 100644 index 0000000000000000000000000000000000000000..c0dd72765a8953adda41a5ae4786f35e1d78fc09 GIT binary patch literal 10 RcmZo*WME+6Vqjpb0RRQV0SN#A literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/hierarchy_control.bin b/fuzz/seed_corpus/fuzz_tpm/hierarchy_control.bin new file mode 100644 index 0000000000000000000000000000000000000000..9680f9024fc7b3ffa56ea1fb660f67422940d627 GIT binary patch literal 32 hcmZo*VqjoUU|?WWbYNiM0kS!PG$)W|U;r|B7yu&k0j~f6 literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/incremental_self_test.bin b/fuzz/seed_corpus/fuzz_tpm/incremental_self_test.bin new file mode 100644 index 0000000000000000000000000000000000000000..74965be41473a64c01e76406225f452e2a0326b5 GIT binary patch literal 16 VcmZo*WME(rU|?W$0@92O+yDx*0O0@t literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_read_public.bin b/fuzz/seed_corpus/fuzz_tpm/nv_read_public.bin new file mode 100644 index 0000000000000000000000000000000000000000..310a0bfa0b2e58227049ab2c2bea3aa12972a4ee GIT binary patch literal 14 TcmZo*WME+6V_;y+1Tq)_39$hD literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/pcr_extend.bin b/fuzz/seed_corpus/fuzz_tpm/pcr_extend.bin new file mode 100644 index 0000000000000000000000000000000000000000..81de1f8e6fb0c082f0e7e4e35301a542dde59d67 GIT binary patch literal 65 hcmZo*Vqjo!WME)y0@5JB=>WuF7LZ_M;D#}A(Ewer0i^%{ literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/pcr_read.bin b/fuzz/seed_corpus/fuzz_tpm/pcr_read.bin new file mode 100644 index 0000000000000000000000000000000000000000..90bea0e5b3b259c1dec32137e88d3a88f3e671bd GIT binary patch literal 20 ZcmZo*WME(rVPIga1JaBP+|2(O7yuLy0wVwb literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/read_clock.bin b/fuzz/seed_corpus/fuzz_tpm/read_clock.bin new file mode 100644 index 0000000000000000000000000000000000000000..5d3aa7e256ddb786fdbf2b2bfb74921a70f4141b GIT binary patch literal 10 RcmZo*WME+6Vqjow1ONrd0S*8F literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/read_public.bin b/fuzz/seed_corpus/fuzz_tpm/read_public.bin new file mode 100644 index 0000000000000000000000000000000000000000..ea6c7b998bdcc09866cfb707967aacd60109b7f6 GIT binary patch literal 14 TcmZo*WME+6V_;w`ZUhnl3*!NX literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/self_test_full.bin b/fuzz/seed_corpus/fuzz_tpm/self_test_full.bin new file mode 100644 index 0000000000000000000000000000000000000000..df17913c23851c4b02ff3dd165ce6adb9123bf29 GIT binary patch literal 11 ScmZo*WME+6W?*1+W&{8RVF1zq literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/shutdown_clear.bin b/fuzz/seed_corpus/fuzz_tpm/shutdown_clear.bin new file mode 100644 index 0000000000000000000000000000000000000000..418aaa9e79c192dce44ce681ae526c651210a0e3 GIT binary patch literal 12 TcmZo*WME+6VPIf%WncgR2R;DQ literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/startup_clear.bin b/fuzz/seed_corpus/fuzz_tpm/startup_clear.bin new file mode 100644 index 0000000000000000000000000000000000000000..359215efe1304562af5b204c5e5b1891c92f564d GIT binary patch literal 12 TcmZo*WME+6VPIf%VPF6N2Ri`M literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/startup_state.bin b/fuzz/seed_corpus/fuzz_tpm/startup_state.bin new file mode 100644 index 0000000000000000000000000000000000000000..24766312be41f861562619e15796defb68bd9b0b GIT binary patch literal 12 TcmZo*WME+6VPIf%VPFIR2Rs1O literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/stream_startup_getrandom.bin b/fuzz/seed_corpus/fuzz_tpm/stream_startup_getrandom.bin new file mode 100644 index 0000000000000000000000000000000000000000..67a39f7ae118c89688cf98a51c68625c130a5cc2 GIT binary patch literal 44 ncmZo*WME+6VPIf%VPI&0v8x#rz-$qqcpXrlk%61}KLY~*RKEls literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/truncated_command.bin b/fuzz/seed_corpus/fuzz_tpm/truncated_command.bin new file mode 100644 index 0000000000000000000000000000000000000000..bd67332a03d35c63427962fd8a3d08675e7ca8aa GIT binary patch literal 10 PcmZo*WMBXy2F7Xt1(pE+ literal 0 HcmV?d00001 diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs new file mode 100644 index 0000000..0404a70 --- /dev/null +++ b/fuzz/src/lib.rs @@ -0,0 +1,405 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +//! Shared plumbing for the `ms-tcg-tpm-sys` fuzz targets. +//! +//! # Determinism +//! +//! Fuzzing is only useful if a crashing input can be replayed, which means +//! every input the TPM sees other than the fuzzer's own bytes has to be +//! reproducible. [`FuzzPlatformCallbacks`] therefore backs the entropy source +//! with a fixed-seed PRNG and the monotonic timer with a call counter, both of +//! which are reset at the top of every iteration. +//! +//! # The global TPM instance +//! +//! The underlying C library keeps its state in globals, so only one +//! [`MsTpm185Platform`] can be live at a time, and manufacturing one is far too +//! slow to do per iteration. [`with_tpm`] instead manufactures a single TPM per +//! process and restores a pristine post-manufacture snapshot before each +//! iteration, which is both much faster and gives every iteration the same +//! starting state. +//! +//! Note that the snapshot only covers the state the library knows how to +//! save + restore. If a command leaves state behind that isn't part of a +//! saved-state blob, it will bleed into subsequent iterations - which is itself +//! a bug worth finding, since the same bleed-through would break a live +//! migration. + +#![warn(missing_docs)] + +use arbitrary::Arbitrary; +use ms_tcg_tpm_sys::DynResult; +use ms_tcg_tpm_sys::Error; +use ms_tcg_tpm_sys::InitKind; +use ms_tcg_tpm_sys::Locality; +use ms_tcg_tpm_sys::MsTpm185Platform; +use ms_tcg_tpm_sys::PlatformCallbacks; +use std::cell::RefCell; +use std::sync::Mutex; +use std::sync::OnceLock; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering::Relaxed; +use std::time::Duration; + +/// `MAX_COMMAND_SIZE` from `TpmProfile_Common.h`. +pub const MAX_COMMAND_SIZE: usize = 8192; + +/// `MAX_RESPONSE_SIZE` from `TpmProfile_Common.h`. +/// +/// `ExecuteCommand` marshals directly into the caller's response buffer without +/// bounds checking it against the response it is building, so anything smaller +/// than this is a heap overflow waiting to happen. Every response buffer handed +/// to the TPM by this harness is exactly this size. +pub const MAX_RESPONSE_SIZE: usize = 8192; + +/// Size of the `tag` + `commandSize` + `commandCode` command header, which is +/// also the size of the `tag` + `responseSize` + `responseCode` response +/// header. +pub const HEADER_SIZE: usize = 10; + +/// `TPM_ST_NO_SESSIONS` +pub const TPM_ST_NO_SESSIONS: u16 = 0x8001; +/// `TPM_ST_SESSIONS` +pub const TPM_ST_SESSIONS: u16 = 0x8002; + +/// `TPM_CC_FIRST` from `TpmTypes.h`. The last implemented command code is +/// `TPM_CC_LAST` (`0x1aa`). +pub const TPM_CC_FIRST: u32 = 0x0000011f; + +/// `TPM_RC_SUCCESS` +pub const TPM_RC_SUCCESS: u32 = 0; + +/// `TPM2_Startup(TPM_SU_CLEAR)` +pub const TPM2_STARTUP_CLEAR: &[u8] = &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x01, 0x44, 0x00, 0x00, +]; + +/// `TPM2_SelfTest(fullTest = YES)` +const TPM2_SELF_TEST_FULL: &[u8] = &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x01, 0x43, 0x01, +]; + +/// Seed for the entropy PRNG. Any fixed value will do; this one is arbitrary. +const PRNG_SEED: u64 = 0x0123_4567_89ab_cdef; + +static PRNG_STATE: AtomicU64 = AtomicU64::new(PRNG_SEED); +static CLOCK_TICKS: AtomicU64 = AtomicU64::new(0); +static COMMITTED_NVMEM: Mutex> = Mutex::new(Vec::new()); + +/// Rewinds the entropy source and the clock, so that a given sequence of TPM +/// operations always sees the same platform inputs. +/// +/// [`with_tpm`] does this for its callers; targets that build their own +/// [`MsTpm185Platform`] have to call it themselves. +pub fn reset_platform_inputs() { + PRNG_STATE.store(PRNG_SEED, Relaxed); + CLOCK_TICKS.store(0, Relaxed); +} + +/// SplitMix64. +fn next_random() -> u64 { + const GAMMA: u64 = 0x9e37_79b9_7f4a_7c15; + + // `fetch_add` hands back the previous state, so re-apply the step to get + // the state this draw corresponds to. + let mut z = PRNG_STATE.fetch_add(GAMMA, Relaxed).wrapping_add(GAMMA); + z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + z ^ (z >> 31) +} + +/// Deterministic [`PlatformCallbacks`] implementation. +pub struct FuzzPlatformCallbacks; + +impl PlatformCallbacks for FuzzPlatformCallbacks { + fn commit_nv_state(&mut self, state: &[u8]) -> DynResult<()> { + // Stashed (rather than dropped) so that `baseline_nvmem` can hand a + // real, TPM-written nvmem blob to the nvmem fuzz target. + let mut committed = COMMITTED_NVMEM.lock().unwrap(); + committed.clear(); + committed.extend_from_slice(state); + Ok(()) + } + + fn get_crypt_random(&mut self, buf: &mut [u8]) -> DynResult { + // The platform layer runs the FIPS continuous RNG test over + // consecutive 4 byte blocks, so this must not return a constant. + for chunk in buf.chunks_mut(size_of::()) { + let random = next_random().to_le_bytes(); + chunk.copy_from_slice(&random[..chunk.len()]); + } + Ok(buf.len()) + } + + fn monotonic_timer(&mut self) -> Duration { + // Advance by a fixed step per call, so that time-dependent code (clock + // updates, lockout self-heal, etc.) makes progress without making the + // TPM's behavior depend on how fast the fuzzer happens to be running. + Duration::from_millis(CLOCK_TICKS.fetch_add(1, Relaxed)) + } + + fn get_unique_value(&self) -> &'static [u8] { + b"ms-tcg-tpm-sys fuzzing platform unique value" + } +} + +thread_local! { + static TPM: RefCell> = const { RefCell::new(None) }; +} + +/// Hands `f` a TPM that has been rolled back to its pristine post-manufacture +/// state. +/// +/// The TPM is manufactured on first use and reused (via save / restore) by +/// every subsequent call. +pub fn with_tpm(f: impl FnOnce(&mut FuzzTpm) -> R) -> R { + TPM.with(|tpm| { + let mut tpm = tpm.borrow_mut(); + let tpm = match &mut *tpm { + Some(tpm) => { + tpm.rollback(); + tpm + } + slot => slot.insert(FuzzTpm::new()), + }; + f(tpm) + }) +} + +/// An nvmem blob written by a real, freshly manufactured TPM, for fuzz targets +/// that want to mutate a plausible blob rather than start from noise. +/// +/// Must not be called while a [`FuzzTpm`] is live, as it briefly manufactures a +/// TPM of its own. +pub fn baseline_nvmem() -> &'static [u8] { + static BASELINE: OnceLock> = OnceLock::new(); + BASELINE.get_or_init(|| { + // Manufacture a TPM purely for the nvmem it writes on the way up, then + // hand the platform singleton back for the fuzz target to claim. + drop(FuzzTpm::new()); + + let committed = COMMITTED_NVMEM.lock().unwrap().clone(); + assert!( + !committed.is_empty(), + "manufacturing a TPM should have committed an nvmem blob" + ); + committed + }) +} + +/// A manufactured TPM, along with the buffers and pristine snapshot used to +/// drive it. +pub struct FuzzTpm { + platform: MsTpm185Platform, + response: Vec, + snapshot: Vec, +} + +impl FuzzTpm { + /// Manufactures a TPM, starts it up, and snapshots the result. + fn new() -> FuzzTpm { + reset_platform_inputs(); + + let platform = + MsTpm185Platform::initialize(Box::new(FuzzPlatformCallbacks), InitKind::ColdInit) + .expect("manufacturing a TPM should succeed"); + + let mut tpm = FuzzTpm { + platform, + response: vec![0; MAX_RESPONSE_SIZE], + snapshot: Vec::new(), + }; + + // Start the TPM up, and get the (slow) self tests out of the way once + // per process, so that iterations start from a state where the bulk of + // the command surface is reachable. + tpm.execute_expecting_success(TPM2_STARTUP_CLEAR, "TPM2_Startup"); + tpm.execute_expecting_success(TPM2_SELF_TEST_FULL, "TPM2_SelfTest"); + + tpm.snapshot = tpm.platform.save_state(); + tpm + } + + /// Rolls the TPM back to the state captured by [`FuzzTpm::new`]. + fn rollback(&mut self) { + reset_platform_inputs(); + self.platform + .restore_state(self.snapshot.clone()) + .expect("restoring the harness' own snapshot should succeed"); + } + + /// The pristine snapshot that every iteration starts from. + pub fn snapshot(&self) -> &[u8] { + &self.snapshot + } + + /// Executes a command through the size-checked entry point, returning the + /// response on success. + pub fn execute_command(&mut self, command: &mut [u8]) -> Result<&[u8], Error> { + let len = self.platform.execute_command(command, &mut self.response)?; + Ok(check_response(&self.response, len)) + } + + /// Executes a command through the unchecked entry point, returning the + /// response. + pub fn execute_command_unchecked(&mut self, command: &mut [u8]) -> &[u8] { + // SAFETY: `self.response` is `MAX_RESPONSE_SIZE` bytes, which is the + // largest response the TPM can produce, and the TPM validates the + // request buffer's size against the size declared in its header. + let len = unsafe { + self.platform + .execute_command_unchecked(command, &mut self.response) + }; + check_response(&self.response, len) + } + + /// Executes a command that is expected to succeed, panicking otherwise. + fn execute_expecting_success(&mut self, command: &[u8], name: &str) { + let mut command = command.to_vec(); + let response = self + .execute_command(&mut command) + .unwrap_or_else(|e| panic!("{name} should be dispatchable: {e}")); + let code = response_code(response).expect("response should have a header"); + assert_eq!(code, TPM_RC_SUCCESS, "{name} returned {code:#010x}"); + } + + /// Simulates a power cycle, optionally swapping in a new nvmem blob. + pub fn reset(&mut self, nvmem: Option<&[u8]>) -> Result<(), Error> { + self.platform.reset(nvmem) + } + + /// Saves the live state into an opaque blob. + pub fn save_state(&self) -> Vec { + self.platform.save_state() + } + + /// Restores previously saved state. + pub fn restore_state(&mut self, state: Vec) -> Result<(), Error> { + self.platform.restore_state(state) + } + + /// Sets the locality subsequent commands run at. + pub fn set_locality(&mut self, locality: Locality) { + self.platform.set_locality(locality); + } + + /// Sets or clears the cancel flag. + pub fn set_cancel_flag(&mut self, enabled: bool) { + self.platform.set_cancel_flag(enabled); + } +} + +/// Validates the invariants every TPM response is expected to uphold, and +/// returns the response. +/// +/// `len` is the response length the TPM reported, and `buffer` the response +/// buffer it was handed. +pub fn check_response(buffer: &[u8], len: usize) -> &[u8] { + assert!( + len <= buffer.len(), + "TPM reported a {len} byte response, which overruns the {} byte response buffer", + buffer.len() + ); + + let response = &buffer[..len]; + + // A response is either empty (the command was cancelled / dropped) or a + // well formed header, whose size field covers the whole response. + if !response.is_empty() { + assert!( + response.len() >= HEADER_SIZE, + "TPM returned a {} byte response, which is too short to hold a header", + response.len() + ); + + let declared = u32::from_be_bytes(response[2..6].try_into().unwrap()) as usize; + assert_eq!( + declared, + response.len(), + "response header declares {declared} bytes, but {} bytes were returned", + response.len() + ); + } + + response +} + +/// Extracts the response code from a response, if it has a header. +pub fn response_code(response: &[u8]) -> Option { + let code = response.get(6..HEADER_SIZE)?; + Some(u32::from_be_bytes(code.try_into().unwrap())) +} + +/// Builds a command with a well formed header wrapped around a fuzzer supplied +/// body (handles, authorization area, and parameters). +/// +/// Random bytes almost never form a valid header, which would leave the fuzzer +/// stuck at the TPM's front door. This gets it past the header parsing so that +/// it can spend its time on the far more interesting per-command unmarshaling +/// code. +pub fn build_command(tag: u16, command_code: u32, body: &[u8]) -> Vec { + let size = (HEADER_SIZE + body.len()) as u32; + + let mut command = Vec::with_capacity(HEADER_SIZE + body.len()); + command.extend_from_slice(&tag.to_be_bytes()); + command.extend_from_slice(&size.to_be_bytes()); + command.extend_from_slice(&command_code.to_be_bytes()); + command.extend_from_slice(body); + command +} + +/// Splits a byte stream into commands along the boundaries declared by each +/// command's own `commandSize` field. +/// +/// A stream of concatenated TPM commands splits exactly, so a corpus entry can +/// simply be a capture of a real command stream, while a stream with a bogus +/// size field is handed over as-is to exercise the size validation. +pub fn split_commands(data: &[u8], max_commands: usize) -> Vec> { + let mut commands = Vec::new(); + let mut rest = data; + + while !rest.is_empty() && commands.len() < max_commands { + let declared = rest + .get(2..6) + .map(|size| u32::from_be_bytes(size.try_into().unwrap()) as usize); + + let len = match declared { + Some(len) if (HEADER_SIZE..=rest.len()).contains(&len) => len, + _ => rest.len(), + }; + + let (command, tail) = rest.split_at(len); + commands.push(command.to_vec()); + rest = tail; + } + + commands +} + +/// A fuzzer directed splice into an existing blob. +/// +/// Used by the targets that fuzz blobs the TPM itself produced (saved state, +/// nvmem), where starting from random bytes would never get past the blob's +/// header validation. +#[derive(Arbitrary, Debug)] +pub struct Patch { + /// Offset to splice at, taken modulo the length of the blob. + pub offset: u32, + /// Bytes to splice in, truncated to fit. + pub bytes: Vec, +} + +impl Patch { + /// Applies a series of patches to `blob`. + pub fn apply_all(blob: &mut [u8], patches: &[Patch]) { + if blob.is_empty() { + return; + } + + for patch in patches { + let offset = patch.offset as usize % blob.len(); + let len = patch.bytes.len().min(blob.len() - offset); + blob[offset..offset + len].copy_from_slice(&patch.bytes[..len]); + } + } +} diff --git a/fuzz/tpm.dict b/fuzz/tpm.dict new file mode 100644 index 0000000..9ddc696 --- /dev/null +++ b/fuzz/tpm.dict @@ -0,0 +1,159 @@ +# Copyright (C) Microsoft Corporation. All rights reserved. +# +# libFuzzer dictionary of TPM 2.0 wire-format constants, all big-endian, as +# they appear on the wire. +# +# cargo +nightly fuzz run fuzz_tpm -- -dict=fuzz/tpm.dict + +# Structure tags (TPM_ST) +tag_no_sessions="\x80\x01" +tag_sessions="\x80\x02" +tag_rsp_command="\x80\x04" +tag_attest_certify="\x80\x17" +tag_creation="\x80\x21" +tag_auth_secret="\x80\x23" +tag_hashcheck="\x80\x24" +tag_auth_signed="\x80\x25" + +# Startup / shutdown types (TPM_SU) +su_clear="\x00\x00" +su_state="\x00\x01" + +# Command codes (TPM_CC), the ones most likely to reach interesting state +cc_nv_undefine_space_special="\x00\x00\x01\x1f" +cc_evict_control="\x00\x00\x01\x20" +cc_hierarchy_control="\x00\x00\x01\x21" +cc_nv_undefine_space="\x00\x00\x01\x22" +cc_clear="\x00\x00\x01\x26" +cc_clear_control="\x00\x00\x01\x27" +cc_clock_set="\x00\x00\x01\x28" +cc_hierarchy_change_auth="\x00\x00\x01\x29" +cc_nv_define_space="\x00\x00\x01\x2a" +cc_pcr_allocate="\x00\x00\x01\x2b" +cc_set_primary_policy="\x00\x00\x01\x2e" +cc_change_pps="\x00\x00\x01\x2f" +cc_change_eps="\x00\x00\x01\x30" +cc_nv_write="\x00\x00\x01\x37" +cc_nv_increment="\x00\x00\x01\x34" +cc_nv_extend="\x00\x00\x01\x36" +cc_dictionary_attack_lock_reset="\x00\x00\x01\x39" +cc_dictionary_attack_parameters="\x00\x00\x01\x3a" +cc_nv_change_auth="\x00\x00\x01\x3b" +cc_pcr_event="\x00\x00\x01\x3c" +cc_pcr_reset="\x00\x00\x01\x3d" +cc_sequence_complete="\x00\x00\x01\x3e" +cc_incremental_self_test="\x00\x00\x01\x42" +cc_self_test="\x00\x00\x01\x43" +cc_startup="\x00\x00\x01\x44" +cc_shutdown="\x00\x00\x01\x45" +cc_stir_random="\x00\x00\x01\x46" +cc_activate_credential="\x00\x00\x01\x47" +cc_certify="\x00\x00\x01\x48" +cc_policy_nv="\x00\x00\x01\x49" +cc_create="\x00\x00\x01\x53" +cc_import="\x00\x00\x01\x56" +cc_load="\x00\x00\x01\x57" +cc_quote="\x00\x00\x01\x58" +cc_rsa_decrypt="\x00\x00\x01\x59" +cc_sequence_update="\x00\x00\x01\x5c" +cc_sign="\x00\x00\x01\x5d" +cc_unseal="\x00\x00\x01\x5e" +cc_context_load="\x00\x00\x01\x61" +cc_context_save="\x00\x00\x01\x62" +cc_flush_context="\x00\x00\x01\x65" +cc_load_external="\x00\x00\x01\x67" +cc_make_credential="\x00\x00\x01\x68" +cc_nv_read_public="\x00\x00\x01\x69" +cc_policy_authorize="\x00\x00\x01\x6a" +cc_policy_secret="\x00\x00\x01\x51" +cc_create_primary="\x00\x00\x01\x31" +cc_nv_read="\x00\x00\x01\x4e" +cc_read_public="\x00\x00\x01\x73" +cc_start_auth_session="\x00\x00\x01\x76" +cc_verify_signature="\x00\x00\x01\x77" +cc_ecc_parameters="\x00\x00\x01\x78" +cc_get_capability="\x00\x00\x01\x7a" +cc_get_random="\x00\x00\x01\x7b" +cc_get_test_result="\x00\x00\x01\x7c" +cc_hash="\x00\x00\x01\x7d" +cc_pcr_read="\x00\x00\x01\x7e" +cc_policy_pcr="\x00\x00\x01\x7f" +cc_policy_restart="\x00\x00\x01\x80" +cc_read_clock="\x00\x00\x01\x81" +cc_pcr_extend="\x00\x00\x01\x82" +cc_event_sequence_complete="\x00\x00\x01\x85" +cc_hash_sequence_start="\x00\x00\x01\x86" +cc_policy_get_digest="\x00\x00\x01\x89" +cc_test_parms="\x00\x00\x01\x8a" +cc_policy_password="\x00\x00\x01\x8c" +cc_create_loaded="\x00\x00\x01\x91" +cc_policy_authorize_nv="\x00\x00\x01\x92" +cc_ac_send="\x00\x00\x01\x94" +cc_certify_x509="\x00\x00\x01\x97" +cc_last="\x00\x00\x01\xaa" + +# Permanent handles (TPM_RH) and session handles +rh_owner="\x40\x00\x00\x01" +rh_null="\x40\x00\x00\x07" +rh_password="\x40\x00\x00\x09" +rh_lockout="\x40\x00\x00\x0a" +rh_endorsement="\x40\x00\x00\x0b" +rh_platform="\x40\x00\x00\x0c" +rh_platform_nv="\x40\x00\x00\x0d" +handle_pcr0="\x00\x00\x00\x00" +handle_hmac_session="\x02\x00\x00\x00" +handle_policy_session="\x03\x00\x00\x00" +handle_transient="\x80\x00\x00\x00" +handle_persistent="\x81\x00\x00\x00" +handle_nv_index="\x01\x00\x00\x01" + +# Algorithm identifiers (TPM_ALG) +alg_error="\x00\x00" +alg_rsa="\x00\x01" +alg_sha1="\x00\x04" +alg_hmac="\x00\x05" +alg_aes="\x00\x06" +alg_keyedhash="\x00\x08" +alg_xor="\x00\x0a" +alg_sha256="\x00\x0b" +alg_sha384="\x00\x0c" +alg_sha512="\x00\x0d" +alg_null="\x00\x10" +alg_sm3_256="\x00\x12" +alg_sm4="\x00\x13" +alg_rsassa="\x00\x14" +alg_rsaes="\x00\x15" +alg_rsapss="\x00\x16" +alg_oaep="\x00\x17" +alg_ecdsa="\x00\x18" +alg_ecdh="\x00\x19" +alg_symcipher="\x00\x25" +alg_camellia="\x00\x26" +alg_sha3_256="\x00\x27" +alg_ctr="\x00\x40" +alg_ofb="\x00\x41" +alg_cbc="\x00\x42" +alg_cfb="\x00\x43" +alg_ecb="\x00\x44" + +# ECC curves (TPM_ECC_CURVE) +curve_nist_p256="\x00\x03" +curve_nist_p384="\x00\x04" +curve_bn_p256="\x00\x10" + +# Capabilities (TPM_CAP) +cap_algs="\x00\x00\x00\x00" +cap_handles="\x00\x00\x00\x01" +cap_commands="\x00\x00\x00\x02" +cap_pcrs="\x00\x00\x00\x05" +cap_tpm_properties="\x00\x00\x00\x06" +cap_pcr_properties="\x00\x00\x00\x07" +cap_ecc_curves="\x00\x00\x00\x08" +cap_vendor_property="\x00\x00\x01\x00" + +# Sizes that tend to sit at the edge of a buffer +size_zero="\x00\x00" +size_max_digest="\x00\x40" +size_max_buffer="\x04\x00" +size_max_command="\x00\x00\x20\x00" +size_overflow="\xff\xff\xff\xff" From 9c2f4c60f3a07b9a929a66eed6f67bc1bdc27216 Mon Sep 17 00:00:00 2001 From: Steven Malis Date: Wed, 26 Aug 2026 15:37:56 -0400 Subject: [PATCH 2/7] feedback --- fuzz/src/lib.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 0404a70..1cf265c 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -169,14 +169,16 @@ pub fn with_tpm(f: impl FnOnce(&mut FuzzTpm) -> R) -> R { /// An nvmem blob written by a real, freshly manufactured TPM, for fuzz targets /// that want to mutate a plausible blob rather than start from noise. /// -/// Must not be called while a [`FuzzTpm`] is live, as it briefly manufactures a -/// TPM of its own. +/// Must not be called from inside a [`with_tpm`] closure, since it reaches the +/// per-process TPM through [`with_tpm`] itself. pub fn baseline_nvmem() -> &'static [u8] { static BASELINE: OnceLock> = OnceLock::new(); BASELINE.get_or_init(|| { - // Manufacture a TPM purely for the nvmem it writes on the way up, then - // hand the platform singleton back for the fuzz target to claim. - drop(FuzzTpm::new()); + // Manufacturing the per-process TPM is what writes the blob, so go + // through `with_tpm` rather than standing up a throwaway TPM here. Only + // one TPM can hold the platform singleton at a time, so a throwaway + // would panic for any caller that got here after the first `with_tpm`. + with_tpm(|_| ()); let committed = COMMITTED_NVMEM.lock().unwrap().clone(); assert!( @@ -244,8 +246,12 @@ impl FuzzTpm { /// response. pub fn execute_command_unchecked(&mut self, command: &mut [u8]) -> &[u8] { // SAFETY: `self.response` is `MAX_RESPONSE_SIZE` bytes, which is the - // largest response the TPM can produce, and the TPM validates the - // request buffer's size against the size declared in its header. + // largest response the TPM can produce. The request buffer needs no + // trimming: `ExecuteCommand` bounds every unmarshal by the + // `requestSize` it was handed, and rejects a command whose header + // declares a different size with `TPM_RC_COMMAND_SIZE` (see + // `commandSize != requestSize` in `ExecCommand.c`), so an oversized + // declared size can't walk off the end of `command`. let len = unsafe { self.platform .execute_command_unchecked(command, &mut self.response) From 0b52d1bb60b7312c668c4f25df10b5445b107362 Mon Sep 17 00:00:00 2001 From: Steven Malis Date: Thu, 27 Aug 2026 16:39:57 -0400 Subject: [PATCH 3/7] more --- fuzz/README.md | 103 ++++ fuzz/fuzz_targets/fuzz_nvmem.rs | 53 +- fuzz/fuzz_targets/fuzz_restore_state.rs | 38 +- fuzz/fuzz_targets/fuzz_tpm_session.rs | 99 +++- fuzz/seed_corpus/fuzz_tpm/certify.bin | Bin 0 -> 48 bytes fuzz/seed_corpus/fuzz_tpm/change_eps.bin | Bin 0 -> 27 bytes fuzz/seed_corpus/fuzz_tpm/change_pps.bin | Bin 0 -> 27 bytes fuzz/seed_corpus/fuzz_tpm/clear.bin | Bin 0 -> 27 bytes fuzz/seed_corpus/fuzz_tpm/clear_control.bin | Bin 0 -> 28 bytes fuzz/seed_corpus/fuzz_tpm/clock_rate.bin | Bin 0 -> 28 bytes fuzz/seed_corpus/fuzz_tpm/clock_set.bin | Bin 0 -> 35 bytes fuzz/seed_corpus/fuzz_tpm/commit.bin | Bin 0 -> 134 bytes fuzz/seed_corpus/fuzz_tpm/commit2.bin | Bin 0 -> 137 bytes fuzz/seed_corpus/fuzz_tpm/context_save.bin | Bin 0 -> 14 bytes fuzz/seed_corpus/fuzz_tpm/create.bin | Bin 0 -> 63 bytes fuzz/seed_corpus/fuzz_tpm/create_loaded.bin | Bin 0 -> 57 bytes fuzz/seed_corpus/fuzz_tpm/create_primary.bin | Bin 0 -> 63 bytes .../fuzz_tpm/createprimary_sha384.bin | Bin 0 -> 63 bytes fuzz/seed_corpus/fuzz_tpm/da_lock_reset.bin | Bin 0 -> 27 bytes fuzz/seed_corpus/fuzz_tpm/da_parameters.bin | Bin 0 -> 39 bytes fuzz/seed_corpus/fuzz_tpm/decapsulate.bin | Bin 0 -> 61 bytes fuzz/seed_corpus/fuzz_tpm/ecc_decrypt2.bin | Bin 0 -> 141 bytes fuzz/seed_corpus/fuzz_tpm/ecc_encrypt.bin | Bin 0 -> 23 bytes fuzz/seed_corpus/fuzz_tpm/ecdh_keygen.bin | Bin 0 -> 14 bytes fuzz/seed_corpus/fuzz_tpm/ecdh_zgen.bin | Bin 0 -> 97 bytes fuzz/seed_corpus/fuzz_tpm/ecdh_zgen2.bin | Bin 0 -> 97 bytes fuzz/seed_corpus/fuzz_tpm/encryptdecrypt.bin | Bin 0 -> 66 bytes fuzz/seed_corpus/fuzz_tpm/encryptdecrypt2.bin | Bin 0 -> 66 bytes .../fuzz_tpm/event_seq_complete.bin | Bin 0 -> 60 bytes fuzz/seed_corpus/fuzz_tpm/flush_context.bin | Bin 0 -> 14 bytes fuzz/seed_corpus/fuzz_tpm/get_cmd_audit.bin | Bin 0 -> 48 bytes .../fuzz_tpm/get_session_audit.bin | Bin 0 -> 52 bytes fuzz/seed_corpus/fuzz_tpm/get_time.bin | Bin 0 -> 48 bytes fuzz/seed_corpus/fuzz_tpm/hash.bin | Bin 0 -> 21 bytes fuzz/seed_corpus/fuzz_tpm/hash_sequence.bin | Bin 0 -> 82 bytes fuzz/seed_corpus/fuzz_tpm/hash_sha1.bin | Bin 0 -> 21 bytes fuzz/seed_corpus/fuzz_tpm/hash_sha384.bin | Bin 0 -> 21 bytes fuzz/seed_corpus/fuzz_tpm/hash_sha512.bin | Bin 0 -> 21 bytes .../fuzz_tpm/hierarchy_changeauth.bin | Bin 0 -> 29 bytes fuzz/seed_corpus/fuzz_tpm/load_external.bin | Bin 0 -> 40 bytes fuzz/seed_corpus/fuzz_tpm/make_credential.bin | Bin 0 -> 84 bytes fuzz/seed_corpus/fuzz_tpm/mldsa_certify.bin | Bin 0 -> 46 bytes .../seed_corpus/fuzz_tpm/mldsa_readpublic.bin | Bin 0 -> 14 bytes fuzz/seed_corpus/fuzz_tpm/mldsa_sign.bin | Bin 0 -> 71 bytes .../fuzz_tpm/mldsa_sign_digest.bin | Bin 0 -> 71 bytes fuzz/seed_corpus/fuzz_tpm/mldsa_sign_seq.bin | Bin 0 -> 80 bytes .../fuzz_tpm/mldsa_verify_digest.bin | Bin 0 -> 131 bytes .../seed_corpus/fuzz_tpm/mldsa_verify_seq.bin | Bin 0 -> 141 bytes fuzz/seed_corpus/fuzz_tpm/nv_certify.bin | Bin 0 -> 56 bytes fuzz/seed_corpus/fuzz_tpm/nv_extend.bin | Bin 0 -> 65 bytes .../fuzz_tpm/nv_globalwritelock.bin | Bin 0 -> 27 bytes fuzz/seed_corpus/fuzz_tpm/nv_increment.bin | Bin 0 -> 31 bytes fuzz/seed_corpus/fuzz_tpm/nv_read.bin | Bin 0 -> 35 bytes fuzz/seed_corpus/fuzz_tpm/nv_readlock.bin | Bin 0 -> 98 bytes fuzz/seed_corpus/fuzz_tpm/nv_setbits.bin | Bin 0 -> 39 bytes fuzz/seed_corpus/fuzz_tpm/nv_undefine.bin | Bin 0 -> 31 bytes fuzz/seed_corpus/fuzz_tpm/nv_write.bin | Bin 0 -> 43 bytes fuzz/seed_corpus/fuzz_tpm/nv_writelock.bin | Bin 0 -> 31 bytes .../fuzz_tpm/object_changeauth.bin | Bin 0 -> 36 bytes fuzz/seed_corpus/fuzz_tpm/pcr_allocate.bin | Bin 0 -> 37 bytes fuzz/seed_corpus/fuzz_tpm/pcr_event.bin | Bin 0 -> 34 bytes .../fuzz_tpm/pcr_setauthpolicy.bin | Bin 0 -> 67 bytes .../seed_corpus/fuzz_tpm/pcr_setauthvalue.bin | Bin 0 -> 29 bytes .../fuzz_tpm/policy_authorizenv.bin | Bin 0 -> 35 bytes .../fuzz_tpm/policy_capability.bin | Bin 0 -> 36 bytes .../fuzz_tpm/policy_command_code.bin | Bin 0 -> 18 bytes .../fuzz_tpm/policy_countertimer.bin | Bin 0 -> 28 bytes fuzz/seed_corpus/fuzz_tpm/policy_cphash.bin | Bin 0 -> 48 bytes .../seed_corpus/fuzz_tpm/policy_dupselect.bin | Bin 0 -> 83 bytes .../fuzz_tpm/policy_get_digest.bin | Bin 0 -> 14 bytes fuzz/seed_corpus/fuzz_tpm/policy_locality.bin | Bin 0 -> 15 bytes fuzz/seed_corpus/fuzz_tpm/policy_namehash.bin | Bin 0 -> 48 bytes fuzz/seed_corpus/fuzz_tpm/policy_nv.bin | Bin 0 -> 49 bytes .../seed_corpus/fuzz_tpm/policy_nvwritten.bin | Bin 0 -> 15 bytes fuzz/seed_corpus/fuzz_tpm/policy_or.bin | Bin 0 -> 86 bytes .../fuzz_tpm/policy_parameters.bin | Bin 0 -> 48 bytes fuzz/seed_corpus/fuzz_tpm/policy_password.bin | Bin 0 -> 14 bytes fuzz/seed_corpus/fuzz_tpm/policy_pcr.bin | Bin 0 -> 26 bytes fuzz/seed_corpus/fuzz_tpm/policy_secret.bin | Bin 0 -> 41 bytes fuzz/seed_corpus/fuzz_tpm/policy_spdm.bin | Bin 0 -> 86 bytes fuzz/seed_corpus/fuzz_tpm/policy_template.bin | Bin 0 -> 48 bytes fuzz/seed_corpus/fuzz_tpm/quote.bin | Bin 0 -> 45 bytes .../fuzz_tpm/read_only_control.bin | Bin 0 -> 28 bytes .../fuzz_tpm/read_public_seeded.bin | Bin 0 -> 14 bytes fuzz/seed_corpus/fuzz_tpm/rsa_certify.bin | Bin 0 -> 48 bytes fuzz/seed_corpus/fuzz_tpm/rsa_decrypt.bin | Bin 0 -> 65 bytes .../seed_corpus/fuzz_tpm/rsa_decrypt_oaep.bin | Bin 0 -> 163 bytes fuzz/seed_corpus/fuzz_tpm/rsa_encrypt.bin | Bin 0 -> 52 bytes fuzz/seed_corpus/fuzz_tpm/rsa_encrypt_es.bin | Bin 0 -> 26 bytes .../seed_corpus/fuzz_tpm/rsa_encrypt_oaep.bin | Bin 0 -> 28 bytes fuzz/seed_corpus/fuzz_tpm/rsa_makecred.bin | Bin 0 -> 84 bytes fuzz/seed_corpus/fuzz_tpm/rsa_quote.bin | Bin 0 -> 45 bytes fuzz/seed_corpus/fuzz_tpm/rsa_readpublic.bin | Bin 0 -> 14 bytes fuzz/seed_corpus/fuzz_tpm/rsa_sign.bin | Bin 0 -> 73 bytes fuzz/seed_corpus/fuzz_tpm/rsa_sign_pss.bin | Bin 0 -> 73 bytes fuzz/seed_corpus/fuzz_tpm/seq_sha384.bin | Bin 0 -> 79 bytes .../fuzz_tpm/set_algorithm_set.bin | Bin 0 -> 31 bytes fuzz/seed_corpus/fuzz_tpm/set_cc_audit.bin | Bin 0 -> 41 bytes .../fuzz_tpm/set_primary_policy.bin | Bin 0 -> 63 bytes fuzz/seed_corpus/fuzz_tpm/sign.bin | Bin 0 -> 73 bytes fuzz/seed_corpus/fuzz_tpm/test_parms.bin | Bin 0 -> 18 bytes fuzz/seed_corpus/fuzz_tpm/unseal.bin | Bin 0 -> 27 bytes fuzz/seed_corpus/fuzz_tpm/zgen_2phase2.bin | Bin 0 -> 171 bytes fuzz/src/lib.rs | 558 +++++++++++++++++- fuzz/tpm.dict | 16 + 105 files changed, 841 insertions(+), 26 deletions(-) create mode 100644 fuzz/seed_corpus/fuzz_tpm/certify.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/change_eps.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/change_pps.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/clear.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/clear_control.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/clock_rate.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/clock_set.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/commit.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/commit2.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/context_save.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/create.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/create_loaded.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/create_primary.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/createprimary_sha384.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/da_lock_reset.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/da_parameters.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/decapsulate.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/ecc_decrypt2.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/ecc_encrypt.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/ecdh_keygen.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/ecdh_zgen.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/ecdh_zgen2.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/encryptdecrypt.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/encryptdecrypt2.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/event_seq_complete.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/flush_context.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/get_cmd_audit.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/get_session_audit.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/get_time.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/hash.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/hash_sequence.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/hash_sha1.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/hash_sha384.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/hash_sha512.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/hierarchy_changeauth.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/load_external.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/make_credential.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/mldsa_certify.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/mldsa_readpublic.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/mldsa_sign.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/mldsa_sign_digest.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/mldsa_sign_seq.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/mldsa_verify_digest.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/mldsa_verify_seq.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/nv_certify.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/nv_extend.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/nv_globalwritelock.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/nv_increment.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/nv_read.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/nv_readlock.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/nv_setbits.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/nv_undefine.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/nv_write.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/nv_writelock.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/object_changeauth.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/pcr_allocate.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/pcr_event.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/pcr_setauthpolicy.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/pcr_setauthvalue.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_authorizenv.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_capability.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_command_code.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_countertimer.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_cphash.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_dupselect.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_get_digest.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_locality.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_namehash.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_nv.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_nvwritten.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_or.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_parameters.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_password.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_pcr.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_secret.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_spdm.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_template.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/quote.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/read_only_control.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/read_public_seeded.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/rsa_certify.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/rsa_decrypt.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/rsa_decrypt_oaep.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/rsa_encrypt.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/rsa_encrypt_es.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/rsa_encrypt_oaep.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/rsa_makecred.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/rsa_quote.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/rsa_readpublic.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/rsa_sign.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/rsa_sign_pss.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/seq_sha384.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/set_algorithm_set.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/set_cc_audit.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/set_primary_policy.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/sign.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/test_parms.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/unseal.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/zgen_2phase2.bin diff --git a/fuzz/README.md b/fuzz/README.md index e7cd123..a701557 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -40,6 +40,24 @@ cargo +nightly fuzz run fuzz_tpm fuzz/artifacts/fuzz_tpm/crash- cargo +nightly fuzz tmin fuzz_tpm fuzz/artifacts/fuzz_tpm/crash- ``` +libFuzzer's default `-timeout` is 1200 seconds, which is longer than any +campaign worth running, so an input that sends the TPM into an infinite loop is +not reported as a hang - it just silently pins a worker for the whole run, and +the only sign is a child still alive after `-max_total_time` has passed. Pass +something realistic: + +```sh +cargo +nightly fuzz run fuzz_nvmem -- -timeout=25 -max_total_time=600 +``` + +To collect every distinct failure in one run rather than stopping at the first, +fork mode keeps going and writes an artifact per failure: + +```sh +cargo +nightly fuzz run fuzz_nvmem -- -fork=4 -timeout=25 \ + -ignore_crashes=1 -ignore_timeouts=1 -ignore_ooms=1 +``` + The `symcrypt` backend can be fuzzed with `--no-default-features --features symcrypt` (after `./scripts/fetch-symcrypt.sh`). @@ -80,6 +98,91 @@ their own, the targets assert that: - an `initialize` that fails leaves the platform singleton free to be claimed again. +## Reaching the command handlers + +Almost every TPM command needs three things before its handler is entered: a +well formed header, handles that name something which exists, and a valid +authorization area. A mutator invents none of them, so a fuzzer left to itself +spends its whole budget being turned away at the front door. A first campaign +reached 14 of the 125 implemented commands, and the only two of those that +take an authorization were the two the seed corpus happened to spell out by +hand. + +Three things address that, all in [`src/lib.rs`](src/lib.rs): + +- `SETUP_COMMANDS` runs once per process, before the snapshot that every + iteration rolls back to, and leaves behind two loaded keys, a persistent copy + of one of them, five NV indices, and one HMAC and one policy session. Their + handles are what `KNOWN_HANDLES` lists, and the setup asserts the TPM handed + those exact handles back, so the table cannot silently drift out of date and + quietly cost coverage. +- `PASSWORD_SESSION` is a `TPM_RS_PW` authorization area with an empty + password, which is what the great majority of commands are asking for. + `fuzz_tpm_session` assembles it structurally; `tpm.dict` carries it so that + `fuzz_tpm` can splice one into a raw byte stream. +- The first authorization of a dictionary-attack protected object does not run + the command at all: it writes the `daUsed` state to NV and returns + `TPM_RC_RETRY` (see `SessionProcess.c`). The harness settles that before + snapshotting, so iterations don't spend their first authorized command on it. + +What gets seeded is shaped by the profile's limits rather than by what would be +convenient: + +- `MAX_LOADED_OBJECTS` is 3, so exactly two objects are seeded and the third + slot is deliberately left empty. Seeding a third would make every + `TPM2_Load`, `TPM2_Create` and `TPM2_CreateLoaded` fail with + `TPM_RC_OBJECT_MEMORY`, and the seeding would cost more coverage than it + bought. For the same reason two of the three session slots are used, not all + three. +- The two objects are an unrestricted ECC signing key and a restricted + decryption key. A signing key cannot be a parent, so without the second one + nothing that needs somewhere to put an object is reachable. +- NV indices are free - they don't consume object slots - so there is one of + each type that has commands specific to it: ordinary, counter, bit field, + extend, and one carrying `READ_STCLEAR`/`WRITE_STCLEAR` for the lock + commands. +- Persistent objects are free too, for the same reason, so the RSA and ML-DSA + keys are generated once, evicted to NV, and their transient slot handed + straight back. Without them neither algorithm runs at all: no RSA key means + key generation, the prime sieve and Miller-Rabin are never entered, and no + ML-DSA key means the streaming signature commands - which take a + `TPM2B_SIGNATURE_CTX`, an ML-DSA context - have nothing to name. RSA is + 1024-bit deliberately: it exercises the same generation path a larger modulus + would, and this runs on every process startup. + +[`seed_corpus/fuzz_tpm/`](seed_corpus/fuzz_tpm) then names those handles from +raw byte streams. Every seed there was checked to actually reach its command +handler rather than being rejected on the way in. + +Some commands can't be seeded at all, because what they take is data only the +TPM can produce: `TPM2_Load` wants a private blob wrapped by a particular +parent, and `TPM2_ContextLoad` wants a saved context. `canned_commands()` +covers those by assembling the commands at setup time out of real responses - +a `TPM2_Create` and a `TPM2_ContextSave` - which `fuzz_tpm_session` dispatches +through `Op::Canned`. Both are built before the snapshot, since a saved context +is only valid against the state it was saved from, which is the state every +iteration rolls back to. + +`fuzz_nvmem` and `fuzz_restore_state` have neither of those advantages: their +input is `arbitrary`-encoded, so there is no seed corpus to hand them and no +dictionary to splice from, and raw bytes almost never clear the command header. +That would waste the interesting half of what they test - not whether a +tampered blob is rejected, but what the TPM does while running on one that +wasn't. Both take commands as a `Known(u8)` index into `known_commands()` or +`Raw(Vec)` bytes, so the fuzzer can issue something real without giving up +the ability to send garbage. + +A handful of commands stay out of reach by construction, and are not worth +hand-holding: `TPM2_VerifySignature` and `TPM2_PolicyAuthorize` need a real +signature or ticket over TPM-generated data, `TPM2_NV_ChangeAuth` needs an +ADMIN-role policy session, `TPM2_PP_Commands` needs physical presence asserted, +and `TPM2_SignSequenceStart` needs an opaque `TPM2B_SIGNATURE_CTX`. + +Note that all four targets share this snapshot, so the effect is not limited to +the command targets: `fuzz_restore_state` now patches a blob that has objects +and sessions in it, and the blob `fuzz_nvmem` corrupts has real NV entries +rather than only what manufacturing wrote. + ## Determinism Replaying a crash has to reproduce it, so every input the TPM sees other than diff --git a/fuzz/fuzz_targets/fuzz_nvmem.rs b/fuzz/fuzz_targets/fuzz_nvmem.rs index c1366b8..643a8fd 100644 --- a/fuzz/fuzz_targets/fuzz_nvmem.rs +++ b/fuzz/fuzz_targets/fuzz_nvmem.rs @@ -24,8 +24,11 @@ use arbitrary::Arbitrary; use libfuzzer_sys::fuzz_target; use ms_tcg_tpm_sys_fuzz::Patch; +use ms_tcg_tpm_sys_fuzz::TPM2_SHUTDOWN_STATE; use ms_tcg_tpm_sys_fuzz::TPM2_STARTUP_CLEAR; +use ms_tcg_tpm_sys_fuzz::TPM2_STARTUP_STATE; use ms_tcg_tpm_sys_fuzz::baseline_nvmem; +use ms_tcg_tpm_sys_fuzz::known_commands; use ms_tcg_tpm_sys_fuzz::split_commands; use ms_tcg_tpm_sys_fuzz::with_tpm; @@ -33,12 +36,29 @@ use ms_tcg_tpm_sys_fuzz::with_tpm; /// executions-per-second up. const MAX_COMMANDS: usize = 8; +/// A command to run against the TPM that came up on the corrupted blob. +#[derive(Arbitrary, Debug)] +enum Command { + /// One of the harness' well formed commands. This target has no seed + /// corpus and no dictionary, so without these it rarely gets a command + /// past the header and never sees what a tampered blob does to a TPM that + /// is actually running. + Known(u8), + /// Fuzzer supplied bytes, split on their declared command sizes. + Raw(Vec), +} + #[derive(Arbitrary, Debug)] struct Input { + /// Whether to shut down orderly before the power cycle, and come back up + /// with `TPM_SU_STATE`. Resuming reads far more out of the blob than a + /// clear start does, but only makes sense against a blob that claims an + /// orderly shutdown - which the patches below are free to lie about. + resume: bool, /// Corruption to apply to the nvmem blob. patches: Vec, /// Commands to run against the TPM that comes up on the corrupted blob. - commands: Vec, + commands: Vec, } fuzz_target!(|input: Input| { @@ -46,6 +66,12 @@ fuzz_target!(|input: Input| { Patch::apply_all(&mut nvmem, &input.patches); with_tpm(|tpm| { + if input.resume { + // Has to happen before the power cycle, on the pristine TPM, so + // that the state being resumed onto is one the TPM really wrote. + let _ = tpm.execute_command(&mut TPM2_SHUTDOWN_STATE.to_vec()); + } + // Rejecting a blob outright is a perfectly good outcome. if tpm.reset(Some(&nvmem)).is_err() { return; @@ -53,10 +79,29 @@ fuzz_target!(|input: Input| { // Start the TPM up before anything else; that's where the bulk of the // nvmem is parsed. - let mut commands = vec![TPM2_STARTUP_CLEAR.to_vec()]; - commands.append(&mut split_commands(&input.commands, MAX_COMMANDS)); + let startup = if input.resume { + TPM2_STARTUP_STATE + } else { + TPM2_STARTUP_CLEAR + }; + + let mut commands = vec![startup.to_vec()]; + for command in &input.commands { + match command { + Command::Known(index) => { + let known = known_commands(); + commands.push(known[*index as usize % known.len()].to_vec()); + } + Command::Raw(bytes) => { + commands.append(&mut split_commands(bytes, MAX_COMMANDS)); + } + } + if commands.len() >= MAX_COMMANDS { + break; + } + } - for command in &mut commands { + for command in commands.iter_mut().take(MAX_COMMANDS) { let _ = tpm.execute_command(command); } }); diff --git a/fuzz/fuzz_targets/fuzz_restore_state.rs b/fuzz/fuzz_targets/fuzz_restore_state.rs index 571dd96..35e9974 100644 --- a/fuzz/fuzz_targets/fuzz_restore_state.rs +++ b/fuzz/fuzz_targets/fuzz_restore_state.rs @@ -16,6 +16,7 @@ use arbitrary::Arbitrary; use libfuzzer_sys::fuzz_target; use ms_tcg_tpm_sys_fuzz::Patch; +use ms_tcg_tpm_sys_fuzz::known_commands; use ms_tcg_tpm_sys_fuzz::split_commands; use ms_tcg_tpm_sys_fuzz::with_tpm; @@ -23,6 +24,37 @@ use ms_tcg_tpm_sys_fuzz::with_tpm; /// executions-per-second up. const MAX_COMMANDS: usize = 4; +/// A command to run against the TPM the blob restored. +#[derive(Arbitrary, Debug)] +enum Command { + /// One of the harness' well formed commands. Like `fuzz_nvmem`, this + /// target is driven by `arbitrary` rather than a seed corpus, so raw bytes + /// alone leave the restored TPM almost untouched. + Known(u8), + /// Fuzzer supplied bytes, split on their declared command sizes. + Raw(Vec), +} + +impl Command { + fn expand(commands: &[Command]) -> Vec> { + let mut out = Vec::new(); + for command in commands { + match command { + Command::Known(index) => { + let known = known_commands(); + out.push(known[*index as usize % known.len()].to_vec()); + } + Command::Raw(bytes) => out.append(&mut split_commands(bytes, MAX_COMMANDS)), + } + if out.len() >= MAX_COMMANDS { + break; + } + } + out.truncate(MAX_COMMANDS); + out + } +} + #[derive(Arbitrary, Debug)] enum Input { /// Restore an arbitrary blob. @@ -30,14 +62,14 @@ enum Input { /// The blob to restore. blob: Vec, /// Commands to run afterwards, if the restore succeeded. - commands: Vec, + commands: Vec, }, /// Restore a corrupted version of a blob the TPM actually saved. Patched { /// Corruption to apply to the saved state. patches: Vec, /// Commands to run afterwards, if the restore succeeded. - commands: Vec, + commands: Vec, }, } @@ -59,7 +91,7 @@ fuzz_target!(|input: Input| { // The restore claimed the state was good, so the TPM has to be able to // keep running on it, and to save it back out. - for command in &mut split_commands(commands, MAX_COMMANDS) { + for command in &mut Command::expand(commands) { let _ = tpm.execute_command(command); } diff --git a/fuzz/fuzz_targets/fuzz_tpm_session.rs b/fuzz/fuzz_targets/fuzz_tpm_session.rs index ad0a4a3..4cd477f 100644 --- a/fuzz/fuzz_targets/fuzz_tpm_session.rs +++ b/fuzz/fuzz_targets/fuzz_tpm_session.rs @@ -16,31 +16,89 @@ use arbitrary::Arbitrary; use libfuzzer_sys::fuzz_target; use ms_tcg_tpm_sys::Locality; +use ms_tcg_tpm_sys_fuzz::KNOWN_HANDLES; +use ms_tcg_tpm_sys_fuzz::PASSWORD_SESSION; use ms_tcg_tpm_sys_fuzz::TPM_CC_FIRST; -use ms_tcg_tpm_sys_fuzz::TPM_ST_NO_SESSIONS; -use ms_tcg_tpm_sys_fuzz::TPM_ST_SESSIONS; -use ms_tcg_tpm_sys_fuzz::build_command; +use ms_tcg_tpm_sys_fuzz::build_structured_command; +use ms_tcg_tpm_sys_fuzz::canned_commands; use ms_tcg_tpm_sys_fuzz::with_tpm; /// Caps how much work a single input can ask for, keeping the fuzzer's /// executions-per-second up. const MAX_OPS: usize = 24; +/// Caps the handle area. `MAX_HANDLE_NUM` is 3 in this profile, so anything +/// beyond that is parameter bytes rather than a handle. +const MAX_HANDLES: usize = 3; + +/// A handle to place in a command's handle area. +#[derive(Arbitrary, Debug)] +enum Handle { + /// One of the handles that actually names something, selected modulo + /// [`KNOWN_HANDLES`]. + Known(u8), + /// An arbitrary handle, for the handle validation itself. + Raw(u32), +} + +impl Handle { + fn resolve(&self) -> u32 { + match self { + Handle::Known(index) => KNOWN_HANDLES[*index as usize % KNOWN_HANDLES.len()], + Handle::Raw(handle) => *handle, + } + } +} + +/// The authorization area to attach to a command. +#[derive(Arbitrary, Debug)] +enum Auth { + /// No authorization area, tagging the command `TPM_ST_NO_SESSIONS`. + None, + /// One to three empty password sessions, which is what the great majority + /// of commands are asking for. + Password(u8), + /// A fuzzer supplied authorization area, to exercise the session parser + /// rather than the command behind it. + Raw(Vec), +} + +impl Auth { + fn build(&self) -> Option> { + match self { + Auth::None => None, + Auth::Password(count) => { + let count = 1 + *count as usize % 3; + Some(PASSWORD_SESSION.repeat(count)) + } + Auth::Raw(bytes) => Some(bytes.clone()), + } + } +} + #[derive(Arbitrary, Debug)] enum Op { - /// Dispatch a command with a well formed header and a fuzzer controlled - /// body (handles, authorization area, and parameters). + /// Dispatch a command with a well formed header, a handle area, and an + /// authorization area, leaving the fuzzer to drive the parameters. Command { - /// Selects between `TPM_ST_SESSIONS` and `TPM_ST_NO_SESSIONS`. - sessions: bool, /// Offset from `TPM_CC_FIRST`, which covers every implemented command /// code, plus a margin of unimplemented ones. code_offset: u8, - /// Everything after the command header. - body: Vec, + /// The command's handle area. + handles: Vec, + /// The command's authorization area. + auth: Auth, + /// Everything after the authorization area. + params: Vec, }, /// Dispatch raw bytes, header and all. Raw(Vec), + /// Dispatch one of the commands the harness built out of data the TPM + /// itself produced - a `TPM2_Load` of a real private blob, or a + /// `TPM2_ContextLoad` of a real saved context. Neither is reachable + /// otherwise, and a loaded child object is what most of the remaining + /// object commands are waiting on. + Canned(u8), /// Dispatch raw bytes through the unchecked entry point, skipping the /// wrapper's request size validation. RawUnchecked(Vec), @@ -65,23 +123,30 @@ fuzz_target!(|ops: Vec| { for op in ops.iter().take(MAX_OPS) { match op { Op::Command { - sessions, code_offset, - body, + handles, + auth, + params, } => { - let tag = if *sessions { - TPM_ST_SESSIONS - } else { - TPM_ST_NO_SESSIONS - }; + let handles: Vec = handles + .iter() + .take(MAX_HANDLES) + .map(Handle::resolve) + .collect(); let code = TPM_CC_FIRST + *code_offset as u32; - let mut command = build_command(tag, code, body); + let mut command = + build_structured_command(code, &handles, auth.build().as_deref(), params); let _ = tpm.execute_command(&mut command); } Op::Raw(bytes) => { let mut command = bytes.clone(); let _ = tpm.execute_command(&mut command); } + Op::Canned(index) => { + let canned = canned_commands(); + let mut command = canned[*index as usize % canned.len()].clone(); + let _ = tpm.execute_command(&mut command); + } Op::RawUnchecked(bytes) => { let mut command = bytes.clone(); tpm.execute_command_unchecked(&mut command); diff --git a/fuzz/seed_corpus/fuzz_tpm/certify.bin b/fuzz/seed_corpus/fuzz_tpm/certify.bin new file mode 100644 index 0000000000000000000000000000000000000000..9655c2842100cc5508436fba0cf90066903f1948 GIT binary patch literal 48 ncmZo*Vqjn}U|?YMXaF)m7zl(M7#KLgEI5OKsW62>f`J9F literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/clock_rate.bin b/fuzz/seed_corpus/fuzz_tpm/clock_rate.bin new file mode 100644 index 0000000000000000000000000000000000000000..b3ac66f21d8f9520e51de0381647589383ca499b GIT binary patch literal 28 ccmZo*Vqjp9VPIf1Z~ziOHYbqg1kykr030jpF literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/clock_set.bin b/fuzz/seed_corpus/fuzz_tpm/clock_set.bin new file mode 100644 index 0000000000000000000000000000000000000000..3bdac62d4d081bd599bb84f642dcae35bc0c39f9 GIT binary patch literal 35 fcmZo*VqjoUW?*2{Z~ziOHYbqg1kz9-z`y_iCK3U1 literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/commit.bin b/fuzz/seed_corpus/fuzz_tpm/commit.bin new file mode 100644 index 0000000000000000000000000000000000000000..6f7a50860c7223903d37d8b5db7d0653ccee0c23 GIT binary patch literal 134 zcmZo*VqjosV_;zHZU8cXfYSkp!7K(B1_cI2CT12^Hg*n9E^Z!PK7Ii~Az=|wF>wh= RDQOv5Ie8*fFcuS51OSG-2~Pk3 literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/commit2.bin b/fuzz/seed_corpus/fuzz_tpm/commit2.bin new file mode 100644 index 0000000000000000000000000000000000000000..b1929500f13597934453f3cd19bda635bfb0b6f6 GIT binary patch literal 137 zcmZo*VqjqCWME+IZU8cXfYSkp!7K(B1_cI2CT12^Hg*n9E^Z!PK7Ii~Az=|wF>wh= UDQOv5Ie8*fuoP#e=Mh#10Lv{3lK=n! literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/context_save.bin b/fuzz/seed_corpus/fuzz_tpm/context_save.bin new file mode 100644 index 0000000000000000000000000000000000000000..62c1479d6d6a716a1c4f292ca631f22442acf5ec GIT binary patch literal 14 TcmZo*WME+6V_;xRY5+0-3yA@9 literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/create.bin b/fuzz/seed_corpus/fuzz_tpm/create.bin new file mode 100644 index 0000000000000000000000000000000000000000..876f9fd48fd4185d62d32cbbe08a93b302bb5d5b GIT binary patch literal 63 ucmZo*VqjpfXJB9qZU7QMHm3s+18E>&0h3}3$_(56lo_~z>>?mf00^0Z7zhA%>jM%1 literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/create_primary.bin b/fuzz/seed_corpus/fuzz_tpm/create_primary.bin new file mode 100644 index 0000000000000000000000000000000000000000..ee9bc26372564cfaac8e552f2ab3394f22197526 GIT binary patch literal 63 ucmZo*VqjpfXJBA7bN~`SHYbqg1kymj0w%>6lo_~z>>?mf00^0Z7!CkwGXi)3 literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/createprimary_sha384.bin b/fuzz/seed_corpus/fuzz_tpm/createprimary_sha384.bin new file mode 100644 index 0000000000000000000000000000000000000000..4b962a3971fef4100a5550222876150a2888726a GIT binary patch literal 63 ucmZo*VqjpfXJBA7bN~`SHYbqg1kymj0w%>6lo@z{>>?mf00^0Z7!CkwOagfT literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/da_lock_reset.bin b/fuzz/seed_corpus/fuzz_tpm/da_lock_reset.bin new file mode 100644 index 0000000000000000000000000000000000000000..8094f8ad1c5d7500f35d5269685200629961cf89 GIT binary patch literal 27 ecmZo*Vqjp9W?*2nbYNiM0K<`*Cg05zZkcK`qY literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/decapsulate.bin b/fuzz/seed_corpus/fuzz_tpm/decapsulate.bin new file mode 100644 index 0000000000000000000000000000000000000000..c3e495359f5d2d3e41c100ef3bb824282c154bff GIT binary patch literal 61 zcmZo*VqjpfWnf@j(Ewxs0jC2HgINp;42(?7EUawo9GqO-JiL7T0)j%qBBEmA5|UEV IGO}{=0FQ literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/ecc_decrypt2.bin b/fuzz/seed_corpus/fuzz_tpm/ecc_decrypt2.bin new file mode 100644 index 0000000000000000000000000000000000000000..0aed782e461e4040a017fdd9fd0670826c3f55d8 GIT binary patch literal 141 zcmZo*VqjqCWnf^O)c_=bY)%It2GT&_!l1yw$i&RT%Er#Y$;HjX%f~MuC?qT*Dkd%= YDJ3l3yuMF literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/ecdh_zgen.bin b/fuzz/seed_corpus/fuzz_tpm/ecdh_zgen.bin new file mode 100644 index 0000000000000000000000000000000000000000..3f2b030e3041afc4c15be247b3d2557ba54d247f GIT binary patch literal 97 zcmZo*VqjoMWME(nX#f&HHm3s+18E>|VNhUTWMXDvWn<^yMC+6cQE@6%&_` Nl#-T_m6Io|0sulO25wh= MDQOv5IeEe=072CTZvX%Q literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/encryptdecrypt.bin b/fuzz/seed_corpus/fuzz_tpm/encryptdecrypt.bin new file mode 100644 index 0000000000000000000000000000000000000000..e934914e78fd95dcf55e5928f3bdd05281566da1 GIT binary patch literal 66 lcmZo*Vqjo!VqjoQX#g^SfYSkp!7K&_X9fW%3qfIs004kF0$%_C literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/encryptdecrypt2.bin b/fuzz/seed_corpus/fuzz_tpm/encryptdecrypt2.bin new file mode 100644 index 0000000000000000000000000000000000000000..a41e4efb326e37322688852e949ceccd9bf91cd4 GIT binary patch literal 66 jcmZo*Vqjo!VqjpL+yG<%0jC2HgINp$5DJ-e#uNkqhAIM% literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/event_seq_complete.bin b/fuzz/seed_corpus/fuzz_tpm/event_seq_complete.bin new file mode 100644 index 0000000000000000000000000000000000000000..10ea20a604cb644fc4503ead7dcf67d11e2e16cf GIT binary patch literal 60 ycmZo*WME+6V_;xx1JW!FOh8f($ZG}C3=IqnAij_T0|O_JW?+Cb7+6vgOA-N)Cj}J% literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/flush_context.bin b/fuzz/seed_corpus/fuzz_tpm/flush_context.bin new file mode 100644 index 0000000000000000000000000000000000000000..b4de2c4dbe28ef5270236340aa5f5d732c221229 GIT binary patch literal 14 TcmZo*WME+6V_;xRZ2&R=3zz|R literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/get_cmd_audit.bin b/fuzz/seed_corpus/fuzz_tpm/get_cmd_audit.bin new file mode 100644 index 0000000000000000000000000000000000000000..da2c23847623147cf71be7e3860f1f2376bd64d9 GIT binary patch literal 48 pcmZo*Vqjn}U|?W0c3@!OZUAzCK*)iCffLMvGZ>f(Qy3%|xB*vs15f|} literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/get_session_audit.bin b/fuzz/seed_corpus/fuzz_tpm/get_session_audit.bin new file mode 100644 index 0000000000000000000000000000000000000000..ff29edfbf7eca6a06b272c18a23a08dc2f2b46e0 GIT binary patch literal 52 rcmZo*Vqjn}VPIhNbzorNZUAyX0ze?-z`(!>X2BT@Oob^75)9k`W1s_Y literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/get_time.bin b/fuzz/seed_corpus/fuzz_tpm/get_time.bin new file mode 100644 index 0000000000000000000000000000000000000000..ff40c707e4cccd33f07993ad8e128794e78c9061 GIT binary patch literal 48 pcmZo*Vqjn}U|?YMabRHJZUAzCK*)iCffLMvGZ>f(Qy3%|xB**918D#N literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/hash.bin b/fuzz/seed_corpus/fuzz_tpm/hash.bin new file mode 100644 index 0000000000000000000000000000000000000000..c73355770e9b48716d177796b4c4a9389b4803f7 GIT binary patch literal 21 ccmZo*WME(rWnf^eWnfNBN@n18U|?Ve032)rkN^Mx literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/hash_sequence.bin b/fuzz/seed_corpus/fuzz_tpm/hash_sequence.bin new file mode 100644 index 0000000000000000000000000000000000000000..661f8f4c85876806207ba80724872babf60d3d97 GIT binary patch literal 82 zcmZo*WME+6V_;xx1Jc|LOh8fr$ct%UU;wc>9e@}}0|9elQZiUx1t@RVfGMAnn&!a3 GzzzT@4hK*G literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/hash_sha1.bin b/fuzz/seed_corpus/fuzz_tpm/hash_sha1.bin new file mode 100644 index 0000000000000000000000000000000000000000..b8740ed87528d6ebfb00aa20e05b7d1009aff912 GIT binary patch literal 21 ccmZo*WME(rWnf^eWnfNBN@iejU|?Ve031mIi2wiq literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/hash_sha384.bin b/fuzz/seed_corpus/fuzz_tpm/hash_sha384.bin new file mode 100644 index 0000000000000000000000000000000000000000..7066f5c9fc644b8c8861bb2578a5ba9c6c7fc67d GIT binary patch literal 21 ccmZo*WME(rWnf^eWnfNBN@n13U|?Ve032}wkpKVy literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/hash_sha512.bin b/fuzz/seed_corpus/fuzz_tpm/hash_sha512.bin new file mode 100644 index 0000000000000000000000000000000000000000..3a667e76e4d12c6e5512bc9055f3c6cc9e2adff2 GIT binary patch literal 21 ccmZo*WME(rWnf^eWnfNBN@n17U|?Ve033D#k^lez literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/hierarchy_changeauth.bin b/fuzz/seed_corpus/fuzz_tpm/hierarchy_changeauth.bin new file mode 100644 index 0000000000000000000000000000000000000000..9cd06e6f74d3c7356edefaff5b7787a940adb959 GIT binary patch literal 29 ccmZo*Vqjp9Wnf^`bN~`SHYbqg1kxY?038|uT>t<8 literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/load_external.bin b/fuzz/seed_corpus/fuzz_tpm/load_external.bin new file mode 100644 index 0000000000000000000000000000000000000000..d745d31cdd4f05f49067600139b0d45a56457d8e GIT binary patch literal 40 ocmZo*WME*>U|?WO2hw5;$_(5LEDS|Jwg3<^12GUdFfgzK06AO&MgRZ+ literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/make_credential.bin b/fuzz/seed_corpus/fuzz_tpm/make_credential.bin new file mode 100644 index 0000000000000000000000000000000000000000..589ef6ad7c98641ea575a1c001d61eb768e7e7e2 GIT binary patch literal 84 zcmZo*WME(jVPIg)XaEun3Ji=)%q*;I>>Qk2+&sK|`~reP!Xlz#;u4Zl(lWAg@(fB0 I+=SEs00+JW?EnA( literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/mldsa_certify.bin b/fuzz/seed_corpus/fuzz_tpm/mldsa_certify.bin new file mode 100644 index 0000000000000000000000000000000000000000..ac8c2a263be925e160e1afec699f7882af7bccc4 GIT binary patch literal 46 ocmZo*Vqjp D6kiAr literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/mldsa_verify_digest.bin b/fuzz/seed_corpus/fuzz_tpm/mldsa_verify_digest.bin new file mode 100644 index 0000000000000000000000000000000000000000..2a5e42ee1e4cde3c56372e089415cd55cf727932 GIT binary patch literal 131 zcmZo*VqjosW?*1k+Q`5FVsknGF^~oW1qMbYW)@a9b`DN1ZXRAfegQ!tVG&U=aS2H& RX&G5Ld4`1y4p6mZQvejy1#$oY literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/mldsa_verify_seq.bin b/fuzz/seed_corpus/fuzz_tpm/mldsa_verify_seq.bin new file mode 100644 index 0000000000000000000000000000000000000000..ea192a3a5495fbaff4a46b7a2b0ef1c4ef5d02a7 GIT binary patch literal 141 zcmZo*VqjoUWME)i*~q{EVsknGF_4CV29RJ5P{HB`paQ5OAqSWuID=s!1CUJ_0052} B20s7* literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_certify.bin b/fuzz/seed_corpus/fuzz_tpm/nv_certify.bin new file mode 100644 index 0000000000000000000000000000000000000000..86c2a89204798451de25bc64ce38f58d4882c6d0 GIT binary patch literal 56 vcmZo*VqjpfU|?WuX#g@DfDA?;1~P?!Y)&8z7KAVumpF literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_increment.bin b/fuzz/seed_corpus/fuzz_tpm/nv_increment.bin new file mode 100644 index 0000000000000000000000000000000000000000..a0ce87b738c1fa491d0a22955bc1d1af162bb362 GIT binary patch literal 31 fcmZo*Vqjp9XJBA7aR3sG3=AMXCy>nvq!}0hAKw9O literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_read.bin b/fuzz/seed_corpus/fuzz_tpm/nv_read.bin new file mode 100644 index 0000000000000000000000000000000000000000..0baebbbe8e6226228d5a4a2d80a26e385760d809 GIT binary patch literal 35 hcmZo*VqjoUW?*3Sa{v;IKn&z_0@<8E8VD2^7yu~!0jdB1 literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_readlock.bin b/fuzz/seed_corpus/fuzz_tpm/nv_readlock.bin new file mode 100644 index 0000000000000000000000000000000000000000..43af978bef532204d8e68b0ff0d1682de93ead69 GIT binary patch literal 98 zcmZo*Vqjo!W?*16cK{NM3=FJ5J|~dP38aBQfq{{UnT3^&or9B$n}?T=UqDbuSVUAz ZTtZSxT1Hk*o`InOWCW0A^mo8!1ON_01u*~s literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_setbits.bin b/fuzz/seed_corpus/fuzz_tpm/nv_setbits.bin new file mode 100644 index 0000000000000000000000000000000000000000..a88688b05d890f6d25483f64315ae63698bba364 GIT binary patch literal 39 hcmZo*VqjoUXJBA7bpR5K3=GUbJ|~dP38dkG9{?nvq!}0h9#a8Z literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_write.bin b/fuzz/seed_corpus/fuzz_tpm/nv_write.bin new file mode 100644 index 0000000000000000000000000000000000000000..2040574523bc6643041d0840e66b8d747ee0f638 GIT binary patch literal 43 pcmZo*Vqjp literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_writelock.bin b/fuzz/seed_corpus/fuzz_tpm/nv_writelock.bin new file mode 100644 index 0000000000000000000000000000000000000000..660d8522fd63a9825176ba1bb54ebad8ea354540 GIT binary patch literal 31 gcmZo*Vqjp9XJBBoZ~zjF3=FJ5J|~dP38Wbq03cxjbN~PV literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/object_changeauth.bin b/fuzz/seed_corpus/fuzz_tpm/object_changeauth.bin new file mode 100644 index 0000000000000000000000000000000000000000..f8b89e58c184bd48ed377ccf8859e0c5f24d1b85 GIT binary patch literal 36 icmZo*VqjoUVPIekXaF*Rm=Q>EIsh?{1_I{1)N%kmW&@Z2 literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/pcr_allocate.bin b/fuzz/seed_corpus/fuzz_tpm/pcr_allocate.bin new file mode 100644 index 0000000000000000000000000000000000000000..b9d3061c29d34310b0cc3ff45ef595ecd8f8fa1e GIT binary patch literal 37 lcmZo*VqjoUWnf^`c3@!O0kS!PG$)V-14agJ=Kufy0{}3F1bzSj literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/pcr_event.bin b/fuzz/seed_corpus/fuzz_tpm/pcr_event.bin new file mode 100644 index 0000000000000000000000000000000000000000..8d8277926724f74151725ca7fd5830cca42a01db GIT binary patch literal 34 gcmZo*VqjoUVqjpj0n#AA=>WuF76WT)S!!Mh04A^lTL1t6 literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/pcr_setauthpolicy.bin b/fuzz/seed_corpus/fuzz_tpm/pcr_setauthpolicy.bin new file mode 100644 index 0000000000000000000000000000000000000000..5bcc04d23b3f35e13b1afad5f1fb86fb3eab5008 GIT binary patch literal 67 zcmZo*Vqjo!W?*2{abRHJ0kS!PG$)V-0tE&}CT12^Hg*n9E^Z!PK7Ii~Az=|wF>wh= ODQOv5Ie7+dkTL*+BLl1e literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/pcr_setauthvalue.bin b/fuzz/seed_corpus/fuzz_tpm/pcr_setauthvalue.bin new file mode 100644 index 0000000000000000000000000000000000000000..bae6fecd761ff5874eaaed17f1d2bb35ee5154f3 GIT binary patch literal 29 bcmZo*Vqjp9Wnf@z2GStF=>WuF76StSA5sB! literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_authorizenv.bin b/fuzz/seed_corpus/fuzz_tpm/policy_authorizenv.bin new file mode 100644 index 0000000000000000000000000000000000000000..ce8ea3757c741b622e3f01ae6cfbe33ebd6574e9 GIT binary patch literal 35 icmZo*VqjoUW?*2P>Qk2+&sK|`~reP!Xlz#;u4Zl(lWAg@&HR- B12q5u literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_dupselect.bin b/fuzz/seed_corpus/fuzz_tpm/policy_dupselect.bin new file mode 100644 index 0000000000000000000000000000000000000000..fec423f0e6863438f7ce90c9cd6fca4383b03572 GIT binary patch literal 83 zcmZo*WME(jW?*3KU>Qk2+&sK|`~reP!Xlz#;u4Zl(lWAg@<{9 literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_namehash.bin b/fuzz/seed_corpus/fuzz_tpm/policy_namehash.bin new file mode 100644 index 0000000000000000000000000000000000000000..644fd87b375b60fc0e5da423783aaec64dabdd8f GIT binary patch literal 48 zcmZo*WME)0U|?V@U>Qk2+&sK|`~reP!Xlz#;u4Zl(lWAg@&HS$ B12+Hw literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_nv.bin b/fuzz/seed_corpus/fuzz_tpm/policy_nv.bin new file mode 100644 index 0000000000000000000000000000000000000000..e4b23bbfd925c055bfc193265847746e6e7d4b49 GIT binary patch literal 49 lcmZo*Vqjn}WME+QbN~{JK+FupKrv1rpA$#}0SAPFkN`ho0iOT> literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_nvwritten.bin b/fuzz/seed_corpus/fuzz_tpm/policy_nvwritten.bin new file mode 100644 index 0000000000000000000000000000000000000000..f8184db96c9cb659916e3793abe41aa8defb4ad5 GIT binary patch literal 15 UcmZo*WME+6XJBCLX9g0C01Ku8CIA2c literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_or.bin b/fuzz/seed_corpus/fuzz_tpm/policy_or.bin new file mode 100644 index 0000000000000000000000000000000000000000..a17fce1ae4c72d3637e150a6f5c8f8a4ccd731ff GIT binary patch literal 86 zcmZo*WME(jV_;w`WCjvIHWPya10xeN3o9Et2PYRd4=*3TfS{1Dh^Uyjgrt3*G^Q literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_certify.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_certify.bin new file mode 100644 index 0000000000000000000000000000000000000000..7fae9bad887d89f95ea5cd6c740a76a60d436280 GIT binary patch literal 48 qcmZo*Vqjn}U|?YMXaF)AfeaueQ5m7O52}vnw M8Cf}b1_1^J0Fa6V5C8xG literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_decrypt_oaep.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_decrypt_oaep.bin new file mode 100644 index 0000000000000000000000000000000000000000..531ac9582459ef1b20ca63b2f4104e6731807d6c GIT binary patch literal 163 mcmZo*Vqjoc%)r1H*$5>Qk2+&sK|`~reP!Xlz#;u4Zl(lWAg@(fB0 I+=SEs00;;L?f?J) literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_quote.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_quote.bin new file mode 100644 index 0000000000000000000000000000000000000000..5a5c26029645931c796dce84c1ac32f7c6c16494 GIT binary patch literal 45 qcmZo*Vqjp&DokMzVc-U`7#X;kfsz1I!2;s| literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_readpublic.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_readpublic.bin new file mode 100644 index 0000000000000000000000000000000000000000..ea6c7b998bdcc09866cfb707967aacd60109b7f6 GIT binary patch literal 14 TcmZo*WME+6V_;w`ZUhnl3*!NX literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_sign.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_sign.bin new file mode 100644 index 0000000000000000000000000000000000000000..b69356309437cbebfa0e00cf05255793008ea23a GIT binary patch literal 73 zcmZo*VqjqKWME*7Z3GfPHm3s+18E>oU|?ioW?^Mx=iubx=Hcbz7Z4N@77-N_mync_ VmXVc{XAoiFZcuSxU|?rp006ti1sDJT literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_sign_pss.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_sign_pss.bin new file mode 100644 index 0000000000000000000000000000000000000000..abf58576619c0fd343154a5ad9df4ca1122f7c8a GIT binary patch literal 73 zcmZo*VqjqKWME*7Z3GfPHm3s+18E>oU|?ioW?^Mx=iubx=Hcbz7Z4N@77-N_mync_ VmXVc{XAooHZcuSxU|?rp006t&1sVVV literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/seq_sha384.bin b/fuzz/seed_corpus/fuzz_tpm/seq_sha384.bin new file mode 100644 index 0000000000000000000000000000000000000000..941dda01e096f2586044c016079799cd5c0e8f98 GIT binary patch literal 79 zcmZo*WME+6V_;xx1JXPVOh8fr$ct%UU;wc>9e@}}0|9elQZiUx5h!ohfFaM|z`(!` E01!t88~^|S literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/set_algorithm_set.bin b/fuzz/seed_corpus/fuzz_tpm/set_algorithm_set.bin new file mode 100644 index 0000000000000000000000000000000000000000..cb76d8ec5e1ad337d4cbc7e9f6c6e44861392655 GIT binary patch literal 31 dcmZo*Vqjp9XJBBocVJ-P0kS!PG$)XT001Jg0e}Di literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/set_cc_audit.bin b/fuzz/seed_corpus/fuzz_tpm/set_cc_audit.bin new file mode 100644 index 0000000000000000000000000000000000000000..7b5301d7983d6e48d97ae1926508a9bddae28404 GIT binary patch literal 41 kcmZo*Vqjpx literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/sign.bin b/fuzz/seed_corpus/fuzz_tpm/sign.bin new file mode 100644 index 0000000000000000000000000000000000000000..5ae4c405b901b52ddd3412a025462e37558714bd GIT binary patch literal 73 zcmZo*VqjqKWME*7Z2&TWfYSkp!7K&^21X`k7FITP4o)s^9$r3v0YM>Q5m7O52}vnw T8Cf}b1_=i41{I(Nb_NCjyLJT{ literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/test_parms.bin b/fuzz/seed_corpus/fuzz_tpm/test_parms.bin new file mode 100644 index 0000000000000000000000000000000000000000..5d24621b34cfe94e27d14bf233c0c9e5e96653ce GIT binary patch literal 18 ZcmZo*WME(rVqjqGVo+sZV`yM-1^^M&0u2BF literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/unseal.bin b/fuzz/seed_corpus/fuzz_tpm/unseal.bin new file mode 100644 index 0000000000000000000000000000000000000000..808e1969885e49aa358815de819474056d681ec3 GIT binary patch literal 27 acmZo*Vqjp9W?*29YXCBUfYSkp!7KnGAOX(+ literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/zgen_2phase2.bin b/fuzz/seed_corpus/fuzz_tpm/zgen_2phase2.bin new file mode 100644 index 0000000000000000000000000000000000000000..83ff87d84c3376e4b7bbac527e87ef31117a398b GIT binary patch literal 171 zcmZo*Vqjoc&A`Ce+W=$$0jC2HgINqN3 [u8; 45] { + let i = index.to_be_bytes(); + let a = attributes.to_be_bytes(); + let d = data_size.to_be_bytes(); + [ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x01, 0x2a, + 0x40, 0x00, 0x00, 0x01, // authHandle = TPM_RH_OWNER + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, // auth (empty) + 0x00, 0x0e, // publicInfo + i[0], i[1], i[2], i[3], // nvIndex + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + a[0], a[1], a[2], a[3], // attributes + 0x00, 0x00, // authPolicy (empty) + d[0], d[1], // dataSize + ] +} + +/// Commands run once per process, after startup, to give the pristine state +/// something for the fuzzer to work with: a loaded key, a persistent copy of +/// it, a written NV index, and one session of each type. +/// +/// Each entry is a command, and the handle its response is expected to report, +/// so that a drift between these blobs and [`KNOWN_HANDLES`] fails loudly +/// rather than quietly costing coverage. +#[rustfmt::skip] +const SETUP_COMMANDS: &[(&str, &[u8], Option)] = &[ + // TPM2_CreatePrimary(TPM_RH_OWNER, ECC NIST P-256 signing key). ECC rather + // than RSA because this runs on every fuzzing process' startup path. + ("TPM2_CreatePrimary", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x01, 0x31, + 0x40, 0x00, 0x00, 0x01, // primaryHandle = TPM_RH_OWNER + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, // inSensitive (empty) + 0x00, 0x16, // inPublic + 0x00, 0x23, // type = TPM_ALG_ECC + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + 0x00, 0x04, 0x00, 0x72, // fixedTPM|fixedParent|sensitiveDataOrigin|userWithAuth|sign + 0x00, 0x00, // authPolicy (empty) + 0x00, 0x10, // symmetric = TPM_ALG_NULL + 0x00, 0x10, // scheme = TPM_ALG_NULL + 0x00, 0x03, // curveID = TPM_ECC_NIST_P256 + 0x00, 0x10, // kdf = TPM_ALG_NULL + 0x00, 0x00, 0x00, 0x00, // unique (empty x, y) + 0x00, 0x00, // outsideInfo (empty) + 0x00, 0x00, 0x00, 0x00, // creationPCR (empty) + ], Some(SEEDED_TRANSIENT)), + + // TPM2_EvictControl, to also reach the key through a persistent handle. + ("TPM2_EvictControl", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x01, 0x20, + 0x40, 0x00, 0x00, 0x01, // auth = TPM_RH_OWNER + 0x80, 0x00, 0x00, 0x00, // objectHandle + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x00, 0x00, // persistentHandle + ], None), + + // TPM2_NV_DefineSpace, an ordinary 32 byte owner/auth read-write index. + ("TPM2_NV_DefineSpace", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x01, 0x2a, + 0x40, 0x00, 0x00, 0x01, // authHandle = TPM_RH_OWNER + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, // auth (empty) + 0x00, 0x0e, // publicInfo + 0x01, 0x00, 0x00, 0x01, // nvIndex + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + 0x00, 0x06, 0x00, 0x06, // OWNERWRITE|AUTHWRITE|OWNERREAD|AUTHREAD + 0x00, 0x00, // authPolicy (empty) + 0x00, 0x20, // dataSize + ], None), + + // TPM2_NV_Write, so the index is written and has contents to read back. + ("TPM2_NV_Write", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x01, 0x37, + 0x40, 0x00, 0x00, 0x01, // authHandle = TPM_RH_OWNER + 0x01, 0x00, 0x00, 0x01, // nvIndex + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x20, // data + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x00, 0x00, // offset + ], None), + + // TPM2_CreatePrimary again, this time a restricted decryption key, which + // is what an object needs as a parent. AES-128-CFB is mandatory for a + // restricted decrypt key; a null symmetric algorithm is rejected. + ("TPM2_CreatePrimary(storage)", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x01, 0x31, + 0x40, 0x00, 0x00, 0x01, // primaryHandle = TPM_RH_OWNER + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, // inSensitive (empty) + 0x00, 0x1a, // inPublic + 0x00, 0x23, // type = TPM_ALG_ECC + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + 0x00, 0x03, 0x00, 0x72, // fixedTPM|fixedParent|sensitiveDataOrigin|userWithAuth|restricted|decrypt + 0x00, 0x00, // authPolicy (empty) + 0x00, 0x06, 0x00, 0x80, 0x00, 0x43, // symmetric = AES-128-CFB + 0x00, 0x10, // scheme = TPM_ALG_NULL + 0x00, 0x03, // curveID = TPM_ECC_NIST_P256 + 0x00, 0x10, // kdf = TPM_ALG_NULL + 0x00, 0x00, 0x00, 0x00, // unique (empty x, y) + 0x00, 0x00, // outsideInfo (empty) + 0x00, 0x00, 0x00, 0x00, // creationPCR (empty) + ], Some(SEEDED_STORAGE_PARENT)), + + // TPM2_CreatePrimary a third time, RSA rather than ECC. Nothing else here + // reaches the RSA code at all - key generation, the prime sieve and + // Miller-Rabin included - and every RSA operation needs a key to name. + // 1024 bits because this runs on every process startup and exercises the + // same generation path a larger modulus would. + ("TPM2_CreatePrimary(RSA)", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x01, 0x31, + 0x40, 0x00, 0x00, 0x01, // primaryHandle = TPM_RH_OWNER + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, // inSensitive (empty) + 0x00, 0x16, // inPublic + 0x00, 0x01, // type = TPM_ALG_RSA + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + // sign and decrypt both, so one key generation covers every RSA + // operation rather than only signing. + 0x00, 0x06, 0x00, 0x72, + 0x00, 0x00, // authPolicy (empty) + 0x00, 0x10, // symmetric = TPM_ALG_NULL + 0x00, 0x10, // scheme = TPM_ALG_NULL + 0x04, 0x00, // keyBits = 1024 + 0x00, 0x00, 0x00, 0x00, // exponent = default + 0x00, 0x00, // unique (empty) + 0x00, 0x00, // outsideInfo (empty) + 0x00, 0x00, 0x00, 0x00, // creationPCR (empty) + ], Some(0x8000_0002)), + + // Park the RSA key in NV and give the transient slot back. A persistent + // object costs nothing against `MAX_LOADED_OBJECTS`, so this buys RSA + // coverage without taking the slot the fuzzer needs for TPM2_Load. + ("TPM2_EvictControl(RSA)", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x01, 0x20, + 0x40, 0x00, 0x00, 0x01, // auth = TPM_RH_OWNER + 0x80, 0x00, 0x00, 0x02, // objectHandle + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x00, 0x01, // persistentHandle + ], None), + + ("TPM2_FlushContext(RSA)", &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x01, 0x65, + 0x80, 0x00, 0x00, 0x02, // flushHandle, a parameter rather than a handle + ], None), + + // ML-DSA, same trick again. The streaming signature commands take a + // `TPM2B_SIGNATURE_CTX`, which is an ML-DSA context: with only ECC and RSA + // keys around, none of them are reachable and the whole ML-DSA provider is + // dead code as far as the fuzzer is concerned. + ("TPM2_CreatePrimary(ML-DSA)", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x01, 0x31, + 0x40, 0x00, 0x00, 0x01, // primaryHandle = TPM_RH_OWNER + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, // inSensitive (empty) + 0x00, 0x0f, // inPublic + 0x00, 0xa1, // type = TPM_ALG_MLDSA + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + 0x00, 0x04, 0x00, 0x72, // fixedTPM|fixedParent|sensitiveDataOrigin|userWithAuth|sign + 0x00, 0x00, // authPolicy (empty) + 0x00, 0x01, // parameterSet = ML-DSA-44 + 0x00, // allowExternalMu = NO + 0x00, 0x00, // unique (empty) + 0x00, 0x00, // outsideInfo (empty) + 0x00, 0x00, 0x00, 0x00, // creationPCR (empty) + ], Some(0x8000_0002)), + + ("TPM2_EvictControl(ML-DSA)", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x01, 0x20, + 0x40, 0x00, 0x00, 0x01, // auth = TPM_RH_OWNER + 0x80, 0x00, 0x00, 0x02, // objectHandle + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x00, 0x02, // persistentHandle + ], None), + + ("TPM2_FlushContext(ML-DSA)", &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x01, 0x65, + 0x80, 0x00, 0x00, 0x02, + ], None), + + // NV indices of each type, so the commands that only work against a + // particular one have something to name. The type lives in bits 4-7 of + // TPMA_NV, and fixes the size: counters and bit fields are 8 bytes, an + // extend index is one digest. + ("TPM2_NV_DefineSpace(counter)", &nv_define(SEEDED_NV_COUNTER, 0x0006_0016, 8), None), + ("TPM2_NV_DefineSpace(bits)", &nv_define(SEEDED_NV_BITS, 0x0006_0026, 8), None), + ("TPM2_NV_DefineSpace(extend)", &nv_define(SEEDED_NV_EXTEND, 0x0006_0046, 32), None), + // READ_STCLEAR (bit 31) and WRITE_STCLEAR (bit 14), for the lock commands. + ("TPM2_NV_DefineSpace(lockable)", &nv_define(SEEDED_NV_LOCKABLE, 0x8006_4006, 32), None), + + // TPM2_StartAuthSession, unbound and unsalted, one HMAC and one policy. + ("TPM2_StartAuthSession(HMAC)", &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x01, 0x76, + 0x40, 0x00, 0x00, 0x07, // tpmKey = TPM_RH_NULL + 0x40, 0x00, 0x00, 0x07, // bind = TPM_RH_NULL + 0x00, 0x20, // nonceCaller + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0x00, // encryptedSalt (empty) + 0x00, // sessionType = TPM_SE_HMAC + 0x00, 0x10, // symmetric = TPM_ALG_NULL + 0x00, 0x0b, // authHash = TPM_ALG_SHA256 + ], Some(SEEDED_HMAC_SESSION)), + + ("TPM2_StartAuthSession(policy)", &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x01, 0x76, + 0x40, 0x00, 0x00, 0x07, // tpmKey = TPM_RH_NULL + 0x40, 0x00, 0x00, 0x07, // bind = TPM_RH_NULL + 0x00, 0x20, // nonceCaller + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x00, 0x00, // encryptedSalt (empty) + 0x01, // sessionType = TPM_SE_POLICY + 0x00, 0x10, // symmetric = TPM_ALG_NULL + 0x00, 0x0b, // authHash = TPM_ALG_SHA256 + ], Some(SEEDED_POLICY_SESSION)), +]; + +/// `TPM_CC_Load` +const TPM_CC_LOAD: u32 = 0x0000_0157; +/// `TPM_CC_ContextLoad` +const TPM_CC_CONTEXT_LOAD: u32 = 0x0000_0161; + +/// `TPM2_Create` of an HMAC key under [`SEEDED_STORAGE_PARENT`]. +/// +/// Run for its output rather than its effect: the private blob it returns is +/// what makes a `TPM2_Load` possible. +#[rustfmt::skip] +const TPM2_CREATE_KEYEDHASH: &[u8] = &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x01, 0x53, + 0x80, 0x00, 0x00, 0x01, // parentHandle + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, // inSensitive (empty) + 0x00, 0x10, // inPublic + 0x00, 0x08, // type = TPM_ALG_KEYEDHASH + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + 0x00, 0x04, 0x00, 0x72, // fixedTPM|fixedParent|sensitiveDataOrigin|userWithAuth|sign + 0x00, 0x00, // authPolicy (empty) + 0x00, 0x05, 0x00, 0x0b, // scheme = HMAC-SHA256 + 0x00, 0x00, // unique (empty) + 0x00, 0x00, // outsideInfo (empty) + 0x00, 0x00, 0x00, 0x00, // creationPCR (empty) +]; + +/// `TPM2_ContextSave(SEEDED_TRANSIENT)`, run for the context blob it returns. +const TPM2_CONTEXT_SAVE_SEEDED: &[u8] = &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x01, 0x62, 0x80, 0x00, 0x00, 0x00, +]; + +/// Commands assembled at setup time out of data the TPM itself produced. +/// +/// `TPM2_Load` wants a private blob that only the TPM can have generated, and +/// `TPM2_ContextLoad` wants a saved context; a mutator will never invent +/// either, so without this those commands - and everything that needs a loaded +/// child object behind them - are unreachable. Built once per process, before +/// the snapshot, so the blobs stay valid for every iteration that rolls back +/// to it. +static CANNED_COMMANDS: OnceLock>> = OnceLock::new(); + +/// The commands described by [`CANNED_COMMANDS`], empty until a [`FuzzTpm`] +/// has been built. +pub fn canned_commands() -> &'static [Vec] { + CANNED_COMMANDS.get().map_or(&[], Vec::as_slice) +} + +/// Well formed commands that the targets which don't have a seed corpus can +/// reach for. +/// +/// `fuzz_nvmem` and `fuzz_restore_state` take `arbitrary`-encoded input, so +/// unlike `fuzz_tpm` there is no seed corpus and no dictionary to spell a real +/// command out with, and raw bytes almost never clear the header. That wastes +/// the interesting half of what those targets do: not whether a blob is +/// rejected, but what the TPM does while running on one that wasn't. +pub fn known_commands() -> &'static [&'static [u8]] { + static KNOWN: OnceLock> = OnceLock::new(); + KNOWN.get_or_init(|| { + let mut commands: Vec<&'static [u8]> = vec![ + TPM2_STARTUP_CLEAR, + TPM2_STARTUP_STATE, + TPM2_SHUTDOWN_STATE, + TPM2_SELF_TEST_FULL, + TPM2_READ_PUBLIC_SEEDED, + TPM2_SIGN_SEEDED, + TPM2_CREATE_KEYEDHASH, + TPM2_CONTEXT_SAVE_SEEDED, + ]; + commands.extend( + SETUP_COMMANDS + .iter() + // Key generation is not something to hand the fuzzer as a + // command it can call in a loop: the RSA primary alone costs + // more than everything else here put together, and the keys + // are seeded already, so replaying these buys nothing and + // costs most of the execution rate. + .filter(|(name, _, _)| !name.starts_with("TPM2_CreatePrimary")) + .map(|(_, command, _)| *command), + ); + commands + }) +} + +/// Splits a `TPM2_Create` response into its `outPrivate` and `outPublic` +/// fields, both keeping the two byte size prefix that `TPM2_Load` wants. +fn split_create_response(response: &[u8]) -> Option<(&[u8], &[u8])> { + // `TPM2_Create` is authorized, so its response is tagged TPM_ST_SESSIONS + // and the parameters start after the header's parameterSize. + let params = response.get(HEADER_SIZE + 4..)?; + + let size_at = |offset: usize| -> Option { + let size = params.get(offset..offset + 2)?; + Some(2 + u16::from_be_bytes(size.try_into().ok()?) as usize) + }; + + let private = size_at(0)?; + let public = size_at(private)?; + Some(( + params.get(..private)?, + params.get(private..private + public)?, + )) +} + +/// `TPM2_Sign(SEEDED_TRANSIENT, ECDSA-SHA256)` over a fixed digest. +/// +/// Used to settle the dictionary attack `daUsed` state, and to confirm the +/// seeded key is actually usable. +#[rustfmt::skip] +const TPM2_SIGN_SEEDED: &[u8] = &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x01, 0x5d, + 0x80, 0x00, 0x00, 0x00, // keyHandle + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x20, // digest + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x00, 0x18, 0x00, 0x0b, // inScheme = ECDSA, SHA256 + 0x80, 0x24, 0x40, 0x00, 0x00, 0x07, 0x00, 0x00, // validation = null ticket +]; + /// Seed for the entropy PRNG. Any fixed value will do; this one is arbitrary. const PRNG_SEED: u64 = 0x0123_4567_89ab_cdef; @@ -218,7 +673,71 @@ impl FuzzTpm { tpm.execute_expecting_success(TPM2_STARTUP_CLEAR, "TPM2_Startup"); tpm.execute_expecting_success(TPM2_SELF_TEST_FULL, "TPM2_SelfTest"); + for (name, command, expected_handle) in SETUP_COMMANDS { + let response = tpm.execute_expecting_success(command, name); + if let Some(expected) = expected_handle { + let handle = response + .get(HEADER_SIZE..HEADER_SIZE + 4) + .map(|h| u32::from_be_bytes(h.try_into().unwrap())); + assert_eq!( + handle, + Some(*expected), + "{name} returned {handle:#x?}, but KNOWN_HANDLES says {expected:#010x}" + ); + } + } + + // The first authorization of a dictionary-attack protected object does + // not run the command: it writes the `daUsed` state to NV and returns + // TPM_RC_RETRY (SessionProcess.c). Settle that here, or every iteration + // would spend its first authorized command on it and never reach the + // handler behind it. The second attempt has to succeed, which also + // confirms the seeded key works. + let _ = tpm.execute_command(&mut TPM2_SIGN_SEEDED.to_vec()); + tpm.execute_expecting_success(TPM2_SIGN_SEEDED, "TPM2_Sign"); + + // Has to happen before the snapshot: a saved context is only valid + // against the state it was saved from, which is the state every + // iteration rolls back to. + let create = tpm.execute_expecting_success(TPM2_CREATE_KEYEDHASH, "TPM2_Create(keyedhash)"); + let context = tpm.execute_expecting_success(TPM2_CONTEXT_SAVE_SEEDED, "TPM2_ContextSave"); + + let mut canned = Vec::new(); + if let Some((private, public)) = split_create_response(&create) { + let mut blobs = private.to_vec(); + blobs.extend_from_slice(public); + canned.push(build_structured_command( + TPM_CC_LOAD, + &[SEEDED_STORAGE_PARENT], + Some(PASSWORD_SESSION), + &blobs, + )); + } + if let Some(context) = context.get(HEADER_SIZE..) { + canned.push(build_structured_command( + TPM_CC_CONTEXT_LOAD, + &[], + None, + context, + )); + } + assert_eq!( + canned.len(), + 2, + "both canned commands should have been built" + ); + let _ = CANNED_COMMANDS.set(canned); + tpm.snapshot = tpm.platform.save_state(); + + // Everything above is only worth anything if it survives the rollback + // that starts every iteration. `s_objects` and `s_sessions` are part of + // the saved state, so it does - but silently losing the seeded handles + // would cost most of the reachable command surface, so check rather + // than assume. + tpm.rollback(); + tpm.execute_expecting_success(TPM2_READ_PUBLIC_SEEDED, "TPM2_ReadPublic"); + tpm } @@ -259,14 +778,16 @@ impl FuzzTpm { check_response(&self.response, len) } - /// Executes a command that is expected to succeed, panicking otherwise. - fn execute_expecting_success(&mut self, command: &[u8], name: &str) { + /// Executes a command that is expected to succeed, returning its response + /// and panicking otherwise. + fn execute_expecting_success(&mut self, command: &[u8], name: &str) -> Vec { let mut command = command.to_vec(); let response = self .execute_command(&mut command) .unwrap_or_else(|e| panic!("{name} should be dispatchable: {e}")); let code = response_code(response).expect("response should have a header"); assert_eq!(code, TPM_RC_SUCCESS, "{name} returned {code:#010x}"); + response.to_vec() } /// Simulates a power cycle, optionally swapping in a new nvmem blob. @@ -354,6 +875,39 @@ pub fn build_command(tag: u16, command_code: u32, body: &[u8]) -> Vec { command } +/// Builds a command out of its structural pieces: handles, an authorization +/// area, and parameters. +/// +/// Getting past a command's front door needs three things a mutator will not +/// invent on its own - a well formed header, handles that name something that +/// exists, and a valid authorization area - and only the parameters are worth +/// spending fuzzing effort on. `auth` picks the tag: `Some` is +/// `TPM_ST_SESSIONS` with `auth` as the authorization area, `None` is +/// `TPM_ST_NO_SESSIONS`. +pub fn build_structured_command( + command_code: u32, + handles: &[u32], + auth: Option<&[u8]>, + params: &[u8], +) -> Vec { + let mut body = Vec::new(); + for handle in handles { + body.extend_from_slice(&handle.to_be_bytes()); + } + + let tag = match auth { + Some(auth) => { + body.extend_from_slice(&(auth.len() as u32).to_be_bytes()); + body.extend_from_slice(auth); + TPM_ST_SESSIONS + } + None => TPM_ST_NO_SESSIONS, + }; + + body.extend_from_slice(params); + build_command(tag, command_code, &body) +} + /// Splits a byte stream into commands along the boundaries declared by each /// command's own `commandSize` field. /// diff --git a/fuzz/tpm.dict b/fuzz/tpm.dict index 9ddc696..c3cfa8f 100644 --- a/fuzz/tpm.dict +++ b/fuzz/tpm.dict @@ -107,6 +107,22 @@ handle_transient="\x80\x00\x00\x00" handle_persistent="\x81\x00\x00\x00" handle_nv_index="\x01\x00\x00\x01" +# Handles the harness seeds into the pristine state, so these name something +# that actually exists. See SETUP_COMMANDS in src/lib.rs. +seeded_transient="\x80\x00\x00\x00" +seeded_persistent="\x81\x00\x00\x00" +seeded_nv_index="\x01\x00\x00\x01" +seeded_hmac_session="\x02\x00\x00\x00" +seeded_policy_session="\x03\x00\x00\x01" + +# Authorization areas. A command tagged TPM_ST_SESSIONS is rejected before its +# handler runs unless it carries a well formed one of these, and a mutator will +# not produce one on its own. +auth_pw_session="\x40\x00\x00\x09\x00\x00\x00\x00\x00" +auth_pw_empty="\x00\x00\x00\x09\x40\x00\x00\x09\x00\x00\x00\x00\x00" +auth_pw_empty_x2="\x00\x00\x00\x12\x40\x00\x00\x09\x00\x00\x00\x00\x00\x40\x00\x00\x09\x00\x00\x00\x00\x00" +auth_pw_continue="\x00\x00\x00\x09\x40\x00\x00\x09\x00\x00\x01\x00\x00" + # Algorithm identifiers (TPM_ALG) alg_error="\x00\x00" alg_rsa="\x00\x01" From fb3f79fe4984ad5228559b98b0e6c1de83609bb7 Mon Sep 17 00:00:00 2001 From: Steven Malis Date: Thu, 27 Aug 2026 16:49:27 -0400 Subject: [PATCH 4/7] little more --- fuzz/fuzz_targets/fuzz_tpm_session.rs | 5 +++++ fuzz/src/lib.rs | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/fuzz/fuzz_targets/fuzz_tpm_session.rs b/fuzz/fuzz_targets/fuzz_tpm_session.rs index 4cd477f..5d689ef 100644 --- a/fuzz/fuzz_targets/fuzz_tpm_session.rs +++ b/fuzz/fuzz_targets/fuzz_tpm_session.rs @@ -112,6 +112,8 @@ enum Op { SetLocality(u8), /// Set or clear the cancel flag. SetCancelFlag(bool), + /// Jump the platform clock forward, in minutes. + AdvanceClock(u16), } fuzz_target!(|ops: Vec| { @@ -176,6 +178,9 @@ fuzz_target!(|ops: Vec| { Op::SetCancelFlag(enabled) => { tpm.set_cancel_flag(*enabled); } + Op::AdvanceClock(minutes) => { + tpm.advance_clock(u64::from(*minutes) * 60_000); + } } } }); diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index e6d9dd3..cb63761 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -814,6 +814,15 @@ impl FuzzTpm { pub fn set_cancel_flag(&mut self, enabled: bool) { self.platform.set_cancel_flag(enabled); } + + /// Jumps the platform clock forward. + /// + /// The clock otherwise advances a millisecond per read, so anything on a + /// realistic timeout - lockout self healing, ACT countdowns, the periodic + /// clock update that forces an NV write - is unreachable in a fuzz run. + pub fn advance_clock(&mut self, millis: u64) { + CLOCK_TICKS.fetch_add(millis, Relaxed); + } } /// Validates the invariants every TPM response is expected to uphold, and From 61d5f2223f6c0ced0dcb4320532e5eb1337cf737 Mon Sep 17 00:00:00 2001 From: Steven Malis Date: Fri, 28 Aug 2026 13:18:20 -0400 Subject: [PATCH 5/7] more --- fuzz/fuzz_targets/fuzz_tpm_session.rs | 16 +++++ fuzz/seed_corpus/fuzz_tpm/ec_ephemeral.bin | Bin 0 -> 12 bytes fuzz/seed_corpus/fuzz_tpm/ecc_parameters.bin | Bin 0 -> 12 bytes fuzz/seed_corpus/fuzz_tpm/nv_read_public2.bin | Bin 0 -> 14 bytes fuzz/seed_corpus/fuzz_tpm/pcr_reset.bin | Bin 0 -> 27 bytes .../fuzz_tpm/policy_auth_value.bin | Bin 0 -> 14 bytes .../fuzz_tpm/policy_physical_presence.bin | Bin 0 -> 14 bytes .../seed_corpus/fuzz_tpm/verify_signature.bin | Bin 0 -> 120 bytes fuzz/src/lib.rs | 68 +++++++++++++++++- 9 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 fuzz/seed_corpus/fuzz_tpm/ec_ephemeral.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/ecc_parameters.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/nv_read_public2.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/pcr_reset.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_auth_value.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/policy_physical_presence.bin create mode 100644 fuzz/seed_corpus/fuzz_tpm/verify_signature.bin diff --git a/fuzz/fuzz_targets/fuzz_tpm_session.rs b/fuzz/fuzz_targets/fuzz_tpm_session.rs index 5d689ef..c50dbe2 100644 --- a/fuzz/fuzz_targets/fuzz_tpm_session.rs +++ b/fuzz/fuzz_targets/fuzz_tpm_session.rs @@ -114,6 +114,10 @@ enum Op { SetCancelFlag(bool), /// Jump the platform clock forward, in minutes. AdvanceClock(u16), + /// Save, probe, restore, probe again. Restoring the state the first probe + /// ran against has to reproduce its answers, so state the blob fails to + /// carry shows up as a divergence rather than staying silent. + RollbackFidelity, } fuzz_target!(|ops: Vec| { @@ -181,6 +185,18 @@ fuzz_target!(|ops: Vec| { Op::AdvanceClock(minutes) => { tpm.advance_clock(u64::from(*minutes) * 60_000); } + Op::RollbackFidelity => { + let saved = tpm.save_state(); + let before = tpm.probe(); + tpm.restore_state(saved) + .expect("state the TPM just saved should restore"); + let after = tpm.probe(); + + assert!( + before == after, + "restore did not roll the TPM back: probe responses diverged" + ); + } } } }); diff --git a/fuzz/seed_corpus/fuzz_tpm/ec_ephemeral.bin b/fuzz/seed_corpus/fuzz_tpm/ec_ephemeral.bin new file mode 100644 index 0000000000000000000000000000000000000000..c5513375d731fad77dfe2af6f556e07baa0dde4e GIT binary patch literal 12 TcmZo*WME+6VPIhFV_*gV2pj<* literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/ecc_parameters.bin b/fuzz/seed_corpus/fuzz_tpm/ecc_parameters.bin new file mode 100644 index 0000000000000000000000000000000000000000..4bbf4c7d4f9583f39ba9c968feb65eb7134301df GIT binary patch literal 12 TcmZo*WME+6VPIgaU|WuF762Dc0WJUl literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_auth_value.bin b/fuzz/seed_corpus/fuzz_tpm/policy_auth_value.bin new file mode 100644 index 0000000000000000000000000000000000000000..b2527c72eb4b6a2e439db920c673bde3ba6e23cd GIT binary patch literal 14 TcmZo*WME+6V_;y+W(E=f3Bv&Z literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_physical_presence.bin b/fuzz/seed_corpus/fuzz_tpm/policy_physical_presence.bin new file mode 100644 index 0000000000000000000000000000000000000000..fe732f709c73c192a8ca71eaa137924d4bb70c1a GIT binary patch literal 14 TcmZo*WME+6V_;xxX9f}g3QqwX literal 0 HcmV?d00001 diff --git a/fuzz/seed_corpus/fuzz_tpm/verify_signature.bin b/fuzz/seed_corpus/fuzz_tpm/verify_signature.bin new file mode 100644 index 0000000000000000000000000000000000000000..2e6ad58cfd2231fd99d364df490361e22cef57dd GIT binary patch literal 120 zcmV-;0Ehp80RR91cmMzacYpu@001BW0RjUA1qKHQ2?`4g4Gs?w5fT#=6&4p585$cL z9UdP57yt_ZAS$%Yl;;b|N;C@mV((1T71@Si5iQZTRC#Uv=e}+9wg4b5UGaZr?92+4 a4muGr!Xuh;VZlo+wd4D|QB8gge25+Mh9pP; literal 0 HcmV?d00001 diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index cb63761..e2d9a75 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -99,6 +99,12 @@ const TPM2_READ_PUBLIC_SEEDED: &[u8] = &[ 0x80, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x01, 0x73, 0x80, 0x00, 0x00, 0x00, ]; +/// `TPM2_GetRandom(16)`. The DRBG state is carried by the saved blob, so a +/// restore has to rewind it and make this repeat itself. +const TPM2_GET_RANDOM: &[u8] = &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x01, 0x7b, 0x00, 0x10, +]; + /// A `TPM_RS_PW` authorization area holding an empty password. /// /// This is what the great majority of commands want, and it is the one thing @@ -417,6 +423,18 @@ const SETUP_COMMANDS: &[(&str, &[u8], Option)] = &[ const TPM_CC_LOAD: u32 = 0x0000_0157; /// `TPM_CC_ContextLoad` const TPM_CC_CONTEXT_LOAD: u32 = 0x0000_0161; +/// `TPM_CC_VerifySignature` +const TPM_CC_VERIFY_SIGNATURE: u32 = 0x0000_0177; + +/// The `TPM2B_DIGEST` that [`TPM2_SIGN_SEEDED`] signs, which is also what the +/// signature it produces has to be verified against. +const SIGNED_DIGEST: &[u8] = &[ + 0x00, 0x20, // size + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, // + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, // + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, +]; /// `TPM2_Create` of an HMAC key under [`SEEDED_STORAGE_PARENT`]. /// @@ -516,6 +534,15 @@ fn split_create_response(response: &[u8]) -> Option<(&[u8], &[u8])> { )) } +/// Extracts the `TPMT_SIGNATURE` from a `TPM2_Sign` response. +fn sign_response_signature(response: &[u8]) -> Option<&[u8]> { + // Authorized, so tagged TPM_ST_SESSIONS: a parameterSize precedes the + // parameters and bounds them, with the session area following. + let size = response.get(HEADER_SIZE..HEADER_SIZE + 4)?; + let size = u32::from_be_bytes(size.try_into().ok()?) as usize; + response.get(HEADER_SIZE + 4..HEADER_SIZE + 4 + size) +} + /// `TPM2_Sign(SEEDED_TRANSIENT, ECDSA-SHA256)` over a fixed digest. /// /// Used to settle the dictionary attack `daUsed` state, and to confirm the @@ -694,7 +721,9 @@ impl FuzzTpm { // handler behind it. The second attempt has to succeed, which also // confirms the seeded key works. let _ = tpm.execute_command(&mut TPM2_SIGN_SEEDED.to_vec()); - tpm.execute_expecting_success(TPM2_SIGN_SEEDED, "TPM2_Sign"); + let signed = tpm + .execute_expecting_success(TPM2_SIGN_SEEDED, "TPM2_Sign") + .to_vec(); // Has to happen before the snapshot: a saved context is only valid // against the state it was saved from, which is the state every @@ -721,10 +750,26 @@ impl FuzzTpm { context, )); } + + // A signature the TPM itself produced. Nothing the fuzzer can invent + // verifies, so without this the whole signature-checking path - and + // the ticket-taking commands behind it - is only ever entered with + // garbage that fails at the first parse. + if let Some(signature) = sign_response_signature(&signed) { + let mut params = SIGNED_DIGEST.to_vec(); + params.extend_from_slice(signature); + canned.push(build_structured_command( + TPM_CC_VERIFY_SIGNATURE, + &[SEEDED_TRANSIENT], + None, + ¶ms, + )); + } + assert_eq!( canned.len(), - 2, - "both canned commands should have been built" + 3, + "all canned commands should have been built" ); let _ = CANNED_COMMANDS.set(canned); @@ -823,6 +868,23 @@ impl FuzzTpm { pub fn advance_clock(&mut self, millis: u64) { CLOCK_TICKS.fetch_add(millis, Relaxed); } + + /// Runs read-only commands whose answers are fixed by the TPM's state. + /// + /// Comparing these either side of a save / restore says whether the blob + /// really carries everything the TPM's behavior depends on. + pub fn probe(&mut self) -> Vec { + let mut answers = Vec::new(); + + for probe in [TPM2_GET_RANDOM, TPM2_READ_PUBLIC_SEEDED] { + let mut command = probe.to_vec(); + if let Ok(response) = self.execute_command(&mut command) { + answers.extend_from_slice(response); + } + } + + answers + } } /// Validates the invariants every TPM response is expected to uphold, and From fa3611a9a14e1b988c59aea813990f1b19b0dc56 Mon Sep 17 00:00:00 2001 From: Steven Malis Date: Fri, 28 Aug 2026 19:10:16 -0400 Subject: [PATCH 6/7] no need for this --- fuzz/Cargo.toml | 7 -- fuzz/fuzz_targets/fuzz_restore_state.rs | 100 ------------------------ 2 files changed, 107 deletions(-) delete mode 100644 fuzz/fuzz_targets/fuzz_restore_state.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index c9dcc06..6dedadb 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -38,13 +38,6 @@ test = false doc = false bench = false -[[bin]] -name = "fuzz_restore_state" -path = "fuzz_targets/fuzz_restore_state.rs" -test = false -doc = false -bench = false - [[bin]] name = "fuzz_nvmem" path = "fuzz_targets/fuzz_nvmem.rs" diff --git a/fuzz/fuzz_targets/fuzz_restore_state.rs b/fuzz/fuzz_targets/fuzz_restore_state.rs deleted file mode 100644 index 35e9974..0000000 --- a/fuzz/fuzz_targets/fuzz_restore_state.rs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. - -//! Fuzzes saved-state restore. -//! -//! `MsTpm185Platform::restore_state` parses a blob that, for a vTPM, comes off -//! a save file or a migration stream: it can be truncated, stale, corrupted, or -//! outright hostile. Restoring one must fail cleanly rather than crash, and a -//! blob that does restore must leave the TPM in a state that can keep running. -//! -//! Random bytes never get past the blob's postcard framing, so the interesting -//! mode here is `Patched`, which splices fuzzer controlled bytes into a blob -//! the TPM itself produced. - -#![no_main] - -use arbitrary::Arbitrary; -use libfuzzer_sys::fuzz_target; -use ms_tcg_tpm_sys_fuzz::Patch; -use ms_tcg_tpm_sys_fuzz::known_commands; -use ms_tcg_tpm_sys_fuzz::split_commands; -use ms_tcg_tpm_sys_fuzz::with_tpm; - -/// Caps how much work a single input can ask for, keeping the fuzzer's -/// executions-per-second up. -const MAX_COMMANDS: usize = 4; - -/// A command to run against the TPM the blob restored. -#[derive(Arbitrary, Debug)] -enum Command { - /// One of the harness' well formed commands. Like `fuzz_nvmem`, this - /// target is driven by `arbitrary` rather than a seed corpus, so raw bytes - /// alone leave the restored TPM almost untouched. - Known(u8), - /// Fuzzer supplied bytes, split on their declared command sizes. - Raw(Vec), -} - -impl Command { - fn expand(commands: &[Command]) -> Vec> { - let mut out = Vec::new(); - for command in commands { - match command { - Command::Known(index) => { - let known = known_commands(); - out.push(known[*index as usize % known.len()].to_vec()); - } - Command::Raw(bytes) => out.append(&mut split_commands(bytes, MAX_COMMANDS)), - } - if out.len() >= MAX_COMMANDS { - break; - } - } - out.truncate(MAX_COMMANDS); - out - } -} - -#[derive(Arbitrary, Debug)] -enum Input { - /// Restore an arbitrary blob. - Raw { - /// The blob to restore. - blob: Vec, - /// Commands to run afterwards, if the restore succeeded. - commands: Vec, - }, - /// Restore a corrupted version of a blob the TPM actually saved. - Patched { - /// Corruption to apply to the saved state. - patches: Vec, - /// Commands to run afterwards, if the restore succeeded. - commands: Vec, - }, -} - -fuzz_target!(|input: Input| { - with_tpm(|tpm| { - let (blob, commands) = match &input { - Input::Raw { blob, commands } => (blob.clone(), commands), - Input::Patched { patches, commands } => { - let mut blob = tpm.snapshot().to_vec(); - Patch::apply_all(&mut blob, patches); - (blob, commands) - } - }; - - // A rejected blob is the expected outcome for most inputs. - if tpm.restore_state(blob).is_err() { - return; - } - - // The restore claimed the state was good, so the TPM has to be able to - // keep running on it, and to save it back out. - for command in &mut Command::expand(commands) { - let _ = tpm.execute_command(command); - } - - let _ = tpm.save_state(); - }); -}); From 437d2e388fa88b17b391f9ae003b4b1f031742e9 Mon Sep 17 00:00:00 2001 From: Steven Malis Date: Mon, 31 Aug 2026 15:28:39 -0400 Subject: [PATCH 7/7] readme --- fuzz/README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/fuzz/README.md b/fuzz/README.md index a701557..1113ea5 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -29,7 +29,7 @@ cargo +nightly fuzz run fuzz_tpm fuzz/corpus/fuzz_tpm fuzz/seed_corpus/fuzz_tpm -- -dict=fuzz/tpm.dict ``` -The other three targets take `arbitrary`-encoded structures rather than raw +The other two targets take `arbitrary`-encoded structures rather than raw bytes, so there's nothing meaningful to hand-write a seed for; they build their corpus from scratch. @@ -67,7 +67,6 @@ symcrypt` (after `./scripts/fetch-symcrypt.sh`). | --- | --- | --- | | `fuzz_tpm` | A raw TPM command stream | `execute_command`: header validation, command unmarshaling, and dispatch - the bytes a guest controls | | `fuzz_tpm_session` | A sequence of platform operations | Commands interleaved with power cycles, live save / restore, locality changes, and cancellation | -| `fuzz_restore_state` | A saved-state blob | `restore_state`, plus running the TPM on whatever the blob restored | | `fuzz_nvmem` | A persisted nvmem blob | Booting on a corrupted, truncated, or hostile nvmem blob | `fuzz_tpm` takes plain bytes: the input is split into commands along the @@ -76,9 +75,9 @@ entry can be a capture of a real command stream, and [`seed_corpus/fuzz_tpm/`](seed_corpus/fuzz_tpm) holds hand-built commands to start from. -The other three take -[`arbitrary`](https://docs.rs/arbitrary)-derived structures. `fuzz_restore_state` -and `fuzz_nvmem` mostly work by splicing fuzzer controlled bytes into a blob the +The other two take +[`arbitrary`](https://docs.rs/arbitrary)-derived structures. `fuzz_nvmem` mostly +works by splicing fuzzer controlled bytes into a blob the TPM itself produced, since random bytes never survive a blob's framing and header validation. @@ -163,8 +162,8 @@ through `Op::Canned`. Both are built before the snapshot, since a saved context is only valid against the state it was saved from, which is the state every iteration rolls back to. -`fuzz_nvmem` and `fuzz_restore_state` have neither of those advantages: their -input is `arbitrary`-encoded, so there is no seed corpus to hand them and no +`fuzz_nvmem` has neither of those advantages: its +input is `arbitrary`-encoded, so there is no seed corpus to hand it and no dictionary to splice from, and raw bytes almost never clear the command header. That would waste the interesting half of what they test - not whether a tampered blob is rejected, but what the TPM does while running on one that @@ -178,9 +177,8 @@ signature or ticket over TPM-generated data, `TPM2_NV_ChangeAuth` needs an ADMIN-role policy session, `TPM2_PP_Commands` needs physical presence asserted, and `TPM2_SignSequenceStart` needs an opaque `TPM2B_SIGNATURE_CTX`. -Note that all four targets share this snapshot, so the effect is not limited to -the command targets: `fuzz_restore_state` now patches a blob that has objects -and sessions in it, and the blob `fuzz_nvmem` corrupts has real NV entries +Note that all three targets share this snapshot, so the effect is not limited to +the command targets: the blob `fuzz_nvmem` corrupts has real NV entries rather than only what manufacturing wrote. ## Determinism