diff --git a/src/cli.rs b/src/cli.rs index 52b70136..77c09dda 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -234,7 +234,9 @@ impl ValueEnum for &'static Format { pub fn main() { let cli = Jay::parse(); - drop_all_pr_caps(); + if not_matches!(cli.command, Cmd::Run(_)) { + drop_all_pr_caps(); + } match cli.command { Cmd::Run(a) => start_compositor(cli.global, a), Cmd::GenerateCompletion(g) => generate::main(g), diff --git a/src/compositor.rs b/src/compositor.rs index 82967771..b80076e4 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -39,6 +39,7 @@ use { logger::Logger, output_schedule::OutputSchedule, portal::{self, PortalStartup}, + pr_caps::pr_caps, scale::Scale, sighand::{self, SighandError}, state::{ConnectorData, IdleState, ScreenlockState, State, XWaylandState}, @@ -51,9 +52,17 @@ use { }, user_session::import_environment, utils::{ - clone3::ensure_reaper, clonecell::CloneCell, errorfmt::ErrorFmt, fdcloser::FdCloser, - numcell::NumCell, oserror::OsError, queue::AsyncQueue, refcounted::RefCounted, - run_toplevel::RunToplevel, tri::Try, + clone3::ensure_reaper, + clonecell::CloneCell, + errorfmt::ErrorFmt, + fdcloser::FdCloser, + nice::{did_elevate_scheduler, elevate_scheduler}, + numcell::NumCell, + oserror::OsError, + queue::AsyncQueue, + refcounted::RefCounted, + run_toplevel::RunToplevel, + tri::Try, }, version::VERSION, video::drm::wait_for_sync_obj::WaitForSyncObj, @@ -72,6 +81,11 @@ pub const MAX_EXTENTS: i32 = (1 << 22) - 1; pub fn start_compositor(global: GlobalArgs, args: RunArgs) { sighand::reset_all(); let reaper_pid = ensure_reaper(); + let caps = pr_caps().into_comp(); + if caps.has_nice() { + elevate_scheduler(); + } + drop(caps); let forker = create_forker(reaper_pid); let portal = portal::run_from_compositor(global.log_level.into()); enable_profiler(); @@ -146,6 +160,9 @@ fn start_compositor2( ) -> Result<(), CompositorError> { log::info!("pid = {}", uapi::getpid()); log::info!("version = {VERSION}"); + if did_elevate_scheduler() { + log::info!("Running with elevated scheduler: SCHED_RR"); + } init_fd_limit(); leaks::init(); clientmem::init()?; @@ -680,7 +697,7 @@ fn create_dummy_output(state: &Rc) { state.dummy_output.set(Some(dummy_output)); } -fn config_dir() -> Option { +pub fn config_dir() -> Option { if let Ok(xdg) = env::var("XDG_CONFIG_HOME") { Some(format!("{}/jay", xdg)) } else if let Ok(home) = env::var("HOME") { diff --git a/src/config.rs b/src/config.rs index ebc993cd..ce5ada32 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,8 +10,13 @@ use { state::State, tree::ToplevelData, utils::{ - clonecell::CloneCell, numcell::NumCell, ptr_ext::PtrExt, - toplevel_identifier::ToplevelIdentifier, unlink_on_drop::UnlinkOnDrop, xrd::xrd, + clonecell::CloneCell, + nice::{JAY_NO_REALTIME, dont_allow_config_so}, + numcell::NumCell, + ptr_ext::PtrExt, + toplevel_identifier::ToplevelIdentifier, + unlink_on_drop::UnlinkOnDrop, + xrd::xrd, }, }, bincode::Options, @@ -26,7 +31,7 @@ use { window::{self, TileState}, }, libloading::Library, - std::{cell::Cell, io, mem, ptr, rc::Rc}, + std::{cell::Cell, io, mem, path::Path, ptr, rc::Rc}, thiserror::Error, }; @@ -42,6 +47,8 @@ pub enum ConfigError { CopyConfigFile(#[source] io::Error), #[error("XDG_RUNTIME_DIR is not set")] XrdNotSet, + #[error("Custom config.so is not permitted")] + NotPermitted, } pub struct ConfigProxy { @@ -280,11 +287,24 @@ impl ConfigProxy { } pub fn from_config_dir(state: &Rc) -> Result { + if dont_allow_config_so() { + if have_config_so(state.config_dir.as_deref()) { + log::warn!("Not loading config.so because"); + log::warn!(" 1. Jay was started with CAP_SYS_NICE"); + log::warn!(" 2. Jay was not started with {}=1", JAY_NO_REALTIME); + log::warn!(" 3. The scheduler was elevated to SCHED_RR"); + log::warn!( + " 4. Jay was not compiled with {}=1", + jay_allow_realtime_config_so!(), + ); + } + return Err(ConfigError::NotPermitted); + } let dir = match state.config_dir.as_deref() { Some(d) => d, _ => return Err(ConfigError::ConfigDirNotSet), }; - let file = format!("{}/config.so", dir); + let file = format!("{}/{CONFIG_SO}", dir); unsafe { Self::from_file(&file, state) } } @@ -355,3 +375,15 @@ pub struct InvokedShortcut { pub effective_mods: Modifiers, pub sym: KeySym, } + +const CONFIG_SO: &str = "config.so"; + +pub fn have_config_so(config_dir: Option<&str>) -> bool { + let Some(dir) = config_dir else { + return false; + }; + let mut dir = dir.to_owned(); + dir.push_str("/"); + dir.push_str(CONFIG_SO); + Path::new(&dir).exists() +} diff --git a/src/macros.rs b/src/macros.rs index 53ada9a6..cf7161b6 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -760,3 +760,15 @@ macro_rules! client_wire_scale_to_logical { } }; } + +macro_rules! not_matches { + ($($tt:tt)*) => { + !matches!($($tt)*) + }; +} + +macro_rules! jay_allow_realtime_config_so { + () => { + "JAY_ALLOW_REALTIME_CONFIG_SO" + }; +} diff --git a/src/pr_caps.rs b/src/pr_caps.rs index 2040918c..1ab65757 100644 --- a/src/pr_caps.rs +++ b/src/pr_caps.rs @@ -1,17 +1,52 @@ use { crate::{ pr_caps::sys::{ - _LINUX_CAPABILITY_U32S_3, _LINUX_CAPABILITY_VERSION_3, cap_user_data_t, + _LINUX_CAPABILITY_U32S_3, _LINUX_CAPABILITY_VERSION_3, CAP_SYS_NICE, cap_user_data_t, cap_user_header_t, }, - utils::{errorfmt::ErrorFmt, oserror::OsError}, + utils::{bitflags::BitflagsExt, errorfmt::ErrorFmt, oserror::OsError}, }, uapi::{ - c::{SYS_capset, syscall}, + c::{SYS_capget, SYS_capset, syscall}, map_err, }, }; +pub struct PrCaps { + effective: u64, + permitted: u64, + inheritable: u64, +} + +pub struct PrCompCaps { + caps: PrCaps, +} + +pub fn pr_caps() -> PrCaps { + let mut hdr = cap_user_header_t { + version: _LINUX_CAPABILITY_VERSION_3, + pid: 0, + }; + let mut caps = [cap_user_data_t::default(); _LINUX_CAPABILITY_U32S_3]; + let ret = unsafe { syscall(SYS_capget, &mut hdr, &mut caps) }; + if let Err(e) = map_err!(ret) { + eprintln!( + "Could not get process capabilities: {}", + ErrorFmt(OsError(e.0)) + ); + return PrCaps { + effective: 0, + permitted: 0, + inheritable: 0, + }; + } + PrCaps { + effective: caps[0].effective as u64 | ((caps[1].effective as u64) << 32), + permitted: caps[0].permitted as u64 | ((caps[1].permitted as u64) << 32), + inheritable: caps[0].inheritable as u64 | ((caps[1].inheritable as u64) << 32), + } +} + pub fn drop_all_pr_caps() { let mut hdr = cap_user_header_t { version: _LINUX_CAPABILITY_VERSION_3, @@ -27,6 +62,55 @@ pub fn drop_all_pr_caps() { } } +impl PrCaps { + pub fn into_comp(mut self) -> PrCompCaps { + let mut caps = 0; + macro_rules! add_cap { + ($name:ident) => { + if self.permitted.contains(1 << $name) { + caps |= 1 << $name; + } + }; + } + add_cap!(CAP_SYS_NICE); + let mut hdr = cap_user_header_t { + version: _LINUX_CAPABILITY_VERSION_3, + pid: 0, + }; + let caps_hi = (caps >> 32) as u32; + let caps_lo = caps as u32; + let mut data = [cap_user_data_t::default(); _LINUX_CAPABILITY_U32S_3]; + data[0].effective = caps_lo; + data[1].effective = caps_hi; + data[0].permitted = caps_lo; + data[1].permitted = caps_hi; + let ret = unsafe { syscall(SYS_capset, &mut hdr, &data) }; + if let Err(e) = map_err!(ret) { + eprintln!( + "Could not get set compositor capabilities: {}", + ErrorFmt(OsError(e.0)) + ); + return PrCompCaps { caps: self }; + } + self.effective = caps; + self.permitted = caps; + self.inheritable = 0; + PrCompCaps { caps: self } + } +} + +impl PrCompCaps { + pub fn has_nice(&self) -> bool { + self.caps.effective.contains(1 << CAP_SYS_NICE) + } +} + +impl Drop for PrCaps { + fn drop(&mut self) { + drop_all_pr_caps(); + } +} + mod sys { #![allow(dead_code)] diff --git a/src/utils.rs b/src/utils.rs index 298b663f..92928ca2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -28,6 +28,7 @@ pub mod line_logger; pub mod linkedlist; pub mod log_on_drop; pub mod mmap; +pub mod nice; pub mod nonblock; pub mod num_cpus; pub mod numcell; diff --git a/src/utils/clone3.rs b/src/utils/clone3.rs index b6ef0d0f..17247682 100644 --- a/src/utils/clone3.rs +++ b/src/utils/clone3.rs @@ -1,6 +1,7 @@ use { crate::{ forker::ForkerError, + pr_caps::drop_all_pr_caps, utils::{errorfmt::ErrorFmt, on_drop::OnDrop, process_name::set_process_name}, }, std::{env, mem::MaybeUninit, process, slice, str::FromStr}, @@ -161,6 +162,7 @@ pub fn ensure_reaper() -> c::pid_t { set_deathsig(); return reaper_pid; }; + drop_all_pr_caps(); set_process_name("jay reaper"); while let Ok((pid, status)) = uapi::wait() { if pid == main_process_id { diff --git a/src/utils/nice.rs b/src/utils/nice.rs new file mode 100644 index 00000000..d7d4b5a8 --- /dev/null +++ b/src/utils/nice.rs @@ -0,0 +1,40 @@ +use { + crate::{compositor::config_dir, config::have_config_so}, + c::sched_setscheduler, + std::{ + env, mem, + sync::atomic::{AtomicBool, Ordering::Relaxed}, + }, + uapi::c::{self, SCHED_RESET_ON_FORK, SCHED_RR, sched_param}, +}; + +static DID_ELEVATE_SCHEDULER: AtomicBool = AtomicBool::new(false); + +pub const JAY_NO_REALTIME: &str = "JAY_NO_REALTIME"; + +pub fn elevate_scheduler() { + if env::var(JAY_NO_REALTIME).as_deref().unwrap_or_default() == "1" { + return; + } + if have_config_so(config_dir().as_deref()) && dont_allow_realtime_config_so() { + return; + } + let mut param = unsafe { mem::zeroed::() }; + param.sched_priority = 1; + let res = unsafe { sched_setscheduler(0, SCHED_RR | SCHED_RESET_ON_FORK, ¶m) }; + if res == 0 { + DID_ELEVATE_SCHEDULER.store(true, Relaxed); + } +} + +pub fn did_elevate_scheduler() -> bool { + DID_ELEVATE_SCHEDULER.load(Relaxed) +} + +fn dont_allow_realtime_config_so() -> bool { + option_env!(jay_allow_realtime_config_so!()).unwrap_or_default() != "1" +} + +pub fn dont_allow_config_so() -> bool { + did_elevate_scheduler() && dont_allow_realtime_config_so() +}