← luna

Commit 8d25664

13
files
+285
added
-109
deleted
@@ -132,6 +132,18 @@ Pliki sesji: `de/session/luna-session` (launcher) i `de/session/selene.desktop`
132 132 Klienta podłączysz ustawiając `WAYLAND_DISPLAY=wayland-0` i `XDG_RUNTIME_DIR` na ten sam
133 133 katalog (renderowanie pojawi się, gdy wejdzie backend wyświetlania).
134 134
135 +## Bezpieczeństwo / Security
136 +
137 +- Klienty Wayland działają z prawami tego samego użytkownika i są **zaufane** — kompozytor
138 + nie sandboxuje aplikacji (jak w każdym Waylandzie). Granica bezpieczeństwa to użytkownik/system.
139 +- Gniazdo Wayland powstaje w `XDG_RUNTIME_DIR` o prawach `0700`; Selene waliduje ten katalog,
140 + a gdy go brak — wybiera bezpieczny fallback (bez setuid, bez gniazd sieciowych).
141 +- Wejście (`/dev/input`, grupa `input`) i master DRM (`/dev/dri`, grupa `video`/root) to
142 + uprawnienia grupowe — świadoma decyzja trybu DRM, nie wymóg trybu nested.
143 +- Odporność: obsługa klienta jest izolowana (`catch_unwind`), blokady odzyskują stan po
144 + „zatruciu” mutexa (`sync::LockExt`), bufor `wl_shm` walidowany (rozmiary, `stride`, bez
145 + SIGBUS/OOM), a kolejki (frame-callbacks, MIME) mają limity.
146 +
135 147 ## Architektura / Architecture
136 148
137 149 ```
@@ -14,6 +14,7 @@ use crate::layer::LayerData;
14 14 use crate::shell::{XdgSurfaceData, XdgToplevelData};
15 15 use crate::shm::BufferData;
16 16 use crate::state::{State, Window};
17 +use crate::sync::LockExt;
17 18
18 19 /// Dane powierzchni (`wl_surface`).
19 20 pub struct SurfaceData {
@@ -109,6 +110,9 @@ impl Dispatch<wl_callback::WlCallback, ()> for State {
109 110 }
110 111 }
111 112
113 +/// Górny limit oczekujących frame-callbacków na powierzchnię (ochrona przed OOM).
114 +const MAX_PENDING_FRAMES: usize = 256;
115 +
112 116 impl Dispatch<wl_surface::WlSurface, SurfaceData> for State {
113 117 fn request(
114 118 state: &mut Self,
@@ -122,15 +126,23 @@ impl Dispatch<wl_surface::WlSurface, SurfaceData> for State {
122 126 match request {
123 127 wl_surface::Request::Frame { callback } => {
124 128 let cb = data_init.init(callback, ());
125 - data.frames.lock().unwrap().push(cb);
129 + let mut frames = data.frames.lock_or_recover();
130 + // Limit oczekujących callbacków — klient bez `commit` nie wysypie pamięci.
131 + if frames.len() >= MAX_PENDING_FRAMES {
132 + let _ = cb.send_event(wl_callback::Event::Done {
133 + callback_data: state.time_ms(),
134 + });
135 + } else {
136 + frames.push(cb);
137 + }
126 138 }
127 139 wl_surface::Request::Attach { buffer, .. } => {
128 - *data.buffer.lock().unwrap() = buffer;
140 + *data.buffer.lock_or_recover() = buffer;
129 141 }
130 142 wl_surface::Request::Commit => {
131 143 // Wywołaj oczekujące frame-callbacki.
132 144 let now = state.time_ms();
133 - for cb in data.frames.lock().unwrap().drain(..) {
145 + for cb in data.frames.lock_or_recover().drain(..) {
134 146 let _ = cb.send_event(wl_callback::Event::Done { callback_data: now });
135 147 }
136 148 // Pierwszy commit powierzchni z `xdg_surface` → wyślij konfigurację.
@@ -146,24 +158,24 @@ impl Dispatch<wl_surface::WlSurface, SurfaceData> for State {
146 158 /// Skomponuj powierzchnię do modelu okien (toplevel, popup lub powierzchnia warstwowa).
147 159 fn composite(state: &mut State, surface: &wl_surface::WlSurface, data: &SurfaceData) {
148 160 // Powierzchnia warstwowa (panel/dock) — własna ścieżka (bez xdg_surface).
149 - if let Some(ls) = data.layer.lock().unwrap().clone() {
161 + if let Some(ls) = data.layer.lock_or_recover().clone() {
150 162 composite_layer(state, surface, data, &ls);
151 163 return;
152 164 }
153 - let Some(xdg_surface) = data.xdg_surface.lock().unwrap().clone() else {
165 + let Some(xdg_surface) = data.xdg_surface.lock_or_recover().clone() else {
154 166 return;
155 167 };
156 168 let (is_toplevel, is_popup) = match xdg_surface.data::<XdgSurfaceData>() {
157 169 Some(d) => (
158 - d.toplevel.lock().unwrap().is_some(),
159 - *d.is_popup.lock().unwrap(),
170 + d.toplevel.lock_or_recover().is_some(),
171 + *d.is_popup.lock_or_recover(),
160 172 ),
161 173 None => (false, false),
162 174 };
163 175 if !is_toplevel && !is_popup {
164 176 return;
165 177 }
166 - let Some(buffer) = data.buffer.lock().unwrap().clone() else {
178 + let Some(buffer) = data.buffer.lock_or_recover().clone() else {
167 179 return;
168 180 };
169 181 let Some(buf_data) = buffer.data::<BufferData>() else {
@@ -185,9 +197,9 @@ fn composite(state: &mut State, surface: &wl_surface::WlSurface, data: &SurfaceD
185 197 xdg_surface
186 198 .data::<XdgSurfaceData>()
187 199 .and_then(|d| {
188 - d.toplevel.lock().unwrap().as_ref().and_then(|tl| {
200 + d.toplevel.lock_or_recover().as_ref().and_then(|tl| {
189 201 tl.data::<XdgToplevelData>()
190 - .map(|td| td.title.lock().unwrap().clone())
202 + .map(|td| td.title.lock_or_recover().clone())
191 203 })
192 204 })
193 205 .unwrap_or_default()
@@ -195,7 +207,7 @@ fn composite(state: &mut State, surface: &wl_surface::WlSurface, data: &SurfaceD
195 207 String::new()
196 208 };
197 209
198 - let count = state.windows.lock().unwrap().len();
210 + let count = state.windows.lock_or_recover().len();
199 211 let (px, py) = popup_xy.unwrap_or((60.0 + count as f64 * 36.0, 60.0 + count as f64 * 36.0));
200 212 let (_, created) = upsert_window(
201 213 state, surface, data, px, py, w, h, z, &title, is_popup, false, false, image,
@@ -220,10 +232,10 @@ fn composite_layer(
220 232 };
221 233 let out_idx = ld.output.unwrap_or(0);
222 234 let (ox, oy, out_w, out_h) = output_rect(state, out_idx);
223 - let buffer = data.buffer.lock().unwrap().clone();
235 + let buffer = data.buffer.lock_or_recover().clone();
224 236 // Pierwszy `commit` (zwykle bez bufora) → wyślij `configure` z rozmiarem.
225 - if !*ld.configured.lock().unwrap() {
226 - let st = *ld.state.lock().unwrap();
237 + if !*ld.configured.lock_or_recover() {
238 + let st = *ld.state.lock_or_recover();
227 239 let (cw, ch) = st.configure_size(out_w, out_h);
228 240 let serial = state.next_serial();
229 241 let _ = ls.send_event(zwlr_layer_surface_v1::Event::Configure {
@@ -231,7 +243,7 @@ fn composite_layer(
231 243 width: cw.max(0) as u32,
232 244 height: ch.max(0) as u32,
233 245 });
234 - *ld.configured.lock().unwrap() = true;
246 + *ld.configured.lock_or_recover() = true;
235 247 if buffer.is_none() {
236 248 return;
237 249 }
@@ -247,7 +259,7 @@ fn composite_layer(
247 259 return;
248 260 };
249 261 let image = std::sync::Arc::new(ImageData::new(w as u32, h as u32, rgba));
250 - let st = *ld.state.lock().unwrap();
262 + let st = *ld.state.lock_or_recover();
251 263 let (rx, ry) = st.position(out_w, out_h, w as f64, h as f64);
252 264 let z = crate::layer::layer_z(st.layer);
253 265 upsert_window(
@@ -285,8 +297,8 @@ fn upsert_window(
285 297 reposition: bool,
286 298 image: std::sync::Arc<ImageData>,
287 299 ) -> (usize, bool) {
288 - let mut slot = data.slot.lock().unwrap();
289 - let mut windows = state.windows.lock().unwrap();
300 + let mut slot = data.slot.lock_or_recover();
301 + let mut windows = state.windows.lock_or_recover();
290 302 let mut created = false;
291 303 let idx = match *slot {
292 304 Some(i) => {
@@ -343,7 +355,7 @@ fn upsert_window(
343 355
344 356 /// Geometria monitora `idx` (x, y, szerokość, wysokość); `idx` poza zakresem → pierwszy.
345 357 fn output_rect(state: &State, idx: usize) -> (f64, f64, f64, f64) {
346 - let outs = state.outputs.lock().unwrap();
358 + let outs = state.outputs.lock_or_recover();
347 359 let o = outs.get(idx).or_else(|| outs.first());
348 360 match o {
349 361 Some(o) => (o.x as f64, o.y as f64, o.width as f64, o.height as f64),
@@ -356,16 +368,18 @@ fn popup_position(state: &State, xdg_surface: &xdg_surface::XdgSurface) -> (f64,
356 368 let Some(d) = xdg_surface.data::<XdgSurfaceData>() else {
357 369 return (0.0, 0.0);
358 370 };
359 - let geom = *d.geom.lock().unwrap();
371 + let geom = *d.geom.lock_or_recover();
360 372 let parent_origin = d
361 373 .parent
362 - .lock()
363 - .unwrap()
374 + .lock_or_recover()
364 375 .clone()
365 - .and_then(|ps| ps.data::<SurfaceData>().map(|sd| *sd.slot.lock().unwrap()))
376 + .and_then(|ps| {
377 + ps.data::<SurfaceData>()
378 + .map(|sd| *sd.slot.lock_or_recover())
379 + })
366 380 .flatten()
367 381 .and_then(|slot| {
368 - state.windows.lock().unwrap().get(slot).map(|w| {
382 + state.windows.lock_or_recover().get(slot).map(|w| {
369 383 let c = w.content();
370 384 (c.x(), c.y())
371 385 })
@@ -387,7 +401,7 @@ fn send_popup_configure(
387 401 ) {
388 402 let serial = state.next_serial();
389 403 if let Some(d) = xdg_surface.data::<XdgSurfaceData>() {
390 - if let Some(popup) = d.popup.lock().unwrap().as_ref() {
404 + if let Some(popup) = d.popup.lock_or_recover().as_ref() {
391 405 let _ = popup.send_event(xdg_popup::Event::Configure {
392 406 x: x as i32,
393 407 y: y as i32,
@@ -395,30 +409,30 @@ fn send_popup_configure(
395 409 height: h,
396 410 });
397 411 }
398 - if !*d.configured.lock().unwrap() {
412 + if !*d.configured.lock_or_recover() {
399 413 let _ = xdg_surface.send_event(xdg_surface::Event::Configure { serial });
400 - *d.configured.lock().unwrap() = true;
414 + *d.configured.lock_or_recover() = true;
401 415 }
402 416 }
403 417 }
404 418
405 419 /// Jeśli toplevel nie ma jeszcze konfiguracji, wyślij `configure` (popupy mają własną).
406 420 fn maybe_configure_xdg(state: &mut State, data: &SurfaceData) {
407 - let Some(xdg_surface) = data.xdg_surface.lock().unwrap().clone() else {
421 + let Some(xdg_surface) = data.xdg_surface.lock_or_recover().clone() else {
408 422 return;
409 423 };
410 424 let Some(xdg_data) = xdg_surface.data::<XdgSurfaceData>() else {
411 425 return;
412 426 };
413 - if *xdg_data.is_popup.lock().unwrap() {
427 + if *xdg_data.is_popup.lock_or_recover() {
414 428 return;
415 429 }
416 - if *xdg_data.configured.lock().unwrap() {
430 + if *xdg_data.configured.lock_or_recover() {
417 431 return;
418 432 }
419 433 let serial = state.next_serial();
420 434 let _ = xdg_surface.send_event(xdg_surface::Event::Configure { serial });
421 - if let Some(toplevel) = xdg_data.toplevel.lock().unwrap().as_ref() {
435 + if let Some(toplevel) = xdg_data.toplevel.lock_or_recover().as_ref() {
422 436 let _ = toplevel.send_event(xdg_toplevel::Event::Configure {
423 437 width: 1024,
424 438 height: 720,
@@ -428,5 +442,5 @@ fn maybe_configure_xdg(state: &mut State, data: &SurfaceData) {
428 442 capabilities: vec![],
429 443 });
430 444 }
431 - *xdg_data.configured.lock().unwrap() = true;
445 + *xdg_data.configured.lock_or_recover() = true;
432 446 }
@@ -26,6 +26,10 @@ use wayland_server::protocol::wl_data_source::{
26 26 use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
27 27
28 28 use crate::state::State;
29 +use crate::sync::LockExt;
30 +
31 +/// Górny limit typów MIME oferowanych przez jedno źródło danych (ochrona przed OOM).
32 +const MAX_MIME_TYPES: usize = 64;
29 33
30 34 /// Dane `wl_data_source`: oferowane typy MIME.
31 35 #[derive(Default)]
@@ -90,7 +94,11 @@ impl Dispatch<WlDataSource, DataSourceData> for State {
90 94 ) {
91 95 match request {
92 96 SourceRequest::Offer { mime_type } => {
93 - data.mimes.lock().unwrap().push(mime_type);
97 + let mut mimes = data.mimes.lock_or_recover();
98 + // Limit liczby oferowanych typów MIME (ochrona przed OOM).
99 + if mimes.len() < MAX_MIME_TYPES {
100 + mimes.push(mime_type);
101 + }
94 102 }
95 103 SourceRequest::Destroy => {
96 104 // Jeśli to była bieżąca selekcja — wyczyść.
@@ -166,7 +174,7 @@ impl Dispatch<WlDataOffer, DataOfferData> for State {
166 174 match request {
167 175 OfferRequest::Receive { mime_type, fd } => {
168 176 // Przekaż żądanie danych do źródła.
169 - if let Some(src) = data.source.lock().unwrap().clone() {
177 + if let Some(src) = data.source.lock_or_recover().clone() {
170 178 let _ = src.send_event(SourceEvent::Send {
171 179 mime_type,
172 180 fd: fd.as_fd(),
@@ -291,7 +299,7 @@ fn enter_target(state: &mut State, handle: &DisplayHandle, slot: usize, pos: (f6
291 299 let mimes = source
292 300 .as_ref()
293 301 .and_then(|s| s.data::<DataSourceData>())
294 - .map(|d| d.mimes.lock().unwrap().clone())
302 + .map(|d| d.mimes.lock_or_recover().clone())
295 303 .unwrap_or_default();
296 304 let offer_data = DataOfferData {
297 305 source: Mutex::new(source),
@@ -321,7 +329,7 @@ fn enter_target(state: &mut State, handle: &DisplayHandle, slot: usize, pos: (f6
321 329
322 330 /// Współrzędne powierzchniowe (lokalne dla treści okna `slot`).
323 331 fn local_coords(state: &State, slot: usize, pos: (f64, f64)) -> (f64, f64) {
324 - match state.windows.lock().unwrap().get(slot) {
332 + match state.windows.lock_or_recover().get(slot) {
325 333 Some(w) => {
326 334 let c = w.content();
327 335 (pos.0 - c.x(), pos.1 - c.y())
@@ -349,7 +357,7 @@ fn broadcast_selection(
349 357 Some(src) => {
350 358 let mimes = src
351 359 .data::<DataSourceData>()
352 - .map(|d| d.mimes.lock().unwrap().clone())
360 + .map(|d| d.mimes.lock_or_recover().clone())
353 361 .unwrap_or_default();
354 362 let offer_data = DataOfferData {
355 363 source: Mutex::new(Some(src.clone())),
@@ -13,6 +13,7 @@ use wayland_server::{
13 13
14 14 use crate::shell::XdgToplevelData;
15 15 use crate::state::State;
16 +use crate::sync::LockExt;
16 17
17 18 /// Uchwyt obiektu dekoracji okna.
18 19 pub type ToplevelDecoration = toplevel_decoration::ZxdgToplevelDecorationV1;
@@ -43,7 +44,7 @@ impl Dispatch<decoration_manager::ZxdgDecorationManagerV1, ()> for State {
43 44 if let decoration_manager::Request::GetToplevelDecoration { id, toplevel } = request {
44 45 let deco = data_init.init(id, ());
45 46 if let Some(td) = toplevel.data::<XdgToplevelData>() {
46 - *td.decoration.lock().unwrap() = Some(deco);
47 + *td.decoration.lock_or_recover() = Some(deco);
47 48 }
48 49 }
49 50 }
@@ -10,6 +10,8 @@ use std::sync::{Arc, Mutex};
10 10
11 11 use libluna_core::Event;
12 12
13 +use crate::sync::LockExt;
14 +
13 15 /// Współdzielona kolejka zdarzeń wejścia.
14 16 pub type Events = Arc<Mutex<Vec<Event>>>;
15 17
@@ -24,7 +26,7 @@ pub fn push(events: &Events, wake: &mut UnixStream, batch: &[Event]) {
24 26 if batch.is_empty() {
25 27 return;
26 28 }
27 - events.lock().unwrap().extend_from_slice(batch);
29 + events.lock_or_recover().extend_from_slice(batch);
28 30 let _ = wake.write_all(&[1u8]);
29 31 }
30 32
@@ -15,6 +15,7 @@ use wayland_server::{
15 15
16 16 use crate::compositor::SurfaceData;
17 17 use crate::state::State;
18 +use crate::sync::LockExt;
18 19
19 20 /// Kotwice krawędzi (`zwlr_layer_surface_v1.anchor`).
20 21 pub const ANCHOR_TOP: u32 = 1;
@@ -173,7 +174,7 @@ impl Dispatch<layer_shell::ZwlrLayerShellV1, ()> for State {
173 174 let ls = data_init.init(id, LayerData::new(surface.clone(), layer, output));
174 175 // Zapamiętaj powiązanie po stronie `wl_surface` (potrzebne przy `commit`).
175 176 if let Some(sd) = surface.data::<SurfaceData>() {
176 - *sd.layer.lock().unwrap() = Some(ls);
177 + *sd.layer.lock_or_recover() = Some(ls);
177 178 }
178 179 }
179 180 }
@@ -189,7 +190,7 @@ impl Dispatch<layer_surface::ZwlrLayerSurfaceV1, LayerData> for State {
189 190 _handle: &DisplayHandle,
190 191 _data_init: &mut DataInit<'_, Self>,
191 192 ) {
192 - let mut st = data.state.lock().unwrap();
193 + let mut st = data.state.lock_or_recover();
193 194 match request {
194 195 layer_surface::Request::SetSize { width, height } => {
195 196 st.width = width as i32;
@@ -230,8 +231,8 @@ impl Dispatch<layer_surface::ZwlrLayerSurfaceV1, LayerData> for State {
230 231 // Zdejmij powierzchnię z widoku.
231 232 drop(st);
232 233 if let Some(sd) = data.surface.data::<SurfaceData>() {
233 - if let Some(slot) = *sd.slot.lock().unwrap() {
234 - if let Some(w) = state.windows.lock().unwrap().get_mut(slot) {
234 + if let Some(slot) = *sd.slot.lock_or_recover() {
235 + if let Some(w) = state.windows.lock_or_recover().get_mut(slot) {
235 236 w.mapped = false;
236 237 }
237 238 }
@@ -18,10 +18,12 @@ mod seat;
18 18 mod shell;
19 19 mod shm;
20 20 mod state;
21 +mod sync;
21 22
22 23 use std::error::Error;
23 24 use std::os::fd::{AsRawFd, BorrowedFd};
24 25 use std::os::unix::fs::MetadataExt;
26 +use std::panic::{catch_unwind, AssertUnwindSafe};
25 27 use std::path::{Path, PathBuf};
26 28 use std::sync::Arc;
27 29
@@ -32,6 +34,7 @@ use wayland_protocols_wlr::layer_shell::v1::server::zwlr_layer_shell_v1;
32 34 use wayland_server::protocol::{wl_compositor, wl_data_device_manager, wl_output, wl_seat, wl_shm};
33 35 use wayland_server::{Display, ListeningSocket};
34 36
37 +use crate::sync::LockExt;
35 38 use state::State;
36 39
37 40 fn main() -> Result<(), Box<dyn Error>> {
@@ -39,6 +42,10 @@ fn main() -> Result<(), Box<dyn Error>> {
39 42 libluna_i18n::set_locale(libluna_i18n::Locale::from_env());
40 43 libluna_i18n::load_catalogs_from_dirs();
41 44
45 + // Krótszy, jednoznaczny log panik — bez zasypywania sesji pełnym backtrace'em.
46 + // Paniki klientów są łapane niżej (`catch_unwind`), więc sesja żyje dalej.
47 + std::panic::set_hook(Box::new(|info| eprintln!("Selene panic: {info}")));
48 +
42 49 let mut display: Display<State> = Display::new()?;
43 50 let mut handle = display.handle();
44 51
@@ -64,10 +71,10 @@ fn main() -> Result<(), Box<dyn Error>> {
64 71 .collect::<Vec<_>>()
65 72 .join(", ")
66 73 );
67 - *state.outputs.lock().unwrap() = outs;
74 + *state.outputs.lock_or_recover() = outs;
68 75 }
69 76 }
70 - let output_count = state.outputs.lock().unwrap().len();
77 + let output_count = state.outputs.lock_or_recover().len();
71 78
72 79 // Globalne interfejsy protokołu.
73 80 handle.create_global::<State, wl_compositor::WlCompositor, ()>(4, ());
@@ -162,8 +169,7 @@ fn main() -> Result<(), Box<dyn Error>> {
162 169 if let Some(input) = evdev_input.as_mut() {
163 170 let bounds = state
164 171 .outputs
165 - .lock()
166 - .unwrap()
172 + .lock_or_recover()
167 173 .first()
168 174 .map(|o| (o.width as f64, o.height as f64))
169 175 .unwrap_or((1280.0, 800.0));
@@ -177,8 +183,10 @@ fn main() -> Result<(), Box<dyn Error>> {
177 183 // Nowy klient.
178 184 if fds[0].revents().contains(rustix::event::PollFlags::IN) {
179 185 while let Ok(Some(stream)) = socket.accept() {
180 - handle.insert_client(stream, Arc::new(()))?;
181 - state.clients += 1;
186 + match handle.insert_client(stream, Arc::new(())) {
187 + Ok(_) => state.clients += 1,
188 + Err(e) => eprintln!("client rejected: {e}"),
189 + }
182 190 }
183 191 }
184 192
@@ -186,7 +194,7 @@ fn main() -> Result<(), Box<dyn Error>> {
186 194 if fds[2].revents().contains(rustix::event::PollFlags::IN) {
187 195 input::drain_wake(&mut wake_read);
188 196 let batch: Vec<Event> = {
189 - let mut queue = events.lock().unwrap();
197 + let mut queue = events.lock_or_recover();
190 198 std::mem::take(&mut *queue)
191 199 };
192 200 for event in &batch {
@@ -194,8 +202,18 @@ fn main() -> Result<(), Box<dyn Error>> {
194 202 }
195 203 }
196 204
197 - display.dispatch_clients(&mut state)?;
198 - display.flush_clients()?;
205 + // Odporność: panika albo błąd w obsłudze pojedynczego klienta nie może zabić
206 + // całej sesji — łapiemy panikę i kontynuujemy pętlę.
207 + match catch_unwind(AssertUnwindSafe(|| display.dispatch_clients(&mut state))) {
208 + Ok(Ok(_)) => {}
209 + Ok(Err(e)) => return Err(e.into()),
210 + Err(_) => eprintln!("{}", libluna_i18n::tr("client request panicked; ignoring")),
211 + }
212 + match catch_unwind(AssertUnwindSafe(|| display.flush_clients())) {
213 + Ok(Ok(())) => {}
214 + Ok(Err(e)) => return Err(e.into()),
215 + Err(_) => eprintln!("{}", libluna_i18n::tr("client request panicked; ignoring")),
216 + }
199 217 }
200 218 }
201 219
@@ -10,6 +10,7 @@ use wayland_server::protocol::wl_output::{self, WlOutput};
10 10 use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
11 11
12 12 use crate::state::State;
13 +use crate::sync::LockExt;
13 14
14 15 /// Informacje o monitorze (współdzielone z wątkiem wyjścia, który zna bieżący rozmiar).
15 16 #[derive(Debug, Clone)]
@@ -106,8 +107,7 @@ impl GlobalDispatch<WlOutput, usize> for State {
106 107 let output = data_init.init(resource, *data);
107 108 let info = state
108 109 .outputs
109 - .lock()
110 - .unwrap()
110 + .lock_or_recover()
111 111 .get(*data)
112 112 .cloned()
113 113 .unwrap_or_default();
@@ -16,6 +16,7 @@ use crate::drm::DrmBackend;
16 16 use crate::input::{self, Events};
17 17 use crate::monitors::Outputs;
18 18 use crate::state::{Window, WindowModel};
19 +use crate::sync::LockExt;
19 20
20 21 /// Uruchamia wątek wyjścia (nested Wayland) o zadanym rozmiarze.
21 22 pub fn spawn(
@@ -35,7 +36,7 @@ pub fn spawn(
35 36 let _ = run(config, move |frame: &mut Frame| {
36 37 // Zgłoś bieżący rozmiar monitora (okno może być zmieniane przez rodzica).
37 38 {
38 - let mut outs = outputs.lock().unwrap();
39 + let mut outs = outputs.lock_or_recover();
39 40 if let Some(out) = outs.first_mut() {
40 41 out.width = frame.size.width as i32;
41 42 out.height = frame.size.height as i32;
@@ -114,7 +115,7 @@ fn build_scene(scene: &mut Scene, size: Size, ox: f64, oy: f64, model: &WindowMo
114 115 );
115 116
116 117 {
117 - let windows = model.lock().unwrap();
118 + let windows = model.lock_or_recover();
118 119 let mut order: Vec<&Window> = windows.iter().collect();
119 120 order.sort_by_key(|w| w.z);
120 121 for w in order {
@@ -19,6 +19,7 @@ use crate::compositor::SurfaceData;
19 19 use crate::data;
20 20 use crate::shell::XdgSurfaceData;
21 21 use crate::state::{DragState, ResizeState, SavedGeom, State, TITLEBAR};
22 +use crate::sync::LockExt;
22 23
23 24 /// Buduje tekst mapy klawiszy XKB dla danego układu (i opcjonalnego wariantu).
24 25 ///
@@ -179,7 +180,7 @@ pub fn handle_input(state: &mut State, handle: &DisplayHandle, event: &Event) {
179 180
180 181 /// Trafienie w okno (najwyższe wg `z`) i w którą jego część.
181 182 fn hit(state: &State, pos: Point) -> Option<(usize, Part)> {
182 - let windows = state.windows.lock().unwrap();
183 + let windows = state.windows.lock_or_recover();
183 184 let mut best: Option<(usize, i32)> = None;
184 185 for (i, w) in windows.iter().enumerate() {
185 186 if w.mapped && !w.minimized && w.outer().contains(pos) {
@@ -212,7 +213,7 @@ fn hit(state: &State, pos: Point) -> Option<(usize, Part)> {
212 213
213 214 /// Najwyższe okno, którego **treść** zawiera punkt (do zdarzeń klienta).
214 215 fn content_window(state: &State, pos: Point) -> Option<usize> {
215 - let windows = state.windows.lock().unwrap();
216 + let windows = state.windows.lock_or_recover();
216 217 let mut best: Option<(usize, i32)> = None;
217 218 for (i, w) in windows.iter().enumerate() {
218 219 if w.mapped && !w.minimized && w.content().contains(pos) {
@@ -229,7 +230,7 @@ fn content_window(state: &State, pos: Point) -> Option<usize> {
229 230 fn dismiss_popups(state: &mut State, pos: Point) {
230 231 loop {
231 232 let top = {
232 - let windows = state.windows.lock().unwrap();
233 + let windows = state.windows.lock_or_recover();
233 234 let mut best: Option<(usize, i32)> = None;
234 235 for (i, w) in windows.iter().enumerate() {
235 236 if w.popup && w.mapped {
@@ -244,8 +245,7 @@ fn dismiss_popups(state: &mut State, pos: Point) {
244 245 let Some(slot) = top else { break };
245 246 let inside = state
246 247 .windows
247 - .lock()
248 - .unwrap()
248 + .lock_or_recover()
249 249 .get(slot)
250 250 .map(|w| w.outer().contains(pos))
251 251 .unwrap_or(false);
@@ -254,16 +254,16 @@ fn dismiss_popups(state: &mut State, pos: Point) {
254 254 }
255 255 if let Some(surface) = state.slots.get(slot).cloned() {
256 256 if let Some(sd) = surface.data::<SurfaceData>() {
257 - if let Some(xs) = sd.xdg_surface.lock().unwrap().clone() {
257 + if let Some(xs) = sd.xdg_surface.lock_or_recover().clone() {
258 258 if let Some(d) = xs.data::<XdgSurfaceData>() {
259 - if let Some(popup) = d.popup.lock().unwrap().as_ref() {
259 + if let Some(popup) = d.popup.lock_or_recover().as_ref() {
260 260 let _ = popup.send_event(xdg_popup::Event::PopupDone);
261 261 }
262 262 }
263 263 }
264 264 }
265 265 }
266 - if let Some(w) = state.windows.lock().unwrap().get_mut(slot) {
266 + if let Some(w) = state.windows.lock_or_recover().get_mut(slot) {
267 267 w.mapped = false;
268 268 }
269 269 }
@@ -275,7 +275,7 @@ pub(crate) fn window_at(state: &State, pos: (f64, f64)) -> Option<usize> {
275 275 }
276 276
277 277 fn window_geometry(state: &State, slot: usize) -> Option<(f64, f64, f64, f64)> {
278 - let windows = state.windows.lock().unwrap();
278 + let windows = state.windows.lock_or_recover();
279 279 windows
280 280 .get(slot)
281 281 .map(|w| (w.x, w.y, w.width as f64, w.height as f64))
@@ -295,7 +295,7 @@ fn pointer_motion(state: &mut State, handle: &DisplayHandle, pos: Point) {
295 295 return;
296 296 }
297 297 if let Some(drag) = state.drag {
298 - let mut windows = state.windows.lock().unwrap();
298 + let mut windows = state.windows.lock_or_recover();
299 299 if let Some(w) = windows.get_mut(drag.slot) {
300 300 w.x = pos.x - drag.off_x;
301 301 w.y = pos.y - drag.off_y;
@@ -502,7 +502,7 @@ fn do_resize(state: &mut State, pos: Point) {
502 502 h = (rs.start_h + dy).max(MIN_H);
503 503 }
504 504 {
505 - let mut windows = state.windows.lock().unwrap();
505 + let mut windows = state.windows.lock_or_recover();
506 506 if let Some(w2) = windows.get_mut(rs.slot) {
507 507 w2.x = x;
508 508 w2.y = y;
@@ -521,14 +521,14 @@ fn send_configure(state: &mut State, slot: usize, w: f64, h: f64) {
521 521 let Some(sd) = surface.data::<SurfaceData>() else {
522 522 return;
523 523 };
524 - let Some(xs) = sd.xdg_surface.lock().unwrap().clone() else {
524 + let Some(xs) = sd.xdg_surface.lock_or_recover().clone() else {
525 525 return;
526 526 };
527 527 let states = window_states(state, slot);
528 528 let serial = state.next_serial();
529 529 let _ = xs.send_event(xdg_surface::Event::Configure { serial });
530 530 if let Some(xd) = xs.data::<XdgSurfaceData>() {
531 - if let Some(tl) = xd.toplevel.lock().unwrap().as_ref() {
531 + if let Some(tl) = xd.toplevel.lock_or_recover().as_ref() {
532 532 let _ = tl.send_event(xdg_toplevel::Event::Configure {
533 533 width: w as i32,
534 534 height: h as i32,
@@ -541,7 +541,7 @@ fn send_configure(state: &mut State, slot: usize, w: f64, h: f64) {
541 541 /// Stany `xdg_toplevel` dla okna (ACTIVATED + ewentualnie MAXIMIZED/FULLSCREEN).
542 542 fn window_states(state: &State, slot: usize) -> Vec<u8> {
543 543 let mut states = vec![4u8]; // ACTIVATED
544 - if let Some(w) = state.windows.lock().unwrap().get(slot) {
544 + if let Some(w) = state.windows.lock_or_recover().get(slot) {
545 545 if w.maximized {
546 546 states.push(1); // MAXIMIZED
547 547 }
@@ -556,15 +556,14 @@ fn window_states(state: &State, slot: usize) -> Vec<u8> {
556 556 fn window_output_rect(state: &State, slot: usize) -> (f64, f64, f64, f64) {
557 557 let (cx, cy) = state
558 558 .windows
559 - .lock()
560 - .unwrap()
559 + .lock_or_recover()
561 560 .get(slot)
562 561 .map(|w| {
563 562 let c = w.content().center();
564 563 (c.x, c.y)
565 564 })
566 565 .unwrap_or((0.0, 0.0));
567 - let outs = state.outputs.lock().unwrap();
566 + let outs = state.outputs.lock_or_recover();
568 567 let idx = crate::monitors::output_at(&outs, cx, cy);
569 568 match outs.get(idx).or_else(|| outs.first()) {
570 569 Some(o) => (o.x as f64, o.y as f64, o.width as f64, o.height as f64),
@@ -575,7 +574,7 @@ fn window_output_rect(state: &State, slot: usize) -> (f64, f64, f64, f64) {
575 574 /// Przełącza minimalizację (ukrycie przez kompozytor).
576 575 fn toggle_minimize(state: &mut State, slot: usize) {
577 576 let minimized = {
578 - let mut windows = state.windows.lock().unwrap();
577 + let mut windows = state.windows.lock_or_recover();
579 578 match windows.get_mut(slot) {
580 579 Some(w) if !w.popup && !w.layer => {
581 580 w.minimized = !w.minimized;
@@ -593,7 +592,7 @@ fn toggle_minimize(state: &mut State, slot: usize) {
593 592 fn toggle_maximize(state: &mut State, slot: usize) {
594 593 let (ox, oy, ow, oh) = window_output_rect(state, slot);
595 594 {
596 - let mut windows = state.windows.lock().unwrap();
595 + let mut windows = state.windows.lock_or_recover();
597 596 let Some(w) = windows.get_mut(slot) else {
598 597 return;
599 598 };
@@ -636,7 +635,7 @@ fn toggle_maximize(state: &mut State, slot: usize) {
636 635 fn toggle_fullscreen(state: &mut State, slot: usize) {
637 636 let (ox, oy, ow, oh) = window_output_rect(state, slot);
638 637 {
639 - let mut windows = state.windows.lock().unwrap();
638 + let mut windows = state.windows.lock_or_recover();
640 639 let Some(w) = windows.get_mut(slot) else {
641 640 return;
642 641 };
@@ -698,11 +697,11 @@ fn send_close(state: &mut State, slot: usize) {
698 697 let Some(sd) = surface.data::<SurfaceData>() else {
699 698 return;
700 699 };
701 - let Some(xs) = sd.xdg_surface.lock().unwrap().clone() else {
700 + let Some(xs) = sd.xdg_surface.lock_or_recover().clone() else {
702 701 return;
703 702 };
704 703 if let Some(xd) = xs.data::<XdgSurfaceData>() {
705 - if let Some(tl) = xd.toplevel.lock().unwrap().as_ref() {
704 + if let Some(tl) = xd.toplevel.lock_or_recover().as_ref() {
706 705 let _ = tl.send_event(xdg_toplevel::Event::Close);
707 706 }
708 707 }
@@ -711,7 +710,7 @@ fn send_close(state: &mut State, slot: usize) {
711 710 fn raise_and_focus(state: &mut State, slot: usize) {
712 711 let z = state.next_z();
713 712 {
714 - let mut windows = state.windows.lock().unwrap();
713 + let mut windows = state.windows.lock_or_recover();
715 714 if let Some(w) = windows.get_mut(slot) {
716 715 // Powierzchnie warstwowe (panele/docki) trzymają stałą kolejność.
717 716 if !w.layer {
@@ -823,7 +822,7 @@ fn window_shortcut(state: &mut State, keysym: u32, st: ButtonState) -> bool {
823 822 /// Przywraca okno z minimalizacji/maksymalizacji/fullscreenu.
824 823 fn restore_window(state: &mut State, slot: usize) {
825 824 let (min, max, full) = {
826 - let windows = state.windows.lock().unwrap();
825 + let windows = state.windows.lock_or_recover();
827 826 match windows.get(slot) {
828 827 Some(w) => (w.minimized, w.maximized, w.fullscreen),
829 828 None => return,
@@ -841,7 +840,7 @@ fn restore_window(state: &mut State, slot: usize) {
841 840 /// Przełącza fokus na kolejne okno (przywracając je, jeśli było zminimalizowane).
842 841 fn cycle_windows(state: &mut State) {
843 842 let slots: Vec<usize> = {
844 - let windows = state.windows.lock().unwrap();
843 + let windows = state.windows.lock_or_recover();
845 844 windows
846 845 .iter()
847 846 .enumerate()
@@ -857,7 +856,7 @@ fn cycle_windows(state: &mut State) {
857 856 Some(p) => slots[(p + 1) % slots.len()],
858 857 None => slots[0],
859 858 };
860 - if let Some(w) = state.windows.lock().unwrap().get_mut(next) {
859 + if let Some(w) = state.windows.lock_or_recover().get_mut(next) {
861 860 w.minimized = false;
862 861 }
863 862 raise_and_focus(state, next);
@@ -12,6 +12,7 @@ use wayland_server::{
12 12
13 13 use crate::compositor::SurfaceData;
14 14 use crate::state::State;
15 +use crate::sync::LockExt;
15 16
16 17 /// Geometria popupu zebrana z `xdg_positioner` (uproszczona: prostokąt kotwicy + offset).
17 18 #[derive(Debug, Clone, Copy, Default)]
@@ -117,7 +118,7 @@ impl Dispatch<xdg_wm_base::XdgWmBase, ()> for State {
117 118 let xdg = data_init.init(id, XdgSurfaceData::new(surface.clone()));
118 119 // Zapamiętaj powiązanie po stronie `wl_surface` (do konfiguracji przy commit).
119 120 if let Some(sd) = surface.data::<SurfaceData>() {
120 - *sd.xdg_surface.lock().unwrap() = Some(xdg);
121 + *sd.xdg_surface.lock_or_recover() = Some(xdg);
121 122 }
122 123 }
123 124 _ => {}
@@ -138,7 +139,7 @@ impl Dispatch<xdg_surface::XdgSurface, XdgSurfaceData> for State {
138 139 match request {
139 140 xdg_surface::Request::GetToplevel { id } => {
140 141 let toplevel = data_init.init(id, XdgToplevelData::new(data.wl_surface.clone()));
141 - *data.toplevel.lock().unwrap() = Some(toplevel);
142 + *data.toplevel.lock_or_recover() = Some(toplevel);
142 143 }
143 144 xdg_surface::Request::GetPopup {
144 145 id,
@@ -146,15 +147,15 @@ impl Dispatch<xdg_surface::XdgSurface, XdgSurfaceData> for State {
146 147 positioner,
147 148 } => {
148 149 let popup = data_init.init(id, ());
149 - *data.popup.lock().unwrap() = Some(popup);
150 - *data.is_popup.lock().unwrap() = true;
151 - *data.parent.lock().unwrap() = parent
150 + *data.popup.lock_or_recover() = Some(popup);
151 + *data.is_popup.lock_or_recover() = true;
152 + *data.parent.lock_or_recover() = parent
152 153 .as_ref()
153 154 .and_then(|p| p.data::<XdgSurfaceData>())
154 155 .map(|p| p.wl_surface.clone());
155 - *data.geom.lock().unwrap() = positioner
156 + *data.geom.lock_or_recover() = positioner
156 157 .data::<XdgPositionerData>()
157 - .map(|p| *p.geom.lock().unwrap())
158 + .map(|p| *p.geom.lock_or_recover())
158 159 .unwrap_or_default();
159 160 }
160 161 xdg_surface::Request::AckConfigure { .. } => {
@@ -177,19 +178,19 @@ impl Dispatch<xdg_toplevel::XdgToplevel, XdgToplevelData> for State {
177 178 ) {
178 179 match request {
179 180 xdg_toplevel::Request::SetTitle { title } => {
180 - *data.title.lock().unwrap() = title.clone();
181 + *data.title.lock_or_recover() = title.clone();
181 182 state.toplevels += 1;
182 183 // Odśwież tytuł w modelu okien (dekoracja rysuje go na pasku).
183 184 if let Some(sd) = data.surface.data::<SurfaceData>() {
184 - if let Some(slot) = *sd.slot.lock().unwrap() {
185 - if let Some(window) = state.windows.lock().unwrap().get_mut(slot) {
185 + if let Some(slot) = *sd.slot.lock_or_recover() {
186 + if let Some(window) = state.windows.lock_or_recover().get_mut(slot) {
186 187 window.title = title;
187 188 }
188 189 }
189 190 }
190 191 }
191 192 xdg_toplevel::Request::SetAppId { app_id } => {
192 - *data.app_id.lock().unwrap() = app_id;
193 + *data.app_id.lock_or_recover() = app_id;
193 194 }
194 195 _ => {}
195 196 }
@@ -206,7 +207,7 @@ impl Dispatch<xdg_positioner::XdgPositioner, XdgPositionerData> for State {
206 207 _handle: &DisplayHandle,
207 208 _data_init: &mut DataInit<'_, Self>,
208 209 ) {
209 - let mut geom = data.geom.lock().unwrap();
210 + let mut geom = data.geom.lock_or_recover();
210 211 match request {
211 212 xdg_positioner::Request::SetSize { width, height } => {
212 213 geom.width = width;
@@ -11,6 +11,10 @@ use wayland_server::{
11 11
12 12 use crate::state::State;
13 13
14 +/// Górny limit pojedynczego obrazu (RGBA) — chroni przed OOM wymuszonym przez klienta.
15 +/// 256 MiB pokrywa 8K na kilku monitorach, a odcina absurdalne żądania.
16 +const MAX_IMAGE_BYTES: usize = 256 * 1024 * 1024;
17 +
14 18 /// Dane puli pamięci (`wl_shm_pool`): deskryptor, rozmiar i **zbuforowana mapa**.
15 19 ///
16 20 /// Mapę tworzymy raz (przy `create_pool`) i zwalniamy przy zniszczeniu puli — zamiast
@@ -28,7 +32,13 @@ unsafe impl Sync for ShmPoolData {}
28 32
29 33 impl ShmPoolData {
30 34 /// Mapuje pulę (tylko odczyt). Przy niepowodzeniu zostaje pusta (bez paniki).
31 - pub fn new(fd: OwnedFd, size: usize) -> Self {
35 + pub fn new(fd: OwnedFd, declared_size: usize) -> Self {
36 + // Faktyczny rozmiar obiektu shm: klient może zadeklarować więcej, niż istnieje,
37 + // a odczyt poza obiekt kończy się SIGBUS (nie do złapania). Przycinamy rozmiar.
38 + let real = rustix::fs::fstat(&fd)
39 + .map(|st| st.st_size.max(0) as usize)
40 + .unwrap_or(0);
41 + let size = declared_size.min(real);
32 42 let map = if size == 0 {
33 43 std::ptr::null_mut()
34 44 } else {
@@ -48,13 +58,17 @@ impl ShmPoolData {
48 58 Self { fd, size, map }
49 59 }
50 60
51 - /// Bajty puli (widok tylko do odczytu; pusty, gdy mapowanie się nie powiodło).
52 - pub fn bytes(&self) -> &[u8] {
53 - if self.map.is_null() {
54 - return &[];
61 + /// Odczyt jednego bajtu przez `read_volatile`.
62 + ///
63 + /// Klient (osobny proces) może pisać do tej pamięci w dowolnym momencie, więc nie
64 + /// tworzymy `&[u8]` na współdzielonym mapowaniu (to formalny wyścig). `None`, gdy
65 + /// indeks poza mapą albo mapowanie się nie powiodło.
66 + pub fn read_u8(&self, index: usize) -> Option<u8> {
67 + if self.map.is_null() || index >= self.size {
68 + return None;
55 69 }
56 - // SAFETY: `map` wskazuje `size` bajtów zamapowanej pamięci.
57 - unsafe { std::slice::from_raw_parts(self.map as *const u8, self.size) }
70 + // SAFETY: `index < self.size`, a mapa obejmuje `self.size` bajtów.
71 + Some(unsafe { std::ptr::read_volatile((self.map as *const u8).add(index)) })
58 72 }
59 73 }
60 74
@@ -82,31 +96,43 @@ pub struct BufferData {
82 96
83 97 impl BufferData {
84 98 /// Odczytuje piksele bufora jako premultiplied RGBA8 (swizzle BGRA→RGBA).
99 + ///
100 + /// Waliduje geometrię względem rozmiaru puli (arytmetyka bez przepełnień) i ogranicza
101 + /// rozmiar alokacji, żeby klient nie wywołał paniki ani OOM.
85 102 pub fn to_rgba(&self) -> Option<Vec<u8>> {
86 103 if self.width <= 0 || self.height <= 0 || self.stride <= 0 {
87 104 return None;
88 105 }
89 106 let (w, h) = (self.width as usize, self.height as usize);
90 107 let stride = self.stride as usize;
91 - // Zakres musi mieścić się w puli.
92 - if self.offset + stride * h > self.pool.size {
108 + // Wiersz musi pomieścić `w` pikseli po 4 bajty (inaczej odczyt wyszedłby poza wiersz).
109 + if stride < w.checked_mul(4)? {
110 + return None;
111 + }
112 + // Cały zakres danych musi mieścić się w puli — na sprawdzonej arytmetyce.
113 + let end = self.offset.checked_add(stride.checked_mul(h)?)?;
114 + if end > self.pool.size {
115 + return None;
116 + }
117 + // Limit alokacji chroniący przed OOM wymuszonym przez klienta.
118 + let out_len = w.checked_mul(h)?.checked_mul(4)?;
119 + if out_len > MAX_IMAGE_BYTES {
93 120 return None;
94 121 }
95 - let bytes = self.pool.bytes();
96 - let mut out = vec![0u8; w * h * 4];
122 + let mut out = vec![0u8; out_len];
97 123 for y in 0..h {
98 124 let row = self.offset + y * stride;
99 125 for x in 0..w {
100 126 let i = row + x * 4;
101 127 let o = (y * w + x) * 4;
102 128 // ARGB8888 (pamięć: B,G,R,A) → RGBA8; XRGB8888 → alfa wymuszona na 1.
103 - out[o] = bytes[i + 2];
104 - out[o + 1] = bytes[i + 1];
105 - out[o + 2] = bytes[i];
129 + out[o] = self.pool.read_u8(i + 2)?;
130 + out[o + 1] = self.pool.read_u8(i + 1)?;
131 + out[o + 2] = self.pool.read_u8(i)?;
106 132 out[o + 3] = if self.format == wl_shm::Format::Xrgb8888 {
107 133 255
108 134 } else {
109 - bytes[i + 3]
135 + self.pool.read_u8(i + 3)?
110 136 };
111 137 }
112 138 }
@@ -114,6 +140,57 @@ impl BufferData {
114 140 }
115 141 }
116 142
143 +#[cfg(test)]
144 +mod tests {
145 + use super::*;
146 + use std::io::Write;
147 +
148 + fn pool(bytes: &[u8]) -> Arc<ShmPoolData> {
149 + let fd =
150 + rustix::fs::memfd_create("selene-shm-test", rustix::fs::MemfdFlags::CLOEXEC).unwrap();
151 + let mut file = std::fs::File::from(fd);
152 + file.write_all(bytes).unwrap();
153 + Arc::new(ShmPoolData::new(file.into(), bytes.len()))
154 + }
155 +
156 + fn buffer(pool: Arc<ShmPoolData>, w: i32, h: i32, stride: i32) -> BufferData {
157 + BufferData {
158 + pool,
159 + offset: 0,
160 + width: w,
161 + height: h,
162 + stride,
163 + format: wl_shm::Format::Argb8888,
164 + }
165 + }
166 +
167 + #[test]
168 + fn to_rgba_swizzles_bgra_to_rgba() {
169 + // 1x1: pamięć B,G,R,A = (10,20,30,40) → RGBA (30,20,10,40).
170 + let rgba = buffer(pool(&[10, 20, 30, 40]), 1, 1, 4).to_rgba().unwrap();
171 + assert_eq!(rgba, vec![30, 20, 10, 40]);
172 + }
173 +
174 + #[test]
175 + fn xrgb_forces_opaque_alpha() {
176 + let mut b = buffer(pool(&[1, 2, 3, 0]), 1, 1, 4);
177 + b.format = wl_shm::Format::Xrgb8888;
178 + assert_eq!(b.to_rgba().unwrap(), vec![3, 2, 1, 255]);
179 + }
180 +
181 + #[test]
182 + fn rejects_stride_smaller_than_row() {
183 + // stride 2 < width*4 → odrzucone, bez paniki/OOB.
184 + assert!(buffer(pool(&[0u8; 64]), 4, 1, 2).to_rgba().is_none());
185 + }
186 +
187 + #[test]
188 + fn rejects_buffer_outside_pool() {
189 + // 2x2 ARGB = 16 bajtów > pula 8 → odrzucone.
190 + assert!(buffer(pool(&[0u8; 8]), 2, 2, 8).to_rgba().is_none());
191 + }
192 +}
193 +
117 194 impl GlobalDispatch<wl_shm::WlShm, ()> for State {
118 195 fn bind(
119 196 _state: &mut Self,
120 197 new file mode 100644
@@ -0,0 +1,42 @@
1 +//! Blokady odporne na „zatrucie” po panice.
2 +//!
3 +//! Po panice w obsłudze klienta (patrz `catch_unwind` w `main`) mutex, którego
4 +//! `MutexGuard` był wtedy żywy, zostaje „zatruty”. Standardowe `.lock().unwrap()`
5 +//! panikowałoby przy każdym kolejnym użyciu i — mimo izolacji panik — skutecznie
6 +//! psuto by daną ścieżkę. [`LockExt::lock_or_recover`] odzyskuje wartość mimo
7 +//! zatrucia, więc jeden błędny klient nie unieruchamia kompozytora.
8 +
9 +use std::sync::{Mutex, MutexGuard};
10 +
11 +/// Rozszerzenie [`Mutex`] o blokadę odzyskującą wartość z zatrutego mutexa.
12 +pub trait LockExt<T> {
13 + /// Blokuje mutex; przy zatruciu (po panice) zwraca wewnętrzną wartość bez paniki.
14 + fn lock_or_recover(&self) -> MutexGuard<'_, T>;
15 +}
16 +
17 +impl<T> LockExt<T> for Mutex<T> {
18 + fn lock_or_recover(&self) -> MutexGuard<'_, T> {
19 + self.lock().unwrap_or_else(|poison| poison.into_inner())
20 + }
21 +}
22 +
23 +#[cfg(test)]
24 +mod tests {
25 + use super::*;
26 + use std::panic::{catch_unwind, AssertUnwindSafe};
27 +
28 + #[test]
29 + fn recovers_value_from_poisoned_mutex() {
30 + let m = Mutex::new(0i32);
31 + // Zatruwamy mutex: panika przy żywym guardzie.
32 + let _ = catch_unwind(AssertUnwindSafe(|| {
33 + let _guard = m.lock().unwrap();
34 + panic!("boom");
35 + }));
36 + assert!(m.lock().is_err(), "mutex powinien być zatruty po panice");
37 +
38 + // Mimo zatrucia odzyskujemy wartość i możemy ją zmienić.
39 + *m.lock_or_recover() = 42;
40 + assert_eq!(*m.lock_or_recover(), 42);
41 + }
42 +}