diff --git a/src/manager/user.rs b/src/manager/user.rs index 5a486a8..1c2f3af 100644 --- a/src/manager/user.rs +++ b/src/manager/user.rs @@ -855,6 +855,7 @@ impl WifiPowerManagement1 { async fn create_config_interfaces( proxy: &Proxy<'static>, object_server: &ObjectServer, + connection: &Connection, job_manager: &UnboundedSender, tdp_manager: Option>, ) -> Result<()> { @@ -885,12 +886,24 @@ async fn create_config_interfaces( job_manager: job_manager.clone(), }; - if config.factory_reset.is_some() { - object_server.at(MANAGER_PATH, factory_reset).await?; + if let Some(config) = config.factory_reset.as_ref() { + match config.is_valid(true).await { + Ok(true) => { + object_server.at(MANAGER_PATH, factory_reset).await?; + } + Ok(false) => (), + Err(e) => error!("Failed to verify if factory reset config is valid: {e}"), + } } - if config.fan_control.is_some() { - object_server.at(MANAGER_PATH, fan_control).await?; + if let Some(config) = config.fan_control.as_ref() { + match config.is_valid(connection, true).await { + Ok(true) => { + object_server.at(MANAGER_PATH, fan_control).await?; + } + Ok(false) => (), + Err(e) => error!("Failed to verify if fan control config is valid: {e}"), + } } if let Some(manager) = tdp_manager { @@ -918,7 +931,7 @@ async fn create_config_interfaces( }); } - if let Some(ref config) = config.performance_profile { + if let Some(config) = config.performance_profile.as_ref() { if !get_available_platform_profiles(&config.platform_profile_name) .await .unwrap_or_default() @@ -928,8 +941,14 @@ async fn create_config_interfaces( } } - if config.storage.is_some() { - object_server.at(MANAGER_PATH, storage).await?; + if let Some(config) = config.storage.as_ref() { + match config.is_valid(true).await { + Ok(true) => { + object_server.at(MANAGER_PATH, storage).await?; + } + Ok(false) => (), + Err(e) => error!("Failed to verify if storage config is valid: {e}"), + } } if let Some(config) = config.update_bios.as_ref() { @@ -1007,7 +1026,7 @@ pub(crate) async fn create_interfaces( let object_server = session.object_server(); object_server.at(MANAGER_PATH, manager).await?; - create_config_interfaces(&proxy, object_server, &job_manager, tdp_manager).await?; + create_config_interfaces(&proxy, object_server, &system, &job_manager, tdp_manager).await?; if device_type().await.unwrap_or_default() == DeviceType::SteamDeck { object_server.at(MANAGER_PATH, als).await?; @@ -1069,8 +1088,8 @@ mod test { use crate::hardware::test::fake_model; use crate::hardware::SteamDeckVariant; use crate::platform::{ - BatteryChargeLimitConfig, PerformanceProfileConfig, PlatformConfig, RangeConfig, - ResetConfig, ScriptConfig, ServiceConfig, StorageConfig, TdpLimitConfig, + BatteryChargeLimitConfig, FormatDeviceConfig, PerformanceProfileConfig, PlatformConfig, + RangeConfig, ResetConfig, ScriptConfig, ServiceConfig, StorageConfig, TdpLimitConfig, }; use crate::power::TdpLimitingMethod; use crate::systemd::test::{MockManager, MockUnit}; @@ -1078,6 +1097,7 @@ mod test { use std::num::NonZeroU32; use std::os::unix::fs::PermissionsExt; + use std::path::PathBuf; use std::time::Duration; use tokio::fs::{set_permissions, write}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; @@ -1273,6 +1293,42 @@ mod test { assert!(test_interface_missing::(&test.connection).await); } + #[tokio::test] + async fn interface_missing_invalid_all_factory_reset1() { + let mut config = all_config().unwrap(); + config.factory_reset.as_mut().unwrap().all = ScriptConfig { + script: PathBuf::from("oxo"), + script_args: Vec::new(), + }; + let test = start(Some(config)).await.expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + + #[tokio::test] + async fn interface_missing_invalid_os_factory_reset1() { + let mut config = all_config().unwrap(); + config.factory_reset.as_mut().unwrap().os = ScriptConfig { + script: PathBuf::from("oxo"), + script_args: Vec::new(), + }; + let test = start(Some(config)).await.expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + + #[tokio::test] + async fn interface_missing_invalid_user_factory_reset1() { + let mut config = all_config().unwrap(); + config.factory_reset.as_mut().unwrap().user = ScriptConfig { + script: PathBuf::from("oxo"), + script_args: Vec::new(), + }; + let test = start(Some(config)).await.expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + #[tokio::test] async fn interface_matches_fan_control1() { let test = start(all_config()).await.expect("start"); @@ -1416,6 +1472,29 @@ mod test { assert!(test_interface_missing::(&test.connection).await); } + #[tokio::test] + async fn interface_missing_invalid_trim_storage1() { + let mut config = all_config().unwrap(); + config.storage.as_mut().unwrap().trim_devices = ScriptConfig { + script: PathBuf::from("oxo"), + script_args: Vec::new(), + }; + let test = start(Some(config)).await.expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + + #[tokio::test] + async fn interface_missing_invalid_format_storage1() { + let mut config = all_config().unwrap(); + let mut format_config = FormatDeviceConfig::default(); + format_config.script = PathBuf::from("oxo"); + config.storage.as_mut().unwrap().format_device = format_config; + let test = start(Some(config)).await.expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + #[tokio::test] async fn interface_matches_update_bios1() { let test = start(all_config()).await.expect("start"); @@ -1432,6 +1511,18 @@ mod test { assert!(test_interface_missing::(&test.connection).await); } + #[tokio::test] + async fn interface_missing_invalid_update_bios1() { + let mut config = all_config().unwrap(); + config.update_bios = Some(ScriptConfig { + script: PathBuf::from("oxo"), + script_args: Vec::new(), + }); + let test = start(Some(config)).await.expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + #[tokio::test] async fn interface_matches_update_dock1() { let test = start(all_config()).await.expect("start"); @@ -1448,6 +1539,18 @@ mod test { assert!(test_interface_missing::(&test.connection).await); } + #[tokio::test] + async fn interface_missing_invalid_update_dock1() { + let mut config = all_config().unwrap(); + config.update_dock = Some(ScriptConfig { + script: PathBuf::from("oxo"), + script_args: Vec::new(), + }); + let test = start(Some(config)).await.expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + #[tokio::test] async fn interface_matches_wifi_power_management1() { let test = start(all_config()).await.expect("start"); diff --git a/src/platform.rs b/src/platform.rs index ad24c78..bea68be 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -19,10 +19,12 @@ use tokio::fs::{metadata, read_to_string}; #[cfg(not(test))] use tokio::sync::OnceCell; use tokio::task::spawn_blocking; +use zbus::Connection; #[cfg(not(test))] use crate::hardware::{device_type, DeviceType}; use crate::power::TdpLimitingMethod; +use crate::systemd::SystemdUnit; #[cfg(not(test))] static CONFIG: OnceCell> = OnceCell::const_new(); @@ -93,6 +95,14 @@ pub(crate) struct ResetConfig { pub user: ScriptConfig, } +impl ResetConfig { + pub(crate) async fn is_valid(&self, root: bool) -> Result { + Ok(self.all.is_valid(root).await? + && self.os.is_valid(root).await? + && self.user.is_valid(root).await?) + } +} + #[derive(Clone, Deserialize, Debug)] pub(crate) enum ServiceConfig { #[serde(rename = "systemd")] @@ -105,12 +115,33 @@ pub(crate) enum ServiceConfig { }, } +impl ServiceConfig { + pub(crate) async fn is_valid(&self, connection: &Connection, root: bool) -> Result { + match self { + ServiceConfig::Systemd(unit) => SystemdUnit::exists(connection, unit).await, + ServiceConfig::Script { + start, + stop, + status, + } => Ok(start.is_valid(root).await? + && stop.is_valid(root).await? + && status.is_valid(root).await?), + } + } +} + #[derive(Clone, Default, Deserialize, Debug)] pub(crate) struct StorageConfig { pub trim_devices: ScriptConfig, pub format_device: FormatDeviceConfig, } +impl StorageConfig { + pub(crate) async fn is_valid(&self, root: bool) -> Result { + Ok(self.trim_devices.is_valid(root).await? && self.format_device.is_valid(root).await?) + } +} + #[derive(Clone, Deserialize, Debug)] pub(crate) struct BatteryChargeLimitConfig { pub suggested_minimum_limit: Option, @@ -153,6 +184,36 @@ pub(crate) struct FormatDeviceConfig { pub no_validate_flag: Option, } +impl FormatDeviceConfig { + pub(crate) async fn is_valid(&self, root: bool) -> Result { + let meta = match metadata(&self.script).await { + Ok(meta) => meta, + Err(e) if [ErrorKind::NotFound, ErrorKind::PermissionDenied].contains(&e.kind()) => { + return Ok(false) + } + Err(e) => return Err(e.into()), + }; + if !meta.is_file() { + return Ok(false); + } + if root { + let script = self.script.clone(); + if !spawn_blocking(move || match access(&script, AccessFlags::X_OK) { + Ok(()) => Ok(true), + Err(Errno::ENOENT | Errno::EACCES) => Ok(false), + Err(e) => Err(e), + }) + .await?? + { + return Ok(false); + } + } else if (meta.mode() & 0o111) == 0 { + return Ok(false); + } + Ok(true) + } +} + impl RangeConfig { #[allow(unused)] pub(crate) fn new(min: T, max: T) -> RangeConfig { @@ -181,14 +242,35 @@ impl PlatformConfig { #[cfg(test)] pub(crate) fn set_test_paths(&mut self) { + use crate::path; + + if let Some(ref mut factory_reset) = self.factory_reset { + if factory_reset.all.script.as_os_str().is_empty() { + factory_reset.all.script = path("exe"); + } + if factory_reset.os.script.as_os_str().is_empty() { + factory_reset.os.script = path("exe"); + } + if factory_reset.user.script.as_os_str().is_empty() { + factory_reset.user.script = path("exe"); + } + } + if let Some(ref mut storage) = self.storage { + if storage.trim_devices.script.as_os_str().is_empty() { + storage.trim_devices.script = path("exe"); + } + if storage.format_device.script.as_os_str().is_empty() { + storage.format_device.script = path("exe"); + } + } if let Some(ref mut update_bios) = self.update_bios { if update_bios.script.as_os_str().is_empty() { - update_bios.script = crate::path("exe"); + update_bios.script = path("exe"); } } if let Some(ref mut update_dock) = self.update_dock { if update_dock.script.as_os_str().is_empty() { - update_dock.script = crate::path("exe"); + update_dock.script = path("exe"); } } } diff --git a/src/systemd.rs b/src/systemd.rs index 1743fa8..d18cfb7 100644 --- a/src/systemd.rs +++ b/src/systemd.rs @@ -62,6 +62,8 @@ trait SystemdManager { ) -> zbus::Result>; async fn reload(&self) -> zbus::Result<()>; + + async fn get_unit(&self, name: &str) -> zbus::Result; } #[derive(Display, EnumString, PartialEq, Debug, Copy, Clone)] @@ -86,6 +88,17 @@ pub async fn daemon_reload(connection: &Connection) -> Result<()> { } impl<'dbus> SystemdUnit<'dbus> { + pub async fn exists(connection: &Connection, name: &str) -> Result { + let manager = SystemdManagerProxy::new(connection).await?; + // This is kinda hacky, but zbus makes it pretty hard to get the proper error name + let expected_error = format!("Unit {name} not loaded."); + match manager.get_unit(name).await { + Ok(_) => Ok(true), + Err(zbus::Error::Failure(message)) if message == expected_error => Ok(false), + Err(e) => Err(e.into()), + } + } + pub async fn new(connection: Connection, name: &str) -> Result> { let path = PathBuf::from("/org/freedesktop/systemd1/unit").join(escape(name)); let path = String::from(path.to_str().ok_or(anyhow!("Unit name {name} invalid"))?); @@ -358,6 +371,14 @@ pub mod test { async fn reload(&self) -> fdo::Result<()> { Ok(()) } + + async fn get_unit(&mut self, unit: &str) -> fdo::Result { + Ok( + ObjectPath::try_from(format!("/org/freedesktop/systemd1/unit/{}", escape(unit))) + .unwrap() + .into(), + ) + } } #[tokio::test]