Two libtool issues

Abstract

GNU libtool is a generic library support script used by many software packages as a portable interface for creating shared libraries. In contrast to the conventional approach where the build process results in binary programs and libraries being created, libtool produces, along with each binary, a wrapper script which is used to invoke that binary from the source tree. The wrapper invokes the binary in such a way as to guarantee that shared objects from the local source tree are preferred over those already installed in the system.

It has been discovered that under certain conditions libtool creates wrappers that prefer installed versions of the shared objects over those built in the source tree. As a result, any tests run in the source tree produce unreliable results.

This article describes two such cases. Minimal test suites are provided. Patches for libtool are proposed, both for the current stable version and the recent commit in the repository.

History

The first version of this article was published on December 9, 2019. The fixes were accepted by the libtool team in August 2024.

The issues were finally fixed in Libtool version 2.5.3, released on September 25, 2024.

The status if this article is historic.

Issue 1 (LTB1)

Synopsis

Imagine a project consisting of at least one installable shared library (libltb1) and an installable binary program (ltb1), which is linked with that library. If the ‘ltb1_LDFLAGS’ variable contains one or more -Wl,-rpath linker options, the created wrapper script will prefer directories listed in them over locations in the build tree. If a previous version of the libltb1 library is already installed in one of these directories, the ltb1 wrapper will use the installed version of the library, instead of the one from the source tree. This leads to undesirable consequences, e.g. if the package provides a test suite, it will likely fail, since it will use the old library version.

Background

The issue was observed when building GNU mailutils with MIT Kerberos. The Mailutils configure script uses the krb5-config tool to obtain the linker options needed for linking with the library. The tool is invoked as ‘krb5-config --libs’ and its output is then added to the ‘*_LDADD’ variable of several Makefile.am’s via the substitution variable. The output in question looks like:

  $ krb5-config --libs
  -L/usr/lib64 -Wl,--enable-new-dtags -Wl,-rpath -Wl,/usr/lib64 -lkrb5 -lk5crypto -lcom_err

When the -Wl,-rpath option makes its way to the ‘*_LDADD’ variable, the wrapper scripts created for mailutils programs use libraries from the prior version of mailutils (installed in /usr/lib64).

The problem is attested on GNU/Linux and FreeBSD systems (see affected systems).

The test case

The problem is illustrated by a testcase available for download from ltbug-1.tar.gz. It consists of the following principal parts:

Testing

Unpack ltbug-1.tar.gz and run sh ltbug-1/test.sh. This test script does the following:

  1. Creates workspace and installation directory.

    Workspace directory ltb-test1 is created in the current working directory. The installation directory with the same name is created in /tmp.

  2. Configures the package with a prominent identifier string.

    The identifier string ‘installed’ will be printed by the ltb_version function if called from the installed version of the library.

  3. Installs it to the installation directory.
  4. Configures the package with another identifier and builds it.

    The identifier string ‘local’ will be printed by the ltb_version function if the library from the local source tree is used.

  5. Runs src/lbt1.

The test produces the following output:

Building and installing project
Building local copy of the project
Running test program
Test failed:
--- expout      2019-12-13 11:46:13.489649518 +0200
+++ stdout      2019-12-13 11:46:13.517649520 +0200
@ -1 +1 @
-ltbug: local
+ltbug: installed
Keeping the directories in place.  When no longer needed, run:
rm -rf /tmp/ltb-test1
rm -rf ltb-test1

This shows that the installed copy of the library is used when the local wrapper src/ltb1 is run.

The test script leaves two directories for examination: the installation prefix directory /tmp/ltb-test1, and working directory ltb-test1. The latter contains the following files and directories:

build_inst

A directory where the installed version of the package was built.

build_local

Local build directory. The test runs build_local/src/ltb1.

expout

Expected standard output.

stdout

Obtained standard output.

ltb2.out

Standard output generated by the build process.

ltb2.err

Standard output generated by the build process.

The test script can be used with the following command line options:

-p dir
--prefix=dir

Set root directory for the installation prefix. The prefix directory is dir/ltb-test1.

-C dir
--directory=dir

Set test working directory root. Default is current working directory. The test directory name is constructed as dir/ltb-test1.

--keep

Keep test and installation directories for inspection. This is the default if the test fails.

--help

Display short usage summary.

Issue 2 (LTB2)

Synopsis

Let a project consist of two installable libraries and a binary (ltb2) linked to both of them. One of the libraries depends on an external library, which has a libtool archive installed along with the shared library. The figure below illustrates this scheme:

  ltb2  <---+-- liba <==== libdir/libdep.la
            |
            +-- libb

Then, for GNU/Linux systems the wrapper script would issue the relink command that ends with the following options (split along multiple lines for readability):

-Wl,-rpath -Wl,srcdir/liba/.libs
-Wl,-rpath -Wl,libdir
-Wl,-rpath -Wl,srcdir/libb/.libs
-Wl,-rpath -Wl,libdir

For FreeBSD, the LD_LIBRARY_PATH used by the wrapper will look like:

srcdir/liba/.libs:libdir:srcdir/libb/.libs:$LD_LIBRARY_PATH

In both cases, the libdir directory appearing before libb will cause the libb library to be searched in libdir first. If a prior version of the project is installed in the same prefix as the libdep library, then the ltb2 wrapper will use the version of liba from the local source tree, but the version of libb installed on the system.

Background

The issue was initially discovered when building GNU mailutils on FreeBSD with support for OpenLDAP enabled. The latter incurred dependency on libldap, which was installed (along with libldap.la) in /usr/local/lib.

The package built in these conditions failed most tests because it was using mailutils libraries from prior version, installed in /usr/local/lib.

The issue has been confirmed to exist on GNU/Linux as well (see affected systems).

The test case

A minimal testcase is available for download from ltbug-2.tar.gz. It consists of two subpackages: ltb2dep implementing the dependency library, and the main test package ltb2. The latter builds and installs two libraries: liba and libb, and a test program ltb2, linked with both of these. The liba library provides the function ‘ltb2a_version’. The libb library implements a similar function ‘ltb2b_version’. Both functions print on the standard output the value of the ‘LTBUG_ID’ macro defined at the compile time. The ltb2 program calls both functions in order.

The test script first builds and installs the dependency library ltb2dep. Then, it builds the package with LTBUG_ID="installed" and installs it. When run, the installed ltb2 program will output

ltb2a: installed
ltb2b: installed

Finally, the test rebuilds the package with LTBUG_ID="local" and runs the ltb2 program from the source tree. The program then outputs:

ltb2a: local
ltb2b: installed

This shows that the command uses libltb2a from the local source tree and libltb2b previously installed on the system.

Follow these steps to run the test:

  1. Unpack the archive
    tar xf ltbug-2.tar.gz
    
  2. Run
    sh ltbug-2/test.sh
    

    or

    make -C ltbug-2 check
    

If the bug is present, you will see the following output:

Building dependency library
Building and installing project
Building local copy of the project
Running test program
Test failed:
--- expout      2019-12-12 13:45:53.193782824 +0200
+++ stdout      2019-12-12 13:45:53.222782826 +0200
@ -1,2 +1,2 @
 ltb2a: local
-ltb2b: local
+ltb2b: installed
Keeping the directories in place.  When no longer needed, run:
rm -rf /tmp/ltb-test2
rm -rf ltb-test2
make: *** [check] Error 1

The directory ltb-test2 will be left on disk for examination. It will contain:

build_dep

Build directory for the dependency library libltb2dep.

build_inst

A directory where the installed version of the package was built.

build_local

Local build directory. The test runs build_local/src/ltb2.

expout

Expected standard output.

stdout

Obtained standard output.

ltb2.out

Standard output generated by the build process.

ltb2.err

Standard output generated by the build process.

The test script takes the same command line options as described above for the ltb1 test.

Affected Systems

The following systems are known to be affected:

Proposed Fixes

Patches are available both for the current stable and for the repository version of GNU libtool. Both patched versions successfully pass libtool tests as well as the ltb1 and ltb2 tests.

Patches for libtool git HEAD

These patches were built over commit b9b44533fb. At the time of this writing (2024-06-11), they apply cleanly (with offset 28 lines) to version 2.4.7 (commit b9b44533fb).

  1. 0001-ltmain.in-append-rpath-option-arguments-to-finalize_.patch
  2. 0002-ltmain.in-ensure-that-local-source-tree-directories-.patch

The patches should be applied in order.

Patch for libtool version 2.4.6

Cumulative patch: libtool-2.4.6-ltb1-2.patch.
Patch for ltmain.sh: ltmain.sh-2.4.6.patch

Patch for libtool version 2.4.7

Cumulative patch: libtool-2.4.7-ltb1-2.patch.
Patch for ltmain.sh: ltmain.sh-2.4.7.patch

Libtool version 2.5.3 includes the fixes