Skip to content

feat(fs): O_NOATIME discipline for pure-read theorems (#94 Gap 9)#120

Merged
hyperpolymath merged 1 commit into
mainfrom
claude/issue-94-noatime-discipline
Jun 2, 2026
Merged

feat(fs): O_NOATIME discipline for pure-read theorems (#94 Gap 9)#120
hyperpolymath merged 1 commit into
mainfrom
claude/issue-94-noatime-discipline

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Summary

Closes part of #94 (Gap 9: noatime discipline).

Vanilla read(2) on Linux mutates the inode access-time (st_atime), so any "pure read" in the Rust CLI is observably mutating from the filesystem's point of view -- which breaks every preservation theorem that quantifies over pure reads. This PR routes all pure-read paths through a new fs_pure helper that sets libc::O_NOATIME on Linux with a graceful EPERM fallback.

Changes

New module: impl/rust-cli/src/fs_pure.rs

Three helpers:

  • open_pure(path) -> io::Result<File>
  • read_to_end(path) -> io::Result<Vec<u8>>
  • read_to_string(path) -> io::Result<String>

On Linux they set libc::O_NOATIME via OpenOptionsExt::custom_flags. The kernel only honours O_NOATIME when the caller owns the file or holds CAP_FOWNER; otherwise it returns EPERM. The helpers catch EPERM and retry with vanilla File::open so the program keeps working -- the purity claim is downgraded for that single call. On non-Linux targets the helpers fall through to File::open.

Call sites converted (8)

File Site Purpose
main.rs:162 read_to_string(script_path) Script load (vsh script.sh)
executable.rs:774 read_to_string(&path) source <file>
audit_log.rs:223 read_to_string(&self.log_path) AuditLog::read_all
state.rs:637 read_to_string(&self.state_file) ShellState::load
redirection.rs:387 File::open(&path) < input redirection (builtins)
external.rs:380 File::open(&target) < input redirection (external)
redirection.rs:331 fs::read(&path) Truncate-redirection backup read
commands.rs:287 fs::read(&full_path) rm undo-backup read

Intentionally NOT converted

  • /dev/urandom reads in commands/secure_deletion.rs (character special device; atime not tracked; O_NOATIME may be rejected by the driver).
  • /sys/block/<dev>/queue/rotational in secure_erase.rs (sysfs; atime not tracked).
  • read_dir callers (directory iteration; not a pure file read).
  • Tests (tests/*.rs) reading back fixture content -- production code is the soundness surface.

Testing

Six new unit tests in fs_pure::tests:

  • read_to_end round-trip with arbitrary bytes (incl. 0x00, 0xff, embedded newlines).
  • read_to_string UTF-8 round-trip.
  • open_pure initial-position-is-BOF.
  • Missing-file surfaces ErrorKind::NotFound.
  • Large-file (200 KB) round-trip (guards against internal-buffer truncation bugs).
  • EPERM-fallback parity (Linux-only) -- O_NOATIME path yields identical bytes to the vanilla fallback path.

Full suite: 757 -> 764 passing (+7 incl. doctest), 0 failing, 14 ignored (pre-existing).

cargo clippy --all-targets -- -D warnings: clean.

No Admitted / believe_me / unsafeCoerce equivalents. No LICENSE / SPDX-header changes.

Test plan

  • cargo build -- finished cleanly (37s)
  • cargo test -- 764 passing / 0 failing
  • cargo clippy --all-targets -- -D warnings -- clean
  • GPG-signed commit verified (key 4A03639C1EB1F86C7F0C97A91835A14A2867091E)
  • CHANGELOG.adoc entry under "Added -- 2026-06-02 / Implementation"

Soundness note

The fallback path (EPERM -> vanilla open) is deliberate. The preservation theorems quantify over reads that the implementation claims are pure; if O_NOATIME is refused we no longer claim purity for that call, but functional behaviour is preserved. Down-stream callers that need guaranteed purity should check the file owner first or propagate the error -- this PR establishes the discipline, not a hard guarantee.

Refs: #94

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

Gap 9 from the issue-#94 remaining-practice list: vanilla read(2) on
Linux mutates the inode access-time (st_atime), which means any
"pure read" in the Rust CLI is observably mutating from the
filesystem's point of view -- and that breaks every preservation
theorem that quantifies over pure reads.

Resolution
----------
New module `impl/rust-cli/src/fs_pure.rs` exposes three helpers:

  * `open_pure(path) -> io::Result<File>`
  * `read_to_end(path) -> io::Result<Vec<u8>>`
  * `read_to_string(path) -> io::Result<String>`

On Linux they set `libc::O_NOATIME` via
`OpenOptionsExt::custom_flags`. The kernel only honours `O_NOATIME`
when the caller owns the file or holds `CAP_FOWNER`; otherwise it
returns `EPERM`. The helpers catch `EPERM` and retry with vanilla
`File::open` so the program keeps working -- the purity claim is
just downgraded for that one call. On non-Linux targets the helpers
fall through to `File::open` (the OS does not provide `O_NOATIME`;
mount-level `noatime`/`relatime` is the equivalent knob).

Call sites converted
--------------------
Eight pure-read sites now route through `fs_pure`:

  1. `main.rs`             -- script load (`vsh script.sh`)
  2. `executable.rs`       -- `source <file>`
  3. `audit_log.rs`        -- `AuditLog::read_all`
  4. `state.rs`            -- `ShellState::load`
  5. `redirection.rs:387`  -- `<` input redirection (builtins)
  6. `external.rs:380`     -- `<` input redirection (external)
  7. `redirection.rs:331`  -- truncate-redirection backup read
  8. `commands.rs:287`     -- `rm` undo-backup read

Intentionally NOT converted
---------------------------
  * `/dev/urandom` reads in `commands/secure_deletion.rs`
    (character special device; atime not tracked; `O_NOATIME` may be
    rejected by the driver).
  * `/sys/block/<dev>/queue/rotational` in `secure_erase.rs`
    (sysfs; atime not tracked).
  * `read_dir` callers (directory iteration; not a pure file read).
  * Tests (`tests/*.rs`) that read back fixture content -- production
    code is the soundness surface.

Testing
-------
Six new unit tests in `fs_pure::tests` cover:

  * `read_to_end` round-trip with arbitrary bytes (including `0x00`,
    `0xff`, embedded newlines).
  * `read_to_string` UTF-8 round-trip.
  * `open_pure` initial-position-is-BOF.
  * Missing-file surfaces `ErrorKind::NotFound`.
  * Large-file (200 KB) round-trip (guards against internal-buffer
    truncation bugs).
  * EPERM-fallback parity (Linux-only) -- asserts `O_NOATIME` path
    yields identical bytes to the fallback path.

Full test suite: 757 -> 764 passing (+7 incl. doctest), 0 failing,
14 ignored (pre-existing). `cargo clippy -- -D warnings` clean.

No `Admitted` / `believe_me` / `unsafeCoerce` equivalents.
No LICENSE / SPDX-header changes.

Closes part of #94 (Gap 9).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hyperpolymath hyperpolymath enabled auto-merge (squash) June 2, 2026 10:44
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

🔍 Hypatia Security Scan

Findings: 121 issues detected

Severity Count
🔴 Critical 10
🟠 High 36
🟡 Medium 75

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Issue in codeql.yml",
    "type": "missing_workflow",
    "file": "codeql.yml",
    "action": "create",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Workflow executes remote script directly (curl/wget piped to shell). Download, verify checksum/signature, then execute.",
    "type": "download_then_run",
    "file": "lean-verification.yml",
    "action": "verify_download_integrity",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Workflow executes remote script directly (curl/wget piped to shell). Download, verify checksum/signature, then execute.",
    "type": "download_then_run",
    "file": "rust-cli.yml",
    "action": "verify_download_integrity",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Issue in boj-build.yml",
    "type": "missing_timeout_minutes",
    "file": "boj-build.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in cflite_batch.yml",
    "type": "missing_timeout_minutes",
    "file": "cflite_batch.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in cflite_pr.yml",
    "type": "missing_timeout_minutes",
    "file": "cflite_pr.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in compilation_tests.yml",
    "type": "missing_timeout_minutes",
    "file": "compilation_tests.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in compilation_tests.yml",
    "type": "missing_timeout_minutes",
    "file": "compilation_tests.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath merged commit af982ce into main Jun 2, 2026
39 of 59 checks passed
@hyperpolymath hyperpolymath deleted the claude/issue-94-noatime-discipline branch June 2, 2026 11:34
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