Guía completa de Rust para desarrolladores: seguridad de memoria, FFI y errores comunes
Objetivo: entender por qué Rust es seguro por defecto, cuáles son los riesgos reales cuando usas unsafe o FFI, y aplicar prácticas y herramientas concretas para auditar y endurecer tu código.
1. Resumen rápido de garantías de Rust
- Ownership y borrowing: enlazan tiempo de vida y acceso mutuo/compartido en tiempo de compilación.
- Tipos Send y Sync: determinan qué puede cruzar hilos y cómo se sincroniza.
- Sin data races en código seguro: las reglas del compilador evitan condiciones de carrera en código safe.
unsafeabre la puerta a UB: cualquier invariantes pueden romperse si no se documentan y verifican.
2. Vectores de riesgo más comunes
- Uso de
unsafesin acotar el alcance ni documentar invariantes. - FFI: pasar ownership o referencias que incumplen lifetimes/alinamiento/representación.
- Panics atravesando fronteras FFI (UB).
- Mutable statics (
static mut) y malas sincronizaciones. - Interior mutability mal usada (RefCell en entornos multi-hilo, UnsafeCell mal documentado).
- Integer overflows asumidos: comportamiento en release puede ser wrapping.
3. Reglas prácticas: cómo escribir unsafe correctamente
- Mantén los bloques
unsafelo más pequeños posible. - Documenta exactamente las invariantes que haces valer dentro del bloque (alineamiento, no-null, exclusividad, etc.).
- Proporciona APIs seguras que encapsulen
unsafey prueben las invariantes. - Escribe tests que validen límites, condiciones de borde y comportamiento concurrente.
- Revisa
unsafecon al menos otra persona y con herramientas automáticas.
4. FFI: reglas y plantillas seguras
Cuando te comunicas con C (o cualquier otra ABI), respeta estas reglas:
- Usa
#[repr(C)]en structs compartidos para garantizar layout. - No permitas que panics crucen la frontera: captura con
std::panic::catch_unwindo configura abort. - Usa
CString/CStrpara cadenas; nunca pases &str directamente. - Define claramente ownership: quién libera memoria, cuándo y cómo.
- Para callbacks, evita capturar referencias temporales; usa contexto con punteros crudos y validaciones.
Ejemplo minimal safe wrapper para FFI
// src/ffi.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::panic::catch_unwind;
#[repr(C)]
pub struct RawPoint {
x: i32,
y: i32,
}
#[no_mangle]
pub extern "C" fn create_point(x: i32, y: i32) -> *mut RawPoint {
Box::into_raw(Box::new(RawPoint { x, y }))
}
#[no_mangle]
pub extern "C" fn destroy_point(ptr: *mut RawPoint) {
if ptr.is_null() { return; }
unsafe { Box::from_raw(ptr); } // se droppea
}
// Función expuesta que captura panics y devuelve código de error
#[no_mangle]
pub extern "C" fn process_safe(s: *const c_char) -> i32 {
let res = catch_unwind(|| {
if s.is_null() { return -1; }
let cstr = unsafe { CStr::from_ptr(s) };
let rust_str = cstr.to_str().map_err(|_| -2)?;
// ... lógica que puede panic
println!("Procesando: {}", rust_str);
Ok(0)
});
match res {
Ok(Ok(code)) => code,
Ok(Err(code)) => code,
Err(_) => -100, // panic atrapado
}
}
//Wrapper seguro para uso desde Rust
pub struct Point { raw: *mut RawPoint }
impl Point {
pub fn new(x: i32, y: i32) -> Self {
let raw = create_point(x, y);
Point { raw }
}
pub fn coords(&self) -> (i32, i32) {
assert!(!self.raw.is_null());
unsafe { let r = &*self.raw; (r.x, r.y) }
}
}
impl Drop for Point {
fn drop(&mut self) {
if !self.raw.is_null() { destroy_point(self.raw); }
}
}
Por qué esto es seguro: encapsulamos punteros crudos en un tipo Rust con Drop, validamos null, y capturamos panics para que no crucen la frontera ABI.
5. Concurrencia: Send, Sync y patrones seguros
- Regla rápida: si un tipo es Send, puede moverse entre threads; si es Sync, puede ser referenciado desde múltiples threads.
- Evita
static mut; usaMutex,RwLock,Atomic*o estructuras de sincronización correctas (Arc, parking_lot). - Para rendimiento: usa primitivas de concurrencia sin bloqueo donde tiene sentido (atomics, crossbeam).
- Si implementas manualmente Send/Sync para un tipo unsafe, documenta y prueba la semántica de sincronización.
Memoria y ordenamiento
Con atomics, entiende al menos Relaxed, Acquire, Release y SeqCst. Empieza con SeqCst para corrección, luego optimiza si necesitas throughput.
6. Herramientas imprescindibles
- Miri: detecta comportamientos indefinidos en tiempo de ejecución. Uso:
cargo +nightly miri test. - Sanitizers (ASan/TSan): detectar use-after-free y data races. Ejemplo:
RUSTFLAGS="-Z sanitizer=address" cargo +nightly test(requiere nightly y setup). - cargo-audit: escanea dependencias por vulnerabilidades conocidas:
cargo install cargo-auditycargo audit. - cargo-fuzz: fuzzing de APIs públicas.
cargo install cargo-fuzz, escribir targets de fuzzing. - clippy: lints de calidad y seguridad:
cargo clippy -- -D warnings. - bindgen: generar bindings C automáticamente, revisar el código generado.
- cargo-deny y cargo-geiger para políticas y detección de crates con
unsafe.
7. Checks y comandos prácticos
- Revisar
unsafeen el repo:grep -R "unsafe" src | sed -n '1,200p' - Miri:
rustup toolchain install nightly && cargo +nightly miri test - ASan:
RUSTFLAGS="-Z sanitizer=address" cargo +nightly test(configura linking con runtime de sanitizer) - Fuzz:
cargo fuzz run my_target - Auditar dependencias:
cargo audit
8. Errores que veo una y otra vez (y cómo evitarlos)
- No documentar invariantes de
unsafe: crea tests unitarios que verifiquen esas invariantes. - Permitir panics en callbacks C: atrapa panics y tradúcelos a códigos de error.
- Asumir que
Vec::into_raw_partsu otras APIs son triviales al cruzar FFI: define claramente ownership y tamaño/ capacidad. - Implementar Send/Sync manualmente sin pruebas: añade stress tests y fuzzing multihilo.
9. Estructura de proyecto recomendada
my-crate/
├─ src/
│ ├─ lib.rs # exports safe API
│ ├─ ffi.rs # funciones extern "C" (pequeñas, documentadas)
│ ├─ unsafe_helpers.rs # bloques unsafe bien acotados
│ └─ sync.rs # abstracciones de concurrencia
├─ tests/ # integration tests, stress tests
├─ fuzz/ # targets para cargo-fuzz
├─ examples/ # pequeños ejemplos de uso
├─ CI/ # scripts CI para miri, sanitizers y cargo-audit
└─ README.md
10. Buenas prácticas rápidas (lista de verificación)
- ¿Encapsulas todo
unsafedetrás de APIs seguras? Sí / No - ¿Has documentado invariantes y precondiciones? Sí / No
- ¿Has corrido Miri y sanitizers en CI? Sí / No
- ¿Tienes fuzz targets para los límites de entrada? Sí / No
- ¿Revisas dependencias con cargo-audit? Sí / No
11. Herramientas avanzadas y siguientes pasos
- Considera Prusti o Creusot para comprobaciones formales sobre invariantes si tu dominio lo requiere.
- Fuzzing intensivo (libfuzzer/cargo-fuzz) en parsers y deserializadores.
- Auditorías externas para código unsafe crítico o librerías públicas.
Consejo avanzado: automatiza un pipeline de CI que ejecute clippy, cargo-audit, Miri y fuzzing básico en PRs que tocan código unsafe. Advertencia: los sanitizers y Miri pueden ser costosos en tiempo; haz que el pipeline tenga niveles (fast/slow) para PRs vs nightlies.
¿Quieres que te genere plantillas concretas (CI, un ejemplo completo de FFI con C y tests, o configuración de sanitizers paso a paso) para tu repositorio? Puedo generarlas y ajustarlas a tu flujo de trabajo.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación