From f97cb4631570fb3c55245f5532209ecb1b15f53c Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 10 May 2025 20:02:03 +0200 Subject: [PATCH 1/6] toml-config: fix `reload-config-so` name --- toml-spec/spec/spec.generated.json | 2 +- toml-spec/spec/spec.generated.md | 2 +- toml-spec/spec/spec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 92c92194..d5a81922 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1529,7 +1529,7 @@ "tile", "quit", "reload-config-toml", - "reload-config-to", + "reload-config-so", "consume", "forward", "none", diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 1ca471be..cd0cb221 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -3428,7 +3428,7 @@ The string should have one of the following values: Reload the `config.toml`. -- `reload-config-to`: +- `reload-config-so`: Reload the `config.so`. diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 2d49ef38..da6c07e6 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -790,7 +790,7 @@ SimpleActionName: description: Terminate the compositor. - value: reload-config-toml description: Reload the `config.toml`. - - value: reload-config-to + - value: reload-config-so description: Reload the `config.so`. - value: consume description: | From b3334449a6f1a524949d1b24e729084d7f124e7e Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 10 May 2025 20:03:34 +0200 Subject: [PATCH 2/6] reaper: setup deathsig in compositor --- src/utils/clone3.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/utils/clone3.rs b/src/utils/clone3.rs index 672d3256..b6ef0d0f 100644 --- a/src/utils/clone3.rs +++ b/src/utils/clone3.rs @@ -135,6 +135,7 @@ pub fn ensure_reaper() -> c::pid_t { if let Ok(id) = env::var(REAPER_VAR) { if let Ok(id) = c::pid_t::from_str(&id) { if uapi::getppid() == id { + set_deathsig(); return id; } } @@ -157,6 +158,7 @@ pub fn ensure_reaper() -> c::pid_t { unsafe { env::set_var(REAPER_VAR, reaper_pid.to_string()); } + set_deathsig(); return reaper_pid; }; set_process_name("jay reaper"); @@ -167,3 +169,9 @@ pub fn ensure_reaper() -> c::pid_t { } process::exit(1); } + +fn set_deathsig() { + unsafe { + c::prctl(c::PR_SET_PDEATHSIG, c::SIGKILL as c::c_ulong); + } +} From af6e868a78e360df5567356f1bbd8d658263c03c Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 10 May 2025 22:36:39 +0200 Subject: [PATCH 3/6] all: drop all privileges at startup --- src/cli.rs | 2 ++ src/main.rs | 1 + src/pr_caps.rs | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 src/pr_caps.rs diff --git a/src/cli.rs b/src/cli.rs index cc31a642..52b70136 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -28,6 +28,7 @@ use { compositor::start_compositor, format::{Format, ref_formats}, portal, + pr_caps::drop_all_pr_caps, }, ::log::Level, clap::{Args, Parser, Subcommand, ValueEnum, ValueHint, builder::PossibleValue}, @@ -233,6 +234,7 @@ impl ValueEnum for &'static Format { pub fn main() { let cli = Jay::parse(); + 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/main.rs b/src/main.rs index 4fa74368..a226be2d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -88,6 +88,7 @@ mod output_schedule; mod pango; mod pipewire; mod portal; +mod pr_caps; mod rect; mod renderer; mod scale; diff --git a/src/pr_caps.rs b/src/pr_caps.rs new file mode 100644 index 00000000..2040918c --- /dev/null +++ b/src/pr_caps.rs @@ -0,0 +1,94 @@ +use { + crate::{ + pr_caps::sys::{ + _LINUX_CAPABILITY_U32S_3, _LINUX_CAPABILITY_VERSION_3, cap_user_data_t, + cap_user_header_t, + }, + utils::{errorfmt::ErrorFmt, oserror::OsError}, + }, + uapi::{ + c::{SYS_capset, syscall}, + map_err, + }, +}; + +pub fn drop_all_pr_caps() { + let mut hdr = cap_user_header_t { + version: _LINUX_CAPABILITY_VERSION_3, + pid: 0, + }; + let caps = [cap_user_data_t::default(); _LINUX_CAPABILITY_U32S_3]; + let ret = unsafe { syscall(SYS_capset, &mut hdr, &caps) }; + if let Err(e) = map_err!(ret) { + eprintln!( + "Could not get drop capabilities: {}", + ErrorFmt(OsError(e.0)) + ); + } +} + +mod sys { + #![allow(dead_code)] + + use uapi::c::pid_t; + + pub const _LINUX_CAPABILITY_VERSION_3: u32 = 0x20080522; + pub const _LINUX_CAPABILITY_U32S_3: usize = 2; + + #[repr(C)] + #[derive(Copy, Clone, Debug)] + pub struct cap_user_header_t { + pub version: u32, + pub pid: pid_t, + } + + #[repr(C)] + #[derive(Copy, Clone, Debug, Default)] + pub struct cap_user_data_t { + pub effective: u32, + pub permitted: u32, + pub inheritable: u32, + } + + pub const CAP_CHOWN: u32 = 0; + pub const CAP_DAC_OVERRIDE: u32 = 1; + pub const CAP_DAC_READ_SEARCH: u32 = 2; + pub const CAP_FOWNER: u32 = 3; + pub const CAP_FSETID: u32 = 4; + pub const CAP_KILL: u32 = 5; + pub const CAP_SETGID: u32 = 6; + pub const CAP_SETUID: u32 = 7; + pub const CAP_SETPCAP: u32 = 8; + pub const CAP_LINUX_IMMUTABLE: u32 = 9; + pub const CAP_NET_BIND_SERVICE: u32 = 10; + pub const CAP_NET_BROADCAST: u32 = 11; + pub const CAP_NET_ADMIN: u32 = 12; + pub const CAP_NET_RAW: u32 = 13; + pub const CAP_IPC_LOCK: u32 = 14; + pub const CAP_IPC_OWNER: u32 = 15; + pub const CAP_SYS_MODULE: u32 = 16; + pub const CAP_SYS_RAWIO: u32 = 17; + pub const CAP_SYS_CHROOT: u32 = 18; + pub const CAP_SYS_PTRACE: u32 = 19; + pub const CAP_SYS_PACCT: u32 = 20; + pub const CAP_SYS_ADMIN: u32 = 21; + pub const CAP_SYS_BOOT: u32 = 22; + pub const CAP_SYS_NICE: u32 = 23; + pub const CAP_SYS_RESOURCE: u32 = 24; + pub const CAP_SYS_TIME: u32 = 25; + pub const CAP_SYS_TTY_CONFIG: u32 = 26; + pub const CAP_MKNOD: u32 = 27; + pub const CAP_LEASE: u32 = 28; + pub const CAP_AUDIT_WRITE: u32 = 29; + pub const CAP_AUDIT_CONTROL: u32 = 30; + pub const CAP_SETFCAP: u32 = 31; + pub const CAP_MAC_OVERRIDE: u32 = 32; + pub const CAP_MAC_ADMIN: u32 = 33; + pub const CAP_SYSLOG: u32 = 34; + pub const CAP_WAKE_ALARM: u32 = 35; + pub const CAP_BLOCK_SUSPEND: u32 = 36; + pub const CAP_AUDIT_READ: u32 = 37; + pub const CAP_PERFMON: u32 = 38; + pub const CAP_BPF: u32 = 39; + pub const CAP_CHECKPOINT_RESTORE: u32 = 40; +} From 7a623006e23d3480bef389d60663c990d1b4b446 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 10 May 2025 22:41:40 +0200 Subject: [PATCH 4/6] compositor: set scheduler to SCHED_RR before dropping CAP_SYS_NICE --- src/cli.rs | 4 +- src/compositor.rs | 25 +++++++++++-- src/config.rs | 40 ++++++++++++++++++-- src/macros.rs | 12 ++++++ src/pr_caps.rs | 90 +++++++++++++++++++++++++++++++++++++++++++-- src/utils.rs | 1 + src/utils/clone3.rs | 2 + src/utils/nice.rs | 40 ++++++++++++++++++++ 8 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 src/utils/nice.rs 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() +} From 68713b2e39016524e4f736eb68e0f8717e16d74d Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 10 May 2025 22:44:00 +0200 Subject: [PATCH 5/6] vulkan: create high-priority queues if possible --- Cargo.lock | 7 +++ Cargo.toml | 1 + src/compositor.rs | 24 ++++++-- src/gfx_apis.rs | 7 ++- src/gfx_apis/vulkan.rs | 18 +++++- src/gfx_apis/vulkan/device.rs | 113 +++++++++++++++++++++++++++------- src/portal/ptl_display.rs | 27 ++++---- src/pr_caps.rs | 97 +++++++++++++++++++++++++++++ src/state.rs | 3 + 9 files changed, 253 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89371e19..43cd50e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -611,6 +611,7 @@ dependencies = [ "num-derive", "num-traits", "once_cell", + "opera", "parking_lot", "pin-project", "png", @@ -859,6 +860,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "opera" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444847837ad2026bf6addabcf0c3c25bf3e9f92e435dedb555f4a6a0180ded1" + [[package]] name = "option-ext" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 254e0146..30e475ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ kbvm = "0.1.4" tiny-skia = { version = "0.11.4", default-features = false, features = ["std"] } regex = "1.11.1" cfg-if = "1.0.0" +opera = "1.0.1" [build-dependencies] repc = "0.1.1" diff --git a/src/compositor.rs b/src/compositor.rs index b80076e4..c220f868 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -39,7 +39,7 @@ use { logger::Logger, output_schedule::OutputSchedule, portal::{self, PortalStartup}, - pr_caps::pr_caps, + pr_caps::{PrCapsThread, pr_caps}, scale::Scale, sighand::{self, SighandError}, state::{ConnectorData, IdleState, ScreenlockState, State, XWaylandState}, @@ -82,10 +82,13 @@ 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() { + let caps_thread = if caps.has_nice() { elevate_scheduler(); - } - drop(caps); + Some(caps.into_thread()) + } else { + drop(caps); + None + }; let forker = create_forker(reaper_pid); let portal = portal::run_from_compositor(global.log_level.into()); enable_profiler(); @@ -97,7 +100,14 @@ pub fn start_compositor(global: GlobalArgs, args: RunArgs) { None } }; - let res = start_compositor2(Some(forker), portal, Some(logger.clone()), args, None); + let res = start_compositor2( + Some(forker), + portal, + Some(logger.clone()), + args, + None, + caps_thread, + ); leaks::log_leaked(); if let Err(e) = res { let e = ErrorFmt(e); @@ -111,7 +121,7 @@ pub fn start_compositor(global: GlobalArgs, args: RunArgs) { #[cfg(feature = "it")] pub fn start_compositor_for_test(future: TestFuture) -> Result<(), CompositorError> { - let res = start_compositor2(None, None, None, RunArgs::default(), Some(future)); + let res = start_compositor2(None, None, None, RunArgs::default(), Some(future), None); leaks::log_leaked(); res } @@ -157,6 +167,7 @@ fn start_compositor2( logger: Option>, run_args: RunArgs, test_future: Option, + caps_thread: Option, ) -> Result<(), CompositorError> { log::info!("pid = {}", uapi::getpid()); log::info!("version = {VERSION}"); @@ -321,6 +332,7 @@ fn start_compositor2( show_pin_icon: Cell::new(false), cl_matcher_manager: ClMatcherManager::new(&crit_ids), tl_matcher_manager: TlMatcherManager::new(&crit_ids), + caps_thread, }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); diff --git a/src/gfx_apis.rs b/src/gfx_apis.rs index 6226e25c..2a393b09 100644 --- a/src/gfx_apis.rs +++ b/src/gfx_apis.rs @@ -4,6 +4,7 @@ use { async_engine::AsyncEngine, gfx_api::{GfxContext, GfxError}, io_uring::IoUring, + pr_caps::PrCapsThread, utils::errorfmt::ErrorFmt, video::drm::Drm, }, @@ -19,12 +20,13 @@ pub fn create_gfx_context( ring: &Rc, drm: &Drm, api: GfxApi, + caps_thread: Option<&PrCapsThread>, ) -> Result, GfxError> { let mut apis = [GfxApi::OpenGl, GfxApi::Vulkan]; apis.sort_by_key(|&a| if a == api { -1 } else { a as i32 }); let mut last_err = None; for api in apis { - let res = create_gfx_context_(eng, ring, drm, api); + let res = create_gfx_context_(eng, ring, drm, api, caps_thread); match res { Ok(_) => return res, Err(e) => { @@ -41,10 +43,11 @@ fn create_gfx_context_( ring: &Rc, drm: &Drm, api: GfxApi, + caps_thread: Option<&PrCapsThread>, ) -> Result, GfxError> { match api { GfxApi::OpenGl => gl::create_gfx_context(drm), - GfxApi::Vulkan => vulkan::create_graphics_context(eng, ring, drm), + GfxApi::Vulkan => vulkan::create_graphics_context(eng, ring, drm, caps_thread), _ => unreachable!(), } } diff --git a/src/gfx_apis/vulkan.rs b/src/gfx_apis/vulkan.rs index cddac79e..4b9282fa 100644 --- a/src/gfx_apis/vulkan.rs +++ b/src/gfx_apis/vulkan.rs @@ -35,8 +35,9 @@ use { image::VulkanImageMemory, instance::VulkanInstance, renderer::VulkanRenderer, }, io_uring::IoUring, + pr_caps::PrCapsThread, rect::Rect, - utils::oserror::OsError, + utils::{errorfmt::ErrorFmt, oserror::OsError}, video::{ dmabuf::DmaBuf, drm::{Drm, DrmError, sync_obj::SyncObjCtx}, @@ -224,16 +225,27 @@ pub fn create_graphics_context( eng: &Rc, ring: &Rc, drm: &Drm, + caps_thread: Option<&PrCapsThread>, ) -> Result, GfxError> { let instance = VulkanInstance::new(Level::Info, *VULKAN_VALIDATION)?; - let device = instance.create_device(drm)?; + let device = 'device: { + if let Some(t) = caps_thread { + match unsafe { t.run(|| instance.create_device(drm, true)) } { + Ok(d) => break 'device d, + Err(e) => { + log::warn!("Could not create high-priority device: {}", ErrorFmt(e)); + } + } + } + instance.create_device(drm, false)? + }; let renderer = device.create_renderer(eng, ring)?; Ok(Rc::new(Context(renderer))) } pub fn create_vulkan_allocator(drm: &Drm) -> Result, AllocatorError> { let instance = VulkanInstance::new(Level::Debug, *VULKAN_VALIDATION)?; - let device = instance.create_device(drm)?; + let device = instance.create_device(drm, false)?; let allocator = device.create_bo_allocator(drm)?; Ok(Rc::new(allocator)) } diff --git a/src/gfx_apis/vulkan/device.rs b/src/gfx_apis/vulkan/device.rs index 5734b4d9..cd139a56 100644 --- a/src/gfx_apis/vulkan/device.rs +++ b/src/gfx_apis/vulkan/device.rs @@ -24,12 +24,12 @@ use { physical_device_drm, queue_family_foreign, }, khr::{ - driver_properties, external_fence_fd, external_memory_fd, external_semaphore_fd, + self, driver_properties, external_fence_fd, external_memory_fd, external_semaphore_fd, push_descriptor, }, vk::{ - self, DeviceCreateInfo, DeviceQueueCreateInfo, DeviceSize, DriverId, - ExternalSemaphoreFeatureFlags, ExternalSemaphoreHandleTypeFlags, + self, DeviceCreateInfo, DeviceQueueCreateInfo, DeviceQueueGlobalPriorityCreateInfoKHR, + DeviceSize, DriverId, ExternalSemaphoreFeatureFlags, ExternalSemaphoreHandleTypeFlags, ExternalSemaphoreProperties, MAX_MEMORY_TYPES, MemoryPropertyFlags, MemoryType, PhysicalDevice, PhysicalDeviceBufferDeviceAddressFeatures, PhysicalDeviceDescriptorBufferFeaturesEXT, PhysicalDeviceDescriptorBufferPropertiesEXT, @@ -39,7 +39,7 @@ use { PhysicalDeviceProperties2, PhysicalDeviceSynchronization2Features, PhysicalDeviceTimelineSemaphoreFeatures, PhysicalDeviceUniformBufferStandardLayoutFeatures, PhysicalDeviceVulkan12Properties, - Queue, QueueFlags, + Queue, QueueFamilyProperties2, QueueFlags, QueueGlobalPriorityKHR, }, }, isnt::std_1::collections::IsntHashMap2Ext, @@ -50,6 +50,7 @@ use { sync::Arc, }, uapi::Ustr, + vk::QueueFamilyGlobalPriorityPropertiesKHR, }; pub struct VulkanDevice { @@ -210,20 +211,42 @@ impl VulkanInstance { fn find_queues( &self, phy_dev: PhysicalDevice, - ) -> Result<(u32, Option<(u32, u32, u32)>), VulkanError> { - let props = unsafe { + ) -> Result< + ( + u32, + Option<(u32, u32, u32)>, + QueueGlobalPriorityKHR, + QueueGlobalPriorityKHR, + ), + VulkanError, + > { + let len = unsafe { self.instance - .get_physical_device_queue_family_properties(phy_dev) + .get_physical_device_queue_family_properties2_len(phy_dev) }; + let mut priority_props = vec![QueueFamilyGlobalPriorityPropertiesKHR::default(); len]; + let mut props: Vec<_> = priority_props + .iter_mut() + .map(|p| QueueFamilyProperties2::default().push_next(p)) + .collect(); + unsafe { + self.instance + .get_physical_device_queue_family_properties2(phy_dev, &mut props[..]) + } let gfx_queue = props .iter() - .position(|p| p.queue_flags.contains(QueueFlags::GRAPHICS)) + .position(|p| { + p.queue_family_properties + .queue_flags + .contains(QueueFlags::GRAPHICS) + }) .ok_or(VulkanError::NoGraphicsQueue)?; let transfer_queue = 'transfer: { let mut transfer_only = None; let mut compute_only = None; let mut separate_gfx = None; for (idx, props) in props.iter().enumerate() { + let props = &props.queue_family_properties; if idx == gfx_queue { continue; } @@ -244,7 +267,7 @@ impl VulkanInstance { if let Some(idx) = transfer_only.or(compute_only).or(separate_gfx) { break 'transfer Some(idx); } - if props[gfx_queue].queue_count > 1 { + if props[gfx_queue].queue_family_properties.queue_count > 1 { break 'transfer Some(gfx_queue); } None @@ -252,13 +275,27 @@ impl VulkanInstance { let mut width_mask = 0; let mut height_mask = 0; if let Some(idx) = transfer_queue { - let g = &props[idx].min_image_transfer_granularity; + let g = &props[idx] + .queue_family_properties + .min_image_transfer_granularity; width_mask = g.width.wrapping_sub(1); height_mask = g.height.wrapping_sub(1); } + let get_priority = |idx: usize| { + let props = &priority_props[idx]; + if props.priority_count > 0 { + props.priorities[props.priority_count as usize - 1] + } else { + QueueGlobalPriorityKHR::MEDIUM + } + }; Ok(( gfx_queue as _, transfer_queue.map(|v| (v as _, width_mask, height_mask)), + get_priority(gfx_queue), + transfer_queue + .map(get_priority) + .unwrap_or(QueueGlobalPriorityKHR::MEDIUM), )) } @@ -275,7 +312,11 @@ impl VulkanInstance { .contains(ExternalSemaphoreFeatureFlags::IMPORTABLE) } - pub fn create_device(self: &Rc, drm: &Drm) -> Result, VulkanError> { + pub fn create_device( + self: &Rc, + drm: &Drm, + mut high_priority: bool, + ) -> Result, VulkanError> { let render_node = drm .get_render_node() .map_err(VulkanError::FetchRenderNode)? @@ -293,7 +334,17 @@ impl VulkanInstance { if !supports_descriptor_buffer { log::warn!("Vulkan device does not support descriptor buffers"); } - let (graphics_queue_family_idx, transfer_queue_family) = self.find_queues(phy_dev)?; + let supports_queue_priority = extensions.contains_key(khr::global_priority::NAME); + if !supports_queue_priority && high_priority { + high_priority = false; + log::warn!("Vulkan device does not support queue priorities"); + } + let ( + graphics_queue_family_idx, + transfer_queue_family, + max_graphics_priority, + max_transfer_priority, + ) = self.find_queues(phy_dev)?; let mut distinct_transfer_queue_family_idx = None; let mut transfer_granularity_mask = (0, 0); if let Some((idx, width_mask, height_mask)) = transfer_queue_family { @@ -325,18 +376,31 @@ impl VulkanInstance { let mut uniform_buffer_standard_layout_features = PhysicalDeviceUniformBufferStandardLayoutFeatures::default() .uniform_buffer_standard_layout(true); + let mut gfx_queue_device_queue_global_priority_create_info = + DeviceQueueGlobalPriorityCreateInfoKHR::default() + .global_priority(max_graphics_priority); + let mut trn_queue_device_queue_global_priority_create_info = + DeviceQueueGlobalPriorityCreateInfoKHR::default() + .global_priority(max_transfer_priority); let mut queue_create_infos = ArrayVec::<_, 2>::new(); - queue_create_infos.push( - DeviceQueueCreateInfo::default() - .queue_family_index(graphics_queue_family_idx) - .queue_priorities(&[1.0]), - ); + let queue_create_info = |idx, priority_info| { + let mut info = DeviceQueueCreateInfo::default() + .queue_family_index(idx) + .queue_priorities(&[1.0]); + if high_priority { + info = info.push_next(priority_info); + } + info + }; + queue_create_infos.push(queue_create_info( + graphics_queue_family_idx, + &mut gfx_queue_device_queue_global_priority_create_info, + )); if let Some((tq, _, _)) = transfer_queue_family { - queue_create_infos.push( - DeviceQueueCreateInfo::default() - .queue_family_index(tq) - .queue_priorities(&[1.0]), - ); + queue_create_infos.push(queue_create_info( + tq, + &mut trn_queue_device_queue_global_priority_create_info, + )); } let mut device_create_info = DeviceCreateInfo::default() .push_next(&mut semaphore_features) @@ -433,6 +497,11 @@ impl VulkanInstance { }; unsafe { device.get_device_queue(family_idx, queue_idx) } }); + if high_priority { + log::info!( + "Created queues with priorities {max_graphics_priority:?}/{max_transfer_priority:?}", + ); + } Ok(Rc::new(VulkanDevice { physical_device: phy_dev, render_node, diff --git a/src/portal/ptl_display.rs b/src/portal/ptl_display.rs index bd0d2dd1..f84c8663 100644 --- a/src/portal/ptl_display.rs +++ b/src/portal/ptl_display.rs @@ -185,17 +185,22 @@ impl UsrJayRenderCtxOwner for PortalDisplay { } } if render_ctx.is_none() { - let ctx = - match create_gfx_context(&self.state.eng, &self.state.ring, &drm, GfxApi::OpenGl) { - Ok(c) => c, - Err(e) => { - log::error!( - "Could not create render context from drm device: {}", - ErrorFmt(e) - ); - return; - } - }; + let ctx = match create_gfx_context( + &self.state.eng, + &self.state.ring, + &drm, + GfxApi::OpenGl, + None, + ) { + Ok(c) => c, + Err(e) => { + log::error!( + "Could not create render context from drm device: {}", + ErrorFmt(e) + ); + return; + } + }; let ctx = Rc::new(PortalRenderCtx { _dev_id: dev_id, ctx, diff --git a/src/pr_caps.rs b/src/pr_caps.rs index 1ab65757..d5599f86 100644 --- a/src/pr_caps.rs +++ b/src/pr_caps.rs @@ -6,6 +6,13 @@ use { }, utils::{bitflags::BitflagsExt, errorfmt::ErrorFmt, oserror::OsError}, }, + opera::PhantomNotSend, + parking_lot::{Condvar, Mutex}, + std::{ + mem, + sync::Arc, + thread::{self, JoinHandle}, + }, uapi::{ c::{SYS_capget, SYS_capset, syscall}, map_err, @@ -22,6 +29,24 @@ pub struct PrCompCaps { caps: PrCaps, } +pub struct PrCapsThread { + thread: Option>, + data: Arc, + _no_send: PhantomNotSend, +} + +#[derive(Default)] +struct ThreadData { + cond: Condvar, + mutex: Mutex, +} + +#[derive(Default)] +struct MutData { + exit: bool, + fun: Option>, +} + pub fn pr_caps() -> PrCaps { let mut hdr = cap_user_header_t { version: _LINUX_CAPABILITY_VERSION_3, @@ -103,6 +128,70 @@ impl PrCompCaps { pub fn has_nice(&self) -> bool { self.caps.effective.contains(1 << CAP_SYS_NICE) } + + pub fn into_thread(self) -> PrCapsThread { + let data = Arc::new(ThreadData::default()); + let data2 = data.clone(); + let jh = thread::Builder::new() + .name("SYS_nice thread".to_string()) + .spawn(move || { + let data2 = data2; + let mut lock = data2.mutex.lock(); + loop { + if lock.exit { + return; + } + if let Some(f) = lock.fun.take() { + f(); + } + data2.cond.wait(&mut lock); + } + }) + .expect("Could not spawn SYS_nice thread"); + PrCapsThread { + thread: Some(jh), + data, + _no_send: Default::default(), + } + } +} + +impl PrCapsThread { + pub unsafe fn run(&self, f: F) -> T + where + F: FnOnce() -> T, + { + struct AssertSend(T); + unsafe impl Send for AssertSend {} + struct Data { + cond: Condvar, + mutex: Mutex>>, + } + let data = Arc::new(Data { + cond: Default::default(), + mutex: Default::default(), + }); + let data2 = data.clone(); + let f = AssertSend(f); + let fun = Box::new(move || { + let f = f; + let t = f.0(); + *data2.mutex.lock() = Some(AssertSend(t)); + data2.cond.notify_all(); + }); + let fun = unsafe { + mem::transmute::, Box>(fun) + }; + self.data.mutex.lock().fun = Some(fun); + self.data.cond.notify_all(); + let mut lock = data.mutex.lock(); + loop { + if let Some(t) = lock.take() { + return t.0; + } + data.cond.wait(&mut lock); + } + } } impl Drop for PrCaps { @@ -111,6 +200,14 @@ impl Drop for PrCaps { } } +impl Drop for PrCapsThread { + fn drop(&mut self) { + self.data.mutex.lock().exit = true; + self.data.cond.notify_all(); + let _ = self.thread.take().unwrap().join(); + } +} + mod sys { #![allow(dead_code)] diff --git a/src/state.rs b/src/state.rs index b03cdddc..cfd3f16b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -74,6 +74,7 @@ use { keyboard::KeyboardStateIds, leaks::Tracker, logger::Logger, + pr_caps::PrCapsThread, rect::{Rect, Region}, renderer::Renderer, scale::Scale, @@ -246,6 +247,7 @@ pub struct State { pub show_pin_icon: Cell, pub cl_matcher_manager: ClMatcherManager, pub tl_matcher_manager: TlMatcherManager, + pub caps_thread: Option, } // impl Drop for State { @@ -446,6 +448,7 @@ impl State { &self.ring, drm, api.unwrap_or(self.default_gfx_api.get()), + self.caps_thread.as_ref(), ) } From d1a2483f0a7a9b75654c925d8d82485ee9c3e0c3 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 10 May 2025 22:44:23 +0200 Subject: [PATCH 6/6] docs: add release notes --- docs/setup.md | 27 +++++++++++++++++++++++++++ release-notes.md | 3 +++ 2 files changed, 30 insertions(+) diff --git a/docs/setup.md b/docs/setup.md index b5d91d96..26f5dd88 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -52,6 +52,33 @@ cargo build --release The binary is then available under `./target/release/jay`. +## Running with CAP_SYS_NICE + +Jay supports being started with CAP_SYS_NICE capabilities. For example, such +capabilities can be added to the binary via + +```shell +~# setcap cap_sys_nice=p jay +``` + +If CAP_SYS_NICE is available, Jay will, by default, elevate its scheduler to +SCHED_RR and create Vulkan queues with the highest available priority. This can +improve responsiveness if the CPU or GPU are under high load. + +If Jay is started with the environment variable `JAY_NO_REALTIME=1` or a +`config.so` exists, then Jay will not elevate its scheduler but will still +create elevated Vulkan queues. + +Jay will drop all capabilities almost immediately after being started. Before +that, it will spawn a dedicated thread that retains the CAP_SYS_NICE capability +to create elevated Vulkan queues later. + +If Jay has elevated its scheduler to SCHED_RR, then it will refuse to load +`config.so` configurations. Otherwise unprivileged applications would be able +to run arbitrary code with SCHED_RR by crafting a dedicated `config.so`. This +behavior can be overridden by compiling Jay with +`JAY_ALLOW_REALTIME_CONFIG_SO=1`. + # Setup ## Configuration diff --git a/release-notes.md b/release-notes.md index 86bccd30..3dcafb8c 100644 --- a/release-notes.md +++ b/release-notes.md @@ -31,6 +31,9 @@ [window-and-client-rules.md](./docs/window-and-client-rules.md). - Add client and tree CLI subcommands to inspect clients and windows, primarily to facilitate the writing of window and client rules. +- Jay now supports being started with CAP_SYS_NICE capabilities to improve + responsiveness under high system load. This is described in detail in + [setup.md](docs/setup.md). # 1.10.0 (2025-04-22)