Files
packfs/CONTRIBUTING.md
T
retoorandClaude Sonnet 5 21bcb4640f
CI / build-and-test (push) Failing after 23s
Move CI to Gitea Actions; this project is hosted on Gitea, not GitHub
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
2026-09-14 11:03:20 +00:00

6.9 KiB
Raw Blame History

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.