Commit Graph
12 Commits
Author SHA1 Message Date
retoorandClaude Sonnet 5 0c0ae4f747 CI: don't hard-fail on the confirmed TSan-can't-start-here runner limitation
CI / build-and-test (push) Failing after 23s
The real Gitea Actions run on this project's own registered runner just
failed exactly as CONTRIBUTING.md already anticipated it might:
FATAL: ThreadSanitizer: unexpected memory mapping, the same signature
already documented as a sandbox/container seccomp restriction blocking
personality(ADDR_NO_RANDOMIZE), which TSan needs to start at all. Build,
test suite, and ASan/UBSan all passed -- only the TSan step failed, on an
environment issue, not a code issue.

Fixed the CI step itself rather than just noting the failure: it now
classifies each binary's TSan run as PASS, a known flake (that exact
FATAL signature and nothing indicating an actual race was found), or a
real failure (anything else -- a genuine data race, a crash, any other
error). Only a real failure fails the build. Verified the classification
logic locally against four cases (a synthetic real race report, a plain
assertion failure, the known flake signature alone, and a clean pass) --
each classified correctly -- and against this sandbox's own six test
binaries, all six of which hit the real flake (this sandbox has never
been able to run TSan either) and correctly did not fail the build.

This does NOT mean TSan verification is happening in CI -- it means CI no
longer conflates "TSan couldn't start" with "the build is broken."
Updated CONTRIBUTING.md and CLAUDE.md from "whether the runner can execute
TSan is unverified" (a hedge) to the now-confirmed fact that it can't, and
corrected an overclaim in README.md that the suite is "regularly run under
ThreadSanitizer" -- as far as this project has been able to confirm, TSan
has not actually completed a run in any environment it's been built in
yet, local sandbox or CI. ASan/UBSan remain the real, run, load-bearing
sanitizer coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
2026-09-14 11:53:08 +00:00
retoorandClaude Sonnet 5 21bcb4640f Move CI to Gitea Actions; this project is hosted on Gitea, not GitHub
CI / build-and-test (push) Failing after 23s
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
v0.1.0
2026-09-14 11:03:20 +00:00
retoorandClaude Sonnet 5 a9143308f5 CHANGELOG.md: reflect that v0.1.0 is now tagged, not still Unreleased
The v0.1.0 tag was created in the same session right after this file was
written, leaving it self-inconsistent: a tagged v0.1.0 whose own
CHANGELOG.md still said "no version has been tagged yet" under an
[Unreleased] heading. Fixed by renaming the heading to "[0.1.0] -
2026-09-14" and updating the framing paragraph.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
2026-09-14 10:59:09 +00:00
retoorandClaude Sonnet 5 419182bb05 Add version API, pkg-config, SPDX headers, SECURITY.md, CHANGELOG.md
Project-hygiene pass toward being a properly citable, embeddable,
professionally-packaged C library rather than just working code:

- PACKFS_VERSION_MAJOR/MINOR/PATCH/STRING in include/packfs.h, the single
  source of truth for the project's version, plus a runtime pfs_version()
  (src/vfs.c, next to vfs_new/vfs_free) so a dynamically-linked consumer
  can check ABI/API compatibility without recompiling. Covered by a new
  assertion in tests/test_mem.c that the macro and the runtime function
  never disagree.
- packfs.pc.in + a `make install` rule that generates packfs.pc with its
  Version: field derived from PACKFS_VERSION_STRING via a Makefile-level
  grep/sed, never hand-maintained separately -- verified end-to-end with a
  scratch `make install PREFIX=...` + `pkg-config --cflags --libs packfs`
  + `make uninstall`, not just by reading the rule.
- SPDX-License-Identifier: MIT added to every src/*.c and src/internal.h
  (include/packfs.h already had one); the whole distributed source tree
  now carries consistent machine-readable license metadata.
- SECURITY.md, stating precisely what this project's containment and pack-
  integrity code actually claims as a security boundary (concept.md
  Section 6/7) versus what it explicitly does not (unenforced `mode`, no
  cross-process concurrency) -- not generic boilerplate -- with a real
  reporting contact rather than a placeholder.
- CHANGELOG.md (Keep a Changelog format), summarizing the real history in
  git log to date; explicitly notes no version is tagged yet.

Verified: clean `make all` + `make test` (all 6 binaries, including the
new version-check assertion), and a full ASan/UBSan sweep of all 6
binaries with zero real findings (one run hit the already-documented
DEADLYSIGNAL sandbox flake on test_pack_write_perf across all 3 retries;
re-verified directly afterward with 5/5 additional clean passes and timing
well under any plausible timeout, confirming it was the flake, not a
regression, before treating this as done).

Deliberately not done here, by the user's explicit choice: no
CODE_OF_CONDUCT.md, and no git remote/publishing -- this repository still
has neither, and both are decisions left to the maintainer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
2026-09-14 10:58:35 +00:00
retoorandClaude Sonnet 5 529935deb4 BENCH.md: fix numbers left stale by the After-table replacement
Doing another literal, no-caveats audit pass (per the standing request to
verify literally everything, not just reassert it) found real, concrete
inconsistencies introduced by the previous commit: it replaced BENCH.md's
"After: complete results" table and Appendix B with a newer, more complete
bench/bench.c run (adding the mount-table-scaling category), but several
other places in the same document still quoted exact figures from the
older run those tables used to hold -- a reader cross-checking the
"Resolution" section's headline table, or the "Analysis" section's bullet
points, against the "After" table below them would have found contradictory
numbers for the same measurement.

Verified programmatically, not just by re-reading: every row's time,
throughput, and MB/s in the Before (45 rows) and After (54 rows) tables now
matches its corresponding appendix line exactly (0 mismatches out of 99
rows, checked with a script, not by eye).

Fixed:
- The "Resolution" section's headline before/after table used the older
  run's mem create/unlink/mkdir/rmdir numbers (0.0372s/0.0320s/0.0052s/
  0.0045s), which don't match the current After table (0.0400s/0.0179s/
  0.0056s/0.0040s) -- for unlink specifically, a genuine near-2x difference
  between two runs of identical, already-fixed code, not just noise. Now
  points at the single current run, with speedups recomputed (239x/590x/
  60x/85x/11x/21x/134x, mem now 25-27x faster than raw fs, complexity-class
  check now 7.14x vs the old 7.15x -- nearly identical, which is the
  reassurance that actually matters), plus an explicit note on the observed
  run-to-run variance rather than silently picking one number.
- The "Analysis" section's read/stat/random-read/compaction/dir-stat/
  large-write bullets all cited the older run's exact ops/s and MB/s
  figures (2.4M small-file reads, ~815K compaction entries/s, 133K dir
  stat, etc.); recomputed against the current After table (now 15-16x for
  small reads, ~9x for stat, ~91MB/s / ~748K entries/s for compaction, 145K
  vs 286K for dir stat, corrected large-write MB/s ranges).
- The same stale 65-330x / 15-27x / 22x range in CLAUDE.md's "Known
  performance characteristics" bulk-write bullet, updated to match and to
  explain the same run-to-run variance rather than presenting one run's
  numbers as if they were exact.

No code changes; make test still passes (all 6 binaries, unaffected by a
documentation-only change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
2026-09-14 10:13:57 +00:00
retoorandClaude Sonnet 5 684c6d95de Fix pack_write's O(n^2) dedup + correctness bug; document mount table O(n^2)
A audit for other instances of the file-index O(n^2) shape (fixed
previously via the persistent treap) found two more real issues:

1. pack_write's Section 9.2 exact-duplicate elimination was a linear scan
   of every previously-seen (hash, size) pair per entry -- O(n^2) total,
   invisible in the existing benchmark because its identical-content test
   files made every scan match on the first comparison. Measured with
   unique content instead: 80,000 entries took 1.74s, with a 20,000->80,000
   step showing 15.6x for a 4x-N step, matching O(n^2)'s 16x prediction.
   The same scan also trusted a (hash, size) match without ever comparing
   actual bytes -- a latent correctness bug, since FNV-1a64 is explicitly
   not collision-resistant. Fixed both at once with an open-addressing hash
   table (load factor 1/2, linear probing) plus a memcmp verification
   before ever reusing a data_off. Post-fix: 80,000 entries in 0.044s
   (39.6x faster), ratio drops to 3.35x (consistent with O(n)).

   Covered permanently by two new/extended tests: a white-box assertion in
   test_pack_overlay.c that duplicate-content entries share one data_off
   and distinct-content entries do not, and a new
   tests/test_pack_write_perf.c regression tripwire against 10,000 unique
   entries.

2. vfs.c's mount table uses the same full-array-copy-per-write pattern the
   file index used to, confirmed O(n^2) via a new bench/bench.c category
   (500/2,000/8,000 mounts, both 4x-N steps showing 15-20x). Deliberately
   NOT rewritten: mount points are created by a program's own source code,
   not workload-driven, so realistic mount counts never reach the scale
   that made the file index's O(n^2) a real problem. Documented with full
   reasoning in BENCH.md and CLAUDE.md rather than silently left as an
   undocumented gap.

Also fixes a real CI gap the new tests exposed: ci.yml's sanitizer-build
steps never passed -D_GNU_SOURCE when compiling test files (only the
library .o's got it), which was harmless while no test included
internal.h and became a link failure once two did (internal.h needs
_GNU_SOURCE for pthread_rwlock_t). And documents, in CONTRIBUTING.md and
CLAUDE.md, a sandbox flake observed directly during this work's own
sanitizer runs: ASan/UBSan test binaries occasionally fail to start with
AddressSanitizer:DEADLYSIGNAL (sometimes looping rather than exiting),
non-deterministically hitting different unrelated binaries across runs --
a startup race, not a memory-safety bug, confirmed by clean passes on
retry; sanitizer runs in such an environment should be timeout-wrapped.

BENCH.md's "After" table and Appendix B are replaced with the current,
complete 54-measurement bench/bench.c run (the original 45 plus the new
mount-scaling category); the pre-fix 45-measurement "Before" table is kept
as the historical record, per this project's documentation standard.

Verified: make test (all 6 binaries, including the 2 new/changed), a clean
make all, and repeated ASan+UBSan runs (0 real findings; the DEADLYSIGNAL
flake above was observed and correctly distinguished from a real finding
by re-running until a clean pass). TSan could not be run in this sandbox
(pre-existing, documented environment limitation).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
2026-09-14 09:55:47 +00:00
retoorandClaude Sonnet 5 edbf97b3f1 BENCH.md: literal, complete record -- every row, plus raw appendices
The prior BENCH.md curated the benchmark output into summary tables
and, in doing so, actually dropped data: the rmdir measurements were
missing entirely, and the large-file read rows were never tabulated
(only write was), for both the before and after runs. Not what
"document this literally" asked for.

Fixed: the "Before"/"After" tables now reproduce all 45 measurements
each run's summary block reported, none omitted or combined. Added
two appendices with the complete verbatim program output for both
runs, exactly as captured, as the primary source record -- the curated
tables are a presentation of that data, not a substitute for it.
Cross-checked every value in both tables against the verbatim
appendices; they match exactly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
2026-09-14 08:53:04 +00:00
retoorandClaude Sonnet 5 64283d587b Fix the O(n^2) bulk create/unlink: flat array -> persistent treap
BENCH.md's earlier finding was real: every structural write (create/
unlink/mkdir) copied the entire sorted UpperSnapshot entry array before
publishing the next snapshot, making bulk sequential creation O(n^2).
concept.md Section 5.3 names the exact condition for reconsidering this
("a persistent structurally-shared tree structure is not required
until this assumption is empirically violated") -- that condition was
measured, not hypothesized, so this closes it rather than leaving it
as a documented-but-open limitation.

The index is now a persistent treap (src/upper.c): a structural write
copies only the O(log n) nodes on the path to the change, sharing
every other node (its own refcount, cascading like MutCell's and
UpperSnapshot's) with whichever snapshot(s) it was built from. Chosen
over a persistent AVL/red-black/weight-balanced tree because deletion
in those can need O(log n) rebalancing rotations -- each a real
allocation in a persistent setting -- where a treap needs only O(1)
amortized rotations for insert and delete (Seidel & Aragon 1996), with
expected O(log n) height regardless of insertion order, including the
sorted-by-creation-order pattern that made the flat array quadratic in
the first place. Liljenzin's "Confluently Persistent Sets and Maps"
(arXiv:1301.3388) documents persistent treaps giving O(1) snapshots
for MVCC specifically, which is this exact use case. Full reasoning
and the ownership convention (functions consume one ref of their tree
arguments, return one owned ref) are in upper.c's comment above
struct TreapNode.

Blast radius kept deliberately small: snapshot_upsert/snapshot_remove
keep their exact original signatures, so upper_create/upper_mkdir/
upper_remove/upper_rename/upper_copy_up needed zero changes.
upper_lookup/upper_has_children keep their exact contracts. Only
upstd_readdir and overlay_readdir's manual array scans became calls to
a new upper_visit_range (O(log n + r) range query, replacing an O(n)
scan in both, a bonus fix beyond what was strictly necessary) since
there's no flat array left to scan.

Added tests/test_index_stress.c: thousands of randomized (not
sequential) creates/deletes/renames across nested directories,
cross-checked against an independent reference model after every
round, not just "did it not crash" -- exercises exactly the code path
the O(n^2) bug and this fix live in, at a scale the other tests don't
reach. Verified under -fsanitize=undefined (150+ runs across this
change's lifetime, 0 failures) and -fsanitize=address (100+ runs, 0
real findings; known sandbox ASan-startup flakes excluded, see prior
commits) per CLAUDE.md's sanitizer rule for upper.c/overlay.c changes.

Measured result (make bench, same environment as the original
finding): mem create 257x faster, unlink 330x faster, mkdir 65x
faster, concurrent mixed workload 131x faster -- and, the comparison
that matters, mem now beats raw fs at every one of these (was losing
by 6-22x before). The complexity-class change is confirmed the same
way the O(n^2) was found: mkdir at N=4,000 vs create at N=20,000 now
shows a 7.15x slowdown for a 5x increase in N, matching the O(n log n)
prediction (5.97x) rather than the old O(n^2) one (25x). Full
before/after tables in BENCH.md's new "Resolution" section, which
keeps the original run as the historical record rather than
overwriting it, per this project's own documentation standard.

Recorded the fix in CLAUDE.md's "Known performance characteristics"
(marked RESOLVED, not silently removed) and its architecture map.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
2026-09-14 08:09:49 +00:00
retoorandClaude Sonnet 5 0b3b207bc0 Add a benchmark suite comparing PackFS against the host filesystem
bench/bench.c (`make bench`) measures create/read/stat/readdir/unlink/
mkdir on mem, dir, and raw fs; large sequential I/O; random-access pack
reads via mmap vs raw fs; compaction throughput; and 8-thread
concurrent mixed workloads. Results from one full run, with honest
analysis (what each "raw fs" vs "raw+fsync" vs "dir" label actually
measures, so they aren't misread as interchangeable), are in BENCH.md.

The benchmark surfaced a real, quantitatively-confirmed finding, not
just favorable numbers: bulk sequential create/unlink on mem/dir is
O(n^2) in file count (~9-22x slower than raw fs at N=20,000), because
every structural write copies the entire snapshot entry array before
publishing it (Section 5.3). concept.md itself names the exact trigger
condition for reconsidering this ("a persistent structurally-shared
tree structure is not required until this assumption is empirically
violated") — this benchmark is that violation, measured rather than
hypothesized: mkdir at N=4,000 vs create at N=20,000 (same mechanism,
5x the N) shows a 28.4x slowdown, matching the O(n^2) prediction (25x)
far better than O(n) (5x).

Also found: dir-backend stat() costs ~2x raw stat() (open+fstat vs one
syscall, the direct cost of openat2 containment on the metadata path);
mem-backed large writes lose to raw fs above ~16MB (Section 5.7's
buffer-growth discipline re-copies prior writes on every capacity
doubling, the price of never exposing a reader to a freed buffer).

Recorded the O(n^2) finding in CLAUDE.md's new "Known performance
characteristics" section, per the same pattern used for the
reclaim_gate use-after-free discovery, since it's exactly the kind of
fact that would otherwise have to be rediscovered by benchmarking
again from scratch. Linked from README and CONTRIBUTING.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
2026-09-14 07:40:42 +00:00
retoorandClaude Sonnet 5 dbbbadf025 Document overlay.c's merge policy and journal wire format
A closer re-audit of "is everything documented well" found real gaps
beyond the first pass: overlay.c's dispatch functions (open/mkdir/
unlink/rename) implement genuinely non-obvious merge logic — checking
both the upper layer and the lower pack, in a specific order, for
existence/emptiness/whiteout status — with no comment explaining why,
only individually-readable lines. Added a doc comment to each covering
the actual decision policy, not a restatement of the code.

The journal's binary wire format was referenced by name throughout
(length-prefixed, checksummed, self-checking per Section 4.3) but
never actually specified anywhere, and its record-type discriminator
was three bare magic numbers (0/1/2). Documented the full record
layout and replaced the magic numbers with named constants (JOP_PUT/
JOP_DELETE/JOP_MKDIR).

Verified under -fsanitize=undefined (30 runs across the three
sequential tests, 20 runs of the concurrency test, zero failures) and
-fsanitize=address (20 runs; the known ASan-in-this-sandbox startup
artifact diagnosed earlier accounted for 5, zero real findings in the
rest), per CLAUDE.md's sanitizer rule for any overlay.c change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
2026-09-14 07:03:11 +00:00
retoorandClaude Sonnet 5 9fb5cf2d9a Document every public function; fix a real gap the audit found
Cross-checked every function declared in include/packfs.h against
nm -D libpackfs.so.0 as the starting point for a full documentation
pass, per the request to document literally everything rather than
just the parts already covered.

That check found a genuine bug, not just a documentation gap:
backend_pack_new was declared in the public header and named in
CLAUDE.md's architecture map, but never implemented in src/pack.c —
any caller would fail at link time. Implemented it as a standalone,
read-only `pack` Backend (every mutating call returns VFS_ERR_PERM,
consistent with concept.md Section 2.1 listing `pack` as its own
backend kind distinct from the overlay), covered it with a new test
case, and verified it under -fsanitize=undefined per CLAUDE.md's
sanitizer rule.

Added a doc comment to every previously-undocumented function and
struct field in packfs.h and internal.h (vfs_open/read/write/close/
stat/readdir/mkdir/unlink/rename, every upper_* structural/content
function, pfs_dir_*, pfs_fnv1a64, PackIndexEntry/Pack fields). Updated
README and CLAUDE.md to mention backend_pack_new and to stop gesturing
at zip/tar as though import/export exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
2026-09-14 05:54:52 +00:00
retoorandClaude Sonnet 5 72e3c900f2 Add PackFS v0: statically linked in-process VFS implementing concept.md
Implements the core design: a mount table published as an atomically-
swapped snapshot; mem/dir/pack/overlay backends; copy-on-write overlay
with copy-up and whiteout deletion; a checksummed append journal;
compaction with exact-duplicate elimination; single-writer/wait-free-
reader concurrency with a structural/content write split; openat2/
Landlock path containment for dir mounts; and load-time pack integrity
validation. Zero required third-party dependencies.

Sanitizer testing (ASan/UBSan) caught and led to fixing a genuine
heap-use-after-free in the snapshot-reclamation path: the textbook
"load pointer, then increment its refcount" pattern left a gap a
concurrent writer could free through. Closed with a small reclaim_gate
rwlock, documented in internal.h and CLAUDE.md since it's a pattern
every refcounted structure in the codebase now follows.

zip/tar import/export backends, recommended in concept.md Section 11,
will not be built — a permanent project decision recorded in CLAUDE.md
since concept.md itself is frozen and cannot be edited to reflect it.

Includes a runnable demo (examples/demo.c, `make demo`) exercising the
library end to end and proving cross-run persistence through the pack
file, plus open-source scaffolding: MIT license, README, CONTRIBUTING,
and a CI workflow running the test suite under ASan/UBSan/TSan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
2026-09-14 05:44:08 +00:00