Commit e5f1b06d authored by Olivier Li's avatar Olivier Li Committed by Commit Bot

Implement ProtoChromePromptIPC.

Bug: 969139
Change-Id: I35d551b6bdb41b4e6bd7689120c21d9d683ad72f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1733747
Commit-Queue: Oliver Li <olivierli@chromium.org>
Reviewed-by: default avatarWill Harris <wfh@chromium.org>
Reviewed-by: default avatarJoe Mason <joenotcharles@google.com>
Cr-Commit-Position: refs/heads/master@{#688357}
parent 3309cce0
......@@ -5,14 +5,6 @@
import("//build/config/jumbo.gni")
import("//third_party/protobuf/proto_library.gni")
proto_library("test_only_proto") {
testonly = true
generate_python = false
sources = [
"chrome_prompt_for_tests.proto",
]
}
source_set("public") {
sources = [
"chrome_cleaner_controller_win.cc",
......
......@@ -28,8 +28,8 @@
#include "base/win/scoped_handle.h"
#include "base/win/win_util.h"
#include "chrome/browser/safe_browsing/chrome_cleaner/chrome_prompt_actions_win.h"
#include "chrome/browser/safe_browsing/chrome_cleaner/chrome_prompt_for_tests.pb.h"
#include "components/chrome_cleaner/public/constants/constants.h"
#include "components/chrome_cleaner/public/proto/chrome_prompt_for_tests.pb.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
......
......@@ -24,14 +24,18 @@ source_set("chrome_prompt_ipc") {
"chrome_prompt_ipc.h",
"mojo_chrome_prompt_ipc.cc",
"mojo_chrome_prompt_ipc.h",
"proto_chrome_prompt_ipc.cc",
"proto_chrome_prompt_ipc.h",
]
deps = [
":mojo_task_runner",
"//base",
"//components/chrome_cleaner/public/interfaces",
"//components/chrome_cleaner/public/proto",
"//mojo/public/cpp/platform",
"//mojo/public/cpp/system",
"//third_party/protobuf:protobuf_lite",
]
}
......@@ -67,6 +71,7 @@ source_set("ipc_test_util") {
]
deps = [
":chrome_prompt_ipc",
":mojo_task_runner",
"//base",
"//base/test:test_support",
......@@ -82,9 +87,10 @@ source_set("unittest_sources") {
testonly = true
sources = [
"chrome_prompt_ipc_unittest.cc",
"mojo_chrome_prompt_ipc_unittest.cc",
"mojo_sandbox_hooks_unittest.cc",
"mojo_task_runner_unittest.cc",
"proto_chrome_prompt_ipc_unittest.cc",
"sandbox_unittest.cc",
]
......@@ -100,7 +106,10 @@ source_set("unittest_sources") {
"//chrome/chrome_cleaner/mojom:mojo_sandbox_hooks_test_interface",
"//chrome/chrome_cleaner/os:common_os",
"//chrome/chrome_cleaner/test:test_util",
"//components/chrome_cleaner/public/constants",
"//components/chrome_cleaner/public/interfaces",
"//components/chrome_cleaner/public/proto",
"//components/chrome_cleaner/public/proto:test_only_proto",
"//components/chrome_cleaner/test:test_name_helper",
"//mojo/core/embedder",
"//sandbox/win:sandbox",
......
......@@ -85,42 +85,6 @@ class ChromePromptIPC {
kDoneInteraction,
};
virtual void RunPromptUserTask(
const std::vector<base::FilePath>& files_to_delete,
const std::vector<base::string16>& registry_keys,
const std::vector<base::string16>& extension_ids,
mojom::ChromePrompt::PromptUserCallback callback) = 0;
virtual void RunDisableExtensionsTask(
const std::vector<base::string16>& extension_ids,
mojom::ChromePrompt::DisableExtensionsCallback callback) = 0;
// Callback for ChromePrompt::PromptUser, internal state must be
// State::kWaitingForResponseFromChrome. Invokes callback(prompt_acceptance)
// and transitions to state State::kDoneInteraction.
virtual void OnChromeResponseReceived(
mojom::ChromePrompt::PromptUserCallback callback,
mojom::PromptAcceptance prompt_acceptance) = 0;
// Callback for ChromePrompt::DisableExtensions, internal state must be
// State::kDoneInteraction. Invokes callback(extensions_deleted_callback).
virtual void OnChromeResponseReceivedExtensions(
mojom::ChromePrompt::DisableExtensionsCallback callback,
bool extensions_deleted_callback) = 0;
// Connection error handler. Invokes either
// error_handler_->OnConnectionClosed() or
// error_handler_->OnConnectionClosedAfterDone(), depending on the internal
// state.
virtual void OnConnectionError() = 0;
virtual void PromptUserCheckVersion(
const std::vector<base::FilePath>& files_to_delete,
const std::vector<base::string16>& registry_keys,
const std::vector<base::string16>& extension_ids,
mojom::ChromePrompt::PromptUserCallback callback,
uint32_t version) = 0;
State state_ = State::kUninitialized;
ErrorHandler* error_handler_ = nullptr;
......
......@@ -63,6 +63,14 @@ class MojoSandboxSetupHooks : public SandboxSetupHooks {
SandboxedParentProcess* parent_process_;
};
} // namespace
namespace internal {
base::FilePath::StringPieceType GetLogPathSuffix() {
return kIPCTestUtilLogSuffix;
}
base::FilePath GetLogPath() {
return ScopedLogging::GetLogFilePath(kIPCTestUtilLogSuffix);
}
......@@ -107,7 +115,7 @@ void PrintChildProcessLogs() {
}
}
} // namespace
} // namespace internal
ParentProcess::ParentProcess(scoped_refptr<MojoTaskRunner> mojo_task_runner)
: command_line_(base::GetMultiProcessTestChildBaseCommandLine()),
......@@ -166,7 +174,7 @@ bool ParentProcess::LaunchConnectedChildProcess(
const std::string& child_main_function,
base::TimeDelta timeout,
int32_t* exit_code) {
if (!DeleteChildProcessLogs())
if (!internal::DeleteChildProcessLogs())
return false;
if (!PrepareAndLaunchTestChildProcess(child_main_function))
......@@ -182,7 +190,7 @@ bool ParentProcess::LaunchConnectedChildProcess(
DestroyImplOnIPCThread();
if (!success || *exit_code != 0)
PrintChildProcessLogs();
internal::PrintChildProcessLogs();
return success;
}
......@@ -289,4 +297,20 @@ std::string ChildProcess::mojo_pipe_token() const {
return command_line_->GetSwitchValueASCII(kMojoPipeTokenSwitch);
}
ChromePromptIPCTestErrorHandler::ChromePromptIPCTestErrorHandler(
base::OnceClosure on_closed,
base::OnceClosure on_closed_after_done)
: on_closed_(std::move(on_closed)),
on_closed_after_done_(std::move(on_closed_after_done)) {}
ChromePromptIPCTestErrorHandler::~ChromePromptIPCTestErrorHandler() = default;
void ChromePromptIPCTestErrorHandler::OnConnectionClosed() {
std::move(on_closed_).Run();
}
void ChromePromptIPCTestErrorHandler::OnConnectionClosedAfterDone() {
std::move(on_closed_after_done_).Run();
}
} // namespace chrome_cleaner
......@@ -14,6 +14,7 @@
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/time/time.h"
#include "chrome/chrome_cleaner/ipc/chrome_prompt_ipc.h"
#include "chrome/chrome_cleaner/ipc/mojo_task_runner.h"
#include "chrome/chrome_cleaner/logging/scoped_logging.h"
#include "mojo/public/cpp/platform/platform_channel.h"
......@@ -126,6 +127,27 @@ class ChildProcess : public base::RefCountedThreadSafe<ChildProcess> {
bool target_services_initialized_ = false;
};
class ChromePromptIPCTestErrorHandler : public ChromePromptIPC::ErrorHandler {
public:
ChromePromptIPCTestErrorHandler(base::OnceClosure on_closed,
base::OnceClosure on_closed_after_done);
~ChromePromptIPCTestErrorHandler() override;
void OnConnectionClosed() override;
void OnConnectionClosedAfterDone() override;
private:
base::OnceClosure on_closed_;
base::OnceClosure on_closed_after_done_;
};
namespace internal {
base::FilePath::StringPieceType GetLogPathSuffix();
bool DeleteChildProcessLogs();
void PrintChildProcessLogs();
} // namespace internal
} // namespace chrome_cleaner
#endif // CHROME_CHROME_CLEANER_IPC_IPC_TEST_UTIL_H_
......@@ -8,8 +8,7 @@
namespace chrome_cleaner {
MockChromePromptIPC::MockChromePromptIPC()
: MojoChromePromptIPC(std::string(), nullptr) {}
MockChromePromptIPC::MockChromePromptIPC() = default;
MockChromePromptIPC::~MockChromePromptIPC() = default;
......@@ -22,4 +21,10 @@ void MockChromePromptIPC::PostPromptUserTask(
&callback);
}
void MockChromePromptIPC::PostDisableExtensionsTask(
const std::vector<base::string16>& extension_ids,
mojom::ChromePrompt::DisableExtensionsCallback callback) {
MockPostDisableExtensionsTask(extension_ids, &callback);
}
} // namespace chrome_cleaner
......@@ -8,13 +8,13 @@
#include <memory>
#include <vector>
#include "chrome/chrome_cleaner/ipc/mojo_chrome_prompt_ipc.h"
#include "chrome/chrome_cleaner/ipc/chrome_prompt_ipc.h"
#include "components/chrome_cleaner/public/interfaces/chrome_prompt.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace chrome_cleaner {
class MockChromePromptIPC : public MojoChromePromptIPC {
class MockChromePromptIPC : public ChromePromptIPC {
public:
MockChromePromptIPC();
~MockChromePromptIPC() override;
......@@ -25,19 +25,26 @@ class MockChromePromptIPC : public MojoChromePromptIPC {
void(base::OnceClosure delete_allowed_callback,
base::OnceClosure delete_not_allowed_callback));
// Workaround for GMock's limitation, in which MOCK_METHOD* doesn't accept
// base::OnceCallback parameters. Will forward any calls to
// MockPostPromptUserTask() and pass along a raw pointer for |callback|.
// Workaround for GMock's limitation, in which MOCK_METHOD* doesn't
// accept base::OnceCallback parameters. Will forward any calls to
// MockPost*() and pass along a raw pointer for |callback|.
void PostPromptUserTask(
const std::vector<base::FilePath>& files_to_delete,
const std::vector<base::string16>& registry_keys,
const std::vector<base::string16>& extension_ids,
mojom::ChromePrompt::PromptUserCallback callback) override;
void PostDisableExtensionsTask(
const std::vector<base::string16>& extension_ids,
mojom::ChromePrompt::DisableExtensionsCallback callback) override;
MOCK_METHOD4(MockPostPromptUserTask,
void(const std::vector<base::FilePath>& files_to_delete,
const std::vector<base::string16>& registry_keys,
const std::vector<base::string16>& extension_ids,
mojom::ChromePrompt::PromptUserCallback* callback));
MOCK_METHOD2(MockPostDisableExtensionsTask,
void(const std::vector<base::string16>& extension_ids,
mojom::ChromePrompt::DisableExtensionsCallback* callback));
};
} // namespace chrome_cleaner
......
// Copyright 2018 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.
#ifndef CHROME_CHROME_CLEANER_IPC_MOJO_CHROME_PROMPT_IPC_H_
#define CHROME_CHROME_CLEANER_IPC_MOJO_CHROME_PROMPT_IPC_H_
......@@ -87,41 +86,40 @@ class MojoChromePromptIPC : public ChromePromptIPC {
// Runs |chrome_prompt_service_->PromptUser()|. Must be called on the IPC
// thread.
void RunPromptUserTask(
const std::vector<base::FilePath>& files_to_delete,
void RunPromptUserTask(const std::vector<base::FilePath>& files_to_delete,
const std::vector<base::string16>& registry_keys,
const std::vector<base::string16>& extension_ids,
mojom::ChromePrompt::PromptUserCallback callback) override;
mojom::ChromePrompt::PromptUserCallback callback);
void RunDisableExtensionsTask(
const std::vector<base::string16>& extension_ids,
mojom::ChromePrompt::DisableExtensionsCallback callback) override;
mojom::ChromePrompt::DisableExtensionsCallback callback);
// Callback for ChromePrompt::PromptUser, internal state must be
// State::kWaitingForResponseFromChrome. Invokes callback(prompt_acceptance)
// and transitions to state State::kDoneInteraction.
void OnChromeResponseReceived(
mojom::ChromePrompt::PromptUserCallback callback,
mojom::PromptAcceptance prompt_acceptance) override;
mojom::PromptAcceptance prompt_acceptance);
// Callback for ChromePrompt::DisableExtensions, internal state must be
// State::kDoneInteraction. Invokes callback(extensions_deleted_callback).
void OnChromeResponseReceivedExtensions(
mojom::ChromePrompt::DisableExtensionsCallback callback,
bool extensions_deleted_callback) override;
bool extensions_deleted_callback);
// Connection error handler. Invokes either
// error_handler_->OnConnectionClosed() or
// error_handler_->OnConnectionClosedAfterDone(), depending on the internal
// state.
void OnConnectionError() override;
void OnConnectionError();
void PromptUserCheckVersion(
const std::vector<base::FilePath>& files_to_delete,
const std::vector<base::string16>& registry_keys,
const std::vector<base::string16>& extension_ids,
mojom::ChromePrompt::PromptUserCallback callback,
uint32_t version) override;
uint32_t version);
private:
scoped_refptr<MojoTaskRunner> task_runner_;
......
......@@ -87,7 +87,7 @@ struct TestConfig {
ParentDisconnected expected_parent_disconnected;
};
// Parent process.
// Class that lives in the parent process and handles that side of the IPC.
class MockChromePrompt : public mojom::ChromePrompt {
public:
MockChromePrompt(TestConfig test_config, mojom::ChromePromptRequest request)
......@@ -151,6 +151,7 @@ class ChromePromptIPCParentProcess : public ParentProcess {
}
if (test_config.with_registry_keys)
AppendSwitch(kIncludeRegistryKeysSwitch);
AppendSwitch(kExpectedPromptResultSwitch,
base::NumberToString(
static_cast<int>(test_config.expected_prompt_acceptance)));
......@@ -178,26 +179,7 @@ class ChromePromptIPCParentProcess : public ParentProcess {
std::unique_ptr<MockChromePrompt> mock_chrome_prompt_;
};
class ChromePromptIPCTestErrorHandler : public ChromePromptIPC::ErrorHandler {
public:
ChromePromptIPCTestErrorHandler(base::OnceClosure on_closed,
base::OnceClosure on_closed_after_done)
: on_closed_(std::move(on_closed)),
on_closed_after_done_(std::move(on_closed_after_done)) {}
~ChromePromptIPCTestErrorHandler() override = default;
void OnConnectionClosed() override { std::move(on_closed_).Run(); }
void OnConnectionClosedAfterDone() override {
std::move(on_closed_after_done_).Run();
}
base::OnceClosure on_closed_;
base::OnceClosure on_closed_after_done_;
};
// Child process.
// Class that lives in the child process and handles that side of the IPC.
class ChromePromptIPCChildProcess : public ChildProcess {
public:
explicit ChromePromptIPCChildProcess(
......@@ -297,6 +279,7 @@ MULTIPROCESS_TEST_MAIN(ChromePromptIPCClientMain) {
auto child_process =
base::MakeRefCounted<ChromePromptIPCChildProcess>(mojo_task_runner);
base::RunLoop on_done_run_loop;
// The parent process can disconnect while the pipe is required or after it's
// no longer needed. In the former case, the child process will immediately
// exit; in the latter, it will break |on_done_run_loop|, which will be
......
// Copyright 2019 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/chrome_cleaner/ipc/proto_chrome_prompt_ipc.h"
#include <windows.h>
#include "base/strings/utf_string_conversions.h"
#include "base/win/win_util.h"
#include "components/chrome_cleaner/public/proto/chrome_prompt.pb.h"
namespace {
// chrome_cleaner <-> chrome protocol version.
constexpr uint8_t kVersion = 1;
} // namespace
namespace chrome_cleaner {
ProtoChromePromptIPC::ProtoChromePromptIPC(
base::win::ScopedHandle response_read_handle,
base::win::ScopedHandle request_write_handle)
: response_read_handle_(std::move(response_read_handle)),
request_write_handle_(std::move(request_write_handle)) {
// All uses of this class, and more specifically its state member need to
// happen on the same sequence but one that is not the construction
// sequence.
DETACH_FROM_SEQUENCE(sequence_checker_);
}
ProtoChromePromptIPC::~ProtoChromePromptIPC() = default;
void ProtoChromePromptIPC::Initialize(ErrorHandler* error_handler) {
DCHECK(task_runner_);
error_handler_ = error_handler;
task_runner_->PostTask(FROM_HERE,
base::BindOnce(&ProtoChromePromptIPC::InitializeImpl,
base::Unretained(this)));
}
void ProtoChromePromptIPC::PostPromptUserTask(
const std::vector<base::FilePath>& files_to_delete,
const std::vector<base::string16>& registry_keys,
const std::vector<base::string16>& extension_ids,
PromptUserCallback callback) {
DCHECK(task_runner_);
task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&ProtoChromePromptIPC::RunPromptUserTask,
base::Unretained(this), files_to_delete, registry_keys,
extension_ids, std::move(callback)));
}
void ProtoChromePromptIPC::PostDisableExtensionsTask(
const std::vector<base::string16>& extension_ids,
DisableExtensionsCallback callback) {
NOTIMPLEMENTED();
OnConnectionError();
}
void ProtoChromePromptIPC::TryDeleteExtensions(
base::OnceClosure delete_allowed_callback,
base::OnceClosure delete_not_allowed_callback) {
NOTIMPLEMENTED();
OnConnectionError();
}
void ProtoChromePromptIPC::InitializeImpl() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(State::kUninitialized, state_);
state_ = State::kWaitingForScanResults;
// Initialize communication with chrome by sending the version.
WriteByValue(kVersion);
}
void ProtoChromePromptIPC::RunPromptUserTask(
const std::vector<base::FilePath>& files_to_delete,
const std::vector<base::string16>& registry_keys,
const std::vector<base::string16>& extension_ids,
PromptUserCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_NE(state_, State::kUninitialized);
DCHECK_NE(state_, State::kWaitingForResponseFromChrome);
// This can be true if any connection error occurred already in which case
// We don't not want to go forward with the prompting.
if (state_ == State::kDoneInteraction) {
return;
}
state_ = State::kWaitingForResponseFromChrome;
// If the contents of the message cannot be represented in a sane way avoid
// sending it on the wire. Returns a denied prompt since Chrome would run
// similar checks and deny it anyway.
// Build the prompt message.
chrome_cleaner::PromptUserRequest prompt_user_message;
for (const base::FilePath& file_to_delete : files_to_delete) {
std::string file_path_utf8;
if (!base::UTF16ToUTF8(file_to_delete.value().c_str(),
file_to_delete.value().size(), &file_path_utf8)) {
std::move(callback).Run(PromptAcceptance::DENIED);
return;
} else {
prompt_user_message.add_files_to_delete(file_path_utf8);
}
}
for (const base::string16& registry_key : registry_keys) {
std::string registry_key_utf8;
if (!base::UTF16ToUTF8(registry_key.c_str(), registry_key.size(),
&registry_key_utf8)) {
std::move(callback).Run(PromptAcceptance::DENIED);
return;
} else {
prompt_user_message.add_registry_keys(registry_key_utf8);
}
}
for (const base::string16& extension_id : extension_ids) {
std::string extension_id_utf8;
if (!base::UTF16ToUTF8(extension_id.c_str(), extension_id.size(),
&extension_id_utf8)) {
std::move(callback).Run(PromptAcceptance::DENIED);
return;
} else {
prompt_user_message.add_extension_ids(extension_id_utf8);
}
}
// This is the top-level message that Chrome is expecting.
ChromePromptRequest chrome_prompt_request;
*chrome_prompt_request.mutable_prompt_user() = prompt_user_message;
std::string request_content;
DCHECK(chrome_prompt_request.SerializeToString(&request_content));
SendBuffer(request_content);
// Sending the request can cause communication errors. If any happened don't
// bother waiting for a response.
if (state_ == State::kDoneInteraction) {
return;
}
// Receive the response from Chrome.
PromptAcceptance prompt_acceptance = WaitForPromptAcceptance();
if (state_ == State::kDoneInteraction) {
return;
}
// Send a message confirming to Chrome that the communication is over.
chrome_cleaner::CloseConnectionRequest close_connection_request;
chrome_prompt_request = ChromePromptRequest();
*chrome_prompt_request.mutable_close_connection() = close_connection_request;
std::string response_content;
DCHECK(chrome_prompt_request.SerializeToString(&response_content));
SendBuffer(response_content);
if (state_ == State::kDoneInteraction) {
return;
}
// Invoke callback with the result.
std::move(callback).Run(prompt_acceptance);
state_ = State::kDoneInteraction;
}
void ProtoChromePromptIPC::OnConnectionError() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_NE(State::kUninitialized, state_);
DCHECK_NE(State::kDoneInteraction, state_);
if (error_handler_) {
error_handler_->OnConnectionClosed();
}
state_ = State::kDoneInteraction;
}
void ProtoChromePromptIPC::SendBuffer(const std::string& request_content) {
DCHECK_NE(State::kDoneInteraction, state_);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Write the message size.
const uint32_t kMessageLength = request_content.size();
WriteByValue(kMessageLength);
// Writing the message length failed. Do not send body.
if (state_ == State::kDoneInteraction) {
return;
}
// Write the message content.
WriteByPointer(request_content.data(), kMessageLength);
}
ProtoChromePromptIPC::PromptAcceptance
ProtoChromePromptIPC::WaitForPromptAcceptance() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(State::kWaitingForResponseFromChrome, state_);
// On any error condition, invoke the error handler.
base::ScopedClosureRunner call_connection_closed(base::BindOnce(
&ProtoChromePromptIPC::OnConnectionError, base::Unretained(this)));
// Read the response length.
DWORD bytes_read = 0;
uint32_t response_length = 0;
if (!::ReadFile(response_read_handle_.Get(), &response_length,
sizeof(response_length), &bytes_read, nullptr)) {
PLOG(ERROR) << "Reading the prompt acceptance message length failed.";
return PromptAcceptance::DENIED;
}
if (bytes_read != sizeof(response_length)) {
PLOG(ERROR) << "Short read on the prompt acceptance message length.";
return PromptAcceptance::DENIED;
}
if (response_length == 0 || response_length > kMaxMessageLength) {
PLOG(ERROR) << "Invalid message length received: " << response_length;
return PromptAcceptance::DENIED;
}
// Read the response.
std::string response_content;
if (!::ReadFile(response_read_handle_.Get(),
base::WriteInto(&response_content, response_length + 1),
response_length, &bytes_read, nullptr)) {
PLOG(ERROR) << "Reading the prompt acceptance message failed";
return PromptAcceptance::DENIED;
}
if (bytes_read != response_length) {
PLOG(ERROR) << "Short read on the prompt acceptance message.";
return PromptAcceptance::DENIED;
}
chrome_cleaner::PromptUserResponse response;
if (!response.ParseFromString(response_content)) {
LOG(ERROR) << "Parsing of prompt acceptance failed.";
return PromptAcceptance::DENIED;
}
// Successful execution.
call_connection_closed.ReplaceClosure(base::DoNothing());
return static_cast<PromptAcceptance>(response.prompt_acceptance());
}
} // namespace chrome_cleaner
// Copyright 2019 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.
#ifndef CHROME_CHROME_CLEANER_IPC_PROTO_CHROME_PROMPT_IPC_H_
#define CHROME_CHROME_CLEANER_IPC_PROTO_CHROME_PROMPT_IPC_H_
#include <windows.h>
#include "base/sequenced_task_runner.h"
#include "base/task/post_task.h"
#include "base/win/scoped_handle.h"
#include "chrome/chrome_cleaner/ipc/chrome_prompt_ipc.h"
#include "components/chrome_cleaner/public/proto/chrome_prompt.pb.h"
namespace chrome_cleaner {
class ProtoChromePromptIPC : public ChromePromptIPC {
public:
static constexpr uint32_t kMaxMessageLength = 1 * 1024 * 1024; // 1M bytes
// Currently some mojom types are used to provide as drop-in replacement
// for the existing mojo based implementation. Since they are very simple
// they will stay essentially identical once the PromptAcceptance enum is
// replaced with a hand rolled one.
using PromptAcceptance = mojom::PromptAcceptance;
using PromptUserCallback = base::OnceCallback<void(PromptAcceptance)>;
using DisableExtensionsCallback = base::OnceCallback<void(bool)>;
ProtoChromePromptIPC(base::win::ScopedHandle response_read_handle,
base::win::ScopedHandle request_write_handle);
~ProtoChromePromptIPC() override;
void Initialize(ErrorHandler* error_handler) override;
void PostPromptUserTask(const std::vector<base::FilePath>& files_to_delete,
const std::vector<base::string16>& registry_keys,
const std::vector<base::string16>& extension_ids,
PromptUserCallback callback) override;
void PostDisableExtensionsTask(
const std::vector<base::string16>& extension_ids,
DisableExtensionsCallback callback) override;
void TryDeleteExtensions(
base::OnceClosure delete_allowed_callback,
base::OnceClosure delete_not_allowed_callback) override;
private:
// Implements the initialization that needs to happen on the task_runner
// sequence.
void InitializeImpl();
void RunPromptUserTask(const std::vector<base::FilePath>& files_to_delete,
const std::vector<base::string16>& registry_keys,
const std::vector<base::string16>& extension_ids,
PromptUserCallback callback);
// Invokes error_handler_->OnConnectionClosed() and updates state_. This
// should not be called more than once.
void OnConnectionError();
void SendBuffer(const std::string& request_content);
PromptAcceptance WaitForPromptAcceptance();
template <typename T>
void WriteByValue(T value) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DWORD bytes_written = 0;
if (!::WriteFile(request_write_handle_.Get(), &value, sizeof(value),
&bytes_written, nullptr)) {
PLOG(ERROR) << "Writing a message to the pipe failed.";
OnConnectionError();
return;
}
if (bytes_written != sizeof(value)) {
LOG(ERROR) << "Incorrect number of bytes written to the pipe. Should be: "
<< sizeof(value) << " but is :" << bytes_written;
OnConnectionError();
}
}
template <typename T>
void WriteByPointer(const T* ptr, uint32_t size) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DWORD bytes_written = 0;
if (!::WriteFile(request_write_handle_.Get(), ptr, size, &bytes_written,
nullptr)) {
PLOG(ERROR) << "Writing a message to the pipe failed.";
OnConnectionError();
return;
}
if (bytes_written != size) {
LOG(ERROR) << "Incorrect number of bytes written to the pipe. Should be: "
<< size << " but is :" << bytes_written;
OnConnectionError();
}
}
base::win::ScopedHandle response_read_handle_;
base::win::ScopedHandle request_write_handle_;
scoped_refptr<base::SequencedTaskRunner> task_runner_ =
base::CreateSequencedTaskRunner({base::MayBlock()});
};
} // namespace chrome_cleaner
#endif // CHROME_CHROME_CLEANER_IPC_PROTO_CHROME_PROMPT_IPC_H_
// Copyright 2019 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 <windows.h>
#include "base/command_line.h"
#include "base/process/process.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/multiprocess_test.h"
#include "base/test/scoped_task_environment.h"
#include "base/test/test_timeouts.h"
#include "base/unguessable_token.h"
#include "base/win/scoped_handle.h"
#include "base/win/win_util.h"
#include "chrome/chrome_cleaner/ipc/ipc_test_util.h"
#include "chrome/chrome_cleaner/ipc/proto_chrome_prompt_ipc.h"
#include "chrome/chrome_cleaner/logging/scoped_logging.h"
#include "components/chrome_cleaner/public/constants/constants.h"
#include "components/chrome_cleaner/public/proto/chrome_prompt.pb.h"
#include "components/chrome_cleaner/public/proto/chrome_prompt_for_tests.pb.h"
#include "components/chrome_cleaner/test/test_name_helper.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
namespace chrome_cleaner {
namespace {
using base::win::ScopedHandle;
using testing::Bool;
using testing::Values;
using PromptAcceptance = ProtoChromePromptIPC::PromptAcceptance;
constexpr char kIncludeUwSSwitch[] = "include-uws";
constexpr char kIncludeRegistryKeysSwitch[] = "include-registry-keys";
constexpr char kExpectedPromptResultSwitch[] = "expected-prompt-result";
constexpr char kExpectedChromeDisconnectPointSwitch[] =
"expected-parent-disconnected";
const base::char16 kInvalidUTF16String[] = {0xDC00, 0xD800, 0xD800, 0xDFFF,
0xDFFF, 0xDBFF, 0};
const base::FilePath kInvalidFilePath(kInvalidUTF16String);
const base::FilePath kNonASCIIFilePath(L"ééààçç");
const base::string16 kInvalidRegistryKey(kInvalidUTF16String);
const base::string16 kInvalidExtensionID(kInvalidUTF16String);
const base::FilePath kBadFilePath(L"/path/to/bad.dll");
const base::string16 kBadRegistryKey(L"HKCU:32\\Software\\ugly-uws\\nasty");
constexpr int kEarlyDisconnectionExitCode = 100;
constexpr int kSuccessExitCode = 0;
constexpr int kFailureExitCode = -1;
// Possible moments when the parent process can disconnect from the IPC to
// check connection error handling in the child process.
enum class ChromeDisconnectPoint {
// Invalid value to initialize to before reading from the command-line.
kUnspecified = 0,
// The parent process will not try to disconnect while the child process
// is running.
kNone,
// The parent process will disconnect before the child process sends
// the version number.
kOnStartup,
// The parent process will disconnect after reading the version.
kAfterVersion,
// The parent process will disconnect after receiving the length of the first
// request.
kAfterRequestLength,
// The parent process will disconnect after receiving the length of the close
// connection message.
kAfterCloseMessageLength,
// The parent process will disconnect after receiving a message from the
// child process and before sending out a response.
kWhileProcessingChildRequest,
};
std::ostream& operator<<(std::ostream& stream,
ChromeDisconnectPoint parent_disconnected) {
switch (parent_disconnected) {
case ChromeDisconnectPoint::kUnspecified:
stream << "Unspecified";
break;
case ChromeDisconnectPoint::kNone:
stream << "NotDisconnected";
break;
case ChromeDisconnectPoint::kOnStartup:
stream << "DisconnectedOnStartup";
break;
case ChromeDisconnectPoint::kAfterVersion:
stream << "DisconnectedAfterVersion";
break;
case ChromeDisconnectPoint::kAfterRequestLength:
stream << "DisconnectedAfterRequestLength";
break;
case ChromeDisconnectPoint::kWhileProcessingChildRequest:
stream << "DisconnectedWhileProcessingChildRequest";
break;
case ChromeDisconnectPoint::kAfterCloseMessageLength:
stream << "DisconnectedAfterCloseMessageLength";
break;
}
return stream;
}
struct TestConfig {
void EnhanceCommandLine(base::CommandLine* command_line) {
if (uws_expected) {
command_line->AppendSwitch(kIncludeUwSSwitch);
}
if (with_registry_keys) {
command_line->AppendSwitch(kIncludeRegistryKeysSwitch);
}
command_line->AppendSwitchASCII(
kExpectedPromptResultSwitch,
base::NumberToString(static_cast<int>(expected_prompt_acceptance)));
command_line->AppendSwitchASCII(
kExpectedChromeDisconnectPointSwitch,
base::NumberToString(static_cast<int>(expected_disconnection_point)));
}
bool uws_expected = false;
bool with_registry_keys = false;
PromptAcceptance expected_prompt_acceptance = PromptAcceptance::DENIED;
ChromeDisconnectPoint expected_disconnection_point =
ChromeDisconnectPoint::kNone;
};
enum class ServerPipeDirection {
kInbound,
kOutbound,
};
// This function is a taken from
// https://cs.chromium.org/chromium/src/chrome/browser/safe_browsing/chrome_cleaner/chrome_prompt_channel_win.cc
// to get the same behavior.
std::pair<ScopedHandle, ScopedHandle> CreateMessagePipe(
ServerPipeDirection server_direction) {
SECURITY_ATTRIBUTES security_attributes = {};
security_attributes.nLength = sizeof(SECURITY_ATTRIBUTES);
// Use this process's default access token.
security_attributes.lpSecurityDescriptor = nullptr;
// Handles to inherit will be added to the LaunchOptions explicitly.
security_attributes.bInheritHandle = false;
base::string16 pipe_name = base::UTF8ToUTF16(
base::StrCat({"\\\\.\\pipe\\chrome-cleaner-",
base::UnguessableToken::Create().ToString()}));
// Create the server end of the pipe.
DWORD direction_flag = server_direction == ServerPipeDirection::kInbound
? PIPE_ACCESS_INBOUND
: PIPE_ACCESS_OUTBOUND;
ScopedHandle server_handle(::CreateNamedPipe(
pipe_name.c_str(), direction_flag | FILE_FLAG_FIRST_PIPE_INSTANCE,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT |
PIPE_REJECT_REMOTE_CLIENTS,
/*nMaxInstances=*/1, /*nOutBufferSize=*/0, /*nInBufferSize=*/0,
/*nDefaultTimeOut=*/0, &security_attributes));
if (!server_handle.IsValid()) {
PLOG(ERROR) << "Error creating server pipe";
return std::make_pair(ScopedHandle(), ScopedHandle());
}
// The client pipe's read/write permissions are the opposite of the server's.
DWORD client_mode = server_direction == ServerPipeDirection::kInbound
? GENERIC_WRITE
: GENERIC_READ;
// Create the client end of the pipe.
ScopedHandle client_handle(::CreateFile(
pipe_name.c_str(), client_mode, /*dwShareMode=*/0,
/*lpSecurityAttributes=*/nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | SECURITY_SQOS_PRESENT | SECURITY_ANONYMOUS,
/*hTemplateFile=*/nullptr));
if (!client_handle.IsValid()) {
PLOG(ERROR) << "Error creating client pipe";
return std::make_pair(ScopedHandle(), ScopedHandle());
}
// Wait for the client end to connect (this should return
// ERROR_PIPE_CONNECTED immediately since it's already connected).
if (::ConnectNamedPipe(server_handle.Get(), /*lpOverlapped=*/nullptr)) {
LOG(ERROR) << "ConnectNamedPipe got an unexpected connection";
return std::make_pair(ScopedHandle(), ScopedHandle());
}
const auto error = ::GetLastError();
if (error != ERROR_PIPE_CONNECTED) {
LOG(ERROR) << "ConnectNamedPipe returned unexpected error: "
<< logging::SystemErrorCodeToString(error);
return std::make_pair(ScopedHandle(), ScopedHandle());
}
return std::make_pair(std::move(server_handle), std::move(client_handle));
}
void AppendHandleToCommandLine(base::CommandLine* command_line,
const std::string& switch_string,
HANDLE handle) {
ASSERT_NE(command_line, nullptr);
command_line->AppendSwitchASCII(
switch_string, base::NumberToString(base::win::HandleToUint32(handle)));
}
// In this test the class representing the cleaner is contained in the parent
// process so it needs the server ends of the pipes. The class that represents
// chrome is in the child process and gets the client end of the pipes.
bool PrepareForCleaner(base::CommandLine* command_line,
base::HandlesToInheritVector* handles_to_inherit,
ScopedHandle* request_read_handle_,
ScopedHandle* request_write_handle_,
ScopedHandle* response_read_handle_,
ScopedHandle* response_write_handle_) {
// Requests flow from the cleaner to Chrome.
std::tie(*request_write_handle_, *request_read_handle_) =
CreateMessagePipe(ServerPipeDirection::kOutbound);
// Responses flow from Chrome to the cleaner.
std::tie(*response_read_handle_, *response_write_handle_) =
CreateMessagePipe(ServerPipeDirection::kInbound);
if (!request_read_handle_->IsValid() || !request_write_handle_->IsValid() ||
!response_read_handle_->IsValid() || !response_write_handle_->IsValid()) {
return false;
}
DCHECK(command_line);
DCHECK(handles_to_inherit);
AppendHandleToCommandLine(command_line,
chrome_cleaner::kChromeWriteHandleSwitch,
response_write_handle_->Get());
handles_to_inherit->push_back(response_write_handle_->Get());
AppendHandleToCommandLine(command_line,
chrome_cleaner::kChromeReadHandleSwitch,
request_read_handle_->Get());
handles_to_inherit->push_back(request_read_handle_->Get());
return true;
}
// Provides the same kind of inputs and outputs on the pipes that Chrome would
// during the prompt process.
class MockChrome {
public:
MockChrome(base::win::ScopedHandle request_read_handle,
base::win::ScopedHandle response_write_handle)
: request_read_handle_(std::move(request_read_handle)),
response_write_handle_(std::move(response_write_handle)) {}
// This is needed for single process tests to avoid blocking on incomplete
// operations.
void CancelAllOperations() {
::CancelIoEx(request_read_handle_.Get(), nullptr);
::CancelIoEx(response_write_handle_.Get(), nullptr);
}
// Read and validate the version sent by the cleaner in a blocking way.
template <typename ValueType>
void ReadValue(ValueType* value) {
uint32_t read_size = sizeof(*value);
DWORD bytes_read = 0;
bool success = ::ReadFile(request_read_handle_.Get(), value, read_size,
&bytes_read, nullptr);
if (!success) {
PLOG(ERROR) << "Could not read value.";
FAIL();
}
if (bytes_read != read_size) {
LOG(ERROR) << "Read the wrong number of bytes: " << bytes_read
<< ". Should have been: " << read_size;
FAIL();
}
}
void ReadRequest(uint32_t request_length,
chrome_cleaner::ChromePromptRequest* request) {
DCHECK(request_read_handle_.IsValid());
DWORD bytes_read = 0;
std::string request_content;
// Read the request.
bool success =
::ReadFile(request_read_handle_.Get(),
base::WriteInto(&request_content, request_length + 1),
request_length, &bytes_read, nullptr);
if (!success) {
PLOG(ERROR) << "Could not read request.";
FAIL();
}
if (bytes_read != request_length) {
LOG(ERROR) << "Read the wrong number of bytes: " << bytes_read
<< ". Should have been: " << request_length;
FAIL();
}
if (!request->ParseFromString(request_content)) {
LOG(ERROR) << "Could not parse request.";
FAIL();
}
}
// Blocking write of anything by value on the pipe.
template <typename T>
void WriteByValue(T value) {
DWORD bytes_written = 0;
bool success = ::WriteFile(response_write_handle_.Get(), &value,
sizeof(value), &bytes_written, nullptr);
if (!success) {
PLOG(ERROR) << "Could not write to pipe.";
FAIL();
}
if (bytes_written != sizeof(value)) {
LOG(ERROR) << "Wrote the wrong number of bytes";
FAIL();
}
}
// Blocking write of anything by pointer on the pipe.
// Does not own the memory.
// Set |should_succeed|=false when testing a failed operation.
template <typename T>
void WriteByPointer(const T* ptr, uint32_t size, bool should_succeed = true) {
DWORD bytes_written = 0;
bool success = ::WriteFile(response_write_handle_.Get(), ptr, size,
&bytes_written, nullptr);
if (should_succeed && !success) {
PLOG(ERROR) << "Could not write to pipe.";
FAIL();
}
// We should not validate |bytes_written| if we know the call will fail.
if (should_succeed) {
if (bytes_written != size) {
LOG(ERROR) << "Wrote the wrong number of bytes";
FAIL();
}
}
}
void SendMessage(google::protobuf::MessageLite& message) {
std::string message_content;
if (!message.SerializeToString(&message_content)) {
LOG(ERROR) << "Could not serialize message for sending";
FAIL();
}
uint32_t message_size = message_content.size();
WriteByValue(message_size);
WriteByPointer(message_content.data(), message_content.size());
}
// Send a response to the cleaner with the expected values.
void SendResponse(PromptAcceptance prompt_acceptance) {
DCHECK(response_write_handle_.IsValid());
chrome_cleaner::PromptUserResponse response;
switch (prompt_acceptance) {
case PromptAcceptance::DENIED:
response.set_prompt_acceptance(
chrome_cleaner::PromptUserResponse::DENIED);
break;
case PromptAcceptance::ACCEPTED_WITH_LOGS:
response.set_prompt_acceptance(
chrome_cleaner::PromptUserResponse::ACCEPTED_WITH_LOGS);
break;
case PromptAcceptance::ACCEPTED_WITHOUT_LOGS:
response.set_prompt_acceptance(
chrome_cleaner::PromptUserResponse::ACCEPTED_WITHOUT_LOGS);
break;
case PromptAcceptance::UNSPECIFIED:
default:
response.set_prompt_acceptance(
chrome_cleaner::PromptUserResponse::UNSPECIFIED);
break;
}
SendMessage(response);
}
private:
base::win::ScopedHandle request_read_handle_;
base::win::ScopedHandle response_write_handle_;
};
// This class mocks the Chrome side of the IPC.
// A note on logging: We want errors to be logged using LOG(*) calls so they are
// captured by the ScopedLogging member. This in turn ensures that all messages
// are visible on buildbot outputs. This is why LOG(*) statements are used even
// though it would have been more readable to use the facilities provided by
// Gtest to log.
class ChildProcess {
public:
ChildProcess()
: scopped_logging_(
std::make_unique<ScopedLogging>(internal::GetLogPathSuffix())) {
mock_chrome_ = std::make_unique<MockChrome>(
ExtractHandleFromCommandLine(chrome_cleaner::kChromeReadHandleSwitch),
ExtractHandleFromCommandLine(chrome_cleaner::kChromeWriteHandleSwitch));
expected_disconnect_point_ = GetEnumFromCommandLine<ChromeDisconnectPoint>(
kExpectedChromeDisconnectPointSwitch);
expected_prompt_acceptance_ =
GetEnumFromCommandLine<PromptAcceptance>(kExpectedPromptResultSwitch);
}
base::win::ScopedHandle ExtractHandleFromCommandLine(
const std::string& handle_switch) {
uint32_t handle_value = 0;
if (!base::StringToUint(command_line_->GetSwitchValueNative(handle_switch),
&handle_value)) {
LOG(ERROR) << handle_switch << " not found on commandline";
return base::win::ScopedHandle();
}
HANDLE handle = base::win::Uint32ToHandle(handle_value);
return base::win::ScopedHandle(handle);
}
template <typename Enum>
Enum GetEnumFromCommandLine(const std::string& flag) {
int val = 0;
if (!base::StringToInt(command_line_->GetSwitchValueASCII(flag), &val)) {
LOG(ERROR) << "Could not get flag:" << flag << " from the command line";
}
return static_cast<Enum>(val);
}
// This simulates the pipes getting cut for any reason.
void CloseConnectionIfDisconectionPointReached(
ChromeDisconnectPoint disconnect_point) {
if (expected_disconnect_point_ == disconnect_point) {
// Immediately exit the child process. Destructors do not get called so we
// do not properly clean up. This mimics a real spurious disconnect.
exit(kEarlyDisconnectionExitCode);
}
}
// Execute all steps of the prompt according to passed in test config.
void Run() {
DCHECK_NE(expected_disconnect_point_, ChromeDisconnectPoint::kUnspecified);
DCHECK_NE(expected_prompt_acceptance_, PromptAcceptance::UNSPECIFIED);
CloseConnectionIfDisconectionPointReached(
ChromeDisconnectPoint::kOnStartup);
// Read the incoming version number, this is NOT echoed back.
constexpr uint8_t kExpectedVersion = 1;
uint8_t version = 0;
mock_chrome_->ReadValue(&version);
if (version != kExpectedVersion) {
LOG(ERROR) << "Wrong version received: " << version;
FAIL();
}
CloseConnectionIfDisconectionPointReached(
ChromeDisconnectPoint::kAfterVersion);
uint32_t request_length = 0;
mock_chrome_->ReadValue(&request_length);
CloseConnectionIfDisconectionPointReached(
ChromeDisconnectPoint::kAfterRequestLength);
chrome_cleaner::ChromePromptRequest request;
mock_chrome_->ReadRequest(request_length, &request);
CloseConnectionIfDisconectionPointReached(
ChromeDisconnectPoint::kWhileProcessingChildRequest);
int32_t expected_files_to_delete_size =
command_line_->HasSwitch(kIncludeUwSSwitch) ? 1 : 0;
if (request.prompt_user().files_to_delete_size() !=
expected_files_to_delete_size) {
LOG(ERROR) << "Wrong number of files to delete received.";
FAIL();
}
if (expected_files_to_delete_size == 1) {
std::string file_path_utf8;
base::UTF16ToUTF8(kBadFilePath.value().c_str(),
kBadFilePath.value().size(), &file_path_utf8);
if (request.prompt_user().files_to_delete(0) != file_path_utf8) {
LOG(ERROR) << "Wrong value for file to delete";
FAIL();
}
}
int32_t expecteed_registry_keys_size =
command_line_->HasSwitch(kIncludeRegistryKeysSwitch) ? 1 : 0;
if (request.prompt_user().registry_keys_size() !=
expecteed_registry_keys_size) {
LOG(ERROR) << "Wrong number of registry keys to delete";
FAIL();
}
if (expecteed_registry_keys_size == 1) {
if (request.prompt_user().registry_keys(0) !=
base::UTF16ToUTF8(kBadRegistryKey)) {
LOG(ERROR) << "Wrong value for registry key";
FAIL();
}
}
if (request.prompt_user().extension_ids_size() != 0) {
LOG(ERROR) << "Cleaning of UwsE not supported. None should be present in "
"message";
FAIL();
}
// Send back a success message.
mock_chrome_->SendResponse(expected_prompt_acceptance_);
// Receive the close connection message.
uint32_t close_message_length = 0;
mock_chrome_->ReadValue(&close_message_length);
CloseConnectionIfDisconectionPointReached(
ChromeDisconnectPoint::kAfterCloseMessageLength);
chrome_cleaner::ChromePromptRequest close_message;
mock_chrome_->ReadRequest(close_message_length, &close_message);
if (!close_message.has_close_connection()) {
LOG(ERROR) << "Wrong close connection message type";
FAIL();
}
}
private:
std::unique_ptr<MockChrome> mock_chrome_;
std::unique_ptr<ScopedLogging> scopped_logging_;
ChromeDisconnectPoint expected_disconnect_point_ =
ChromeDisconnectPoint::kUnspecified;
PromptAcceptance expected_prompt_acceptance_ = PromptAcceptance::UNSPECIFIED;
const base::CommandLine* command_line_ =
base::CommandLine::ForCurrentProcess();
};
// This mimics the Chrome side of the IPC.
MULTIPROCESS_TEST_MAIN(ProtoChromePromptIPCClientMain) {
base::test::ScopedTaskEnvironment scoped_task_environment;
ChildProcess child_process;
child_process.Run();
return ::testing::Test::HasFailure() ? kFailureExitCode : kSuccessExitCode;
}
class ProtoChromePromptIPCTest
: public ::testing::TestWithParam<
std::tuple<bool, bool, PromptAcceptance, ChromeDisconnectPoint>> {
private:
base::test::ScopedTaskEnvironment scoped_task_environment;
};
class ParentProcess {
public:
bool Initialize() {
// Inject the flags related to the the config in the command line.
test_config_.EnhanceCommandLine(&command_line_);
return PrepareForCleaner(&command_line_,
&launch_options_.handles_to_inherit,
&request_read_handle_, &request_write_handle_,
&response_read_handle_, &response_write_handle_);
}
void ValidateAcceptance(PromptAcceptance prompt_acceptance) {
EXPECT_EQ(prompt_acceptance, test_config_.expected_prompt_acceptance);
main_runloop_.Quit();
}
// A call to this function indicates that the connection was closed
// prematurely which signifies an error.
void ConnectionWasClosed() {
error_occurred_ = true;
main_runloop_.Quit();
}
void ConnectionWasClosedAfterDone() {
main_runloop_.Quit();
FAIL() << "ConnectionWasClosedAfterDone should only be called in the Mojo "
"IPC implementation";
}
void Run() {
ASSERT_TRUE(internal::DeleteChildProcessLogs());
// Pass the command to the child process and launch the child process.
base::Process child_process = base::SpawnMultiProcessTestChild(
"ProtoChromePromptIPCClientMain", command_line_, launch_options_);
ASSERT_TRUE(child_process.IsRunning());
// Close our references to the handles as they are now handled by the child
// process.
request_read_handle_.Close();
response_write_handle_.Close();
ProtoChromePromptIPC chrome_prompt_ipc(std::move(response_read_handle_),
std::move(request_write_handle_));
// Send the protocol version, blocking the child process until it is read.
auto error_handler = std::make_unique<ChromePromptIPCTestErrorHandler>(
base::BindOnce(&ParentProcess::ConnectionWasClosed,
base::Unretained(this)),
base::BindOnce(&ParentProcess::ConnectionWasClosedAfterDone,
base::Unretained(this)));
chrome_prompt_ipc.Initialize(error_handler.get());
std::vector<base::FilePath> files_to_delete;
if (test_config_.uws_expected) {
files_to_delete.push_back(kBadFilePath);
}
std::vector<base::string16> registry_keys;
if (test_config_.with_registry_keys) {
registry_keys.push_back(kBadRegistryKey);
}
// Send the user prompt, blocking the child process until it is read.
// The test thread will block too until a response is received.
chrome_prompt_ipc.PostPromptUserTask(
files_to_delete, registry_keys, {},
base::BindOnce(&ParentProcess::ValidateAcceptance,
base::Unretained(this)));
main_runloop_.Run();
// During a normal execution where the full prompt exchange takes place
// there should be no errors.
bool should_have_errors = test_config_.expected_disconnection_point !=
ChromeDisconnectPoint::kNone;
EXPECT_EQ(error_occurred_, should_have_errors);
WaitForChildProcess(child_process);
}
void WaitForChildProcess(const base::Process& child_process) {
// Expect the return code that can indicate one of three outcomes
// 1: Success
// 2: Early disconnection which is expected if we simulate Chrome crashing.
// 3: Failure which will be triggered if any EXPECT or ASSERT call fails in
// the child process.
int rv = -1;
bool success = base::WaitForMultiprocessTestChildExit(
child_process, TestTimeouts::action_timeout(), &rv);
ASSERT_TRUE(success);
int expected_exit_code = kSuccessExitCode;
if (test_config_.expected_disconnection_point !=
ChromeDisconnectPoint::kNone) {
expected_exit_code = kEarlyDisconnectionExitCode;
}
EXPECT_EQ(expected_exit_code, rv);
if (!success || rv != 0) {
internal::PrintChildProcessLogs();
}
}
TestConfig& GetTestConfig() { return test_config_; }
private:
// Handles for Chrome.
ScopedHandle request_read_handle_;
ScopedHandle response_write_handle_;
// Handles for the cleaner.
ScopedHandle request_write_handle_;
ScopedHandle response_read_handle_;
TestConfig test_config_;
base::CommandLine command_line_ =
base::GetMultiProcessTestChildBaseCommandLine();
base::LaunchOptions launch_options_;
// Blocks until we receive the response from Chrome or an error occurs.
base::RunLoop main_runloop_;
bool error_occurred_ = false;
};
// This contains calls to the chrome_cleaner_ipc implementation.
TEST_P(ProtoChromePromptIPCTest, Communication) {
ParentProcess parent_process;
TestConfig& test_config = parent_process.GetTestConfig();
std::tie(test_config.uws_expected, test_config.with_registry_keys,
test_config.expected_prompt_acceptance,
test_config.expected_disconnection_point) = GetParam();
ASSERT_TRUE(parent_process.Initialize());
parent_process.Run();
}
INSTANTIATE_TEST_SUITE_P(NoUwSPresent,
ProtoChromePromptIPCTest,
testing::Combine(
/*[>uws_expected=<]*/ Values(false),
/*[>with_registry_keys=<]*/ Values(false),
Values(PromptAcceptance::DENIED),
Values(ChromeDisconnectPoint::kNone,
ChromeDisconnectPoint::kOnStartup)),
GetParamNameForTest());
INSTANTIATE_TEST_SUITE_P(
UwSPresent,
ProtoChromePromptIPCTest,
testing::Combine(
/*uws_expected=*/Values(true),
/*with_registry_keys=*/Bool(),
Values(PromptAcceptance::ACCEPTED_WITH_LOGS,
PromptAcceptance::ACCEPTED_WITHOUT_LOGS,
PromptAcceptance::DENIED),
Values(ChromeDisconnectPoint::kNone,
ChromeDisconnectPoint::kOnStartup,
ChromeDisconnectPoint::kAfterVersion,
ChromeDisconnectPoint::kAfterRequestLength,
ChromeDisconnectPoint::kAfterCloseMessageLength,
ChromeDisconnectPoint::kWhileProcessingChildRequest)),
GetParamNameForTest());
class ProtoChromePromptSameProcessTest : public ::testing::Test {
public:
void SetUp() override {
// Requests flow from the cleaner to Chrome.
base::win::ScopedHandle request_read_handle;
base::win::ScopedHandle request_write_handle;
std::tie(request_write_handle, request_read_handle) =
CreateMessagePipe(ServerPipeDirection::kOutbound);
// Responses flow from Chrome to the cleaner.
base::win::ScopedHandle response_read_handle;
base::win::ScopedHandle response_write_handle;
std::tie(response_read_handle, response_write_handle) =
CreateMessagePipe(ServerPipeDirection::kInbound);
EXPECT_TRUE(request_read_handle.IsValid());
EXPECT_TRUE(request_write_handle.IsValid());
EXPECT_TRUE(response_read_handle.IsValid());
EXPECT_TRUE(response_write_handle.IsValid());
mock_chrome_ = std::make_unique<MockChrome>(
std::move(request_read_handle), std::move(response_write_handle));
chrome_prompt_ipc_ = std::make_unique<ProtoChromePromptIPC>(
std::move(response_read_handle), std::move(request_write_handle));
error_handler_ = std::make_unique<ChromePromptIPCTestErrorHandler>(
base::BindOnce(&ProtoChromePromptSameProcessTest ::ConnectionWasClosed,
base::Unretained(this)),
base::BindOnce(
&ProtoChromePromptSameProcessTest::ConnectionWasClosedAfterDone,
base::Unretained(this)));
}
void InitCommunication() {
chrome_prompt_ipc_->Initialize(error_handler_.get());
uint8_t version = 0;
mock_chrome_->ReadValue(&version);
}
// A call to this function indicates that the connection was closed
// prematurely which signifies an error.
void ConnectionWasClosed() {
// Unblock the main test thread is there are pending operations.
mock_chrome_->CancelAllOperations();
error_occurred_ = true;
main_runloop_.Quit();
}
void ConnectionWasClosedAfterDone() {
main_runloop_.Quit();
FAIL() << "ConnectionWasClosedAfterDone should only be called in the Mojo "
"IPC implementation";
}
void ExpectMessage() {
uint32_t request_length = 0;
mock_chrome_->ReadValue(&request_length);
chrome_cleaner::ChromePromptRequest request;
mock_chrome_->ReadRequest(request_length, &request);
}
void ValidateAcceptance(PromptAcceptance expected_prompt_acceptance,
PromptAcceptance prompt_acceptance) {
EXPECT_EQ(prompt_acceptance, expected_prompt_acceptance);
main_runloop_.Quit();
}
protected:
base::test::ScopedTaskEnvironment scoped_task_environment;
std::unique_ptr<MockChrome> mock_chrome_;
std::unique_ptr<ProtoChromePromptIPC> chrome_prompt_ipc_;
std::unique_ptr<ChromePromptIPCTestErrorHandler> error_handler_;
bool error_occurred_ = false;
// Blocks until we receive the response from Chrome.
base::RunLoop main_runloop_;
};
TEST_F(ProtoChromePromptSameProcessTest, InvalidUTF16Path) {
InitCommunication();
chrome_prompt_ipc_->PostPromptUserTask(
{kInvalidFilePath}, {}, {},
base::BindOnce(&ProtoChromePromptSameProcessTest::ValidateAcceptance,
base::Unretained(this),
chrome_cleaner::PromptAcceptance::DENIED));
// Providing an invalid file path will trigger an immediate denial from the
// cleaner side. No communication will happen with Chrome so we do not call
// ExpectMessage() as it would timeout waiting for the prompt message.
main_runloop_.Run();
// This is not considered an error but validation of user provided input.
ASSERT_FALSE(error_occurred_);
}
TEST_F(ProtoChromePromptSameProcessTest, InvalidUTF16RegistryKey) {
InitCommunication();
chrome_prompt_ipc_->PostPromptUserTask(
{}, {kInvalidRegistryKey}, {},
base::BindOnce(&ProtoChromePromptSameProcessTest::ValidateAcceptance,
base::Unretained(this),
chrome_cleaner::PromptAcceptance::DENIED));
// Providing an invalid registry key will trigger an immediate denial from the
// cleaner side. No communication will happen with Chrome so we do not call
// ExpectMessage() as it would timeout waiting for the prompt message.
main_runloop_.Run();
// This is not considered an error but validation of user provided input.
ASSERT_FALSE(error_occurred_);
}
TEST_F(ProtoChromePromptSameProcessTest, InvalidUTF16ExtensionID) {
InitCommunication();
chrome_prompt_ipc_->PostPromptUserTask(
{}, {}, {kInvalidExtensionID},
base::BindOnce(&ProtoChromePromptSameProcessTest::ValidateAcceptance,
base::Unretained(this),
chrome_cleaner::PromptAcceptance::DENIED));
// Providing an invalid extension id will trigger an immediate denial from the
// cleaner side. No communication will happen with Chrome so we do not call
// ExpectMessage() as it would timeout waiting for the prompt message.
main_runloop_.Run();
// This is not considered an error but validation of user provided input.
ASSERT_FALSE(error_occurred_);
}
TEST_F(ProtoChromePromptSameProcessTest, ValidNonASCIIPath) {
InitCommunication();
chrome_prompt_ipc_->PostPromptUserTask(
{kNonASCIIFilePath}, {}, {},
base::BindOnce(&ProtoChromePromptSameProcessTest::ValidateAcceptance,
base::Unretained(this),
chrome_cleaner::PromptAcceptance::ACCEPTED_WITH_LOGS));
// Expect the prompt message.
ExpectMessage();
// Send back the response.
mock_chrome_->SendResponse(PromptAcceptance::ACCEPTED_WITH_LOGS);
// Expect the close connection message.
ExpectMessage();
main_runloop_.Run();
// There are no errors here. Non-ASCII characters are supported.
ASSERT_FALSE(error_occurred_);
}
TEST_F(ProtoChromePromptSameProcessTest, ReponseSizeOverMax) {
InitCommunication();
chrome_prompt_ipc_->PostPromptUserTask(
{kNonASCIIFilePath}, {}, {},
base::BindOnce(&ProtoChromePromptSameProcessTest::ValidateAcceptance,
base::Unretained(this),
chrome_cleaner::PromptAcceptance::ACCEPTED_WITH_LOGS));
// Expect the prompt message.
ExpectMessage();
// Size over max.
uint32_t invalid_size = ProtoChromePromptIPC::kMaxMessageLength + 1;
mock_chrome_->WriteByValue(invalid_size);
// Notice the absence of ExpectMessage() here. The cleaner will never get to
// sending a close connection message.
main_runloop_.Run();
// This is an error scenario.
ASSERT_TRUE(error_occurred_);
}
TEST_F(ProtoChromePromptSameProcessTest, ReponseSizeZero) {
InitCommunication();
chrome_prompt_ipc_->PostPromptUserTask(
{kNonASCIIFilePath}, {}, {},
base::BindOnce(&ProtoChromePromptSameProcessTest::ValidateAcceptance,
base::Unretained(this),
chrome_cleaner::PromptAcceptance::ACCEPTED_WITH_LOGS));
// Expect the prompt message.
ExpectMessage();
// Size of zero.
uint32_t invalid_size = 0;
mock_chrome_->WriteByValue(invalid_size);
// Notice the absence of ExpectMessage() here. The cleaner will never get to
// sending a close connection message.
main_runloop_.Run();
// This is an error scenario.
ASSERT_TRUE(error_occurred_);
}
TEST_F(ProtoChromePromptSameProcessTest, ReponseSizeSentTooSmall) {
InitCommunication();
chrome_prompt_ipc_->PostPromptUserTask(
{kNonASCIIFilePath}, {}, {},
base::BindOnce(&ProtoChromePromptSameProcessTest::ValidateAcceptance,
base::Unretained(this),
chrome_cleaner::PromptAcceptance::ACCEPTED_WITH_LOGS));
// Expect the prompt message.
ExpectMessage();
chrome_cleaner::PromptUserResponse response;
response.set_prompt_acceptance(
chrome_cleaner::PromptUserResponse::ACCEPTED_WITH_LOGS);
std::string response_content;
response.SerializeToString(&response_content);
// Size too small.
uint32_t invalid_size = response_content.size() - 1;
mock_chrome_->WriteByValue(invalid_size);
// Send the correct data.
mock_chrome_->WriteByPointer(response_content.data(), response_content.size(),
/*should_succeed=*/false);
// Notice the absence of ExpectMessage() here. The cleaner will never get to
// sending a close connection message.
main_runloop_.Run();
// This is an error scenario.
ASSERT_TRUE(error_occurred_);
}
TEST_F(ProtoChromePromptSameProcessTest, ReponseSizeSentTooBig) {
InitCommunication();
chrome_prompt_ipc_->PostPromptUserTask(
{kNonASCIIFilePath}, {}, {},
base::BindOnce(&ProtoChromePromptSameProcessTest::ValidateAcceptance,
base::Unretained(this),
chrome_cleaner::PromptAcceptance::ACCEPTED_WITH_LOGS));
// Expect the prompt message.
ExpectMessage();
chrome_cleaner::PromptUserResponse response;
response.set_prompt_acceptance(
chrome_cleaner::PromptUserResponse::ACCEPTED_WITH_LOGS);
std::string response_content;
response.SerializeToString(&response_content);
// Size too big.
uint32_t invalid_size = response_content.size() + 1;
mock_chrome_->WriteByValue(invalid_size);
// Send the correct data.
mock_chrome_->WriteByPointer(response_content.data(),
response_content.size());
// Notice the absence of ExpectMessage() here. The cleaner will never get to
// sending a close connection message.
main_runloop_.Run();
// This is an error scenario.
ASSERT_TRUE(error_occurred_);
}
TEST_F(ProtoChromePromptSameProcessTest, OutOfRangeAcceptance) {
InitCommunication();
chrome_prompt_ipc_->PostPromptUserTask(
{kNonASCIIFilePath}, {}, {},
base::BindOnce(&ProtoChromePromptSameProcessTest::ValidateAcceptance,
base::Unretained(this),
chrome_cleaner::PromptAcceptance::UNSPECIFIED));
// Expect the prompt message.
ExpectMessage();
// Send back the response with an out of range acceptance.
chrome_cleaner_test_only::PromptUserResponse response;
response.set_prompt_acceptance(
chrome_cleaner_test_only::
PromptUserResponse_PromptAcceptance_FOR_TESTS_ONLY);
mock_chrome_->SendMessage(response);
// Expect the close connection message.
ExpectMessage();
main_runloop_.Run();
// There are no errors here. An out of range PromptAcceptance value means
// that the protocol evolved and parsing it as UNSPECIFIED will not trigger
// a cleaning that the user actually denied.
ASSERT_FALSE(error_occurred_);
}
} // namespace
} // namespace chrome_cleaner
......@@ -3859,8 +3859,7 @@ test("unit_tests") {
"//ui/native_theme:test_support",
]
if (is_win) {
deps +=
[ "//chrome/browser/safe_browsing/chrome_cleaner:test_only_proto" ]
deps += [ "//components/chrome_cleaner/public/proto:test_only_proto" ]
}
if (is_mac) {
deps += [ ":firefox_importer_interface" ]
......
......@@ -9,3 +9,11 @@ proto_library("proto") {
"chrome_prompt.proto",
]
}
proto_library("test_only_proto") {
testonly = true
generate_python = false
sources = [
"chrome_prompt_for_tests.proto",
]
}
......@@ -6,7 +6,7 @@
// contains added fields to validate that they do not break the parsing. This is
// to simulate an outdated Chrome handling messages from an updated cleaner.
// This file contains minimal comments for descriptions of non-test
// This file contains minimal comments. For descriptions of non-test
// fields/messages please refer to the original file.
syntax = "proto2";
......@@ -35,6 +35,7 @@ message PromptUserResponse {
ACCEPTED_WITH_LOGS = 1;
ACCEPTED_WITHOUT_LOGS = 2;
DENIED = 3;
FOR_TESTS_ONLY = 255;
};
optional PromptAcceptance prompt_acceptance = 1 [default = UNSPECIFIED];
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment