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..31ef4f1 100644 --- a/src/systemd/options.rs +++ b/src/systemd/options.rs @@ -20,11 +20,14 @@ use crate::{ systemd::{KernelVersion, SystemdVersion}, }; +use super::resolver::OptionValueUpdater; + /// Systemd option with its possibles values, and their effect #[derive(Debug)] pub(crate) struct OptionDescription { pub name: &'static str, pub possible_values: Vec, + pub value_updater: Option, } impl fmt::Display for OptionDescription { @@ -862,6 +865,7 @@ pub(crate) fn build_options( })), }, ], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHome= @@ -918,6 +922,7 @@ pub(crate) fn build_options( )), }, ], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateTmp= @@ -936,6 +941,7 @@ pub(crate) fn build_options( }), ])), }], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateDevices= @@ -968,6 +974,7 @@ pub(crate) fn build_options( OptionValueEffect::DenySyscalls(DenySyscalls::Class("raw-io")), ])), }], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelTunables= @@ -1014,6 +1021,7 @@ pub(crate) fn build_options( .collect(), )), }], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelModules= @@ -1034,6 +1042,7 @@ pub(crate) fn build_options( OptionValueEffect::DenySyscalls(DenySyscalls::Class("module")), ])), }], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelLogs= @@ -1053,6 +1062,7 @@ pub(crate) fn build_options( }), ])), }], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectControlGroups= @@ -1065,6 +1075,7 @@ pub(crate) fn build_options( exceptions: vec![], })), }], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectProc= @@ -1085,6 +1096,7 @@ pub(crate) fn build_options( regex::bytes::Regex::new("^/proc/[0-9]+(/|$)").unwrap(), ))), }], + value_updater: None, }); } @@ -1098,6 +1110,7 @@ pub(crate) fn build_options( ProgramAction::WriteExecuteMemoryMapping, )), }], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies= @@ -1173,6 +1186,7 @@ pub(crate) fn build_options( .collect(), ), }], + value_updater: None, }); if let HardeningMode::Aggressive = mode { @@ -1194,6 +1208,7 @@ pub(crate) fn build_options( }), )), }], + value_updater: None, }); } @@ -1234,6 +1249,7 @@ pub(crate) fn build_options( .collect(), ), }], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#LockPersonality= @@ -1248,6 +1264,7 @@ pub(crate) fn build_options( "personality", ))), }], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictRealtime= @@ -1259,6 +1276,7 @@ pub(crate) fn build_options( ProgramAction::SetRealtimeScheduler, )), }], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectClock= @@ -1271,6 +1289,7 @@ pub(crate) fn build_options( "clock", ))), }], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#CapabilityBoundingSet= @@ -1430,6 +1449,7 @@ pub(crate) fn build_options( }, desc: OptionEffect::Cumulative(cap_effects.into_iter().map(|(_c, e)| e).collect()), }], + value_updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter= @@ -1464,6 +1484,7 @@ pub(crate) fn build_options( .collect(), ), }], + value_updater: None, }); if let HardeningMode::Aggressive = mode { @@ -1477,6 +1498,7 @@ pub(crate) fn build_options( value: OptionValue::String("native".to_owned()), desc: OptionEffect::None, }], + value_updater: None, }); } diff --git a/src/systemd/resolver.rs b/src/systemd/resolver.rs index 5843fdb..08bff3e 100644 --- a/src/systemd/resolver.rs +++ b/src/systemd/resolver.rs @@ -7,8 +7,15 @@ use crate::{ }, }; +pub(crate) type OptionValueUpdater = fn(&OptionValueEffect) -> OptionValue; + impl OptionValueEffect { - fn compatible(&self, action: &ProgramAction, prev_actions: &[ProgramAction]) -> bool { + fn compatible( + &self, + action: &ProgramAction, + prev_actions: &[ProgramAction], + value_updater: Option<&OptionValueUpdater>, + ) -> ActionOptionEffectCompatibility { match self { OptionValueEffect::DenyAction(denied) => match denied { ProgramAction::NetworkActivity(denied) => { @@ -19,19 +26,43 @@ impl OptionValueEffect { local_port, }) = action { - !denied.af.intersects(af) - || !denied.proto.intersects(proto) - || !denied.kind.intersects(kind) - || !denied.local_port.intersects(local_port) + 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); + if !af_match || !proto_match || !kind_match || !local_port_match { + ActionOptionEffectCompatibility::Compatible + } else if let Some(value_updater) = value_updater { + // This option supports altering the effects and generating the option value, to make it + // compatible + let mut new_eff_local_port = denied.local_port.clone(); + new_eff_local_port.remove(local_port); + let new_eff = OptionValueEffect::DenyAction( + ProgramAction::NetworkActivity(NetworkActivity { + af: denied.af.clone(), + proto: denied.proto.clone(), + kind: denied.kind.clone(), + local_port: new_eff_local_port, + }), + ); + ActionOptionEffectCompatibility::CompatibleIfChanged( + ChangedOptionValueDescription { + value: value_updater(&new_eff), + effect: new_eff, + }, + ) + } else { + ActionOptionEffectCompatibility::Incompatible + } } else { - true + ActionOptionEffectCompatibility::Compatible } } ProgramAction::WriteExecuteMemoryMapping | ProgramAction::SetRealtimeScheduler | ProgramAction::Wakeup | ProgramAction::MknodSpecial - | ProgramAction::SetAlarm => action != denied, + | ProgramAction::SetAlarm => (action != denied).into(), ProgramAction::Syscalls(_) | ProgramAction::Read(_) | ProgramAction::Write(_) @@ -39,46 +70,102 @@ impl OptionValueEffect { }, 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(), + } + } +} + +/// 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]) -> bool { +pub(crate) fn actions_compatible( + eff: &OptionValueEffect, + actions: &[ProgramAction], + value_updater: Option<&OptionValueUpdater>, +) -> 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], value_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 +186,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.value_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 +216,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.value_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());