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)
- Scope:
xfstestsgeneric/035directory subcase (dir_overwrite_opendir_fstat). - Current behavior: returns
ESTALEafter overwrite rename when a directory handle from the replaced target is still open. - Classification: high-level FUSE node identity / directory-handle lifecycle gap (not regular-file handle lifetime, not namespace corruption).
- V7 handling: explicit
NOTRUNforgeneric/035with reason:Slim64FS high-level FUSE backend does not yet support directory-handle fstat after overwrite rename. - V8.5 resolution (2026-05-20): Retained as explicit
documented unsupported semantic (option 2). The
generic/035directory subcase is gatedNOTRUNin the xfstests harness. No recovery-path implication — the gap is in FUSE node identity lifetime, not in crash recovery or namespace consistency. Deferred to V9 nodeid/lifetime semantics work.
Oracle Classification Boundary (C1 / C2)
- Scope: V8.2 pre-fsync/no-fsync and write-large/mid-write cells.
- Current behavior: pre-remount images can show recoverable dirty-primary metadata while remount-visible namespace truth rolls back/coheres correctly.
- Classification:
C1_RECOVERABLE_DIRTY_PRIMARYC2_MIDWRITE_RECOVERABLE
- V8.5 resolution (2026-05-20): Confirmed handled
correctly. The crash matrix (
docs/crash-matrix.html) preserves C1/C2 asPASS+WARNentries — not collapsed into clean PASS, not mislabeled as corruption. No further action required.
ORPHAN_INODE — deferred-reclaim archaeology (V8.4 closed)
- Scope: post-crash
slim64fs-checkfindings after fsstress crash-loop iterations. - Resolved (2026-05-16): V8.4 gate passes 100/100 iterations with zero
corruption-class findings (
DIRENT_TO_FREE_INODE,BAD_DIRLOG_BLOCK_RANGE). ORPHAN_INODEfindings may still appear on post-crash or post-recovery images and are explicitly tolerated as deferred-reclaim archaeology:- The inode exists on disk (mode ≠ 0, blocks intact) but has no path from root.
- Mount-time reclamation was removed because eager freeing risks
DIRENT_TO_FREE_INODEwhen a stale dirlog chain masks the CREATE that would have kept the inode referenced. slim64fs-checkreports ORPHAN_INODE at severity UNREACHABLE (exit 1), which the fsstress harness treats as non-fatal. Exit 2+ (corruption) is still fatal.- Reclamation is deferred to an explicit
slim64fs-fsckmaintenance pass.
- This is not "orphans don't matter." Orphan inodes represent leaked disk space and possibly unreachable user data. They are logged at mount, reported by the checker, and must be explicitly reclaimed. They do not propagate silently.
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?"
V10.3 — Hard Links (EOPNOTSUPP by design)
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–385 —
s64_link() discards both arguments and returns
-EOPNOTSUPP unconditionally.
xfstests impact:
generic/005— NOTRUN. Tests hard link creation andnlinkcounts.generic/013— NOTRUN. Testslink(2)behavior across rename and unlink sequences.- Any other test that calls
link(2)and assertsnlink > 1will NOTRUN or FAIL withEOPNOTSUPP.
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.
generic/005 — Hard links
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.
generic/013 — Hard link semantics across rename/unlink
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_readdir:s64_get_caller_infocall removed entirely — no/proc/<pid>/commopen per directory listing.s64_unlink,s64_rmdir,s64_rename: boths64_get_caller_info()(the expensive/proc/<pid>/commread) and thefprintfare moved insideif (getenv("S64_DEBUG_OPS"))— the proc lookup is fully gated, not just the output.S64_DEBUG_OPSis independent ofS64_CHECKPOINT_LOG; no recompile required.
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:
renamedoes 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).- 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
mkdirinside 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
- [[durability_contract|Durability Contract]] — the contract these limitations bound
- [[recovery_model|Recovery Model]] — recovery open risks cross-referenced here