blob: 43e9fb3655d83b9f13deb2e7688426d718a434c4 [file] [log] [blame]
// Copyright 2019 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.
#pragma once
// Include our sister file from the same directory we're in, first.
#include "hermetic-data.h"
#include <cstring>
#include <fbl/macros.h>
#include <lib/zx/process.h>
#include <lib/zx/suspend_token.h>
#include <lib/zx/thread.h>
#include <lib/zx/vmar.h>
#include <optional>
#include <tuple>
#include <type_traits>
#include <utility>
#include <zircon/assert.h>
// Forward declaration.
template <typename T>
class HermeticExportAgent;
// Manage a process that will run a hermetic compute module.
class HermeticComputeProcess {
public:
// Only Init() and the accessors should be called on default-constructed.
HermeticComputeProcess() = default;
// An object created from an existing process and its root VMAR
// (or smaller child VMAR) is ready to be used.
HermeticComputeProcess(zx::process proc, zx::vmar vmar) :
process_(std::move(proc)), vmar_(std::move(vmar)) {
}
// Create a new process.
zx_status_t Init(const zx::job& job, const char* name) {
return zx::process::create(
job, name, static_cast<uint32_t>(strlen(name)), 0,
&process_, &vmar_);
}
// The process handle lives as long as the object.
const zx::process& process() const { return process_; }
// The VMAR handle is not usually needed after setting up the module.
// It's reset by Start() but can be used, reset, or moved before that.
const zx::vmar& vmar() const { return vmar_; }
zx::vmar& vmar() { return vmar_; }
//
// Low-level interface: loading up the memory.
//
// Load an ET_DYN file from the VMO.
zx_status_t LoadElf(const zx::vmo& vmo,
uintptr_t* out_base, uintptr_t* out_entry,
size_t* out_stack_size);
// Allocate a stack VMO and map it into the process, yielding the base of
// the stack (corresponding to VMO offset 0).
zx_status_t LoadStack(size_t* size, zx::vmo* out_vmo,
uintptr_t* out_stack_base);
// Acquire the VMO for the vDSO.
static const zx::vmo& GetVdso(const char* variant = nullptr);
//
// Low-level interface: take-off and landing.
//
// This can be used with any kind of code. The low-level
// setup and communication details are left to the caller.
//
// Start the process with an initial thread.
// Parameters are passed directly into zx_process_start().
zx_status_t Start(uintptr_t entry, uintptr_t sp,
zx::handle arg1, uintptr_t arg2);
// Start the process with complete control over its registers.
// The initial thread is left suspended so its state can be modified.
zx_status_t Start(zx::handle handle,
zx::thread* out_thread, zx::suspend_token* out_token);
// Wait for the process to finish and (optionally) yield its exit status.
// This is just a convenient way to wait for the ZX_PROCESS_TERMINATED
// signal on process() and then collect zx_info_process_t::return_code.
// To synchronize in more complex ways, use process() directly.
zx_status_t Wait(int64_t* result = nullptr,
zx::time deadline = zx::time::infinite());
// Map a VMO into the process. The location is always randomized and kept
// far away from any other mappings.
zx_status_t Map(const zx::vmo& vmo, uint64_t vmo_offset, size_t size,
bool writable, uintptr_t* ptr);
//
// High-level interface: argument-driven loading and launching.
//
// This is intended to be used with hermetic compute modules built
// using the <lib/hermetic-compute/hermetic-engine.h> library and
// sharing data via <lib/hermetic-compute/hermetic-data.h> types.
//
// By calling the HermeticComputeProcess as a function, everything can be
// controlled via arguments. The HermeticExportAgent specializations for
// the argument types do all the work. Any number of arguments get
// forwarded to the engine's entry point (see HermeticComputeEngine).
// Arguments can be of any type for which there is a HermeticExportAgent
// specialization (see below). Everything else is done as a side effect
// by HermeticExportAgent specializations, including loading the code
// itself into the process. Several special wrapper types are provided
// below just to have particular side effects when passed as arguments.
//
// On success, the initial thread is always created and set up to receive
// the arguments in its registers and stack. It can be left suspended by
// passing a Suspended argument that will receive the token to let it run.
// Otherwise it's already running when this returns.
template <typename... Args>
zx_status_t operator()(Args&&... args) {
// Side effects of transforming the arguments do all the ELF loading
// and miscellaneous setup before Launcher::Launch does the final
// stack setup and thread creation.
Launcher launcher(*this);
launcher.Flatten(std::forward<Args>(args)...);
return launcher.status();
}
// Keep the initial thread suspended so its state can be modified and take
// responsibility for letting the thread run. The thread's register state
// will be updated at the end of launching and should not be modified
// before then. Once the launch steps are all complete, the thread and
// token handles will be moved into the locations pointed to. The thread
// will be allowed to run as soon as the token handle is closed.
struct Suspended {
zx::thread* thread;
zx::suspend_token* token;
};
// Set the entry point PC for the engine process.
struct EntryPoint {
uintptr_t pc;
};
// Set the minimum stack size for the engine process.
struct StackSize {
size_t size;
};
// Load an ET_DYN file from the VMO. The initial thread will start at its
// entry point and its PT_GNU_STACK will determine the stack size, but
// there are no corresponding import arguments. Note that nothing
// prevents passing multiple of these. They will all be loaded, and
// the entry point and stack size from the last one will prevail.
struct Elf {
const zx::vmo& vmo;
};
// Load an ET_DYN file from the VMO. Imported as const Elf64_Ehdr*.
struct ExtraElf {
const zx::vmo& vmo;
};
// This exports exactly the same as ExtraElf. HermeticComputeEngine
// requires that this be the first imported argument. It's distinct just
// for clarity in its use and for its second constructor, which doubles as
// its default constructor too.
struct Vdso : public ExtraElf {
explicit Vdso(const zx::vmo& vmo) : ExtraElf{vmo} {}
explicit Vdso(const char* variant = nullptr) :
ExtraElf{GetVdso(variant)} {}
};
// Launcher is a single-use object that only lives during a call.
// It's only ever visible to HermeticExportAgent specializations.
class Launcher {
public:
zx_status_t status() const { return status_; }
auto& engine() { return engine_; }
// Mark the launcher as having failed so later methods will
// short-circuit. This can be called to report a failure in a
// complex transfer.
void Abort(zx_status_t status) {
ZX_DEBUG_ASSERT(status != ZX_OK);
status_ = status;
}
// Map a VMO into the engine process and return the address of
// the mapping. This returns 0 if the mapping failed or wasn't
// attempted due to an error shown in status().
uintptr_t Map(const zx::vmo& vmo, uint64_t vmo_offset, size_t size,
bool writable) {
uintptr_t ptr = 0;
if (status_ == ZX_OK) {
status_ = engine_.Map(vmo, vmo_offset, size, writable, &ptr);
}
return ptr;
}
private:
friend HermeticComputeProcess;
HermeticComputeProcess& engine_;
zx::thread thread_;
zx::suspend_token token_;
// Only HermeticExportAgent<Suspended> sets suspended_.
friend HermeticExportAgent<Suspended>;
std::optional<Suspended> suspended_;
// Only HermeticExportAgent<EntryPoint> sets entry_pc_;
friend HermeticExportAgent<EntryPoint>;
uintptr_t entry_pc_ = 0;
// Only HermeticExportAgent<StackSize> sets stack_size_;
friend HermeticExportAgent<StackSize>;
size_t stack_size_ = 0;
zx_status_t status_ = ZX_OK;
DISALLOW_COPY_ASSIGN_AND_MOVE(Launcher);
Launcher() = delete;
explicit Launcher(HermeticComputeProcess& engine) : engine_(engine) {}
~Launcher();
friend HermeticExportAgent<zx::handle>; // Only caller of SendHandle.
zx_handle_t SendHandle(zx::handle handle);
// Forwards any number of uintptr_t arguments.
void Launch(size_t nargs, ...);
// An argument list is fully flattened when it's all uintptr_t.
// Then it's ready to pass to Launch.
template <typename... T>
static constexpr bool kLaunchable =
(std::is_same_v<uintptr_t, T> && ...);
template <typename Arg>
using Agent = HermeticExportAgent<std::decay_t<Arg>>;
template <typename Arg>
auto MakeAgent(Launcher& launcher) {
return Agent<Arg>(launcher);
}
// HermeticExportAgent converts an argument to a tuple of simpler
// arguments. Paste all the tuples together and reflatten.
template <typename... Args>
void Flatten(Args&&... args) {
// Calls evaluate arguments in unspecified order. So the
// make_tuple call (that's not actually evaluated) would invoke
// agents in unspecified order, just like doing it directly in
// the tuple_cat call. But list initialization evaluates its
// initializers left to right. So this invokes the agents in
// left-to-right order as their results go into the pack.
decltype(std::make_tuple(
MakeAgent<Args>(*this)(std::forward<Args>(args))...))
pack{MakeAgent<Args>(*this)(std::forward<Args>(args))...};
// Perfect forwarding in a generic lambda!
auto cat =
[](auto&&... x) {
return std::tuple_cat(std::forward<decltype(x)>(x)...);
};
// Flatten the pack of tuples to a single tuple and flatten that.
FlattenTuple(std::apply(cat, std::move(pack)));
}
template <typename... T>
using IfLaunchable = std::enable_if_t<kLaunchable<T...>>;
template <typename... T>
using IfNotLaunchable = std::enable_if_t<!kLaunchable<T...>>;
// Unwrap a tuple that's not all uintptr_t and come back around.
template <typename... T>
IfNotLaunchable<T...> FlattenTuple(std::tuple<T...> args) {
// Perfect forwarding in a generic lambda!
std::apply(
[&](auto&&... x) { Flatten(std::forward<decltype(x)>(x)...); },
std::move(args));
}
// Unwrap a tuple of all uintptr_t and actually make the call.
template <typename... T>
IfLaunchable<T...> FlattenTuple(std::tuple<T...> args) {
// Technically a generic lambda, but they're always all uintptr_t.
std::apply(
[&](auto... args) { Launch(sizeof...(T), args...); },
std::move(args));
}
};
// Shorthand for simplest cases.
template <typename... Args>
zx_status_t Call(int64_t *result, Args&&... args) {
zx_status_t status = (*this)(Vdso{}, std::forward<Args>(args)...);
if (status == ZX_OK) {
status = Wait(result);
}
return status;
}
private:
zx::process process_;
zx::vmar vmar_;
};
// A base class for the HermeticExportAgent<T> specialization.
template <typename T>
class HermeticExportAgentBase {
public:
using type = T;
explicit HermeticExportAgentBase(
HermeticComputeProcess::Launcher& launcher) : launcher_(launcher) {}
auto& launcher() const { return launcher_; }
auto& engine() const { return launcher_.engine(); }
protected:
using Base = HermeticExportAgentBase<T>;
zx_status_t status() const { return launcher().status(); }
void Abort(zx_status_t status) { launcher().Abort(status); }
bool Ok() const {
return status() == ZX_OK;
}
bool Ok(zx_status_t status) {
if (status != ZX_OK) {
Abort(status);
}
return Ok();
}
private:
HermeticComputeProcess::Launcher& launcher_;
};
// This can be specialized to provide transparent argument-passing support for
// nontrivial types. The default implementation handles trivial types that
// don't have padding bits, and zx::* handle types.
//
// The HermeticExportAgent for a type packs arguments "exported" to the
// hermetic environment, ultimately by flattening everything into uintptr_t[].
// The packing protocol for an "export" type is understood in the hermetic
// engine to form a corresponding "import" type. The engine code has a
// specialization of HermeticImportAgent that unpacks the uinptr_t[] into the
// "import" type. Note that the "export" and "import" types need not be the
// same type, just corresponding types with a compatible packing protocol.
//
// HermeticExportAgent() takes a reference to the HermeticComputeProcess. The
// agent object is then called with the value as argument and returns a
// std::tuple of arguments to pass instead. Each of those arguments then gets
// passed through a HermeticExportAgent of its own if it's not already of type
// uintptr_t. Note an agent is free to return an empty tuple, as well as a
// tuple of one or more complex types that themselves need separate packing.
// Thus marker types can be used as dummy parameters just for side effects.
//
// The agent object can poke the process, e.g. to map things into its address
// space. The agent runs after the engine has been loaded into the process but
// before its first thread has been created and before its stacks have been
// allocated. It can call engine() for e.g. process() or vmar(). If it
// encounters any errors it should call Launcher::Abort(zx_status_t). This
// will short-circuit the launch. Additional agents will be called to pack
// parameters, but the final stack setup and process start will never happen.
// An agent can check Launcher::status() to short-circuit its own work when the
// launch has been aborted, though it still has to return some value of its
// std::tuple<...> return type.
//
// Handle types (zx::object et al) yield zx_handle_t. There can be only one
// handle-typed argument in a call, since only one handle is transferred.
//
// TODO(mcgrathr): When the engine side can actually make use of handles, add
// a magic type that makes a channel stashed in the Launcher and sends that
// handle, unpacked on the other side to fetch the multiple handles and/or
// large(r) data blobs(?). Then make this automagically detect if that has
// been used and send handles into the channel instead.
template <typename T>
class HermeticExportAgent : public HermeticExportAgentBase<T> {
private:
static constexpr bool kIsHandle = std::is_base_of_v<zx::object_base, T>;
public:
using Base = HermeticExportAgentBase<T>;
using typename Base::type;
explicit HermeticExportAgent(
HermeticComputeProcess::Launcher& launcher) : Base(launcher) {}
static_assert(kIsHandle || std::is_standard_layout_v<T>,
"need converter for non-standard-layout type");
static_assert(kIsHandle ||
std::has_unique_object_representations_v<T> ||
std::is_floating_point_v<T>,
"need converter for type with padding bits");
template <typename ArgType>
auto operator()(ArgType&& x) {
static_assert(std::is_same_v<std::decay_t<ArgType>, type>);
if constexpr (kIsHandle) {
return std::make_tuple(zx::handle{std::move(x)});;
} else if constexpr (std::is_integral_v<T> &&
sizeof(T) <= sizeof(uintptr_t)) {
// Small integer types can just be coerced to uintptr_t.
return std::make_tuple(static_cast<uintptr_t>(x));
} else {
// Other things can be turned into an array of uintptr_t.
constexpr auto nwords =
(sizeof(T) + sizeof(uintptr_t) - 1) / sizeof(uintptr_t);
std::tuple<std::array<uintptr_t, nwords>> result{{}};
memcpy(std::get<0>(result).data(), &x, sizeof(x));
return result;
}
}
};
// Specialization for tuples.
template <typename... T>
struct HermeticExportAgent<std::tuple<T...>> :
public HermeticExportAgentBase<std::tuple<T...>> {
using Base = HermeticExportAgentBase<std::tuple<T...>>;
using typename Base::type;
explicit HermeticExportAgent(
HermeticComputeProcess::Launcher& launcher) : Base(launcher) {}
auto operator()(type x) {
// Tuples get flattened. Each element will then get converted.
return x;
}
};
// Specialization for pairs.
template <typename T1, typename T2>
struct HermeticExportAgent<std::pair<T1, T2>> :
public HermeticExportAgentBase<std::pair<T1, T2>> {
using Base = HermeticExportAgentBase<std::pair<T1, T2>>;
using typename Base::type;
explicit HermeticExportAgent(
HermeticComputeProcess::Launcher& launcher) : Base(launcher) {}
auto operator()(type x) {
// Tuplize the pair.
return std::make_from_tuple<std::tuple<T1, T2>>(std::move(x));
}
};
// Specialization for std::array.
template <typename T, size_t N>
class HermeticExportAgent<std::array<T, N>> :
public HermeticExportAgentBase<std::array<T, N>> {
public:
using Base = HermeticExportAgentBase<std::array<T, N>>;
using typename Base::type;
explicit HermeticExportAgent(
HermeticComputeProcess::Launcher& launcher) : Base(launcher) {}
auto operator()(type x) {
// Tuplize. Note that for sizeof(T) < uintptr_t, this is less optimal
// packing than simply treating the whole array as a block of bytes,
// which is what happens for structs. But it's fully general for all
// element types, allowing recursive type-specific packing.
return ArrayTuple(std::move(x), std::make_index_sequence<N>());
}
private:
template <typename Array, size_t... I>
auto ArrayTuple(Array a, std::index_sequence<I...>) {
return std::make_tuple(std::move(a[I])...);
}
};
// Specialization for simple ELF loading.
// Yields an imported argument of const Elf64_Ehdr*.
template <>
struct HermeticExportAgent<HermeticComputeProcess::ExtraElf> :
public HermeticExportAgentBase<HermeticComputeProcess::ExtraElf> {
using Base = HermeticExportAgentBase<HermeticComputeProcess::ExtraElf>;
using typename Base::type;
explicit HermeticExportAgent(
HermeticComputeProcess::Launcher& launcher) : Base(launcher) {}
auto operator()(const type& elf) {
uintptr_t base = 0;
if (Ok()) {
Ok(engine().LoadElf(elf.vmo, &base, nullptr, nullptr));
}
return std::make_tuple(base);
}
};
// Vdso is the same as ExtraElf.
template <>
struct HermeticExportAgent<HermeticComputeProcess::Vdso> :
public HermeticExportAgent<HermeticComputeProcess::ExtraElf> {
using Base = HermeticExportAgent<HermeticComputeProcess::ExtraElf>;
using typename Base::type;
explicit HermeticExportAgent(
HermeticComputeProcess::Launcher& launcher) : Base(launcher) {}
};
// Specialization for loading the main ELF file.
// Yields no imported arguments.
template <>
struct HermeticExportAgent<HermeticComputeProcess::Elf> :
public HermeticExportAgentBase<HermeticComputeProcess::Elf> {
using Base = HermeticExportAgentBase<HermeticComputeProcess::Elf>;
using typename Base::type;
explicit HermeticExportAgent(
HermeticComputeProcess::Launcher& launcher) : Base(launcher) {}
// Load the ELF image by side effect and then reduce to the arguments
// that set the entry point and stack size.
using return_type = std::tuple<HermeticComputeProcess::EntryPoint,
HermeticComputeProcess::StackSize>;
return_type operator()(const type& elf) {
uintptr_t entry = 0;
size_t stack_size = 0;
if (Ok()) {
Ok(engine().LoadElf(elf.vmo, nullptr, &entry, &stack_size));
}
return {{entry}, {stack_size}};
}
};
// Specialization for catching the thread before it runs.
// Yields no imported arguments.
template <>
struct HermeticExportAgent<HermeticComputeProcess::Suspended> :
public HermeticExportAgentBase<HermeticComputeProcess::Suspended> {
using Base = HermeticExportAgentBase<HermeticComputeProcess::Suspended>;
using typename Base::type;
explicit HermeticExportAgent(
HermeticComputeProcess::Launcher& launcher) : Base(launcher) {}
auto operator()(const type& suspended) {
// TODO(mcgrathr): Make it statically impossible to have two.
if (launcher().suspended_) {
Abort(ZX_ERR_BAD_STATE);
} else {
launcher().suspended_ = suspended;
}
return std::make_tuple();
}
};
// Specialization for setting the entry point.
// Yields no imported arguments.
template <>
struct HermeticExportAgent<HermeticComputeProcess::EntryPoint> :
public HermeticExportAgentBase<HermeticComputeProcess::EntryPoint> {
using Base = HermeticExportAgentBase<HermeticComputeProcess::EntryPoint>;
using typename Base::type;
explicit HermeticExportAgent(
HermeticComputeProcess::Launcher& launcher) : Base(launcher) {}
auto operator()(const type& entry) {
// TODO(mcgrathr): Make it statically impossible to have two.
if (launcher().entry_pc_ != 0) {
Abort(ZX_ERR_BAD_STATE);
} else {
launcher().entry_pc_ = entry.pc;
}
return std::make_tuple();
}
};
// Specialization for setting the stack size.
// Yields no imported arguments.
template <>
struct HermeticExportAgent<HermeticComputeProcess::StackSize> :
public HermeticExportAgentBase<HermeticComputeProcess::StackSize> {
using Base = HermeticExportAgentBase<HermeticComputeProcess::StackSize>;
using typename Base::type;
explicit HermeticExportAgent(
HermeticComputeProcess::Launcher& launcher) : Base(launcher) {}
auto operator()(const type& stack) {
// TODO(mcgrathr): Make it statically impossible to have two.
if (launcher().stack_size_ != 0) {
Abort(ZX_ERR_BAD_STATE);
} else {
launcher().stack_size_ = stack.size;
}
return std::make_tuple();
}
};
// Specialization for passing a handle into the process.
// Yields zx_handle_t (remote handle value as seen in the process).
//
// This can only be used once in the whole call, since only a single handle
// can be transferred at process startup. A second use will fail and set
// status() to ZX_ERR_BAD_STATE. Note it's valid to use this with an
// invalid handle; it will yield ZX_HANDLE_INVALID and prevent other uses.
//
// TODO(mcgrathr): Make it statically impossible to have two.
template <>
struct HermeticExportAgent<zx::handle> :
public HermeticExportAgentBase<zx::handle> {
using Base = HermeticExportAgentBase<zx::handle>;
using typename Base::type;
explicit HermeticExportAgent(
HermeticComputeProcess::Launcher& launcher) : Base(launcher) {}
auto operator()(zx::handle handle) {
zx_handle_t remote_handle = ZX_HANDLE_INVALID;
if (Ok()) {
remote_handle = this->launcher().SendHandle(std::move(handle));
}
return std::make_tuple(remote_handle);
}
};