| // 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/errors.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/pseudo_dir.h> |
| #include <sdk/lib/vfs/cpp/vmo_file.h> |
| |
| namespace early_boot_instrumentation { |
| namespace { |
| |
| constexpr auto kFlags = fuchsia::io::OpenFlags::RIGHT_READABLE; |
| |
| // Serve vmos from arbitrary paths in the local namespace. |
| class FakeBootItemsFixture : public testing::Test { |
| public: |
| // Root path from where to serve the hierarchy. |
| 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(debugdata_dir_.Serve(kFlags, std::move(dir_server), loop_.dispatcher()), ZX_OK); |
| loop_.StartThread("kernel_data_dir"); |
| } |
| |
| void BindFile(std::string_view path) { BindHierarchy(debugdata_dir_, path); } |
| |
| 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: |
| // directory components end with '/' and all paths are relative (no leading /). |
| void BindHierarchy(vfs::PseudoDir& root, std::string_view path) { |
| auto curr = path.find("/"); |
| // path is a file to be bound. |
| if (curr == std::string_view::npos) { |
| zx::vmo path_vmo; |
| ASSERT_EQ(zx::vmo::create(4096, 0, &path_vmo), 0); |
| auto file = std::make_unique<vfs::VmoFile>(std::move(path_vmo), 4096); |
| ASSERT_EQ(root.AddEntry(std::string(path), std::move(file)), ZX_OK); |
| return; |
| } |
| |
| // curr is a directory, and we continue to bind directories and strip components. |
| std::string dir_name(path.substr(0, curr)); |
| path = path.substr(curr + 1, path.length()); |
| // see if dir exists. |
| vfs::Node* existing_entry = nullptr; |
| if (root.Lookup(dir_name, &existing_entry) == ZX_ERR_NOT_FOUND) { |
| std::unique_ptr<vfs::PseudoDir> new_dir = std::make_unique<vfs::PseudoDir>(); |
| existing_entry = new_dir.get(); |
| root.AddEntry(dir_name, std::move(new_dir)); |
| } |
| |
| ASSERT_NE(existing_entry, nullptr); |
| BindHierarchy(*reinterpret_cast<vfs::PseudoDir*>(existing_entry), path); |
| } |
| |
| async::Loop loop_ = async::Loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| vfs::PseudoDir debugdata_dir_; |
| |
| std::string path_; |
| }; |
| |
| using ExposeDebugdataTest = FakeBootItemsFixture; |
| |
| TEST_F(ExposeDebugdataTest, SingleSinkStatic) { |
| BindFile("random-sink/s/my-sink-data.my-data"); |
| ASSERT_NO_FATAL_FAILURE(Serve("/boot/kernel/i")); |
| |
| fbl::unique_fd debugdata_dir(open("/boot/kernel/i", O_RDONLY)); |
| ASSERT_TRUE(debugdata_dir) << strerror(errno); |
| |
| SinkDirMap sink_map; |
| |
| ASSERT_TRUE(ExposeBootDebugdata(debugdata_dir, sink_map).is_ok()); |
| vfs::PseudoDir* lookup = nullptr; |
| ASSERT_EQ(sink_map["random-sink"]->Lookup("static", reinterpret_cast<vfs::Node**>(&lookup)), |
| ZX_OK); |
| vfs::PseudoDir& out_dir = *lookup; |
| ASSERT_FALSE(out_dir.IsEmpty()); |
| |
| vfs::Node* node = nullptr; |
| ASSERT_EQ(out_dir.Lookup("my-sink-data.my-data", &node), ZX_OK); |
| ASSERT_NE(node, nullptr); |
| } |
| |
| TEST_F(ExposeDebugdataTest, SingleSinkDynamic) { |
| BindFile("random-sink/d/my-sink-data.my-data"); |
| ASSERT_NO_FATAL_FAILURE(Serve("/boot/kernel/i")); |
| |
| fbl::unique_fd debugdata_dir(open("/boot/kernel/i", O_RDONLY)); |
| ASSERT_TRUE(debugdata_dir) << strerror(errno); |
| |
| SinkDirMap sink_map; |
| |
| ASSERT_TRUE(ExposeBootDebugdata(debugdata_dir, sink_map).is_ok()); |
| vfs::PseudoDir* lookup = nullptr; |
| ASSERT_EQ(sink_map["random-sink"]->Lookup("dynamic", reinterpret_cast<vfs::Node**>(&lookup)), |
| ZX_OK); |
| vfs::PseudoDir& out_dir = *lookup; |
| ASSERT_FALSE(out_dir.IsEmpty()); |
| |
| vfs::Node* node = nullptr; |
| ASSERT_EQ(out_dir.Lookup("my-sink-data.my-data", &node), ZX_OK); |
| ASSERT_NE(node, nullptr); |
| } |
| |
| TEST_F(ExposeDebugdataTest, MultipleSinks) { |
| BindFile("random-sink/s/my-sink-data.my-data"); |
| BindFile("random-sink/d/my-dsink-data.my-data"); |
| BindFile("other-random-sink/s/my-other-sink-data.my-data"); |
| BindFile("other-random-sink/d/my-other-dsink-data.my-data"); |
| ASSERT_NO_FATAL_FAILURE(Serve("/boot/kernel/i")); |
| |
| fbl::unique_fd debugdata_dir(open("/boot/kernel/i", O_RDONLY)); |
| ASSERT_TRUE(debugdata_dir) << strerror(errno); |
| |
| SinkDirMap sink_map; |
| |
| ASSERT_TRUE(ExposeBootDebugdata(debugdata_dir, sink_map).is_ok()); |
| |
| std::vector<std::tuple<std::string, std::string, std::string>> lookup_entries = { |
| {"random-sink", "static", "my-sink-data.my-data"}, |
| {"random-sink", "dynamic", "my-dsink-data.my-data"}, |
| {"other-random-sink", "static", "my-other-sink-data.my-data"}, |
| {"other-random-sink", "dynamic", "my-other-dsink-data.my-data"}, |
| }; |
| |
| for (const auto& [sink, data_dir, file_name] : lookup_entries) { |
| vfs::PseudoDir* lookup = nullptr; |
| ASSERT_EQ(sink_map[sink]->Lookup(data_dir, reinterpret_cast<vfs::Node**>(&lookup)), ZX_OK); |
| vfs::PseudoDir& out_dir = *lookup; |
| ASSERT_FALSE(out_dir.IsEmpty()); |
| |
| vfs::Node* node = nullptr; |
| ASSERT_EQ(out_dir.Lookup(file_name, &node), ZX_OK); |
| ASSERT_NE(node, nullptr); |
| } |
| } |
| |
| 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::Node* lookup_node = nullptr; |
| ASSERT_EQ(sink_root.Lookup(path, &lookup_node), ZX_OK); |
| |
| 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(kData.size()); |
| ASSERT_EQ(vmo_file->vmo()->read(actual_data.data(), kDataOffset + i, kData.size()), 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()); |
| |
| auto [client_end, server_end] = fidl::Endpoints<fuchsia_io::Directory>::Create(); |
| |
| 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 |