1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/common/mac/cfbundle_blocker.h"
7 #include <CoreFoundation/CoreFoundation.h>
8 #import <Foundation/Foundation.h>
10 #include "base/logging.h"
11 #include "base/mac/mac_util.h"
12 #include "base/mac/scoped_cftyperef.h"
13 #include "base/mac/scoped_nsautorelease_pool.h"
14 #import "base/mac/scoped_nsobject.h"
15 #include "base/strings/sys_string_conversions.h"
16 #include "third_party/mach_override/mach_override.h"
20 // _CFBundleLoadExecutableAndReturnError is the internal implementation that
21 // results in a dylib being loaded via dlopen. Both CFBundleLoadExecutable and
22 // CFBundleLoadExecutableAndReturnError are funneled into this routine. Other
23 // CFBundle functions may also call directly into here, perhaps due to
24 // inlining their calls to CFBundleLoadExecutable.
26 // See CF-476.19/CFBundle.c (10.5.8), CF-550.43/CFBundle.c (10.6.8), and
27 // CF-635/Bundle.c (10.7.0) and the disassembly of the shipping object code.
29 // Because this is a private function not declared by
30 // <CoreFoundation/CoreFoundation.h>, provide a declaration here.
31 Boolean _CFBundleLoadExecutableAndReturnError(CFBundleRef bundle,
43 // Returns an autoreleased array of paths that contain plug-ins that should be
44 // forbidden to load. Each element of the array will be a string containing
45 // an absolute pathname ending in '/'.
46 NSArray* BlockedPaths() {
47 NSMutableArray* blocked_paths;
50 base::mac::ScopedNSAutoreleasePool autorelease_pool;
52 // ~/Library, /Library, and /Network/Library. Things in /System/Library
53 // aren't blacklisted.
54 NSArray* blocked_prefixes =
55 NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
61 // Everything in the suffix list has a trailing slash so as to only block
62 // loading things contained in these directories.
63 NSString* const blocked_suffixes[] = {
64 #if !defined(__LP64__)
65 // Contextual menu manager plug-ins are unavailable to 64-bit processes.
66 // http://developer.apple.com/library/mac/releasenotes/Cocoa/AppKitOlderNotes.html#NSMenu
67 // Contextual menu plug-ins are loaded when a contextual menu is opened,
68 // for example, from within
69 // +[NSMenu popUpContextMenu:withEvent:forView:].
70 @"Contextual Menu Items/",
72 // Input managers are deprecated, would only be loaded under specific
73 // circumstances, and are entirely unavailable to 64-bit processes.
74 // http://developer.apple.com/library/mac/releasenotes/Cocoa/AppKitOlderNotes.html#NSInputManager
75 // Input managers are loaded when the NSInputManager class is
80 // Don't load third-party scripting additions either. Scripting
81 // additions are loaded by AppleScript from within AEProcessAppleEvent
82 // in response to an Apple Event.
83 @"ScriptingAdditions/"
85 // This list is intentionally incomplete. For example, it doesn't block
86 // printer drivers or Internet plug-ins.
89 NSUInteger blocked_paths_count = [blocked_prefixes count] *
90 arraysize(blocked_suffixes);
92 // Not autoreleased here, because the enclosing pool is scoped too
95 [[NSMutableArray alloc] initWithCapacity:blocked_paths_count];
97 // Build a flat list by adding each suffix to each prefix.
98 for (NSString* blocked_prefix in blocked_prefixes) {
99 for (size_t blocked_suffix_index = 0;
100 blocked_suffix_index < arraysize(blocked_suffixes);
101 ++blocked_suffix_index) {
102 NSString* blocked_suffix = blocked_suffixes[blocked_suffix_index];
103 NSString* blocked_path =
104 [blocked_prefix stringByAppendingPathComponent:blocked_suffix];
106 [blocked_paths addObject:blocked_path];
110 DCHECK_EQ([blocked_paths count], blocked_paths_count);
113 return [blocked_paths autorelease];
116 // Returns true if bundle_path identifies a path within a blocked directory.
117 // Blocked directories are those returned by BlockedPaths().
118 bool IsBundlePathBlocked(NSString* bundle_path) {
119 static NSArray* blocked_paths = [BlockedPaths() retain];
121 for (NSString* blocked_path in blocked_paths) {
122 NSUInteger blocked_path_length = [blocked_path length];
124 // Do a case-insensitive comparison because most users will be on
125 // case-insensitive HFS+ filesystems and it's cheaper than asking the
126 // disk. This is like [bundle_path hasPrefix:blocked_path] but is
128 if ([bundle_path length] >= blocked_path_length &&
129 [bundle_path compare:blocked_path
130 options:NSCaseInsensitiveSearch
131 range:NSMakeRange(0, blocked_path_length)] ==
133 // If bundle_path is inside blocked_path (it has blocked_path as a
134 // prefix), refuse to load it.
139 // bundle_path is not inside any blocked_path from blocked_paths.
143 typedef Boolean (*_CFBundleLoadExecutableAndReturnError_Type)(CFBundleRef,
147 // Call this to execute the original implementation of
148 // _CFBundleLoadExecutableAndReturnError.
149 _CFBundleLoadExecutableAndReturnError_Type
150 g_original_underscore_cfbundle_load_executable_and_return_error;
152 Boolean ChromeCFBundleLoadExecutableAndReturnError(CFBundleRef bundle,
153 Boolean force_global,
155 base::mac::ScopedNSAutoreleasePool autorelease_pool;
157 DCHECK(g_original_underscore_cfbundle_load_executable_and_return_error);
159 base::ScopedCFTypeRef<CFURLRef> url_cf(CFBundleCopyBundleURL(bundle));
160 base::scoped_nsobject<NSString> path(base::mac::CFToNSCast(
161 CFURLCopyFileSystemPath(url_cf, kCFURLPOSIXPathStyle)));
163 NSString* bundle_id = base::mac::CFToNSCast(CFBundleGetIdentifier(bundle));
165 NSDictionary* bundle_dictionary =
166 base::mac::CFToNSCast(CFBundleGetInfoDictionary(bundle));
167 NSString* version = [bundle_dictionary objectForKey:
168 base::mac::CFToNSCast(kCFBundleVersionKey)];
169 if (![version isKindOfClass:[NSString class]]) {
170 // Deal with pranksters.
174 if (IsBundlePathBlocked(path) && !IsBundleAllowed(bundle_id, version)) {
175 NSString* bundle_id_print = bundle_id ? bundle_id : @"(nil)";
176 NSString* version_print = version ? version : @"(nil)";
178 // Provide a hint for the user (or module developer) to figure out
179 // that the bundle was blocked.
180 LOG(INFO) << "Blocking attempt to load bundle "
181 << [bundle_id_print UTF8String]
183 << [version_print UTF8String]
185 << [path fileSystemRepresentation];
188 base::ScopedCFTypeRef<CFStringRef> app_bundle_id(
189 base::SysUTF8ToCFStringRef(base::mac::BaseBundleID()));
191 // 0xb10c10ad = "block load"
192 const CFIndex kBundleLoadBlocked = 0xb10c10ad;
194 NSMutableDictionary* error_dict =
195 [NSMutableDictionary dictionaryWithCapacity:4];
197 [error_dict setObject:bundle_id forKey:@"bundle_id"];
200 [error_dict setObject:version forKey:@"version"];
203 [error_dict setObject:path forKey:@"path"];
205 NSURL* url_ns = base::mac::CFToNSCast(url_cf);
206 NSString* url_absolute_string = [url_ns absoluteString];
207 if (url_absolute_string) {
208 [error_dict setObject:url_absolute_string forKey:@"url"];
211 *error = CFErrorCreate(NULL,
214 base::mac::NSToCFCast(error_dict));
220 // Not blocked. Call through to the original implementation.
221 return g_original_underscore_cfbundle_load_executable_and_return_error(
222 bundle, force_global, error);
227 void EnableCFBundleBlocker() {
228 mach_error_t err = mach_override_ptr(
229 reinterpret_cast<void*>(_CFBundleLoadExecutableAndReturnError),
230 reinterpret_cast<void*>(ChromeCFBundleLoadExecutableAndReturnError),
231 reinterpret_cast<void**>(
232 &g_original_underscore_cfbundle_load_executable_and_return_error));
233 if (err != err_none) {
234 DLOG(WARNING) << "mach_override _CFBundleLoadExecutableAndReturnError: "
241 struct AllowedBundle {
242 // The bundle identifier to permit. These are matched with a case-sensitive
243 // literal comparison. "Children" of the declared bundle ID are permitted:
244 // if bundle_id here is @"org.chromium", it would match both @"org.chromium"
245 // and @"org.chromium.Chromium".
248 // If bundle_id should only be permitted as of a certain minimum version,
249 // this string defines that version, which will be compared to the bundle's
250 // version with a numeric comparison. If bundle_id may be permitted at any
251 // version, set minimum_version to nil.
252 NSString* minimum_version;
257 bool IsBundleAllowed(NSString* bundle_id, NSString* version) {
258 // The list of bundles that are allowed to load. Before adding an entry to
259 // this list, be sure that it's well-behaved. Specifically, anything that
260 // uses mach_override
261 // (https://github.com/rentzsch/mach_star/tree/master/mach_override) must
262 // use version 51ae3d199463fa84548f466d649f0821d579fdaf (July 22, 2011) or
263 // newer. Products added to the list must not cause crashes. Entries should
264 // include the name of the product, URL, and the name and e-mail address of
265 // someone responsible for the product's engineering. To add items to this
266 // list, file a bug at http://crbug.com/new using the "Defect on Mac OS"
267 // template, and provide the bundle ID (or IDs) and minimum CFBundleVersion
268 // that's safe for Chrome to load, along with the necessary product and
269 // contact information. Whitelisted bundles in this list may be removed if
270 // they are found to cause instability or otherwise behave badly. With
271 // proper contact information, Chrome developers may try to contact
272 // maintainers to resolve any problems.
273 const AllowedBundle kAllowedBundles[] = {
274 // Google Authenticator BT
275 // Dave MacLachlan <dmaclach@google.com>
276 { @"com.google.osax.Google_Authenticator_BT", nil },
278 // Default Folder X, http://www.stclairsoft.com/DefaultFolderX/
279 // Jon Gotow <gotow@stclairsoft.com>
280 { @"com.stclairsoft.DefaultFolderX", @"4.4.3" },
282 // MySpeed, http://www.enounce.com/myspeed
283 // Edward Bianchi <ejbianchi@enounce.com>
284 { @"com.enounce.MySpeed.osax", @"1201" },
286 // SIMBL (fork), https://github.com/albertz/simbl
287 // Albert Zeyer <albzey@googlemail.com>
288 { @"net.culater.SIMBL", nil },
290 // Smart Scroll, http://marcmoini.com/sx_en.html
291 // Marc Moini <marc@a9ff.com>
292 { @"com.marcmoini.SmartScroll", @"3.9" },
295 for (size_t index = 0; index < arraysize(kAllowedBundles); ++index) {
296 const AllowedBundle& allowed_bundle = kAllowedBundles[index];
297 NSString* allowed_bundle_id = allowed_bundle.bundle_id;
298 NSUInteger allowed_bundle_id_length = [allowed_bundle_id length];
300 // Permit bundle identifiers that are exactly equal to the allowed
301 // identifier, as well as "children" of the allowed identifier.
302 if ([bundle_id isEqualToString:allowed_bundle_id] ||
303 ([bundle_id length] > allowed_bundle_id_length &&
304 [bundle_id characterAtIndex:allowed_bundle_id_length] == '.' &&
305 [bundle_id hasPrefix:allowed_bundle_id])) {
306 NSString* minimum_version = allowed_bundle.minimum_version;
307 if (!minimum_version) {
308 // If the rule didn't declare any version requirement, the bundle is
314 // If there wasn't any version but one was required, the bundle isn't
319 // A numeric search is appropriate for comparing version numbers.
320 NSComparisonResult result = [version compare:minimum_version
321 options:NSNumericSearch];
322 return result != NSOrderedAscending;
331 } // namespace common
332 } // namespace chrome