Skip to content

review: document bugs and add missing test coverage#69

Open
assafvayner wants to merge 5 commits intomainfrom
review/bugs-and-missing-tests
Open

review: document bugs and add missing test coverage#69
assafvayner wants to merge 5 commits intomainfrom
review/bugs-and-missing-tests

Conversation

@assafvayner
Copy link
Copy Markdown

Summary

  • Add REVIEW.md documenting 7 correctness/invariant concerns found during code review, prioritized by severity (1 high, 4 medium, 2 low)
  • Add 21 new unit tests covering previously untested areas
  • Add 3 bug-proving tests (#[ignore]) that fail when run directly, demonstrating the bugs

Bugs documented

# Severity Issue
1 High Poll thread can delete dirty inodes (TOCTOU data loss)
2 Medium Poll thread can delete inodes with open handles
3 Medium apply_commit clobbers size/xet_hash on generation mismatch
4 Medium setattr truncate races with concurrent write in advanced mode
5 Medium Rename phase 2/3 non-atomicity
6 Low-Med NFS handle pool TOCTOU under heavy load
7 Low NFS serializes reads on same inode through one prefetch lock

New tests (21 total)

Prefetch prepare_fetch (9): StartStream, ContinueStream, window doubling to MAX_WINDOW, backward seek RangeDownload, forward skip boundary, fetch_size clamping near EOF

Inode (2): remove_orphan with nlink=0 and nlink>0

VFS (7): poll + dirty inode interaction, symlink create/readlink, readlink on regular file, flush dedup, cancel_delete, cancel_delete_prefix, pending_deletes retry on failure

Bug-proving #[ignore] (3): Run with cargo test --lib --features nfs -- --ignored bug_ to see failures with descriptive messages. Remove #[ignore] after fixing each bug.

Test plan

  • cargo test --lib --features nfs — 234 passed, 3 ignored, 0 failed
  • cargo test --lib --features nfs -- --ignored bug_ — 3 failed (expected, proves bugs)
  • Review REVIEW.md for accuracy of bug descriptions and suggested fixes

Add REVIEW.md documenting 7 correctness/invariant concerns found during
code review, including a high-severity TOCTOU in poll's deletion path
and apply_commit clobbering size on generation mismatch.

Add 18 new unit tests covering previously untested areas:
- PrefetchState::prepare_fetch adaptive window logic (9 tests)
- InodeTable::remove_orphan (2 tests)
- VFS symlink/readlink (2 tests)
- Poll interaction with dirty inodes (1 test)
- FlushManager dedup, cancel_delete, retry on failure (4 tests)
Add tests that demonstrate the bugs documented in REVIEW.md:

1. bug_apply_commit_clobbers_size_on_generation_mismatch (#[should_panic]):
   Proves apply_commit unconditionally overwrites size and xet_hash even
   when dirty_generation doesn't match, causing stale metadata after
   concurrent write + flush.

2. bug_poll_deletes_dirty_inode_toctou (#[should_panic]):
   Proves the TOCTOU race where an inode becomes dirty between poll's
   snapshot and Phase 2 write-lock, causing poll to delete it and lose
   uncommitted data.

3. bug_setattr_write_no_staging_lock:
   Proves write() does not acquire the staging lock, meaning a concurrent
   setattr(truncate) can race with pwrite() on the same staging fd.
Replace #[should_panic] with #[ignore] so the tests assert CORRECT
behavior and visibly fail when run with --ignored:

  cargo test --lib --features nfs -- --ignored bug_

Each test now panics with a descriptive message explaining the bug.
Normal test runs skip them (3 ignored). When a bug is fixed, remove
#[ignore] and the test becomes a passing regression guard.
@github-actions
Copy link
Copy Markdown

POSIX Compliance (pjdfstest)

============================================================
  pjdfstest POSIX Compliance Results
------------------------------------------------------------
  Files: 130/130 passed    Tests: 832 total (0 subtests failed)
  Result: PASS
------------------------------------------------------------
  Category               Passed    Total   Status
  -------------------- -------- -------- --------
  chflags                     5        5       OK
  chmod                       8        8       OK
  chown                       6        6       OK
  ftruncate                  13       13       OK
  granular                    5        5       OK
  mkdir                       9        9       OK
  open                       19       19       OK
  posix_fallocate             1        1       OK
  rename                     10       10       OK
  rmdir                      11       11       OK
  symlink                    10       10       OK
  truncate                   13       13       OK
  unlink                     11       11       OK
  utimensat                   9        9       OK
============================================================

@github-actions
Copy link
Copy Markdown

Benchmark Results

============================================================
  Benchmark — 50MB
------------------------------------------------------------
  Metric                                 FUSE          NFS
  ------------------------------ ------------ ------------
  Sequential read                    277.0 MB/s     253.3 MB/s
  Sequential re-read                1478.2 MB/s    2443.8 MB/s
  Range read (1MB@25MB)               31.5 ms         0.2 ms
  Random reads (100x4KB avg)          33.2 ms         0.0 ms
  Sequential write (FUSE)           1304.8 MB/s
  Close latency (CAS+Hub)            0.089 s
  Write end-to-end                   392.8 MB/s
  Dedup write                       1619.7 MB/s
  Dedup close latency                0.077 s
  Dedup end-to-end                   463.0 MB/s
============================================================
============================================================
  Benchmark — 200MB
------------------------------------------------------------
  Metric                                 FUSE          NFS
  ------------------------------ ------------ ------------
  Sequential read                    943.5 MB/s     973.5 MB/s
  Sequential re-read                1670.5 MB/s    2409.2 MB/s
  Range read (1MB@25MB)               32.6 ms         0.2 ms
  Random reads (100x4KB avg)          32.2 ms         0.0 ms
  Sequential write (FUSE)           1319.4 MB/s
  Close latency (CAS+Hub)            0.113 s
  Write end-to-end                   757.2 MB/s
  Dedup write                       1424.2 MB/s
  Dedup close latency                0.112 s
  Dedup end-to-end                   793.2 MB/s
============================================================
============================================================
  Benchmark — 500MB
------------------------------------------------------------
  Metric                                 FUSE          NFS
  ------------------------------ ------------ ------------
  Sequential read                   1618.5 MB/s    1573.0 MB/s
  Sequential re-read                1728.3 MB/s    2446.0 MB/s
  Range read (1MB@25MB)               31.0 ms         0.2 ms
  Random reads (100x4KB avg)          32.0 ms         0.0 ms
  Sequential write (FUSE)           1268.0 MB/s
  Close latency (CAS+Hub)            0.160 s
  Write end-to-end                   901.5 MB/s
  Dedup write                       1264.8 MB/s
  Dedup close latency                0.112 s
  Dedup end-to-end                   985.2 MB/s
============================================================
============================================================
  fio Benchmark Results
------------------------------------------------------------
  Job                        FUSE MB/s   NFS MB/s  FUSE IOPS   NFS IOPS
  ------------------------- ---------- ---------- ---------- ----------
  seq-read-100M                  444.4      425.5                      
  seq-reread-100M               2381.0      227.3                      
  rand-read-4k-100M                0.1        0.1         16         20
  seq-read-5x10M                 454.5      657.9                      
  rand-read-10x1M                  0.1        0.1         36         37
  Random Read Latency           FUSE avg      NFS avg
  ------------------------- ------------ ------------
  rand-read-4k-100M           64247.9 us   49642.1 us
  rand-read-10x1M             27622.1 us   27036.0 us
============================================================

XciD added a commit that referenced this pull request Mar 26, 2026
## Summary

Fixes REVIEW.md finding
2-[#69](#69): the poll
thread could remove an inode from the table while file handles were
still referencing it, causing `read()`/`getattr()`/`release()` on those
handles to fail with `ENOENT`.

**Root cause**: `apply_poll_diff` called `inode_table.remove(ino)`
without checking whether any `OpenFile` entries referenced that inode.

### Fix

- Wrap `open_files` in `Arc` so it can be shared with the poll task
- Pass `open_files` to `apply_poll_diff`, skip deletion when handles
exist
- Files with open handles stay in the inode table until all handles are
released

### Test

- `poll_skips_deletion_with_open_handles`: opens a file, simulates
remote deletion via poll, verifies the inode survives while the handle
is open
XciD added a commit that referenced this pull request Mar 26, 2026
…match (#75)

## Summary

Fixes REVIEW.md finding 3 in #69 : `apply_commit` overwrote `size` and
`xet_hash` unconditionally before checking
`clear_dirty_if(dirty_generation)`. If a concurrent writer advanced the
generation, the in-progress file's size was clobbered with the stale
flushed value.

**Scenario:**
1. Writer A flushes 10 MB file (captures gen=5)
2. Writer B extends file to 15 MB (gen=6)
3. Flush completes, `apply_commit("hash", 10MB, gen=5)` runs
4. Old code: sets `size=10MB`, then `clear_dirty_if(5)` returns false
(gen is 6)
5. Applications see 10 MB until next flush, even though staging file is
15 MB

**Fix:** Move `xet_hash`, `size`, and `pending_deletes.clear()` inside
the `clear_dirty_if` success branch.

One-line semantic change in `src/virtual_fs/inode.rs`, existing test
updated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant