Bug 87520 - [8 Regression] ODR violations in std::make_shared when mixing -fno-rtti and -frtti
Summary: [8 Regression] ODR violations in std::make_shared when mixing -fno-rtti and -...
Status: RESOLVED FIXED
Alias: None
Product: gcc
Classification: Unclassified
Component: libstdc++ (show other bugs)
Version: 8.2.1
: P3 normal
Target Milestone: 8.3
Assignee: Jonathan Wakely
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2018-10-04 13:39 UTC by Jonathan Wakely
Modified: 2019-01-21 13:29 UTC (History)
3 users (show)

See Also:
Host:
Target:
Build:
Known to work:
Known to fail: 8.2.0
Last reconfirmed: 2018-10-04 00:00:00


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Jonathan Wakely 2018-10-04 13:39:51 UTC
cat > main.cc << EOT
#include <memory>

extern int f();

// For the purposes of this demo, force a non-weak definition of
// _Sp_counted_ptr_inplace::_M_get_deleter(const type_info&) to be emitted
// in this translation unit:
template class
  std::_Sp_counted_ptr_inplace<int, std::allocator<int>, std::_S_atomic>;

int main()
{
  return f();
}
EOT

cat > sp.cc << EOT
#include <memory>

int f()
{
  return *std::make_shared<int>();
}
EOT

g++ -fno-rtti main.cc -c -g
g++ -frtti sp.cc -c -g
g++ main.o sp.o
./a.out


This crashes:

Segmentation fault (core dumped)

The problem is that the definition of _Sp_counter_ptr_inplace::_M_get_deleter that the linker chooses was compiled with -fno-rtti but the caller was compiled with -frtti. The caller passes typeid(_Sp_make_shared_tag) which does not match the expected value in _M_get_deleter, so a null pointer is returned. That leaves the new shared_ptr holding a null pointer, so dereferencing it in f() crashes.

This crash happens for all versions.

A nastier failure happens with gcc-8 and trunk if we swap which object is built with RTTI and which without:

g++ -frtti main.cc -c -g
g++ -fno-rtti sp.cc -c -g
g++ main.o sp.o
./a.out


This crashes earlier, during construction of the shared_ptr:


Program received signal SIGSEGV, Segmentation fault.
0x00000000004008cb in std::type_info::operator== (this=0x401700 <std::_Sp_make_shared_tag::_S_ti()::__tag>, __arg=...) at /home/jwakely/gcc/8/include/c++/8.2.1/typeinfo:123
(gdb) bt
#0  0x00000000004008cb in std::type_info::operator== (this=0x401700 <std::_Sp_make_shared_tag::_S_ti()::__tag>, __arg=...) at /home/jwakely/gcc/8/include/c++/8.2.1/typeinfo:123
#1  0x0000000000400a31 in std::_Sp_counted_ptr_inplace<int, std::allocator<int>, (__gnu_cxx::_Lock_policy)2>::_M_get_deleter (this=0x615e70,  type_info node=...) at /home/jwakely/gcc/8/include/c++/8.2.1/bits/shared_ptr_base.h:569
#2  0x0000000000401264 in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::_M_get_deleter (this=0x7fffffffd858,  type_info node=...) at /home/jwakely/gcc/8/include/c++/8.2.1/bits/shared_ptr_base.h:749
#3  0x00000000004010ba in std::__shared_ptr<int, (__gnu_cxx::_Lock_policy)2>::__shared_ptr<std::allocator<int>>(std::_Sp_make_shared_tag, std::allocator<int> const&) (this=0x7fffffffd850, __tag=..., __a=...) at /home/jwakely/gcc/8/include/c++/8.2.1/bits/shared_ptr_base.h:1329
#4  0x0000000000401055 in std::shared_ptr<int>::shared_ptr<std::allocator<int>>(std::_Sp_make_shared_tag, std::allocator<int> const&) (this=0x7fffffffd850, __tag=..., __a=...) at /home/jwakely/gcc/8/include/c++/8.2.1/bits/shared_ptr.h:360
#5  0x0000000000400f8a in std::allocate_shared<int, std::allocator<int>>(std::allocator<int> const&) (__a=...) at /home/jwakely/gcc/8/include/c++/8.2.1/bits/shared_ptr.h:707
#6  0x0000000000400ed5 in std::make_shared<int> () at /home/jwakely/gcc/8/include/c++/8.2.1/bits/shared_ptr.h:723
#7  0x0000000000400d80 in f () at sp.cc:5
#8  0x000000000040089b in main () at main.cc:13


This is due to the fix for PR 80285. The __shared_ptr constructor is built without RTTI so calls _M_get_deleter(_Sp_make_shared_tag::_S_ti()). The definition of _M_get_deleter is built with RTTI so expects a real typeinfo object. When accessing the fake _S_ti() typeinfo object we crash, because it's an invalid reference that isn't bound to a real typeinfo.
Comment 1 Jonathan Wakely 2018-10-04 13:59:22 UTC
A possible (partial) fix would be:

--- a/libstdc++-v3/include/bits/shared_ptr_base.h
+++ b/libstdc++-v3/include/bits/shared_ptr_base.h
@@ -500,7 +500,6 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
 
   struct _Sp_make_shared_tag
   {
-#if !__cpp_rtti
   private:
     template<typename _Tp, _Lock_policy _Lp>
       friend class __shared_ptr;
@@ -513,7 +512,6 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
       alignas(type_info) static constexpr _Sp_make_shared_tag __tag;
       return reinterpret_cast<const type_info&>(__tag);
     }
-#endif
   };
 
   template<typename _Tp, typename _Alloc, _Lock_policy _Lp>
@@ -562,16 +560,19 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
        this->~_Sp_counted_ptr_inplace();
       }
 
-      // Sneaky trick so __shared_ptr can get the managed pointer
+      // Sneaky trick so __shared_ptr can get the managed pointer.
       virtual void*
-      _M_get_deleter(const std::type_info& __ti) noexcept
+      _M_get_deleter(const std::type_info& __ti) noexcept override
       {
-#if __cpp_rtti
-       if (__ti == typeid(_Sp_make_shared_tag))
-#else
+       // Check for the fake type_info first, so we don't try to access it
+       // as a real type_info object.
        if (&__ti == &_Sp_make_shared_tag::_S_ti())
-#endif
          return const_cast<typename remove_cv<_Tp>::type*>(_M_ptr());
+#if __cpp_rtti
+       // Callers compiled with RTTI enabled pass this instead:
+       else if (__ti == typeid(_Sp_make_shared_tag))
+         return const_cast<typename remove_cv<_Tp>::type*>(_M_ptr());
+#endif
        return nullptr;
       }
 
@@ -1323,11 +1324,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
        {
          // _M_ptr needs to point to the newly constructed object.
          // This relies on _Sp_counted_ptr_inplace::_M_get_deleter.
-#if __cpp_rtti
-         void* __p = _M_refcount._M_get_deleter(typeid(__tag));
-#else
          void* __p = _M_refcount._M_get_deleter(_Sp_make_shared_tag::_S_ti());
-#endif
          _M_ptr = static_cast<_Tp*>(__p);
          _M_enable_shared_from_this_with(_M_ptr);
        }


This always passes the fake _S_ti() even when rtti is enabled. Inside _M_get_deleter we check for the fake tag first, and so avoid trying to dereference it as a real typeinfo object.

Old code already built with rtti could still call the function with typeid(_Sp_make_shared_tag) and so we still need to check for that (which will still fail and return a null pointer if the definition of _M_get_deleter is built without rtti).

A more complete fix would be to avoid using the virtual _M_get_deleter backchannel entirely, as suggested in PR 87514 (that would need to be done in addition to the patch above, so that existing callers of the virtual function will still be able to use it).
Comment 2 Eric Gallager 2018-10-04 15:48:33 UTC
Related to bug 43105
Comment 3 Axel Naumann 2018-10-19 14:02:32 UTC
(In reply to Jonathan Wakely from comment #1)
> A possible (partial) fix would be:
> 
> --- a/libstdc++-v3/include/bits/shared_ptr_base.h
> +++ b/libstdc++-v3/include/bits/shared_ptr_base.h

I can confirm that this also fixes our actual "in the wild" issue. I would certainly appreciate this being included in 8.3. Thank you for the reproducer and the suggested patch, Jonathan!
Comment 4 Jonathan Wakely 2018-11-09 18:29:41 UTC
(In reply to Jonathan Wakely from comment #1)
> @@ -513,7 +512,6 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>        alignas(type_info) static constexpr _Sp_make_shared_tag __tag;

This needs to be fixed to have the right size as well as alignment, so the reference binds to suitable storage (as per core DR 453).
Comment 5 Jonathan Wakely 2018-11-22 13:43:13 UTC
Author: redi
Date: Thu Nov 22 13:42:39 2018
New Revision: 266376

URL: https://gcc.gnu.org/viewcvs?rev=266376&root=gcc&view=rev
Log:
PR libstdc++/87520 Always pass type-punned type_info reference

The implementations of std::make_shared for -frtti and -fno-rtti are not
compatible, because they pass different arguments to
_Sp_counted_ptr_inplace::_M_get_deleter and so can't interoperate.
Either the argument doesn't match the expected value, and so the
shared_ptr::_M_ptr member is never set, or the type-punned reference is
treated as a real std::type_info object and gets dereferenced.

This patch removes the differences between -frtti and -fno-rtti, so that
typeid is never used, and the type-punned reference is used in both
cases. For backwards compatibility with existing code that passes
typeid(_Sp_make_shared_tag) that still needs to be handled, but only
after checking that the argument is not the type-punned reference (so
it's safe to treat as a real std::type_info object). The reference is
bound to an object of literal type, so that it doesn't need a guard
variable to make its initialization thread-safe.

This patch also fixes 87520 by ensuring that the type-punned reference
is bound to "a region of storage of suitable size and alignment to
contain an object of the reference's type" (as per the proposed
resolution of Core DR 453).

If all objects are built with the fixed version of GCC then -frtti and
-fno-rtti can be mixed freely and std::make_shared will work correctly.
If some objects are built with unfixed GCC versions then problems can
still arise, depending on which template instantiations are kept by the
linker.

	PR libstdc++/85930
	PR libstdc++/87520
	* include/bits/shared_ptr_base.h (_Sp_make_shared_tag::_S_ti)
	[__cpp_rtti]: Define even when RTTI is enabled. Use array of
	sizeof(type_info) so that type-punned reference binds to an object
	of the correct size as well as correct alignment.
	(_Sp_counted_ptr_inplace::_M_get_deleter) [__cpp_rtti]: Check for
	_S_ti() reference even when RTTI is enabled.
	(__shared_ptr(_Sp_make_shared_tag, const _Alloc&, _Args&&...))
	[__cpp_rtti]: Pass _S_ti() instead of typeid(_Sp_make_shared_tag).

Modified:
    trunk/libstdc++-v3/ChangeLog
    trunk/libstdc++-v3/include/bits/shared_ptr_base.h
Comment 6 Jonathan Wakely 2018-11-22 13:43:54 UTC
Fixed on trunk so far.
Comment 7 Jonathan Wakely 2018-11-22 15:03:18 UTC
Author: redi
Date: Thu Nov 22 15:02:46 2018
New Revision: 266380

URL: https://gcc.gnu.org/viewcvs?rev=266380&root=gcc&view=rev
Log:
PR libstdc++/87520 Always pass type-punned type_info reference

The implementations of std::make_shared for -frtti and -fno-rtti are not
compatible, because they pass different arguments to
_Sp_counted_ptr_inplace::_M_get_deleter and so can't interoperate.
Either the argument doesn't match the expected value, and so the
shared_ptr::_M_ptr member is never set, or the type-punned reference is
treated as a real std::type_info object and gets dereferenced.

This patch removes the differences between -frtti and -fno-rtti, so that
typeid is never used, and the type-punned reference is used in both
cases. For backwards compatibility with existing code that passes
typeid(_Sp_make_shared_tag) that still needs to be handled, but only
after checking that the argument is not the type-punned reference (so
it's safe to treat as a real std::type_info object). The reference is
bound to an object of literal type, so that it doesn't need a guard
variable to make its initialization thread-safe.

This patch also fixes 87520 by ensuring that the type-punned reference
is bound to "a region of storage of suitable size and alignment to
contain an object of the reference's type" (as per the proposed
resolution of Core DR 453).

If all objects are built with the fixed version of GCC then -frtti and
-fno-rtti can be mixed freely and std::make_shared will work correctly.
If some objects are built with unfixed GCC versions then problems can
still arise, depending on which template instantiations are kept by the
linker.

	PR libstdc++/85930
	PR libstdc++/87520
	* include/bits/shared_ptr_base.h (_Sp_make_shared_tag::_S_ti)
	[__cpp_rtti]: Define even when RTTI is enabled. Use array of
	sizeof(type_info) so that type-punned reference binds to an object
	of the correct size as well as correct alignment.
	(_Sp_counted_ptr_inplace::_M_get_deleter) [__cpp_rtti]: Check for
	_S_ti() reference even when RTTI is enabled.
	(__shared_ptr(_Sp_make_shared_tag, const _Alloc&, _Args&&...))
	[__cpp_rtti]: Pass _S_ti() instead of typeid(_Sp_make_shared_tag).

Modified:
    branches/gcc-8-branch/libstdc++-v3/ChangeLog
    branches/gcc-8-branch/libstdc++-v3/include/bits/shared_ptr_base.h
Comment 8 Jonathan Wakely 2018-11-22 15:04:33 UTC
Fixed for GCC 8.3
Comment 9 Jonathan Wakely 2018-11-24 00:34:08 UTC
*** Bug 88177 has been marked as a duplicate of this bug. ***
Comment 10 Jonathan Wakely 2019-01-18 21:29:24 UTC
Author: redi
Date: Fri Jan 18 21:28:48 2019
New Revision: 268086

URL: https://gcc.gnu.org/viewcvs?rev=268086&root=gcc&view=rev
Log:
PR libstdc++/88782 avoid ODR problems in std::make_shared

The old version of _Sp_counted_ptr_inplace::_M_get_deleter (up to GCC
8.2.0) expects to be passed a real std::typeinfo object, so mixing that
with the new definition of the __shared_ptr constructor (which always
passes the fake tag) leads to accessing the fake object as a real
std::typeinfo. Instead of trying to make it safe to mix the old and new
definitions, just stop using that function. By passing a reference to
__shared_ptr::_M_ptr to the __shared_count constructor it can be set
directly, without needing to obtain the pointer via the _M_get_deleter
back-channel. This avoids a virtual dispatch (which fixes PR 87514).

This means that code built against new libstdc++ headers doesn't use
_M_get_deleter at all, and so make_shared works the same whether RTTI is
enabled or not.

Also change _M_get_deleter so that it checks for a real type_info object
even when RTTI is disabled, by calling a library function. Unless
libstdc++ itself is built without RTTI that library function will be
able to test if it's the right type_info. This means the new definition
of _M_get_deleter can handle both the fake type_info tag and a real
type_info object, even if built without RTTI.

If linking to objects built against older versions of libstdc++ then if
all objects use -frtti or all use -fno-rtti, then the caller of
_M_get_deleter and the definition of _M_get_deleter will be consistent
and it will work. If mixing -frtti with -fno-rtti it can still fail if
the linker picks an old definition of _M_get_deleter and an old
__shared_ptr constructor that are incompatible. In that some or all
objects might need to be recompiled.

	PR libstdc++/87514
	PR libstdc++/87520
	PR libstdc++/88782
	* config/abi/pre/gnu.ver (GLIBCXX_3.4.26): Export new symbol.
	* include/bits/shared_ptr.h
	(shared_ptr(_Sp_make_shared_tag, const Alloc&, Args&&...))
	(allocate_shared): Change to use new tag type.
	* include/bits/shared_ptr_base.h (_Sp_make_shared_tag::_S_eq):
	Declare new member function.
	(_Sp_alloc_shared_tag): Define new type.
	(_Sp_counted_ptr_inplace): Declare __shared_count<_Lp> as a friend.
	(_Sp_counted_ptr_inplace::_M_get_deleter) [!__cpp_rtti]: Use
	_Sp_make_shared_tag::_S_eq to check type_info.
	(__shared_count(Ptr, Deleter),__shared_count(Ptr, Deleter, Alloc)):
	Constrain to prevent being called with _Sp_alloc_shared_tag.
	(__shared_count(_Sp_make_shared_tag, const _Alloc&, Args&&...)):
	Replace constructor with ...
	(__shared_count(Tp*&, _Sp_alloc_shared_tag<_Alloc>, Args&&...)): Use
	reference parameter so address of the new object can be returned to
	the caller. Obtain the allocator from the tag type.
	(__shared_ptr(_Sp_make_shared_tag, const Alloc&, Args&&...)): Replace
	constructor with ...
	(__shared_ptr(_Sp_alloc_shared_tag<Alloc>, Args&&...)): Pass _M_ptr
	to the __shared_count constructor.
	(__allocate_shared): Change to use new tag type.
	* src/c++11/shared_ptr.cc (_Sp_make_shared_tag::_S_eq): Define.

Modified:
    trunk/libstdc++-v3/ChangeLog
    trunk/libstdc++-v3/config/abi/pre/gnu.ver
    trunk/libstdc++-v3/include/bits/shared_ptr.h
    trunk/libstdc++-v3/include/bits/shared_ptr_base.h
    trunk/libstdc++-v3/src/c++11/shared_ptr.cc
Comment 11 Jonathan Wakely 2019-01-21 13:17:04 UTC
Author: redi
Date: Mon Jan 21 13:16:25 2019
New Revision: 268114

URL: https://gcc.gnu.org/viewcvs?rev=268114&root=gcc&view=rev
Log:
PR libstdc++/88782 avoid ODR problems in std::make_shared

The old version of _Sp_counted_ptr_inplace::_M_get_deleter (up to GCC
8.2.0) expects to be passed a real std::typeinfo object, so mixing that
with the new definition of the __shared_ptr constructor (which always
passes the fake tag) leads to accessing the fake object as a real
std::typeinfo. Instead of trying to make it safe to mix the old and new
definitions, just stop using that function. By passing a reference to
__shared_ptr::_M_ptr to the __shared_count constructor it can be set
directly, without needing to obtain the pointer via the _M_get_deleter
back-channel. This avoids a virtual dispatch (which fixes PR 87514).

This means that code built against new libstdc++ headers doesn't use
_M_get_deleter at all, and so make_shared works the same whether RTTI is
enabled or not.

Unlike on trunk, where _M_get_deleter calls a new library function that
can detect the real type_info object even when RTTI is disabled, this
commit for gcc-8-branch cannot add a new symbol to the shared library.
That means _M_get_deleter still returns null if compiled without RTTI
and it gets called from a translation unit that was compiled with RTTI.

If linking to objects built against older versions of libstdc++ then if
all objects use -frtti or all use -fno-rtti, then the caller of
_M_get_deleter and the definition of _M_get_deleter will be consistent
and it will work. If mixing -frtti with -fno-rtti it can still fail if
the linker picks an old definition of _M_get_deleter and an old
__shared_ptr constructor that are incompatible. In that case some or all
objects might need to be recompiled (or just relinked in a different
order).

Backport from mainline
2019-01-18  Jonathan Wakely  <jwakely@redhat.com>

	PR libstdc++/87514
	PR libstdc++/87520
	PR libstdc++/88782
	* config/abi/pre/gnu.ver (GLIBCXX_3.4.26): Export new symbol.
	* include/bits/shared_ptr.h
	(shared_ptr(_Sp_make_shared_tag, const Alloc&, Args&&...))
	(allocate_shared): Change to use new tag type.
	* include/bits/shared_ptr_base.h (_Sp_make_shared_tag::_S_eq):
	Declare new member function.
	(_Sp_alloc_shared_tag): Define new type.
	(_Sp_counted_ptr_inplace): Declare __shared_count<_Lp> as a friend.
	(_Sp_counted_ptr_inplace::_M_get_deleter) [!__cpp_rtti]: Use
	_Sp_make_shared_tag::_S_eq to check type_info.
	(__shared_count(Ptr, Deleter),__shared_count(Ptr, Deleter, Alloc)):
	Constrain to prevent being called with _Sp_alloc_shared_tag.
	(__shared_count(_Sp_make_shared_tag, const _Alloc&, Args&&...)):
	Replace constructor with ...
	(__shared_count(Tp*&, _Sp_alloc_shared_tag<_Alloc>, Args&&...)): Use
	reference parameter so address of the new object can be returned to
	the caller. Obtain the allocator from the tag type.
	(__shared_ptr(_Sp_make_shared_tag, const Alloc&, Args&&...)): Replace
	constructor with ...
	(__shared_ptr(_Sp_alloc_shared_tag<Alloc>, Args&&...)): Pass _M_ptr
	to the __shared_count constructor.
	(__allocate_shared): Change to use new tag type.
	* src/c++11/shared_ptr.cc (_Sp_make_shared_tag::_S_eq): Define.

Modified:
    branches/gcc-8-branch/libstdc++-v3/ChangeLog
    branches/gcc-8-branch/libstdc++-v3/include/bits/shared_ptr.h
    branches/gcc-8-branch/libstdc++-v3/include/bits/shared_ptr_base.h
Comment 12 Jonathan Wakely 2019-01-21 13:29:42 UTC
A more reliable fix has been committed for 8.3 and 9.1