Files d'attente et gestion des IRP dans un driver WDM

Ce cours dédiée au développement de drivers en kernel mode sans Framework

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 144
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Files d'attente et gestion des IRP dans un driver WDM

Post by Hydraxx »

Files d'attente et gestion des IRP dans un driver WDM

Un driver ne peut pas toujours traiter immédiatement chaque IRP. Le périphérique peut être occupé, stoppé, en cours de suppression, ou le driver peut volontairement sérialiser les opérations.

Il faut alors mettre l'IRP en attente, conserver sa propriété, permettre son annulation et le reprendre plus tard sans race condition.

Ce cours présente les principaux modèles WDM de queue, en distinguant les primitives natives de Windows des abstractions pédagogiques comme DEVQUEUE souvent rencontrées dans les anciens exemples WDM.

1. Pourquoi mettre une IRP en file ?

Exemples :
  • Un périphérique ne peut exécuter qu'une commande à la fois.
  • Une lecture attend que le hardware produise une donnée.
  • Le driver attend une interruption.
  • Le périphérique est temporairement stoppé par le PnP.
  • Une requête doit être différée jusqu'à ce qu'une ressource soit disponible.
Le driver doit alors décider :

Code: Select all

IRP reçue
   |
   +--> traiter immédiatement
   |
   +--> transmettre au driver inférieur
   |
   +--> mettre en attente
2. Règle fondamentale : STATUS_PENDING

Lorsqu'un driver conserve une IRP pour la compléter plus tard :

Code: Select all

IoMarkIrpPending(Irp);
return STATUS_PENDING;
IoMarkIrpPending indique au système que l'IRP survivra au retour de la dispatch routine.

Une fois l'IRP complétée avec IoCompleteRequest, le driver ne doit plus l'utiliser.

3. Propriété d'une IRP

Le problème central des queues est la propriété.

Tant qu'un driver conserve une IRP en attente, il doit savoir :
  • Qui peut la compléter ?
  • Qui peut l'annuler ?
  • Dans quelle liste elle se trouve ?
  • Quel verrou protège cette liste ?
  • Que se passe-t-il si l'annulation arrive exactement au moment où la requête sort de la file ?
C'est ce dernier point qui rend les cancel routines manuelles délicates.

4. File manuelle avec LIST_ENTRY

Une file très simple peut être construite avec LIST_ENTRY :

Code: Select all

typedef struct _DEVICE_EXTENSION
{
    LIST_ENTRY PendingIrpList;
    KSPIN_LOCK QueueLock;
} DEVICE_EXTENSION;
Initialisation :

Code: Select all

InitializeListHead(&ext->PendingIrpList);
KeInitializeSpinLock(&ext->QueueLock);
Insertion conceptuelle :

Code: Select all

KIRQL oldIrql;
KeAcquireSpinLock(&ext->QueueLock, &oldIrql);
InsertTailList(&ext->PendingIrpList, &Irp->Tail.Overlay.ListEntry);
KeReleaseSpinLock(&ext->QueueLock, oldIrql);
Mais cette version n'est pas suffisante si l'IRP peut être annulée. Il faut synchroniser la liste avec le mécanisme de cancellation de l'I/O Manager.

5. Pourquoi IoSetCancelRoutine est délicat

L'API historique :

IoSetCancelRoutine

permet d'associer une cancel routine à une IRP.

Mais une race classique existe :

Code: Select all

CPU 0                               CPU 1
-----                               -----
retire l'IRP de la queue            annulation demandée
IoSetCancelRoutine(Irp, NULL)   <-> I/O Manager lance CancelRoutine
Si le code ne respecte pas exactement les règles de synchronisation, deux threads peuvent croire qu'ils possèdent l'IRP et la compléter deux fois.

Résultat possible :
  • double completion
  • use-after-free
  • corruption de liste
  • BSOD
Pour cette raison, Windows fournit les Cancel-Safe Queues.

6. IO_CSQ : Cancel-Safe Queue

Une IO_CSQ délègue à l'I/O Manager la synchronisation critique de l'annulation.

Structure principale :

Code: Select all

IO_CSQ Csq;
Initialisation :

IoCsqInitialize

ou :

IoCsqInitializeEx

Le driver fournit plusieurs callbacks.

7. Callbacks d'une Cancel-Safe Queue

Avec IoCsqInitialize, le driver fournit :
  • PIO_CSQ_INSERT_IRP : insérer une IRP dans la structure de données du driver.
  • PIO_CSQ_REMOVE_IRP : retirer une IRP précise.
  • PIO_CSQ_PEEK_NEXT_IRP : trouver la prochaine IRP répondant au critère demandé.
  • PIO_CSQ_ACQUIRE_LOCK : acquérir le verrou de la queue.
  • PIO_CSQ_RELEASE_LOCK : relâcher le verrou.
  • PIO_CSQ_COMPLETE_CANCELED_IRP : terminer une IRP annulée.
Le framework IO_CSQ orchestre ces callbacks afin que l'annulation et le retrait d'une IRP de la queue ne se disputent pas la propriété.

8. Initialisation d'une IO_CSQ

Exemple de structure :

Code: Select all

typedef struct _DEVICE_EXTENSION
{
    IO_CSQ Csq;
    LIST_ENTRY PendingIrps;
    KSPIN_LOCK QueueLock;
} DEVICE_EXTENSION;
Initialisation :

Code: Select all

InitializeListHead(&ext->PendingIrps);
KeInitializeSpinLock(&ext->QueueLock);

NTSTATUS status = IoCsqInitialize(
    &ext->Csq,
    CsqInsertIrp,
    CsqRemoveIrp,
    CsqPeekNextIrp,
    CsqAcquireLock,
    CsqReleaseLock,
    CsqCompleteCanceledIrp
);
9. Insérer et retirer des IRP avec IO_CSQ

Insertion :

Code: Select all

IoMarkIrpPending(Irp);
IoCsqInsertIrp(&ext->Csq, Irp, NULL);
return STATUS_PENDING;
Retirer la prochaine IRP :

Code: Select all

PIRP irp = IoCsqRemoveNextIrp(&ext->Csq, NULL);

if (irp != NULL)
{
    irp->IoStatus.Status = STATUS_SUCCESS;
    irp->IoStatus.Information = 0;
    IoCompleteRequest(irp, IO_NO_INCREMENT);
}
Retirer une IRP connue :

IoCsqRemoveIrp

La variante Ex propose notamment :
  • IoCsqInsertIrpEx
  • IoCsqInitializeEx
utile lorsque l'insertion elle-même doit retourner un statut ou utiliser un contexte plus riche.

10. Modèle StartIo

Le WDM historique propose aussi le modèle StartIo.

Le driver enregistre :

Code: Select all

DriverObject->DriverStartIo = StartIo;
Les requêtes sont démarrées avec :

IoStartPacket

et le driver passe à la suivante avec :

IoStartNextPacket

ou :

IoStartNextPacketByKey

Ce modèle sérialise naturellement les IRP pour un périphérique qui ne traite qu'une requête à la fois.

Exemple conceptuel :

Code: Select all

DispatchReadWrite(...)
{
    IoMarkIrpPending(Irp);
    IoStartPacket(DeviceObject, Irp, NULL, CancelRoutine);
    return STATUS_PENDING;
}

StartIo(DeviceObject, Irp)
{
    // programmer le hardware pour CETTE IRP
}

ISR/DPC/fin hardware
{
    // compléter l'IRP courante
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    IoStartNextPacket(DeviceObject, FALSE);
}
Le modèle StartIo est important à connaître pour lire du code WDM historique, même si beaucoup de nouveaux drivers privilégient des abstractions plus modernes ou des queues explicites.

11. Stalled, accepting et rejecting

Pour intégrer une queue avec le PnP, il est utile de distinguer trois états logiques :
  • Accepting : les nouvelles requêtes peuvent être démarrées.
  • Stalled : les nouvelles requêtes sont conservées mais ne démarrent pas.
  • Rejecting : les nouvelles requêtes sont refusées car le périphérique n'est plus utilisable.
Ce ne sont pas des états imposés par une structure WDM standard : c'est une manière de modéliser proprement le comportement du driver.

12. Interaction avec QUERY_STOP

Lors d'un QUERY_STOP :

Code: Select all

Started
   |
QUERY_STOP
   v
Stall queue
   |
attendre l'opération active si nécessaire
   |
StopPending
Une IRP reçue pendant cette période peut être placée dans la queue mais ne doit pas lancer une nouvelle opération matérielle.

13. Interaction avec CANCEL_STOP

CANCEL_STOP signifie que l'arrêt n'aura finalement pas lieu.

Le driver :
  • repasse en état Started ;
  • débloque la queue ;
  • démarre la prochaine IRP si le hardware est libre.
14. Interaction avec SURPRISE_REMOVAL

Après SURPRISE_REMOVAL, le hardware n'est plus disponible.

La queue ne doit donc pas simplement rester stallée : les IRP qui nécessitent le matériel doivent être terminées avec un statut d'échec approprié.

Exemple conceptuel :

Code: Select all

while ((irp = IoCsqRemoveNextIrp(&ext->Csq, NULL)) != NULL)
{
    irp->IoStatus.Status = STATUS_DEVICE_REMOVED;
    irp->IoStatus.Information = 0;
    IoCompleteRequest(irp, IO_NO_INCREMENT);
}
15. Attendre l'IRP courante

Certains drivers doivent attendre qu'une opération active se termine avant STOP_DEVICE.

Une stratégie classique combine :
  • un pointeur CurrentIrp ;
  • un spin lock protégeant l'état ;
  • un KEVENT signalé lorsque CurrentIrp devient NULL.
APIs utiles :
  • KeInitializeEvent
  • KeSetEvent
  • KeClearEvent
  • KeWaitForSingleObject
Attention : une attente bloquante doit respecter l'IRQL et ne doit pas être faite sous spin lock.

16. IoCancelIrp

IoCancelIrp demande l'annulation d'une IRP lorsque le driver possède légitimement un pointeur vers cette IRP et que le protocole concerné autorise son annulation.

Cette fonction ne signifie pas « détruire immédiatement l'IRP ». Elle déclenche le mécanisme de cancellation prévu pour l'IRP ; la terminaison peut rester asynchrone.

17. DEVQUEUE des livres WDM

Certains livres, dont les anciens exemples WDM, utilisent une abstraction appelée DEVQUEUE avec des helpers comme :

Code: Select all

StartPacket
StartNextPacket
StallRequests
RestartRequests
AbortRequests
CleanupRequests
WaitForCurrentIrp
DEVQUEUE n'est pas un objet natif du noyau Windows.

C'est une abstraction de bibliothèque/exemple qui encapsule une file, un spin lock, l'IRP courante et les transitions PnP.

Les concepts sont très utiles, mais il faut les distinguer des vraies APIs WDM comme IO_CSQ, IoStartPacket ou IoSetCancelRoutine.

18. API essentielles du cours
  • IoMarkIrpPending : marque une IRP conservée pour traitement asynchrone.
  • IoCompleteRequest : termine une IRP.
  • IoSetCancelRoutine : installe/retire une cancel routine ; délicat à utiliser directement.
  • IoCancelIrp : demande l'annulation d'une IRP.
  • IoCsqInitialize / IoCsqInitializeEx : initialise une cancel-safe queue.
  • IoCsqInsertIrp / IoCsqInsertIrpEx : insère une IRP dans une IO_CSQ.
  • IoCsqRemoveNextIrp : retire la prochaine IRP correspondant au contexte.
  • IoCsqRemoveIrp : retire une IRP précise.
  • IoStartPacket : enfile/démarre une IRP avec le modèle StartIo.
  • IoStartNextPacket : démarre la requête suivante du modèle StartIo.
  • IoStartNextPacketByKey : sélectionne la prochaine IRP par clé.
  • InitializeListHead / InsertTailList / RemoveHeadList / RemoveEntryList : primitives de LIST_ENTRY pour les files internes.
  • KeInitializeSpinLock / KeAcquireSpinLock / KeReleaseSpinLock : synchronisation d'une queue partagée à IRQL élevé.
  • KeInitializeEvent / KeSetEvent / KeWaitForSingleObject : coordination PASSIVE_LEVEL lors des transitions.
19. Erreurs fréquentes
  • Retourner STATUS_PENDING sans IoMarkIrpPending lorsque le driver prend réellement l'IRP en charge asynchroniquement.
  • Compléter deux fois la même IRP.
  • Utiliser l'IRP après IoCompleteRequest.
  • Construire une cancel routine naïve sans maîtriser les races.
  • Attendre un KEVENT sous spin lock.
  • Démarrer une nouvelle opération hardware pendant StopPending/RemovePending.
  • Laisser des IRP dormir éternellement dans la queue après SURPRISE_REMOVAL.
  • Présenter DEVQUEUE comme une API Windows alors qu'il s'agit d'une abstraction d'exemple.
20. Résumé

Une queue WDM correcte doit résoudre quatre problèmes :

Code: Select all

1. Stockage de l'IRP
2. Synchronisation de la queue
3. Cancellation
4. Transition PnP / suppression
Pour du code WDM manuel moderne, les Cancel-Safe Queues sont un mécanisme particulièrement important à connaître car elles réduisent fortement les races liées à IoSetCancelRoutine.

Who is online

Users browsing this forum: No registered users and 1 guest