feat(fs): O_NOATIME discipline for pure-read theorems (#94 Gap 9)#120
Merged
Conversation
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>
🔍 Hypatia Security ScanFindings: 121 issues detected
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 newfs_purehelper that setslibc::O_NOATIMEon Linux with a graceful EPERM fallback.Changes
New module:
impl/rust-cli/src/fs_pure.rsThree 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_NOATIMEviaOpenOptionsExt::custom_flags. The kernel only honoursO_NOATIMEwhen the caller owns the file or holdsCAP_FOWNER; otherwise it returnsEPERM. The helpers catchEPERMand retry with vanillaFile::openso the program keeps working -- the purity claim is downgraded for that single call. On non-Linux targets the helpers fall through toFile::open.Call sites converted (8)
main.rs:162read_to_string(script_path)vsh script.sh)executable.rs:774read_to_string(&path)source <file>audit_log.rs:223read_to_string(&self.log_path)AuditLog::read_allstate.rs:637read_to_string(&self.state_file)ShellState::loadredirection.rs:387File::open(&path)<input redirection (builtins)external.rs:380File::open(&target)<input redirection (external)redirection.rs:331fs::read(&path)commands.rs:287fs::read(&full_path)rmundo-backup readIntentionally NOT converted
/dev/urandomreads incommands/secure_deletion.rs(character special device; atime not tracked;O_NOATIMEmay be rejected by the driver)./sys/block/<dev>/queue/rotationalinsecure_erase.rs(sysfs; atime not tracked).read_dircallers (directory iteration; not a pure file read).tests/*.rs) reading back fixture content -- production code is the soundness surface.Testing
Six new unit tests in
fs_pure::tests:read_to_endround-trip with arbitrary bytes (incl.0x00,0xff, embedded newlines).read_to_stringUTF-8 round-trip.open_pureinitial-position-is-BOF.ErrorKind::NotFound.O_NOATIMEpath 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/unsafeCoerceequivalents. No LICENSE / SPDX-header changes.Test plan
cargo build-- finished cleanly (37s)cargo test-- 764 passing / 0 failingcargo clippy --all-targets -- -D warnings-- clean4A03639C1EB1F86C7F0C97A91835A14A2867091E)Soundness note
The fallback path (EPERM -> vanilla
open) is deliberate. The preservation theorems quantify over reads that the implementation claims are pure; ifO_NOATIMEis 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