blob: cd91310851fa159ee02b04e03ab5082607c04dfe [file] [log] [blame]
// Copyright 2019 The Fuchsia 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 "src/developer/debug/debug_agent/debug_agent.h"
#include <lib/syslog/cpp/macros.h>
#include <zircon/status.h>
#include <gtest/gtest.h>
#include "src/developer/debug/debug_agent/arch.h"
#include "src/developer/debug/debug_agent/mock_debug_agent_harness.h"
#include "src/developer/debug/debug_agent/mock_exception_handle.h"
#include "src/developer/debug/debug_agent/mock_process.h"
#include "src/developer/debug/debug_agent/mock_process_handle.h"
#include "src/developer/debug/debug_agent/mock_thread_handle.h"
#include "src/developer/debug/debug_agent/test_utils.h"
#include "src/developer/debug/ipc/message_writer.h"
#include "src/developer/debug/ipc/protocol.h"
#include "src/developer/debug/shared/logging/debug.h"
#include "src/developer/debug/shared/message_loop.h"
#include "src/developer/debug/shared/test_with_loop.h"
#include "src/lib/fxl/strings/string_printf.h"
namespace debug_agent {
namespace {
bool HasAttachedProcessWithKoid(DebugAgent* debug_agent, zx_koid_t koid) {
DebuggedProcess* proc = debug_agent->GetDebuggedProcess(koid);
if (!proc)
return false;
// All of our process handles should be mock ones.
return static_cast<MockProcessHandle&>(proc->process_handle()).is_attached();
}
// Setup -------------------------------------------------------------------------------------------
class DebugAgentMockProcess : public MockProcess {
public:
DebugAgentMockProcess(DebugAgent* debug_agent, zx_koid_t koid, std::string name)
: MockProcess(debug_agent, koid, std::move(name)) {}
~DebugAgentMockProcess() = default;
void SuspendAndSendModules() override {
// Send the modules over to the ipc.
debug_agent()->SendNotification(modules_to_send_);
}
void set_modules_to_send(debug_ipc::NotifyModules m) { modules_to_send_ = std::move(m); }
private:
debug_ipc::NotifyModules modules_to_send_;
};
} // namespace
// Tests -------------------------------------------------------------------------------------------
class DebugAgentTests : public debug::TestWithLoop {};
TEST_F(DebugAgentTests, OnGlobalStatus) {
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
debug_ipc::StatusRequest request = {};
debug_ipc::StatusReply reply = {};
remote_api->OnStatus(request, &reply);
ASSERT_EQ(reply.processes.size(), 0u);
constexpr uint64_t kProcessKoid1 = 0x1234;
const std::string kProcessName1 = "process-1";
constexpr uint64_t kProcess1ThreadKoid1 = 0x1;
auto process1 = std::make_unique<MockProcess>(nullptr, kProcessKoid1, kProcessName1);
process1->AddThread(kProcess1ThreadKoid1);
harness.debug_agent()->InjectProcessForTest(std::move(process1));
reply = {};
remote_api->OnStatus(request, &reply);
ASSERT_EQ(reply.processes.size(), 1u);
EXPECT_EQ(reply.processes[0].process_koid, kProcessKoid1);
EXPECT_EQ(reply.processes[0].process_name, kProcessName1);
ASSERT_EQ(reply.processes[0].threads.size(), 1u);
EXPECT_EQ(reply.processes[0].threads[0].id.process, kProcessKoid1);
EXPECT_EQ(reply.processes[0].threads[0].id.thread, kProcess1ThreadKoid1);
constexpr uint64_t kProcessKoid2 = 0x5678;
const std::string kProcessName2 = "process-2";
constexpr uint64_t kProcess2ThreadKoid1 = 0x1;
constexpr uint64_t kProcess2ThreadKoid2 = 0x2;
auto process2 = std::make_unique<MockProcess>(nullptr, kProcessKoid2, kProcessName2);
process2->AddThread(kProcess2ThreadKoid1);
process2->AddThread(kProcess2ThreadKoid2);
harness.debug_agent()->InjectProcessForTest(std::move(process2));
reply = {};
remote_api->OnStatus(request, &reply);
ASSERT_EQ(reply.processes.size(), 2u);
EXPECT_EQ(reply.processes[0].process_koid, kProcessKoid1);
EXPECT_EQ(reply.processes[0].process_name, kProcessName1);
ASSERT_EQ(reply.processes[0].threads.size(), 1u);
EXPECT_EQ(reply.processes[0].threads[0].id.process, kProcessKoid1);
EXPECT_EQ(reply.processes[0].threads[0].id.thread, kProcess1ThreadKoid1);
EXPECT_EQ(reply.processes[1].process_koid, kProcessKoid2);
EXPECT_EQ(reply.processes[1].process_name, kProcessName2);
ASSERT_EQ(reply.processes[1].threads.size(), 2u);
EXPECT_EQ(reply.processes[1].threads[0].id.process, kProcessKoid2);
EXPECT_EQ(reply.processes[1].threads[0].id.thread, kProcess2ThreadKoid1);
EXPECT_EQ(reply.processes[1].threads[1].id.process, kProcessKoid2);
EXPECT_EQ(reply.processes[1].threads[1].id.thread, kProcess2ThreadKoid2);
// Set a limbo provider.
constexpr zx_koid_t kProcKoid1 = 100;
constexpr zx_koid_t kThreadKoid1 = 101;
harness.system_interface()->mock_limbo_provider().AppendException(
MockProcessHandle(kProcKoid1, "proc1"), MockThreadHandle(kThreadKoid1, "thread1"),
MockExceptionHandle(kThreadKoid1));
constexpr zx_koid_t kProcKoid2 = 102;
constexpr zx_koid_t kThreadKoid2 = 103;
harness.system_interface()->mock_limbo_provider().AppendException(
MockProcessHandle(kProcKoid2, "proc2"), MockThreadHandle(kThreadKoid1, "thread2"),
MockExceptionHandle(kThreadKoid2));
reply = {};
remote_api->OnStatus(request, &reply);
// The attached processes should still be there.
ASSERT_EQ(reply.processes.size(), 2u);
// The limbo processes should be there.
ASSERT_EQ(reply.limbo.size(), 2u);
EXPECT_EQ(reply.limbo[0].process_koid, kProcKoid1);
EXPECT_EQ(reply.limbo[0].process_name, "proc1");
ASSERT_EQ(reply.limbo[0].threads.size(), 1u);
// TODO(donosoc): Add exception type.
}
TEST_F(DebugAgentTests, OnAttachNotFound) {
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
debug_ipc::AttachRequest attach_request;
attach_request.koid = -1;
// Invalid koid should fail.
debug_ipc::AttachReply attach_reply;
remote_api->OnAttach(attach_request, &attach_reply);
EXPECT_TRUE(attach_reply.status.has_error());
constexpr zx_koid_t kProcKoid1 = 100;
constexpr zx_koid_t kThreadKoid1 = 101;
harness.system_interface()->mock_limbo_provider().AppendException(
MockProcessHandle(kProcKoid1, "proc1"), MockThreadHandle(kThreadKoid1, "thread1"),
MockExceptionHandle(kThreadKoid1));
// Even with limbo it should fail.
remote_api->OnAttach(attach_request, &attach_reply);
EXPECT_TRUE(attach_reply.status.has_error());
}
TEST_F(DebugAgentTests, OnAttach) {
constexpr zx_koid_t kProcess1Koid = 11u; // Koid for job1-p2 from the GetMockJobTree() hierarchy.
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
debug_ipc::AttachRequest attach_request;
attach_request.koid = kProcess1Koid;
debug_ipc::AttachReply attach_reply;
remote_api->OnAttach(attach_request, &attach_reply);
// We should've received a watch command (which does the low level exception watching).
EXPECT_TRUE(HasAttachedProcessWithKoid(harness.debug_agent(), kProcess1Koid));
// We should've gotten an attach reply.
EXPECT_TRUE(attach_reply.status.ok());
EXPECT_EQ(attach_reply.koid, kProcess1Koid);
EXPECT_EQ(attach_reply.name, "job1-p2");
// Asking for some invalid process should fail.
attach_request.koid = 0x231315; // Some invalid value.
remote_api->OnAttach(attach_request, &attach_reply);
// We should've gotten an error reply.
EXPECT_TRUE(attach_reply.status.has_error());
// Attaching to a third process should work.
attach_request.koid = 21u;
remote_api->OnAttach(attach_request, &attach_reply);
EXPECT_TRUE(attach_reply.status.ok());
EXPECT_EQ(attach_reply.koid, 21u);
EXPECT_EQ(attach_reply.name, "job121-p2");
// Attaching again to a process should fail.
remote_api->OnAttach(attach_request, &attach_reply);
EXPECT_TRUE(attach_reply.status.has_error());
}
TEST_F(DebugAgentTests, AttachToLimbo) {
// debug::SetDebugMode(true);
// debug::SetLogCategories({debug::LogCategory::kAll});
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
constexpr zx_koid_t kProcKoid = 100;
constexpr zx_koid_t kThreadKoid = 101;
MockProcessHandle mock_process(kProcKoid, "proc");
MockThreadHandle mock_thread(kThreadKoid, "thread");
mock_process.set_threads({mock_thread});
harness.system_interface()->mock_limbo_provider().AppendException(
mock_process, mock_thread, MockExceptionHandle(kThreadKoid));
debug_ipc::AttachRequest attach_request = {};
attach_request.koid = kProcKoid;
debug_ipc::AttachReply attach_reply;
remote_api->OnAttach(attach_request, &attach_reply);
// The threads are only populated on the next tick.
debug::MessageLoop::Current()->RunUntilNoTasks();
// Process should be watching.
EXPECT_TRUE(HasAttachedProcessWithKoid(harness.debug_agent(), kProcKoid));
// We should've gotten an attach reply.
EXPECT_TRUE(attach_reply.status.ok());
EXPECT_EQ(attach_reply.koid, kProcKoid);
EXPECT_EQ(attach_reply.name, "proc");
{
DebuggedProcess* process = harness.debug_agent()->GetDebuggedProcess(kProcKoid);
ASSERT_TRUE(process);
auto threads = process->GetThreads();
ASSERT_EQ(threads.size(), 1u);
// Search for the exception thread.
DebuggedThread* exception_thread = nullptr;
for (DebuggedThread* thread : threads) {
if (thread->koid() == kThreadKoid) {
exception_thread = thread;
break;
}
}
ASSERT_TRUE(exception_thread);
ASSERT_TRUE(exception_thread->in_exception());
EXPECT_EQ(exception_thread->exception_handle()->GetThreadHandle()->GetKoid(), kThreadKoid);
}
}
TEST_F(DebugAgentTests, OnEnterLimbo) {
MockDebugAgentHarness harness;
constexpr zx_koid_t kProcKoid1 = 100;
constexpr zx_koid_t kThreadKoid1 = 101;
harness.system_interface()->mock_limbo_provider().AppendException(
MockProcessHandle(kProcKoid1, "proc1"), MockThreadHandle(kThreadKoid1, "thread1"),
MockExceptionHandle(kThreadKoid1));
// Call the limbo.
harness.system_interface()->mock_limbo_provider().CallOnEnterLimbo();
// Should've sent a notification.
{
ASSERT_EQ(harness.stream_backend()->process_starts().size(), 1u);
auto& process_start = harness.stream_backend()->process_starts()[0];
EXPECT_EQ(process_start.type, debug_ipc::NotifyProcessStarting::Type::kLimbo);
EXPECT_EQ(process_start.koid, kProcKoid1);
EXPECT_EQ(process_start.name, "proc1");
}
}
TEST_F(DebugAgentTests, DetachFromLimbo) {
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
constexpr zx_koid_t kProcKoid = 14; // MockJobTree job11-p1
// Attempting to detach to a process that doesn't exist should fail.
{
debug_ipc::DetachRequest request = {};
request.koid = kProcKoid;
debug_ipc::DetachReply reply = {};
remote_api->OnDetach(request, &reply);
ASSERT_TRUE(reply.status.has_error());
ASSERT_EQ(harness.system_interface()->mock_limbo_provider().release_calls().size(), 0u);
}
// Adding it should now find it and remove it.
constexpr zx_koid_t kProcKoid1 = 100;
constexpr zx_koid_t kThreadKoid1 = 101;
harness.system_interface()->mock_limbo_provider().AppendException(
MockProcessHandle(kProcKoid1, "proc1"), MockThreadHandle(kThreadKoid1, "thread1"),
MockExceptionHandle(kThreadKoid1));
{
debug_ipc::DetachRequest request = {};
request.koid = kProcKoid1;
debug_ipc::DetachReply reply = {};
remote_api->OnDetach(request, &reply);
ASSERT_TRUE(reply.status.ok());
ASSERT_EQ(harness.system_interface()->mock_limbo_provider().release_calls().size(), 1u);
EXPECT_EQ(harness.system_interface()->mock_limbo_provider().release_calls()[0], kProcKoid1);
}
// This should've remove it from limbo, trying it again should fail.
{
debug_ipc::DetachRequest request = {};
request.koid = kProcKoid1;
debug_ipc::DetachReply reply = {};
remote_api->OnDetach(request, &reply);
ASSERT_TRUE(reply.status.has_error());
ASSERT_EQ(harness.system_interface()->mock_limbo_provider().release_calls().size(), 1u);
EXPECT_EQ(harness.system_interface()->mock_limbo_provider().release_calls()[0], kProcKoid1);
}
}
TEST_F(DebugAgentTests, Kill) {
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
constexpr zx_koid_t kProcKoid = 14; // MockJobTree job11-p1
// Attempt to kill a process that's not there should fail.
{
debug_ipc::KillRequest kill_request = {};
kill_request.process_koid = kProcKoid;
debug_ipc::KillReply kill_reply = {};
remote_api->OnKill(kill_request, &kill_reply);
ASSERT_TRUE(kill_reply.status.has_error());
}
// Attach to a process so that the debugger knows about it.
{
debug_ipc::AttachRequest attach_request = {};
attach_request.koid = kProcKoid;
debug_ipc::AttachReply attach_reply;
remote_api->OnAttach(attach_request, &attach_reply);
// There should be a process.
ASSERT_EQ(harness.debug_agent()->procs_.size(), 1u);
// Should not come from limbo.
EXPECT_FALSE(harness.debug_agent()->procs_.begin()->second->from_limbo());
}
// Killing now should work.
{
debug_ipc::KillRequest kill_request = {};
kill_request.process_koid = kProcKoid;
debug_ipc::KillReply kill_reply = {};
remote_api->OnKill(kill_request, &kill_reply);
// There should be no more processes.
ASSERT_EQ(harness.debug_agent()->procs_.size(), 0u);
// Killing again should fail.
remote_api->OnKill(kill_request, &kill_reply);
ASSERT_TRUE(kill_reply.status.has_error());
}
// Add the process to the limbo.
constexpr zx_koid_t kLimboProcKoid = 100;
constexpr zx_koid_t kLimboThreadKoid = 101;
MockProcessHandle mock_process(kLimboProcKoid, "proc");
// This is a limbo process so we can not kill it.
mock_process.set_kill_status(debug::Status("Access denied"));
MockThreadHandle mock_thread(kLimboThreadKoid, "thread");
MockExceptionHandle mock_exception(kLimboThreadKoid);
mock_process.set_threads({mock_thread});
harness.system_interface()->mock_limbo_provider().AppendException(mock_process, mock_thread,
mock_exception);
// There should be no more processes.
ASSERT_EQ(harness.debug_agent()->procs_.size(), 0u);
// Killing now should release it.
{
debug_ipc::KillRequest kill_request = {};
kill_request.process_koid = kLimboProcKoid;
debug_ipc::KillReply kill_reply = {};
remote_api->OnKill(kill_request, &kill_reply);
ASSERT_TRUE(kill_reply.status.ok());
ASSERT_EQ(harness.system_interface()->mock_limbo_provider().release_calls().size(), 1u);
EXPECT_EQ(harness.system_interface()->mock_limbo_provider().release_calls()[0], kLimboProcKoid);
// Killing again should not find it.
remote_api->OnKill(kill_request, &kill_reply);
ASSERT_TRUE(kill_reply.status.has_error());
}
harness.system_interface()->mock_limbo_provider().AppendException(mock_process, mock_thread,
mock_exception);
debug_ipc::AttachRequest attach_request = {};
attach_request.koid = kLimboProcKoid;
debug_ipc::AttachReply attach_reply;
remote_api->OnAttach(attach_request, &attach_reply);
// There should be a process.
ASSERT_EQ(harness.debug_agent()->procs_.size(), 1u);
{
auto it = harness.debug_agent()->procs_.find(kLimboProcKoid);
ASSERT_NE(it, harness.debug_agent()->procs_.end());
EXPECT_TRUE(harness.debug_agent()->procs_.begin()->second->from_limbo());
// Killing it should free the process.
debug_ipc::KillRequest kill_request = {};
kill_request.process_koid = kLimboProcKoid;
debug_ipc::KillReply kill_reply = {};
remote_api->OnKill(kill_request, &kill_reply);
ASSERT_TRUE(kill_reply.status.ok());
ASSERT_EQ(harness.debug_agent()->procs_.size(), 0u);
// There should be a limbo process to be killed.
ASSERT_EQ(harness.debug_agent()->killed_limbo_procs_.size(), 1u);
EXPECT_EQ(harness.debug_agent()->killed_limbo_procs_.count(kLimboProcKoid), 1u);
// There should've have been more release calls (yet).
ASSERT_EQ(harness.system_interface()->mock_limbo_provider().release_calls().size(), 1u);
// When the process "re-enters" the limbo, it should be removed.
harness.system_interface()->mock_limbo_provider().AppendException(mock_process, mock_thread,
mock_exception);
harness.system_interface()->mock_limbo_provider().CallOnEnterLimbo();
// There should not be an additional proc in the agent.
ASSERT_EQ(harness.debug_agent()->procs_.size(), 0u);
// There should've been a release call.
ASSERT_EQ(harness.system_interface()->mock_limbo_provider().release_calls().size(), 2u);
EXPECT_EQ(harness.system_interface()->mock_limbo_provider().release_calls()[1], kLimboProcKoid);
}
}
TEST_F(DebugAgentTests, OnUpdateGlobalSettings) {
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
// The default strategy should be first chance for a type that has yet to be
// updated.
EXPECT_EQ(debug_ipc::ExceptionStrategy::kFirstChance,
harness.debug_agent()->GetExceptionStrategy(debug_ipc::ExceptionType::kGeneral));
EXPECT_EQ(debug_ipc::ExceptionStrategy::kFirstChance,
harness.debug_agent()->GetExceptionStrategy(debug_ipc::ExceptionType::kPageFault));
{
const debug_ipc::UpdateGlobalSettingsRequest request = {
.exception_strategies =
{
{
.type = debug_ipc::ExceptionType::kGeneral,
.value = debug_ipc::ExceptionStrategy::kSecondChance,
},
{
.type = debug_ipc::ExceptionType::kPageFault,
.value = debug_ipc::ExceptionStrategy::kSecondChance,
},
},
};
debug_ipc::UpdateGlobalSettingsReply reply;
remote_api->OnUpdateGlobalSettings(request, &reply);
EXPECT_TRUE(reply.status.ok());
}
EXPECT_EQ(debug_ipc::ExceptionStrategy::kSecondChance,
harness.debug_agent()->GetExceptionStrategy(debug_ipc::ExceptionType::kGeneral));
EXPECT_EQ(debug_ipc::ExceptionStrategy::kSecondChance,
harness.debug_agent()->GetExceptionStrategy(debug_ipc::ExceptionType::kPageFault));
{
const debug_ipc::UpdateGlobalSettingsRequest request = {
.exception_strategies =
{
{
.type = debug_ipc::ExceptionType::kGeneral,
.value = debug_ipc::ExceptionStrategy::kFirstChance,
},
},
};
debug_ipc::UpdateGlobalSettingsReply reply;
remote_api->OnUpdateGlobalSettings(request, &reply);
EXPECT_TRUE(reply.status.ok());
}
EXPECT_EQ(debug_ipc::ExceptionStrategy::kFirstChance,
harness.debug_agent()->GetExceptionStrategy(debug_ipc::ExceptionType::kGeneral));
EXPECT_EQ(debug_ipc::ExceptionStrategy::kSecondChance,
harness.debug_agent()->GetExceptionStrategy(debug_ipc::ExceptionType::kPageFault));
}
TEST_F(DebugAgentTests, WeakFilterMatchDoesNotSendModules) {
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
constexpr char kProcessName[] = "process-1";
constexpr uint64_t kProcessKoid = 0x12345;
debug_ipc::UpdateFilterRequest request;
auto& filter = request.filters.emplace_back();
filter.type = debug_ipc::Filter::Type::kProcessName;
filter.pattern = kProcessName;
filter.id = 1;
filter.weak = true;
debug_ipc::UpdateFilterReply reply;
remote_api->OnUpdateFilter(request, &reply);
EXPECT_TRUE(reply.matched_processes_for_filter.empty());
harness.debug_agent()->OnProcessStarting(
std::make_unique<MockProcessHandle>(kProcessKoid, kProcessName));
// We should have sent a process starting notification, but no modules.
EXPECT_FALSE(harness.stream_backend()->process_starts().empty());
EXPECT_TRUE(harness.stream_backend()->modules().empty());
}
TEST_F(DebugAgentTests, RecursiveFilterAppliesImplicitFilter) {
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
constexpr char kRootComponentUrl[] = "fuchsia-pkg://devhost/root_package#meta/root_component.cm";
constexpr char kRootComponentMoniker[] = "some/moniker/root:test";
constexpr char kSubpackageUrl[] = "#meta/child.cm";
constexpr char kSubpackageMoniker[] = "some/moniker/root:test/driver";
debug_ipc::UpdateFilterRequest request;
auto& filter = request.filters.emplace_back();
filter.type = debug_ipc::Filter::Type::kComponentUrl;
filter.pattern = kRootComponentUrl;
filter.recursive = true;
debug_ipc::UpdateFilterReply reply;
remote_api->OnUpdateFilter(request, &reply);
harness.debug_agent()->OnComponentDiscovered(kRootComponentMoniker, kRootComponentUrl);
// There should now be TWO filters, one of which the frontend doesn't know about.
// Now we start the child component, which contains a program.
harness.debug_agent()->OnComponentStarted(kSubpackageMoniker, kSubpackageUrl);
EXPECT_EQ(harness.stream_backend()->component_starts().size(), 1u);
EXPECT_EQ(harness.stream_backend()->component_starts()[0].component.url, kSubpackageUrl);
EXPECT_EQ(harness.stream_backend()->component_starts()[0].component.moniker, kSubpackageMoniker);
}
} // namespace debug_agent