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
+}