Skip to main content

onelf_format/
cache_layout.rs

1//! Shared layout and safety rules for the on-disk extraction cache.
2//!
3//! The runtime and the packer CLI both read and write this cache. They used
4//! to answer "where is it, and is it safe to use" independently, and drifted:
5//! one required an absolute env-derived path and a `0700` uid-owned
6//! directory, the other happily fell back to a shared `/tmp`. Both now go
7//! through here.
8//!
9//! The current user id is a parameter rather than a call, so this crate stays
10//! dependency-free; each caller already has a way to ask the OS.
11//!
12//! Layout under the resolved root:
13//!
14//! ```text
15//! pkg/<id>              extracted package tree
16//! pkg/.<id>.ready       completion marker, written only after a full extract
17//! pkg/.<id>.tmp         extraction scratch, renamed to pkg/<id> on success
18//! cas/<aa>/<hash>       content blob, hardlinked into package trees
19//! meta/<id>             last-used timestamp
20//! lock/<id>             in-use lock, held shared for an instance's lifetime
21//! lock/<id>.extract     extraction mutex
22//! ```
23
24use std::path::{Path, PathBuf};
25
26use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
27
28/// True if `path` is a real directory (not a symlink) owned by `uid` with no
29/// group or other permission bits. Uses `symlink_metadata`, so a planted
30/// symlink is rejected rather than followed.
31pub fn is_safe_owned_dir(path: &Path, uid: u32) -> bool {
32    let Ok(md) = std::fs::symlink_metadata(path) else {
33        return false;
34    };
35    md.is_dir() && md.uid() == uid && (md.mode() & 0o077) == 0
36}
37
38/// Ensure `dir` is a `0700` directory owned by `uid`, creating it atomically
39/// at that mode when absent. Returns false if it exists as a symlink or as
40/// another user's directory, which are never chmod'd through; a real
41/// directory we own but with loose permissions is tightened in place.
42pub fn ensure_safe_dir(dir: &Path, uid: u32) -> bool {
43    if let Some(parent) = dir.parent() {
44        let _ = std::fs::create_dir_all(parent);
45    }
46    match std::fs::DirBuilder::new().mode(0o700).create(dir) {
47        Ok(()) => return true,
48        Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
49        Err(_) => return false,
50    }
51    if let Ok(md) = std::fs::symlink_metadata(dir)
52        && md.is_dir()
53        && md.uid() == uid
54        && md.mode() & 0o077 != 0
55    {
56        let _ = std::fs::set_permissions(dir, PermissionsExt::from_mode(0o700));
57    }
58    is_safe_owned_dir(dir, uid)
59}
60
61/// Resolve the cache root, or `None` when no safe location can be
62/// established. Callers MUST treat `None` as a refusal rather than
63/// substituting a guess: a shared world-writable fallback is what this
64/// function exists to prevent.
65///
66/// Prefers `$XDG_CACHE_HOME`, then `~/.cache`, both of which are already
67/// user-private. Only an *absolute* env-derived path is trusted, because an
68/// empty or relative one would resolve against the current directory, which
69/// the caller does not control. `fallback` supplies the last resort (the
70/// runtime passes its `0700` per-uid directory; a caller with none passes
71/// `None`).
72pub fn resolve_root(uid: u32, fallback: Option<PathBuf>) -> Option<PathBuf> {
73    let abs = |v: std::ffi::OsString| -> Option<PathBuf> {
74        let p = PathBuf::from(v);
75        p.is_absolute().then_some(p)
76    };
77    let base = std::env::var_os("XDG_CACHE_HOME")
78        .and_then(abs)
79        .or_else(|| {
80            std::env::var_os("HOME")
81                .and_then(abs)
82                .map(|h| h.join(".cache"))
83        })
84        .or(fallback)?;
85    let onelf = base.join("onelf");
86    ensure_safe_dir(&onelf, uid).then_some(onelf)
87}
88
89/// Extracted tree for `package_id`.
90pub fn pkg_dir(root: &Path, package_id: &str) -> PathBuf {
91    root.join("pkg").join(package_id)
92}
93
94/// Completion marker for `package_id`. Its presence is what distinguishes a
95/// fully extracted tree from one an interrupted run left behind.
96pub fn ready_marker(root: &Path, package_id: &str) -> PathBuf {
97    root.join("pkg").join(format!(".{package_id}.ready"))
98}
99
100/// Extraction scratch for `package_id`, renamed to [`pkg_dir`] once the
101/// tree is complete. An interrupted extraction leaves it behind.
102pub fn extract_tmp_dir(root: &Path, package_id: &str) -> PathBuf {
103    root.join("pkg").join(format!(".{package_id}.tmp"))
104}
105
106/// In-use lock for `package_id`, held shared for an instance's lifetime.
107pub fn lock_path(root: &Path, package_id: &str) -> PathBuf {
108    root.join("lock").join(package_id)
109}
110
111/// Extraction mutex for `package_id`, distinct from the in-use lock so a
112/// second runner waits only for extraction, not for the first instance's
113/// whole lifetime.
114pub fn extract_lock_path(root: &Path, package_id: &str) -> PathBuf {
115    root.join("lock").join(format!("{package_id}.extract"))
116}
117
118/// Last-used timestamp for `package_id`.
119pub fn meta_path(root: &Path, package_id: &str) -> PathBuf {
120    root.join("meta").join(package_id)
121}
122
123/// Store-wide lock separating extraction from collection.
124///
125/// Extraction holds it *shared* while it populates the content store, because
126/// a blob is briefly reachable only from the store itself: it is renamed into
127/// place before being hardlinked into the package tree, and in that window its
128/// link count does not yet reflect the reference that is about to exist.
129/// Collection holds it *exclusive*, which proves no extraction is inside that
130/// window and therefore that link counts can be trusted.
131/// The GL build store: one directory per label holding the pinned
132/// build's file. Its extraction lives under `pkg/` like any package.
133pub fn platform_dir(root: &Path) -> PathBuf {
134    root.join("platform")
135}
136
137pub fn cas_lock_path(root: &Path) -> PathBuf {
138    root.join("lock").join("cas")
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    /// Distinguishes scratch paths between concurrently running tests, which
146    /// share a process and therefore a pid.
147    fn unique(tag: &str) -> std::path::PathBuf {
148        use std::sync::atomic::{AtomicU64, Ordering};
149        static SEQ: AtomicU64 = AtomicU64::new(0);
150        std::env::temp_dir().join(format!(
151            "onelf-{tag}-{}-{}",
152            std::process::id(),
153            SEQ.fetch_add(1, Ordering::Relaxed)
154        ))
155    }
156
157    /// The uid that owns what this process creates, read back from a scratch
158    /// file. The crate has no dependencies, so there is no `getuid` to call.
159    fn uid() -> u32 {
160        let p = unique("uidprobe");
161        std::fs::write(&p, b"").unwrap();
162        let u = std::fs::symlink_metadata(&p).unwrap().uid();
163        let _ = std::fs::remove_file(&p);
164        u
165    }
166
167    #[test]
168    fn safe_dir_rules() {
169        let me = uid();
170        let dir = unique("layout");
171        let _ = std::fs::remove_dir_all(&dir);
172
173        assert!(ensure_safe_dir(&dir, me), "fresh dir is created 0700");
174        assert!(is_safe_owned_dir(&dir, me));
175
176        std::fs::set_permissions(&dir, PermissionsExt::from_mode(0o755)).unwrap();
177        assert!(!is_safe_owned_dir(&dir, me), "0755 is not owner-private");
178        assert!(ensure_safe_dir(&dir, me), "our own dir is tightened back");
179        assert!(is_safe_owned_dir(&dir, me));
180
181        assert!(
182            !is_safe_owned_dir(&dir, me.wrapping_add(1)),
183            "another uid's directory is refused, never chmod'd"
184        );
185
186        let _ = std::fs::remove_dir_all(&dir);
187    }
188
189    #[test]
190    fn missing_and_non_directory_are_unsafe() {
191        let me = uid();
192        assert!(!is_safe_owned_dir(Path::new("/nonexistent/onelf/xyz"), me));
193
194        let f = unique("layout-file");
195        std::fs::write(&f, b"x").unwrap();
196        assert!(!is_safe_owned_dir(&f, me), "a regular file is not a dir");
197        let _ = std::fs::remove_file(&f);
198    }
199
200    /// A relative `XDG_CACHE_HOME` must be ignored in favour of the
201    /// fallback, so the cache never lands under the current directory.
202    ///
203    /// This is the only test in the crate that touches the environment, and
204    /// it restores both variables before returning.
205    #[test]
206    fn relative_env_roots_are_not_trusted() {
207        let me = uid();
208        let fallback = unique("fb");
209        let _ = std::fs::remove_dir_all(&fallback);
210        std::fs::create_dir_all(&fallback).unwrap();
211
212        let prev_xdg = std::env::var_os("XDG_CACHE_HOME");
213        let prev_home = std::env::var_os("HOME");
214        // SAFETY: no other test in this binary reads the environment.
215        unsafe {
216            std::env::set_var("XDG_CACHE_HOME", "relative/path");
217            std::env::remove_var("HOME");
218        }
219
220        let root = resolve_root(me, Some(fallback.clone()));
221
222        unsafe {
223            match prev_xdg {
224                Some(v) => std::env::set_var("XDG_CACHE_HOME", v),
225                None => std::env::remove_var("XDG_CACHE_HOME"),
226            }
227            if let Some(v) = prev_home {
228                std::env::set_var("HOME", v);
229            }
230        }
231
232        assert_eq!(root, Some(fallback.join("onelf")));
233        let _ = std::fs::remove_dir_all(&fallback);
234    }
235
236    #[test]
237    fn layout_paths_are_stable() {
238        let root = Path::new("/c/onelf");
239        assert_eq!(pkg_dir(root, "ab"), Path::new("/c/onelf/pkg/ab"));
240        assert_eq!(
241            ready_marker(root, "ab"),
242            Path::new("/c/onelf/pkg/.ab.ready")
243        );
244        assert_eq!(lock_path(root, "ab"), Path::new("/c/onelf/lock/ab"));
245        assert_eq!(
246            extract_lock_path(root, "ab"),
247            Path::new("/c/onelf/lock/ab.extract")
248        );
249        assert_eq!(meta_path(root, "ab"), Path::new("/c/onelf/meta/ab"));
250    }
251}