Skip to content

Security Deep

Security Deep #244

Workflow file for this run

name: Security Deep
on:
schedule:
- cron: '0 20 * * *' # 20:00 UTC = 7 AM AEDT next day
workflow_dispatch: # Allow manual triggers
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
# Deep security analysis (< 2 hours) - nightly scheduled
kani-verification:
name: Kani Formal Verification
runs-on: cachekit
timeout-minutes: 30
env:
# Pin rustup/cargo to /tmp (single fs) to avoid EXDEV on toolchain staging,
# and so the kani-verifier binary lands in a known, PATH-able location. The
# runner's default CARGO_HOME/bin is not on PATH, so cargo-kani exited 127.
RUSTUP_HOME: /tmp/rustup
CARGO_HOME: /tmp/cargo
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Put cargo bin on PATH
run: echo "/tmp/cargo/bin" >> "$GITHUB_PATH"
- name: Ensure stable toolchain
run: |
# Runner image ships stable; only install if missing.
# Full `toolchain install` can hit EXDEV in containers (rust-lang/rustup#1239).
rustup toolchain list | grep -q stable || rustup toolchain install stable
- name: Install Kani
run: |
cargo +stable install --locked kani-verifier --version 0.67.0
cargo-kani setup
- name: Run Kani verification
run: |
cd rust
cargo-kani --tests --no-default-features --features compression,checksum,messagepack,encryption
fuzzing:
name: Extended Fuzzing (3 targets × 1h)
runs-on: cachekit
timeout-minutes: 200
env:
# Avoid EXDEV "cross-device link" errors when rustup stages a nightly
# toolchain across overlay/hostPath boundaries on the ARC runner pod
# (rust-lang/rustup#1239). Same pattern as fuzz-smoke.yml.
RUSTUP_HOME: /tmp/rustup
CARGO_HOME: /tmp/cargo
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install nightly Rust
run: |
# Pin nightly: cargo-fuzz 0.13.1 → rustix uses rustc_layout_scalar_valid_range_*
# attributes reserved after nightly-2026-04-27. Last known-good date.
# Same pin as fuzz-smoke.yml.
rustup toolchain install nightly-2026-04-27
rustup default nightly-2026-04-27
- name: Install cargo-fuzz
run: cargo install --locked cargo-fuzz
- name: Fuzz byte_storage_compress (1 hour)
run: |
cd rust
timeout 3600 cargo fuzz run byte_storage_compress --no-default-features --features compression,checksum || true
- name: Fuzz byte_storage_decompress (1 hour)
run: |
cd rust
timeout 3600 cargo fuzz run byte_storage_decompress --no-default-features --features compression,checksum || true
# NOTE: encryption_roundtrip and 8 other encryption targets are stale against
# cachekit-core 0.1.1 (encrypt_aes_gcm → encrypt_with_keys). See #114. Using
# encryption_key_derivation, which compiles, until those targets are migrated.
- name: Fuzz encryption_key_derivation (1 hour)
run: |
cd rust
timeout 3600 cargo fuzz run encryption_key_derivation --no-default-features --features encryption || true
- name: Check for crashes
run: |
cd rust/fuzz
CRASHES=$(find artifacts -name "crash-*" 2>/dev/null | wc -l)
if [ "$CRASHES" -gt 0 ]; then
echo "❌ Found $CRASHES crashes during fuzzing"
find artifacts -name "crash-*" -exec echo "Crash: {}" \;
exit 1
fi
echo "✅ No crashes found during fuzzing"
- name: Generate coverage report
run: |
cd rust
for target in byte_storage_compress byte_storage_decompress encryption_key_derivation; do
echo "=== Coverage for $target ==="
cargo fuzz coverage $target || true
done
atheris-fuzzing:
name: Atheris Python-Rust Fuzzing
runs-on: cachekit
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# Pin to 3.11: atheris 2.3.0's newest prebuilt wheel is cp311. On a newer
# interpreter uv builds atheris from sdist, which needs clang + libFuzzer
# (find_libfuzzer.sh) — absent on the self-hosted runner, so install fails
# before any fuzzing runs. Both uv sync and uv run must pin, or uv run
# re-resolves to a different interpreter and re-triggers the source build.
- name: Install dependencies
run: |
uv sync --group dev --group fuzz --python 3.11
- name: Run Atheris fuzz targets (10 min each)
run: |
for fuzz_target in tests/fuzzing/fuzz_*.py; do
if [ -f "$fuzz_target" ]; then
echo "Fuzzing $fuzz_target..."
timeout 10m uv run --python 3.11 python "$fuzz_target" -max_total_time=600 || true
fi
done
- name: Upload crash corpus
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: atheris-crashes-${{ github.run_id }}
path: tests/fuzzing/corpus/
- name: Report fuzzing results
if: always()
run: |
if [ -d tests/fuzzing/corpus/ ] && find tests/fuzzing/corpus/ -mindepth 1 ! -name '.gitignore' -print -quit | grep -q .; then
echo "⚠️ Crashes discovered during Atheris fuzzing!"
ls -lh tests/fuzzing/corpus/
exit 1
fi
echo "✅ No crashes discovered during Atheris fuzzing"
miri-full:
name: Miri Full Suite
runs-on: cachekit
timeout-minutes: 30
env:
# Avoid EXDEV "cross-device link" errors when rustup stages a nightly
# toolchain across overlay/hostPath boundaries on the ARC runner pod
# (rust-lang/rustup#1239). Same pattern as fuzz-smoke.yml.
RUSTUP_HOME: /tmp/rustup
CARGO_HOME: /tmp/cargo
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install nightly Rust with Miri
run: |
rustup toolchain install nightly --component miri
rustup default nightly
- name: Run full Miri test suite
run: |
cd rust
cargo miri test --no-default-features --features compression,checksum,messagepack,encryption
sanitizers:
name: Sanitizers (ASan, TSan, MSan)
runs-on: cachekit
timeout-minutes: 40
env:
# Avoid EXDEV "cross-device link" errors when rustup stages the nightly
# toolchain / rust-src across overlay/hostPath boundaries on the ARC runner
# pod (rust-lang/rustup#1239). Same pattern as fuzzing / miri-full jobs.
RUSTUP_HOME: /tmp/rustup
CARGO_HOME: /tmp/cargo
strategy:
fail-fast: false
matrix:
sanitizer: [address, thread, memory]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install nightly Rust
run: |
rustup toolchain install nightly
rustup default nightly
- name: Add source for sanitizers
if: matrix.sanitizer == 'memory' || matrix.sanitizer == 'thread'
run: rustup component add rust-src --toolchain nightly
- name: Run AddressSanitizer
if: matrix.sanitizer == 'address'
env:
RUSTFLAGS: -Zsanitizer=address
run: |
cd rust
cargo +nightly test --lib --target x86_64-unknown-linux-gnu \
--no-default-features --features compression,checksum,messagepack,encryption
- name: Run ThreadSanitizer
if: matrix.sanitizer == 'thread'
env:
RUSTFLAGS: -Zsanitizer=thread
TSAN_OPTIONS: suppressions=${{ github.workspace }}/rust/tsan_suppressions.txt
run: |
cd rust
cargo +nightly test --lib --target x86_64-unknown-linux-gnu \
-Zbuild-std --no-default-features --features compression,checksum,messagepack,encryption
- name: Run MemorySanitizer
if: matrix.sanitizer == 'memory'
env:
RUSTFLAGS: -Zsanitizer=memory -Zsanitizer-memory-track-origins
run: |
cd rust
cargo +nightly test --lib --target x86_64-unknown-linux-gnu \
-Zbuild-std --no-default-features --features compression,checksum,messagepack,encryption
generate-security-report:
name: Generate Security Report
runs-on: cachekit
needs: [kani-verification, fuzzing, atheris-fuzzing, miri-full, sanitizers]
if: always()
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Generate security report
run: |
mkdir -p reports/security
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
REPORT="reports/security/deep-analysis-$TIMESTAMP.md"
cat > "$REPORT" <<EOF
# Deep Security Analysis Report
Generated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
Workflow Run: ${{ github.run_id }}
## Summary
- **Kani Verification**: ${{ needs.kani-verification.result }}
- **Extended Fuzzing**: ${{ needs.fuzzing.result }}
- **Atheris Python-Rust Fuzzing**: ${{ needs.atheris-fuzzing.result }}
- **Miri Full Suite**: ${{ needs.miri-full.result }}
- **Sanitizers**: ${{ needs.sanitizers.result }}
## Results
### Formal Verification (Kani)
Status: ${{ needs.kani-verification.result }}
- ByteStorage roundtrip properties
- Checksum integrity verification
- Decompression bomb protection
- Encryption roundtrip correctness
- Key derivation determinism
### Fuzzing (3 hours total)
Status: ${{ needs.fuzzing.result }}
- byte_storage_compress (1 hour)
- byte_storage_decompress (1 hour)
- encryption_key_derivation (1 hour)
### Undefined Behavior Detection (Miri)
Status: ${{ needs.miri-full.result }}
- Full test suite with strict provenance
- Symbolic alignment checking
- Memory leak detection
### Runtime Sanitizers
Status: ${{ needs.sanitizers.result }}
- AddressSanitizer (memory errors)
- ThreadSanitizer (race conditions)
- MemorySanitizer (uninitialized memory)
## Conclusion
$(if [[ "${{ needs.kani-verification.result }}" == "success" ]] && \
[[ "${{ needs.fuzzing.result }}" == "success" ]] && \
[[ "${{ needs.miri-full.result }}" == "success" ]] && \
[[ "${{ needs.sanitizers.result }}" == "success" ]]; then
echo "✅ All deep security checks passed"
else
echo "❌ One or more deep security checks failed"
fi)
EOF
cat "$REPORT"
- name: Archive security report
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: security-deep-report
path: reports/security/
retention-days: 90
# Summary job
security-deep-success:
name: Security Deep Success
runs-on: ubuntu-latest
needs: [kani-verification, fuzzing, atheris-fuzzing, miri-full, sanitizers]
if: always()
permissions:
contents: read
issues: write
steps:
- name: Check all deep security checks passed
run: |
if [[ "${{ needs.kani-verification.result }}" != "success" ]] || \
[[ "${{ needs.fuzzing.result }}" != "success" ]] || \
[[ "${{ needs.atheris-fuzzing.result }}" != "success" ]] || \
[[ "${{ needs.miri-full.result }}" != "success" ]] || \
[[ "${{ needs.sanitizers.result }}" != "success" ]]; then
echo "❌ One or more deep security checks failed"
echo "Kani: ${{ needs.kani-verification.result }}"
echo "Fuzzing: ${{ needs.fuzzing.result }}"
echo "Atheris: ${{ needs.atheris-fuzzing.result }}"
echo "Miri: ${{ needs.miri-full.result }}"
echo "Sanitizers: ${{ needs.sanitizers.result }}"
exit 1
fi
echo "✅ All deep security checks passed"
# Surface nightly failures: a schedule-only job that fails silently is worse
# than no job. De-duplicate by title so we don't open a new issue every night.
- name: File / update tracking issue on failure
if: failure()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TITLE: "Security Deep nightly is failing"
run: |
body=$(printf '%s\n' \
"Security Deep failed on run ${{ github.run_id }}." \
"" \
"- Kani: ${{ needs.kani-verification.result }}" \
"- Fuzzing: ${{ needs.fuzzing.result }}" \
"- Atheris: ${{ needs.atheris-fuzzing.result }}" \
"- Miri: ${{ needs.miri-full.result }}" \
"- Sanitizers: ${{ needs.sanitizers.result }}" \
"" \
"Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}")
existing=$(gh issue list --repo "${{ github.repository }}" --state open \
--search "$TITLE in:title" --json number --jq '.[0].number // empty')
if [ -n "$existing" ]; then
gh issue comment "$existing" --repo "${{ github.repository }}" --body "$body"
else
gh issue create --repo "${{ github.repository }}" --title "$TITLE" --label bug --body "$body"
fi