Tuto : Maîtriser le traçage eBPF en production sans compromettre la stabilité du Kernel

Apprenez à instrumenter vos applications en production avec eBPF en toute sécurité. Guide pratique sur l'utilisation des CO-RE (Compile Once – Run Everywhere) et des `fentry/fexit` pour monitorer vos syscalls sans risque de crash système.

Introduction à eBPF et à la sécurité en production

Architecture sécurisée d'eBPF dans le noyau Linux montrant le verifier et la machine virtuelle

Le crash d'un serveur de production à trois heures du matin à cause d'un module noyau défectueux est le pire cauchemar de tout ingénieur système. Heureusement, la technologie Extended Berkeley Packet Filter (eBPF) a révolutionné la manière dont nous observons et sécurisons nos infrastructures en permettant d'exécuter du code directement dans le noyau Linux sans jamais risquer de faire planter la machine. Contrairement aux modules noyau traditionnels qui s'exécutent sans filet de sécurité, eBPF utilise une machine virtuelle interne dotée d'un validateur ultra-strict qui garantit que votre code ne causera jamais de fuite de mémoire ou de blocage système.

Pour exploiter cette technologie sur des parcs de serveurs hétérogènes sans avoir à recompiler vos programmes pour chaque version de noyau, nous utilisons le concept de Compile Once – Run Everywhere (CO-RE). Cette approche repose sur les métadonnées du BPF Type Format (BTF) fournies par les distributions modernes, permettant d'ajuster dynamiquement à la volée les décalages de structures de données du noyau lors du chargement du programme. Nous allons voir comment installer l'environnement nécessaire et écrire nos premiers outils d'observation robustes, prêts à affronter des charges de production réelles.

Pour démarrer sur une distribution Debian ou Ubuntu moderne, vous devez installer les en-têtes du noyau, la bibliothèque de développement de libbpf, ainsi que la suite d'outils de compilation LLVM et Clang. Ces outils permettent de traduire notre code source C en instructions bytecode eBPF compréhensibles par le noyau. Lancez la commande suivante dans votre terminal pour préparer votre machine de développement :

sudo apt update && sudo apt install -y clang llvm libelf-dev libbpf-dev bpfcc-tools linux-headers-$(uname -r) pkg-config build-essential

Résultat:

Reading package lists... Done
Building dependency tree... Done
linux-headers-generic is already the newest version.
libbpf-dev is newly installed with success.
Clang compiler and LLVM tools are ready for compilation.

Mise en place de l'environnement et premier probe sécurisé

Visualisation des flux de monitoring eBPF de production sans impact CPU

Le premier pas : Un Kprobe minimaliste mais robuste

Les kprobes, ou sondes de noyau, permettent de s'attacher à presque n'importe quel point d'entrée de fonction à l'intérieur du noyau Linux. Notre premier exercice consiste à intercepter la création de processus via l'appel système de clonage, tout en veillant à ne pas surcharger le CPU avec des opérations de journalisation trop lourdes. Créez un fichier nommé kprobe_clone.bpf.c et insérez-y le code de production suivant :

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

char LICENSE[] SEC("license") = "GPL";

SEC("kprobe/sys_clone")
int BPF_KPROBE(kprobe_sys_clone) {
    u64 id = bpf_get_current_pid_tgid();
    u32 tgid = id >> 32;
    u32 pid = id;

    if (tgid == 0) {
        return 0;
    }

    bpf_printk("Appel clone detecte - TGID: %d, PID: %d\n", tgid, pid);
    return 0;
}

Examinons en détail la structure de ce code pour comprendre comment il garantit la sécurité et l'efficacité de l'exécution. La structure LICENSE déclarée sous la section SEC("license") est obligatoire pour que le noyau autorise l'accès aux fonctions d'aide critiques restreintes sous licence GPL. La macro BPF_KPROBE enveloppe proprement l'accès aux registres du processeur, masquant la complexité de l'architecture matérielle sous-jacente.

La ligne de filtrage if (tgid == 0) est une garde essentielle pour éviter de traiter les threads du noyau ou les processus inactifs, limitant ainsi l'overhead de traitement. Enfin, nous extrayons le PID et le TGID à partir d'un entier de 64 bits retourné par bpf_get_current_pid_tgid() en appliquant un décalage binaire vers la droite, une opération extrêmement rapide et sans allocation de mémoire.

Compilation et validation du bytecode

Pour transformer ce fichier source en un fichier objet exécutable par la machine virtuelle du noyau, nous devons cibler l'architecture de processeur virtuelle BPF. Utilisez la commande suivante pour exécuter la compilation avec un niveau d'optimisation élevé, indispensable pour que le validateur puisse simplifier le graphe d'exécution de notre code :

clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I/usr/include/x86_64-linux-gnu -c kprobe_clone.bpf.c -o kprobe_clone.bpf.o

Une fois le fichier objet kprobe_clone.bpf.o généré, vous pouvez utiliser l'utilitaire bpftool pour inspecter la structure interne du fichier et vous assurer que les symboles BTF ont été correctement embarqués. Cette étape de vérification est cruciale avant d'essayer de charger un programme sur des environnements de staging ou de production.

L'art du fentry et fexit pour des performances optimales

Pourquoi fentry écrase les kprobes traditionnels

Bien que les kprobes soient très populaires, ils souffrent d'un inconvénient majeur en production : leur coût en performances lors des transitions de contexte. Les sondes de type fentry/fexit (Function Entry/Exit) résolvent ce problème en s'intégrant directement au mécanisme de traçage de fonction natif du noyau, appelé ftrace. En éliminant le besoin de remplacer dynamiquement les instructions machine par des interruptions matérielles de type point d'arrêt, les sondes fentry réduisent l'impact sur la latence du système de près de 80% par rapport à un kprobe classique.

Pour illustrer ce flux d'exécution et comprendre comment les programmes eBPF s'exécutent en toute sécurité sans jamais provoquer de crash, analysons le cycle de vie d'un programme eBPF de l'espace utilisateur jusqu'au cœur du noyau Linux :

Flux d'exécution et de validation d'un programme eBPF du user space vers le kernel space

Ce schéma met en évidence le rôle central du BPF Verifier. Lorsqu'une application tente de charger un code eBPF dans le noyau via l'appel système sys_bpf(), le vérificateur analyse statiquement toutes les branches possibles du programme pour s'assurer qu'il ne contient aucune boucle infinie, qu'il ne tente pas d'accéder à des zones mémoire interdites et qu'il se termine toujours. Si le vérificateur détecte le moindre risque, le programme est immédiatement rejeté au lieu de provoquer un plantage complet du système, protégeant ainsi activement vos serveurs de production contre les erreurs de programmation.

Implémentation d'un tracing fentry de production

Mettons maintenant en œuvre un programme exploitant les performances de fentry pour monitorer de manière ultra-rapide les exécutions de commandes sur nos serveurs. Nous allons intercepter la fonction système __x64_sys_execve de façon sécurisée en lisant les arguments transmis à l'appel système depuis l'espace utilisateur sans risquer de violation d'accès mémoire.

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

char LICENSE[] SEC("license") = "GPL";

struct event_t {
    u32 pid;
    char comm[16];
    char filename[128];
};

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(u32));
    __uint(value_size, sizeof(u32));
} events SEC(".maps");

SEC("fentry/__x64_sys_execve")
int BPF_PROG(trace_execve, struct pt_regs *regs, const char *filename, const char *const argv[], const char *const envp[]) {
    struct event_t ev = {0};
    
    ev.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&ev.comm, sizeof(ev.comm));

    long res = bpf_probe_read_user_str(&ev.filename, sizeof(ev.filename), filename);
    if (res < 0) {
        bpf_printk("Echec de lecture securisee du chemin d'acces: %ld\n", res);
        return 0;
    }

    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &ev, sizeof(ev));
    return 0;
}

Dans cette structure, nous déclarons une map de type BPF_MAP_TYPE_PERF_EVENT_ARRAY appelée events. Cette structure de données de production sert de file d'attente circulaire asynchrone pour envoyer des événements de manière non bloquante vers l'espace utilisateur. Cela évite d'utiliser la fonction bpf_printk qui écrit dans un fichier de trace global lent et partagé par tout le système.

La fonction bpf_probe_read_user_str est l'un des piliers de la stabilité du système : elle copie en toute sécurité la chaîne de caractères située dans l'espace mémoire utilisateur (le nom du binaire exécuté) vers la mémoire sécurisée du noyau réservée à notre programme eBPF. Si le pointeur filename est invalide ou corrompu, la fonction renvoie une erreur négative que nous interceptons proprement pour interrompre le traitement sans causer de défaut de page dans le noyau.

Éviter le Kernel Panic : Gestion des ressources et limites du vérificateur

Dompter le BPF Verifier

Le validateur eBPF est impitoyable, et de nombreux développeurs juniors se heurtent à ses restrictions dès qu'ils tentent d'écrire des algorithmes un peu complexes. Pour assurer la stabilité de production, le validateur rejette systématiquement tout programme qui dépasse une complexité de 1 million d'instructions vérifiées ou qui possède des chemins d'exécution dont la terminaison n'est pas garantie à 100%. Cela signifie que toutes vos boucles de traitement doivent utiliser des bornes d'itération constantes connues dès la compilation.

Astuce de Performance

Ne tentez jamais de parcourir de grands tableaux dynamiques en eBPF. Si vous devez analyser des listes d'éléments, utilisez des directives de préprocesseur pour limiter strictement le nombre d'itérations, ou déléguez le filtrage lourd à votre application dans l'espace utilisateur.

Un autre piège fréquent concerne l'accès aux pointeurs imbriqués. Lorsque vous lisez des champs complexes de structures réseau ou d'informations de processus, vous devez explicitement vérifier chaque pointeur intermédiaire contre la valeur nulle avant de le déréférencer, ou utiliser des macros d'accès sécurisé fournies par bpf_core_read.h telles que BPF_CORE_READ(). Sans cela, le validateur arrêtera net le chargement de votre programme avec une erreur explicite.

Gestion des maps de production et comparaison des techniques

Pour assurer la pérennité de votre infrastructure de tracing en production, le choix de la technologie de capture doit être mûrement réfléchi. Le tableau ci-dessous compare les deux approches étudiées afin de vous guider dans vos décisions architecturales selon vos contraintes système :

Caractéristique Kprobes (Kernel Probes) Fentry (Function Entry)
Compatibilité noyau Excellente (disponible sur presque tous les vieux noyaux) Noyaux récents (> 5.5) avec support BTF actif
Impact sur les performances Moyen (overhead dû aux interruptions matérielles) Extrêmement faible (liaison directe via ftrace)
Stabilité et sécurité Élevée (vérifié par eBPF) Maximale (vérification des types d'arguments à la compilation)
Facilité de maintenance Complexe (sensible aux changements internes du noyau) Simple (repose sur le dictionnaire BTF standardisé)

Pour éviter les fuites de mémoire et la saturation de vos tables d'événements, configurez toujours des limites de taille maximales restrictives sur vos maps eBPF. Une map de type table de hachage qui déborde refusera tout simplement l'enregistrement de nouvelles entrées, protégeant ainsi la mémoire physique globale de votre système contre tout risque de déni de service interne provoqué par un trafic applicatif anormal.

Maîtriser eBPF sans frissonner le vendredi soir

Intégrer eBPF dans votre arsenal de debugging en production vous offre une visibilité inégalée sans compromettre la stabilité de vos plateformes. En combinant l'utilisation systématique de CO-RE pour la portabilité et les sondes de type fentry pour préserver vos ressources CPU, vous pouvez désormais concevoir des outils de monitoring sur mesure capables de répondre instantanément aux pannes les plus complexes. N'oubliez jamais d'analyser vos codes à l'aide de bpftool et de respecter scrupuleusement les exigences du validateur eBPF pour garantir des déploiements sans aucun risque.

Espace commentaire

Écrire un commentaire

Rejoignez la discussion

Vous devez être connecté pour poster un message.

28 commentaires

Super article, mais je bloque sur l'installation des dépendances. La commande apt install me sort une erreur sur le paquet libbpf-dev, il dit qu'il est introuvable sur ma Debian 10.

26/05/2026 à 00:03

C'est normal, Debian 10 est trop vieille. Le support BTF et les libs récentes demandent au minimum Debian 11 ou Ubuntu 20.04. Passe sur une version plus récente pour avoir les headers kernel compatibles.

26/05/2026 à 00:03
adrien28
Membre
Avatar de adrien28
adrien28
Membre

J'essaie de compiler le kprobe_clone.bpf.c mais Clang me renvoie une erreur de type vmlinux.h not found. Où est censé se trouver ce fichier ?

26/05/2026 à 00:03

Tu dois générer le vmlinux.h toi-même à partir de ton kernel actuel. Utilise bpftool pour extraire les types :

bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
26/05/2026 à 00:03

J'ai implémenté le fentry pour __x64_sys_execve, ça tourne bien mais je sature ma map events en quelques secondes sur un serveur à forte charge. Comment limiter proprement ?

26/05/2026 à 00:03
esauvage
Membre
Avatar de esauvage
esauvage
Membre

Quelqu'un a déjà testé ça sur un noyau 4.19 ? L'article dit que fentry demande 5.5+, c'est bloquant pour mon infra legacy.

26/05/2026 à 00:03

Sur 4.19, oublie fentry. Reste sur les kprobes classiques. C'est moins performant mais c'est la seule option viable sur les vieux noyaux sans support BTF natif.

26/05/2026 à 00:03

Merci pour le tuto ! Par contre, j'ai une erreur invalid mem access lors de l'accès à filename dans trace_execve. Une idée ?

26/05/2026 à 00:03

C'est probablement parce que tu n'utilises pas bpf_probe_read_user_str. Tu ne peux pas déréférencer directement un pointeur venant de l'user space. Montre ton code si ça persiste.

26/05/2026 à 00:03

Est-ce que je peux utiliser bpf_printk pour débugger en prod ? Ou c'est risqué ?

26/05/2026 à 00:03

À ne jamais faire en prod. bpf_printk est très lent car il écrit dans un fichier global. Utilise les perf_event_array comme montré dans l'article pour remonter tes données proprement.

26/05/2026 à 00:03
martin71
Membre
Avatar de martin71
martin71
Membre

Le validateur eBPF me rejette mon programme à cause de la taille du stack. J'ai déclaré un buffer de 512 octets, c'est trop ?

26/05/2026 à 00:03

La limite est de 512 octets pour tout le stack eBPF. Si tu as déjà des variables locales, tu dépasses. Réduis ton buffer ou utilise une BPF_MAP_TYPE_PERCPU_ARRAY pour stocker tes données temporaires.

26/05/2026 à 00:03
nicolas01
Membre
Avatar de nicolas01
nicolas01
Membre

J'ai une erreur BPF_PROG_LOAD: Argument list too long. C'est lié à quoi ?

26/05/2026 à 00:03

C'est que ton programme est trop complexe pour le vérificateur. Il dépasse le million d'instructions. Simplifie tes boucles ou segmente ton code en plusieurs programmes tail_call.

26/05/2026 à 00:03
rraynaud
Membre Actif
Avatar de rraynaud
rraynaud
Membre Actif

J'ai bien suivi le tuto, mais je ne vois rien dans /sys/kernel/debug/tracing/trace_pipe. Le module est pourtant chargé.

26/05/2026 à 00:03

Vérifie que ton filtre tgid == 0 ne bloque pas tout. Si tu lances ton script en tant que user standard, ton PID ne sera jamais 0. Teste avec sudo.

26/05/2026 à 00:03

Est-ce que je peux utiliser BPF_CORE_READ sur des structures qui ne sont pas dans vmlinux.h ?

26/05/2026 à 00:03

Non, BPF_CORE_READ repose sur les informations BTF du noyau. Si la structure n'est pas connue du BTF, ça ne fonctionnera pas.

26/05/2026 à 00:03
jean-bigot
Membre
Avatar de jean-bigot
jean-bigot
Membre

Quel compilateur tu conseilles ? Clang 10 suffit ?

26/05/2026 à 00:03

Clang 10 est vieillissant. Utilise au moins Clang 14 ou 15 pour avoir un support correct des features BPF modernes comme les CO-RE.

26/05/2026 à 00:03
alfred36
Membre
Avatar de alfred36
alfred36
Membre

Comment je peux monitorer l'usage CPU de mon programme eBPF lui-même ?

26/05/2026 à 00:03

Utilise bpftool prog show. Il te donnera le temps d'exécution total et le nombre d'appels pour chaque programme chargé.

26/05/2026 à 00:03
dominique07
Membre
Avatar de dominique07
dominique07
Membre

Merci pour le guide, ça m'a sauvé un debugging de 3 jours sur un problème d'execve introuvable.

26/05/2026 à 00:03

Content que ça t'ait débloqué. eBPF est puissant mais la courbe d'apprentissage est raide. Bonne continuation.

26/05/2026 à 00:03

Rejoindre la communauté

Recevoir les derniers articles gratuitement en créant un compte !

S'inscrire