https://gcc.gnu.org/bugzilla/show_bug.cgi?id=92577
Bug ID: 92577
Summary: Undefined behavior when using std::map with a noexcept
allocator
Product: gcc
Version: 9.2.1
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: libstdc++
Assignee: unassigned at gcc dot gnu.org
Reporter: lucas.bader at sap dot com
Target Milestone: ---
Created attachment 47296
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=47296&action=edit
minimal code example
Using std::map with an allocator with a noexcept specifier leads to undefined
behavior when constructing a node in-place.
Compiled with GCC9 this results in the generation of a ud2 instruction that is
encountered when nullptr be retured by the allocation function:
$ g++ -std=c++17 -O3 allocator_ub.cpp
$ ./a.out
[2]114342 illegal hardware instruction (core dumped) ./a.out
$ objdump -d a.out
00400780
<_ZNSt8_Rb_treeIiSt4pairIKiiESt10_Select1stIS2_ESt4lessIiE10mallocatorIS2_EE17_M_emplace_uniqueIJS0_IiiS0_ISt17_Rb_tree_iteratorIS2_EbEDpOT_>:
...
4008b3: 0f 0b ud2
The corresponding stack trace in gdb is:
#0 0x004008b3 in std::pair >, bool> std::_Rb_tree,
std::_Select1st >, std::less,
mallocator > >::_M_emplace_unique
>(std::pair&&) ()
#1 0x00400632 in main ()
According to the C++17 working draft (8.3.4 paragraph 16):
"If the allocation function has a non-throwing exception specification, it
returns null to
indicate failure to allocate storage and a non-null pointer otherwise. [...]
If the allocation function is a non-allocating form (21.6.2.3) that returns
null, the behavior is undefined."
This is also supported by cppreference.com
(https://en.cppreference.com/w/cpp/language/new) with:
"If the standard placement allocation function returns a null pointer, which is
possible if the user passes a null pointer as the argument, the behavior is
undefined. (since C++17)"
The std::map implementation should handle this appropriately or prohibit using
it with an allocator that is defined noexcept.
When using an allocation function with the noexcept specifier (and thus
returning nullptr on failure), nullptr is passed to placement new (in
stl_tree.h) via the following code:
2406 _M_emplace_unique(_Args&&... __args)
2407 {
2408 _Link_type __z = _M_create_node(std::forward<_Args>(__args)...);
628 _M_create_node(_Args&&... __args)
629 {
630 _Link_type __tmp = _M_get_node();
631 _M_construct_node(__tmp, std::forward<_Args>(__args)...);
579 _M_get_node()
580 { return _Alloc_traits::allocate(_M_get_Node_allocator(), 1); }
609 _M_construct_node(_Link_type __node, _Args&&... __args)
610 {
611 __try
612{
613 ::new(__node) _Rb_tree_node<_Val>;
614 _Alloc_traits::construct(_M_get_Node_allocator(),
615 __node->_M_valptr(),
616 std::forward<_Args>(__args)...);
617}
618 __catch(...)
619{
620 __node->~_Rb_tree_node<_Val>();
621 _M_put_node(__node);
622 __throw_exception_again;
623}
624 }