Skip to main content

wowlab_fs/
private_file.rs

1// #t(file: rust_ambient_syscall, rust_concrete_io_param) This module is the reviewed native boundary for no-follow opens, restrictive permissions, and durable private-file commits.
2
3//! Create-once storage for private local state.
4//!
5//! Unix receives restrictive permissions and final-component no-follow protection.
6//! Other platforms retain regular-file and create-once guarantees.
7//! They also retain atomic commit and file synchronization.
8//! This module does not configure platform account ACLs.
9//! Parent-directory synchronization is currently provided on Unix.
10
11use std::io::{Read as _, Write as _};
12
13use crate::{
14    atomic, directory,
15    error::{Error, Operation, Result},
16    path::Path,
17};
18
19#[cfg(unix)]
20const PRIVATE_FILE_MODE: u32 = 0o600;
21
22/// Load a private regular file, or create it once from `create`.
23///
24/// Creation uses a temporary file beside `path`.
25/// Its contents are synchronized before a no-clobber commit.
26/// The containing directory is synchronized where supported.
27/// A concurrent winner's bytes are loaded and returned.
28///
29/// On Unix, existing files are restricted to mode `0600` before reading.
30/// The final path is opened without following a symbolic link.
31/// Other platforms retain the operating system's inherited access controls.
32///
33/// # Errors
34///
35/// Returns a contextual error when the parent cannot be created.
36/// Non-regular entries and file operation failures are also reported.
37pub fn load_or_create_with<F>(path: &Path, create: F) -> Result<Vec<u8>>
38where
39    F: FnOnce() -> Vec<u8>,
40{
41    match load(path) {
42        Ok(contents) => return Ok(contents),
43        Err(error) if error.is_not_found() => {}
44        Err(error) => return Err(error),
45    }
46
47    let parent = path.parent().ok_or_else(|| {
48        Error::new(
49            Operation::CreateFile,
50            path,
51            std::io::Error::new(
52                std::io::ErrorKind::InvalidInput,
53                "private file destination has no parent",
54            ),
55        )
56    })?;
57
58    directory::ensure(parent)?;
59
60    let contents = create();
61    let mut temporary = tempfile::NamedTempFile::new_in(parent)
62        .map_err(|source| Error::new(Operation::CreateFile, path, source))?;
63
64    restrict(temporary.as_file(), path)?;
65    temporary
66        .write_all(&contents)
67        .map_err(|source| Error::new(Operation::WriteFile, path, source))?;
68    temporary
69        .flush()
70        .map_err(|source| Error::new(Operation::WriteFile, path, source))?;
71    temporary
72        .as_file()
73        .sync_all()
74        .map_err(|source| Error::new(Operation::Sync, path, source))?;
75
76    match temporary.persist_noclobber(path) {
77        Ok(file) => {
78            file.sync_all()
79                .map_err(|source| Error::new(Operation::Sync, path, source))?;
80            atomic::sync_parent(parent, path)?;
81
82            Ok(contents)
83        }
84        Err(error) if error.error.kind() == std::io::ErrorKind::AlreadyExists => load(path),
85        Err(error) => Err(Error::new(Operation::Rename, path, error.error)),
86    }
87}
88
89fn load(path: &Path) -> Result<Vec<u8>> {
90    let entry = std::fs::symlink_metadata(path)
91        .map_err(|source| Error::new(Operation::Inspect, path, source))?;
92
93    if !entry.file_type().is_file() {
94        return Err(not_regular_file(path));
95    }
96
97    let mut options = std::fs::OpenOptions::new();
98
99    options.read(true);
100
101    #[cfg(unix)]
102    {
103        use std::os::unix::fs::OpenOptionsExt as _;
104
105        options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK)
106    };
107
108    let mut file = options
109        .open(path)
110        .map_err(|source| Error::new(Operation::OpenFile, path, source))?;
111    let opened_entry = file
112        .metadata()
113        .map_err(|source| Error::new(Operation::Inspect, path, source))?;
114
115    if !opened_entry.file_type().is_file() {
116        return Err(not_regular_file(path));
117    }
118
119    restrict(&file, path)?;
120
121    let mut contents = Vec::new();
122
123    file.read_to_end(&mut contents)
124        .map_err(|source| Error::new(Operation::ReadFile, path, source))?;
125
126    Ok(contents)
127}
128
129fn not_regular_file(path: &Path) -> Error {
130    Error::new(
131        Operation::OpenFile,
132        path,
133        std::io::Error::new(
134            std::io::ErrorKind::InvalidData,
135            "private state must be a regular file",
136        ),
137    )
138}
139
140#[cfg(unix)]
141fn restrict(file: &std::fs::File, path: &Path) -> Result<()> {
142    use std::os::unix::fs::PermissionsExt as _;
143
144    file.set_permissions(std::fs::Permissions::from_mode(PRIVATE_FILE_MODE))
145        .map_err(|source| Error::new(Operation::SecureFile, path, source))
146}
147
148#[cfg(not(unix))]
149fn restrict(_file: &std::fs::File, _path: &Path) -> Result<()> {
150    Ok(())
151}
152
153#[cfg(test)]
154mod tests {
155    use std::sync::{
156        Arc, Barrier,
157        atomic::{AtomicU8, Ordering},
158    };
159
160    use googletest::prelude::*;
161
162    use super::{PRIVATE_FILE_MODE, load_or_create_with};
163    use crate::{directory, file, temporary::Directory};
164
165    #[gtest]
166    fn created_bytes_roundtrip_without_visible_temporary_entries() -> Result<()> {
167        let directory = Directory::new().or_fail()?;
168        let path = directory.path().join("identity");
169
170        let created = load_or_create_with(&path, || b"private".to_vec()).or_fail()?;
171        let loaded = load_or_create_with(&path, || b"replacement".to_vec()).or_fail()?;
172
173        verify_that!(created, eq(b"private"))?;
174        verify_that!(loaded, eq(b"private"))?;
175
176        verify_that!(directory::entries(directory.path()).or_fail()?, len(eq(1)))
177    }
178
179    #[gtest]
180    fn creation_ensures_missing_parent_directories() -> Result<()> {
181        let directory = Directory::new().or_fail()?;
182        let path = directory.path().join("config/node/identity");
183
184        let contents = load_or_create_with(&path, || b"private".to_vec()).or_fail()?;
185
186        verify_that!(contents, eq(b"private"))?;
187
188        verify_that!(file::read_bytes(&path).or_fail()?, eq(b"private"))
189    }
190
191    #[gtest]
192    fn existing_non_regular_entry_is_rejected() -> Result<()> {
193        let directory = Directory::new().or_fail()?;
194        let path = directory.path().join("identity");
195
196        directory::ensure(&path).or_fail()?;
197
198        verify_that!(
199            load_or_create_with(&path, Vec::new),
200            err(displays_as(contains_substring("regular file")))
201        )
202    }
203
204    #[gtest]
205    fn concurrent_creators_return_the_committed_winner() -> Result<()> {
206        let directory = Directory::new().or_fail()?;
207        let path = Arc::new(directory.path().join("identity"));
208        let barrier = Arc::new(Barrier::new(2));
209        let sequence = Arc::new(AtomicU8::new(1));
210        let mut threads = Vec::new();
211
212        for _ in 0..2 {
213            let path = Arc::clone(&path);
214            let barrier = Arc::clone(&barrier);
215            let sequence = Arc::clone(&sequence);
216
217            threads.push(std::thread::spawn(move || {
218                load_or_create_with(&path, || {
219                    let byte = sequence.fetch_add(1, Ordering::Relaxed);
220
221                    barrier.wait();
222
223                    vec![byte]
224                })
225            }));
226        }
227
228        let left = threads.remove(0).join().or_fail()?.or_fail()?;
229        let right = threads.remove(0).join().or_fail()?.or_fail()?;
230
231        verify_that!(left, eq(&right))?;
232
233        verify_that!(file::read_bytes(&path).or_fail()?, eq(&left))
234    }
235
236    #[cfg(unix)]
237    #[gtest]
238    fn created_and_loaded_files_are_restricted_before_use() -> Result<()> {
239        use std::os::unix::fs::PermissionsExt as _;
240
241        let directory = Directory::new().or_fail()?;
242        let path = directory.path().join("identity");
243
244        load_or_create_with(&path, || b"private".to_vec()).or_fail()?;
245        let created_mode = std::fs::metadata(&path).or_fail()?.permissions().mode() & 0o777;
246
247        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).or_fail()?;
248        load_or_create_with(&path, Vec::new).or_fail()?;
249        let loaded_mode = std::fs::metadata(&path).or_fail()?.permissions().mode() & 0o777;
250
251        verify_that!(created_mode, eq(PRIVATE_FILE_MODE))?;
252
253        verify_that!(loaded_mode, eq(PRIVATE_FILE_MODE))
254    }
255
256    #[cfg(unix)]
257    #[gtest]
258    fn final_symbolic_link_is_rejected() -> Result<()> {
259        use std::os::unix::fs::symlink;
260
261        let directory = Directory::new().or_fail()?;
262        let target = directory.path().join("target");
263        let path = directory.path().join("identity");
264
265        file::write_bytes(&target, b"private").or_fail()?;
266        symlink(&target, &path).or_fail()?;
267
268        verify_that!(
269            load_or_create_with(&path, Vec::new),
270            err(displays_as(contains_substring("regular file")))
271        )
272    }
273}