diff --git a/crates/engine/src/autosave.rs b/crates/engine/src/autosave.rs index cbb5e56b..e1a30f08 100644 --- a/crates/engine/src/autosave.rs +++ b/crates/engine/src/autosave.rs @@ -334,10 +334,22 @@ mod native { type WriteHook = fn(&Drawing, &str) -> std::result::Result, String>; + /// Create the autosave folder `dir` and any missing parents. On Unix the folders made here are + /// owner-only (`0o700`): autosave files are unsaved drawings, and with the default mode other + /// accounts on the machine could read them. A folder that already exists keeps its mode, since + /// SAVEFILEPATH can be any folder the user picked. + fn create_folder(dir: &Path) -> std::io::Result<()> { + let mut builder = std::fs::DirBuilder::new(); + builder.recursive(true); + #[cfg(unix)] + std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700); + builder.create(dir) + } + fn write_file(write: WriteHook, doc: &Drawing, path: &str) -> std::result::Result<(), String> { let bytes = write(doc, "autosave.dxf")?; if let Some(dir) = Path::new(path).parent() { - std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; + create_folder(dir).map_err(|e| e.to_string())?; } // Create a fresh temporary file exclusively and rename it into place, so a planted // `.tmp` symlink in a shared autosave directory cannot redirect the write (CWE-59). @@ -434,7 +446,7 @@ mod native { } self.last_beat = Some(now); let dir = Path::new(dir); - let r = std::fs::create_dir_all(dir).and_then(|()| std::fs::write(lock_path(dir, &self.instance), std::process::id().to_string())); + let r = create_folder(dir).and_then(|()| std::fs::write(lock_path(dir, &self.instance), std::process::id().to_string())); if let Err(e) = r { log::warn!("autosave: lock file in {}: {e}", dir.display()); } diff --git a/crates/engine/tests/autosave.rs b/crates/engine/tests/autosave.rs index 699f04e7..abd0ecbd 100644 --- a/crates/engine/tests/autosave.rs +++ b/crates/engine/tests/autosave.rs @@ -211,6 +211,40 @@ fn autosave_files_are_written_in_the_background_and_removed_on_exit() { let _ = std::fs::remove_dir_all(&dir); } +/// Autosave files are unsaved drawings: on Unix an autosave folder that doesn't exist yet is made +/// owner-only, and one that already exists (SAVEFILEPATH can be any folder) keeps its mode. +#[cfg(unix)] +#[test] +fn a_new_autosave_folder_is_owner_only() { + use std::os::unix::fs::PermissionsExt as _; + let mode = |p: &std::path::Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o777; + hooks(); + let base = temp_dir("modes"); + let picked = base.join("picked"); + std::fs::create_dir_all(&picked).unwrap(); + std::fs::set_permissions(&picked, std::fs::Permissions::from_mode(0o755)).unwrap(); + for (dir, expected) in [(base.join("new").join("autosave"), 0o700), (picked, 0o755)] { + let mut s = Session::new(); + draw(&mut s); + let mut a = Autosaver::new(autosave::instance_id()); + a.plan(&s, 0.0, 1, &dir.to_string_lossy()); + let jobs = a.plan(&s, 60.0, 1, &dir.to_string_lossy()); + let path = writes(&jobs).pop().unwrap(); + a.perform(jobs); + for _ in 0..500 { + if std::path::Path::new(&path).exists() { + break; + } + std::thread::sleep(Duration::from_millis(10)); + } + assert!(std::path::Path::new(&path).exists()); + assert_eq!(mode(&dir), expected, "{}", dir.display()); + a.shutdown(); + } + assert_eq!(mode(&base.join("new")), 0o700); + let _ = std::fs::remove_dir_all(&base); +} + #[test] fn a_failed_autosave_is_tried_again_without_another_change() { hooks();