Las abstracciones seguras: encapsular el unsafe una sola vez
El patrón clave de Rust for Linux: las APIs C del kernel son intrínsecamente unsafe, pero el crate kernel las envuelve en interfaces Rust seguras, de modo que el código de un driver casi nunca escribe unsafe. Esta lección diseca ese patrón con un ejemplo conceptual completo — un Mutex seguro sobre el mutex de C — y explica el contrato de una API segura, el comentario SAFETY y por qué concentrar el unsafe lo convierte en una superficie auditable.
La FFI de la lección anterior nos deja en un lugar incómodo: podemos llamar al kernel de C, pero cada llamada es unsafe, y un driver que estuviera sembrado de bloques unsafe no tendría ninguna de las ventajas por las que trajimos Rust. La respuesta es el patrón que define todo Rust for Linux, y es tan simple de enunciar como profundo en consecuencias: se escribe el unsafe una sola vez, dentro del crate kernel, envuelto en una interfaz que ningún uso puede romper. El driver hereda la seguridad sin pagar el precio de auditar cada llamada.
- Entender por qué toda API C del kernel es
unsafevista desde Rust. - Diseccionar el patrón de abstracción segura: encapsular el
unsafetras una interfaz sólida. - Leer un ejemplo conceptual completo: un
Mutex<T>seguro sobre el mutex de C. - Reconocer el contrato de una API segura y el papel del comentario
SAFETY.
Por qué toda API C es unsafe
Una función es unsafe en Rust cuando su uso correcto depende de condiciones que el compilador no puede verificar y que, de incumplirse, provocan comportamiento indefinido. Casi toda la API C del kernel cae ahí. mutex_lock exige que el puntero apunte a un mutex inicializado; kfree exige que el puntero venga de kmalloc y no se use después; copy_to_user exige un puntero de usuario válido. Ninguna de esas precondiciones está en la firma; viven en la documentación y en la cabeza del programador. Desde Rust, los bindings crudos reflejan esa realidad: son unsafe sin excepción.
// Uso directo del binding: funcional, pero inseguro y ruidoso.
unsafe {
kernel::bindings::mutex_lock(ptr);
// ...seccion critica...
kernel::bindings::mutex_unlock(ptr);
}
Si el driver entero se escribiera así, Rust no aportaría nada: habríamos cambiado C por un C más verboso. El valor aparece cuando ese unsafe se encapsula.
El patrón: encapsular el unsafe tras una interfaz sólida
Una abstracción segura es un tipo que contiene el recurso peligroso, ejecuta las operaciones unsafe en su interior, y expone métodos seguros con la propiedad decisiva: ninguna combinación de entradas desde código seguro puede provocar comportamiento indefinido. Esa es la definición operativa de “seguro” en Rust. El unsafe no desaparece —el hardware sigue siendo peligroso— sino que se concentra en un punto donde el autor de la abstracción demuestra, de una vez y para siempre, que se cumplen las precondiciones.
flowchart TD C[API C del kernel aun unsafe] --> B[bindings crudos aun unsafe] B --> K[crate kernel envuelve el unsafe auditado] K --> D[Driver sin unsafe totalmente seguro] style C fill:#f38ba8,color:#11111b style K fill:#f9e2af,color:#11111b style D fill:#a6e3a1,color:#11111b
Ejemplo conceptual: un Mutex seguro
Veámoslo con el mutex del kernel. La abstracción Mutex<T> del crate kernel no guarda un puntero suelto: contiene el dato que protege y el mutex de C juntos, de modo que es imposible tener uno sin el otro. Adquirir el lock no devuelve void: devuelve un Guard, un permiso con forma de objeto que da acceso al dato y que, al destruirse, libera el lock.
/// Envuelve el dato T junto al mutex de C que lo protege.
pub struct Mutex<T> {
mutex: Opaque<bindings::mutex>, // el mutex de C, no movible
dato: UnsafeCell<T>, // el dato protegido
}
impl<T> Mutex<T> {
pub fn lock(&self) -> Guard<'_, T> {
// SAFETY: self.mutex esta inicializado y permanece fijo en
// memoria mientras exista este Mutex; el tipo lo garantiza.
unsafe { bindings::mutex_lock(self.mutex.get()) };
Guard { mutex: self }
}
}
El único unsafe está en lock, y va precedido de un comentario SAFETY que justifica por qué la llamada es correcta: el mutex está inicializado y no se moverá. El Guard cierra el ciclo con la implementación de Drop, que se ejecuta automáticamente cuando el permiso sale de ámbito:
impl<T> Drop for Guard<'_, T> {
fn drop(&mut self) {
// SAFETY: existe este Guard, luego tenemos el lock tomado.
unsafe { bindings::mutex_unlock(self.mutex.mutex.get()) };
}
}
Desde el driver, todo esto desaparece. El código es seguro, breve, y no puede olvidar liberar el lock ni acceder al dato sin tomarlo:
// Codigo de driver: ni un solo unsafe.
fn incrementar(contador: &Mutex<u32>) {
let mut guard = contador.lock(); // toma el lock
*guard += 1; // acceso exclusivo garantizado
} // aqui Drop libera el lock, siempre
Olvidar el mutex_unlock —un bug clásico de C que cuelga el sistema— es literalmente inexpresable: el Guard lo libera al salir de ámbito, incluso si la función retorna antes por un ?. Y acceder al dato sin el lock es imposible porque el dato vive dentro del Mutex y solo el Guard da acceso a él.
La norma de Rust for Linux es inflexible: todo bloque unsafe lleva un comentario // SAFETY: que explica por qué las precondiciones se cumplen, y toda función unsafe lleva un # Safety que documenta las que impone a quien la llame. No es burocracia: es la prueba, legible por humanos y exigida por el clippy del kernel, de que alguien razonó la corrección en ese punto exacto. Cuando algo falla, la auditoría empieza por leer esos comentarios.
El unsafe como superficie auditable
La consecuencia económica es enorme. En un driver de C, la superficie de auditoría de memoria es todo el driver: cualquier línea puede tener el bug. En un driver de Rust bien escrito, la superficie es el conjunto de bloques unsafe, que un grep localiza en segundos y que, por diseño, se concentran en las abstracciones del crate kernel, no en el driver. Miles de drivers comparten las mismas pocas abstracciones auditadas; revisar una vez el Mutex<T> protege a todos sus usuarios.
La otra mitad del patrón es la unsafe fn: una función que no puede garantizar su propia corrección y traslada la obligación a quien la llame, documentándola en una sección # Safety. Es el mecanismo con el que una abstracción a medio construir propaga hacia arriba las precondiciones que aún no puede clausurar, hasta que una capa superior las convierte en comprobaciones y expone, por fin, un método seguro.
/// Escribe `val` en el registro MMIO situado en `offset`.
///
/// # Safety
///
/// El llamante debe garantizar que `offset` cae dentro de la region
/// mapeada y que ese registro admite escrituras de 32 bits.
unsafe fn escribir_reg(&self, offset: usize, val: u32) {
// SAFETY: el contrato de la funcion traslada la precondicion al llamante.
unsafe { bindings::writel(val, self.base.add(offset).cast()) };
}
Leer un tipo de Rust for Linux es, en el fondo, seguir el rastro de las precondiciones: cada # Safety es una deuda que alguien, más arriba, tiene que pagar con una comprobación antes de ofrecer una interfaz segura. Cuando esa deuda llega a cero, el driver puede escribirse sin unsafe porque toda la peligrosidad quedó saldada en las capas de abajo.
# La superficie de auditoria de un driver Rust: contable a mano.
grep -rn "unsafe" drivers/mio/
# Comparalo con la de C, donde CADA linea que toca un puntero cuenta.
Esta reducción no es cuantitativa sino cualitativa. En C, la auditoría de memoria no tiene fin porque no tiene frontera: cualquier función podría, en principio, corromper cualquier cosa, así que revisar un driver significa mantener en la cabeza el estado de todos sus punteros a la vez. En Rust, el compilador certifica que el código fuera de los bloques unsafe no puede causar comportamiento indefinido por construcción, de modo que la auditoría se detiene en la frontera de esos bloques. El revisor ya no pregunta “¿es correcto todo el driver?” sino “¿son correctos estos tres bloques unsafe, y respetan las abstracciones del crate kernel sus contratos?”. Es una pregunta acotada, y las preguntas acotadas se responden.
Detente en el cambio de escala que acabas de presenciar, porque es la idea que hace viable todo el proyecto. En C, la corrección de la memoria no se puede factorizar: cada driver la vuelve a jugar entera, porque el lenguaje no ofrece ninguna forma de empaquetar una garantía y entregarla ya demostrada. Si el Mutex de C es seguro, no es por su tipo —que no existe— sino porque cada uno de sus miles de usuarios recuerda tomarlo y soltarlo bien; la garantía no está en la abstracción, está repartida y reimplementada en cada punto de uso. El patrón de la abstracción segura rompe esa maldición. Concentra el razonamiento peligroso en un lugar diminuto, lo somete a un comentario SAFETY que obliga a hacer explícito el argumento, y a partir de ahí emite un tipo cuyo contrato el compilador impone sin descanso a todos sus usuarios futuros. La demostración se hace una vez, la escribe quien más sabe del recurso, y se hereda de forma gratuita en cada driver que use el tipo. Esto convierte la seguridad de memoria en algo componible, que es la palabra clave: propiedades que antes había que verificar de nuevo en cada composición ahora se propagan solas a través de las interfaces, porque el sistema de tipos las transporta. Y transforma el trabajo de auditoría de una tarea imposible —revisar cada línea de cada driver— en una tarea acotada: revisar los pocos bloques unsafe, casi todos viviendo en un puñado de abstracciones del crate kernel. Cuando internalizas que el objetivo no es eliminar el unsafe sino concentrarlo, aislarlo y demostrarlo una sola vez, entiendes que Rust no le pide al kernel que renuncie a operaciones peligrosas, sino que deje de esparcirlas por todo el código y las recoja en fronteras nítidas, auditables y reutilizables.
- Explica, para
mutex_lock,kfreeycopy_to_user, qué precondición no verificable las haceunsafeen Rust y dónde vive esa precondición hoy en C. - En el
Mutex<T>del ejemplo, razona por qué guardar el dato dentro del tipo, y no un puntero a él, es lo que impide acceder al dato sin el lock. - Describe qué ocurre con el lock si
incrementarretornara a mitad por un?, y por qué en C ese mismo camino es una fuente clásica de bugs. - Escribe el comentario
SAFETYque justificaría ununsafeque desreferencia un puntero recién devuelto porioremap. - Argumenta por qué revisar una vez el
Mutex<T>del cratekernelaporta más seguridad al conjunto que revisar mil veces cada driver de C.