Building extensions with hardening flags (via manylinux)

Posting here at the request of @mayeut to get a broader audience.

The Fedora project aims to enable user-level support for x86_64 shadow stacks in the Fedora Linux 46 release, which is scheduled for spring 2027. The effort is being coordinated by @submachine; technical details can be found in the Fedora change proposal.

This will affect Python in the following way.

A multi-threaded process that runs with the shadow-stack feature on will refuse to dlopen() a shared library that was built without the -fcf-protection compiler option. This may (or will?) break Python extensions that are shipped in binary form.

I have been pointed to PEP 600 and the manylinux project, which defines how re-distributable binary Python extensions are built. In my understanding, what needs to be done is roughly the following:

  1. Add -fcf-protection to manylinux’ hardening build flags.
  2. Make sure the hardening flags end up in the sysconfig module (by passing them to ./configure as e.g. CFLAGS rather than CFLAGS_NODIST).

There is a GitHub pull request with a first draft of what the implementation could look like.

First impression: this looks quite breaking, and not only for already-released wheels on PyPI (which are non-fixable of course, so that alone might be a showstopper). A couple of comments:

  • Your point (2) doesn’t do what you think it does. sysconfig.get_config_var('CFLAGS') may be used by setuptools, but isn’t read by any other build backend - only a CFLAGS already set as an environment variable will have an effect.
    • Even if you could convince manylinux to export CFLAGS, that can be easily and silently overridden by an export CFLAGS in the build scripts for a package (e.g., in cibw_before_build.sh)
    • You have to worry about other languages beyond C/C++ too (Rust, Fortran, Zig, etc.).
  • There’s more than just extension modules. For example:
    • Regular shared libraries repackaged into wheels and shipped on PyPI. Concrete example: CUDA libraries like nvidia-cublas · PyPI, which is explicitly dlopen’d by PyTorch et al.
    • Other ways of installing Python itself, e.g. through uv (python-build-standalone) or pixi (from conda-forge); that may or may not yield a Shadow Stack-enabled python binary.
  • There are vendored shared libraries in wheels (pulled in via auditwheel), they get built in all sorts of ad-hoc ways, or grabbed from other distros (Ubuntu, Conda, etc.).
  • What about hand-written assembly[1]? I’m leaving my comfort zone there, but a bit of help from Claude suggests it needs either #include <cet.h> or forcing -Wl,-z,shstk.

I think these are the critical sentences in the doc you linked:

Processes that load any non-compliant shared object will have Shadow Stack silently disabled at startup.

The only exception is applications that are fully Shadow Stack compliant at startup and then use dlopen to load a shared object without Shadow Stack markup at runtime.

Is only python checked at startup, and anything pulled in via an import statement isn’t? So all extension modules and everything they depend on with DT_NEEDED (not just what they explicitly dlopen) is a potential problem?

Last question: will the glibc.cpu.x86_shstk default be permissive or on? That seems important for the impact, and I don’t think the doc says anything, only that one can opt out by changing the setting to off.


  1. Not uncommon, OpenBLAS, OpenSSL, libjpeg-turbo, ffmpeg, etc. ↩︎

4 Likes

I’m going to speak on the side of the Fedora Project. Please note that this is not the Fedora Project’s opinion, as I’m merely a community member

I’m sure @sicherha expected these changes to be at least somewhat breaking. This was discussed in some aspects on Fedora’s discussion mediums (like the devel mailing list, and the Fedora Project discussion forums, for instance).

(note that I did throw out some criticisms against the GCC developers on the ML, they don’t matter for this discussion, so please ignore them :slight_smile:, also, I believe the GCC developers do a fine job for the most part)


I believe it is more important to note that this is an objective that some within the Fedora Project wish to meet eventually. CET features help to increase security by preventing potential hackers from taking part in stack shenanigans, which may involve rewriting various call-stack pointers to suit their purposes.

At the moment, the Fedora Project plans on switching the feature to permissive, rather than on, so this isn’t something that CPython/PyPI should be concerned with immediately. However, it is likely that down the line, Fedora will want to switch the feature to on. I suggest to start a dialog with the developers on PyPI regarding this issue as soon as possible, so that everything may be accounted for in the future.

I’m curious to know what the current status of implementing stack canaries are, in regards to PyPI packages? Was there ever a call made to do so, or do some packages implement them, while others do not?

edit: I should probably state that CET does not prevent the overwriting of call stack pointers entirely, but that it prevents a subroutine from returning via a modified call stack pointer.

edit 2: I should also state that shadow stack protection is not the only feature that CET provides. It also provides indirect branch tracking. I would post a link to the kernel docs, but I can only provide two links at a time. Just search for Control-flow Enforcement Technology (CET) Shadow Stack.

I would be shocked if there was any degree of consistency among packages on PyPI w/r/t use of stack canaries :sweat_smile:

(This would probably be pretty easy to inventory/check, by looking at the top 10k/50k native wheels and using something like checksec to see what mitigations they apply.)

2 Likes

Good to know. I can already see that we’re going to have a bit of a mess soon, once people, businesses, and enterprises, start wanting CET switched to on (and I’m sure many will)! If a program fails CET checks while its enabled, it will crash immediately, which will break a lot of things, like handwritten assembly that doesn’t take these features into account, for instance.

Obviously, nobody can force anyone to start compiling (or assembling) their programs with it enabled. This is going to be a bit of a tough problem to solve.

Have you considered doing a mass rebuild for this in Fedora, in the sense of going through the DAG of python dependencies and building each next package with the aggressive checks, seeing what fails, fixing the package, upstreaming those fixes, and then moving on?

I initially typed out a reply that it would be possible to do this with conda-forge’s infrastructure (assuming buy-in from cf/core), but I know that Fedora has this capability as well. The biggest difference would be that conda-forge also builds for macOS and Windows, and that our infrastructure is perhaps more publicly visible (example), which might aid with coordination and getting people to participate in this effort.

Either way, the “ecosystem-sized” problem you’re encountering sounds to me like it can only be solved with this kind of slow, bottom-up approach. As such, it sounds to me like it is going to be a major undertaking, and very unlikely to be ready in Spring 2027.

6 Likes

I’m not sure what you mean by “breaking”. The situation is that all of Fedora code is compiled with -fcf-protection since a few years, but this has no effect until enabled in glibc. The change being discussed in Fedora is about the second step only, the change in glibc to actually enable those protections at runtime. In general this works for our own packages, the only problem is 3rd party compiled code.

Enabling of -fcf-protection in pypi builds and other places is “free”: there is a tiny code size overhead and linker metadata, so it should be completely OK to enable this even if it is not immediately used.

There’s more than just extension modules. [list trimmed]

This is OK. We know that we won’t be able to enable those protections in all cases, but it’s useful to enable it as many places as possible.

Yes. With glibc.cpu.x86_shstk=on, dlopen of an extension module without the appropriate flags would fail. Dlopen of an extension module that has the appropriate flags itself, but is linked to some other libraries that don’t have appropriate flags would also fail. With glibc.cpu.x86_shstk=permissive, if it loading is done early enough, the load would succeed and cause the runtime protection to be disabled.

This setting can be configured per application. So I expect it’ll default to “on”, with an override to “permissive” for apps like Python which are known to use dlopen, and “off” for some other ones where it’s known to cause problems.

So doesn’t that mean that all existing binaries would no longer be compatible with a Fedora-built Python? Which in turn means that everyone would have to switch on this flag, and then we’d have to wait until all existing builds were old enough that no-one would be using them any more, and only then could the option be set to on.

That’s a pretty long timescale, much more like multiple years than “by Spring 2027”. Unless you’re only setting the option to permissive in Spring 2027, I don’t think you stand much chance.

And even then, people pinning to specific versions of their dependencies (for reproducibility, for example) will be broken unless they can find an old version of CPython that doesn’t enable the new flag.

Yeah, it looks like a major breakage to me, as well…

1 Like

First of all, many use cases do not involve non-distro modules. For example, a lot of the system-level software in Fedora is written in Python and installed as packages. So for some subset of users, shadow stacks would “just work”.

In cases that don’t work, it’s possible to opt-out via a config file and an environment variable. So we’d probably start with a partial enablement, and as time goes on, crank up the default. People who need to run old modules would need to do the opt-out.

You are or course correct in saying that the full transition would take a very long time. But we don’t need a full transition, we get an incremental benefit with each and every program that starts running with the protection enabled. The most-commonly used packages are also updated regularly, so if we update the default build flags in the build tools, we can get quite good support across the ecosystem in a few months.

This is definitely a huge undertaking, and frankly I personally do not see a great chance of everything being in shape in time for Fedora 46 - especially given the fragmentation of the ecosystem. So I wouldn’t be surprised if we end up having to make an exemption for Python.

That said, I believe we can all agree that …

  1. enabling control-flow enforcement in particular and security mitigations in general is definitely worth pursuing, and
  2. the fact that most binary Python extensions distributed via PyPI are currently completely unhardened is not a good thing.

So yes, this is going to be a lot of work - but we have to start somewhere.

The GitHub pull request linked in the initial post would enable hardening for all extensions that are written in C, built in a manylinux container and honour the compiler flags in sysconfig. Do we have a very rough estimate of what percentage of relevant binary extensions that would cover?

2 Likes

I think there’s a misunderstanding here. There enablement in different modules is independent. For a given program, we need to have the right build flags enabled in the full set of linked and dlopen’ed libraries. So there is no DAG of dependencies.

In most cases, adjusting the build flags is all that is needed. And generally, failures are not expected. For packages that honor the build flags defaults, no changes would be needed.

Not an expert in stack protection mechanisms, I was just going by the statement that any shared library (that hasn’t been compiled with stack protection) being loaded would cause the program to abort. That means you need not just your package to be clean, but also all of its dependencies (to the extent that those could also be using affected mechanisms). And then you’re in a DAG, even though the definition of an edge may not be “bar is a python dependency of foo”, but some variation that reflects the library loading.

1 Like

Just to clarify this point, let me spell out what dlopen() will do when loading a .so file that was built without -fcf-protection (or a library in the DAG of its DT_NEEDED dependencies was built without it).

  • If the process is running with stack protection off[1], dlopen() succeeds.
  • If the process is running with stack protection on:
    • If glibc.cpu.x86_shstk is permissive:
      • If the process is single-threaded, stack protection is turned off; dlopen() succeeds.
      • If the process is multi-threaded, dlopen() fails and returns an error. Python then raises an ImportError.
    • If glibc.cpu.x86_shstk is on, dlopen() fails and returns an error. Python then raises an ImportError.

  1. Either because glibc.cpu.x86_shstk is off or because the program itself or a library in the DAG of its DT_NEEDED dependencies was built without -fcf-protection. ↩︎

1 Like

I meant you’ll be breaking a couple of workflows that developers using Python on Fedora may rely on. In particular, creating Python virtual environments based on the distro’s shipped python interpreter, which is commonly done:

# Inside a git repo one is working on, or in some random directory:
python -m venv venv
source venv/bin/activate

and then one of:

  • pip install <a-package-from-pypi>
  • working on a package containing native code locally (many ways, e.g., pip install -e . --no-build-isolation)

The proposed change to the manylinux Docker images is a first attempt at fixing the first bullet. Even if that approach can be completed, patching up manylinux won’t help at all for the second bullet, which is also common.

Just a reality check here. For distros the case seems to be compelling. For wheels on PyPI and for local development, it isn’t very compelling - the supply chain security problems we have on PyPI are way more important, and we have a lot to do there. So if we collectively start spending effort on enabling -fcf-protection now, it is going to cost a lot of time that would be better spent on supply chain issues. The real reason we’d want to prioritize this is to not break the workflows I outlined above.

As I pointed out in a comment on that PR, it’s setuptools-specific. It will not work for any other build backends, and many of the most-used packages have switched away from setuptools by now (and lots more are working on switching away). Today, you’d get maybe 60%[1] of packages with an extension module, but almost no coverage at the application/environment level, given many of the largest and actively-maintained ones (the likes of numpy) have long since switched.

If manylinux would use export CFLAGS=..., export CXXFLAGS=..., etc. then it could work, at least getting it to >95% support. Setting env vars like that is considered bad practice though, so I’m not sure that’s the way to go. There’ll also still be things that won’t work, like greenlet[2]


This looks a lot like -fPIC: your .so’s must have it, or your application may break at runtime. That flag is added at the build system level[3], rather than at a higher level like manylinux. That would be in principle a better place, because there’s no way for manylinux to affect the “build a package locally” workflow. There are a couple of hurdles to deal with though; the story for -fcf-protection isn’t as good as for -fPIC - less support, and more intrusive:

  • It’s glibc-specific, and not supported by Musl libc. That’s a difference that can’t be expressed natively by build systems, they tend to not want to know that - you’re supposed to write portable C/C++ code, and if your code depends on glibc-specific extensions, you as build system user get to deal with that
  • It may require source-level changes, like for greenlet and some assembly code

That said, I think either at the build system level or at the compiler defaults level would be more appropriate than at the manylinux level, given the need to support local package builds as well.


  1. Exact number isn’t super relevant here; it’s dropping over time ↩︎

  2. ↩︎

  3. Today I mean. Of course it’s possible to implement, but it’s not something build systems expose right now, unlike “you’re targeting x86-64” ↩︎

4 Likes

From your footnote:

Can you explain how that “or” works? Does it depend more or less randomly on which library gets loaded first? At least that what it sounds like to me (but more than happy to be corrected on this!): if it’s one built with stack protection first, then later ones fail to load. If it’s one without protection that gets loaded first, then the rest succeeds?

We have to distinguish here between direct library references (via DT_NEEDED) and dynamic loading of libraries at runtime (via dlopen()).

A DT_NEEDED entry referencing, say, libfoo.so.42 gets generated when you link your application with -lfoo. You can check these with readelf -d (search the output for (NEEDED)) or with the libtree tool.

When you launch your program, the dynamic loader first loads the executable itself and then goes through the list of referenced libraries. For each library, it loads the library itself and then processes the list of its referenced libraries, and so on. The whole procedure happens in a depth-first manner if I’m not mistaken.

For each binary file loaded, the loader additionally looks at the file’s .note.gnu.property section and checks whether the SHSTK property is set (as a user, run readelf -n to see it). If all files have the annotation, then the loader enables the shadow-stack feature for the process; otherwise - i.e. if any of them lack the SHSTK annotation - it turns the feature off.

All of this happens before the first line of code of main() gets executed. So this is the easy part.

Things become tricky when a shadow-stack-enabled process later at runtime tries to dlopen() a library. Here, the loader takes the same steps as described above: it loads the transitive hull of dependencies and checks their SHSTK annotation. If any of the newly loaded libraries do not claim conformance, the loader either has to reject the operation (if glibc.cpu.x86_shstk is on) or turn shadow stacks off (if it is permissive).

It gets trickier: retroactively disabling shadow stacks is easy for a single-threaded process but hard if the process has multiple threads. It would require dedicated support by the kernel that is not implemented as far as I’m aware - @submachine, please correct me if I’m wrong.

The bottom line for Python is this: if the Python interpreter is launched in permissive mode and all imports are at the top of the .py files and no module launches threads during its initialisation, then things will be fine. That’s a lot of ifs, obviously, and there might be further pitfalls in Python that I’m unaware of.

This is not really an issue in my view. Code that is built with shadow-stack support is 100 % backward-compatible; it will run fine even if the loader does not support shadow stacks.

In fact, this is precisely the status quo in all major Linux distributions: most user-level code is built with -fcf-protection but the loader does not enable it yet.


What might be a bit of an issue, though, is that the -fcf-protection flag is architecture-specific unlike -fPIC.

As I pointed out in a comment on that PR, it’s setuptools-specific. It will not work for any other build backends, and many of the most-used packages have switched away from setuptools by now (and lots more are working on switching away). Today, you’d get maybe 60% of packages with an extension module, but almost no coverage at the application/environment level, given many of the largest and actively-maintained ones (the likes of numpy) have long since switched.

I would have thought newer backends would respect sysconfig flags. It doesn’t seem clear if not honoring sysconfig flags is a feature or a bug in newer backends. At least scikit-build-core documentation and issue tracker don’t seem to have any information about this. cc @henryiii for this backend. I think this needs to be clarified in any case. This would be one way to achieve “build system level”.

I can also see “the compiler defaults level” being implemented in manylinux if it’s possible (I guess that would be the case with Fedora ?)

If manylinux would use export CFLAGS=..., export CXXFLAGS=..., etc. then it could work, at least getting it to >95% support. Setting env vars like that is considered bad practice though, so I’m not sure that’s the way to go.

I agree that setting env vars like that is considered bad practice so that’s not something I’d like to see introduced in manylinux.

I think there’s one place that’s used by a majority of projects regardless of the build backend, that’s auditwheel. Adding a check in there is likely needed to go forward with this. It could be a warning at first, then an error at some point (probably with a way to bypass at first).

1 Like

I’m fairly sure it’s a feature - at least, it was done on purpose. It’s bad practice to just copy over some compile flags from one of one’s dependencies from first principles. sysconfig.get_config_var(...) is discouraged API[1] in CPython, CPython can be built with different CFLAGS by different distributors, and we have a mix of C and C++ extension modules but sysconfig.get_config_var('CXXFLAGS') does not exist.

You can also check what your local interpreter(s) give back, there’s lots of stuff in there that’s just random and unrelated to building extension modules. On my macOS machine:

# system python CFLAGS
% python3 -c "import sysconfig; print(sysconfig.get_config_var('CFLAGS'))"
-Wno-unused-result -Wsign-compare -Wunreachable-code -fno-common -dynamic -DNDEBUG -g -fwrapv -O3 -Wall -iwithsysroot/System/Library/Frameworks/System.framework/PrivateHeaders -iwithsysroot/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/Headers -arch arm64 -arch x86_64 -Werror=implicit-function-declaration -Wno-error=unreachable-code -Wno-error=unused-but-set-variable -Wno-error=cast-function-type-mismatch -Wno-unknown-warning-option

# conda-forge python CFLAGS
% python3 -c "import sysconfig; print(sysconfig.get_config_var('CFLAGS'))"         
-fno-strict-overflow -Wsign-compare -Wunreachable-code -DNDEBUG -O2 -Wall  -fPIC  -O2  -isystem /Users/rgommers/code/pixi-dev-scipystack/scipy/scipy/.pixi/envs/build/include   -arch arm64   -fPIC  -O2  -isystem /Users/rgommers/code/pixi-dev-scipystack/scipy/scipy/.pixi/envs/build/include   -arch arm64

Very little of that is related to extension modules; from -O2 to lots of warning-related flags, as well as duplicated flags - it’s a mess. It gets worse when you compare across platform; if I check on Linux aarch64 I get -O3 instead of -O2 and -flto is included, which will break quite a few packages that aren’t LTO-ready.

Not much is documented either way, and what’s there tends to be conflicting with each other. The split between CFLAGS and CFLAGS_NODIST in the configure docs and the existence of python3-config --cflags are hints at there being intent of reuse, but I read it as outdated setuptools-specific code.

After writing this, I did a bit more searching and found PEP 831. The most compelling evidence for CFLAGS reuse is this section. The authors knew though that not all build systems use the sysconfig values, see the Ecosystem Impact section[2].

I think we need something new (preferably in code, but docs-only would already help) which contains only the flags essential to or strongly recommended for extension module building. There’s only a couple, and they’re already included in build-details.json (libpython.link_extensions, abi.extension_suffix, c_api.headers, see PEP 739). That looks like the best place to add a new key, and add frame pointer and -fcf-protection flags as values. PEP 831 is also a nice model of how to propose adding a new default compile flag to extension modules - a PEP seems appropriate, rather than manylinux trying to add something that isn’t specific to distributing on PyPI.


  1. We had a lot of discussions previously, and my understanding is that while get_config_var is public, there’s no guarantees on contents of configure flags captured at build time. There’s a desire, and open issues, for a proper design of public API for what Python packages/users may need. ↩︎

  2. See also the bold in the abstract for “Strongly recommend that all build systems in the Python ecosystem build with frame pointers by default” ↩︎

5 Likes

I did a bit of testing, and confirmed none of meson-python, scikit-build-core, maturin and Bazel look at sysconfig’s CFLAGS.

After having a look at how that could actually be implemented in Meson, I think I changed my mind[1]. Reason: these flags are both compiler- and platform-dependent. It’s possible to do it at the build system level, just not very clean. It’d require something like a base option with options auto (follow toolchain defaults) / true / false. And then in meson-python passing that flag with a different default. It can’t be extension_module-specific, because it’d miss common things like static libraries that are built separately and then linked into the extension module.

Distros seem to be doing this with compiler spec files, and that’s the cleaner way of doing it. It can be done in manylinux, which sidesteps the compiler/platform-specific issues. Here’s an experiment of adding both -fcf-protection=return and the frame pointer flags in numpy wheel builds, via adding a spec file into the manylinux containers[2]: diff, CI log:

Control flow protection and frame pointer results per .so in the numpy wheel

With the hardening we get:

| object | control-flow protection | frame pointers |
|---|---|---|
| `_operand_flag_tests.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 1/6 (16.7%) |
| `_struct_ufunc_tests.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 1/7 (14.3%) |
| `_reduction_loop_tests.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 4/14 (28.6%) |
| `_multiarray_tests.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 52/299 (17.4%) |
| `_simd.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 30/2479 (1.2%) |
| `_rational_tests.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 41/85 (48.2%) |
| `_multiarray_umath.cpython-313-x86_64-linux-gnu.so` | **none** | 4265/8657 (49.3%) |
| `_umath_tests.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 19/40 (47.5%) |
| `lapack_lite.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 13/18 (72.2%) |
| `_umath_linalg.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 95/291 (32.6%) |
| `_pocketfft_umath.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 124/185 (67.0%) |
| `bit_generator.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 90/149 (60.4%) |
| `_bounded_integers.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 87/121 (71.9%) |
| `_generator.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 208/332 (62.7%) |
| `mtrand.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 169/275 (61.5%) |
| `_mt19937.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 57/93 (61.3%) |
| `_pcg64.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 72/109 (66.1%) |
| `_sfc64.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 46/78 (59.0%) |
| `_philox.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 53/86 (61.6%) |
| `_common.cpython-313-x86_64-linux-gnu.so` | x86 feature: IBT, SHSTK | 69/92 (75.0%) |
| `libscipy_openblas64_-61654e39.so` | **none** | 399/12752 (3.1%) |
| `libgfortran-83c28eba-468e71e5.so.5.0.0` | x86 feature: IBT, SHSTK | 0/1359 (0.0%) |
| `libquadmath-2284e583-a9307bba.so.0.0.0` | x86 feature: IBT, SHSTK | 0/93 (0.0%) |

and without it we get:

| object | control-flow protection | frame pointers |
|---|---|---|
| `_operand_flag_tests.cpython-313-x86_64-linux-gnu.so` | **none** | 0/6 (0.0%) |
| `_struct_ufunc_tests.cpython-313-x86_64-linux-gnu.so` | **none** | 0/7 (0.0%) |
| `_reduction_loop_tests.cpython-313-x86_64-linux-gnu.so` | **none** | 0/14 (0.0%) |
| `_multiarray_tests.cpython-313-x86_64-linux-gnu.so` | **none** | 0/299 (0.0%) |
| `_simd.cpython-313-x86_64-linux-gnu.so` | **none** | 6/2479 (0.2%) |
| `_rational_tests.cpython-313-x86_64-linux-gnu.so` | **none** | 0/85 (0.0%) |
| `_multiarray_umath.cpython-313-x86_64-linux-gnu.so` | **none** | 471/8657 (5.4%) |
| `_umath_tests.cpython-313-x86_64-linux-gnu.so` | **none** | 0/40 (0.0%) |
| `lapack_lite.cpython-313-x86_64-linux-gnu.so` | **none** | 0/18 (0.0%) |
| `_umath_linalg.cpython-313-x86_64-linux-gnu.so` | **none** | 0/291 (0.0%) |
| `_pocketfft_umath.cpython-313-x86_64-linux-gnu.so` | **none** | 12/185 (6.5%) |
| `bit_generator.cpython-313-x86_64-linux-gnu.so` | **none** | 0/149 (0.0%) |
| `_bounded_integers.cpython-313-x86_64-linux-gnu.so` | **none** | 0/121 (0.0%) |
| `_generator.cpython-313-x86_64-linux-gnu.so` | **none** | 0/332 (0.0%) |
| `mtrand.cpython-313-x86_64-linux-gnu.so` | **none** | 0/275 (0.0%) |
| `_mt19937.cpython-313-x86_64-linux-gnu.so` | **none** | 0/93 (0.0%) |
| `_pcg64.cpython-313-x86_64-linux-gnu.so` | **none** | 0/109 (0.0%) |
| `_sfc64.cpython-313-x86_64-linux-gnu.so` | **none** | 0/78 (0.0%) |
| `_philox.cpython-313-x86_64-linux-gnu.so` | **none** | 0/86 (0.0%) |
| `_common.cpython-313-x86_64-linux-gnu.so` | **none** | 0/92 (0.0%) |
| `libscipy_openblas64_-61654e39.so` | **none** | 399/12752 (3.1%) |
| `libgfortran-83c28eba-468e71e5.so.5.0.0` | x86 feature: IBT, SHSTK | 0/1359 (0.0%) |
| `libquadmath-2284e583-a9307bba.so.0.0.0` | x86 feature: IBT, SHSTK | 0/93 (0.0%) |

Frame-pointer percentages are a disassembly heuristic: leaf functions and functions touching no stack never get a frame pointer regardless of flags.

Binary sizes aren’t affected (by much at least), 16.8 MB in both cases.

That all looks about as expected. The extensions that end up missing protection are _multiarray_umath.so (due to it including already-built assembly objects from SVML) and libscipy_openblas64_ (vendored in by auditwheel) - both fixable with a bit of effort.

For -fcf_protection this is likely enough, also for local development, given that the compiler toolchains on distros that want to flip the switch already use -fcf_protection=full[3].

For frame pointers, that won’t be the default in toolchains, so if the desired behavior really is “do what CPython did” also for local dev builds, then I think adding it to build-details.json as semantic info rather than as a “inherit the flags for the compiler with which the target interpreter was built”[4] might be necessary - that’s a fair amount of work per build system and build backend though.


  1. Mostly at least, see last paragraph on where PEP 739 may still come in for frame pointers. ↩︎

  2. Some nontrivial bash generated with help from Claude ↩︎

  3. That still leaves out non-system compilers like from conda-forge or Nix - but they might flip the default as well at some point, probably well before things really start breaking ↩︎

  4. Remember that compilers might not even match, e.g. interpreter built with Clang and Python package with GCC or vice versa will be common - armv7 is a case where they have incompatible flag names, there might be others. More niche compilers or folks stuck on the old (non-LLVM-based) Intel compilers might also need special handling. ↩︎

2 Likes