Skip to content

Bound HEAD reads, refuse generation overflow, and tighten hex and temp-file handling - #23

Merged
hmirin merged 2 commits into
mainfrom
fix/storage-hardening
Oct 28, 2026
Merged

hmirin merged 2 commits into
mainfrom
fix/storage-hardening

Conversation

@hmirin

@hmirin hmirin commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Fixes C2, C5 and C8 from the implementation review. Each was reproduced with a failing test before the fix.

  • C2: unbounded HEAD reads. read_bounded_head_file checked the file size but then used an unbounded read_to_end. Two directory-backend reads (FilesystemStorage::read_head and compare_and_swap_observed_head) skipped it entirely.
    • Reproduced: a FIFO as HEAD hung past a 5-second guard. An oversized HEAD gave "invalid opaque object id" instead of a size error.
    • Fix: the function refuses non-regular files, both by path and on the opened handle. It reads take(MAX + 1) and returns BudgetExhausted above MAX. Every directory HEAD read, including the CAS path, now goes through it.
  • C5: generation overflow. generation + 1 on storage-controlled values panicked in debug builds at u64::MAX.
    • Reproduced: panics at manifest.rs:108 and policy.rs:128.
    • Fix: manifest and policy validation use checked_add, so overflow is an ordinary validation error. PolicyState::successor reports "policy generation overflow". Three local increments in repository.rs were converted too.
  • C8: uppercase hex accepted. validate_root and validate_id said lowercase but accepted uppercase. Both now accept only 0-9a-f.
  • C8: leftover temporary files. A failed directory HEAD write left .HEAD-* behind. This was reproduced by making HEAD a non-empty directory so the rename fails. Every error after the temporary exists now removes it. The same applies to write_file_durable (README.md and FORMAT).

Tests

  • storage::tests::filesystem_head_reads_refuse_a_fifo (unix; each call runs on a thread with a 5 s timeout)
  • storage::tests::filesystem_head_reads_reject_oversized_heads
  • manifest::tests::generation_overflow_is_a_validation_error
  • policy::tests::generation_overflow_is_a_validation_error
  • crypto::tests::repository_root_must_be_lowercase_hex
  • storage::tests::object_ids_must_be_lowercase_hex
  • storage::tests::failed_head_write_removes_its_temporary
  • cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test --all-targets (156 passed, with current main merged in).

Known limit

If HEAD is swapped for a FIFO between the regular-file check and the open, the open can still block. Closing that needs O_NONBLOCK from libc or rustix. I did not add the dependency. The impact is a hang, not an integrity failure, and it needs someone who can rewrite the storage directory at that moment.

…p-file handling

- Directory-backend HEAD reads went through read_to_end after a size check,
  so a FIFO as HEAD blocked forever and a symlink to /dev/zero could exhaust
  memory. Every directory HEAD read now goes through read_bounded_head_file,
  which refuses non-regular files and reads at most MAX + 1 bytes, failing
  with BudgetExhausted beyond MAX.
- Manifest and policy generation increments used plain + 1 on
  storage-controlled values and panicked in debug builds at u64::MAX. They
  now use checked_add, so overflow is an ordinary validation error.
- validate_root and validate_id said lowercase hex but accepted uppercase.
  Both now accept only 0-9a-f.
- A failed directory HEAD write, or README/FORMAT write, left its .HEAD-*
  temporary behind. Every error path after the temporary exists removes it.
@hmirin
hmirin merged commit 404ced5 into main Oct 28, 2026
1 check passed
@hmirin
hmirin deleted the fix/storage-hardening branch October 29, 2026 00:10
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