12:43 Sat, 06 Dec 2008 PST -0800

iPhone: Safari Bookbag + Private API

I occasionally use O'Reilly Publishing's Bookbag iPhone application to read my Safari Bookshelf books when I'm away from my computer (Safari Bookbag has no relation to Apple's Safari browser). Last night I was suprised to find that the Bookbag application was crashing on launch. Checking the app store application description, I found this note from O'Reilly:

This version does not work with iPhone OS v2.2. We are working on a
2.2-compatible version and will be submitting soon.

That's odd -- the most likely cause of an application breaking on a new iPhoneOS release would be if they used private API. Taking a look at the iPhone crash log, that appears to be the case:

Process:         Bookbag [2386]
[snip]
Thread 0 Crashed:
0   libSystem.B.dylib               0x31459c58 __kill + 8
1   libSystem.B.dylib               0x31459c46 kill + 4
2   libSystem.B.dylib               0x31459c3a raise + 10
3   libSystem.B.dylib               0x31474424 abort + 36
[snip]
8   libobjc.A.dylib                 0x300c1f84 objc_exception_throw + 92
9   CoreFoundation                  0x302c883e -[NSObject doesNotRecognizeSelector:] + 106
10  CoreFoundation                  0x30287222 ___forwarding___ + 490
11  CoreFoundation                  0x3026d618 _CF_forwarding_prep_0 + 40
12  Bookbag                         0x0000406c 0x1000 + 12396
13  Bookbag                         0x0000463a 0x1000 + 13882
14  Bookbag                         0x00004402 0x1000 + 13314

We can see here that Bookbag calls some method that triggers a call to -[NSObject doesNotRecognizeSelector:]. The 'doesNotRecognizeSelector:' message is sent by the Objective-C runtime when an object receives a message it can't respond to or forward. The doesNotRecognizeSelector method raises an exception, and the application terminates.

Objective-C methods don't simply disappear from an application, but they can disappear from a library. Private APIs are private because they may be modified or removed at any time -- if a method disappeared from one of the iPhoneOS libraries, chances are it was a private method.

To be sure that Bookbag really is using private API -- and to determine what API they used -- I decided to strip the DRM from the Bookbag application and disassemble their binary. Thanks to the crash log, we already know where to look -- address 0x406c, the last return address located in the Bookbag binary before doesNotRecognizeSelector: was triggered.

Apple's iPhone Application DRM utilizes Mach-O binary encryption to protect the application code. Briefly put, application decryption is handled by the kernel -- within a process space, the encrypted section of its Mach-O binary is automatically decrypted by the kernel as it is read. To strip the DRM from an iPhone app, one can simply attach a debugger to the application on a jailbroken phone, and dump the text section containing the program code. For a more comprehensive description of Mach-O binary encryption, read Amit Singh's article: Understanding Apple's Binary Protection in Mac OS X.

So after stripping the DRM (let's only use this power for good, not evil, yes?), I can disassemble the relevant Bookbag code and determine what private API is being called to trigger the crash. Here's my abridged annotated disassembly. (see unabridged)

-[CoverFlowView initWithFrame:andCount:]: 
[snip]
0x4050            1c22        mov     r2, r4          (add r2, r4, #0)
0x4052            ca0b        ldmia   r2!,{r0, r1, r3}
0x4054            c50b        stmia   r5!,{r0, r1, r3}
// argument 1 - saved pointer to @selector(initWithFrame:numberOfCovers:)
0x4056            4651        mov     r1, r10
0x4058            9b09        ldr     r3, [sp, #36]
0x405a            9a20        ldr     r2, [sp, #128]
0x405c            9311        str     r3, [sp, #68]
0x405e            9202        str     r2, [sp, #8]
0x4060            ab10        add     r3, sp, #64
0x4062            466a        mov     r2, sp
// argument 0 - UICoverFlowLayer instance
0x4064            4658        mov     r0, r11
0x4066            cb30        ldmia   r3!,{r4, r5}
0x4068            c230        stmia   r2!,{r4, r5}
// argument 2 - value of -[UIScreen frame]
0x406a            9a0e        ldr     r2, [sp, #56]
// argument 3 - number of covers
0x406c            9b0f        ldr     r3, [sp, #60]
// Call -[UICoverFlowLayer initWithFrame:numberOfCovers] (UICoverFlow is private API)
0x406e        efa8f006        blx     0xafc0  ; symbol stub for: _objc_msgSend

It's clear from the disassembly that Bookbag is using the private UICoverFlowLayer API. When Apple released iPhoneOS 2.2, the -[UICoverFlowLayer initWithFrame:numberOfCovers:] method was removed, and Safari Bookbag started crashing.

Safari Bookbag's use of UICoverFlowLayer is a discovery that has caused me some chagrin -- I held off on implementing Peeps -- which includes a fully functional CoverFlow clone -- until I saw that Apple had approved Safari Bookbag. I had assumed the authors of Bookbag implemented their own CoverFlow, too. To increase my vexation, Apple still hasn't approved Peeps nearly a month after it was submitted for review (on November 9th).

Lastly, this accutely demonstrates why one should be incredibly circumspect about using undocumented API -- it's undocumented because it can change at any time, and if it changes, your application will very likely start crashing. Even if your app slips through the cracks in Apple's review system, users will notice when your app breaks (see the Safari Bookbag reviews).

[/code/iphone] permanent link