CI / build-and-test (push) Failing after 38s
Prompted by a direct question about data-integrity trustworthiness: the crash-safety design (self-checking journal records, atomic-rename compaction, Section 4.3/4.4) had never actually been tested against a real crash -- only reasoned about statically and tested against an already-corrupted file (test_pack_overlay.c, a different scenario). Added tests/test_crash_consistency.c: real fork()+SIGKILL fault injection against an actual child process, not a hand-truncated file standing in for a crash, with empirically-calibrated kill-delay sampling (measured via throwaway scripts, not guessed) so trials land genuine mid-operation interruptions rather than always completing first: - Journal-write crash injection (25 trials): kills a child mid-burst of individually-journaled, individually-fsynced writes; verifies a strict clean-prefix recovery (every completed write present and correct, every write after the kill cleanly absent, no gaps). 20-22/25 trials per run land a genuine interruption; zero corruption found. - Compaction crash injection (40 trials): kills a child mid-vfs_sync; verifies every write durably journaled *before* vfs_sync was called survives regardless of whether compaction itself completed. 40/40 trials per run land a genuine interruption; zero corruption found. A real finding from building this test, not from the code under test: the first draft reported 128 failures, every one a bug in the test itself -- it couldn't distinguish "the child was killed before ever attempting this write" from "the write completed and was then lost," since SIGKILL can't be caught to report progress. Fixed with an independent progress side-channel (plain write()+fsync() on a separate file, entirely outside packfs) as ground truth. Documented in CLAUDE.md's new "Known reliability characteristics" section, including this finding, because a claim is only as trustworthy as what's measuring it. Also investigated whether this sandbox's TSan block is actually unfixable, rather than re-asserting the known limitation: tried personality(ADDR_NO_RANDOMIZE) directly (EPERM), setarch -R (fails identically), searched TSAN_OPTIONS for a bypass (none exists), and checked capabilities/seccomp/unshare --user (zero effective caps, active seccomp filter, namespace escape also blocked). Confirmed categorical and documented as such in CLAUDE.md/CONTRIBUTING.md, rather than left as an unexamined "it doesn't work here." Also closed a real documentation gap SECURITY.md had: the pack integrity checksum's non-cryptographic nature (FNV-1a64, already documented in the context of the compaction-dedup bug) had never been explicitly connected to its *other* use, load-time pack integrity validation -- added, since it's a real, relevant caveat for anyone relying on that checksum against a deliberate adversary rather than accidental corruption. Verified: clean make all + make test (all 7 binaries), full ASan/UBSan sweep of all 7 binaries with zero real findings, and the new test run standalone 4+ times (this session) with stable, consistent results. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
108 lines
5.8 KiB
Markdown
108 lines
5.8 KiB
Markdown
# Security Policy
|
|
|
|
## Supported versions
|
|
|
|
No version has been tagged yet (see `CHANGELOG.md`); this project is
|
|
pre-1.0 (`README.md`'s "Status"). Only the latest commit on the default
|
|
branch is supported — there is no LTS branch and no backport policy while
|
|
the project is at `0.x`.
|
|
|
|
## What PackFS actually claims as a security boundary
|
|
|
|
Stated precisely, per this project's documentation standard (`CLAUDE.md`),
|
|
rather than a generic "we take security seriously":
|
|
|
|
- **Path containment for `dir` mounts is a hard requirement** (`concept.md`
|
|
Section 6, `CLAUDE.md`'s "Load-bearing constraints"). On Linux 5.6+,
|
|
every lookup beneath a `dir` mount uses `openat2(RESOLVE_BENEATH |
|
|
RESOLVE_NO_SYMLINKS)` to atomically block `..` escapes, symlink escapes,
|
|
and the canonicalize-then-open TOCTOU window in one kernel call.
|
|
`vfs_harden_process_with_landlock` (Section 6.2) is an additional,
|
|
**opt-in** kernel-level layer — not applied automatically, since
|
|
Landlock restrictions are cumulative, irreversible, and process-wide,
|
|
which would be a surprising side effect for an embeddable library to
|
|
trigger on its own.
|
|
- **On platforms or kernels without `openat2`, containment falls back to
|
|
per-component `O_NOFOLLOW` plus canonicalize-and-verify** (Section 6.3)
|
|
— an accepted, **explicitly weaker** residual-risk posture, not a claim
|
|
of equal strength. Code and documentation that describe this fallback as
|
|
equivalent to the `openat2` path would themselves be a bug worth
|
|
reporting.
|
|
- **The VFS core canonicalizes `.`/`..` in the virtual namespace itself**,
|
|
before any backend is reached, so a crafted path cannot escape a mount's
|
|
prefix even when no host directory is involved.
|
|
- **A pack file is treated as untrusted input unless it came from this
|
|
run's own compaction** (Section 7). Every index entry's offsets and
|
|
sizes are bounds-checked against the actual file/strings-region length
|
|
before being trusted; a failed check invalidates the whole pack load. An
|
|
externally-sourced pack additionally needs a checksum verified at load.
|
|
Loading an attacker-supplied `.img` file that PackFS accepts without
|
|
detecting corruption, or that causes an out-of-bounds read/write, is a
|
|
security bug. **That checksum (FNV-1a64, `src/hash.c`) is explicitly
|
|
non-cryptographic and not collision-resistant** — stated plainly here
|
|
because this is the one place in this document where it matters most:
|
|
it is a real, effective check against *accidental* corruption (a
|
|
truncated copy, a bit flip, a bad transfer), but it is not a tamper-
|
|
evident guarantee against a deliberate adversary who can choose the
|
|
bytes of a `.img` file — someone with that capability could in
|
|
principle construct a corrupted pack whose checksum still matches. Do
|
|
not treat "checksum verified at load" as a substitute for verifying the
|
|
*source* of an externally-supplied pack through some other channel if
|
|
the threat model includes a deliberate adversary, not just accidental
|
|
corruption.
|
|
- **Journal records are length/checksum self-describing** (Section 4.4),
|
|
so replay stops cleanly at the first torn or corrupted record rather
|
|
than reading past it.
|
|
- **The crash-safety design above is verified by actual fault injection,
|
|
not only by reasoning about the design.** `tests/test_crash_consistency.c`
|
|
forks a real child process and `SIGKILL`s it at randomized points during
|
|
journaled writes and during compaction, then checks what a fresh reopen
|
|
recovers, across dozens of trials per run — not a hand-truncated file
|
|
standing in for a crash. See `CLAUDE.md`'s "Known reliability
|
|
characteristics" for the methodology and results, including a real bug
|
|
the *test itself* had on its first draft (conflating "never attempted
|
|
before the kill" with "lost after completing") — fixed before treating
|
|
the crash-safety claim as verified.
|
|
|
|
## What PackFS explicitly does *not* claim
|
|
|
|
Reporting these as vulnerabilities is not necessary — they are documented,
|
|
known limitations, not silent gaps:
|
|
|
|
- **`mode` (permission bits, symlinks, hard links) is stored and restored
|
|
but not enforced.** Nothing in the VFS core interprets it as an access
|
|
control boundary in v0 (`concept.md` Section 10).
|
|
- **Cross-process concurrency is specified (an LMDB-style reader-table
|
|
design, Section 5.6) but not implemented.** Two uncoordinated processes
|
|
writing the same host directory through a `dir` mount are not
|
|
synchronized against each other.
|
|
- **`zip`/`tar` are permanently out of scope** (`CLAUDE.md`, "Project
|
|
decisions that supersede concept.md") — there is no archive-format
|
|
parsing code in this project to have a vulnerability in.
|
|
|
|
## Reporting a vulnerability
|
|
|
|
This project is hosted on Gitea, not GitHub (see `CONTRIBUTING.md`).
|
|
Please do not open a public issue on the issue tracker for a suspected
|
|
security vulnerability — report it privately, by email, to the
|
|
maintainer:
|
|
|
|
**retoor** — `retoor@molodetz.nl`
|
|
|
|
When reporting, include: the affected function(s) or file(s), a minimal
|
|
reproduction (ideally as a `tests/test_*.c`-style program using only the
|
|
public API in `include/packfs.h`), and which of the guarantees above (or
|
|
which `concept.md` section) you believe is violated — this project's own
|
|
verification discipline (`make test`, ASan/UBSan/TSan per `CONTRIBUTING.md`)
|
|
depends on being able to turn a report into a permanent regression test.
|
|
|
|
## Verification this project already does
|
|
|
|
Every change to `src/upper.c`, `src/overlay.c`, or `src/vfs.c` is required
|
|
to pass under `-fsanitize=address,undefined` and `-fsanitize=thread`
|
|
before being considered done (`CLAUDE.md`) — this process has already
|
|
caught and fixed a real heap-use-after-free in the snapshot-reclamation
|
|
logic during development (see the `reclaim_gate` note in `src/internal.h`).
|
|
`.gitea/workflows/ci.yml` (Gitea Actions) runs the full test suite plus
|
|
both sanitizer passes on every push.
|