diff --git a/Cargo.lock b/Cargo.lock index 56814df4..75edf1ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,7 +75,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -86,7 +86,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -672,7 +672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1092,7 +1092,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1601,7 +1601,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1734,6 +1734,7 @@ dependencies = [ "tracing-subscriber", "ureq", "zbus", + "zbus_polkit", ] [[package]] @@ -2098,7 +2099,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2602,7 +2603,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2853,7 +2854,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2960,7 +2961,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2970,7 +2971,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3281,7 +3282,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3584,7 +3585,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4091,6 +4092,19 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zbus_polkit" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad23d5c4d198c7e2641b33e6e0d1f866f117408ba66fe80bbe52e289eeb77c52" +dependencies = [ + "enumflags2", + "serde", + "serde_repr", + "static_assertions", + "zbus", +] + [[package]] name = "zerocopy" version = "0.8.48" diff --git a/Makefile b/Makefile index 81d7ce82..101b6542 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,7 @@ install-resources: install -Dm644 res/io.github.ilya_zlobintsev.LACT.png $(DESTDIR)$(PREFIX)/share/icons/hicolor/512x512/apps/io.github.ilya_zlobintsev.LACT.png install -Dm644 res/io.github.ilya_zlobintsev.LACT.svg $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/io.github.ilya_zlobintsev.LACT.svg install -Dm644 res/io.github.ilya_zlobintsev.LACT.metainfo.xml $(DESTDIR)$(PREFIX)/share/metainfo/io.github.ilya_zlobintsev.LACT.metainfo.xml + install -Dm644 res/io.github.ilya_zlobintsev.LACT.policy $(DESTDIR)$(PREFIX)/share/polkit-1/actions/io.github.ilya_zlobintsev.LACT.policy .PHONY: install install: install-resources @@ -40,6 +41,7 @@ uninstall: rm $(DESTDIR)$(PREFIX)/share/icons/hicolor/512x512/apps/io.github.ilya_zlobintsev.LACT.png rm $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/io.github.ilya_zlobintsev.LACT.svg rm $(DESTDIR)$(PREFIX)/share/metainfo/io.github.ilya_zlobintsev.LACT.metainfo.xml + rm $(DESTDIR)$(PREFIX)/share/polkit-1/actions/io.github.ilya_zlobintsev.LACT.policy .PHONY: update-vulkan-schema update-vulkan-schema: diff --git a/flatpak/generated-sources.json b/flatpak/generated-sources.json index 9401c64c..692e1553 100644 --- a/flatpak/generated-sources.json +++ b/flatpak/generated-sources.json @@ -5238,6 +5238,19 @@ "dest": "cargo/vendor/zbus_names-4.3.1", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zbus_polkit/zbus_polkit-5.0.0.crate", + "sha256": "ad23d5c4d198c7e2641b33e6e0d1f866f117408ba66fe80bbe52e289eeb77c52", + "dest": "cargo/vendor/zbus_polkit-5.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"ad23d5c4d198c7e2641b33e6e0d1f866f117408ba66fe80bbe52e289eeb77c52\", \"files\": {}}", + "dest": "cargo/vendor/zbus_polkit-5.0.0", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", diff --git a/lact-daemon/Cargo.toml b/lact-daemon/Cargo.toml index d22a9084..c146888d 100644 --- a/lact-daemon/Cargo.toml +++ b/lact-daemon/Cargo.toml @@ -38,6 +38,7 @@ nvml-wrapper = "0.12.1" bitflags = "2.11.1" pciid-parser = { version = "0.8", features = ["serde"] } zbus = { version = "5.14.0", default-features = false, features = ["tokio"] } +zbus_polkit = { version = "5.0.0", default-features = false, features = ["tokio"] } libdrm_amdgpu_sys = { version = "0.8.16", default-features = false, features = [ "dynamic_loading", ] } diff --git a/lact-daemon/src/lib.rs b/lact-daemon/src/lib.rs index d4839115..77db2e13 100644 --- a/lact-daemon/src/lib.rs +++ b/lact-daemon/src/lib.rs @@ -28,6 +28,7 @@ use tracing::level_filters::LevelFilter; use tracing::{Instrument, debug_span, error, info, warn}; use tracing_subscriber::EnvFilter; +use crate::server::ClientContext; pub use system::BASE_MODULE_CONF_PATH; const DRM_EVENT_TIMEOUT_PERIOD_MS: u64 = 100; @@ -83,8 +84,9 @@ pub fn run_embedded(stream: StdUnixStream) -> anyhow::Result<()> { let config = Config::default(); let handler = Handler::new(config).await?; let stream = UnixStream::try_from(stream)?; + let ctx = ClientContext::default(); - handle_stream(stream, handler).await + handle_stream(stream, handler, ctx).await }) } diff --git a/lact-daemon/src/server.rs b/lact-daemon/src/server.rs index 9ec8ffd5..99829ce6 100644 --- a/lact-daemon/src/server.rs +++ b/lact-daemon/src/server.rs @@ -12,6 +12,7 @@ use crate::{config::Config, socket, system}; use anyhow::Context; use futures::future::join_all; use lact_schema::{Pong, Request, Response}; +use nix::sys::socket::{getsockopt, sockopt::PeerCredentials}; use serde::Serialize; use std::fmt::Debug; use tokio::{ @@ -19,7 +20,7 @@ use tokio::{ net::{TcpListener, UnixListener}, sync::Notify, }; -use tracing::{debug, error, info, instrument, trace}; +use tracing::{debug, error, info, instrument, trace, warn}; pub struct Server { pub handler: Handler, @@ -74,9 +75,15 @@ impl Server { loop { match self.unix_listener.accept().await { Ok((stream, _)) => { + let client_credentials = getsockopt(&stream, PeerCredentials) + .inspect_err(|err| warn!("could not get client credentials: {err:#}")) + .ok(); + let ctx = ClientContext { + client_pid: client_credentials.map(|creds| creds.pid().cast_unsigned()), + }; let handler = unix_handler.clone(); tokio::task::spawn_local(async move { - if let Err(error) = handle_stream(stream, handler).await { + if let Err(error) = handle_stream(stream, handler, ctx).await { error!("{error}"); } }); @@ -94,9 +101,11 @@ impl Server { loop { match tcp_listener.accept().await { Ok((stream, _)) => { + let ctx = ClientContext::default(); let handler = self.handler.clone(); + tokio::task::spawn_local(async move { - if let Err(error) = handle_stream(stream, handler).await { + if let Err(error) = handle_stream(stream, handler, ctx).await { error!("{error}"); } }); @@ -114,10 +123,16 @@ impl Server { } } +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct ClientContext { + pub client_pid: Option, +} + #[instrument(level = "debug", skip(stream, handler))] pub async fn handle_stream( stream: T, handler: Handler, + ctx: ClientContext, ) -> anyhow::Result<()> { let disconnect_notify = std::sync::Arc::new(Notify::new()); @@ -129,7 +144,7 @@ pub async fn handle_stream( let maybe_request = serde_json::from_str(&buf); let response = match maybe_request { - Ok(request) => match handle_request(request, &handler, &disconnect_notify).await { + Ok(request) => match handle_request(request, &handler, &disconnect_notify, ctx).await { Ok(response) => response, Err(error) => serde_json::to_vec(&Response::<()>::from(error))?, }, @@ -154,6 +169,7 @@ async fn handle_request<'a>( request: Request<'a>, handler: &'a Handler, disconnect_notify: &std::sync::Arc, + ctx: ClientContext, ) -> anyhow::Result> { match request { Request::Ping => ok_response(ping()), @@ -209,7 +225,7 @@ async fn handle_request<'a>( .await?, ), Request::CreateProfile { name, base } => { - ok_response(handler.create_profile(name, base).await?) + ok_response(handler.create_profile(name, base, ctx).await?) } Request::DeleteProfile { name } => ok_response(handler.delete_profile(name).await?), Request::MoveProfile { name, new_position } => { @@ -223,7 +239,7 @@ async fn handle_request<'a>( Request::ReleaseProfile { cookie } => ok_response(handler.release_profile(cookie).await?), Request::EvaluateProfileRule { rule } => ok_response(handler.evaluate_profile_rule(&rule)?), Request::SetProfileRule { name, rule, hooks } => { - ok_response(handler.set_profile_rule(&name, rule, hooks).await?) + ok_response(handler.set_profile_rule(&name, rule, hooks, ctx).await?) } Request::GetGpuConfig { id } => ok_response(handler.get_gpu_config(id).await?), Request::SetGpuConfig { id, config } => { diff --git a/lact-daemon/src/server/handler.rs b/lact-daemon/src/server/handler.rs index 8019b468..03db1a18 100644 --- a/lact-daemon/src/server/handler.rs +++ b/lact-daemon/src/server/handler.rs @@ -8,7 +8,7 @@ use crate::server::display; use crate::{ bindings::intel::IntelDrm, config::Config, - server::{gpu_controller::init_controller, profiles, system::DAEMON_VERSION}, + server::{ClientContext, gpu_controller::init_controller, profiles, system::DAEMON_VERSION}, }; use crate::{server::gpu_controller::NvidiaLibs, system::run_command}; use amdgpu_sysfs::gpu_handle::{ @@ -32,13 +32,11 @@ use nix::libc; use nvml_wrapper::Nvml; use pciid_parser::Database; use serde_json::json; -#[cfg(not(test))] -use std::collections::HashMap; #[cfg(all(not(test), feature = "nvidia"))] use std::sync::Arc; use std::{ cell::{Cell, RefCell}, - collections::BTreeMap, + collections::{BTreeMap, HashMap}, env, fs::{self, File, Permissions}, io::{BufWriter, Cursor, Write}, @@ -55,6 +53,7 @@ use tokio::{ time::sleep, }; use tracing::{debug, error, info, trace, warn}; +use zbus_polkit::policykit1::{self, AuthorityProxy}; const CONTROLLERS_LOAD_RETRY_ATTEMPTS: u8 = 5; const CONTROLLERS_LOAD_RETRY_INTERVAL: u64 = 3; @@ -86,6 +85,10 @@ const SNAPSHOT_EXCLUDED_FILENAME_PREFIXES: &[&str] = &[ ]; const CONFIG_RESET_CMDLINE_ARG: &str = "lact-reset"; +mod polkit_actions { + pub const PROFILE_HOOK: &str = "io.github.ilya_zlobintsev.LACT.profile-hook"; +} + type ProfileHolds = Rc, mpsc::Sender<()>)>>>; type ProfileHoldSnapshot = Rc>, bool)>>>; @@ -101,6 +104,7 @@ pub struct Handler { profile_holds: ProfileHolds, profile_hold_snapshot: ProfileHoldSnapshot, next_hold_cookie: Rc>, + polkit_proxy: Option>, } impl<'a> Handler { @@ -178,6 +182,13 @@ impl<'a> Handler { config.save(&Cell::new(Instant::now()))?; } + let polkit_proxy = connect_polkit_proxy() + .await + .inspect_err(|err| { + warn!("could not connect to polkit, admin checks will not be available: {err:#}"); + }) + .ok(); + let handler = Self { gpu_controllers: Rc::new(RwLock::new(controllers)), config: Rc::new(RwLock::new(config)), @@ -189,6 +200,7 @@ impl<'a> Handler { profile_holds: Rc::new(RefCell::new(Vec::new())), profile_hold_snapshot: Rc::new(RefCell::new(None)), next_hold_cookie: Rc::new(Cell::new(1)), + polkit_proxy, }; if let Err(err) = handler.apply_current_config().await { error!("could not apply config: {err:#}"); @@ -913,7 +925,12 @@ impl<'a> Handler { Ok(()) } - pub async fn create_profile(&self, name: String, base: ProfileBase) -> anyhow::Result<()> { + pub async fn create_profile( + &self, + name: String, + base: ProfileBase, + ctx: ClientContext, + ) -> anyhow::Result<()> { { let mut config = self.config.write().await; if config.profiles.contains_key(name.as_str()) { @@ -924,7 +941,18 @@ impl<'a> Handler { ProfileBase::Empty => Profile::default(), ProfileBase::Default => config.default_profile(), ProfileBase::Profile(name) => config.profile(&name)?.clone(), - ProfileBase::Provided(profile) => profile, + ProfileBase::Provided(profile) => { + if !profile.hooks.is_empty() { + self.check_auth( + polkit_actions::PROFILE_HOOK, + "User was not authorized to set profile rule hooks", + ctx, + ) + .await?; + } + + profile + } }; config.profiles.insert(name.into(), profile); config.save(&self.config_last_saved)?; @@ -996,7 +1024,17 @@ impl<'a> Handler { name: &str, rule: Option, hooks: ProfileHooks, + ctx: ClientContext, ) -> anyhow::Result<()> { + if !hooks.is_empty() { + self.check_auth( + polkit_actions::PROFILE_HOOK, + "User was not authorized to set profile rule hooks", + ctx, + ) + .await?; + } + { let mut config = self.config.write().await; let profile = config @@ -1195,6 +1233,37 @@ impl<'a> Handler { controller.cleanup().await; } } + + async fn check_auth( + &self, + action: &str, + error_msg: &str, + ctx: ClientContext, + ) -> anyhow::Result<()> { + let polkit_proxy = self + .polkit_proxy + .as_ref() + .context("Polkit not available, cannot ask for authorization")?; + + let pid = ctx.client_pid.context("No client PID available")?; + let subject = policykit1::Subject::new_for_owner(pid, None, None)?; + let result = polkit_proxy + .check_authorization( + &subject, + action, + &HashMap::new(), + policykit1::CheckAuthorizationFlags::AllowUserInteraction.into(), + "", + ) + .await + .context("Authorization could not be granted")?; + + if result.is_authorized { + Ok(()) + } else { + bail!("{error_msg}"); + } + } } async fn apply_config_to_controllers( @@ -1495,3 +1564,15 @@ fn controller_vendor_is_unique( == *vendor_id }) } + +async fn connect_polkit_proxy() -> anyhow::Result> { + let conn = zbus::Connection::system() + .await + .context("Could not establish dbus connection")?; + + let proxy = AuthorityProxy::new(&conn) + .await + .context("Could not connect to polkit")?; + + Ok(proxy) +} diff --git a/res/io.github.ilya_zlobintsev.LACT.policy b/res/io.github.ilya_zlobintsev.LACT.policy new file mode 100644 index 00000000..6620bad8 --- /dev/null +++ b/res/io.github.ilya_zlobintsev.LACT.policy @@ -0,0 +1,13 @@ + + + + + Set LACT profile hooks + Setting root profile hooks requires authorization + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + +