C++, Rust y el Windows Kernel: donde la seguridad ayuda y los límites aún persisten

C++, Rust y el Windows Kernel: donde la seguridad ayuda y los límites aún persisten

C++, Rust y el Windows Kernel: donde la seguridad ayuda y los límites aún persisten

Introducción

El núcleo de Windows es adonde van las convicciones limpias de la pizarra para descubrir que le deben un alquiler a la realidad. En el trabajo de aplicación normal, un equipo a veces puede permitirse una explicación vaga de por qué algo se rompió. En el trabajo del kernel, las explicaciones vagas tienden a convertirse en comprobaciones de errores, pantallas azules, operadores enojados y sesiones de depuración que te hacen sentir como si la máquina estuviera personalmente decepcionada con tu educación.

Es por eso que la conversación entre C++ y Rust sobre el kernel Windows es importante. No porque un lado sea nostálgico y el otro sea ilustrado, sino porque el trabajo de bajo nivel Windows obliga a cada reclamo a sobrevivir al contacto con los límites IOCTL, las reglas IRQL, los supuestos DMA, la sincronización, la disciplina de por vida y las herramientas que aún esperan que usted se comporte como un adulto incluso si su plataforma de arquitectura no lo hizo.

Rust merece su impulso aquí. La seguridad de la memoria no es falsa. Una propiedad más clara no es falsa. Las superficies de falla más explícitas no son falsas. Si está escribiendo código de sistemas cercano a los privilegios y el lenguaje puede eliminar una categoría completa de errores fáciles de crear, eso no es cosmético. Esa es una importante ventaja de ingeniería que los equipos de C y C++ históricamente han tenido que recrear con disciplina, revisión y cierta cantidad de paranoia leve.

Pero el núcleo Windows no premia las buenas intenciones. Recompensa a los equipos que pueden operar dentro del ecosistema que realmente existe: restricciones de WDK, interfaces basadas en C, controladores heredados, bases de código existentes, flujos de trabajo de WinDbg, reglas de duración de los objetos del kernel, realidades de sincronización y DMA, y la dolorosamente importante pregunta de si toda la cadena de depuración e implementación sigue siendo comprensible cuando algo falla en producción.

Entonces, la pregunta útil no es "¿Rust o C++?" La pregunta útil es la siguiente: ¿dónde Rust crea una ventaja genuina, dónde C++ sigue siendo el valor predeterminado práctico y cómo se diseña el límite para que el sistema se vuelva más seguro en lugar de simplemente más autocomplaciente?

Por qué el Windows Kernel no es un campo de juego de sistemas genéricos

La gente suele hablar de programación de sistemas como si cada dominio de bajo nivel compartiera un clima emocional. Eso no es cierto. El kernel Windows no es sólo "un código de bajo nivel". Es un entorno operativo con contratos estrictos y formas muy costosas de descubrir que no los entendiste.

IRQL existe. Existen rutas de envío. Existen restricciones de paginación. Existen pilas de dispositivos. Existen contratos IOCTL. Los errores de sincronización no son teóricos por mucho tiempo. Una lógica de limpieza deficiente no sólo crea una salida del proceso desordenada. Puede corromper el estado, bloquear la ruta de un dispositivo o bloquear una máquina que otra persona hubiera preferido que siguiera funcionando.

Esto significa que el núcleo castiga dos ilusiones opuestas. La primera ilusión es que todo el trabajo de bajo nivel debería permanecer en C o C++ para siempre porque así es como siempre ha estado conectado el mundo. La segunda ilusión es que usar Rust automáticamente transforma el trabajo en victoria moral. Ambas son formas perezosas de evitar el verdadero problema de diseño.

El verdadero problema es dar forma al sistema de modo que el límite más peligroso sea pequeño, mensurable, depurable y claramente identificado. A veces eso significa que C++ sigue siendo la mejor opción práctica porque el modelo del controlador, el código existente, las herramientas y la experiencia del equipo apuntan allí. A veces significa que un componente Rust realmente reduce el riesgo y aumenta la claridad. La mayoría de las veces esto significa que la respuesta es mixta, y sólo los adultos se sienten cómodos con respuestas mixtas.

Dónde Rust realmente ayuda en Windows-Trabajo de bajo nivel

Rust ayuda más cuando elimina la confusión del código que no tiene derecho a ser confuso. Análisis de límites, higiene de la máquina de estado, propiedad explícita, patrones de limpieza más claros y una disciplina más estricta en torno a lo que puede alias o sobrevivir a lo que son todas victorias significativas. En sistemas adyacentes al kernel o con muchos controladores, eso es importante porque los errores de bajo nivel rara vez son poéticos. Son repetitivos, estructuralmente familiares y humillantes en una forma en la que los equipos de ingeniería han pasado décadas fingiendo que eran parte del romance.

Rust es especialmente atractivo en componentes delimitados donde la interfaz se puede mantener explícita. Las capas de utilidad, los módulos auxiliares, los analizadores bien definidos, algunos compañeros de modo de usuario para los controladores, las herramientas internas y piezas cuidadosamente aisladas de la lógica del kernel o del controlador pueden beneficiarse de las limitaciones del lenguaje si la historia de ingeniería circundante es lo suficientemente madura como para soportarlas.

También ayuda culturalmente. Los equipos que llevan Rust a un entorno de sistemas Windows a menudo obtienen conversaciones más saludables sobre la vida útil, los alias, la limpieza y lo que promete exactamente un límite. Esto es útil incluso cuando la arquitectura final sigue siendo híbrida. Los idiomas dan forma a las discusiones y, a veces, una mejor discusión ya es un progreso material.

Pero nada de esto significa que el núcleo Windows sea ahora un campo de juego donde los equipos deban reescribir con entusiasmo teológico. Una victoria limitada sigue siendo una victoria limitada. Esa distinción es cómo la ingeniería seria evita convertirse en una marca de estilo de vida muy costosa.

Donde C++ todavía mantiene un terreno real

C++ sigue siendo fuerte en el trabajo del kernel y del controlador de Windows por razones que son obstinadamente prácticas. Existe una gran cantidad de código de controlador, muestras, patrones, conocimientos de depuración e historial de integración de proveedores creados en torno a C y C++. Los equipos que trabajan en este espacio rara vez parten de un terreno vacío. Están heredando controladores, cadenas de filtros, contratos de dispositivos, clientes en modo de usuario, C++ heredadas, suposiciones de construcción y hábitos operativos que ya están C++ moldeados incluso cuando el código es mitad C y emocionalmente 100 por ciento deuda.

La historia de las herramientas también importa. WinDbg, muestras de WDK, hábitos de KMDF y WDM, flujos de trabajo de verificación de controladores, interpretación de símbolos, investigación de volcados de fallas y la cultura de depuración más amplia en torno al trabajo del kernel de Windows todavía tienen profundas raíces en el mundo nativo existente. Cuando un equipo está bajo presión, la madurez del diagnóstico no es un beneficio decorativo. Así la obra evita convertirse en una temporada de arqueología realizada en público.

También está el problema de la integración. Los controladores a menudo conviven con código antiguo, ayudantes de modo de usuario, lógica de instalación existente, proveedores SDKs o herramientas de seguridad que ya están vinculadas a suposiciones de C y C++. C++ no es automáticamente mejor en abstracto. A menudo es mejor en lo inmediato porque el sistema circundante ya está capacitado para hablarlo.

Eso no invalida Rust. Simplemente significa que la carga de la prueba cambia dependiendo de dónde se encuentre el componente. Un nuevo módulo aislado es un argumento. Otra es una pila de impulsores entrelazados a través de años de suposiciones nativas. Los equipos serios dejan de fingir que se trata de la misma situación.

La superficie insegura no desaparece. Se mueve.

Este es el punto más importante de toda la conversación. El comportamiento inseguro no desaparece porque una parte del código base esté escrita en Rust. Se reubica. Se reúne en FFI bordes, límites de buffer, costuras de sincronización, rutas de asignación, contratos de dispositivos y lugares donde el modelo operativo todavía está definido por Windows en sí mismo en lugar de por las sutilezas de un solo lenguaje.

Es por eso que los equipos pueden hacer un mal intercambio si celebran el idioma demasiado pronto y el límite demasiado tarde. Un módulo Rust que cruza descuidadamente el código del controlador heredado aún puede heredar el antiguo caos, además de un nuevo impuesto de integración. Un controlador C++ que aísla su superficie peligrosa, documenta sus invariantes, mantiene la semántica IOCTL aburrida y sigue siendo profundamente comprobable puede crear menos sorpresas totales que una arquitectura más moderna que amplió el límite mientras narra su virtud con mucha confianza.

Por lo tanto, la cuestión del diseño para adultos es más pequeña y más aguda. ¿Qué módulo se puede aislar? ¿Qué interfaz puede permanecer estable? ¿Qué reglas de propiedad se pueden establecer con suficiente claridad para que las diferencias de idioma no se conviertan en confusión en tiempo de ejecución? ¿Qué ruta de depuración seguirá funcionando cuando el sistema ya esté en llamas y nadie esté de humor para matices filosóficos?

Esas preguntas no son anti-Rust. Están a favor de la supervivencia.

lo bueno que parece

Una buena ingeniería del kernel de Windows no suena heroica. Suena tranquilo.

El camino arriesgado es conocido. El contrato IOCTL es explícito. La historia de la concurrencia es aburrida en el mejor de los sentidos. Los supuestos de propiedad están documentados. Es posible realizar un análisis de volcado de memoria. El plan de implementación no es un desafío. El límite del impulsor es lo suficientemente estrecho como para que alguien fuera del equipo de implementación original pueda entender qué es seguro cambiar y qué es seguro dejarlo como está.

Si se utiliza Rust, debería ser obvio por qué. No debería estar allí porque "futuro" estaba escrito en una diapositiva con una fuente fuerte. Debería estar ahí porque un componente definido realmente se beneficia de las limitaciones del lenguaje y porque el equipo puede respaldar la depuración, la construcción y la historia operativa que se deriva de esa elección.

Si C++ permanece en el camino crítico, eso no debería defenderse como un destino. Debe defenderse con evidencia: madurez de las herramientas, costo de integración, limitaciones del conductor, experiencia del equipo y una visión medida de dónde vendría realmente la inestabilidad si se moviera el componente. C++ debería estar en el diseño porque se ganó el lugar, no porque el sistema estuviera demasiado cansado para discutir.

Casos prácticos que vale la pena resolver primero

Limpieza de límites IOCTL

Muchos sistemas con muchos conductores corren menos peligro por su código más inteligente que por límites contractuales descuidados. La limpieza del manejo de IOCTL, la validación, el control de versiones de la estructura y los supuestos de usuario a kernel a menudo produce resultados más seguros y más rápidos que las reescrituras ambiciosas.

Endurecimiento del núcleo del conductor estrecho

Un pequeño núcleo de impulsores con invariantes explícitas y una mejor disciplina de propiedad suele valer más que una enorme migración teórica. A veces ese endurecimiento ocurre en C++. A veces hace posible un futuro componente Rust. De cualquier manera, la recompensa es real.

Compañeros y herramientas del modo de usuario

Aquí es donde Rust a menudo brilla sin dramatismo. Las herramientas de diagnóstico, las utilidades de reproducción, los validadores de configuración, los analizadores de captura o los procesos auxiliares controlados pueden volverse más claros y seguros sin arrastrar el camino más frágil del núcleo hacia una nueva religión de integración antes de que el sistema esté listo.

Laboratorio práctico: decodifica un IOCTL Windows de forma aburrida

El kernel Windows castiga a los equipos que tratan los códigos de control como enteros decorativos. Construyamos una pequeña utilidad que decodifica un valor IOCTL para que el límite deje de ser misterioso.

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";
}

Construir

En Windows con MSVC:

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

En Linux o macOS con un compilador multiplataforma:

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

Lo que esto te enseña

La cuestión no es la aritmética. El punto es que el trabajo de bajo nivel Windows se vuelve más fácil en el momento en que la estructura oculta deja de ser tratada como magia. Decodifica los límites, nombra los campos, haz visible el contrato y, de repente, la conversación de depuración se vuelve más corta y menos religiosa.

Tareas de prueba para entusiastas

  1. Recrea el mismo decodificador en Rust y compara no solo la longitud del código, sino también la claridad del límite que expondrías al resto de una cadena de herramientas del controlador.
  2. Amplíe el decodificador para imprimir nombres legibles por humanos para METHOD_BUFFERED, METHOD_IN_DIRECT y valores relacionados.
  3. Agregue un analizador para obtener una lista de códigos IOCTL de un archivo de texto y ordénelos por tipo de dispositivo y función.
  4. Cree un pequeño conjunto de entradas fuzz de valores IOCTL aleatorios y verifique que su decodificador se mantenga estable y aburrido.
  5. Agregue una suposición de límites intencionalmente descuidada, luego inspeccione qué tan rápido un atajo "inofensivo" convierte toda la herramienta en una mentirosa.

Resumen

Rust es una mejora real en Windows ingeniería de bajo nivel cuando se utiliza para reducir la confusión, aclarar la propiedad y reducir ciertas categorías de errores evitables. C++ sigue siendo un valor predeterminado real y a menudo justificado cuando el trabajo está vinculado a los controladores existentes, las herramientas existentes, la cultura de depuración existente y las rutas operativas que aún viven en un ecosistema fuertemente nativo.

El verdadero trabajo no es elegir un ganador moral. El verdadero trabajo es diseñar límites que sigan siendo comprensibles cuando el sistema ya esté bajo presión. En el trabajo del núcleo, esa es la diferencia entre ingeniería y optimismo.

Referencias

  1. Windows documentación de controladores: Windows
  2. Definición de códigos de control de E/S: https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/defining-i-o-control-codes
  3. Gestión de prioridades de hardware e IRQL: https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/managing-hardware-priorities
  4. Descripción general del desarrollo del controlador WDF: https://learn.microsoft.com/en-us/windows-hardware/drivers/wdf/
  5. Windows documentación de herramientas de depuración: Windows
  6. Inseguro Rust: Rust
Philip P.

Philip P. – CTO

Volver a blogs

Contacto

Iniciar la conversación

Unas pocas líneas claras son suficientes. Describe el sistema, la presión y la decisión que está bloqueada. O escribe directamente a midgard@stofu.io.

01 Que hace el sistema
02 que duele ahora
03 ¿Qué decisión está bloqueada?
04 Opcional: registros, especificaciones, seguimientos, diferencias
0 / 10000
Ningún archivo seleccionado