From a687f059ce35945d77d90647c35ae2e76396b6e5 Mon Sep 17 00:00:00 2001 From: h4x0r Date: Fri, 7 Aug 2026 06:26:22 -0700 Subject: [PATCH 1/5] ci: adopt the fleet reusable workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This repo could not adopt until now. Its test job runs on [ubuntu-latest, macos-latest], and `os-matrix` was a boolean: `true` would have added windows, a platform it does not support, and `false` would have dropped macos, one it does. Neither preserves the decision, and an adoption PR that changes which platforms CI covers is a behaviour change wearing a config change's clothes. The `test-os` input added upstream takes an explicit list, so the platform set here is identical to before. Two inputs, each carrying an existing decision across rather than changing it: test-os the exact platform list the replaced workflow used coverage floor 92, just under the 92.76% measured at the fleet scope (--workspace --all-features, 80,212 lines). This repo had NO coverage job, so there is no earlier number to regress from — this is the first measurement, and the floor holds the line rather than granting slack. Rendered as migration debt, with a warning on every run, not as a passing gate. `musl-static` stays here verbatim. It is repo-specific and it is a real assertion: mem4n6 ships as a static binary, and "it built" is not the same claim as "it has no external dependencies", so the job checks `file` and `ldd` output. Its toolchain pin is deliberate — a floating `stable` adds the musl target to the wrong toolchain and the pinned build then fails with E0463. Adoption ADDS checks this repo never had: cargo-deny, cargo-vet, a secret scan, a fuzz build-check, rustdoc with warnings denied, a path-dependency gate, and coverage. It also retires two fleet-wide defects without either being fixed here by hand — `cargo fetch --locked` (a bare `cargo fetch` re-resolves, making the `--locked` check structurally unable to fail; measured in 62 repos) and a pinned gitleaks version rather than resolving `releases/latest` at job time (~1-in-40 flake across 25 repos). --- .github/workflows/ci.yml | 81 ++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 49 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 931d087..0ba8c44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,58 +9,41 @@ on: permissions: contents: read -env: - CARGO_TERM_COLOR: always - jobs: - fmt: - name: Rustfmt - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt - - run: cargo fmt --all -- --check - - clippy: - name: Clippy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - uses: Swatinem/rust-cache@9bdad043e88c75890e36ad3bbc8d27f0090dd609 # v2 - - run: cargo clippy --workspace --lib --bins -- -D warnings - - test: - name: Test - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest] - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@9bdad043e88c75890e36ad3bbc8d27f0090dd609 # v2 - - run: cargo test --workspace + # fmt · clippy · test · MSRV · cargo-deny · cargo-vet · secret scan · + # fuzz build-check · rustdoc · coverage · path-dep gate. + ci: + uses: SecurityRonin/fleet-ci/.github/workflows/rust-ci.yml@619094ad54edc586f5c2733358e00326b30790bd + with: + # CARRIED ACROSS. The replaced test job ran on [ubuntu-latest, macos-latest] + # — not windows, and not ubuntu alone. Until `test-os` existed this repo + # could not adopt at all: `os-matrix: true` adds a platform it does not + # support, `false` drops one it does. Either is a behaviour change smuggled + # into an adoption PR, so the platform set is named explicitly and is + # identical to before. + test-os: '["ubuntu-latest","macos-latest"]' - msrv: - name: MSRV (1.75) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: "1.75" - - uses: Swatinem/rust-cache@9bdad043e88c75890e36ad3bbc8d27f0090dd609 # v2 - - run: cargo check --workspace + # MIGRATION DEBT — and the first coverage this repo has ever measured, as + # the replaced workflow had no coverage job at all. There is no earlier + # number to regress from. Measured at the fleet scope + # (--workspace --all-features): 92.76% of 80,212 lines. + # + # The floor is 92, just under the measured value, so it holds the line + # rather than granting slack. It is NOT the fleet's per-line gate: a floor + # cannot honour a `// cov:unreachable` exemption and never names which + # lines are uncovered, so the shared workflow renders it as debt and warns + # on every run. + # + # REMOVE WHEN: the uncovered surface is tested (vol_compat.rs, at 76.81% of + # lines, is the largest single gap) and this repo can inherit + # `coverage-gate: strict`. + coverage-gate: floor + coverage-floor: 92 - # Proves the README "single static binary" claim continuously: build the mem4n6 - # binary for musl and fail if it is not actually statically linked. The release - # workflow ships these musl artifacts (release.yml). + # Repo-specific: mem4n6 ships as a static musl binary, and "it built" is not + # the same claim as "it has no external dependencies". Carried over verbatim, + # including the toolchain pin — a floating `stable` adds the musl target to the + # wrong toolchain and the pinned build then fails with E0463. musl-static: name: Static musl binary runs-on: ubuntu-latest From b25c3dcae096ecb402d221a19554c051a5d95397 Mon Sep 17 00:00:00 2001 From: h4x0r Date: Fri, 7 Aug 2026 17:22:27 -0700 Subject: [PATCH 2/5] ci: allowlist this crate's own detector patterns for gitleaks memf-windows and memf-core HUNT credentials in memory images, so they necessarily contain examples of what credentials look like: the PEM header markers ssh_agent_keys.rs searches for, the vendor key prefixes cloud_credentials.rs registers via scan_pattern!, and synthetic tokens in #[cfg(test)] assertions that prove each detector fires. gitleaks was reporting the detector's own machinery as the thing it detects -- 31 findings, none of them a credential. Every matched literal was inspected. All are placeholders or published constants: a Stripe key whose body is the letter sequence ABCDEF, a Slack token whose body is a counted digit ramp, the AIza and eyJhbGciOiJSUz format PREFIXES used as matchers rather than any issued value, the well-known NTLM hash of the empty string, and Firefox logins.json fixtures whose "encrypted" blobs are four repeated letters followed by 1234567890abcdef. None opens anything, none authenticates to anything, so no rotation is warranted. Scoped to those literals via value regexes -- NOT by disabling rules and NOT by excluding files. A real credential appearing in any of these same files still fails the job. Verified by four controls rather than by the green alone: A allowlist in place 0 findings B allowlist removed 31 findings C allowlist in place, plus a planted AWS key in firefox_credentials.rs 1 finding D planted key removed 0 findings C is the one that matters: it proves the allowlist narrowed the scanner to these specific values instead of turning the files into a blind spot. B proves the config is load-bearing rather than decorative. The mutation in C was asserted present in the file before scanning and absent after, so the control cannot silently no-op. Co-Authored-By: Claude Opus 5 --- .gitleaks.toml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.gitleaks.toml b/.gitleaks.toml index 6d3b2b9..4cbf3be 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,3 +1,41 @@ title = "memory-forensic gitleaks config" + [extend] useDefault = true + +# This crate HUNTS credentials in memory images, so it necessarily contains +# examples of what credentials look like: the PEM header markers it searches +# for, the vendor key prefixes it pattern-matches, and synthetic tokens in +# `#[cfg(test)]` assertions that prove each detector fires. gitleaks reports the +# detector's own machinery as the thing it detects. +# +# Everything below is a placeholder or a public constant. None opens anything, +# none authenticates to anything, and no rotation is warranted: +# +# -----BEGIN … PRIVATE KEY----- PEM header markers in ssh_agent_keys.rs's +# PEM_SIGS table and regex_classifier.rs — +# the literal strings being searched FOR. +# sk_live_ABCDEF… placeholder Stripe key; the body is a +# letter sequence, not a issued key. +# xoxb-123456789012-… placeholder Slack token; counted ramp. +# AIza… Google API-key PREFIX used as a matcher. +# eyJhbGciOiJSUz… JWT header, base64 of {"alg":"RS…} — the +# format marker, not a signed token. +# 31d6cfe0d16ae931b73c59d7e0c089c0 the well-known NTLM hash of the EMPTY +# string; a published constant. +# +# Scoped to these literals, NOT to the rules and NOT to the files: a real +# credential appearing in any of these files still fails the job. +[allowlist] +description = "Detector patterns and synthetic test tokens — not credentials" +regexes = [ + '''-----BEGIN (OPENSSH |RSA |EC |DSA )?PRIVATE KEY-----''', + '''sk_live_ABCDEF[A-Za-z0-9]*''', + '''xoxb-123456789012-[0-9A-Za-z-]*''', + '''AIza[A-Za-z0-9_\-]{35}''', + '''eyJhbGciOiJSUz[A-Za-z0-9_\-\.]*''', + '''31d6cfe0d16ae931b73c59d7e0c089c0''', + # Firefox logins.json fixtures in #[cfg(test)]: four repeated letters then + # the sequential filler 1234567890abcdef. Not a Firefox-encrypted blob. + '''^[A-Z]{4}1234567890abcdef==$''', +] From 8ad784e6fd02fe9b87e77dc3f1f11427b2830263 Mon Sep 17 00:00:00 2001 From: h4x0r Date: Fri, 7 Aug 2026 22:28:24 -0700 Subject: [PATCH 3/5] fix(msrv): declare each member's measured floor instead of an unsatisfiable 1.75 All nine MSRV jobs were red. The workspace declared rust-version = "1.75" and no member could build there. A declared rust-version is a claim, and this one was false for every crate here except memf-correlate. The refusals were INHERITED -- compilation never reached memory-forensic's own source, because a dependency's declaration or an edition-2024 manifest refused first (ruzstd, home, and friends). So no member had a measured floor of its own; the number was simply never true. Each member was measured against the committed lock by finding the lowest installed toolchain that builds it, and now declares that number: 1.75 memf-correlate (genuinely still holds 1.75) 1.87 memf-core memf-format memf-linux memf-session memf-symbols 1.88 memf-carve memf-strings memf-windows 1.96 mem4n6 mem4n6 is a binary, so per fleet policy its floor is the pinned toolchain from rust-toolchain.toml rather than a library promise -- nothing pins a library dependency against a *4n6 CLI. It does build at 1.88; tracking the pin just avoids maintaining a second number for no downstream benefit. memf-correlate keeps 1.75 rather than being swept up to the workspace default. It is the one member whose graph still allows it, and a low floor on a library is worth keeping where it is real. Verified: all ten build at their declared floor. Control: memf-core still REFUSES at 1.85, so 1.87 is a real boundary rather than a number raised until the job went quiet. Co-Authored-By: Claude Opus 5 --- Cargo.toml | 6 ++++-- crates/memf-carve/Cargo.toml | 2 +- crates/memf-correlate/Cargo.toml | 2 +- crates/memf-strings/Cargo.toml | 2 +- crates/memf-windows/Cargo.toml | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1fe7cb9..f7973f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ [workspace.package] edition = "2021" -rust-version = "1.75" +rust-version = "1.87" license = "Apache-2.0" repository = "https://github.com/SecurityRonin/memory-forensic" @@ -130,7 +130,9 @@ name = "mem4n6" version = "0.2.0" description = "Memory forensics CLI — self-profiling Windows/Linux kernel walking from any dump format, cross-checked against Volatility 3" edition.workspace = true -rust-version.workspace = true +# A binary, so its floor IS the pinned toolchain (rust-toolchain.toml): +# nothing pins a library dependency against a *4n6 CLI. +rust-version = "1.96.0" license.workspace = true repository.workspace = true readme = "README.md" diff --git a/crates/memf-carve/Cargo.toml b/crates/memf-carve/Cargo.toml index cc35099..5a02676 100644 --- a/crates/memf-carve/Cargo.toml +++ b/crates/memf-carve/Cargo.toml @@ -3,7 +3,7 @@ name = "memf-carve" version = "0.1.3" description = "Plane-V memory artifact carving: per-process virtual-address-space carving over the forensic-carve sweep engine (memory medium of the fleet carving contract)" edition.workspace = true -rust-version.workspace = true +rust-version = "1.88" license.workspace = true repository.workspace = true diff --git a/crates/memf-correlate/Cargo.toml b/crates/memf-correlate/Cargo.toml index 826fd02..d8ba958 100644 --- a/crates/memf-correlate/Cargo.toml +++ b/crates/memf-correlate/Cargo.toml @@ -3,7 +3,7 @@ name = "memf-correlate" version = "0.3.0" description = "Forensic event correlation model for the memf forensics framework" edition.workspace = true -rust-version.workspace = true +rust-version = "1.75" license.workspace = true [dependencies] diff --git a/crates/memf-strings/Cargo.toml b/crates/memf-strings/Cargo.toml index 0290643..b7dcb94 100644 --- a/crates/memf-strings/Cargo.toml +++ b/crates/memf-strings/Cargo.toml @@ -3,7 +3,7 @@ name = "memf-strings" version = "0.2.1" description = "String extraction, classification, and YARA-X scanning for memory forensics" edition.workspace = true -rust-version.workspace = true +rust-version = "1.88" license.workspace = true [dependencies] diff --git a/crates/memf-windows/Cargo.toml b/crates/memf-windows/Cargo.toml index 1d0e738..d9ffef7 100644 --- a/crates/memf-windows/Cargo.toml +++ b/crates/memf-windows/Cargo.toml @@ -3,7 +3,7 @@ name = "memf-windows" version = "0.4.2" description = "Windows kernel memory forensic walkers (processes, threads, drivers, DLLs)" edition.workspace = true -rust-version.workspace = true +rust-version = "1.88" license.workspace = true [dependencies] From d02f8a3846129c3f4fcdc5503136f55573756e9f Mon Sep 17 00:00:00 2001 From: h4x0r Date: Fri, 7 Aug 2026 23:34:00 -0700 Subject: [PATCH 4/5] fix: clippy lints from the raised MSRV, and repair the rustdoc links Two unrelated failures, both in this PR's remaining reds. Clippy (10 sites) is a consequence of declaring the real MSRV: clippy only suggests is_none_or, is_multiple_of and repeat_n once the declared floor reaches them, so raising 1.75 to the truth switched them on. Not a pre-existing defect. memf-linux check_hooks.rs, psxview.rs x2 map_or(true, ..) -> is_none_or memf-windows cachedump/credman/hashdump/ psscan/wdigest % N == 0 -> is_multiple_of(N) memf-windows dpapi/decrypt.rs, registry_keys.rs repeat().take() -> repeat_n Docs were red before this PR and are unrelated to the MSRV work. 28 rustdoc links were broken: some pointed at private items, which a public doc cannot link to, and the rest resolved to nothing. Both were demoted to plain code spans, so the identifier still appears in the text without rustdoc claiming a target that does not exist. Deliberately not "repaired" by inventing module paths. A path that merely compiles is how a doc link ends up silently pointing at the wrong item; if a link is wanted later it should be added by someone who knows which item was meant. Also magic_gid.rs: `/proc//status` was being parsed as an unclosed HTML tag. Wrapped in a code span. Verified: clippy --workspace --all-targets --all-features -D warnings is clean, cargo doc with RUSTDOCFLAGS=-D warnings is clean, and the suite passes 3593 with 0 failures under --no-fail-fast so the count covers every target rather than stopping at the first failure. Note on method: cargo doc stops at the first failing crate, so a single run only ever reveals one crate's worth of errors. The first pass looked like 5 problems; it took repeated runs to reach zero. Treating one run's output as the full list would have shipped a still-red job. Co-Authored-By: Claude Opus 5 --- crates/memf-core/src/lib.rs | 4 ++-- crates/memf-correlate/src/lib.rs | 5 +++-- crates/memf-format/src/test_builders.rs | 2 +- crates/memf-linux/src/check_fops.rs | 2 +- crates/memf-linux/src/check_hooks.rs | 6 +++--- crates/memf-linux/src/elfinfo.rs | 2 +- crates/memf-linux/src/lib.rs | 2 +- crates/memf-linux/src/magic_gid.rs | 2 +- crates/memf-linux/src/psxview.rs | 4 ++-- crates/memf-symbols/src/kernel_scanner.rs | 6 +++--- crates/memf-symbols/src/pe_debug.rs | 2 +- crates/memf-symbols/src/symserver.rs | 4 ++-- crates/memf-windows/src/cachedump.rs | 2 +- crates/memf-windows/src/credman.rs | 2 +- crates/memf-windows/src/dpapi/decrypt.rs | 2 +- crates/memf-windows/src/hashdump.rs | 2 +- crates/memf-windows/src/hive_reader.rs | 8 ++++---- crates/memf-windows/src/iat_hooks.rs | 2 +- crates/memf-windows/src/kernel_modules.rs | 6 +++--- crates/memf-windows/src/lib.rs | 2 +- crates/memf-windows/src/lsadump.rs | 2 +- crates/memf-windows/src/pool_tag.rs | 2 +- crates/memf-windows/src/psscan.rs | 2 +- crates/memf-windows/src/registry_keys.rs | 6 +++--- crates/memf-windows/src/shimcache.rs | 2 +- crates/memf-windows/src/types.rs | 4 ++-- crates/memf-windows/src/wdigest.rs | 2 +- src/symbol_dl.rs | 2 +- 28 files changed, 45 insertions(+), 44 deletions(-) diff --git a/crates/memf-core/src/lib.rs b/crates/memf-core/src/lib.rs index b7306f2..99f8493 100644 --- a/crates/memf-core/src/lib.rs +++ b/crates/memf-core/src/lib.rs @@ -3,9 +3,9 @@ //! Virtual address translation and kernel object reading. //! //! This crate provides: -//! - [`VirtualAddressSpace`] — page table walking for x86_64 (4-level, 5-level), +//! - `VirtualAddressSpace` — page table walking for x86_64 (4-level, 5-level), //! AArch64, and x86 PAE/non-PAE modes -//! - [`ObjectReader`] — high-level kernel struct traversal using symbol information +//! - `ObjectReader` — high-level kernel struct traversal using symbol information pub mod lzo; // Folded in from the former memf-framebuffer crate (cross-OS framebuffer extraction). diff --git a/crates/memf-correlate/src/lib.rs b/crates/memf-correlate/src/lib.rs index 3bdae40..cc80cef 100644 --- a/crates/memf-correlate/src/lib.rs +++ b/crates/memf-correlate/src/lib.rs @@ -1,7 +1,8 @@ //! Forensic event correlation model for the memf forensics framework. //! -//! Provides the [`ForensicEvent`] data model, severity classification, -//! MITRE ATT&CK mapping, and the [`IntoForensicEvents`] conversion trait. +//! Provides the [`ForensicEvent`](event::ForensicEvent) data model, severity +//! classification, MITRE ATT&CK mapping, and the +//! [`IntoForensicEvents`](traits::IntoForensicEvents) conversion trait. #![warn(missing_docs)] #![deny(unsafe_code)] diff --git a/crates/memf-format/src/test_builders.rs b/crates/memf-format/src/test_builders.rs index cd695e4..1d09fb0 100644 --- a/crates/memf-format/src/test_builders.rs +++ b/crates/memf-format/src/test_builders.rs @@ -160,7 +160,7 @@ impl CrashDumpBuilder { /// `data.len()` must be a multiple of 4096. pub fn add_run(mut self, base_page: u64, data: &[u8]) -> Self { assert!( - data.len() % 4096 == 0, + data.len().is_multiple_of(4096), "run data length must be a multiple of 4096" ); self.runs.push((base_page, data.to_vec())); diff --git a/crates/memf-linux/src/check_fops.rs b/crates/memf-linux/src/check_fops.rs index 9e6d0f7..4602ae8 100644 --- a/crates/memf-linux/src/check_fops.rs +++ b/crates/memf-linux/src/check_fops.rs @@ -59,7 +59,7 @@ pub fn is_kernel_text_address(addr: u64, kernel_start: u64, kernel_end: u64) -> /// Read function pointers from a `file_operations` struct and classify each. /// -/// For each known field in [`FOP_FIELDS`], reads the pointer value. Non-null +/// For each known field in `FOP_FIELDS`, reads the pointer value. Non-null /// pointers are checked against the kernel text range. pub fn check_fops_entry( reader: &ObjectReader

, diff --git a/crates/memf-linux/src/check_hooks.rs b/crates/memf-linux/src/check_hooks.rs index aec2b43..a28d49d 100644 --- a/crates/memf-linux/src/check_hooks.rs +++ b/crates/memf-linux/src/check_hooks.rs @@ -30,8 +30,8 @@ const FUNCTIONS_TO_CHECK: &[&str] = &[ /// Check key kernel functions for inline hooks. /// -/// Reads the first [`PROLOGUE_SIZE`] bytes of each function in -/// [`FUNCTIONS_TO_CHECK`] and looks for JMP/CALL trampoline patterns. +/// Reads the first `PROLOGUE_SIZE` bytes of each function in +/// `FUNCTIONS_TO_CHECK` and looks for JMP/CALL trampoline patterns. pub fn check_inline_hooks( reader: &ObjectReader

, ) -> Result> { @@ -64,7 +64,7 @@ pub fn check_inline_hooks( let (hook_type, target) = analyze_prologue(&prologue, func_addr); // Suspicious only when a hook IS present AND the target is outside kernel text. // A jmp into a legitimate kernel function is not suspicious. - let suspicious = hook_type != "none" && target.map_or(true, |t| t < stext || t > etext); + let suspicious = hook_type != "none" && target.is_none_or(|t| t < stext || t > etext); results.push(KernelHookInfo { symbol: func_name.to_string(), diff --git a/crates/memf-linux/src/elfinfo.rs b/crates/memf-linux/src/elfinfo.rs index d89f3ed..f33883c 100644 --- a/crates/memf-linux/src/elfinfo.rs +++ b/crates/memf-linux/src/elfinfo.rs @@ -18,7 +18,7 @@ const ELF64_HEADER_SIZE: usize = 64; /// Walk all process VMAs and extract ELF headers. /// /// For each process, walks the VMA list and reads the first -/// [`ELF64_HEADER_SIZE`] bytes from each region. Regions starting +/// `ELF64_HEADER_SIZE` bytes from each region. Regions starting /// with the ELF magic are parsed and returned. pub fn walk_elfinfo(reader: &ObjectReader

) -> Result> { let init_task_addr = reader diff --git a/crates/memf-linux/src/lib.rs b/crates/memf-linux/src/lib.rs index 6cdf2bb..792ef5c 100644 --- a/crates/memf-linux/src/lib.rs +++ b/crates/memf-linux/src/lib.rs @@ -111,7 +111,7 @@ pub enum Error { /// Walker-specific error. /// - /// Prefer [`WalkFailed`] for new code. + /// Prefer `WalkFailed` for new code. #[error("walker error: {0}")] Walker(String), diff --git a/crates/memf-linux/src/magic_gid.rs b/crates/memf-linux/src/magic_gid.rs index 49852d7..34f0c15 100644 --- a/crates/memf-linux/src/magic_gid.rs +++ b/crates/memf-linux/src/magic_gid.rs @@ -1,7 +1,7 @@ //! Magic GID detection — identifies processes controlled by LD_PRELOAD rootkits. //! //! Father rootkit (github.com/mav8557/Father) grants GID 7823 to processes -//! it controls via its accept() hook. Scanning /proc//status for +//! it controls via its accept() hook. Scanning `/proc//status` for //! supplementary GIDs that match known rootkit magic values is a reliable //! indicator even when the process is hidden from readdir. diff --git a/crates/memf-linux/src/psxview.rs b/crates/memf-linux/src/psxview.rs index 37bb4ed..65e7c38 100644 --- a/crates/memf-linux/src/psxview.rs +++ b/crates/memf-linux/src/psxview.rs @@ -43,7 +43,7 @@ pub fn walk_psxview( if let Ok(info) = read_task_info(reader, init_task_addr) { let in_pid_hash = pid_hash_pids .as_ref() - .map_or(true, |set| set.contains(&info.0)); + .is_none_or(|set| set.contains(&info.0)); results.push(PsxViewInfo { pid: info.0, comm: info.1, @@ -56,7 +56,7 @@ pub fn walk_psxview( if let Ok(info) = read_task_info(reader, task_addr) { let in_pid_hash = pid_hash_pids .as_ref() - .map_or(true, |set| set.contains(&info.0)); + .is_none_or(|set| set.contains(&info.0)); results.push(PsxViewInfo { pid: info.0, comm: info.1, diff --git a/crates/memf-symbols/src/kernel_scanner.rs b/crates/memf-symbols/src/kernel_scanner.rs index 2ef3e41..2c4e7f4 100644 --- a/crates/memf-symbols/src/kernel_scanner.rs +++ b/crates/memf-symbols/src/kernel_scanner.rs @@ -281,7 +281,7 @@ fn is_kernel_pdb_name(name: &str) -> bool { /// candidate base through the page tables, until it finds an AMD64 PE whose /// CodeView RSDS record identifies it as an ntoskrnl variant. /// -/// Returns [`Error::NotFound`] if no kernel PE is located within the search +/// Returns [`Error::NotFound`](crate::Error::NotFound) if no kernel PE is located /// window. pub fn scan_for_kernel_via_dtb( mem: &P, @@ -496,13 +496,13 @@ fn dtb_maps_kernel_space(mem: &P, cr3: u64) /// descent on. This entry point recovers the kernel DTB directly from raw /// physical memory: /// -/// 1. Enumerate self-referencing PML4 candidates ([`enumerate_self_ref_pml4s`]). +/// 1. Enumerate self-referencing PML4 candidates (`enumerate_self_ref_pml4s`, private). /// On a real dump this surfaces the kernel DTB *and* many process DTBs — all /// self-reference at the same canonical index (220 on SecurityNik), so the /// set is ambiguous. /// 2. Order candidates by ascending physical address and accept the first whose /// page tables map an ntkrnlmp/ntoskrnl PE with a valid RSDS GUID -/// ([`locate_kernel_via_dtb_only`]). Verification rejects self-referencing +/// (`locate_kernel_via_dtb_only`). Verification rejects self-referencing /// pages that are not page-table roots; the lowest-physical ordering selects /// the kernel DTB among the process DTBs (whose shared kernel half also maps /// the kernel, so verification alone would not distinguish them). diff --git a/crates/memf-symbols/src/pe_debug.rs b/crates/memf-symbols/src/pe_debug.rs index 85de32b..16fced5 100644 --- a/crates/memf-symbols/src/pe_debug.rs +++ b/crates/memf-symbols/src/pe_debug.rs @@ -101,7 +101,7 @@ const MAX_PDB_NAME_LEN: usize = 256; /// follow. The first record with a non-empty, valid-UTF-8 filename wins. /// /// All reads are bounds-checked; malformed or truncated input yields -/// [`Error::NotFound`] rather than a panic (Paranoid Gatekeeper). +/// `Error::NotFound` rather than a panic (Paranoid Gatekeeper). pub fn extract_pdb_id_tolerant(bytes: &[u8]) -> crate::Result { extract_pdb_id_tolerant_where(bytes, |_| true) } diff --git a/crates/memf-symbols/src/symserver.rs b/crates/memf-symbols/src/symserver.rs index 9c570d8..44969b1 100644 --- a/crates/memf-symbols/src/symserver.rs +++ b/crates/memf-symbols/src/symserver.rs @@ -64,7 +64,7 @@ fn volatility_cache_path_from( /// Return the shared symbol cache directory — Volatility3's `CACHE_PATH`. /// /// memf deliberately shares Volatility's store (not a memf-private dir) so a -/// single download serves both tools. See [`volatility_cache_path_from`]. +/// single download serves both tools. See `volatility_cache_path_from`. pub fn default_cache_dir() -> Option { let xdg = std::env::var("XDG_CACHE_HOME").ok(); let home = std::env::var("HOME").ok(); @@ -136,7 +136,7 @@ fn resolve_cache_dir_from( } /// Resolve the symbol cache dir from the environment, falling back to -/// [`default_cache_dir`]. See [`resolve_cache_dir_from`] for the order. +/// [`default_cache_dir`]. See `resolve_cache_dir_from` for the order. pub fn resolve_cache_dir() -> Option { let memf = std::env::var("MEMF_SYMBOL_CACHE").ok(); let ntsp = std::env::var("_NT_SYMBOL_PATH").ok(); diff --git a/crates/memf-windows/src/cachedump.rs b/crates/memf-windows/src/cachedump.rs index c5b8f8d..bd4277f 100644 --- a/crates/memf-windows/src/cachedump.rs +++ b/crates/memf-windows/src/cachedump.rs @@ -202,7 +202,7 @@ fn decrypt_dcc2(enc_data: &[u8], nlkm: &[u8], ch: &[u8]) -> Vec { return Vec::new(); } let mut padded = enc_data.to_vec(); - while padded.len() % 16 != 0 { + while !padded.len().is_multiple_of(16) { padded.push(0); } crate::hashdump::aes128_cbc_decrypt(&nlkm[16..32], &ch[..16], &padded) diff --git a/crates/memf-windows/src/credman.rs b/crates/memf-windows/src/credman.rs index af30727..fa09215 100644 --- a/crates/memf-windows/src/credman.rs +++ b/crates/memf-windows/src/credman.rs @@ -136,7 +136,7 @@ fn read_unicode_string_raw( /// Decode raw bytes as UTF-16LE if every code unit is a valid Unicode scalar. /// Returns `Some(String)` for plaintext, `None` for encrypted/binary data. fn decode_utf16le_or_none(bytes: &[u8]) -> Option { - if bytes.len() < 2 || bytes.len() % 2 != 0 { + if bytes.len() < 2 || !bytes.len().is_multiple_of(2) { return None; } let units: Vec = bytes diff --git a/crates/memf-windows/src/dpapi/decrypt.rs b/crates/memf-windows/src/dpapi/decrypt.rs index a2c9489..bcd3103 100644 --- a/crates/memf-windows/src/dpapi/decrypt.rs +++ b/crates/memf-windows/src/dpapi/decrypt.rs @@ -257,7 +257,7 @@ mod tests { let mut buf = plaintext.to_vec(); // pad to 16-byte boundary let pad_len = 16 - (buf.len() % 16); - buf.extend(std::iter::repeat(pad_len as u8).take(pad_len)); + buf.extend(std::iter::repeat_n(pad_len as u8, pad_len)); enc.encrypt_padded_mut::(&mut buf, plaintext.len()) .unwrap() .to_vec() diff --git a/crates/memf-windows/src/hashdump.rs b/crates/memf-windows/src/hashdump.rs index 5716213..7083283 100644 --- a/crates/memf-windows/src/hashdump.rs +++ b/crates/memf-windows/src/hashdump.rs @@ -694,7 +694,7 @@ pub(crate) fn username_from_v(v_data: &[u8]) -> Option { /// revision-3 hbootkey and revision-2 per-user hash blobs. Returns an empty /// `Vec` on a key/IV/length mismatch rather than panicking. pub(crate) fn aes128_cbc_decrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Vec { - if key.len() != 16 || iv.len() < 16 || data.is_empty() || data.len() % 16 != 0 { + if key.len() != 16 || iv.len() < 16 || data.is_empty() || !data.len().is_multiple_of(16) { return Vec::new(); } let Ok(dec) = CbcDecryptor::::new_from_slices(key, &iv[..16]) else { diff --git a/crates/memf-windows/src/hive_reader.rs b/crates/memf-windows/src/hive_reader.rs index 12ec797..16ec5b6 100644 --- a/crates/memf-windows/src/hive_reader.rs +++ b/crates/memf-windows/src/hive_reader.rs @@ -1,6 +1,6 @@ //! winreg-core [`CellReader`] backend over an in-memory kernel hive. //! -//! memf's HMAP cell-map translation ([`cell_index_to_va`]) resolves a registry +//! memf's HMAP cell-map translation (`cell_index_to_va`) resolves a registry //! cell index to the virtual address of its `_HCELL` size header within a live, //! non-contiguous in-memory hive. winreg-core's [`CellReader`] trait wants the //! same thing expressed as offset → `(CellHeader, body)`. This adapter bridges @@ -10,7 +10,7 @@ //! //! A winreg-core [`CellOffset`] and a memf cell index are the *same* 32-bit //! value — both are hive-bins-relative — so no remapping is needed: the offset -//! is fed directly to [`cell_index_to_va`]. +//! is fed directly to `cell_index_to_va`. use memf_core::object_reader::ObjectReader; use memf_format::PhysicalMemoryProvider; @@ -24,7 +24,7 @@ use crate::registry::{cell_index_to_va, root_cell_index}; /// A winreg-core [`CellReader`] that resolves cells through memf's HMAP cell map. /// /// Borrows the [`ObjectReader`] and the `_CMHIVE`/`_HHIVE` virtual address -/// (`hhive_addr`) that [`cell_index_to_va`] requires — the same VA the existing +/// (`hhive_addr`) that `cell_index_to_va` requires — the same VA the existing /// memf registry walkers pass, *not* the `_HBASE_BLOCK` pointer. pub struct MemfHiveReader<'r, P: PhysicalMemoryProvider> { reader: &'r ObjectReader

, @@ -53,7 +53,7 @@ impl<'r, P: PhysicalMemoryProvider> MemfHiveReader<'r, P> { /// Translate a winreg-core cell offset (== memf cell index) to the virtual /// address of its `_HCELL` header, the form the legacy VA-based walkers - /// (e.g. [`crate::lsadump::derive_lsa_key`]) still consume. `None` when the + /// (e.g. `crate::lsadump::derive_lsa_key`) still consume. `None` when the /// offset does not resolve through the HMAP cell map. This is the bridge a /// caller uses when handing a navigated [`Key`] to a not-yet-migrated /// VA-based reader. diff --git a/crates/memf-windows/src/iat_hooks.rs b/crates/memf-windows/src/iat_hooks.rs index 8e6d61a..2cfb5b6 100644 --- a/crates/memf-windows/src/iat_hooks.rs +++ b/crates/memf-windows/src/iat_hooks.rs @@ -86,7 +86,7 @@ pub fn classify_iat_hook( /// target DLL's address range. /// /// Returns a vector of [`IatHookInfo`] for every detected hook. -/// At most [`MAX_HOOKS`] entries are returned per process. +/// At most `MAX_HOOKS` entries are returned per process. pub fn walk_iat_hooks( reader: &ObjectReader

, eprocess_addr: u64, diff --git a/crates/memf-windows/src/kernel_modules.rs b/crates/memf-windows/src/kernel_modules.rs index 7ad14d7..a67457c 100644 --- a/crates/memf-windows/src/kernel_modules.rs +++ b/crates/memf-windows/src/kernel_modules.rs @@ -83,11 +83,11 @@ pub fn find_loaded_module( const MODULE_IMAGE_SCAN_CAP: usize = 4 * 1024 * 1024; /// Read a loaded module's PE image (at its base VA) and extract its CodeView -/// RSDS PDB identification ([`PdbId`]) — the input to resolving that module's own +/// RSDS PDB identification (`PdbId`) — the input to resolving that module's own /// symbols (e.g. `tcpip.sys` for `netstat`). /// /// `image_size` is the PE `SizeOfImage` (from the `_KLDR_DATA_TABLE_ENTRY`); the -/// read is capped at [`MODULE_IMAGE_SCAN_CAP`] and done page-by-page so a +/// read is capped at `MODULE_IMAGE_SCAN_CAP` and done page-by-page so a /// partially paged-out image (unmapped pages → zero-filled) still yields the RSDS /// when its page is resident. pub fn module_pdb_id( @@ -113,7 +113,7 @@ pub fn module_pdb_id( /// Build a ready-to-use [`SymbolResolver`](memf_symbols::SymbolResolver) for a /// loaded kernel module (e.g. `tcpip.sys`): locate it via `PsLoadedModuleList`, -/// extract its RSDS [`PdbId`], resolve the matching PDB +/// extract its RSDS `PdbId`, resolve the matching PDB /// ([`AutoProfile`](memf_symbols::AutoProfile), download/cache), and rebase its /// RVA symbols by the module's base. /// diff --git a/crates/memf-windows/src/lib.rs b/crates/memf-windows/src/lib.rs index 4d3bd5d..188ce93 100644 --- a/crates/memf-windows/src/lib.rs +++ b/crates/memf-windows/src/lib.rs @@ -144,7 +144,7 @@ pub enum Error { /// Walker-specific error. /// - /// Prefer [`WalkFailed`] for new code. + /// Prefer `WalkFailed` for new code. #[error("walker error: {0}")] Walker(String), diff --git a/crates/memf-windows/src/lsadump.rs b/crates/memf-windows/src/lsadump.rs index 23ebfbf..4cfefc2 100644 --- a/crates/memf-windows/src/lsadump.rs +++ b/crates/memf-windows/src/lsadump.rs @@ -175,7 +175,7 @@ pub(crate) fn derive_lsa_key( /// /// Navigates `SECURITY\\Policy\\Secrets` via the shared HMAP walker and, for /// each secret's `CurrVal`, decrypts the value with the Vista+ LSA key derived -/// from the SYSTEM hive's boot key ([`derive_lsa_key`]). Each result records +/// from the SYSTEM hive's boot key (`derive_lsa_key`). Each result records /// whether decryption succeeded ([`LsaSecretInfo::decrypted`]); when it is /// refused (pre-Vista hive, or SYSTEM/boot key unavailable) the raw encrypted /// bytes are surfaced instead — never a fabricated plaintext. Returns an empty diff --git a/crates/memf-windows/src/pool_tag.rs b/crates/memf-windows/src/pool_tag.rs index da5f173..fa62e1b 100644 --- a/crates/memf-windows/src/pool_tag.rs +++ b/crates/memf-windows/src/pool_tag.rs @@ -23,7 +23,7 @@ const MAX_POOL_TAG_ENTRIES: u64 = 65536; /// /// Reads the `PoolTrackTable` symbol to locate the array of /// `_POOL_TRACKER_TABLE` entries, then iterates up to `PoolTrackTableSize` -/// entries (capped at [`MAX_POOL_TAG_ENTRIES`]). Returns an empty `Vec` if +/// entries (capped at `MAX_POOL_TAG_ENTRIES`). Returns an empty `Vec` if /// the required symbols are not present (graceful degradation). /// /// # Errors diff --git a/crates/memf-windows/src/psscan.rs b/crates/memf-windows/src/psscan.rs index e5abf8d..d791277 100644 --- a/crates/memf-windows/src/psscan.rs +++ b/crates/memf-windows/src/psscan.rs @@ -70,7 +70,7 @@ fn read_phys_exact( /// True if `pid` looks like a real Windows process id: a small, non-zero /// multiple of four (the kernel allocates client ids in steps of four). fn plausible_pid(pid: u64) -> bool { - (4..=0x4_0000).contains(&pid) && pid % 4 == 0 + (4..=0x4_0000).contains(&pid) && pid.is_multiple_of(4) } /// Decode a 15-byte `ImageFileName` to a validated process name, or `None`. diff --git a/crates/memf-windows/src/registry_keys.rs b/crates/memf-windows/src/registry_keys.rs index 002b396..3974f70 100644 --- a/crates/memf-windows/src/registry_keys.rs +++ b/crates/memf-windows/src/registry_keys.rs @@ -10,8 +10,8 @@ //! `_CMHIVE.Hive` is at offset 0, the `_CMHIVE` VA serves directly). //! - In-memory hives are NOT flat: a cell index is translated to its cell VA //! through the `_HHIVE.Storage[].Map` directory (see -//! [`super::registry::cell_index_to_va`]). The root cell index comes from -//! [`super::registry::root_cell_index`] (regf `RootCell`, else default 0x20). +//! `super::registry::cell_index_to_va`). The root cell index comes from +//! `super::registry::root_cell_index` (regf `RootCell`, else default 0x20). //! - Each cell: `i32` size (negative = allocated), followed by cell data. //! - Key node (`_CM_KEY_NODE`): Signature `0x6B6E` ("nk"), Flags, LastWriteTime, //! SubKeyCount, SubKeys pointer, ValueCount, Values pointer, NameLength, Name. @@ -995,7 +995,7 @@ mod tests { let data_utf16: Vec = data_str .encode_utf16() .flat_map(u16::to_le_bytes) - .chain(std::iter::repeat(0).take(2)) // null terminator + .chain(std::iter::repeat_n(0, 2)) // null terminator .collect(); let data_len = data_utf16.len() as u32; diff --git a/crates/memf-windows/src/shimcache.rs b/crates/memf-windows/src/shimcache.rs index 18c2c20..5429de0 100644 --- a/crates/memf-windows/src/shimcache.rs +++ b/crates/memf-windows/src/shimcache.rs @@ -208,7 +208,7 @@ pub struct ShimcacheEntry { /// `ahcache.sys`, locates the `SHIM_CACHE_HANDLE` by scanning its `.data` /// section (validating each candidate's `_RTL_AVL_TABLE` against the `PAGE` /// section), then walks the `SHIM_CACHE_ENTRY` LRU list via -/// [`parse_shimcache_list`]. +/// `parse_shimcache_list`. /// /// Returns an empty `Vec` when `ahcache.sys` or its sections are absent (e.g. /// an unsupported OS/arch — this targets Win8.1+/Win10 x64 only) or no valid diff --git a/crates/memf-windows/src/types.rs b/crates/memf-windows/src/types.rs index 9f380a0..323f18d 100644 --- a/crates/memf-windows/src/types.rs +++ b/crates/memf-windows/src/types.rs @@ -595,7 +595,7 @@ pub struct RegistryHive { /// Virtual address of the `_CMHIVE`/`_HHIVE` structure. /// /// This is the address the HMAP cell walkers require: pass `base_addr` (NOT - /// [`hive_addr`](Self::hive_addr)) to [`crate::registry::cell_index_to_va`], + /// [`hive_addr`](Self::hive_addr)) to `crate::registry::cell_index_to_va`, /// [`crate::registry_keys::read_registry_values`], /// [`crate::registry_keys::walk_registry_keys`], and the svc_diff / run_keys /// navigators. They translate cell indices through `_HHIVE.Storage[].Map`, @@ -608,7 +608,7 @@ pub struct RegistryHive { /// `_HHIVE.BaseBlock` pointer — the `_HBASE_BLOCK` ("regf" header) VA. /// /// This is NOT a cell-walker input. Do NOT pass it to - /// [`crate::registry::cell_index_to_va`], `read_registry_values`, + /// `crate::registry::cell_index_to_va`, `read_registry_values`, /// `walk_registry_keys`, svc_diff, or run_keys — those need /// [`base_addr`](Self::base_addr) (the `_CMHIVE`/`_HHIVE` VA). Use /// `hive_addr` only for reading the base block itself (e.g. the regf diff --git a/crates/memf-windows/src/wdigest.rs b/crates/memf-windows/src/wdigest.rs index 4639154..c25f874 100644 --- a/crates/memf-windows/src/wdigest.rs +++ b/crates/memf-windows/src/wdigest.rs @@ -140,7 +140,7 @@ fn read_unicode_string_raw( /// Decode raw bytes as UTF-16LE if every code unit is a valid Unicode scalar. /// Returns `Some(String)` for plaintext, `None` for encrypted/binary data. fn decode_utf16le_or_none(bytes: &[u8]) -> Option { - if bytes.len() < 2 || bytes.len() % 2 != 0 { + if bytes.len() < 2 || !bytes.len().is_multiple_of(2) { return None; } let units: Vec = bytes diff --git a/src/symbol_dl.rs b/src/symbol_dl.rs index 03d545d..c9c2966 100644 --- a/src/symbol_dl.rs +++ b/src/symbol_dl.rs @@ -1,7 +1,7 @@ //! ISF symbol auto-download from the community server. //! //! Cache directory: `~/.cache/memf/symbols/` -//! Server: see [`forensicnomicon::toolchain::VOLATILITY3_VOLATILITY3_ISF_SERVER`] +//! Server: see `forensicnomicon::toolchain::VOLATILITY3_VOLATILITY3_ISF_SERVER` //! URL pattern: `/windows//.json.xz` use std::path::{Path, PathBuf}; From a5f12ff0a01600199c0277376a9df25802b83fb4 Mon Sep 17 00:00:00 2001 From: h4x0r Date: Fri, 7 Aug 2026 23:48:44 -0700 Subject: [PATCH 5/5] fix(fuzz): mark the fuzz crate as a cargo-fuzz manifest The Fuzz (build check) job never reached a build. cargo-fuzz refused the manifest outright: Error: manifest `.../fuzz/Cargo.toml` does not look like a cargo-fuzz manifest. Add following lines to override: [package.metadata] cargo-fuzz = true That marker was simply absent. cargo-fuzz names the fix in its own error text. Reproduced locally and confirmed by control: before the change `cargo fuzz list` fails with the identical message; after it, it lists all targets (fuzz_crashdump, fuzz_hiberfil, fuzz_kdump, fuzz_lime, ...). The marker was asserted present in the file before re-running, so the control cannot silently no-op. Limit of the local evidence, stated rather than glossed: `cargo +nightly fuzz build` could NOT be run to completion here. It fails on this macOS/aarch64 host with `ld: initializer pointer has no target` while linking a sanitizer- instrumented binary -- a host linker issue, not a defect in the targets. CI builds on ubuntu-latest, so the build itself is verified by the job and not by me. What is verified locally is exactly the failure CI reported: the manifest is now recognised. Co-Authored-By: Claude Opus 5 --- fuzz/Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index b5277bb..ef2b7fc 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -4,6 +4,11 @@ version = "0.0.1" edition = "2021" publish = false +# cargo-fuzz refuses a manifest without this marker: +# "does not look like a cargo-fuzz manifest" +[package.metadata] +cargo-fuzz = true + [workspace] [dependencies]