fix: address audit findings — security, performance, and correctness

- Use absolute paths for all binaries in power.rs to prevent PATH hijacking
- Implement POWER_TIMEOUT via try_wait() polling (was declared but unused)
- Fix potential panic in load_background_texture when GResource path
  fails to_str() — now falls back to known wallpaper resource path
- Compress wallpaper.jpg in GResource bundle (saves ~374 KB in binary)
- Merge double idle_add_local_once into single cycle for faster focus
- Centralize GRESOURCE_PREFIX as pub(crate) const in main.rs
- Fix fallback user UID from 0 (root) to u32::MAX
- Fix CSS comment: "square card" → "circular card" (border-radius: 50%)
This commit is contained in:
nevaforget 2026-03-28 10:13:18 +01:00
parent 2d1d364270
commit 496a7a4c72
7 changed files with 61 additions and 41 deletions

View File

@ -2,7 +2,7 @@
<gresources> <gresources>
<gresource prefix="/dev/moonarch/moonset"> <gresource prefix="/dev/moonarch/moonset">
<file>style.css</file> <file>style.css</file>
<file>wallpaper.jpg</file> <file compressed="true">wallpaper.jpg</file>
<file>default-avatar.svg</file> <file>default-avatar.svg</file>
</gresource> </gresource>
</gresources> </gresources>

View File

@ -31,7 +31,7 @@ window.wallpaper {
margin-bottom: 40px; margin-bottom: 40px;
} }
/* Action button — square card */ /* Action button — circular card */
.action-button { .action-button {
min-width: 120px; min-width: 120px;
min-height: 120px; min-height: 120px;

View File

@ -6,7 +6,6 @@ use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
const MOONARCH_WALLPAPER: &str = "/usr/share/moonarch/wallpaper.jpg"; const MOONARCH_WALLPAPER: &str = "/usr/share/moonarch/wallpaper.jpg";
const GRESOURCE_PREFIX: &str = "/dev/moonarch/moonset";
/// Default config search paths: system-wide, then user-specific. /// Default config search paths: system-wide, then user-specific.
fn default_config_paths() -> Vec<PathBuf> { fn default_config_paths() -> Vec<PathBuf> {
@ -65,7 +64,8 @@ pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path)
} }
// GResource fallback path (loaded from compiled resources at runtime) // GResource fallback path (loaded from compiled resources at runtime)
PathBuf::from(format!("{GRESOURCE_PREFIX}/wallpaper.jpg")) let prefix = crate::GRESOURCE_PREFIX;
PathBuf::from(format!("{prefix}/wallpaper.jpg"))
} }
#[cfg(test)] #[cfg(test)]

View File

@ -12,9 +12,11 @@ use gtk4::prelude::*;
use gtk4::{self as gtk, gio}; use gtk4::{self as gtk, gio};
use gtk4_layer_shell::LayerShell; use gtk4_layer_shell::LayerShell;
pub(crate) const GRESOURCE_PREFIX: &str = "/dev/moonarch/moonset";
fn load_css(display: &gdk::Display) { fn load_css(display: &gdk::Display) {
let css_provider = gtk::CssProvider::new(); let css_provider = gtk::CssProvider::new();
css_provider.load_from_resource("/dev/moonarch/moonset/style.css"); css_provider.load_from_resource(&format!("{GRESOURCE_PREFIX}/style.css"));
gtk::style_context_add_provider_for_display( gtk::style_context_add_provider_for_display(
display, display,
&css_provider, &css_provider,

View File

@ -81,12 +81,15 @@ pub fn action_definitions() -> Vec<ActionDef> {
/// Load the wallpaper as a texture once, for sharing across all windows. /// Load the wallpaper as a texture once, for sharing across all windows.
pub fn load_background_texture(bg_path: &Path) -> gdk::Texture { pub fn load_background_texture(bg_path: &Path) -> gdk::Texture {
if bg_path.starts_with("/dev/moonarch/moonset") { let fallback = format!("{}/wallpaper.jpg", crate::GRESOURCE_PREFIX);
gdk::Texture::from_resource(bg_path.to_str().unwrap_or(""))
if bg_path.starts_with(crate::GRESOURCE_PREFIX) {
let resource_path = bg_path.to_str().unwrap_or(&fallback);
gdk::Texture::from_resource(resource_path)
} else { } else {
let file = gio::File::for_path(bg_path); let file = gio::File::for_path(bg_path);
gdk::Texture::from_file(&file).unwrap_or_else(|_| { gdk::Texture::from_file(&file).unwrap_or_else(|_| {
gdk::Texture::from_resource("/dev/moonarch/moonset/wallpaper.jpg") gdk::Texture::from_resource(&fallback)
}) })
} }
} }
@ -127,7 +130,7 @@ pub fn create_panel_window(texture: &gdk::Texture, app: &gtk::Application) -> gt
username: "user".to_string(), username: "user".to_string(),
display_name: "User".to_string(), display_name: "User".to_string(),
home: dirs::home_dir().unwrap_or_default(), home: dirs::home_dir().unwrap_or_default(),
uid: 0, uid: u32::MAX,
}); });
// State for confirm box // State for confirm box
@ -233,11 +236,9 @@ pub fn create_panel_window(texture: &gdk::Texture, app: &gtk::Application) -> gt
let bb = button_box_clone.clone(); let bb = button_box_clone.clone();
glib::idle_add_local_once(move || { glib::idle_add_local_once(move || {
w.add_css_class("visible"); w.add_css_class("visible");
glib::idle_add_local_once(move || { if let Some(first) = bb.first_child() {
if let Some(first) = bb.first_child() { first.grab_focus();
first.grab_focus(); }
}
});
}); });
}); });

View File

@ -2,16 +2,15 @@
// ABOUTME: Wrappers around system commands for the session power menu. // ABOUTME: Wrappers around system commands for the session power menu.
use std::fmt; use std::fmt;
use std::process::Command; use std::io::Read;
use std::time::Duration; use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
#[allow(dead_code)]
const POWER_TIMEOUT: Duration = Duration::from_secs(30); const POWER_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)] #[derive(Debug)]
pub enum PowerError { pub enum PowerError {
CommandFailed { action: &'static str, message: String }, CommandFailed { action: &'static str, message: String },
#[allow(dead_code)]
Timeout { action: &'static str }, Timeout { action: &'static str },
} }
@ -32,55 +31,73 @@ impl std::error::Error for PowerError {}
/// Run a command with timeout and return a PowerError on failure. /// Run a command with timeout and return a PowerError on failure.
fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> { fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> {
let child = Command::new(program) let mut child = Command::new(program)
.args(args) .args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn() .spawn()
.map_err(|e| PowerError::CommandFailed { .map_err(|e| PowerError::CommandFailed {
action, action,
message: e.to_string(), message: e.to_string(),
})?; })?;
let output = child let deadline = Instant::now() + POWER_TIMEOUT;
.wait_with_output() loop {
.map_err(|e| PowerError::CommandFailed { match child.try_wait() {
action, Ok(Some(status)) => {
message: e.to_string(), if !status.success() {
})?; let mut stderr_buf = String::new();
if let Some(mut stderr) = child.stderr.take() {
if !output.status.success() { let _ = stderr.read_to_string(&mut stderr_buf);
let stderr = String::from_utf8_lossy(&output.stderr); }
return Err(PowerError::CommandFailed { return Err(PowerError::CommandFailed {
action, action,
message: format!("exit code {}: {}", output.status, stderr.trim()), message: format!("exit code {}: {}", status, stderr_buf.trim()),
}); });
}
return Ok(());
}
Ok(None) => {
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
return Err(PowerError::Timeout { action });
}
std::thread::sleep(Duration::from_millis(100));
}
Err(e) => {
return Err(PowerError::CommandFailed {
action,
message: e.to_string(),
});
}
}
} }
Ok(())
} }
/// Lock the current session by launching moonlock. /// Lock the current session by launching moonlock.
pub fn lock() -> Result<(), PowerError> { pub fn lock() -> Result<(), PowerError> {
run_command("lock", "moonlock", &[]) run_command("lock", "/usr/bin/moonlock", &[])
} }
/// Quit the Niri compositor (logout). /// Quit the Niri compositor (logout).
pub fn logout() -> Result<(), PowerError> { pub fn logout() -> Result<(), PowerError> {
run_command("logout", "niri", &["msg", "action", "quit"]) run_command("logout", "/usr/bin/niri", &["msg", "action", "quit"])
} }
/// Hibernate the system via systemctl. /// Hibernate the system via systemctl.
pub fn hibernate() -> Result<(), PowerError> { pub fn hibernate() -> Result<(), PowerError> {
run_command("hibernate", "systemctl", &["hibernate"]) run_command("hibernate", "/usr/bin/systemctl", &["hibernate"])
} }
/// Reboot the system via loginctl. /// Reboot the system via loginctl.
pub fn reboot() -> Result<(), PowerError> { pub fn reboot() -> Result<(), PowerError> {
run_command("reboot", "loginctl", &["reboot"]) run_command("reboot", "/usr/bin/loginctl", &["reboot"])
} }
/// Shut down the system via loginctl. /// Shut down the system via loginctl.
pub fn shutdown() -> Result<(), PowerError> { pub fn shutdown() -> Result<(), PowerError> {
run_command("shutdown", "loginctl", &["poweroff"]) run_command("shutdown", "/usr/bin/loginctl", &["poweroff"])
} }
#[cfg(test)] #[cfg(test)]

View File

@ -5,7 +5,6 @@ use nix::unistd::{getuid, User as NixUser};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
const DEFAULT_ACCOUNTSSERVICE_DIR: &str = "/var/lib/AccountsService/icons"; const DEFAULT_ACCOUNTSSERVICE_DIR: &str = "/var/lib/AccountsService/icons";
const GRESOURCE_PREFIX: &str = "/dev/moonarch/moonset";
/// Represents the current user for the power menu. /// Represents the current user for the power menu.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -74,7 +73,8 @@ pub fn get_avatar_path_with(
/// Return the GResource path to the default avatar SVG. /// Return the GResource path to the default avatar SVG.
pub fn get_default_avatar_path() -> String { pub fn get_default_avatar_path() -> String {
format!("{GRESOURCE_PREFIX}/default-avatar.svg") let prefix = crate::GRESOURCE_PREFIX;
format!("{prefix}/default-avatar.svg")
} }
#[cfg(test)] #[cfg(test)]