redox-wayland-compositor/docs/phase7-5-robustness.md
Votre Nom 7e81dec637 🎉 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
2026-05-13 11:57:21 +02:00

8.5 KiB

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.