This is a follow-up to my recent blog post about the hardened runtime, which was itself a follow-up to my earlier blog post about the hardened runtime. So you can call this a hardened runtime trilogy. Just don't call it a comeback. In episode III (Revenge of the Service), I wish to expand on something I said in episode II (Attack of the Contacts):
If the main executable has the hardened runtime enabled, and it has the Contacts hardened runtime entitlement, then both the main executable and the xpc service can access the Contacts API, regardless of whether the xpc service has the Contacts entitlement. The hardened runtime entitlements of the embedded xpc are irrelevant, only the main executable matters. Likewise, if the main executable does not have the Contacts hardened runtime entitlement, then the xpc service will not be able to access the Contacts API, even if the xpc service has the Contacts entitlement. Moreover, user permission to access Contacts is granted to the app as a whole, including both the main executable and the xpc service, not to each separately. If the app has the Contacts hardened runtime entitlement, the xpc service accesses the Contacts API, and the user clicks "OK" to grant access to Contacts, then the main executable will also be able to access the Contacts API without causing another user consent dialog to display.
This is all true, but my lingering question was, can the hardened runtime on an embedded xpc service be wholly irrelevant? Does it not matter at all, and everything is determined by the main executable of the embedding app? The answer, I discovered, is that it does matter in some situations, but not in others. And how did I discover this answer, you may ask? In the face of overwhelming odds, I was left with only one option: I had to to science the… well, I had to do some hard science.
In Xcode I created a sample Mac app with an embedded xpc service. Then I embedded a dynamic library in the xpc service. (It's executables all the way down.) Make sure that the library target build setting DYLIB_INSTALL_NAME_BASE
has the value @executable_path/../Frameworks
, or else the linker will incorrectly assume the path is in /usr/lib
. I also set CODE_SIGN_IDENTITY
to the empty string to prevent the library from getting code signed on build (for reasons that will be apparent soon), and I added a build phase to the xpc service to copy the dylib to Frameworks and Code Sign On Copy.
The experiment goes as follows. Enable the hardened runtime on the xpc service target, with no special entitlements. Build the app. Then delete the signed copy of the dylib that's embedded in the xpc service that's embedded in the app, and replace it with the unsigned copy of the dylib that's already conveniently in the build products folder next to the app. Then launch the app.
The result is that the app launches, but the embedded xpc service crashes. (This is one reason why you want xpc services, to prevent the whole app from crashing when one thing goes wrong.) The crash log has a dyld error message saying that the embedded dylib is "not valid for use in process using Library Validation: mapped file has no cdhash, completely unsigned? Code has to be at least ad-hoc signed." The crash occurs even if the hardened runtime is not enabled on the app target. So clearly the hardened runtime on the xpc service is effective.
By the way you can see where the error message comes from, because dyld is open source:
fchecklv checkInfo;
char messageBuffer[512];
messageBuffer[0] = '\0';
checkInfo.lv_file_start = offsetInFatFile;
checkInfo.lv_error_message_size = sizeof(messageBuffer);
checkInfo.lv_error_message = messageBuffer;
int res = fcntl(fd, F_CHECK_LV, &checkInfo);
if ( res == -1 ) {
dyld::throwf("code signature in (%s) not valid for use in process using Library Validation: %s", this->getPath(), messageBuffer);
}
Here dyld is calling out to the kernel, and /System/Library/Extensions/AppleMobileFileIntegrity.kext
produces the rest of the error message. There's also some open source for the kernel too, showing how F_CHECK_LV
is used.
Now repeat the experiment, this time giving the xpc service the hardened runtime exception Disable Library Validation. Guess what, in this case the xpc service doesn't crash anymore! So the entitlement given to the xpc service applies directly to the xpc service. Indeed, if you enable the hardened runtime for the app target, it makes no difference. The hardened runtime of the app can neither allow nor prevent the unsigned dylib from getting loaded by the xpc service. The setting for Disable Library Validation on the app target has no effect at all on the behavior of the embedded xpc service. The xpc service hardened runtime does matter, at least for library loading.
What accounts for the difference in behavior between library loading and Contacts access? In the Xcode target's Hardened Runtime Capabilities, there are two sections: Runtime Exceptions and Resource Access. I believe that these two different sections reflect two different macOS subsystems that use the hardened runtime: dyld and TCC. The dynamic linker, dyld, controls the loading of executable code when a process is launched. When the xpc service crashes trying to load the unsigned library, this crash occurs in dyld code, before any of the xpc service's own code has been executed. Thus, dyld helps to ensure that no unauthorized code gets executed by the system. Each process is loaded separately by dyld, so an executable's hardened runtime settings apply only to that executable.
In contrast to dyld, the Transparency, Consent, and Control (TCC) subsystem operates at the level of the app as a whole. The TCC database located at ~/Library/Application Support/com.apple.TCC/TCC.db
stores a reference to an app as a bundle identifier; it doesn't distinguish between the app's main executable and the embedded xpc service's executable. Thus, for the purpose of resources access, all that matters is the hardened runtime on the main executable. This is also why privacy usage descriptions such as NSContactsUsageDescription
must be added to the Info.plist
file of the app rather than the Info.plist
file of the embedded xpc service, even if only the xpc service accesses the protected API.
Needless to say — ok, that's wrong, it does need to be said — none of this is documented by Apple. I only figured it out by guessing and experimentation. The documentation of the hardened runtime is mainly my blog. You're welcome! Gratitude may be expressed in the form of purchasing my software. I also accept cash, or a check made out to cash.