Home | History | Annotate | Download | only in mac
      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 
     18 extern "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.
     31 Boolean _CFBundleLoadExecutableAndReturnError(CFBundleRef bundle,
     32                                               Boolean force_global,
     33                                               CFErrorRef* error);
     34 
     35 }  // extern "C"
     36 
     37 namespace chrome {
     38 namespace common {
     39 namespace mac {
     40 
     41 namespace {
     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 '/'.
     46 NSArray* 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().
    118 bool 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 
    143 typedef 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 
    152 Boolean 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 
    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: "
    235                   << err;
    236   }
    237 }
    238 
    239 namespace {
    240 
    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".
    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 
    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 (a] google.com>
    276     { @"com.google.osax.Google_Authenticator_BT", nil },
    277 
    278     // Default Folder X, http://www.stclairsoft.com/DefaultFolderX/
    279     // Jon Gotow <gotow (a] stclairsoft.com>
    280     { @"com.stclairsoft.DefaultFolderX", @"4.4.3" },
    281 
    282     // MySpeed, http://www.enounce.com/myspeed
    283     // Edward Bianchi <ejbianchi (a] enounce.com>
    284     { @"com.enounce.MySpeed.osax", @"1201" },
    285 
    286     // SIMBL (fork), https://github.com/albertz/simbl
    287     // Albert Zeyer <albzey (a] googlemail.com>
    288     { @"net.culater.SIMBL", nil },
    289 
    290     // Smart Scroll, http://marcmoini.com/sx_en.html
    291     // Marc Moini <marc (a] a9ff.com>
    292     { @"com.marcmoini.SmartScroll", @"3.9" },
    293 
    294     // Bartender for Mac, http://www.macbartender.com
    295     // Ben Surtees <ben (a] surteesstudios.com>
    296     { @"com.surteesstudios.BartenderHelper", @"1.2.20" },
    297     { @"com.surteesstudios.BartenderHelperSeventy", @"1.2.20" },
    298     { @"com.surteesstudios.BartenderHelperBundle", @"1.2.20" },
    299   };
    300 
    301   for (size_t index = 0; index < arraysize(kAllowedBundles); ++index) {
    302     const AllowedBundle& allowed_bundle = kAllowedBundles[index];
    303     NSString* allowed_bundle_id = allowed_bundle.bundle_id;
    304     NSUInteger allowed_bundle_id_length = [allowed_bundle_id length];
    305 
    306     // Permit bundle identifiers that are exactly equal to the allowed
    307     // identifier, as well as "children" of the allowed identifier.
    308     if ([bundle_id isEqualToString:allowed_bundle_id] ||
    309         ([bundle_id length] > allowed_bundle_id_length &&
    310          [bundle_id characterAtIndex:allowed_bundle_id_length] == '.' &&
    311          [bundle_id hasPrefix:allowed_bundle_id])) {
    312       NSString* minimum_version = allowed_bundle.minimum_version;
    313       if (!minimum_version) {
    314         // If the rule didn't declare any version requirement, the bundle is
    315         // allowed to load.
    316         return true;
    317       }
    318 
    319       if (!version) {
    320         // If there wasn't any version but one was required, the bundle isn't
    321         // allowed to load.
    322         return false;
    323       }
    324 
    325       // A numeric search is appropriate for comparing version numbers.
    326       NSComparisonResult result = [version compare:minimum_version
    327                                            options:NSNumericSearch];
    328       return result != NSOrderedAscending;
    329     }
    330   }
    331 
    332   // Nothing matched.
    333   return false;
    334 }
    335 
    336 }  // namespace mac
    337 }  // namespace common
    338 }  // namespace chrome
    339