| // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "gtest/gtest.h" |
| #include "chrome_frame/exception_barrier.h" |
| |
| namespace { |
| |
| // retrieves the top SEH registration record |
| __declspec(naked) EXCEPTION_REGISTRATION* GetTopRegistration() { |
| __asm { |
| mov eax, FS:0 |
| ret |
| } |
| } |
| |
| // This function walks the SEH chain and attempts to ascertain whether it's |
| // sane, or rather tests it for any obvious signs of insanity. |
| // The signs it's capable of looking for are: |
| // # Is each exception registration in bounds of our stack |
| // # Is the registration DWORD aligned |
| // # Does each exception handler point to a module, as opposed to |
| // e.g. into the stack or never-never land. |
| // # Do successive entries in the exception chain increase |
| // monotonically in address |
| void TestSEHChainSane() { |
| // get the skinny on our stack segment |
| MEMORY_BASIC_INFORMATION info = { 0 }; |
| // Note that we pass the address of the info struct just as a handy |
| // moniker to anything at all inside our stack allocation |
| ASSERT_NE(0u, ::VirtualQuery(&info, &info, sizeof(info))); |
| |
| // The lower bound of our stack. |
| // We use the address of info as a lower bound, this assumes that if this |
| // function has an SEH handler, it'll be higher up in our invocation |
| // record. |
| EXCEPTION_REGISTRATION* limit = |
| reinterpret_cast<EXCEPTION_REGISTRATION*>(&info); |
| // the very top of our stack segment |
| EXCEPTION_REGISTRATION* top = |
| reinterpret_cast<EXCEPTION_REGISTRATION*>( |
| reinterpret_cast<char*>(info.BaseAddress) + info.RegionSize); |
| |
| EXCEPTION_REGISTRATION* curr = GetTopRegistration(); |
| // there MUST be at least one registration |
| ASSERT_TRUE(NULL != curr); |
| |
| EXCEPTION_REGISTRATION* prev = NULL; |
| const EXCEPTION_REGISTRATION* kSentinel = |
| reinterpret_cast<EXCEPTION_REGISTRATION*>(0xFFFFFFFF); |
| for (; kSentinel != curr; prev = curr, curr = curr->prev) { |
| // registrations must increase monotonically |
| ASSERT_TRUE(curr > prev); |
| // Check it's in bounds |
| ASSERT_GE(top, curr); |
| ASSERT_LT(limit, curr); |
| |
| // check for DWORD alignment |
| ASSERT_EQ(0, (reinterpret_cast<UINT_PTR>(prev) & 0x00000003)); |
| |
| // find the module hosting the handler |
| ASSERT_NE(0u, ::VirtualQuery(curr->handler, &info, sizeof(info))); |
| wchar_t module_filename[MAX_PATH]; |
| ASSERT_NE(0u, ::GetModuleFileName( |
| reinterpret_cast<HMODULE>(info.AllocationBase), |
| module_filename, ARRAYSIZE(module_filename))); |
| } |
| } |
| |
| void AccessViolationCrash() { |
| volatile char* null = NULL; |
| *null = '\0'; |
| } |
| |
| // A simple crash over the exception barrier |
| void CrashOverExceptionBarrier() { |
| ExceptionBarrierCustomHandler barrier; |
| |
| TestSEHChainSane(); |
| |
| AccessViolationCrash(); |
| |
| TestSEHChainSane(); |
| } |
| |
| #pragma warning(push) |
| // Inline asm assigning to 'FS:0' : handler not registered as safe handler |
| // This warning is in error (the compiler can't know that we register the |
| // handler as a safe SEH handler in an .asm file) |
| #pragma warning(disable:4733) |
| // Hand-generate an SEH frame implicating the ExceptionBarrierCallCustomHandler, |
| // then crash to invoke it. |
| __declspec(naked) void CrashOnManualSEHBarrierHandler() { |
| __asm { |
| push ExceptionBarrierCallCustomHandler |
| push FS:0 |
| mov FS:0, esp |
| call AccessViolationCrash |
| ret |
| } |
| } |
| #pragma warning(pop) |
| |
| |
| class ExceptionBarrierTest: public testing::Test { |
| public: |
| ExceptionBarrierTest() { |
| } |
| |
| // Install an exception handler for the ExceptionBarrier, and |
| // set the handled flag to false. This allows us to see whether |
| // the ExceptionBarrier gets to handle the exception |
| virtual void SetUp() { |
| ExceptionBarrierConfig::set_enabled(true); |
| ExceptionBarrierCustomHandler::set_custom_handler(&ExceptionHandler); |
| s_handled_ = false; |
| |
| TestSEHChainSane(); |
| } |
| |
| virtual void TearDown() { |
| TestSEHChainSane(); |
| ExceptionBarrierCustomHandler::set_custom_handler(NULL); |
| ExceptionBarrierConfig::set_enabled(false); |
| } |
| |
| // The exception notification callback, sets the handled flag. |
| static void CALLBACK ExceptionHandler(EXCEPTION_POINTERS* ptrs) { |
| TestSEHChainSane(); |
| s_handled_ = true; |
| } |
| |
| // Flag is set by handler |
| static bool s_handled_; |
| }; |
| |
| bool ExceptionBarrierTest::s_handled_ = false; |
| |
| bool TestExceptionExceptionBarrierHandler() { |
| TestSEHChainSane(); |
| __try { |
| CrashOnManualSEHBarrierHandler(); |
| return false; // not reached |
| } __except(EXCEPTION_ACCESS_VIOLATION == GetExceptionCode() ? |
| EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { |
| TestSEHChainSane(); |
| return true; |
| } |
| |
| return false; // not reached |
| } |
| |
| typedef EXCEPTION_DISPOSITION |
| (__cdecl* ExceptionBarrierHandlerFunc)( |
| struct _EXCEPTION_RECORD* exception_record, |
| void* establisher_frame, |
| struct _CONTEXT* context, |
| void* reserved); |
| |
| TEST_F(ExceptionBarrierTest, RegisterUnregister) { |
| // Assert that registration modifies the chain |
| // and the registered record as expected |
| EXCEPTION_REGISTRATION* top = GetTopRegistration(); |
| ExceptionBarrierHandlerFunc handler = top->handler; |
| EXCEPTION_REGISTRATION* prev = top->prev; |
| |
| EXCEPTION_REGISTRATION registration; |
| ::RegisterExceptionRecord(®istration, ExceptionBarrierHandler); |
| EXPECT_EQ(GetTopRegistration(), ®istration); |
| EXPECT_EQ(&ExceptionBarrierHandler, registration.handler); |
| EXPECT_EQ(top, registration.prev); |
| |
| // test the whole chain for good measure |
| TestSEHChainSane(); |
| |
| // Assert that unregistration restores |
| // everything as expected |
| ::UnregisterExceptionRecord(®istration); |
| EXPECT_EQ(top, GetTopRegistration()); |
| EXPECT_EQ(handler, top->handler); |
| EXPECT_EQ(prev, top->prev); |
| |
| // and again test the whole chain for good measure |
| TestSEHChainSane(); |
| } |
| |
| |
| TEST_F(ExceptionBarrierTest, ExceptionBarrierHandler) { |
| EXPECT_TRUE(TestExceptionExceptionBarrierHandler()); |
| EXPECT_TRUE(s_handled_); |
| } |
| |
| bool TestExceptionBarrier() { |
| __try { |
| CrashOverExceptionBarrier(); |
| } __except(EXCEPTION_ACCESS_VIOLATION == GetExceptionCode() ? |
| EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { |
| TestSEHChainSane(); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| TEST_F(ExceptionBarrierTest, HandlesAccessViolationException) { |
| TestExceptionBarrier(); |
| EXPECT_TRUE(s_handled_); |
| } |
| |
| void RecurseAndCrash(int depth) { |
| __try { |
| __try { |
| if (0 == depth) |
| AccessViolationCrash(); |
| else |
| RecurseAndCrash(depth - 1); |
| |
| TestSEHChainSane(); |
| } __except(EXCEPTION_CONTINUE_SEARCH) { |
| TestSEHChainSane(); |
| } |
| } __finally { |
| TestSEHChainSane(); |
| } |
| } |
| |
| // This test exists only for comparison with TestExceptionBarrierChaining, and |
| // to "document" how the SEH chain is manipulated under our compiler. |
| // The two tests are expected to both fail if the particulars of the compiler's |
| // SEH implementation happens to change. |
| bool TestRegularChaining(EXCEPTION_REGISTRATION* top) { |
| // This test relies on compiler-dependent details, notably we rely on the |
| // compiler to generate a single SEH frame for the entire function, as |
| // opposed to e.g. generating a separate SEH frame for each __try __except |
| // statement. |
| EXCEPTION_REGISTRATION* my_top = GetTopRegistration(); |
| if (my_top == top) |
| return false; |
| |
| __try { |
| // we should have the new entry in effect here still |
| if (GetTopRegistration() != my_top) |
| return false; |
| } __except(EXCEPTION_EXECUTE_HANDLER) { |
| return false; |
| } |
| |
| __try { |
| AccessViolationCrash(); |
| return false; // not reached |
| } __except(EXCEPTION_EXECUTE_HANDLER) { |
| // and here |
| if (GetTopRegistration() != my_top) |
| return false; |
| } |
| |
| __try { |
| RecurseAndCrash(10); |
| return false; // not reached |
| } __except(EXCEPTION_EXECUTE_HANDLER) { |
| // we should have unrolled to our frame by now |
| if (GetTopRegistration() != my_top) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void RecurseAndCrashOverBarrier(int depth, bool crash) { |
| ExceptionBarrierCustomHandler barrier; |
| |
| if (0 == depth) { |
| if (crash) |
| AccessViolationCrash(); |
| } else { |
| RecurseAndCrashOverBarrier(depth - 1, crash); |
| } |
| } |
| |
| // Test that ExceptionBarrier doesn't molest the SEH chain, neither |
| // for regular unwinding, nor on exception unwinding cases. |
| // |
| // Note that while this test shows the ExceptionBarrier leaves the chain |
| // sane on both those cases, it's not clear that it does the right thing |
| // during first-chance exception handling. I can't think of a way to test |
| // that though, because first-chance exception handling is very difficult |
| // to hook into and to observe. |
| static bool TestExceptionBarrierChaining(EXCEPTION_REGISTRATION* top) { |
| TestSEHChainSane(); |
| |
| // This test relies on compiler-dependent details, notably we rely on the |
| // compiler to generate a single SEH frame for the entire function, as |
| // opposed to e.g. generating a separate SEH frame for each __try __except |
| // statement. |
| // Unfortunately we can't use ASSERT macros here, because they create |
| // temporary objects and the compiler doesn't grok non POD objects |
| // intermingled with __try and other SEH constructs. |
| EXCEPTION_REGISTRATION* my_top = GetTopRegistration(); |
| if (my_top == top) |
| return false; |
| |
| __try { |
| // we should have the new entry in effect here still |
| if (GetTopRegistration() != my_top) |
| return false; |
| } __except(EXCEPTION_EXECUTE_HANDLER) { |
| return false; // Not reached |
| } |
| |
| __try { |
| CrashOverExceptionBarrier(); |
| return false; // Not reached |
| } __except(EXCEPTION_EXECUTE_HANDLER) { |
| // and here |
| if (GetTopRegistration() != my_top) |
| return false; |
| } |
| TestSEHChainSane(); |
| |
| __try { |
| RecurseAndCrashOverBarrier(10, true); |
| return false; // not reached |
| } __except(EXCEPTION_EXECUTE_HANDLER) { |
| // we should have unrolled to our frame by now |
| if (GetTopRegistration() != my_top) |
| return false; |
| } |
| TestSEHChainSane(); |
| |
| __try { |
| RecurseAndCrashOverBarrier(10, false); |
| |
| // we should have unrolled to our frame by now |
| if (GetTopRegistration() != my_top) |
| return false; |
| } __except(EXCEPTION_EXECUTE_HANDLER) { |
| return false; // not reached |
| } |
| TestSEHChainSane(); |
| |
| // success. |
| return true; |
| } |
| |
| static bool TestChaining() { |
| EXCEPTION_REGISTRATION* top = GetTopRegistration(); |
| |
| return TestRegularChaining(top) && TestExceptionBarrierChaining(top); |
| } |
| |
| // Test that the SEH chain is unmolested by exception barrier, both under |
| // regular unroll, and under exception unroll. |
| TEST_F(ExceptionBarrierTest, SEHChainIsSaneAfterException) { |
| EXPECT_TRUE(TestChaining()); |
| } |
| |
| } // namespace |