Artificial truth

The more you see, the less you believe.

[archives] [latest] | [homepage] | [atom/rss/twitter]

Security features of musl
Wed 04 November 2020 — download

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.