blob: bee9f7b78b4722676d37917bf0c7f141cc4622e7 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/tracing/common/active_processes_win.h"
#include <windows.h>
#include <optional>
#include "base/base_paths.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/process/process.h"
#include "base/test/scoped_path_override.h"
#include "base/threading/platform_thread.h"
#include "base/version_info/version_info_values.h"
#include "base/win/access_token.h"
#include "base/win/sid.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace tracing {
class ActiveProcessesTest : public testing::Test {
protected:
// ActiveProcesses explicitly uses uint32_t for pid/tid, rather than the
// base typedefs for process/thread id. Assert that these match in size.
using ProcessId = uint32_t;
using ThreadId = uint32_t;
static_assert(sizeof(ProcessId) == sizeof(base::ProcessId));
static_assert(sizeof(ThreadId) ==
sizeof(base::PlatformThreadId::UnderlyingType));
using PidAndTid = std::pair<ProcessId, ThreadId>;
// The pid and tid of the client. Named "base" because the offsets below are
// applied to compute distinct pids and tids for other processes.
static constexpr ProcessId kBasePid = 0x1000;
static constexpr ThreadId kBaseTid = 0x1000;
// Offsets from the bases above for pids and tids for other processes.
static constexpr int kChildOffset = 100;
static constexpr int kUnrelatedOffset = 200;
static constexpr int kIndependentOffset = 300;
static constexpr int kSystemOffset = 400;
// testing::Test:
void SetUp() override {
// Create a version directory %TMP%\random\W.X.Y.Z.
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
fake_dir_exe_ =
temp_dir_.GetPath().Append(FILE_PATH_LITERAL(PRODUCT_VERSION));
ASSERT_TRUE(base::CreateDirectory(fake_dir_exe_));
// Pretend that it is the dir in which the current executable is running.
dir_exe_override_.emplace(base::DIR_EXE, fake_dir_exe_,
/*is_absolute=*/true, /*create=*/false);
// Make a command line for a client residing in %TMP%\random.
base::CommandLine client_command_line(
temp_dir_.GetPath().AppendASCII(client_image_file_name_));
client_command_line.AppendArgNative(L"arg1");
client_command_line.AppendSwitchNative("switch1", L"value1");
client_command_line.AppendSwitch("switch2");
client_command_line.AppendArgNative(L"arg2");
client_command_line_ = client_command_line.GetCommandLineString();
// Make a command line for an external client residing in %TMP%.
external_client_command_line_ =
base::CommandLine(
temp_dir_.GetPath().DirName().AppendASCII(client_image_file_name_))
.GetCommandLineString();
// Now construct the instance to test.
active_processes_.emplace(kBasePid);
}
ActiveProcesses& active_processes() { return *active_processes_; }
// Some random sid.
const base::win::Sid& random_user_sid() const { return random_user_sid_; }
// The file name of the current process's image.
const std::string& client_image_file_name() const {
return client_image_file_name_;
}
// The command line string of a client that belongs to the application.
const std::wstring& client_command_line() const {
return client_command_line_;
}
// The command line string of a client that is unrelated to the application.
const std::wstring& external_client_command_line() const {
return external_client_command_line_;
}
// Adds the client.
PidAndTid AddClient() {
active_processes().AddProcess(
kBasePid, /*parent_pid=*/0, /*session_id=*/4, random_user_sid().Clone(),
client_image_file_name(), client_command_line());
active_processes().AddThread(kBasePid, kBaseTid, /*thread_name=*/{});
return {kBasePid, kBaseTid};
}
// Adds a child process of the client -- same image file name, parent pid
// matches the client.
PidAndTid AddChild() {
const ProcessId child_pid = kBasePid + kChildOffset;
const ThreadId child_tid = kBaseTid + kChildOffset;
active_processes().AddProcess(
child_pid, kBasePid, /*session_id=*/4, random_user_sid().Clone(),
client_image_file_name(), client_command_line());
active_processes().AddThread(child_pid, child_tid,
/*thread_name=*/{});
return {child_pid, child_tid};
}
// Adds a process unrelated to the client and returns its pid and a tid.
// `unrelated_pid` may be set to force a specific pid to be used.
PidAndTid AddUnrelated(ProcessId unrelated_pid = kBasePid +
kUnrelatedOffset) {
base::CommandLine unrelated_command_line(base::CommandLine::NO_PROGRAM);
unrelated_command_line.ParseFromString(client_command_line());
unrelated_command_line.SetProgram(
unrelated_command_line.GetProgram().DirName().DirName().Append(
FILE_PATH_LITERAL("unrelated.exe")));
const ThreadId unrelated_tid = kBaseTid + kUnrelatedOffset;
active_processes().AddProcess(
unrelated_pid, /*parent_pid=*/0, /*session_id=*/4,
random_user_sid().Clone(), "unrelated.exe",
unrelated_command_line.GetCommandLineString());
active_processes().AddThread(unrelated_pid, unrelated_tid,
/*thread_name=*/{});
return {unrelated_pid, unrelated_tid};
}
// Adds an independent process of the client -- in the same directory tree.
PidAndTid AddIndependent() {
base::CommandLine independent_command_line(base::CommandLine::NO_PROGRAM);
independent_command_line.ParseFromString(client_command_line());
independent_command_line.SetProgram(
independent_command_line.GetProgram()
.DirName()
.Append(FILE_PATH_LITERAL("1.2.3"))
.Append(FILE_PATH_LITERAL("independent.exe")));
const ProcessId independent_pid = kBasePid + kIndependentOffset;
const ThreadId independent_tid = kBaseTid + kIndependentOffset;
active_processes().AddProcess(
independent_pid, /*parent_pid=*/0, /*session_id=*/4,
random_user_sid().Clone(), "independent.exe",
independent_command_line.GetCommandLineString());
active_processes().AddThread(independent_pid, independent_tid,
/*thread_name=*/{});
return {independent_pid, independent_tid};
}
private:
const std::string client_image_file_name_{"client.exe"};
const base::win::Sid random_user_sid_{base::win::Sid::GenerateRandomSid()};
base::ScopedTempDir temp_dir_;
base::FilePath fake_dir_exe_;
std::optional<base::ScopedPathOverride> dir_exe_override_;
std::wstring client_command_line_;
std::wstring external_client_command_line_;
// The base pid identifies the tracing client.
std::optional<ActiveProcesses> active_processes_;
};
TEST_F(ActiveProcessesTest, UnknownTidIsOther) {
ASSERT_EQ(active_processes().GetThreadCategory(kBaseTid),
ActiveProcesses::Category::kOther);
}
// Tests that a thread added for an unknown process is considered an "other"
// process.
TEST_F(ActiveProcessesTest, TidForUnknownPidIsOther) {
active_processes().AddThread(kBasePid, kBaseTid, /*thread_name=*/{});
ASSERT_EQ(active_processes().GetThreadCategory(kBaseTid),
ActiveProcesses::Category::kOther);
}
TEST_F(ActiveProcessesTest, ClientIsClient) {
// Add the client process and a thread, and confirm that it is categorized
// properly.
const auto [client_pid, client_tid] = AddClient();
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
}
// Tests that categorization of the client works as threads come and go.
TEST_F(ActiveProcessesTest, ClientThreadActivity) {
// Add the client and a couple of threads.
const auto [client_pid, client_tid] = AddClient();
const auto client_tid2 = client_tid + 1;
active_processes().AddThread(client_pid, client_tid2, /*thread_name=*/{});
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
ASSERT_EQ(active_processes().GetThreadCategory(client_tid2),
ActiveProcesses::Category::kClient);
// Now remove a thread.
active_processes().RemoveThread(client_pid, client_tid2);
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
ASSERT_EQ(active_processes().GetThreadCategory(client_tid2),
ActiveProcesses::Category::kOther);
// Now remove the process.
active_processes().RemoveProcess(client_pid);
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kOther);
ASSERT_EQ(active_processes().GetThreadCategory(client_tid2),
ActiveProcesses::Category::kOther);
}
// Tests that a child process of the client using the same image name is
// categorized as a client proc.
TEST_F(ActiveProcessesTest, ChildOfClient) {
// Add the client.
const auto [client_pid, client_tid] = AddClient();
// Add a child process of the client -- same image file name, parent pid
// matches the client.
const auto [child_pid, child_tid] = AddChild();
// The child is categorized as a Client process.
ASSERT_EQ(active_processes().GetThreadCategory(child_tid),
ActiveProcesses::Category::kClient);
// The child going away shouldn't change the categorization of the client.
active_processes().RemoveProcess(child_pid);
ASSERT_EQ(active_processes().GetThreadCategory(child_tid),
ActiveProcesses::Category::kOther);
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
}
// Tests that a child process of the client using the same image name is
// categorized as a client proc once the client is added.
TEST_F(ActiveProcessesTest, ClientAfterChild) {
// Add a child process of the client -- same image file name, parent pid
// matches the client.
const auto [child_pid, child_tid] = AddChild();
// The client hasn't been added, so the child is still "other".
ASSERT_EQ(active_processes().GetThreadCategory(child_tid),
ActiveProcesses::Category::kOther);
// Add the client.
const auto [client_pid, client_tid] = AddClient();
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
// Now the child is categorized as a Client process.
ASSERT_EQ(active_processes().GetThreadCategory(child_tid),
ActiveProcesses::Category::kClient);
// When the client goes away, the child is recategorized as "other".
active_processes().RemoveProcess(client_pid);
ASSERT_EQ(active_processes().GetThreadCategory(child_tid),
ActiveProcesses::Category::kOther);
}
// Tests that a process unrelated to the client is categorized as "other".
TEST_F(ActiveProcessesTest, UnrelatedProcess) {
// Add a process unrelated to the client -- in some other directory tree.
const auto [unrelated_pid, unrelated_tid] = AddUnrelated();
ASSERT_EQ(active_processes().GetThreadCategory(unrelated_tid),
ActiveProcesses::Category::kOther);
// Add the client.
const auto [client_pid, client_tid] = AddClient();
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
// The unrelated process is still categorized as "other".
ASSERT_EQ(active_processes().GetThreadCategory(unrelated_tid),
ActiveProcesses::Category::kOther);
}
// Tests that a process whose program resides within the directory tree of the
// client is categorized as a client proc.
TEST_F(ActiveProcessesTest, ProgramOfClient) {
// Add the client.
const auto [client_pid, client_tid] = AddClient();
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
// Add an independent process of the client -- in the same directory tree.
const auto [independent_pid, independent_tid] = AddIndependent();
// The independent is categorized as a Client process.
ASSERT_EQ(active_processes().GetThreadCategory(independent_tid),
ActiveProcesses::Category::kClient);
// The independent going away shouldn't change the categorization of the
// client.
active_processes().RemoveProcess(independent_pid);
ASSERT_EQ(active_processes().GetThreadCategory(independent_tid),
ActiveProcesses::Category::kOther);
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
}
// Tests that a process whose program resides within the directory tree of the
// client is categorized as a client proc once the client is added.
TEST_F(ActiveProcessesTest, ClientAfterProgram) {
// Add an independent process of the client -- in the same directory tree.
const auto [independent_pid, independent_tid] = AddIndependent();
// The client hasn't been added, so the independent is still "other".
ASSERT_EQ(active_processes().GetThreadCategory(independent_tid),
ActiveProcesses::Category::kOther);
// Add the client.
const auto [client_pid, client_tid] = AddClient();
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
// Now the independent is categorized as a Client process.
ASSERT_EQ(active_processes().GetThreadCategory(independent_tid),
ActiveProcesses::Category::kClient);
// When the client goes away, the independent is recategorized as "other".
active_processes().RemoveProcess(client_pid);
ASSERT_EQ(active_processes().GetThreadCategory(independent_tid),
ActiveProcesses::Category::kOther);
}
// Tests that a process matching the client, but running as another user, is
// categorized as "other".
TEST_F(ActiveProcessesTest, ClientOtherUser) {
// Add the client.
const auto [client_pid, client_tid] = AddClient();
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
// Add another instance of the client running as another user.
const ProcessId other_user_pid = client_pid + 1;
const ThreadId other_user_tid = client_tid + 1;
active_processes().AddProcess(
other_user_pid, /*parent_pid=*/0, /*session_id=*/4,
base::win::Sid::GenerateRandomSid(), client_image_file_name(),
client_command_line());
active_processes().AddThread(other_user_pid, other_user_tid,
/*thread_name=*/{});
// This process running as another user is categorized as "other".
ASSERT_EQ(active_processes().GetThreadCategory(other_user_tid),
ActiveProcesses::Category::kOther);
}
// Tests that a process matching the client, but running in another window
// session, is categorized as "other".
TEST_F(ActiveProcessesTest, ClientOtherSession) {
// Add the client.
const auto [client_pid, client_tid] = AddClient();
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
// Add another instance of the client running in a different session
const ProcessId other_session_pid = client_pid + 1;
const ThreadId other_session_tid = client_tid + 1;
active_processes().AddProcess(other_session_pid, /*parent_pid=*/0,
/*session_id=*/5, random_user_sid().Clone(),
client_image_file_name(),
client_command_line());
active_processes().AddThread(other_session_pid, other_session_tid,
/*thread_name=*/{});
// This process running in another session is categorized as "other".
ASSERT_EQ(active_processes().GetThreadCategory(other_session_tid),
ActiveProcesses::Category::kOther);
}
// Tests that a client that is external to the application is considered the
// client and that other processes belonging to the application are not.
TEST_F(ActiveProcessesTest, ExternalClient) {
// Add a client that does not reside in the application's directory.
const ProcessId client_pid = kBasePid;
const ThreadId client_tid = kBaseTid;
active_processes().AddProcess(
client_pid, /*parent_pid=*/0, /*session_id=*/4, random_user_sid().Clone(),
client_image_file_name(), external_client_command_line());
active_processes().AddThread(client_pid, client_tid, /*thread_name=*/{});
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
// If it launches a child of the application, those children are considered
// "other" with respect to the client.
const ProcessId child_pid = kBasePid + kChildOffset;
const ThreadId child_tid = kBaseTid + kChildOffset;
active_processes().AddProcess(
child_pid, client_pid, /*session_id=*/4, random_user_sid().Clone(),
client_image_file_name(), client_command_line());
active_processes().AddThread(child_pid, child_tid, /*thread_name=*/{});
ASSERT_EQ(active_processes().GetThreadCategory(child_tid),
ActiveProcesses::Category::kOther);
}
// Tests that a system process is categorized as "other".
TEST_F(ActiveProcessesTest, SystemProcess) {
// Any process running in session 0xFFFFFFFF is a system process.
const ProcessId system_pid = kBasePid + kSystemOffset;
const ThreadId system_tid = kBaseTid + kSystemOffset;
active_processes().AddProcess(system_pid, /*parent_pid=*/0,
/*session_id=*/0xFFFFFFFF,
base::win::Sid::GenerateRandomSid(), "system",
/*command_line=*/{});
active_processes().AddThread(system_pid, system_tid, /*thread_name=*/{});
ASSERT_EQ(active_processes().GetThreadCategory(system_tid),
ActiveProcesses::Category::kSystem);
}
// Tests that inconsistencies from lost events are handled gracefully.
TEST_F(ActiveProcessesTest, LostEvents) {
// Add the client, a child of it, and an unrelated process.
const auto [client_pid, client_tid] = AddClient();
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
const auto [child_pid, child_tid] = AddChild();
ASSERT_EQ(active_processes().GetThreadCategory(child_tid),
ActiveProcesses::Category::kClient);
const auto [unrelated_pid, unrelated_tid] = AddUnrelated();
ASSERT_EQ(active_processes().GetThreadCategory(unrelated_tid),
ActiveProcesses::Category::kOther);
// Removing an unknown tid from an unknown pid is a noop.
active_processes().RemoveThread(client_pid + 1, client_tid + 1);
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
ASSERT_EQ(active_processes().GetThreadCategory(child_tid),
ActiveProcesses::Category::kClient);
// Removing an unknown tid from a known pid is a noop.
active_processes().RemoveThread(client_pid, client_tid + 1);
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
ASSERT_EQ(active_processes().GetThreadCategory(child_tid),
ActiveProcesses::Category::kClient);
// Removing a known tid from an unknown pid is a noop.
active_processes().RemoveThread(client_pid + 1, client_tid);
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
ASSERT_EQ(active_processes().GetThreadCategory(child_tid),
ActiveProcesses::Category::kClient);
// Adding an existing tid to a different process removes the tid from the
// original.
active_processes().AddThread(client_pid, client_tid + 1, /*thread_name=*/{});
ASSERT_EQ(active_processes().GetThreadCategory(client_tid + 1),
ActiveProcesses::Category::kClient);
active_processes().AddThread(unrelated_pid, client_tid + 1,
/*thread_name=*/{});
ASSERT_EQ(active_processes().GetThreadCategory(client_tid + 1),
ActiveProcesses::Category::kOther);
// Removing a pid also removes its tids.
active_processes().RemoveProcess(client_pid);
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kOther);
}
// Tests that a second process added with the same pid as a child of the client
// leads to the child no longer categorized as a client proc.
TEST_F(ActiveProcessesTest, DuplicatePid) {
// Add the client and a child of it.
const auto [client_pid, client_tid] = AddClient();
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
const auto [child_pid, child_tid] = AddChild();
ASSERT_EQ(active_processes().GetThreadCategory(child_tid),
ActiveProcesses::Category::kClient);
// Imagine that events removing the child's thread and process are lost, and
// its pid is reused for a new process that is not related to the client.
const auto [unrelated_pid, unrelated_tid] = AddUnrelated(child_pid);
ASSERT_EQ(unrelated_pid, child_pid);
ASSERT_EQ(active_processes().GetThreadCategory(unrelated_tid),
ActiveProcesses::Category::kOther);
ASSERT_NE(unrelated_tid, child_tid);
// The tid of the replaced child is no longer considered related to the
// client.
ASSERT_EQ(active_processes().GetThreadCategory(child_tid),
ActiveProcesses::Category::kOther);
}
// Tests that a second process added with the same pid as the client leads to
// the client being forgotten.
TEST_F(ActiveProcessesTest, DuplicateClient) {
// Add the client.
const auto [client_pid, client_tid] = AddClient();
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
// Imagine a new process with the same pid as the client is added. This should
// not be possible in practice since the tracing service observes termination
// of the client and exits, but a shutdown race plus lost events could make an
// unlikely event possible. The new process should be considered as "other".
const auto [unrelated_pid, unrelated_tid] = AddUnrelated(client_pid);
ASSERT_EQ(unrelated_pid, client_pid);
ASSERT_EQ(active_processes().GetThreadCategory(unrelated_tid),
ActiveProcesses::Category::kOther);
// The tid of the replaced process is no longer considered the client.
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kOther);
}
// Tests that SetThreadName works in the general case, and in the face of lost
// events.
TEST_F(ActiveProcessesTest, SetThreadName) {
// Getters return empty view for unknown inputs, while setter does nothing.
ASSERT_EQ(active_processes().GetThreadName(3), L"");
active_processes().SetThreadName(/*pid=*/2, /*tid=*/3, L"Hi");
ASSERT_EQ(active_processes().GetThreadName(3), L"");
// Add the client and a child.
const auto [client_pid, client_tid] = AddClient();
ASSERT_EQ(active_processes().GetThreadCategory(client_tid),
ActiveProcesses::Category::kClient);
const auto [child_pid, child_tid] = AddChild();
ASSERT_EQ(active_processes().GetThreadCategory(child_tid),
ActiveProcesses::Category::kClient);
// Set works on valid inputs.
ASSERT_EQ(active_processes().GetThreadName(client_tid), std::wstring_view());
active_processes().SetThreadName(client_pid, client_tid, L"Hi");
ASSERT_EQ(active_processes().GetThreadName(client_tid), L"Hi");
// Set is ignored for pid/tid mismatch.
active_processes().SetThreadName(child_pid, client_tid, L"Bye");
ASSERT_EQ(active_processes().GetThreadName(client_tid), L"Hi");
// Set is ignore for invalid tid.
active_processes().SetThreadName(client_pid, client_tid + 1, L"Bye");
ASSERT_EQ(active_processes().GetThreadName(client_tid), L"Hi");
ASSERT_EQ(active_processes().GetThreadName(client_tid + 1), L"");
// Set is ignore for invalid pid.
active_processes().SetThreadName(client_pid + 1, client_tid, L"Bye");
ASSERT_EQ(active_processes().GetThreadName(client_tid), L"Hi");
}
} // namespace tracing