🎉 Phase 7.5 — robustesse paquet A validée runtime
Compositor anti-panic face à 4 cas malformés exercés en succession : brutal exit sans destroy, ack_configure avec mauvais serial, create_buffer avec dimensions nulles, create_buffer avec stride incohérent. Aucun crash, aucun blocage, ticks compositor continus 30s+ après la fin du fuzz. Frontend hardening : - BufferData.valid: bool, mis à false dans wl_shm_pool.create_buffer si dimensions/stride/offset incohérents avec la taille du pool. Le wl_buffer est quand même créé (contrat wayland-server) mais ignoré au commit. - ShmPool::read_argb signature passée de Vec<u32> à Option<Vec<u32>>. Refuse de lire si w/h/stride invalides ou si l'accès final dépasse self.size. Calculs en checked_add/checked_mul pour éviter tout overflow sur des params adversariaux. Évite tout accès UB. - xdg_surface.ack_configure refuse les serials > last_sent (log + ignore, pas de post_error pour 7.5 — tolérance volontaire). - wl_surface.commit court-circuite la lecture pour les buffers invalides ou si read_argb retourne None (log warning, surface garde son ancien contenu). Nouveau crate : redox-wl-test-fuzz-protocol (~370 lignes) - fork() pour chaque cas afin qu'un crash potentiel d'un cas ne contamine pas les suivants - 4 cas : brutal exit, bad ack serial, null dimensions, bad stride - Le parent attend chaque enfant via waitpid avant le suivant Validation runtime QEMU : - [fuzz1..4] tous PASS, [fuzz] PASS final - [frontend] xdg_surface.ack_configure: serial 99999 > last_sent 2, ignoring - [frontend] wl_shm_pool.create_buffer rejected: offset=0 width=0 height=0 stride=0 - [frontend] wl_shm_pool.create_buffer rejected: offset=0 width=100 height=10 stride=10 - Compositor continue à ticker 30+ s post-fuzz, curseur actif, surfaces des fuzz suivants créées et focusées normalement. Sub-bug documenté (à corriger 7.6) : la surface du fuzz1 (exit brutal sans destroy) persiste après la déconnexion du client. wayland-server détecte le close socket mais ne réveille pas automatiquement le wl_surface.Destroy handler. À hooker dans DumbClientData::disconnected pour le cleanup explicite. Doc complète : docs/phase7-5-robustness.md. Leyoda 2026 – GPLv3
This commit is contained in:
parent
c40ca9fcc8
commit
7e81dec637
5 changed files with 731 additions and 18 deletions
10
crates/redox-wl-test-fuzz-protocol/Cargo.toml
Normal file
10
crates/redox-wl-test-fuzz-protocol/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "redox-wl-test-fuzz-protocol"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
wayland-client = { path = "../../../wayland-rs/wayland-client", default-features = false }
|
||||
wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false }
|
||||
wayland-protocols = { path = "../../../wayland-rs/wayland-protocols", default-features = false, features = ["client"] }
|
||||
libc = "0.2"
|
||||
440
crates/redox-wl-test-fuzz-protocol/src/main.rs
Normal file
440
crates/redox-wl-test-fuzz-protocol/src/main.rs
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
//! Phase 7.5 — Tests négatifs (paquet A) sur le protocole Wayland.
|
||||
//!
|
||||
//! Lance N sous-processus séquentiellement, chacun exerce un cas
|
||||
//! malformé puis exit. Entre chaque, le parent vérifie indirectement
|
||||
//! que le compositor est toujours là (le test suivant arrive à se
|
||||
//! connecter au socket).
|
||||
//!
|
||||
//! Cas couverts :
|
||||
//! 1. Exit brutal sans destroy : connect → create_surface →
|
||||
//! xdg_toplevel + ack + commit normal → process::exit(0) immédiat
|
||||
//! sans destroy(). Doit forcer le serveur à nettoyer via les Drop
|
||||
//! handlers (DumbClientData::disconnected).
|
||||
//! 2. AckConfigure avec mauvais serial : connect → toplevel →
|
||||
//! attendre configure → ack(99999) → ack(real_serial). Le serveur
|
||||
//! doit refuser le 99999 et accepter le real_serial.
|
||||
//! 3. create_buffer dimensions invalides : connect → create_pool de
|
||||
//! 1024 bytes → create_buffer(width=0, height=0, stride=0) → attach
|
||||
//! + commit. Le serveur doit accepter le wl_buffer mais ignorer
|
||||
//! son contenu au commit (pas de buffer overrun).
|
||||
//! 4. create_buffer stride trop petit : create_pool(4096) →
|
||||
//! create_buffer(w=100, h=10, stride=10) [stride < w*4]. Idem :
|
||||
//! accepté mais ignoré au commit.
|
||||
//!
|
||||
//! Validation : à la fin, vérifier dans le serial / comp.log que le
|
||||
//! compositor a logué les warnings appropriés et que ses ticks
|
||||
//! continuent sans interruption (donc aucun panic, aucun deadlock).
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::os::fd::{AsFd, FromRawFd, OwnedFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::process::{self, ExitCode};
|
||||
use std::ptr;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
|
||||
backend::Backend,
|
||||
protocol::{
|
||||
wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_shm::WlShm,
|
||||
wl_shm_pool::WlShmPool, wl_surface::WlSurface,
|
||||
},
|
||||
};
|
||||
use wayland_protocols::xdg::shell::client::{
|
||||
xdg_surface::{self, XdgSurface},
|
||||
xdg_toplevel::{self, XdgToplevel},
|
||||
xdg_wm_base::{self, XdgWmBase},
|
||||
};
|
||||
|
||||
const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock";
|
||||
|
||||
struct DebugSink(Mutex<Option<std::fs::File>>);
|
||||
impl DebugSink {
|
||||
fn new() -> Self {
|
||||
Self(Mutex::new(
|
||||
OpenOptions::new().write(true).open("/scheme/debug").ok(),
|
||||
))
|
||||
}
|
||||
fn writeln(&self, s: &str) {
|
||||
println!("{s}");
|
||||
if let Ok(mut g) = self.0.lock() {
|
||||
if let Some(f) = g.as_mut() {
|
||||
let _ = writeln!(f, "{s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn dlog(s: &str) {
|
||||
static SINK: OnceLock<DebugSink> = OnceLock::new();
|
||||
SINK.get_or_init(DebugSink::new).writeln(s);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClientState {
|
||||
compositor: Option<WlCompositor>,
|
||||
shm: Option<WlShm>,
|
||||
wm_base: Option<XdgWmBase>,
|
||||
pending_serial: Option<u32>,
|
||||
configured: bool,
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, ()> for ClientState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
registry: &wl_registry::WlRegistry,
|
||||
event: wl_registry::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
if let wl_registry::Event::Global { name, interface, version } = event {
|
||||
match interface.as_str() {
|
||||
"wl_compositor" => {
|
||||
state.compositor = Some(registry.bind(name, version.min(5), qh, ()));
|
||||
}
|
||||
"wl_shm" => {
|
||||
state.shm = Some(registry.bind(name, version.min(1), qh, ()));
|
||||
}
|
||||
"xdg_wm_base" => {
|
||||
state.wm_base = Some(registry.bind(name, version.min(5), qh, ()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! noop {
|
||||
($ty:ty) => {
|
||||
impl Dispatch<$ty, ()> for ClientState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_r: &$ty,
|
||||
_ev: <$ty as Proxy>::Event,
|
||||
_: &(),
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
noop!(WlCompositor);
|
||||
noop!(WlShm);
|
||||
noop!(WlShmPool);
|
||||
noop!(WlBuffer);
|
||||
noop!(WlSurface);
|
||||
noop!(XdgToplevel);
|
||||
|
||||
impl Dispatch<XdgWmBase, ()> for ClientState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
wm_base: &XdgWmBase,
|
||||
event: xdg_wm_base::Event,
|
||||
_: &(),
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
) {
|
||||
if let xdg_wm_base::Event::Ping { serial } = event {
|
||||
wm_base.pong(serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgSurface, ()> for ClientState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_xdg_surf: &XdgSurface,
|
||||
event: xdg_surface::Event,
|
||||
_: &(),
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
) {
|
||||
if let xdg_surface::Event::Configure { serial } = event {
|
||||
state.pending_serial = Some(serial);
|
||||
state.configured = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_socket() -> Result<(), String> {
|
||||
for _ in 0..50 {
|
||||
if std::path::Path::new(SOCKET_PATH).exists() {
|
||||
return Ok(());
|
||||
}
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err("compositor socket missing".into())
|
||||
}
|
||||
|
||||
fn connect() -> Result<(Connection, EventQueue<ClientState>, ClientState), Box<dyn std::error::Error>> {
|
||||
wait_socket()?;
|
||||
let stream = UnixStream::connect(SOCKET_PATH)?;
|
||||
let backend = Backend::connect(stream)?;
|
||||
let conn = Connection::from_backend(backend);
|
||||
let mut event_queue: EventQueue<ClientState> = conn.new_event_queue();
|
||||
let qh = event_queue.handle();
|
||||
let _registry = conn.display().get_registry(&qh, ());
|
||||
let mut state = ClientState::default();
|
||||
event_queue.roundtrip(&mut state)?;
|
||||
Ok((conn, event_queue, state))
|
||||
}
|
||||
|
||||
unsafe fn alloc_shm(name: &str, size: i32) -> Result<OwnedFd, String> {
|
||||
let cname = CString::new(name).unwrap();
|
||||
let _ = libc::shm_unlink(cname.as_ptr());
|
||||
let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR | libc::O_CREAT, 0o600);
|
||||
if fd < 0 {
|
||||
return Err("shm_open failed".into());
|
||||
}
|
||||
if libc::ftruncate(fd, size as _) != 0 {
|
||||
libc::close(fd);
|
||||
return Err("ftruncate failed".into());
|
||||
}
|
||||
Ok(OwnedFd::from_raw_fd(fd))
|
||||
}
|
||||
|
||||
/// Cas 1 : exit brutal sans destroy.
|
||||
fn case_brutal_exit() -> Result<(), Box<dyn std::error::Error>> {
|
||||
dlog("[fuzz1] brutal exit without destroy");
|
||||
let (_conn, mut event_queue, mut state) = connect()?;
|
||||
let qh = event_queue.handle();
|
||||
let compositor = state.compositor.clone().ok_or("no wl_compositor")?;
|
||||
let wm_base = state.wm_base.clone().ok_or("no xdg_wm_base")?;
|
||||
let shm = state.shm.clone().ok_or("no wl_shm")?;
|
||||
|
||||
let surface = compositor.create_surface(&qh, ());
|
||||
let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ());
|
||||
let _toplevel = xdg_surface.get_toplevel(&qh, ());
|
||||
surface.commit();
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
while !state.configured && start.elapsed() < Duration::from_secs(3) {
|
||||
event_queue.roundtrip(&mut state)?;
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
if let Some(serial) = state.pending_serial {
|
||||
xdg_surface.ack_configure(serial);
|
||||
}
|
||||
|
||||
// Allouer un buffer + attach + commit pour avoir un état complet
|
||||
let size = 320 * 240 * 4;
|
||||
let fd = unsafe { alloc_shm("/redox-wl-fuzz-1", size) }?;
|
||||
let pool = shm.create_pool(fd.as_fd(), size, &qh, ());
|
||||
let buffer = pool.create_buffer(
|
||||
0,
|
||||
320,
|
||||
240,
|
||||
320 * 4,
|
||||
wayland_client::protocol::wl_shm::Format::Argb8888,
|
||||
&qh,
|
||||
(),
|
||||
);
|
||||
surface.attach(Some(&buffer), 0, 0);
|
||||
surface.commit();
|
||||
event_queue.flush()?;
|
||||
|
||||
dlog("[fuzz1] state complete, exiting brutally NOW");
|
||||
// Pas de destroy, pas de disconnect, pas de drop. Le kernel ferme
|
||||
// les fds quand le process exit. C'est le serveur qui doit nettoyer.
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
/// Cas 2 : ack_configure avec mauvais serial.
|
||||
fn case_bad_ack_serial() -> Result<(), Box<dyn std::error::Error>> {
|
||||
dlog("[fuzz2] ack_configure with bad serial 99999");
|
||||
let (_conn, mut event_queue, mut state) = connect()?;
|
||||
let qh = event_queue.handle();
|
||||
let compositor = state.compositor.clone().ok_or("no wl_compositor")?;
|
||||
let wm_base = state.wm_base.clone().ok_or("no xdg_wm_base")?;
|
||||
|
||||
let surface = compositor.create_surface(&qh, ());
|
||||
let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ());
|
||||
let _toplevel = xdg_surface.get_toplevel(&qh, ());
|
||||
surface.commit();
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
while !state.configured && start.elapsed() < Duration::from_secs(3) {
|
||||
event_queue.roundtrip(&mut state)?;
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
let real_serial = state.pending_serial.ok_or("no configure")?;
|
||||
|
||||
// 1) Mauvais serial (compositor doit ignorer)
|
||||
dlog("[fuzz2] sending ack_configure(99999) — should be refused");
|
||||
xdg_surface.ack_configure(99999);
|
||||
event_queue.flush()?;
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
|
||||
// 2) Bon serial (compositor doit accepter)
|
||||
dlog(&format!(
|
||||
"[fuzz2] sending ack_configure({real_serial}) — should be accepted"
|
||||
));
|
||||
xdg_surface.ack_configure(real_serial);
|
||||
event_queue.flush()?;
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
|
||||
// Cleanup propre pour vérifier que la séquence post-bad-ack continue OK
|
||||
surface.destroy();
|
||||
event_queue.flush()?;
|
||||
dlog("[fuzz2] PASS");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cas 3 : create_buffer avec dimensions nulles.
|
||||
fn case_null_dimensions() -> Result<(), Box<dyn std::error::Error>> {
|
||||
dlog("[fuzz3] create_buffer with null dimensions");
|
||||
let (_conn, mut event_queue, mut state) = connect()?;
|
||||
let qh = event_queue.handle();
|
||||
let compositor = state.compositor.clone().ok_or("no wl_compositor")?;
|
||||
let shm = state.shm.clone().ok_or("no wl_shm")?;
|
||||
let wm_base = state.wm_base.clone().ok_or("no xdg_wm_base")?;
|
||||
|
||||
let surface = compositor.create_surface(&qh, ());
|
||||
let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ());
|
||||
let _toplevel = xdg_surface.get_toplevel(&qh, ());
|
||||
surface.commit();
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
while !state.configured && start.elapsed() < Duration::from_secs(3) {
|
||||
event_queue.roundtrip(&mut state)?;
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
if let Some(s) = state.pending_serial {
|
||||
xdg_surface.ack_configure(s);
|
||||
}
|
||||
|
||||
// Pool de 1024 octets
|
||||
let fd = unsafe { alloc_shm("/redox-wl-fuzz-3", 1024) }?;
|
||||
let pool = shm.create_pool(fd.as_fd(), 1024, &qh, ());
|
||||
|
||||
// Buffer 0x0
|
||||
let buffer = pool.create_buffer(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
wayland_client::protocol::wl_shm::Format::Argb8888,
|
||||
&qh,
|
||||
(),
|
||||
);
|
||||
surface.attach(Some(&buffer), 0, 0);
|
||||
surface.commit();
|
||||
event_queue.flush()?;
|
||||
thread::sleep(Duration::from_millis(300));
|
||||
|
||||
surface.destroy();
|
||||
event_queue.flush()?;
|
||||
dlog("[fuzz3] PASS");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cas 4 : stride < width*4.
|
||||
fn case_bad_stride() -> Result<(), Box<dyn std::error::Error>> {
|
||||
dlog("[fuzz4] create_buffer with stride < width*4");
|
||||
let (_conn, mut event_queue, mut state) = connect()?;
|
||||
let qh = event_queue.handle();
|
||||
let compositor = state.compositor.clone().ok_or("no wl_compositor")?;
|
||||
let shm = state.shm.clone().ok_or("no wl_shm")?;
|
||||
let wm_base = state.wm_base.clone().ok_or("no xdg_wm_base")?;
|
||||
|
||||
let surface = compositor.create_surface(&qh, ());
|
||||
let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ());
|
||||
let _toplevel = xdg_surface.get_toplevel(&qh, ());
|
||||
surface.commit();
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
while !state.configured && start.elapsed() < Duration::from_secs(3) {
|
||||
event_queue.roundtrip(&mut state)?;
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
if let Some(s) = state.pending_serial {
|
||||
xdg_surface.ack_configure(s);
|
||||
}
|
||||
|
||||
// Pool de 4096 octets, buffer 100x10 avec stride=10 (au lieu de 400)
|
||||
let fd = unsafe { alloc_shm("/redox-wl-fuzz-4", 4096) }?;
|
||||
let pool = shm.create_pool(fd.as_fd(), 4096, &qh, ());
|
||||
let buffer = pool.create_buffer(
|
||||
0,
|
||||
100,
|
||||
10,
|
||||
10, // stride invalide
|
||||
wayland_client::protocol::wl_shm::Format::Argb8888,
|
||||
&qh,
|
||||
(),
|
||||
);
|
||||
surface.attach(Some(&buffer), 0, 0);
|
||||
surface.commit();
|
||||
event_queue.flush()?;
|
||||
thread::sleep(Duration::from_millis(300));
|
||||
|
||||
surface.destroy();
|
||||
event_queue.flush()?;
|
||||
dlog("[fuzz4] PASS");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_in_child<F>(label: &str, f: F)
|
||||
where
|
||||
F: FnOnce() -> Result<(), Box<dyn std::error::Error>>,
|
||||
{
|
||||
let pid = unsafe { libc::fork() };
|
||||
if pid < 0 {
|
||||
dlog(&format!("[fuzz] fork failed for {label}"));
|
||||
return;
|
||||
}
|
||||
if pid == 0 {
|
||||
// Enfant : exécute le cas et exit. Si erreur, log.
|
||||
if let Err(e) = f() {
|
||||
dlog(&format!("[{label}] error: {e}"));
|
||||
}
|
||||
process::exit(0);
|
||||
}
|
||||
// Parent : attend l'enfant
|
||||
let mut status: i32 = 0;
|
||||
unsafe {
|
||||
libc::waitpid(pid, &mut status, 0);
|
||||
}
|
||||
dlog(&format!("[fuzz] child {label} exited (status={status})"));
|
||||
}
|
||||
|
||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
dlog("[fuzz] phase 7.5 — protocole tests négatifs");
|
||||
|
||||
// Attente initiale pour que le compositor démarre
|
||||
wait_socket()?;
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
|
||||
run_in_child("fuzz1", case_brutal_exit);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
|
||||
run_in_child("fuzz2", case_bad_ack_serial);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
|
||||
run_in_child("fuzz3", case_null_dimensions);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
|
||||
run_in_child("fuzz4", case_bad_stride);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
|
||||
dlog("[fuzz] all cases exercised, compositor should still be up");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => {
|
||||
dlog("[fuzz] PASS");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
dlog(&format!("[fuzz] FAIL: {e}"));
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -87,11 +87,27 @@ impl ShmPool {
|
|||
})
|
||||
}
|
||||
|
||||
/// Lit les pixels du buffer à partir de l'offset, taille connue.
|
||||
/// Pas de validation alignment ; le caller doit fournir des params
|
||||
/// cohérents (offset + h * stride <= self.size, stride == w*4).
|
||||
unsafe fn read_argb(&self, offset: usize, w: u32, h: u32, stride: i32) -> Vec<u32> {
|
||||
/// Lit les pixels du buffer à partir de l'offset.
|
||||
///
|
||||
/// Phase 7.5 : retourne `None` si les paramètres demandent une lecture
|
||||
/// hors-pool (overrun) ou si `stride < w*4`. Le caller doit alors
|
||||
/// ignorer le buffer côté composition. Évite tout accès mémoire UB
|
||||
/// face à un client malformé.
|
||||
unsafe fn read_argb(&self, offset: usize, w: u32, h: u32, stride: i32) -> Option<Vec<u32>> {
|
||||
if w == 0 || h == 0 || stride <= 0 {
|
||||
return None;
|
||||
}
|
||||
let stride = stride as usize;
|
||||
if stride < (w as usize).saturating_mul(4) {
|
||||
return None;
|
||||
}
|
||||
// last_byte = offset + (h-1)*stride + w*4
|
||||
let last_byte = offset
|
||||
.checked_add(stride.checked_mul((h as usize).saturating_sub(1))?)
|
||||
.and_then(|v| v.checked_add((w as usize).checked_mul(4)?))?;
|
||||
if last_byte > self.size {
|
||||
return None;
|
||||
}
|
||||
let n = (w as usize) * (h as usize);
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for y in 0..h as usize {
|
||||
|
|
@ -100,7 +116,7 @@ impl ShmPool {
|
|||
out.push(unsafe { *row_ptr.add(x) });
|
||||
}
|
||||
}
|
||||
out
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,6 +139,9 @@ struct BufferData {
|
|||
stride: i32,
|
||||
/// Format Wayland brut (Argb8888 = 0, Xrgb8888 = 1)
|
||||
format: wl_shm::Format,
|
||||
/// Phase 7.5 : true si les paramètres sont valides (width/height/stride
|
||||
/// cohérents avec la taille du pool). Si false, le commit ignore ce buffer.
|
||||
valid: bool,
|
||||
}
|
||||
|
||||
/// Données par-surface : SurfaceId du compositor + buffer attaché en pending.
|
||||
|
|
@ -863,6 +882,35 @@ impl wayland_server::Dispatch<wl_shm_pool::WlShmPool, Arc<Mutex<ShmPool>>> for W
|
|||
Ok(f) => f,
|
||||
Err(_) => return, // format inconnu, on ignore
|
||||
};
|
||||
// Phase 7.5 : valider les dimensions et la cohérence avec la
|
||||
// taille du pool. La spec wl_shm.create_buffer demande
|
||||
// width/height/stride positifs et offset+stride*height <= pool size.
|
||||
// Si invalide, on init quand même le wl_buffer (sinon le client
|
||||
// aurait un id orphelin) mais on le marque `valid = false` pour
|
||||
// que le commit l'ignore.
|
||||
let mut valid = true;
|
||||
if width <= 0 || height <= 0 || stride <= 0 || offset < 0 {
|
||||
valid = false;
|
||||
}
|
||||
if valid && stride < width.saturating_mul(4) {
|
||||
valid = false;
|
||||
}
|
||||
if valid {
|
||||
let pool_size = pool.lock().unwrap().size as i64;
|
||||
let needed = (offset as i64).saturating_add(
|
||||
(stride as i64).saturating_mul((height as i64).saturating_sub(1))
|
||||
.saturating_add((width as i64).saturating_mul(4)),
|
||||
);
|
||||
if needed > pool_size {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
println!(
|
||||
"[frontend] wl_shm_pool.create_buffer rejected: \
|
||||
offset={offset} width={width} height={height} stride={stride}"
|
||||
);
|
||||
}
|
||||
let bd = BufferData {
|
||||
pool: Arc::clone(pool),
|
||||
offset,
|
||||
|
|
@ -870,6 +918,7 @@ impl wayland_server::Dispatch<wl_shm_pool::WlShmPool, Arc<Mutex<ShmPool>>> for W
|
|||
height: height as u32,
|
||||
stride,
|
||||
format,
|
||||
valid,
|
||||
};
|
||||
data_init.init(id, bd);
|
||||
}
|
||||
|
|
@ -953,19 +1002,30 @@ impl wayland_server::Dispatch<wl_surface::WlSurface, Arc<SurfaceData>> for Wayla
|
|||
// Lire le buffer attaché (s'il y en a un)
|
||||
let bd_opt = data.pending_buffer.lock().unwrap().clone();
|
||||
if let Some(bd) = bd_opt {
|
||||
// Lire les pixels et créer un SurfaceBuffer compositor-core
|
||||
let pool = bd.pool.lock().unwrap();
|
||||
let pixels = unsafe {
|
||||
pool.read_argb(bd.offset as usize, bd.width, bd.height, bd.stride)
|
||||
};
|
||||
let sb = SurfaceBuffer::from_pixels(bd.width, bd.height, pixels);
|
||||
// Pour une surface curseur, on stocke le buffer mais on
|
||||
// garde visible=false (la surface ne doit pas apparaître
|
||||
// dans la composition normale, seulement via draw_cursor).
|
||||
state.registry.modify_pending(id, |s| {
|
||||
s.buffer = Some(sb);
|
||||
s.visible = !is_cursor;
|
||||
});
|
||||
if !bd.valid {
|
||||
// Phase 7.5 : buffer marqué invalide à la création
|
||||
// (dimensions ou offset incohérents avec le pool).
|
||||
// On ignore plutôt que de lire des octets hors-pool.
|
||||
} else {
|
||||
// Lire les pixels et créer un SurfaceBuffer compositor-core
|
||||
let pool = bd.pool.lock().unwrap();
|
||||
let pixels_opt = unsafe {
|
||||
pool.read_argb(bd.offset as usize, bd.width, bd.height, bd.stride)
|
||||
};
|
||||
if let Some(pixels) = pixels_opt {
|
||||
let sb = SurfaceBuffer::from_pixels(bd.width, bd.height, pixels);
|
||||
// Pour une surface curseur, on stocke le buffer
|
||||
// mais visible=false (cf draw_cursor).
|
||||
state.registry.modify_pending(id, |s| {
|
||||
s.buffer = Some(sb);
|
||||
s.visible = !is_cursor;
|
||||
});
|
||||
} else {
|
||||
println!(
|
||||
"[frontend] commit: read_argb refused buffer (overrun guard)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
state.registry.commit(id);
|
||||
if !is_cursor {
|
||||
|
|
@ -1170,6 +1230,19 @@ impl wayland_server::Dispatch<xdg_surface::XdgSurface, Arc<XdgSurfaceData>> for
|
|||
// sert pour les decorations CSD que nous ne supportons pas).
|
||||
}
|
||||
xdg_surface::Request::AckConfigure { serial } => {
|
||||
// Phase 7.5 : refuser un ack pour un serial jamais émis.
|
||||
// Spec xdg-shell : si le client ack un serial inconnu, c'est
|
||||
// une erreur de protocole. Pour 7.5 on tolère (ignore silent +
|
||||
// log) plutôt que post_error, le temps de mesurer si des
|
||||
// toolkits réels en abusent. À durcir si on observe des
|
||||
// abus délibérés.
|
||||
let last = *data.last_serial.lock().unwrap();
|
||||
if serial > last {
|
||||
println!(
|
||||
"[frontend] xdg_surface.ack_configure: serial {serial} > last_sent {last}, ignoring"
|
||||
);
|
||||
return;
|
||||
}
|
||||
*data.acked_serial.lock().unwrap() = serial;
|
||||
// Si c'était l'initial configure, débloquer l'affichage
|
||||
if let Some(sd) = data.wl_surface.data::<Arc<SurfaceData>>() {
|
||||
|
|
|
|||
BIN
docs/phase7-5-after-fuzz.png
Normal file
BIN
docs/phase7-5-after-fuzz.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 973 B |
190
docs/phase7-5-robustness.md
Normal file
190
docs/phase7-5-robustness.md
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
# Phase 7.5 — Robustesse paquet A (tests négatifs protocole)
|
||||
|
||||
> Document produit le 2026-05-13 dans le cadre du plan directeur
|
||||
> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`.
|
||||
>
|
||||
> **Scope strict** :
|
||||
> - le compositor ne DOIT JAMAIS paniquer face à un client malformé
|
||||
> - face à des paramètres invalides, soit ignore proprement (silent
|
||||
> drop), soit `post_error` sur la resource fautive
|
||||
> - 4 cas exercés a minima : exit brutal, mauvais ack serial,
|
||||
> dimensions buffer nulles, stride incohérent avec width
|
||||
>
|
||||
> **Hors scope 7.5** : fuzzing exhaustif (à brancher dans CI plus
|
||||
> tard), filtrage multi-client (7.6), move/resize (7.7).
|
||||
|
||||
## Verdict
|
||||
|
||||
**✅ Compositor anti-panic validé runtime sur 4 cas malformés
|
||||
exécutés en succession dans des sous-processus séparés.**
|
||||
|
||||
Capture :  — état du framebuffer après
|
||||
les 4 cas. Le rectangle noir à `(60, 60)` est la surface persistante
|
||||
du fuzz1 (commit-puis-exit-brutal, buffer ARGB rempli de 0). Le
|
||||
curseur software est toujours actif au centre, fond compositor bleu
|
||||
nuit, aucun crash ni "Display output not active". Le compositor a
|
||||
continué à ticker pendant 30+ secondes après la fin du fuzz.
|
||||
|
||||
Logs `/tmp/fuzz.log` :
|
||||
```
|
||||
[fuzz] phase 7.5 — protocole tests négatifs
|
||||
[fuzz1] brutal exit without destroy
|
||||
[fuzz1] state complete, exiting brutally NOW
|
||||
[fuzz] child fuzz1 exited (status=0)
|
||||
[fuzz2] ack_configure with bad serial 99999
|
||||
[fuzz2] sending ack_configure(99999) — should be refused
|
||||
[fuzz2] sending ack_configure(2) — should be accepted
|
||||
[fuzz2] PASS
|
||||
[fuzz3] create_buffer with null dimensions
|
||||
[fuzz3] PASS
|
||||
[fuzz4] create_buffer with stride < width*4
|
||||
[fuzz4] PASS
|
||||
[fuzz] all cases exercised, compositor should still be up
|
||||
[fuzz] PASS
|
||||
```
|
||||
|
||||
Logs côté compositor `/tmp/comp.log` filtrés :
|
||||
```
|
||||
[frontend] xdg_surface.ack_configure: serial 99999 > last_sent 2, ignoring
|
||||
[frontend] wl_shm_pool.create_buffer rejected: offset=0 width=0 height=0 stride=0
|
||||
[frontend] wl_shm_pool.create_buffer rejected: offset=0 width=100 height=10 stride=10
|
||||
```
|
||||
|
||||
Chaque garde a été déclenché exactement une fois, dans l'ordre
|
||||
attendu. Aucun panic, aucun deadlock, aucune corruption d'état (les
|
||||
surfaces des fuzz suivants se créent et reçoivent bien le focus).
|
||||
|
||||
## Modifications apportées
|
||||
|
||||
### `redox-wl-wayland-frontend`
|
||||
|
||||
**`xdg_surface::Request::AckConfigure`** : vérifie que `serial <=
|
||||
last_serial` avant de l'enregistrer dans `acked_serial`. Si le client
|
||||
ack un serial jamais émis, on log et on ignore. Pas de `post_error`
|
||||
pour 7.5 (à durcir si on observe des abus délibérés ; les toolkits
|
||||
réels peuvent envoyer des ack stale légitimes après un reconfigure
|
||||
race).
|
||||
|
||||
**`BufferData`** : nouveau champ `valid: bool`. Mis à `false` au
|
||||
`wl_shm_pool::Request::CreateBuffer` si l'une des conditions est
|
||||
violée :
|
||||
- `width <= 0 || height <= 0 || stride <= 0 || offset < 0`
|
||||
- `stride < width * 4` (pour Argb8888 ARGB 4 octets/pixel)
|
||||
- `offset + (height-1)*stride + width*4 > pool.size`
|
||||
|
||||
Le buffer wl_buffer est quand même initialisé via `data_init.init`
|
||||
(pour ne pas violer le contrat wayland-server et laisser un id
|
||||
orphelin côté client), mais marqué invalide. Le commit handler
|
||||
court-circuite la lecture pour ces buffers.
|
||||
|
||||
**`ShmPool::read_argb`** : signature passée de `Vec<u32>` à
|
||||
`Option<Vec<u32>>`. Refuse de lire si `w == 0`, `h == 0`,
|
||||
`stride <= 0`, `stride < w*4`, ou si l'accès final dépasse `self.size`.
|
||||
Calculs en `checked_add` / `checked_mul` pour éviter tout overflow
|
||||
sur des params adversariaux. Évite tout accès mémoire UB.
|
||||
|
||||
**`wl_surface::Request::Commit`** : vérifie `bd.valid` avant de
|
||||
tenter la lecture, et gère le `None` retourné par `read_argb` comme
|
||||
un buffer ignoré (log warning, surface garde son ancien contenu).
|
||||
|
||||
### `redox-wl-test-fuzz-protocol` (nouveau crate, ~370 lignes)
|
||||
|
||||
Binaire qui exerce 4 cas malformés, chacun dans un `fork()` séparé
|
||||
pour qu'un éventuel crash d'un cas ne contamine pas les suivants :
|
||||
|
||||
| Cas | Description |
|
||||
|---|---|
|
||||
| fuzz1 | Connect → surface + xdg_toplevel + ack + buffer + commit → `process::exit(0)` brutal sans destroy. Le kernel ferme les fds, le compositor doit gérer la déconnexion via les Drop des UserData wayland-server. |
|
||||
| fuzz2 | Connect → toplevel → attendre configure → `ack_configure(99999)` puis `ack_configure(real_serial)`. Le compositor doit refuser le premier (log warning, ignore) et accepter le second. |
|
||||
| fuzz3 | Connect → toplevel → `create_buffer(width=0, height=0, stride=0)` → attach + commit. Le compositor doit accepter le wl_buffer (pour ne pas violer le protocole client) mais marquer invalid et ignorer au commit. |
|
||||
| fuzz4 | Idem, avec `width=100, height=10, stride=10` (au lieu de stride=400). Le guard `stride < width*4` doit déclencher. |
|
||||
|
||||
Le parent attend chaque enfant via `waitpid` puis sleep 1 s avant le
|
||||
suivant, pour laisser au compositor le temps de logger sa réaction
|
||||
au cas précédent. À la fin, log `[fuzz] PASS` si tout s'est exécuté
|
||||
sans crash côté client.
|
||||
|
||||
## Validation runtime
|
||||
|
||||
QEMU headless, image Redox boot complet, service init
|
||||
`40_phase75_fuzz` qui lance le compositor puis le fuzz binary
|
||||
4 secondes plus tard via un wrapper script `/usr/bin/launch_phase75.sh`
|
||||
(rappel 7.4 : `nowait sh -c "..."` direct ne passe pas le parsing
|
||||
init Redox, il faut un script).
|
||||
|
||||
Vérification post-run :
|
||||
- ✅ Tous les sous-processus enfants exit avec status=0
|
||||
- ✅ `[fuzz] PASS` final
|
||||
- ✅ Tous les logs `[frontend]` attendus sont présents
|
||||
- ✅ Le compositor continue à ticker pendant 30+ s après le PASS
|
||||
- ✅ Pas de message `panicked at` dans `/tmp/comp.log`
|
||||
- ✅ Pas de blocage : les surfaces des fuzz suivants se créent
|
||||
normalement, les focus changes s'enchaînent (SurfaceId(0), 2, 3
|
||||
vus dans les logs — les ids 1, 4 correspondent probablement aux
|
||||
xdg_surface comptés à part)
|
||||
|
||||
## Limitations connues (à traiter en sous-tickets ultérieurs)
|
||||
|
||||
- **Surface "fantôme" du fuzz1 persiste après l'exit brutal du
|
||||
client** : visible comme un rectangle noir à (60,60) dans la
|
||||
capture finale. Le client a fait `commit` (donc buffer copié dans
|
||||
le registry) puis exit. wayland-server détecte la déconnexion mais
|
||||
le `wl_surface::Destroy` handler n'est PAS appelé automatiquement
|
||||
pour les resources d'un client qui ferme socket sans destroy
|
||||
propre — il faudrait hooker `DumbClientData::disconnected` pour
|
||||
faire le cleanup explicite. À ajouter en 7.6 (multi-clients) où ce
|
||||
cas deviendra fréquent.
|
||||
- **Pas de `post_error` envoyé** : pour 7.5 on ignore silencieusement
|
||||
(avec log). Plus permissif que la spec. Si un toolkit réel envoie
|
||||
un mauvais serial par bug, on reste tolérant. À durcir si on
|
||||
observe des bugs systématiques côté serveur causés par ce laxisme.
|
||||
- **Pas testés en 7.5** :
|
||||
- `xdg_surface.destroy` avant `xdg_toplevel.destroy` (spec :
|
||||
error, mais pas dangereux côté serveur — on ignore)
|
||||
- destroy d'un wl_buffer attaché à une surface (la sémantique
|
||||
Wayland est qu'on doit envoyer `wl_buffer.release` avant ; on
|
||||
n'envoie jamais `release` actuellement — TODO 7.6)
|
||||
- `wl_shm_pool.resize` (no-op actuellement — TODO)
|
||||
- Format `wl_shm` non advertised (formats inconnus déjà ignorés)
|
||||
- Surface avec rôle déjà assigné (`wl_data_source` n'existe pas,
|
||||
le rôle xdg_surface est protégé par `xdg_pending_initial_configure`)
|
||||
- **Pas de fuzzing automatique avec corpus random** : reportable
|
||||
beaucoup plus tard, avec libfuzzer + cargo-fuzz. Pour 7.5 on se
|
||||
contente de cas dirigés.
|
||||
|
||||
## Critère de fin 7.5
|
||||
|
||||
> Face aux 4 cas malformés exercés par le fuzz protocol binary, le
|
||||
> compositor ne panique pas, ne se bloque pas, et reste pleinement
|
||||
> opérationnel pour les clients suivants.
|
||||
|
||||
**✅ Validé.** Le compositor a tourné 30+ s post-fuzz avec ticks
|
||||
continus, le curseur reste actif, les surfaces des fuzz suivants se
|
||||
créent et reçoivent leur focus.
|
||||
|
||||
## Code
|
||||
|
||||
```
|
||||
crates/redox-wl-wayland-frontend/ # +~50 lignes (BufferData.valid,
|
||||
# AckConfigure validation, read_argb safe)
|
||||
crates/redox-wl-test-fuzz-protocol/ # nouveau crate (~370 lignes)
|
||||
```
|
||||
|
||||
## Suite phase 7.6
|
||||
|
||||
Multi-clients paquet B :
|
||||
- Au moins 3 clients fork parallèle, chacun avec sa fenêtre + frame
|
||||
callbacks rapides
|
||||
- Vérifier que les frame callbacks `done` arrivent à chaque client
|
||||
- Vérifier l'ordre Z stable sous load
|
||||
- Filtrage des events button/key au client focused uniquement
|
||||
(au lieu du broadcast actuel)
|
||||
- Cleanup automatique post-disconnect (corrige le sub-bug 7.5 :
|
||||
surface fantôme du fuzz1)
|
||||
- `wl_buffer.release` envoyé après chaque copy au commit
|
||||
|
||||
Estimé : 1-2 sessions.
|
||||
|
||||
---
|
||||
|
||||
*Fin du document de phase 7.5.*
|
||||
Loading…
Add table
Add a link
Reference in a new issue