Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion crates/compositor/src/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@
//! positions anything here; placement and rendering belong to later
//! tasks. Behavioral reference: cosmic-comp's layer-shell handler (record
//! on map, configure on map) with original code throughout.
//!
//! Admission (#1048): the global is visible, and bindable, only to the
//! supervised shell's connection — the native connection whose peer is
//! the shell child the runtime spawned and still holds ([`is_shell_peer`],
//! the same pid identity the session lock and control socket use). Every
//! other client's registry omits it and a blind bind by name is a protocol
//! error. GNOME 51 has no client-placed shell layers at all; Mutter hides
//! its privileged globals per client through a global filter
//! (meta-wayland-filter-manager.c `global_filter_func`,
//! meta-wayland-x11-interop.c `x11_interop_filter`), the model followed
//! here. Recorded divergence: Mutter keys admission to a capability on a
//! compositor-created client; ours is the accepted connection's pid
//! until the shell gets an inherited socket (spek 004, ADR 0005).

use smithay::{
delegate_layer_shell,
Expand Down Expand Up @@ -39,6 +52,14 @@ pub use smithay::wayland::shell::wlr_layer::Layer;
/// from the same source to prevent drift.
pub use tuna_shell_control::{BANNER_NAMESPACE, OVERVIEW_NAMESPACE, PANEL_HEIGHT, PANEL_NAMESPACE};

/// Whether a native connection's peer is the supervised shell: its pid is
/// the shell child the runtime holds. An unknown pid on either side (no
/// shell running, between restarts, or unreadable peer credentials) never
/// matches.
pub fn is_shell_peer(peer_pid: Option<u32>, shell_pid: Option<u32>) -> bool {
matches!((peer_pid, shell_pid), (Some(peer), Some(shell)) if peer == shell)
}

/// One layer surface's placement request, as committed.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LayerRequest {
Expand Down Expand Up @@ -572,7 +593,18 @@ impl State {

#[cfg(test)]
mod tests {
use super::{front_to_back, hit_layer_rect, press_takes_keyboard, KeyboardInteractivity};
use super::{
front_to_back, hit_layer_rect, is_shell_peer, press_takes_keyboard, KeyboardInteractivity,
};

#[test]
fn only_the_held_shell_child_is_a_shell_peer() {
assert!(is_shell_peer(Some(4242), Some(4242)));
assert!(!is_shell_peer(Some(4243), Some(4242)), "another process");
assert!(!is_shell_peer(Some(4242), None), "no shell running");
assert!(!is_shell_peer(None, Some(4242)), "unreadable peer");
assert!(!is_shell_peer(None, None));
}

#[test]
fn a_press_never_gives_the_keyboard_to_a_surface_that_wants_none() {
Expand Down
46 changes: 45 additions & 1 deletion crates/compositor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,10 @@ pub(crate) struct ClientState {
/// one client allowed a virtual keyboard (to hand back the keys
/// IBus does not take).
pub(crate) ime_bridge: bool,
/// The supervised shell's own connection: the one client offered
/// `zwlr_layer_shell_v1` (#1048). Set at accept time only when the
/// peer is the shell child the runtime holds (`layer::is_shell_peer`).
pub(crate) supervised_shell: bool,
/// Optional tag applied to windows of a D-Bus-created ordinary client.
pub(crate) connection_window_tag: Option<String>,
/// Optional liveness marker for an ordinary service-channel connection.
Expand Down Expand Up @@ -369,6 +373,27 @@ impl ClientState {
..Self::default()
}
}
/// State for the supervised shell's connection (layer-shell admitted).
pub(crate) fn supervised_shell() -> Self {
Self {
supervised_shell: true,
..Self::default()
}
}
/// Admit an accepted native connection to layer-shell when its peer is
/// the shell child `shell_pid` the runtime still holds. The peer is the
/// pinned original process when the kernel supplied a pidfd (no identity
/// once it exited), else its `SO_PEERCRED` pid, as the session lock's
/// shell check reads it.
pub(crate) fn admit_supervised_shell(&mut self, socket: &UnixStream, shell_pid: Option<u32>) {
let peer = match &self.original_credentials {
Some(credentials) => crate::capture_security::pinned_process_id(credentials),
None => rustix::net::sockopt::socket_peercred(socket)
.ok()
.and_then(|peer| u32::try_from(peer.pid.as_raw_pid()).ok()),
};
self.supervised_shell = crate::layer::is_shell_peer(peer, shell_pid);
}
}

/// Client-initiated window state request (002 window actions).
Expand Down Expand Up @@ -1071,7 +1096,15 @@ impl State {
seat_state,
seat,
dh: dh.clone(),
layer_shell_state: WlrLayerShellState::new::<State>(dh),
// Only the supervised shell may place layer surfaces (#1048):
// GNOME has no client-placed shell layers at all, and Mutter
// hides privileged globals from clients that lack the
// capability (meta-wayland-x11-interop.c `x11_interop_filter`).
layer_shell_state: WlrLayerShellState::new_with_filter::<State, _>(dh, |client| {
client
.get_data::<ClientState>()
.is_some_and(|data| data.supervised_shell)
}),
data_device_state: DataDeviceState::new::<State>(dh),
dnd_icon: None,
cursor_image: smithay::input::pointer::CursorImageStatus::default_named(),
Expand Down Expand Up @@ -1495,6 +1528,17 @@ impl TestCompositor {
.unwrap()
}

/// Attach the supervised shell's connection in native protocol
/// fixtures: the one client offered layer-shell. The runtime grants
/// this only to the shell child's own pid; public clients use
/// `add_client`.
pub fn add_shell_client(&mut self, stream: UnixStream) -> Client {
self.display
.handle()
.insert_client(stream, Arc::new(ClientState::supervised_shell()))
.unwrap()
}

/// Dispatch pending client requests and flush replies. Drives the
/// display without an event loop; tests interleave this with client
/// actions instead of blocking on either side.
Expand Down
4 changes: 3 additions & 1 deletion crates/compositor/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1166,7 +1166,9 @@ impl Runtime {
event_loop
.handle()
.insert_source(socket, |stream, _, runtime: &mut Runtime| {
let client_data = ClientState::native_connection(&stream);
let mut client_data = ClientState::native_connection(&stream);
// Layer-shell is the supervised shell's alone (#1048).
client_data.admit_supervised_shell(&stream, runtime.shell.child_pid());
if let Ok(client) = runtime
.display
.handle()
Expand Down
23 changes: 14 additions & 9 deletions crates/compositor/tests/input_protocols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,20 +427,24 @@ struct Peer {
}

fn connect(comp: &mut TestCompositor) -> Peer {
connect_with_admission(comp, false)
connect_with_admission(comp, TestCompositor::add_client)
}

fn connect_ime_bridge(comp: &mut TestCompositor) -> Peer {
connect_with_admission(comp, true)
connect_with_admission(comp, TestCompositor::add_ime_bridge_client)
}

fn connect_with_admission(comp: &mut TestCompositor, ime_bridge: bool) -> Peer {
/// The supervised shell's connection: the only client offered layer-shell.
fn connect_shell(comp: &mut TestCompositor) -> Peer {
connect_with_admission(comp, TestCompositor::add_shell_client)
}

fn connect_with_admission(
comp: &mut TestCompositor,
admit: fn(&mut TestCompositor, UnixStream) -> wayland_server::Client,
) -> Peer {
let (server, stream) = UnixStream::pair().unwrap();
let server = if ime_bridge {
comp.add_ime_bridge_client(server)
} else {
comp.add_client(server)
};
let server = admit(comp, server);
let conn = Connection::from_socket(stream).unwrap();
let queue = conn.new_event_queue();
let registry = conn.display().get_registry(&queue.handle(), ());
Expand Down Expand Up @@ -829,7 +833,8 @@ fn layer_constraints_use_actual_origin_and_commit_regions_before_activation() {
comp.state.set_output_size(1280, 800);
comp.state.set_output_location("tuna-0", (-400, 100));
let mut manager = comp.window_manager();
let mut app = connect(&mut comp);
// Layer-shell is the supervised shell's alone (#1048).
let mut app = connect_shell(&mut comp);
pump(&mut comp, &mut manager, &mut [&mut app]);
let qh = app.queue.handle();
let surface = app
Expand Down
5 changes: 3 additions & 2 deletions crates/compositor/tests/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,11 @@ fn pump(
panic!("pump budget exhausted");
}

/// Connect one client and bind compositor plus layer shell.
/// Connect the supervised shell (the only client offered layer-shell,
/// #1048) and bind compositor plus layer shell.
fn connect(comp: &mut TestCompositor) -> (Connection, EventQueue<Client>, Client) {
let (server_stream, client_stream) = UnixStream::pair().unwrap();
comp.add_client(server_stream);
comp.add_shell_client(server_stream);
let conn = Connection::from_socket(client_stream).unwrap();
let mut queue = conn.new_event_queue();
let qh = queue.handle();
Expand Down
6 changes: 4 additions & 2 deletions crates/compositor/tests/layer_teardown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ fn destroying_a_layer_role_then_committing_keeps_the_client_connected() {
let mut comp = TestCompositor::new();
let _manager = comp.window_manager();
let (server, stream) = UnixStream::pair().unwrap();
comp.add_client(server);
// gtk4-layer-shell runs in the supervised shell, the only client
// offered layer-shell (#1048).
comp.add_shell_client(server);
let conn = Connection::from_socket(stream).unwrap();
let mut queue = conn.new_event_queue();
let qh = queue.handle();
Expand Down Expand Up @@ -246,7 +248,7 @@ fn destroying_a_layer_role_releases_its_shm_backing() {
let mut comp = TestCompositor::new();
let _manager = comp.window_manager();
let (server, stream) = UnixStream::pair().unwrap();
comp.add_client(server);
comp.add_shell_client(server);
let conn = Connection::from_socket(stream).unwrap();
let mut queue = conn.new_event_queue();
let qh = queue.handle();
Expand Down
14 changes: 12 additions & 2 deletions crates/compositor/tests/popups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,20 @@ fn sync(f: &mut Fixture) {
}

fn fixture() -> Fixture {
fixture_with(TestCompositor::add_client)
}

/// The fixture on the supervised shell's connection: the only client
/// offered layer-shell (#1048), as the GTK shell's panel is.
fn shell_fixture() -> Fixture {
fixture_with(TestCompositor::add_shell_client)
}

fn fixture_with(admit: fn(&mut TestCompositor, UnixStream) -> wayland_server::Client) -> Fixture {
let mut comp = TestCompositor::new();
let manager = comp.window_manager();
let (server, client_stream) = UnixStream::pair().unwrap();
comp.add_client(server);
admit(&mut comp, server);
let conn = Connection::from_socket(client_stream).unwrap();
let mut queue = conn.new_event_queue();
let qh = queue.handle();
Expand Down Expand Up @@ -635,7 +645,7 @@ fn keyboard_focus_returns_to_the_exclusive_layer_after_the_grab() {
// and keyboard focus must return to the Exclusive panel — not to
// the focused window (or nothing), or the bar goes inactive with
// GTK focus intact and the unwind assertion fails.
let mut f = fixture();
let mut f = shell_fixture();
let layer = map_exclusive_layer(&mut f);
assert_eq!(
f.client.keyboard_entered.last().copied(),
Expand Down
83 changes: 77 additions & 6 deletions crates/compositor/tests/protocols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//! The globals list is golden: adding or dropping a protocol is a
//! deliberate change that updates `EXPECTED` and `docs/protocols.md`
//! together. xdg-decoration stays absent on purpose (Mutter parity).
//! `EXPECTED` is what an ordinary client sees; `SHELL_ONLY` holds the
//! globals the supervised shell's connection sees on top of it (#1048).

use std::collections::BTreeMap;
use std::os::unix::net::UnixStream;
Expand Down Expand Up @@ -40,12 +42,14 @@ wayland_client::delegate_noop!(Client: ignore XdgToplevelIconV1);
wayland_client::delegate_noop!(Client: ignore wayland_client::protocol::wl_shm::WlShm);
wayland_client::delegate_noop!(Client: ignore wayland_client::protocol::wl_shm_pool::WlShmPool);
wayland_client::delegate_noop!(Client: ignore wayland_client::protocol::wl_buffer::WlBuffer);
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_shell_v1::ZwlrLayerShellV1;
wayland_client::delegate_noop!(Client: ignore ZwlrLayerShellV1);

const ROUNDS: usize = 200;

/// Every global a client sees on a headless compositor, by version.
/// linux-dmabuf joins once a renderer reports formats (see below), and
/// wl_output once an output is connected.
/// Every global an ordinary client sees on a headless compositor, by
/// version. linux-dmabuf joins once a renderer reports formats (see
/// below), and wl_output once an output is connected.
const EXPECTED: &[(&str, u32)] = &[
("ext_session_lock_manager_v1", 1),
("wl_compositor", 6),
Expand All @@ -68,7 +72,6 @@ const EXPECTED: &[(&str, u32)] = &[
("xdg_toplevel_tag_manager_v1", 1),
("xdg_wm_base", 6),
("xdg_wm_dialog_v1", 1),
("zwlr_layer_shell_v1", 4),
("zwp_idle_inhibit_manager_v1", 1),
("zwp_keyboard_shortcuts_inhibit_manager_v1", 1),
("zwp_pointer_constraints_v1", 1),
Expand All @@ -82,6 +85,10 @@ const EXPECTED: &[(&str, u32)] = &[
("zxdg_output_manager_v1", 3),
];

/// Globals filtered to the supervised shell's connection (#1048): its
/// registry is `EXPECTED` plus these, an ordinary client's lacks them.
const SHELL_ONLY: &[(&str, u32)] = &[("zwlr_layer_shell_v1", 4)];

#[derive(Default)]
struct Client {
globals: BTreeMap<String, (u32, u32)>,
Expand Down Expand Up @@ -215,18 +222,27 @@ wayland_client::delegate_noop!(Client: ignore XdgActivationV1);

struct Peer {
_conn: Connection,
registry: WlRegistry,
queue: EventQueue<Client>,
client: Client,
}

fn connect(comp: &mut TestCompositor) -> Peer {
connect_with(comp, TestCompositor::add_client)
}

fn connect_with(
comp: &mut TestCompositor,
admit: fn(&mut TestCompositor, UnixStream) -> wayland_server::Client,
) -> Peer {
let (server, stream) = UnixStream::pair().unwrap();
comp.add_client(server);
admit(comp, server);
let conn = Connection::from_socket(stream).unwrap();
let queue = conn.new_event_queue();
conn.display().get_registry(&queue.handle(), ());
let registry = conn.display().get_registry(&queue.handle(), ());
Peer {
_conn: conn,
registry,
queue,
client: Client::default(),
}
Expand Down Expand Up @@ -310,6 +326,61 @@ fn advertised_globals_match_the_golden_list() {
);
}

fn seen_globals(peer: &Peer) -> Vec<(String, u32)> {
peer.client
.globals
.iter()
.map(|(k, (_, v))| (k.clone(), *v))
.collect()
}

#[test]
fn layer_shell_is_visible_and_bindable_only_by_the_supervised_shell() {
let mut comp = TestCompositor::new();
let mut manager = comp.window_manager();
let mut app = connect(&mut comp);
let mut shell = connect_with(&mut comp, TestCompositor::add_shell_client);
pump(&mut comp, &mut manager, &mut [&mut app, &mut shell]);

// The shell's registry is the ordinary one plus the filtered globals.
let mut expected: Vec<(String, u32)> = EXPECTED
.iter()
.chain(SHELL_ONLY)
.map(|(k, v)| ((*k).to_owned(), *v))
.collect();
expected.sort();
assert_eq!(seen_globals(&shell), expected);
for (interface, _) in SHELL_ONLY {
assert!(
!app.client.globals.contains_key(*interface),
"{interface} must stay out of an ordinary client's registry"
);
}

// Knowing the global's name (from the shell's registry) must not let
// an ordinary client bind it blind: the bind is a protocol error.
let (name, _) = shell.client.globals["zwlr_layer_shell_v1"];
let _forced: ZwlrLayerShellV1 = app.registry.bind(name, 4, &app.queue.handle(), ());
let mut denied = false;
for _ in 0..ROUNDS {
let _ = app.queue.flush();
comp.pump();
if let Some(guard) = app.queue.prepare_read() {
let _ = guard.read();
}
let _ = app.queue.dispatch_pending(&mut app.client);
if app._conn.backend().last_error().is_some() {
denied = true;
break;
}
}
assert!(denied, "a blind bind of the shell-only global must fail");
// The shell's own connection is untouched by the denial.
let _bound: ZwlrLayerShellV1 = shell.registry.bind(name, 4, &shell.queue.handle(), ());
pump(&mut comp, &mut manager, &mut [&mut shell]);
assert!(shell._conn.backend().last_error().is_none());
}

#[test]
fn dmabuf_is_advertised_only_with_renderer_formats() {
let mut comp = TestCompositor::new();
Expand Down
Loading
Loading