fix: audit fixes — power timeout, timing mitigation, release profile, GREETD_SOCK cache (v0.7.1)
Update PKGBUILD version / update-pkgver (push) Successful in 2s

- Add 30s timeout with SIGKILL to power actions (adapted from moonset)
- Add 500ms minimum login response time against timing enumeration
- Cache GREETD_SOCK in GreeterState at startup
- Add [profile.release] with LTO, codegen-units=1, strip
- Add compressed="true" to GResource CSS/SVG entries
- Add SYNC comments to duplicated blur/background functions
- Add nix dependency for signal handling in power timeout
This commit is contained in:
2026-03-31 11:08:40 +02:00
parent 60d294fa37
commit f6f33a13ab
6 changed files with 125 additions and 26 deletions
+70 -19
View File
@@ -2,11 +2,18 @@
// ABOUTME: Wrappers around system commands for the greeter UI.
use std::fmt;
use std::process::Command;
use std::io::Read;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
const POWER_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)]
pub enum PowerError {
CommandFailed { action: &'static str, message: String },
Timeout { action: &'static str },
}
impl fmt::Display for PowerError {
@@ -15,41 +22,79 @@ impl fmt::Display for PowerError {
PowerError::CommandFailed { action, message } => {
write!(f, "{action} failed: {message}")
}
PowerError::Timeout { action } => {
write!(f, "{action} timed out")
}
}
}
}
impl std::error::Error for PowerError {}
/// Run a command and return a PowerError on failure.
/// Run a command with timeout and return a PowerError on failure.
///
/// Uses blocking `child.wait()` with a separate timeout thread that sends
/// SIGKILL after POWER_TIMEOUT. This runs inside `gio::spawn_blocking`,
/// so blocking is expected.
fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> {
log::debug!("Power action: {action} ({program} {args:?})");
let child = Command::new(program)
let mut child = Command::new(program)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| PowerError::CommandFailed {
action,
message: e.to_string(),
})?;
let output = child
.wait_with_output()
.map_err(|e| PowerError::CommandFailed {
action,
message: e.to_string(),
})?;
let child_pid = nix::unistd::Pid::from_raw(child.id() as i32);
let done = Arc::new(AtomicBool::new(false));
let done_clone = done.clone();
if output.status.success() {
log::debug!("Power action {action} completed successfully");
let timeout_thread = std::thread::spawn(move || {
let interval = Duration::from_millis(100);
let mut elapsed = Duration::ZERO;
while elapsed < POWER_TIMEOUT {
std::thread::sleep(interval);
if done_clone.load(Ordering::Relaxed) {
return;
}
elapsed += interval;
}
// ESRCH if the process already exited — harmless
let _ = nix::sys::signal::kill(child_pid, nix::sys::signal::Signal::SIGKILL);
});
let status = child.wait().map_err(|e| PowerError::CommandFailed {
action,
message: e.to_string(),
})?;
done.store(true, Ordering::Relaxed);
let _ = timeout_thread.join();
if status.success() {
log::debug!("Power action {action} completed");
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(PowerError::CommandFailed {
action,
message: format!("exit code {}: {}", output.status, stderr.trim()),
});
}
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if status.signal() == Some(9) {
return Err(PowerError::Timeout { action });
}
}
Ok(())
let mut stderr_buf = String::new();
if let Some(mut stderr) = child.stderr.take() {
let _ = stderr.read_to_string(&mut stderr_buf);
}
Err(PowerError::CommandFailed {
action,
message: format!("exit code {}: {}", status, stderr_buf.trim()),
})
}
}
/// Reboot the system via loginctl.
@@ -75,6 +120,12 @@ mod tests {
assert_eq!(err.to_string(), "reboot failed: No such file or directory");
}
#[test]
fn power_error_timeout_display() {
let err = PowerError::Timeout { action: "shutdown" };
assert_eq!(err.to_string(), "shutdown timed out");
}
#[test]
fn run_command_returns_error_for_missing_binary() {
let result = run_command("test", "nonexistent-binary-xyz", &[]);
@@ -99,7 +150,7 @@ mod tests {
#[test]
fn run_command_passes_args() {
let result = run_command("test", "true", &["--ignored-arg"]);
let result = run_command("test", "echo", &["hello", "world"]);
assert!(result.is_ok());
}
}