Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
61c93a4
feat(lib): expose internals for fuzz targets with __fuzz feature
unclesp1d3r Mar 8, 2026
d4e359c
feat(fuzz): add fuzz targets for mmap-guard functionality
unclesp1d3r Mar 8, 2026
f575508
chore(.gitignore): add fuzzing directories to ignore list
unclesp1d3r Mar 8, 2026
a34293d
feat(ci): add compatibility and fuzz workflows for testing
unclesp1d3r Mar 8, 2026
c3cf160
docs(fuzz): add fuzzing and property testing details to documentation
unclesp1d3r Mar 8, 2026
b9983fc
docs(gotchas): add notes on verifying action SHAs and fuzzing require…
unclesp1d3r Mar 8, 2026
fdf5242
feat(ci): update compatibility and fuzz workflows for pull requests
unclesp1d3r Mar 8, 2026
b17b6fb
fix(fuzz): improve error handling and assertions in fuzz targets
unclesp1d3r Mar 8, 2026
aec8806
fix(io): improve assertions in read_bounded for better error handling
unclesp1d3r Mar 8, 2026
b037dc4
test(mmap): move empty file rejection test outside proptest block
unclesp1d3r Mar 8, 2026
7188647
feat(ci): add fuzzing checks to merge conditions for improved stability
unclesp1d3r Mar 8, 2026
999f171
feat(ci): enhance CI workflows with merge queue gates for fuzzing and…
unclesp1d3r Mar 8, 2026
b44b427
feat(ci): add local CI simulation commands for workflow testing
unclesp1d3r Mar 8, 2026
31ff87d
docs(docs): add local CI usage notes and clarify permission issues
unclesp1d3r Mar 8, 2026
12d44b6
chore(deps): update release-plz to version 0.3.157
unclesp1d3r Mar 8, 2026
22986f2
style(tests): allow additional clippy lints in property tests
unclesp1d3r Mar 8, 2026
7a33c3b
style(docs): improve table formatting in testing documentation
unclesp1d3r Mar 8, 2026
8ce2c00
docs: Dosu updates for PR #10
dosubot[bot] Mar 9, 2026
a088954
docs: Dosu updates for PR #10
dosubot[bot] Mar 9, 2026
a24558f
Merge of #10
mergify[bot] Mar 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/compat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Compatibility

on:
schedule:
- cron: "0 6 * * 1" # Weekly, Monday 06:00 UTC
pull_request:
branches:
- main
workflow_dispatch:

permissions:
contents: read

defaults:
run:
shell: bash

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always

jobs:
compat:
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'mergify/merge-queue/')
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
toolchain:
- stable
- stable minus 2 releases
- stable minus 5 releases
- "1.85.0" # MSRV — edition 2024 minimum
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # master
with:
toolchain: ${{ matrix.toolchain }}

- name: Build
run: cargo build

- name: Run tests
run: cargo test
60 changes: 60 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Fuzz

on:
schedule:
- cron: "0 6 * * 1" # Weekly, Monday 06:00 UTC
pull_request:
branches:
- main
workflow_dispatch:

permissions:
contents: read

defaults:
run:
shell: bash

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always
# Duration (seconds) each fuzz target runs before moving on.
FUZZ_SECONDS: "60"

jobs:
fuzz:
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'mergify/merge-queue/')
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target: [fuzz_read_bounded, fuzz_map_file]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install nightly toolchain
uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # master
with:
toolchain: nightly

- name: Install cargo-fuzz
env:
RUSTUP_TOOLCHAIN: nightly
run: cargo install cargo-fuzz --locked

- name: Run fuzz target
env:
TARGET: ${{ matrix.target }}
RUSTUP_TOOLCHAIN: nightly
run: cargo fuzz run "$TARGET" -- -max_total_time="$FUZZ_SECONDS"

- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: fuzz-crashes-${{ matrix.target }}
path: fuzz/artifacts/${{ matrix.target }}/
retention-days: 30
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ docs/book/
.history/
.ionide

# Fuzzing (machine-generated, not committed)
fuzz/artifacts/
fuzz/corpus/

# Testing
**/mutants.out*/

Expand Down
7 changes: 7 additions & 0 deletions .mergify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ queue_rules:
- check-success = test-cross-platform (macos-latest, macOS)
- check-success = test-cross-platform (windows-latest, Windows)
- check-success = coverage
# Weekly workflows — block merge if they ran and failed, pass if absent
- check-success-or-neutral = fuzz (fuzz_read_bounded)
- check-success-or-neutral = fuzz (fuzz_map_file)
- check-success-or-neutral = compat (stable)
- check-success-or-neutral = compat (stable minus 2 releases)
- check-success-or-neutral = compat (stable minus 5 releases)
- check-success-or-neutral = compat (1.85.0)

# NOTE: The check-success conditions in pull_request_rules duplicate those in
# queue_rules.merge_conditions. This is intentional defense-in-depth: the rule
Expand Down
42 changes: 41 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,47 @@ The crate is a thin library with four source files:
- `src/map.rs` — `map_file()` with pre-flight stat check; contains the single `unsafe` block
- `src/load.rs` — `load()` routes `"-"` to `load_stdin(Some(1 GiB))`; other paths to `map_file()`. `load_stdin(max_bytes)` reads stdin into a heap buffer with optional byte cap

Runtime dependencies: `memmap2`, `fs4` (advisory file locking). Dev-dependency: `tempfile`.
Runtime dependencies: `memmap2`, `fs4` (advisory file locking). Dev-dependencies: `tempfile`, `proptest`.

## Fuzzing & Property Tests

Coverage-guided fuzzing via `cargo-fuzz` (nightly) and property tests via `proptest` (stable).

### Fuzz targets (`fuzz/`)

The `fuzz/` directory is a separate Cargo workspace (not published). It depends on `mmap-guard` with the `__fuzz` feature to access internal functions.

```bash
# Install cargo-fuzz (one-time)
cargo install cargo-fuzz --locked

# Run a fuzz target (nightly required)
cargo +nightly fuzz run fuzz_read_bounded -- -max_total_time=60
cargo +nightly fuzz run fuzz_map_file -- -max_total_time=60

# List available targets
cargo +nightly fuzz list
```

Targets:

- `fuzz_read_bounded` — structured input (`Arbitrary`) exercising the bounded-read logic with fuzzer-controlled data and cap
- `fuzz_map_file` — writes fuzzer bytes to a temp file, maps it, asserts round-trip integrity

### Property tests

- `tests/prop_map_file.rs` — proptest integration test for `map_file` round-trip
- `src/load.rs` `mod tests::prop` — proptest for `read_bounded` (unit test, has access to private API)

### `__fuzz` feature flag

The `__fuzz` feature exposes `read_bounded` (normally private) as `#[doc(hidden)] pub`. It is not part of the public API — the leading underscores signal internal-only use. Only the fuzz crate enables it.

### CI workflows

- `.github/workflows/fuzz.yml` — weekly nightly fuzzing + merge queue gate, matrix over targets, uploads crash artifacts on failure
- `.github/workflows/compat.yml` — weekly Rust version compatibility matrix (stable, stable minus 2, stable minus 5, MSRV 1.85) + merge queue gate, runs build + tests with default features
- Both fuzz and compat workflows use the two-step CI pattern: they trigger on `pull_request` but skip on regular PRs via `if: startsWith(github.head_ref, 'mergify/merge-queue/')`. Mergify's `merge_conditions` use `check-success-or-neutral` so skipped jobs pass on regular PRs but block merge if they fail in the queue.

## Lint Configuration

Expand Down
16 changes: 13 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.

## Gotchas

Before working in a specific area, check [GOTCHAS.md](GOTCHAS.md) for hard-won lessons and edge cases organized by domain. It covers unsafe code rules, clippy/rustdoc pitfalls, CI quirks, pre-commit hook behavior, and platform-specific mmap limitations.
Before working in a specific area, check [GOTCHAS.md](GOTCHAS.md) for hard-won lessons and edge cases organized by domain. It covers unsafe code rules, clippy/rustdoc pitfalls, CI quirks, pre-commit hook behavior, platform-specific mmap limitations, and fuzzing with cargo-fuzz.

## Getting Started

### Prerequisites

- **Rust 1.89+** (edition 2024, stable toolchain)
- **Rust 1.85+** (edition 2024, stable toolchain)
- **Git** for version control
- **[mise](https://mise.jdx.dev/)** for tool management (recommended)

Expand Down Expand Up @@ -45,6 +45,7 @@ All tools are managed via mise — run `mise install` to bootstrap:
- **cargo-llvm-cov** — code coverage
- **cargo-audit** / **cargo-deny** — security auditing
- **cargo-about** — third-party license notices
- **cargo-fuzz** — coverage-guided fuzzing (requires nightly)
- **just** — task runner
- **pre-commit** — git hooks
- **mdbook** — documentation
Expand Down Expand Up @@ -166,6 +167,13 @@ cargo nextest run -- --nocapture

# Check coverage
just coverage-check # 85% threshold

# Run property tests
cargo test --test prop_map_file

# Run fuzz targets (requires nightly)
cargo +nightly fuzz run fuzz_read_bounded -- -max_total_time=60
cargo +nightly fuzz run fuzz_map_file -- -max_total_time=60
```

### Writing Tests
Expand All @@ -174,6 +182,8 @@ just coverage-check # 85% threshold
- Use `#[cfg(test)]` modules with `#[allow(clippy::unwrap_used, clippy::expect_used)]`
- Include doc tests for public API examples
- Test both success and error cases
- Property tests using `proptest` are included in the test suite
- Fuzz targets live in the `fuzz/` workspace and require nightly

Example test structure:

Expand Down Expand Up @@ -232,7 +242,7 @@ All pull requests require review before merging. Reviewers check for:
- **Style** — Follows project conventions, passes `cargo fmt` and `cargo clippy -- -D warnings`
- **Documentation** — Public APIs have rustdoc with examples, AGENTS.md updated if architecture changes

CI checks run before merge, including quality checks, tests, coverage, and cross-platform tests (Ubuntu, macOS, Windows).
CI checks run before merge, including quality checks, tests, coverage, and cross-platform tests (Ubuntu, macOS, Windows). Weekly workflows run fuzzing (`fuzz.yml`) and compatibility checks across Rust versions (`compat.yml`). These weekly workflows use `check-success-or-neutral` for merge gating, allowing merges when checks are skipped on regular PRs but blocking merges if they fail in the merge queue.

### Developer Certificate of Origin (DCO)

Expand Down
9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ repository = "https://github.com/EvilBit-Labs/mmap-guard"
documentation = "https://docs.rs/mmap-guard"
keywords = ["mmap", "memmap", "memory-map", "file", "io"]
categories = ["filesystem", "memory-management"]
rust-version = "1.89"
rust-version = "1.85"
readme = "README.md"
exclude = [
# Directories
"/.github",
"/.git",
"/.vscode",
"/docs",
"/fuzz",
"/reports",
# CI / tooling config
"/.coderabbitai.yaml",
Expand Down Expand Up @@ -138,12 +139,16 @@ module_name_repetitions = "warn"
similar_names = "warn"
too_many_lines = "warn"

[features]
__fuzz = []

[dependencies]
fs4 = "0.13.1"
memmap2 = "0.9.10"

[dev-dependencies]
tempfile = "3.26.0"
proptest = "1.6.0"
tempfile = "3.26.0"

[lints]
workspace = true
Expand Down
18 changes: 18 additions & 0 deletions GOTCHAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ Referenced from [AGENTS.md](AGENTS.md) and [CONTRIBUTING.md](CONTRIBUTING.md) --
- `cargo-dist` plan/build does nothing for a library crate (no binary targets). That's why `dist-plan` is excluded from `just ci-check`.
- Mergify merge protections evaluate from the **main branch** config, not the PR branch.
- The docs workflow builds rustdoc with `--document-private-items` -- see Rustdoc section above for link pitfalls.
- Always verify pinned action SHAs with `gh api repos/{owner}/{repo}/commits/{sha} --jq '.sha'` before using them. Do not fabricate SHAs.

## Local CI with `act`

- `act` defaults to `push` event -- schedule-only workflows need `workflow_dispatch` passed as the event argument.
- `act` Docker containers run as root -- Unix permission tests (e.g., `chmod 000` → expect `PermissionDenied`) false-positive because root bypasses file permission checks.
- Use `--container-architecture linux/amd64` on Apple Silicon to avoid image pull failures.

## Pre-commit Hooks

Expand All @@ -62,6 +69,17 @@ Referenced from [AGENTS.md](AGENTS.md) and [CONTRIBUTING.md](CONTRIBUTING.md) --
- SIGBUS from concurrent file truncation is a **known, documented limitation** -- it cannot be fully prevented without advisory file locking. It is explicitly out of scope for security reports (see SECURITY.md).
- `map_file()` acquires a shared advisory lock via `fs4::fs_std::FileExt::try_lock_shared()` before mapping. Lock contention returns `WouldBlock`. The lock is held by the `File` inside `FileData::Mapped` and released on drop.

## Fuzzing

- The `__fuzz` feature flag exposes `read_bounded` as `#[doc(hidden)] pub` for fuzz targets. Do not use this feature in production or library code.
- `read_bounded` is `pub fn` in `src/load.rs` but the module is private — it is only reachable outside the crate when re-exported via `#[cfg(feature = "__fuzz")]` in `lib.rs`.
- Fuzz targets live in `fuzz/` (separate workspace, edition 2021). They require nightly and `cargo-fuzz`.
- The `fuzz/Cargo.toml` uses `edition = "2021"` (not 2024) because `cargo-fuzz` / `libfuzzer-sys` requires nightly and edition 2021 avoids compatibility issues.
- Property tests (`proptest`) run on stable and are part of the normal test suite. The `read_bounded` proptest is a unit test inside `src/load.rs` (not in `tests/`) because it needs access to the private function.
- `rust-toolchain.toml` overrides `rustup default` -- CI workflows that need nightly must set `RUSTUP_TOOLCHAIN: nightly` as an env var on the run step, not just install the toolchain.
- `read_bounded` needs `#[allow(unreachable_pub)]` and `#[allow(clippy::missing_errors_doc)]` because it's `pub` (for re-export) in a private module -- clippy flags both even though the function is `#[doc(hidden)]`.
- `#[derive(Arbitrary)]` generates code referencing `arbitrary::` by path -- `use libfuzzer_sys::arbitrary::{self, Arbitrary}` requires the `self` import. Do not remove it; the derive macro will fail without it.

## load / load_stdin

- `load("-")` delegates to `load_stdin(Some(1_073_741_824))` (1 GiB default cap). Callers needing a custom limit should call `load_stdin(Some(n))` directly.
Expand Down
40 changes: 40 additions & 0 deletions docs/src/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,39 @@ This installs the Rust toolchain, cargo extensions (nextest, llvm-cov, audit, de
| `just docs-build` | Build mdBook + rustdoc |
| `just docs-serve` | Serve docs locally with live reload |

### Running Tests

**Standard tests:**

```bash
just test
```

**Property-based tests:**

```bash
cargo test --test prop_map_file
```

Property tests use [proptest](https://github.com/proptest-rs/proptest) to verify round-trip integrity with randomized inputs.

**Fuzz tests:**

Fuzzing requires nightly Rust and `cargo-fuzz`. Install it first:

```bash
cargo install cargo-fuzz
```

Run a specific fuzz target (available targets: `fuzz_read_bounded`, `fuzz_map_file`):

```bash
cargo +nightly fuzz run fuzz_read_bounded
cargo +nightly fuzz run fuzz_map_file
```

Fuzz tests use the `__fuzz` feature flag to expose internal APIs for testing. This feature is for internal use only and should not be enabled in production code.

## Pre-commit Hooks

Pre-commit hooks run automatically on `git commit`:
Expand All @@ -49,3 +82,10 @@ The GitHub Actions CI runs on every push to `main` and on pull requests:
2. **test** — nextest + release build
3. **test-cross-platform** — Linux (x2), macOS, Windows
4. **coverage** — llvm-cov uploaded to Codecov

**Weekly scheduled workflows:**

- **fuzz** — runs fuzzing tests (`fuzz_read_bounded`, `fuzz_map_file`) with nightly Rust. Also runs on merge queue PRs.
- **compat** — tests Rust version compatibility across stable, stable-2, stable-5, and MSRV 1.85. Also runs on merge queue PRs.

These weekly workflows use `check-success-or-neutral` conditions for merge gating, allowing merges when the checks pass or are skipped.
8 changes: 4 additions & 4 deletions docs/src/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ just test-all

Tests are co-located with their source modules using `#[cfg(test)]` blocks:

| Module | Tests |
| -------------- | ------------------------------------------------------ |
| `file_data.rs` | `Deref`/`AsRef` impls, empty variant |
| `map.rs` | Successful mapping, empty file rejection, missing file |
| Module | Tests |
| -------------- | ---------------------------------------------------------------------------------------------------------------- |
| `file_data.rs` | `Deref`/`AsRef` impls, empty variant |
| `map.rs` | Successful mapping, empty file rejection, missing file |
| `load.rs` | File loading via mmap, stdin handling with byte caps, path resolution (`"-"` routing), empty/missing file errors |

### Clippy in Tests
Expand Down
Loading