Problème initial
L’agent de sécurité eBPF s’appuie sur le hook LSM security_file_open pour appliquer des politiques basées sur le chemin d’accès. Avant la mémoïsation, chaque ouverture de fichier déclenchait un « slow path » : reconstruction du chemin complet, remontée des dentries parentales et vérification de chaque niveau contre la table de politiques. Cette opération était répétée pour chaque accès, même lorsqu’il s’agissait du même fichier ou de fichiers situés dans le même sous‑arbre, comme les multiples lectures de /var/lib/postgres/data/base/123. Le profilage a montré que la fonction tail_call_security_check occupait 89,2 % du temps CPU, révélant un goulet d’étranglement majeur.
Conception du cache
Pour éviter la traversée répétée, les auteurs ont introduit un cache LRU‑hash stocké dans un eBPF map. La clé du cache combine trois champs : l’identifiant du namespace de montage (mntns_id), l’identifiant du point de montage (mount_id) et le numéro d’inode (inode). Cette combinaison garantit l’unicité même lorsque plusieurs arbres de montage partagent des numéros d’inode. La valeur du cache contient un access_index (position du bit dans le masque de politique) et un état compact (u8 state). Le map est limité à 10 000 entrées pour rester léger dans le kernel.
#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");
Le flux d’exécution devient : construction de la clé, recherche dans le map, utilisation du résultat en cas de hit ou exécution du slow path suivi d’une insertion en cas de miss.
Résultats de performance
Les benchmarks ont ouvert le même fichier 200 000 fois. Sans cache, le compteur de cycles du kernel a atteint 28 milliards ; avec le cache, il est tombé à 3,03 milliards, soit une réduction d’environ 90 %. Les pourcentages d’occurrence dans les flamegraphs sont passés de 81,9 % à 0,02 % pour is_restricted_filepath et de 63,7 % à 0,02 % pour path_check_callback. Ces chiffres confirment que la traversée du chemin disparaît après le premier lookup, laissant le coût du cache négligeable.
Limites et cas particuliers
Un inode peut être référencé par plusieurs chemins (hard‑links). Le cache ne peut alors garantir la bonne politique, car deux chemins différents pourraient partager le même inode mais être soumis à des règles distinctes. La solution implémentée lit le compteur de liens (i_nlink) ; si la valeur dépasse 1, le code contourne le cache et reprend le slow path. Cette stratégie sacrifie une partie de la couverture du cache, mais préserve l’exactitude des décisions de sécurité, ce qui est prioritaire dans un contexte de protection du système.