Per explicit direction: this repository is not going on GitHub. Moved .github/workflows/ci.yml to .gitea/workflows/ci.yml (Gitea Actions' convention) and updated every doc that referenced the old path or assumed GitHub-specific features: - .gitea/workflows/ci.yml: added a header comment on the two things that are genuinely Gitea-specific and instance-dependent, not just a renamed file -- `runs-on: ubuntu-latest` must match a label the actual registered Gitea runner advertises (there is no GitHub-hosted-runner equivalent, this is self-hosted), and `actions/checkout@v4` resolves against whatever action source that runner is configured with. - CONTRIBUTING.md: corrected a claim that no longer holds -- it previously said CI "runs on a normal, unrestricted GitHub Actions VM where TSan is expected to work"; since this is actually a self-hosted Gitea runner whose environment isn't controlled by this repo, that assumption isn't something this repo can vouch for, so the text now says so rather than carrying the old (GitHub-shaped) assumption forward silently. - SECURITY.md: removed a claim this project can't back up (that "private security advisories" are available once hosted -- that's a GitHub feature this repo never had access to); reporting is by direct email to the maintainer only. - README.md: fixed a real gap while in here -- the "Security" section never actually linked to SECURITY.md despite it existing since the previous commit. - CLAUDE.md, CHANGELOG.md: updated path references; CLAUDE.md's self-evaluation section (a historical record of an audit finding) keeps the old .github path where it describes what was literally true at that time, with a note explaining the rename, rather than rewriting history. Verified: clean make all + make test, all 6 binaries pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
6.9 KiB
Contributing to PackFS
Before changing anything
Read concept.md in full. It is the project's specification,
not background reading — every backend, lock, and on-disk field in src/
exists because a section of concept.md requires it, and most functions'
comments cite the section they implement rather than re-explaining it.
concept.md is frozen (see CLAUDE.md): if you believe the
design itself needs to change, that belongs in a new document that amends
or supersedes it, not in an edit to concept.md.
Then read CLAUDE.md, which states the load-bearing constraints
a change must respect (static linking, the write model, the concurrency
model, path containment, pack integrity) and the documentation register this
project is written in.
Workflow
make test # must pass before any PR
cc ... -fsanitize=address,undefined # ASan/UBSan: see .gitea/workflows/ci.yml for exact flags
cc ... -fsanitize=thread # TSan, for anything touching src/upper.c, src/overlay.c, or src/vfs.c
Any change to the concurrency-sensitive files (upper.c, overlay.c,
vfs.c) must be run under ThreadSanitizer, not just the plain test suite —
a data race there is exactly the class of bug Section 5 of concept.md
exists to prevent, and the plain build will not surface it.
Known environment limitation, stated plainly rather than glossed over:
ThreadSanitizer cannot run at all in some sandboxed/containerized
development environments — including the one this project's own commit
history was largely developed in — because TSan requires disabling ASLR
for itself via personality(ADDR_NO_RANDOMIZE), and some sandboxes block
that syscall outright (confirmed here: personality() returns EPERM,
and even a trivial unrelated pthread program fails identically with
FATAL: ThreadSanitizer: unexpected memory mapping, not just PackFS code).
If your environment can't run TSan, say so rather than silently skipping
it or claiming verification that didn't happen — ASan/UBSan still catch
real bugs (they found and fixed a genuine heap-use-after-free during this
project's development, see the reclaim_gate note in internal.h) but do
not do TSan's happens-before race analysis, so they are not a substitute
for it. CI (.gitea/workflows/ci.yml, Gitea Actions — this project is
hosted on Gitea, not GitHub) runs on this project's own self-hosted
runner; whether that runner can execute TSan depends on that runner's own
environment, which is not controlled by this repository the way a
hosted-runner VM would be — do not assume CI's TSan step passing means
what it would on an unrestricted machine without having actually checked
what environment the registered runner provides. Treat a change as
TSan-verified only once it has actually passed on a machine confirmed able
to run it, not merely because a CI step reported success.
A second, separate environment quirk, also observed directly rather than
assumed: in the same kind of sandboxed environment, an ASan/UBSan-built
test binary occasionally (non-deterministically, roughly 1 run in 5–10 in
this project's own experience) fails to start at all, printing
AddressSanitizer:DEADLYSIGNAL — and, in its worse form, printing that same
line in an unbounded loop rather than printing it once and exiting, which
can run for minutes and consume unbounded CPU/memory if left unattended.
This has been observed hitting different, unrelated test binaries from run
to run (in one session: test_dir and test_mem, in another: nothing at
all, in another: test_pack_overlay), which — together with the fact that
every one of those binaries passes cleanly on a repeat run — indicates a
sandbox startup race (plausibly in the same family as the ASLR/mapping
restrictions behind the TSan limitation above), not a bug in the binary
being tested. Always wrap sanitizer-build test runs in timeout in such
an environment (e.g. timeout 30 ./build/san/test_name) so a stuck run
fails loudly and bounded instead of hanging; treat a DEADLYSIGNAL exit as
inconclusive, re-run, and only treat the run as a real finding if the output
contains an actual ERROR: AddressSanitizer or runtime error: string —
DEADLYSIGNAL alone, with neither of those strings present, is this flake,
not a memory-safety bug in PackFS. As with the TSan limitation, the correct
response is to say this plainly and re-run until a clean pass is obtained
(or hand off to CI, which runs on an unrestricted VM where this has not been
observed), not to suppress or silently ignore a DEADLYSIGNAL exit.
A change to the index structure in upper.c (the TreapNode/treap_*/
node_* functions, snapshot_upsert, snapshot_remove, or the
UpperSnapshot/UpperEntry representation) should be run through
make bench before and after, not just make test: BENCH.md records a
real, measured O(n²) cost the flat array this index used to be had in bulk
sequential create/unlink, and the persistent treap that replaced it exists
specifically to avoid regressing back into it — a correctness-preserving
change to this code can still reintroduce that regression (e.g. an
implementation that silently degrades to a linked list under adversarial
insertion order) in a way make test alone cannot detect, only make bench
plus tests/test_index_stress.c's randomized-order correctness check can.
Scope
Changes that add functionality concept.md Section 10 lists as explicitly
out of scope for v0 (full POSIX semantics, enforced permissions/symlinks/hard
links, cross-process concurrency, content-defined chunking/delta
compression) should discuss the tradeoff with a maintainer first — those
exclusions were deliberate design decisions, not gaps waiting to be filled.
zip and tar import/export backends will not be accepted, full stop —
not discussed, not behind a flag. concept.md Section 11 recommends them,
but that recommendation is permanently superseded; see CLAUDE.md, "Project
decisions that supersede concept.md." This is a harder line than the v0
exclusions above, which are open to future discussion — this one is not.
Static-linking constraint
concept.md Section 11.1 is a hard constraint: the core must build with
zero required third-party libraries and zero dynamic loading. In practice
this project has no optional-dependency boundary at all: concept.md
described one at the zip/tar backend for miniz/libarchive, but per
the decision above, that backend will never exist, so there is no path by
which a third-party dependency enters this codebase — a change proposing
one, for any backend, will not be accepted.
Style
Match the register already in the file you're editing — precise,
constraint-labeled comments citing the concept.md section they implement,
no filler. See the "Documentation standard" section of CLAUDE.md for the
full rule; it applies to code comments and commit messages, not only to
.md files.