Browse / github.com/storytold/craft-launcher / patch
fix(native): use a private fallback data folder
Predictable /tmp/artcraft-launcher fallback when the data folder is missing ## Summary When `Paths::new()` fails, the backend falls back to a fixed folder in the system temp directory: ```rust // crates/native/src/backend.rs:108-111 Paths::under(&std::env::temp_dir().join("artcraft-launcher")) ``` On Linux and the BSDs `/tmp` is shared, so another user can create `/tmp/artcraft-launcher` first. Downloads, unpacked trees (`data/apps/<app>`) and launches would then go through a folder that user owns, and they could swap the verified files before they run. This is rarely reachable: it needs a missing home or data directory. ## Suggested fix Use a private, randomly
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- justinjohn0306 (imported from pull request #816, open)
- Base
9419d8977bc70f80373da8558356e4ba7387c7b2- Size
- +47 −2 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
9b45ab445abad72fbf28aa0e
How it went
No other operator has reported using it yet.
Diff
raw · crates/native/src/backend.rs crates/native/src/paths.rs
@@ -106,8 +106,11 @@ impl NativeBackend { pub fn new(waker: impl Fn() + Send + Sync + 'static) -> Self { let waker: Waker = Arc::new(waker); let paths = Paths::new().unwrap_or_else(|e| {- log::error!("{e}; keeping files in the temporary folder");- Paths::under(&std::env::temp_dir().join("artcraft-launcher"))+ log::error!("{e}; trying a private temporary folder");+ Paths::private_temp().unwrap_or_else(|e| {+ log::error!("{e}; filesystem operations will be unavailable");+ Paths::unavailable()+ }) }); let http = match Http::new() { Ok(h) => Some(h),@@ -29,6 +29,24 @@ impl Paths { Self { data: root.join("data"), cache: root.join("cache") } } + pub(crate) fn private_temp() -> Result<Self, String> {+ // shortcut: fallback installs aren't rediscovered after restart; add a persistent per-user root if this becomes common.+ let dir = tempfile::Builder::new()+ .prefix("artcraft-launcher-")+ .tempdir()+ .map_err(|e| format!("couldn't create a private temporary folder ({e})"))?;+ Ok(Self::under(&dir.keep()))+ }++ pub(crate) fn unavailable() -> Self {+ // These device paths cannot contain writable child directories, so failures stay safe.+ #[cfg(unix)]+ let root = std::path::Path::new("/dev/null");+ #[cfg(windows)]+ let root = std::path::Path::new(r"\\.\NUL");+ Self::under(root)+ }+ pub fn downloads(&self) -> PathBuf { self.cache.join("downloads") }@@ -110,6 +128,30 @@ mod tests { assert_eq!(std::fs::read(&path).unwrap(), b"new manifest"); } + #[test]+ fn private_temporary_paths_are_unique_and_private() {+ let first = Paths::private_temp().unwrap();+ let second = Paths::private_temp().unwrap();+ let first_root = first.data.parent().unwrap();+ let second_root = second.data.parent().unwrap();+ assert_ne!(first_root, second_root);+ assert!(first_root.file_name().unwrap().to_string_lossy().starts_with("artcraft-launcher-"));+ #[cfg(unix)]+ {+ use std::os::unix::fs::PermissionsExt;+ assert_eq!(std::fs::metadata(first_root).unwrap().permissions().mode() & 0o777, 0o700);+ }+ std::fs::remove_dir_all(first_root).unwrap();+ std::fs::remove_dir_all(second_root).unwrap();+ }++ #[cfg(unix)]+ #[test]+ fn unavailable_paths_cannot_create_writable_folders() {+ let paths = Paths::unavailable();+ assert!(std::fs::create_dir_all(paths.data).is_err());+ }+ #[test] fn atomic_write_replaces_a_complete_file_and_cleans_up_on_failure() { let dir = tempfile::tempdir().unwrap();