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.

En esa última parte es donde muchas conversaciones de moda se vuelven extrañamente silenciosas. El trabajo del kernel no se trata solo de escribir código que se compile. Se trata de tener un controlador que pueda diagnosticarse cuando se comporta mal en la máquina de un cliente, dentro de una imagen empresarial, junto a software hostil de terceros o después de una secuencia de actualización que nadie en el equipo quería depurar a medianoche. La realidad de la entrega aquí importa tanto como la semántica del lenguaje.

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.

El trabajo central también amplifica la debilidad organizacional. Si un equipo no documenta las invariantes, si la revisión es débil, si el conocimiento de depuración reside en la cabeza de una persona o si la higiene de la liberación se trata como un papeleo opcional, el código de bajo nivel magnificará ese desorden rápidamente. El lenguaje no puede proteger completamente a un equipo de una cultura de ingeniería caótica. Puede ayudar. No puede reemplazar la disciplina.

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.

Hay otra ventaja práctica: Rust puede facilitar la entrega de componentes de alcance limitado. Si el modelo de propiedad es visible y las interfaces son más estrechas, los futuros ingenieros tendrán más posibilidades de cambiar el código sin necesidad de absorber la tradición tribal sólo para evitar detonarla. En el trabajo adyacente al kernel, ese tipo de mantenibilidad no es académico. Así es como los equipos mantienen saludables los sistemas físicos a lo largo del tiempo.

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.

Otro factor es la confianza operativa. Muchos equipos de controladores de Windows ya saben cómo leer los volcados de memoria, validar los símbolos, reproducir la falla y enviar una revisión en un primer mundo de C++. Ese músculo operativo no es glamoroso, pero es costoso reemplazarlo. Una migración que mejora la elegancia del código y al mismo tiempo debilita la respuesta a incidentes no es una ganancia neta.

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.

También están a favor de la cooperación. Un límite de conductor que esté lo suficientemente bien documentado como para que varios ingenieros razonen al respecto reduce el impuesto al heroísmo. Fortalece la revisión del código. Hace que las auditorías sean menos teatrales. Hace que las soluciones futuras dependan menos de la memoria y más de la verdad explícita de la ingeniería. Eso es importante en cualquier sistema, pero es especialmente importante en el software privilegiado.

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.

Los equipos de kernel más fuertes también saben que la calma técnica es parte de la salud de la entrega. Cuando el código, las herramientas y los contratos son legibles, el trabajo deja de depender de la adrenalina. Eso hace que el sistema sea más seguro a largo plazo porque se realizan menos cambios bajo pánico interpretativo.

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.

Esto se debe a que los errores de IOCTL no son errores aislados. Contaminan toda la relación de confianza entre el modo de usuario y el modo kernel. Cuando un límite es vago, cada decisión posterior se vuelve más difícil de razonar.

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.

También es mensurable. Las firmas de fallos se vuelven más limpias. La revisión se vuelve más fácil. Las conversaciones de verificación se acortan. Cuando se puede demostrar el progreso en esos términos, los equipos se sienten menos tentados a buscar una reescritura dramática sólo para lograr un cierre emocional.

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.

Aquí también es donde las organizaciones suelen recuperar primero el valor más práctico, porque mejores herramientas mejoran cada investigación futura, cada implementación y cada análisis posterior al incidente. Unas herramientas circundantes más sólidas hacen que el equipo central de ingeniería sea más saludable, lo cual es un resultado real de los sistemas, incluso si nunca aparece en un punto de referencia de una conferencia.

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.

La cuestión no es el truco aritmético en sí. El punto es practicar cómo convertir la estructura implícita en un lenguaje de ingeniería explícito. Gran parte del dolor de bajo nivel proviene de equipos que transmiten valores codificados como si todos supieran naturalmente lo que significan.

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.

Si extiende la herramienta para etiquetar métodos y modos de acceso comunes, también comienza a desarrollar un hábito que se generaliza bien: cada contrato opaco del kernel se vuelve menos peligroso una vez que se presenta en términos aburridos y explícitos que el resto del equipo puede inspeccionar.

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.

Los equipos que manejan bien esto no confunden modernidad con madurez. Utilizan el lenguaje, las herramientas y los hábitos operativos que hacen que todo el sistema sea más gobernable. Se trata de un resultado menos teatral que un sermón amplio, pero también es el que tiende a sobrevivir al contacto con máquinas reales.

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, la decisión que está bloqueada. O escribe directamente a midgard@stofu.io.

0 / 10000
Ningún archivo seleccionado