// 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 <Cocoa/Cocoa.h>

#include <CoreFoundation/CFTimeZone.h>
extern "C" {
#include <sandbox.h>
}
#include <signal.h>
#include <sys/param.h>

#include "base/basictypes.h"
#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.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/mac/scoped_nsobject.h"
#include "base/rand_util.h"
#include "base/strings/string16.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/sys_info.h"
#include "content/grit/content_resources.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "third_party/icu/source/common/unicode/uchar.h"
#include "ui/base/layout.h"
#include "ui/gl/gl_surface.h"

extern "C" {
void CGSSetDenyWindowServerConnections(bool);
void CGSShutdownServerConnections();
};

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_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<base::FilePath> 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<base::FilePath>::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<char>(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<unsigned int>(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<char>(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<char>(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::ScopedCFTypeRef<CGColorSpaceRef> rgb_colorspace(
        CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB));

    // Allocate a 1x1 image.
    char data[4];
    base::ScopedCFTypeRef<CGContextRef> context(CGBitmapContextCreate(
        data,
        1,
        1,
        8,
        1 * 4,
        rgb_colorspace,
        kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host));

    // Load in the color profiles we'll need (as a side effect).
    ignore_result(base::mac::GetSRGBColorSpace());
    ignore_result(base::mac::GetSystemColorSpace());

    // CGColorSpaceCreateSystemDefaultCMYK - 10.6
    base::ScopedCFTypeRef<CGColorSpaceRef> cmyk_colorspace(
        CGColorSpaceCreateWithName(kCGColorSpaceGenericCMYK));
  }

  { // 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::ScopedCFTypeRef<CGImageSourceRef> img(
        CGImageSourceCreateWithData((CFDataRef)data, NULL));
    CGImageSourceGetStatus(img);
  }

  {
    // Allow access to /dev/urandom.
    base::GetUrandomFD();
  }

  { // IOSurfaceLookup() - 10.7
    // Needed by zero-copy texture update framework - crbug.com/323338
    base::ScopedCFTypeRef<IOSurfaceRef> io_surface(IOSurfaceLookup(0));
  }

  // Process-type dependent warm-up.
  if (sandbox_type == SANDBOX_TYPE_UTILITY) {
    // CFTimeZoneCopyZone() tries to read /etc and /private/etc/localtime - 10.8
    // Needed by Media Galleries API Picasa - crbug.com/151701
    CFTimeZoneCopySystem();
  }

  if (sandbox_type == SANDBOX_TYPE_GPU) {
    // Preload either the desktop GL or the osmesa so, depending on the
    // --use-gl flag.
    gfx::GLSurface::InitializeOneOff();
  }

  if (sandbox_type == SANDBOX_TYPE_PPAPI) {
    // Preload AppKit color spaces used for Flash/ppapi. http://crbug.com/348304
    NSColor* color = [NSColor controlTextColor];
    [color colorUsingColorSpaceName:NSCalibratedRGBColorSpace];
  }

  if (sandbox_type == SANDBOX_TYPE_RENDERER &&
      base::mac::IsOSMountainLionOrLater()) {
    // Now disconnect from WindowServer, after all objects have been warmed up.
    // Shutting down the connection requires connecting to WindowServer,
    // so do this before actually engaging the sandbox. This is only done on
    // 10.8 and higher because doing it on earlier OSes causes layout tests to
    // fail <http://crbug.com/397642#c48>. This may cause two log messages to
    // be printed to the system logger on certain OS versions.
    CGSSetDenyWindowServerConnections(true);
    CGSShutdownServerConnections();
  }
}

// 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;
  }

  base::scoped_nsobject<NSString> common_sandbox_prefix_data(
      [[NSString alloc] initWithBytes:common_sandbox_definition.data()
                               length:common_sandbox_definition.length()
                             encoding:NSUTF8StringEncoding]);

  base::scoped_nsobject<NSString> 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<std::string> 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<std::string> processed_sandbox_pieces(raw_sandbox_pieces.size());
  for (std::vector<std::string>::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<std::string>::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 base::CommandLine* command_line =
      base::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) {
  base::ScopedFD fd(HANDLE_EINTR(open(path.value().c_str(), O_RDONLY)));
  if (!fd.is_valid()) {
    DPLOG(FATAL) << "GetCanonicalSandboxPath() failed for: "
                 << path.value();
    return path;
  }

  base::FilePath::CharType canonical_path[MAXPATHLEN];
  if (HANDLE_EINTR(fcntl(fd.get(), F_GETPATH, canonical_path)) != 0) {
    DPLOG(FATAL) << "GetCanonicalSandboxPath() failed for: "
                 << path.value();
    return path;
  }

  return base::FilePath(canonical_path);
}

}  // namespace content