#3195

MSVC /fsanitize-address-use-after-return: stack-buffer-underflow in makeTestInvoker during static test registration

Openhscheidleropened 2mo ago

Describe the bug When Catch2 and a test executable are both compiled with MSVC AddressSanitizer plus the experimental /fsanitize-address-use-after-return flag, the test binary aborts during static test registration (before main()), with ASan reporting a stack-buffer-underflow in the Catch::Detail::unique_ptr move constructor. The identical build with /fsanitize=address alone (no use-after-return) runs correctly. A clear and concise description of what the bug is.

Expected behavior No warnings. Catch interacts with AddressSanitizer correctly.

Reproduction steps repro.cpp: #include <catch_amalgamated.hpp> TEST_CASE("minimal", "[repro]") { REQUIRE(1 + 1 == 2); }

Build (Catch2 v3.15.3, amalgamated) + repro, all with the flag, then run: cl /EHsc -std:c++latest /MDd /Od /utf-8 /fsanitize=address /fno-sanitize-address-vcasan-lib /fsanitize-address-use-after-return /I extras /c extras/catch_amalgamated.cpp cl /EHsc -std:c++latest /MDd /Od /utf-8 /fsanitize=address /fno-sanitize-address-vcasan-lib /fsanitize-address-use-after-return /I extras repro.cpp catch_amalgamated.obj .\repro.exe --list-tests

Result (with the flag): ==16880==ERROR: AddressSanitizer: stack-buffer-underflow WRITE of size 8 thread T0 #0 Catch::Detail::unique_ptr::unique_ptr catch_amalgamated.hpp:949 #1 Catch::Detail::make_unique<TestInvokerAsFunction, void(&)(void)> catch_amalgamated.hpp:1029 #2 Catch::makeTestInvoker(void()()) catch_amalgamated.cpp:7700 #3 dynamic initializer for 'autoRegistrar1' frame has 1 object: [32, 40) 'compiler temporary' HINT: this may be a false positive if your program uses some custom stack unwind mechanism

Result (without /fsanitize-address-use-after-return) — lists and runs normally.

Platform information: OS: Windows 11 Compiler: MSVC 19.51 / VS 2026 (14.51.36231) Catch2: v3.15.3 (also reproduced on devel via the non-amalgamated catch_test_registry.cpp / catch_unique_ptr.hpp)

Additional context I can't tell if this is a problem in Catch2 or in AddressSanitizer. Filing here first, if you determine that catch is behaving correctly I'll move it to Microsofts ASan.

Activity

1
horenmar2mo ago

I am gonna go with MSVC's ASan bug, for two reasons:

  1. Catch2 runs its own CI with sanitizers on other platforms, and they don't complain.
  2. After few reductions, I got ASan to bugcheck:
#include <type_traits>

#define CATCH_MOVE(...) static_cast<std::remove_reference_t<decltype(__VA_ARGS__)>&&>(__VA_ARGS__)
#define CATCH_FORWARD(...) static_cast<decltype(__VA_ARGS__)&&>(__VA_ARGS__)

namespace Catch {
namespace Detail {

    template <typename T>
    class unique_ptr {
        T* m_ptr;
    public:
        explicit constexpr unique_ptr(T* ptr):
            m_ptr(ptr)
        {}

        template <typename U, typename = std::enable_if_t<std::is_base_of<T, U>::value>>
        unique_ptr(unique_ptr<U>&& from):
            m_ptr(from.release())
        {}

        unique_ptr(unique_ptr const&) = delete;
        unique_ptr& operator=(unique_ptr const&) = delete;

        unique_ptr(unique_ptr&& rhs) noexcept:
            m_ptr(rhs.m_ptr) {
            rhs.m_ptr = nullptr;
        }

        ~unique_ptr() {
            delete m_ptr;
        }

        T* release() {
            auto temp = m_ptr;
            m_ptr = nullptr;
            return temp;
        }
    };

    template <typename T, typename... Args>
    unique_ptr<T> make_unique(Args&&... args) {
        return unique_ptr<T>(new T(CATCH_FORWARD(args)...));
    }

} // end namespace Detail
} // end namespace Catch

namespace Catch {

    class ITestInvoker {
    public:
        virtual void invoke() const = 0;
        virtual ~ITestInvoker() = default;
    };

} // namespace Catch


namespace repro {
    
    class TestInvokerAsFunction final : public Catch::ITestInvoker {
        using TestType = void ( * )();
        TestType m_testAsFunction;

    public:
        constexpr TestInvokerAsFunction( TestType testAsFunction ) noexcept:
            m_testAsFunction( testAsFunction ) {}

        void invoke() const override { m_testAsFunction(); }
    };

    
    Catch::Detail::unique_ptr<Catch::ITestInvoker> makeTestInvoker( void(*testAsFunction)() ) {
        return Catch::Detail::make_unique<repro::TestInvokerAsFunction>( testAsFunction );
    }

}

static void TestFunc();

namespace {
  auto invoker = repro::makeTestInvoker( &TestFunc );
}

static void TestFunc() {}

int main() {}
> cl /EHsc -std:c++latest /MDd /utf-8 /fsanitize=address /fsanitize-address-use-after-return /Zi /fno-sanitize-address-vcasan-lib main.cpp  && main.exe
Microsoft (R) C/C++ Optimizing Compiler Version 19.51.36256 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

/std:c++latest is provided as a preview of language features from the latest C++
working draft, and we're eager to hear about bugs and suggestions for improvements.
However, note that these features are provided as-is without support, and subject
to changes or removal as the working draft evolves. See
https://go.microsoft.com/fwlink/?linkid=2045807 for details.

main.cpp
Microsoft (R) Incremental Linker Version 14.51.36256.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:main.exe
/InferAsanLibs
/debug
main.obj
=================================================================
AddressSanitizer: CHECK failed: asan_thread.cpp:404 "((ptr[0] == kCurrentStackFrameMagic)) != (0)" (0x0, 0x0) (tid=21952)
    #0 0x6872620b in _asan::CheckUnwind D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_rtl.cpp:75
    #1 0x686aec0e in __sanitizer::CheckFailed(char const *, int, char const *, unsigned __int64, unsigned __int64) D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\sanitizer_common\sanitizer_termination.cpp:86
    #2 0x6872a67c in __asan::AsanThread::GetStackFrameAccessByAddr(unsigned int, struct __asan::AsanThread::StackFrameAccess *) D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_thread.cpp:404
    #3 0x686d6e79 in __asan::GetStackAddressInformation(unsigned int, unsigned int, struct __asan::StackAddressDescription *) D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_descriptions.cpp:211
    #4 0x686d5d7b in __asan::AddressDescription::AddressDescription(unsigned int, unsigned int, bool) D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_descriptions.cpp:515
    #5 0x686d7ced in __asan::ErrorGeneric::ErrorGeneric(unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, bool, unsigned int) D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_errors.cpp:473
    #6 0x68723693 in __asan::ReportGenericError(unsigned int, unsigned int, unsigned int, unsigned int, bool, unsigned int, unsigned int, bool) D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_report.cpp:667
    #7 0x68727bc4 in __asan_report_store4 D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_rtl.cpp:146
    #8 0x00981340 in Catch::Detail::unique_ptr<class repro::TestInvokerAsFunction>::unique_ptr<class repro::TestInvokerAsFunction>(class repro::TestInvokerAsFunction *) C:\ubuntu\asan-repro\main.cpp:14
    #9 0x009812e0 in Catch::Detail::make_unique<class repro::TestInvokerAsFunction, void (__cdecl *&)(void)>(void (__cdecl *&)(void)) C:\ubuntu\asan-repro\main.cpp:43
    #10 0x00981135 in repro::makeTestInvoker(void (__cdecl *)(void)) C:\ubuntu\asan-repro\main.cpp:75
    #11 0x00981011 in `anonymous namespace'::`dynamic initializer for 'invoker'' (C:\ubuntu\asan-repro\main.exe+0x401011)
    #12 0x6b93a73e  (C:\Windows\SYSTEM32\ucrtbased.dll+0x100ca73e)
    #13 0x00982e2f in _scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:256
    #14 0x00982d6c in _scrt_common_main D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:330
    #15 0x00983027 in mainCRTStartup D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp:16
    #16 0x7664fcc8  (C:\Windows\System32\KERNEL32.DLL+0x6b81fcc8)
    #17 0x774f843d  (C:\Windows\SYSTEM32\ntdll.dll+0x4b2e843d)
    #18 0x774f840d  (C:\Windows\SYSTEM32\ntdll.dll+0x4b2e840d)