Replace -[NSKeyedUnarchiver unarchiveObjectWithData:] so it doesn't crash on corrupt archive

  • If -[NSKeyedUnarchiver unarchiveObjectWithData:] is handed a corrupt
    archive, it raises an exception AND crashes the program.  I find this
    to be very annoying since, almost always, archives come from files or
    network sources where corruption is possible.  Therefore, I have
    always protected this invocation in a try/catch block.

    As I was about to do this for about the fifth time this year, I
    decided to use the Method Replacement feature of Leopard to replace
    this method, once and for all, with one that wouldn't crash.  I
    understand that Method Replacement should not be done casually because
    any plug-in code and even Cocoa itself will use the replaced method.
    However, I can't think of any usage where  a crash would be required
    behavior.

    It seems to work fine, after 10 minutes of testing.  Does anyone see
    any problem with this?

    Sincerely,

    Jerry Krinock

    #import <Cocoa/Cocoa.h>

    /*!
      @brief    Improvements to NSKeyedUnarchiver

      @details  Method +unarchiveObjectWithData: has been replaced so that
      instead of raising an exception and crashing if given a corrupt
    archive,
      it just returns nil.  Also, another method has been added which
      returns the exception.
    */
    @interface NSKeyedUnarchiver (CatchExceptions)

    /*!
      @brief    Like unarchiveObjectWithData:, except it returns the
      exception by reference.

      @param    exception_p  Pointer which will, upon return, if an
      exception occurred and said pointer is not NULL, point to said
      NSException.
      */
    + (id)unarchiveObjectWithData:(NSData*)data
                      exception_p:(NSException**)exception_p ;

    @end

    #import "NSKeyedUnarchiver+CatchExceptions.h"
    #import <objc/runtime.h>

    @implementation NSKeyedUnarchiver (CatchExceptions)

    + (id)unarchiveObjectWithData:(NSData*)data
                      exception_p:(NSException**)exception_p {
        id object = nil ;

        @try {
            // Note: Since methods were swapped, this is invoking the
    original method
            object = [NSKeyedUnarchiver
    replacement_unarchiveObjectWithData:data] ;
        }
        @catch (NSException* exception) {
            if (exception_p) {
                *exception_p = exception ;
            }
        }
        @finally{
        }

        return object ;
    }

    + (void)load {
        // Swap the implementations of +unarchiveObjectWithData: and
    +replacement_unarchiveObjectWithData:.
        // When the +unarchiveObjectWithData: message is sent to the
    NSKeyedUnarchiver class object,
        // +replacement_unarchiveObjectWithData: will be invoked
    instead.  Conversely,
        // +replacement_unarchiveObjectWithData: will invoke
    +unarchiveObjectWithData:.
        Method originalMethod = class_getClassMethod(self,
    @selector(unarchiveObjectWithData:)) ;
        Method replacedMethod = class_getClassMethod(self,
    @selector(replacement_unarchiveObjectWithData:)) ;
        method_exchangeImplementations(originalMethod, replacedMethod) ;
    }

    + (id)replacement_unarchiveObjectWithData:(NSData*)data {
        return [self unarchiveObjectWithData:data
                                  exception_p:NULL] ;
    }

    @end

    TEST CODE:

    int main (int argc, const char * argv[]) {
        NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

        // Try to unarchive your bash profile
        // This ain't gonna be able to unarchive
        NSData* data = [NSData dataWithContentsOfFile:[NSHomeDirectory()
    stringByAppendingPathComponent:@".bash_profile"]] ;
        NSLog(@"Your bash profile data is %d bytes.", [data length]) ;

        id whatever ;

        // Try it using the normal method
        whatever = [NSKeyedUnarchiver unarchiveObjectWithData:data] ;
        NSLog(@"1.  unarchived whatever = %@", whatever) ;

        // Try it using the improved method that returns the exception:
        NSException** exception ;
        whatever = [NSKeyedUnarchiver unarchiveObjectWithData:data
                                                  exception_p:&exception] ;
        NSLog(@"2.  unarchived whatever = %@", whatever) ;
        NSLog(@"exception = %@", exception) ;

        [pool release] ;
    }
  • On Thu, Jul 2, 2009 at 11:17 PM, Jerry Krinock<jerry...> wrote:
    > If -[NSKeyedUnarchiver unarchiveObjectWithData:] is handed a corrupt
    > archive, it raises an exception AND crashes the program.  I find this to be
    > very annoying since, almost always, archives come from files or network
    > sources where corruption is possible.  Therefore, I have always protected
    > this invocation in a try/catch block.
    >
    > As I was about to do this for about the fifth time this year, I decided to
    > use the Method Replacement feature of Leopard to replace this method, once
    > and for all, with one that wouldn't crash.  I understand that Method
    > Replacement should not be done casually because any plug-in code and even
    > Cocoa itself will use the replaced method.  However, I can't think of any
    > usage where  a crash would be required behavior.
    >
    > It seems to work fine, after 10 minutes of testing.  Does anyone see any
    > problem with this?

    Is there a reason you can't just implement a new method in a category
    and not replace the existing one? It will be just as safe in your
    code, just as easy to use, and it won't run the potential risk of
    screwing up code that you don't control.

    Mike
  • On 2009 Jul 02, at 21:11, Michael Ash wrote:

    > Is there a reason you can't just implement a new method in a category
    > and not replace the existing one? It will be just as safe in your
    > code, just as easy to use,

    I'd considered doing that and yes it could be done.  But then it would
    have a different method name that I'd have to remember to use instead
    of -unarchiveObjectWithData:.

    Replacing a Cocoa method seems like it should only be done if the
    original method shipped by Apple is buggy.  I believe that's the case
    here.  The only behavior I'm losing is a potential crash.

    I suppose Method Replacement has disadvantages too.  I have to
    remember to include this method replacement file in any new project I
    start, or else I'll get the old crashy behavior, since I'll be out of
    the habit of using try/catch.

    > and it won't run the potential risk of screwing up code that you
    > don't control.

    Interesting -- but the only "screwing up" I'm doing is allowing it to
    execute beyond a point where it otherwise would have crashed.  I
    suppose that if someone is using this crashing "feature" of -
    unarchiveObjectWithData: as some kind of self-destruct security
    mechanism, I could be introducing a security hole  :))

    Anyhow, I get your point.

    Thanks, Michael.
  • On Thu, Jul 2, 2009 at 11:17 PM, Jerry Krinock <jerry...> wrote:

    If -[NSKeyedUnarchiver unarchiveObjectWithData:] is handed a corrupt
    > archive, it raises an exception AND crashes the program.

    Do you have a sample app & data combo which demonstrates the crash?

    +[NSKeyedUnarchiver unarchiveObjectWithData:] is documented to raise an
    exception when feed an incomprehensible archive.

    If you have no exception handler in place, the program will terminate. But
    that isn't due to a bug in NSKeyedUnarchiver - it is just working as
    advertised - whether or not you like the advertised behavior.

    - Jim
  • On 2009 Jul 02, at 22:53, Jim Correia wrote:

    > Do you have a sample app & data combo which demonstrates the crash?

    Yes, if you take the TEST CODE at the bottom of my original post and
    revert to Apple's method by commenting out the +[NSKeyedUnarchiver
    load] method in there, it will crash.

    > If you have no exception handler in place, the program will
    > terminate. But that isn't due to a bug in NSKeyedUnarchiver - it is
    > just working as advertised - whether or not you like the advertised
    > behavior.

    Indeed, if I enclose it in a @try block, then it will not crash.  But
    I think it's error-prone and inconvenient to have to enclose it in a
    @try block.

    Is that what you mean by "exception handler in place"?  --  enclose it
    in a @try block ?
  • On Fri, Jul 3, 2009 at 1:42 AM, Jerry Krinock<jerry...> wrote:
    >
    > On 2009 Jul 02, at 21:11, Michael Ash wrote:
    >
    >> Is there a reason you can't just implement a new method in a category
    >> and not replace the existing one? It will be just as safe in your
    >> code, just as easy to use,
    >
    > I'd considered doing that and yes it could be done.  But then it would have
    > a different method name that I'd have to remember to use instead of
    > -unarchiveObjectWithData:.
    >
    > Replacing a Cocoa method seems like it should only be done if the original
    > method shipped by Apple is buggy.  I believe that's the case here.  The only
    > behavior I'm losing is a potential crash.

    Well, that's wrong. The behavior you're losing is that of throwing an
    exception on error. It is entirely possible that Apple's code relies
    on this method throwing an exception on error. As your own code
    demonstrates, you can catch the exception in question without
    crashing.

    You're not fixing a bug, unless you consider throwing an exception to
    always be a bug AND you consider any code which relies on catching an
    exception to likewise be a bug. You're changing existing non-buggy
    behavior. That's a bad thing to do unless you have a VERY good reason,
    and IMHO "a different method name that I'd have to remember to use" is
    a pretty crappy reason for this.

    Mike
  • On 2009 Jul 03, at 05:50, Michael Ash wrote:

    > It is entirely possible that Apple's code relies
    > on this method throwing an exception on error.

    Ah, you're correct.  So I've changed it to be a regular category
    method, +unarchiveObjectSafelyWithData:, instead of replacing the
    method.