blob: b91a0d7c359f9d6089ff1e5b5bab1dad411251c1 [file]
// Copyright 2022 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/sys/early_boot_instrumentation/coverage_source.h"
#include <fcntl.h>
#include <fidl/fuchsia.boot/cpp/markers.h>
#include <fidl/fuchsia.boot/cpp/wire.h>
#include <fidl/fuchsia.debugdata/cpp/wire.h>
#include <fidl/fuchsia.io/cpp/markers.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/fdio/namespace.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/fidl/cpp/wire/connect_service.h>
#include <lib/fidl/cpp/wire/internal/transport_channel.h>
#include <lib/fidl/cpp/wire/message_storage.h>
#include <lib/fidl/cpp/wire/string_view.h>
#include <lib/stdcompat/array.h>
#include <lib/stdcompat/span.h>
#include <lib/zx/channel.h>
#include <lib/zx/event.h>
#include <lib/zx/eventpair.h>
#include <lib/zx/vmo.h>
#include <stdio.h>
#include <sys/stat.h>
#include <zircon/syscalls/object.h>
#include <cstdint>
#include <memory>
#include <string_view>
#include <fbl/unique_fd.h>
#include <gtest/gtest.h>
#include <sdk/lib/vfs/cpp/flags.h>
#include <sdk/lib/vfs/cpp/pseudo_dir.h>
#include <sdk/lib/vfs/cpp/vmo_file.h>
namespace early_boot_instrumentation {
namespace {
constexpr auto kFlags = fuchsia::io::OpenFlags::RIGHT_READABLE;
zx_koid_t GetKoid(zx_handle_t handle) {
zx_info_handle_basic_t info;
zx_status_t status =
zx_object_get_info(handle, ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr);
return status == ZX_OK ? info.koid : ZX_KOID_INVALID;
}
// Serve the vmos from /somepath/kernel/vmofile.name.
class FakeBootItemsFixture : public testing::Test {
public:
void Serve(const std::string& path) {
zx::channel dir_server, dir_client;
ASSERT_EQ(zx::channel::create(0, &dir_server, &dir_client), 0);
fdio_ns_t* root_ns = nullptr;
path_ = path;
ASSERT_EQ(fdio_ns_get_installed(&root_ns), ZX_OK);
ASSERT_EQ(fdio_ns_bind(root_ns, path.c_str(), dir_client.release()), ZX_OK);
ASSERT_EQ(kernel_dir_.Serve(kFlags, std::move(dir_server), loop_.dispatcher()), ZX_OK);
loop_.StartThread("kernel_data_dir");
}
void BindFile(std::string_view path) {
zx::vmo path_vmo;
ASSERT_EQ(zx::vmo::create(4096, 0, &path_vmo), 0);
zx_koid_t koid = GetKoid(path_vmo.get());
ASSERT_NE(koid, ZX_KOID_INVALID);
auto str_path = std::string(path);
path_to_koid_[str_path] = koid;
auto file = std::make_unique<vfs::VmoFile>(std::move(path_vmo), 4096);
ASSERT_EQ(kernel_dir_.AddEntry(str_path, std::move(file)), ZX_OK);
}
void TearDown() override {
// Best effort.
fdio_ns_t* root_ns = nullptr;
ASSERT_EQ(fdio_ns_get_installed(&root_ns), ZX_OK);
fdio_ns_unbind(root_ns, path_.c_str());
loop_.Shutdown();
}
private:
async::Loop loop_ = async::Loop(&kAsyncLoopConfigNoAttachToCurrentThread);
vfs::PseudoDir kernel_dir_;
std::map<std::string, zx_koid_t> path_to_koid_;
std::string path_;
};
using ExposeKernelProfileDataTest = FakeBootItemsFixture;
using ExposePhysbootProfileDataTest = FakeBootItemsFixture;
TEST_F(ExposeKernelProfileDataTest, WithSymbolizerLogExposesBoth) {
BindFile("zircon.elf.profraw");
BindFile("symbolizer.log");
ASSERT_NO_FATAL_FAILURE(Serve("/boot/kernel/data"));
fbl::unique_fd kernel_data_dir(open("/boot/kernel/data", O_RDONLY));
ASSERT_TRUE(kernel_data_dir) << strerror(errno);
SinkDirMap sink_map;
zx::result result = ExposeKernelProfileData(kernel_data_dir, sink_map);
ASSERT_TRUE(result.is_ok()) << result.status_string();
vfs::PseudoDir* lookup = nullptr;
ASSERT_EQ(
sink_map["llvm-profile"]->Lookup("dynamic", reinterpret_cast<vfs::internal::Node**>(&lookup)),
ZX_OK);
vfs::PseudoDir& out_dir = *lookup;
ASSERT_FALSE(out_dir.IsEmpty());
std::string kernel_file(kKernelFile);
vfs::internal::Node* node = nullptr;
ASSERT_EQ(out_dir.Lookup(kernel_file, &node), ZX_OK);
ASSERT_NE(node, nullptr);
node = nullptr;
std::string symbolizer_file(kKernelSymbolizerFile);
ASSERT_EQ(out_dir.Lookup(symbolizer_file, &node), ZX_OK);
ASSERT_NE(node, nullptr);
}
TEST_F(ExposeKernelProfileDataTest, OnlyKernelFileIsOk) {
// Dispatcher
BindFile("zircon.elf.profraw");
ASSERT_NO_FATAL_FAILURE(Serve("/boot/kernel/data"));
fbl::unique_fd kernel_data_dir(open("/boot/kernel/data", O_RDONLY));
ASSERT_TRUE(kernel_data_dir) << strerror(errno);
SinkDirMap sink_map;
zx::result result = ExposeKernelProfileData(kernel_data_dir, sink_map);
ASSERT_TRUE(result.is_ok()) << result.status_string();
vfs::PseudoDir* lookup = nullptr;
ASSERT_EQ(
sink_map["llvm-profile"]->Lookup("dynamic", reinterpret_cast<vfs::internal::Node**>(&lookup)),
ZX_OK);
vfs::PseudoDir& out_dir = *lookup;
ASSERT_FALSE(out_dir.IsEmpty());
std::string kernel_file(kKernelFile);
vfs::internal::Node* node = nullptr;
ASSERT_EQ(out_dir.Lookup(kernel_file, &node), ZX_OK);
ASSERT_NE(node, nullptr);
node = nullptr;
std::string symbolizer_file(kKernelSymbolizerFile);
ASSERT_NE(out_dir.Lookup(symbolizer_file, &node), ZX_OK);
}
TEST_F(ExposeKernelProfileDataTest, KernelProfrawFileDoesNotExist) {
// Don't bind profraw file to the directory
ASSERT_NO_FATAL_FAILURE(Serve("/boot/kernel/data"));
fbl::unique_fd kernel_data_dir(open("/boot/kernel/data", O_RDONLY));
ASSERT_TRUE(kernel_data_dir) << strerror(errno);
SinkDirMap sink_map;
zx::result result = ExposeKernelProfileData(kernel_data_dir, sink_map);
// We don't fail when profraw file does not exist.
ASSERT_TRUE(result.is_ok()) << result.status_string();
ASSERT_EQ(sink_map.find("llvm-profile"), sink_map.end());
}
TEST_F(ExposePhysbootProfileDataTest, WithSymbolizerFileIsOk) {
// Dispatcher
BindFile("physboot.profraw");
BindFile("symbolizer.log");
ASSERT_NO_FATAL_FAILURE(Serve("/boot/kernel/data/phys"));
fbl::unique_fd kernel_data_dir(open("/boot/kernel/data/phys", O_RDONLY));
ASSERT_TRUE(kernel_data_dir) << strerror(errno);
SinkDirMap sink_map;
zx::result result = ExposePhysbootProfileData(kernel_data_dir, sink_map);
ASSERT_TRUE(result.is_ok()) << result.status_string();
vfs::PseudoDir* lookup = nullptr;
ASSERT_EQ(
sink_map["llvm-profile"]->Lookup("static", reinterpret_cast<vfs::internal::Node**>(&lookup)),
ZX_OK);
vfs::PseudoDir& out_dir = *lookup;
ASSERT_FALSE(out_dir.IsEmpty());
std::string phys_file(kPhysFile);
vfs::internal::Node* node = nullptr;
ASSERT_EQ(out_dir.Lookup(phys_file, &node), ZX_OK);
ASSERT_NE(node, nullptr);
node = nullptr;
std::string symbolizer_file(kPhysSymbolizerFile);
ASSERT_EQ(out_dir.Lookup(symbolizer_file, &node), ZX_OK);
ASSERT_NE(node, nullptr);
}
TEST_F(ExposePhysbootProfileDataTest, OnlyProfrawFileIsOk) {
// Dispatcher
BindFile("physboot.profraw");
ASSERT_NO_FATAL_FAILURE(Serve("/boot/kernel/data/phys"));
fbl::unique_fd kernel_data_dir(open("/boot/kernel/data/phys", O_RDONLY));
ASSERT_TRUE(kernel_data_dir) << strerror(errno);
SinkDirMap sink_map;
zx::result result = ExposePhysbootProfileData(kernel_data_dir, sink_map);
ASSERT_TRUE(result.is_ok()) << result.status_string();
vfs::PseudoDir* lookup = nullptr;
ASSERT_EQ(
sink_map["llvm-profile"]->Lookup("static", reinterpret_cast<vfs::internal::Node**>(&lookup)),
ZX_OK);
vfs::PseudoDir& out_dir = *lookup;
ASSERT_FALSE(out_dir.IsEmpty());
std::string phys_file(kPhysFile);
vfs::internal::Node* node = nullptr;
ASSERT_EQ(out_dir.Lookup(phys_file, &node), ZX_OK);
ASSERT_NE(node, nullptr);
node = nullptr;
std::string symbolizer_file(kPhysSymbolizerFile);
ASSERT_NE(out_dir.Lookup(symbolizer_file, &node), ZX_OK);
}
TEST_F(ExposeKernelProfileDataTest, PhysbootProfrawFileDoesNotExist) {
// Don't bind profraw file to the directory
ASSERT_NO_FATAL_FAILURE(Serve("/boot/kernel/data/phys"));
fbl::unique_fd kernel_data_dir(open("/boot/kernel/data/phys", O_RDONLY));
ASSERT_TRUE(kernel_data_dir) << strerror(errno);
SinkDirMap sink_map;
zx::result result = ExposePhysbootProfileData(kernel_data_dir, sink_map);
// We don't fail when profraw file does not exist.
ASSERT_TRUE(result.is_ok()) << result.status_string();
ASSERT_EQ(sink_map.find("llvm-profile"), sink_map.end());
}
struct PublishRequest {
std::string sink;
bool peer_closed;
};
constexpr std::string_view kData = "12345670123";
constexpr size_t kDataOffset = 0xAD;
zx::result<zx::vmo> MakeTestVmo(uint32_t data_offset) {
zx::vmo vmo;
if (auto status = zx::vmo::create(4096, 0, &vmo); status != ZX_OK) {
return zx::error(status);
}
if (auto status = vmo.write(kData.data(), kDataOffset + data_offset, kDataOffset);
status != ZX_OK) {
return zx::error(status);
}
return zx::ok(std::move(vmo));
}
void ValidatePublishedRequests(uint32_t svc_index, cpp20::span<PublishRequest> requests,
SinkDirMap& sink_map) {
for (uint32_t i = 0; i < requests.size(); ++i) {
std::string path(requests[i].peer_closed ? kStaticDir : kDynamicDir);
std::string name = std::to_string(svc_index) + "-" + std::to_string(i);
if (requests[i].sink == kLlvmSink) {
name += "." + std::string(kLlvmSinkExtension);
}
auto it = sink_map.find(requests[i].sink);
ASSERT_NE(it, sink_map.end());
auto& sink_root = *it->second;
vfs::internal::Node* lookup_node = nullptr;
ASSERT_EQ(sink_root.Lookup(path, &lookup_node), ZX_OK);
ASSERT_TRUE(lookup_node->IsDirectory());
auto* typed_dir = reinterpret_cast<vfs::PseudoDir*>(lookup_node);
ASSERT_EQ(typed_dir->Lookup(name, &lookup_node), ZX_OK) << name;
auto* vmo_file = reinterpret_cast<vfs::VmoFile*>(lookup_node);
std::vector<uint8_t> actual_data;
ASSERT_EQ(vmo_file->ReadAt(kData.size(), kDataOffset + i, &actual_data), ZX_OK);
EXPECT_TRUE(memcmp(kData.data(), actual_data.data(), kData.size()) == 0);
}
}
void ValidatePublishedRequests(uint32_t svc_index, PublishRequest& request, SinkDirMap& sink_map) {
ValidatePublishedRequests(svc_index, {&request, 1}, sink_map);
}
class ExtractDebugDataTest : public ::testing::Test {
public:
void SetUp() final {
zx::result client_end = fidl::CreateEndpoints(&svc_stash_read_);
ASSERT_TRUE(client_end.is_ok()) << client_end.status_string();
svc_stash_.Bind(std::move(client_end.value()));
}
void StashSvcWithPublishedData(const PublishRequest& publish_info, zx::eventpair& out_token) {
StashSvcWithPublishedData({&publish_info, 1}, {&out_token, 1});
}
// Same as above, but published multiple pairs of |<sink, vmo>| represented by sinks[i],
// vmos[i]. |out| is the write end of the handle.
void StashSvcWithPublishedData(cpp20::span<const PublishRequest> publish_info,
cpp20::span<zx::eventpair> out_tokens) {
ASSERT_EQ(publish_info.size(), out_tokens.size());
zx::result endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>();
ASSERT_TRUE(endpoints.is_ok()) << endpoints.status_string();
auto& [client_end, server_end] = endpoints.value();
const fidl::OneWayStatus status = svc_stash_->Store(std::move(server_end));
ASSERT_TRUE(status.ok()) << status.FormatDescription();
for (uint32_t i = 0; i < publish_info.size(); ++i) {
zx::result vmo_or = MakeTestVmo(i);
ASSERT_TRUE(vmo_or.is_ok()) << vmo_or.status_string();
if (publish_info[i].sink == kLlvmSink) {
vmo_or.value().set_property(ZX_PROP_NAME, kLlvmSinkExtension.data(),
kLlvmSinkExtension.size());
}
PublishOne(client_end, publish_info[i].sink, std::move(vmo_or).value(), out_tokens[i]);
if (publish_info[i].peer_closed) {
out_tokens[i].reset();
}
}
}
auto&& take_stash_read() { return std::move(svc_stash_read_); }
private:
static void PublishOne(const fidl::ClientEnd<fuchsia_io::Directory>& directory,
std::string_view sink_name, zx::vmo vmo, zx::eventpair& out_token) {
zx::eventpair token1, token2;
ASSERT_EQ(zx::eventpair::create(0, &token1, &token2), ZX_OK);
zx::result client_end = component::ConnectAt<fuchsia_debugdata::Publisher>(directory);
ASSERT_TRUE(client_end.is_ok()) << client_end.status_string();
fidl::WireSyncClient client(std::move(client_end.value()));
const fidl::OneWayStatus result = client->Publish(fidl::StringView::FromExternal(sink_name),
std::move(vmo), std::move(token1));
ASSERT_TRUE(result.ok()) << result.FormatDescription();
out_token = std::move(token2);
}
fidl::ServerEnd<fuchsia_boot::SvcStash> svc_stash_read_;
fidl::WireSyncClient<fuchsia_boot::SvcStash> svc_stash_;
};
TEST_F(ExtractDebugDataTest, NoRequestsIsEmpty) {
auto svc_stash = take_stash_read();
auto sink_map = ExtractDebugData(std::move(svc_stash));
ASSERT_TRUE(sink_map.empty());
}
TEST_F(ExtractDebugDataTest, SingleStashedSvcWithSingleOutstandingPublishRequest) {
auto svc_stash = take_stash_read();
PublishRequest req = {"my-custom-sink", true};
zx::eventpair token;
ASSERT_NO_FATAL_FAILURE(StashSvcWithPublishedData(req, token));
auto sink_map = ExtractDebugData(std::move(svc_stash));
ASSERT_FALSE(sink_map.empty());
ValidatePublishedRequests(0u, req, sink_map);
}
TEST_F(ExtractDebugDataTest, LlvmSinkHaveProfrawExtension) {
auto svc_stash = take_stash_read();
auto reqs = cpp20::to_array<PublishRequest>(
{{std::string(kLlvmSink), true}, {std::string(kLlvmSink), false}});
std::vector<zx::eventpair> tokens;
tokens.resize(reqs.size());
ASSERT_NO_FATAL_FAILURE(StashSvcWithPublishedData(reqs, tokens));
auto sink_map = ExtractDebugData(std::move(svc_stash));
ASSERT_FALSE(sink_map.empty());
ValidatePublishedRequests(0u, reqs, sink_map);
}
TEST_F(ExtractDebugDataTest, SingleStashedSvcWithMultipleOutstandingPublishRequest) {
auto svc_stash = take_stash_read();
auto reqs = cpp20::to_array<PublishRequest>(
{{"my-custom-sink", true}, {"another-sink", true}, {"my-custom-sink", false}});
std::vector<zx::eventpair> tokens;
tokens.resize(reqs.size());
ASSERT_NO_FATAL_FAILURE(StashSvcWithPublishedData(reqs, tokens));
auto sink_map = ExtractDebugData(std::move(svc_stash));
ASSERT_FALSE(sink_map.empty());
ValidatePublishedRequests(0u, reqs, sink_map);
}
TEST_F(ExtractDebugDataTest, MultipleStashedSvcWithSingleOutstandingPublishRequest) {
auto svc_stash = take_stash_read();
auto reqs = cpp20::to_array<PublishRequest>(
{{"my-custom-sink", true}, {"another-sink", true}, {"my-custom-sink", false}});
std::vector<zx::eventpair> tokens;
tokens.resize(reqs.size());
for (uint32_t i = 0; i < reqs.size(); ++i) {
ASSERT_NO_FATAL_FAILURE(StashSvcWithPublishedData(reqs[i], tokens[i]));
}
auto sink_map = ExtractDebugData(std::move(svc_stash));
ASSERT_FALSE(sink_map.empty());
for (uint32_t i = 0; i < reqs.size(); ++i) {
ValidatePublishedRequests(i, reqs[i], sink_map);
}
}
} // namespace
} // namespace early_boot_instrumentation