Hello.
In Fedora and RHEL, we have recently been bitten by a problem of the pyexpat module being linked to the libexpat.so.1 library as we build Python --with-system-expat.
When Python is built with expat version 2.7.2 or newer, it uses the XML_SetAllocTrackerActivationThreshold symbol added in that expat version. The usage in pyexpat was added in Expose Expat XML attack protection APIs · Issue #90949 · python/cpython · GitHub
However, as the libexpat.so.1 “system” library is a standalone thing, it may happen that the user then installs this Python with expat 2.7.1 and the thing blows up on import time:
>>> import pyexpat
Traceback...
ImportError: ...pyexpat.cpython-315-x86_64-linux-gnu.so: undefined symbol: XML_SetAllocTrackerActivationThreshold
This is not the first time we have encountered this problem; in the past, it also occurred with expat 2.6.0 and another symbol. The problem boils down to the fact that the check for expat version happens on buildttime, but is not checked at runtime at all.
In Fedora, we have hotpatched this situation for now by adding a runtime dependency on an expat version that is >= the expat version which was used during the build. This is a bit too strict, because when we build Python with expat 2.7.3, it will require expat >= 2.7.3, despite it really only needing >= 2.7.2. But we decided it was better to be safe than sorry.
An alternative approach, which we used in RHEL, is to hardcode the required expat version (to >= 2.7.2) and naïvly parse the pyexpat sources for all the relevant #if XML_COMBINED_VERSION >= ... or #if XML_COMBINED_VERSION > ... conditionals, find the highest satisfiable version, and assert that our hard-coded required minimal expat version matches. This reduces the needlessly harsh expat dependency constraint, but might turn out to be fragile if the XML_COMBINED_VERSION conditionals ever start looking differently than expected. It is also only feasible because RHEL is a slow-moving target.
We would very much like to avoid this in the future and were wondering if it was possible to move the checks to runtime instead.
@fweimer suggested that it should be possible to make the symbol references weak to solve this:
Another possibility would be to make the symbol reference weak in Python, and use the functionality only if it is available in the installed version of Expat.
You can think of it as a slightly more type-safe version of dlsym.
You’d write
#pragma weak XML_SetAllocTrackerActivationThresholdand then before calling the function, you’d check
if (XML_SetAllocTrackerActivationThreshold != NULL) { // Use new functionality here. } else { // Other code, presumably with vulnerability. }I assume the two code paths already exist because Python can still build with the old version of Expat. In a sense, it’s just a matter of moving the check from compile to run time.
At the same time, Gordon Messmer thinks we should ask expat to use versioned symbols, which is something we can turn into an automatic runtime dependency in RPM packages. We’d like to explore both options, so here I am.
Is moving the checks to runtime something Python would be inclined to accept? Of course, I am willing to work on this myself.
cc @picnixz