// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "content/common/sandbox_mac.h" #import extern "C" { #include } #include #include #include "base/basictypes.h" #include "base/command_line.h" #include "base/compiler_specific.h" #include "base/file_util.h" #include "base/mac/bundle_locations.h" #include "base/mac/mac_util.h" #include "base/mac/scoped_cftyperef.h" #include "base/mac/scoped_nsautorelease_pool.h" #include "base/memory/scoped_nsobject.h" #include "base/rand_util.h" #include "base/string16.h" #include "base/string_piece.h" #include "base/string_util.h" #include "base/stringprintf.h" #include "base/strings/sys_string_conversions.h" #include "base/sys_info.h" #include "base/utf_string_conversions.h" #include "content/public/common/content_client.h" #include "content/public/common/content_switches.h" #include "grit/content_resources.h" #include "third_party/icu/public/common/unicode/uchar.h" #include "ui/base/layout.h" #include "ui/gl/gl_surface.h" namespace content { namespace { // Is the sandbox currently active. bool gSandboxIsActive = false; struct SandboxTypeToResourceIDMapping { SandboxType sandbox_type; int sandbox_profile_resource_id; }; // Mapping from sandbox process types to resource IDs containing the sandbox // profile for all process types known to content. SandboxTypeToResourceIDMapping kDefaultSandboxTypeToResourceIDMapping[] = { { SANDBOX_TYPE_RENDERER, IDR_RENDERER_SANDBOX_PROFILE }, { SANDBOX_TYPE_WORKER, IDR_WORKER_SANDBOX_PROFILE }, { SANDBOX_TYPE_UTILITY, IDR_UTILITY_SANDBOX_PROFILE }, { SANDBOX_TYPE_GPU, IDR_GPU_SANDBOX_PROFILE }, { SANDBOX_TYPE_PPAPI, IDR_PPAPI_SANDBOX_PROFILE }, }; COMPILE_ASSERT(arraysize(kDefaultSandboxTypeToResourceIDMapping) == \ size_t(SANDBOX_TYPE_AFTER_LAST_TYPE), \ sandbox_type_to_resource_id_mapping_incorrect); // Try to escape |c| as a "SingleEscapeCharacter" (\n, etc). If successful, // returns true and appends the escape sequence to |dst|. bool EscapeSingleChar(char c, std::string* dst) { const char *append = NULL; switch (c) { case '\b': append = "\\b"; break; case '\f': append = "\\f"; break; case '\n': append = "\\n"; break; case '\r': append = "\\r"; break; case '\t': append = "\\t"; break; case '\\': append = "\\\\"; break; case '"': append = "\\\""; break; } if (!append) { return false; } dst->append(append); return true; } // Errors quoting strings for the Sandbox profile are always fatal, report them // in a central place. NOINLINE void FatalStringQuoteException(const std::string& str) { // Copy bad string to the stack so it's recorded in the crash dump. char bad_string[256] = {0}; base::strlcpy(bad_string, str.c_str(), arraysize(bad_string)); DLOG(FATAL) << "String quoting failed " << bad_string; } } // namespace // static NSString* Sandbox::AllowMetadataForPath(const base::FilePath& allowed_path) { // Collect a list of all parent directories. base::FilePath last_path = allowed_path; std::vector subpaths; for (base::FilePath path = allowed_path; path.value() != last_path.value(); path = path.DirName()) { subpaths.push_back(path); last_path = path; } // Iterate through all parents and allow stat() on them explicitly. NSString* sandbox_command = @"(allow file-read-metadata "; for (std::vector::reverse_iterator i = subpaths.rbegin(); i != subpaths.rend(); ++i) { std::string subdir_escaped; if (!QuotePlainString(i->value(), &subdir_escaped)) { FatalStringQuoteException(i->value()); return nil; } NSString* subdir_escaped_ns = base::SysUTF8ToNSString(subdir_escaped.c_str()); sandbox_command = [sandbox_command stringByAppendingFormat:@"(literal \"%@\")", subdir_escaped_ns]; } return [sandbox_command stringByAppendingString:@")"]; } // static bool Sandbox::QuotePlainString(const std::string& src_utf8, std::string* dst) { dst->clear(); const char* src = src_utf8.c_str(); int32_t length = src_utf8.length(); int32_t position = 0; while (position < length) { UChar32 c; U8_NEXT(src, position, length, c); // Macro increments |position|. DCHECK_GE(c, 0); if (c < 0) return false; if (c < 128) { // EscapeSingleChar only handles ASCII. char as_char = static_cast(c); if (EscapeSingleChar(as_char, dst)) { continue; } } if (c < 32 || c > 126) { // Any characters that aren't printable ASCII get the \u treatment. unsigned int as_uint = static_cast(c); base::StringAppendF(dst, "\\u%04X", as_uint); continue; } // If we got here we know that the character in question is strictly // in the ASCII range so there's no need to do any kind of encoding // conversion. dst->push_back(static_cast(c)); } return true; } // static bool Sandbox::QuoteStringForRegex(const std::string& str_utf8, std::string* dst) { // Characters with special meanings in sandbox profile syntax. const char regex_special_chars[] = { '\\', // Metacharacters '^', '.', '[', ']', '$', '(', ')', '|', // Quantifiers '*', '+', '?', '{', '}', }; // Anchor regex at start of path. dst->assign("^"); const char* src = str_utf8.c_str(); int32_t length = str_utf8.length(); int32_t position = 0; while (position < length) { UChar32 c; U8_NEXT(src, position, length, c); // Macro increments |position|. DCHECK_GE(c, 0); if (c < 0) return false; // The Mac sandbox regex parser only handles printable ASCII characters. // 33 >= c <= 126 if (c < 32 || c > 125) { return false; } for (size_t i = 0; i < arraysize(regex_special_chars); ++i) { if (c == regex_special_chars[i]) { dst->push_back('\\'); break; } } dst->push_back(static_cast(c)); } // Make sure last element of path is interpreted as a directory. Leaving this // off would allow access to files if they start with the same name as the // directory. dst->append("(/|$)"); return true; } // Warm up System APIs that empirically need to be accessed before the Sandbox // is turned on. // This method is layed out in blocks, each one containing a separate function // that needs to be warmed up. The OS version on which we found the need to // enable the function is also noted. // This function is tested on the following OS versions: // 10.5.6, 10.6.0 // static void Sandbox::SandboxWarmup(int sandbox_type) { base::mac::ScopedNSAutoreleasePool scoped_pool; { // CGColorSpaceCreateWithName(), CGBitmapContextCreate() - 10.5.6 base::mac::ScopedCFTypeRef rgb_colorspace( CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB)); // Allocate a 1x1 image. char data[4]; base::mac::ScopedCFTypeRef context( CGBitmapContextCreate(data, 1, 1, 8, 1 * 4, rgb_colorspace, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host)); // Load in the color profiles we'll need (as a side effect). (void) base::mac::GetSRGBColorSpace(); (void) base::mac::GetSystemColorSpace(); // CGColorSpaceCreateSystemDefaultCMYK - 10.6 base::mac::ScopedCFTypeRef cmyk_colorspace( CGColorSpaceCreateWithName(kCGColorSpaceGenericCMYK)); } { // [-NSColor colorUsingColorSpaceName] - 10.5.6 NSColor* color = [NSColor controlTextColor]; [color colorUsingColorSpaceName:NSCalibratedRGBColorSpace]; } { // localtime() - 10.5.6 time_t tv = {0}; localtime(&tv); } { // Gestalt() tries to read /System/Library/CoreServices/SystemVersion.plist // on 10.5.6 int32 tmp; base::SysInfo::OperatingSystemVersionNumbers(&tmp, &tmp, &tmp); } { // CGImageSourceGetStatus() - 10.6 // Create a png with just enough data to get everything warmed up... char png_header[] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; NSData* data = [NSData dataWithBytes:png_header length:arraysize(png_header)]; base::mac::ScopedCFTypeRef img( CGImageSourceCreateWithData((CFDataRef)data, NULL)); CGImageSourceGetStatus(img); } { // Allow access to /dev/urandom. base::GetUrandomFD(); } // Process-type dependent warm-up. if (sandbox_type == SANDBOX_TYPE_GPU) { // Preload either the desktop GL or the osmesa so, depending on the // --use-gl flag. gfx::GLSurface::InitializeOneOff(); } } // static NSString* Sandbox::BuildAllowDirectoryAccessSandboxString( const base::FilePath& allowed_dir, SandboxVariableSubstitions* substitutions) { // A whitelist is used to determine which directories can be statted // This means that in the case of an /a/b/c/d/ directory, we may be able to // stat the leaf directory, but not its parent. // The extension code in Chrome calls realpath() which fails if it can't call // stat() on one of the parent directories in the path. // The solution to this is to allow statting the parent directories themselves // but not their contents. We need to add a separate rule for each parent // directory. // The sandbox only understands "real" paths. This resolving step is // needed so the caller doesn't need to worry about things like /var // being a link to /private/var (like in the paths CreateNewTempDirectory() // returns). base::FilePath allowed_dir_canonical = GetCanonicalSandboxPath(allowed_dir); NSString* sandbox_command = AllowMetadataForPath(allowed_dir_canonical); sandbox_command = [sandbox_command substringToIndex:[sandbox_command length] - 1]; // strip trailing ')' // Finally append the leaf directory. Unlike its parents (for which only // stat() should be allowed), the leaf directory needs full access. (*substitutions)["ALLOWED_DIR"] = SandboxSubstring(allowed_dir_canonical.value(), SandboxSubstring::REGEX); sandbox_command = [sandbox_command stringByAppendingString:@") (allow file-read* file-write*" " (regex #\"@ALLOWED_DIR@\") )"]; return sandbox_command; } // Load the appropriate template for the given sandbox type. // Returns the template as an NSString or nil on error. NSString* LoadSandboxTemplate(int sandbox_type) { // We use a custom sandbox definition to lock things down as tightly as // possible. int sandbox_profile_resource_id = -1; // Find resource id for sandbox profile to use for the specific sandbox type. for (size_t i = 0; i < arraysize(kDefaultSandboxTypeToResourceIDMapping); ++i) { if (kDefaultSandboxTypeToResourceIDMapping[i].sandbox_type == sandbox_type) { sandbox_profile_resource_id = kDefaultSandboxTypeToResourceIDMapping[i].sandbox_profile_resource_id; break; } } if (sandbox_profile_resource_id == -1) { // Check if the embedder knows about this sandbox process type. bool sandbox_type_found = GetContentClient()->GetSandboxProfileForSandboxType( sandbox_type, &sandbox_profile_resource_id); CHECK(sandbox_type_found) << "Unknown sandbox type " << sandbox_type; } base::StringPiece sandbox_definition = GetContentClient()->GetDataResource( sandbox_profile_resource_id, ui::SCALE_FACTOR_NONE); if (sandbox_definition.empty()) { LOG(FATAL) << "Failed to load the sandbox profile (resource id " << sandbox_profile_resource_id << ")"; return nil; } base::StringPiece common_sandbox_definition = GetContentClient()->GetDataResource( IDR_COMMON_SANDBOX_PROFILE, ui::SCALE_FACTOR_NONE); if (common_sandbox_definition.empty()) { LOG(FATAL) << "Failed to load the common sandbox profile"; return nil; } scoped_nsobject common_sandbox_prefix_data( [[NSString alloc] initWithBytes:common_sandbox_definition.data() length:common_sandbox_definition.length() encoding:NSUTF8StringEncoding]); scoped_nsobject sandbox_data( [[NSString alloc] initWithBytes:sandbox_definition.data() length:sandbox_definition.length() encoding:NSUTF8StringEncoding]); // Prefix sandbox_data with common_sandbox_prefix_data. return [common_sandbox_prefix_data stringByAppendingString:sandbox_data]; } // static bool Sandbox::PostProcessSandboxProfile( NSString* sandbox_template, NSArray* comments_to_remove, SandboxVariableSubstitions& substitutions, std::string *final_sandbox_profile_str) { NSString* sandbox_data = [[sandbox_template copy] autorelease]; // Remove comments, e.g. ;10.7_OR_ABOVE . for (NSString* to_remove in comments_to_remove) { sandbox_data = [sandbox_data stringByReplacingOccurrencesOfString:to_remove withString:@""]; } // Split string on "@" characters. std::vector raw_sandbox_pieces; if (Tokenize([sandbox_data UTF8String], "@", &raw_sandbox_pieces) == 0) { DLOG(FATAL) << "Bad Sandbox profile, should contain at least one token (" << [sandbox_data UTF8String] << ")"; return false; } // Iterate over string pieces and substitute variables, escaping as necessary. size_t output_string_length = 0; std::vector processed_sandbox_pieces(raw_sandbox_pieces.size()); for (std::vector::iterator it = raw_sandbox_pieces.begin(); it != raw_sandbox_pieces.end(); ++it) { std::string new_piece; SandboxVariableSubstitions::iterator replacement_it = substitutions.find(*it); if (replacement_it == substitutions.end()) { new_piece = *it; } else { // Found something to substitute. SandboxSubstring& replacement = replacement_it->second; switch (replacement.type()) { case SandboxSubstring::PLAIN: new_piece = replacement.value(); break; case SandboxSubstring::LITERAL: if (!QuotePlainString(replacement.value(), &new_piece)) FatalStringQuoteException(replacement.value()); break; case SandboxSubstring::REGEX: if (!QuoteStringForRegex(replacement.value(), &new_piece)) FatalStringQuoteException(replacement.value()); break; } } output_string_length += new_piece.size(); processed_sandbox_pieces.push_back(new_piece); } // Build final output string. final_sandbox_profile_str->reserve(output_string_length); for (std::vector::iterator it = processed_sandbox_pieces.begin(); it != processed_sandbox_pieces.end(); ++it) { final_sandbox_profile_str->append(*it); } return true; } // Turns on the OS X sandbox for this process. // static bool Sandbox::EnableSandbox(int sandbox_type, const base::FilePath& allowed_dir) { // Sanity - currently only SANDBOX_TYPE_UTILITY supports a directory being // passed in. if (sandbox_type < SANDBOX_TYPE_AFTER_LAST_TYPE && sandbox_type != SANDBOX_TYPE_UTILITY) { DCHECK(allowed_dir.empty()) << "Only SANDBOX_TYPE_UTILITY allows a custom directory parameter."; } NSString* sandbox_data = LoadSandboxTemplate(sandbox_type); if (!sandbox_data) { return false; } SandboxVariableSubstitions substitutions; if (!allowed_dir.empty()) { // Add the sandbox commands necessary to access the given directory. // Note: this function must be called before PostProcessSandboxProfile() // since the string it inserts contains variables that need substitution. NSString* allowed_dir_sandbox_command = BuildAllowDirectoryAccessSandboxString(allowed_dir, &substitutions); if (allowed_dir_sandbox_command) { // May be nil if function fails. sandbox_data = [sandbox_data stringByReplacingOccurrencesOfString:@";ENABLE_DIRECTORY_ACCESS" withString:allowed_dir_sandbox_command]; } } NSMutableArray* tokens_to_remove = [NSMutableArray array]; // Enable verbose logging if enabled on the command line. (See common.sb // for details). const CommandLine* command_line = CommandLine::ForCurrentProcess(); bool enable_logging = command_line->HasSwitch(switches::kEnableSandboxLogging);; if (enable_logging) { [tokens_to_remove addObject:@";ENABLE_LOGGING"]; } bool lion_or_later = base::mac::IsOSLionOrLater(); // Without this, the sandbox will print a message to the system log every // time it denies a request. This floods the console with useless spew. if (!enable_logging) { substitutions["DISABLE_SANDBOX_DENIAL_LOGGING"] = SandboxSubstring("(with no-log)"); } else { substitutions["DISABLE_SANDBOX_DENIAL_LOGGING"] = SandboxSubstring(""); } // Splice the path of the user's home directory into the sandbox profile // (see renderer.sb for details). std::string home_dir = [NSHomeDirectory() fileSystemRepresentation]; base::FilePath home_dir_canonical = GetCanonicalSandboxPath(base::FilePath(home_dir)); substitutions["USER_HOMEDIR_AS_LITERAL"] = SandboxSubstring(home_dir_canonical.value(), SandboxSubstring::LITERAL); if (lion_or_later) { // >=10.7 Sandbox rules. [tokens_to_remove addObject:@";10.7_OR_ABOVE"]; } substitutions["COMPONENT_BUILD_WORKAROUND"] = SandboxSubstring(""); #if defined(COMPONENT_BUILD) // dlopen() fails without file-read-metadata access if the executable image // contains LC_RPATH load commands. The components build uses those. // See http://crbug.com/127465 if (base::mac::IsOSSnowLeopard()) { base::FilePath bundle_executable = base::mac::NSStringToFilePath( [base::mac::MainBundle() executablePath]); NSString* sandbox_command = AllowMetadataForPath( GetCanonicalSandboxPath(bundle_executable)); substitutions["COMPONENT_BUILD_WORKAROUND"] = SandboxSubstring(base::SysNSStringToUTF8(sandbox_command)); } #endif // All information needed to assemble the final profile has been collected. // Merge it all together. std::string final_sandbox_profile_str; if (!PostProcessSandboxProfile(sandbox_data, tokens_to_remove, substitutions, &final_sandbox_profile_str)) { return false; } // Initialize sandbox. char* error_buff = NULL; int error = sandbox_init(final_sandbox_profile_str.c_str(), 0, &error_buff); bool success = (error == 0 && error_buff == NULL); DLOG_IF(FATAL, !success) << "Failed to initialize sandbox: " << error << " " << error_buff; sandbox_free_error(error_buff); gSandboxIsActive = success; return success; } // static bool Sandbox::SandboxIsCurrentlyActive() { return gSandboxIsActive; } // static base::FilePath Sandbox::GetCanonicalSandboxPath(const base::FilePath& path) { int fd = HANDLE_EINTR(open(path.value().c_str(), O_RDONLY)); if (fd < 0) { DPLOG(FATAL) << "GetCanonicalSandboxPath() failed for: " << path.value(); return path; } file_util::ScopedFD file_closer(&fd); base::FilePath::CharType canonical_path[MAXPATHLEN]; if (HANDLE_EINTR(fcntl(fd, F_GETPATH, canonical_path)) != 0) { DPLOG(FATAL) << "GetCanonicalSandboxPath() failed for: " << path.value(); return path; } return base::FilePath(canonical_path); } } // namespace content