logo

drewdevault.com

[mirror] blog and personal website of Drew DeVault
commit: 66b44a4fee0a8df0afbf5c18d738997ecd780c89
parent 6964eed9a58e178d778046b6f274bee7b556499c
Author: Drew DeVault <sir@cmpwn.com>
Date:   Fri, 25 Sep 2020 18:57:18 -0400

fuck you glibc

Diffstat:

Massets/main.scss10+++++++++-
Acontent/blog/2020-09-25-A-story-of-two-libcs.md243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 252 insertions(+), 1 deletion(-)

diff --git a/assets/main.scss b/assets/main.scss @@ -136,7 +136,7 @@ pre { font-style: italic; } - .c1 { + .cm, .c1 { color: #333; font-style: italic; } @@ -228,3 +228,11 @@ details[open] { .text-center { text-align: center; } + +blockquote { + border-left: 5px solid #bbb; + background-color: #eee; + padding: 0 1rem; + margin-left: calc(-1rem - 5px); + margin-right: -1rem; +} diff --git a/content/blog/2020-09-25-A-story-of-two-libcs.md b/content/blog/2020-09-25-A-story-of-two-libcs.md @@ -0,0 +1,243 @@ +--- +title: A story of two libc's +date: 2020-09-25 +--- + +I received a bug report from Debian today, who had fed some garbage into +[scdoc](https://git.sr.ht/~sircmpwn/scdoc), and it gave them a SIGSEV back. +Diving into this problem gave me a good opportunity to draw a comparison between +musl libc and glibc. Let's start with the stack trace: + +``` +==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) +``` + +And if we pull up that line of code, we find... + +```c +if (!isalnum(last) || ((p->flags & FORMAT_UNDERLINE) && !isalnum(next))) { +``` + +Hint: p is a valid pointer. "last" and "next" are both uint32_t. The segfault +happens in the second call to isalnum. And, the key: it can only be reproduced +on glibc, not on musl libc. If you did a double-take, you're not alone. There's +nothing here which could have caused a segfault. + +Since it was narrowed down to glibc, I pulled up the source code and went +digging for the isalnum implementation, expecting some stupid bullshit. But +before I get into their stupid bullshit, of which I can assure you there is *a +lot*, let's briefly review the happy version. This is what the musl libc +`isalnum` implementation looks like: + +```c +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; +} +``` + +As expected, for any value of `c`, isalnum will never segfault. Because why the +fuck would isalnum segfault? Okay, now, let's compare this to the +[glibc implementation][ctype]. When opening this header, you're greeted with the +typical GNU bullshit, but let's trudge through and grep for isalnum. + +[ctype]: https://sourceware.org/git/?p=glibc.git;a=blob;f=ctype/ctype.h;h=351495aa4feaf23993fe65afc0760615268d044e;hb=HEAD + +The first result is this: + +```c +enum +{ + _ISupper = _ISbit (0), /* UPPERCASE. */ + _ISlower = _ISbit (1), /* lowercase. */ + // ... + _ISalnum = _ISbit (11) /* Alphanumeric. */ +}; +``` + +This looks like an implementation detail, let's move on. + +```c +__exctype (isalnum); +``` + +But what's `__exctype`? Back up the file a few lines... + +```c +#define __exctype(name) extern int name (int) __THROW +``` + +Okay, apparently that's just the prototype. Not sure why they felt the need to +write a macro for that. Next search result... + +```c +#if !defined __NO_CTYPE +# ifdef __isctype_f +__isctype_f (alnum) +// ... +``` + + +Okay, this looks useful. What is `__isctype_f`? Back up the file now... + +```c +#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 +``` + +Oh.... oh dear. It's okay, we'll work through this together. Let's see, +`__isctype_f` is some kind of inline function... wait, this is the else branch +of `#ifndef __cplusplus`. Dead end. Where the fuck is isalnum *actually* +defined? Grep again... okay... here we are? + +```c +#if !defined __NO_CTYPE +# ifdef __isctype_f +__isctype_f (alnum) +// ... +# elif defined __isctype +# define isalnum(c) __isctype((c), _ISalnum) +``` + +Hey, there's that implementation detail from earlier! Remember this? + +```c +enum +{ + _ISupper = _ISbit (0), /* UPPERCASE. */ + _ISlower = _ISbit (1), /* lowercase. */ + // ... + _ISalnum = _ISbit (11) /* Alphanumeric. */ +}; +``` + +Let's suss that out macro real quick: + +```c +# 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 +``` + +Oh, for fuck's sake. Whatever, let's move on and just assume this is a magic +number. The other macro is `__isctype`, which is similar to the `__isctype_f` we +were just looking at a moment ago. Let's go look at that `ifndef __cplusplus` +branch again: + +```c +#ifndef __cplusplus +# define __isctype(c, type) \ + ((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type) +#elif defined __USE_EXTERN_INLINES +// ... +#endif +``` + +... + +Well, at least we have a pointer dereference now, that could explain the +segfault. What's `__ctype_b_loc`? + +```c +/* These are defined in ctype-info.c. + The declarations here must match those in localeinfo.h. + + In the thread-specific locale model (see `uselocale' in <locale.h>) + we cannot use global variables for these as was done in the past. + Instead, the following accessor functions return the address of + each variable, which is local to the current thread if multithreaded. + + These point into arrays of 384, so they can be indexed by any `unsigned + char' value [0,255]; by EOF (-1); or by any `signed char' value + [-128,-1). ISO C requires that the ctype functions work for `unsigned + char' values and for EOF; we also support negative `signed char' values + for broken old programs. The case conversion arrays are of `int's + rather than `unsigned char's because tolower (EOF) must be EOF, which + doesn't fit into an `unsigned char'. But today more important is that + the arrays are also used for multi-byte character sets. */ +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__)); +``` + +That is just so, super cool of you, glibc. I just *love* dealing with locales. +Anyway, my segfaulted process is sitting in gdb, and equipped with all of this +information I wrote the following monstrosity: + +``` +(gdb) print ((unsigned int **(*)(void))__ctype_b_loc)()[next] +Cannot access memory at address 0x11dfa68 +``` + +Segfault found. Reading that comment again, we see "ISO C requires that the +ctype functions work for 'unsigned char' values and for EOF". If we +cross-reference that with the specification: + +> In all cases [of functions defined by ctype.h,] the argument is an int, the +> value of which shall be representable as an unsigned char or shall equal the +> value of the macro EOF. + +So the fix is obvious at this point. Okay, fine, my bad. My code is wrong. I +apparently cannot just hand a UCS-32 codepoint to isalnum and expect it to tell +me if it's between 0x30-0x39, 0x41-0x5A, or 0x61-0x7A. + +But, I'm going to go out on a limb here: maybe isalnum should never cause a +program to segfault no matter what input you give it. Maybe because the spec +says you *can* does not mean you *should*. Maybe, just maybe, the behavior of +this function should not depend on five macros, whether or not you're using a +C++ compiler, the endianness of your machine, a look-up table, thread-local +storage, and two pointer dereferences. + +Here's the musl version as a quick reminder: + +```c +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; +} +``` + +Bye!