When building a small OS X app some time ago (more about this soon), I needed a way to access and parse the ObjC segment of a given executable. This can easily be achieved with the command line utility otool but the output is not very readable (or parsable).
Steve Nygard wrote an amazing tool called class-dump for generating declarations of classes, categories and protocols by examining the Objective-C runtime information stored in a Mach-O file. It also has the option to write these as headers on disk.
As a command-line utility
Since class-dump is a command line utility my first attempt was to use an NSTask in order to fork/exec the class-dump process, read the task’s standard out and get the result back into my application’s process.
In order to nicely encapsulate the task I wrapped it in a concurrent NSOperation subclass, launching the task from the
start method and waiting for the task to exit to finish the operation. I’ve been using dispatch_io to read the standard output and standard error out of the class-dump process and it has proven to work very nicely.
While this approach worked very well it had one particular caveat: error handling. Class-dump being a command line utility, its main medium to report error is by printing to
stderr. While this works well when running the program in Terminal, one expects a nicer interface than a few logs from a Cocoa application. Also, the program exits with a termination status different from 0 most of the times it encounters an error but there seems to be times where an error is logged even though the termination status is 0. A noticeable example is a Mach-O executable that doesn’t contain any Objective-C runtime information.
In summary, I couldn’t rely 100% on the termination status being different from 0 when an error occurred and relying on
stderr receiving data to determine a failure was just too fragile.
Run in process
Luckily, class-dump is open-source and instead of trying to patch the command line utility to behave the way I needed I decided to use the class-dump Objective-C interface directly in my Cocoa application, in process.
If you ever took a look inside the Ember application you might have noticed that I really like frameworks. This said, I couldn’t just drag a few classes from the class-dump project into my own project but I instead forked the project on GitHub and added a new target for a Framework: ClassDump.framework. Its public interface only consists of a single NSOperation subclass, the whole complexity being nicely abstracted away. I’ve also added a few categories that help providing better error reporting.
Now, class-dump is a very well written and tested program but it is still dealing with untrusted input data. By having class-dump run in process, if something goes wrong the whole application goes down. This is bad.
Class-dump as a service
Luckily, we have tools to work around this problem. We could step back and decide to launch the class-dump process as an NSTask but we would lose the nicer error handling API that we adopted. Introduce Services! By having our code running in an XPC service we can make sure that the service only goes down if something should go wrong, but still be able to directly interface with our Objective-C interface by mean of an XPC connection. This seems like the best of both worlds.
We could have the XPC service as a target of the main application’s project or the class-dump project. However, since I consider it an implementation detail I don’t like the idea of exposing its internals to the rest of the system.
Let me introduce my favorite pattern: Operation-Framework-Service! A framework encapsulating an XPC service and vending a simple NSOperation as interface. Boom!
As far as the client application is concerned a simple framework import is needed. The API is also extremely simple since it only consists of an NSOperation subclass which has a well-known and simple, yet extremely powerful, API.
The operation implementation takes care of setting up an XPC connection with the XPC service. The service itself runs the actual internal operation. As far as the client is concerned however, the whole architecture is abstracted away it just feels as if the operation were running in process.
In order to make full use of the asynchronous behavior of the XPC setup we want our in-process operation to be concurrent. It will simply message the service through the XPC connection on
start and wait for the message’s response handler to finish.
Fitting in the Sandbox
Now, when designing a framework, one needs to plan for every use case. In particular, one needs to take into account the environment the host application will be running in. We want our framework to work nicely in the most restrictive environment while still supporting looser ones. Translating this into modern security tools for Cocoa application this means that our framework will need to play nicely with a sandboxed application, assuming no particular entitlements have been specified.
The XPC service will thus have to be sandboxed (and can specify its own entitlements even though one can argue that it shouldn’t require more than what the host application does, which could well be none). The client-side part of the framework will be running in the host application’s process and will thus inherit its sandbox restrictions.
Keeping this in mind, we should design our framework to work with a sandboxed application with no entitlements.
Luckily, the framework will only need two URLs, one of the executable and one of the directory in which the headers will be written into, and won’t require any network connectivity or fancier capabilities that would require a special entitlement. We can safely assume that the URLs have been acquired by the application in a sandbox-safe manner and we have write access to the export directory.
Sharing URL between sandboxed applications
With this in mind, our problem is only reduced to providing the XPC service with valid URL that it can resolve. NSURL has a very nice feature to send URL containing security information between processes called bookmark data. By making sure that the client-side part of the framework create bookmark data before messaging the XPC service we can be sure that the service will be able to resolve this bookmark to a valid URL it will have right to read and/or write.
By slightly extending class-dump and running it in process as an operation we were able to provide a nicer error API. In order to provide fault tolerance we moved the execution of class-dump to an XPC service. Finally, we abstract this service away in a Cocoa framework vending a simple NSOperation. Both the service and the framework play nicely in a sandboxed environment.