7

I'm working on a macOS tool which uses Apple's Safari framework. When running in macOS 10.13, the tool links to and loads it from

/System/Library/PrivateFrameworks/Safari.framework

and all works fine. But when running in macOS 10.12.6, some behaviors are missing. Based on some probing with DTrace, I think that this is because my tool needs to load instead the latest Staged framework, which is here:

/System/Library/StagedFrameworks/Safari/Safari.framework

This is apparently what Safari does, because if I attach to Safari with lldb and run image list, in 10.13 the list includes only the former path, and in 10.12.6 only the latter.

I tried the following:

NSBundle* stagedBundle = [NSBundle bundleWithPath:@"/System/Library/StagedFrameworks/Safari/Safari.framework"];

That returns nil in 10.13 because there is, at this time, no such directory. However, in 10.12.6, I get a stagedBundle, and then:

NSBundle* privateBundle = [NSBundle bundleForClass:[BookmarksController class]];
[privateBundle unload];
[stagedBundle load];

The unloading and loading apparently works, because if I log -description of those two bundles, before running that code the Private bundle is (loaded) and the Staged bundle is (not yet loaded), but after running that code those states are swapped, as desired.

But it is not effective. (1) If I again invoke -bundleForClass:, passing a class known to be in both frameworks, it gives me the Private bundle. (2) If I invoke -respondsToSelector:, passing a selector which is known to exist only in the Staged framework, I get NO.

I tried calling _CFBundleFlushBundleCaches(), as suggested here, but that did not help.

I've also tried changing my target's FRAMEWORK_SEARCH_PATHS, and installing the Staged framework on my Mac and linking to it, but since this post is already too long I'll just say that this resulted in more heat than light.

How can one selectively load a framework in this situation?

UPDATE

I've tried another approach. After re-reading Apple's Framework Programming Guide, even though it seems really dated, I decided that this framework needs to be weakly linked. Did this:

  • In the code, removed those NSBundle -load and -unload calls
  • In my tool's target,
    • In Build Phases > Link Binary with Libraries, removed path to the Safari Private framework, because this was a strong link.
    • In Build Settings > Other Linker Flags added -weak_framework Safari
    • In Build Settings > Framework Search Paths, listed paths to both frameworks' parent directories, with the Staged path before the Private path, because I want this one to load in macOS 10.12.6, where both exist.

It makes sense to me, builds and runs in both 10.13 and 10.12.6, but it is apparently still loading the undesired Private framework in 10.12.6. NSLog reports that as the bundle's path, and a class does not respond to a selector known to be in Staged framework only.

Any other ideas?

Jerry Krinock
  • 4,860
  • 33
  • 39
  • Personally I spotted this on one project: if (_NSSoftLinkingLoadFramework(0x0, @"CharacterPicker")) {NSLog(@"hello");} (located in Foundation or AppKit) and try to look for similar with hopper how apple is doing it – Marek H Mar 20 '18 at 09:13

1 Answers1

7

First, a disclaimer: I'd strongly suggest you don't rely on loading private frameworks in any application that you ship to users. It's fragile and unsupported.

That said, if you really want to do this, my suggestion would be to use the same technique that Safari itself uses to select between the two copies of the framework, which is dyld's DYLD_VERSIONED_FRAMEWORK_PATH environment variable.

To quote the dyld man page:

This is a colon separated list of directories that contain potential override frameworks. The dynamic linker searches these directories for frameworks. For each framework found dyld looks at its LC_ID_DYLIB and gets the current_version and install name. Dyld then looks for the framework at the install name path. Whichever has the larger current_version value will be used in the process whenever a framework with that install name is required. This is similar to DYLD_FRAMEWORK_PATH except instead of always overriding, it only overrides is the supplied framework is newer. Note: dyld does not check the framework's Info.plist to find its version. Dyld only checks the -current_version number supplied when the framework was created.

In short, this results in dyld performing a version check between the framework being loaded and the one in the versioned framework path, with the higher version being loaded. If the versioned framework path doesn't exist or the framework in question doesn't exist within it, the original framework path will be used.

Safari makes use of a second dyld feature to simplify its use of DYLD_VERSIONED_FRAMEWORK_PATH, the LC_DYLD_ENVIRONMENT load command. This load command allows DYLD_* environment variables to be specified at link time that will be applied by dyld at runtime prior to it attempting to load any dependent libraries. Without this trick you'd need to set DYLD_VERSIONED_FRAMEWORK_PATH as an environment variable prior to your application being launched, which typically requires a cumbersome re-exec to achieve.

Putting these two building blocks together, you end up adding a configuration setting like:

OTHER_LDFLAGS = -Wl,-dyld_env -Wl,DYLD_VERSIONED_FRAMEWORK_PATH=/System/Library/StagedFrameworks/Safari;

You can then either link statically against /S/L/PrivateFrameworks/Safari.framework, or attempt to load it dynamically at runtime. Either should result in the appropriate framework being loaded at runtime.


To address some of the misunderstandings your question reveals:

The unloading and loading apparently works, because if I log -description of those two bundles, before running that code the Private bundle is (loaded) and the Staged bundle is (not yet loaded), but after running that code those states are swapped, as desired.

Unloading shared libraries containing Objective-C code isn't supported. I suspect the only thing it does is result in a "loaded" flag being toggled on the NSBundle instance, since at dyld's level it is ignored.

In Build Settings > Framework Search Paths, listed paths to both frameworks' parent directories, with the Staged path before the Private path, because I want this one to load in macOS 10.12.6, where both exist.

Framework search paths are a concept that's only used at compile-time. At runtime, the library's install name is what tells dyld where to find the binary to load.

bdash
  • 18,110
  • 1
  • 59
  • 91
  • Thank you for the great explanaiton, @bdash. DYLD_VERSIONED_FRAMEWORK_PATH looked great until I read this on the dyld(1) man page: *Note: If System Integrity Protection is enabled, these environment variables are ignored when executing binaries protected by System Integrity Protection.* Arghhh. – Jerry Krinock Mar 20 '18 at 17:39
  • Literally, if *binaries* refers to my my "binary" (the one that is *executing*), maybe DYLD_VERSIONED_FRAMEWORK_PATH will work. But I'm afraid they meant *when loading frameworks protected by SIP*. Well, I'll give it a shot and find out. – Jerry Krinock Mar 20 '18 at 17:48
  • 1
    It means what it says. Your application binary isn't likely to be protected by SIP, so that warning is unlikely to be relevant. – bdash Mar 20 '18 at 17:49
  • And for what it's worth, the reason that behavior exists is to protect against injecting code into processes protected by SIP that could alter their behavior. This is only relevant for processes protected by SIP, not for libraries, since you can already do most anything you want within the confines of your own process. – bdash Mar 20 '18 at 19:06
  • Ah, that makes sense. Whenever I see some new Apple security measure, I impulsively conclude that its purpose is to make life difficult for me. Not always :)) – Jerry Krinock Mar 21 '18 at 03:52
  • Your answer works, as you predicted, yes even with SIP. I just had to remove the semicolon at the end of the flags when entering into Xcode. (The semicolon got stuck onto the path in the generated Load Command, causing the search to fail.) – Jerry Krinock Mar 21 '18 at 03:53
  • Regarding my use of Apple private API – In all seriousness, I regard this as a temporary measure, until Apple addresses Bug 10473679 which I filed on 2011-Nov-18. Said bug requests a replacement API for *Sync Services*, which Apple never updated to work with Safari bookmarks in iCloud, and has since disappeared. – Jerry Krinock Mar 21 '18 at 03:55