From 7166bee750fb514acb94c1684d5c380d6c7d656b Mon Sep 17 00:00:00 2001 From: Wildan M Date: Tue, 16 Jun 2026 14:02:40 +0700 Subject: [PATCH] Implement sudo for linux --- gui/Cargo.lock | 1 + gui/Cargo.toml | 7 +- gui/src/main.rs | 193 ++++++++++++++++--------------------------- gui/src/sys.rs | 12 +++ gui/src/sys/linux.rs | 76 +++++++++++++++++ gui/src/sys/redox.rs | 93 +++++++++++++++++++++ 6 files changed, 259 insertions(+), 123 deletions(-) create mode 100644 gui/src/sys.rs create mode 100644 gui/src/sys/linux.rs create mode 100644 gui/src/sys/redox.rs diff --git a/gui/Cargo.lock b/gui/Cargo.lock index 620a6806b3..f51cfba387 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -4250,6 +4250,7 @@ version = "0.1.0" dependencies = [ "anyhow", "futures-channel", + "libc", "libcosmic", "libredox", "pkgar", diff --git a/gui/Cargo.toml b/gui/Cargo.toml index f62837a0b6..3034926dab 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" [dependencies] anyhow = "1" -libredox = "0.1" pkgar = "0.2" pkgar-core = "0.2" pkgar-keys = "0.2" @@ -15,6 +14,12 @@ toml = "0.8" # TODO: remove after pkgutils update futures-channel = "0.3.32" +[target.'cfg(target_os = "redox")'.dependencies] +libredox = "0.1" + +[target.'cfg(target_os = "linux")'.dependencies] +libc = "0.2" + [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic.git" # use the same rev with other cosmic app diff --git a/gui/src/main.rs b/gui/src/main.rs index ddf9ea991f..7338cab789 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -2,9 +2,12 @@ use anyhow::format_err; use cosmic::{ app::{self, Task}, iced::{ - self, executor, futures::sink::SinkExt, widget::row, window, Alignment, Size, Subscription, + self, executor, + futures::sink::SinkExt, + widget::{row, text_input}, + window, Alignment, Size, Subscription, }, - widget::{button, column, progress_bar, radio, space, text, text_input}, + widget::{button, column, progress_bar, radio, space, text}, Application, ApplicationExt, Core, Element, }; use futures_channel::mpsc; @@ -21,6 +24,9 @@ use std::{ sync::Arc, }; +mod sys; +pub use sys::*; + fn main() -> iced::Result { let mut settings = app::Settings::default(); settings = settings.size(Size::new(608.0, 416.0)); @@ -28,91 +34,6 @@ fn main() -> iced::Result { app::run::(settings, ()) } -fn sudo(password: &str) -> Result<(), String> { - let file = libredox::call::open("/scheme/sudo", libredox::flag::O_CLOEXEC, 0) - .map_err(|err| err.to_string())?; - - libredox::call::write(file, password.as_bytes()).map_err(|err| err.to_string())?; - - // FIXME move to libredox - unsafe extern "C" { - safe fn redox_cur_procfd_v0() -> usize; - } - - // Elevate privileges of our own process with help from the sudo daemon - syscall::sendfd( - file, - syscall::dup(redox_cur_procfd_v0(), &[]).map_err(|err| err.to_string())?, - 0, - 0, - ) - .map_err(|err| err.to_string())?; - - Ok(()) -} - -fn disk_paths() -> Result, String> { - let mut schemes = Vec::new(); - match fs::read_dir("/scheme/") { - Ok(entries) => { - for entry_res in entries { - if let Ok(entry) = entry_res { - let path = entry.path(); - if let Ok(path_str) = path.into_os_string().into_string() { - let scheme = path_str.trim_start_matches("/scheme/").trim_matches('/'); - if scheme.starts_with("disk") { - if scheme == "disk/live" { - // Skip live disks - continue; - } - - schemes.push(format!("/scheme/{}", scheme)); - } - } - } - } - } - Err(err) => { - return Err(format!("failed to list schemes: {}", err)); - } - } - - let mut paths = Vec::new(); - for scheme in schemes { - let is_dir = fs::metadata(&scheme).map(|x| x.is_dir()).unwrap_or(false); - if is_dir { - match fs::read_dir(&scheme) { - Ok(entries) => { - for entry_res in entries { - if let Ok(entry) = entry_res { - if let Ok(file_name) = entry.file_name().into_string() { - if file_name.contains('p') { - // Skip partitions - continue; - } - - if let Ok(path) = entry.path().into_os_string().into_string() { - if let Ok(metadata) = entry.metadata() { - let size = metadata.len(); - if size > 0 { - paths.push((path, size)); - } - } - } - } - } - } - } - Err(err) => { - return Err(format!("failed to list '{}': {}", scheme, err)); - } - } - } - } - - Ok(paths) -} - const KIB: u64 = 1024; const MIB: u64 = 1024 * KIB; const GIB: u64 = 1024 * MIB; @@ -465,7 +386,7 @@ enum Message { struct Window { core: Core, page: Page, - disk_paths: Vec<(String, u64)>, + disk_paths: Vec<(String, bool, u64)>, worker_opt: Option, } @@ -526,15 +447,10 @@ impl Application for Window { const APP_ID: &'static str = "org.redox-os.InstallerGui"; fn init(core: Core, _flags: ()) -> (Self, Task) { - let uid = libredox::call::geteuid().unwrap(); - let (page, disk_paths) = if uid == 0 { - //TODO: load in background - match disk_paths() { - Ok(disk_paths) => (Page::Disk(None), disk_paths), - Err(err) => (Page::Error(err), Vec::new()), - } - } else { - (Page::Sudo(String::new()), Vec::new()) + //TODO: load in background + let (page, disk_paths) = match disk_paths() { + Ok(disk_paths) => (Page::Disk(None), disk_paths), + Err(err) => (Page::Error(err), Vec::new()), }; let mut app = Self { @@ -565,28 +481,34 @@ impl Application for Window { self.page = Page::Sudo(password); } Message::SudoSubmit => { + #[cfg(target_os = "redox")] if let Page::Sudo(password) = &self.page { - //TODO: run async? - match sudo(password) { + match ask_root(&password) { Ok(()) => { - (self.page, self.disk_paths) = match disk_paths() { - Ok(disk_paths) => (Page::Disk(None), disk_paths), - Err(err) => (Page::Error(err), Vec::new()), - }; + self.page = Page::Disk(None); } Err(err) => { - //TODO: show error in GUI eprintln!("{err}"); - self.page = Page::Sudo(String::new()); } } } + #[cfg(target_os = "linux")] + match ask_root() { + Ok(()) => { + if let Some(window_id) = self.core.main_window_id() { + return window::close(window_id); + } + } + Err(err) => { + eprintln!("{err}"); + } + } } Message::DiskChoose(disk_i) => { self.page = Page::Disk(Some(disk_i)); } Message::DiskConfirm(disk_i) => match self.disk_paths.get(disk_i) { - Some((disk_path, _disk_size)) => match &self.worker_opt { + Some((disk_path, _is_partition, _disk_size)) => match &self.worker_opt { Some(worker) => match worker.command_sender.send((disk_path.clone(), None)) { Ok(()) => self.page = Page::Install(0, format!("Starting install...")), Err(err) => { @@ -631,34 +553,61 @@ impl Application for Window { widgets.push(text("Enter your password:").into()); widgets.push( text_input("", password) - .password() .on_input(Message::SudoInput) - .on_submit(|_| Message::SudoSubmit) + .secure(true) + .on_submit(Message::SudoSubmit) .into(), ); } Page::Disk(disk_i_opt) => { if !self.disk_paths.is_empty() { - widgets.push(text("Choose a drive:").size(24).into()); + if is_root() { + widgets.push(text("Choose a drive:").size(24).into()); - for (disk_i, (disk_path, disk_size)) in self.disk_paths.iter().enumerate() { - widgets.push( - row![ - radio(text(disk_path), disk_i, *disk_i_opt, Message::DiskChoose), - space::horizontal(), - text(format_size(*disk_size)), - ] - .into(), - ); - } + for (disk_i, (disk_path, is_partition, disk_size)) in + self.disk_paths.iter().enumerate() + { + if !is_partition { + widgets.push( + row![ + radio( + text(disk_path), + disk_i, + *disk_i_opt, + Message::DiskChoose + ), + space::horizontal(), + text(format_size(*disk_size)), + ] + .into(), + ); + } + } + + if let Some(disk_i) = *disk_i_opt { + widgets.push(space::vertical().into()); + widgets.push( + row![ + space::horizontal(), + button::destructive("Confirm") + .on_press(Message::DiskConfirm(disk_i)), + ] + .into(), + ); + } + } else { + #[cfg(target_os = "linux")] + let page = Message::SudoSubmit; + + #[cfg(target_os = "redox")] + let page = Message::SudoInput(String::new()); - if let Some(disk_i) = *disk_i_opt { widgets.push(space::vertical().into()); widgets.push( row![ + text("Ask superuser permission to install into drives"), space::horizontal(), - button::destructive("Confirm") - .on_press(Message::DiskConfirm(disk_i)), + button::suggested("Ask root access").on_press(page), ] .into(), ); diff --git a/gui/src/sys.rs b/gui/src/sys.rs new file mode 100644 index 0000000000..8d99022714 --- /dev/null +++ b/gui/src/sys.rs @@ -0,0 +1,12 @@ +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "redox")] +mod redox; + +#[cfg(target_os = "linux")] +pub use linux::*; +#[cfg(target_os = "redox")] +pub use redox::*; + +#[cfg(not(any(target_os = "linux", target_os = "redox")))] +compile_error!("Platform is not ported"); diff --git a/gui/src/sys/linux.rs b/gui/src/sys/linux.rs new file mode 100644 index 0000000000..c665e7be3c --- /dev/null +++ b/gui/src/sys/linux.rs @@ -0,0 +1,76 @@ +use std::{fs, os::unix::process::CommandExt, process::Command}; + +pub fn ask_root() -> Result<(), String> { + let Ok(exe_path) = std::env::current_exe() else { + return Err(format!("Could not determine current_exe")); + }; + let mut cmd = Command::new("pkexec"); + cmd.arg("env"); + for (key, value) in std::env::vars() { + if [ + "DISPLAY", + "WAYLAND_DISPLAY", + "XAUTHORITY", + "XDG_RUNTIME_DIR", + "DBUS_SESSION_BUS_ADDRESS", + ] + .contains(&key.as_str()) + { + cmd.arg(format!("{}={}", key, value)); + } + } + cmd.arg(exe_path); + // will never return unless fail + let e = cmd.exec(); + return Err(format!("Failed to escalate: {e}")); +} + +pub fn is_root() -> bool { + let euid = unsafe { libc::geteuid() }; + euid == 0 +} + +pub fn disk_paths() -> Result, String> { + let mut paths = Vec::new(); + + let entries = match fs::read_dir("/sys/class/block") { + Ok(entries) => entries, + Err(err) => return Err(format!("failed to read /sys/class/block: {}", err)), + }; + + for entry_res in entries { + if let Ok(entry) = entry_res { + let file_name = entry.file_name(); + let name = file_name.to_string_lossy(); + + if name.starts_with("loop") + || name.starts_with("ram") + || name.starts_with("sr") + || name.starts_with("zram") + { + continue; + } + + let path = entry.path(); + let is_partition = path.join("partition").exists(); + let size_path = path.join("size"); + let Ok(size_str) = fs::read_to_string(&size_path) else { + continue; + }; + let Ok(sectors) = size_str.trim().parse::() else { + continue; + }; + // /sys/class/block/*/size is 512 (cmiiw) + let size_bytes = sectors * 512; + if size_bytes == 0 { + continue; + } + paths.push((format!("/dev/{}", name), is_partition, size_bytes)); + continue; + } + } + + paths.sort_by(|a, b| a.0.cmp(&b.0)); + + Ok(paths) +} diff --git a/gui/src/sys/redox.rs b/gui/src/sys/redox.rs new file mode 100644 index 0000000000..867b00517b --- /dev/null +++ b/gui/src/sys/redox.rs @@ -0,0 +1,93 @@ +use std::fs; + +pub fn ask_root(password: &str) -> Result<(), String> { + let file = libredox::call::open("/scheme/sudo", libredox::flag::O_CLOEXEC, 0) + .map_err(|err| err.to_string())?; + + libredox::call::write(file, password.as_bytes()).map_err(|err| err.to_string())?; + + // FIXME move to libredox + unsafe extern "C" { + safe fn redox_cur_procfd_v0() -> usize; + } + + // Elevate privileges of our own process with help from the sudo daemon + syscall::sendfd( + file, + syscall::dup(redox_cur_procfd_v0(), &[]).map_err(|err| err.to_string())?, + 0, + 0, + ) + .map_err(|err| err.to_string())?; + + Ok(()) +} + +pub fn is_root() -> bool { + let euid = libredox::call::geteuid().unwrap(); + euid == 0 +} + +pub fn disk_paths() -> Result, String> { + let mut schemes = Vec::new(); + match fs::read_dir("/scheme/") { + Ok(entries) => { + for entry_res in entries { + if let Ok(entry) = entry_res { + let path = entry.path(); + if let Ok(path_str) = path.into_os_string().into_string() { + let scheme = path_str.trim_start_matches("/scheme/").trim_matches('/'); + if scheme.starts_with("disk") { + if scheme == "disk/live" { + // Skip live disks + continue; + } + + schemes.push(format!("/scheme/{}", scheme)); + } + } + } + } + } + Err(err) => { + return Err(format!("failed to list schemes: {}", err)); + } + } + + let mut paths = Vec::new(); + for scheme in schemes { + let is_dir = fs::metadata(&scheme).map(|x| x.is_dir()).unwrap_or(false); + if !is_dir { + continue; + } + match fs::read_dir(&scheme) { + Ok(entries) => { + for entry_res in entries { + let Ok(entry) = entry_res else { + continue; + }; + let Ok(file_name) = entry.file_name().into_string() else { + continue; + }; + let Ok(path) = entry.path().into_os_string().into_string() else { + continue; + }; + let Ok(metadata) = entry.metadata() else { + continue; + }; + let is_partition = file_name.contains(p); + let size = metadata.len(); + if size == 0 { + continue; + } + paths.push((path, is_partition, size)); + } + } + Err(err) => { + return Err(format!("failed to list {:?}: {}", scheme, err)); + } + } + } + + Ok(paths) +}