Drew Fisher fb841071c2
fuchsia: remove use of replace_as_executable (second try) (#17313)
On Fuchsia, we can now get executable VMOs from trusted backing
filesystems.  This allows us to remove the use of replace_as_executable
in favor of opening files with `fdio_open_fd_at` with the
`OPEN_RIGHT_EXECUTABLE` flag and getting VMOs by calling
`fdio_get_vmo_exec`.

By moving the responsibility for executability into the filesystem, we
are able to remove `deprecated-ambient-replace-as-executable` from
component manifests for non-JIT runners (the JIT runners still call
replace_as_executable in Dart's allocator).  It wasn't abundantly clear
whether .cmx files for tests were used purely in AOT runtime
environments or also saw JIT usage, so I left those as-is.

For context: this is a second attempt at #16690, which was reverted
because it broke the Dart JIT runner.  The primary difference is that
this time around, we correctly handle absolute vs relative paths,
depending on whether library loading bottoms out in `fdio_open_fd` or
`fdio_open_fd_at`.  I've added additional assertions to help ensure any
new usages use the correct shape of path.

Testing: I verified locally that the flutter product runner works on
Astro, and also successfully ran the Dart JIT example test (which was
the thing blocking the google3 roll with the previous attempt at this
patchset).

Co-authored-by: Drew Fisher <zarvox@google.com>
2020-03-25 13:17:28 -07:00

625 lines
23 KiB
C++

// Copyright 2013 The Flutter 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 "component.h"
#include <dlfcn.h>
#include <fuchsia/mem/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/async/default.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/namespace.h>
#include <lib/ui/scenic/cpp/view_token_pair.h>
#include <lib/vfs/cpp/composed_service_dir.h>
#include <lib/vfs/cpp/remote_dir.h>
#include <lib/vfs/cpp/service.h>
#include <sys/stat.h>
#include <zircon/dlfcn.h>
#include <zircon/status.h>
#include <zircon/types.h>
#include <memory>
#include <regex>
#include <sstream>
#include "flutter/fml/mapping.h"
#include "flutter/fml/synchronization/waitable_event.h"
#include "flutter/fml/unique_fd.h"
#include "flutter/runtime/dart_vm_lifecycle.h"
#include "flutter/shell/common/switches.h"
#include "lib/fdio/io.h"
#include "runtime/dart/utils/files.h"
#include "runtime/dart/utils/handle_exception.h"
#include "runtime/dart/utils/mapped_resource.h"
#include "runtime/dart/utils/tempfs.h"
#include "runtime/dart/utils/vmo.h"
#include "task_observers.h"
#include "task_runner_adapter.h"
#include "thread.h"
// TODO(kaushikiska): Use these constants from ::llcpp::fuchsia::io
// Can read from target object.
constexpr uint32_t OPEN_RIGHT_READABLE = 1u;
// Connection can map target object executable.
constexpr uint32_t OPEN_RIGHT_EXECUTABLE = 8u;
namespace flutter_runner {
constexpr char kDataKey[] = "data";
constexpr char kTmpPath[] = "/tmp";
constexpr char kServiceRootPath[] = "/svc";
ActiveApplication Application::Create(
TerminationCallback termination_callback,
fuchsia::sys::Package package,
fuchsia::sys::StartupInfo startup_info,
std::shared_ptr<sys::ServiceDirectory> runner_incoming_services,
fidl::InterfaceRequest<fuchsia::sys::ComponentController> controller) {
std::unique_ptr<Thread> thread = std::make_unique<Thread>();
std::unique_ptr<Application> application;
fml::AutoResetWaitableEvent latch;
async::PostTask(thread->dispatcher(), [&]() mutable {
application.reset(
new Application(std::move(termination_callback), std::move(package),
std::move(startup_info), runner_incoming_services,
std::move(controller)));
latch.Signal();
});
latch.Wait();
return {.thread = std::move(thread), .application = std::move(application)};
}
static std::string DebugLabelForURL(const std::string& url) {
auto found = url.rfind("/");
if (found == std::string::npos) {
return url;
} else {
return {url, found + 1};
}
}
static std::unique_ptr<fml::FileMapping> MakeFileMapping(const char* path,
bool executable) {
uint32_t flags = OPEN_RIGHT_READABLE;
if (executable) {
flags |= OPEN_RIGHT_EXECUTABLE;
}
int fd = 0;
// The returned file descriptor is compatible with standard posix operations
// such as close, mmap, etc. We only need to treat open/open_at specially.
zx_status_t status = fdio_open_fd(path, flags, &fd);
if (status != ZX_OK) {
return nullptr;
}
using Protection = fml::FileMapping::Protection;
std::initializer_list<Protection> protection_execute = {Protection::kRead,
Protection::kExecute};
std::initializer_list<Protection> protection_read = {Protection::kRead};
auto mapping = std::make_unique<fml::FileMapping>(
fml::UniqueFD{fd}, executable ? protection_execute : protection_read);
if (!mapping->IsValid()) {
return nullptr;
}
return mapping;
}
// Defaults to readonly. If executable is `true`, we treat it as `read + exec`.
static flutter::MappingCallback MakeDataFileMapping(const char* absolute_path,
bool executable = false) {
return [absolute_path, executable = executable](void) {
return MakeFileMapping(absolute_path, executable);
};
}
Application::Application(
TerminationCallback termination_callback,
fuchsia::sys::Package package,
fuchsia::sys::StartupInfo startup_info,
std::shared_ptr<sys::ServiceDirectory> runner_incoming_services,
fidl::InterfaceRequest<fuchsia::sys::ComponentController>
application_controller_request)
: termination_callback_(std::move(termination_callback)),
debug_label_(DebugLabelForURL(startup_info.launch_info.url)),
application_controller_(this),
outgoing_dir_(new vfs::PseudoDir()),
runner_incoming_services_(runner_incoming_services),
weak_factory_(this) {
application_controller_.set_error_handler(
[this](zx_status_t status) { Kill(); });
FML_DCHECK(fdio_ns_.is_valid());
// LaunchInfo::url non-optional.
auto& launch_info = startup_info.launch_info;
// LaunchInfo::arguments optional.
if (auto& arguments = launch_info.arguments) {
settings_.dart_entrypoint_args = arguments.value();
}
// Determine /pkg/data directory from StartupInfo.
std::string data_path;
for (size_t i = 0; i < startup_info.program_metadata->size(); ++i) {
auto pg = startup_info.program_metadata->at(i);
if (pg.key.compare(kDataKey) == 0) {
data_path = "pkg/" + pg.value;
}
}
if (data_path.empty()) {
FML_DLOG(ERROR) << "Could not find a /pkg/data directory for "
<< package.resolved_url;
return;
}
// Setup /tmp to be mapped to the process-local memfs.
dart_utils::RunnerTemp::SetupComponent(fdio_ns_.get());
// LaunchInfo::flat_namespace optional.
for (size_t i = 0; i < startup_info.flat_namespace.paths.size(); ++i) {
const auto& path = startup_info.flat_namespace.paths.at(i);
if (path == kTmpPath) {
continue;
}
zx::channel dir;
if (path == kServiceRootPath) {
svc_ = std::make_unique<sys::ServiceDirectory>(
std::move(startup_info.flat_namespace.directories.at(i)));
dir = svc_->CloneChannel().TakeChannel();
} else {
dir = std::move(startup_info.flat_namespace.directories.at(i));
}
zx_handle_t dir_handle = dir.release();
if (fdio_ns_bind(fdio_ns_.get(), path.data(), dir_handle) != ZX_OK) {
FML_DLOG(ERROR) << "Could not bind path to namespace: " << path;
zx_handle_close(dir_handle);
}
}
application_directory_.reset(fdio_ns_opendir(fdio_ns_.get()));
FML_DCHECK(application_directory_.is_valid());
application_assets_directory_.reset(openat(
application_directory_.get(), data_path.c_str(), O_RDONLY | O_DIRECTORY));
// TODO: LaunchInfo::out.
// TODO: LaunchInfo::err.
// LaunchInfo::service_request optional.
if (launch_info.directory_request) {
outgoing_dir_->Serve(fuchsia::io::OPEN_RIGHT_READABLE |
fuchsia::io::OPEN_RIGHT_WRITABLE |
fuchsia::io::OPEN_FLAG_DIRECTORY,
std::move(launch_info.directory_request));
}
directory_request_ = directory_ptr_.NewRequest();
fidl::InterfaceHandle<fuchsia::io::Directory> flutter_public_dir;
// TODO(anmittal): when fixing enumeration using new c++ vfs, make sure that
// flutter_public_dir is only accessed once we receive OnOpen Event.
// That will prevent FL-175 for public directory
auto request = flutter_public_dir.NewRequest().TakeChannel();
fdio_service_connect_at(directory_ptr_.channel().get(), "svc",
request.release());
auto composed_service_dir = std::make_unique<vfs::ComposedServiceDir>();
composed_service_dir->set_fallback(std::move(flutter_public_dir));
// Clone and check if client is servicing the directory.
directory_ptr_->Clone(fuchsia::io::OPEN_FLAG_DESCRIBE |
fuchsia::io::OPEN_RIGHT_READABLE |
fuchsia::io::OPEN_RIGHT_WRITABLE,
cloned_directory_ptr_.NewRequest());
cloned_directory_ptr_.events().OnOpen =
[this](zx_status_t status, std::unique_ptr<fuchsia::io::NodeInfo> info) {
cloned_directory_ptr_.Unbind();
if (status != ZX_OK) {
FML_LOG(ERROR) << "could not bind out directory for flutter app("
<< debug_label_
<< "): " << zx_status_get_string(status);
return;
}
const char* other_dirs[] = {"debug", "ctrl", "diagnostics"};
// add other directories as RemoteDirs.
for (auto& dir_str : other_dirs) {
fidl::InterfaceHandle<fuchsia::io::Directory> dir;
auto request = dir.NewRequest().TakeChannel();
auto status = fdio_service_connect_at(directory_ptr_.channel().get(),
dir_str, request.release());
if (status == ZX_OK) {
outgoing_dir_->AddEntry(
dir_str, std::make_unique<vfs::RemoteDir>(dir.TakeChannel()));
} else {
FML_LOG(ERROR) << "could not add out directory entry(" << dir_str
<< ") for flutter app(" << debug_label_
<< "): " << zx_status_get_string(status);
}
}
};
cloned_directory_ptr_.set_error_handler(
[this](zx_status_t status) { cloned_directory_ptr_.Unbind(); });
// TODO: LaunchInfo::additional_services optional.
// All launch arguments have been read. Perform service binding and
// final settings configuration. The next call will be to create a view
// for this application.
composed_service_dir->AddService(
fuchsia::ui::app::ViewProvider::Name_,
std::make_unique<vfs::Service>(
[this](zx::channel channel, async_dispatcher_t* dispatcher) {
shells_bindings_.AddBinding(
this, fidl::InterfaceRequest<fuchsia::ui::app::ViewProvider>(
std::move(channel)));
}));
outgoing_dir_->AddEntry("svc", std::move(composed_service_dir));
// Setup the application controller binding.
if (application_controller_request) {
application_controller_.Bind(std::move(application_controller_request));
}
// Compare flutter_jit_runner in BUILD.gn.
settings_.vm_snapshot_data =
MakeDataFileMapping("/pkg/data/vm_snapshot_data.bin");
settings_.vm_snapshot_instr =
MakeDataFileMapping("/pkg/data/vm_snapshot_instructions.bin", true);
settings_.isolate_snapshot_data =
MakeDataFileMapping("/pkg/data/isolate_core_snapshot_data.bin");
settings_.isolate_snapshot_instr = MakeDataFileMapping(
"/pkg/data/isolate_core_snapshot_instructions.bin", true);
{
// Check if we can use the snapshot with the framework already loaded.
std::string runner_framework;
std::string app_framework;
if (dart_utils::ReadFileToString("pkg/data/runner.frameworkversion",
&runner_framework) &&
dart_utils::ReadFileToStringAt(application_assets_directory_.get(),
"app.frameworkversion",
&app_framework) &&
(runner_framework.compare(app_framework) == 0)) {
settings_.vm_snapshot_data =
MakeDataFileMapping("/pkg/data/framework_vm_snapshot_data.bin");
settings_.vm_snapshot_instr =
MakeDataFileMapping("/pkg/data/vm_snapshot_instructions.bin", true);
settings_.isolate_snapshot_data = MakeDataFileMapping(
"/pkg/data/framework_isolate_core_snapshot_data.bin");
settings_.isolate_snapshot_instr = MakeDataFileMapping(
"/pkg/data/isolate_core_snapshot_instructions.bin", true);
FML_LOG(INFO) << "Using snapshot with framework for "
<< package.resolved_url;
} else {
FML_LOG(INFO) << "Using snapshot without framework for "
<< package.resolved_url;
}
}
#if defined(DART_PRODUCT)
settings_.enable_observatory = false;
#else
settings_.enable_observatory = true;
// TODO(cbracken): pass this in as a param to allow 0.0.0.0, ::1, etc.
settings_.observatory_host = "127.0.0.1";
#endif
// Controls whether category "skia" trace events are enabled.
settings_.trace_skia = true;
settings_.icu_data_path = "";
settings_.assets_dir = application_assets_directory_.get();
// Compare flutter_jit_app in flutter_app.gni.
settings_.application_kernel_list_asset = "app.dilplist";
settings_.log_tag = debug_label_ + std::string{"(flutter)"};
// No asserts in debug or release product.
// No asserts in release with flutter_profile=true (non-product)
// Yes asserts in non-product debug.
#if !defined(DART_PRODUCT) && (!defined(FLUTTER_PROFILE) || !defined(NDEBUG))
// Debug mode
settings_.disable_dart_asserts = false;
#else
// Release mode
settings_.disable_dart_asserts = true;
#endif
settings_.task_observer_add =
std::bind(&CurrentMessageLoopAddAfterTaskObserver, std::placeholders::_1,
std::placeholders::_2);
settings_.task_observer_remove = std::bind(
&CurrentMessageLoopRemoveAfterTaskObserver, std::placeholders::_1);
// TODO(FL-117): Re-enable causal async stack traces when this issue is
// addressed.
settings_.dart_flags = {"--no_causal_async_stacks"};
// Disable code collection as it interferes with JIT code warmup
// by decreasing usage counters and flushing code which is still useful.
settings_.dart_flags.push_back("--no-collect_code");
if (!flutter::DartVM::IsRunningPrecompiledCode()) {
// The interpreter is enabled unconditionally in JIT mode. If an app is
// built for debugging (that is, with no bytecode), the VM will fall back on
// ASTs.
settings_.dart_flags.push_back("--enable_interpreter");
}
// Don't collect CPU samples from Dart VM C++ code.
settings_.dart_flags.push_back("--no_profile_vm");
// Scale back CPU profiler sampling period on ARM64 to avoid overloading
// the tracing engine.
#if defined(__aarch64__)
settings_.dart_flags.push_back("--profile_period=10000");
#endif // defined(__aarch64__)
auto weak_application = weak_factory_.GetWeakPtr();
auto platform_task_runner =
CreateFMLTaskRunner(async_get_default_dispatcher());
const std::string component_url = package.resolved_url;
settings_.unhandled_exception_callback = [weak_application,
platform_task_runner,
runner_incoming_services,
component_url](
const std::string& error,
const std::string& stack_trace) {
if (weak_application) {
// TODO(cbracken): unsafe. The above check and the PostTask below are
// happening on the UI thread. If the Application dtor and thread
// termination happen (on the platform thread) between the previous
// line and the next line, a crash will occur since we'll be posting
// to a dead thread. See Runner::OnApplicationTerminate() in
// runner.cc.
platform_task_runner->PostTask([weak_application,
runner_incoming_services, component_url,
error, stack_trace]() {
if (weak_application) {
dart_utils::HandleException(runner_incoming_services, component_url,
error, stack_trace);
} else {
FML_LOG(WARNING)
<< "Exception was thrown which was not caught in Flutter app: "
<< error;
}
});
} else {
FML_LOG(WARNING)
<< "Exception was thrown which was not caught in Flutter app: "
<< error;
}
// Ideally we would return whether HandleException returned ZX_OK, but
// short of knowing if the exception was correctly handled, we return
// false to have the error and stack trace printed in the logs.
return false;
};
AttemptVMLaunchWithCurrentSettings(settings_);
}
Application::~Application() = default;
const std::string& Application::GetDebugLabel() const {
return debug_label_;
}
class FileInNamespaceBuffer final : public fml::Mapping {
public:
FileInNamespaceBuffer(int namespace_fd, const char* path, bool executable)
: address_(nullptr), size_(0) {
fuchsia::mem::Buffer buffer;
if (!dart_utils::VmoFromFilenameAt(namespace_fd, path, executable,
&buffer)) {
return;
}
if (buffer.size == 0) {
return;
}
uint32_t flags = ZX_VM_PERM_READ;
if (executable) {
flags |= ZX_VM_PERM_EXECUTE;
}
uintptr_t addr;
zx_status_t status =
zx::vmar::root_self()->map(0, buffer.vmo, 0, buffer.size, flags, &addr);
if (status != ZX_OK) {
FML_LOG(FATAL) << "Failed to map " << path << ": "
<< zx_status_get_string(status);
}
address_ = reinterpret_cast<void*>(addr);
size_ = buffer.size;
}
~FileInNamespaceBuffer() {
if (address_ != nullptr) {
zx::vmar::root_self()->unmap(reinterpret_cast<uintptr_t>(address_),
size_);
address_ = nullptr;
size_ = 0;
}
}
// |fml::Mapping|
const uint8_t* GetMapping() const override {
return reinterpret_cast<const uint8_t*>(address_);
}
// |fml::Mapping|
size_t GetSize() const override { return size_; }
private:
void* address_;
size_t size_;
FML_DISALLOW_COPY_AND_ASSIGN(FileInNamespaceBuffer);
};
std::unique_ptr<fml::Mapping> CreateWithContentsOfFile(int namespace_fd,
const char* file_path,
bool executable) {
FML_TRACE_EVENT("flutter", "LoadFile", "path", file_path);
auto source = std::make_unique<FileInNamespaceBuffer>(namespace_fd, file_path,
executable);
return source->GetMapping() == nullptr ? nullptr : std::move(source);
}
void Application::AttemptVMLaunchWithCurrentSettings(
const flutter::Settings& settings) {
if (!flutter::DartVM::IsRunningPrecompiledCode()) {
// We will be initializing the VM lazily in this case.
return;
}
// Compare with flutter_aot_app in flutter_app.gni.
fml::RefPtr<flutter::DartSnapshot> vm_snapshot;
std::shared_ptr<dart_utils::ElfSnapshot> snapshot =
std::make_shared<dart_utils::ElfSnapshot>();
if (snapshot->Load(application_assets_directory_.get(),
"app_aot_snapshot.so")) {
const uint8_t* isolate_data = snapshot->IsolateData();
const uint8_t* isolate_instructions = snapshot->IsolateInstrs();
const uint8_t* vm_data = snapshot->VmData();
const uint8_t* vm_instructions = snapshot->VmInstrs();
if (isolate_data == nullptr || isolate_instructions == nullptr ||
vm_data == nullptr || vm_instructions == nullptr) {
FML_LOG(FATAL) << "ELF snapshot missing AOT symbols.";
return;
}
auto hold_snapshot = [snapshot](const uint8_t* _, size_t __) {};
vm_snapshot = fml::MakeRefCounted<flutter::DartSnapshot>(
std::make_shared<fml::NonOwnedMapping>(vm_data, 0, hold_snapshot),
std::make_shared<fml::NonOwnedMapping>(vm_instructions, 0,
hold_snapshot));
isolate_snapshot_ = fml::MakeRefCounted<flutter::DartSnapshot>(
std::make_shared<fml::NonOwnedMapping>(isolate_data, 0, hold_snapshot),
std::make_shared<fml::NonOwnedMapping>(isolate_instructions, 0,
hold_snapshot));
} else {
vm_snapshot = fml::MakeRefCounted<flutter::DartSnapshot>(
CreateWithContentsOfFile(
application_assets_directory_.get() /* /pkg/data */,
"vm_snapshot_data.bin", false),
CreateWithContentsOfFile(
application_assets_directory_.get() /* /pkg/data */,
"vm_snapshot_instructions.bin", true));
isolate_snapshot_ = fml::MakeRefCounted<flutter::DartSnapshot>(
CreateWithContentsOfFile(
application_assets_directory_.get() /* /pkg/data */,
"isolate_snapshot_data.bin", false),
CreateWithContentsOfFile(
application_assets_directory_.get() /* /pkg/data */,
"isolate_snapshot_instructions.bin", true));
}
auto vm = flutter::DartVMRef::Create(settings_, //
std::move(vm_snapshot), //
isolate_snapshot_ //
);
FML_CHECK(vm) << "Mut be able to initialize the VM.";
}
// |fuchsia::sys::ComponentController|
void Application::Kill() {
application_controller_.events().OnTerminated(
last_return_code_.second, fuchsia::sys::TerminationReason::EXITED);
termination_callback_(this);
// WARNING: Don't do anything past this point as this instance may have been
// collected.
}
// |fuchsia::sys::ComponentController|
void Application::Detach() {
application_controller_.set_error_handler(nullptr);
}
// |flutter::Engine::Delegate|
void Application::OnEngineTerminate(const Engine* shell_holder) {
auto found = std::find_if(shell_holders_.begin(), shell_holders_.end(),
[shell_holder](const auto& holder) {
return holder.get() == shell_holder;
});
if (found == shell_holders_.end()) {
return;
}
// We may launch multiple shell in this application. However, we will
// terminate when the last shell goes away. The error code return to the
// application controller will be the last isolate that had an error.
auto return_code = shell_holder->GetEngineReturnCode();
if (return_code.first) {
last_return_code_ = return_code;
}
shell_holders_.erase(found);
if (shell_holders_.size() == 0) {
Kill();
// WARNING: Don't do anything past this point because the delegate may have
// collected this instance via the termination callback.
}
}
// |fuchsia::ui::app::ViewProvider|
void Application::CreateView(
zx::eventpair view_token,
fidl::InterfaceRequest<fuchsia::sys::ServiceProvider> incoming_services,
fidl::InterfaceHandle<fuchsia::sys::ServiceProvider> outgoing_services) {
if (!svc_) {
FML_DLOG(ERROR)
<< "Component incoming services was invalid when attempting to "
"create a shell for a view provider request.";
return;
}
shell_holders_.emplace(std::make_unique<Engine>(
*this, // delegate
debug_label_, // thread label
svc_, // Component incoming services
runner_incoming_services_, // Runner incoming services
settings_, // settings
std::move(isolate_snapshot_), // isolate snapshot
scenic::ToViewToken(std::move(view_token)), // view token
std::move(fdio_ns_), // FDIO namespace
std::move(directory_request_) // outgoing request
));
}
#if !defined(DART_PRODUCT)
void Application::WriteProfileToTrace() const {
for (const auto& engine : shell_holders_) {
engine->WriteProfileToTrace();
}
}
#endif // !defined(DART_PRODUCT)
} // namespace flutter_runner