Je débute la programmation en eBPF avec Aya. L’idée de cette série d’articles est d’apprendre un nouveau type de programme eBPF et de l’expérimenter avec le framework Rust Aya.
Aujourd’hui, nous allons nous plonger dans les uProbes et les uRetProbes : des programmes eBPF qui sondent les fonctions de l’espace utilisateur sans laisser de trace.
Vous allez voir que cela peut être très intéressant pour du profilage, du debug ou même de la rétro-ingénierie.
Qu’est-ce qu’une u•Ret•Probe ?#
En anglais, une probe peut se traduire par une sonde pour examiner ou explorer quelque chose. En eBPF, il y en a de plusieurs types : kProbe, kRetProbe, uProbe, uRetProbe et USDT.
kProbe : la sonde pour le kernel#
Si tu consultes la documentation d’eBPF, il n’y a pas de section consacrée aux programmes de type uProbe ou uRetProbe. Mais il y en a une dédiée à la kProbe :
La kProbe a pour but d’observer des fonctions du kernel Linux. Elle peut être considérée comme la probe parente. Toutes les autres probes sont en fait le même type de programme BPF_PROG_TYPE_KPROBE
mais c’est juste le point d’attache qui va déterminer comment le programme est exécuté.
kRetProbe : retour sur la sonde kernel#
La kRetProbe est simplement dédiée à l’observation du retour des fonctions du kernel Linux. Cette sonde permet ainsi de vérifier si l’appel de la fonction s’est bien terminé.
Nous avons vu brièvement kProbe et kRetProbe qui pourraient faire l’objet d’autres articles. Parlons maintenant des probes qui nous intéressent aujourd’hui : uProbe et uRetProbe.
uProbe : la sonde pour les utilisateurs#
Contrairement aux kProbes qui sont dédiées à observer les fonctions du kernel Linux, les uProbes sont dédiées aux fonctions de l’espace utilisateur : User-space Probes. Par exemple, on pourrait s’en servir pour compter le nombre d’appels aux fonctions malloc
et free
dans un programme C.
uProbe permet également de récupérer le contenu des arguments de la fonction observée. Ainsi on pourrait regarder la quantité de mémoire allouée à chaque malloc
ou vérifier que free
libère réellement des bons pointeurs.
Ainsi les uProbes pourrait s’intégrer à une CI pour automatiser des vérifications de sécurité, faciliter le debogage ou aider au diagnostic mémoire.
uRetProbe : retour sur la sonde utilisateur#
De la même manière que la kRetProbe, uRetProbe a pour but d’étudier le retour de la fonction cible de l’espace utilisateur : User-space Return Probe. On peut donc découvrir la valeur que retourne la fonction. Cela permet ainsi de debugger ou d’observer le comportement final de la fonction.
Mais il y a un autre intérêt : en combinant les temps de l’uProbe et de l’uRetProbe, on peut récupérer la durée que met une fonction à s’exécuter assez facilement. Il est ainsi possible de profiler une fonction de son programme.
Les uRetProbe et uProbes peuvent être utilisées pour débugger et comprendre un programme dont tu n’as pas le code source. Ça peut donc être un bel outil de rétro-ingénierie (reverse engineering).
Par contre, elles sont limitées aux programmes dont le langage est compilé : C/C++, Rust, Go, etc. Si on a un programme développé avec un autre langage, USDT pourrait vous convenir. Parlons-en.
USDT : le tracepoint de l’espace utilisateur#
USDT veut dire User Statically-Defined Tracing. Comme son nom l’indique, elle est également dédiée aux programmes de l’espace utilisateur mais il faut rajouter dans le code des sondes usdt pour les utiliser. USDT est, en fait, dérivée de l’uProbe.
Par contre, elle est beaucoup plus précise que l’uProbe. En effet, la sonde uProbe est cantonnée au début de fonction alors que la sonde usdt peut être mise à n’importe quel endroit dans le code.
Voici un exemple de code Python :
def benchmark_module():
loop = 0
for _ in range(100000):
pyusdt.trace_start_loop(loop)
calculate_pi(1000)
pyusdt.trace_stop_loop(loop)
loop += 1
Avec ce code, on peut avoir la durée pour calculer les 1000 décimales de π.
Nous allons maintenant nous consacrer pour la suite de l’article aux uProbes et uRetProbes. Parlons d’abord un peu de leur histoire.
Origin story#
uTrace l’ancêtre#
Vouloir tracer des fonctions de l’espace utilisateur depuis le noyau Linux ne date pas de l’introduction d’eBPF. Par exemple, une (première ?) tentative est apparue en 2007 avec les uTraces :
Mais elles n’ont jamais été incluses dans le code principal du fait d’opposition de certains mainteneurs.
Habemus uProbe#
Il a fallu attendre 2012 pour que le consensus finisse par arriver et les uProbes ont été introduites lors de la version 3.5 du noyau Linux :
À l’époque, les uProbes étaient limités par rapport à celles qu’on connaît aujourd’hui.
Ainsi, elles ont ensuite été améliorées avec la version 3.14 (sortie en 2014, la même année que l’introduction d’eBPF) :
Ce patch a permis de récupérer un nombre plus important de données comme la valeur de retour d’une fonction.
Les uProbes sont alors devenues pleinement exploitables alors qu’eBPF n’était pas encore sortie. Voyons quand son intégration s’est faite.
uProbe avec eBPF#
D’après la documentation d’eBPF, kProbe est apparue en 2015 dans la version 4.1 du noyau Linux. C’est Alexei Starovoitov, l’un des créateurs d’eBPF, qui l’a initié :
Comme une uProbe est une kProbe avec un point d’attache différent, on pouvait commencer à développer des uProbes avec eBPF à partir du 2 avril 2015.
Cependant, il fallait encore attendre que les frameworks eBPF de l’époque puissent le gérer.
Ainsi on pouvait déjà l’utiliser en 2016 avec BCC comme l’atteste le tutoriel de Brendan Gregg :
On peut voir également son issue sur GitHub datant d’octobre 2015 :
Pour finir, voici une petite frise chronologique de l’histoire des uProbes :
Maintenant qu’uProbe a plus de 10 ans d’existence, on peut se poser une question bien légitime : est-elle encore utilisée et par quel projet ?
Quels projets utilisent u•Ret•Probe ?#
Pour défier le lieu commun : “eBPF c’est utilisé que par 3 grosses boîtes”, j’ai fait une petite recherche des outils qui utilisaient réellement les uProbes et que donc vous l’utilisiez peut-être sans le savoir…
Pixie : where is my mind?#
Le projet Pixie utilise uProbe notamment pour tracer les connexions TLS :
D’ailleurs il y a un article de Douglas Mendez pour capturer le traffic HTTPs avec Aya.
Parca : l’hiver vient !#
Le projet Parca utilise également les uProbes.
Inspektor Gadget : hé là, qui va là?#
Le projet Inspektor Gadget a créé des outils basés sur des uProbes et sur des sondes USDT depuis 2024 :
Bonus Track#
Pour finir la présentation, je vous partage quelques liens bien sympathiques que j’ai trouvé lors de mes recherches sur les uProbes :
- Utilisation des uprobes sans eBPF par Brendan Gregg en 2015 :
- L’excellent article de blog de Julia Evans sur tous les systèmes de tracing sous Linux. Vous pouvez également lire son zine :
- Si les uProbes ne vous conviennent pas, peut-être que les bpftimes d’Eunomia peuvent vous intéresser :
Maintenant qu’on a présenté uProbe et uRetProbe, voyons comment débuter son développement avec Aya.
Comment débuter son programme Aya ?#
Quand on démarre le développement d’un nouveau programme eBPF, la première difficulté est de réussir à le démarrer. Pour cela, il a besoin d’un événement déclencheur (event-driven). Dans cet épisode, cet événement sera donc le passage d’une uProbe ou d’une uRetProbe dans le noyau Linux.
Aya nous facilite la tâche. Quand on lance la commande :
cargo generate https://github.com/aya-rs/aya-template
Tu devras répondre à deux questions importantes qui permettront de définir cet événement :
🤷 Target to attach the (u|uret)probe? (e.g libc):
🤷 Function name to attach the (u|uret)probe? (e.g getaddrinfo):
Voyons comment y répondre.
Cible pour attacher l’u•Ret•Probe#
La première question demande le nom d’une bibliothèque (comme la libc
) ou d’un binaire. La question aurait pu être posée autrement : quel fichier tu veux debugger ou tracer ?
Il faut voir ça comme un filtre :
- Si tu choisis
libc
, le programme eBPF ne pourra démarrer que si un programme de lalibc
est exécuté - Si tu choisis un binaire, il ne pourra démarrer que si le binaire est exécuté.
Mais cela n’est pas suffisant pour démarrer le programme eBPF. Il faut être plus précis : donner le nom d’une fonction.
Nom de la fonction pour attacher l’u•Ret•Probe#
La seconde question demande ainsi la fonction du binaire ou de la bibliothèque que tu veux débugger.
Par exemple :
- si tu choisis le nom d’une fonction d’un programme C, le programme eBPF sera lancé à chaque fois qu’il passe par cette fonction.
- si tu choisis une fonction de la
libc
, il ne sera lancé lorsqu’un programme appellera cette fonction de lalibc
.
Si cela vous parait un peu trop théorique, nous allons finir le chapitre en parlant d’un outil bien sympathique qui va nous permettre d’illustrer cela.
S’initier à eBPF avec bpftrace#
Le projet bpftrace permet de créer rapidement la plupart des types de programme eBPF dédiés aux tracings dont notamment uProbe et uRetProbe mais également USDT, kProbe et kRetProbe (Voir la prise en charge ici).
N’hésitez pas à l’installer, il est probablement packagé pour votre distribution Linux favorite.
Trève de bavardage, prenons un exemple :
sudo bpftrace -e \
'uretprobe:/bin/bash:readline { printf("%s\n", str(retval)); }'
Que veut dire cela ?
uretprobe
: le type de programme eBPF/bin/bash
: le binaire ciblereadline
: le nom de la fonction{ printf("%s\n", str(retval)); }
: le code du programme bpftrace (il affiche la valeur retour de la fonction)
Cette commande crée ainsi un programme eBPF de type uRetProbe avec comme point d’attache la fonction readline du binaire bash.
Si vous avez vraiment lancé la commande, vous allez voir que cette création est quasi immédiate ! Vérifions qu’il fonctionne bien.
Démarrez un autre terminal et lancez quelques commandes de ton choix. Voici un exemple de ce que vous pourrez voir sur le terminal bpftrace :
Attaching 1 probe...
ls -lrth
hello
man woman
Vous voyez toutes les commandes que vous avez tapé sur le terminal !
bpftrace peut ainsi être un bon moyen de prototyper un programme uProbe ou uRetProbe avant de le générer avec Aya.
Et si je veux observer une autre fonction que readline
dans le programme bash ? Comment faire ? Le premier reflexe serait d’aller dans le code de bash et de chercher une autre fonction mais il y a plus simple et plus sûr :
bpftrace -l 'uretprobe:/bin/bash:*'
Cette commande va te lister toutes les fonctions disponibles dans bash.
bpftrace -l 'uretprobe:/bin/bash:*' | wc -l
Ainsi bpftrace va nous permetre pour la suite de vérifier la faisabilité avant de créer le programme en Rust avec Aya.
Dans cet épisode, on a vu les bases des uProbes et des uRetProbes : à quoi elles servent, leur histoire, qui les utilise et comment trouver le bon point d’accroche. Nous avons également vu bpftrace, un outil qui permet de créer des probes rapidement.
Nous allons maintenant passer à la pratique dans l’épisode suivant : on va créer un petit programme Go et on va le faire réagir avec des programmes eBPF de type uProbe et uRetProbe avec Aya.