Now that I'm running most of my services on Alpine Linux, I was curious about the state security-wise of its libc, musl, which used to be dire to say the least last time I looked at it.
Zeroing memory
Albeit bzero
is deprecated (marked as LEGACY in POSIX.1-2001, removed in POSIX.1-2008),
musl provides bzero
,
as well as the nonstandard explicit_bzero
(also known as memset_explicit()
or memset_s()
in other libc)
to ensure that secret don't stay laying around in memory.
Support of _FORTIFY_SOURCE
_FORTIFY_SOURCE is supported via a libc-agnostic set of headers.
Invalid state handling
musl uses assert
, which is a wrapper around __assert_fail
to validate
assumptions and catch invalid states/violations, resulting in an immediate halt
of the program, instead of doing some dangerous magic to parse the callstack
and the environment.
CSPRNG
Currently, musl doesn't provide a CSPRNG, but it's on the todo-list.
Support for %n
Nowadays, the only reasonable usage of the %n
specifier in printf
and its
friends is to mount format-string attacks.
But since musl aims at being as compliant as possible, this specifier is unfortunately implemented and
available.
It could be interesting to put it being a define, a bit like Visual Studio is
doing.
RELRO
RELRO is supported since musl 1.1.0, released in April 2014.
atexit hardening
It's old
news that atexit
is a
cool vector to transform and arbitrary r/w into arbitrary code exec. glibc is
using
mangling,
like
microsoft,
OpenBSD marks them as
RO. Unfortunately,
musl doesn't do any defensive in this area.
Stack cookies
Building musl with stack-based overflow hardening (think "cookies") is possible since musl 1.1.9, released in May 2015.
Setjmp/longjmp hardening
setjmp
/longjmp
can be used to transform a limited write + jmp into an
arbitrary write to registers and more. The glibc does some mangling, like the OpenBSD libc,
while Microsoft does sanity check (validating that the stack pointer is pointing to the stack, that the instruction pointers points within the current module, all well as validated setjmp
callsites, …). musl doesn't do any hardening there.
Compatibility with *SAN
Currently, musl isn't compatible with ASAN/MSAN/TSAN/UBSAN/… which isn't really super-duper important since I'm not fuzzing with musl anyway.
Development practises
musl's development is pretty old school: a mailing list for bug tracker, a cgit repository, no continuous builds nor integrations with static analysers like coverity or LGTM, no fuzzing, … meh.
Memory allocator
This is the juicy part of the blogpost: userland heap management. musl
used to have a terrible (security-wise) allocator, replaced with malloc-ng
as part of musl 1.2 released in August
2020.
- jemalloc, ptmalloc, nedmalloc, … are all using per thread memory pools, while musl uses a global lock, likely to guarantee global consistency and avoid race-conditions that could result in undetected memory corruption exploitations. On the downside, this might result in contention on multi-threaded programs.
- Freed and allocated memory isn't zeroed, so data leak from previously
allocated memory is possible. Zeroing on free and checking if the zone is
still zeroed upon allocation would detect
write-after-free
at this time, but this would likely be too expensive. Zeroing the first 8-bytes would be a good compromise, and doing so on allocation is not that expensive, since it's priming up caches. Android is doing it. - There are no guard pages everywhere à la Electric Fence, but only a fixed-size one below the out-of-band metadata. But since the allocator is often returning freed memory to the system, this tends to leave unmmaped chunks scattered around.
- There is a single global secret, generated once via ASLR, or using
AT_RANDOM
if available, used to check that chunks aren't forged. Unfortunately, it's not recomputed for every chunk but simply stored in them, meaning that if leaked once, it's game over. - There are pointers to the OOB metadata inside of the chunk, allowing an attacker with limited reads to rapidly gain access to the secret.
- It's not possible to free unaligned chunks, and if the attacker doesn't have read capabilities, it's also impossible to free non-allocated chunks, making it possible to detect double-free and invalid-free deterministically.
- When freed, chunks are put in a FIFO, making it a bit harder to trigger an
use-after-free, but since no randomization is applied there, it's not that
hard. Also, it could be interesting to quarantine the memory by marking it as
PROT_NONE
to catch low-hanging stuff. - Zero-size allocations aren't treated as a special case. They used to be
pretty wild due to their underspecified interaction with
realloc
, but are still scary nowadays. malloc-ng's design prevents using pointing at a non-rw area instead, and decision was made that the added complexity of wiring something to handle this special case wasn't worth it. - Single-byte overflows are detected via a NULL-canary at the end of the allocation at re-allocation/free time, to prevent string-related problems, but it could be interesting to make it a bit larger, and add some randomization to it.
Conclusion
Generally better than glibc/µClibc/dietlibc/µClibc-ng/… allocator-wise, which is the biggest attack-surface, but worse than OpenBSD's one. More than decent in my threat model.