August 2, 2021·6 min read·engineeringcglibclinkingabi

glibc 2.34 moves thread and loader interfaces into libc

glibc integrates several formerly separate libraries while preserving old dependencies. Newly available weak symbols and opt-in 64-bit time interfaces make this an ABI change with practical source consequences.

A weak reference to a thread function can act as a decision about which code path to execute. If the symbol is absent, a program takes one path. If another loaded library supplies it, the program takes another. Moving that symbol into libc changes the decision even when the application has not added a dependency or changed its source.

The August 2 glibc 2.34 announcement explicitly identifies that consequence. Functionality formerly implemented in libpthread, libdl, libutil, and libanl now resides in libc. The stated reasons are smoother in-place upgrades and a simpler runtime implementation. The compatibility work preserves existing dependencies, but symbol availability can still change which application code executes.

I see this as a significant C event because the platform ABI is part of the language's practical importance. C interfaces often outlive the library packaging that first provided them. A change can preserve the callable interface and still affect a program that inferred runtime state from where a symbol used to live.

Existing dependencies remain loadable

New applications no longer need the corresponding -lpthread, -ldl, -lutil, and -lanl linker options to obtain this functionality from glibc. The release retains empty static archives with those names so that established link commands continue to work. Older applications linked against earlier glibc versions still load the corresponding shared objects, which are now empty.

This is a deliberately different compatibility requirement from allowing new source to link. An already-built binary can name an old shared-object dependency. Removing the file because its implementation moved would leave that dependency unresolved. Keeping a compatibility object allows the runtime arrangement to change without requiring every existing consumer to be relinked first.

The release also changes how glibc installs shared objects. Earlier installations used versioned filenames such as libc-2.33.so with ABI sonames provided through symbolic links. In 2.34, the files are installed directly under their ABI sonames. The announcement links this to package managers that remove obsolete files late in an upgrade or downgrade.

Both changes concern intermediate installation states. A running system's package manager does not replace every file in a single indivisible action. Reducing the number of separate implementation libraries and avoiding the described filename arrangement makes those transitions easier to support. That is the operational motivation stated by the release, rather than a promise that every arbitrary mixture of runtime files is valid.

The compatibility archives also explain why removing old linker flags is not the central migration task. A retained option may now select an empty archive. Its presence does not prove the runtime still has the earlier implementation split. Conversely, a successful link without the option is an observation about this glibc arrangement, not a portable conclusion about other C libraries or older systems.

A weak-symbol test can select different code

Weak references create a subtler compatibility issue because the binary can load successfully. The announcement warns that additional symbols are now available by default. Programs that used those symbols as conditions may enter branches previously reached only when a library such as libpthread.so.0 was preloaded.

The important distinction is between preserving a name's callable meaning and preserving its absence. An application may have treated absence as a useful signal about its environment. Integrating the implementation into a dependency already present invalidates that signal. Nothing about preserving old shared-object filenames can preserve the earlier absence at the same time.

This is an inference worth applying to optional runtime behavior. A symbol lookup answers whether a symbol is available under the current loading arrangement. It does not by itself establish the higher-level condition a programmer hoped to infer, such as which execution strategy is needed. The release changes the loading arrangement and therefore exposes programs that coupled those questions.

The maintainers describe such newly reached paths as potentially exposing application bugs. That qualification matters. An integration change need not introduce an incorrect function implementation to cause an application failure. It can make existing conditional code reachable under a broader set of ordinary launches. The weak reference was part of the application's control flow even if it looked like a harmless availability check.

I prefer explicit runtime assumptions to that kind of indirect test. The release gives a concrete reason for the preference. Library packaging is allowed to change while interfaces remain available, so a packaging fact is a fragile proxy for application state.

Time width remains an explicit choice

glibc 2.34 also adds 64-bit time_t support on Linux configurations where the traditional type is 32-bit. It does not switch those configurations to a new default. The announcement requires _TIME_BITS=64 together with large-file support through _FILE_OFFSET_BITS=64, and says full support requires Linux 5.1 or later.

The 2.34 feature-selection header enforces the pairing. It rejects _TIME_BITS=64 unless _FILE_OFFSET_BITS is also 64. On a configuration whose underlying time size is 32, it then enables the internal selection for 64-bit time interfaces. It also rejects an explicit request for 32-bit time on a configuration whose native time size is greater.

A build selecting the new interfaces can express the required pair through preprocessing definitions:

-D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64

These definitions select header behavior during compilation. They do not rewrite an existing binary's layouts or change how that binary was compiled. That follows from the mechanism in the header and is why the opt-in must be understood as an interface choice made by the build. An application and a library that exchange time-bearing C structures need an agreed representation at their boundary.

This resembles the library integration in its need to preserve existing consumers, but it uses a different strategy. The integration moves implementations while retaining dependency names. Time support provides a newly selectable type configuration while retaining the traditional default where specified. Treating both as a universal automatic ABI conversion would miss the distinction.

It is also necessary to distinguish choosing a wider type from obtaining full kernel support. The header can select declarations during compilation, while the running kernel determines the available system facilities. The announcement's kernel requirement is part of describing the complete supported configuration. A macro in a command line cannot independently establish that runtime environment.

Other runtime assumptions become visible

The release changes stack-size macros under _DYNAMIC_STACK_SIZE_SOURCE or _GNU_SOURCE. PTHREAD_STACK_MIN becomes a sysconf query, and Linux signal-stack size macros similarly become dynamic. The stated reason is support for dynamically sized register sets, including Arm SVE. Code that requires those expressions to be compile-time constants must account for the selected feature mode.

This change provides another example of an interface retaining its name while an assumption about its form changes. A size once usable as a constant expression can instead require a runtime query. That affects where the expression can appear in C source, independently of whether the eventual numeric value is suitable for a particular stack allocation.

The loader gains --list-diagnostics, which dumps information concerning IFUNC resolution and glibc-hwcaps selection. This exposes data relevant to which implementation the runtime can select. It fits the broader need to understand the actual runtime configuration when the installation contains alternatives or when capability-dependent behavior matters.

Allocation debugging has another explicit loading requirement. The announcement moves facilities such as mtrace and mcheck out of the default main-library behavior. Users who need them must preload the new libc_malloc_debug.so object. Existing environment settings alone therefore no longer describe the whole setup needed to obtain the old debugging behavior.

The deprecated allocation hooks also stop affecting the main allocator. Compatibility symbols remain for legacy applications, while new applications can no longer link against those hooks. The debugging object temporarily supports them, but the release describes that support as transitional. This is a concrete reminder that a symbol remaining linkable and a symbol retaining its previous effect are different compatibility claims. For tooling that instruments allocation, the documented alternative is an allocation interposition library, and the release's debugging object provides a separate path for the specified older facilities.

For source maintainers, the release's consequences are therefore specific. Weak references may select different branches because more symbols exist by default. A build selecting 64-bit time on a traditional configuration must make the matching large-file choice. Feature modes can turn stack-size macros into queries. These are separate obligations documented by the release, and each concerns a particular assumption in source or at an ABI boundary.