SIGSEGV
. Investigar el problema me permitió hacer una excelente comparación entre musl libc
y 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
p
es un puntero válido y no nulo. Variables last
y next
son 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 isalnum
implementa 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
c
función funcionará sin un segfault, porque ¿por qué demonios isalnum
deberí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_f
esta 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
isalnum
un 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
isalnum
no 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.