Durant mes essais de fuzzing, j'ai compris assez vite qu'espérer écrire un programme parfait n'est qu'un idéal. Plutôt que de corriger les erreurs, je pense qu'il vaut mieux écrire du code tolérant aux erreurs. Je veux dire par là que le programme continuera à fonctionner même si une procédure échoue.

Utiliser les exceptions

On peut utiliser pour ça le couple try/except en Python. Exemple trivial :

value = (...)
try:
   print "Date : %s" % datetime.datetime.fromtimestamp(value)
except ValueError:
   print "Date invalide (%r) !" % value

Mais le fuzzing mène souvent à une situation d'épuisement (monopolisation) des ressources : votre programme va manger tout le temps processeur, toute la mémoire, remplir le disque dur, utiliser toute sa pile, etc. J'ai alors cherché comment détecter ces situations de crise. D'ailleurs elles ne doivent pas être vue comme critiques mais normales et il faut les avoir à l'esprit en écrivant un programme. Effectivement, les ressources sont limitées : il faut apprendre à partager.

Limiter la mémoire

Sous Linux, on peut utiliser resource.setrlimit(RLIMIT_AS, ...). Si la mémoire totale dépasse max_mem, une exception MemoryError est émise par Python.

J'ai implémenté une fonction limitedMemory() qui va limiter temporairement la mémoire : lire memory.py d'hachoir_core. L'erreur apparait si la mémoire grossit de la quantité d'octets indiquée. Il suffit alors d'utiliser « try: limitedMemory(maxmem, ...) except MemoryError: ... ».

Limiter le temps processeur

Pour éviter que le programme reste bloqué au même endroit pendant un temps excessif (cas typique : une boucle infinie), il faut pouvoir appeler une fonction avec une durée maximale. Sous Linux, on peut utiliser au choix : time.alarm() ou resource.setrlimit(RLIMIT_CPU, ...). À noter que pour la seconde solution, les pauses (time.sleep()) et le temps passé dans le noyau ne sont pas pris en compte : il vaut donc mieux utiliser une alarme. Une alarme déclanche un signal SIGALRM alors que RLIMIT_CPU va générer un signal SIGXCPU.

J'ai implémenté les deux méthodes dans la fonction limitedTime(sec) : lire timeout.py d'hachoir_core.

Lorsque c'est possible, il vaut mieux utiliser des fonctions offrant déjà cette fonctionnalité comme par exemple la fonction select().

Limiter la pile

En testant dpkg, j'ai réussi à le planter avec « COLUMNS=10000000 dpkg -l ». Après investigation, il s'est avéré que l'erreur venait de la libc (chose qui semblait impensable à mes yeux). En creusant encore, j'ai vu que vfprintf() utilisait massiment la pile pour écrire la sortie de dpkg (qui configure stdout pour ne pas utiliser de tampon).

Bref, j'ai cherché à voir s'il était possible d'attraper l'erreur « épuisement de la pile ». Et bien sûr que oui : c'est possible ! Par contre, quand la pile est hors-service, hors de question d'utiliser printf() ou autre fonction succeptible de réutiliser la pile. Linux permet d'utiliser une pile dédiée aux gestionnaires de signaux. Ah là là, il est quand même fort ce système d'exploitation, hein !

Les fonctions clés sont sigaltstack() pour créer une pile dédiée à notre gestionnaire de signal, sigaction() pour appeler notre fonction quand le signal SIGSEGV est émis, setjmp()/longjmp() pour quitter le code bogué et revenir à la « borne de sauvegarde » (renseignée par setjmp()).

Exemple d'implémentation : stack.c.

En réunissant tous ces élements (try/except, limiter la mémoire, temps et pile), je pense qu'on peut commencer à écrire des programmes robustes. Bien sûr, rien ne vaut un audit minutieux du code source.