From 82bcf5007d34effa6f30751713e64b5f57c4c687 Mon Sep 17 00:00:00 2001 From: desbma-s1n Date: Thu, 14 Nov 2024 15:59:40 +0100 Subject: [PATCH] feat: changeable effects (WIP) --- src/summarize.rs | 27 +++++- src/systemd/options.rs | 68 +++++++++++++ src/systemd/resolver.rs | 208 ++++++++++++++++++++++++++++++---------- 3 files changed, 249 insertions(+), 54 deletions(-) diff --git a/src/summarize.rs b/src/summarize.rs index 4fccf9e..cb3a0e2 100644 --- a/src/summarize.rs +++ b/src/summarize.rs @@ -90,7 +90,7 @@ impl ValueCounted for u16 { } } -impl CountableSetSpecifier { +impl CountableSetSpecifier { fn contains_one(&self, needle: &T) -> bool { match self { Self::None => false, @@ -116,6 +116,31 @@ impl CountableSetSpecifier { Self::All => !matches!(other, Self::None), } } + + /// Remove a single element from the set + /// The element to remove **must** be in the set, otherwise may panic + #[expect(clippy::unwrap_used)] + pub(crate) fn remove(&mut self, to_rm: &Self) { + debug_assert!(self.intersects(to_rm)); + let Self::One(e) = to_rm else { unreachable!() }; + match self { + Self::None => unreachable!(), + Self::One(_) => { + *self = Self::None; + } + Self::Some(es) => { + let idx = es.iter().position(|e2| e == e2).unwrap(); + es.remove(idx); + } + Self::AllExcept(excs) => { + let idx = excs.binary_search(e).unwrap_err(); + excs.insert(idx, e.to_owned()); + } + Self::All => { + *self = Self::AllExcept(vec![e.to_owned()]); + } + } + } } /// Quantify something that is done or denied diff --git a/src/systemd/options.rs b/src/systemd/options.rs index b2a99e8..38b53d0 100644 --- a/src/systemd/options.rs +++ b/src/systemd/options.rs @@ -20,11 +20,21 @@ use crate::{ systemd::{KernelVersion, SystemdVersion}, }; +/// Callbacks to dynamic update an option to make it compatible with an action +#[derive(Debug)] +pub(crate) struct OptionUpdater { + /// Generate a new option effect compatible with the previously incompatible action + pub effect: fn(&OptionValueEffect, &ProgramAction) -> Option, + /// Generate the option value from the new effect + pub value: fn(&OptionValueEffect) -> OptionValue, +} + /// Systemd option with its possibles values, and their effect #[derive(Debug)] pub(crate) struct OptionDescription { pub name: &'static str, pub possible_values: Vec, + pub updater: Option, } impl fmt::Display for OptionDescription { @@ -862,6 +872,7 @@ pub(crate) fn build_options( })), }, ], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHome= @@ -918,6 +929,7 @@ pub(crate) fn build_options( )), }, ], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateTmp= @@ -936,6 +948,7 @@ pub(crate) fn build_options( }), ])), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateDevices= @@ -968,6 +981,7 @@ pub(crate) fn build_options( OptionValueEffect::DenySyscalls(DenySyscalls::Class("raw-io")), ])), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelTunables= @@ -1014,6 +1028,7 @@ pub(crate) fn build_options( .collect(), )), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelModules= @@ -1034,6 +1049,7 @@ pub(crate) fn build_options( OptionValueEffect::DenySyscalls(DenySyscalls::Class("module")), ])), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelLogs= @@ -1053,6 +1069,7 @@ pub(crate) fn build_options( }), ])), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectControlGroups= @@ -1065,6 +1082,7 @@ pub(crate) fn build_options( exceptions: vec![], })), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectProc= @@ -1085,6 +1103,7 @@ pub(crate) fn build_options( regex::bytes::Regex::new("^/proc/[0-9]+(/|$)").unwrap(), ))), }], + updater: None, }); } @@ -1098,6 +1117,7 @@ pub(crate) fn build_options( ProgramAction::WriteExecuteMemoryMapping, )), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies= @@ -1173,6 +1193,7 @@ pub(crate) fn build_options( .collect(), ), }], + updater: None, }); if let HardeningMode::Aggressive = mode { @@ -1194,6 +1215,7 @@ pub(crate) fn build_options( }), )), }], + updater: None, }); } @@ -1234,6 +1256,46 @@ pub(crate) fn build_options( .collect(), ), }], + updater: Some(OptionUpdater { + effect: |e, a| { + let OptionValueEffect::DenyAction(ProgramAction::NetworkActivity(effect_na)) = e + else { + unreachable!(); + }; + let ProgramAction::NetworkActivity(denied_na) = a else { + unreachable!(); + }; + let mut new_eff_local_port = effect_na.local_port.clone(); + new_eff_local_port.remove(&denied_na.local_port); + Some(OptionValueEffect::DenyAction( + ProgramAction::NetworkActivity(NetworkActivity { + af: effect_na.af.clone(), + proto: effect_na.proto.clone(), + kind: effect_na.kind.clone(), + local_port: new_eff_local_port, + }), + )) + }, + value: |e| { + let OptionValueEffect::DenyAction(ProgramAction::NetworkActivity(denied_na)) = e + else { + unreachable!(); + }; + OptionValue::List { + values: denied_na + .af + .iter() + .zip(denied_na.proto) + .zip(denied_na.local_port.iter_ranges()) + .map(|(af, proto)| format!("{af}:{proto}:{port_range}")) + .collect(), + value_if_empty: None, + negation_prefix: false, + repeat_option: true, + mode: ListMode::BlackList, + } + }, + }), }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#LockPersonality= @@ -1248,6 +1310,7 @@ pub(crate) fn build_options( "personality", ))), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictRealtime= @@ -1259,6 +1322,7 @@ pub(crate) fn build_options( ProgramAction::SetRealtimeScheduler, )), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectClock= @@ -1271,6 +1335,7 @@ pub(crate) fn build_options( "clock", ))), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#CapabilityBoundingSet= @@ -1430,6 +1495,7 @@ pub(crate) fn build_options( }, desc: OptionEffect::Cumulative(cap_effects.into_iter().map(|(_c, e)| e).collect()), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter= @@ -1464,6 +1530,7 @@ pub(crate) fn build_options( .collect(), ), }], + updater: None, }); if let HardeningMode::Aggressive = mode { @@ -1477,6 +1544,7 @@ pub(crate) fn build_options( value: OptionValue::String("native".to_owned()), desc: OptionEffect::None, }], + updater: None, }); } diff --git a/src/systemd/resolver.rs b/src/systemd/resolver.rs index 5843fdb..4ed6406 100644 --- a/src/systemd/resolver.rs +++ b/src/systemd/resolver.rs @@ -7,78 +7,160 @@ use crate::{ }, }; +use super::options::OptionUpdater; + impl OptionValueEffect { - fn compatible(&self, action: &ProgramAction, prev_actions: &[ProgramAction]) -> bool { + fn compatible( + &self, + action: &ProgramAction, + prev_actions: &[ProgramAction], + updater: Option<&OptionUpdater>, + ) -> ActionOptionEffectCompatibility { match self { - OptionValueEffect::DenyAction(denied) => match denied { - ProgramAction::NetworkActivity(denied) => { - if let ProgramAction::NetworkActivity(NetworkActivity { - af, - proto, - kind, - local_port, - }) = action - { - !denied.af.intersects(af) - || !denied.proto.intersects(proto) - || !denied.kind.intersects(kind) - || !denied.local_port.intersects(local_port) + OptionValueEffect::DenyAction(denied) => { + let compatible = match denied { + ProgramAction::NetworkActivity(denied) => { + if let ProgramAction::NetworkActivity(NetworkActivity { + af, + proto, + kind, + local_port, + }) = action + { + let af_match = denied.af.intersects(af); + let proto_match = denied.proto.intersects(proto); + let kind_match = denied.kind.intersects(kind); + let local_port_match = denied.local_port.intersects(local_port); + !af_match || !proto_match || !kind_match || !local_port_match + } else { + true + } + } + ProgramAction::WriteExecuteMemoryMapping + | ProgramAction::SetRealtimeScheduler + | ProgramAction::Wakeup + | ProgramAction::MknodSpecial + | ProgramAction::SetAlarm => action != denied, + ProgramAction::Syscalls(_) + | ProgramAction::Read(_) + | ProgramAction::Write(_) + | ProgramAction::Create(_) => unreachable!(), + }; + if compatible { + ActionOptionEffectCompatibility::Compatible + } else if let Some(updater) = updater { + if let Some(new_eff) = (updater.effect)(self, action) { + ActionOptionEffectCompatibility::CompatibleIfChanged( + ChangedOptionValueDescription { + value: (updater.value)(&new_eff), + effect: new_eff, + }, + ) } else { - true + ActionOptionEffectCompatibility::Incompatible } + } else { + ActionOptionEffectCompatibility::Incompatible } - ProgramAction::WriteExecuteMemoryMapping - | ProgramAction::SetRealtimeScheduler - | ProgramAction::Wakeup - | ProgramAction::MknodSpecial - | ProgramAction::SetAlarm => action != denied, - ProgramAction::Syscalls(_) - | ProgramAction::Read(_) - | ProgramAction::Write(_) - | ProgramAction::Create(_) => unreachable!(), - }, + } OptionValueEffect::DenyWrite(ro_paths) => match action { ProgramAction::Write(path_action) | ProgramAction::Create(path_action) => { - !ro_paths.matches(path_action) + ActionOptionEffectCompatibility::from(!ro_paths.matches(path_action)) } - _ => true, + _ => ActionOptionEffectCompatibility::Compatible, }, OptionValueEffect::Hide(hidden_paths) => { if let ProgramAction::Read(path_action) = action { - !hidden_paths.matches(path_action) - || prev_actions.contains(&ProgramAction::Create(path_action.clone())) + (!hidden_paths.matches(path_action) + || prev_actions.contains(&ProgramAction::Create(path_action.clone()))) + .into() } else { - true + ActionOptionEffectCompatibility::Compatible } } OptionValueEffect::DenySyscalls(denied) => { if let ProgramAction::Syscalls(syscalls) = action { let denied_syscalls = denied.syscalls(); let syscalls = syscalls.iter().map(String::as_str).collect(); - denied_syscalls.intersection(&syscalls).next().is_none() + denied_syscalls + .intersection(&syscalls) + .next() + .is_none() + .into() } else { - true + ActionOptionEffectCompatibility::Compatible } } - OptionValueEffect::Multiple(effects) => { - effects.iter().all(|e| e.compatible(action, prev_actions)) - } + OptionValueEffect::Multiple(effects) => effects + .iter() + .all(|e| match e.compatible(action, prev_actions, None) { + ActionOptionEffectCompatibility::Compatible => true, + ActionOptionEffectCompatibility::CompatibleIfChanged(_) => todo!(), + ActionOptionEffectCompatibility::Incompatible => false, + }) + .into(), } } } -pub(crate) fn actions_compatible(eff: &OptionValueEffect, actions: &[ProgramAction]) -> bool { +/// A systemd option value and its effect, altered from original +pub(crate) struct ChangedOptionValueDescription { + pub value: OptionValue, + pub effect: OptionValueEffect, +} + +/// How compatible is an action with an option effect? +pub(crate) enum ActionOptionEffectCompatibility { + Compatible, + CompatibleIfChanged(ChangedOptionValueDescription), + Incompatible, +} + +impl From for ActionOptionEffectCompatibility { + fn from(value: bool) -> Self { + if value { + Self::Compatible + } else { + Self::Incompatible + } + } +} + +pub(crate) fn actions_compatible( + eff: &OptionValueEffect, + actions: &[ProgramAction], + updater: Option<&OptionUpdater>, +) -> ActionOptionEffectCompatibility { + let mut changed_desc: Option = None; for i in 0..actions.len() { - if !eff.compatible(&actions[i], &actions[..i]) { - log::debug!( - "Option effect {:?} is incompatible with {:?}", - eff, - actions[i] - ); - return false; + let cur_eff = changed_desc.as_ref().map_or(eff, |d| &d.effect); + match cur_eff.compatible(&actions[i], &actions[..i], updater) { + ActionOptionEffectCompatibility::Compatible => {} + ActionOptionEffectCompatibility::CompatibleIfChanged(new_desc) => { + log::debug!( + "Option effect {:?} is incompatible with {:?}, changing effect to {:?}", + cur_eff, + actions[i], + new_desc.effect + ); + changed_desc = Some(new_desc); + } + ActionOptionEffectCompatibility::Incompatible => { + log::debug!( + "Option effect {:?} is incompatible with {:?}", + cur_eff, + actions[i] + ); + return ActionOptionEffectCompatibility::Incompatible; + } } } - true + + if let Some(new_desc) = changed_desc { + ActionOptionEffectCompatibility::CompatibleIfChanged(new_desc) + } else { + ActionOptionEffectCompatibility::Compatible + } } pub(crate) fn resolve( @@ -99,12 +181,22 @@ pub(crate) fn resolve( break; } OptionEffect::Simple(effect) => { - if actions_compatible(effect, actions) { - candidates.push(OptionWithValue { - name: opt.name.to_owned(), - value: opt_value_desc.value.clone(), - }); - break; + match actions_compatible(effect, actions, opt.updater.as_ref()) { + ActionOptionEffectCompatibility::Compatible => { + candidates.push(OptionWithValue { + name: opt.name.to_owned(), + value: opt_value_desc.value.clone(), + }); + break; + } + ActionOptionEffectCompatibility::CompatibleIfChanged(opt_new_desc) => { + candidates.push(OptionWithValue { + name: opt.name.to_owned(), + value: opt_new_desc.value.clone(), + }); + break; + } + ActionOptionEffectCompatibility::Incompatible => {} } } OptionEffect::Cumulative(effects) => { @@ -119,10 +211,20 @@ pub(crate) fn resolve( let mut compatible_opts = Vec::new(); debug_assert_eq!(values.len(), effects.len()); for (optv, opte) in values.iter().zip(effects) { - let compatible = actions_compatible(opte, actions); + let compatible = + actions_compatible(opte, actions, opt.updater.as_ref()); let enable_opt = match mode { - ListMode::WhiteList => !compatible, - ListMode::BlackList => compatible, + ListMode::WhiteList => matches!( + compatible, + ActionOptionEffectCompatibility::Incompatible + ), + ListMode::BlackList => match compatible { + ActionOptionEffectCompatibility::Compatible => true, + ActionOptionEffectCompatibility::CompatibleIfChanged(_) => { + unimplemented!() + } + ActionOptionEffectCompatibility::Incompatible => false, + }, }; if enable_opt { compatible_opts.push(optv.to_string());