2

I'm running into a situation in iOS (and OS X) where exceptions from NSKeyedUnarchiver cause binary-only third-party frameworks to crash my app (when deployed in the field) when they try to unarchive a corrupt archive, thus forcing users to delete and reinstall the app. This doesn't happen often, but I'd like that number to be zero.

I can't solve the problem by wrapping the NSKeyedUnarchiver calls, both because I don't have the source code and because those calls are not the direct result of anything that my code does; they run on arbitrary background threads at arbitrary times.

I'm currently swizzling the NSKeyedUnarchiver class so that reading a corrupt archive returns nil (as though the file were not there) rather than throwing an exception, but I can't be certain whether any of those third-party frameworks might do things correctly (with an @try/@catch block) and might break in interesting ways if I do so.

It would be helpful if I could somehow examine the Objective-C exception handling tree (or equivalent) to determine whether an exception handler would catch an exception if thrown, and if so, which handler. That way, my patched method could return nil if the exception would make it all the way up to Crashlytics (which would rethrow it, causing a crash), but could rethrow the exception if some other handler would catch it.

Is such a thing possible, and if so, how?

dgatwood
  • 10,129
  • 1
  • 28
  • 49
  • Did you try with Xcode http://stackoverflow.com/questions/17802662/exception-breakpoint-in-xcode ? – tuledev Aug 20 '15 at 17:31
  • I think the real problem that needs to be solved is having a corrupt archive. – gnasher729 Aug 20 '15 at 18:19
  • When an app has over a million active users, you're guaranteed to get at least one or two failures of NSKeyedArchiver in a month just from random bad luck (flaky RAM, glitchy CPUs, cosmic rays, bugs in the API, random corruption of malloc's memory pools, defective flash blocks, etc.). So guaranteeing that the archives aren't corrupt isn't really practical (and because I'm working with closed-source frameworks, I can't even get a list of all the possible archives to pre-scan them for damage). – dgatwood Aug 20 '15 at 23:21
  • The issue is not one of catching the exception during debugging; the issue is trying to ensure that real users in the field won't find themselves unable to launch the app because of some random piece of data used by some random ad API that isn't critical to the application's function. Basically, I'm trying to minimize the user-experienced pain caused by some badly written third-party code. – dgatwood Aug 20 '15 at 23:24

2 Answers2

1

Why not wrap your exception-throwing callsite in a try/catch/finally?

    @try {
      //call to your third party unarchiver
    }

    @catch {
      //remove your corrupted archive
    }

    @finally {
      //party
    }

Rolling your own global exception handler may also be of use here, ala: How do you implement global iPhone Exception Handling?

Community
  • 1
  • 1
Keller
  • 17,051
  • 8
  • 55
  • 72
  • Because I have no control over the code that is calling NSKeyedUnarchiver. The code in question is inside large binary blobs from a third party. The best I can do is to swizzle the methods in NSKeyedUnarchiver to prevent them from throwing exceptions (wrapping the original method in an @try block), but that still doesn't help if any of those callers are expecting to get an exception. – dgatwood Aug 21 '15 at 23:05
  • Also, AFAIK, a global exception handler also won't help, because by the time that code executes, the stack has already unwound, and the code that triggered the exception is no longer executing, so there's no way to make the binary-only third-party code continue executing as though the file weren't there (short of doing something truly unholy). – dgatwood Aug 21 '15 at 23:24
0

If you're not sure that wrapping third-party library code with @try/@catch is good enough you can hook NSKeyedUnarchiver methods to replace them with exact same wrapper thus making sure that exception is never gets thrown outside. Here is pseudo-code:

@try {
  //call original NSKeyedUnarchiver implementation
}
@catch {
  return nil;
}

Objc runtime has public APIs that can do such a thing

creker
  • 9,400
  • 1
  • 30
  • 47
  • I'm already swizzling NSKeyedUnarchiver to catch the exception and return nil. The question is whether there's a way for me to detect whether there's already an @try block in the exception handling stack, so that if one of those callers (which I have no direct control over) is actually expecting to get an exception, I can throw one instead of just returning nil, to avoid breaking binary compatibility. – dgatwood Aug 21 '15 at 23:08
  • And just to clarify, I can't wrap the third-party library code at all. My code isn't in the backtrace for the crashes I'm trying to fix. Those libraries schedule these operations on various run loops at various random times. Swizzling is literally the only way to fix it. I'm just hoping I can detect whether the callers have a proper @try block in place so that my changes to the method's behavior can be limited to the situations where not doing so would result in a crash. – dgatwood Aug 21 '15 at 23:13
  • You can disassemble the library and found out which objc methods inside it expect exception. Hook them and somehow let hooked `NSKeyedUnarchiver` method know that it shouldn't suppress exceptions. For example, through a global variable. – creker Aug 21 '15 at 23:59
  • Other than that there's no other easy way to do it. Of course you can think of detecting @try blocks at runtime by examining surrounding code in the call stack. But that seems to difficult to do properly. You either have to examine caller's code by examining predefined region of bytes which may easily break (region may be too small so you don't find anything or too big and you find something in another procedure). Or you need to somehow detect caller function boundaries by analyzing assembly to detect typical prologue and epilogue. And to make matter worse, there's also thumb mode – creker Aug 22 '15 at 00:12
  • I'm assuming something down in the Objective-C runtime has to know where all those try blocks are; after all, different try blocks can catch exceptions with different specificity, so it can't just automatically jumps to the most recent destination address; there must be some sort of stack. Either that or the code that starts a try block must have some way of finding out where the next higher block is so that it can longjmp to it when it decides not to catch a particular exception type. Either way, it seems like it should be possible to hook into the same data, at least in theory. – dgatwood Aug 22 '15 at 01:11
  • Yes, forgot about that but that certainly not an easy way. I would start by examining the disassembly. Finding `NSKeyedUnarchiver` callers and @try blocks around them will be easy. May be there are no @try blocks. After all, they're very rare in Objc-C code. – creker Aug 22 '15 at 01:30