Una caché por inodo recorta un 90% el coste de CPU de un agente eBPF en el kernel
El agente guarda qué política aplica a cada inodo en un hash LRU en lugar de recorrer la ruta entera en cada apertura de fichero, y su repositorio ya es open source
Un agente de seguridad en eBPF ha recortado el coste de CPU en el kernel alrededor de un 90% metiendo una caché por inodo por delante de la evaluación de políticas. La idea se cuenta rápido: antes, cada apertura de fichero obligaba a reconstruir la ruta y recorrer los dentries padre uno a uno para ver qué política aplicaba; ahora ese resultado se reutiliza. El agente, bomfather/agent, se ha publicado como open source hace poco y todo el trabajo descrito está en el repositorio.
El diseño original era path-based. El agente engancha un LSM hook en el open de fichero, reconstruye la ruta y sube por los dentries padre comprobando si el fichero o alguno de sus directorios ancestros tiene una política asociada. El problema es que ese trabajo se repetía una y otra vez. El caso que ponen los autores es Postgres leyendo de /var/lib/postgres: el proceso abre muchos ficheros bajo el mismo subárbol y cada uno repetía el mismo recorrido. A eso lo llaman el camino lento.
Qué se guarda y qué se gana
La caché no usa dentries como clave: son punteros y no se pueden almacenar dentro de un mapa de eBPF, y copiar su contenido a un struct sería demasiado pesado. La clave son tres campos: el ID del namespace de montaje, el ID del montaje y el número de inodo. El inodo por sí solo no sirve porque su numeración es única dentro de un árbol de montaje, y el namespace evita reutilizar entradas observadas en otro contexto. El valor lleva un access_index y un estado, y las políticas se guardan como bitmasks para ahorrar espacio. Todo va en un hash LRU de 10.000 entradas.
En la prueba de rendimiento abren el mismo fichero 200.000 veces. Sin caché, el coste en ciclos de kernel baja de 28.000 millones a 3.030 millones con ella. Las funciones is_restricted_filepath y path_check_callback, que aparecían en el 81,9% y el 63,7% de las muestras de pila sin caché, se quedan alrededor del 0,02%, lo bastante poco como para desaparecer del gráfico. El perfilado se hizo con perf sobre el evento cycles:k, que mide el coste de CPU del lado del kernel durante las aperturas.
Los hardlinks obligan a renunciar a cobertura
Un mismo inodo puede tener varias rutas apuntándole, y los hardlinks son el caso evidente. Como aquí la corrección importa más que el rendimiento, si el i_nlink del inodo es mayor que 1 el agente descarta esa entrada y vuelve al camino lento. Es una concesión: se pierde parte de la cobertura de la caché, pero se evita aplicar a una ruta la política que corresponde a otra.
Para quien administre esto, lo relevante es que la caché es interna y las políticas del usuario no cambian. Y para quien tenga un agente de seguridad en kernel con hooks de open, el patrón se puede reutilizar tal cual: la parte cara no era decidir, era averiguar qué regla tocaba. Los números, medidos con perf, dejan poco margen a la duda.
