Tag: Apple

  • When I was building Dashman, I hit a wall that Apple’s paid developer support told me was unsolvable. Then I solved it by reading the binaries in assembly until I understood them well enough to swap them out.

    A note up front: this happened a long time ago and I’m writing it from memory. The shape of the work is right; small specifics, API names, and exact sequence of events may be slightly off.

    What was Dashman?

    Dashman was a B2B SaaS for displaying business dashboards (which are, technically, just web pages) on the screens around an office. An early version of it was a native macOS app with an embedded browser. That’s the version this post is about.

    The problem

    On macOS, an embedded browser meant Apple’s WKWebView, which shared a single, system-wide cookie jar with Safari.

    That was a non-starter. Dashman needed to be logged into the dashboards it displayed, typically with accounts different from the user’s: Google Analytics, AWS consoles, internal admin tools. Mixing Dashman’s and Safari’s cookies would have caused constant overwrites in both directions. The longer-term goal was stricter still: one cookie jar per dashboard, so the same install could display two accounts of the same web application side by side.

    Long story short, I needed full control of the cookie jar. Other forms of local storage existed by then but were still nascent; the cookies were where the leakage actually happened.

    I paid Apple’s developer support fee and asked. The reply was friendly, brief, and unambiguous:

    After investigating whether different WKWebViews can each have their own cookie jar the answer is no they cannot — they all share a single container. If you feel that this is something that you need for your application, please file an enhancement request here:

    Challenge accepted, Mr Apple.

    Reading the cookie jar in assembly

    The macOS cookie jar is NSHTTPCookieStorage, a Foundation class with a small public API: get cookies, set cookies, delete by URL, the usual. Underneath, it talks to CFNetwork, the C framework Foundation wraps. The implementation I’d need to replace lived in a binary I didn’t have source for.

    So I opened CFNetwork.framework in Hopper Disassembler and started reading.

    Disassembled CFNetwork in Hopper, showing the implementation of -[NSHTTPCookieStorageInternal initInternalWithCFStorage:] and the symbol list of every NSHTTPCookieStorage and NSHTTPCookieStorageInternal method.

    I sometimes wonder what this would look like on Apple Silicon today: ARM instead of x86, Swift sneaking into Apple’s frameworks. Different bytes, presumably the same archaeology.

    What the disassembly revealed was a three-layer onion. The public class, NSHTTPCookieStorage, was a thin façade. Most of its methods forwarded to an internal Objective-C class, NSHTTPCookieStorageInternal, that held the actual state behind an NSRecursiveLock. That class in turn called down into a family of plain C functions named _CFHTTPCookieStorageCreateDefault, _CFHTTPCookieStorageGetClass, _CFHTTPCookieStorageCreateInMemory, and friends. Foundation wrapper on top, private Objective-C in the middle, C core at the bottom.

    Three-layer diagram of Apple's cookie storage stack, with the public Foundation façade above the private internal class and the CFNetwork store shared with Safari.

    The disassembly told me five things at once. Which selectors did real work. Which were forwarded. Which were vestigial. Where cookies actually got persisted. And, as a bonus, the error strings the binary fell back to when something went wrong: literal byte sequences like Cannot get default cookie store - using a memory store for this process were sitting right there in the data segment, telling me which failure modes Apple’s engineers had anticipated and which they hadn’t.

    It was, honestly, fun. Hopper’s cross-references made it possible to climb up from any C function to every Objective-C selector that called it, and from any selector back down to the persistence calls. After a while I had a mental map of which methods I’d need to satisfy and which I could safely ignore.

    There’s no symbols in the source, which made this hard. Method names survive (Objective-C runtime needs them) but everything else is x86 instructions. What looks in the source like if (self.policy == NSHTTPCookieAcceptPolicyAlways) shows up as a cmp against an immediate, with a jne to a label that another four objc_msgSend calls eventually reveal as the rejection branch. You build the abstraction back up by hand.

    The replacement: a private cookie jar

    What I built was a single Objective-C class, CAHTTPCookieStorage, that implemented the subset of NSHTTPCookieStorage‘s interface that real callers actually used. Cookies live in a dictionary keyed by a composite of (domain, path, name), all lowercased, so duplicates collapse on insert:

    - (NSUInteger)hash {
    return self.domain.hash ^ self.path.hash ^ self.name.hash;
    }
    - (BOOL)isEqual:(id)other {
    if (![other isKindOfClass:[CookieKey class]]) {
    return NO;
    }
    CookieKey *otherCookieKey = (CookieKey *)other;
    return [self.domain isEqual: otherCookieKey.domain]
    && [self.path isEqual: otherCookieKey.path]
    && [self.name isEqual: otherCookieKey.name];
    }

    The lowercasing matters more than it looks. The HTTP cookie spec is case-insensitive on domains and paths in some places and case-sensitive in others, and NSHTTPCookie itself doesn’t normalise consistently. Without the lowercase hash, you can end up with two distinct entries for www.example.org and WWW.EXAMPLE.ORG and behaviour that diverges from Safari’s depending on which one you set last. The test suite (next section) caught this kind of thing immediately.

    Persistence is NSKeyedArchiver to a path under ~/Library/Application Support/<bundle>/Cookies, with the directory created lazily on first write. If the file is corrupted on read we surface it via a NSLog and start with an empty jar rather than crashing the host process. That seems obvious in retrospect; it wasn’t obvious the first time the archive deserialiser threw an exception three frames deep into application launch.

    Domain matching is the one piece that always trips people up. Cookies with a leading dot match subdomains; without a leading dot, they only match exactly. The whole rule collapses to a single ternary once you accept that:

    + (BOOL)match:(NSURL *)url toDomain:(NSString *)domain {
    NSString *host = [[url host] lowercaseString];
    if (host != nil) {
    return [domain hasPrefix:@"."]
    ? ([[@"." stringByAppendingString: host] hasSuffix: domain])
    : [host isEqualToString:domain];
    } else {
    return false;
    }
    }

    Expiry I made lazy. Every call to cookies walks the dictionary and evicts anything past its expiry. Cheaper than running a timer, and it always returns a clean view:

    - (NSArray *)cookies {
    NSMutableArray *cookiesToRemove = [NSMutableArray array];
    for (CookieKey *key in _cookies) {
    NSHTTPCookie *cookie = [_cookies objectForKey: key];
    if ([cookie expiresDate] != nil
    && [[cookie expiresDate] isLessThan:[NSDate date]]) {
    [cookiesToRemove addObject: key];
    }
    }
    for (CookieKey *key in cookiesToRemove) {
    [_cookies removeObjectForKey: key];
    }
    return [_cookies allValues];
    }

    About 300 lines of core logic across the storage class, the cookie key, and the swizzle category. Most of it is RFC 6265 plumbing.

    A test suite for someone else’s code

    Once I had a working mental model, I wrote a test suite. Not against my own code: against Apple’s. The suite exercised every observable behaviour I could find. Cookie expiry, including Max-Age versus Expires. Domain matching, including the leading-dot wildcard rules and the prefix trap (prefixexample.com must not match a cookie set on example.com). Path prefix matching. Secure flag handling. Set-Cookie parsing. The order in which cookies came back. Case sensitivity quirks across domain, path, and name. Cookie acceptance policies (Always, Never). Round-tripping through archive and unarchive. I deliberately included behaviours I suspected were abandoned, on the theory that if they still existed in the binary, something somewhere depended on them.

    The trick that paid off: every test was written so it could run against either the real NSHTTPCookieStorage or my CAHTTPCookieStorage. Same assertions, swap the class name at the top of the file, run again. When something passed Apple’s run and failed mine, I had a divergence to chase. When it passed both, I knew I’d matched behaviour.

    There was a charming downside, immortalised in a comment at the top of the test file:

    Be aware though that these tests are cookie destructive. If you run them against NSHTTPCookieStorage it will destroy all your Safari cookies and if you run them against CAHTTPCookieStorage it will destroy all the Ninja cookies.

    The tests cleared the jar before each run so the assertions had a known starting state. Validate them against Apple’s class to confirm the spec, and you’d lose every Safari cookie on the machine. Bank logins, session tokens, the lot.

    Once swizzling was in (more on that later), I went a step further. Every test got a twin with the suffix ThroughSharedStorage: same assertion, but reaching in through NSHTTPCookieStorage.sharedHTTPCookieStorage() rather than my class directly. This proved two things at once. First, the swizzle was alive: a call into the Foundation singleton was actually landing in my code. Second, my class behaved identically whether you called it directly or arrived through the Apple façade. Both halves had to be true for the integration to be safe, and the dual-path testing made any drift between them impossible to miss.

    This was the contract. Whatever I built next had to satisfy it.

    Method swizzling, the final step

    The last piece was making the rest of the system call my implementation instead of Apple’s. Objective-C method swizzling lets you swap the implementation of a selector with another at runtime: the class is still NSHTTPCookieStorage, the API surface is unchanged, but underneath it now calls into mine.

    I used JRSwizzle as the runtime helper, ran the swap from a +load method (which the Objective-C runtime calls before main), and guarded it with dispatch_once so a category accidentally loaded twice wouldn’t try to double-swap and undo itself:

    + (void)load {
    static dispatch_once_t swizzleMethodsToken;
    dispatch_once(&swizzleMethodsToken, ^{
    NSError *error = nil;
    [[self class] jr_swizzleMethod:@selector(deleteCookie:)
    withMethod:@selector(caDeleteCookie:) error:&error];
    [[self class] jr_swizzleMethod:@selector(cookieAcceptPolicy)
    withMethod:@selector(caCookieAcceptPolicy) error:&error];
    [[self class] jr_swizzleMethod:@selector(setCookieAcceptPolicy:)
    withMethod:@selector(caSetCookieAcceptPolicy:) error:&error];
    [[self class] jr_swizzleMethod:@selector(cookies)
    withMethod:@selector(caCookies) error:&error];
    [[self class] jr_swizzleMethod:@selector(cookiesForURL:)
    withMethod:@selector(caCookiesForURL:) error:&error];
    [[self class] jr_swizzleMethod:@selector(sortedCookiesUsingDescriptors:)
    withMethod:@selector(caSortedCookiesUsingDescriptors:) error:&error];
    [[self class] jr_swizzleMethod:@selector(setCookie:)
    withMethod:@selector(caSetCookie:) error:&error];
    [[self class] jr_swizzleMethod:@selector(setCookies:forURL:mainDocumentURL:)
    withMethod:@selector(caSetCookies:forURL:mainDocumentURL:) error:&error];
    });
    }

    Eight selectors. That was all the public API anyone called. The list maps one-to-one to the symbols I’d seen in Hopper’s left sidebar.

    There was one subtlety. My first instinct was to override +sharedHTTPCookieStorage so that anyone asking for the singleton would get my object instead of Apple’s. That didn’t work, and I wrote down the reason at the top of the swizzle header so I wouldn’t forget:

    We are not hijacking sharedHTTPCookieStorage because something from Safari calls it, gets a copy of NSHTTPCookieStorage and then calls private methods in it that CAHTTPCookieStorage doesn’t implement.

    The crash was deep in CFNetwork, with a stack trace pointing at internal selectors I’d seen but deliberately not bothered to ship. Swizzling on the class itself, on the real NSHTTPCookieStorage, sidestepped it. The shared instance stays the shared instance. Its public methods just do something different now. The private ones still work, because they’re still Apple’s.

    Two side-by-side process containers: a cyan Dashman process whose NSHTTPCookieStorage has the eight swizzled public selectors routed to a remotely synchronized cookie jar, and a red Safari process whose NSHTTPCookieStorage delegates unchanged through CFNetwork to the local cookie jar.

    There was a second subtlety, easy to miss. WebKit, by default, processes cookies for you on every request. If you only swizzle storage, you still get Safari-cookie behaviour because WebKit’s request pipeline reaches into its own cookie machinery before your storage runs. The fix is to disable WebKit’s cookie handling per-request and call the storage manually:

    request.HTTPShouldHandleCookies = false
    CAHTTPCookieStorage.sharedHTTPCookieStorage().handleCookiesInRequest(request)

    Same on the response side: receive the response yourself, extract Set-Cookie headers, hand them to the storage, then return. This had to be wired into WKWebView‘s request pipeline in two places: once on outgoing requests (to attach our cookies) and once on incoming responses (to harvest Set-Cookie). Redirects had to be handled twice, on both legs, because the redirect response itself can carry cookies that the next request needs.

    What shipped

    A single, fully isolated cookie jar separate from Safari, for the macOS build. That solved the immediate problem: nothing else on the user’s machine could be authenticated as Dashman just by being on the same machine, and Dashman couldn’t accidentally inherit the user’s personal Safari sessions either. The longer-term goal of one jar per site I designed for but didn’t ship before the architecture pivoted to a Java/JavaFX client where I owned the entire HTTP stack and the swizzling story became unnecessary.

    I’d call that a healthy outcome. The hack solved a real, present problem. The future problem got solved a different way, by changing the architecture rather than by deepening the hack.

    What I took from it

    A few things I’ve carried with me since:

    One more proof: “can’t be done” is, for me, a starting line. Long before Apple’s reply, treating “impossible” as a cue to dig in had become a reflex. When someone tells me a thing can’t be done, that’s the moment I commit to doing it, and I’ll go as deep as the problem needs. Apple’s reply was technically accurate from inside their boundaries. I went outside them. Here that meant x86 assembly. Next time it’ll be whatever the next problem demands.

    Behavioural equivalence is a real specification. When the source is unavailable, the binary is the spec, and a test suite written against the binary is the next best thing. The suite was as valuable as the replacement, because it told me, precisely, when I’d drifted. The dual-target trick (run the same tests against Apple’s class and mine) and the dual-path trick (run them direct and through the swizzled singleton) were what made the substitution trustworthy rather than hopeful.

  • My main computer was an Apple MacBook Pro for about 8 or 9 years. That is, until last January, when I said good-bye to Apple. It wasn’t easy, but the last iteration of the MacBook Pro is terrible.

    I’m not against the touch bar. I think keyboards need more innovation and I applaud the effort. But aside from the touch bar, the keyboard feels weird because they tried to make their power-user product super thin.

    Let me repeat that: for their power user product Apple favors a bit of thinness over usability.

    I don’t know how much of that also pushed them to produce an underpowered product with not a lot of RAM, very expensive hard drive, very expensive in general.

    At the same time as I was in need of a new laptop, I was putting together a gaming computer and I decided instead to add some more funding to that project and turn it into a proper workstation. For the price of a MacBook Pro, I got the most amazing workstation I could ever want. Granted, it’s not mobile, but I need my nice keyboard and monitors to work anyway, so, it suits me well.

    I’m really surprised to be back using Microsoft Windows as my main operating system; something that hasn’t happened since Windows NT 4.0. And I’m happy about it.

    Goodbye Apple, it was fun while it lasted.

  • This blog post was originally published in Screensaver Ninja‘s blog.

    We were recently challenged by someone who asked what was so special about adding websites into a screensaver. Perhaps, at first, this doesn’t seem like a tough task but after months of challenging work, I can confirm it is. I realized I never shared exactly why yet, so here it is. Putting a browser into your screensaver is like putting a square peg in a round hole.

    Chromium on Mac

    Screensaver Ninja for Mac is already out there. It’s working and it’s robust, but getting there wasn’t without its pains. Initially, we wanted the Mac and Windows versions to have the exact same rendering engine and thus, we went for Chrome’s WebKit, packaged as Chromium Embedded Framework, or CEF for short.

    open-package-contentsChromium, the open source version of Chrome, follows the same structure as Chrome to handle page isolation: by running it in different processes. On Mac, applications are distributed as bundles which you may know as .app files. They are actually directories and you can inspect the contents by right‐clicking or control‐clicking on it and then choosing Show Package Contents. (Warning: if you change anything the app will likely not work anymore.) When you use CEF, you end up with a secondary bundle application inside your application. This is sort‐of supported, but weird and not without issues.

    In Mac OS X, screensavers are dynamic libraries that are loaded by a special screensaver program. When you make a screensaver, you are not in control of the running program, you just have a few entry points to start doing your animation. The Mac OS X screensaver framework doesn’t like it at all when you have a secondary app bundle that you trigger from your library.

    During this research, we found a bunch of issues, many of which were not clear‐cut as solving it for us might break it for other people. We still needed those issues solved so we wrote scripts that would pre-process CEF and solve them for us. Ultimately we ended up dropping CEF; more on that later.

    Swift

    We decided to use Swift for our project. It’s the new way and we prefer higher level languages whenever possible. Swift saves us a bit of pain with memory management, syntax and other things. But we inadvertently caused ourselves quite a bit of pain. The Mac screensaver framework is still an Objective-C application and since we are not building an app, but a library, we need to write and compile Swift with Objective-C binary compatibility. This is not commonly done so it took us quite a bit of bumping our head against a brick wall to figure it out. Furthermore, not being in control of the program loading our library made getting error messages tricky at best.

    Apple’s WebKit

    When we dropped CEF, our alternative was Apple’s WebKit, which was so much easier to integrate and have running, despite the fact that there are two of them; one that works out of the box, but it’s deprecated, called WebView, and a new one that’s not so well supported, called KWWebView. We played a lot with both and both had the same problem: cookies shared with Safari.

    Apple decided that all users of a web view must share cookies with Safari. This was not acceptable for us because we want to have separate independent sessions and in the future we are even planning on having separate cookie jars per site so that you can, for example, be logged in into two different Twitter accounts at the same time. We contacted Apple about it, paying our tech support fee, and the answer was a resounding “can’t be done, we’ll add it to the list of things that we’ll consider in the future”.

    Challenge accepted Mr Apple! We embarked on a quest to achieve this anyway that took us into the dark innards of the Cookie Jar, debugging it at the assembly level to understand its interface and workings:

    debugging-the-cookie-jar

    I have to admit it, that was fun. After understanding Apple’s cookie jar’s implementation, we wrote a test suite that was exercising it all, as far as we know, including bits that we believe are abandoned. After that we wrote our own implementation that stored the cookies separately and used the same test suite to make sure our implementation was equivalent to Apple’s. This code had to be done in a mix of C and Objective-C. Then we used method swizzling to replace Apple’s with our own and ta-da! Cookie separation.

    Windows events

    The Windows version of Screensaver Ninja is of course not done yet, but we’ve already started working on it and we are partly there. One interesting problem that we run into is that Windows doesn’t help you at all with the workflow of a screensaver. It is of paramount importance to us that while the screensaver is running, nobody should be able to interact with those websites. We don’t want any keystrokes, mouse moves, mouse clicks, etc to reach the pages, otherwise it would be a breach of our security approach to dashboards.

    Windows has a long history all the way back to Windows 1.0 that ran as a little program on top of MS DOS. Awww, good old days. Through the decades, ways to code Windows applications have changed radically and thus the way events travel through applications also did. That means that there’s a lot of different ways for an app to get keystrokes, mouse events, etc. Finding them all and plugging all those holes was not trivial and since we are talking about security this required a lot of testing.

    I’m sure that as we go along, we are going to find many more issues like those in the Windows environment, and we are going to solve them and we are going to strive for elegant, stable, robust code.

     

  • It seems iPad lack of Flash support is the debate of the moment. On one camp: “You can’t use the web without Flash”, on the other camp: “We don’t need no stinking Flash”. Although I do realize how a technology like Flash is sometimes needed, I’m more on the second camp. The less flash, the better.

    I think iPad’s lack of Flash cause two things to happen:

    • Slow down the adoption of the iPad: surely someone will say “No Flash, No iPad“.
    • Speed up the adoption of HTML5: surely someone will consider using HTML5 to support the tablet.

    Giving that the iPad is a closed device, probably the closedest non-phone computer consumers ever had access to and that HTML5 is good progress for the web, I consider both results of the iPad not having Flash positive. If I have to say anything about it, it’d be: please, stop trying to wake Steve Jobs up regarding this, you’ll ruin it.