Skip to content

unix: link libpython with -Bsymbolic-functions#1019

Open
indygreg wants to merge 1 commit intomainfrom
gps-symbolic-functions
Open

unix: link libpython with -Bsymbolic-functions#1019
indygreg wants to merge 1 commit intomainfrom
gps-symbolic-functions

Conversation

@indygreg
Copy link
Collaborator

This changes the runtime loader lookup semantics for libpython so function references are resolved in the local library first.

  • May improve startup time by eliding symbol lookups in the executable and other libraries.
  • Prevents symbols provided by libpython from resolving to other loaded libraries.
  • Enables additional compiler+linker optimizations by guaranteeing that libpython symbols resolve to the local library. (e.g. more aggressive inlining across translations units.)

I believe this change is safe since we're already disabling semantic interposition and PGO+LTO+BOLT result in substantial inlining.

However, this change can break LD_PRELOAD where a separate DSO providing libpython symbols is injected at the front of the loader search path. In this scenario, the libpython symbol will be used instead of the variant injected via LD_PRELOAD. I'm unsure if any popular software (that isn't malware) is relying on intercepting libpython symbols via LD_PRELOAD. But because of our aggressive build optimizations, various functions would have been inlined and wouldn't have been LD_PRELOAD interceptable anyway since the PLT was already elided. So this change effectively finishes the "migration" of making LD_PRELOAD unreliable.

@indygreg indygreg force-pushed the gps-symbolic-functions branch from 33424ee to 44f8e67 Compare March 20, 2026 07:34
@indygreg
Copy link
Collaborator Author

This change should only affect ELF/Linux. I don't have access to my Linux machine at the moment and can't conduct reliable performance testing. There's a possibility this improves python startup time as well as general interpreter performance. It shouldn't cause any performance regressions. Someone else will have to test performance.

@indygreg indygreg force-pushed the gps-symbolic-functions branch from 44f8e67 to 4d19e45 Compare March 20, 2026 08:14
This changes the runtime loader lookup semantics for libpython so
function references are resolved in the local library first.

* May improve startup time by eliding symbol lookups in the executable
  and other libraries.
* Prevents symbols provided by libpython from resolving to other loaded
  libraries.
* Enables additional compiler+linker optimizations by guaranteeing that
  libpython symbols resolve to the local library. (e.g. more aggressive
  inlining across translations units.)

I believe this change is safe since we're already disabling semantic
interposition and PGO+LTO+BOLT result in substantial inlining.

However, this change can break `LD_PRELOAD` where a separate DSO providing
libpython symbols is injected at the front of the loader search path.
In this scenario, the libpython symbol will be used instead of the variant
injected via `LD_PRELOAD`. I'm unsure if any popular software (that isn't
malware) is relying on intercepting libpython symbols via `LD_PRELOAD`. But
because of our aggressive build optimizations, various functions would have
been inlined and wouldn't have been `LD_PRELOAD` interceptable anyway since
the PLT was already elided. So this change effectively finishes the
"migration" of making `LD_PRELOAD` unreliable.
@indygreg indygreg force-pushed the gps-symbolic-functions branch from 4d19e45 to 868c3fa Compare March 20, 2026 08:29
@indygreg indygreg marked this pull request as ready for review March 20, 2026 11:23
@indygreg
Copy link
Collaborator Author

Hmmm. I forgot we integrated libpython into python. So this change may not have as substantial impact as I initially thought. But I think it is still desired on principle to tighten the guarantees on how symbols resolve.

@zanieb zanieb requested review from geofft and jjhelmus March 20, 2026 11:58
@jjhelmus
Copy link
Contributor

I agree that this will likely have a limited effect given that libpython is statically linked in the python binary.

I ran fastmark with and without this change on Python 3.14 for a quick benchmark, taking the average of three runs:

host CPU ref with flag change
i5-4570S 8077 8022 -0.68%
i9-9900k 14484 14507 +0.16 %

I can run more in-depth benchmarks if there is interest but that will need to wait until Monday.

@geofft
Copy link
Collaborator

geofft commented Mar 20, 2026

I would not list this in quirks at all. We've noticed that people tend to read the quirks page as pre-emptive documentation instead of as debugging steps, and so a heading "LD_PRELOAD does not work" is going to make people think "oh, this version of Python is fully statically linked and doesn't support LD_PRELOAD at all, I am not going to try it out" instead of being useful to people who are actually running into an issue with not being able to interpose on a symbol. As I understand it, this does not at all change the ability to interpose on non-libpython functions (e.g. libc functions), nor does it change the ability to interpose on calls from a main executable to libpython.so. If I understand correctly, the case that it impacts is libpython.so looking up other symbols in libpython.so, which seems so niche and questionable that I'm not sure it's worth calling attention to, and given LTO + BOLT, it's already unlikely to work anyway.

I see that the linked maskray blog post (which is excellent and I need to reread it a few times, thank you!) makes the same point:

Note: it is unfair and usually incorrect to just state "it breaks LD_PRELOAD". Please categorize your LD_PRELOAD use cases.

That said, I am actually worried about a specific type of breakage, given our statically-linked bin/python: one thing we were worried about is programs that incorrectly do a ctypes.CDLL(f"libpython3.{sys.version_info[1]}.so.1.0") and access Python API functions from it. Currently, those programs will open up a second copy of libpython, unrelated to the statically-linked copy in bin/python, and access it, and that kind of works depending on what you do (e.g., accessors on Python objects that do not interact with global state will be fine, because the two libpythons are guaranteed to be ABI-compatible). I am a little afraid that before this change, the opened libpython.so would resolve some symbols out of the main executable bin/python, and now it will resolve those to itself. Is that something to be concerned about? Probably -Bsymbolic-functions instead of -Bsymbolic makes this less of a risk?

I think one philosophical question here is how much we're interested in improving the performance of shared libpython. In theory, I'd be interested in seeing the benchmark of the following program linked against -lpython3.x:

#include <Python.h>

int main(int argc, char *argv[]) {
	return Py_BytesMain(argc, argv);
}

(and I kind of want to build and ship that in our distributions in a corner somewhere, it's useful for regression-testing other things like getpath changes), but I think there's a pretty strong argument for not carrying a patch to the build if the only improvement is on shared libpython.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants