wget - segfault: Résumé

Salut à tous, winw m’a montré récemment un truc assez sympa. Dans un terminal, tapez la commande

$ wget -r %3a
Segmentation fault

Vous obtiendrez un segfault.

C’est d’autant plus sympa que wget est un binaire très largement utilisé. Les bugs comme celui-ci se font rares ! On s’est alors demandé ce qu’on pouvait bien en faire, et si on pouvait le corriger. Nous ne nous sommes donc pas arrêtés là, et on a cherché la cause du problème.

Environnement de debug

Pour cela, nous nous sommes armés de ce bon vieux gdb, ainsi que des sources de la dernière version de wget en date (1.16.3) disponible ici :

http://ftp.gnu.org/gnu/wget/wget-1.16.3.tar.gz

Dans un premier temps, nous avons recompilé le binaire afin d’en avoir une version non strippée et donc avoir accès aux symboles. Dans le dossiers des sources de wget :

$ ./configure --user-prefix=/home/hackndo/wget
$ make && sudo make install

Reproduction du bug

Ensuite nous avons provoqué le segfault dans gdb puis affiché la backtrace pour trouver où se situe le problème

gdb$ r -r %3a
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Program received signal SIGSEGV, Segmentation fault.
-----------------------------------------------------------------------------------------------------------------------[regs]
RAX: 0x0000000000000006  RBX: 0x0000000000000000  RBP: 0x000000000065FFE0  RSP: 0x00007FFFFFFFDF10  o d I t s z a p c
RDI: 0x00000000FFFFFFFF  RSI: 0x00007FFFF7FF7000  RDX: 0x00007FFFF799CDF0  RCX: 0x00007FFFF76E59D0  RIP: 0x0000000000421ADB
R8 : 0x00007FFFF7FF7001  R9 : 0x00007FFFF7FE9700  R10: 0x0000000000000000  R11: 0x0000000000000246  R12: 0x000000000065FFB0
R13: 0x000000000065F950  R14: 0x00007FFFFFFFE05A  R15: 0x00007FFFFFFFE060
CS: 0033  DS: 0000  ES: 0000  FS: 0000  GS: 0000  SS: 002B
-----------------------------------------------------------------------------------------------------------------------

Recherche de la cause

=> 0x421adb <getproxy+27>:  mov    esi,DWORD PTR [rbx+0x18]
   0x421ade <getproxy+30>:  mov    edi,0x44af12
   0x421ae3 <getproxy+35>:  xor    eax,eax
   0x421ae5 <getproxy+37>:  call   0x402f10 <printf@plt>
   0x421aea <getproxy+42>:  mov    rdi,QWORD PTR [rip+0x23b2bf]        # 0x65cdb0 <opt+304>
   0x421af1 <getproxy+49>:  mov    rsi,QWORD PTR [rbx+0x10]
   0x421af5 <getproxy+53>:  test   rdi,rdi
   0x421af8 <getproxy+56>:  je     0x421b03 <getproxy+67>
-----------------------------------------------------------------------------------------------------------------------------
0x0000000000421adb in getproxy ()
gdb$ bt
#0  0x0000000000421adb in getproxy ()
#1  0x00000000004226fa in retrieve_url ()
#2  0x00000000004204a0 in retrieve_tree ()
#3  0x0000000000404168 in main ()

Le segfault se produit dans la fonction getproxy se trouvant dans retr.c

getproxy (struct url *u)

Après quelques petites recherches, on remarque que le pointeur u sur une structure url est un pointeur null, et du coup à la ligne

if (no_proxy_match (u->host, (const char **)opt.no_proxy))

la tentative d’accès au champ host de la structure provoque le segfault.

Très bien, nous avons isolé la cause du segfault. Cependant, comment se fait-il que le pointeur u passé à getproxy soit nul ? Nous remontons alors un peu la backtrace.

Dans retrieve_url, toujours dans le même fichier

uerr_t retrieve_url (struct url * orig_parsed, const char *origurl, char **file,
char **newloc, const char *refurl, int *dt, bool recursive,
struct iri *iri, bool register_status)

On voit l’appel à getproxy

proxy = getproxy (u);

Et on voit plus haut que u est défini comme ceci :

struct url *u = orig_parsed

En mettant un breakpoint à l’entrée de la fonction retrieve_url, on se rend compte que le paramètre orig_parsed est déjà un pointeur nul. On continue et on remonte la backtrace d’un cran, en allant voir la fonction retrieve_tree située dans le fichier recur.c

uerr_t retrieve_tree (struct url *start_url_parsed, struct iri *pi)

On voit l’appel à la fonction retrieve_url ici

status = retrieve_url (url_parsed, url, &file, &redirected, referer,
&dt, false, i, true);

Nous avons dit que le paramètre url_parsed était nul. Ce pointeur est défini une ligne au dessus :

struct url *url_parsed = url_parse (url, &url_err, i, true);

Cette fois-ci, aucun des paramètres passés à url_parse ne sont nuls. Cette fonction renvoie donc un pointeur nul. En mettant un breakpoint juste après l’appel à cette fonction, on peut voir ce qu’il y a dans url_err : Le numéro 8.

Le code d’erreur 8 est défini dans le fichier url.c (dans lequel il y a la fonction url_parse)

#define PE_INVALID_IPV6_ADDRESS         8

Effectivement, dans la fonction url_parse, nous avons la vérification suivante :

/* Check if the IPv6 address is valid. */
if (!is_valid_ipv6_address(host_b, host_e))
{
    error_code = PE_INVALID_IPV6_ADDRESS;
    goto error;
}

/* Continue parsing after the closing ']'. */

Je vous rappelle que l’argument que nous avons passé à wget était -r %3a or %3a est le code ASCII de : . En amont, wget a détecté notre : et a donc considéré que c’était une adresse IPv6. Celle-ci étant invalide, is_valid_ipv6_address() renvoie false, et nous avons le code d’erreur. Tout est bien et se passe comme prévu par les développeurs à ce moment là.

L’erreur, c’est dans le fichier recur.c avec ces lignes :

struct url *url_parsed = url_parse (url, &url_err, i, true);
status = retrieve_url (url_parsed, url, &file, &redirected, referer,
&dt, false, i, true);

Il n’y a aucune vérification de faite sur le retour de la fonction url_parse, et le pointeur url_parsed est utilisé sans vérifier s’il est nul, ou non.

Nous avons donc, logiquement, un segfault. De notre point de vue, cet oubli ne permet aucune exploitation, mais c’était une analyse intéressante. Un fix est de vérifier que la fonction url_parse a renvoyé un pointeur non nul, de la manière suivante :

struct url *url_parsed = url_parse (url, &url_err, i, true);

if (!url_parsed)
{
    char *error = url_error (url, url_err);
    logprintf (LOG_NOTQUIET, "%s: %s.\n",url, error);
    xfree (error);
    inform_exit_status (URLERROR);
}
else
{
    status = retrieve_url (url_parsed, url, &file, &redirected, referer,
    &dt, false, i, true);
    // [...]

Nous avons d’ailleurs proposé un fix à GNU. Nous verrons s’il sera accepté !

Ce problème n’existe pas si le paramètre -r est omis, puisque cet oubli de vérification se situe seulement dans le fichier recur.c, et nulle part ailleurs.

Correction du bug

Nous avons envoyé un fix qui a été accepté et est mergé dans la branche master ! Voilà, une petite contribution au monde libre, ça fait plaisir 🙂