iosautomatic-ref-countingsignalsvirtual-memorymach

How do I recover from EXC_BAD_ACCESS?


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);
}

Solution

  • 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);
    }