Picking Glibc Versions at Runtime
blogsystem5.substack.com
blogsystem5.substack.com
- pldd (https://man7.org/linux/man-pages/man1/pldd.1.html) shows the actual dynamic libs linked into a running process. (Contrast this with ldd, which shows what the dynamic libs would be based on the current shell environment).
- libtree (https://github.com/haampie/libtree) which shows dependencies similarly to ldd, but in tree format.
In particular, if you need to replace not just glibc, but also a bunch of system libraries (pretty common case for complex apps), it's often easier to unshare(CLONE_NEWNS), followed by bind-mounting over new /lib64 and /usr/lib to override specific directories. This is much lighter than full-on containers, and allows overriding any specific directories - for example if your app looks at /usr/share/appname, you can override it too.
This method has a bunch of upsides: you can use binaries unmodified, subprocesses work, and hard-coded data locations can be taken care of as well.
I feel like for this, LD_LIBRARY_PATH is usually sufficient. Just seems like glibc is the special case.
You can also create a temp directory with symlinks as a poor mans overlay fs. You bind mount the original dir in an out of the way location so you can link to it and bind the temp dir over the standard location.
I believe thats what Nix's bubblewrap based FHSenv was doing last I checked.
This is basically what Flatpak does.
For non-gui the closest thing is Docker/Podman.
> What’s the moral of the story? Containers are, usually, not the best solution to a systems problem.
Unfortunately I think they are a good solution to "I don't really know how it works but if I use containers I don't need to learn".
Using containers is often more of a "quick hack" than a good solution, which is consistent with the feeling that the software industry is more and more becoming a big hack.
Was thinking that version skew might be another use case that's perhaps a little less pejorative. I can't tell if the OP method is sensitive to upgrades but this is another area where appropriately setup containers can insulate the functionality from the rest of the system.
How do you mean that? Can you provide an example situation where an upgrade could potentially be problematic?
That is a wild conclusion to make considering previous paragraph. It's only cheaper and simpler if you value your time at 0.
Or read this blog post once, learning three options to run a binary with non-default glibc:
# Set dynamic loader version at link time
cc -o hello_c -Wl,--dynamic-linker=/tmp/sysroot/lib/ld-linux-x86-64.so.2 hello.c
# Set dynamic loader version at run time
/tmp/sysroot/lib/ld-linux-x86-64.so.2 ./hello_c
# Edit dynamic loader version in binary
patchelf --set-interpreter /tmp/sysroot/lib/ld-linux-x86-64.so.2 ./hello_cThere's already like 100 tools I need to know for my job, I don't want low-level OS and C stuff to add another 50 or even another 20.
This is a little bit "Whatever the world was like when I was 20 is perfect, everything before that is too old, everything after that is too new", but, I'm definitely just reaching for Docker for this. Unless I'm running a GUI application or something else that's hard to containerize.
Where do you think all those libraries come from? And for anything more complex than "hello world", how did we know which libraries to include?
Once you solve those two, your thing will be more lines of code than most Dockerfiles.
Your "Whatever the world was like when I was 20..." quote kinda boils down to "I am fine with the tools I already know how to use and, and I don't want to learn something new".
And then you say... you already have your 100 tools and don't want to learn any others.
Same deal, really.
If you are designing the program yourself, you can use this trick to make your distribution more portable. You'll have to configure linker to change RPATH as well, and modify your packaging scripts to also grab any .so files associated with it, like libm, libpthread, librt and so on. You'll also want to make sure no library uses hardcoded data/configs with incompatible settings (like /etc/nsswitch.conf).
No public Linux will ever accept this, but it would be a reasonable way to distribute your internal app to your non-homogeneous linux fleet.
For complex third-party apps, it's going to be harder - you'll want some sort of dependency collector script that follows both load-time and run-time loading; you'll also want to do something about data and config files. If there are hardcoded paths (like Python or gcc), you will have to do something about them too.
That will be basically a custom, non-trivial effort for each program. Definitely possible, but also much more complex than sticking some "apt install" in Dockerfile.
If you are for example distributing "native" (I hate that word) programs, this is a way to only need one version for all (well, almost ;) Linux distributions.
If I'm debugging something, the last thing I want to do is spin up a container, make sure I've mounted the paths I need inside it, and/or ferry things in and out of it.
I'd much rather just run the binary under a different interpreter.
Granted, this is only useful if I'm already building my own glibc for some reason. If I'm debugging a problem where someone tells me my app is broken on some other version of some other distro, and I've narrowed the problem down to glibc, it is probably easier just to spin up a container image of that version of that other distro, rather than building my own glibc, using the same version as in that other distro.
Go has demonstrated that "mostly-static" linking is a viable alternative to the orthodox choices of "fully static" or "fully dynamic"; for example, they link to libsystem on macOS, libc on OpenBSD, or sometimes to the "real" libc on other unices (to be able to use getaddrinfo, which might do more than just look at /etc/resolv.conf).
That approach wasn't without issues, including security holes - there were instances of bugs where the only solution was: "rebuild every Go executable with a newer toolchain". But I think this approach is under-explored: distributing "fat" binaries that bundle musl libc, libpng42, libfoo666, libquux1337, but e.g. dynamically link to things like Mesa.
However we just don't get rid of static linking and decided to go back into 1980's UNIX, because there are use cases where dynamic linking is actually usefull, and achieving the same via OS IPC is too resource demanding.
There is though a fine balance between offering such kind of features via dynamic code loading and OS IPC, because host stability and security exploits also are a relevant point of interest in modern computing.
Exactly my point with Mesa, it would be a huge mess to try to statically link it into your game, and it gets even funnier with each newly released GPU / driver update.
But when you are in charge of a remote execution service, CI pipelines, and production builds… well, this is not about a single developer machine anymore and containers may not be the right solution when you know about the details of how the software is put together.
(Yes, author here.)
In our company we luckily have the possibility to run containers at all those stages (we are also developing container images for customers) but as developer it's still a good thing to know alternatives. It may save you time because requiring containers decides whether your build can only run on 10 new linux build agents or on all 80 that are currently deployed.
Programs including their dependencies.
The Linux model of global shared libraries is an abject failure. It’s a failed model — and the prevalence of requiring Docker simply to launch a program is evidence of this.
Windows doesn’t have global library folders that are polluted by a million user scripts. And you can reliably launch software that’s 25 years old. It works great. Linux’s model was a nice effort but ultimately a failure.
C:\WINDOWS\SYSTEM32
It's called "DLL Hell" for a reason. It used to be very common for every program you installed to dump several libraries in that directory. It included program-specific libraries (so that directory would have a mix of internal libraries for every program you ever installed; pray there were no naming conflicts!), compiler runtime libraries like the C library (and it was not uncommon for a program to overwrite an already existing runtime DLL with an older version, breaking other programs which expected the newer version), and sometimes even operating system libraries. It got so bad that IIRC Microsoft made Windows automatically detect when an important operating system DLL had been overwritten, and restore it from a secret copy of the original DLL it had stashed elsewhere.
Maybe back on Windows 95? Hasn’t been the case or an issue in as long as I can remember.
> It's called "DLL Hell" for a reason.
Linux shared library hell is at least a full order of magnitude more complex, fragile, and error prone. Launching a Linux program is so outrageously unreliable that everyone uses Docker just to run a bloody program! It’s so so bad. At least in the year two thousand and twenty four.
wat?
Worst case is linear. Every program duplicates everything it needs. Which honestly is just totally fine.
How is this quadratic?
In any case I don’t think library duplication is a meaningful issue in most cases. As evidenced by everyone using Docker which duplicates libraries!
Feels like this could be totally solved at the filesystem level with copy-on-write de-duplication. The best of both worlds!
Docker's way of doing things is basically an admittance of defeat by lazy engineers. We can do better than that.
Setting LD_LIBRARY_PATH pretty much solves any FHS problem once you get past the linker.
LD_LIBRARY_PATH users deserve the death sentence.
Glibc changes a lot. (But is at least very good at running old binaries.)
https://www.baeldung.com/linux/multiple-glibc
https://www.hudsonrivertrading.com/hrtbeat/how-our-engineers...
https://stackoverflow.com/questions/847179/multiple-glibc-li...
> humans praying to scrapers that feed AI
See Alan Kay's writing on Quora, which has a data partnership with OpenAI, https://www.quora.com/profile/Alan-Kay-11?share=1
You're right, this is wrong. You need to set up containers in your developer machines to test against *everything*. You need to take the exact environment that you, the developer, are using to build and test the app, and reproduce that in production. Not just glibc, but the whole gosh darn filesystem, the environment variables, and anything else capturable in the container. (That is, if you care about your app working correctly in production....)
> Consider this: how do the developers of glibc test their changes? glibc has existed for much longer than containers have. And before containers existed, they surely weren’t testing glibc changes by installing modified versions of the library over the system-wide one and YOLOing it.
No, they were just developing against one version of glibc, for each major release of their product.
Back in the day, software developers took incredibly seriously the idea of backwards compatibility. You were fairly sure that if the user could run your app with at least the same version of glibc as you, they could run your app. So the developers would pick one old-ass version of glibc to test with, to ensure as many customers as possible could run their app.
Eventually a new version of the product would require a breaking change in glibc, and the old product would fall out of support eventually, but until then they had to keep around something to test the told version for fixes.
Either they'd develop on an old-ass system, or have a "test machine" with that old-ass version of glibc, or use chroot. You know, the thing that lets you execute binaries in a fake root filesystem, including with a completely different glibc, and everything else? Yeah. We had fake containers before containers. Tar up a filesystem, copy your app into it, run it with a chroot wrapper.
You don't have to wonder when you should use containers or not, i'll make it very simple for you:
Q: Are you developing and testing an application on
your laptop, and then running it ("in production")
on a completely different machine, and is it
important that it works as you expect?
A: Use containers.
(p.s. that system you thought up? i worked for a company 20 years ago that took RPM and Frankenstein'd it to do what you describe. even did immutable versioned config files, data files, etc not just binaries. it was really cool at the time. they use containers now. soooo much less maintenance hassle.)Containers are a great testing tool. When running your CI pipeline, absolutely, run it in a container environment that looks as close as possible to production. That will help shake out those non-portable, environment-leaking things that do end up in your software sometimes.
And for production itself, sure, run things in containers for isolation, ease of bin-packing, sort of as a poor-man's virtual machine. (Some security benefits, but not as many as some people believe.)
The funny thing is that people who advocate container use in order to duplicate the runtime environment always end up reluctant to update the runtime environment itself. Because then you just have the same problem again: you're using containers because you don't want to have to care about portability and environment leakage... so then you end up with something that doesn't work right when you do more than trivial upgrades to your runtime environment.
When I first started doing backend work ~15 years ago, I came from a background writing desktop and embedded software. I thought backend was some mysterious, mystical thing, but of course it turned out not to be. In some ways backend development is easier than desktop development, where your software has to run well in every random environment imaginable. And when a user reports an issue, you sometimes have to work to duplicate their environment as closely as possible in order to reproduce it. But backend apps mostly run in the same environment all the time, and it's an environment you have access to for debugging. (Certainly backend dev comes with its own new challenges; it's not easier on all axes.)
In that situation, where any customer could have any kind of environment, it's much less effort for both the provider and the customer to just duplicate an environment, rather than spend time trying to make portable or compatible environments. And it's more accurate. As you say, a lot of people can't or won't write portable software. Many devs use Macs, and then ship their apps to Linux... more than a few inconsistencies, to say nothing of packages, versions, system files. So if we want to do more work, faster, more reliably, with people who don't write portable code, on incompatible systems, containers are the best possible option.
And it's great for frontend / GUI apps. I use Alpine Linux for a desktop, because I'm a moron. But that means there's many GUI apps that just won't run on my system, for many reasons. Docker and Flatpak allow me to run those GUI apps, like 1Password and FreeCAD, on my wacky non-portable system. It's a boon for me, the user, and for the developers/vendors. (Alternatives like 'AppImage' don't work on musl systems)
Or, if you deploy on Windows, you don't even need containers to validate basic system functionality. The whole glibc mess is one of the biggest selling points of Windows Server to me, and I don't like Windows Server at all.
Containers don't even give you guarantees about your production environment. I've seen more than a few cases where a local deployment failed in production because of a missing instruction set on the production machine. Containers also don't solve your firewall's nftables rules being different from prod's, and they don't solve for eBPF programs running on your system either. If you go down to troubleshooting at the glibc level, containers aren't enough anymore to guarantee the things you want to guarantee, because you share things like a kernel; you can maybe get away with a VM, assuming your dev machine has a superset of CPU features to cover the CPU features your production machines have, unless you limit yourself to a very restrictive subset if you don't know what CPUs your code will run on. And god forbid you need access to special hardware, your only remaining option then is to mess with PCI forwarding or to take a production server and run your tests on that.
Usually, portable software doesn't really need that kind of verification. What works for me in practice is "statically compile for the oldest glibc you want to support and hope for the best" or "develop for the distro you're running on your desktop and don't run anything newer than the oldest distro you still need to support".
Alternatively, you can just switch to a language like C# or Java or Python or PHP or whatever other language deals with the glibc mess for you. It's not always an option, but avoiding native code when you can sure makes testing and deployment a whole lot easier.
I have been playing on native elf/linux for more than 10 years, and ABI issues has been a nightmare (the worst being game devs or game engine devs forgetting to add the -static-libgcc and -static-libstdc++ options, since libgcc and libstdc++ ABIs are just HORRIBLE and not reliable even on the medium run).
I'll explain the "Right Way" down below, but let's warn people now: it requires more work to craft "correct" ELF files for binary-only distribution than for windows. FACT, deal with it, eat the bullet, etc... and 99% of game devs and many game engine devs DO NOT KNOW THAT.
Indeed, glibc is heavy on the usage of GNU ELF symbol "versions" (called version names) for its ABI, and cherry picking of the "right" version name is FULLY MANUAL.
Open source "toolchains" default to building "open source", aka for a symbol, it will select by default the most recent version, which is exactly what you DON'T WANT while building binaries for games!!
While building for a game, you want to select "not too recent" version names for your binaries, that to be able to load on "not too recent" distros. Because, and you can check it, glibc devs are _REALLY_ heavy on the usage of version names, and that for external AND INTERNAL symbols. (I put aside the nasty issue of the brand new ELF relative relocation only supported in latest ELF loaders...).
The cheap way is to have a "target"/not too recent glibc installed somewhere then link your game binaries with it. Easy to say as toolchain reconfiguration can be a nightmare (game engine builders are supposed to deal with that). But this is the most "easy" way to be sure not too recent version names get selected for symbols. And I don't talk about building properly a glibc... (yeah, it is sadistical).
"Not Too Recent" glibc ABI:
https://sourceware.org/glibc/wiki/Glibc%20Timeline
First AAA vulkan native elf/linux games are from 2017/2019, then based on the previous document, the version names of a glibc close to 2.30 should be appropriate for video game distributed ELF files.
Now, there is a way (the "Right Way")to manually select the version names you want, it is documented in the second part of the page there:
https://sourceware.org/binutils/docs/ld/VERSION.html
It means that for all distributed ELF files, the game devs, via some specific build system support, _MUST_ select the version name for earch symbol (or some presets) they use, and this does include the glibc _INTERNAL_ symbols too: for instance the glibc internal "libc_start" symbol is versioned, namely if you link with a glibc >= 2.34, your ELF executables will refuse to load on distros with a glibc < 2.34... whaaaaaaat?!
In practice, game devs after generating the ELF files they will distribute, must audit their version names, for instance with the command "$readelf -a -W some_game_elf_file", at the end of the output you have the "required version names". Then they would have to cherry pick "less recent" version names for the symbols with "too recent" version names.
If I recall properly, in glibc source code you have somewhere some text files with the list of version names per symbols.
Additional note: Ofc, all system interface ELF shared libs must be libdl-ed (dlopen/dlsym/dlclose) with proper fallback code paths. This will work around any version names in those shared libs. Video game core system interface shared libs: vulkan loader->legacy libglvnd loader (or directly libGL)->CPU rendering, libasound (alsa-lib), libxkbcommon(-11) for user key symbols (if needed), x11 xcb-libs (wayland code is static in game binaries), and that's it.
At some point in time, we thought libdl-ing the libc itself... but I was told (have to be checked) that some specific libc runtime setup must be performed at process startup for some critical services, which setup code won't be run upon libdl-ing the libc. In this case, you would not have a main() function but the crude ELF entry point... which is nearly a main() anyway. That would have solved for good the issues of version names since everything would happen through libdl with its 3 symbols (dlopen/dlsym/dlclose) which have extremely old version names then are (at this time) "safe".
To check dependencies of ELF files, like the version names, "$readelf -a -W some_game_binary" and do inspect the NEEDED entries which should have only glibc libs (and private ELF shared libs with location relative to the process current working directory as you should avoid the usage of $ORIGIN like hell, namely ELF executable must manually verify the process current working directory is "correct" for their private stuff).
An alternative would be pure "IPC" based _SIMPLE_ system interfaces: The _real_ wayland core set of interfaces seems ok... but pulseaudio[012] IPC interfaces are just too complex, and obviously do fail hard stability in time (currently nothing beat the stability in time of the alsa-lib imperfect ABI... yeah, you still need free() from the libc). There is no IPC interfaces for vulkan3D (and those would have to be seriously tested for performance and probably tied to wayland, but shared memory command ring buffers with shared memory atomic pointers/counters may do the trick). Neither there are some IPC interfaces for user key symbols, probably because of the ultra-complex data format, xkb, and because the location and configuration of those user files are not rigorously defined. Joypad support is "linux device files", then is naturally "IPC-ed".
Don't forget, games want a small set of as simple as possible binary interfaces, and that very stable-in-time.