Autosave: a new autosave folder is owner-only on Unix (#1868)

Autosave files are unsaved drawings. The autosave folder was made with
create_dir_all, by the first write or for the lock file, so on Unix it
got the default mode and other accounts on the machine could read the
files. A folder the autosaver creates is now 0o700. A folder that already
exists keeps its mode, since SAVEFILEPATH can be any folder the user
picked.

The same change as storytold/pdfcraft#3320 and storytold/vectorcraft#3406.
This commit is contained in:
oracola91
2026-10-24 01:50:24 -04:00
committed by GitHub
parent 587d1dcd69
commit 4c327cccd0
2 changed files with 48 additions and 2 deletions
+14 -2
View File
@@ -334,10 +334,22 @@ mod native {
type WriteHook = fn(&Drawing, &str) -> std::result::Result<Vec<u8>, 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
// `<path>.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());
}
+34
View File
@@ -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();