Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
74 changes: 74 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Fuzz

# Nightly cargo-fuzz run of every target in crates/markkit/fuzz (too slow
# for PRs). Each target checks the S17 output oracle, not just "no panic".
# Crashes upload as artifacts; reproduce with
# cargo +nightly fuzz run <target> <artifact> (from crates/markkit)
# See knowledge/security/fuzzing.md.

on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
inputs:
duration:
description: "Seconds per target (positive integer)"
required: false
default: "300"

permissions:
contents: read

env:
CARGO_TERM_COLOR: always

jobs:
fuzz:
name: Fuzz ${{ matrix.target }}
runs-on: ubuntu-latest
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
target: [html, document, docx_xml, xlsx_xml, xlsb_bin, ods_xml, xls_biff, pdf, markdown, csv, render_ir]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
with:
workspaces: crates/markkit/fuzz
- name: Install cargo-fuzz
run: cargo install cargo-fuzz --locked
- name: Cache corpus
uses: actions/cache@v4
with:
path: crates/markkit/fuzz/corpus/${{ matrix.target }}
key: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }}
restore-keys: fuzz-corpus-${{ matrix.target }}-
- name: Run ${{ matrix.target }}
working-directory: crates/markkit
env:
TARGET: ${{ matrix.target }}
DURATION_INPUT: ${{ github.event.inputs.duration || '300' }}
run: |
# workflow_dispatch input: validate before it reaches the shell.
if ! [[ "$DURATION_INPUT" =~ ^[1-9][0-9]{0,4}$ ]]; then
echo "::error::duration must be a positive integer (max 5 digits), got: $DURATION_INPUT"
exit 1
fi
# New inputs go to the first (cached) dir; the rest are read-only seeds.
mkdir -p "fuzz/corpus/$TARGET"
seeds=()
[ -d "fuzz/seeds/$TARGET" ] && seeds+=("fuzz/seeds/$TARGET")
[ "$TARGET" = html ] && seeds+=(tests/fixtures/html)
cargo +nightly fuzz run -O "$TARGET" "fuzz/corpus/$TARGET" "${seeds[@]}" -- \
-max_total_time="$DURATION_INPUT" -max_len=65536 -print_final_stats=1
- name: Upload crashes
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: fuzz-crashes-${{ matrix.target }}
path: crates/markkit/fuzz/artifacts/${{ matrix.target }}
retention-days: 30
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Fix root cause. Unsure: read more code; if stuck, ask w/ short options. Unrecogn
| formats/format-support | Formats, bytes-in API, detection order, DOCX + spreadsheet mapping |
| integrations/tool-contract | `MarkkitTool` LLM tool (`tool` feature), Everruns toolkit contract |
| security/security-model | One-page threat/invariant summary |
| security/fuzzing | cargo-fuzz targets, S17 oracle, nightly workflow, crash triage |
| security/threat-model | Threats T1-T9, mitigations, test map, resource limits, invariants S1-S21 |
| runtimes/python-package | PyO3 `markkit` module, maturin, PyPI |
| runtimes/wasm-package | wasm-bindgen `@everruns/markkit-wasm`, browser/edge, build + tests |
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
pulldown-cmark and rendered back, so raw HTML never passes through. Front matter `title:` or the
first `# ` heading becomes `Extraction::title`. Available in the CLI, Python, Node and WASM packages; `format` serializes as
`"text"` and `"markdown"`.
- Nightly cargo-fuzz targets for every parser and the renderer (`crates/markkit/fuzz`, `just fuzz`,
`.github/workflows/fuzz.yml`), each checking the output-safety oracle.
- HTML page metadata: `extract_document` fills `Extraction::title` (Open Graph, JSON-LD or
`<title>`) and the new `Extraction::metadata` (description, author, published date, language,
canonical URL, site name, lead image) from `<meta>`, Open Graph, `<link rel=canonical>` and
Expand Down Expand Up @@ -115,6 +117,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- The JSON IR of DOCX and spreadsheet tables now holds raw text (pipes were pre-escaped before).
- Math TeX (taken from page markup) renders as code, GitHub's `` $`tex`$ `` and ` ```math `
forms, instead of `$tex$` / `$$`, so it can no longer emit raw HTML or links in plain GFM.
- XLS files with truncated or inconsistent BIFF records (found by fuzzing) are rejected with an
error instead of aborting the process inside calamine. Formula token streams are ignored
(cached values were already the only thing rendered).
- Hidden content is skipped by default as a prompt-injection defence: HTML elements with `hidden`,
`aria-hidden="true"` or inline `display:none` / `visibility:hidden` / `opacity:0` / `font-size:0`,
`<template>`, and DOCX hidden (`w:vanish`) runs (warning `hidden_content_skipped`). Opt back in
Expand Down
4 changes: 4 additions & 0 deletions crates/markkit/fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target/
corpus/
artifacts/
coverage/
Loading
Loading