I'm intentionally causing an EXC_BAD_ACCESS
. By triggering a write to an NSObject
in a read-only virtual memory page. Ideally, I'd like to catch EXC_BAD_ACCESS
, mark the virtual memory page as read-write and have execution continue as it normally would have. Is this even possible? The code I've written to cause the EXC_BAD_ACCESS
is below.
WeakTargetObject.h (ARC)
@interface WeakTargetObject : NSObject
@property (nonatomic, weak) NSObject *target;
@end
WeakTargetObject.m (ARC)
@implementation WeakTargetObject
@end
main.m (MRR)
- (void)main {
char *mem = NULL;
vm_allocate(mach_task_self(), (vm_address_t *)&mem, vm_page_size, VM_FLAGS_ANYWHERE);
NSLog(@"mem: %p", mem);
WeakTargetObject *weakTargetObject = objc_constructInstance([WeakTargetObject class], (void *)mem);
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSObject *target = [[NSObject alloc] init];
weakTargetObject.target = target;
[pool drain];
pool = [[NSAutoreleasePool alloc] init];
NSLog(@"expect non-nil. weakTargetObject.target: %@", weakTargetObject.target);
[pool drain];
vm_protect(mach_task_self(),
(vm_address_t)mem,
vm_page_size,
1,
VM_PROT_READ);
// triggers EXC_BAD_ACCESS when objc runtime
// tries to nil weakTargetObject.target
[weakTargetObject release];
NSLog(@"expect nil. weakTargetObject.target: %@", weakTargetObject.target);
}
I found a darwin-dev post that has the answer!
WARNING
This answer has a major downside. My debugger wouldn't work in any thread other than the mach exception thread. Putting a breakpoint in any other thread caused Xcode5 to hang. I had to force-quit it. Inside my catch_exception_raise
function, it worked fine. I asked the LLDB folks about this.
END WARNING
This code is the skeleton of the answer. It will infinite loop, because (according to the follow-up) you need to do something to make the error recoverable. In my case, I need to mark the page as read-write.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <stdarg.h>
#include <pthread.h>
#include <assert.h>
#include <mach/mach.h>
kern_return_t
catch_exception_raise(mach_port_t exception_port,
mach_port_t thread,
mach_port_t task,
exception_type_t exception,
exception_data_t code_vector,
mach_msg_type_number_t code_count)
{
fprintf(stderr, "catch_exception_raise %d\n", exception);
return KERN_SUCCESS; // loops infinitely...
}
void *exception_handler(void *arg)
{
extern boolean_t exc_server();
mach_port_t port = (mach_port_t) arg;
mach_msg_server(exc_server, 2048, port, 0);
abort(); // without this GCC complains (it doesn't know that mach_msg_server never returns)
}
void setup_mach_exception_port()
{
static mach_port_t exception_port = MACH_PORT_NULL;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &exception_port);
mach_port_insert_right(mach_task_self(), exception_port, exception_port, MACH_MSG_TYPE_MAKE_SEND);
task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, exception_port, EXCEPTION_DEFAULT, MACHINE_THREAD_STATE);
pthread_t returned_thread;
pthread_create(&returned_thread, NULL, exception_handler, (void*) exception_port);
}
void test_crash()
{
id *obj = NULL;
*obj = @"foo";
}
int main(int argc, char** argv)
{
setup_mach_exception_port();
test_crash();
return 0;
}
This is my new code that works:
WeakTargetObject.h (ARC)
@interface WeakTargetObject : NSObject
@property (nonatomic, weak) NSObject *target;
@end
WeakTargetObject.m (ARC)
@implementation WeakTargetObject
@end
main.m (MRR)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <stdarg.h>
#include <pthread.h>
#include <assert.h>
#include <mach/mach.h>
static char * mem = NULL;
kern_return_t
catch_exception_raise(mach_port_t exception_port,
mach_port_t thread,
mach_port_t task,
exception_type_t exception,
exception_data_t code_vector,
mach_msg_type_number_t code_count)
{
fprintf(stderr, "catch_exception_raise %d, mem: %p\n", exception, mem);
kern_return_t success = vm_protect(mach_task_self(),
(vm_address_t)mem,
vm_page_size,
0,
VM_PROT_DEFAULT);
fprintf(stderr, "switched to read-write: %d\n", success);
return KERN_SUCCESS;
}
void *exception_handler(void *arg)
{
extern boolean_t exc_server();
mach_port_t port = (mach_port_t) arg;
mach_msg_server(exc_server, 2048, port, 0);
abort(); // without this GCC complains (it doesn't know that mach_msg_server never returns)
}
void setup_mach_exception_port()
{
static mach_port_t exception_port = MACH_PORT_NULL;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &exception_port);
mach_port_insert_right(mach_task_self(), exception_port, exception_port, MACH_MSG_TYPE_MAKE_SEND);
task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, exception_port, EXCEPTION_DEFAULT, MACHINE_THREAD_STATE);
pthread_t returned_thread;
pthread_create(&returned_thread, NULL, exception_handler, (void*) exception_port);
}
- (void)main {
setup_mach_exception_port();
vm_allocate(mach_task_self(), (vm_address_t *)&mem, vm_page_size, VM_FLAGS_ANYWHERE);
NSLog(@"mem: %p", mem);
WeakTargetObject *weakTargetObject = objc_constructInstance([WeakTargetObject class], (void *)mem);
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSObject *target = [[NSObject alloc] init];
weakTargetObject.target = target;
[pool drain];
pool = [[NSAutoreleasePool alloc] init];
NSLog(@"expect non-nil. weakTargetObject.target: %@", weakTargetObject.target);
[pool drain];
vm_protect(mach_task_self(),
(vm_address_t)mem,
vm_page_size,
// zero means don't set VM_PROT_READ as the maximum protection
// one means DO set VM_PROT_READ as the maximum protection
// we want zero because the if VM_PROT_READ is the maximum protection
// we won't be able to set it to VM_PROT_DEFAULT later
0,
VM_PROT_READ);
// triggers EXC_BAD_ACCESS when objc runtime
// tries to nil weakTargetObject.target
[weakTargetObject release];
NSLog(@"expect nil. weakTargetObject.target: %@", weakTargetObject.target);
}