fix: audit fixes — power timeout, timing mitigation, release profile, GREETD_SOCK cache (v0.7.1)
Update PKGBUILD version / update-pkgver (push) Successful in 2s
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:
+19
-3
@@ -133,6 +133,10 @@ pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
|
||||
|
||||
// -- GPU blur via GskBlurNode -------------------------------------------------
|
||||
|
||||
// SYNC: MAX_BLUR_DIMENSION, render_blurred_texture, and create_background_picture
|
||||
// are duplicated in moonlock/src/lockscreen.rs and moonset/src/panel.rs.
|
||||
// Changes here must be mirrored to the other two projects.
|
||||
|
||||
/// Maximum texture dimension before downscaling for blur.
|
||||
/// Keeps GPU work reasonable on 4K+ displays.
|
||||
const MAX_BLUR_DIMENSION: f32 = 1920.0;
|
||||
@@ -240,6 +244,7 @@ struct GreeterState {
|
||||
default_avatar_texture: Option<gdk::Texture>,
|
||||
failed_attempts: HashMap<String, u32>,
|
||||
greetd_sock: Arc<Mutex<Option<UnixStream>>>,
|
||||
greetd_sock_path: Option<String>,
|
||||
login_cancelled: Arc<std::sync::atomic::AtomicBool>,
|
||||
fingerprint_available: bool,
|
||||
/// Incremented on each user switch to discard stale async results.
|
||||
@@ -281,12 +286,16 @@ pub fn create_greeter_window(
|
||||
log::debug!("GTK theme: {theme}");
|
||||
}
|
||||
|
||||
// Cache GREETD_SOCK at startup — it never changes during runtime
|
||||
let greetd_sock_path = std::env::var("GREETD_SOCK").ok().filter(|p| !p.is_empty());
|
||||
|
||||
let state = Rc::new(RefCell::new(GreeterState {
|
||||
selected_user: None,
|
||||
avatar_cache: HashMap::new(),
|
||||
default_avatar_texture: None,
|
||||
failed_attempts: HashMap::new(),
|
||||
greetd_sock: Arc::new(Mutex::new(None)),
|
||||
greetd_sock_path,
|
||||
login_cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
fingerprint_available: false,
|
||||
user_switch_generation: 0,
|
||||
@@ -960,9 +969,9 @@ fn attempt_login(
|
||||
session_dropdown: >k::DropDown,
|
||||
) {
|
||||
log::debug!("Login attempt for user: {}", user.username);
|
||||
let sock_path = match std::env::var("GREETD_SOCK") {
|
||||
Ok(p) if !p.is_empty() => p,
|
||||
_ => {
|
||||
let sock_path = match state.borrow().greetd_sock_path.clone() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
show_error(error_label, password_entry, strings.greetd_sock_not_set);
|
||||
return;
|
||||
}
|
||||
@@ -1009,6 +1018,8 @@ fn attempt_login(
|
||||
state,
|
||||
async move {
|
||||
let session_name_clone = session_name.clone();
|
||||
// Minimum response time to prevent username enumeration via timing
|
||||
let login_start = std::time::Instant::now();
|
||||
let result = gio::spawn_blocking(move || {
|
||||
login_worker(
|
||||
&username,
|
||||
@@ -1022,6 +1033,11 @@ fn attempt_login(
|
||||
)
|
||||
})
|
||||
.await;
|
||||
let elapsed = login_start.elapsed();
|
||||
let min_response = std::time::Duration::from_millis(500);
|
||||
if elapsed < min_response {
|
||||
glib::timeout_future(min_response - elapsed).await;
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(Ok(LoginResult::Success { username })) => {
|
||||
|
||||
+70
-19
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user