3

I manage a complex C++ project

I am getting field reports that a binary built on one system will not run on another system because of glibc version mismatch.

I just want to verify: is the version of glibc required by the binary completely determined by which version of g++ is used to compile it?

In this case, the binary is built with g++ 11, and run on another system that doesn't have g++ 11 installed.

Jacko
  • 12,665
  • 18
  • 75
  • 126
  • 1
    Roughly speaking, the version of glibc required by the binary depends on what version of glibc the gcc installation is *configured* to use. glibc is backward compatible, not forward compatible - binaries built for a newer glibc will not run with older glibc versions, but will run with subsequent (more recent) glibc versions than you have built with. [It's more complicated than that, as glibc uses features provided by gcc, and those features vary between gcc versions (e.g. new features added, older ones removed)]. – Peter Jul 30 '21 at 15:35

1 Answers1

4

I just want to verify: is the version of glibc required by the binary completely determined by which version of g++ is used to compile it?

Not at all. The version of GLIBC required at runtime is determined by several factors:

  1. The version of GLIBC used at link time.
  2. The features of GLIBC actually used (which in turn depend on the compiler version, and possibly compilation flags).
  3. Whether or not any of the features used have had changes in their ABI.

In this case, the binary is built with g++ 11

You can install g++-11 on a system with GLIBC-2.15, and on a system with GLIBC-2.31. For non-trivial source, the resulting binaries are likely to have different requirements on GLIBC at runtime.

See this answer to understand how symbol versioning comes into play.

TL;DR: if you want to build an executable or a shared library which is portable to different Linux distributions, select the oldest version of GLIBC you are willing to support, and build on a system with that version (you don't have to have a physical machine for this -- a VM or a docker container work find).

Due to backwards compatibility guarantees, your binary will work on all systems with the same or newer version of GLIBC.

Update:

Let's say that you build libfoo.a on GLIBC-NN, and the end user links a.out on GLIBC-MM, where MM < NN.

This is a very dangerous practice: the program may silently corrupt its data, and finding the root cause of such corruption will be very difficult.

Now suppose the end user links a.out on GLIBC-MM where NN <= MM (this is the actual situation according to comments), then runs a.out on a system with GLIBC-JJ, where JJ < NN.

In that case, I the possibility of "silent" corruption is minimized, but not completely eliminated.

Consider the following hypothetical example (foo() is part of libfoo.a you provide):

struct passwd *foo()
{
   struct passwd *pw = getpwuid(42);
   if (pw->field_a == 123)              // 1
     pw->field_a = 1234;                // 2
   return pw;
}

Now suppose that in GLIBC-NN field_a exists as part of struct passwd, but in GLIBC-JJ it does not. In that case, at point 1 above the program may read past an end of allocated buffer, and at point 2 it may write past the end.

Note: the program above is already violating "application shall not modify the structure to which the return value points" requirement and so has undefined behavior anyway; it's just an example of how bugs due to mismatched GLIBC may come about.

Employed Russian
  • 199,314
  • 34
  • 295
  • 362
  • Thanks for this excellent answer, @Employed Russian . And what happens if I build a static library instead of a dynamic one? The user who reported this issue switched to static build, and no longer had these versioning issues. – Jacko Aug 07 '21 at 21:22
  • @jacko What happens in that case is undefined. It's possible that the binary will work fine. It's also possible that it will crash in mysterious ways, or corrupt the data it outputs. Generally you don't want to be in a position where the library you provided ends up causing customer data corruption. – Employed Russian Aug 08 '21 at 05:33
  • Thanks! To be clearer - by static I mean simply to build a static *.a library, not static linking with GLIBC. – Jacko Aug 09 '21 at 15:34
  • @Jacko Yes, I understood. You are building an archive `libfoo.a` against GLIBC-newer, and then the customer uses it to link `a.out` using `libfoo.a` and GLIBC-older. This can cause _all_ kinds of problems (as I said earlier). – Employed Russian Aug 09 '21 at 17:00
  • Thanks! Actually, the user links the static `libfoo.a` to a binary `binfoo` on system with GLIBC-newer, then runs `binfoo` on system with GLIBC-older. Is this still a risk ? – Jacko Aug 10 '21 at 13:45
  • Can you please explain why binary that is statically linked is safer than dynamically linked ? They both are dynamically linked to glibc. – Jacko Aug 11 '21 at 11:14
  • @Jacko "why binary that is statically linked is safer than dynamically linked" -- it's not. The opposite is true -- binary linked against `libfoo.so` will _fail to run_ (which is _safer_ than silently corrupting data). – Employed Russian Aug 11 '21 at 16:51
  • Thanks. What I meant was : 1) `binfoo` linked against `libfoo.a` with GLIBC-older, then running on system with GLIBC-newer or 2) `binfoo` linked against `libfoo.so` with GLIBC-older, then running on system with GLIBC-newer. Why is #1 any safer than #2, since they both use GLIBC dynamic libraries ? – Jacko Aug 12 '21 at 15:55