C++, Rust et le Windows Kernel : là où la sécurité aide et où les limites mordent encore

C++, Rust et le Windows Kernel : là où la sécurité aide et où les limites mordent encore

C++, Rust et le Windows Kernel : là où la sécurité aide et où les limites mordent encore

Introduction

Le noyau Windows est l'endroit où les convictions claires du tableau blanc vont découvrir qu'elles doivent un loyer à la réalité. Dans le cadre d’un travail d’application ordinaire, une équipe peut parfois se permettre une vague explication de la raison pour laquelle une chose s’est cassée. Dans le travail du noyau, les explications vagues ont tendance à se transformer en vérifications de bugs, en écrans bleus, en opérateurs en colère et en sessions de débogage qui vous donnent l'impression que la machine est personnellement déçue de votre éducation.

C'est pourquoi la conversation C++ et Rust autour du noyau Windows est importante. Le travail Windows de bas niveau oblige chaque réclamation à survivre au contact avec les limites IOCTL, les règles IRQL, les hypothèses DMA, la synchronisation, la discipline à vie et les outils qui s'attendent toujours à ce que vous vous comportiez comme un adulte même si votre architecture ne l'a pas fait.

Rust mérite ici son élan. La sécurité de la mémoire n’est pas fausse. Une propriété plus claire n’est pas fausse. Les surfaces de défaillance plus explicites ne sont pas fausses. Si vous écrivez du code système proche du privilège et que le langage peut supprimer toute une catégorie de bogues faciles à créer, cela constitue un sérieux avantage en matière d'ingénierie. Les équipes C et C++ ont historiquement recréé cet avantage avec de la discipline, de la révision et une certaine paranoïa légère.

Mais le noyau Windows ne récompense pas les bonnes intentions. Il récompense les équipes capables d'opérer au sein de l'écosystème qui existe réellement : contraintes WDK, interfaces basées sur C, pilotes existants, bases de code existantes, flux de travail WinDbg, règles de durée de vie des objets du noyau, réalités DMA et de synchronisation, et la question douloureusement importante de savoir si l'ensemble de la chaîne de débogage et de déploiement reste compréhensible en cas de panne en production.

C’est dans cette dernière partie que de nombreuses conversations à la mode deviennent étrangement calmes. Le travail du noyau nécessite un code qui se compile et un pilote qui peut être diagnostiqué lorsqu'il se comporte mal sur une machine client, dans une image d'entreprise, à côté d'un logiciel tiers hostile ou après une séquence de mise à jour que personne dans l'équipe n'a voulu déboguer à minuit. La réalité de la livraison compte ici autant que la sémantique du langage.

La question utile n'est donc pas "Rust ou C++ ?" La question utile est la suivante : où Rust crée-t-il un véritable avantage, où C++ reste-t-il la valeur par défaut pratique, et comment concevoir la frontière pour que le système devienne plus sûr au lieu de simplement se féliciter davantage ?

Pourquoi le Windows Kernel n'est pas un terrain de jeu pour les systèmes génériques

Les gens parlent souvent de programmation système comme si chaque domaine de bas niveau partageait un même climat émotionnel. Le noyau Windows a son propre climat de fonctionnement. Le noyau Windows est un environnement d'exploitation avec des contrats stricts et des moyens très coûteux de découvrir que vous les avez mal compris.

IRQL existe. Des chemins de répartition existent. Des contraintes de pagination existent. Des piles de périphériques existent. Des contrats IOCTL existent. Les erreurs de synchronisation ne restent pas longtemps théoriques. Une mauvaise logique de nettoyage peut corrompre l'état, bloquer le chemin d'un périphérique ou faire planter une machine que quelqu'un d'autre aurait préféré faire fonctionner.

Cela signifie que le noyau punit deux illusions opposées. La première illusion est que tout travail de bas niveau devrait rester pour toujours en C ou C++ car c'est ainsi que le monde a toujours été câblé. La deuxième illusion est que l'utilisation de Rust transforme automatiquement l'œuvre en victoire morale. Les deux sont des moyens paresseux d’éviter le véritable problème de conception.

Le vrai problème est de façonner le système de manière à ce que la frontière la plus dangereuse soit petite, mesurable, déboguable et clairement maîtrisée. Parfois, cela signifie que C++ reste la meilleure solution pratique, car le modèle de pilote, le code existant, les outils et l'expérience de l'équipe y sont tous indiqués. Parfois, cela signifie qu'un composant Rust réduit véritablement les risques et augmente la clarté. La plupart du temps, cela signifie que les réponses sont mitigées et que seuls les adultes sont à l’aise avec des réponses mitigées.

Le travail de noyau amplifie également les faiblesses organisationnelles. Si une équipe ne documente pas les invariants, si la révision est faible, si les connaissances en matière de débogage vivent dans la tête d’une seule personne ou si l’hygiène des versions est traitée comme une paperasse facultative, le code de bas niveau amplifiera rapidement ce désordre. Le langage ne peut pas protéger complètement une équipe d’une culture d’ingénierie chaotique. Cela peut aider. Cela ne peut pas remplacer la discipline.

Où Rust aide réellement dans Windows - Travail de bas niveau

Rust est plus utile lorsqu'il supprime la confusion dans un code qui n'a pas le droit de prêter à confusion. L'analyse des limites, l'hygiène des machines à états, la propriété explicite, des modèles de nettoyage plus clairs et une discipline plus stricte autour de ce qui peut créer un alias ou survivre à ce qui sont autant de victoires significatives. Dans les systèmes adjacents au noyau ou lourds en pilotes, cela est important car les bogues de bas niveau sont rarement poétiques. Ils sont répétitifs, structurellement familiers et humiliants d’une manière que les équipes d’ingénieurs ont passé des décennies à prétendre faire partie de la romance.

Rust est particulièrement attrayant dans les composants délimités où l'interface peut rester explicite. Les couches utilitaires, les modules d'assistance, les analyseurs bien définis, certains compagnons en mode utilisateur pour les pilotes, les outils internes et les éléments soigneusement isolés de la logique du noyau ou du pilote peuvent bénéficier des contraintes du langage si l'histoire d'ingénierie environnante est suffisamment mature pour les prendre en charge.

Cela aide également culturellement. Les équipes qui intègrent Rust dans un environnement système Windows ont souvent des conversations plus saines sur les durées de vie, les alias, le nettoyage et ce que promet exactement une limite. C’est utile même lorsque l’architecture finale reste hybride. Les langues façonnent les discussions, et parfois une meilleure discussion constitue déjà un progrès matériel.

Il existe un autre avantage pratique : Rust peut faciliter le transfert de composants à portée limitée. Si le modèle de propriété est visible et les interfaces plus étroites, les futurs ingénieurs ont plus de chances de modifier le code sans avoir besoin de s'imprégner des traditions tribales pour éviter de le faire exploser. Dans les travaux adjacents au noyau, ce type de maintenabilité n’est pas académique. C’est ainsi que les équipes maintiennent les systèmes durs en bonne santé au fil du temps.

Mais rien de tout cela ne signifie que le noyau Windows est désormais un terrain de jeu où les équipes devraient réécrire par enthousiasme théologique. Une victoire limitée reste une victoire limitée. Cette distinction montre à quel point une ingénierie sérieuse évite de devenir une marque de style de vie très coûteuse.

Où C++ garde toujours le terrain réel

C++ reste fort dans le travail du noyau et des pilotes de Windows pour des raisons obstinément pratiques. Il existe un énorme corpus de code de pilote, d'échantillons, de modèles, de connaissances de débogage et d'historique d'intégration de fournisseurs construits autour de C et C++. Les équipes travaillant dans cet espace partent rarement de terrain vide. Ils héritent de pilotes, de chaînes de filtres, de contrats de périphériques, de clients en mode utilisateur, d'anciens C++, d'hypothèses de construction et d'habitudes opérationnelles qui sont déjà C++ façonnées même lorsque le code est à moitié C et émotionnellement endetté à 100 %.

L’histoire de l’outillage compte également. WinDbg, les échantillons WDK, les habitudes KMDF et WDM, les flux de travail de vérification des pilotes, l'interprétation des symboles, les enquêtes sur les vidages sur incident et la culture de débogage plus large autour du travail du noyau Windows ont tous encore des racines profondes dans le monde natif existant. Lorsqu’une équipe est sous pression, la maturité du diagnostic n’est pas un bénéfice décoratif. C’est ainsi que le travail évite de devenir une saison d’archéologie menée en public.

Il y a aussi le problème de l’intégration. Les pilotes cohabitent souvent à côté d'anciens codes, d'assistants en mode utilisateur, de logique d'installation existante, de fournisseurs SDKs ou d'outils de sécurité déjà liés aux hypothèses C et C++. C++ n'est pas automatiquement meilleur dans l'abstrait. C'est souvent mieux dans l'immédiat car le système environnant est déjà entraîné à le prononcer.

Cela n'invalide pas Rust. Cela signifie simplement que la charge de la preuve change en fonction de l'endroit où se trouve le composant. Un nouveau module isolé est un argument. Une pile de pilotes s’appuyant sur des années d’hypothèses natives en est une autre. Les équipes sérieuses arrêtent de prétendre que c’est la même situation.

Un autre facteur est la confiance opérationnelle. De nombreuses équipes de pilotes Windows savent déjà comment lire les crash dumps, valider les symboles, reproduire l'échec et fournir un correctif dans un monde C++-first. Ce muscle opérationnel semble simple et coûte cher à remplacer. Une migration qui améliore l’élégance du code tout en affaiblissant la réponse aux incidents n’est pas une victoire nette.

La surface dangereuse ne disparaît pas. Ça bouge.

C'est le point le plus important de toute la conversation. Les comportements dangereux ne disparaissent pas car une partie de la base de code est écrite en Rust. Il déménage. Il se rassemble aux bords de FFI, aux limites de tampon, aux joints de synchronisation, aux chemins d'allocation, aux contrats de périphériques et aux endroits où le modèle opérationnel est toujours défini par Windows lui-même plutôt que par les subtilités d'un seul langage.

C'est pourquoi les équipes peuvent faire de mauvais échanges si elles célèbrent la langue trop tôt et la frontière trop tard. Un module Rust qui pénètre négligemment dans le code du pilote existant peut toujours hériter de l'ancien chaos, ainsi que d'une nouvelle taxe d'intégration. Un pilote C++ qui isole sa surface dangereuse, documente ses invariants, garde la sémantique IOCTL ennuyeuse et reste profondément testable peut créer moins de surprises totales qu'une architecture plus à la mode qui élargit les limites tout en racontant sa vertu avec beaucoup de confiance.

La question du design pour adultes est donc plus petite et plus pointue. Quel module peut être isolé ? Quelle interface peut rester stable ? Quelles règles de propriété peuvent être énoncées suffisamment clairement pour que les différences linguistiques ne se transforment pas en confusion à l'exécution ? Quelle voie de débogage fonctionnera encore lorsque le système est déjà en feu et que personne n’est d’humeur à apporter des nuances philosophiques ?

Ces questions ne sont pas anti-Rust. Ils sont pro-survie.

Ils sont également favorables à la coopération. Une limite de conducteur suffisamment bien documentée pour que plusieurs ingénieurs puissent y réfléchir réduit la taxe sur l'héroïsme. Cela renforce la révision du code. Cela rend les audits moins théâtraux. Cela rend les correctifs futurs moins dépendants de la mémoire et plus dépendants de vérités techniques explicites. Cela est important dans n’importe quel système, mais cela compte particulièrement dans les logiciels privilégiés.

À quoi ressemble le bien

Une bonne ingénierie du noyau Windows ne semble pas héroïque. Cela semble calme.

Le chemin risqué est connu. Le contrat IOCTL est explicite. L’histoire de la concurrence est ennuyeuse de la meilleure des manières. Les hypothèses de propriété sont documentées. Une analyse de crash-dump est possible. Le plan de déploiement n’est pas un défi. La limite du moteur est suffisamment étroite pour que quelqu'un extérieur à l'équipe de mise en œuvre d'origine puisse toujours comprendre ce qui peut être modifié en toute sécurité et ce qui peut être laissé tranquille.

Si Rust est utilisé, la raison devrait être évidente. Il ne devrait pas être là car « futur » était écrit sur une diapositive dans une police forte. Il devrait être là parce qu'un composant défini bénéficie véritablement des contraintes du langage et parce que l'équipe peut prendre en charge l'histoire de débogage, de construction et opérationnelle qui découle de ce choix.

Si C++ reste sur le chemin critique, cela ne doit pas être défendu comme un destin. Il doit être défendu par des preuves : maturité de l'outillage, coût d'intégration, contraintes des pilotes, expérience de l'équipe et une vision mesurée de l'origine réelle de l'instabilité si le composant était déplacé. C++ devrait figurer dans la conception car il a mérité le siège, car les preuves le soutiennent.

Les équipes noyau les plus solides savent également que le calme technique fait partie de la santé de la livraison. Lorsque le code, les outillages et les contrats sont lisibles, le travail s'arrête en fonction de l'adrénaline. Cela rend le système plus sûr à long terme, car moins de changements sont apportés en cas de panique interprétative.

Cas pratiques à résoudre en premier

Nettoyage des limites IOCTL

De nombreux systèmes gourmands en pilotes sont moins menacés par leur code le plus intelligent que par des limites contractuelles bâclées. Le nettoyage de la gestion, de la validation, de la gestion des versions de la structure et des hypothèses utilisateur-noyau d'IOCTL produit souvent des résultats plus sûrs plus rapidement que des réécritures ambitieuses.

En effet, les erreurs IOCTL ne sont pas des erreurs isolées. Ils contaminent toute la relation de confiance entre le mode utilisateur et le mode noyau. Lorsqu’une frontière est vague, chaque décision en aval devient plus difficile à raisonner.

Durcissement étroit du noyau du pilote

Un petit noyau de pilotes avec des invariants explicites et une meilleure discipline de propriété vaut généralement plus qu’une énorme migration théorique. Parfois, ce durcissement se produit dans C++. Parfois, cela rend possible un futur composant Rust. Quoi qu’il en soit, la récompense est réelle.

C’est également mesurable. Les signatures de crash deviennent plus propres. La révision devient plus facile. Les conversations de vérification deviennent plus courtes. Lorsque les progrès peuvent être démontrés en ces termes, les équipes sont moins tentées de se lancer dans une réécriture dramatique juste pour mettre un terme à leurs émotions.

Compagnons et outils en mode utilisateur

C'est là que Rust brille souvent sans drame. Les outils de diagnostic, les utilitaires de relecture, les validateurs de configuration, les analyseurs de capture ou les processus d'assistance contrôlés peuvent devenir plus clairs et plus sûrs sans entraîner le chemin du noyau le plus fragile dans une nouvelle religion d'intégration avant que le système ne soit prêt.

C’est également là que les organisations récupèrent souvent en premier la valeur la plus pratique, car de meilleurs outils améliorent chaque enquête future, chaque déploiement et chaque analyse post-incident. Des outils environnants plus solides rendent l'équipe d'ingénierie de base plus saine, ce qui est un véritable résultat système même s'il n'apparaît jamais dans une conférence de référence.

Laboratoire pratique : décoder un IOCTL Windows de manière ennuyeuse

Le noyau Windows punit les équipes qui traitent les codes de contrôle comme des entiers décoratifs. Créons un petit utilitaire qui décode une valeur IOCTL afin que la limite cesse d'être mystérieuse.

Le problème n’est pas l’astuce arithmétique elle-même. Le but est de s’entraîner à transformer une structure implicite en un langage d’ingénierie explicite. Une grande partie des problèmes de bas niveau proviennent du fait que les équipes transmettent des valeurs codées comme si tout le monde savait naturellement ce qu'elles signifient.

main.cpp

#include <cstdint>
#include <iomanip>
#include <iostream>

struct IoctlParts {
    std::uint32_t device_type;
    std::uint32_t access;
    std::uint32_t function;
    std::uint32_t method;
};

IoctlParts decode_ioctl(std::uint32_t code) {
    return IoctlParts{
        (code >> 16) & 0xFFFFu,
        (code >> 14) & 0x3u,
        (code >> 2) & 0x0FFFu,
        code & 0x3u
    };
}

int main() {
    constexpr std::uint32_t ioctl = 0x222004;
    const auto parts = decode_ioctl(ioctl);

    std::cout << "IOCTL 0x" << std::hex << std::uppercase << ioctl << "\n";
    std::cout << "device_type=0x" << parts.device_type << "\n";
    std::cout << "access=0x" << parts.access << "\n";
    std::cout << "function=0x" << parts.function << "\n";
    std::cout << "method=0x" << parts.method << "\n";
}

Construire

Sur Windows avec MSVC :

cl /O2 /std:c++20 main.cpp
.\main.exe

Sur Linux ou macOS avec un compilateur multiplateforme :

g++ -O2 -std=c++20 -o ioctl_decode main.cpp
./ioctl_decode

Ce que cela t'apprend

Le problème n’est pas l’arithmétique. Le fait est que le travail de bas niveau Windows devient plus facile dès que la structure cachée cesse d'être traitée comme par magie. Décodez la limite, nommez les champs, rendez le contrat visible, et tout à coup, la conversation de débogage devient plus courte et moins religieuse.

Si vous étendez l'outil pour étiqueter les méthodes courantes et les modes d'accès, vous commencez également à créer une habitude qui se généralise bien : chaque contrat opaque du noyau devient moins dangereux une fois rendu dans des termes ennuyeux et explicites que le reste de l'équipe peut inspecter.

Tâches de test pour les passionnés

  1. Recréez le même décodeur dans Rust et comparez la longueur du code avec la clarté de la limite que vous exposeriez au reste d'une chaîne d'outils de pilote.
  2. Étendez le décodeur pour imprimer des noms lisibles par l'homme pour METHOD_BUFFERED, METHOD_IN_DIRECT et les valeurs associées.
  3. Ajoutez un analyseur pour une liste de codes IOCTL à partir d'un fichier texte et triez-les par type d'appareil et fonction.
  4. Créez un petit ensemble d'entrées fuzz de valeurs IOCTL aléatoires et vérifiez que votre décodeur reste stable et ennuyeux.
  5. Ajoutez une hypothèse de limite intentionnellement bâclée, puis inspectez la rapidité avec laquelle un raccourci « inoffensif » transforme l'ensemble de l'outil en menteur.

Résumé

Rust constitue une réelle amélioration de l'ingénierie de bas niveau de Windows lorsqu'il est utilisé pour réduire la confusion, clarifier la propriété et réduire certaines catégories de bogues évitables. C++ reste un défaut réel et souvent justifié lorsque le travail est lié aux pilotes existants, aux outils existants, à la culture de débogage existante et aux chemins opérationnels qui vivent toujours dans un écosystème fortement natif.

Le vrai travail n’est pas de choisir un gagnant moral. Le véritable travail consiste à définir des limites qui restent compréhensibles lorsque le système est déjà sous pression. Dans le travail du noyau, c'est la différence entre l'ingénierie et l'optimisme.

Les équipes qui gèrent bien cela ne confondent pas modernité et maturité. Ils utilisent le langage, les outils et les habitudes opérationnelles qui rendent l’ensemble du système plus gouvernable. C’est un résultat moins théâtral qu’un sermon radical, mais c’est aussi celui qui tend à survivre au contact des machines réelles.

Références

  1. Documentation des pilotes Windows : Windows
  2. Définition des codes de contrôle d'E/S : https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/defining-i-o-control-codes
  3. Gestion des priorités matérielles et IRQL : https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/managing-hardware-priorities
  4. Présentation du développement du pilote WDF : https://learn.microsoft.com/en-us/windows-hardware/drivers/wdf/
  5. Documentation sur les outils de débogage Windows : Windows
  6. Dangereux Rust : Rust
Philip P.

Philip P., CTO

Retour aux blogs

Contact

Démarrer la conversation

Quelques lignes claires suffisent. Décrivez le système, la pression, la décision qui est bloquée. Ou écrivez directement à midgard@stofu.io.

0 / 10000
Aucun fichier choisi