Le programme dvidvi

Ce programme, bien connu, permet de faire quelques réécritures sur des fichiers DVI (extraire des pages, par exemple).

Il comporte un bug assez embêtant, mais que j'ai eu du mal à caractériser. Sous des conditions mal définies, dvidvi plante avec des messages d'erreurs assez sybilins (on est voisin du segmentation fault, ça parle d'un appel a free sur une mauvaise adresse mémoire.

Par exemple:

bayartb@edgard:~$ dvidvi -f '(@1)91' -l '(@1)180' manuel3.dvi essai.dvi

This is dvidvi 1.0, Copyright (C) 1988-91, Radical Eye Software
manuel3.dvi -> essai.dvi
[111] [112] [113] [114] [115] [116] [117] [118] [119] [120] [121] [122] [123] 
[124] [125] [126] [127] [128] [129] [130] [131] [132] [133] [134] [135] [136] 
[137] [138] [139] [140] [141] [142free(): invalid pointer 0x40018000!
! unexpected eof on DVI file

Après analyse, il semble que la seule fonction défectueuse qui génère ce plantage soit celle qui est utilisée pour extraire une chaîne de texte arbitraire dans le fichier DVI pour tenter de localiser certains appels à special. La fonction est manifestement buggée.

Elle lit du texte en le copiant dans une zone mémoire, puis renvoie un pointeur sur la fin de cette zone mémoire, avançant encore et toujours plus loin au fur et à mesure des appels dans cette zone mémoire. Non seulement elle renvoie un pointeur sur la fin du texte (alors que les fonctions appelantes s'attendent à un pointeur sur le début), meis en plus elle doit finir par écrire n'importe où si le DVI est suffisament long (cette fonction est appelée au moins au début de chaque page, et pour toutes les commandes DVI d'un groupe donné).

La patch suivant corrige cet bug évident, et fait que, chez moi, les appels qui ne marchaient pas se sont mis à marcher.

--- dvidvi-1.0/dvidvi.c 1997-10-05 13:28:02.000000000 +0200
+++ dvidvi-1.0-benj/dvidvi.c    2004-01-31 14:56:06.000000000 +0100
@@ -289,9 +289,12 @@
 void stringdvibuf(p,n)
 integer p,n;
 {
+  char *tmp;
   fseek(infile,p,SEEK_SET);
+  tmp = temp;
   while ( n-- > 0 )
     *temp++ = dvibyte();
+  temp=tmp;
 }
 
 /*

Le bug a été envoyé au mainteneur du paquet Debian. Impossible de joindre l'auteur de dvidvi (Thomas Rockiki).

Un autre bug, ou plus exactement une fonctionnalité qui fait gravement défaut. Si le fichier DVI fait appel à des choses un peu avancées en matières de PostScript (PStricks, par exemple, mais la couleur devrait suffir à poser problème), ça se manifeste par la présence dans la première page d'un \special qui indique à dvips et à xdvi quels fichiers PostScript il faut incorporer. Le programme dvidvi ne tient pas compte de ces informations, du coup si la première page ne fait pas partie des pages à garder, l'information est perdue et le fichier devien invalide (ou pour le moins ne produit pas le résultat escompté: dvips produit un fichier PostScript invalide, et xdvi râle et échoue à l'affiche de certaines pages).

Pour ajouter cette fonctionnalité à dvidvi il a fallu un peu plus de ruse et de modifications. Le patch suivant ajoute la fonctionnalité voulue, un peu crade, mais ça marche. Le goto permet de rester avec un patch relativement court.

--- dvidvi-1.0.orig/dvidvi.c
+++ dvidvi-1.0/dvidvi.c
@@ -136,6 +136,12 @@
 integer *pagenumbers ;
 int prettycolumn ;       /* the column we are at when running pretty */
 
+// To handle xxx1 for headers
+// Lets suppose there will be less than 256 of those.
+char *headers[256];
+integer ln_headers[256];
+integer nb_headers = 0L;
+
 /*
  *   This array holds values that indicate the length of a command, if
  *   we aren't concerned with that command (which is most of them) or
@@ -835,6 +844,7 @@
    (void)dvibuf(p);
    pagenumbers[num]=signedquad();
 }
+
 /*
  *   This routine simply reads the entire dvi file, and then initializes
  *   some values about it.
@@ -909,6 +919,7 @@
  */
 
    p = pageloc[0L] + 45 ;
+nextcmd:
    c=dvibuf(p);
    while (comlen[c]) {
       p += comlen[c] ;
@@ -920,6 +931,29 @@
         landscape = p ;
         rem0special = 1 ;
       }
+      stringdvibuf(p+2L,6L);
+      if (strncmp(temp, "header", 6)==0) {
+	char l1;
+	l1 = dvibuf(p+1);
+	stringdvibuf(p+2,l1);
+	if ( nb_headers == 256 ) {
+	   fprintf(stderr,"\n"
+			  "There are more than 256 PS headers in your DVI file.\n"
+			  "Please ask a wizzard to enlarge me :-)\n");
+	}
+	ln_headers[nb_headers] = l1;
+	headers[nb_headers++] = strdup(temp);
+	/*
+	 * Seek at the end of the xxx1, then goto reading next commands.
+	 * In a normal DVI file, as produced by LaTeX, all of those special
+	 * commands (xxx1 with PS headers) are outputed on the very first
+	 * page, all together. So a simple loop might be enough. Just in
+	 * case we have a strange file we accept that there might be some
+	 * typesetting commandes between those xxx1.
+	 */
+	p = p + l1 + 2;
+	goto nextcmd;
+      }
    }
 }
 /*
@@ -1043,6 +1077,7 @@
    integer v, oldp ;
    unsigned char c;
 
+
 /*
  *   We want to take the base 10 log of the number.  It's probably
  *   small, so we do it quick.
@@ -1072,6 +1107,31 @@
    p = pageloc[num] + 45 ;
    c=dvibuf(p);
    while (c != 140) {
+      if ( nb_headers ) {
+	 integer i;
+	 /*
+	  * Need to output the 'nb_headers' headers that we collected.
+	  * If we are outputing the real first page of the previous
+	  * DVI file, then the headers will be doubled, which is not
+	  * troublesome: dvips will keep only one of those.
+	  */
+	 for ( i=0; i",headers[i]+7);
+	       prettycolumn += ln_headers[i]+2-7;
+	    }
+	 }
+	 /*
+	  * We will output the headers only one time. That is possible that
+	  * those headers are not usefull (the pages where they are used might
+	  * not be kept in the out file, but there is no simple way to avoid
+	  * it.
+	  */
+         nb_headers = 0;
+      }
       if ((len=comlen[c]) > 0) {    /* most commands are simple */
          outdvibyte(c);
          putbuf((long)len-1) ;
@@ -1198,5 +1258,8 @@
    processargs(argc, argv) ;
    readdvifile() ;
    writedvifile() ;
+   if (!quiet) {
+      fprintf(stderr,"\n");
+   }
 }

Le patch téléchargeable ici, regroupe ces deux modifications.