redox-wayland-compositor/crates/redox-wl-test-fuzz-protocol/src/main.rs
Votre Nom 8795f39f08 Sprint 0 — industrialisation : CI, tracing, tests xdg-shell, GPLv3
5 livrables d'industrialisation posés avant la phase 13 (client réel).

- .gitlab-ci.yml : pipeline minimal (fmt-check + tests core +
  tests frontend nightly). Pas de cross-compile Redox dans le MVP.
- rustfmt.toml + fmt.sh : config formatter racine + boucle sur les 23
  crates ; fmt sweep appliqué (d'où les diffs cosmétiques).
- tracing dans wayland-frontend : 22 println/eprintln remplacés par
  debug/info/warn/error selon sévérité. Préfixe "[frontend]"
  supprimé (le target tracing le fournit).
- tracing-subscriber dans le compositor : TeeWriter écrit sur
  stdout + /scheme/debug, filtre via RUST_LOG (défaut info).
  DebugSink/dlog supprimés.
- 15 tests unitaires xdg-shell après extraction de 2 helpers libres
  (clamp_to_min_max + should_throttle_configure). Couvre
  compute_resize_geom, contraintes min/max et throttling configure.
- LICENSE (GPLv3 texte officiel FSF) + .editorconfig.

Total tests : 27 → 42 automatisés (compositor-core + frontend).

Leyoda 2026 – GPLv3
2026-05-14 20:46:07 +02:00

446 lines
14 KiB
Rust

//! 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<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
}
}
}