SIGSEGV
. Pesquisar o problema me permitiu fazer uma excelente comparação entre musl libc
e glibc
. Primeiro, vamos dar uma olhada no rastreamento de pilha:
==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)
O código-fonte nesta linha diz o seguinte:
if (!isalnum(last) || ((p->flags & FORMAT_UNDERLINE) && !isalnum(next))) {
Dica: Este
p
é um ponteiro válido e não nulo. Variáveis last
e next
são do tipo uint32_t
. Segfault acontece na segunda chamada de função isalnum
. E, o mais importante, reproduzível apenas ao usar glibc, não musl libc. Se você tiver que reler o código várias vezes, não está sozinho: simplesmente não há nada para acionar um segfault.
Como se sabia que tudo girava em torno da biblioteca glibc, peguei seus fontes e comecei a procurar por uma implementação
isalnum
, me preparando para encontrar alguma porcaria estúpida. Mas antes de ir para a merda estúpida, que é, acredite, em massa , primeiro vamos dar uma olhada rápida em uma boa opção. É assim que a função é isalnum
implementada em 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;
}
Como esperado, para qualquer valor a
c
função funcionará sem segfault, porque por que diabos isalnum
um segfault deveria ser lançado?
Ok, agora vamos comparar isso com a implementação glibc . Assim que você abrir o título, será saudado com bobagens GNU típicas, mas vamos pular e tentar encontrá-lo
isalnum
.
O primeiro resultado é este:
enum
{
_ISupper = _ISbit (0), /* UPPERCASE. */
_ISlower = _ISbit (1), /* lowercase. */
// ...
_ISalnum = _ISbit (11) /* Alphanumeric. */
};
Parece um detalhe de implementação, vamos prosseguir.
__exctype (isalnum);
Mas o que é
__exctype
? Voltamos algumas linhas para cima ...
#define __exctype(name) extern int name (int) __THROW
Ok, aparentemente este é apenas um protótipo. Não está claro, entretanto, por que uma macro é necessária aqui. Olhando mais longe ...
#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...
Então, isso já parece algo útil. O que é isso
__isctype_f
? Agitando ...
#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
Bem, começa ... Ok, vamos descobrir de alguma forma. Aparentemente,
__isctype_f
esta é uma função embutida ... pare, está tudo dentro do bloco else da instrução do pré-processador #ifndef __cplusplus. Fim da linha. Onde isalnum
, sua mãe, está realmente definida? Olhando mais longe ... Talvez seja isso?
#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...
# elif defined __isctype
# define isalnum(c) __isctype((c), _ISalnum) // <-
Ei, este é o "detalhe de implementação" que vimos anteriormente. Lembrar?
enum
{
_ISupper = _ISbit (0), /* UPPERCASE. */
_ISlower = _ISbit (1), /* lowercase. */
// ...
_ISalnum = _ISbit (11) /* Alphanumeric. */
};
Vamos tentar escolher rapidamente esta 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
Que porra é essa? Ok, vamos prosseguir e considerar que esta é apenas uma constante mágica. Outra macro é chamada
__isctype
, que é semelhante à que vimos recentemente __isctype_f
. Vamos dar outra olhada no ramo #ifndef __cplusplus
:
#ifndef __cplusplus
# define __isctype(c, type) \
((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
#elif defined __USE_EXTERN_INLINES
// ...
#endif
Uh ...
Bem, pelo menos encontramos uma desreferência de ponteiro que pode explicar o segfault. O que é isso
__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 legal da sua parte, glibc! Eu adoro lidar com locais. De qualquer forma, o gdb está conectado ao meu aplicativo travado e, com todas as informações que recebi em mente, escrevo esta miséria:
(gdb) print ((unsigned int **(*)(void))__ctype_b_loc)()[next]
Cannot access memory at address 0x11dfa68
Segfault encontrado. Havia uma linha sobre isso no comentário: "ISO C requer funções ctype para trabalhar com valores como ʻunsigned char 'e EOF". Se encontrarmos isso na especificação, vemos:
Em todas as implementações [das funções declaradas em ctype.h], o argumento é int, o valor do qual deve caber em um char unsigned ou igual ao valor da macro EOF.
Agora fica óbvio como resolver o problema. Minha junta. Acontece que não posso alimentar
isalnum
um caractere UCS-32 arbitrário para verificar sua ocorrência nos intervalos 0x30-0x39, 0x41-0x5A e 0x61-0x7A.
Mas aqui vou tomar a liberdade de sugerir: talvez a função
isalnum
não deva lançar um segfault de forma alguma, independentemente do que aconteça? Talvez mesmo se a especificação permitir , isso não significa que deva ser feito dessa forma ? Talvez, bem como uma ideia maluca, o comportamento desta função não deveria conter cinco macros, verificar o uso do compilador C ++, depender da ordem de bytes de sua arquitetura, tabela de consulta, dados de local do stream e desreferenciar dois ponteiros?
Vamos dar outra olhada na versão musl como um lembrete rápido:
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;
}
Essas são as tortas.
Nota do tradutor: Obrigado a MaxGraey por criar um link para o original.