[graphics] Start on conformance test for ICDs

Validates
* That the JSON Manifest is valid
* That the ICD's DT_NEEDED only contains allowlisted shared libraries.

Change-Id: I7a2f76f706ed3aa445444290dc25920c56a66179
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/693829
Commit-Queue: John Bauman <jbauman@google.com>
Reviewed-by: Casey Dahlin <sadmac@google.com>
Reviewed-by: Craig Stout <cstout@google.com>
diff --git a/src/graphics/tests/BUILD.gn b/src/graphics/tests/BUILD.gn
index 19875b2..e2ad95f 100644
--- a/src/graphics/tests/BUILD.gn
+++ b/src/graphics/tests/BUILD.gn
@@ -45,6 +45,7 @@
   testonly = true
   deps = [
     "common:vkcontext",
+    "icd_conformance",
     "icd_load",
     "libvulkan",
     "mali_vulkan_test",
diff --git a/src/graphics/tests/icd_conformance/BUILD.gn b/src/graphics/tests/icd_conformance/BUILD.gn
new file mode 100644
index 0000000..79da61f
--- /dev/null
+++ b/src/graphics/tests/icd_conformance/BUILD.gn
@@ -0,0 +1,55 @@
+# 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.
+
+import("//build/components.gni")
+import("//src/graphics/lib/magma/gnbuild/magma.gni")
+
+source_set("src") {
+  testonly = true
+
+  sources = [ "icd_conformance.cc" ]
+  public_deps = [
+    "//sdk/fidl/fuchsia.io:fuchsia.io_llcpp",
+    "//sdk/fidl/fuchsia.vulkan.loader:fuchsia.vulkan.loader_llcpp",
+    "//sdk/lib/fdio",
+    "//src/lib/elflib",
+    "//src/lib/files",
+    "//src/lib/fxl/test:gtest_main",
+    "//src/lib/fxl/test:test_settings",
+    "//src/lib/json_parser",
+    "//third_party/googletest:gtest",
+    "//zircon/system/ulib/fzl",
+    "//zircon/system/ulib/service:service-llcpp",
+  ]
+  public_deps += [ "//src/lib/vulkan" ]
+}
+
+executable("icd_conformance_bin") {
+  testonly = true
+  output_name = "icd_conformance"
+
+  deps = [ ":src" ]
+}
+
+fuchsia_component("icd_conformance_cmp") {
+  testonly = true
+  component_name = "icd_conformance"
+  deps = [ ":icd_conformance_bin" ]
+
+  manifest = "meta/icd_conformance.cml"
+}
+
+fuchsia_test_package("icd-conformance") {
+  package_name = "icd_conformance"
+  test_components = [ ":icd_conformance_cmp" ]
+  test_specs = {
+    # TODO(fxbug.dev/103295): Add aemu.
+    environments = magma_libvulkan_hardware_envs
+  }
+}
+
+group("icd_conformance") {
+  testonly = true
+  deps = [ ":icd-conformance" ]
+}
diff --git a/src/graphics/tests/icd_conformance/icd_conformance.cc b/src/graphics/tests/icd_conformance/icd_conformance.cc
new file mode 100644
index 0000000..c4b7461
--- /dev/null
+++ b/src/graphics/tests/icd_conformance/icd_conformance.cc
@@ -0,0 +1,148 @@
+// 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 <fidl/fuchsia.io/cpp/wire.h>
+#include <fidl/fuchsia.vulkan.loader/cpp/wire.h>
+#include <lib/fdio/namespace.h>
+#include <lib/fidl/llcpp/connect_service.h>
+#include <lib/fit/defer.h>
+#include <lib/fzl/vmo-mapper.h>
+#include <lib/service/llcpp/service.h>
+
+#include <set>
+
+#include <gtest/gtest.h>
+
+#include "rapidjson/prettywriter.h"
+#include "rapidjson/schema.h"
+#include "src/lib/elflib/elflib.h"
+#include "src/lib/files/directory.h"
+#include "src/lib/json_parser/json_parser.h"
+#include "src/lib/json_parser/pretty_print.h"
+
+namespace {
+const char* kManifestFsPath = "/manifestfs";
+
+const char* kManifestSchema = R"(
+{
+  "$schema":"http://json-schema.org/schema#",
+  "type":"object",
+  "properties":{
+    "file_format_version":{
+      "type":"string"
+    },
+    "ICD":{
+      "type":"object",
+      "properties":{
+        "library_path":{
+          "type":"string"
+        },
+        "api_version":{
+          "type":"string"
+        }
+      },
+      "required":[
+        "library_path",
+        "api_version"
+      ]
+    }
+  },
+  "required":[
+    "file_format_version",
+    "ICD"
+  ]
+}
+)";
+
+void ValidateSharedObject(const zx::vmo& vmo) {
+  fzl::VmoMapper mapper;
+  ASSERT_EQ(ZX_OK, mapper.Map(vmo, 0, 0, ZX_VM_PERM_READ));
+
+  std::unique_ptr<elflib::ElfLib> lib =
+      elflib::ElfLib::Create(reinterpret_cast<uint8_t*>(mapper.start()), mapper.size());
+  ASSERT_TRUE(lib);
+
+  auto deps = lib->GetSharedObjectDependencies();
+
+  ASSERT_TRUE(deps);
+  const std::set<std::string> kSoAllowlist{"libzircon.so", "libc.so"};
+
+  // Validate all needed shared libraries against allowlist.
+  for (const std::string& dep : *deps) {
+    EXPECT_TRUE(kSoAllowlist.count(dep)) << "Disallowed library: " << dep;
+  }
+  EXPECT_LT(0u, deps->size());
+
+  auto warnings = lib->GetAndClearWarnings();
+
+  for (const std::string& warning : warnings) {
+    ADD_FAILURE() << warning;
+  }
+}
+
+bool ValidateManifestJson(const rapidjson::GenericDocument<rapidjson::UTF8<>>& doc) {
+  rapidjson::Document schema_doc;
+  schema_doc.Parse(kManifestSchema);
+  EXPECT_FALSE(schema_doc.HasParseError()) << schema_doc.GetParseError();
+
+  rapidjson::SchemaDocument schema(schema_doc);
+  rapidjson::SchemaValidator validator(schema);
+  if (!doc.Accept(validator)) {
+    rapidjson::StringBuffer sb;
+    rapidjson::PrettyWriter<rapidjson::StringBuffer> w(sb);
+    validator.GetError().Accept(w);
+    ADD_FAILURE() << "manifest.json failed validation " << sb.GetString();
+    return false;
+  }
+  return true;
+}
+
+void ValidateIcd(fidl::WireSyncClient<fuchsia_vulkan_loader::Loader>& loader,
+                 const std::string& manifest_filename) {
+  json_parser::JSONParser manifest_parser;
+  auto manifest_doc =
+      manifest_parser.ParseFromFile(std::string(kManifestFsPath) + "/" + manifest_filename);
+
+  ASSERT_FALSE(manifest_parser.HasError()) << manifest_parser.error_str();
+
+  ASSERT_TRUE(ValidateManifestJson(manifest_doc));
+
+  std::string library_path = manifest_doc["ICD"].GetObject()["library_path"].GetString();
+
+  auto res = loader->Get(fidl::StringView::FromExternal(library_path));
+  ASSERT_EQ(res.status(), ZX_OK);
+  zx::vmo vmo = std::move(res->lib);
+  ValidateSharedObject(vmo);
+}
+
+TEST(IcdConformance, SharedLibraries) {
+  auto svc = service::OpenServiceRoot();
+  auto client_end = service::ConnectAt<fuchsia_vulkan_loader::Loader>(*svc);
+
+  auto client = fidl::BindSyncClient(std::move(*client_end));
+
+  auto manifest_fs_endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>();
+  ASSERT_EQ(ZX_OK, manifest_fs_endpoints.status_value());
+  auto manifest_result =
+      client->ConnectToManifestFs(fuchsia_vulkan_loader::ConnectToManifestOptions::kWaitForIdle,
+                                  manifest_fs_endpoints->server.TakeChannel());
+  EXPECT_EQ(ZX_OK, manifest_result.status());
+
+  fdio_ns_t* ns;
+  EXPECT_EQ(ZX_OK, fdio_ns_get_installed(&ns));
+  EXPECT_EQ(ZX_OK, fdio_ns_bind(ns, kManifestFsPath,
+                                manifest_fs_endpoints->client.TakeChannel().release()));
+  auto defer_unbind = fit::defer([&]() { fdio_ns_unbind(ns, kManifestFsPath); });
+
+  std::vector<std::string> manifests;
+  EXPECT_TRUE(files::ReadDirContents(kManifestFsPath, &manifests));
+
+  for (auto& manifest : manifests) {
+    if (manifest == ".")
+      continue;
+    SCOPED_TRACE(manifest);
+    ValidateIcd(client, manifest);
+  }
+}
+}  // namespace
diff --git a/src/graphics/tests/icd_conformance/meta/icd_conformance.cml b/src/graphics/tests/icd_conformance/meta/icd_conformance.cml
new file mode 100644
index 0000000..7b927b5
--- /dev/null
+++ b/src/graphics/tests/icd_conformance/meta/icd_conformance.cml
@@ -0,0 +1,14 @@
+// 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/lib/vulkan/vulkan-test.shard.cml",
+        "//src/sys/test_runners/gtest/default.shard.cml",
+        "syslog/client.shard.cml",
+        "vulkan/client.shard.cml",
+    ],
+    program: {
+        binary: "bin/icd_conformance",
+    },
+}
diff --git a/src/lib/elflib/elflib.cc b/src/lib/elflib/elflib.cc
index f0358ca..401f612 100644
--- a/src/lib/elflib/elflib.cc
+++ b/src/lib/elflib/elflib.cc
@@ -991,6 +991,40 @@
                                           .size = *dynstr_.size});
 }
 
+std::optional<std::vector<std::string>> ElfLib::GetSharedObjectDependencies() {
+  LoadProgramHeaders();
+
+  std::vector<std::string> output;
+
+  for (size_t idx = 0; idx < segments_.size(); idx++) {
+    if (segments_[idx].p_type != PT_DYNAMIC) {
+      continue;
+    }
+
+    auto data = GetSegmentData(idx);
+
+    if (!data.ptr) {
+      return std::nullopt;
+    }
+
+    const Elf64_Dyn* start = reinterpret_cast<const Elf64_Dyn*>(data.ptr);
+    const Elf64_Dyn* end = start + (data.size / sizeof(Elf64_Dyn));
+
+    for (auto dyn = start; dyn != end; dyn++) {
+      if (dyn->d_tag == DT_NEEDED) {
+        auto string = GetDynamicString(dyn->d_un.d_val);
+        if (!string) {
+          Warn("Invalid symbol table string");
+          continue;
+        }
+        output.push_back(std::move(*string));
+      }
+    }
+  }
+
+  return output;
+}
+
 bool ElfLib::ProbeHasDebugInfo() {
   if (!header_.e_shnum) {
     // No sections, no debug info.
diff --git a/src/lib/elflib/elflib.h b/src/lib/elflib/elflib.h
index 97387b6..ffc957f 100644
--- a/src/lib/elflib/elflib.h
+++ b/src/lib/elflib/elflib.h
@@ -92,6 +92,8 @@
   // ".dynsym" section.
   std::optional<std::map<std::string, Elf64_Sym>> GetAllDynamicSymbols();
 
+  std::optional<std::vector<std::string>> GetSharedObjectDependencies();
+
   // Attempt to discern whether this file has debug symbols (otherwise it is
   // presumably stripped).
   //
diff --git a/src/lib/elflib/elflib_fuzztest.cc b/src/lib/elflib/elflib_fuzztest.cc
index 49e71bc..6048dc2 100644
--- a/src/lib/elflib/elflib_fuzztest.cc
+++ b/src/lib/elflib/elflib_fuzztest.cc
@@ -17,6 +17,7 @@
     elf->GetAllSymbols();
     elf->GetAllDynamicSymbols();
     elf->GetPLTOffsets();
+    elf->GetSharedObjectDependencies();
     elf->GetAndClearWarnings();
   }
   return 0;
diff --git a/src/lib/elflib/elflib_unittest.cc b/src/lib/elflib/elflib_unittest.cc
index 9aa839e..f5fff7b 100644
--- a/src/lib/elflib/elflib_unittest.cc
+++ b/src/lib/elflib/elflib_unittest.cc
@@ -451,5 +451,19 @@
   ASSERT_TRUE(warnings.empty());
 }
 
+TEST(ElfLib, GetSharedObjectDependencies) {
+  std::unique_ptr<ElfLib> elf =
+      ElfLib::Create(GetTestBinaryPath(std::string(kStrippedExampleFile)));
+  ASSERT_NE(elf.get(), nullptr);
+
+  auto objects = elf->GetSharedObjectDependencies();
+  ASSERT_TRUE(objects);
+  auto& object_vec = objects.value();
+  ASSERT_EQ(object_vec.size(), 3u);
+  EXPECT_EQ(object_vec[0], "libclang_rt.scudo.so");
+  EXPECT_EQ(object_vec[1], "libfdio.so");
+  EXPECT_EQ(object_vec[2], "libc.so");
+}
+
 }  // namespace
 }  // namespace elflib