Index: chrome_frame/test/exception_barrier_unittest.cc |
=================================================================== |
--- chrome_frame/test/exception_barrier_unittest.cc (revision 0) |
+++ chrome_frame/test/exception_barrier_unittest.cc (revision 0) |
@@ -0,0 +1,361 @@ |
+// 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(0, ::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(0, ::VirtualQuery(curr->handler, &info, sizeof(info))); |
+ wchar_t module_filename[MAX_PATH]; |
+ ASSERT_NE(0, ::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() { |
+ ExceptionBarrier 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 ExceptionBarrierHandler, |
+// then crash to invoke it. |
+__declspec(naked) void CrashOnManualSEHBarrierHandler() { |
+ __asm { |
+ push ExceptionBarrierHandler |
+ push FS:0 |
+ mov FS:0, esp |
+ call AccessViolationCrash |
+ ret |
+ } |
+} |
+#pragma warning(pop) |
+ |
+class ExceptionBarrierTest: public testing::Test { |
+public: |
+ ExceptionBarrierTest() : old_handler_(NULL) { |
+ } |
+ |
+ // 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() { |
+ old_handler_ = ExceptionBarrier::handler(); |
+ ExceptionBarrier::set_handler(ExceptionHandler); |
+ s_handled_ = false; |
+ |
+ TestSEHChainSane(); |
+ } |
+ |
+ virtual void TearDown() { |
+ ExceptionBarrier::set_handler(old_handler_); |
+ |
+ TestSEHChainSane(); |
+ } |
+ |
+ // The exception notification callback, sets the handled flag. |
+ static void CALLBACK ExceptionHandler(EXCEPTION_POINTERS* ptrs) { |
+ TestSEHChainSane(); |
+ |
+ s_handled_ = true; |
+ } |
+ |
+ // Old handler to restore on TearDown |
+ ExceptionBarrier::ExceptionHandler old_handler_; |
+ |
+ // Flag is set by handler |
+ static bool s_handled_; |
+}; |
+ |
+bool ExceptionBarrierTest::s_handled_; |
+ |
+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) { |
+ ExceptionBarrier 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 |