FlawOpen

flawopen.com/Seguridad de memoria/Rust unsafe

¿El bloque unsafe de Rust sigue siendo seguro para la memoria?

CWE-787: Out-of-bounds WritePágina de referencia
Respuesta breve

unsafe no desactiva las comprobaciones de Rust. Permite operaciones concretas cuyas condiciones de seguridad el compilador no puede demostrar; el programador debe cumplir esas obligaciones. La consecuencia clave es que una abstracción unsafe no sound puede permitir que llamadas safe comunes provoquen comportamiento indefinido (UB), aunque nunca escriban unsafe.

Términos clave

Operación unsafe:
Operación cuyas condiciones de seguridad no puede demostrar el compilador, como desreferenciar un puntero raw.
Abstracción sound:
API safe cuyos llamadores safe no pueden provocar comportamiento indefinido.
size_hint():
Estimación del tamaño de un iterador para optimizar; no es un recuento exacto ni una prueba de seguridad de memoria.
Comportamiento indefinido (UB):
Comportamiento que Rust no define; el código unsafe debe impedir que lo provoquen los llamadores safe.

Cómo falla una abstracción safe no sound

Paso 1

Una API safe recibe un iterador

El llamador pasa un iterador normal a una función sin precondiciones unsafe.

Paso 2

La implementación reserva según una estimación

La función reserva solo el límite inferior indicado, que puede ser menor que la cantidad de elementos que produzca el iterador.

Paso 3

Las escrituras sin comprobación superan la capacidad

La implementación escribe cada elemento mediante un puntero raw sin ampliar la asignación ni comprobar la capacidad en cada paso.

Paso 4

El uso safe llega a UB

Si el iterador produce más elementos de los que caben, una llamada safe puede escribir fuera de los límites. Use Vec::extend u otra implementación que crezca de forma segura.

NO SOUND
// UNSOUND: reserve uses only the lower hint,
// then unchecked writes consume every item.
fn append_unsound(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) {
    let (lower, _) = iter.size_hint();
    dst.reserve(lower);
    let start = dst.len();
    let mut written = 0;
    unsafe {
        let out = dst.as_mut_ptr().add(start);
        for item in iter {
            // UB once writes exceed spare capacity.
            out.add(written).write(item);
            written += 1;
        }
        dst.set_len(start + written);
    }
}
SOUND
// SOUND: Vec::extend handles capacity
// growth and length updates safely.
fn append(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) {
    dst.extend(iter);
}

Qué garantiza realmente Rust

La garantía precisa es: Rust safe no puede causar comportamiento indefinido si el código unsafe del que depende es sound. La Rust Reference define soundness en el límite: el código unsafe debe impedir que los clientes safe provoquen UB. El compilador no verifica todas las obligaciones de seguridad de una operación unsafe; programadores y revisores deben justificarlas.

Esto sigue siendo una gran ventaja práctica frente a C o C++: Rust hace más visibles las operaciones sensibles a memoria y sus obligaciones, para que las revisiones se centren en los límites unsafe y en las abstracciones safe construidas sobre ellos. Incluyen bloques unsafe, contratos de funciones unsafe, implementaciones de traits unsafe, declaraciones FFI y atributos unsafe; no siempre se limitan a un bloque pequeño. Un caso concreto es CVE-2021-25900 en smallvec: SmallVec::insert_many reservaba usando el límite inferior del tamaño del iterador y escribía más elementos de los que cabían en la asignación; recibió CVSS 9.8.

Soundness es el estándar, no la prudencia

Una API es no sound si código safe puede usarla para provocar comportamiento indefinido. Por ejemplo, Iterator::size_hint ofrece límites para optimizar, no un recuento exacto: un iterador válido puede producir más elementos que el límite inferior, y uno defectuoso puede incumplir su estimación. La documentación estándar indica expresamente que esta sugerencia no debe usarse para eliminar comprobaciones de límites en código unsafe. Un trait safe con comportamiento incorrecto también puede causar errores lógicos o panics; eso no es automáticamente UB, pero el código unsafe debe conservar las invariantes de memoria incluso ante código del llamador inesperado.

El código unsafe no está aislado localmente: una API safe puede depender de una invariante de memoria establecida en otro punto. Si es incorrecta, el defecto está en la abstracción que expuso la API safe, no en los llamadores que la usan conforme al contrato.

Reglas prácticas

Preguntas frecuentes

¿Puedo prohibir unsafe por completo?

#![forbid(unsafe_code)] puede impedir la sintaxis unsafe en el crate donde se activa, algo útil en crates de aplicación. No afecta a las dependencias, y algunas implementaciones, interfaces FFI y bibliotecas necesitan operaciones unsafe.

¿Puede el código unsafe confiar en un trait?

Sí. Un unsafe trait documenta obligaciones que deben cumplir sus implementaciones; el código unsafe puede confiar en ellas. TrustedLen es un trait inestable de la biblioteca estándar para iteradores que prometen una sugerencia de tamaño exacta. El Iterator::size_hint normal no ofrece esa garantía.

¿Esto debilita el argumento a favor de Rust?

No. El modelo de Rust reduce dónde aparecen las obligaciones de seguridad de memoria y las hace explícitas en los límites unsafe. Aun así, hay que auditar código unsafe, dependencias, contratos FFI y APIs safe construidas sobre implementaciones unsafe.

Referencias

Véase también ¿strncpy() es más segura que strcpy()? CWE frente a CVE: ¿cuál es la diferencia? Mass assignment: riesgos y listas seguras de campos Filtración de estado serverless: aísla datos por solicitud PHP == y las comparaciones de magic hash