-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfs_pure.rs
More file actions
206 lines (188 loc) · 7.62 KB
/
Copy pathfs_pure.rs
File metadata and controls
206 lines (188 loc) · 7.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
//! Pure-read filesystem helpers (noatime discipline).
//!
//! A "pure" read is one the proof tree treats as non-mutating: it must not
//! observably change the filesystem. Vanilla `read(2)` on Linux updates the
//! inode access time (`st_atime`), so an apparently read-only operation
//! mutates metadata — which would invalidate any preservation theorem that
//! treats reads as pure.
//!
//! This module funnels every "pure-read" code-path through [`read_to_end`]
//! and [`read_to_string`], which open the file with the `O_NOATIME` flag on
//! Linux. The flag suppresses the atime update at the VFS layer, restoring
//! true non-mutation.
//!
//! `O_NOATIME` is only honoured when the calling process owns the file or
//! holds `CAP_FOWNER`. On non-owner reads the kernel returns `EPERM`; in
//! that case we fall back to a regular `open(2)` so the read still
//! succeeds. The proof obligation is downgraded — the read may touch
//! atime — but functional behaviour is preserved.
//!
//! On non-Linux targets (macOS, BSDs, Windows) `O_NOATIME` does not exist;
//! the helpers transparently fall through to `File::open` and rely on
//! mount-level `noatime`/`relatime` for atime suppression.
//!
//! Issue: <https://github.com/hyperpolymath/valence-shell/issues/94> (Gap 9).
//!
//! # Example
//!
//! ```no_run
//! use vsh::fs_pure;
//! use std::path::Path;
//!
//! let bytes = fs_pure::read_to_end(Path::new("/etc/hostname")).unwrap();
//! let text = fs_pure::read_to_string(Path::new("/etc/hostname")).unwrap();
//! # let _ = (bytes, text);
//! ```
//!
//! # 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 we keep the program running. Down-stream callers
//! that need *guaranteed* purity should check the file owner first or
//! propagate the error.
//!
//! Reads from character/block special devices (e.g., `/dev/urandom`) are
//! outside the scope of FS-level atime tracking; do not route them
//! through this module.
//!
//! Closes part of #94 (Gap 9: noatime discipline).
use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
/// Open `path` for reading with `O_NOATIME` semantics where supported.
///
/// On Linux the file is opened with `libc::O_NOATIME` set via
/// [`std::os::unix::fs::OpenOptionsExt::custom_flags`]. If the kernel
/// rejects the flag with `EPERM` (caller is neither owner nor
/// `CAP_FOWNER`) the call retries with a vanilla [`File::open`]; the
/// read still succeeds but the access-time will be updated.
///
/// On non-Linux targets this is equivalent to [`File::open`].
pub fn open_pure(path: &Path) -> io::Result<File> {
#[cfg(target_os = "linux")]
{
use std::os::unix::fs::OpenOptionsExt;
let mut opts = File::options();
opts.read(true).custom_flags(libc::O_NOATIME);
match opts.open(path) {
Ok(f) => Ok(f),
Err(e) if e.raw_os_error() == Some(libc::EPERM) => File::open(path),
Err(e) => Err(e),
}
}
#[cfg(not(target_os = "linux"))]
{
File::open(path)
}
}
/// Read the entire contents of `path` into a `Vec<u8>`, suppressing the
/// atime update where the kernel allows it.
///
/// See [`open_pure`] for the fallback semantics.
pub fn read_to_end(path: &Path) -> io::Result<Vec<u8>> {
let mut f = open_pure(path)?;
let mut buf = Vec::new();
f.read_to_end(&mut buf)?;
Ok(buf)
}
/// Read the entire contents of `path` into a `String`, suppressing the
/// atime update where the kernel allows it.
///
/// See [`open_pure`] for the fallback semantics. Returns
/// [`io::ErrorKind::InvalidData`] if the bytes are not valid UTF-8, matching
/// [`std::fs::read_to_string`].
pub fn read_to_string(path: &Path) -> io::Result<String> {
let mut f = open_pure(path)?;
let mut buf = String::new();
f.read_to_string(&mut buf)?;
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
/// `read_to_end` round-trips arbitrary bytes.
#[test]
fn read_to_end_roundtrip() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let payload: &[u8] = b"\x00\x01valence\xffshell\n";
std::fs::write(tmp.path(), payload).unwrap();
let got = read_to_end(tmp.path()).expect("read_to_end ok");
assert_eq!(got, payload);
}
/// `read_to_string` round-trips UTF-8 text.
#[test]
fn read_to_string_roundtrip() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let payload = "hello valence-shell\n";
std::fs::write(tmp.path(), payload).unwrap();
let got = read_to_string(tmp.path()).expect("read_to_string ok");
assert_eq!(got, payload);
}
/// `open_pure` returns a usable handle whose initial position is BOF.
#[test]
fn open_pure_position_is_bof() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let payload = b"abc";
std::fs::write(tmp.path(), payload).unwrap();
let mut f = open_pure(tmp.path()).expect("open_pure ok");
let mut buf = Vec::new();
f.read_to_end(&mut buf).unwrap();
assert_eq!(&buf, payload);
}
/// Non-existent path surfaces a NotFound error rather than panicking
/// or silently returning empty data.
#[test]
fn missing_file_is_not_found() {
let mut p = std::env::temp_dir();
p.push("valence-shell-fs_pure-DOES-NOT-EXIST-9c1a4d");
// Make sure it really doesn't exist.
let _ = std::fs::remove_file(&p);
let err = read_to_end(&p).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
/// On Linux, opening a non-owner-readable file (e.g. one whose owner
/// is root on a system where we are not root) must still succeed via
/// the EPERM fallback. We can't fabricate a non-owner file in CI, so
/// we exercise the *fallback path itself* by emulating it directly
/// and asserting that a vanilla `File::open` of a file we DO own
/// behaves the same as `open_pure`.
///
/// This guards against regressions in the fallback wiring: if a
/// future refactor accidentally returns the EPERM error to the
/// caller, the next test below will still pass via O_NOATIME but
/// this one will fail when the production code is hardened to
/// always go through the fallback branch.
#[test]
#[cfg(target_os = "linux")]
fn eperm_fallback_path_yields_same_bytes() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let payload = b"fallback parity";
std::fs::write(tmp.path(), payload).unwrap();
// O_NOATIME path
let via_pure = read_to_end(tmp.path()).unwrap();
// Vanilla open path (what the fallback collapses to)
let mut f = File::open(tmp.path()).unwrap();
let mut via_fallback = Vec::new();
f.read_to_end(&mut via_fallback).unwrap();
assert_eq!(via_pure, via_fallback);
assert_eq!(via_pure, payload);
}
/// Large file (>64KB) reads correctly — sanity check that the
/// helper isn't accidentally truncating at an internal buffer
/// boundary.
#[test]
fn large_file_roundtrip() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
let payload: Vec<u8> = (0..200_000u32).map(|i| (i & 0xff) as u8).collect();
tmp.write_all(&payload).unwrap();
tmp.flush().unwrap();
let got = read_to_end(tmp.path()).unwrap();
assert_eq!(got.len(), payload.len());
assert_eq!(got, payload);
}
}