Les NTSTATUS

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

Moderator: Rick

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

Les NTSTATUS

Post by Hydraxx »

Les codes NTSTATUS sous Windows

Les fonctions du noyau Windows et les drivers utilisent très fréquemment le type NTSTATUS pour indiquer le résultat d'une opération.

Un NTSTATUS ne représente pas uniquement une erreur. Il peut représenter :
  • une opération réussie ;
  • une information ;
  • un avertissement ;
  • une erreur.
On rencontre par exemple très souvent ce type de code dans un driver :

Code: Select all

NTSTATUS status = IoCreateDevice(...);

if (!NT_SUCCESS(status))
{
    KdPrint(("IoCreateDevice failed: 0x%08X\n", status));
    return status;
}
Comprendre les NTSTATUS est donc indispensable pour gérer correctement les erreurs et les résultats retournés par le noyau Windows.


1. Le type NTSTATUS

Un NTSTATUS est une valeur codée sur 32 bits.

Une fonction kernel peut par exemple être déclarée ainsi :

Code: Select all

NTSTATUS MyFunction();
Puis appelée :

Code: Select all

NTSTATUS status = MyFunction();

if (!NT_SUCCESS(status))
{
    return status;
}
Contrairement à une fonction qui retourne simplement 0 ou -1, le NTSTATUS contient plusieurs informations permettant de connaître la nature exacte du résultat.


2. Structure d'un NTSTATUS

Un NTSTATUS est composé de plusieurs champs répartis dans ses 32 bits.

La structure générale est :

Code: Select all

31 30 29 28                    16 15                     0
+-----+--+--+--------------------+------------------------+
| Sev | C| R|      Facility      |          Code          |
+-----+--+--+--------------------+------------------------+
Les différents champs sont :
  • Severity : indique la gravité du résultat ;
  • C (Customer) : indique notamment si le code appartient à un ensemble de codes personnalisés ;
  • R : bit réservé ;
  • Facility : indique la catégorie ou le composant ayant produit le statut ;
  • Code : identifie la condition précise.
Le champ Code occupe les 16 bits de poids faible.


3. Severity

Les deux bits de poids fort déterminent la sévérité du NTSTATUS.

Il existe quatre catégories :
  • 00 : Success ;
  • 01 : Informational ;
  • 10 : Warning ;
  • 11 : Error.
On peut donc représenter grossièrement les plages ainsi :

Code: Select all

0x00000000 ... -> Success
0x40000000 ... -> Informational
0x80000000 ... -> Warning
0xC0000000 ... -> Error
Cette distinction est importante : un NTSTATUS différent de zéro n'est pas automatiquement une erreur.


4. STATUS_SUCCESS

Le statut le plus simple est :

Code: Select all

STATUS_SUCCESS
Sa valeur est :

Code: Select all

0x00000000
Il indique que l'opération s'est terminée normalement.

Exemple :

Code: Select all

NTSTATUS status = STATUS_SUCCESS;
Une routine de dispatch simple peut par exemple terminer un IRP ainsi :

Code: Select all

Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

return STATUS_SUCCESS;

5. Ne pas tester uniquement STATUS_SUCCESS

Une erreur fréquente consiste à écrire :

Code: Select all

if (status == STATUS_SUCCESS)
{
    // succès
}
Ce test vérifie uniquement si le code est exactement égal à STATUS_SUCCESS.

Or plusieurs NTSTATUS peuvent représenter des résultats qui ne doivent pas être traités comme des erreurs fatales.

Il faut généralement utiliser :

Code: Select all

if (NT_SUCCESS(status))
{
    // résultat considéré comme réussi
}
Ou :

Code: Select all

if (!NT_SUCCESS(status))
{
    // échec ou résultat nécessitant un traitement particulier
}
NT_SUCCESS ne signifie donc pas simplement "status == 0".


6. Les macros NT_SUCCESS, NT_INFORMATION, NT_WARNING et NT_ERROR

Windows fournit plusieurs macros permettant d'analyser la catégorie d'un NTSTATUS.

La plus utilisée est :

Code: Select all

NT_SUCCESS(status)
Elle permet de tester si un statut appartient aux catégories considérées comme réussies par cette convention.

On rencontre également :

Code: Select all

NT_INFORMATION(status)
NT_WARNING(status)
NT_ERROR(status)
Elles permettent de déterminer plus précisément la classe du statut.

Exemple :

Code: Select all

if (NT_ERROR(status))
{
    KdPrint(("Erreur : 0x%08X\n", status));
}
Ces macros sont préférables à une comparaison manuelle des bits lorsque l'objectif est simplement de connaître la catégorie du statut.


7. Codes NTSTATUS courants

De nombreux codes sont utilisés régulièrement dans les drivers.

Quelques exemples importants :
  • Code: Select all

    STATUS_SUCCESS
    : opération réussie ;
  • Code: Select all

    STATUS_PENDING
    : opération en attente ;
  • Code: Select all

    STATUS_INVALID_PARAMETER
    : paramètre invalide ;
  • Code: Select all

    STATUS_INVALID_DEVICE_REQUEST
    : requête non supportée par le périphérique ;
  • Code: Select all

    STATUS_BUFFER_TOO_SMALL
    : buffer trop petit ;
  • Code: Select all

    STATUS_BUFFER_OVERFLOW
    : le buffer ne permet pas de retourner toutes les données ;
  • Code: Select all

    STATUS_INSUFFICIENT_RESOURCES
    : ressources insuffisantes ;
  • Code: Select all

    STATUS_ACCESS_DENIED
    : accès refusé ;
  • Code: Select all

    STATUS_NOT_SUPPORTED
    : opération non supportée ;
  • Code: Select all

    STATUS_OBJECT_NAME_NOT_FOUND
    : objet demandé introuvable ;
  • Code: Select all

    STATUS_DEVICE_NOT_READY
    : périphérique non prêt.
Exemple :

Code: Select all

if (bufferSize < sizeof(MY_DATA))
{
    return STATUS_BUFFER_TOO_SMALL;
}

8. STATUS_PENDING

STATUS_PENDING est particulièrement important dans le modèle d'I/O Windows.

Il signifie qu'une opération n'est pas encore terminée et qu'elle sera complétée ultérieurement.

Exemple conceptuel :

Code: Select all

IoMarkIrpPending(Irp);

return STATUS_PENDING;
Cela peut arriver lorsqu'une requête nécessite un traitement asynchrone.

Le driver ne doit alors pas traiter STATUS_PENDING comme une erreur classique.

La requête sera complétée plus tard, par exemple avec :

Code: Select all

Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = bytesTransferred;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

9. NTSTATUS et IRP

Les NTSTATUS sont très importants dans le traitement des IRP.

Une routine de dispatch possède généralement cette forme :

Code: Select all

NTSTATUS DispatchDeviceControl(
    PDEVICE_OBJECT DeviceObject,
    PIRP Irp
)
{
    UNREFERENCED_PARAMETER(DeviceObject);

    NTSTATUS status = STATUS_SUCCESS;

    // traitement...

    Irp->IoStatus.Status = status;
    Irp->IoStatus.Information = 0;

    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return status;
}
Deux éléments sont importants :

Code: Select all

Irp->IoStatus.Status
indique le résultat de la requête.

Et :

Code: Select all

Irp->IoStatus.Information
contient généralement une information supplémentaire liée à l'opération, très souvent le nombre d'octets effectivement transférés.


10. IoStatus.Information

Une erreur classique consiste à ne gérer que :

Code: Select all

Irp->IoStatus.Status
et à oublier :

Code: Select all

Irp->IoStatus.Information
Pour une opération retournant des données :

Code: Select all

Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = sizeof(MY_DATA);
Pour une requête qui ne retourne aucune donnée :

Code: Select all

Irp->IoStatus.Information = 0;
Le contenu exact de Information dépend du type de requête.


11. Retourner un NTSTATUS et compléter un IRP

Il faut distinguer deux opérations.

Premièrement :

Code: Select all

return status;
retourne un NTSTATUS à l'appelant de la routine.

Deuxièmement :

Code: Select all

Irp->IoStatus.Status = status;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
termine l'IRP avec ce résultat.

Dans une routine qui possède la responsabilité de compléter l'IRP, on rencontre donc souvent :

Code: Select all

Irp->IoStatus.Status = status;
Irp->IoStatus.Information = 0;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

return status;
Retourner un NTSTATUS et compléter un IRP ne représentent pas exactement la même opération.


12. Warning et Error

La différence entre un avertissement et une erreur peut être importante.

Deux codes qui semblent proches peuvent entraîner des comportements différents.

Par exemple :

Code: Select all

STATUS_BUFFER_OVERFLOW
est un warning.

Alors que :

Code: Select all

STATUS_BUFFER_TOO_SMALL
est une erreur.

Cette distinction peut notamment avoir une influence sur le traitement des données retournées par le système d'I/O.

Il ne faut donc pas considérer automatiquement tous les codes non nuls comme identiques.


13. NTSTATUS et erreurs Win32

Les codes NTSTATUS utilisés dans le noyau ne sont pas les mêmes que les codes d'erreur Win32 récupérés avec :

Code: Select all

GetLastError()
Par exemple, on peut rencontrer côté kernel :

Code: Select all

STATUS_INVALID_PARAMETER
alors que le programme user mode reçoit :

Code: Select all

ERROR_INVALID_PARAMETER
Quelques correspondances courantes sont :

Code: Select all

STATUS_SUCCESS
    -> ERROR_SUCCESS

STATUS_INVALID_PARAMETER
    -> ERROR_INVALID_PARAMETER

STATUS_OBJECT_NAME_NOT_FOUND / certains statuts de fichier
    -> erreur Win32 correspondante selon le contexte

STATUS_ACCESS_DENIED
    -> ERROR_ACCESS_DENIED

STATUS_INVALID_DEVICE_REQUEST
    -> ERROR_INVALID_FUNCTION
La correspondance n'est pas toujours une simple égalité numérique.


14. RtlNtStatusToDosError

Le système fournit une fonction permettant de convertir de nombreux NTSTATUS en codes d'erreur Win32 :

Code: Select all

RtlNtStatusToDosError
Exemple :

Code: Select all

NTSTATUS status = STATUS_ACCESS_DENIED;

ULONG error = RtlNtStatusToDosError(status);
Le résultat correspond alors à un code Win32 approprié, par exemple :

Code: Select all

ERROR_ACCESS_DENIED
Il ne faut donc pas faire :

Code: Select all

DWORD error = (DWORD)status;
en supposant que le NTSTATUS et le code Win32 sont identiques.


15. NTSTATUS et HRESULT

NTSTATUS et HRESULT sont deux systèmes de codes différents.

NTSTATUS est principalement rencontré dans :
  • le noyau Windows ;
  • les drivers ;
  • les API natives NT ;
  • certaines parties internes de Windows.
HRESULT est notamment très utilisé dans :
  • COM ;
  • certaines API Windows user mode ;
  • de nombreuses bibliothèques Microsoft.
Même si les deux types sont des valeurs 32 bits structurées, il ne faut pas les considérer comme interchangeables.


16. Lire un NTSTATUS en hexadécimal

Les NTSTATUS sont très souvent affichés sous forme hexadécimale.

Exemple :

Code: Select all

0xC000000D
Les bits de poids fort commencent ici par :

Code: Select all

11
La catégorie est donc :

Code: Select all

Error
Ce code correspond à :

Code: Select all

STATUS_INVALID_PARAMETER
Autre exemple :

Code: Select all

0x00000000
Les bits de sévérité sont :

Code: Select all

00
Il s'agit donc d'un statut de catégorie Success, ici STATUS_SUCCESS.


17. Afficher un NTSTATUS pendant le debugging

Pendant le développement d'un driver, il est utile d'afficher directement le code retourné par une fonction.

Exemple :

Code: Select all

NTSTATUS status = IoCreateDevice(...);

if (!NT_SUCCESS(status))
{
    KdPrint(("IoCreateDevice failed: 0x%08X\n", status));
    return status;
}
L'affichage hexadécimal est particulièrement pratique :

Code: Select all

0xC000000D
On peut ensuite rechercher ou décoder le statut correspondant.

Dans WinDbg, les commandes de diagnostic des codes d'erreur peuvent également aider à interpréter une valeur rencontrée pendant le debugging.


18. Propager une erreur

Lorsqu'une fonction appelée par un driver retourne une erreur, il est souvent préférable de conserver le NTSTATUS original.

Exemple :

Code: Select all

NTSTATUS status = IoCreateDevice(...);

if (!NT_SUCCESS(status))
{
    return status;
}
Cela permet à l'appelant de connaître la véritable cause de l'échec.

Écrire systématiquement :

Code: Select all

if (!NT_SUCCESS(status))
{
    return STATUS_UNSUCCESSFUL;
}
fait perdre de l'information.

Si IoCreateDevice retourne par exemple un code précis, il est généralement plus utile de propager ce code.


19. Nettoyage après une erreur

Propager le NTSTATUS ne signifie pas qu'il faut immédiatement retourner sans réfléchir.

Si des ressources ont déjà été créées, elles doivent être libérées.

Exemple :

Code: Select all

status = IoRegisterDeviceInterface(...);

if (!NT_SUCCESS(status))
{
    IoDeleteDevice(DeviceObject);
    return status;
}
Le principe est :
  • détecter l'erreur ;
  • annuler ou libérer ce qui a déjà été créé ;
  • retourner le NTSTATUS approprié.

20. Codes NTSTATUS personnalisés

Le format NTSTATUS prévoit notamment un bit Customer permettant de distinguer certains codes définis par des utilisateurs ou des composants des codes standards du système.

Il est cependant préférable d'utiliser un code NTSTATUS standard lorsqu'il décrit correctement la situation.

Par exemple, pour un paramètre invalide :

Code: Select all

STATUS_INVALID_PARAMETER
est préférable à la création arbitraire d'un nouveau code.

Les codes personnalisés sont surtout intéressants lorsqu'une application ou un composant doit représenter une condition réellement spécifique qui n'est pas correctement décrite par les codes existants.


21. Erreurs fréquentes

Plusieurs erreurs sont fréquentes lors de l'utilisation des NTSTATUS.
  • Tester uniquement

    Code: Select all

    status == STATUS_SUCCESS
    alors que l'on veut tester la réussite selon la convention NTSTATUS.
  • Considérer toute valeur différente de zéro comme une erreur.
  • Confondre un NTSTATUS avec un code retourné par

    Code: Select all

    GetLastError()
    .
  • Confondre NTSTATUS et HRESULT.
  • Remplacer systématiquement une erreur précise par

    Code: Select all

    STATUS_UNSUCCESSFUL
    .
  • Oublier de renseigner

    Code: Select all

    Irp->IoStatus.Status
    .
  • Oublier de renseigner correctement

    Code: Select all

    Irp->IoStatus.Information
    .
  • Considérer

    Code: Select all

    STATUS_PENDING
    comme une erreur classique.
  • Créer inutilement un code personnalisé alors qu'un NTSTATUS standard existe déjà.

22. Exemple dans une routine IOCTL

Voici un exemple simple :

Code: Select all

NTSTATUS DeviceControl(
    PDEVICE_OBJECT DeviceObject,
    PIRP Irp
)
{
    UNREFERENCED_PARAMETER(DeviceObject);

    PIO_STACK_LOCATION stack =
        IoGetCurrentIrpStackLocation(Irp);

    NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
    ULONG_PTR information = 0;

    switch (stack->Parameters.DeviceIoControl.IoControlCode)
    {
        case IOCTL_MY_COMMAND:
        {
            if (stack->Parameters.DeviceIoControl.InputBufferLength
                < sizeof(LONG))
            {
                status = STATUS_BUFFER_TOO_SMALL;
                break;
            }

            LONG* value =
                (LONG*)Irp->AssociatedIrp.SystemBuffer;

            (*value)++;

            status = STATUS_SUCCESS;
            information = sizeof(LONG);

            break;
        }

        default:
            status = STATUS_INVALID_DEVICE_REQUEST;
            break;
    }

    Irp->IoStatus.Status = status;
    Irp->IoStatus.Information = information;

    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return status;
}
Cet exemple montre plusieurs concepts importants :
  • un statut par défaut est choisi ;
  • les paramètres sont vérifiés ;
  • une erreur précise est utilisée lorsque le buffer est trop petit ;
  • STATUS_SUCCESS est utilisé lorsque la commande réussit ;
  • IoStatus.Status reçoit le résultat ;
  • IoStatus.Information indique la quantité de données retournées ;
  • l'IRP est finalement complété.

23. Exemple de gestion d'une fonction kernel

Un schéma extrêmement courant dans un driver est :

Code: Select all

NTSTATUS status;

status = SomeKernelFunction(...);

if (!NT_SUCCESS(status))
{
    KdPrint(("SomeKernelFunction failed: 0x%08X\n", status));
    return status;
}

// continuer le traitement
Cette structure permet :
  • de vérifier immédiatement le résultat ;
  • d'afficher le code pendant le debugging ;
  • de conserver l'information précise sur l'erreur ;
  • d'éviter de continuer avec un état invalide.

24. Résumé

Un NTSTATUS est une valeur 32 bits utilisée massivement par Windows pour représenter le résultat d'une opération.

Il contient notamment :
  • une sévérité ;
  • une facility ;
  • un code identifiant la condition ;
  • des bits supplémentaires tels que Customer et Reserved.
Les quatre catégories principales sont :

Code: Select all

Success
Informational
Warning
Error
Dans un driver, le test le plus courant est :

Code: Select all

if (!NT_SUCCESS(status))
{
    return status;
}
Pour les IRP, il faut également gérer correctement :

Code: Select all

Irp->IoStatus.Status
Irp->IoStatus.Information
puis compléter la requête lorsque le driver en a la responsabilité :

Code: Select all

IoCompleteRequest(Irp, IO_NO_INCREMENT);
Enfin, il faut retenir que :
  • NTSTATUS n'est pas synonyme d'erreur ;
  • un statut différent de STATUS_SUCCESS n'est pas automatiquement une erreur ;
  • STATUS_PENDING représente une opération qui n'est pas encore terminée ;
  • Warning et Error sont deux catégories distinctes ;
  • NTSTATUS, erreurs Win32 et HRESULT sont des systèmes différents ;
  • une erreur précise doit généralement être propagée plutôt que remplacée par STATUS_UNSUCCESSFUL ;
  • les macros NT_SUCCESS, NT_INFORMATION, NT_WARNING et NT_ERROR permettent d'interpréter correctement les différentes catégories de statut.

Who is online

Users browsing this forum: No registered users and 0 guests