La evaluación de políticas de seguridad basadas en rutas de sistema de archivos en el kernel, especialmente en entornos de alta concurrencia de I/O, introduce una sobrecarga computacional significativa. La reconstrucción y el recorrido repetitivo de rutas de dentries para cada operación de apertura de archivo, aunque funcional, es inherentemente ineficiente. Este problema se agrava en cargas de trabajo que acceden repetidamente a archivos dentro de los mismos subárboles, como las bases de datos.

La solución propuesta aborda esta ineficiencia fundamental mediante la aplicación del principio de memoización. Al almacenar en caché el resultado de la evaluación de políticas para un identificador de archivo único (inode), se evita la recomputación costosa para accesos subsiguientes. Esto traslada el costo de una operación de O(profundidad_de_ruta) a una operación de O(1) en el caso de un acierto de caché, un patrón clásico de optimización de rendimiento en sistemas de archivos y bases de datos.

Arquitectura del Sistema

El sistema se basa en un agente de seguridad eBPF que utiliza un hook LSM (Linux Security Module) para interceptar eventos de apertura de archivos. Antes de la optimización, cada evento desencadenaba un recorrido completo de la ruta del archivo (dentries) para determinar la política aplicable. Este proceso implicaba:

1. Obtención de la ruta del archivo.
2. Recorrido ascendente de la jerarquía de dentries.
3. Verificación de políticas en cada nivel de la ruta.
4. Combinación de resultados para una decisión final de permitir/denegar.

La optimización introduce una caché LRU (Least Recently Used) implementada como un BPF_MAP_TYPE_LRU_HASH dentro del kernel. La clave de la caché es una estructura inode_cache_key compuesta por mntns_id (ID del namespace de montaje), mount_id (ID del montaje) e inode (número de inode). Esta combinación asegura la unicidad de la clave a través de diferentes namespaces y montajes. El valor de la caché, inode_policy_cache_value, almacena un access_index (posición de bit para la política de ruta) y un state (estado de la caché). Cuando ocurre un evento de apertura de archivo, el agente primero intenta una búsqueda en la caché. Si hay un acierto, se aplica la política almacenada. Si hay un fallo, se ejecuta el "slow path" original (recorrido de ruta) y el resultado se almacena en la caché para futuros accesos. Se manejan casos de hardlinks (donde múltiples rutas comparten un inode) verificando el i_nlink del inode; si es mayor que 1, se ignora la caché y se recurre al "slow path" para garantizar la precisión.

Flujo de Evaluación de Política de Archivos (Pre-Caché)

  1. 1 Hook LSM (File Open) El agente eBPF intercepta la llamada al sistema de apertura de archivo.
  2. 2 Obtener Ruta de Archivo Reconstruir la ruta completa del archivo.
  3. 3 Recorrer Dentries Ascender por la jerarquía de directorios (dentries) del sistema de archivos.
  4. 4 Verificar Política En cada nivel, comprobar si existe una política de seguridad aplicable.
  5. 5 Combinar Políticas Consolidar los resultados para obtener la política final.
  6. 6 Aplicar Decisión Permitir o denegar la operación de apertura de archivo.

Flujo de Evaluación de Política de Archivos (Post-Caché)

  1. 1 Hook LSM (File Open) El agente eBPF intercepta la llamada al sistema de apertura de archivo.
  2. 2 Construir Clave de Caché Generar `inode_cache_key` (mntns_id, mount_id, inode).
  3. 3 Buscar en Caché LRU Consultar `bomfather_inode_policy_cache`.
  4. 4 Acierto de Caché? Si la clave existe y `i_nlink` es 1.
  5. 5 Aplicar Política Caché Usar `access_index` y `state` para la decisión.
  6. 6 Fallo de Caché / Nlink > 1 Ejecutar el "slow path" (recorrido de dentries).
  7. 7 Almacenar en Caché Guardar el resultado del "slow path" en la caché.
  8. 8 Aplicar Decisión Permitir o denegar la operación de apertura de archivo.
CapaTecnologíaJustificación
security eBPF Framework para la implementación del agente de seguridad en el kernel, permitiendo la intercepción de eventos y la ejecución de lógica personalizada sin modificar el código fuente del kernel. vs LSM (Linux Security Modules) directamente, FUSE (Filesystem in Userspace)
storage Linux Filesystem (inodes, dentries, mount namespaces) Proporciona las estructuras de datos fundamentales (inodes, dentries) y el contexto (mount namespaces) para la identificación única de archivos y la evaluación de políticas de acceso.
cache BPF_MAP_TYPE_LRU_HASH Implementación de una caché de clave-valor en el kernel con política de reemplazo LRU, utilizada para almacenar los resultados de las evaluaciones de políticas de acceso a archivos. vs BPF_MAP_TYPE_HASH (sin LRU), estructuras de datos en userspace __uint(max_entries, 10000)

Trade-offs

Ganancias
  • ▲▲ Costo de CPU del kernel
  • ▲ Latencia de evaluación de políticas
Costes
  • △ Cobertura de caché para hardlinks
  • △ Complejidad del código eBPF
#define INODE_POLICY_CACHE_NO_POLICY 0
#define INODE_POLICY_CACHE_ACCESS_INDEX 1
#define INODE_POLICY_CACHE_GLOBAL_READ_ONLY 2
#define INODE_POLICY_CACHE_ACCESS_INDEX_AND_GLOBAL_RO 3

struct inode_cache_key {
    u64 mntns_id;
    u64 mount_id;
    u64 inode;
};

struct inode_policy_cache_value {
    u32 access_index;
    u8 state;
};

struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __uint(max_entries, 10000);
    __type(key, struct inode_cache_key);
    __type(value, struct inode_policy_cache_value);
} bomfather_inode_policy_cache SEC(".maps");
Muestra cómo se definen las estructuras de datos para la clave y el valor de la caché de políticas de inode, junto con la declaración del mapa eBPF.
if (BPF_CORE_READ_INTO(&nlink, inode, i_nlink)) {
    return false;
}
if (nlink != 1) {
    inode_cache_stats_inc(INODE_CACHE_STATS_SKIPS_NLINK);
    return false;
}
Fragmento de código que demuestra cómo se lee el contador de enlaces (`i_nlink`) de un inode y se decide si usar o no la caché basándose en este valor.

Fundamentos Teóricos

El problema de la evaluación repetitiva de políticas en sistemas de archivos se relaciona directamente con los principios de localidad de referencia y memoización, conceptos fundamentales en la ciencia de la computación. La memoización, una técnica de optimización que almacena los resultados de llamadas a funciones costosas y devuelve el resultado en caché cuando se repiten las mismas entradas, fue formalizada por Donald Michie en 1968. En el contexto de sistemas operativos, la gestión de la caché de nombres de directorios (dcache) y la caché de inodes (icache) son ejemplos de la aplicación de estos principios para mejorar el rendimiento del sistema de archivos.

La elección de una caché LRU para la implementación es un algoritmo clásico de reemplazo de caché, ampliamente estudiado y optimizado. Su efectividad se basa en la heurística de que los elementos accedidos recientemente son propensos a ser accedidos de nuevo en el futuro cercano, un principio que se alinea con el comportamiento de acceso a archivos en muchas cargas de trabajo. La complejidad de la gestión de inodes y dentries en sistemas de archivos distribuidos o con múltiples montajes ha sido un tema recurrente en la investigación de sistemas operativos, con papers como "The Design and Implementation of a Log-Structured File System" (Rosenblum y Ousterhout, 1992) discutiendo las interacciones entre estructuras de datos de archivos y rendimiento.