Skip to content

[analyzer] New FP with libcxx unique_ptr which uses empty base class optimization #157467

@steakhal

Description

@steakhal

With clang-20, a new FP appeared with libcxx (aka. clang stdlib), which in some configuration uses empty base classes in the unique_ptr implementation.

Empty base class optimization allows simulating [[no_unique_address]] in earlier C++ versions, before C++20.
https://en.cppreference.com/w/cpp/language/ebo.html

The unique_ptr has essentially two members, the pointer to the heap and the deleter.
If the deleter is empty, then due to the compressed_pair implementation class, the deleter will be at the same byte offset as the pointer.
In the engine, we model CXXConstructExpr of the compressed_pair would first initialize the pointer member, then would go and initialize the deleter which will clobber the just set value, and losing the reference to the HeapRegion, surfacing the FP leak diagnostic.

Reproducer code: https://godbolt.org/z/hnjnPnojW

// clang --analyze test.cpp
template <class T, int Idx, bool CanBeEmptyBase = __is_empty(T) && (!__is_final(T))>
struct compressed_pair_elem {
  explicit compressed_pair_elem(T u) : value(u) {}
  T value;
};

template <class T, int Idx>
struct compressed_pair_elem<T, Idx, /*CanBeEmptyBase=*/true> : T {
  explicit compressed_pair_elem(T u) : T(u) {}
};

template <class T1, class T2, class Base1 = compressed_pair_elem<T1, 0>, class Base2 = compressed_pair_elem<T2, 1>>
struct compressed_pair : Base1, Base2 {
  explicit compressed_pair(T1 t1, T2 t2) : Base1(t1), Base2(t2) {}
};

template <class T>
struct default_delete {
  void operator()(T* p) {
    delete p;
  }
};

template <class T, class Deleter = default_delete<T> >
struct some_unique_ptr {
  compressed_pair<int*, Deleter> ptr;
  some_unique_ptr(int* p, Deleter d) : ptr(p, d) {}
  ~some_unique_ptr();
};

void entry_point() {
  some_unique_ptr<int, default_delete<int> > u3(new int(12), default_delete<int>());
}

I had to rename the unique_ptr class to avoid the automatic suppression implemented by #152751.
Remember, no matter of that suppression, clobbering the overlapped subobject with a subsequent write (in case of the empty base object optimization) is still wrong!

This regression was introduced by 0dfae06 Nov 15, 2024 in #115918 by me.
The regression is present in clang-20, clang-21, and on trunk right now.

The motivation of that patch was to make all struct objects behave the same way w.r.t. copy and initialization operations no matter if they have non-static data members or not (aka. empty). However, I did not think about empty base classes and their implication.
So there is no real benefit of fighting for this behavior, so the simplest would be to revert it.

One of the fallout patches was reported in #137252, which impacted the [[no_unique_address]] implementation of libcxx unique_ptr (probably coming from the unstable ABI variant?), thus it's different than what I observed here with empty base classes, albeit that's also derived from libcxx.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions