Una historia de dos bibliotecas C estándar

Hoy recibí un informe de error de un usuario de Debian que le dio algunas tonterías a la utilidad scdoc y lo obtuvo SIGSEGV. Investigar el problema me permitió hacer una excelente comparación entre musl libcy glibc. Primero, veamos el seguimiento de la pila:



==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)


El código fuente en esta línea dice esto:



if (!isalnum(last) || ((p->flags & FORMAT_UNDERLINE) && !isalnum(next))) {


Sugerencia: este pes un puntero válido y no nulo. Variables lasty nextson de tipo uint32_t. Segfault ocurre en la segunda llamada de función isalnum. Y, lo que es más importante, reproducible solo cuando se usa glibc, no musl libc. Si tiene que volver a leer el código varias veces, no está solo: simplemente no hay nada que desencadene una falla secundaria.



Como se sabía que todo estaba en la biblioteca glibc, obtuve sus fuentes y comencé a buscar una implementación isalnum, preparándome para enfrentar una estupidez. Pero antes de llegar a la estupidez, que es, créanme, a granel , primero echemos un vistazo rápido a una buena opción. Así es como se isalnumimplementa la función en 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 era de esperar, para cualquier valor, la cfunción funcionará sin un segfault, porque ¿por qué demonios isalnumdebería lanzarse un segfault?



Bien, ahora comparemos esto con la implementación glibc . Tan pronto como abra el encabezado, será recibido con tonterías típicas de GNU, pero salteémoslo e intentemos encontrarlo isalnum.



El primer resultado es este:



enum
{
  _ISupper = _ISbit (0),        /* UPPERCASE.  */
  _ISlower = _ISbit (1),        /* lowercase.  */
  // ...
  _ISalnum = _ISbit (11)        /* Alphanumeric.  */
};


Parece un detalle de implementación, sigamos adelante.



__exctype (isalnum);


Pero ¿qué es __exctype? Retrocedamos algunas líneas ...



#define __exctype(name) extern int name (int) __THROW


Bien, aparentemente esto es solo un prototipo. Sin embargo, no está claro por qué se necesita una macro aquí. Mirando más allá ...



#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...


Entonces, esto ya parece algo útil. ¿Qué es __isctype_f? Sacudiendo ...



#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


Bueno, comienza ... Está bien, juntos lo resolveremos de alguna manera. Aparentemente, __isctype_festa es una función en línea ... stop, todo está dentro del bloque else de la instrucción del preprocesador #ifndef __cplusplus. Callejón sin salida. ¿Dónde se define realmenteisalnum su madre ? Mirando más allá ... ¿Quizás esto es todo?



#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...
# elif defined __isctype
# define isalnum(c)     __isctype((c), _ISalnum) // <-  


Oye, este es el "detalle de implementación" que vimos anteriormente. ¿Recuerda?



enum
{
  _ISupper = _ISbit (0),        /* UPPERCASE.  */
  _ISlower = _ISbit (1),        /* lowercase.  */
  // ...
  _ISalnum = _ISbit (11)        /* Alphanumeric.  */
};


Intentemos elegir rápidamente 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


¿Qué diablos es esto? Bien, sigamos adelante y consideremos que esto es solo una constante mágica. Se llama otra macro __isctype, que es similar a la que vimos recientemente __isctype_f. Echemos otro vistazo a la rama #ifndef __cplusplus:



#ifndef __cplusplus
# define __isctype(c, type) \
  ((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
#elif defined __USE_EXTERN_INLINES
// ...
#endif


Uh ...



Bueno, al menos encontramos una desreferencia de puntero que podría explicar el error de segmentación. ¿Qué es __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__));


¡Qué genial de tu parte, glibc! Me encanta tratar con lugares. De todos modos, gdb está conectado a mi aplicación bloqueada, y con toda la información que recibí en mente, escribo esta miseria:



(gdb) print ((unsigned int **(*)(void))__ctype_b_loc)()[next]
Cannot access memory at address 0x11dfa68


Segfault encontrado. Había una línea sobre esto en el comentario: "ISO C requiere que las funciones ctype funcionen con valores como 'carácter no firmado' y EOF". Si encontramos esto en la especificación, vemos:



En todas las implementaciones [de las funciones declaradas en ctype.h], el argumento es un int, cuyo valor debe caber en un carácter sin firmar, o igual al valor de la macro EOF.



Ahora resulta obvio cómo solucionar el problema. Mi porro. Resulta que no puedo alimentar isalnumun carácter UCS-32 arbitrario para comprobar su aparición en los rangos 0x30-0x39, 0x41-0x5A y 0x61-0x7A.



Pero aquí me tomaré la libertad de sugerir: ¿tal vez la función isalnumno debería lanzar un segfault en absoluto, independientemente de lo que obtenga? ¿Quizás incluso si la especificación lo permite , no significa que deba hacerse de esta manera ? Tal vez, como una idea loca, el comportamiento de esta función no debería contener cinco macros, verificar el uso del compilador C ++, depender del orden de bytes de su arquitectura, tabla de búsqueda, datos de configuración regional de flujo y desreferenciar dos punteros.



Echemos otro vistazo a la versión musl como recordatorio 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;
}


Estos son los pasteles.



Nota del traductor: Gracias a MaxGraey por vincular al original.



All Articles