SIGSEGV
. La recherche du problème m'a permis de faire une excellente comparaison entre musl libc
et glibc
. Tout d'abord, jetons un œil à la trace de la pile:
==26267==ERROR: AddressSanitizer: SEGV on unknown address 0x7f9925764184
(pc 0x0000004c5d4d bp 0x000000000002 sp 0x7ffe7f8574d0 T0)
==26267==The signal is caused by a READ memory access.
0 0x4c5d4d in parse_text /scdoc/src/main.c:223:61
1 0x4c476c in parse_document /scdoc/src/main.c
2 0x4c3544 in main /scdoc/src/main.c:763:2
3 0x7f99252ab0b2 in __libc_start_main
/build/glibc-YYA7BZ/glibc-2.31/csu/../csu/libc-start.c:308:16
4 0x41b3fd in _start (/scdoc/scdoc+0x41b3fd)
Le code source sur cette ligne dit ceci:
if (!isalnum(last) || ((p->flags & FORMAT_UNDERLINE) && !isalnum(next))) {
Astuce: il
p
s'agit d'un pointeur valide, non nul. Les variables last
et next
sont de type uint32_t
. Segfault se produit lors du deuxième appel de fonction isalnum
. Et, surtout, reproductible uniquement lors de l'utilisation de glibc, et non de musl libc. Si vous devez relire le code plusieurs fois, vous n'êtes pas seul: il n'y a simplement rien pour déclencher un segfault.
Comme on savait que tout était dans la bibliothèque glibc, j'ai récupéré ses sources et j'ai commencé à chercher une implémentation
isalnum
, me préparant à faire face à des conneries stupides. Mais avant d'arriver à la merde stupide, qui est, croyez-moi, en gros , examinons d'abord rapidement une bonne option. Voici comment la fonction est isalnum
implémentée dans musl libc:
int isalnum(int c)
{
return isalpha(c) || isdigit(c);
}
int isalpha(int c)
{
return ((unsigned)c|32)-'a' < 26;
}
int isdigit(int c)
{
return (unsigned)c-'0' < 10;
}
Comme prévu, quelle que soit la valeur, la
c
fonction fonctionnera sans segfault, car pourquoi diable isalnum
un segfault devrait-il être lancé?
Bon, comparons maintenant cela avec l'implémentation de la glibc . Dès que vous ouvrez le titre, vous serez accueilli avec des absurdités GNU typiques, mais sautons-le et essayons de le trouver
isalnum
.
Le premier résultat est le suivant:
enum
{
_ISupper = _ISbit (0), /* UPPERCASE. */
_ISlower = _ISbit (1), /* lowercase. */
// ...
_ISalnum = _ISbit (11) /* Alphanumeric. */
};
Cela ressemble à un détail d'implémentation, passons à autre chose.
__exctype (isalnum);
Mais qu'est-ce que c'est
__exctype
? Revenons en arrière quelques lignes ...
#define __exctype(name) extern int name (int) __THROW
D'accord, apparemment, ce n'est qu'un prototype. On ne sait cependant pas pourquoi une macro est nécessaire ici. En regardant plus loin ...
#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...
Donc, cela ressemble déjà à quelque chose d'utile. Qu'est-ce que c'est
__isctype_f
? Secouer ...
#ifndef __cplusplus
# define __isctype(c, type) \
((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
#elif defined __USE_EXTERN_INLINES
# define __isctype_f(type) \
__extern_inline int \
is##type (int __c) __THROW \
{ \
return (*__ctype_b_loc ())[(int) (__c)] & (unsigned short int) _IS##type; \
}
#endif
Eh bien, ça commence ... Ok, on va le découvrir d'une manière ou d'une autre. Apparemment,
__isctype_f
c'est une fonction en ligne ... arrêtez, tout est dans le bloc else de l'instruction du préprocesseur #ifndef __cplusplus. Impasse. Où isalnum
, sa mère, est-elle réellement définie? En regardant plus loin ... C'est peut-être ça?
#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...
# elif defined __isctype
# define isalnum(c) __isctype((c), _ISalnum) // <-
Hé, c'est le "détail de l'implémentation" que nous avons vu plus tôt. Rappelles toi?
enum
{
_ISupper = _ISbit (0), /* UPPERCASE. */
_ISlower = _ISbit (1), /* lowercase. */
// ...
_ISalnum = _ISbit (11) /* Alphanumeric. */
};
Essayons de choisir rapidement cette macro:
# include <bits/endian.h>
# if __BYTE_ORDER == __BIG_ENDIAN
# define _ISbit(bit) (1 << (bit))
# else /* __BYTE_ORDER == __LITTLE_ENDIAN */
# define _ISbit(bit) ((bit) < 8 ? ((1 << (bit)) << 8) : ((1 << (bit)) >> 8))
# endif
C'est quoi ce bordel? Bon, passons à autre chose et considérons que ce n'est qu'une constante magique. Une autre macro s'appelle
__isctype
, qui est similaire à celle que nous avons vue récemment __isctype_f
. Jetons un autre regard sur la branche #ifndef __cplusplus
:
#ifndef __cplusplus
# define __isctype(c, type) \
((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
#elif defined __USE_EXTERN_INLINES
// ...
#endif
Euh ...
Eh bien, au moins nous avons trouvé un déréférencement de pointeur qui pourrait expliquer le segfault. Qu'est-ce que c'est
__ctype_b_loc
?
/* ctype-info.c.
localeinfo.h.
, , (. `uselocale' <locale.h>)
, .
, -,
, , .
384 ,
`unsigned char' [0,255]; EOF (-1);
`signed char' value [-128,-1). ISO C , ctype
`unsigned char' EOF;
`signed char' .
`int`,
`unsigned char`, `tolower(EOF)' EOF,
`unsigned char`. - ,
. */
extern const unsigned short int **__ctype_b_loc (void)
__THROW __attribute__ ((__const__));
extern const __int32_t **__ctype_tolower_loc (void)
__THROW __attribute__ ((__const__));
extern const __int32_t **__ctype_toupper_loc (void)
__THROW __attribute__ ((__const__));
Que c'est cool de ta part, glibc! J'adore m'occuper des lieux. Quoi qu'il en soit, gdb est connecté à mon application en panne, et avec toutes les informations que j'ai reçues à l'esprit, j'écris cette misère:
(gdb) print ((unsigned int **(*)(void))__ctype_b_loc)()[next]
Cannot access memory at address 0x11dfa68
Segfault trouvé. Il y avait une ligne à ce sujet dans le commentaire: "ISO C nécessite que les fonctions ctype fonctionnent avec des valeurs comme ʻunigned char 'et EOF". Si nous trouvons cela dans la spécification, nous voyons:
Dans toutes les implémentations [des fonctions déclarées dans ctype.h], l'argument est int, dont la valeur doit tenir dans un caractère non signé, ou égale à la valeur de la macro EOF.
Maintenant, il devient évident comment résoudre le problème. Mon joint. Il s'avère que je ne peux pas alimenter
isalnum
un caractère UCS-32 arbitraire pour vérifier son occurrence dans les plages 0x30-0x39, 0x41-0x5A et 0x61-0x7A.
Mais ici, je prendrai la liberté de suggérer: peut-être que la fonction
isalnum
ne devrait pas du tout lancer un segfault, quoi qu'il en soit? Peut-être que même si la spécification le permet , cela ne signifie pas que cela devrait être fait de cette façon ? Peut-être, tout comme une idée folle, que le comportement de cette fonction ne devrait pas contenir cinq macros, vérifier l'utilisation du compilateur C ++, dépendre de l'ordre des octets de votre architecture, table de recherche, flux de données locales et déréférencer deux pointeurs?
Jetons un autre regard sur la version musulmane comme un petit rappel:
int isalnum(int c)
{
return isalpha(c) || isdigit(c);
}
int isalpha(int c)
{
return ((unsigned)c|32)-'a' < 26;
}
int isdigit(int c)
{
return (unsigned)c-'0' < 10;
}
Ce sont les tartes.
Note du traducteur: Merci à MaxGraey pour le lien vers l'original.