🎉 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:
Votre Nom 2026-05-13 11:57:21 +02:00
parent c40ca9fcc8
commit 7e81dec637
5 changed files with 731 additions and 18 deletions

View file

@ -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>>() {