AddressSanitizer: SEGV on unknown address when using GENERATE with function pointers
Describe the bug
I'm getting SIGSEGV when using GENERATE returning function pointers. Unsure if it's issue in catch2 or maybe it's not really supppported.
fail.cpp:
#include "catch.hpp"
#include <string>
const char* string_literal(const char* s)
{
return s;
}
std::string string(const char* s)
{
return s;
}
TEST_CASE("tests")
{
auto f = GENERATE(string, string_literal);
CHECK(f("X") == std::string("Y"));
}
main.cpp:
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
Makefile:
CXXFLAGS=-std=c++20 -Wall -Wextra -pedantic -O2 -ggdb3 -fsanitize=address
fail: main.o fail.o
${CXX} ${CXXFLAGS} -o fail fail.o main.o
./fail
Executing this code produces SIGSEGV:
AddressSanitizer:DEADLYSIGNAL
=================================================================
==38196==ERROR: AddressSanitizer: SEGV on unknown address 0x03e800009534 (pc 0x7fe48acf0d34 bp 0x000000000007 sp 0x7ffdbceaf448 T0)
==38196==The signal is caused by a READ memory access.
#0 0x7fe48acf0d34 (/usr/lib/libc.so.6+0x164d34)
#1 0x7fe48afc7c83 in std::char_traits<char>::copy(char*, char const*, unsigned long) /usr/src/debug/gcc-build/x86_64-pc-linux-gnu/libstdc++-v3/include/bits/char_traits.h:431
#2 0x7fe48afc7c83 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_S_copy(char*, char const*, unsigned long) /usr/src/debug/gcc-build/x86_64-pc-linux-gnu/libstdc++-v3/include/bits/basic_string.h:423
#3 0x7fe48afc7c83 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_S_copy(char*, char const*, unsigned long) /usr/src/debug/gcc-build/x86_64-pc-linux-gnu/libstdc++-v3/include/bits/basic_string.h:418
#4 0x7fe48afc7c83 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_append(char const*, unsigned long) /usr/src/debug/gcc-build/x86_64-pc-linux-gnu/libstdc++-v3/include/bits/basic_string.tcc:417
#5 0x556d93408d7d in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > std::operator+<char, std::char_traits<char>, std::allocator<char> >(char, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (/mnt/data/data/git/bits/str-join.cpp/fail+0x5bd7d)
#6 0x556d933dec40 in Catch::StringMaker<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, void>::convert(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (/mnt/data/data/git/bits/str-join.cpp/fail+0x31c40)
#7 0x556d933c3425 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > Catch::Detail::stringify<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /home/user/data/git/bits/str-join.cpp/catch.hpp:1645
#8 0x556d933c3425 in Catch::BinaryExpr<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>::streamReconstructedExpression(std::ostream&) const /home/user/data/git/bits/str-join.cpp/catch.hpp:2231
#9 0x556d933c4bf9 in Catch::(anonymous namespace)::operator<<(std::ostream&, Catch::ITransientExpression const&) (/mnt/data/data/git/bits/str-join.cpp/fail+0x17bf9)
#10 0x556d933c4d33 in Catch::operator<<(std::ostream&, Catch::LazyExpression const&) (/mnt/data/data/git/bits/str-join.cpp/fail+0x17d33)
#11 0x556d9340658a in Catch::ReusableStringStream& Catch::ReusableStringStream::operator<< <Catch::LazyExpression>(Catch::LazyExpression const&) (/mnt/data/data/git/bits/str-join.cpp/fail+0x5958a)
#12 0x556d933c5292 in Catch::AssertionResultData::reconstructExpression[abi:cxx11]() const (/mnt/data/data/git/bits/str-join.cpp/fail+0x18292)
#13 0x556d933c5796 in Catch::AssertionResult::getExpandedExpression[abi:cxx11]() const (/mnt/data/data/git/bits/str-join.cpp/fail+0x18796)
#14 0x556d933c56bd in Catch::AssertionResult::hasExpandedExpression() const (/mnt/data/data/git/bits/str-join.cpp/fail+0x186bd)
#15 0x556d933e47cc in Catch::(anonymous namespace)::ConsoleAssertionPrinter::printReconstructedExpression() const (/mnt/data/data/git/bits/str-join.cpp/fail+0x377cc)
#16 0x556d933e45b7 in Catch::(anonymous namespace)::ConsoleAssertionPrinter::print() const (/mnt/data/data/git/bits/str-join.cpp/fail+0x375b7)
#17 0x556d933e58ff in Catch::ConsoleReporter::assertionEnded(Catch::AssertionStats const&) (/mnt/data/data/git/bits/str-join.cpp/fail+0x388ff)
#18 0x556d933d4040 in Catch::RunContext::assertionEnded(Catch::AssertionResult const&) (/mnt/data/data/git/bits/str-join.cpp/fail+0x27040)
#19 0x556d933d56b4 in Catch::RunContext::reportExpr(Catch::AssertionInfo const&, Catch::ResultWas::OfType, Catch::ITransientExpression const*, bool) (/mnt/data/data/git/bits/str-join.cpp/fail+0x286b4)
#20 0x556d933d558c in Catch::RunContext::handleExpr(Catch::AssertionInfo const&, Catch::ITransientExpression const&, Catch::AssertionReaction&) (/mnt/data/data/git/bits/str-join.cpp/fail+0x2858c)
#21 0x556d933c4e2d in Catch::AssertionHandler::handleExpr(Catch::ITransientExpression const&) (/mnt/data/data/git/bits/str-join.cpp/fail+0x17e2d)
#22 0x556d933c2292 in C_A_T_C_H_T_E_S_T_0 /home/user/data/git/bits/str-join.cpp/fail.cpp:17
#23 0x556d933dbb6b in Catch::TestInvokerAsFunction::invoke() const (/mnt/data/data/git/bits/str-join.cpp/fail+0x2eb6b)
#24 0x556d933dadee in Catch::TestCase::invoke() const (/mnt/data/data/git/bits/str-join.cpp/fail+0x2ddee)
#25 0x556d933d539d in Catch::RunContext::invokeActiveTestCase() (/mnt/data/data/git/bits/str-join.cpp/fail+0x2839d)
#26 0x556d933d50de in Catch::RunContext::runCurrentTest(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&) (/mnt/data/data/git/bits/str-join.cpp/fail+0x280de)
#27 0x556d933d3c1f in Catch::RunContext::runTest(Catch::TestCase const&) (/mnt/data/data/git/bits/str-join.cpp/fail+0x26c1f)
#28 0x556d933d6c1c in Catch::(anonymous namespace)::TestGroup::execute() (/mnt/data/data/git/bits/str-join.cpp/fail+0x29c1c)
#29 0x556d933d8045 in Catch::Session::runInternal() (/mnt/data/data/git/bits/str-join.cpp/fail+0x2b045)
#30 0x556d933d7d4b in Catch::Session::run() (/mnt/data/data/git/bits/str-join.cpp/fail+0x2ad4b)
#31 0x556d93415832 in int Catch::Session::run<char>(int, char const* const*) (/mnt/data/data/git/bits/str-join.cpp/fail+0x68832)
#32 0x556d933ee677 in main (/mnt/data/data/git/bits/str-join.cpp/fail+0x41677)
#33 0x7fe48abaf28f (/usr/lib/libc.so.6+0x2328f)
#34 0x7fe48abaf349 in __libc_start_main (/usr/lib/libc.so.6+0x23349)
#35 0x556d933c15d4 in _start ../sysdeps/x86_64/start.S:115
When I reverse the order of string and string_literal functions to auto f = GENERATE(string_literal, string) I have a different error:
==38326==ERROR: AddressSanitizer: global-buffer-overflow on address 0x564b9398b220 at pc 0x564b938ca94b bp 0x7ffe43d40b40 sp 0x7ffe43d40b30
WRITE of size 8 at 0x564b9398b220 thread T0
#0 0x564b938ca94a in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_Alloc_hider::_Alloc_hider(char*, std::allocator<char> const&) /usr/include/c++/12.2.0/bits/basic_string.h:200
#1 0x564b938ca94a in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&) /usr/include/c++/12.2.0/bits/basic_string.h:635
#2 0x564b938ca94a in string[abi:cxx11](char const*) /home/user/data/git/bits/str-join.cpp/fail.cpp:11
#3 0x564b938cafe5 in C_A_T_C_H_T_E_S_T_0 /home/user/data/git/bits/str-join.cpp/fail.cpp:17
#4 0x564b938e4aeb in Catch::TestInvokerAsFunction::invoke() const (/mnt/data/data/git/bits/str-join.cpp/fail+0x2eaeb)
#5 0x564b938e3d6e in Catch::TestCase::invoke() const (/mnt/data/data/git/bits/str-join.cpp/fail+0x2dd6e)
#6 0x564b938de31d in Catch::RunContext::invokeActiveTestCase() (/mnt/data/data/git/bits/str-join.cpp/fail+0x2831d)
#7 0x564b938de05e in Catch::RunContext::runCurrentTest(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&) (/mnt/data/data/git/bits/str-join.cpp/fail+0x2805e)
#8 0x564b938dcb9f in Catch::RunContext::runTest(Catch::TestCase const&) (/mnt/data/data/git/bits/str-join.cpp/fail+0x26b9f)
#9 0x564b938dfb9c in Catch::(anonymous namespace)::TestGroup::execute() (/mnt/data/data/git/bits/str-join.cpp/fail+0x29b9c)
#10 0x564b938e0fc5 in Catch::Session::runInternal() (/mnt/data/data/git/bits/str-join.cpp/fail+0x2afc5)
#11 0x564b938e0ccb in Catch::Session::run() (/mnt/data/data/git/bits/str-join.cpp/fail+0x2accb)
#12 0x564b9391e7b2 in int Catch::Session::run<char>(int, char const* const*) (/mnt/data/data/git/bits/str-join.cpp/fail+0x687b2)
#13 0x564b938f75f7 in main (/mnt/data/data/git/bits/str-join.cpp/fail+0x415f7)
#14 0x7ff2da6b728f (/usr/lib/libc.so.6+0x2328f)
#15 0x7ff2da6b7349 in __libc_start_main (/usr/lib/libc.so.6+0x23349)
#16 0x564b938ca5b4 in _start ../sysdeps/x86_64/start.S:115
0x564b9398b222 is located 0 bytes to the right of global variable '*.LC14' defined in 'fail.cpp' (0x564b9398b220) of size 2
'*.LC14' is ascii string 'X'
SUMMARY: AddressSanitizer: global-buffer-overflow /usr/include/c++/12.2.0/bits/basic_string.h:200 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_Alloc_hider::_Alloc_hider(char*, std::allocator<char> const&)
Shadow bytes around the buggy address:
0x0ac9f27295f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0ac9f2729600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0ac9f2729610: 00 00 00 00 00 00 02 f9 f9 f9 f9 f9 00 00 00 02
0x0ac9f2729620: f9 f9 f9 f9 00 01 f9 f9 f9 f9 f9 f9 00 03 f9 f9
0x0ac9f2729630: f9 f9 f9 f9 00 00 00 03 f9 f9 f9 f9 06 f9 f9 f9
=>0x0ac9f2729640: f9 f9 f9 f9[02]f9 f9 f9 f9 f9 f9 f9 03 f9 f9 f9
0x0ac9f2729650: f9 f9 f9 f9 06 f9 f9 f9 f9 f9 f9 f9 01 f9 f9 f9
0x0ac9f2729660: f9 f9 f9 f9 00 00 00 00 00 00 00 00 00 00 00 00
0x0ac9f2729670: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0ac9f2729680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0ac9f2729690: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==38326==ABORTING
When I change the test case body to
CHECK(string("X") == std::string("Y"));
CHECK(string_literal("X") == std::string("Y"));
I don't get a crash.
Expected behavior
No crash and no issue reported by sanitizer when using GENERATE with function pointers.
Reproduction steps
make fail
Platform information:
- OS:
Linux arc 5.10.148-1-MANJARO #1 SMP PREEMPT Sat Oct 15 13:41:09 UTC 2022 x86_64 GNU/Linux - Compiler+version: GCC v12.2.0
- Catch version: v2.13.9-1
Additional context N/A
You can use function pointers with GENERATE, but they have to have the same signature, or bad things happen (like here). I am honestly surprised this compiles.
The issue is that the first fptr decides the type of f, and the second fptr ends up coerced to that type. This then leads to either interpreting a std::string as char const*, or vice versa, but these types are obviously not compatible, which then leads to the segfault.
Function pointers behave as expected. Out of these, only the first one and the last one compiles:
std::string(*pa)(const char*) = string;
std::string(*pb)(const char*) = string_literal;
std::string_view(*pc)(const char*) = string;
std::string_view(*pd)(const char*) = string_literal;
const char*(*pe)(const char*) = string;
const char*(*pf)(const char*) = string_literal;
But it's different with std::function, where only p5 cause compilation error:
std::function<std::string(const char*)> p1 = string;
std::function<std::string(const char*)> p2 = string_literal;
std::function<std::string_view(const char*)> p3 = string;
std::function<std::string_view(const char*)> p4 = string_literal;
std::function<const char*(const char*)> p5 = string;
std::function<const char*(const char*)> p6 = string_literal;
When I then use these in test case body:
CHECK(p1("X") == std::string("Y"));
CHECK(p2("X") == std::string("Y"));
CHECK(p3("X") == std::string("Y"));
CHECK(p4("X") == std::string("Y"));
CHECK(p6("X") == std::string("Y"));
CHECK(pa("X") == std::string("Y"));
CHECK(pf("X") == std::string("Y"));
the call to p3 produces ASAN error, the rest works as expected.
It doesn't produce an error when used outside of CHECK, like
std::cout << (p3("X") == std::string("Y"));