Subject: Libtool incorrectly tries to use shared library when linking statically

From: Mike Krinkin <[email-masked]>
Date: Fri, 25 Apr 2025 19:53:01 +0100

Hi There,

I hit a weird behaviour when build colm as part of building Envoy proxy.
Cutting through layers of the stack on top of the libtool things here is
what things boil down to:

1. we have a project using autogen/configure/libtool that was configured
with `--enable-static` and `--disable-shared`
2. when I run make on the project one of the linking steps failed with the
following error:

```
ld.lld: error: attempted static link of dynamic object /usr/lib/libstdc++.so
```

Looking at the debug logs for the libtool invocations here is what I see:

```
/bin/sh ../libtool
  --tag=CXX
  --mode=link
  /usr/bin/clang-18
  -Wall

-DINCLUDEDIR='"/root/.cache/bazel/_bazel_root/37e3aec351bcd85a6ea8b58e3592ef6e/sandbox/processwrapper-sandbox/3/execroot/envoy/bazel-out/k8-opt/bin/bazel/foreign_cc/colm.build_tmpdir/colm/include"'

-DLIBDIR='"/root/.cache/bazel/_bazel_root/37e3aec351bcd85a6ea8b58e3592ef6e/sandbox/processwrapper-sandbox/3/execroot/envoy/bazel-out/k8-opt/bin/bazel/foreign_cc/colm.build_tmpdir/colm/lib"'

-DABS_TOP_BUILDDIR='"/root/.cache/bazel/_bazel_root/37e3aec351bcd85a6ea8b58e3592ef6e/sandbox/processwrapper-sandbox/3/execroot/envoy/bazel-out/k8-opt/bin/bazel/foreign_cc/colm.build_tmpdir"'

-DABS_BUILDDIR='"/root/.cache/bazel/_bazel_root/37e3aec351bcd85a6ea8b58e3592ef6e/sandbox/processwrapper-sandbox/3/execroot/envoy/bazel-out/k8-opt/bin/bazel/foreign_cc/colm.build_tmpdir/src"'
  -DCONS_INIT
  -U_FORTIFY_SOURCE
  -fstack-protector
  -Wall
  -Wthread-safety
  -Wself-assign
  -Wunused-but-set-parameter
  -Wno-free-nonheap-object
  -fcolor-diagnostics
  -fno-omit-frame-pointer
  -g0
  -O2
  -D_FORTIFY_SOURCE=1
  -DNDEBUG
  -ffunction-sections
  -fdata-sections
  -stdlib=libc++
  -no-canonical-prefixes
  -Wno-builtin-macro-redefined
  -D__DATE__=redacted
  -D__TIMESTAMP__=redacted
  -D__TIME__=redacted
  -DABSL_MIN_LOG_LEVEL=4
  -fdebug-types-section
  -fPIC
  -Wno-deprecated-declarations
  -std=c++20 -fsized-deallocation
  --static
  -lstdc++
  -Wno-unused-command-line-argument
  -L.
  -Wl,--gdb-index
  -fuse-ld=/usr/bin/ld.lld
  -B/usr/bin
  -Wl,-no-as-needed
  -Wl,-z,relro,-z,now
  -lm
  -pthread
  -Wl,--gc-sections
  -l:libc++.a
  -l:libc++abi.a
  -fuse-ld=lld
  -o bootstrap0 bootstrap0-consinit.o bootstrap0-main.o libprog.a libcolm.la
```

NOTE: I split it in multiple lines to make it easier to read, originally it
was a single line.

There is a lot there, but two things that I believe are particularly
relevant are: `--static` and `-lstdc++` flags.
I believe that libtool next translated it into an actual command to link
the binary and from the logs here is what I saw:

```
/usr/bin/clang-18
  -Wall

-DINCLUDEDIR=\"/root/.cache/bazel/_bazel_root/37e3aec351bcd85a6ea8b58e3592ef6e/sandbox/processwrapper-sandbox/3/execroot/envoy/bazel-out/k8-opt/bin/bazel/foreign_cc/colm.build_tmpdir/colm/include\"

-DLIBDIR=\"/root/.cache/bazel/_bazel_root/37e3aec351bcd85a6ea8b58e3592ef6e/sandbox/processwrapper-sandbox/3/execroot/envoy/bazel-out/k8-opt/bin/bazel/foreign_cc/colm.build_tmpdir/colm/lib\"

-DABS_TOP_BUILDDIR=\"/root/.cache/bazel/_bazel_root/37e3aec351bcd85a6ea8b58e3592ef6e/sandbox/processwrapper-sandbox/3/execroot/envoy/bazel-out/k8-opt/bin/bazel/foreign_cc/colm.build_tmpdir\"

-DABS_BUILDDIR=\"/root/.cache/bazel/_bazel_root/37e3aec351bcd85a6ea8b58e3592ef6e/sandbox/processwrapper-sandbox/3/execroot/envoy/bazel-out/k8-opt/bin/bazel/foreign_cc/colm.build_tmpdir/src\"
  -DCONS_INIT
  -U_FORTIFY_SOURCE
  -fstack-protector
  -Wall
  -Wthread-safety
  -Wself-assign
  -Wunused-but-set-parameter
  -Wno-free-nonheap-object
  -fcolor-diagnostics
  -fno-omit-frame-pointer
  -g0
  -O2
  -D_FORTIFY_SOURCE=1
  -DNDEBUG
  -ffunction-sections
  -fdata-sections
  -stdlib=libc++
  -no-canonical-prefixes
  -Wno-builtin-macro-redefined
  -D__DATE__=redacted
  -D__TIMESTAMP__=redacted
  -D__TIME__=redacted
  -DABSL_MIN_LOG_LEVEL=4
  -fdebug-types-section
  -fPIC
  -Wno-deprecated-declarations
  -std=c++20
  -fsized-deallocation
  --static
  -Wno-unused-command-line-argument
  -Wl,--gdb-index
  -fuse-ld=/usr/bin/ld.lld
  -B/usr/bin
  -Wl,-no-as-needed
  -Wl,-z
  -Wl,relro
  -Wl,-z
  -Wl,now
  -Wl,--gc-sections
  -fuse-ld=lld
  -o bootstrap0 bootstrap0-consinit.o bootstrap0-main.o
 /usr/lib/libstdc++.so
  -L. libprog.a
  ./.libs/libcolm.a
  -lm
  -l:libc++.a
  -l:libc++abi.a
  -pthread
```

It's pretty close to what we asked libtool to do with a few differences: it
does not have -lstdc++ flag and instead passes /usr/lib/libstdc++.so along
with other object files as input to the linker which obviously does not
work.

My understanding is that libtool looked at -lstdc++ flag, found a
corresponding libstdc++.la file and "resolved" it into a shared library.

What I'm baffled by in this case is why did libtool resolve it to a shared
library instead of a static one, even though it's clearly a static build?
Moreover, even if we wanted to link against the dynamic library, it should
be done via -l flag, so what libtool looks very weird - we can't just link
shared library as if it was a object file or a static library.
Am I missing something or libtool is completely off the mark here and
trying to do something weird?

Both shared and static libraries are present (and in the right directory)
and la file looks reasonable otherwise:

```
# libstdc++.la - a libtool library file
# Generated by libtool (GNU libtool 1.3134 2009-11-29) 2.2.7a
#
# Please DO NOT delete this file!
# It is necessary for linking the library.

# The name that we can dlopen(3).
dlname='libstdc++.so.6'

# Names of this library.
library_names='libstdc++.so.6.0.32 libstdc++.so.6 libstdc++.so'

# The name of the static archive.
old_library='libstdc++.a'

# Linker flags that can not go in dependency_libs.
inherited_linker_flags=''

# Libraries that this one depends upon.
dependency_libs=' -lm'

# Names of additional weak libraries provided by this library
weak_library_names=''

# Version information for libstdc++.
current=6
age=0
revision=32

# Is this an already installed library?
installed=yes

# Should we warn about portability when linking against -modules?
shouldnotlink=no

# Files to dlopen/dlpreopen
dlopen=''
dlpreopen=''

# Directory that this library needs to be installed in:
libdir='/usr/lib'
```

If I just deleate the .la file all together, it skips the libtool siliness
and things just work (that's how we are working around the issue now).

Thank you.