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>
<gresource prefix="/dev/moonarch/moonset">
<file>style.css</file>
<file>wallpaper.jpg</file>
<file compressed="true">wallpaper.jpg</file>
<file>default-avatar.svg</file>
</gresource>
</gresources>

View File

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

View File

@ -6,7 +6,6 @@ use std::fs;
use std::path::{Path, PathBuf};
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.
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)
PathBuf::from(format!("{GRESOURCE_PREFIX}/wallpaper.jpg"))
let prefix = crate::GRESOURCE_PREFIX;
PathBuf::from(format!("{prefix}/wallpaper.jpg"))
}
#[cfg(test)]

View File

@ -12,9 +12,11 @@ use gtk4::prelude::*;
use gtk4::{self as gtk, gio};
use gtk4_layer_shell::LayerShell;
pub(crate) const GRESOURCE_PREFIX: &str = "/dev/moonarch/moonset";
fn load_css(display: &gdk::Display) {
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(
display,
&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.
pub fn load_background_texture(bg_path: &Path) -> gdk::Texture {
if bg_path.starts_with("/dev/moonarch/moonset") {
gdk::Texture::from_resource(bg_path.to_str().unwrap_or(""))
let fallback = format!("{}/wallpaper.jpg", crate::GRESOURCE_PREFIX);
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 {
let file = gio::File::for_path(bg_path);
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(),
display_name: "User".to_string(),
home: dirs::home_dir().unwrap_or_default(),
uid: 0,
uid: u32::MAX,
});
// 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();
glib::idle_add_local_once(move || {
w.add_css_class("visible");
glib::idle_add_local_once(move || {
if let Some(first) = bb.first_child() {
first.grab_focus();
}
});
if let Some(first) = bb.first_child() {
first.grab_focus();
}
});
});

View File

@ -2,16 +2,15 @@
// ABOUTME: Wrappers around system commands for the session power menu.
use std::fmt;
use std::process::Command;
use std::time::Duration;
use std::io::Read;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
#[allow(dead_code)]
const POWER_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)]
pub enum PowerError {
CommandFailed { action: &'static str, message: String },
#[allow(dead_code)]
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.
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)
.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(),
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(PowerError::CommandFailed {
action,
message: format!("exit code {}: {}", output.status, stderr.trim()),
});
let deadline = Instant::now() + POWER_TIMEOUT;
loop {
match child.try_wait() {
Ok(Some(status)) => {
if !status.success() {
let mut stderr_buf = String::new();
if let Some(mut stderr) = child.stderr.take() {
let _ = stderr.read_to_string(&mut stderr_buf);
}
return Err(PowerError::CommandFailed {
action,
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.
pub fn lock() -> Result<(), PowerError> {
run_command("lock", "moonlock", &[])
run_command("lock", "/usr/bin/moonlock", &[])
}
/// Quit the Niri compositor (logout).
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.
pub fn hibernate() -> Result<(), PowerError> {
run_command("hibernate", "systemctl", &["hibernate"])
run_command("hibernate", "/usr/bin/systemctl", &["hibernate"])
}
/// Reboot the system via loginctl.
pub fn reboot() -> Result<(), PowerError> {
run_command("reboot", "loginctl", &["reboot"])
run_command("reboot", "/usr/bin/loginctl", &["reboot"])
}
/// Shut down the system via loginctl.
pub fn shutdown() -> Result<(), PowerError> {
run_command("shutdown", "loginctl", &["poweroff"])
run_command("shutdown", "/usr/bin/loginctl", &["poweroff"])
}
#[cfg(test)]

View File

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