[Pre-RFC] target-glibc: Proposal to add native support for GLIBC versions for the -gnu targets

Summary

Add first-class support to rustc and cargo for declaring a binary's glibc deployment target — the oldest glibc release the binary is required to run on. Users opt in via either a target-tuple suffix (--target x86_64-unknown-linux-gnu.2.28), a Cargo config field ([target.x86_64-unknown-linux-gnu] libc = "2.28"), or a tracked codegen flag (-C target-libc=glibc:2.28). At build time, rustc synthesizes versioned glibc stub shared libraries containing only symbols introduced at or before the requested version, and points the linker at them; the resulting ELF's VERNEED entries cannot exceed the deployment target. The binary then loads on any host with that glibc version or newer.

This is exactly the shape of macOS MACOSX_DEPLOYMENT_TARGET: a single declared value with a build-time consequence ("don't use APIs introduced after this release") and a runtime consequence ("requires at least this OS version installed"). Today, accomplishing the same thing for glibc requires out-of-tree tooling such as cargo-zigbuild — typically because someone is building on Ubuntu 24.04 (glibc 2.39) but needs to ship a binary that runs on Ubuntu 18.04 (glibc 2.27) or RHEL 7 (glibc 2.17).

Rendered

3 Likes

Are there plans on making the tool that does the "local glibc -> stub library" standalone? I can forsee this being very useful even outside of Rust. Would a linker script be easier (it may well might not be, but I'm curious if it was considered)?

Also, has an equivalent for libstdc++ and/or libc++ been considered? I know Red Hat has their SCL toolchains and whatever dark arts are used to make those still work on base RHEL is also likely of interest here (at least as a learning opportunity).

Are there plans on making the tool that does the "local glibc -> stub library" standalone? I can forsee this being very useful even outside of Rust.

This is quite a trivial task. A standalone binary may either automatically download the required glibc version sources or use whatever sources the user provides (not from the linked libc.so.6 on your system).

Would a linker script be easier (it may well might not be, but I'm curious if it was considered)?

Linker script is generated/used under the hood anyway. It can be preserved next to the generated stub libraries.

Also, has an equivalent for libstdc++ and/or libc++ been considered?

No. This is way outside of the scope of this RFC. Moreover, I do not know of any success stories involving libstdc++/libc++ from languages other than C++. If it were possible with reasonable effort, Rust could automatically use libraries that expose API as C++ classes with std::* types. In other words, why would you even need something like that?

This is written by AI, right?

Still, it would be a great feature to have.

This is written by me using AI tools based on the MVP implementation I tried. Actually, that was assisted with multiple AI models, agents, and tools.

For a more substantive point: It’s odd that you propose two different ways to specify the glibc deployment target (triple suffix and -C argument), especially when they are both different from how Apple deployment targets are specified (environment variable).

In my opinion, though, the fact that rustc checks an environment variable on Apple targets feels like a hack. Compilers don’t normally accept options via environment variable, and currently Cargo doesn’t even know about the environment variable and won’t trigger rebuilds if it changes. The choice of environment variable was not arbitrary – Clang actually does check that same environment variable, as one of multiple different ways it supports to pass in a deployment target – but I think we should deprecate it in favor of something more explicit.

In other words, I would suggest picking one new mechanism, and adding support for it for both glibc and Apple targets.

A triple suffix would have the benefit of matching a different one of the ways Clang supports to pass in a deployment target. It would also make intuitive sense in that different OS versions are literally different target platforms.

But for Rust it might fit in better for the deployment target to be a “target modifier” of the sort that Linux people are currently adding zillions of. Those are too complicated to be stuffed into the triple, so they will be using separate arguments – probably under a new top-level option, perhaps -T or -M.

2 Likes

Triple suffix is to support the approach zig-build uses today, which should simplify migration. IMHO, the -C target-glibc:X.Y and the config file are the proper approach.

As you mentioned, the MacOS env variable is hacky, and I do not think that following that path is the right way to handle this feature.

In other words, I would suggest picking one new mechanism, and adding support for it for both glibc and Apple targets.

Making that change for Apple is a bit of a stretch for this RFC (and completely irrelevant for the stub libs part).

I support this idea in principle, but there's a major problem with it:

At build time, rustc synthesizes versioned glibc stub shared libraries containing only symbols introduced at or before the requested version, and points the linker at them; the resulting ELF's VERNEED entries cannot exceed the deployment target. The binary then loads on any host with that glibc version or newer.

It is not even remotely that simple. People keep thinking it's that simple because there are a bunch of cases where it'll work by accident, but usually when glibc adds a new version for one or more of its public symbols, it's because, at the assembly-language level, you need to call the new version differently than the old one. The difference is invisible at the C level, because the header files shipped with glibc-2.X tell the C compiler how to call all the @GLIBC_2.X symbols correctly. But rustc would have to know what the difference is, for every glibc symbol that has multiple versions, and that's a big reverse engineering job. Way too big, IMHO, to ask the rust team to take on all by themselves.

If you're serious about this, I'd recommend you pursue the idea through the glibc project. Propose addition of a feature selection macro (-D_GLIBC_DEPLOYMENT_TARGET=2.NN) which, if you set it, you get machine code targeting that version of glibc -- as you say, works exactly the same way as MACOSX_DEPLOYMENT_TARGET. I think they'll be at least a little receptive to the idea, although they will probably expect you to do all the work yourself and/or find a way to get people paid to do the work (e.g. a Sovereign Tech Fund grant).

Once that work is done, the libc crate will be able to expose it via cargo features, without needing to grovel through glibc's entire sources and change history to figure out what the actual difference is between how you call timer_create@GLIBC_2.3.3 and how you call timer_create@GLIBC_2.34, multiply by 2,400 public symbols.


Besides that, a couple smaller notes:

Users opt in via either a target-tuple suffix (--target x86_64-unknown-linux-gnu.2.28)

You shouldn't make up target tuple variations without coordination across the entire ecosystem of programs that use target tuples.

Please don't do this anymore. Please don't use AI in any way to "help" you write anything you want other people to read.

For a whole lot of reasons, but the most relevant one right now is that the overwhelming majority of people in this forum, and indeed the overwhelming majority of people working on open source software, are going to stop reading and forget about your proposal the moment they realize they're reading something that wasn't written by a human. It's simply not a good use of their time.

Another very important reason for this proposal is that I expect your AI-assisted MVP implementation probably hits all the cases where "just call the old symbol" works by accident, and none of the ones where it'll link OK but then when you run the program it'll fail to load, crash, corrupt memory, or worse. And that, in turn, has led you to underestimate how much work it's going to be to implement your proposal correctly.

Of course you might have made the same mistake if you'd set out to do the MVP entirely by hand, but there's a better chance that you would have noticed at least some of the problem cases.

4 Likes

Oh, I was thinking of something that would take my existing libc.so.6 and make the stub directly from it. Building from sources seems complicated for cross-compilations, but I understand this is not a trivial problem that is guaranteed to be amenable to such "shortcuts".

Yeah, I suppose that would be something for one of the C++ binding crates to work with rather than rustc itself.

I'm not finding the proposal easy to read but it would be useful to enumerate how this relates to previous proposals in this area rather than starting from scratch. E.g. cfg(version_since(...)) and other such RFCs.

1 Like

I think you missed the point. This RFC would not prevent you from doing something like this workaround. Rust declares GLIBC 2.17 compatibility. This means that today, I can grab any distro with GLIBC 2.17, build my program there, and there will be no GLIBC functions in VERNEED that use GLIBC versions newer than 2.17. This is exactly why Rust docker images are based on quite old distros, why zig-build works (it just uses zig linker that fakes the libc symbols for the linker).

Today, to produce a binary that works everywhere (meaning old distros with old GLIBC), I have the following options:

  • Use that old distro as a build environment. Very Linux/GNU way, very unfriendly for any sort of commercial development, and just inconvenient for people who want to use modern tools in their work.
  • Use zig cc + zig-build. Requires the Zig compiler, which has an unclear support status and can be challenging to justify for production environments.
  • Use custom GCC/LLVM builds that link binaries against the required version of GLIBC (crosstool-ng, etc)
  • Use cross, that is, in general a combination of the two above (can use zig or can use custom GCC toolchain)

None of the above removes the requirement for a developer to actually know what GLIBC function is called and make that decision consciously. All they do is either isolate the host glibc environment from the linker or just replace the host environment entirely. But all those workarounds are highly desirable for people who have to produce software that runs on multiple platforms and cannot be distributed only in source form (yes, commercial software exists).

I would not even bother reaching out to them. GLIBC position is clear - they only support a couple of recent versions and a couple of recent GCC releases. If you want to have GCC 16 that links against GLIBC 2.17, you are on your own. That burden rests on projects like crosstool-ng.

This is why we collect feedback, right? As I already said above, this is the zig-build-like way to specify the desired glibc (which, in its way, borrowed that from zig itself to specify the target). And if you read the RFC, the idea is that the suffix should be downgraded to -C target-libc=glibc:X.Y ASAP, since many internals rely on the exact triple name. Also, users are not required to use exactly this form if, say, external tools are breaking (and anyone using zig-build today with that form should already have handled it in their environments).

As I mentioned above - this suffix form was proposed exclusively to simplify migration from the existing zig-build-dependent project.

Could you point to the RFC with cfg(version_since(...))? I looked through the RFC and found several related, but no one mentioned version_since. Was that 2523?

Relevant RFCs:

I will review those more thoroughly and update my RFC