Search code examples
macososx-maverickssandboxdyld

Setting DYLD variables for sandboxed app at runtime


I have a Cocoa application that uses several system and third-party frameworks, some of which spin off XPC services. I would like to insert a library of my own into the process space of those XPC services so that I can feed information back to my main application. Audio data as it is being played, as a concrete example, via interposing or mach interception.

TL;DR: How do I set DYLD environment variables, for my sandboxed application, programmatically, when my application starts?

Longer version…

I can insert the library easily enough by setting a few shell variables before launching my application (contrary to what this question suggests):

$ export __XPC_DYLD_FORCE_FLAT_NAMESPACE=1 
$ export __XPC_DYLD_INSERT_LIBRARIES=`pwd`/My.app/Contents/Frameworks/XpcMonitor.dylb
$ open My.app

I thought I was home free at this point, and "just" needed to set those variables when my application started up:

int main(int argc, const char * argv[])
{
     NSString* libPath = [[[NSBundle mainBundle] privateFrameworksPath] stringByAppendingPathComponent:@"XpcMonitor.dylib"];
     setenv("__XPC_FORCE_FLAT_NAMESPACE", "1", 1);
     setenv("__XPC_DYLD_INSERT_LIBRARIES", [libPath UTF8String], 1);
     return NSApplicationMain(argc, argv);
}

But this had no effect. Which seems strange because, as far as I can tell, the XPC processes haven't spun up yet. Apparently Launch Services has already decided how it will handle sub-processes before it gets to my main(). To be sure it wasn't happening in a library constructor I moved all of my application logic to a dylib, and loaded it dynamically after setting the variables; same results.

I tried adding them to the LSEnvironment section of my Info.plist.

 <key>LSEnvironment</key>
 <dict>
      <key>__XPC_DYLD_FORCE_FLAT_NAMESPACE</key>
      <string>1</string>
      <key>__XPC_DYLD_INSERT_LIBRARIES</key>
      <string>/Applications/My.app/Contents/Frameworks/XpcMonitor.dylib</string>
 </dict>

This works, but hard-codes the absolute path to the library into my application. So if my bundle gets placed anywhere other than /Applications, it will crash with an "unable to locate inserted library" message.

I tried changing that path to "@executable_path/../Frameworks/XpcMonitor.dylib". That works if I am just inserting a library into my own application (i.e. using DYLD_INSERT_LIBRARIES) but fails to find the library for XPC services, presumably because those services are separate executables with different paths. I tried using "@rpath/XpcMonitor.dylib" and setting my application's search paths to "@executable_path/../Frameworks" but that also failed to find the library. There might be some magic possible here, but I haven't been able to figure it out.

I tried setting the variables and then relaunching my app with:

 [NSTask launchedTaskWithLaunchPath:[[NSBundle mainBundle] executablePath] 
                          arguments:[NSArray array]];

Works great outside the sandbox, fails inside it with: deny forbidden-sandbox-reinit.

I tried relaunching the app with -[NSWorkspace launchApplicationAtURL:options:configuration:error], using the NSWorkspaceLaunchNewInstance and NSWorkspaceLaunchAsync flags, and passing the environment variables in the configuration dictionary. This works great outside the sandbox, but when sandboxed the contents of the configuration dictionary are ignored.

I've tried several variations on setting the variables and then launching a child process with [NSTask launchedTaskWithLaunchPath:arguments], including a nested application bundle (tried in Frameworks, MacOS, Helpers, PlugIns), and a second executable with and without an embedded Info.plist. All of these variations run and work, but raise some worrisome sandboxing errors, including lots of these:

 5/30/14 9:55:04.000 AM kernel[0]: Sandbox: appleeventsd(57) deny file-read-metadata /Library

A few of these:

 5/30/14 9:55:04.000 AM kernel[0]: Sandbox: appleeventsd(57) deny mach-lookup com.apple.ocspd

And finally this:

 appleeventsd[57]: <rdar://problem/11489077> A sandboxed application with pid 2119, "MyAppHelper" checked in with appleeventsd, but its code signature could not be validated ( either because it was corrupt, or could not be read by appleeventsd ) and so it cannot receive AppleEvents targeted by name, bundle id, or signature. Error=ERROR: #-67061  { "NSDescription"="SecCodeCheckValidity() returned -67061, <SecCode 0x7fa30bc0bd20 [0x7fff7b46ff00]>." }  (handleMessage()/appleEventsD.cp #2072) client-reqs-q

…and Sparkle updates of this app fail Sparkle's pre-installation code signing checks. Spctl checks out:

 $ spctl --verbose=4 --assess --type execute ~/Desktop/MyApp.app/
 /Users/Jason/Desktop/MyApp.app/: accepted
 source=Developer ID

This still seems like the most likely approach to succeed, but I'm stumped on how to set up the code signing, either for a helper executable that can access the main app's resources and frameworks (because the entire UI is in the helper), or a nested application bundle running as a child process. Google searches on the error above don't turn up anything useful that I could find, and all of the information on code signing helper executables, other than the one linked above, are for login items.

(By the way, I can't use an XPC service for this, because I need a significant portion of the UI (web views and the like) to be running with these variables set, and you can't do UI in an XPC service).

Whew…I think that's everything, but let me know if more information is needed. So once again: How do I set DYLD environment variables, for my sandboxed application, programmatically, when my application starts? (Or it is simply impossible, in which I case I can put this aside with a clear conscience and stop puzzling?)


Solution

  • Ok. You have several things inaccurately:

    First of all, the Setting DYLD variables for sandboxed app at runtime answer is correct: DYLD will restrict itself from processing vars for three reasons, one of which being that the app is code signed with entitlements (the other two is restrictedBySetGUid, or restrictedBySegment). The reason why things worked for you is because at the time you likely didn't have your app code signed.

    Second, the __XPC_* environment vars are just wrappers over normal ones, which libxpc.dylib uses in _xpc_collect_environment: specifically, check 10.10.3's code and you'll see:

    __xpc_collect_environment:
        0000000000004970        pushq   %rbp
       ....
        00000000000049f7        leaq    0x1daa1(%rip), %rsi     ## literal pool for: "__XPC_"
    00000000000049fe            callq   __xpc_has_prefix
    

    The variables are passed via XPC (and launchd) prior to spawning your xpc service. When the service is spawned, the __XPC_ prefix is stripped, so as to once more have DYLD_*.. and you're back to square one - dyld will then ignore if the service is signed (i.e. with entitlements, therefore restricted), which is corroborated by your saying the dictionary is ignored (non DYLD vars are unaffected).

    And, when you set DYLD variables post the launch, they no longer have any effect. They are only processed by DYLD on a Mach-O load. Stop puzzling. If you do find a way, you will have stumbled across a 0-day vulnerability (since DYLD_INSERT_LIBRARIES, in particular, is the r00t of all evil..)