| // Copyright 2021 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/graphics/bin/vulkan_loader/icd_component.h" |
| |
| #include <fidl/fuchsia.component.decl/cpp/wire.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fdio/io.h> |
| #include <lib/fit/defer.h> |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <fbl/unique_fd.h> |
| |
| #include "rapidjson/prettywriter.h" |
| #include "rapidjson/schema.h" |
| #include "src/graphics/bin/vulkan_loader/app.h" |
| #include "src/lib/files/file.h" |
| #include "src/lib/json_parser/json_parser.h" |
| #include "src/lib/json_parser/pretty_print.h" |
| |
| namespace { |
| |
| const char* kSchema = R"( |
| { |
| "$schema": "http://json-schema.org/schema#", |
| "type": "object", |
| "properties": { |
| "version": {"type":"number", "maximum": 1, "minimum": 1}, |
| "file_path": {"type":"string"}, |
| "manifest_path": {"type":"string"} |
| }, |
| "required": ["version", "file_path", "manifest_path"] |
| } |
| )"; |
| |
| 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" |
| ] |
| } |
| )"; |
| |
| const char* kCollectionName = "icd-loaders"; |
| |
| } // namespace |
| |
| IcdComponent::IcdComponent(LoaderApp* app, fidl::ClientEnd<fuchsia_component::Realm> realm, |
| std::string component_url) |
| : app_(app), |
| realm_(std::move(realm), app->dispatcher()), |
| component_url_(std::move(component_url)) {} |
| |
| IcdComponent::~IcdComponent() { |
| RemoveManifestFromFs(); |
| if (realm_ && !child_instance_name_.empty()) { |
| fuchsia_component_decl::wire::ChildRef child_ref{ |
| .name = fidl::StringView::FromExternal(child_instance_name_), |
| .collection = fidl::StringView::FromExternal(kCollectionName), |
| }; |
| auto result = realm_.sync()->DestroyChild(child_ref); |
| if (!result.ok()) { |
| FX_LOGS(ERROR) << child_instance_name_ << " DestroyChild transport error: " << result; |
| } else if (result->is_error()) { |
| FX_LOGS(ERROR) << child_instance_name_ |
| << " DestroyChild error: " << static_cast<uint32_t>(result->error_value()); |
| } |
| } |
| } |
| |
| void IcdComponent::AddManifestToFs() { |
| assert(manifest_file_); |
| std::optional<std::string> manifest_file_name = GetManifestFileName(); |
| assert(manifest_file_name); |
| app_->manifest_fs_root_node()->AddEntry(*manifest_file_name, manifest_file_); |
| } |
| |
| void IcdComponent::RemoveManifestFromFs() { |
| if (!manifest_file_) |
| return; |
| std::optional<std::string> manifest_file_name = GetManifestFileName(); |
| if (manifest_file_name) { |
| app_->manifest_fs_root_node()->RemoveEntry(*manifest_file_name, manifest_file_.get()); |
| } |
| } |
| |
| void IcdComponent::Initialize(inspect::Node* parent_node) { |
| static uint64_t name_id; |
| auto pending_action_token = app_->GetPendingActionToken(); |
| |
| child_instance_name_ = std::to_string(name_id++); |
| node_ = parent_node->CreateChild(child_instance_name_); |
| initialization_status_ = node_.CreateString("status", "uninitialized"); |
| node_.CreateString("component_url", component_url_, &value_list_); |
| fidl::Arena arena; |
| fuchsia_component_decl::wire::CollectionRef collection{ |
| .name = fidl::StringView::FromExternal(kCollectionName), |
| }; |
| fuchsia_component_decl::wire::Child decl = |
| fuchsia_component_decl::wire::Child::Builder(arena) |
| .name(child_instance_name_) |
| .url(component_url_) |
| .startup(fuchsia_component_decl::wire::StartupMode::kLazy) |
| .Build(); |
| |
| auto failure_callback = |
| fit::defer_callback([this, pending_action_token = std::move(pending_action_token)]() { |
| std::lock_guard lock(vmo_lock_); |
| stage_ = LookupStages::kFailed; |
| app_->NotifyIcdsChanged(); |
| }); |
| |
| realm_->CreateChild(collection, decl, fuchsia_component::wire::CreateChildArgs()) |
| .Then([this, failure_callback = std::move(failure_callback)]( |
| fidl::WireUnownedResult<fuchsia_component::Realm::CreateChild>& result) mutable { |
| if (!result.ok()) { |
| FX_LOGS(ERROR) << component_url_ << " CreateChild transport error: " << result; |
| node_.CreateUint("create_response", static_cast<uint32_t>(result.status()), &value_list_); |
| child_instance_name_ = ""; |
| return; |
| } |
| if (result->is_error()) { |
| FX_LOGS(ERROR) << component_url_ |
| << " CreateChild error: " << static_cast<uint32_t>(result->error_value()); |
| node_.CreateUint("create_response", static_cast<uint32_t>(result->error_value()), |
| &value_list_); |
| child_instance_name_ = ""; |
| return; |
| } |
| |
| initialization_status_.Set("created"); |
| |
| fuchsia_component_decl::wire::ChildRef child_ref{ |
| .name = fidl::StringView::FromExternal(child_instance_name_), |
| .collection = fidl::StringView::FromExternal(kCollectionName), |
| }; |
| |
| auto endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>(); |
| if (endpoints.is_error()) { |
| FX_LOGS(ERROR) << "Failed to create endpoints: " << endpoints.status_string(); |
| return; |
| } |
| |
| realm_->OpenExposedDir(child_ref, std::move(endpoints->server)) |
| .Then([this, client_end = std::move(endpoints->client), |
| failure_callback = std::move(failure_callback)]( |
| fidl::WireUnownedResult<fuchsia_component::Realm::OpenExposedDir>& |
| result) mutable { |
| if (!result.ok()) { |
| FX_LOGS(ERROR) << component_url_ << " OpenExposedDir transport error: " << result; |
| node_.CreateUint("bind_response", static_cast<uint32_t>(result.status()), |
| &value_list_); |
| return; |
| } |
| if (result->is_error()) { |
| FX_LOGS(ERROR) << component_url_ << " OpenExposedDir error: " |
| << static_cast<uint32_t>(result->error_value()); |
| node_.CreateUint("bind_response", static_cast<uint32_t>(result->error_value()), |
| &value_list_); |
| return; |
| } |
| |
| initialization_status_.Set("bound"); |
| async::PostTask( |
| app_->fdio_loop_dispatcher(), [this, shared_this = this->shared_from_this(), |
| failure_callback = std::move(failure_callback), |
| directory = std::move(client_end)]() mutable { |
| ReadFromComponent(std::move(failure_callback), std::move(directory)); |
| }); |
| }); |
| }); |
| } |
| |
| std::optional<std::string> IcdComponent::ReadManifest(int contents_dir_fd, |
| const std::string& manifest_path) { |
| std::string manifest_result; |
| if (!files::ReadFileToStringAt(contents_dir_fd, manifest_path, &manifest_result)) { |
| FX_LOGS(ERROR) << component_url_ << " Failed to read manifest path " << manifest_path; |
| return {}; |
| } |
| json_parser::JSONParser manifest_parser; |
| auto manifest_doc = manifest_parser.ParseFromString(manifest_result, manifest_path); |
| if (manifest_parser.HasError()) { |
| FX_LOGS(ERROR) << component_url_ << " JSON parser had error " << manifest_parser.error_str(); |
| return {}; |
| } |
| if (!ValidateManifestJson(component_url_, manifest_doc)) { |
| return {}; |
| } |
| |
| // Update library_path in manifest with a unique name. |
| auto& library_path_node = manifest_doc["ICD"].GetObject()["library_path"]; |
| std::string library_path = library_path_node.GetString(); |
| library_path = child_instance_name_ + "-" + library_path; |
| node_.CreateString("library_path", library_path, &value_list_); |
| library_path_node.SetString(library_path.c_str(), manifest_doc.GetAllocator()); |
| |
| manifest_result = json_parser::JsonValueToPrettyString(manifest_doc); |
| |
| node_.CreateString("manifest_contents", manifest_result, &value_list_); |
| manifest_file_ = |
| fbl::MakeRefCounted<fs::BufferedPseudoFile>([manifest_result](fbl::String* out_string) { |
| *out_string = manifest_result.c_str(); |
| return ZX_OK; |
| }); |
| return library_path; |
| } |
| |
| // static |
| bool IcdComponent::ValidateMetadataJson(const std::string& component_url, |
| const rapidjson::GenericDocument<rapidjson::UTF8<>>& doc) { |
| rapidjson::Document schema_doc; |
| schema_doc.Parse(kSchema); |
| FX_CHECK(!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); |
| FX_LOGS(ERROR) << component_url << " metadata.json failed validation " << sb.GetString(); |
| return false; |
| } |
| return true; |
| } |
| bool IcdComponent::ValidateManifestJson(const std::string& component_url, |
| const rapidjson::GenericDocument<rapidjson::UTF8<>>& doc) { |
| rapidjson::Document schema_doc; |
| schema_doc.Parse(kManifestSchema); |
| FX_CHECK(!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); |
| FX_LOGS(ERROR) << component_url << " manifest.json failed validation " << sb.GetString(); |
| return false; |
| } |
| return true; |
| } |
| |
| zx::result<zx::vmo> IcdComponent::CloneVmo() const { |
| std::lock_guard lock(vmo_lock_); |
| if (!vmo_info_) |
| return zx::error(ZX_ERR_BAD_STATE); |
| |
| uint64_t size; |
| zx_status_t status = vmo_info_->vmo.get_size(&size); |
| if (status != ZX_OK) |
| return zx::error(status); |
| zx::vmo vmo; |
| // Snapshot is ok because we never modify our VMO, and blobfs should never modify it either. We |
| // use ZX_VMO_CHILD_NO_WRITE because otherwise ZX_RIGHT_EXECUTE is removed. |
| status = vmo_info_->vmo.create_child( |
| ZX_VMO_CHILD_SNAPSHOT_AT_LEAST_ON_WRITE | ZX_VMO_CHILD_NO_WRITE, 0, size, &vmo); |
| if (status != ZX_OK) |
| return zx::error(status); |
| return zx::ok(std::move(vmo)); |
| } |
| |
| // See the accompanying README.md for a description of what a Vulkan component needs to have. |
| void IcdComponent::ReadFromComponent(fit::deferred_callback failure_callback, |
| fidl::ClientEnd<fuchsia_io::Directory> out_dir) { |
| initialization_status_.Set("reading from package"); |
| auto metadata_endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>(); |
| if (metadata_endpoints.is_error()) { |
| FX_LOGS(ERROR) << "Failed to create endpoints: " << metadata_endpoints.status_string(); |
| return; |
| } |
| |
| zx_status_t status = fdio_open_at(out_dir.channel().get(), "metadata", |
| static_cast<uint32_t>(fuchsia_io::OpenFlags::kRightReadable), |
| metadata_endpoints->server.TakeChannel().release()); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << component_url_ << " Failed opening metadata dir"; |
| return; |
| } |
| auto contents_endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>(); |
| if (contents_endpoints.is_error()) { |
| FX_LOGS(ERROR) << "Failed to create endpoints: " << contents_endpoints.status_string(); |
| return; |
| } |
| status = fdio_open_at(out_dir.channel().get(), "contents", |
| static_cast<uint32_t>(fuchsia_io::OpenFlags::kRightReadable | |
| fuchsia_io::OpenFlags::kRightExecutable), |
| contents_endpoints->server.TakeChannel().release()); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << component_url_ << " Failed opening pkg dir"; |
| return; |
| } |
| fbl::unique_fd metadata_dir_fd; |
| status = fdio_fd_create(metadata_endpoints->client.TakeChannel().release(), |
| metadata_dir_fd.reset_and_get_address()); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << component_url_ << " Failed creating FD for metadata"; |
| return; |
| } |
| |
| json_parser::JSONParser parser; |
| auto doc = parser.ParseFromFileAt(metadata_dir_fd.get(), "metadata.json"); |
| if (parser.HasError()) { |
| FX_LOGS(ERROR) << component_url_ << " JSON parser had error " << parser.error_str(); |
| return; |
| } |
| |
| fbl::unique_fd contents_dir_fd; |
| status = fdio_fd_create(contents_endpoints->client.TakeChannel().release(), |
| contents_dir_fd.reset_and_get_address()); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << component_url_ << " Failed creating FD"; |
| return; |
| } |
| if (!ValidateMetadataJson(component_url_, doc)) { |
| return; |
| } |
| node_.CreateUint("version", doc["version"].GetInt(), &value_list_); |
| std::string file_path = doc["file_path"].GetString(); |
| node_.CreateString("file_path", file_path, &value_list_); |
| initialization_status_.Set("opening manifest"); |
| std::string manifest_path = doc["manifest_path"].GetString(); |
| std::optional<std::string> library_path_option = |
| ReadManifest(contents_dir_fd.get(), manifest_path); |
| if (!library_path_option) { |
| return; |
| } |
| |
| // Manifest file will be added to the filesystem in IcdList::UpdateCurrentComponent. |
| |
| fbl::unique_fd fd; |
| |
| initialization_status_.Set("opening VMO"); |
| status = fdio_open_fd_at(contents_dir_fd.get(), file_path.c_str(), |
| static_cast<uint32_t>(fuchsia_io::OpenFlags::kRightReadable | |
| fuchsia_io::OpenFlags::kRightExecutable), |
| fd.reset_and_get_address()); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << component_url_ << " Could not open path " << file_path; |
| return; |
| } |
| zx::vmo vmo; |
| status = fdio_get_vmo_exec(fd.get(), vmo.reset_and_get_address()); |
| fd.reset(); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << component_url_ << " Could not clone vmo exec"; |
| return; |
| } |
| // Create another pending action token to keep everything alive until we're done initializing |
| // the data. |
| auto pending_action_token = app_->GetPendingActionToken(); |
| VmoInfo info; |
| info.library_path = *library_path_option; |
| info.vmo = std::move(vmo); |
| { |
| std::lock_guard lock(vmo_lock_); |
| vmo_info_ = std::move(info); |
| failure_callback.cancel(); |
| stage_ = LookupStages::kFinished; |
| } |
| app_->NotifyIcdsChanged(); |
| initialization_status_.Set("initialized"); |
| } |