//! 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::{ backend::Backend, protocol::{ wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_shm::WlShm, wl_shm_pool::WlShmPool, wl_surface::WlSurface, }, Connection, Dispatch, EventQueue, Proxy, QueueHandle, }; 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>); 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 = OnceLock::new(); SINK.get_or_init(DebugSink::new).writeln(s); } #[derive(Default)] struct ClientState { compositor: Option, shm: Option, wm_base: Option, pending_serial: Option, configured: bool, } impl Dispatch for ClientState { fn event( state: &mut Self, registry: &wl_registry::WlRegistry, event: wl_registry::Event, _data: &(), _conn: &Connection, qh: &QueueHandle, ) { 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, ) { } } }; } noop!(WlCompositor); noop!(WlShm); noop!(WlShmPool); noop!(WlBuffer); noop!(WlSurface); noop!(XdgToplevel); impl Dispatch for ClientState { fn event( _state: &mut Self, wm_base: &XdgWmBase, event: xdg_wm_base::Event, _: &(), _conn: &Connection, _qh: &QueueHandle, ) { if let xdg_wm_base::Event::Ping { serial } = event { wm_base.pong(serial); } } } impl Dispatch for ClientState { fn event( state: &mut Self, _xdg_surf: &XdgSurface, event: xdg_surface::Event, _: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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), Box> { wait_socket()?; let stream = UnixStream::connect(SOCKET_PATH)?; let backend = Backend::connect(stream)?; let conn = Connection::from_backend(backend); let mut event_queue: EventQueue = 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 { 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> { 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> { 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> { 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> { 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(label: &str, f: F) where F: FnOnce() -> Result<(), Box>, { 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> { 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 } } }