// 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(&info); // the very top of our stack segment EXCEPTION_REGISTRATION* top = reinterpret_cast( reinterpret_cast(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(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(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(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