chromium/sandbox/mac/seatbelt_extension_unittest.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "sandbox/mac/seatbelt_extension.h"

#include <sys/stat.h>
#include <unistd.h>

#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/bind.h"
#include "base/test/multiprocess_test.h"
#include "base/test/test_timeouts.h"
#include "sandbox/mac/sandbox_compiler.h"
#include "sandbox/mac/sandbox_test.h"
#include "sandbox/mac/seatbelt_extension_token.h"
#include "testing/multiprocess_func_list.h"

namespace sandbox {
namespace {

const char kSandboxProfile[] = R"(
  (version 1)
  (deny default (with no-log))
  (allow file-read* (extension "com.apple.app-sandbox.read"))
  (allow file-read* file-write*
    (extension "com.apple.app-sandbox.read-write")
  )
)";

const char kTestData[] = "hello world";
const char kSwitchFile[] = "test-file";
const char kSwitchExtension[] = "test-extension";

class SeatbeltExtensionTest : public SandboxTest {
 public:
  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    file_path_ = temp_dir_.GetPath().AppendASCII("sbox.test");

    ASSERT_TRUE(base::WriteFile(file_path_, kTestData));
  }

  base::FilePath file_path() { return file_path_; }

  base::Process SpawnChildForPathWithToken(
      const std::string& procname,
      const base::FilePath& path,
      const SeatbeltExtensionToken& token) {
    // Ensure any symlinks in the path are canonicalized.
    const base::FilePath canonicalized_path = base::MakeAbsoluteFilePath(path);
    const std::string token_value = token.token();
    CHECK(!canonicalized_path.empty());
    CHECK(!token_value.empty());
    return SpawnChild(
        procname,
        base::BindLambdaForTesting([&](base::CommandLine& command_line) {
          command_line.AppendSwitchPath(kSwitchFile, canonicalized_path);
          command_line.AppendSwitchASCII(kSwitchExtension, token_value);
        }));
  }

 private:
  base::ScopedTempDir temp_dir_;
  base::FilePath file_path_;
};

TEST_F(SeatbeltExtensionTest, FileReadAccess) {
  base::CommandLine command_line(
      base::GetMultiProcessTestChildBaseCommandLine());

  auto token = sandbox::SeatbeltExtension::Issue(
      sandbox::SeatbeltExtension::FILE_READ, file_path().value());
  ASSERT_TRUE(token.get());

  base::Process test_child =
      SpawnChildForPathWithToken("FileReadAccess", file_path(), *token);
  int exit_code = 42;
  test_child.WaitForExitWithTimeout(TestTimeouts::action_max_timeout(),
                                    &exit_code);
  EXPECT_EQ(0, exit_code);
}

MULTIPROCESS_TEST_MAIN(FileReadAccess) {
  sandbox::SandboxCompiler compiler;
  compiler.SetProfile(kSandboxProfile);
  std::string error;
  CHECK(compiler.CompileAndApplyProfile(error)) << error;

  auto* command_line = base::CommandLine::ForCurrentProcess();

  base::FilePath file_path = command_line->GetSwitchValuePath(kSwitchFile);
  CHECK(!file_path.empty());
  const char* path = file_path.value().c_str();

  std::string token_str = command_line->GetSwitchValueASCII(kSwitchExtension);
  CHECK(!token_str.empty());

  auto token = sandbox::SeatbeltExtensionToken::CreateForTesting(token_str);
  auto extension = sandbox::SeatbeltExtension::FromToken(std::move(token));
  CHECK(extension);
  CHECK(token.token().empty());

  // Without consuming the extension, file access is denied.
  errno = 0;
  base::ScopedFD fd(open(path, O_RDONLY));
  CHECK_EQ(-1, fd.get());
  CHECK_EQ(EPERM, errno);

  CHECK(extension->Consume());

  // After consuming the extension, file access is still denied for writing.
  errno = 0;
  fd.reset(open(path, O_RDWR));
  CHECK_EQ(-1, fd.get());
  CHECK_EQ(EPERM, errno);

  // ... but it is allowed to read.
  errno = 0;
  fd.reset(open(path, O_RDONLY));
  PCHECK(fd.get() > 0);

  // Close the file and revoke the extension.
  fd.reset();
  extension->Revoke();

  // File access is denied again.
  errno = 0;
  fd.reset(open(path, O_RDONLY));
  CHECK_EQ(-1, fd.get());
  CHECK_EQ(EPERM, errno);

  // Re-acquire the access by using the token, but this time consume it
  // permanetly.
  token = sandbox::SeatbeltExtensionToken::CreateForTesting(token_str);
  extension = sandbox::SeatbeltExtension::FromToken(std::move(token));
  CHECK(extension);
  CHECK(extension->ConsumePermanently());

  // Check that reading still works.
  errno = 0;
  fd.reset(open(path, O_RDONLY));
  PCHECK(fd.get() > 0);

  return 0;
}

TEST_F(SeatbeltExtensionTest, DirReadWriteAccess) {
  base::CommandLine command_line(
      base::GetMultiProcessTestChildBaseCommandLine());

  auto token = sandbox::SeatbeltExtension::Issue(
      sandbox::SeatbeltExtension::FILE_READ_WRITE,
      file_path().DirName().value());
  ASSERT_TRUE(token.get());

  base::Process test_child =
      SpawnChildForPathWithToken("DirReadWriteAccess", file_path(), *token);
  int exit_code = 42;
  test_child.WaitForExitWithTimeout(TestTimeouts::action_max_timeout(),
                                    &exit_code);
  EXPECT_EQ(0, exit_code);
}

MULTIPROCESS_TEST_MAIN(DirReadWriteAccess) {
  sandbox::SandboxCompiler compiler;
  compiler.SetProfile(kSandboxProfile);
  std::string error;
  CHECK(compiler.CompileAndApplyProfile(error)) << error;

  auto* command_line = base::CommandLine::ForCurrentProcess();

  base::FilePath file_path = command_line->GetSwitchValuePath(kSwitchFile);
  CHECK(!file_path.empty());
  const char* path = file_path.value().c_str();

  std::string token_str = command_line->GetSwitchValueASCII(kSwitchExtension);
  CHECK(!token_str.empty());

  auto token = sandbox::SeatbeltExtensionToken::CreateForTesting(token_str);
  auto extension = sandbox::SeatbeltExtension::FromToken(std::move(token));
  CHECK(extension);
  CHECK(token.token().empty());

  // Without consuming the extension, file access is denied.
  errno = 0;
  base::ScopedFD fd(open(path, O_RDONLY));
  CHECK_EQ(-1, fd.get());
  CHECK_EQ(EPERM, errno);

  CHECK(extension->ConsumePermanently());

  // After consuming the extension, file read/write access is allowed.
  errno = 0;
  fd.reset(open(path, O_RDWR));
  PCHECK(fd.get() > 0);

  char c = 'a';
  PCHECK(write(fd.get(), &c, sizeof(c)) == sizeof(c));

  // A new file can be created in the extension directory.
  base::FilePath new_file = file_path.DirName().AppendASCII("new_file.txt");
  fd.reset(open(path, O_RDWR | O_CREAT));
  PCHECK(fd.get() > 0);

  // Test reading and writing to the new file.
  PCHECK(write(fd.get(), &c, sizeof(c)) == sizeof(c));

  PCHECK(lseek(fd.get(), 0, SEEK_SET) == 0);

  c = 'b';
  PCHECK(read(fd.get(), &c, sizeof(c)) == sizeof(c));
  CHECK_EQ(c, 'a');

  // Accessing the parent directory is forbidden.
  errno = 0;
  struct stat sb;
  PCHECK(stat(file_path.DirName().DirName().value().c_str(), &sb) == -1);
  CHECK_EQ(EPERM, errno);

  // ... but the dir itself is okay.
  errno = 0;
  PCHECK(stat(file_path.DirName().value().c_str(), &sb) == 0);

  return 0;
}

}  // namespace
}  // namespace sandbox