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
446 lines
14 KiB
Rust
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
|
|
}
|
|
}
|
|
}
|