Uma história de duas bibliotecas C padrão

Hoje recebi um relatório de bug de um usuário Debian que alimentou o utilitário scdoc e conseguiu SIGSEGV. Pesquisar o problema me permitiu fazer uma excelente comparação entre musl libce 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 laste nextsã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 é isalnumimplementada 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 cfunção funcionará sem segfault, porque por que diabos isalnumum 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_festa é 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 isalnumum 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 isalnumnã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.



All Articles