feat: enhance safety filters for shell commands by normalizing ANSI-C quoting
This commit is contained in:
+50
-15
@@ -3,6 +3,7 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
|
||||
/// A PID-file lock (`<session_dir>/.lock`) tied to the current process,
|
||||
/// auto-removed on drop.
|
||||
@@ -21,29 +22,63 @@ impl SessionLock {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to acquire the session lock.
|
||||
/// Attempt to acquire the session lock using an atomic file creation.
|
||||
///
|
||||
/// Flow: if `.lock` exists, read the PID inside it and check
|
||||
/// `is_alive` — if that process is still running, fail to acquire →
|
||||
/// otherwise (no lock file, unreadable PID, or dead owner) write our
|
||||
/// own PID into `.lock` and succeed.
|
||||
/// Flow: try `O_CREAT | O_EXCL` via `create_new(true)` → if that
|
||||
/// succeeds, the lock is ours — write our PID and return ok. If the
|
||||
/// file already exists, read the PID inside it and check `is_alive`:
|
||||
/// if that process is still running, fail to acquire; otherwise the
|
||||
/// lock is stale — overwrite it with our own PID and succeed.
|
||||
///
|
||||
/// Why: a stale lock file from a crashed process must not permanently
|
||||
/// block new sessions, so liveness is re-checked via `kill(pid, 0)`
|
||||
/// rather than trusting the file's mere existence.
|
||||
/// Why: `create_new(true)` is atomic on POSIX (unlike the previous
|
||||
/// read-then-write pattern which had a TOCTOU race between checking
|
||||
/// `path.exists()` and writing). The stale-lock recovery path reads
|
||||
/// the stale PID and verifies liveness via `kill(pid, 0)`.
|
||||
///
|
||||
/// Return: `Ok(true)` if acquired, `Ok(false)` if another live
|
||||
/// process holds it, `Err` on I/O failure.
|
||||
pub fn try_lock(&self) -> std::io::Result<bool> {
|
||||
if self.path.exists() {
|
||||
let content = fs::read_to_string(&self.path).unwrap_or_default();
|
||||
if let Ok(pid) = content.trim().parse::<u32>() {
|
||||
if self.is_alive(pid) {
|
||||
return Ok(false);
|
||||
}
|
||||
// Phase 1: try atomic create. If it succeeds, the lock is ours.
|
||||
match fs::OpenOptions::new()
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(&self.path)
|
||||
{
|
||||
Ok(mut file) => {
|
||||
write!(file, "{}", self.pid)?;
|
||||
file.sync_all()?;
|
||||
return Ok(true);
|
||||
}
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
// Lock file exists — check if it's stale.
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
// Phase 2: lock file exists — check liveness of the owning process.
|
||||
let content = fs::read_to_string(&self.path).unwrap_or_default();
|
||||
if let Ok(pid) = content.trim().parse::<u32>() {
|
||||
if self.is_alive(pid) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
fs::write(&self.path, self.pid.to_string())?;
|
||||
|
||||
// Phase 3: stale lock — overwrite it atomically (best-effort).
|
||||
// Use a temp file + rename to avoid partial writes corrupting the lock.
|
||||
let tmp = self.path.with_extension("lock.tmp");
|
||||
{
|
||||
let mut tmp_file = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.open(&tmp)?;
|
||||
write!(tmp_file, "{}", self.pid)?;
|
||||
tmp_file.sync_all()?;
|
||||
}
|
||||
fs::rename(&tmp, &self.path)?;
|
||||
// Sync the parent directory so the rename survives a crash.
|
||||
if let Some(parent) = self.path.parent() {
|
||||
let _ = fs::File::open(parent).and_then(|d| d.sync_all());
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user