Harden temp-project cleanup with stacked safety guards
The previous remove_dir_all on a path returned by Project::path() was too trusting: an unusual CLI argument or a hand-edited project.yaml could in principle have steered cleanup into deleting the wrong directory. Replace it with safely_delete_temp_project, which refuses unless every one of the following passes: 1. Path is not a symlink (checked before canonicalize so a symlink can't smuggle a different target through). 2. Path is a directory. 3. Canonical path is under <active-data-root>/projects/ (canonical-prefix containment). 4. Directory basename contains the literal `[temp]` marker. 5. Direct children are exclusively well-known project artefacts (project.yaml, data/, history.log, playground.db, .gitignore, lock file) plus migration .bak files and atomic-write .tmp files. Any stranger file (notes.md, .git/, screenshots, etc.) makes the helper refuse. is_unmodified_temp now also requires data/ to be empty, in addition to project.yaml's tables and relationships being empty. A hand-edited yaml that drops the schema list but leaves CSVs in data/ no longer passes. Failure to delete is non-fatal -- the helper returns SafeDeleteError, the runtime logs a tracing::warn!, and the project stays on disk. Leaving an unexpected directory alone is always preferable to a wrong delete. Tests: 345 passing (272 lib + 9 + 5 + 6 + 27 + 9 + 17), 0 failing, 0 skipped. 7 new tests covering each guard, including a unix-only symlink-rejection test.
This commit is contained in:
+24
-20
@@ -34,7 +34,7 @@ use crate::dsl::Command;
|
||||
use crate::event::AppEvent;
|
||||
use crate::project::{
|
||||
Project, ProjectKind, copy_project, list_projects, open_or_create, projects_dir,
|
||||
resolve_data_root,
|
||||
resolve_data_root, safely_delete_temp_project,
|
||||
};
|
||||
use crate::theme::Theme;
|
||||
use crate::ui;
|
||||
@@ -276,7 +276,9 @@ async fn run_loop(
|
||||
// Auto-delete the active project on quit if it's an
|
||||
// unmodified temp — same rule as on project switch (see
|
||||
// perform_switch). Captures the path first, drops the
|
||||
// project (releasing the lock), then removes the dir.
|
||||
// project (releasing the lock), then asks
|
||||
// safely_delete_temp_project to verify the directory
|
||||
// before removing it.
|
||||
let cleanup_on_quit: Option<std::path::PathBuf> = session
|
||||
.project
|
||||
.as_ref()
|
||||
@@ -284,17 +286,16 @@ async fn run_loop(
|
||||
let _ = session.database.take();
|
||||
let _ = session.project.take();
|
||||
if let Some(stale) = cleanup_on_quit {
|
||||
if let Err(e) = std::fs::remove_dir_all(&stale) {
|
||||
tracing::warn!(
|
||||
path = %stale.display(),
|
||||
error = %e,
|
||||
"could not clean up unmodified temp project on quit",
|
||||
);
|
||||
} else {
|
||||
tracing::info!(
|
||||
match safely_delete_temp_project(&stale, &session.data_root) {
|
||||
Ok(()) => tracing::info!(
|
||||
path = %stale.display(),
|
||||
"cleaned up unmodified temp project on quit",
|
||||
);
|
||||
),
|
||||
Err(e) => tracing::warn!(
|
||||
path = %stale.display(),
|
||||
error = %e,
|
||||
"did not clean up unmodified temp project on quit",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,18 +410,21 @@ async fn perform_switch(
|
||||
|
||||
// The outgoing project's lock is now released; it's
|
||||
// safe to remove its directory if it was unmodified.
|
||||
// The safely_delete_temp_project helper layers multiple
|
||||
// guards (containment under data root, [temp] marker,
|
||||
// contents allowlist, no symlinks) so a bug elsewhere
|
||||
// can't escalate into deleting the wrong directory.
|
||||
if let Some(stale) = outgoing_cleanup_path {
|
||||
if let Err(e) = std::fs::remove_dir_all(&stale) {
|
||||
tracing::warn!(
|
||||
path = %stale.display(),
|
||||
error = %e,
|
||||
"could not clean up unmodified temp project",
|
||||
);
|
||||
} else {
|
||||
tracing::info!(
|
||||
match safely_delete_temp_project(&stale, &session.data_root) {
|
||||
Ok(()) => tracing::info!(
|
||||
path = %stale.display(),
|
||||
"cleaned up unmodified temp project on switch",
|
||||
);
|
||||
),
|
||||
Err(e) => tracing::warn!(
|
||||
path = %stale.display(),
|
||||
error = %e,
|
||||
"did not clean up unmodified temp project on switch",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user