• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
5#include "chrome/common/mac/cfbundle_blocker.h"
6
7#include <CoreFoundation/CoreFoundation.h>
8#import <Foundation/Foundation.h>
9
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"
17
18extern "C" {
19
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.
25//
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.
28//
29// Because this is a private function not declared by
30// <CoreFoundation/CoreFoundation.h>, provide a declaration here.
31Boolean _CFBundleLoadExecutableAndReturnError(CFBundleRef bundle,
32                                              Boolean force_global,
33                                              CFErrorRef* error);
34
35}  // extern "C"
36
37namespace chrome {
38namespace common {
39namespace mac {
40
41namespace {
42
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 '/'.
46NSArray* BlockedPaths() {
47  NSMutableArray* blocked_paths;
48
49  {
50    base::mac::ScopedNSAutoreleasePool autorelease_pool;
51
52    // ~/Library, /Library, and /Network/Library. Things in /System/Library
53    // aren't blacklisted.
54    NSArray* blocked_prefixes =
55       NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
56                                           NSUserDomainMask |
57                                               NSLocalDomainMask |
58                                               NSNetworkDomainMask,
59                                           YES);
60
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/",
71
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
76      // initialized.
77      @"InputManagers/",
78#endif  // __LP64__
79
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/"
84
85      // This list is intentionally incomplete. For example, it doesn't block
86      // printer drivers or Internet plug-ins.
87    };
88
89    NSUInteger blocked_paths_count = [blocked_prefixes count] *
90                                     arraysize(blocked_suffixes);
91
92    // Not autoreleased here, because the enclosing pool is scoped too
93    // narrowly.
94    blocked_paths =
95        [[NSMutableArray alloc] initWithCapacity:blocked_paths_count];
96
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];
105
106        [blocked_paths addObject:blocked_path];
107      }
108    }
109
110    DCHECK_EQ([blocked_paths count], blocked_paths_count);
111  }
112
113  return [blocked_paths autorelease];
114}
115
116// Returns true if bundle_path identifies a path within a blocked directory.
117// Blocked directories are those returned by BlockedPaths().
118bool IsBundlePathBlocked(NSString* bundle_path) {
119  static NSArray* blocked_paths = [BlockedPaths() retain];
120
121  for (NSString* blocked_path in blocked_paths) {
122    NSUInteger blocked_path_length = [blocked_path length];
123
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
127    // case-insensitive.
128    if ([bundle_path length] >= blocked_path_length &&
129        [bundle_path compare:blocked_path
130                     options:NSCaseInsensitiveSearch
131                       range:NSMakeRange(0, blocked_path_length)] ==
132        NSOrderedSame) {
133      // If bundle_path is inside blocked_path (it has blocked_path as a
134      // prefix), refuse to load it.
135      return true;
136    }
137  }
138
139  // bundle_path is not inside any blocked_path from blocked_paths.
140  return false;
141}
142
143typedef Boolean (*_CFBundleLoadExecutableAndReturnError_Type)(CFBundleRef,
144                                                              Boolean,
145                                                              CFErrorRef*);
146
147// Call this to execute the original implementation of
148// _CFBundleLoadExecutableAndReturnError.
149_CFBundleLoadExecutableAndReturnError_Type
150    g_original_underscore_cfbundle_load_executable_and_return_error;
151
152Boolean ChromeCFBundleLoadExecutableAndReturnError(CFBundleRef bundle,
153                                                   Boolean force_global,
154                                                   CFErrorRef* error) {
155  base::mac::ScopedNSAutoreleasePool autorelease_pool;
156
157  DCHECK(g_original_underscore_cfbundle_load_executable_and_return_error);
158
159  base::ScopedCFTypeRef<CFURLRef> url_cf(CFBundleCopyBundleURL(bundle));
160  base::scoped_nsobject<NSString> path(base::mac::CFToNSCast(
161      CFURLCopyFileSystemPath(url_cf, kCFURLPOSIXPathStyle)));
162
163  NSString* bundle_id = base::mac::CFToNSCast(CFBundleGetIdentifier(bundle));
164
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.
171    version = nil;
172  }
173
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)";
177
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]
182              << " version "
183              << [version_print UTF8String]
184              << " at "
185              << [path fileSystemRepresentation];
186
187    if (error) {
188      base::ScopedCFTypeRef<CFStringRef> app_bundle_id(
189          base::SysUTF8ToCFStringRef(base::mac::BaseBundleID()));
190
191      // 0xb10c10ad = "block load"
192      const CFIndex kBundleLoadBlocked = 0xb10c10ad;
193
194      NSMutableDictionary* error_dict =
195          [NSMutableDictionary dictionaryWithCapacity:4];
196      if (bundle_id) {
197        [error_dict setObject:bundle_id forKey:@"bundle_id"];
198      }
199      if (version) {
200        [error_dict setObject:version forKey:@"version"];
201      }
202      if (path) {
203        [error_dict setObject:path forKey:@"path"];
204      }
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"];
209      }
210
211      *error = CFErrorCreate(NULL,
212                             app_bundle_id,
213                             kBundleLoadBlocked,
214                             base::mac::NSToCFCast(error_dict));
215    }
216
217    return FALSE;
218  }
219
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);
223}
224
225}  // namespace
226
227void 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: "
235                  << err;
236  }
237}
238
239namespace {
240
241struct 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".
246  NSString* bundle_id;
247
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;
253};
254
255}  // namespace
256
257bool 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 },
277
278    // Default Folder X, http://www.stclairsoft.com/DefaultFolderX/
279    // Jon Gotow <gotow@stclairsoft.com>
280    { @"com.stclairsoft.DefaultFolderX", @"4.4.3" },
281
282    // MySpeed, http://www.enounce.com/myspeed
283    // Edward Bianchi <ejbianchi@enounce.com>
284    { @"com.enounce.MySpeed.osax", @"1201" },
285
286    // SIMBL (fork), https://github.com/albertz/simbl
287    // Albert Zeyer <albzey@googlemail.com>
288    { @"net.culater.SIMBL", nil },
289
290    // Smart Scroll, http://marcmoini.com/sx_en.html
291    // Marc Moini <marc@a9ff.com>
292    { @"com.marcmoini.SmartScroll", @"3.9" },
293  };
294
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];
299
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
309        // allowed to load.
310        return true;
311      }
312
313      if (!version) {
314        // If there wasn't any version but one was required, the bundle isn't
315        // allowed to load.
316        return false;
317      }
318
319      // A numeric search is appropriate for comparing version numbers.
320      NSComparisonResult result = [version compare:minimum_version
321                                           options:NSNumericSearch];
322      return result != NSOrderedAscending;
323    }
324  }
325
326  // Nothing matched.
327  return false;
328}
329
330}  // namespace mac
331}  // namespace common
332}  // namespace chrome
333