Bogue, fonctionnalité, mauvais usage ? Un cas pratique

Posté par (page perso) . Édité par Davy Defaud, David Marec, ZeroHeure et tankey. Modéré par tankey. Licence CC by-sa.
Tags :
51
31
mar.
2019
Ligne de commande

Dans la rétrospective LinuxFr.org de la première quinzaine de mars 2019, il était question de la réponse surprenante de diff : « Le fichier /var/lib/lxc/alpha/rootfs/dev/full est un fichier spécial‐caractères alors que le fichier /var/lib/lxc/beta/rootfs/dev/full est un fichier spécial‐caractères. »

Les choses auraient pu en rester là. Mais quand même, ça reste une bonne occasion de se demander si c’est un bogue, une fonctionnalité ou un mauvais usage, non ? Jouons un peu avec ce cas pratique.

Sommaire

De quels fichiers parlions‐nous ?

$ ls -l /var/lib/lxc/alpha/rootfs/dev/full /var/lib/lxc/beta/rootfs/dev/full
crw-rw-rw- 1 root root 1, 7 mars   4  2011 /var/lib/lxc/alpha/rootfs/dev/full
crw-rw-rw- 1 root root 1, 7 oct.  21  2017 /var/lib/lxc/beta/rootfs/dev/full

Il s’agit donc de fichiers spéciaux en mode caractère (le c au début de la ligne), ayant pour majeur 1 et pour mineur 7. Ce type de fichiers peut être créé avec la commande mknod (paquet coreutils chez Debian, logiciel tiré du projet GNU coreutils).

Et le binaire /usr/bin/diff provenait du paquet Debian Stretch diffutils 1:3.5-3, également tiré du projet GNU diffutils.

$ diff --version
diff (GNU diffutils) 3.5

Reproduire le problème

D’abord créons un petit labo pour reproduire :

$ mkdir dir1 dir2
$ sudo mknod dir1/file c 1 7
$ sudo mknod dir2/file c 1 7
$ ls -l dir*/file
crw-r--r-- 1 root root 1, 7 mars  28 17:43 dir1/file
crw-r--r-- 1 root root 1, 7 mars  28 17:43 dir2/file
$ diff -r dir1 dir2
Le fichier dir1/file est un fichier spécial-caractères alors que le fichier dir2/file est un fichier spécial-caractères

OK, c’est reproduit, ça arrive (au moins) sur la comparaison récursive entre deux répertoires contenant chacun un fichier spécial en mode caractère avec même majeur, même mineur, même nom.

Un problème de traduction ?

Un effet de la langue ? Il s’agit peut‐être juste d’une erreur de traduction en français ?

$ export LC_ALL=C
$ export LANG=C
$ diff -r dir1 dir2
File dir1/file is a character special file while file dir2/file is a character special file

Pas mieux en anglais. Et d’ailleurs ce n’est pas mieux avec diffutils 1:3.7-2 de Debian Sid, en sachant que la 3.7 est la dernière version publiée par le projet GNU.

Avant de continuer à creuser, notons que la comparaison directe des deux fichiers est une mauvaise idée :

$ diff dir1/file dir2/file
(ne rend pas la main)

Débogage

Par curiosité un coup d’œil avec l’outil de débogage (sous GNU/Linux) strace (logiciel venant de strace.io) pour voir les appels système utilisés par un programme et les signaux reçus :

$ strace -f diff dir1/file dir2/file
…
stat("dir1/file", {st_mode=S_IFCHR|0644, st_rdev=makedev(0x1, 0x7), ...}) = 0
stat("dir2/file", {st_mode=S_IFCHR|0644, st_rdev=makedev(0x1, 0x7), ...}) = 0
openat(AT_FDCWD, "dir1/file", O_RDONLY) = 3
openat(AT_FDCWD, "dir2/file", O_RDONLY) = 4
read(3, "(que des NUL)"..., 4096) = 4096
read(4, "(que des NUL)"..., 4096) = 4096
read(3, "(que des NUL)"..., 4096) = 4096
read(4, "(que des NUL)"..., 4096) = 4096
…

Comme on peut lire en boucle sur ces fichiers, ça peut durer longtemps…

En revanche, on peut essayer sur la version récursive sur les deux répertoires :

$ strace -f diff -r dir1 dir2
…
stat("dir1/file", {st_mode=S_IFCHR|0644, st_rdev=makedev(0x1, 0x7), ...}) = 0
stat("dir2/file", {st_mode=S_IFCHR|0644, st_rdev=makedev(0x1, 0x7), ...}) = 0
fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(0x88, 0x3), ...}) = 0
write(1, "File dir1/file is a character sp"..., 92File dir1/file is a character special file while file dir2/file is a character special file
) = 92
…

OK, il n’a pas l’air de pousser la comparaison bien loin… On regarde les attributs des deux fichiers, ce sont des fichiers spéciaux, alors on sort la phrase peu explicative.

Les sources

Et si on regardait les sources du paquet Debian ?

$ apt source diffutils
$ cd diffutils-3.7/

La traduction ?

$ grep -1 "character special file" ./po/fr.po
#: lib/file-type.c:69
msgid "character special file"
msgstr "fichier spécial-caractères"
--
#: lib/file-type.c:84
msgid "multiplexed character special file"
msgstr "fichier spécial avec des caractères multiplexés"

Le premier cas est celui qui nous intéresse. On retrouve bien les lignes attendues côté code en excluant les fichiers de traduction :

$ grep -nr "character special file" .|grep -v "/po/"
./lib/file-type.c:69:    return _("character special file");
./lib/file-type.c:84:    return _("multiplexed character special file");

Il s’agit en gros d’une fonction file_type qui écrit « character special file » ou sa traduction lorsqu’elle croise un fichier spécial de type caractère.

Reste à trouver d’où vient le reste de la phrase :

$ grep -2 while po/fr.po
#: src/diff.c:1337 src/diff.c:1387
#, c-format
msgid "File %s is a %s while file %s is a %s\n"
msgstr "Le fichier %s est un %s alors que le fichier %s est un %s\n"

Visiblement une phrase générique prévue pour dire « ah, flûte, dommage, le premier fichier est un machin, tandis que le second est un bidule », sauf qu’ici machin == bidule (ici on sourit légèrement parce qu’on va aller voir la ligne 1 337 de diff.c, et que c’est l33t).

Le code

La portion de code concernée :

  {
    /* We have two files that are not to be compared.  */

    /* See POSIX 1003.1-2001 for this format.  */
    message5 ("File %s is a %s while file %s is a %s\n",
        file_label[0] ? file_label[0] : cmp.file[0].name,
        file_type (&cmp.file[0].stat),
        file_label[1] ? file_label[1] : cmp.file[1].name,
        file_type (&cmp.file[1].stat));

    /* This is a difference.  */
    status = EXIT_FAILURE;
  }

En gros, on est dans une zone concernant les fichiers non comparables, alors on affiche le message à base de machin et de bidule sans se poser la question du cas machin == bidule.

Le même code est utilisé ligne 1387, mais il semble plus pertinent, d’après le commentaire qui précise que l’un des fichiers est un lien symbolique et l’autre non, donc ici machin != bidule.

  {
    /* We have two files that are not to be compared, because
       one of them is a symbolic link and the other one is not.  */

    message5 ("File %s is a %s while file %s is a %s\n",
        file_label[0] ? file_label[0] : cmp.file[0].name,
        file_type (&cmp.file[0].stat),
        file_label[1] ? file_label[1] : cmp.file[1].name,
        file_type (&cmp.file[1].stat));

    /* This is a difference.  */
    status = EXIT_FAILURE;
  }

Tout laisse à penser que nous sommes en présence d’un bogue, ou en tout cas d’une imprécision dans la réponse fournie à l’utilisateur : la réponse sera toujours la même lorsque l’on compare deux fichiers spéciaux‐caractères, indépendamment de leurs majeur et mineur d’ailleurs, et le fait qu’ils soient différents (le but de diff donc) est seulement sous‐entendu, par le fait que le message apparaît (il n’y aurait pas de tel message sur deux fichiers comparables identiques).

Et avec un autre diff ?

Le commentaire côté GNU « See POSIX 1003.1-2001 for this format » pourrait laisser penser qu’il s’agit d’une contrainte de la norme POSIX. Mais sous FreeBSD diff se comporte différemment :

$ mkdir t1 t2
$ mkfifo t1/toto 
$ mkfifo t2/toto 
$ diff -r t1 t2
File t1/toto is not a regular file or directory and was skipped

Le BSD diff bloque aussi sur la comparaison entre les deux fichiers. Mais le message est donc plus explicite lorsqu’il s’agit d’une comparaison de deux répertoires.

De la sorte, ce comportement est tel que défini par l’open Group :

« If both file1 and file2 are directories, diff shall not compare block special files, character special files, or FIFO special files to any files and shall not compare regular files to directories. Further details are as specified in Diff Directory Comparison Format. The behavior of diff on other file types is implementation‐defined when found in directories. »

Si file1 et file2 sont des répertoires, diff ne devrait pas entreprendre la comparaison des fichiers spéciaux de type blocs ou caractères ou les tubes nommés (FIFO) à n’importe quel autre fichier ni comparer un fichier à un répertoire.

Ouvrir des bogues ?

Passons à l’étape suivante, récupérer le code source directement du projet en amont et soumettre une proposition de correction ? Cette partie est laissée à l’attention du lecteur.

$ git clone https://git.savannah.gnu.org/git/diffutils.git

ou https://svnweb.freebsd.org/base/release/12.0.0/usr.bin/diff/.

Aller plus loin

  • # Super

    Posté par . Évalué à 10 (+11/-0).

    Super dépêche qui remontre à ceux pour qui ça n'est pas encore clair que lorsqu'ils utilisent des logiciels libres, ils peuvent aller voir comment ils fonctionnent et ainsi :

    • apprendre des choses (pourquoi c'est fait ainsi ? comment est-ce que ça fonctionne en vrai ?)
    • améliorer les outils (en remontant un souci dans le code, la traduction ou la doc ou en proposant carrément un patch)

    Bon, ça demande un peu de connaissance technique, sinon il faut se faire aider :)

    • [^] # Re: Super

      Posté par . Évalué à 9 (+7/-0).

      Je plussoie et dirais même plus :

      L'open source est une incroyable matière première pour qui à la volonté de chercher un peu …

      Pour le vivre au quotidien rien de plus frustrant que d'avoir un binaire merdique que l'on pourrait améliorer mais dont on ne dispose pas des sources et que l'éditeur ne veut pas transmettre.

      • [^] # Re: Super

        Posté par . Évalué à 8 (+7/-0).

        Pour le vivre au quotidien rien de plus frustrant que d'avoir un binaire merdique que l'on pourrait améliorer mais dont on ne dispose pas des sources et que l'éditeur ne veut pas transmettre.

        J'ai une variante, tout aussi frustrante : une API qui crashe, qui te transmet sa stacktrace qui te montre le problème, mais que tu ne peux pas toucher puisque c'est pas toi qui a le service…

        • [^] # Re: Super

          Posté par . Évalué à 2 (+1/-1).

          Ouais pas mal …

          Mais le binaire dont je te parle permet d'exporter des données de base Oracle ou SQL Server de manière neutre ce qui permet de transférer des données indépendemment de la base de données

          Mais non seulement il est pas du tout optimisé, pour chaque table il prend un ligne, la transforme en ascii et l'écrit dans un fichier.
          Au lieu de lieu n lignes ( n dépendant du nombre de lignes totales ) de les transformer en ascii et de les écrire dans un fichier

          Mais en plus il génére beaucoup de données : ainsi un nombre codé sur 4 octets va se retrouver transformer en ascii du style " 123.56"
          Ceci dit cela se compresse à quasiment 80 % mais il faut de l'espace disque difficile à recupérer après

          Résultat entre un export base de données datapump de chez Oracle et cet utilitaire un rapport de 1 à 10 en terme de temps et de TRES TRES BEAUCOUP en terme de volume

          Cet utilitaire n'a quasiment pas évolué depuis des années et est incontournable dans certain cas.

          bref BEAUCOUP de temps perdu pour … pas grand chose

  • # Une petite étape en plus

    Posté par (page perso) . Évalué à 5 (+3/-0).

    Pour la didactique et/ou l'exégèse apétiste :

    Et si on regardait les sources du paquet Debian ?

    L'utilitaire est diff. Son chemin est
    $ which diff
    /usr/bin/diff

    Le paquet qui l'a installé est
    $ dpkg -S /usr/bin/diff
    diffutils: /usr/bin/diff

    Adhérer à l'April, ça vous tente ?

    • [^] # Re: Une petite étape en plus

      Posté par (page perso) . Évalué à 3 (+1/-0).

      Et sous FreeBSD,

      david@poudriere:~ % which diff
      /usr/bin/diff
      david@poudriere:~ % cat /usr/src/usr.bin/diff/Makefile 
      # $FreeBSD: stable/12/usr.bin/diff/Makefile 334894 2018-06-09 20:24:17Z bapt $
      
      .include <src.opts.mk>
      
      PROG=   diff
      SRCS=   diff.c diffdir.c diffreg.c xmalloc.c pr.c
      
      HAS_TESTS=
      SUBDIR.${MK_TESTS}+= tests
      
      .include <bsd.prog.mk>

      A noter que le traitement sur répertoires et celui sur fichiers sont bien séparés.
      Le test S_ISREG sur stat(2) n'est effectué que dans le premier cas; aussi diff bloque comme sous linux dans le deuxième.

      Ce qui m'a fait supposer que ce comportement est voulu par la norme. Mais je ne l'ai pas trouvé énoncé de manière claire.

    • [^] # Re: Une petite étape en plus

      Posté par . Évalué à 2 (+2/-0). Dernière modification le 02/04/19 à 11:59.

      Une étape en moins :

      dpkg -S $(which diff)
      diffutils: /usr/bin/diff

      ;op

  • # Si personne ne se lance ...

    Posté par (page perso) . Évalué à 4 (+2/-0).

    Passons à l’étape suivante, récupérer le code source directement du projet en amont et soumettre une proposition de correction ? Cette partie est laissée à l’attention du lecteur.

    Surtout pas malheureux ! Ça risque de casser des zillions de workflows qui comptent sur cette fonctionnalité.
    https://xkcd.com/1172/

    Adhérer à l'April, ça vous tente ?

  • # Fichier spécial caractère ?

    Posté par . Évalué à 2 (+1/-0).

    Merci pour ce très bon article.

    Par contre je n'ai pas compris ce que c'est qu'un fichier spécial caractère, je n'ai jamais croisé ce truc avant. De ce que Internet m'apprend rapidement, j'ai compris que les fichiers spéciaux sont utilisés pour la gestion de périphériques, et qu'il est de bloc ou caractère selon le périphérique.

    Mais si quelqu'un pouvait m'expliquer à quoi ça sert exactement, pourquoi diff n'arrive pas à les comparer et ce que sont les numéros majeurs et mineurs, je lui serait très reconnaissant.

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.