blob: 0b7639d1a6efd592e82cebfe8ac91dc0ceced932 [file] [log] [blame]
// Copyright 2014 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 "chrome/browser/process_singleton.h"
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stddef.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <unistd.h>
#include <memory>
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/location.h"
#include "base/posix/eintr_wrapper.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/stringprintf.h"
#include "base/synchronization/waitable_event.h"
#include "base/task/post_task.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/test_timeouts.h"
#include "base/test/thread_test_helper.h"
#include "base/threading/thread.h"
#include "build/build_config.h"
#include "chrome/common/chrome_constants.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "net/base/network_interfaces.h"
#include "testing/gtest/include/gtest/gtest.h"
using content::BrowserThread;
namespace {
class ProcessSingletonPosixTest : public testing::Test {
public:
// A ProcessSingleton exposing some protected methods for testing.
class TestableProcessSingleton : public ProcessSingleton {
public:
explicit TestableProcessSingleton(const base::FilePath& user_data_dir)
: ProcessSingleton(
user_data_dir,
base::Bind(&TestableProcessSingleton::NotificationCallback,
base::Unretained(this))) {}
std::vector<base::CommandLine::StringVector> callback_command_lines_;
using ProcessSingleton::NotifyOtherProcessWithTimeout;
using ProcessSingleton::NotifyOtherProcessWithTimeoutOrCreate;
using ProcessSingleton::OverrideCurrentPidForTesting;
using ProcessSingleton::OverrideKillCallbackForTesting;
private:
bool NotificationCallback(const base::CommandLine& command_line,
const base::FilePath& current_directory) {
callback_command_lines_.push_back(command_line.argv());
return true;
}
};
ProcessSingletonPosixTest()
: kill_callbacks_(0),
test_browser_thread_bundle_(
content::TestBrowserThreadBundle::REAL_IO_THREAD),
wait_event_(base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED),
signal_event_(base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED),
process_singleton_on_thread_(NULL) {}
void SetUp() override {
testing::Test::SetUp();
ProcessSingleton::DisablePromptForTesting();
ProcessSingleton::SkipIsChromeProcessCheckForTesting(false);
ProcessSingleton::SetUserOptedUnlockInUseProfileForTesting(false);
// Put the lock in a temporary directory. Doesn't need to be a
// full profile to test this code.
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
// Use a long directory name to ensure that the socket isn't opened through
// the symlink.
user_data_path_ = temp_dir_.GetPath().Append(
std::string(sizeof(sockaddr_un::sun_path), 'a'));
ASSERT_TRUE(CreateDirectory(user_data_path_));
lock_path_ = user_data_path_.Append(chrome::kSingletonLockFilename);
socket_path_ = user_data_path_.Append(chrome::kSingletonSocketFilename);
cookie_path_ = user_data_path_.Append(chrome::kSingletonCookieFilename);
}
void TearDown() override {
scoped_refptr<base::ThreadTestHelper> io_helper(new base::ThreadTestHelper(
base::CreateSingleThreadTaskRunnerWithTraits({BrowserThread::IO})
.get()));
ASSERT_TRUE(io_helper->Run());
// Destruct the ProcessSingleton object before the IO thread so that its
// internals are destructed properly.
if (process_singleton_on_thread_) {
worker_thread_->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&ProcessSingletonPosixTest::DestructProcessSingleton,
base::Unretained(this)));
scoped_refptr<base::ThreadTestHelper> helper(
new base::ThreadTestHelper(worker_thread_->task_runner().get()));
ASSERT_TRUE(helper->Run());
}
testing::Test::TearDown();
}
void CreateProcessSingletonOnThread() {
ASSERT_EQ(NULL, worker_thread_.get());
worker_thread_.reset(new base::Thread("BlockingThread"));
worker_thread_->Start();
worker_thread_->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(
&ProcessSingletonPosixTest::CreateProcessSingletonInternal,
base::Unretained(this)));
scoped_refptr<base::ThreadTestHelper> helper(
new base::ThreadTestHelper(worker_thread_->task_runner().get()));
ASSERT_TRUE(helper->Run());
}
TestableProcessSingleton* CreateProcessSingleton() {
return new TestableProcessSingleton(user_data_path_);
}
void VerifyFiles() {
struct stat statbuf;
ASSERT_EQ(0, lstat(lock_path_.value().c_str(), &statbuf));
ASSERT_TRUE(S_ISLNK(statbuf.st_mode));
char buf[PATH_MAX];
ssize_t len = readlink(lock_path_.value().c_str(), buf, PATH_MAX);
ASSERT_GT(len, 0);
ASSERT_EQ(0, lstat(socket_path_.value().c_str(), &statbuf));
ASSERT_TRUE(S_ISLNK(statbuf.st_mode));
len = readlink(socket_path_.value().c_str(), buf, PATH_MAX);
ASSERT_GT(len, 0);
base::FilePath socket_target_path = base::FilePath(std::string(buf, len));
ASSERT_EQ(0, lstat(socket_target_path.value().c_str(), &statbuf));
ASSERT_TRUE(S_ISSOCK(statbuf.st_mode));
len = readlink(cookie_path_.value().c_str(), buf, PATH_MAX);
ASSERT_GT(len, 0);
std::string cookie(buf, len);
base::FilePath remote_cookie_path = socket_target_path.DirName().
Append(chrome::kSingletonCookieFilename);
len = readlink(remote_cookie_path.value().c_str(), buf, PATH_MAX);
ASSERT_GT(len, 0);
EXPECT_EQ(cookie, std::string(buf, len));
}
ProcessSingleton::NotifyResult NotifyOtherProcess(bool override_kill) {
std::unique_ptr<TestableProcessSingleton> process_singleton(
CreateProcessSingleton());
base::CommandLine command_line(
base::CommandLine::ForCurrentProcess()->GetProgram());
command_line.AppendArg("about:blank");
if (override_kill) {
process_singleton->OverrideCurrentPidForTesting(
base::GetCurrentProcId() + 1);
process_singleton->OverrideKillCallbackForTesting(
base::Bind(&ProcessSingletonPosixTest::KillCallback,
base::Unretained(this)));
}
return process_singleton->NotifyOtherProcessWithTimeout(
command_line, kRetryAttempts, timeout(), true);
}
// A helper method to call ProcessSingleton::NotifyOtherProcessOrCreate().
ProcessSingleton::NotifyResult NotifyOtherProcessOrCreate(
const std::string& url) {
std::unique_ptr<TestableProcessSingleton> process_singleton(
CreateProcessSingleton());
base::CommandLine command_line(
base::CommandLine::ForCurrentProcess()->GetProgram());
command_line.AppendArg(url);
return process_singleton->NotifyOtherProcessWithTimeoutOrCreate(
command_line, kRetryAttempts, timeout());
}
void CheckNotified() {
ASSERT_TRUE(process_singleton_on_thread_ != NULL);
ASSERT_EQ(1u, process_singleton_on_thread_->callback_command_lines_.size());
bool found = false;
for (size_t i = 0;
i < process_singleton_on_thread_->callback_command_lines_[0].size();
++i) {
if (process_singleton_on_thread_->callback_command_lines_[0][i] ==
"about:blank") {
found = true;
break;
}
}
ASSERT_TRUE(found);
ASSERT_EQ(0, kill_callbacks_);
}
void BlockWorkerThread() {
worker_thread_->task_runner()->PostTask(
FROM_HERE, base::BindOnce(&ProcessSingletonPosixTest::BlockThread,
base::Unretained(this)));
}
void UnblockWorkerThread() {
wait_event_.Signal(); // Unblock the worker thread for shutdown.
signal_event_.Wait(); // Ensure thread unblocks before continuing.
}
void BlockThread() {
wait_event_.Wait();
signal_event_.Signal();
}
base::FilePath user_data_path_;
base::FilePath lock_path_;
base::FilePath socket_path_;
base::FilePath cookie_path_;
int kill_callbacks_;
private:
static const int kRetryAttempts = 2;
base::TimeDelta timeout() const {
return TestTimeouts::tiny_timeout() * kRetryAttempts;
}
void CreateProcessSingletonInternal() {
ASSERT_TRUE(!process_singleton_on_thread_);
process_singleton_on_thread_ = CreateProcessSingleton();
ASSERT_EQ(ProcessSingleton::PROCESS_NONE,
process_singleton_on_thread_->NotifyOtherProcessOrCreate());
}
void DestructProcessSingleton() {
ASSERT_TRUE(process_singleton_on_thread_);
delete process_singleton_on_thread_;
}
void KillCallback(int pid) {
kill_callbacks_++;
}
content::TestBrowserThreadBundle test_browser_thread_bundle_;
base::ScopedTempDir temp_dir_;
base::WaitableEvent wait_event_;
base::WaitableEvent signal_event_;
std::unique_ptr<base::Thread> worker_thread_;
TestableProcessSingleton* process_singleton_on_thread_;
};
} // namespace
// Test if the socket file and symbol link created by ProcessSingletonPosix
// are valid.
// If this test flakes, use http://crbug.com/74554.
TEST_F(ProcessSingletonPosixTest, CheckSocketFile) {
CreateProcessSingletonOnThread();
VerifyFiles();
}
// TODO(james.su@gmail.com): port following tests to Windows.
// Test success case of NotifyOtherProcess().
TEST_F(ProcessSingletonPosixTest, NotifyOtherProcessSuccess) {
CreateProcessSingletonOnThread();
EXPECT_EQ(ProcessSingleton::PROCESS_NOTIFIED, NotifyOtherProcess(true));
CheckNotified();
}
// Test failure case of NotifyOtherProcess().
TEST_F(ProcessSingletonPosixTest, NotifyOtherProcessFailure) {
base::HistogramTester histogram_tester;
CreateProcessSingletonOnThread();
BlockWorkerThread();
EXPECT_EQ(ProcessSingleton::PROCESS_NONE, NotifyOtherProcess(true));
ASSERT_EQ(1, kill_callbacks_);
UnblockWorkerThread();
histogram_tester.ExpectUniqueSample(
"Chrome.ProcessSingleton.RemoteHungProcessTerminateReason",
ProcessSingleton::SOCKET_READ_FAILED, 1u);
}
// Test that we don't kill ourselves by accident if a lockfile with the same pid
// happens to exist.
TEST_F(ProcessSingletonPosixTest, NotifyOtherProcessNoSuicide) {
base::HistogramTester histogram_tester;
CreateProcessSingletonOnThread();
// Replace lockfile with one containing our own pid.
EXPECT_EQ(0, unlink(lock_path_.value().c_str()));
std::string symlink_content = base::StringPrintf(
"%s%c%u",
net::GetHostName().c_str(),
'-',
base::GetCurrentProcId());
EXPECT_EQ(0, symlink(symlink_content.c_str(), lock_path_.value().c_str()));
// Remove socket so that we will not be able to notify the existing browser.
EXPECT_EQ(0, unlink(socket_path_.value().c_str()));
// Pretend we are browser process.
ProcessSingleton::SkipIsChromeProcessCheckForTesting(true);
EXPECT_EQ(ProcessSingleton::PROCESS_NONE, NotifyOtherProcess(false));
// If we've gotten to this point without killing ourself, the test succeeded.
histogram_tester.ExpectUniqueSample(
"Chrome.ProcessSingleton.RemoteProcessInteractionResult",
ProcessSingleton::SAME_BROWSER_INSTANCE, 1u);
}
// Test that we can still notify a process on the same host even after the
// hostname changed.
TEST_F(ProcessSingletonPosixTest, NotifyOtherProcessHostChanged) {
CreateProcessSingletonOnThread();
EXPECT_EQ(0, unlink(lock_path_.value().c_str()));
EXPECT_EQ(0, symlink("FAKEFOOHOST-1234", lock_path_.value().c_str()));
EXPECT_EQ(ProcessSingleton::PROCESS_NOTIFIED, NotifyOtherProcess(false));
CheckNotified();
}
// Test that we kill hung browser when lock says process is on another host and
// we can't notify it over the socket.
TEST_F(ProcessSingletonPosixTest, NotifyOtherProcessDifferingHost) {
base::HistogramTester histogram_tester;
CreateProcessSingletonOnThread();
BlockWorkerThread();
EXPECT_EQ(0, unlink(lock_path_.value().c_str()));
EXPECT_EQ(0, symlink("FAKEFOOHOST-1234", lock_path_.value().c_str()));
EXPECT_EQ(ProcessSingleton::PROCESS_NONE, NotifyOtherProcess(true));
ASSERT_EQ(1, kill_callbacks_);
// lock_path_ should be unlinked in NotifyOtherProcess().
base::FilePath target_path;
EXPECT_FALSE(base::ReadSymbolicLink(lock_path_, &target_path));
UnblockWorkerThread();
histogram_tester.ExpectUniqueSample(
"Chrome.ProcessSingleton.RemoteHungProcessTerminateReason",
ProcessSingleton::SOCKET_READ_FAILED, 1u);
}
// Test that we'll start creating ProcessSingleton when we have old lock file
// that says process is on another host and there is browser with the same pid
// but with another user data dir. Also suppose that user opted to unlock
// profile.
TEST_F(ProcessSingletonPosixTest,
NotifyOtherProcessDifferingHost_UnlockedProfileBeforeKill) {
base::HistogramTester histogram_tester;
CreateProcessSingletonOnThread();
BlockWorkerThread();
EXPECT_EQ(0, unlink(lock_path_.value().c_str()));
EXPECT_EQ(0, symlink("FAKEFOOHOST-1234", lock_path_.value().c_str()));
// Remove socket so that we will not be able to notify the existing browser.
EXPECT_EQ(0, unlink(socket_path_.value().c_str()));
// Unlock profile that was locked by process on another host.
ProcessSingleton::SetUserOptedUnlockInUseProfileForTesting(true);
// Treat process with pid 1234 as browser with different user data dir.
ProcessSingleton::SkipIsChromeProcessCheckForTesting(true);
EXPECT_EQ(ProcessSingleton::PROCESS_NONE, NotifyOtherProcess(false));
// lock_path_ should be unlinked in NotifyOtherProcess().
base::FilePath target_path;
EXPECT_FALSE(base::ReadSymbolicLink(lock_path_, &target_path));
UnblockWorkerThread();
histogram_tester.ExpectUniqueSample(
"Chrome.ProcessSingleton.RemoteHungProcessTerminateReason",
ProcessSingleton::NOTIFY_ATTEMPTS_EXCEEDED, 1u);
histogram_tester.ExpectUniqueSample(
"Chrome.ProcessSingleton.RemoteProcessInteractionResult",
ProcessSingleton::PROFILE_UNLOCKED_BEFORE_KILL, 1u);
}
// Test that we unlock profile when lock says process is on another host and we
// can't notify it over the socket.
TEST_F(ProcessSingletonPosixTest, NotifyOtherProcessOrCreate_DifferingHost) {
base::HistogramTester histogram_tester;
CreateProcessSingletonOnThread();
BlockWorkerThread();
EXPECT_EQ(0, unlink(lock_path_.value().c_str()));
EXPECT_EQ(0, symlink("FAKEFOOHOST-1234", lock_path_.value().c_str()));
// Remove socket so that we will not be able to notify the existing browser.
EXPECT_EQ(0, unlink(socket_path_.value().c_str()));
// Unlock profile that was locked by process on another host.
ProcessSingleton::SetUserOptedUnlockInUseProfileForTesting(true);
std::string url("about:blank");
EXPECT_EQ(ProcessSingleton::PROCESS_NONE, NotifyOtherProcessOrCreate(url));
ASSERT_EQ(0, unlink(lock_path_.value().c_str()));
UnblockWorkerThread();
histogram_tester.ExpectUniqueSample(
"Chrome.ProcessSingleton.RemoteProcessInteractionResult",
ProcessSingleton::PROFILE_UNLOCKED, 1u);
}
// Test that Create fails when another browser is using the profile directory.
TEST_F(ProcessSingletonPosixTest, CreateFailsWithExistingBrowser) {
CreateProcessSingletonOnThread();
std::unique_ptr<TestableProcessSingleton> process_singleton(
CreateProcessSingleton());
process_singleton->OverrideCurrentPidForTesting(base::GetCurrentProcId() + 1);
EXPECT_FALSE(process_singleton->Create());
}
// Test that Create fails when another browser is using the profile directory
// but with the old socket location.
TEST_F(ProcessSingletonPosixTest, CreateChecksCompatibilitySocket) {
CreateProcessSingletonOnThread();
std::unique_ptr<TestableProcessSingleton> process_singleton(
CreateProcessSingleton());
process_singleton->OverrideCurrentPidForTesting(base::GetCurrentProcId() + 1);
// Do some surgery so as to look like the old configuration.
char buf[PATH_MAX];
ssize_t len = readlink(socket_path_.value().c_str(), buf, sizeof(buf));
ASSERT_GT(len, 0);
base::FilePath socket_target_path = base::FilePath(std::string(buf, len));
ASSERT_EQ(0, unlink(socket_path_.value().c_str()));
ASSERT_EQ(0, rename(socket_target_path.value().c_str(),
socket_path_.value().c_str()));
ASSERT_EQ(0, unlink(cookie_path_.value().c_str()));
EXPECT_FALSE(process_singleton->Create());
}
// Test that we fail when lock says process is on another host and we can't
// notify it over the socket before of a bad cookie.
TEST_F(ProcessSingletonPosixTest, NotifyOtherProcessOrCreate_BadCookie) {
CreateProcessSingletonOnThread();
// Change the cookie.
EXPECT_EQ(0, unlink(cookie_path_.value().c_str()));
EXPECT_EQ(0, symlink("INCORRECTCOOKIE", cookie_path_.value().c_str()));
// Also change the hostname, so the remote does not retry.
EXPECT_EQ(0, unlink(lock_path_.value().c_str()));
EXPECT_EQ(0, symlink("FAKEFOOHOST-1234", lock_path_.value().c_str()));
std::string url("about:blank");
EXPECT_EQ(ProcessSingleton::PROFILE_IN_USE, NotifyOtherProcessOrCreate(url));
}
TEST_F(ProcessSingletonPosixTest, IgnoreSocketSymlinkWithTooLongTarget) {
base::HistogramTester histogram_tester;
CreateProcessSingletonOnThread();
// Change the symlink to one with a too-long target.
char buf[PATH_MAX];
ssize_t len = readlink(socket_path_.value().c_str(), buf, PATH_MAX);
ASSERT_GT(len, 0);
base::FilePath socket_target_path = base::FilePath(std::string(buf, len));
base::FilePath long_socket_target_path = socket_target_path.DirName().Append(
std::string(sizeof(sockaddr_un::sun_path), 'b'));
ASSERT_EQ(0, unlink(socket_path_.value().c_str()));
ASSERT_EQ(0, symlink(long_socket_target_path.value().c_str(),
socket_path_.value().c_str()));
// A new ProcessSingleton should ignore the invalid socket path target.
std::string url("about:blank");
EXPECT_EQ(ProcessSingleton::PROCESS_NONE, NotifyOtherProcessOrCreate(url));
// Lock file contains PID of unit_tests process. It is non browser process so
// we treat lock file as orphaned.
histogram_tester.ExpectUniqueSample(
"Chrome.ProcessSingleton.RemoteProcessInteractionResult",
ProcessSingleton::ORPHANED_LOCK_FILE, 1u);
}
#if defined(OS_MACOSX)
// Test that if there is an existing lock file, and we could not flock()
// it, then exit.
TEST_F(ProcessSingletonPosixTest, CreateRespectsOldMacLock) {
std::unique_ptr<TestableProcessSingleton> process_singleton(
CreateProcessSingleton());
base::ScopedFD lock_fd(HANDLE_EINTR(
open(lock_path_.value().c_str(), O_RDWR | O_CREAT | O_EXLOCK, 0644)));
ASSERT_TRUE(lock_fd.is_valid());
EXPECT_FALSE(process_singleton->Create());
base::File::Info info;
EXPECT_TRUE(base::GetFileInfo(lock_path_, &info));
EXPECT_FALSE(info.is_directory);
EXPECT_FALSE(info.is_symbolic_link);
}
// Test that if there is an existing lock file, and it's not locked, we replace
// it.
TEST_F(ProcessSingletonPosixTest, CreateReplacesOldMacLock) {
std::unique_ptr<TestableProcessSingleton> process_singleton(
CreateProcessSingleton());
EXPECT_EQ(0, base::WriteFile(lock_path_, "", 0));
EXPECT_TRUE(process_singleton->Create());
VerifyFiles();
}
#endif // defined(OS_MACOSX)