ariadne.space/content/blog/there-is-no-such-thing-as-a...

5.9 KiB

title date
there is no such thing as a "glibc based alpine image" 2021-08-26

For whatever reason, the alpine-glibc project is apparently being used in production.  Worse yet, some are led to believe that Alpine officially supports or at least approves of its usage.  For the reasons I am about to outline, we don't.  I have also proposed an update to Alpine which will block the installation of the glibc packages produced by the alpine-glibc project, and have referred acceptance of that update to the TSC to determine if we actually want to put our foot down or not.  I have additionally suggested that the TSC may wish to have the Alpine Council reach out to the alpine-glibc project to find a solution which appropriately communicates that the project is not supported in any way by Alpine.  It should be hopefully clear that there is no such thing as a "glibc based alpine image" because Alpine does not use glibc, it uses musl.

Update: the TSC has decided that it is better to approach this problem as a documentation issue.  We will therefore try to identify common scenarios, including using the glibc package, that cause stability issues to Alpine and document them as scenarios that should ideally be avoided.

What the alpine-glibc project actually does

The alpine-glibc project attempts to package the GNU C library (glibc) in such a way that it can be used on Alpine transparently.  However, it is conceptually flawed, because it uses system libraries where available, which have been compiled against the musl C library.  Combining code built for musl with code built for glibc is like trying to run Windows programs on OS/2: both understand .EXE files to some extent, but they are otherwise very different.

But why are they different?  They are both libraries designed to run ELF binaries, after all.  The answer is due to differences in the application binary interface, also known as an ABI.  Specifically, glibc supports and heavily uses a backwards compatibility technique called symbol versioning, and musl does not support it at all.

How symbol versioning works

Binary programs, such as those compiled against musl or glibc, have something called a symbol table.  The symbol table contains a list of symbols needed from the system libraries, for example the C library functions like printf are known as symbols.  When a binary program is run, it is not executed directly by the kernel: instead, a special program known as an ELF interpreter is loaded, which sets up the mapping from symbols in the symbol table to the actual locations where those symbols exist.  That mapping is known as the global object table.

On a system with symbol versioning, additional data in the symbol table designates what version of a symbol is actually wanted.  For example, when you request printf on a glibc system, you might actually wind up requesting printf@GLIBC_2_34 or some other kind of versioned symbol.  This allows newer programs to prefer the newer printf function, while older programs can reference an older version of the implementation.  That allows for low-cost backwards compatibility: all you have to do is keep around the old versions of the routines until you decide to drop support in the ABI for them.

Why mixing these two worlds is bad

However, if you combine a world expecting symbol versioning and one which does not, you wind up with undefined behavior.  For very simple programs, it appears to work, but for more complicated programs, you will wind up with strange behavior and possible crashes, as the global object table references routines with different behavior than expected by the program.  For example, a program expecting a C99 compliant printf routine will get one on musl if it asks for printf.  But a program expecting a C99 compliant printf routine on glibc will ask for printf@GLIBC_2_12 or similar.

The symbol versioning problem spreads to the system libraries too: on Alpine, libraries don't provide versioned symbols: instead, you get the latest version of each symbol.  But if a glibc program is expecting foo to be an older routine without the semantics of the current implementation of foo, then it will either crash or do something weird.

This has security impacts: the lack of consistency for whether versioned symbols are actually supported by the system basically turns any interaction with versioned symbols into what is called a weird machine.  This means that an attacker possibly controls more attack surface than they would in a situation where the system ran either pure glibc or pure musl.

Alternatives to alpine-glibc

As alpine-glibc is primarily discussed in the context of containers, we will keep this conversation largely focused on that.  There are a few options if you want a small container to run a binary blob linked against glibc in a container that are far better than using alpine-glibc.  For example, you can use Google's distroless tools, which will build a Debian-based container with only the application and its runtime dependencies, which allows for a fairly small container.  You can also try to use the gcompat package, which emulates the GNU C library ABI in the same way that WINE emulates the Windows ABI.

But whatever you do, you shouldn't use the alpine-glibc project to do this.  You will wind up with something completely broken.