← Slim64FS

Known Limitations

This file is the explicit honesty surface for semantics that are currently unsupported or only partially supported.

V8.5 Recovery Audit — Closed 2026-05-20

All three open items from the V8.5 recovery audit are resolved below.

Directory-handle fstat after overwrite rename (FUSE identity gap)

Oracle Classification Boundary (C1 / C2)

ORPHAN_INODE — deferred-reclaim archaeology (V8.4 closed)


V10 Semantic Commitments

These entries document intentional design decisions made explicit in V10. Each entry is a contract, not an apology. The audience is cooldude reading an xfstests delta and asking "why is this NOTRUN?"


POSIX requirement: link(2) must create a new directory entry pointing to an existing inode, incrementing the inode's nlink count. Both the original and new names refer to the same inode. POSIX does not allow link(2) to return EOPNOTSUPP on a general-purpose filesystem.

Design rationale: Hard links require nlink > 1 semantics in the unlink path: the inode may only be freed when nlink reaches zero. This complicates the inode free path and the dirlog model, which currently treats every file unlink as an immediate free. The Jay and Chris ingest workloads (card offload, video ingest) never create hard links — they write files, read them back, and move on. Removable media workflows have no use case for hard links. Keeping nlink always 1 for files preserves a simple, auditable inode free path with zero reference-count edge cases on crash recovery.

Error returned: EOPNOTSUPP

Source: slim64_fuse.c:381–385s64_link() discards both arguments and returns -EOPNOTSUPP unconditionally.

xfstests impact:

baby_xfstests coverage: Test 085 — asserts link(2) returns EOPNOTSUPP. Existing, passing.


V10.6 — atime Policy (noatime by design)

POSIX requirement: POSIX requires that a file's st_atime be updated on each successful read(2) or readdir(3) call. The timestamp must reflect the last access time. Linux defaults to relatime semantics (update atime if atime is older than mtime or more than 24 hours old), which satisfies POSIX while reducing write frequency.

Design rationale: Slim64FS targets removable flash media in read-heavy ingest workloads (Jay: card offload; Chris: archival ingest). Updating atime on every read would require a checkpoint write for every read operation, adding inode I/O on the hot read path. On flash media this is measurable wear amplification with no benefit to the target workload. atime is not used by any Jay/Chris tool for correctness decisions. The policy is noatime: reads never update atime. This is equivalent to mounting with -o noatime and is the correct choice for the target use case.

Policy: atime is NOT updated on read(2) or readdir(3). The atime field in the inode reflects the last explicit utimens(2) call only.

What does update atime: Only an explicit utimens(2) / utime(2) call targeting the atime field. The FUSE utimens handler passes through to s64_vfs_utimens_disk() and persists the caller-supplied timestamp.

Source: s64_vfs.c:3002 — the read path never writes the atime field. atime is only set via the utimens FUSE op.

xfstests impact: Tests that assert st_atime is updated after read(2) will FAIL or produce incorrect stat comparisons. These tests are expected to NOTRUN or FAIL under noatime mounts. The rationale is documented here: Slim64FS mounts as noatime by design. No xfstests test is skipped silently — any atime-dependent test failure is a known, expected consequence of this policy.

No test added for V10.6: The policy is documentation-only at this milestone. The noatime behavior is verifiable by: stat a file, cat the file, stat again — atime must be unchanged.


V10.7 — xfstests NOTRUN Rationales

Every NOTRUN in xfstests has a written rationale below. Zero unexplained NOTRUNs is the V10.7 acceptance criterion. A reader must be able to understand each NOTRUN without grepping source.

Reason: link(2) returns EOPNOTSUPP. Hard links are not implemented. See V10.3 entry above.

baby_xfstests coverage: Test 085 — asserts link(2) returns EOPNOTSUPP.

Reason: link(2) returns EOPNOTSUPP. The test exercises nlink > 1 unlink sequencing which depends on hard link support. See V10.3 entry above.

baby_xfstests coverage: Test 085 — asserts link(2) returns EOPNOTSUPP.

generic/035 — Directory-handle fstat after overwrite rename

Reason: FUSE node identity / directory-handle lifetime gap. When a directory handle from the replaced target is still open across an overwrite rename, fstat returns ESTALE. This is a high-level FUSE backend limitation, not a namespace or crash recovery issue. Retained as explicit documented unsupported semantic per V8.5 audit (2026-05-20). Deferred to V9 nodeid/lifetime semantics work. Documented in this file under "V8.5 Recovery Audit."

xattr / ACL tests (generic/xattr-*, generic/062 xattr subcase, etc.)

Reason: setxattr, getxattr, listxattr, and removexattr are not registered in the FUSE fuse_operations struct. The FUSE kernel layer returns ENOSYS for all xattr calls. Slim64FS does not implement extended attributes. The design rationale: xattr adds schema and I/O complexity with no benefit for Jay/Chris ingest workloads. xattr is not needed for video/photo card offload. Any xfstests test requiring xattr or ACL support will NOTRUN. This is an enshrined stub (V10.2).

baby_xfstests coverage: Test 098 — asserts setxattr and getxattr both return ENOSYS.

mmap write durability tests

Reason: mmap write durability is undefined without an explicit fsync/msync. FUSE provides mmap read support via the page cache, but write-back from a dirty mmap mapping is not guaranteed to reach the Slim64FS data path until fsync is called. msync(MS_SYNC) is not mapped to s64_checkpoint_now(). Any xfstests test asserting that mmap writes are durable without fsync will NOTRUN or produce undefined results. The design rationale: mmap write durability requires intercepting msync in the FUSE layer and wiring it to checkpoint — complexity that is out of scope for V10. Already documented in durability.html §3.2.

baby_xfstests coverage: Test 102 — asserts mmap MAP_SHARED write + msync is visible via normal read() (page-cache coherence only; test explicitly documents that durability requires fsync).

O_DIRECT tests

Reason: O_DIRECT is silently ignored by the FUSE kernel layer. FUSE does not support O_DIRECT natively — the kernel strips the flag before passing the request to the FUSE daemon. This is a FUSE architecture limitation, not a Slim64FS implementation gap. Any xfstests test that requires O_DIRECT to bypass the page cache will NOTRUN or behave incorrectly because O_DIRECT has no effect. The behavior is not a bug in Slim64FS; it is a documented property of the FUSE kernel interface.

baby_xfstests coverage: Test 101 — opens a file with O_DIRECT, writes 4096 bytes, reads back and verifies content; asserts the operation succeeds (flag silently stripped by FUSE kernel layer).

flock / fcntl advisory lock tests

Reason: .lock is not registered in the FUSE fuse_operations struct. The FUSE kernel layer returns ENOSYS for flock(2) and fcntl(2) advisory lock operations. Slim64FS does not implement advisory locking. The design rationale: advisory locks are not needed for Jay/Chris ingest workloads — these are single-writer, single-reader card offload workflows. ENOSYS is the correct signal for callers that probe for lock support before using it. Any xfstests test requiring flock or fcntl advisory locks will NOTRUN.

baby_xfstests coverage: Test 099 — asserts flock(LOCK_EX) returns ENOSYS; skips gracefully if the kernel handles flock locally without reaching the FUSE daemon.

fallocate tests

Reason: .fallocate is not registered in the FUSE fuse_operations struct. The FUSE kernel layer returns ENOSYS for fallocate(2). Slim64FS does not support space preallocation or sparse files. The design rationale: the target workloads write sequentially and do not preallocate space; ENOSYS correctly signals that fallocate is not available, allowing callers to fall back to standard write. Any xfstests test requiring fallocate will NOTRUN.

baby_xfstests coverage: Test 100 — asserts posix_fallocate(3) returns ENOSYS or EOPNOTSUPP.


V10.8 — FUSE Op Coverage

Complete audit of every entry in the s64_ops fuse_operations struct. "Registered" means the handler is non-NULL. "Stub" means the handler is registered but returns a fixed error code by design (no real implementation). "Lifecycle" means the handler is called by FUSE infrastructure, not by user-visible syscalls.

Handler Registered Tests Notes
s64_getattr yes 001, 002, 010, 014, 020, 027, 050, 052, 053, 054, 081 Exercised by nearly every test via stat.
s64_readdir yes 003, 004, 024, 026 Also exercised by ls calls in many other tests.
s64_open yes 010, 015, 080, 084, 086 080=ENOENT, 084=O_RDONLY write error, 086=EISDIR.
s64_read yes 010, 013, 016, 017, 018, 019, 044 044=read through symlink.
s64_write yes 010, 011, 012, 016, 017, 018, 019 Includes single-block, multi-block, pwrite, and partial overwrite.
s64_create yes 010, 011, 012, 013, 015 Invoked by shell > and echo ... >. 015=O_EXCL.
s64_truncate yes 060, 061, 062 Truncate to 0, shrink, and extend with zero-fill.
s64_mkdir yes 020, 021, 054, 082 082=EEXIST regression.
s64_unlink yes 014, 025, 072 072=unlink+fsync+remount persistence.
s64_rmdir yes 022, 023, 025, 087 087=rmdir "." and ".." return errors.
s64_rename yes 030, 031, 032, 033, 034, 035, 036, 073 033=RENAME_NOREPLACE, 036=self-rename no-op.
s64_link stub 085 Returns -EOPNOTSUPP unconditionally. See V10.3.
s64_symlink yes 040, 042, 043, 044 042=255-byte target, 043=dangling symlink.
s64_readlink yes 040, 041, 042, 043 041=readlink on non-symlink returns error.
s64_chmod yes 050 050=chmod 000 reflected in stat. Also called internally by s64_create and s64_mkdir when mode ≠ 0.
s64_chown yes 051, 104 051=privilege-gated (may skip). 104=self-chown, always runs, guarantees handler is exercised.
s64_utimens yes 052, 053, 054 052=explicit mtime set, 053=mtime updated on write, 054=mtime set by mkdir.
s64_statfs yes 001, 096 096=f_bfree/f_bavail/f_ffree accuracy under writes.
s64_release yes 103 Orphan-reclaim path (unlink-while-open). 103=inode reclaimed on close, no inode leak.
s64_opendir yes 003, 004, 024, 026 Called implicitly before every readdir.
s64_releasedir yes (all readdir tests) Always returns 0; no disk I/O. Since V14.1 it releases the directory's ino-recycle pin (handle_refs). Exercised by every test that uses ls or readdir.
s64_fsync yes 070, 071, 072, 073 Triggers s64_fs_checkpoint(). 070=file, 071=dir, 072=unlink, 073=rename.
s64_flush yes (implicit) No-op by design (Option A: flush is not a durability boundary). Exercised by every file close. No assertion possible.
s64_init lifecycle (mount) Called at FUSE mount. Sets kernel_cache=0, entry_timeout=0, max_write=1MB. The mount always adds -o default_permissions (V14.1) so the kernel enforces stored uid/gid/mode. Not user-visible.
s64_destroy lifecycle 074 Called at FUSE unmount. 074 documents no-fsync durability contract.

Absent ops (not in s64_ops): setxattr, getxattr, listxattr, removexattr, lock (flock), fallocate, ioctl, poll, write_buf, read_buf, flock. All absent ops return ENOSYS from the FUSE kernel layer. See V10.7 NOTRUN rationales and tests 098–101.



V13 Multi-Model Audit Findings (2026-05-27)

V13 Finding E — Cross-Directory Rename Crash Recovery (Investigated, Not Reproduced)

Theoretical concern: For cross-directory renames, s64_vfs_rename_disk flushes to_parent before from_parent. Because RENAME_FROM and RENAME_TO use independent per-directory sequence counters, they cannot be paired during per-directory isolated replay (s64_vfs.c:3313). A crash between the two flushes could theoretically leave the file visible in both source and destination directories.

Investigation (V13.1 — 2026-05-27): A deterministic crash harness (tools/v13_crash_harness.sh) was built using a cooperative abort() hook (#ifdef S64_CRASH_INJECT / S64_CRASH_AFTER_DST_FLUSH env var) placed in the crashinject binary (slim64fs-fuse-crashinject) immediately after maybe_flush_dir(to_parent) returns in both cross-directory rename paths.

Result: NOT REPRODUCED. The dual-slot shadow/primary inode table mechanism provides implicit crash safety. Recovery (s64_vfs_load_from_fs) uses the SHADOW inode table (last checkpoint), not the primary. When maybe_flush_dir(to_parent) writes a new directory block, it updates the PRIMARY inode's first_block pointer but not the shadow. Recovery sees the pre-mutation directory block via the shadow and rolls back to the pre-rename state.

Additionally, s64_dirlog_replay_cb_apply (line 1177) silently skips any dirlog record whose child inode is absent from the shadow — so CREATE records for inodes added after the last checkpoint are never applied, preventing phantom entries.

Crash window outcome table:

Crash point Outcome
Before any op File at source (correct)
After dirlog appends (FROM/TO), before flush File at source (shadow rollback)
After maybe_flush_dir(to_parent) [tested] File at source (shadow doesn't see new /dst/ block)
During checkpoint, before shadow sync File at source (shadow atomic)
After shadow sync (checkpoint complete) File at destination (correct)

No duplicate can appear because the shadow mechanism ensures that either the full rename is captured (after checkpoint) or neither directory shows the change.

V13.2 (flush ordering reversal) is a NO-OP. The theoretical fix (reverse from "add-to-destination first" to "remove-from-source first") is not applied because the bug was not reproduced and the shadow mechanism provides equivalent protection.

Regression artifact: logs/v13/v13.1-crash-harness.nok documents the full analysis and expected outcome (file at source only). tools/v13_crash_harness.sh is retained as a regression guard; any future change to recovery that weakens shadow protection should re-run this harness.


V13 Finding F — Debug Logging Cleanup (Applied)

Unconditional fprintf calls in s64_unlink, s64_rmdir, s64_rename (slim64_fuse.c:339–379) and per-listing /proc/<pid>/comm reads in s64_readdir (slim64_fuse.c:202–210) were V9-era instrumentation. V12 is closed.

Change applied (V13.0):

s64_get_caller_info remains defined in the file for use when S64_DEBUG_OPS is set.

Artifact: logs/v13/v13.0-debug-cleanup.ok


V14 Audit Findings (2026-06-11)

V14 — Truncate / non-append rewrite materializes the whole file in RAM (Enshrined)

s64_vfs_truncate_disk and the non-append, non-in-place branch of s64_vfs_write_file_at_disk build the new file image in memory (calloc(new_size)) before handing it to s64_fs_write_file_simple. A truncate(fd, N) or a hole-creating write therefore attempts an allocation of the full new size — truncate(f, 100G) on a mounted volume attempts a 100GB allocation and fails with ENOMEM rather than producing a sparse file.

Why enshrined: the rewrite path's whole-file semantics are what make fill-or-kill trivial to reason about (new run written completely, inode switched, old run freed — no partial state). Block-level truncate is a redesign of that path, not a patch. Exposure is bounded by volume size, the target media is removable flash (small volumes, camera-pattern workloads: sequential append, no sparse files), and the failure mode is a clean ENOMEM to the caller — no on-disk state is touched before the allocation succeeds.

Append (off == size) and fully in-place (end <= size) writes are unaffected — they use s64_fs_extend_file / s64_fs_write_file_inplace and never materialize the file.

Artifact: spec/v14.html §Claude Fable 5 finding #7, logs/v14/v14.0-fable5-audit.ok


Security Hardening Pass (2026-07-09)

Directory depth cap is writer-side only; rename can exceed it (Enshrined)

mkdir enforces the spec's depth ≤ 16 (S64_DIR_DEPTH_MAX, root children = depth 1; refusal is ENAMETOOLONG; baby_xfstests test 106). Two deliberate gaps remain:

  1. rename does not re-check depth. Moving an existing subtree of height h under a parent at depth d can leave descendants at d + h > 16. Closing this requires computing subtree height on every rename plus a mount-time tree walk to police foreign images — cost out of proportion to a case that requires deliberate construction and harms nothing internally (all traversal is iterative and cycle-guarded).
  2. Readers tolerate deeper trees from foreign or hostile images by design (writer-MUST / reader-SHOULD-tolerate): a nonconforming card mounts and remains recoverable rather than being refused on a shape technicality. Note that mkdir inside an over-deep foreign subtree is refused, so slim64fs never deepens a tree that already violates the cap.

Coverage: baby_xfstests 106 (16 ok / 17th refused).


See Also