Files
packfs/CHANGELOG.md
T
retoorandClaude Sonnet 5 4aad7d6f64
CI / build-and-test (push) Failing after 38s
Fix real memory leak Gitea CI caught, that local testing had been masking
Gitea CI flagged two things on the last push:

1. A -Wunused-result warning on an intentionally-ignored write() return
   value in test_crash_consistency.c's progress side-channel. Fixed with
   an explicit (void) cast and a comment explaining why ignoring it is
   safe (a short write there only makes the progress count more
   conservative, per that function's own existing documented tolerance).

2. A real LeakSanitizer failure -- 256 bytes across 4 allocations from
   vfs_new/vfs_unmount. Root cause: tests/test_journal_failure.c's two
   "reopen after the failure, verify recovery" blocks called
   vfs_unmount/backend_free/backend_free inside their `if (ov2)` branch
   (the normal, expected path) but vfs_free(v2) only on the `else`
   branch, which is never actually reached in practice. Fixed by moving
   vfs_free(v2) to run unconditionally after the if, in both blocks.

This bug was invisible locally across many runs because local sanitizer
verification had been using ASAN_OPTIONS=detect_leaks=0 -- adopted
originally for a real reason (a SIGKILLed forked child in
test_crash_consistency.c never runs its own exit-time leak check, so its
allocations were never the actual concern) but applied to the whole test
run, which also suppressed detection of this real bug in the *parent*
process's own code. CI doesn't set that option, so it caught what local
runs couldn't. Documented in CONTRIBUTING.md as a real process gap, not
just a code bug: a local verification habit that diverges from what CI
actually runs can let a real finding through until it reaches CI.

Verified: confirmed the leak directly first (reproduced locally by
dropping the detect_leaks=0 override, matching CI exactly, before
touching any code), then confirmed the fix by re-running the same
no-override sweep across all 8 test binaries with zero leaks found, plus
a clean make all + make test.

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

7.5 KiB

Changelog

All notable changes to this project are documented in this file, in the style of Keep a Changelog. This project follows Semantic Versioning; see PACKFS_VERSION_STRING in include/packfs.h for the authoritative current version (packfs.pc, generated by make install, is derived from it, not maintained separately).

This project is pre-release (0.x, an initial, partial implementation of concept.md, not a completed one; see README.md's "Status"). v0.1.0 is tagged locally in this repository's own git history but has not been pushed to any remote — there is no public release yet, only a local one.

[Unreleased]

Fixed

  • A failed journal write (disk full, quota, an I/O error) used to be silently reported as success. journal_append_record and its callers were void and never checked fwrite/fflush/fsync's return values, so vfs_write/vfs_mkdir/vfs_unlink/vfs_rename on an overlay-backed file all reported success even when the durable journal append actually failed. Fixed by propagating success/failure through the whole call chain; the affected call now returns VFS_ERR_IO.
  • A second bug, found only by reproducing the first fix's edge case directly: a partial write left a torn record in the journal that made every later, individually-successful write unrecoverable on reopen too, not just the failed one — journal_replay stops at the first record it can't fully read, regardless of what valid records follow it. Fixed by rolling the journal file back to its exact pre-record length whenever a record fails partway. Both bugs covered permanently by the new tests/test_journal_failure.c, which forces a real write failure via RLIMIT_FSIZE rather than a mock.
  • Neither bug was caught by an existing test — both were found by code review while investigating a question about data-integrity trustworthiness, then confirmed by direct reproduction before and after each fix, not merely inspected and assumed correct.
  • tests/test_journal_failure.c itself leaked memory on its normal, expected code path (vfs_free(v2) was only called on the unreached else branch, in two separate reopen blocks) — invisible locally because local sanitizer runs had been using ASAN_OPTIONS=detect_leaks=0, caught by Gitea CI, which does not set that option. Fixed, and the local/CI sanitizer-invocation mismatch that let it go unnoticed is now called out explicitly in CONTRIBUTING.md. Also fixed a -Wunused-result warning on an intentionally-ignored write() return value in tests/test_crash_consistency.c's progress side-channel.

Added

  • tests/test_crash_consistency.c: real fork()+SIGKILL fault injection (not a hand-truncated file) against journaled writes and compaction, with an independent progress side-channel so the test's own oracle can distinguish "never attempted before the kill" from "attempted and lost" — a distinction its own first draft got wrong, reporting 128 false failures before that side-channel was added. Zero real corruption found across dozens of genuine interruptions per run, both scenarios, on repeated runs including under ASan/UBSan.

Documented

  • The pack integrity checksum's (FNV-1a64) non-cryptographic nature — already documented for the compaction-dedup bug — was never explicitly connected to its other job, load-time pack integrity validation, where it matters more (SECURITY.md).
  • TSan's inability to run is now confirmed in a second, independent sandboxed environment (a separate remote cloud sandbox, tried specifically to check whether the restriction was one environment's fluke), not just this project's own — same root cause (personality(ADDR_NO_RANDOMIZE) blocked by seccomp) confirmed both generically and against this project's actual test suite.

[0.1.0] - 2026-09-14

Added

  • Initial implementation of concept.md: vfs_* core, mem/dir/pack backends, the copy-on-write overlay (copy-up, whiteouts, compaction, an append journal), path containment (openat2/Landlock on Linux 5.6+/5.13+, a documented weaker fallback elsewhere), and pack integrity validation.
  • tests/test_*.c covering each backend, concurrency, and randomized structural-index stress testing against an independent reference model.
  • bench/bench.c (make bench) and BENCH.md, comparing PackFS against the host filesystem across metadata operations, large sequential I/O, random-access pack reads, mount-table scaling, and concurrent workloads.
  • PACKFS_VERSION_MAJOR/MINOR/PATCH/STRING and pfs_version() for compile-time and runtime version/ABI checks.
  • packfs.pc (pkg-config), generated by make install from packfs.pc.in, version always derived from include/packfs.h.
  • SPDX-License-Identifier: MIT on every file under src/ and include/.

Fixed

  • Bulk sequential create/unlink/mkdir on mem/dir was O(n²) in file count. The index (UpperSnapshot) was a flat sorted array copied in full on every structural write; rewritten as a persistent treap (src/upper.c), reducing a structural write to the O(log n) nodes on the path to the change. See BENCH.md's "Resolution" for the full before/after measurement and complexity-class confirmation.
  • pack_write's compaction-time exact-duplicate elimination (Section 9.2) was O(n²) in entry count, and trusted a hash match without comparing actual bytes (a latent correctness bug: FNV-1a64 is explicitly not collision-resistant). Rewritten as an open-addressing hash table with a memcmp verification before ever reusing a data_off, fixing both at once. See BENCH.md's "Resolution #2".
  • backend_pack_new was declared in include/packfs.h but never implemented — any program calling it failed at link time. Implemented as a standalone, read-only pack Backend.
  • The CI config's (.gitea/workflows/ci.yml) sanitizer-build steps never passed -D_GNU_SOURCE when compiling test files (only the library object files got it), which was harmless until a test included internal.h (needed for pthread_rwlock_t) and became a link failure.

Changed

  • CI moved from a GitHub-Actions-convention config (.github/workflows/) to Gitea Actions (.gitea/workflows/) — this project is hosted on Gitea, not GitHub, and was never actually hosted on GitHub; only the scaffolding's starting convention changed. SECURITY.md, CLAUDE.md, CONTRIBUTING.md, and README.md updated to match.

Documented

  • vfs.c's mount table (MountSnapshot) is O(n²) in mount count, the same pattern the file index used to have — confirmed, and deliberately left unfixed. Mount counts are bounded by a program's own source code, not workload-driven, so they do not reach the scale that made the file index's O(n²) a real problem. See BENCH.md's "Finding: mount table scaling" for the numbers and reasoning.
  • A ThreadSanitizer environment limitation (some sandboxes block the personality(ADDR_NO_RANDOMIZE) syscall TSan needs) and a related ASan/UBSan sandbox startup flake (AddressSanitizer:DEADLYSIGNAL), both in CONTRIBUTING.md and CLAUDE.md, with the confirming tests and the required mitigation (timeout-wrapped sanitizer runs).