Files
driftwood/src/core/wayland.rs
lashman e9343da249 Fix 29 audit findings across all severity tiers
Critical: fix unsquashfs arg order, quote Exec paths with spaces,
fix compare_versions antisymmetry, chunk-based signature detection,
bounded ELF header reads.

High: handle NULL CVE severity, prevent pipe deadlock in inspector,
fix glob_match edge case, fix backup archive path collisions, async
crash detection with stderr capture.

Medium: gate scan on auto-scan setting, fix window size persistence,
fix announce() for Stack containers, claim lightbox gesture, use
serde_json for CLI output, remove dead CSS @media blocks, add
detail-tab persistence, remove invalid metainfo categories, byte-level
fuse signature search.

Low: tighten Wayland env var detection, ELF magic validation,
timeout for update info extraction, quoted arg parsing, stop watcher
timer on window destroy, GSettings choices/range constraints, remove
unused CSS classes, define status-ok/status-attention CSS.
2026-02-27 22:08:53 +02:00

497 lines
16 KiB
Rust

use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone, PartialEq)]
pub enum WaylandStatus {
/// Native Wayland support detected (GTK4, Qt6+Wayland, Electron 38+)
Native,
/// Will run under XWayland compatibility layer
XWayland,
/// Toolkit supports Wayland but plugins may be missing or env vars needed
Possible,
/// X11-only toolkit with no Wayland path (GTK2, old Electron, Java)
X11Only,
/// Could not determine (uncommon toolkit, static binary, etc.)
Unknown,
}
impl WaylandStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Native => "native",
Self::XWayland => "xwayland",
Self::Possible => "possible",
Self::X11Only => "x11_only",
Self::Unknown => "unknown",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"native" => Self::Native,
"xwayland" => Self::XWayland,
"possible" => Self::Possible,
"x11_only" => Self::X11Only,
_ => Self::Unknown,
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Native => "Native Wayland",
Self::XWayland => "XWayland",
Self::Possible => "Wayland possible",
Self::X11Only => "X11 only",
Self::Unknown => "Unknown",
}
}
pub fn badge_class(&self) -> &'static str {
match self {
Self::Native => "success",
Self::XWayland | Self::Possible => "warning",
Self::X11Only => "error",
Self::Unknown => "neutral",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DetectedToolkit {
Gtk4,
Gtk3Wayland,
Gtk3X11Only,
Gtk2,
Qt6Wayland,
Qt6X11Only,
Qt5Wayland,
Qt5X11Only,
ElectronNative(u32), // version >= 38
ElectronFlagged(u32), // version 28-37 with ozone flag
ElectronLegacy(u32), // version < 28
JavaSwing,
Flutter,
Unknown,
}
impl DetectedToolkit {
pub fn label(&self) -> String {
match self {
Self::Gtk4 => "GTK4".to_string(),
Self::Gtk3Wayland => "GTK3 (Wayland)".to_string(),
Self::Gtk3X11Only => "GTK3 (X11)".to_string(),
Self::Gtk2 => "GTK2".to_string(),
Self::Qt6Wayland => "Qt6 (Wayland)".to_string(),
Self::Qt6X11Only => "Qt6 (X11)".to_string(),
Self::Qt5Wayland => "Qt5 (Wayland)".to_string(),
Self::Qt5X11Only => "Qt5 (X11)".to_string(),
Self::ElectronNative(v) => format!("Electron {} (native Wayland)", v),
Self::ElectronFlagged(v) => format!("Electron {} (Wayland with flags)", v),
Self::ElectronLegacy(v) => format!("Electron {} (X11)", v),
Self::JavaSwing => "Java/Swing".to_string(),
Self::Flutter => "Flutter".to_string(),
Self::Unknown => "Unknown".to_string(),
}
}
pub fn wayland_status(&self) -> WaylandStatus {
match self {
Self::Gtk4 | Self::Gtk3Wayland | Self::Qt6Wayland | Self::Qt5Wayland
| Self::ElectronNative(_) | Self::Flutter => WaylandStatus::Native,
Self::ElectronFlagged(_) => WaylandStatus::Possible,
Self::Gtk3X11Only | Self::Qt6X11Only | Self::Qt5X11Only => WaylandStatus::XWayland,
Self::Gtk2 | Self::ElectronLegacy(_) | Self::JavaSwing => WaylandStatus::X11Only,
Self::Unknown => WaylandStatus::Unknown,
}
}
}
#[derive(Debug, Clone)]
pub struct WaylandAnalysis {
pub status: WaylandStatus,
pub toolkit: DetectedToolkit,
pub libraries_found: Vec<String>,
}
/// Analyze an AppImage's Wayland compatibility by inspecting its bundled libraries.
/// Uses unsquashfs to list files inside the squashfs.
pub fn analyze_appimage(appimage_path: &Path) -> WaylandAnalysis {
let libs = list_bundled_libraries(appimage_path);
let toolkit = detect_toolkit(&libs);
let status = toolkit.wayland_status();
WaylandAnalysis {
status,
toolkit,
libraries_found: libs,
}
}
/// List shared libraries bundled inside the AppImage squashfs.
fn list_bundled_libraries(appimage_path: &Path) -> Vec<String> {
// Get the squashfs offset using binary scan (never execute the AppImage -
// some apps like Affinity have custom AppRun scripts that ignore flags)
let offset = match crate::core::inspector::find_squashfs_offset_for(appimage_path) {
Some(o) => o.to_string(),
None => return Vec::new(),
};
// Use unsquashfs to list files (just filenames, no extraction)
let output = Command::new("unsquashfs")
.args(["-o", &offset, "-l", "-no-progress"])
.arg(appimage_path)
.output();
match output {
Ok(out) if out.status.success() => {
let stdout = String::from_utf8_lossy(&out.stdout);
stdout
.lines()
.filter(|line| line.contains(".so"))
.map(|line| {
// unsquashfs -l output format: "squashfs-root/usr/lib/libfoo.so.1"
// Extract just the filename
line.rsplit('/').next().unwrap_or(line).to_string()
})
.collect()
}
_ => Vec::new(),
}
}
/// Detect the UI toolkit from the bundled library list.
fn detect_toolkit(libs: &[String]) -> DetectedToolkit {
let has = |pattern: &str| -> bool {
libs.iter().any(|l| l.contains(pattern))
};
// Check for GTK4 (always Wayland-native)
if has("libgtk-4") || has("libGdk-4") {
return DetectedToolkit::Gtk4;
}
// Check for Flutter (GTK backend, Wayland-native)
if has("libflutter_linux_gtk") {
return DetectedToolkit::Flutter;
}
// Check for Java/Swing (X11 only)
if has("libjvm.so") || has("libjava.so") || has("libawt.so") {
return DetectedToolkit::JavaSwing;
}
// Check for Electron (version-dependent)
if has("libElectron") || has("electron") || has("libnode.so") || has("libchromium") {
let version = detect_electron_version(libs);
if let Some(v) = version {
if v >= 38 {
return DetectedToolkit::ElectronNative(v);
} else if v >= 28 {
return DetectedToolkit::ElectronFlagged(v);
} else {
return DetectedToolkit::ElectronLegacy(v);
}
}
// Can't determine version - assume modern enough for XWayland at minimum
return DetectedToolkit::ElectronFlagged(0);
}
// Check for Qt6
if has("libQt6Core") || has("libQt6Gui") {
if has("libQt6WaylandClient") || has("libqwayland") {
return DetectedToolkit::Qt6Wayland;
}
return DetectedToolkit::Qt6X11Only;
}
// Check for Qt5
if has("libQt5Core") || has("libQt5Gui") {
if has("libQt5WaylandClient") || has("libqwayland") {
return DetectedToolkit::Qt5Wayland;
}
return DetectedToolkit::Qt5X11Only;
}
// Check for GTK3
if has("libgtk-3") || has("libGdk-3") {
if has("libwayland-client") {
return DetectedToolkit::Gtk3Wayland;
}
return DetectedToolkit::Gtk3X11Only;
}
// Check for GTK2 (X11 only, forever)
if has("libgtk-x11-2") || has("libgdk-x11-2") {
return DetectedToolkit::Gtk2;
}
DetectedToolkit::Unknown
}
/// Try to detect Electron version from bundled files.
fn detect_electron_version(libs: &[String]) -> Option<u32> {
for lib in libs {
// Look for version patterns in Electron-related files
if lib.contains("electron") {
// Try to extract version number from filenames like "electron-v28.0.0"
for part in lib.split(&['-', '_', 'v'][..]) {
if let Some(major) = part.split('.').next() {
if let Ok(v) = major.parse::<u32>() {
if v > 0 && v < 200 {
return Some(v);
}
}
}
}
}
}
None
}
/// Detect the current desktop session type.
#[derive(Debug, Clone, PartialEq)]
pub enum SessionType {
Wayland,
X11,
Unknown,
}
impl SessionType {
pub fn label(&self) -> &'static str {
match self {
Self::Wayland => "Wayland",
Self::X11 => "X11",
Self::Unknown => "Unknown",
}
}
}
pub fn detect_session_type() -> SessionType {
// Check XDG_SESSION_TYPE first (most reliable)
if let Ok(session) = std::env::var("XDG_SESSION_TYPE") {
return match session.as_str() {
"wayland" => SessionType::Wayland,
"x11" => SessionType::X11,
_ => SessionType::Unknown,
};
}
// Check WAYLAND_DISPLAY
if std::env::var("WAYLAND_DISPLAY").is_ok() {
return SessionType::Wayland;
}
// Check DISPLAY (X11 fallback)
if std::env::var("DISPLAY").is_ok() {
return SessionType::X11;
}
SessionType::Unknown
}
/// Get desktop environment info string.
pub fn detect_desktop_environment() -> String {
let de = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
let session = std::env::var("DESKTOP_SESSION").unwrap_or_default();
if !de.is_empty() {
de
} else if !session.is_empty() {
session
} else {
"Unknown".to_string()
}
}
/// Result of analyzing a running process for Wayland usage.
#[derive(Debug, Clone)]
pub struct RuntimeAnalysis {
pub pid: u32,
pub has_wayland_socket: bool,
pub has_x11_connection: bool,
pub env_vars: Vec<(String, String)>,
}
impl RuntimeAnalysis {
/// Human-readable status label.
pub fn status_label(&self) -> &'static str {
match (self.has_wayland_socket, self.has_x11_connection) {
(true, false) => "Native Wayland",
(true, true) => "Wayland + X11 fallback",
(false, true) => "X11 / XWayland",
(false, false) => "Unknown",
}
}
/// Machine-readable status string for database storage.
pub fn as_status_str(&self) -> &'static str {
match (self.has_wayland_socket, self.has_x11_connection) {
(true, false) => "native",
(true, true) => "native",
(false, true) => "xwayland",
(false, false) => "unknown",
}
}
}
/// Analyze a running process to determine its actual Wayland/X11 usage.
/// Inspects /proc/<pid>/fd for Wayland and X11 sockets, and reads
/// relevant environment variables from /proc/<pid>/environ.
pub fn analyze_running_process(pid: u32) -> Result<RuntimeAnalysis, String> {
let proc_path = format!("/proc/{}", pid);
if !std::path::Path::new(&proc_path).exists() {
return Err(format!("Process {} not found", pid));
}
// Check file descriptors for Wayland and X11 sockets
let fd_dir = format!("{}/fd", proc_path);
let mut has_wayland_socket = false;
let mut has_x11_connection = false;
if let Ok(entries) = std::fs::read_dir(&fd_dir) {
for entry in entries.flatten() {
if let Ok(target) = std::fs::read_link(entry.path()) {
let target_str = target.to_string_lossy();
if target_str.contains("wayland") {
has_wayland_socket = true;
}
if target_str.contains("/tmp/.X11-unix/") || target_str.contains("@/tmp/.X11") {
has_x11_connection = true;
}
}
}
}
// Read relevant environment variables
let environ_path = format!("{}/environ", proc_path);
let mut env_vars = Vec::new();
let relevant_vars = [
"WAYLAND_DISPLAY", "DISPLAY", "GDK_BACKEND", "QT_QPA_PLATFORM",
"XDG_SESSION_TYPE", "SDL_VIDEODRIVER", "CLUTTER_BACKEND",
];
if let Ok(data) = std::fs::read(&environ_path) {
for entry in data.split(|&b| b == 0) {
if let Ok(s) = std::str::from_utf8(entry) {
if let Some((key, value)) = s.split_once('=') {
if relevant_vars.contains(&key) {
env_vars.push((key.to_string(), value.to_string()));
}
}
}
}
}
// Also check env vars for hints if fd inspection was inconclusive
if !has_wayland_socket {
has_wayland_socket = env_vars.iter().any(|(k, v)| {
(k == "GDK_BACKEND" && v.contains("wayland"))
|| (k == "QT_QPA_PLATFORM" && v.contains("wayland"))
});
}
Ok(RuntimeAnalysis {
pid,
has_wayland_socket,
has_x11_connection,
env_vars,
})
}
/// Check if XWayland is available on the system.
pub fn has_xwayland() -> bool {
// Check if Xwayland process is running
Command::new("pgrep")
.arg("Xwayland")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wayland_status_roundtrip() {
let statuses = [
WaylandStatus::Native,
WaylandStatus::XWayland,
WaylandStatus::Possible,
WaylandStatus::X11Only,
WaylandStatus::Unknown,
];
for status in &statuses {
assert_eq!(&WaylandStatus::from_str(status.as_str()), status);
}
}
#[test]
fn test_detect_toolkit_gtk4() {
let libs = vec!["libgtk-4.so.1".to_string(), "libglib-2.0.so.0".to_string()];
let toolkit = detect_toolkit(&libs);
assert!(matches!(toolkit, DetectedToolkit::Gtk4));
assert_eq!(toolkit.wayland_status(), WaylandStatus::Native);
}
#[test]
fn test_detect_toolkit_qt5_wayland() {
let libs = vec![
"libQt5Core.so.5".to_string(),
"libQt5Gui.so.5".to_string(),
"libQt5WaylandClient.so.5".to_string(),
];
let toolkit = detect_toolkit(&libs);
assert!(matches!(toolkit, DetectedToolkit::Qt5Wayland));
assert_eq!(toolkit.wayland_status(), WaylandStatus::Native);
}
#[test]
fn test_detect_toolkit_qt5_x11() {
let libs = vec![
"libQt5Core.so.5".to_string(),
"libQt5Gui.so.5".to_string(),
];
let toolkit = detect_toolkit(&libs);
assert!(matches!(toolkit, DetectedToolkit::Qt5X11Only));
assert_eq!(toolkit.wayland_status(), WaylandStatus::XWayland);
}
#[test]
fn test_detect_toolkit_gtk2() {
let libs = vec!["libgtk-x11-2.0.so.0".to_string()];
let toolkit = detect_toolkit(&libs);
assert!(matches!(toolkit, DetectedToolkit::Gtk2));
assert_eq!(toolkit.wayland_status(), WaylandStatus::X11Only);
}
#[test]
fn test_detect_toolkit_gtk3_with_wayland() {
let libs = vec![
"libgtk-3.so.0".to_string(),
"libwayland-client.so.0".to_string(),
];
let toolkit = detect_toolkit(&libs);
assert!(matches!(toolkit, DetectedToolkit::Gtk3Wayland));
assert_eq!(toolkit.wayland_status(), WaylandStatus::Native);
}
#[test]
fn test_detect_toolkit_unknown() {
let libs = vec!["libfoo.so.1".to_string()];
let toolkit = detect_toolkit(&libs);
assert!(matches!(toolkit, DetectedToolkit::Unknown));
assert_eq!(toolkit.wayland_status(), WaylandStatus::Unknown);
}
#[test]
fn test_badge_classes() {
assert_eq!(WaylandStatus::Native.badge_class(), "success");
assert_eq!(WaylandStatus::XWayland.badge_class(), "warning");
assert_eq!(WaylandStatus::X11Only.badge_class(), "error");
assert_eq!(WaylandStatus::Unknown.badge_class(), "neutral");
}
}