llvm / llvm-project

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
http://llvm.org
Other
29.25k stars 12.08k forks source link

dynamic_cast to final class from thinLTO shared library incorrectly returns nullptr #71196

Open nbronson opened 1 year ago

nbronson commented 1 year ago

If a polymorphic subclass is defined only in a header file, shared libraries built with -flto=thin give the vtable symbol LOCAL visibility. This means that there may be more than one vtable in a binary that uses this shared library. The dynamic_cast optimizations in llvm 17's libcxxapi (https://reviews.llvm.org/D138005) don't seem to expect that this can happen for final classes, causing dynamic_cast to incorrectly return nullptr.

To repro: a.h

struct Foo {
  virtual ~Foo();
};

struct Bar final : public Foo {
};

Foo* makeBar();

a.cc

#include "a.h"

Foo::~Foo() {}

Foo* makeBar() {
  return new Bar();
}

b.cc

#include <cstdio>
#include <cstring>

#include "a.h"

int main(int argc, char **argv) {
  Foo* f = makeBar();
  Bar* b = dynamic_cast<Bar*>(f); // b will incorrectly be nullptr
  void* v;
  memcpy(&v, static_cast<void*>(f), sizeof(v));
  printf("%p %p %p\n", f, b, v);
  delete f;

  f = new Bar();
  asm volatile ("":"+r"(f));
  b = dynamic_cast<Bar*>(f);
  memcpy(&v, static_cast<void*>(f), sizeof(v));
  printf("%p %p %p\n", f, b, v);
  delete f;

  return 0;
}

Compile with

clang++-17 -flto=thin -O3 -fPIC -c -o a.o a.cc
clang++-17 -O3 -fPIC -flto=thin -shared -o liba.so a.o
clang++-17 -O3 -o b b.cc -Wl,-rpath,$PWD liba.so

When run, the first dynamic_cast returns nullptr

$ ./b
0x55b5b3dc12b0 (nil) 0x7f555c037dc8
0x55b5b3dc12b0 0x55b5b3dc12b0 0x55b5b337dd80

Changing the final clang invocation to clang++-16 avoids the issue.

readelf shows that Bar's vtable gets LOCAL visibility (in both clang 16 and 17) when -flto=thin is used, but WEAK in the default case

$ readelf -Wl --syms liba.so | c++filt | grep 'for Bar'
    11: 0000000000002005     5 OBJECT  WEAK   DEFAULT   14 typeinfo name for Bar
    14: 0000000000003dd8    24 OBJECT  WEAK   DEFAULT   19 typeinfo for Bar
    35: 0000000000003db8    32 OBJECT  LOCAL  DEFAULT   19 vtable for Bar
    49: 0000000000002005     5 OBJECT  WEAK   DEFAULT   14 typeinfo name for Bar
    57: 0000000000003dd8    24 OBJECT  WEAK   DEFAULT   19 typeinfo for Bar
$ clang++-17 -O3 -fPIC -c -o a.o a.cc && clang++-17 -O3 -fPIC -shared -o liba_no_lto.so a.o
$ readelf -Wl --syms liba_no_lto.so | c++filt | grep 'for Bar'
    11: 0000000000002005     5 OBJECT  WEAK   DEFAULT   14 typeinfo name for Bar
    13: 0000000000003db0    32 OBJECT  WEAK   DEFAULT   19 vtable for Bar
    15: 0000000000003dd0    24 OBJECT  WEAK   DEFAULT   19 typeinfo for Bar
    47: 0000000000002005     5 OBJECT  WEAK   DEFAULT   14 typeinfo name for Bar
    55: 0000000000003db0    32 OBJECT  WEAK   DEFAULT   19 vtable for Bar
    57: 0000000000003dd0    24 OBJECT  WEAK   DEFAULT   19 typeinfo for Bar

I am using Ubuntu clang version 17.0.4 (++20231025123955+afbe3549af4d-1~exp1~20231025124009.57)

nbronson commented 1 year ago

It seems that a workaround is to add -fno-assume-unique-vtables when compiling the executable.