Skip to content

Commit

Permalink
[Mac] Redo NSException handling, and generate better stacks for C++ e…
Browse files Browse the repository at this point in the history
…xceptions.

This introduces base::mac::CallWithEHFrame(), which will run a block in a
stack frame that is set up to stop searching for an exception handler when it
is reached. This is used to prevent a top-level exception handler, installed
in CFRunLoopRunSpecific(), from catching and rethrowing C++ exceptions. When
it does this, the resulting stack traces are not useful for triage purposes. By
stoping the search for an exception handler, the runtime will call
std::terminate directly from the throwing stack trace.

In addition, NSException handling is converted from swizzling the designated
initializer to an ObjC 2.0 exception preprocessor.

Some exceptions could still escape this mechanism, if they are thrown
without CallWithEHFrame() on the stack. This could happen if the exception
were thrown outside of the MessagePump's work sources or -sendEvent:. Possible
situations are things like the CFRunLoop block and Mach port callouts. This
shouldn't happen from Chromium code, so it would only affect system-thrown
exceptions. These exceptions would still be fatal, just with the less-useful
run loop stack.

To summarize: all uncaught exceptions are fatal one way or another. If the
exception isn't caught before it unwinds to CallWithEHFrame, the crash will have
an immediately actionable stack from the point of throw. If the exception is
caught by the run loop or some other top-level exception handler, the exception
will be rethrown and the crash stack will be less useful. In that case, if it's
an ObjC exception, a crash key will still record a trace from the point-of-
throw. Using try/catch is now unrestricted, and no special whitelist is
maintained to allow certain NSExceptions to be alloc/init'd.

BUG=503128
R=shess@chromium.org, thakis@chromium.org

Review URL: https://codereview.chromium.org/1212093002

Cr-Commit-Position: refs/heads/master@{#338332}
  • Loading branch information
rsesek authored and Commit bot committed Jul 10, 2015
1 parent 900658c commit 73dc3d5
Show file tree
Hide file tree
Showing 9 changed files with 374 additions and 337 deletions.
6 changes: 6 additions & 0 deletions base/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ component("base") {
"mac/bind_objc_block.h",
"mac/bundle_locations.h",
"mac/bundle_locations.mm",
"mac/call_with_eh_frame.cc",
"mac/call_with_eh_frame.h",
"mac/call_with_eh_frame_asm.S",
"mac/cocoa_protocols.h",
"mac/dispatch_source_mach.cc",
"mac/dispatch_source_mach.h",
Expand Down Expand Up @@ -833,6 +836,8 @@ component("base") {
"files/file_util_mac.mm",
"mac/bundle_locations.h",
"mac/bundle_locations.mm",
"mac/call_with_eh_frame.cc",
"mac/call_with_eh_frame.h",
"mac/foundation_util.h",
"mac/foundation_util.mm",
"mac/mac_logging.cc",
Expand Down Expand Up @@ -1203,6 +1208,7 @@ test("base_unittests") {
"lazy_instance_unittest.cc",
"logging_unittest.cc",
"mac/bind_objc_block_unittest.mm",
"mac/call_with_eh_frame_unittest.mm",
"mac/dispatch_source_mach_unittest.cc",
"mac/foundation_util_unittest.mm",
"mac/libdispatch_task_runner_unittest.cc",
Expand Down
1 change: 1 addition & 0 deletions base/base.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@
'lazy_instance_unittest.cc',
'logging_unittest.cc',
'mac/bind_objc_block_unittest.mm',
'mac/call_with_eh_frame_unittest.mm',
'mac/dispatch_source_mach_unittest.cc',
'mac/foundation_util_unittest.mm',
'mac/libdispatch_task_runner_unittest.cc',
Expand Down
4 changes: 4 additions & 0 deletions base/base.gypi
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,9 @@
'mac/bind_objc_block.h',
'mac/bundle_locations.h',
'mac/bundle_locations.mm',
'mac/call_with_eh_frame.cc',
'mac/call_with_eh_frame.h',
'mac/call_with_eh_frame_asm.S',
'mac/close_nocancel.cc',
'mac/cocoa_protocols.h',
'mac/dispatch_source_mach.cc',
Expand Down Expand Up @@ -875,6 +878,7 @@
['include', '^files/file_util_mac\\.'],
['include', '^file_version_info_mac\\.'],
['include', '^mac/bundle_locations\\.'],
['include', '^mac/call_with_eh_frame\\.'],
['include', '^mac/foundation_util\\.'],
['include', '^mac/mac_logging\\.'],
['include', '^mac/mach_logging\\.'],
Expand Down
37 changes: 37 additions & 0 deletions base/mac/call_with_eh_frame.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2015 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 "base/mac/call_with_eh_frame.h"

#include <unwind.h>

#include "build/build_config.h"

namespace base {
namespace mac {

_Unwind_Reason_Code CxxPersonalityRoutine(
int version,
_Unwind_Action actions,
uint64_t exceptionClass,
struct _Unwind_Exception* exceptionObject,
struct _Unwind_Context* context) {
// Tell libunwind that this is the end of the stack. When it encounters the
// CallWithEHFrame, it will stop searching for an exception handler. The
// result is that no exception handler has been found higher on the stack,
// and any that are lower on the stack (e.g. in CFRunLoopRunSpecific), will
// now be skipped. Since this is reporting the end of the stack, and no
// exception handler will have been found, std::terminate() will be called.
return _URC_END_OF_STACK;
}

#if defined(OS_IOS)
// No iOS assembly implementation exists, so just call the block directly.
void CallWithEHFrame(void (^block)(void)) {
block();
}
#endif

} // namespace mac
} // namespace base
26 changes: 26 additions & 0 deletions base/mac/call_with_eh_frame.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2015 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.

#ifndef BASE_MAC_CALL_WITH_EH_FRAME_H_
#define BASE_MAC_CALL_WITH_EH_FRAME_H_

#include "base/base_export.h"

namespace base {
namespace mac {

// Invokes the specified block in a stack frame with a special exception
// handler. This function creates an exception handling stack frame that
// specifies a custom C++ exception personality routine, which terminates the
// search for an exception handler at this frame.
//
// The purpose of this function is to prevent a try/catch statement in system
// libraries, acting as a global exception handler, from handling exceptions
// in such a way that disrupts the generation of useful stack traces.
void BASE_EXPORT CallWithEHFrame(void (^block)(void));

} // namespace mac
} // namespace base

#endif // BASE_MAC_CALL_WITH_EH_FRAME_H_
89 changes: 89 additions & 0 deletions base/mac/call_with_eh_frame_asm.S
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2015 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.

// base::mac::CallWithEHFrame(void () block_pointer)
#define CALL_WITH_EH_FRAME __ZN4base3mac15CallWithEHFrameEU13block_pointerFvvE

.section __TEXT,__text,regular,pure_instructions
#if !defined(COMPONENT_BUILD)
.private_extern CALL_WITH_EH_FRAME
#endif
.globl CALL_WITH_EH_FRAME
.align 4
CALL_WITH_EH_FRAME:

.cfi_startproc

// Configure the C++ exception handler personality routine. Normally the
// compiler would emit ___gxx_personality_v0 here. The purpose of this
// function is to use a custom personality routine.
.cfi_personality 155, __ZN4base3mac21CxxPersonalityRoutineEi14_Unwind_ActionyP17_Unwind_ExceptionP15_Unwind_Context
.cfi_lsda 16, CallWithEHFrame_exception_table

Lfunction_start:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp

// Load the function pointer from the block descriptor.
movq 16(%rdi), %rax

// Execute the block in the context of a C++ try{}.
Ltry_start:
callq *%rax
Ltry_end:
popq %rbp
ret

// Landing pad for the exception handler. This should never be called, since
// the personality routine will stop the search for an exception handler,
// which will cause the runtime to invoke the default terminate handler.
Lcatch:
movq %rax, %rdi
callq ___cxa_begin_catch // The ABI requires a call to the catch handler.
ud2 // In the event this is called, make it fatal.

Lfunction_end:
.cfi_endproc

// The C++ exception table that is used to identify this frame as an
// exception handler. See http://llvm.org/docs/ExceptionHandling.html and
// http://mentorembedded.github.io/cxx-abi/exceptions.pdf.
.section __TEXT,__gcc_except_tab
.align 2
CallWithEHFrame_exception_table:
.byte 255 // DW_EH_PE_omit
.byte 155 // DW_EH_PE_indirect | DW_EH_PE_pcrel | DW_EH_PE_sdata4
.asciz "\242\200\200" // LE int128 for the number of bytes in this table.
.byte 3 // DW_EH_PE_udata4
.byte 26 // Callsite table length.

// First callsite.
CS1_begin = Ltry_start - Lfunction_start
.long CS1_begin
CS1_end = Ltry_end - Ltry_start
.long CS1_end

// First landing pad.
LP1 = Lcatch - Lfunction_start
.long LP1
.byte 1 // Action record.

// Second callsite.
CS2_begin = Ltry_end - Lfunction_start
.long CS2_begin
CS2_end = Lfunction_end - Ltry_end
.long CS2_end

// Second landing pad (none).
.long 0
.byte 0 // No action.

// Action table.
.byte 1 // Action record 1.
.byte 0 // No further action to take.
.long 0 // No type filter for this catch(){} clause.
.align 2
53 changes: 53 additions & 0 deletions base/mac/call_with_eh_frame_unittest.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2015 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 "base/mac/call_with_eh_frame.h"

#import <Foundation/Foundation.h>

#include "testing/gtest/include/gtest/gtest.h"

namespace base {
namespace mac {
namespace {

class CallWithEHFrameTest : public testing::Test {
protected:
void ThrowException() {
[NSArray arrayWithObject:nil];
}
};

// Catching from within the EHFrame is allowed.
TEST_F(CallWithEHFrameTest, CatchExceptionHigher) {
bool __block saw_exception = false;
base::mac::CallWithEHFrame(^{
@try {
ThrowException();
} @catch (NSException* exception) {
saw_exception = true;
}
});
EXPECT_TRUE(saw_exception);
}

// Trying to catch an exception outside the EHFrame is blocked.
TEST_F(CallWithEHFrameTest, CatchExceptionLower) {
auto catch_exception_lower = ^{
bool saw_exception = false;
@try {
base::mac::CallWithEHFrame(^{
ThrowException();
});
} @catch (NSException* exception) {
saw_exception = true;
}
ASSERT_FALSE(saw_exception);
};
EXPECT_DEATH(catch_exception_lower(), "");
}

} // namespace
} // namespace mac
} // namespace base
44 changes: 29 additions & 15 deletions base/message_loop/message_pump_mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <limits>

#include "base/logging.h"
#include "base/mac/call_with_eh_frame.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/message_loop/timer_slack.h"
#include "base/run_loop.h"
Expand Down Expand Up @@ -300,7 +301,9 @@ explicit MessagePumpScopedAutoreleasePool(MessagePumpCFRunLoopBase* pump) :
// static
void MessagePumpCFRunLoopBase::RunWorkSource(void* info) {
MessagePumpCFRunLoopBase* self = static_cast<MessagePumpCFRunLoopBase*>(info);
self->RunWork();
base::mac::CallWithEHFrame(^{
self->RunWork();
});
}

// Called by MessagePumpCFRunLoopBase::RunWorkSource.
Expand Down Expand Up @@ -359,7 +362,9 @@ explicit MessagePumpScopedAutoreleasePool(MessagePumpCFRunLoopBase* pump) :
// static
void MessagePumpCFRunLoopBase::RunIdleWorkSource(void* info) {
MessagePumpCFRunLoopBase* self = static_cast<MessagePumpCFRunLoopBase*>(info);
self->RunIdleWork();
base::mac::CallWithEHFrame(^{
self->RunIdleWork();
});
}

// Called by MessagePumpCFRunLoopBase::RunIdleWorkSource.
Expand Down Expand Up @@ -393,7 +398,9 @@ explicit MessagePumpScopedAutoreleasePool(MessagePumpCFRunLoopBase* pump) :
// static
void MessagePumpCFRunLoopBase::RunNestingDeferredWorkSource(void* info) {
MessagePumpCFRunLoopBase* self = static_cast<MessagePumpCFRunLoopBase*>(info);
self->RunNestingDeferredWork();
base::mac::CallWithEHFrame(^{
self->RunNestingDeferredWork();
});
}

// Called by MessagePumpCFRunLoopBase::RunNestingDeferredWorkSource.
Expand Down Expand Up @@ -440,15 +447,16 @@ explicit MessagePumpScopedAutoreleasePool(MessagePumpCFRunLoopBase* pump) :
CFRunLoopActivity activity,
void* info) {
MessagePumpCFRunLoopBase* self = static_cast<MessagePumpCFRunLoopBase*>(info);

// Attempt to do some idle work before going to sleep.
self->RunIdleWork();

// The run loop is about to go to sleep. If any of the work done since it
// started or woke up resulted in a nested run loop running,
// nesting-deferred work may have accumulated. Schedule it for processing
// if appropriate.
self->MaybeScheduleNestingDeferredWork();
base::mac::CallWithEHFrame(^{
// Attempt to do some idle work before going to sleep.
self->RunIdleWork();

// The run loop is about to go to sleep. If any of the work done since it
// started or woke up resulted in a nested run loop running,
// nesting-deferred work may have accumulated. Schedule it for processing
// if appropriate.
self->MaybeScheduleNestingDeferredWork();
});
}

// Called from the run loop.
Expand All @@ -463,7 +471,9 @@ explicit MessagePumpScopedAutoreleasePool(MessagePumpCFRunLoopBase* pump) :
// level did not sleep or exit, nesting-deferred work may have accumulated
// if a nested loop ran. Schedule nesting-deferred work for processing if
// appropriate.
self->MaybeScheduleNestingDeferredWork();
base::mac::CallWithEHFrame(^{
self->MaybeScheduleNestingDeferredWork();
});
}

// Called from the run loop.
Expand Down Expand Up @@ -498,15 +508,19 @@ explicit MessagePumpScopedAutoreleasePool(MessagePumpCFRunLoopBase* pump) :
// to sleep or exiting. It must be called before decrementing the
// value so that the value still corresponds to the level of the exiting
// loop.
self->MaybeScheduleNestingDeferredWork();
base::mac::CallWithEHFrame(^{
self->MaybeScheduleNestingDeferredWork();
});
--self->nesting_level_;
break;

default:
break;
}

self->EnterExitRunLoop(activity);
base::mac::CallWithEHFrame(^{
self->EnterExitRunLoop(activity);
});
}

// Called by MessagePumpCFRunLoopBase::EnterExitRunLoop. The default
Expand Down
Loading

0 comments on commit 73dc3d5

Please sign in to comment.