¿Qué significa inseguro en Rust?

Hola Habr! Te presento la traducción del artículo "¿Qué es inseguro de Rust?" autora Nora Codes.


He visto muchos malentendidos sobre lo que significa la palabra clave insegura para la utilidad y corrección del lenguaje Rust y su promoción como un "lenguaje de programación de sistema seguro". La verdad es mucho más complicada de lo que se puede describir en un breve tuit, desafortunadamente. Así es como la veo.


En general, la palabra clave insegura no desactiva el sistema de tipos que mantiene el código Rust correcto . Solo hace posible utilizar algunos "superpoderes", como desreferenciar punteros. inseguro se usa para implementar abstracciones seguras basadas en un mundo fundamentalmente inseguro para que la mayoría del código Rust pueda usar estas abstracciones y evitar el acceso inseguro a la memoria.


Garantía de seguridad


El óxido garantiza la seguridad como uno de sus principios fundamentales. Podemos decir que este es el significado de la existencia del lenguaje. Sin embargo, no proporciona seguridad en el sentido tradicional, durante la ejecución del programa y el uso del recolector de basura. En cambio, Rust utiliza un sistema de tipos muy avanzado para realizar un seguimiento de cuándo y a qué valores se puede acceder. Luego, el compilador analiza estáticamente cada programa Rust para asegurarse de que siempre esté en el estado correcto.


Python Security


Tomemos Python como ejemplo. El código puro de Python no puede dañar la memoria. El acceso a los elementos de la lista tiene controles para ir más allá de las fronteras; los enlaces devueltos por funciones se cuentan para evitar la aparición de enlaces colgantes; No hay forma de hacer aritmética arbitraria con punteros.


Esto tiene dos consecuencias. Primero, muchos tipos deben ser "especiales". Por ejemplo, no es posible implementar una lista o diccionario efectivo en Python puro. En cambio, el intérprete de CPython tiene su implementación interna. En segundo lugar, el acceso a funciones externas (funciones no implementadas en Python), llamado interfaz de una función externa, requiere el uso de un módulo de tipos especiales y viola las garantías de seguridad del lenguaje.


En cierto sentido, esto significa que todo lo escrito en Python no garantiza un acceso seguro a la memoria.


Seguridad en óxido


Rust también proporciona seguridad, pero en lugar de implementar estructuras inseguras en C, proporciona un truco: la palabra clave insegura. Esto significa que las estructuras de datos fundamentales en Rust, como Vec, VecDeque, BTreeMap y String, se implementan en Rust.


Puede preguntar: "Pero, si Rust ofrece un truco contra las garantías de seguridad de su código, y la biblioteca estándar se implementa utilizando este truco, ¿no se considerará que todo en Rust es inseguro?"


En una palabra, querido lector, , exactamente como era en Python. Miremos con más detalle.


¿Qué está prohibido en Rust seguro?


La seguridad en Rust está bien definida: pensamos mucho en ello. En resumen, los programas Rust seguros no pueden:


  • Desreferenciar un puntero que apunta a un tipo diferente del que conoce el compilador . Esto significa que no hay punteros para anular (porque no apuntan a ningún lado), no hay errores de salirse de los límites y / o errores de segmentación (fallas de segmentación), ni desbordamientos de búfer. Pero también significa que no hay usos después de liberar la memoria o volver a liberar la memoria (porque liberar la memoria se considera desreferenciar el puntero) y ningún juego de palabras destinado a escribir .
  • Tener múltiples referencias mutables a un objeto o simultáneamente referencias mutables e inmutables a un objeto . Es decir, si tiene una referencia mutable a un objeto, solo puede tenerlo, y si tiene una referencia inmutable al objeto, no cambiará hasta que lo conserve. Esto significa que no puede forzar una carrera de datos en Safe Rust, que es una garantía que la mayoría de los otros idiomas seguros no pueden proporcionar.

Rust codifica esta información en un sistema de tipos o usando tipos de datos algebraicos , como la opción para indicar la existencia / ausencia de un valor y el resultado <T, E> para indicar error / éxito, o referencias y su vida útil , por ejemplo, & T vs & mut T para indicar un enlace común (inmutable) y un enlace exclusivo (mutable) y 'a T vs' b T para distinguir los enlaces que son correctos en diferentes contextos (esto generalmente se omite ya que el compilador es lo suficientemente inteligente como para descubrirlo usted mismo) .


Ejemplos


Por ejemplo, el siguiente código no se compilará ya que contiene un enlace colgante. Más específicamente, my_struct no vive lo suficiente . En otras palabras, la función devolverá un enlace a algo que ya no existe y, por lo tanto, el compilador no puede (y, de hecho, ni siquiera sabe) compilar esto.


fn dangling_reference(v: &u64) -> &MyStruct { //     MyStruct   ,  v,   . let my_struct = MyStruct { value: v }; //      my_struct. return &my_struct; //  - my_struct  (  ). } 

Este código hace lo mismo, pero intenta solucionar este problema colocando el valor en el montón (Box es el nombre del puntero inteligente base en Rust).


 fn dangling_heap_reference(v: &u64) -> &Box<MyStruct> { let my_struct = MyStruct { value: v }; //    Box         . let my_box = Box::new(my_struct); //      my_box. return &my_box; // my_box   .   "" my_struct       - , //    - MyStruct  . } 

El código correcto lo devuelve Box en lugar de una referencia a él. Esto codifica la transferencia de propiedad, la responsabilidad de liberar memoria, en la firma de la función. Al mirar la firma, queda claro que el código de llamada es responsable de lo que sucede con Box y, de hecho, el compilador lo procesa automáticamente.

 fn no_dangling_reference(v: &u64) -> Box<MyStruct> { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct); //    my_box  . return my_box; //    .         , //    ;       //  Box<MyStruct>       ,      . } 

Algunas cosas malas no están prohibidas en Safe Rust. Por ejemplo, está permitido desde el punto de vista del compilador:
  • causar un punto muerto en el programa
  • perder una cantidad arbitrariamente grande de memoria
  • no cierra los identificadores de archivo, las conexiones de la base de datos o las cubiertas del eje de misiles


La fortaleza del ecosistema Rust es que muchos proyectos eligen usar un sistema de tipos para garantizar que el código sea lo más preciso posible, pero el compilador no requiere tal coerción, excepto en los casos en que se proporciona acceso seguro a la memoria.

¿Qué está permitido en Rust inseguro?


El código Rust inseguro es un código Rust con la palabra clave insegura. inseguro se puede aplicar a una función o bloque de código. Cuando se aplica a una función, significa "esta función requiere que el código llamado proporcione manualmente el invariante que generalmente proporciona el compilador". Cuando se aplica a un bloque de código, significa que "este bloque de código proporciona manualmente la invariante necesaria para evitar el acceso inseguro a la memoria y, por lo tanto, está permitido hacer cosas inseguras".


En otras palabras, inseguro para la función significa "necesita verificar todo", y en el bloque de código - "Ya verifiqué todo".


Como se señaló en The Rust Programming Language , el código en un bloque marcado con la palabra clave insegura puede:


  • Desreferenciar un puntero. Esta es una "superpotencia" clave que le permite implementar listas doblemente vinculadas, hashmap y otras estructuras de datos fundamentales.
  • Llamar a una función o método inseguro. Más sobre esto a continuación.
  • Acceda o modifique una variable estática mutable. Las variables estáticas cuyo alcance no está controlado no se pueden verificar estáticamente, por lo tanto, su uso no es seguro.
  • Implementar rasgo inseguro. Los rasgos inseguros se utilizan para indicar si determinados tipos garantizan ciertos invariantes. Por ejemplo, Enviar y Sincronizar determinan si un tipo puede enviarse entre los límites del hilo o si puede ser usado por varios hilos al mismo tiempo.

¿Recuerdas los punteros colgantes de arriba? Agregue la palabra inseguro, y el compilador jurará el doble porque no le gusta usar inseguro donde no es necesario.


En cambio, la palabra clave insegura se usa para implementar abstracciones seguras basadas en operaciones de puntero arbitrarias. Por ejemplo, el tipo Vec se implementa usando inseguro, pero es seguro usarlo, ya que verifica los intentos de acceder a elementos y no permite desbordamientos. Aunque proporciona operaciones como set_len, que pueden causar acceso inseguro a la memoria, están marcadas como inseguras.


Por ejemplo, podríamos hacer lo mismo que en el ejemplo no_dangling_reference, pero con un uso irrazonable de inseguro:


 fn manual_heap_reference(v: u64) -> *mut MyStruct { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct); //  Box    . let struct_pointer = Box::into_raw(my_box); return struct_pointer; //   ;     . // MyStruct     . } 

Observe la falta de la palabra insegura. Crear punteros es absolutamente seguro. Como se escribió, esto es un riesgo de pérdida de memoria, pero nada más, y las pérdidas de memoria son seguras. Llamar a esta función también es seguro. inseguro solo se requiere cuando algo intenta desreferenciar un puntero. Como una ventaja adicional, la desreferenciación liberará automáticamente la memoria asignada.


 fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct = unsafe { Box::from_raw(my_pointer) }; //  "Value: 1337" println!("Value: {}", my_boxed_struct.value); // my_boxed_struct    .       ,  //    - MyStruct } 

Después de la optimización, este código es equivalente a simplemente devolver un Box. Box es una abstracción segura basada en punteros porque evita la distribución de punteros en todas partes. Por ejemplo, la próxima versión de main dará lugar a una memoria doble libre (doble libre).


 fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct_1 = unsafe { Box::from_raw(my_pointer) }; // DOUBLE FREE BUG! let my_boxed_struct_2 = unsafe { Box::from_raw(my_pointer) }; //  "Value: 1337" . println!("Value: {}", my_boxed_struct_1.value); println!("Value: {}", my_boxed_struct_2.value); // my_boxed_struct_2    .     ,  //    - MyStruct. //  my_boxed_struct_1    .      , //      - MyStruct.  double-free bug. } 

Entonces, ¿qué es la abstracción segura?


La abstracción segura es una abstracción que utiliza un sistema de tipos para proporcionar una API que no se puede utilizar para violar las garantías de seguridad que se mencionaron anteriormente. Box es más seguro * mut T, ya que no puede conducir a una doble desasignación de memoria, como se ilustra arriba.


Otro ejemplo es el tipo Rc en Rust. Este es un puntero de recuento de referencias: una referencia no modificable a los datos en el montón. Dado que permite el acceso simultáneo múltiple a un área de memoria, debe evitar cambios para ser considerado seguro.


Además de esto, no es seguro para subprocesos. Si necesita seguridad de subprocesos, tendrá que usar el tipo Arc (Recuento de referencia atómica), que tiene una penalización de rendimiento debido al uso de valores atómicos para el recuento de enlaces y para evitar posibles carreras de datos en entornos de subprocesos múltiples.


El compilador no le permitirá usar Rc donde debería usar Arc, porque los creadores como Rc no lo marcaron como seguro para subprocesos. Si lo hicieran, no sería razonable: una falsa promesa de seguridad.


¿Cuándo se necesita óxido inseguro?


El óxido inseguro siempre es necesario cuando es necesario realizar una operación que viole una de las dos reglas descritas anteriormente. Por ejemplo, en una lista doblemente vinculada, la ausencia de enlaces mutables a los mismos datos (para el siguiente elemento y el elemento anterior) lo priva completamente de beneficios. Con inseguro, un implementador de lista doblemente enlazado puede escribir código usando punteros de nodo * mut y luego encapsularlo en una abstracción segura.

Otro ejemplo es trabajar con sistemas embebidos. A menudo, los microcontroladores usan un conjunto de registros cuyos valores están determinados por el estado físico del dispositivo. El mundo no puede detenerse mientras toma & mut u8 de dicho registro, por lo tanto, se requiere inseguro para trabajar con cajas de soporte de dispositivos. Típicamente, tales cajas encapsulan el estado en envoltorios transparentes y seguros que copian datos siempre que sea posible, o usan otras técnicas que brindan garantías del compilador.


A veces es necesario llevar a cabo una operación que puede conducir a la lectura y escritura simultáneas, o al acceso inseguro a la memoria, y aquí es donde se necesita inseguridad. Pero mientras exista la oportunidad de asegurarse de que los invariantes seguros se mantengan antes de que un usuario toque algo (es decir, inseguro, inseguro), todo está bien.


¿En los hombros de quién descansa esta responsabilidad?


Llegamos a una declaración hecha anteriormente: , la utilidad del código Rust se basa en un código inseguro. A pesar de que esto se hace de una manera ligeramente diferente a la implementación insegura de estructuras de datos básicas en Python, la implementación de Vec, Hashmap, etc., debería utilizar manipulaciones de puntero en cierta medida.


Decimos que Rust es seguro, con la suposición fundamental de que el código inseguro que usamos a través de nuestras dependencias en la biblioteca estándar o en el código de otras bibliotecas está correctamente escrito y encapsulado. La ventaja fundamental de Rust es que el código inseguro se convierte en bloques inseguros que deben ser revisados ​​cuidadosamente por sus autores.


En Python, la carga de verificar la seguridad de las manipulaciones de memoria recae solo en los desarrolladores de los intérpretes y usuarios de las interfaces de funciones externas. En C, esta carga recae en cada programador.


En Rust, se encuentra con los usuarios de la palabra clave insegura. Esto es obvio, ya que los invariantes deben mantenerse manualmente dentro de dicho código y, por lo tanto, es necesario esforzarse por obtener la menor cantidad de dicho código en la biblioteca o en el código de la aplicación. La inseguridad se detecta, resalta e indica. Por lo tanto, si se producen fallas seguras en su código Rust, entonces encontrará un error en el compilador o un error en varias líneas de su código inseguro.


Este no es un sistema perfecto, pero si necesita velocidad, seguridad y multihilo al mismo tiempo, entonces esta es la única opción.

Source: https://habr.com/ru/post/460295/


All Articles