platform: Improve validation of platform config items

This commit is contained in:
Vicki Pfau 2025-06-10 18:33:53 -07:00
parent f32e354b64
commit d5d2d2c9a3
3 changed files with 218 additions and 12 deletions

View file

@ -855,6 +855,7 @@ impl WifiPowerManagement1 {
async fn create_config_interfaces(
proxy: &Proxy<'static>,
object_server: &ObjectServer,
connection: &Connection,
job_manager: &UnboundedSender<JobManagerCommand>,
tdp_manager: Option<UnboundedSender<TdpManagerCommand>>,
) -> Result<()> {
@ -885,13 +886,25 @@ async fn create_config_interfaces(
job_manager: job_manager.clone(),
};
if config.factory_reset.is_some() {
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() {
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 {
let low_power_mode = LowPowerMode1 {
@ -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,9 +941,15 @@ async fn create_config_interfaces(
}
}
if config.storage.is_some() {
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() {
match config.is_valid(true).await {
@ -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::<FactoryReset1>(&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::<FactoryReset1>(&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::<FactoryReset1>(&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::<FactoryReset1>(&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::<Storage1>(&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::<Storage1>(&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::<Storage1>(&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::<UpdateBios1>(&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::<UpdateBios1>(&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::<UpdateDock1>(&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::<UpdateDock1>(&test.connection).await);
}
#[tokio::test]
async fn interface_matches_wifi_power_management1() {
let test = start(all_config()).await.expect("start");

View file

@ -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<Option<PlatformConfig>> = 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<bool> {
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<bool> {
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<bool> {
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<i32>,
@ -153,6 +184,36 @@ pub(crate) struct FormatDeviceConfig {
pub no_validate_flag: Option<String>,
}
impl FormatDeviceConfig {
pub(crate) async fn is_valid(&self, root: bool) -> Result<bool> {
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<T: Clone> RangeConfig<T> {
#[allow(unused)]
pub(crate) fn new(min: T, max: T) -> RangeConfig<T> {
@ -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");
}
}
}

View file

@ -62,6 +62,8 @@ trait SystemdManager {
) -> zbus::Result<Vec<(String, String, String)>>;
async fn reload(&self) -> zbus::Result<()>;
async fn get_unit(&self, name: &str) -> zbus::Result<OwnedObjectPath>;
}
#[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<bool> {
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<SystemdUnit<'dbus>> {
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<OwnedObjectPath> {
Ok(
ObjectPath::try_from(format!("/org/freedesktop/systemd1/unit/{}", escape(unit)))
.unwrap()
.into(),
)
}
}
#[tokio::test]