Durée de vie d'un DEVICE_OBJECT et Remove Locks en 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:

Durée de vie d'un DEVICE_OBJECT et Remove Locks en WDM

Post by Hydraxx »

Durée de vie d'un DEVICE_OBJECT et Remove Locks en WDM

La suppression d'un périphérique est l'un des endroits les plus dangereux d'un driver WDM.

Le problème est simple à énoncer : le PnP Manager peut demander la suppression alors que du code du driver est encore en train de travailler.

Une IRP, un timer, un DPC ou un callback peut encore utiliser la device extension au moment où un autre thread tente IoDeleteDevice.

Ce cours explique comment IO_REMOVE_LOCK évite ce type de use-after-free.

1. Le problème de lifetime

Imaginez :

Code: Select all

CPU 0                                CPU 1
-----                                -----
DispatchRead                         IRP_MN_REMOVE_DEVICE
  |                                     |
  +--> lance une opération               +--> IoDeleteDevice
  |                                     |
  +--> DPC plus tard                    DEVICE_EXTENSION libérée
        |
        +--> ext->RegisterBase
             ^
             |
          mémoire détruite
Le DPC accède maintenant à de la mémoire libérée.

Le bug peut produire :
  • BSOD
  • corruption mémoire
  • double free
  • appel de fonction dans une image de driver déchargée
2. Principe de IO_REMOVE_LOCK

IO_REMOVE_LOCK est un mécanisme de comptage des opérations qui empêchent la suppression finale du device.

Principe :

Code: Select all

Operation A -> Acquire -> compteur +1
Operation B -> Acquire -> compteur +1
Operation A -> Release -> compteur -1
REMOVE_DEVICE -> attente du compteur
Operation B -> Release -> compteur -1
compteur suffisamment bas -> suppression autorisée
Le remove lock ne « verrouille » pas le hardware comme un mutex. Il suit surtout le nombre de références logiques en cours.

3. Stocker le remove lock

Il est généralement placé dans la device extension :

Code: Select all

typedef struct _DEVICE_EXTENSION
{
    PDEVICE_OBJECT LowerDeviceObject;
    IO_REMOVE_LOCK RemoveLock;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
4. IoInitializeRemoveLock

Initialisation typique dans AddDevice :

Code: Select all

IoInitializeRemoveLock(
    &ext->RemoveLock,
    'LrmR',
    0,
    0
);
Paramètres conceptuels :
  • RemoveLock : adresse de IO_REMOVE_LOCK.
  • AllocateTag : tag non nul utile au suivi/debug.
  • MaxLockedMinutes : durée maximale attendue pour une acquisition, principalement utile avec Driver Verifier/debug.
  • HighWatermark : nombre maximal d'acquisitions attendu, également utile au diagnostic.
Les deux dernières valeurs peuvent être 0 pour désactiver ces limites.

5. IoAcquireRemoveLock

Avant de commencer une opération qui doit garder le device vivant :

Code: Select all

NTSTATUS status = IoAcquireRemoveLock(&ext->RemoveLock, Irp);

if (!NT_SUCCESS(status))
{
    Irp->IoStatus.Status = status;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return status;
}
Le tag est souvent le pointeur de l'IRP :

Code: Select all

Tag = Irp
Cela aide à associer une acquisition à une opération précise.

Après que la suppression finale a commencé, une nouvelle acquisition peut échouer avec STATUS_DELETE_PENDING.

6. IoReleaseRemoveLock

Quand l'opération ne dépend plus du DEVICE_OBJECT :

Code: Select all

IoReleaseRemoveLock(&ext->RemoveLock, Irp);
Chaque acquisition doit avoir une libération correspondante, sauf l'acquisition spéciale consommée par IoReleaseRemoveLockAndWait pendant REMOVE_DEVICE.

7. Le remove lock ne concerne pas seulement les IRP

Il doit aussi protéger toute référence temporaire à du code/état du périphérique :
  • timer armé ;
  • DPC qui sera exécuté plus tard ;
  • work item ;
  • callback enregistré auprès d'un autre composant ;
  • opération asynchrone interne ;
  • IRP créée/envoyée par le driver ;
  • attente hardware encore active.
Exemple avec un timer :

Code: Select all

NTSTATUS status = IoAcquireRemoveLock(&ext->RemoveLock, &ext->Timer);
if (NT_SUCCESS(status))
{
    KeSetTimer(&ext->Timer, dueTime, &ext->Dpc);
}
Puis le chemin qui garantit que le timer/DPC ne sera plus utilisé doit relâcher le même tag :

Code: Select all

IoReleaseRemoveLock(&ext->RemoveLock, &ext->Timer);
8. Acquire tôt dans la dispatch routine

Pattern :

Code: Select all

NTSTATUS DispatchDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    PDEVICE_EXTENSION ext = DeviceObject->DeviceExtension;

    NTSTATUS status = IoAcquireRemoveLock(&ext->RemoveLock, Irp);
    if (!NT_SUCCESS(status))
    {
        Irp->IoStatus.Status = status;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
        return status;
    }

    // traitement

    IoReleaseRemoveLock(&ext->RemoveLock, Irp);
    return status;
}
L'acquisition doit avoir lieu avant de commencer à utiliser l'état dont la durée de vie dépend du device.

9. IRP transmises sans completion routine

Si le driver transmet simplement une IRP et ne souhaite pas rester propriétaire après IoCallDriver :

Code: Select all

IoSkipCurrentIrpStackLocation(Irp);
NTSTATUS status = IoCallDriver(ext->LowerDeviceObject, Irp);
IoReleaseRemoveLock(&ext->RemoveLock, Irp);
return status;
Après IoCallDriver, il ne faut évidemment plus accéder à l'IRP si le contrat ne le permet pas ; la libération du remove lock concerne ici la référence logique du driver, pas une nouvelle utilisation de l'IRP.

10. IRP avec completion routine

Si le driver doit rester impliqué jusqu'à la completion du driver inférieur, le remove lock doit rester acquis pendant toute cette durée.

Pattern :

Code: Select all

NTSTATUS CompletionRoutine(
    PDEVICE_OBJECT DeviceObject,
    PIRP Irp,
    PVOID Context)
{
    PDEVICE_EXTENSION ext = (PDEVICE_EXTENSION)Context;

    // traitement de completion

    IoReleaseRemoveLock(&ext->RemoveLock, Irp);
    return STATUS_CONTINUE_COMPLETION;
}
Dans la dispatch routine :

Code: Select all

IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(
    Irp,
    CompletionRoutine,
    ext,
    TRUE,
    TRUE,
    TRUE
);

return IoCallDriver(ext->LowerDeviceObject, Irp);
Le lock n'est pas relâché immédiatement après IoCallDriver, car la completion routine doit encore accéder au contexte.

11. IRP_MN_REMOVE_DEVICE et IoReleaseRemoveLockAndWait

C'est le moment critique.

Le driver acquiert normalement le remove lock au début de DispatchPnP, y compris pour IRP_MN_REMOVE_DEVICE.

Le schéma recommandé est :
  • Acquérir le remove lock pour REMOVE_DEVICE.
  • Préparer l'arrêt des nouvelles opérations.
  • Transmettre l'IRP_MN_REMOVE_DEVICE au driver inférieur.
  • Appeler IoReleaseRemoveLockAndWait.
  • Attendre ainsi toutes les autres acquisitions encore vivantes.
  • Libérer les ressources internes.
  • IoDetachDevice.
  • IoDeleteDevice.
Exemple simplifié :

Code: Select all

case IRP_MN_REMOVE_DEVICE:
{
    IoSkipCurrentIrpStackLocation(Irp);
    NTSTATUS status = IoCallDriver(ext->LowerDeviceObject, Irp);

    IoReleaseRemoveLockAndWait(&ext->RemoveLock, Irp);

    IoDetachDevice(ext->LowerDeviceObject);
    IoDeleteDevice(DeviceObject);

    return status;
}
Point essentiel : IoReleaseRemoveLockAndWait doit être exécuté après avoir transmis le REMOVE_DEVICE au driver inférieur et avant de libérer la mémoire, détacher ou supprimer le device.

12. Que fait exactement IoReleaseRemoveLockAndWait ?

Cette routine :
  • libère l'acquisition correspondant à son Tag ;
  • marque le remove lock comme engagé dans la suppression ;
  • attend que toutes les autres acquisitions soient relâchées ;
  • empêche de nouvelles opérations de repartir normalement après le début de la suppression.
Après son retour, le driver doit considérer le périphérique comme définitivement non opérationnel.

Il ne faut pas réinitialiser le même remove lock pour « ressusciter » l'objet.

13. Remove lock et PnP state

Le remove lock et l'état PnP résolvent deux problèmes différents :
  • PnP state : faut-il accepter, staller ou refuser une opération ?
  • Remove lock : existe-t-il encore du code qui doit empêcher la destruction finale ?
Exemple :

Code: Select all

State = SurpriseRemoved
=> refuser une nouvelle lecture hardware

RemoveLock count > 0
=> un DPC / IRP / callback existant n'a pas encore terminé
=> ne pas IoDeleteDevice
14. Remove lock et queues

Une IRP mise en queue peut garder un remove lock acquis pendant tout son séjour dans la queue.

Quand elle est :
  • traitée normalement : release à la fin ;
  • annulée : release dans le chemin d'annulation/completion ;
  • purge après surprise removal : release pour chaque IRP terminée ;
Sinon IoReleaseRemoveLockAndWait peut attendre indéfiniment.

15. Le problème du driver unload

La destruction d'un DEVICE_OBJECT et le déchargement de l'image du driver sont liés mais distincts.

Un driver peut avoir supprimé un device tout en conservant d'autres devices.

Inversement, l'image du driver ne doit jamais être déchargée tant qu'un callback, timer, DPC ou completion routine peut encore sauter dans son code.

Le principe général reste :

Code: Select all

Retirer les callbacks
Annuler les timers
Vider/attendre les DPC et work items selon le mécanisme
Terminer les IRP
Attendre les remove locks
Détacher
Supprimer les DEVICE_OBJECT
Puis seulement permettre l'unload final
16. API essentielles
  • IO_REMOVE_LOCK : structure de suivi des références empêchant la suppression.
  • IoInitializeRemoveLock : initialise le remove lock.
  • IoAcquireRemoveLock : prend une référence logique ; peut échouer après DELETE_PENDING.
  • IoReleaseRemoveLock : relâche une référence.
  • IoReleaseRemoveLockAndWait : utilisé dans REMOVE_DEVICE pour attendre toutes les références restantes.
  • IoDetachDevice : détache le DEVICE_OBJECT de la stack.
  • IoDeleteDevice : détruit le DEVICE_OBJECT.
  • IoSetCompletionRoutine : permet de conserver une implication jusqu'à la completion inférieure.
17. Erreurs fréquentes
  • Acquérir un remove lock et oublier de le relâcher.
  • Relâcher deux fois le même tag/opération.
  • Utiliser un tag différent entre Acquire et Release.
  • Appeler IoReleaseRemoveLockAndWait sans acquisition préalable.
  • Appeler IoReleaseRemoveLockAndWait avant de transmettre REMOVE_DEVICE au driver inférieur.
  • Appeler IoDeleteDevice avant que les opérations en cours aient fini.
  • Oublier que timers, DPC, callbacks et work items sont aussi des références au code/état du driver.
  • Réinitialiser le remove lock après IoReleaseRemoveLockAndWait.
18. Résumé

IO_REMOVE_LOCK transforme la question :

Code: Select all

"Est-ce que je peux supprimer ce device ?"
en :

Code: Select all

"Toutes les opérations qui dépendent encore de ce device ont-elles relâché leur référence ?"
C'est un mécanisme fondamental pour éviter que le PnP ne détruise un DEVICE_OBJECT pendant qu'une autre partie du driver l'utilise encore.

Who is online

Users browsing this forum: No registered users and 1 guest