// Copyright 2017 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 "sandbox/mac/seatbelt_extension.h" #include #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/stl_util.h" #include "base/test/multiprocess_test.h" #include "base/test/test_timeouts.h" #include "sandbox/mac/sandbox_compiler.h" #include "sandbox/mac/seatbelt_extension_token.h" #include "testing/multiprocess_func_list.h" namespace sandbox { namespace { const char kSandboxProfile[] = "(version 1)\n" "(deny default (with no-log))\n" "(allow file-read* (extension \"com.apple.app-sandbox.read\"))"; const char kTestData[] = "hello world"; constexpr int kTestDataLen = base::size(kTestData); const char kSwitchFile[] = "test-file"; const char kSwitchExtension[] = "test-extension"; class SeatbeltExtensionTest : public base::MultiProcessTest { public: void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); file_path_ = temp_dir_.GetPath().AppendASCII("sbox.test"); ASSERT_EQ(kTestDataLen, base::WriteFile(file_path_, kTestData, kTestDataLen)); } base::FilePath file_path() { return file_path_; } 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()); // Ensure any symlinks in the path are canonicalized. base::FilePath path = base::MakeAbsoluteFilePath(file_path()); ASSERT_FALSE(path.empty()); command_line.AppendSwitchPath(kSwitchFile, path); command_line.AppendSwitchASCII(kSwitchExtension, token->token()); base::Process test_child = base::SpawnMultiProcessTestChild( "FileReadAccess", command_line, base::LaunchOptions()); 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(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; } } // namespace } // namespace sandbox