January 30, 2025·5 min read·engineeringcglibcruntimec23

glibc 2.41 changes the rules for executable stacks

glibc 2.41 stops dynamically loaded libraries from making a non-executable stack executable and adds the C23 pi-scaled trigonometric functions.

A shared library can carry a requirement that affects the whole process. In glibc 2.41, a library that requires an executable stack can no longer cause dlopen or dlmopen to change a non-executable stack into an executable one. Loading fails instead. That compatibility change is among the most consequential parts of the January 30 release announcement, even though the release also adds another set of C23 library functions.

C was my first language, and its continuing importance to me has much to do with interoperability. A library boundary carries calling conventions and data layouts, but it can also carry requirements on the process that loads it. This release makes one of those requirements less negotiable after startup. Understanding that distinction is more useful than treating the change as a general prohibition on executable stacks.

Startup and dynamic loading have different rules

The release announcement distinguishes two situations. At startup, the loader still permits an executable stack by default. A new tunable, glibc.rtld.execstack, controls that permission. During a later dlopen or dlmopen, however, the loader will no longer grant execute permission to satisfy a newly loaded object if the stack is not already executable.

The manual shipped with glibc 2.41 describes the startup decision in terms of the architecture ABI and the ELF PT_GNU_STACK header. If the header exists, its flags contribute the requirement. If it is absent, the architecture defaults matter. A missing header therefore does not mean that every architecture makes the same decision, and it does not establish that the library needs executable stack memory for a legitimate reason.

Before this change, late loading could alter stack permissions to accommodate the object. The new failure occurs at the load boundary. A host application can start successfully and encounter the incompatibility only when it loads the affected extension. For applications with optional libraries, startup success consequently says little about whether that later operation will succeed under the new loader.

The tunable has two documented values. Zero disables the startup negotiation that permits executable stacks. One enables it, which is the default, but does not itself require an executable stack. For a program whose normal invocation is ./application, a stricter startup policy can be expressed as follows.

env GLIBC_TUNABLES=glibc.rtld.execstack=0 ./application

That command can make a program fail to load if its main executable requires an executable stack. Setting the tunable to one is not a general repair for libraries rejected during later loading. The versioned manual explicitly says the late-loading restriction applies regardless of the tunable value when the main program did not require an executable stack at loading time.

There is a hardware qualification as well. Some systems do not distinguish read permission from execute permission in the usual way, and the kernel can affect that behavior. The manual says the tunable policy is independent of those hardware capabilities and kernel settings. It would overstate the feature to describe it as a universal hardware guarantee that stack bytes can never execute.

The compatibility cost belongs to the library requirement

The practical consequence is that executable-stack metadata becomes a deployment concern for dynamically loaded dependencies. The failure is tied to the object and the state of the process when loading occurs. Changing application source to avoid a particular C language construct will not necessarily change the requirement recorded in a separately built library.

There are two questions to answer about an affected object. One is whether it declares or implicitly acquires an executable-stack requirement. The other is whether its implementation actually needs that permission. The release notes establish the loader behavior, but they cannot answer the second question for an arbitrary binary. Removing a requirement without understanding the object is therefore a different operation from adapting to the loader change.

I prefer a library load to fail when it would otherwise change process memory permissions in this way. The cost is a visible compatibility failure for affected libraries. That judgment does not imply that every such library is defective, only that the host process should not acquire this permission as a side effect of an ordinary extension load.

C23 support also depends on the runtime

The numerical additions illustrate another part of the C implementation boundary. glibc 2.41 adds the C23 pi-scaled trigonometric families, including sinpi, cospi, and tanpi, with corresponding inverse functions. The announcement traces these functions to TS 18661-4:2015. They have variants for the supported floating types and type-generic macros in tgmath.h.

The versioned mathematics manual defines sinpi(x) as the sine of pi multiplied by x. It similarly defines cospi and tanpi. These interfaces let a program express an argument in multiples of pi directly. An application does not have to introduce its own approximate pi constant merely to express that operation through the ordinary trigonometric interface.

The distinction is in the operation requested from the library. A source expression that first multiplies by a floating approximation to pi supplies the result of that multiplication to sin. A call to sinpi supplies the original scaled argument to the library. The API preserves that information until the library implements the requested operation. This explains the value of a separate function name without claiming that every call has a particular performance advantage.

Compiler acceptance of a language dialect does not by itself establish that these library functions exist in the target runtime. Their addition here is a glibc release change. Code that starts calling them has acquired a library dependency as well as a source-level expectation about declarations. That matters for C interoperability because the same function can sit behind interfaces used by programs written in other languages.

Draft support remains an explicit choice

The release also introduces _ISOC2Y_SOURCE for selected features of the next C draft. According to the announcement, _GNU_SOURCE and compilation with gcc -std=gnu2y enable those features too. The maintainers explicitly describe the draft as active work and the enabled feature set as subject to change. The macro is an opt-in mechanism for an evolving specification, not a declaration that the next language revision is complete.

These changes put several kinds of compatibility in the same runtime release. Pi-scaled mathematics adds callable functionality, draft feature selection changes what headers expose, and executable-stack handling changes which objects the loader accepts. For an application that uses dynamically loaded libraries, the immediate behavioral difference is specific. A later load can now fail where an older loader would have changed the stack permissions to accommodate the library.