Rust en el kernel de Linux





En  una publicación anterior  , Google anunció que Android ahora es compatible con el lenguaje de programación Rust utilizado en el desarrollo del propio sistema operativo. En este sentido, los autores de esta publicación también decidieron evaluar la demanda del lenguaje Rust en el desarrollo del kernel de Linux. Esta publicación cubre los aspectos técnicos de este trabajo con algunos ejemplos simples.



Durante casi medio siglo, C ha sido el lenguaje principal para el desarrollo del kernel, porque C proporciona el grado de control y rendimiento predecible necesarios para un componente tan crítico. La densidad de errores de seguridad de la memoria en el kernel de Linux suele ser muy baja porque el código es de muy alta calidad, las revisiones del código cumplen con estándares estrictos y las salvaguardas se implementan cuidadosamente. Sin embargo, los  errores relacionados con la seguridad de la memoria siguen apareciendo con regularidad . En Android, las vulnerabilidades del kernel generalmente se consideran un defecto grave, ya que a veces permiten que se omita el modelo de seguridad debido a que el kernel se ejecuta en modo privilegiado.



Se supone que Rust ha madurado lo suficiente como para cooperar con C como lenguaje para la implementación práctica del kernel. Rust ayuda a reducir posibles errores y vulnerabilidades de seguridad en el código privilegiado, mientras se integra bien con el núcleo central y mantiene un buen rendimiento.



Soporte de óxido



Se desarrolló un  prototipo principal del  controlador Binder para comparar adecuadamente las características de seguridad y rendimiento de la versión C existente y su contraparte Rust. El kernel de Linux tiene más de 30 millones de líneas de código, por lo que no nos fijamos el objetivo de reescribirlo completamente en Rust, sino brindar la capacidad de agregar el código requerido en Rust. Creemos que este enfoque incremental ayuda a aprovechar la implementación de alto rendimiento en el kernel al tiempo que proporciona a los desarrolladores del kernel nuevas herramientas para mejorar la seguridad de la memoria y mantener el rendimiento en tiempo de ejecución.



Nos unimos a la organización  Rust para Linuxdonde la comunidad ha hecho y sigue haciendo mucho para agregar soporte de Rust al sistema de compilación del kernel de Linux. También necesitamos diseñar sistemas para que los fragmentos de código escritos en dos lenguajes puedan interactuar entre sí: estamos especialmente interesados ​​en abstracciones seguras sin gastos generales que permitan que el código de Rust use la funcionalidad central escrita en C, así como la capacidad de implementar funcionalidad en Rust idiomático, que se puede llamar sin problemas desde partes del kernel escritas en C. 



Dado que Rust es un nuevo lenguaje en el núcleo, también tenemos la capacidad de obligar a los desarrolladores a adherirse a las mejores prácticas de documentación y coherencia. Por ejemplo, tenemos requisitos específicos verificables por máquina con respecto al uso de código inseguro: para cada función insegura, el desarrollador debe documentar los requisitos que debe cumplir la persona que llama, lo que garantiza que el uso sea seguro. Además, cada vez que se llama a una función insegura, es responsabilidad del desarrollador documentar los requisitos que debe cumplir la persona que llama como garantía de que el uso será seguro. Además, para cada llamada a funciones inseguras (o uso de construcciones inseguras, por ejemplo,al desreferenciar un puntero sin formato), el desarrollador debe documentar la razón por la que es tan seguro hacerlo.



Rust es famoso no solo por su seguridad, sino también por lo útil y fácil de usar que es para los desarrolladores. A continuación, veamos algunos ejemplos que demuestran cómo Rust puede ser útil para los desarrolladores de kernel al escribir controladores seguros y correctos.



Ejemplo de controlador



Considere la implementación de un dispositivo simbólico de semáforo. Cada dispositivo tiene un valor real; al escribir n  bytes, el valor del dispositivo aumenta en n ; con cada lectura, este valor se reduce en 1 hasta que el valor llega a 0, en cuyo caso este dispositivo se bloquea hasta que se pueda realizar dicha operación de reducción en él sin bajar de 0.



Digamos que  semaphore



 este es un archivo que representa nuestro dispositivo. Podemos interactuar con él desde el shell de esta manera: 



> cat semaphore

      
      





Cuando  semaphore



 es el dispositivo que se acaba de inicializar, el comando que se muestra arriba está bloqueado porque el valor actual del dispositivo es 0. Se desbloqueará si ejecutamos el siguiente comando desde otro shell, ya que incrementará el valor en 1, permitiendo así la lectura de operación original para completar:



> echo -n a > semaphore

      
      





También podemos aumentar el contador en más de 1 si escribimos más datos, por ejemplo:



> echo -n abc > semaphore

      
      





aumenta el contador en 3, por lo que las siguientes 3 lecturas no se bloquearán. 



Para demostrar algunos aspectos más de Rust, agreguemos las siguientes características a nuestro controlador: recuerde cuál es el valor máximo alcanzado durante todo el ciclo de vida, y también recuerde cuántas lecturas realizó cada archivo en el dispositivo.



Ahora veamos cómo se implementaría un controlador de este tipo  en Rust , comparando esta opción con la implementación en C.... Sin embargo, notamos que el desarrollo de este tema en Google apenas está comenzando, y en el futuro todo puede cambiar. Nos gustaría destacar cómo Rust puede ser útil para un desarrollador en todos los aspectos. Por ejemplo, en tiempo de compilación, nos permite eliminar o reducir en gran medida la probabilidad de que clases enteras de errores se introduzcan en el código, mientras mantiene el código flexible y se ejecuta con una sobrecarga mínima.



Dispositivos de personajes



El desarrollador debe hacer lo siguiente para implementar el controlador para el nuevo dispositivo de caracteres en Rust:



  1. Implementar un rasgo  FileOperations



    : todas las funciones asociadas con él son opcionales, por lo que el desarrollador solo necesita implementar aquellas que sean relevantes para el escenario dado. Corresponden a los campos de la estructura C  struct file_operations



    .
  2. Implementar un rasgo  FileOpener



     es el equivalente de tipo seguro de un campo C  open



     de una estructura  struct file_operations



    .
  3. Registre un nuevo tipo de dispositivo para el kernel: esto le dirá al kernel qué funciones necesitarán ser llamadas en respuesta a operaciones en archivos de un nuevo tipo.


La siguiente es una comparación de los dos primeros pasos de nuestro primer ejemplo en Rust y C:







Los dispositivos de caracteres en Rust tienen una serie de beneficios de seguridad:



  • Gestión del estado del ciclo de vida archivo por archivo:  FileOpener::open



     devuelve un objeto cuya vida útil desde entonces es propiedad del autor de la llamada. Se puede devolver cualquier objeto que implemente el rasgo  PointerWrapper



    , y proporcionamos implementaciones para 
    Cuadro <T>
     y 
    Arco <T>
    , , Rust, , .



     FileOperations



      self



     ( ),  release



    , ( ).  release



      , - , . «» ( , ).



    , Rust , C. C , Rust, , Rust, , , . , C, Rust , Rust. , open , , , , ioctl



    /read



    /write



      ( ) ,  filp->private_data



    , ..



  • : , open



      release



       self



    , , Rust , .



    ( ),   : Mutex



      
    SpinLock



     ( 
    atomics) .



    , ( ), ( ). 





    : , , Rust . , , ,  FileOperation::open



    .  Arc, .



    ,  FileOperation




      ( , ,  open



    ,  FileOperations



    ) – .



    , , . , C miscdevice



    ,  filp->private_data



    ;  cdev



    , inode->i_cdev



    . , ,  container_of



    , . Rust .









    : , Rust , . . C , , (void



     *) : , , . Rust .





    : ,  FileOperations



    , . , impl FileOperations for Device



    ,  Device



     – , ( FileState



    ). , , , . (  neovim



      LSP- rust-analyzer



     .)



    Rust, , C, struct file_operations



    . (  declare_file_operations



    ): , , ,  const



    , , .



    Ioctl 



     ioctl, ioctl



    , FileOperations



    , . 







    Ioctl , , , , , (, , , , ) . Rust  (  cmd.dispatch



    ), .



       . , , ioctl, Rust : cmd.raw



      ioctl ( , ).



    , , , - , :





    C, ( , ) ; Rust  unsafe



    , . Rust:





     



    . ; , C Rust , , , , : 







    , C, ,    «» ,  unix  ,     ,   .



    Rust:



    •  Semaphore::inner



        , ,  lock



      . , . C, , count



        max_seen



        semaphore_state



        , . there is no enforcement that the lock is held while they're accessed.
    • (RAII): , (inner



        ) . , : , , , , ; : , , , drop



      .
    • ,  Lock



      , , , Mutex



        SpinLock



      , , C, . , , , .
    • Rust , . , , . C  semaphore_consume



        Linux: , ,  mutex_unlock



         prepare_to_wait



      , . 
    • : , , , , , . , ioctl , , . Rust ,   . , C, atomic64_t



      , , . 






    ,  open



    read



     write



    :















    Rust:



    •  ?



       operator: open



        read



        Rust ; , , , . C , - . 
    • : Rust , , - . C . open



      , , C kref_get



       ( ); Rust  clone



       ( ), .
    • RAII: Rust , , inner



        , , .
    • : Rust , . write



      , , . C , , . 







    .



    10% !






All Articles