But du cours : comprendre les races les plus dangereuses du traitement IRP : annulation pendant qu’un IRP est queued, retrait concurrent, timeout, cleanup d’un handle et synchronisation de fin de vie. L’objectif est de savoir non seulement quelles API appeler, mais pourquoi elles existent et quel problème précis elles résolvent.
1. Pourquoi la cancellation est une machine à états
Un IRP queued peut basculer vers plusieurs états :
Code: Select all
QUEUED
|
+--> ACTIVE
|
+--> CANCELLED
|
+--> CLEANUP
Exemple :
Code: Select all
CPU 0 CPU 1
RemoveHeadList IoCancelIrp
| |
v v
pense posséder IRP CancelRoutine
| |
v v
IoCompleteRequest IoCompleteRequest
La cancellation correcte revient à garantir qu’un seul chemin obtient la propriété finale de l’IRP.
IoSetCancelRoutine
À quoi sert cette API ?
Installe ou retire la routine d’annulation associée à un IRP. L’I/O Manager peut appeler cette routine lorsqu’un thread ou un composant demande l’annulation de l’IRP.
Quand l’utiliser ?
À utiliser lorsqu’un IRP peut rester en attente dans une queue que le driver contrôle.
Paramètres importants :
- : IRP concerné.
Code: Select all
Irp - : callback ou NULL pour retirer la routine.
Code: Select all
CancelRoutine
Retourne l’ancienne cancel routine. Cette valeur est importante dans certains protocoles de synchronisation.
IRQL / contexte :
Le protocole de cancellation historique implique le cancel spin lock et un contexte élevé.
Propriété de l’IRP / durée de vie :
Le fait d’installer une CancelRoutine signifie que le driver accepte d’être rappelé pour cet IRP tant que la routine reste installée.
Pièges classiques :
- Mettre CancelRoutine à NULL sans synchronisation avec IoCancelIrp.
- Supposer qu’un retour NULL signifie toujours la même chose sans regarder Irp->Cancel.
- Oublier de retirer la routine lorsque l’IRP devient actif.
Code: Select all
PDRIVER_CANCEL oldCancel =
IoSetCancelRoutine(
Irp,
CancelRoutine
);
À quoi sert cette API ?
Demande l’annulation d’un IRP. L’I/O Manager marque l’IRP comme annulé et, si une cancel routine est installée, orchestre son appel selon le protocole de cancellation.
Quand l’utiliser ?
À utiliser lorsqu’un driver veut annuler un IRP qu’il a créé ou qu’il contrôle encore, par exemple après timeout.
Paramètres importants :
- : IRP à annuler.
Code: Select all
Irp
Retourne un BOOLEAN indiquant si une cancel routine a été appelée/était présente selon le chemin. Cela ne veut pas dire que toute la complétion est déjà finie.
IRQL / contexte :
Peut être invoquée dans des contextes noyau variés ; le code autour doit respecter le modèle de l’IRP.
Propriété de l’IRP / durée de vie :
Après IoCancelIrp, il faut souvent attendre la vraie fin de l’IRP avant de libérer les ressources. L’appel n’est pas une destruction instantanée.
Pièges classiques :
- Appeler IoCancelIrp puis IoFreeIrp immédiatement.
- Considérer TRUE/FALSE comme une preuve absolue que l’IRP est déjà libéré.
- Annuler un IRP dont on ne contrôle plus la durée de vie.
Code: Select all
if (timeoutExpired)
{
IoCancelIrp(Irp);
// attendre ensuite la vraie fin via
// événement / completion routine
}
À quoi sert cette API ?
Acquiert le verrou global historique utilisé par l’I/O Manager pour synchroniser la manipulation des champs de cancellation de l’IRP.
Quand l’utiliser ?
À utiliser uniquement dans les protocoles historiques de CancelRoutine manuelle qui l’exigent.
Paramètres importants :
- : reçoit l’IRQL précédent.
Code: Select all
Irql
Pas de valeur de retour.
IRQL / contexte :
Élève l’IRQL au niveau requis par le cancel spin lock.
Propriété de l’IRP / durée de vie :
Ce lock ne protège pas automatiquement votre queue privée. Il synchronise surtout l’état de cancellation de l’IRP.
Pièges classiques :
- Le combiner avec un QueueLock dans un ordre incohérent.
- Conserver le lock trop longtemps.
- L’utiliser alors qu’une Cancel-Safe Queue éliminerait le besoin du protocole manuel.
Code: Select all
KIRQL oldIrql;
IoAcquireCancelSpinLock(
&oldIrql
);
// opérations très courtes
IoReleaseCancelSpinLock(
oldIrql
);
À quoi sert cette API ?
Libère le cancel spin lock et restaure l’IRQL précédent.
Quand l’utiliser ?
À utiliser exactement selon le protocole associé à l’acquisition ou à l’entrée d’une CancelRoutine.
Paramètres importants :
- : IRQL à restaurer.
Code: Select all
Irql
Pas de valeur de retour.
IRQL / contexte :
Restaure l’IRQL.
Propriété de l’IRP / durée de vie :
Après libération, un autre CPU peut immédiatement continuer une opération de cancellation.
Pièges classiques :
- Restaurer un mauvais IRQL.
- Toucher encore des champs supposés protégés comme si le lock était détenu.
Code: Select all
IoReleaseCancelSpinLock(
Irp->CancelIrql
);
La CancelRoutine ne doit pas être vue comme « une fonction appelée si Cancel vaut TRUE ».
Elle représente le chemin qui prend possession d’un IRP annulé encore sous contrôle du driver.
Elle doit généralement :
- synchroniser l’accès à la queue ;
- retirer l’IRP s’il y est encore ;
- libérer les verrous ;
- positionner STATUS_CANCELLED ;
- mettre Information à 0 ;
- compléter l’IRP.
Code: Select all
VOID CancelRoutine(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
{
PDEVICE_EXTENSION devExt =
DeviceObject->DeviceExtension;
KIRQL cancelIrql =
Irp->CancelIrql;
IoReleaseCancelSpinLock(
cancelIrql
);
// prendre le verrou privé de queue,
// vérifier que l'IRP est toujours queued,
// le retirer exactement une fois.
Irp->IoStatus.Status =
STATUS_CANCELLED;
Irp->IoStatus.Information = 0;
IoCompleteRequest(
Irp,
IO_NO_INCREMENT
);
}
4. Pourquoi Irp->Cancel seul ne suffit pas
Code naïf :
Code: Select all
if (Irp->Cancel)
{
CompleteCancelled(Irp);
}
else
{
InsertTailList(...);
}
Code: Select all
CPU 0 CPU 1
lit Cancel == FALSE
IoCancelIrp()
Cancel = TRUE
InsertTailList()
La vérification et l’installation du mécanisme de cancellation doivent donc être atomiques du point de vue logique.
5. Transition QUEUED -> ACTIVE
Quand StartPacket retire un IRP de la queue pour le rendre actif, il faut désactiver sa CancelRoutine de queue.
Sinon :
Code: Select all
IRP devient actif
|
+--> matériel démarre
|
+--> IoCancelIrp voit encore CancelRoutine
|
v
CancelRoutine essaie de
retirer un IRP qui n'est
plus dans la queue
6. InterlockedExchangePointer comme arbitre de propriété
Une technique générale consiste à stocker le PIRP dans un emplacement partagé et à utiliser :
Code: Select all
InterlockedExchangePointer
Exemple :
Code: Select all
PIRP irp =
(PIRP)InterlockedExchangePointer(
(PVOID*)&devExt->CurrentIrp,
NULL
);
if (irp != NULL)
{
// ce chemin est devenu propriétaire
}
- un seul récupère le vrai pointeur ;
- l’autre récupère NULL.
7. Pourquoi les Cancel-Safe Queues existent
Les Cancel-Safe Queues encapsulent le problème :
Code: Select all
queue privée
+
verrou
+
cancellation
+
retrait
+
énumération
IoCsqInitialize
À quoi sert cette API ?
Initialise une Cancel-Safe Queue en enregistrant les callbacks fournis par le driver pour insérer, retirer, rechercher, verrouiller et compléter les IRP annulés.
Quand l’utiliser ?
À utiliser lorsqu’un driver conserve des IRP annulables dans une queue pendant une durée non triviale.
Paramètres importants :
- PIO_CSQ.
- CsqInsertIrp.
- CsqRemoveIrp.
- CsqPeekNextIrp.
- CsqAcquireLock.
- CsqReleaseLock.
- CsqCompleteCanceledIrp.
Retourne un NTSTATUS.
IRQL / contexte :
Généralement appelée pendant l’initialisation du device, à PASSIVE_LEVEL.
Propriété de l’IRP / durée de vie :
L’IO_CSQ et toutes les structures qu’elle référence doivent rester valides pendant toute la durée de vie de la queue.
Pièges classiques :
- Callbacks qui n’utilisent pas tous le même lock.
- Callback PeekNextIrp incohérent avec RemoveIrp.
- Libérer la device extension alors que des IRP restent dans la CSQ.
Code: Select all
status = IoCsqInitialize(
&devExt->Csq,
CsqInsertIrp,
CsqRemoveIrp,
CsqPeekNextIrp,
CsqAcquireLock,
CsqReleaseLock,
CsqCompleteCanceledIrp
);
But : insérer physiquement l’IRP dans la structure du driver.
Code: Select all
VOID CsqInsertIrp(
PIO_CSQ Csq,
PIRP Irp
)
{
PDEVICE_EXTENSION devExt =
CONTAINING_RECORD(
Csq,
DEVICE_EXTENSION,
Csq
);
InsertTailList(
&devExt->IrpQueue,
&Irp->Tail.Overlay.ListEntry
);
}
9. Callback CsqRemoveIrp
But : retirer physiquement l’IRP de la structure.
Code: Select all
VOID CsqRemoveIrp(
PIO_CSQ Csq,
PIRP Irp
)
{
UNREFERENCED_PARAMETER(Csq);
RemoveEntryList(
&Irp->Tail.Overlay.ListEntry
);
}
C’est un des callbacks les plus importants.
Il sert à :
- parcourir la queue ;
- trouver le premier IRP ;
- continuer après un IRP donné ;
- filtrer selon un contexte.
Code: Select all
PIRP CsqPeekNextIrp(
PIO_CSQ Csq,
PIRP Irp,
PVOID PeekContext
)
{
PDEVICE_EXTENSION devExt =
CONTAINING_RECORD(
Csq,
DEVICE_EXTENSION,
Csq
);
PFILE_OBJECT target =
(PFILE_OBJECT)PeekContext;
PLIST_ENTRY entry;
if (Irp == NULL)
{
entry =
devExt->IrpQueue.Flink;
}
else
{
entry =
Irp->Tail.Overlay.ListEntry.Flink;
}
while (entry != &devExt->IrpQueue)
{
PIRP current =
CONTAINING_RECORD(
entry,
IRP,
Tail.Overlay.ListEntry
);
PIO_STACK_LOCATION stack =
IoGetCurrentIrpStackLocation(
current
);
if (target == NULL ||
stack->FileObject == target)
{
return current;
}
entry = entry->Flink;
}
return NULL;
}
11. AcquireLock / ReleaseLock
Ces callbacks définissent le verrou réellement utilisé par la queue.
Code: Select all
VOID CsqAcquireLock(
PIO_CSQ Csq,
PKIRQL Irql
)
{
PDEVICE_EXTENSION devExt =
CONTAINING_RECORD(
Csq,
DEVICE_EXTENSION,
Csq
);
KeAcquireSpinLock(
&devExt->QueueLock,
Irql
);
}
VOID CsqReleaseLock(
PIO_CSQ Csq,
KIRQL Irql
)
{
PDEVICE_EXTENSION devExt =
CONTAINING_RECORD(
Csq,
DEVICE_EXTENSION,
Csq
);
KeReleaseSpinLock(
&devExt->QueueLock,
Irql
);
}
12. CompleteCanceledIrp
Ce callback est appelé lorsque la CSQ détermine qu’un IRP doit être complété comme annulé.
Code: Select all
VOID CsqCompleteCanceledIrp(
PIO_CSQ Csq,
PIRP Irp
)
{
UNREFERENCED_PARAMETER(Csq);
Irp->IoStatus.Status =
STATUS_CANCELLED;
Irp->IoStatus.Information = 0;
IoCompleteRequest(
Irp,
IO_NO_INCREMENT
);
}
À quoi sert cette API ?
Insère un IRP dans la Cancel-Safe Queue en laissant l’infrastructure CSQ gérer la coordination avec l’annulation.
Quand l’utiliser ?
À utiliser lorsqu’un IRP doit rester pending dans une queue annulable.
Paramètres importants :
- : queue.
Code: Select all
Csq - : IRP à insérer.
Code: Select all
Irp - : contexte optionnel pour certaines variantes.
Code: Select all
InsertContext
Pas de NTSTATUS de retour pour la variante de base.
IRQL / contexte :
Le helper orchestre les callbacks de verrouillage appropriés.
Propriété de l’IRP / durée de vie :
Après insertion, l’IRP appartient à la queue jusqu’à ce qu’il soit retiré, annulé ou nettoyé.
Pièges classiques :
- Toucher l’IRP après insertion comme s’il était encore local.
- Compléter l’IRP sans l’avoir retiré de la CSQ.
Code: Select all
IoMarkIrpPending(Irp);
IoCsqInsertIrp(
&devExt->Csq,
Irp,
NULL
);
return STATUS_PENDING;
À quoi sert cette API ?
Retire le prochain IRP correspondant éventuellement à un contexte de recherche. La CSQ coordonne le retrait avec la cancellation pour que l’IRP ne soit pas simultanément pris par une CancelRoutine.
Quand l’utiliser ?
À utiliser pour dépiler le prochain IRP à traiter, ou pour retirer successivement tous les IRP associés à un FileObject pendant CLEANUP.
Paramètres importants :
- : queue.
Code: Select all
Csq - : filtre transmis au callback PeekNextIrp, peut être NULL.
Code: Select all
PeekContext
Retourne un PIRP ou NULL si aucun IRP correspondant.
IRQL / contexte :
Le helper appelle les callbacks de verrouillage de la CSQ.
Propriété de l’IRP / durée de vie :
Si un PIRP non NULL est retourné, le code appelant devient responsable du traitement/complétion de cet IRP.
Pièges classiques :
- Supposer qu’un pointeur non NULL est encore dans la queue.
- Ne pas compléter ou transmettre l’IRP après retrait.
Code: Select all
PIRP irp =
IoCsqRemoveNextIrp(
&devExt->Csq,
fileObject
);
if (irp != NULL)
{
// le driver possède maintenant l'IRP
}
« Parker » un IRP signifie le conserver pendant une attente logique.
Exemples :
- attendre une donnée reçue plus tard ;
- attendre qu’un périphérique se libère ;
- attendre un événement produit par un autre thread ;
- implémenter une lecture bloquante.
14. Timeout : ce que signifie vraiment STATUS_TIMEOUT
Exemple :
Code: Select all
status = KeWaitForSingleObject(
&event,
Executive,
KernelMode,
FALSE,
&timeout
);
Code: Select all
STATUS_TIMEOUTCode: Select all
l'événement n'a pas été signalé avant l'expiration
Code: Select all
l'IRP a été détruit
- être en train de traiter l’IRP ;
- le compléter une microseconde plus tard ;
- exécuter une completion routine.
- demander l’annulation ;
- attendre que la vraie fin soit confirmée ;
- ensuite seulement libérer les ressources.
Code: Select all
status = IoCallDriver(
lower,
Irp
);
if (status == STATUS_PENDING)
{
status = KeWaitForSingleObject(
&event,
Executive,
KernelMode,
FALSE,
&timeout
);
if (status == STATUS_TIMEOUT)
{
IoCancelIrp(Irp);
KeWaitForSingleObject(
&event,
Executive,
KernelMode,
FALSE,
NULL
);
}
}
16. Completion routine et STATUS_MORE_PROCESSING_REQUIRED
Lorsqu’un driver veut attendre un IRP qu’il a envoyé, une completion routine peut :
- signaler un événement ;
- empêcher la continuation immédiate du cleanup ;
- retourner STATUS_MORE_PROCESSING_REQUIRED.
Code: Select all
NTSTATUS WaitCompletion(
PDEVICE_OBJECT DeviceObject,
PIRP Irp,
PVOID Context
)
{
UNREFERENCED_PARAMETER(DeviceObject);
PKEVENT event =
(PKEVENT)Context;
if (Irp->PendingReturned)
{
IoMarkIrpPending(Irp);
}
KeSetEvent(
event,
IO_NO_INCREMENT,
FALSE
);
return STATUS_MORE_PROCESSING_REQUIRED;
}
17. IRP_MJ_CLEANUP : fonction réelle
Quand le dernier handle utilisateur vers un FileObject est fermé, l’I/O Manager envoie un IRP_MJ_CLEANUP.
Le but principal est :
annuler ou purger les opérations encore associées à ce FileObject.
Ce n’est pas simplement « fermer le driver ».
18. Différence CLEANUP / CLOSE
Code: Select all
CreateFile
|
v
IRP_MJ_CREATE
|
v
lectures / IOCTL / requêtes pending
|
v
CloseHandle
|
v
IRP_MJ_CLEANUP
|
| purge des IRP du FileObject
v
plus tard
|
v
IRP_MJ_CLOSE
CLOSE correspond à la destruction/finalisation de l’objet fichier lorsque les références noyau restantes disparaissent.
19. Récupérer le FileObject de CLEANUP
Code: Select all
PIO_STACK_LOCATION stack =
IoGetCurrentIrpStackLocation(Irp);
PFILE_OBJECT fileObject =
stack->FileObject;
20. Cleanup avec CSQ
Pattern :
Code: Select all
PIRP queuedIrp;
while (
(queuedIrp =
IoCsqRemoveNextIrp(
&devExt->Csq,
fileObject
)) != NULL
)
{
queuedIrp->IoStatus.Status =
STATUS_CANCELLED;
queuedIrp->IoStatus.Information = 0;
IoCompleteRequest(
queuedIrp,
IO_NO_INCREMENT
);
}
Code: Select all
Irp->IoStatus.Status =
STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(
Irp,
IO_NO_INCREMENT
);
return STATUS_SUCCESS;
Un même DEVICE_OBJECT peut servir plusieurs handles :
Code: Select all
Handle A -> FileObject A -> IRP1, IRP2
Handle B -> FileObject B -> IRP3
Code: Select all
IRP1
IRP2
Code: Select all
IRP3
22. Direct I/O : nettoyage associé
Avec DO_DIRECT_IO, un IRP peut contenir :
Code: Select all
Irp->MdlAddress
Code: Select all
MmUnlockPages(mdl);
IoFreeMdl(mdl);
Ne jamais libérer un MDL simplement parce qu’il apparaît dans Irp->MdlAddress. Il faut savoir qui l’a alloué et qui doit le détruire.
23. Buffered I/O
Avec DO_BUFFERED_IO, l’I/O Manager peut utiliser :
Code: Select all
Irp->AssociatedIrp.SystemBuffer
24. Les 8 scénarios de gestion IRP
Scénario 1 — Pass down avec completion routine
À utiliser quand le driver veut laisser travailler le lower driver puis faire quelque chose au retour.
Code: Select all
IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(
Irp,
CompletionRoutine,
Context,
TRUE,
TRUE,
TRUE
);
return IoCallDriver(
lower,
Irp
);
À utiliser pour un vrai pass-through.
Code: Select all
IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(lower, Irp);
À utiliser si le driver peut répondre immédiatement.
Code: Select all
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
Code: Select all
IoMarkIrpPending(Irp);
IoCsqInsertIrp(&devExt->Csq, Irp, NULL);
return STATUS_PENDING;
Le driver construit une requête secondaire, l’envoie au lower driver, puis reçoit la fin dans une completion routine.
Scénario 6 — IRP synchrone créé par le driver
Le driver construit un IRP avec événement/IO_STATUS_BLOCK puis attend la fin.
Scénario 7 — IRP reçu, envoyé en bas puis attente synchrone
Le driver reçoit un IRP, installe une completion routine, le forwarde, attend un événement, fait son traitement local puis reprend la logique de complétion.
Scénario 8 — IRP asynchrone traité synchroniquement
Le driver construit un IRP asynchrone mais utilise une completion routine + événement pour attendre sa fin. Le caractère « asynchrone » décrit ici la construction/gestion de l’IRP, pas nécessairement l’absence d’attente côté appelant.
25. API essentielles de ce cours
Code: Select all
IoSetCancelRoutineCode: Select all
IoCancelIrpCode: Select all
IoAcquireCancelSpinLockCode: Select all
IoReleaseCancelSpinLockCode: Select all
InterlockedExchangePointerCode: Select all
IoCsqInitializeCode: Select all
IoCsqInsertIrpCode: Select all
IoCsqRemoveIrpCode: Select all
IoCsqRemoveNextIrpCode: Select all
KeWaitForSingleObjectCode: Select all
IoSetCompletionRoutineCode: Select all
IoMarkIrpPendingCode: Select all
IoCompleteRequestCode: Select all
IoGetCurrentIrpStackLocationCode: Select all
MmUnlockPagesCode: Select all
IoFreeMdl
- Double completion.
- Double retrait de queue.
- CancelRoutine encore installée après passage QUEUED -> ACTIVE.
- IoCancelIrp suivi d’une libération immédiate.
- Confondre timeout avec fin réelle.
- Nettoyer les IRP d’un autre FileObject.
- Confondre CLEANUP et CLOSE.
- Libérer un MDL que le driver ne possède pas.
- Créer une queue annulable sans protocole de verrouillage.
Avant de garder un IRP en attente, répondre à :
- La requête peut-elle être annulée ?
- Où l’IRP est-il stocké ?
- Quel lock protège cette structure ?
- Qui retire la CancelRoutine quand l’IRP devient actif ?
- Que se passe-t-il si IoCancelIrp arrive exactement pendant le retrait ?
- Qui complète l’IRP annulé ?
- Que fait IRP_MJ_CLEANUP ?
- Comment un timeout attend-il la vraie fin ?
