classification
Title: distutils UnixCCompiler: Remove standard library path from rpath
Type: Stage: resolved
Components: Library (Lib) Versions: Python 3.8
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: Nosy List: cstratak, fweimer, koobs, mcepl, vstinner
Priority: normal Keywords: patch

Created on 2019-04-18 15:02 by vstinner, last changed 2019-04-25 13:16 by vstinner. This issue is now closed.

Pull Requests
URL Status Linked Edit
PR 12876 closed vstinner, 2019-04-18 15:09
Messages (17)
msg340496 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-18 15:02
Since 2010, the Fedora packages of Python are using a patch on distutils UnixCCompiler to remove standard library path from rpath. The patch has been written by David Malcolm for Python 2.6.4:

* https://src.fedoraproject.org/rpms/python38/blob/master/f/00001-rpath.patch
* https://src.fedoraproject.org/rpms/python2/c/f5df1f834310948b32407933e3b8713e1121105b

I propose to make this change upstream so other Linux distributions will benefit on this change: see attached PR.

"rpath" stands for "run-time search path". Dynamic linking loaders use the rpath to find required libraries:
https://en.wikipedia.org/wiki/Rpath

Full example. Install Python in /opt/py38 with RPATH=/opt/py38/lib, to ensure that Python looks for libpython in this directory:

$ cd path/to/python/sources
$ ./configure --prefix /opt/py38 LDFLAGS="-Wl,-rpath=/opt/py38/lib/" --enable-shared
$ make
$ make install  # on my system, my user can write into /opt ;-)
$ objdump -a -x /opt/py38/bin/python3.8|grep -i rpath
  RPATH                /opt/py38/lib/
$ objdump -a -x /opt/py38/lib/libpython3.8m.so|grep -i rpath
  RPATH                /opt/py38/lib/

Python is installed with RPATH:

$ /opt/py38/bin/python3.8 -m sysconfig|grep -i rpath
	BLDSHARED = "gcc -pthread -shared -Wl,-rpath=/opt/py38/lib/"
	CONFIGURE_LDFLAGS = "-Wl,-rpath=/opt/py38/lib/"
	CONFIG_ARGS = "'--prefix' '/opt/py38' 'LDFLAGS=-Wl,-rpath=/opt/py38/lib/' '--enable-shared'"
	LDFLAGS = "-Wl,-rpath=/opt/py38/lib/"
	LDSHARED = "gcc -pthread -shared -Wl,-rpath=/opt/py38/lib/"
	PY_CORE_LDFLAGS = "-Wl,-rpath=/opt/py38/lib/"
	PY_LDFLAGS = "-Wl,-rpath=/opt/py38/lib/"

Now the difference is how these flags are passed to third party C extensions.

$ cd $HOME
$ /opt/py38/bin/python3.8 -m venv opt_env
$ opt_env/bin/python -m pip install lxml
$ objdump -a -x $(opt_env/bin/python -c 'import lxml.etree; print(lxml.etree.__file__)')|grep -i rpath
  RPATH                /opt/py38/lib/

lxml is compiled with the RPATH. This issue proposes to omit the Python RPATH here.

Comparison with Fedora Python which already contains the change:

$ python3 -m venv fed_venv  # FYI: it's Python 3.7 on Fedora 29
$ fed_venv/bin/python -m pip install lxml
$ objdump -a -x $(fed_venv/bin/python -c 'import lxml.etree; print(lxml.etree.__file__)')|grep -i rpath

^^ empty output: no RPATH, it's the expected behavior

... I'm not sure that the example using /usr/bin/python3.7 is useful, because it's not built using RPATH ...

$ objdump -a -x /usr/bin/python3.7 |grep -i rpath
$ python3.7 -m sysconfig|grep -i rpath
$ objdump -a -x /usr/lib64/libpython3.7m.so |grep -i rpath

^^ no output, it's not built with RPATH
msg340497 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-18 15:09
Output with attached PR 12876:

$ /opt/py38/bin/python3.8 -m venv opt_env
$ opt_env/bin/python -m pip install lxml
$ objdump -a -x $(opt_env/bin/python -c 'import lxml.etree; print(lxml.etree.__file__)')|grep -i rpath
  RPATH                /opt/py38/lib/

... Oops, the RPATH is still here. Maybe I misunderstood the purpose of the change :-(
msg340500 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-18 15:27
My colleague Miro Hrončok points me to:
https://docs.fedoraproject.org/en-US/packaging-guidelines/#_beware_of_rpath

"Any rpath flagged by check-rpaths MUST be removed."

Note: On Linux, "chrpath" tool can be used to read, modify or remove the RPATH of an ELF binary (program or dynamic library).
msg340508 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-18 16:02
Another test, without my PR 12876.

Python compiled without RPATH:

$ ./configure --prefix=/opt/py38
$ make
$ make install
$ /opt/py38/bin/python3.8 -m sysconfig|grep LIBDIR
	LIBDIR = "/opt/py38/lib"

Build lxml manually using "setup.py build_ext --rpath /opt/py38/lib" (which is equal to the Python sysconfig LIBDIR variable):

$ /opt/py38/bin/python3.8 -m venv ~/opt_env
$ wget https://files.pythonhosted.org/packages/7d/29/174d70f303016c58bd790c6c86e6e86a9d18239fac314d55a9b7be501943/lxml-4.3.3.tar.gz
$ tar -xf lxml-4.3.3.tar.gz 
$ cd lxml-4.3.3/
$ LD_LIBRARY_PATH=/opt/py38/lib ~/opt_env/bin/python setup.py build_ext --rpath /opt/py38/lib
$ objdump -a -x build/lib.linux-x86_64-3.8/lxml/etree.cpython-38m-x86_64-linux-gnu.so|grep -i rpath

^^ no output, no RPATH

Hum, distutils removed the RPATH because it's equal to Python sysconfig LIBDIR? Without my PR? Strange.

New try with a different RPATH:

$ rm -rf build
$ ~/opt_env/bin/python setup.py build_ext --rpath /custom/rpath
$ objdump -a -x build/lib.linux-x86_64-3.8/lxml/etree.cpython-38m-x86_64-linux-gnu.so|grep -i rpath
  RUNPATH              /custom/rpath

The RPATH is correctly written in the .so file.

... Now, I'm confused. Is PR 12876 useless? Was distutils already fixed?
msg340709 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-23 10:07
> ... Now, I'm confused. Is PR 12876 useless? Was distutils already fixed?

My test was wrong, objdump sometimes uses "RUNPATH", sometimes "RPATH":

$ objdump -a -x build/lib.linux-x86_64-*/lxml/etree.cpython-*-x86_64-linux-gnu.so|grep -i -E 'rpath|RUNPATH'
  RUNPATH              /opt/py38/lib

Wait, they are not the same. Now I'm confused :-)

More doc about RPATH and RUNPATH:

https://blog.qt.io/blog/2011/10/28/rpath-and-runpath/

"An additional source of confusion is that, depending on your distribution, the -rpath argument in ‘ld’ behaves differently. For some it generates a DT_RPATH and for others DT_RPATH and DT_RUNPATH."

"""
Unless loading object has RUNPATH:
    RPATH of the loading object,
        then the RPATH of its loader (unless it has a RUNPATH), ...,
        until the end of the chain, which is either the executable
        or an object loaded by dlopen
    Unless executable has RUNPATH:
        RPATH of the executable
LD_LIBRARY_PATH
RUNPATH of the loading object
ld.so.cache
default dirs
"""


I'm now also confused by 

UnixCCompiler.runtime_library_dir_option() of distutils.unixccompiler which starts with a long comment:

# XXX Hackish, at the very least.  See Python bug #445902:
# http://sourceforge.net/tracker/index.php
#   ?func=detail&aid=445902&group_id=5470&atid=105470
# Linkers on different platforms need different options to
# specify that directories need to be added to the list of
# directories searched for dependencies when a dynamic library
# is sought.  GCC on GNU systems (Linux, FreeBSD, ...) has to
# be told to pass the -R option through to the linker, whereas
# other compilers and gcc on other systems just know this.
# Other compilers may need something slightly different.  At
# this time, there's no way to determine this information from
# the configuration data stored in the Python installation, so
# we use this hack.

Linux code path:

if self._is_gcc(compiler):
    # gcc on non-GNU systems does not need -Wl, but can
    # use it anyway.  Since distutils has always passed in
    # -Wl whenever gcc was used in the past it is probably
    # safest to keep doing so.
    if sysconfig.get_config_var("GNULD") == "yes":
        # GNU ld needs an extra option to get a RUNPATH
        # instead of just an RPATH.
        return "-Wl,--enable-new-dtags,-R" + dir
    else:
        return "-Wl,-R" + dir
else:
    # No idea how --enable-new-dtags would be passed on to
    # ld if this system was using GNU ld.  Don't know if a
    # system like this even exists.
    return "-R" + dir

When GCC is detected on Linux and GNULD sysconfig variable is equal to "yes", distutils uses "-Wl,--enable-new-dtags,-R <path>" rather than "-Wl,-R <path>" or just "-R <path>".


See also https://wiki.debian.org/RpathIssue
msg340713 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-23 10:58
I'm also confused by the diversity of ways to declare runtime paths. I found these linker flags related to rpath:

* -rpath=PATH
* -rpath-link=PATH
* -R FILENAME, --just-symbols=FILENAME: "Read symbol names and their addresses from FILENAME, (...). For compatibility with other ELF linkers, if the -R option is followed by a directory name, rather than a file name, it is treated as the -rpath option."
* --enable-new-dtags, --disable-new-dtags: "new dynamic tags in ELF"

configure and Makefile manipulates *compiler* flags, not *linker* flags, and so options like "-Wl,-rpath=PATH" is used: "-Wl,OPTION" pas OPTION as an option to the linker.
msg340714 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-23 11:08
See also bpo-445902 which modified distutils to use "-Wl,-R <path>" rather than "-R <path>".
msg340716 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-23 11:26
My understanding of Fedora 00001-rpath.patch is that it ignores -rpath PATH option passed to the setup.py bdist command if PATH is equal to Python sysconfig LIBDIR variable. So it impacts how third party C extensions are built. On my Fedora, LIBDIR is set to:

$ python3 -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))"
/usr/lib64

Only LIBDIR directory is ignored: other -rpath directories are passed to the linker.

---

UnixCCompiler.link() has a runtime_library_dirs parameter which can be modified by the _fix_lib_args() method and then is passed to gen_lib_options().

gen_lib_options() calls compiler.runtime_library_dir_option() on each directory of runtime_library_dirs: this part is responsible to use the proper linker option: -R, -rpath, -L, etc.

build_ext.build_extension() method of distutils.commands.build_ext pass Extension.runtime_library_dirs to compiler.link_shared_object()

build_ext.check_extensions_list() modify the runtime_library_dirs attribute:

    # Medium-easy stuff: same syntax/semantics, different names.
    ext.runtime_library_dirs = build_info.get('rpath')

I understand that build_info.get('rpath') comes from -rpath command line option of setup.py bdist_ext command:

        ('rpath=', 'R',
         "directories to search for shared C libraries at runtime"),
msg340727 - (view) Author: Florian Weimer (fweimer) Date: 2019-04-23 13:53
Note that linking with -Wl,-rpath,/usr/lib64 (even if it's redundant) disables part of the ld.so cache on Linux.  Instead, paths under /usr/lib64 are probed explicitly.  This can add quite a few failing open/openat system calls to the process startup.
msg340728 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-23 14:25
> Since 2010, the Fedora packages of Python are using a patch on distutils UnixCCompiler to remove standard library path from rpath. The patch has been written by David Malcolm for Python 2.6.4: https://src.fedoraproject.org/rpms/python2/c/f5df1f834310948b32407933e3b8713e1121105b

Hum, in fact the initial patch author is: Ignacio Vazquez-Abrams.

The commit message says:

"fixup distutils/unixccompiler.py to remove standard library path from rpath (patch 17)"

the specfile contains this comment:

# Fixup distutils/unixccompiler.py to remove standard library path from rpath:
# Adapted from Patch0 in ivazquez' python3000 specfile, removing usage of
# super() as it's an old-style class

"ivazquez" is Ignacio Vazquez-Abrams who wrote an early version of the Python 3 specfile for Fedora and the rpath patch, but he stopped to contribute to Fedora around 2006. David Malcolm wrote the final specfile using Ignacio's patch.
msg340729 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-23 15:02
I'm confused between RPATH and RUNPATH which are *different*.

When I configure Python using LDFLAGS="-Wl,-rpath=/opt/py38/lib/" (Python configure and Makefile), Python, libpython, and dynamic libraries ELF of C extensions get a RPATH.

When I use setup.py build_ext -rpath (distutils), dynamic libraries ELF of C extensions get a RUNPATH.

More details below.

--

When I build lxml using patched Fedora Python (/usr/bin/python3), I get:

$ python3 setup.py build_ext --rpath /custom/rpath
...
$ objdump -a -x build/lib.linux-x86_64-3.7/lxml/etree.cpython-37m-x86_64-linux-gnu.so|grep -Ei 'rpath|runpath'
  RUNPATH              /custom/rpath

Note: when I use patched Fedora Python, "--rpath /usr/lib64" of "python3 setup.py build_ext --rpath /usr/lib64" is ignored: the .so file doesn't have RPATH nor RUNPATH. That's the purpose of the patch.

--

When I built Python using ./configure --prefix /opt/py38 LDFLAGS="-Wl,-rpath=/opt/py38/lib/" --enable-shared, I got:

$ objdump -a -x /opt/py38/bin/python3.8|grep -i rpath
  RPATH                /opt/py38/lib/
$ objdump -a -x /opt/py38/lib/libpython3.8m.so|grep -i rpath
  RPATH                /opt/py38/lib/

If I build lxml without an explicit rpath, it inherits the Python rpath:

$ LD_LIBRARY_PATH=/opt/py38/lib ./opt_env/bin/python setup.py build_ext 
$ objdump -a -x build/lib.linux-x86_64-3.8/lxml/etree.cpython-38m-x86_64-linux-gnu.so|grep -Ei 'rpath|runpath'
  RPATH                /opt/py38/lib/

If I build lxml with an explicit rpath, I get:

$ LD_LIBRARY_PATH=/opt/py38/lib ./opt_env/bin/python setup.py build_ext --rpath /custom/rpath
$ objdump -a -x build/lib.linux-x86_64-3.8/lxml/etree.cpython-38m-x86_64-linux-gnu.so|grep -Ei 'rpath|runpath'
  RUNPATH              /opt/py38/lib/:/custom/rpath
msg340730 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-23 15:04
In 2015, Robert Kuska proposed to remove the patch from Fedora:
"This check should be covered by rpmlint which MUST be run during the review so I propose to drop this patch."
https://bugzilla.redhat.com/show_bug.cgi?id=1287566#c1

But Toshio Ernie Kuratomi explained that the patch is a practical solution to avoid rpath on /usr/lib64 for all Fedora libraries (built by Python):
"I would recommend against dropping the patch.  Since it's only removing LIBDIR, it's removing an rpath that should never be needed (the dynamic linker will try LIBDIR on its own, no need for an rpath to make it do so).  The patch means that distutils does not add the unneeded rpath to any binaries it generates which is what that Debian page recommends (patch the build system).  If we were to drop the patch, then package maintainers of C extensions would need to add boilerplate to their packages which strips the unnecessary rpath.  Better to do that in a single place (the python package) if we can.

If you do decide to drop rpath, you should also coordinate updating all existing C extension packages to include the boilerplate removing rpath.  Python packaging guidelines could also be updated to mention it (but not strictly necessary -- the rpath guidelines cover this... it would just be keeping the information in one place)."
https://bugzilla.redhat.com/show_bug.cgi?id=1287566#c2
msg340731 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-23 15:18
Ratione from my colleague Carlos O'Donell:
"The intent of this patch is to remove the default search paths of the dynamic loader from DT_RPATH/DT_RUNPATH. This allows the default system paths to be searched last as is expected by most programs, and enables LD_LIBRARY_PATH and cache searches to happen first. The patch does not forbid rpath in any form, but forbids promoting default system search paths in this way."

He added:

If you allow the default library paths in rpath, it bypasses the usual hierarchy of checks -> (a) DT_RPATH -> (b) LD_LIBRARY_PATH -> (c) DT_RUNPATH -> (d) cache -> (e) system defaults. Allowing /usr/lib64 in DT_RPATH moves that path from (e) -> (a). In terms of a search hierarchy.

It's not really the behaviour you want in the [Fedora/RHEL] distribution, you want the system defaults to be at the end of the search order instead of being promoted to early searches. The intent of the patch should be documented as such so we remember why we made the change. It's the same problem as adding /usr/lib64 to LD_LIBRARY_PATH, which on RHEL7 actually has a bug in some cases which can lead to crash.
msg340733 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-23 15:26
To come back to RPATH vs RUNPATH: these 2 are very different in term of priorities in search (see other comments).

distutils uses an heuristic to opt-in for RUNPATH using GNU ld --enable-new-dtags option. This option is only used if the following 3 conditions are met:

(1) the OS is not macOS, not FreeBSD, nor HP-UX
(2) the C compiler is detected as GCC
(3) Python detected the linker as being GNU ld

Problem: the heuristic is not very reliable:

(3) only checks the linker when Python is build, it's implemented as: sysconfig.get_config_var("GNULD") == "yes". In practice, the linker can be changed at runtime using environment variables and so Python may miss the oportunity for opt-in for --enable-new-dtags, or pass this option which is unknown to another linker.

(2) The detection of GCC checks for "gcc" or "g++" string if the name of the compiler program name. If you use "cc", it's not detected as GCC.

I'm not sure that these 2 problems are real problems "in practice".

My main concern is that you might get RPATH or RUNPATH depending on operating system, the C compiler and the linker, whereas RPATH semantics is very different from RUNPATH semantics (search priority).
msg340769 - (view) Author: Matej Cepl (mcepl) * Date: 2019-04-24 11:12
Just to add my CZK 0.02 (or €0,0008 ;)). We don't have this patch in openSUSE Python packages, we run rpmlint on all submissions to our build system (so unwanted rpath shouldn't sneak into the distribution), and yet the only Python C extension which needs RPATH manipulation is cx_Freeze, and that is apparently somehow strange package.
msg340771 - (view) Author: Matej Cepl (mcepl) * Date: 2019-04-24 11:39
OK, vstinner asked me for more explanation:

 * we at openSUSE are as opposed to rpath as Fedora developers (actually, https://en.opensuse.org/openSUSE:Packaging_checks#Beware_of_Rpath is stolen from Fedora wiki, apparently)

 * none of Python packages (python2, python3 in all various *SUSE related distributions) has the mentioned patch

 * in all 11794 SPEC files in openSUSE Factory only 36 packages use chrpath (many other packages either patch ./configure or use various --disable-rpath options), and there is only one Python related package, python-cx_Freeze among them. Its SPEC file is https://build.opensuse.org/package/view_file/devel:languages:python/python-cx_Freeze/python-cx_Freeze.spec?expand=1 , but I would think that is (being a compiler) quite specialized package which may do something strange.

 * We run rpmlint with checks for rpath enabled on all submissions to our build systems, so it shouldn't go unnoticed.

 * generally, it seems to me, that the patch is really unnecessary for us.
msg340839 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2019-04-25 13:16
I discussed the "rpath question" with different people. At the end, I think that the Fedora patch is "temporary fix" which we kept for way too long :-) We should remove this patch and ensure that upstream C extensions respect our rpath policy (Debian, OpenSuse, Fedora are against the usage of rpath).

I discussed with Miro Hrončok who is part of FESco (Fedora Engineering Steering Committee) and he created an issue to propose to make the build of a package *fail* if it uses rpath:
https://pagure.io/packaging-committee/issue/886

That change would be affect the whole Fedora project, not just python packages. IMHO it's a better approach than trying to exclude rpath for LIBDIR which is equal to /usr/lib64.

For these reasons, I close the issue and we will change Fedora instead.

Thanks everybody who was involved in this discussion, I learnt a lot! :-)
History
Date User Action Args
2019-04-25 13:16:58vstinnersetstatus: open -> closed
resolution: fixed
messages: + msg340839

stage: patch review -> resolved
2019-04-24 11:39:46mceplsetmessages: + msg340771
2019-04-24 11:12:49mceplsetnosy: + mcepl
messages: + msg340769
2019-04-23 15:26:25vstinnersetmessages: + msg340733
2019-04-23 15:18:05vstinnersetmessages: + msg340731
2019-04-23 15:04:36vstinnersetmessages: + msg340730
2019-04-23 15:02:44vstinnersetmessages: + msg340729
2019-04-23 14:32:38cstrataksetnosy: + cstratak
2019-04-23 14:25:41vstinnersetmessages: + msg340728
2019-04-23 13:53:34fweimersetmessages: + msg340727
2019-04-23 13:09:46fweimersetnosy: + fweimer
2019-04-23 11:34:33koobssetnosy: + koobs
2019-04-23 11:26:08vstinnersetmessages: + msg340716
2019-04-23 11:08:08vstinnersetmessages: + msg340714
2019-04-23 10:58:36vstinnersetmessages: + msg340713
2019-04-23 10:07:03vstinnersetmessages: + msg340709
2019-04-18 16:02:57vstinnersetmessages: + msg340508
2019-04-18 15:27:26vstinnersetmessages: + msg340500
2019-04-18 15:09:21vstinnersetmessages: + msg340497
2019-04-18 15:09:00vstinnersetkeywords: + patch
stage: patch review
pull_requests: + pull_request12801
2019-04-18 15:02:16vstinnercreate