fix: security hardening, blur geometry, and performance audit fixes (v0.6.2)
All checks were successful
Update PKGBUILD version / update-pkgver (push) Successful in 2s
All checks were successful
Update PKGBUILD version / update-pkgver (push) Successful in 2s
Security: cache dirs now 0o700 via DirBuilder::mode(), blur config validated (finite + clamp 0–200), TOCTOU socket pre-check removed. Quality: GPU blur geometry fixed (texture shifted instead of stretched), is_valid_username hardened, is_valid_gtk_theme extracted as testable fn, save_last_session error handling consistent with save_last_user. Performance: blurred texture cached across monitors (1x GPU renderpass instead of N), FingerprintProbe device proxy cached in GreeterState with generation counter to prevent race condition on fast user-switch. Clippy: all 7 warnings resolved (collapsible if-let, redundant closure, manual_range_contains, too_many_arguments suppressed for GTK widget fns). Tests: 109 → 118 (GTK theme validation, Unicode usernames, cache dir permissions, unwritable dir handling, blur config edge cases).
This commit is contained in:
parent
f3f4db1ab1
commit
a2dc89854d
10
CLAUDE.md
10
CLAUDE.md
@ -45,8 +45,8 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
|
||||
- `power.rs` — Reboot/Shutdown via loginctl
|
||||
- `i18n.rs` — Locale-Erkennung (LANG / /etc/locale.conf) und String-Tabellen (DE/EN), alle UI- und Login-Fehlermeldungen
|
||||
- `fingerprint.rs` — fprintd D-Bus Probe (gio::DBusProxy) — Geräteerkennung und Enrollment-Check für UI-Feedback
|
||||
- `config.rs` — TOML-Config ([appearance] background, gtk-theme, fingerprint-enabled) + Wallpaper-Fallback
|
||||
- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC (Multi-Stage-Auth für fprintd), Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o600 Permissions)
|
||||
- `config.rs` — TOML-Config ([appearance] background, gtk-theme, fingerprint-enabled) + Wallpaper-Fallback + Blur-Validierung (finite, clamp 0–200)
|
||||
- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC (Multi-Stage-Auth für fprintd), Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o700 Dirs, 0o600 Files)
|
||||
- `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor, systemd-journal-logger
|
||||
- `resources/style.css` — Catppuccin-inspiriertes Theme
|
||||
|
||||
@ -57,13 +57,13 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
|
||||
- **Async Login**: `glib::spawn_future_local` + `gio::spawn_blocking` statt raw Threads
|
||||
- **Socket-Cancellation**: `Arc<Mutex<Option<UnixStream>>>` + `AtomicBool` für saubere Abbrüche
|
||||
- **Avatar-Cache**: `HashMap<String, gdk::Texture>` in `Rc<RefCell<GreeterState>>`
|
||||
- **GPU-Blur via GskBlurNode**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` im `connect_realize` Callback — kein CPU-Blur, kein Disk-Cache, kein `image`-Crate
|
||||
- **Fingerprint via greetd Multi-Stage PAM**: fprintd D-Bus nur als Probe (Gerät/Enrollment), eigentliche Verifizierung läuft über PAM im greetd-Auth-Loop. `auth_message_type: "secret"` → Passwort, alles andere → `None` (PAM entscheidet). 60s Socket-Timeout bei fprintd.
|
||||
- **GPU-Blur via GskBlurNode**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` im `connect_realize` Callback — kein CPU-Blur, kein Disk-Cache, kein `image`-Crate. Blurred Texture wird per `Rc<RefCell<Option<gdk::Texture>>>` über alle Monitore gecacht (1x GPU-Renderpass statt N).
|
||||
- **Fingerprint via greetd Multi-Stage PAM**: fprintd D-Bus nur als Probe (Gerät/Enrollment), eigentliche Verifizierung läuft über PAM im greetd-Auth-Loop. `auth_message_type: "secret"` → Passwort, alles andere → `None` (PAM entscheidet). 60s Socket-Timeout bei fprintd. Device-Proxy in `GreeterState` gecacht, Generation-Counter gegen Race Conditions bei schnellem User-Switch.
|
||||
- **Symmetrie mit moonlock/moonset**: Gleiche Patterns (i18n, config, users, power, GResource, GPU-Blur)
|
||||
- **Session-Validierung**: Relative Pfade erlaubt (greetd löst PATH auf), nur `..`/Null-Bytes werden abgelehnt
|
||||
- **GTK-Theme-Validierung**: Nur alphanumerisch + `_-+.` erlaubt, verhindert Path-Traversal über Config
|
||||
- **Journal-Logging**: `systemd-journal-logger` statt File-Logging — `journalctl -t moongreet`, Debug-Level per `MOONGREET_DEBUG` Env-Var
|
||||
- **File Permissions**: Cache-Dateien 0o600
|
||||
- **File Permissions**: Cache-Verzeichnisse 0o700 via `DirBuilder::mode()`, Cache-Dateien 0o600
|
||||
- **Testbare Persistence**: `save_*_to`/`load_*_from` Varianten mit konfigurierbarem Pfad für Unit-Tests
|
||||
- **Shared Wallpaper Texture**: `gdk::Texture` wird einmal in `load_background_texture()` dekodiert und per Ref-Count an alle Fenster geteilt — vermeidet redundante JPEG-Dekodierung pro Monitor
|
||||
- **Wallpaper-Validierung**: GResource-Zweig via `resources_lookup_data()` + `from_bytes()` (kein Abort bei fehlendem Pfad), Dateigröße-Limit 50 MB, non-UTF-8-Pfade → `None`
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "moongreet"
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
edition = "2024"
|
||||
description = "A greetd greeter for Wayland with GTK4 and Layer Shell"
|
||||
license = "MIT"
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
# Decisions
|
||||
|
||||
## 2026-03-30 – Full audit fix: security, quality, performance (v0.6.2)
|
||||
|
||||
- **Who**: Ragnar, Dom
|
||||
- **Why**: Three parallel audits (security, code quality, performance) identified 10 actionable findings across the codebase — from world-readable cache dirs to a GPU blur geometry bug to a race condition in fingerprint probing.
|
||||
- **Tradeoffs**: `too_many_arguments` Clippy warnings suppressed with `#[allow]` rather than introducing a `UiWidgets` struct — GTK's `clone!` macro with `#[weak]` refs requires individual widget parameters, a struct would fight the idiom. Async avatar loading skipped because `Pixbuf` is `!Send`; cache already prevents repeat loads. TOCTOU socket pre-check removed entirely — `connect()` in login_worker already handles errors, the `metadata()` check gave false security guarantees.
|
||||
- **How**: Cache dirs use `DirBuilder::mode(0o700)` instead of `create_dir_all`. Blur config clamped to `0.0..=200.0` with `is_finite()` guard. Blur texture cached in `Rc<RefCell<Option<gdk::Texture>>>` across monitors. FingerprintProbe device proxy cached in `GreeterState` with generation counter to prevent stale async writes. GPU blur geometry fixed (`-pad` origin shift instead of texture stretching). `is_valid_gtk_theme` extracted as testable function. 9 new tests.
|
||||
|
||||
## 2026-03-29 – Fingerprint authentication via greetd multi-stage PAM
|
||||
|
||||
- **Who**: Ragnar, Dom
|
||||
|
||||
@ -72,8 +72,12 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
||||
Some(parent.join(&bg).to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
if appearance.background_blur.is_some() {
|
||||
merged.background_blur = appearance.background_blur;
|
||||
if let Some(blur) = appearance.background_blur {
|
||||
if blur.is_finite() {
|
||||
merged.background_blur = Some(blur.clamp(0.0, 200.0));
|
||||
} else {
|
||||
log::warn!("Ignoring non-finite background-blur value");
|
||||
}
|
||||
}
|
||||
if appearance.gtk_theme.is_some() {
|
||||
merged.gtk_theme = appearance.gtk_theme;
|
||||
@ -283,4 +287,45 @@ mod tests {
|
||||
let config = load_config(Some(&paths));
|
||||
assert!(!config.fingerprint_enabled);
|
||||
}
|
||||
|
||||
// -- Blur validation tests --
|
||||
|
||||
#[test]
|
||||
fn load_config_blur_clamped_to_max() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let conf = dir.path().join("moongreet.toml");
|
||||
fs::write(&conf, "[appearance]\nbackground-blur = 999.0\n").unwrap();
|
||||
let config = load_config(Some(&[conf]));
|
||||
assert_eq!(config.background_blur, Some(200.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_blur_negative_clamped_to_zero() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let conf = dir.path().join("moongreet.toml");
|
||||
fs::write(&conf, "[appearance]\nbackground-blur = -5.0\n").unwrap();
|
||||
let config = load_config(Some(&[conf]));
|
||||
assert_eq!(config.background_blur, Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_blur_nan_rejected() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let conf = dir.path().join("moongreet.toml");
|
||||
// TOML doesn't support NaN literals, but the parser may return NaN for nan
|
||||
fs::write(&conf, "[appearance]\nbackground-blur = nan\n").unwrap();
|
||||
let config = load_config(Some(&[conf]));
|
||||
// nan is not valid TOML float, so the whole config parse fails → no blur
|
||||
assert!(config.background_blur.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_blur_inf_rejected() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let conf = dir.path().join("moongreet.toml");
|
||||
fs::write(&conf, "[appearance]\nbackground-blur = inf\n").unwrap();
|
||||
let config = load_config(Some(&[conf]));
|
||||
// inf is valid TOML → parsed as f32::INFINITY → rejected by is_finite() guard
|
||||
assert!(config.background_blur.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
184
src/greeter.rs
184
src/greeter.rs
@ -87,7 +87,9 @@ fn is_valid_username(name: &str) -> bool {
|
||||
if name.is_empty() || name.len() > MAX_USERNAME_LENGTH {
|
||||
return false;
|
||||
}
|
||||
let first = name.chars().next().unwrap();
|
||||
let Some(first) = name.chars().next() else {
|
||||
return false;
|
||||
};
|
||||
if !first.is_ascii_alphanumeric() && first != '_' {
|
||||
return false;
|
||||
}
|
||||
@ -95,6 +97,14 @@ fn is_valid_username(name: &str) -> bool {
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-' || c == '@')
|
||||
}
|
||||
|
||||
/// Validate a GTK theme name — alphanumeric plus `_-+.` only.
|
||||
fn is_valid_gtk_theme(name: &str) -> bool {
|
||||
!name.is_empty()
|
||||
&& name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '+' | '.'))
|
||||
}
|
||||
|
||||
/// Load background texture from filesystem.
|
||||
pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
|
||||
if let Ok(meta) = std::fs::metadata(bg_path)
|
||||
@ -140,8 +150,8 @@ fn render_blurred_texture(
|
||||
// Clip output to original texture size
|
||||
snapshot.push_clip(&graphene_rs::Rect::new(pad, pad, w, h));
|
||||
snapshot.push_blur(sigma as f64);
|
||||
// Render texture with padding on all sides (edges repeat via oversized bounds)
|
||||
snapshot.append_texture(texture, &graphene_rs::Rect::new(0.0, 0.0, w + 2.0 * pad, h + 2.0 * pad));
|
||||
// Render texture at native size, shifted so edge pixels fill the padding area
|
||||
snapshot.append_texture(texture, &graphene_rs::Rect::new(-pad, -pad, w + 2.0 * pad, h + 2.0 * pad));
|
||||
snapshot.pop(); // blur
|
||||
snapshot.pop(); // clip
|
||||
|
||||
@ -154,6 +164,7 @@ fn render_blurred_texture(
|
||||
pub fn create_wallpaper_window(
|
||||
texture: &gdk::Texture,
|
||||
blur_radius: Option<f32>,
|
||||
blur_cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||
app: >k::Application,
|
||||
) -> gtk::ApplicationWindow {
|
||||
let window = gtk::ApplicationWindow::builder()
|
||||
@ -161,14 +172,19 @@ pub fn create_wallpaper_window(
|
||||
.build();
|
||||
window.add_css_class("wallpaper");
|
||||
|
||||
let background = create_background_picture(texture, blur_radius);
|
||||
let background = create_background_picture(texture, blur_radius, blur_cache);
|
||||
window.set_child(Some(&background));
|
||||
|
||||
window
|
||||
}
|
||||
|
||||
/// Create a Picture widget for the wallpaper background, optionally with GPU blur.
|
||||
fn create_background_picture(texture: &gdk::Texture, blur_radius: Option<f32>) -> gtk::Picture {
|
||||
/// Uses `blur_cache` to compute the blurred texture only once across all monitors.
|
||||
fn create_background_picture(
|
||||
texture: &gdk::Texture,
|
||||
blur_radius: Option<f32>,
|
||||
blur_cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||
) -> gtk::Picture {
|
||||
let background = gtk::Picture::for_paintable(texture);
|
||||
background.set_content_fit(gtk::ContentFit::Cover);
|
||||
background.set_hexpand(true);
|
||||
@ -176,9 +192,16 @@ fn create_background_picture(texture: &gdk::Texture, blur_radius: Option<f32>) -
|
||||
|
||||
if let Some(sigma) = blur_radius.filter(|s| *s > 0.0) {
|
||||
let texture = texture.clone();
|
||||
let blur_cache = blur_cache.clone();
|
||||
background.connect_realize(move |picture| {
|
||||
// Use cached blurred texture if available
|
||||
if let Some(ref cached) = *blur_cache.borrow() {
|
||||
picture.set_paintable(Some(cached));
|
||||
return;
|
||||
}
|
||||
if let Some(blurred) = render_blurred_texture(picture, &texture, sigma) {
|
||||
picture.set_paintable(Some(&blurred));
|
||||
*blur_cache.borrow_mut() = Some(blurred);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -195,12 +218,17 @@ struct GreeterState {
|
||||
greetd_sock: Arc<Mutex<Option<UnixStream>>>,
|
||||
login_cancelled: Arc<std::sync::atomic::AtomicBool>,
|
||||
fingerprint_available: bool,
|
||||
/// Incremented on each user switch to discard stale async results.
|
||||
user_switch_generation: u64,
|
||||
/// Cached fprintd device proxy — initialized once on first use.
|
||||
fingerprint_probe: Option<crate::fingerprint::FingerprintProbe>,
|
||||
}
|
||||
|
||||
/// Create the main greeter window with login UI.
|
||||
pub fn create_greeter_window(
|
||||
texture: Option<&gdk::Texture>,
|
||||
config: &Config,
|
||||
blur_cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||
app: >k::Application,
|
||||
) -> gtk::ApplicationWindow {
|
||||
let window = gtk::ApplicationWindow::builder()
|
||||
@ -211,11 +239,7 @@ pub fn create_greeter_window(
|
||||
|
||||
// Apply GTK theme from config
|
||||
if let Some(ref theme_name) = config.gtk_theme {
|
||||
if !theme_name.is_empty()
|
||||
&& theme_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '+' | '.'))
|
||||
{
|
||||
if is_valid_gtk_theme(theme_name) {
|
||||
if let Some(settings) = gtk::Settings::default() {
|
||||
settings.set_gtk_theme_name(Some(theme_name));
|
||||
}
|
||||
@ -241,6 +265,8 @@ pub fn create_greeter_window(
|
||||
greetd_sock: Arc::new(Mutex::new(None)),
|
||||
login_cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
fingerprint_available: false,
|
||||
user_switch_generation: 0,
|
||||
fingerprint_probe: None,
|
||||
}));
|
||||
|
||||
// Root overlay for layering
|
||||
@ -249,7 +275,7 @@ pub fn create_greeter_window(
|
||||
|
||||
// Background wallpaper
|
||||
if let Some(texture) = texture {
|
||||
overlay.set_child(Some(&create_background_picture(texture, config.background_blur)));
|
||||
overlay.set_child(Some(&create_background_picture(texture, config.background_blur, blur_cache)));
|
||||
}
|
||||
|
||||
// Main layout: 3 rows (top spacer, center login, bottom bar)
|
||||
@ -559,6 +585,7 @@ pub fn create_greeter_window(
|
||||
}
|
||||
|
||||
/// Select the last user or the first available user.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn select_initial_user(
|
||||
users: &[User],
|
||||
state: &Rc<RefCell<GreeterState>>,
|
||||
@ -601,6 +628,7 @@ fn select_initial_user(
|
||||
}
|
||||
|
||||
/// Update the UI to show the selected user.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn switch_to_user(
|
||||
user: &User,
|
||||
state: &Rc<RefCell<GreeterState>>,
|
||||
@ -616,11 +644,13 @@ fn switch_to_user(
|
||||
strings: &'static Strings,
|
||||
) {
|
||||
log::debug!("Switching to user: {}", user.username);
|
||||
{
|
||||
let generation = {
|
||||
let mut s = state.borrow_mut();
|
||||
s.selected_user = Some(user.clone());
|
||||
s.fingerprint_available = false;
|
||||
}
|
||||
s.user_switch_generation += 1;
|
||||
s.user_switch_generation
|
||||
};
|
||||
|
||||
username_label.set_text(user.display_name());
|
||||
password_entry.set_text("");
|
||||
@ -650,7 +680,7 @@ fn switch_to_user(
|
||||
// Pre-select last used session for this user
|
||||
select_last_session(&user.username, session_dropdown, sessions);
|
||||
|
||||
// Probe fprintd for fingerprint availability
|
||||
// Probe fprintd for fingerprint availability (cached device proxy, generation-guarded)
|
||||
if fingerprint_enabled {
|
||||
let username = user.username.clone();
|
||||
glib::spawn_future_local(clone!(
|
||||
@ -659,9 +689,29 @@ fn switch_to_user(
|
||||
#[strong]
|
||||
state,
|
||||
async move {
|
||||
let mut probe = crate::fingerprint::FingerprintProbe::new();
|
||||
probe.init_async().await;
|
||||
let available = probe.is_available_async(&username).await;
|
||||
// Initialize probe on first use, then reuse cached device proxy
|
||||
let needs_init = state.borrow().fingerprint_probe.is_none();
|
||||
if needs_init {
|
||||
let mut probe = crate::fingerprint::FingerprintProbe::new();
|
||||
probe.init_async().await;
|
||||
state.borrow_mut().fingerprint_probe = Some(probe);
|
||||
}
|
||||
|
||||
// Take probe out of state to avoid holding borrow across await
|
||||
let probe = state.borrow_mut().fingerprint_probe.take();
|
||||
let available = match &probe {
|
||||
Some(p) => p.is_available_async(&username).await,
|
||||
None => false,
|
||||
};
|
||||
state.borrow_mut().fingerprint_probe = probe;
|
||||
|
||||
// Discard result if user switched while we were probing
|
||||
let s = state.borrow();
|
||||
if s.user_switch_generation != generation {
|
||||
return;
|
||||
}
|
||||
drop(s);
|
||||
|
||||
state.borrow_mut().fingerprint_available = available;
|
||||
fp_label.set_visible(available);
|
||||
if available {
|
||||
@ -851,10 +901,10 @@ fn cancel_pending_session(state: &Rc<RefCell<GreeterState>>) {
|
||||
let s = state.borrow();
|
||||
s.login_cancelled
|
||||
.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
if let Ok(mut sock_guard) = s.greetd_sock.lock() {
|
||||
if let Some(sock) = sock_guard.take() {
|
||||
let _ = sock.shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
if let Ok(mut sock_guard) = s.greetd_sock.lock()
|
||||
&& let Some(sock) = sock_guard.take()
|
||||
{
|
||||
let _ = sock.shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
}
|
||||
|
||||
@ -902,28 +952,6 @@ fn attempt_login(
|
||||
return;
|
||||
}
|
||||
|
||||
match std::fs::metadata(&sock_pathbuf) {
|
||||
Ok(meta) => {
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
if !meta.file_type().is_socket() {
|
||||
show_error(
|
||||
error_label,
|
||||
password_entry,
|
||||
strings.greetd_sock_not_socket,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
show_error(
|
||||
error_label,
|
||||
password_entry,
|
||||
strings.greetd_sock_unreachable,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset cancellation flag and disable UI
|
||||
{
|
||||
let s = state.borrow();
|
||||
@ -1030,6 +1058,7 @@ enum LoginResult {
|
||||
}
|
||||
|
||||
/// Run greetd IPC in a background thread.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn login_worker(
|
||||
username: &str,
|
||||
password: &str,
|
||||
@ -1190,7 +1219,7 @@ fn execute_power_action(
|
||||
#[weak]
|
||||
button,
|
||||
async move {
|
||||
let result = gio::spawn_blocking(move || action_fn()).await;
|
||||
let result = gio::spawn_blocking(action_fn).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => {}
|
||||
@ -1213,6 +1242,15 @@ fn execute_power_action(
|
||||
|
||||
// -- Last user/session persistence --
|
||||
|
||||
/// Create a cache directory with restricted permissions (0o700).
|
||||
fn create_cache_dir(path: &Path) -> std::io::Result<()> {
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
std::fs::DirBuilder::new()
|
||||
.recursive(true)
|
||||
.mode(0o700)
|
||||
.create(path)
|
||||
}
|
||||
|
||||
fn load_last_user() -> Option<String> {
|
||||
load_last_user_from(Path::new(LAST_USER_PATH))
|
||||
}
|
||||
@ -1236,7 +1274,7 @@ fn save_last_user(username: &str) {
|
||||
fn save_last_user_to(path: &Path, username: &str) {
|
||||
log::debug!("Saving last user: {username}");
|
||||
if let Some(parent) = path.parent()
|
||||
&& let Err(e) = std::fs::create_dir_all(parent)
|
||||
&& let Err(e) = create_cache_dir(parent)
|
||||
{
|
||||
log::warn!("Failed to create cache dir {}: {e}", parent.display());
|
||||
return;
|
||||
@ -1289,7 +1327,10 @@ fn save_last_session(username: &str, session_name: &str) {
|
||||
return;
|
||||
}
|
||||
let dir = Path::new(LAST_SESSION_DIR);
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
if let Err(e) = create_cache_dir(dir) {
|
||||
log::warn!("Failed to create session cache dir {}: {e}", dir.display());
|
||||
return;
|
||||
}
|
||||
save_last_session_to(&dir.join(username), session_name);
|
||||
}
|
||||
|
||||
@ -1884,4 +1925,55 @@ mod tests {
|
||||
let resp = serde_json::json!({"type": "error"});
|
||||
assert_eq!(extract_greetd_description(&resp, "fallback"), "fallback");
|
||||
}
|
||||
|
||||
// -- GTK theme validation --
|
||||
|
||||
#[test]
|
||||
fn valid_gtk_themes() {
|
||||
assert!(is_valid_gtk_theme("Adwaita"));
|
||||
assert!(is_valid_gtk_theme("Catppuccin-Mocha"));
|
||||
assert!(is_valid_gtk_theme("Arc_Dark"));
|
||||
assert!(is_valid_gtk_theme("Theme+Variant"));
|
||||
assert!(is_valid_gtk_theme("v1.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_gtk_themes() {
|
||||
assert!(!is_valid_gtk_theme(""));
|
||||
assert!(!is_valid_gtk_theme("../evil"));
|
||||
assert!(!is_valid_gtk_theme("theme/path"));
|
||||
assert!(!is_valid_gtk_theme("theme name"));
|
||||
assert!(!is_valid_gtk_theme("thème"));
|
||||
assert!(!is_valid_gtk_theme("theme\0null"));
|
||||
}
|
||||
|
||||
// -- Username validation: Unicode edge cases --
|
||||
|
||||
#[test]
|
||||
fn invalid_unicode_usernames() {
|
||||
assert!(!is_valid_username("üser"));
|
||||
assert!(!is_valid_username("用户"));
|
||||
assert!(!is_valid_username("user🔑"));
|
||||
}
|
||||
|
||||
// -- Cache directory permissions --
|
||||
|
||||
#[test]
|
||||
fn create_cache_dir_sets_mode_0o700() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let cache_dir = tmp.path().join("cache");
|
||||
create_cache_dir(&cache_dir).unwrap();
|
||||
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = std::fs::metadata(&cache_dir).unwrap().permissions().mode() & 0o777;
|
||||
assert_eq!(mode, 0o700, "Cache dir should be 0o700, got {mode:#o}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_last_session_with_unwritable_dir() {
|
||||
// Attempt to save in a non-existent dir under /proc (guaranteed unwritable)
|
||||
let path = Path::new("/proc/nonexistent-moongreet-test/session");
|
||||
save_last_session_to(path, "niri");
|
||||
// Should not panic — just logs a warning
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,8 +19,6 @@ pub struct Strings {
|
||||
pub no_session_selected: &'static str,
|
||||
pub greetd_sock_not_set: &'static str,
|
||||
pub greetd_sock_not_absolute: &'static str,
|
||||
pub greetd_sock_not_socket: &'static str,
|
||||
pub greetd_sock_unreachable: &'static str,
|
||||
pub auth_failed: &'static str,
|
||||
pub wrong_password: &'static str,
|
||||
pub fingerprint_prompt: &'static str,
|
||||
@ -43,8 +41,6 @@ const STRINGS_DE: Strings = Strings {
|
||||
no_session_selected: "Keine Session ausgewählt",
|
||||
greetd_sock_not_set: "GREETD_SOCK nicht gesetzt",
|
||||
greetd_sock_not_absolute: "GREETD_SOCK ist kein absoluter Pfad",
|
||||
greetd_sock_not_socket: "GREETD_SOCK zeigt nicht auf einen Socket",
|
||||
greetd_sock_unreachable: "GREETD_SOCK nicht erreichbar",
|
||||
auth_failed: "Authentifizierung fehlgeschlagen",
|
||||
wrong_password: "Falsches Passwort",
|
||||
fingerprint_prompt: "Fingerabdruck auflegen oder Passwort eingeben",
|
||||
@ -65,8 +61,6 @@ const STRINGS_EN: Strings = Strings {
|
||||
no_session_selected: "No session selected",
|
||||
greetd_sock_not_set: "GREETD_SOCK not set",
|
||||
greetd_sock_not_absolute: "GREETD_SOCK is not an absolute path",
|
||||
greetd_sock_not_socket: "GREETD_SOCK does not point to a socket",
|
||||
greetd_sock_unreachable: "GREETD_SOCK unreachable",
|
||||
auth_failed: "Authentication failed",
|
||||
wrong_password: "Wrong password",
|
||||
fingerprint_prompt: "Place finger on reader or enter password",
|
||||
|
||||
@ -58,11 +58,13 @@ fn activate(app: >k::Application) {
|
||||
greeter::load_background_texture(&path)
|
||||
});
|
||||
|
||||
let blur_cache = std::rc::Rc::new(std::cell::RefCell::new(None));
|
||||
|
||||
let use_layer_shell = std::env::var("MOONGREET_NO_LAYER_SHELL").is_err();
|
||||
log::debug!("Layer shell: {use_layer_shell}");
|
||||
|
||||
// Main greeter window (login UI) — compositor picks focused monitor
|
||||
let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, app);
|
||||
let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
|
||||
if use_layer_shell {
|
||||
setup_layer_shell(&greeter_window, true, gtk4_layer_shell::Layer::Top);
|
||||
}
|
||||
@ -79,7 +81,7 @@ fn activate(app: >k::Application) {
|
||||
.item(i)
|
||||
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
||||
{
|
||||
let wallpaper = greeter::create_wallpaper_window(texture, config.background_blur, app);
|
||||
let wallpaper = greeter::create_wallpaper_window(texture, config.background_blur, &blur_cache, app);
|
||||
setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom);
|
||||
wallpaper.set_monitor(Some(&monitor));
|
||||
wallpaper.present();
|
||||
|
||||
@ -36,14 +36,14 @@ fn parse_desktop_file(path: &Path, session_type: &str) -> Option<Session> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(value) = line.strip_prefix("Name=") {
|
||||
if name.is_none() {
|
||||
name = Some(value.to_string());
|
||||
}
|
||||
} else if let Some(value) = line.strip_prefix("Exec=") {
|
||||
if exec_cmd.is_none() {
|
||||
exec_cmd = Some(value.to_string());
|
||||
}
|
||||
if let Some(value) = line.strip_prefix("Name=")
|
||||
&& name.is_none()
|
||||
{
|
||||
name = Some(value.to_string());
|
||||
} else if let Some(value) = line.strip_prefix("Exec=")
|
||||
&& exec_cmd.is_none()
|
||||
{
|
||||
exec_cmd = Some(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ pub fn get_users(passwd_path: Option<&Path>) -> Vec<User> {
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if uid < MIN_UID || uid > MAX_UID {
|
||||
if !(MIN_UID..=MAX_UID).contains(&uid) {
|
||||
continue;
|
||||
}
|
||||
if NOLOGIN_SHELLS.contains(&shell) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user