Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::map argument with empty brace-initializers for default argument segfaults in GCC

Problem

I got a bug report from user reporting a segfault in library I develop.

The minimal example of the faulty code is:

#include <map>
#include <string>
#include <iostream>

void f(std::map<std::string, std::string> m = {})
{
        std::cout << m.size() << "\n";
        for (const auto& s: m) {
                std::cout << s.first << "->" << s.second <<"\n";
        }
}

int main()
{
        f();
}

When compiled with GCC (I tested 4.8.2 and 4.7.3) it correctly prints 0 as size of the container, but segfaults inside the loop (which shouldn't be executed at all).

Workarounds

However, I can fix the problem by changing the declaration to:

void f(std::map<std::string, std::string> m = std::map<std::string, std::string>{})

Copying the map works as well:

void f(std::map<std::string, std::string> mx = {})
{
        auto m = mx;
        std::cout << m.size() << "\n";
        for (const auto& s: m) {
                std::cout << s.first << "->" << s.second <<"\n";
        }
}

Changing the parameter to const std::map<...>& also works.

GCC 4.9.1 works fine.

Clang also compiles and runs the code just fine. (even when using the same libstdc++ as failing gcc 4.8.2)

Working example: http://coliru.stacked-crooked.com/a/eb64a7053f542efd

Question

The map is definitely not in valid state inside the function (details bellow). It looks like a GCC (or libstdc++) bug, but I want to be sure I'm not making some stupid mistake here. It's hard to believe such a bug would stay in gcc for at least 2 major version.

So my question is: Is the way of initializing default std::map parameter wrong (and bug in my code) or is it a bug in stdlibc++ (or gcc)?

I'm not looking for workarounds (as I know what to do to make to code work) When integrated in the application, the offending code executes fine on some computers (even when compiled with gcc 4.8.2) on some doesn't.

Details

I compile it using:

g++-4.8.2 -g -Wall -Wextra -pedantic  -std=c++11 /tmp/c.cpp -o /tmp/t

Backtrace from gdb:

#0  std::operator<< <char, std::char_traits<char>, std::allocator<char> > (__os=..., __str=...) at /usr/src/debug/sys-devel/gcc-4.8.2/build/x86_64-pc-linux-gnu/libstdc++-v3/include/bits/basic_string.h:2758
#1  0x0000000000400f36 in f (m=std::map with 0 elements) at /tmp/c.cpp:9
#2  0x0000000000400fe0 in main () at /tmp/c.cpp:15

/tmp/c.cpp:9 is the line with std::cout << ...

ASAN reports:

AddressSanitizer: SEGV on unknown address 0xffffffffffffffe8 

This seems like nullptr - 8

valgrind shows:

==28183== Invalid read of size 8
==28183==    at 0x4ECC863: std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (in /usr/lib64/gcc/x86_64-pc-linux-gnu/4.8.2/libstdc++.so.6.0.18)
==28183==    by 0x400BD5: f(std::map<std::string, std::string, std::less<std::string>, std::allocator<std::pair<std::string const, std::string> > >) (c.cpp:9)
==28183==    by 0x400C7F: main (c.cpp:15)
==28183==  Address 0xffffffffffffffe8 is not stack'd, malloc'd or (recently) free'd

Looking at internal state of the map shows that the code really has to fail:

std::map::begin() in libstdc++ returns value of

this->_M_impl._M_header._M_parent

from it's internal representation, std::map::end() returns:

&this->_M_impl._M_header

gdb shows:

(gdb) print m._M_t._M_impl._M_header
$5 = {_M_color = std::_S_red, _M_parent = 0x0, _M_left = 0x7fffffffd6d8, _M_right = 0x7fffffffd6d8}
(gdb) print &m._M_t._M_impl._M_header
$6 = (std::_Rb_tree_node_base *) 0x7fffffffd6a8

So value of begin() and end() are not the same (begin() is nullptr) as mandated by standard for empty std::map.

like image 412
v154c1 Avatar asked Mar 03 '15 16:03

v154c1


1 Answers

Looks like this bug was fixed in 4.8.3/4.9.0, the bug report which has a similar example and also seg-faults says:

The attached minimal testcase has the following function with default-constructed default argument:

void do_something( foo f = {} )
{     std::cout << "default argument is at " << &f << std::endl;
}

The constructor for foo outputs its address; I got the following output from a single run: constructed foo @ 0x7ffff10bdb7f default argument is at 0x7ffff10bdb60

It shows that only 1 foo was constructed, and not at the same address as that of the default argument. It's been a loooong week, but I can't see anything wrong with the code. In the real code on which this was based, a segfault was occurring when running the destructor of a foo that was move-constructed from the default argument, because the underlying memory was seemingly uninitialised.

We can see from a live example that 4.9.0 does not demonstrate this problem.

We can see this was intentional functionality from defect report 994 and the subsequent resolution N3217:

This paper presents detailed wording changes relative to the current C++ Working Draft N3126 to implement brace-initializers for default arguments for functions, as proposed in N3139 "An Incomplete Language Feature" by Bjarne Stroustrup, thereby also addressing core issue 994.

This is also covered in the proposal N3139: An Incomplete Language Feature.

Interesting to note that Visual Studio also has a bug with respect to brace-initializers as default arguments which I think is still unresolved.

like image 94
Shafik Yaghmour Avatar answered Nov 11 '22 06:11

Shafik Yaghmour