Blog Haypo

Aller au contenu | Aller au menu | Aller à la recherche

mardi 24 avril 2007

Imposer des limites arbitraires

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.

jeudi 12 avril 2007

Mes correctifs Python intégrés dans le trunk officiel

La nuit porte conseil. J'ai continué à traquer les bugs que j'avais trouvé dans Python (rappel : plantage de Python lorsque la mémoire est épuisée) et j'en ai trouvé un autre (comparaison d'un entier court et d'un entier long). Je les ai isolés et corrigés.

J'ai alors écrit un rapport de bug sur Sourceforge et j'ai contacté des développeurs Python sur IRC (salon #python-dev du serveur Freenode). Ils sont très sympas et réactifs.

Finalement, mes correctifs ont été appliqués dans le trunk de Python (la version de développement) et feront partie de Python 2.5.2 (la version 2.5.1 étant en cours de finalisation). 48h pour corriger un bug, je trouve ça tout de même très court comme délai quand on sait que Microsoft Windows et Internet Explorer ont des bugs vieux de plus de 6 ans :-)

mercredi 4 avril 2007

Nouvelles d'hachoir-metadata

hachoir-metadata est un programme permettant de lire les métadonnées d'un fichier : taille d'une image, auteur d'une vidéo, durée d'un son, etc. Il repose sur hachoir-parser pour lire les informations d'un fichier.

Traitements automatiques

hachoir-metadata réalise de plus en plus de traitements automatiques haut niveaux tels que :

  • supprimer les espaces inutiles
  • ignorer les chaînes de caractère vides
  • filtrer les valeurs : ignore les valeurs abbérantes (ex: image ayant une largeur nulle)
  • supprimer les doublons

La suppression des doublons ne concerne pas simplement les valeurs identiques. Pour les chaînes de caractère, hachoir-metadata est capable de reconnaître qu'une chaîne est le début d'une autre. Exemple : si on trouve les deux auteurs "James Brown" et "James Br" pour une chanson, seule la chaîne la plus longue est conservée (James Brown !).

Réutilisation des valeurs

L'extracteur de métadonnée est de plus en plus rigoureux : les valeurs doivent être d'un type précis. Par exemple, la durée d'une chanson est maintenant du type Python « timedelta ». Avant les dates, durées, débit en bit/sec, nombre de canaux audios étaient tantôt une chaîne de caractère, tantôt un entier, tantôt une date, ...

Le fait que le type des données soit strict a permis de faire des calculs sur les documents multimédias. On peut maintenant obtenir le débit en bits par seconde pour du son et de la vidéo, et le taux de compression pour une image et du son. Ceci permet de comparer la qualité d'un codec.

Option --quality

J'ai également ajouté l'option --quality permettant de choisir la « qualité » des métadonnées extraites. En fait, cette option détermine la vitesse d'extraction : les opérations lourdes ne seront faites que pour quality=1.0, alors que pour quality=0.0 toutes les opérations lentes sont ignorées. Cette option influe par exemple sur le calcul de la durée d'un MP3 à débit variable : pour un calcul exact, il faut lire le fichier en entier mais ceci est très long. L'option quality va donc faire varier le nombre de champs traités.

Lire la suite

Nouvelles d'Hachoir (core et parser)

Je viens de me rendre compte que ça fait pas mal de temps que je code sans écrire de journal sur les derniers développement de mon projet Hachoir. Voici donc un premier billet donnant des nouvelles du front.

hachoir-core

hachoir-core est le cœur d'Hachoir : la partie bas niveau qui va découper un fichier en une multitudes de champs. Mais ce composant contient également énormément d'outils divers comme une humanDuration() qui va convertir une durée en une représentation « humaine » (ex: "22 sec 320 ms"). Aujourd'hui, hachoir-core évolue peu car il commence à couvrir l'ensemble des besoins d'un parseur.

Le plus gros changement récent est la tolérance aux erreurs. En fait, l'erreur n'est pas corrigée mais rattrapée. Par exemple, si une erreur est détectée durant la génération de la description d'un champ : l'erreur est affichée et la description devient une chaîne vide. Ceci peut sembler naturel, mais ce n'était pas le cas avant. Précédemment, si la génération d'une description échouait, on perdait beaucoup d'informations car l'erreur déclanchait une cascade d'autres erreurs et finalement plusieurs champs étaient détruits. Ce principe de rattrapage d'erreurs est utilisé dans un maximum de code. Il reste peu de fonctions qui ne sont pas « protégées » ce qui rend le code toujours plus robustes.

hachoir-parser

hachoir-parser est un ensemble de parseurs de fichiers (images, vidéos, archives, programmes, ...). Ce composant est celui auquel contribue le plus de monde car il est simple de le modifier (corriger) ou d'ajouter son propre parseur. J'ai notamment reçu un gros coup de main de Christophe GISQUET qui a écrit les parseurs ACE, RAR, Torrent et d'autres. Mike Melanson, le chef de projet du greffon Flash pour Linux, a également écrit des parseurs pour les formats Real Audio et Real Media. Enfin, Olivier SCHWAB a écrit un parseur 7-zip.

La liste des ajouts récents de parseur montre la forte activité de ce composant :

  • Archive : archive ACE, Microsoft cabinet (CAB), Roshal archive (RAR), archive Microsoft (MAR)
  • Audio : Uncompressed amiga module (MOD), ScreamTracker3 module (S3M), FastTracker II Extended Module (XM), Audio Interchange File Format (AIFF et AIFC), Real audio (RA), image Targa (TGA)
  • Image : Photoshop (PSD), icone animé Windows (ANI), Aldus Placeable Metafile (APM), Microsoft Enhanced Metafile (EMF) et Microsoft Windows Metafile (WMF)
  • Divers : BitTorrent (.torrent), police de caractère TrueType (TTF), document PDF, exécutable Windows 16-bit (NE), vidéo MPEG-2 Transport Stream (MPEG TS), raccourci Windows (LNK), X11 Portable Compiled Font (PCF), aide Windows HTML (CHM), ...

À l'heure actuelle il y a 70 parseurs dans la version de développement d'Hachoir. J'ai du mal à savoir si c'est beaucoup ou peu. En consultant la liste complète des parseurs, je pense tout de même que c'est assez conséquent pour ne pas dire énorme. Il faut d'ailleurs savoir que certains parseurs gèrent plusieurs formats. Exemples : RIFF parse les formats AVI, WAV, ANI et CDA; WMF parse les formats AMF, EMF et WMF; etc.