blob: 4e2d8f20bc3cfe446570ccef3bf47cd30ec0837e [file] [log] [blame] [edit]
// 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_job_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/remote_api.h"
#include "src/developer/debug/debug_agent/test_utils.h"
#include "src/developer/debug/ipc/filter_utils.h"
#include "src/developer/debug/ipc/protocol.h"
#include "src/developer/debug/ipc/records.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()).IsAttached();
}
// 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 = debug_ipc::Filter::Identifier(1, debug_ipc::Filter::Originator::kUnknown);
filter.config.weak = true;
debug_ipc::UpdateFilterReply reply;
remote_api->OnUpdateFilter(request, &reply);
EXPECT_TRUE(reply.matched_processes_for_filter.empty());
harness.debug_agent()->OnProcessChanged(
DebugAgent::ProcessChangedHow::kStarting,
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, RecursiveFiltersDoNotCollideUpdateFilter) {
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
debug_ipc::UpdateFilterRequest request;
debug_ipc::Filter filter;
filter.type = debug_ipc::Filter::Type::kComponentMonikerSuffix;
filter.pattern = "test:test_root";
filter.id = debug_ipc::Filter::Identifier(1, debug_ipc::Filter::Originator::kUnknown);
filter.config.recursive = true;
request.filters.push_back(filter);
debug_ipc::Filter filter2;
filter2.type = debug_ipc::Filter::Type::kComponentUrl;
filter2.pattern = "fuchsia-pkg://devhost/root_package#meta/root_component.cm";
filter2.id = debug_ipc::Filter::Identifier(2, debug_ipc::Filter::Originator::kUnknown);
filter2.config.recursive = true;
request.filters.push_back(filter2);
debug_ipc::UpdateFilterReply reply;
remote_api->OnUpdateFilter(request, &reply);
loop().RunUntilNoTasks();
// Should have a filter created message.
EXPECT_EQ(harness.stream_backend()->filters().size(), 3u);
constexpr char kExpectedMonikerPrefixFilterPattern[] = "/moniker/generated/test:test_root";
const auto& job4_filter = std::ranges::find_if(
harness.stream_backend()->filters(),
[kExpectedMonikerPrefixFilterPattern](const debug_ipc::NotifyFilterCreated& notify) {
return notify.filter.pattern == kExpectedMonikerPrefixFilterPattern;
});
ASSERT_NE(job4_filter, harness.stream_backend()->filters().end());
// When creating a new filter for matching during UpdateFilter, the client does not have to send
// another UpdateFilter message to match against the new filter.
EXPECT_TRUE(job4_filter->participated_in_matching);
// The first matching filter is the creator of the new filter.
EXPECT_EQ(job4_filter->originating_filter_id, filter.id);
EXPECT_EQ(job4_filter->filter.pattern, "/moniker/generated/test:test_root");
EXPECT_EQ(job4_filter->filter.type, debug_ipc::Filter::Type::kComponentMonikerPrefix);
EXPECT_EQ(job4_filter->filter.config.recursive, false);
}
TEST_F(DebugAgentTests, RecursiveFiltersDoNotCollideComponentStarting) {
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
constexpr char kFakeMoniker[] = "/moniker/generated/test_some_root";
debug_ipc::UpdateFilterRequest request;
debug_ipc::Filter filter;
filter.type = debug_ipc::Filter::Type::kComponentMonikerSuffix;
// Intentionally does not match the components in MockSystemInterface.
filter.pattern = "test_some_root";
filter.id = debug_ipc::Filter::Identifier(1, debug_ipc::Filter::Originator::kUnknown);
filter.config.recursive = true;
request.filters.push_back(filter);
debug_ipc::Filter filter2;
filter2.type = debug_ipc::Filter::Type::kComponentUrl;
// Intentionally does not match the components in MockSystemInterface.
filter2.pattern = "fuchsia-pkg://devhost/some_package#meta/root_component.cm";
filter2.id = debug_ipc::Filter::Identifier(2, debug_ipc::Filter::Originator::kUnknown);
filter2.config.recursive = true;
request.filters.push_back(filter2);
debug_ipc::UpdateFilterReply reply;
remote_api->OnUpdateFilter(request, &reply);
// Shouldn't match anything yet since these component details are not specified in
// MockSystemInterface.
EXPECT_TRUE(reply.matched_processes_for_filter.empty());
// This event matches both filters created above, but we should only add a single moniker prefix
// filter to match this.
harness.system_interface()->mock_component_manager().InjectComponentEvent(
FakeEventType::kDebugStarted, kFakeMoniker,
"fuchsia-pkg://devhost/some_package#meta/root_component.cm");
loop().RunUntilNoTasks();
// Should have a filter created message.
EXPECT_EQ(harness.stream_backend()->filters().size(), 1u);
// The new filter doesn't participate in matching until the client sends it back in an
// UpdateFilter request.
EXPECT_FALSE(harness.stream_backend()->filters()[0].participated_in_matching);
// The first matching filter is the creator of the new filter.
EXPECT_EQ(harness.stream_backend()->filters()[0].originating_filter_id, filter.id);
EXPECT_EQ(harness.stream_backend()->filters()[0].filter.pattern, kFakeMoniker);
EXPECT_EQ(harness.stream_backend()->filters()[0].filter.type,
debug_ipc::Filter::Type::kComponentMonikerPrefix);
EXPECT_EQ(harness.stream_backend()->filters()[0].filter.config.recursive, false);
}
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[] = "/moniker/generated/test:test_root";
// From MockSystemInterface.
constexpr zx_koid_t kJob5P1Koid = 33;
debug_ipc::UpdateFilterRequest request;
auto& filter = request.filters.emplace_back();
filter.type = debug_ipc::Filter::Type::kComponentUrl;
filter.pattern = kRootComponentUrl;
filter.config.recursive = true;
debug_ipc::UpdateFilterReply reply;
remote_api->OnUpdateFilter(request, &reply);
// We should have received a filter created notification.
ASSERT_FALSE(harness.stream_backend()->filters().empty());
const auto& filters = harness.stream_backend()->filters();
ASSERT_EQ(filters.size(), 1u);
EXPECT_EQ(filters[0].originating_filter_id, request.filters[0].id);
EXPECT_EQ(filters[0].filter.id.Decode().originator, debug_ipc::Filter::Originator::kAgent);
EXPECT_EQ(filters[0].filter.pattern, kRootComponentMoniker);
EXPECT_EQ(filters[0].filter.type, debug_ipc::Filter::Type::kComponentMonikerPrefix);
EXPECT_FALSE(filters[0].filter.config.recursive);
// Matching for the new filter happens along with the rest of the new filters during the
// UpdateFilter request.
EXPECT_TRUE(filters[0].participated_in_matching);
// The root component that matches the filter in our |UpdateFilterRequest| doesn't have a process
// associated with it, but we should match the process in that component's realm in the
// subpackaged component.
ASSERT_EQ(reply.matched_processes_for_filter.size(), 1u);
EXPECT_EQ(reply.matched_processes_for_filter[0].id, filters[0].filter.id);
EXPECT_EQ(reply.matched_processes_for_filter[0].matches.size(), 1u);
EXPECT_EQ(reply.matched_processes_for_filter[0].matches[0].koid, kJob5P1Koid);
EXPECT_EQ(reply.matched_processes_for_filter[0].matches[0].type, debug_ipc::TaskType::kProcess);
}
TEST_F(DebugAgentTests, AttachToExistingJob) {
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
constexpr zx_koid_t kJobKoid = 8;
constexpr zx_koid_t kProcessKoid = 9;
debug_ipc::AttachRequest request;
request.koid = kJobKoid;
request.config.target = debug_ipc::TaskType::kJob;
request.config.priority = debug_ipc::AttachConfig::Priority::kWeak;
debug_ipc::AttachReply reply;
remote_api->OnAttach(request, &reply);
// Because we didn't inject all of the notifications.
auto debugged_job = harness.debug_agent()->GetDebuggedJob(kJobKoid);
ASSERT_TRUE(debugged_job);
// Simply attaching to a job won't necessarily create the DebuggedProcess objects (i.e. the client
// issued a direct attach command to an already running component instead of installing a filter
// and waiting for a matching component to start).
EXPECT_FALSE(harness.debug_agent()->GetDebuggedProcess(kProcessKoid));
}
TEST_F(DebugAgentTests, JobOnlyFilter) {
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
// Matches the default job tree in MockSystemInterface.
constexpr zx_koid_t kJobKoid = 25;
constexpr zx_koid_t kProcessKoid = 26;
constexpr char kComponentRootMoniker[] = "fixed/moniker";
debug_ipc::UpdateFilterRequest request;
auto& filter = request.filters.emplace_back();
filter.type = debug_ipc::Filter::Type::kComponentMonikerSuffix;
filter.pattern = kComponentRootMoniker;
filter.config.job_only = true;
// By specifying a strong attach, we'll claim the job's exception channel, not the debugger
// exception channel.
filter.config.weak = false;
debug_ipc::UpdateFilterReply reply;
remote_api->OnUpdateFilter(request, &reply);
// Should have matched the existing component.
EXPECT_EQ(reply.matched_processes_for_filter.size(), 1u);
EXPECT_EQ(reply.matched_processes_for_filter[0].id, filter.id);
EXPECT_EQ(reply.matched_processes_for_filter[0].matches.size(), 1u);
EXPECT_EQ(reply.matched_processes_for_filter[0].matches[0].koid, kJobKoid);
EXPECT_EQ(reply.matched_processes_for_filter[0].matches[0].type, debug_ipc::TaskType::kJob);
debug_ipc::AttachRequest attach_request;
attach_request.koid = reply.matched_processes_for_filter[0].matches[0].koid;
attach_request.config = debug_ipc::FilterConfig::ToAttachConfig(filter.config);
debug_ipc::AttachReply attach_reply;
remote_api->OnAttach(attach_request, &attach_reply);
EXPECT_TRUE(attach_reply.status.ok()) << attach_reply.status.message();
// The job_only filter will cause the DebuggedProcess object to be created and propagate the
// process starting notification.
harness.debug_agent()->OnProcessChanged(
DebugAgent::ProcessChangedHow::kStarting,
harness.debug_agent()->system_interface().GetProcess(kProcessKoid));
// We should now have both a DebuggedJob and a DebuggedProcess for this job and process. The
// process should *not* have the exception channel bound, because the filter was configured as
// job_only.
EXPECT_EQ(harness.stream_backend()->process_starts().size(), 1u);
EXPECT_TRUE(harness.debug_agent()->GetDebuggedJob(kJobKoid));
// We should have a DebuggedProcess object, but should not be attached to its exception channel.
ASSERT_TRUE(harness.debug_agent()->GetDebuggedProcess(kProcessKoid));
EXPECT_FALSE(harness.debug_agent()->GetDebuggedProcess(kProcessKoid)->IsAttached());
}
TEST_F(DebugAgentTests, JobOnlyFilterDoesNotAttachToChildJobs) {
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
// Matches the default job tree in MockSystemInterface. This is "job1".
constexpr zx_koid_t kJobKoid = 8;
// Direct child process of "job1".
constexpr zx_koid_t kProcessKoid = 9;
// This is a child job of the above, "job11".
constexpr zx_koid_t kSecondJobKoid = 13;
// Child of job11.
constexpr zx_koid_t kSecondProcessKoid = 14;
// Component moniker associated with all above jobs.
constexpr char kComponentRootMoniker[] = "/moniker";
debug_ipc::UpdateFilterRequest request;
auto& filter = request.filters.emplace_back();
filter.type = debug_ipc::Filter::Type::kComponentMonikerSuffix;
filter.pattern = kComponentRootMoniker;
filter.config.job_only = true;
// By specifying a strong attach, we'll claim the job's exception channel, not the debugger
// exception channel.
filter.config.weak = false;
debug_ipc::UpdateFilterReply reply;
// We don't need to worry about the contents of the reply, it will have all of the child processes
// under the component. In a real system, the client will choose what to attach to.
remote_api->OnUpdateFilter(request, &reply);
// Attach to the root job of the component from the component starting event.
harness.debug_agent()->OnComponentStarted(kComponentRootMoniker, "some/url", kJobKoid);
ASSERT_EQ(harness.stream_backend()->component_starts().size(), 1u);
auto component_start = harness.stream_backend()->component_starts()[0];
ASSERT_EQ(component_start.matching_filters.size(), 1u);
auto attach_configs = debug_ipc::GetAttachConfigsForFilterMatches(
component_start.matching_filters, harness.debug_agent()->GetIpcFilters());
ASSERT_EQ(attach_configs.size(), 1u);
ASSERT_TRUE(attach_configs.contains(kJobKoid));
const auto& attach_config = attach_configs.find(kJobKoid)->second;
EXPECT_EQ(attach_config.priority, debug_ipc::AttachConfig::Priority::kStrong);
EXPECT_EQ(attach_config.target, debug_ipc::TaskType::kJob);
debug_ipc::AttachRequest attach_request;
attach_request.koid = kJobKoid;
attach_request.config = attach_config;
debug_ipc::AttachReply attach_reply;
harness.debug_agent()->OnAttach(attach_request, &attach_reply);
ASSERT_TRUE(attach_reply.status.ok());
// Send the notification that the child process of "job1" started. This one will always start
// first, and will create the DebuggedProcess objects.
harness.debug_agent()->OnProcessChanged(
DebugAgent::ProcessChangedHow::kStarting,
harness.debug_agent()->system_interface().GetProcess(kProcessKoid));
// Send the notification that the child process of "job11" started. This should _not_ attach to
harness.debug_agent()->OnProcessChanged(
DebugAgent::ProcessChangedHow::kStarting,
harness.debug_agent()->system_interface().GetProcess(kSecondProcessKoid));
// We should now have both processes specified above. Neither process should have the exception
// channel bound, because the filter was configured as job_only. Furthermore, the parent job
// _should_ be attached (i.e. we'll have a DebuggedJob object for it) and the child job should
// _not_ be attached.
EXPECT_EQ(harness.stream_backend()->process_starts().size(), 2u);
EXPECT_TRUE(harness.debug_agent()->GetDebuggedJob(kJobKoid));
// We are _not_ attached to this second job, even though it would also match the component filter.
EXPECT_FALSE(harness.debug_agent()->GetDebuggedJob(kSecondJobKoid));
}
// This test is very similar to the above test. The difference is in the component structure. This
// test's filter matches multiple components, which respectively in a real system would have unique
// job_ids, and therefore return multiple results in the filter matching logic. This is different
// from the above situation where one component (and its corresponding job) contains multiple jobs
// but no child components.
TEST_F(DebugAgentTests, DoNotAttachToChildJobs) {
MockDebugAgentHarness harness;
RemoteAPI* remote_api = harness.debug_agent();
// From the default MockSystemInterface.
constexpr zx_koid_t kParentJobKoid = 35;
constexpr zx_koid_t kChildJobKoid = 38;
constexpr char kRootComponentMoniker[] = "/some/moniker";
constexpr char kChildComponentMoniker[] = "/some/other/moniker";
// Start the components before the filter. The filter should match immediately in |OnUpdateFilter|
// below.
harness.debug_agent()->OnComponentStarted(kRootComponentMoniker, "some/url", kParentJobKoid);
harness.debug_agent()->OnComponentStarted(kChildComponentMoniker, "some/url", kParentJobKoid);
// This realm has two ELF components, one is the realm's root, the other is a child. They each
// have distinctive job_ids.
constexpr char kMonikerPrefix[] = "/some";
debug_ipc::UpdateFilterRequest request;
auto& filter = request.filters.emplace_back();
filter.type = debug_ipc::Filter::Type::kComponentMonikerPrefix;
filter.pattern = kMonikerPrefix;
filter.config.job_only = true;
// By specifying a strong attach, we'll claim the job's exception channel, not the debugger
// exception channel.
filter.config.weak = false;
debug_ipc::UpdateFilterReply reply;
remote_api->OnUpdateFilter(request, &reply);
// We should have 1 filter match and two job koids.
EXPECT_FALSE(reply.matched_processes_for_filter.empty());
EXPECT_EQ(reply.matched_processes_for_filter.size(), 1u);
EXPECT_EQ(reply.matched_processes_for_filter[0].matches.size(), 2u);
auto configs = debug_ipc::GetAttachConfigsForFilterMatches(
reply.matched_processes_for_filter, harness.debug_agent()->GetIpcFilters());
// We will attempt to attach to both.
EXPECT_EQ(configs.size(), 2u);
for (const auto& [koid, config] : configs) {
debug_ipc::AttachRequest request;
request.koid = koid;
request.config = config;
debug_ipc::AttachReply reply;
remote_api->OnAttach(request, &reply);
if (koid == kParentJobKoid) {
EXPECT_TRUE(reply.status.ok());
} else {
// We should get an already exists error for the child job.
EXPECT_TRUE(reply.status.has_error());
EXPECT_EQ(reply.status.type(), debug::Status::Type::kAlreadyExists);
}
}
EXPECT_TRUE(harness.debug_agent()->GetDebuggedJob(kParentJobKoid));
EXPECT_FALSE(harness.debug_agent()->GetDebuggedJob(kChildJobKoid));
}
TEST_F(DebugAgentTests, RecursiveJobOnlyFilterDoesNotCannibalizeChildComponents) {
constexpr zx_koid_t kRootJobKoid = 1001;
constexpr zx_koid_t kParentJobKoid = 1002;
constexpr zx_koid_t kParentJobProcessKoid = 1003;
constexpr zx_koid_t kChildJobKoid = 1005;
constexpr zx_koid_t kChildJobProcessKoid = 1006;
// We're going to create a custom component topology on the fly for this test. Here is what the
// initial state looks like:
//
// j: 1001 root
// j: 1002 parent-job
// p: 1003 parent-job-p1 some/moniker fuchsia-pkg://some/url
// t: 1004 initial-thread
MockProcessHandle parent_job_process(kParentJobProcessKoid, "parent-job-p1");
parent_job_process.set_threads({MockThreadHandle(1004, "initial-thread")});
MockJobHandle parent_component_job(kParentJobKoid, "parent-job");
parent_component_job.set_child_processes({std::move(parent_job_process)});
MockJobHandle root_job(kRootJobKoid, "root");
root_job.set_child_jobs({std::move(parent_component_job)});
auto unique_mock_system_interface = std::make_unique<MockSystemInterface>(std::move(root_job));
auto mock_system_interface = unique_mock_system_interface.get();
constexpr char kParentComponentMoniker[] = "some/moniker";
constexpr char kChildComponentMoniker[] = "some/moniker/other/moniker";
mock_system_interface->mock_component_manager().AddComponentInfo(
kParentJobKoid, {.moniker = kParentComponentMoniker, .url = "fuchsia-pkg://some/url"});
MockDebugAgentHarness harness(std::move(unique_mock_system_interface));
RemoteAPI* remote_api = harness.debug_agent();
// Install a recursive job-only filter. This will install its recursive (process-only) counterpart
// immediately upon installation since it should already match.
debug_ipc::UpdateFilterRequest request;
auto& filter = request.filters.emplace_back();
filter.type = debug_ipc::Filter::Type::kComponentMonikerSuffix;
filter.pattern = "moniker";
filter.config.job_only = true;
filter.config.recursive = true;
// This will immediately match the parent component and install the recursive filter.
debug_ipc::UpdateFilterReply reply;
remote_api->OnUpdateFilter(request, &reply);
// We should have gotten a filter created event.
ASSERT_EQ(harness.stream_backend()->filters().size(), 1u);
EXPECT_EQ(harness.stream_backend()->filters()[0].originating_filter_id, filter.id);
const auto& moniker_prefix_filter = harness.stream_backend()->filters()[0].filter;
EXPECT_EQ(moniker_prefix_filter.pattern, kParentComponentMoniker);
EXPECT_FALSE(moniker_prefix_filter.config.job_only);
EXPECT_FALSE(moniker_prefix_filter.config.recursive);
EXPECT_TRUE(moniker_prefix_filter.config.never_attach);
// Now, we want to inject a new child component that matches both the original recursive job-only
// filter and the new moniker prefix filter.
MockProcessHandle child_job_process(kChildJobProcessKoid, "child-job-p1");
child_job_process.set_threads({MockThreadHandle(1007, "initial-thread")});
MockJobHandle child_job(kChildJobKoid, "child-job");
child_job.set_child_processes({std::move(child_job_process)});
debug_ipc::ComponentInfo child_info = {.moniker = kChildComponentMoniker,
.url = "fuchsia-pkg://some/other/url"};
// The job is now inserted into the job tree along with the component info, so the entire tree
// looks like this now:
//
// j: 1001 root
// j: 1002 parent-job some/moniker fuchsia-pkg://some/url
// p: 1003 parent-job-p1
// t: 1004 initial-thread
// j: 1005 child-job some/moniker/other/moniker fuchsia-pkg://some/other/url
// p: 1006 child-job-p1
// t: 1007 initial-thread
mock_system_interface->AddJob(std::move(child_job), kParentJobKoid, child_info);
// Inject the component starting event, so that our filters notice.
harness.debug_agent()->OnComponentStarted(child_info.moniker, child_info.url, kChildJobKoid);
// We should have received the event. This will be for the job, which we don't want to issue an
// attach for (the DebugAgent would ignore it anyway since we're attached to the parent).
EXPECT_EQ(harness.stream_backend()->component_starts().size(), 1u);
// We should still only have the one extra filter added from before, there should *not* be a new
// recursive filter installed for the child's complete moniker (which is redundant since the
// parent's moniker will match everything in the child component).
ASSERT_EQ(harness.stream_backend()->filters().size(), 1u);
EXPECT_EQ(harness.stream_backend()->filters()[0].originating_filter_id, filter.id);
EXPECT_EQ(harness.stream_backend()->filters()[0].filter.pattern, kParentComponentMoniker);
// Send the notification that the child process of "child-job" started.
harness.debug_agent()->OnProcessChanged(
DebugAgent::ProcessChangedHow::kStarting,
harness.debug_agent()->system_interface().GetProcess(kChildJobProcessKoid));
EXPECT_EQ(harness.stream_backend()->process_starts().size(), 1u);
// Both of our filters should be reported as a match. The order is not guaranteed.
ASSERT_EQ(harness.stream_backend()->process_starts()[0].filter_ids.size(), 2u);
auto found = std::ranges::find(harness.stream_backend()->process_starts()[0].filter_ids,
moniker_prefix_filter.id);
EXPECT_NE(found, harness.stream_backend()->process_starts()[0].filter_ids.end());
found = std::ranges::find(harness.stream_backend()->process_starts()[0].filter_ids, filter.id);
EXPECT_NE(found, harness.stream_backend()->process_starts()[0].filter_ids.end());
// And we should still not have any other filter created events.
ASSERT_EQ(harness.stream_backend()->filters().size(), 1u);
EXPECT_EQ(harness.stream_backend()->filters()[0].originating_filter_id, filter.id);
EXPECT_EQ(harness.stream_backend()->filters()[0].filter.pattern, kParentComponentMoniker);
// We should be explicitly attached to the child job's process, since it was created after we had
// a matching filter in place. Note that since we didn't issue explicit attach requests in
// response to the UpdateFilter request at the beginning of this test, we are _not_ attached to
// the parent job or its process.
EXPECT_TRUE(harness.debug_agent()->GetDebuggedProcess(kChildJobProcessKoid));
// We are _not_ attached to this second job, even though it would also match the component moniker
// prefix filter, because that filter is not configured as job-only.
EXPECT_FALSE(harness.debug_agent()->GetDebuggedJob(kChildJobKoid));
// Check that we don't have any extra filters installed other than the original recursive job
// filter and the highest level component moniker prefix filter.
debug_ipc::StatusReply status;
remote_api->OnStatus({}, &status);
ASSERT_EQ(status.filters.size(), 2u);
EXPECT_FALSE(status.filters[0].config.job_only);
EXPECT_FALSE(status.filters[0].config.recursive);
EXPECT_EQ(status.filters[0].pattern, kParentComponentMoniker);
EXPECT_EQ(status.filters[0].type, debug_ipc::Filter::Type::kComponentMonikerPrefix);
EXPECT_TRUE(status.filters[1].config.job_only);
EXPECT_TRUE(status.filters[1].config.recursive);
EXPECT_EQ(status.filters[1].pattern, "moniker");
EXPECT_EQ(status.filters[1].type, debug_ipc::Filter::Type::kComponentMonikerSuffix);
}
} // namespace debug_agent