From 93e9efbc28fce9a9167d62331e38be7b7d42d0a5 Mon Sep 17 00:00:00 2001 From: desbma-s1n Date: Thu, 28 Sep 2023 19:32:14 +0200 Subject: [PATCH] feat: support RestrictRealtime systemd option --- src/summarize.rs | 22 ++++++++-- src/systemd/options.rs | 15 ++++++- src/systemd/resolver.rs | 7 +++- tests/cl.rs | 93 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 7 deletions(-) diff --git a/src/summarize.rs b/src/summarize.rs index 0831922..b1d593d 100644 --- a/src/summarize.rs +++ b/src/summarize.rs @@ -22,7 +22,9 @@ pub enum ProgramAction { /// Network (socket) activity NetworkActivity { af: String }, /// Memory mapping with write and execute bits - WxMemoryMapping, + WriteExecuteMemoryMapping, + /// Set scheduler to a real time one + SetRealtimeScheduler, /// Names of the syscalls made by the program Syscalls(HashSet), } @@ -48,6 +50,7 @@ enum SyscallInfo { path_dst_idx: usize, flags_idx: Option, }, + SetScheduler, Socket, StatFd { fd_idx: usize, @@ -130,6 +133,9 @@ lazy_static! { }, ), + // set scheduler + ("sched_setscheduler", SyscallInfo::SetScheduler), + // socket ("socket", SyscallInfo::Socket), @@ -369,6 +375,16 @@ where _ => (), } } + Some(SyscallInfo::SetScheduler) => { + let policy = if let Some(SyscallArg::Integer { value, .. }) = syscall.args.get(1) { + value + } else { + anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args); + }; + if policy.is_flag_set("SCHED_FIFO") | policy.is_flag_set("SCHED_RR") { + actions.push(ProgramAction::SetRealtimeScheduler); + } + } Some(SyscallInfo::Socket) => { let af = if let Some(SyscallArg::Integer { value: IntegerExpression::NamedConst(af), @@ -390,7 +406,7 @@ where anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args); }; if prot.is_flag_set("PROT_WRITE") && prot.is_flag_set("PROT_EXEC") { - actions.push(ProgramAction::WxMemoryMapping); + actions.push(ProgramAction::WriteExecuteMemoryMapping); } } None => (), @@ -408,7 +424,7 @@ where syscall_names.sort(); for syscall_name in syscall_names { let count = stats.get(syscall_name).unwrap(); - log::debug!("{:20} {: >12}", format!("{syscall_name}:"), count); + log::debug!("{:24} {: >12}", format!("{syscall_name}:"), count); } Ok(actions) diff --git a/src/systemd/options.rs b/src/systemd/options.rs index 15040c4..5f1ca72 100644 --- a/src/systemd/options.rs +++ b/src/systemd/options.rs @@ -96,7 +96,9 @@ pub enum OptionValueEffect { /// Deny a socket family DenySocketFamily(String), /// Deny a write execute memory mapping - DenyWxMemoryMapping, + DenyWriteExecuteMemoryMapping, + /// Deny real time scheduling + DenyRealtimeScheduler, /// Union of multiple effects Multiple(Vec), } @@ -1029,7 +1031,7 @@ pub fn build_options( name: "MemoryDenyWriteExecute".to_string(), possible_values: vec![OptionValueDescription { value: OptionValue::Boolean(true), - desc: OptionEffect::Simple(OptionValueEffect::DenyWxMemoryMapping), + desc: OptionEffect::Simple(OptionValueEffect::DenyWriteExecuteMemoryMapping), }], }); @@ -1123,6 +1125,15 @@ pub fn build_options( }], }); + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictRealtime= + options.push(OptionDescription { + name: "RestrictRealtime".to_string(), + possible_values: vec![OptionValueDescription { + value: OptionValue::Boolean(true), + desc: OptionEffect::Simple(OptionValueEffect::DenyRealtimeScheduler), + }], + }); + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter= // // Also change the default behavior when calling a denied syscall to return EPERM instead og killing diff --git a/src/systemd/resolver.rs b/src/systemd/resolver.rs index c84e867..61958ae 100644 --- a/src/systemd/resolver.rs +++ b/src/systemd/resolver.rs @@ -37,8 +37,11 @@ impl OptionValueEffect { true } } - OptionValueEffect::DenyWxMemoryMapping => { - !matches!(action, ProgramAction::WxMemoryMapping) + OptionValueEffect::DenyWriteExecuteMemoryMapping => { + !matches!(action, ProgramAction::WriteExecuteMemoryMapping) + } + OptionValueEffect::DenyRealtimeScheduler => { + !matches!(action, ProgramAction::SetRealtimeScheduler) } OptionValueEffect::Multiple(effects) => { effects.iter().all(|e| e.compatible(action, prev_actions)) diff --git a/tests/cl.rs b/tests/cl.rs index 60f2d67..13935f4 100644 --- a/tests/cl.rs +++ b/tests/cl.rs @@ -44,6 +44,7 @@ fn run_true() { .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); } @@ -77,6 +78,7 @@ fn run_write_dev_null() { .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); } @@ -110,6 +112,7 @@ fn run_ls_dev() { .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); } @@ -143,6 +146,7 @@ fn run_ls_proc() { .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); } @@ -176,6 +180,7 @@ fn run_read_kallsyms() { .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); } @@ -209,6 +214,7 @@ fn run_ls_modules() { .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); } @@ -216,6 +222,7 @@ fn run_ls_modules() { #[cfg_attr(not(feature = "as-root"), ignore)] fn run_dmesg() { assert!(Uid::effective().is_root()); + Command::cargo_bin(env!("CARGO_PKG_NAME")) .unwrap() .args(["run", "--", "dmesg"]) @@ -234,6 +241,7 @@ fn run_dmesg() { .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); } @@ -241,6 +249,7 @@ fn run_dmesg() { #[cfg_attr(feature = "as-root", ignore)] fn run_systemctl() { assert!(!Uid::effective().is_root()); + Command::cargo_bin(env!("CARGO_PKG_NAME")) .unwrap() .args(["run", "--", "systemctl", "--user"]) @@ -263,6 +272,7 @@ fn run_systemctl() { .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=AF_UNIX\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicates::boolean::OrPredicate::new( predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1), predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1), @@ -299,6 +309,7 @@ fn run_ss() { .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=AF_NETLINK AF_UNIX\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); } @@ -326,5 +337,87 @@ fn run_mmap_wx() { .stdout(predicate::str::contains("MemoryDenyWriteExecute=\n").not()) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); + + Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["run", "--", "python3", "-c", "import mmap, os, tempfile; f = tempfile.NamedTemporaryFile(\"wb\"); f.write(os.urandom(16)); f.flush(); mmap.mmap(f.file.fileno(), 0, prot=mmap.PROT_WRITE)"]) + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("ProtectSystem=full\n").count(1)) + .stdout(predicate::str::contains("ProtectHome=read-only\n").count(1)) + .stdout(if env::current_exe().unwrap().starts_with("/tmp") { + predicate::str::contains("PrivateTmp=true\n").count(0) + } else { + predicate::str::contains("PrivateTmp=true\n").count(1) + }) + .stdout(predicate::str::contains("PrivateDevices=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelTunables=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1)) + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) + .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) + .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) + .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) + .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); +} + +#[test] +#[cfg_attr(not(feature = "as-root"), ignore)] +fn run_sched_realtime() { + assert!(Uid::effective().is_root()); + + Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["run", "--", "python3", "-c", "import os; os.sched_setscheduler(0, os.SCHED_RR, os.sched_param(os.sched_get_priority_min(os.SCHED_RR)))"]) + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) + .stdout(predicate::str::contains("ProtectHome=read-only\n").count(1)) + .stdout(if !Uid::effective().is_root() && env::current_exe().unwrap().starts_with("/tmp") { + predicate::str::contains("PrivateTmp=true\n").count(0) + } else { + predicate::str::contains("PrivateTmp=true\n").count(1) + }) + .stdout(predicate::str::contains("PrivateDevices=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelTunables=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1)) + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) + .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) + .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) + .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=").not()) + .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @sandbox:EPERM @setuid:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); + + Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["run", "--", "python3", "-c", "import os; os.sched_setscheduler(0, os.SCHED_IDLE, os.sched_param(0))"]) + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) + .stdout(predicate::str::contains("ProtectHome=read-only\n").count(1)) + .stdout(if !Uid::effective().is_root() && env::current_exe().unwrap().starts_with("/tmp") { + predicate::str::contains("PrivateTmp=true\n").count(0) + } else { + predicate::str::contains("PrivateTmp=true\n").count(1) + }) + .stdout(predicate::str::contains("PrivateDevices=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelTunables=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1)) + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) + .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) + .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) + .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) + .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @sandbox:EPERM @setuid:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); }