| 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
|
|
|