blob: 11c2348d391071ac69ccdc3bca63f642e96d5cd4 [file] [log] [blame]
// 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.
#ifndef SRC_LIB_ELFLDLTL_INCLUDE_LIB_ELFLDLTL_VMAR_LOADER_H_
#define SRC_LIB_ELFLDLTL_INCLUDE_LIB_ELFLDLTL_VMAR_LOADER_H_
#include <lib/stdcompat/span.h>
#include <lib/zx/result.h>
#include <lib/zx/vmar.h>
#include <lib/zx/vmo.h>
#include <zircon/assert.h>
#include <zircon/syscalls/object.h>
#include <type_traits>
#include "diagnostics.h"
#include "memory.h"
#include "zircon.h"
namespace elfldltl {
/// This is the base class for LocalVmarLoader and RemoteVmarLoader, below.
/// All versions have mostly the same API. The protected Load method here has
/// the same signature as the public Load methods in the derived classes, so
/// the one API comment on Load here applies to those public methods.
///
/// This object encapsulates the work needed to load an object into a VMAR
/// based on an elfldltl::LoadInfo struct (see load.h) previously populated
/// from program headers. The Load method does the loading and initializes
/// the VmarLoader object's state. If Load fails, then the object should be
/// destroyed without calling other methods.
class VmarLoader {
public:
// This is returned by Commit(), which completes the use of an VmarLoader.
// It represents the capability to apply RELRO protections to a loaded image.
// Unlike the VmarLoader object itself, its lifetime is not tied to the image
// mappings. After Commit(), the image mapping won't be destroyed by the
// VmarLoader's destructor.
class Relro {
public:
Relro() = default;
// Movable, not copyable: the object represents capability ownership.
Relro(const Relro&) = delete;
Relro(Relro&&) = default;
Relro& operator=(const Relro&) = delete;
Relro& operator=(Relro&&) = default;
// This is the only method that can be called, and it must be last.
// It makes the RELRO region passed to MmapLoader::Commit read-only.
template <class Diagnostics>
[[nodiscard]] bool Commit(Diagnostics& diag) && {
zx::vmar vmar = std::exchange(vmar_, {});
if (size_ > 0) {
zx_status_t status = vmar.protect(ZX_VM_PERM_READ, addr_, size_);
if (status != ZX_OK) {
return diag.SystemError("cannot protect PT_GNU_RELRO region: ", ZirconError{status});
}
}
return true;
}
// Alternatively, the VMAR handle can be extracted without doing
// protections.
[[nodiscard]] zx::vmar TakeVmar() && { return std::move(vmar_); }
private:
friend VmarLoader;
template <class Region>
Relro(zx::vmar vmar, const Region& region, uintptr_t load_bias) : vmar_{std::move(vmar)} {
if (!region.empty()) {
addr_ = region.start + load_bias;
size_ = region.size();
}
}
zx_vaddr_t addr_ = 0;
size_t size_ = 0;
zx::vmar vmar_;
};
// This can be specialized by the user for particular LoadInfo types passed
// to Load(). It's constructed by Load() for each segment using the specific
// LoadInfo::*Segment type, not the LoadInfo::Segment std::variant type, so
// it can have a constructor that works differently for each type. The
// constructor will be called with the individual segment object reference,
// and the VMO for the whole file. Both will be valid for the lifetime of
// the SegmentVmo object. The handle returned by .vmo() will only be used
// during that lifetime. So, a specialization might return an object that
// owns the VMO handle it yields.
template <class LoadInfo>
class SegmentVmo {
public:
SegmentVmo() = delete;
template <class Segment>
SegmentVmo(const Segment& segment, zx::unowned_vmo vmo)
: vmo_{vmo->borrow()}, offset_{segment.offset()} {}
// This is the VMO to map the segment's contents from.
zx::unowned_vmo vmo() const { return vmo_->borrow(); }
// If true and the segment is writable, make a copy-on-write child VMO.
// This is the usual behavior when using the original file VMO directly.
// A specialization can return anything convertible to bool.
constexpr std::true_type copy_on_write() const { return {}; }
// This is the offset within the VMO whence the segment should be mapped
// (or cloned if this->copy_on_write()).
uint64_t offset() const { return offset_; }
private:
zx::unowned_vmo vmo_;
uint64_t offset_;
};
// When default-constructed, only the zx::vmar signature of Load can be used.
// The default-constructed object can be assigned to another that has a
// parent VMAR handle.
VmarLoader() = default;
explicit VmarLoader(const zx::vmar& vmar) : vmar_(vmar.borrow()) {}
VmarLoader(VmarLoader&& other) = default;
VmarLoader& operator=(VmarLoader&& other) = default;
~VmarLoader() {
if (load_image_vmar_) {
load_image_vmar_.destroy();
}
}
[[gnu::const]] static size_t page_size() { return zx_system_get_page_size(); }
/// This is called implicitly by Load if not explicitly called before Load.
/// Allocate a child VMAR from the containing VMAR for the whole load image.
/// Only the vaddr_size() and vaddr_start() from the LoadInfo are used here.
/// The kernel places it using ASLR within the parent VMAR provided at
/// construction, unless the optional argument sets the offset from the base
/// of that parent VMAR.
template <class Diagnostics, class LoadInfo>
[[nodiscard]] bool Allocate(Diagnostics& diag, const LoadInfo& load_info,
std::optional<size_t> vmar_offset = std::nullopt) {
if (zx_status_t status =
AllocateVmar(load_info.vaddr_size(), load_info.vaddr_start(), vmar_offset);
status != ZX_OK) [[unlikely]] {
return diag.SystemError("Failed to allocate address space", ZirconError{status});
}
return true;
}
/// Given the base address of some parent VMAR, LoadInfo of an image to be
/// passed to Allocate, and the desired exact load bias, this yields the
/// value to pass for Allocate's optional argument. For example, if another
/// VmarLoader object has been used to load this image before, then passing
/// this the .load_bias() value will give the Allocate argument to ensure
/// that Load replicates the previous load layout exactly in a new process.
template <class LoadInfo>
static size_t VmarOffsetForLoadBias(zx_vaddr_t vmar_base, const LoadInfo& load_info,
zx_vaddr_t load_bias) {
return load_info.vaddr_start() + load_bias - vmar_base;
}
/// This installs a pre-allocated VMAR for the image that will be passed to
/// Load(). The VMAR must be large enough for the .vaddr_size() of the
/// LoadInfo passed to Load(). This takes ownership of the VMAR handle and
/// of the VMAR itself--the VMAR will be destroyed if this object is
/// destroyed before Commit() is called. The load bias must match the
/// difference between the base of the VMAR and .vaddr_start().
void Place(zx::vmar load_image_vmar, zx_vaddr_t load_bias) {
ZX_DEBUG_ASSERT_MSG(!load_image_vmar_, "Place or Allocate called twice");
ZX_DEBUG_ASSERT(load_image_vmar);
load_image_vmar_ = std::move(load_image_vmar);
load_bias_ = load_bias;
}
/// After Allocate() or Place() and/or Load(), this is the bias added to the
/// given LoadInfo::vaddr_start() to find the runtime load address.
zx_vaddr_t load_bias() const { return load_bias_; }
/// Commit is used to keep the mapping created by Load around even after the
/// VmarLoader object is destroyed. This method must be the last thing
/// called on the object if it is used, hence it can only be called with
/// `auto relro = std::move(loader).Commit(relro_bounds);`. If Commit() is
/// not called, then loading is aborted by destroying the VMAR when the
/// VmarLoader object is destroyed. Commit() returns the Relro object (see
/// above) that holds the VMAR handle allowing it to change page protections
/// of the load image.
template <class Region>
[[nodiscard]] Relro Commit(const Region& relro_bounds) && {
return {std::exchange(load_image_vmar_, {}), relro_bounds, load_bias()};
}
protected:
// This encapsulates the main differences between the Load methods of the
// derived classes. Each derived class's Load method just calls a different
// VmarLoader::Load<Policy> instantiaton. The PartialPagePolicy applies
// specifically to LoadInfo::DataWithZeroFillSegment segments where the
// segment's memsz > filesz and its vaddr + filesz is not page-aligned.
enum class PartialPagePolicy {
kProhibited, // vaddr + filesz must be page-aligned.
kCopyInProcess, // zx_vmo_read the partial page into zero-fill memory.
kZeroInVmo, // ZX_VMO_OP_ZERO the remainder after a COW partial page.
};
// This loads the segments according to the elfldltl::LoadInfo<...>
// instructions, mapping contents from the given VMO handle. It's the real
// initializer for the object, and other methods can only be used after it
// returns success (true). If it fails (returns false), the Diagnostics
// object gets calls with the error details, and the VmarLoader object should
// be destroyed promptly.
//
// After Load() returns, the caller must assume that the containing VMAR
// passed to the constructor may have new mappings whether the call succeeded
// or not. Any mappings made are cleared out by destruction of the VmarLoader
// object unless Commit() is called, see below.
template <PartialPagePolicy PartialPage, class Diagnostics, class LoadInfo>
[[nodiscard]] bool Load(Diagnostics& diag, const LoadInfo& load_info, zx::unowned_vmo vmo) {
if (!load_image_vmar_) {
// Allocate wasn't called yet, so do it now. If it returns true after
// failure because Diagnostics::SystemError returned true, then still
// bail out early but also return true.
if (bool ok = Allocate(diag, load_info); !ok || !load_image_vmar_) {
return ok;
}
}
VmoName base_name_storage = VmarLoader::GetVmoName(vmo->borrow());
std::string_view base_name = std::string_view(base_name_storage.data());
auto mapper = [this, vaddr_start = load_info.vaddr_start(), vmo, &diag, base_name,
num_data_segments = size_t{0},
num_zero_segments = size_t{0}](const auto& segment) mutable {
// Where in the VMAR the segment begins, accounting for load bias.
const uintptr_t vmar_offset = segment.vaddr() - vaddr_start;
// Let the Segment type choose a different VMO and offset if it wants to.
SegmentVmo<LoadInfo> segment_vmo{segment, vmo->borrow()};
zx::unowned_vmo map_vmo = segment_vmo.vmo();
const auto map_cow = segment_vmo.copy_on_write();
const uint64_t map_offset = segment_vmo.offset();
// The read-only ConstantSegment shares little in common with the others.
// The segment.writable() is statically true in the ConstantSegment
// instantiation of the mapper call operator, so the rest of the code
// will be compiled away.
if (!segment.writable()) {
// This is tautologically true in ConstantSegment.
ZX_DEBUG_ASSERT(segment.filesz() == segment.memsz());
zx_vm_option_t options = kVmCommon | //
(segment.readable() ? ZX_VM_PERM_READ : 0) |
(segment.executable() ? kVmExecutable : 0);
zx_status_t status =
Map(vmar_offset, options, map_vmo->borrow(), map_offset, segment.filesz());
if (status != ZX_OK) [[unlikely]] {
diag.SystemError("cannot map read-only segment", FileAddress{segment.vaddr()}, " from ",
FileOffset{segment.offset()}, ": ", ZirconError{status});
return false;
}
return true;
}
// All the writable segment types can be handled as if they were the most
// general case: DataWithZeroFillSegment. This comprises up to three
// regions, depending on the segment:
//
// [file pages]* [intersecting page]? [anon pages]*
//
// The DataSegment and ZeroFillSegment cases are statically just the
// first or the last, with no intersecting page. The arithmetic and if
// branches below will compile away in those instantiations of the mapper
// lambda to calling just MapWritable or just MapZeroFill.
//
// * "file pages" are present when filesz > 0, 'map_size' below.
// * "anon pages" are present when memsz > filesz, 'zero_size' below.
// * "intersecting page" exists when both file pages and anon pages
// exist, and file pages are not an exact multiple of pagesize. At most
// a SINGLE intersecting page exists. It is represented by 'copy_size'
// below.
//
// IMPLEMENTATION NOTE: The VmarLoader performs only two mappings.
// * Mapping file pages up to the last full page of file data.
// * Mapping zero-fill pages, including the intersecting page, to the
// * end of the segment.
//
// TODO(https://fxbug.dev/42172744): Support mapping objects into VMAR from out of
// process.
//
// After the second mapping, the VmarLoader then reads in the partial
// file data into the intersecting page.
//
// The alternative would be to map filesz page rounded up into memory and
// then zero out the zero fill portion of the intersecting page. This
// isn't preferable because we would immediately cause a page fault and
// spend time zero'ing a page when the OS may already have copied this
// page for us.
// This is tautologically zero in ZeroFillSegment.
size_t map_size = segment.filesz();
size_t zero_size = 0;
size_t copy_size = 0;
// This is tautologically false in DataSegment. In ZeroFillSegment
// map_size is statically zero so copy_size statically stays zero too.
if (segment.memsz() > segment.filesz()) {
switch (PartialPage) {
case PartialPagePolicy::kProhibited:
// MapWritable will assert that map_size is aligned.
zero_size = segment.memsz() - map_size;
break;
case PartialPagePolicy::kCopyInProcess:
// Round down what's mapped, and copy the difference.
copy_size = map_size & (page_size() - 1);
map_size &= -page_size();
// Zero the remaining whole pages.
zero_size = segment.memsz() - map_size;
break;
case PartialPagePolicy::kZeroInVmo:
// MapWritable will round up what's mapped and zero the difference.
// Just zero the remaining whole pages.
zero_size = segment.memsz() - ((map_size + page_size() - 1) & -page_size());
break;
}
}
// First map data from the file, if any.
if (map_size > 0) {
zx_status_t status = MapWritable<PartialPage == PartialPagePolicy::kZeroInVmo>(
vmar_offset, map_vmo->borrow(), map_cow, base_name, map_offset, map_size,
num_data_segments);
if (status != ZX_OK) [[unlikely]] {
diag.SystemError("cannot map writable segment", FileAddress{segment.vaddr()},
" from file", FileOffset{map_offset}, ": ", ZirconError{status});
return false;
}
}
// Then map zero-fill data, if any.
if (zero_size > 0) {
if constexpr (PartialPage == PartialPagePolicy::kZeroInVmo) {
// MapWritable<ZeroInVmo=true> will have rounded up its true mapping
// size and zeroed the trailing partial page. So skip over that true
// mapping size to the first page that's entirely zero-fill.
map_size = (map_size + page_size() - 1) & -page_size();
} else {
// This should have been rounded down earlier.
ZX_DEBUG_ASSERT((map_size & (page_size() - 1)) == 0);
}
const uintptr_t zero_fill_vmar_offset = vmar_offset + map_size;
zx_status_t status =
MapZeroFill(zero_fill_vmar_offset, base_name, zero_size, num_zero_segments);
if (status != ZX_OK) [[unlikely]] {
diag.SystemError("cannot map zero-fill pages", FileAddress{segment.vaddr() + map_size},
": ", ZirconError{status});
return false;
}
// Finally, copy the partial intersecting page, if any.
if constexpr (PartialPage != PartialPagePolicy::kCopyInProcess) {
ZX_DEBUG_ASSERT(copy_size == 0);
} else if (copy_size > 0) {
const size_t copy_offset = map_offset + map_size;
void* const copy_data =
reinterpret_cast<void*>(vaddr_start + zero_fill_vmar_offset + load_bias_);
zx_status_t status = map_vmo->read(copy_data, copy_offset, copy_size);
if (status != ZX_OK) [[unlikely]] {
diag.SystemError("cannot read segment data from file", FileOffset{copy_offset}, ": ",
ZirconError{status});
return false;
}
}
}
return true;
};
return load_info.VisitSegments(mapper);
}
private:
using VmoName = std::array<char, ZX_MAX_NAME_LEN>;
static VmoName GetVmoName(zx::unowned_vmo vmo);
// Permissions on Fuchsia are trickier than Linux, so read these comments
// carefully if you are encountering permission issues!
//
// * Every segment is mapped to a specific location in the child VMAR. The
// permission to do so is given by the ZX_VM_CAN_MAP_SPECIFIC
// permission on the fresh allocation.
//
// * ZX_VM_ALLOW_FAULTS is required by the kernel when mapping resizable
// or pager-backed VMOs, which we might be.
static constexpr zx_vm_option_t kVmCommon = ZX_VM_SPECIFIC | ZX_VM_ALLOW_FAULTS;
// Execute-only pages may or may not be available on the system.
// The READ_IF_XOM_UNSUPPORTED gives the system license to make the
// pages readable as well if execute-only was specified but can't
// be honored.
static constexpr zx_vm_option_t kVmExecutable =
ZX_VM_PERM_EXECUTE | ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED;
// Segment types other than ConstantSegment always use R/W permissions.
static constexpr zx_vm_option_t kMapWritable = kVmCommon | ZX_VM_PERM_READ | ZX_VM_PERM_WRITE;
// Allocate a contiguous address space region to hold all the segments and
// store its handle in load_image_vmar_. The base address of the region is
// chosen by the kernel, which can do ASLR, unless specified by the optional
// vmar_offset argument (relative to the parent VMAR given at construction).
// The load_image_vmar_ and load_bias_ members are updated.
zx_status_t AllocateVmar(size_t vaddr_size, size_t vaddr_start,
std::optional<size_t> vmar_offset);
zx_status_t Map(uintptr_t vmar_offset, zx_vm_option_t options, zx::unowned_vmo vmo,
uint64_t vmo_offset, size_t size) {
zx_vaddr_t vaddr;
return load_image_vmar_.map(options, vmar_offset, *vmo, vmo_offset, size, &vaddr);
}
template <bool ZeroPartialPage>
zx_status_t MapWritable(uintptr_t vmar_offset, zx::unowned_vmo vmo, bool copy_vmo,
std::string_view base_name, uint64_t vmo_offset, size_t size,
size_t& num_data_segments);
zx_status_t MapZeroFill(uintptr_t vmar_offset, std::string_view base_name, size_t size,
size_t& num_zero_segments);
// The region of the address space that the module is being loaded into. The
// load_image_vmar_ is initialized during loading and only cleared by either
// Commit() or destruction.
//
// The VMAR handle must be preserved because some Fuchsia syscalls that act
// on VMARs, like modifying protections, cannot be applied through parent
// regions.
zx::vmar load_image_vmar_;
// This is the root VMAR that the mapping is placed into.
zx::unowned_vmar vmar_;
zx_vaddr_t load_bias_ = 0;
};
/// elfldltl::LocalVmarLoader performs loading within the current process only.
/// See VmarLoader above for the primary API details. LocalVmarLoader can be
/// default-constructed to place the module inside the root VMAR. It also
/// provides the memory() method for access to the image after Load.
class LocalVmarLoader : public VmarLoader {
public:
explicit LocalVmarLoader(const zx::vmar& vmar = *zx::vmar::root_self()) : VmarLoader(vmar) {}
LocalVmarLoader(LocalVmarLoader&& other) noexcept
: VmarLoader(static_cast<VmarLoader&&>(other)),
memory_(other.memory_.image(), other.memory_.base()) {
other.memory().set_image({});
}
LocalVmarLoader& operator=(LocalVmarLoader&& other) noexcept {
memory_.set_image(other.memory_.image());
memory_.set_base(other.memory_.base());
VmarLoader::operator=(static_cast<VmarLoader&&>(other));
other.memory_.set_image({});
return *this;
}
template <class Diagnostics, class LoadInfo>
[[nodiscard]] bool Load(Diagnostics& diag, const LoadInfo& load_info, zx::unowned_vmo vmo) {
if (!VmarLoader::Load<PartialPagePolicy::kCopyInProcess>(diag, load_info, vmo->borrow())) {
return false;
}
const uintptr_t image = load_info.vaddr_start() + load_bias();
memory_.set_image({reinterpret_cast<std::byte*>(image), load_info.vaddr_size()});
memory_.set_base(load_info.vaddr_start());
return true;
}
// This returns the DirectMemory of the mapping created by Load(). It should
// not be used after destruction or after Commit(). If Commit() has been
// called before destruction then the address range will continue to be
// usable, in which case one should save memory().image() before Commit().
DirectMemory& memory() { return memory_; }
private:
DirectMemory memory_;
};
/// elfldltl::RemoteVmarLoader performs loading in any process, given a VMAR
/// (that process's root VMAR or a smaller region). See VmarLoader above for
/// the primary API details. RemoteVmarLoader works fine for the current
/// process too, but LocalVmarLoader may optimize that case better.
class RemoteVmarLoader : public VmarLoader {
public:
// Can be default-constructed or constructed with a const zx::vmar& argument.
using VmarLoader::VmarLoader;
template <class Diagnostics, class LoadInfo>
[[nodiscard]] bool Load(Diagnostics& diag, const LoadInfo& load_info, zx::unowned_vmo vmo) {
return VmarLoader::Load<PartialPagePolicy::kZeroInVmo>(diag, load_info, vmo->borrow());
}
};
/// elfldltl::AlignedRemoteVmarLoader is like elfldltl::RemoteVmarLoader but it
/// expects that any DataWithZeroFillSegment segments have been prepared to
/// have a page-aligned filesz() by zero-filling the final partial page of
/// the data portion in the VMO.
class AlignedRemoteVmarLoader : public VmarLoader {
public:
// Can be default-constructed or constructed with a const zx::vmar& argument.
using VmarLoader::VmarLoader;
template <class Diagnostics, class LoadInfo>
[[nodiscard]] bool Load(Diagnostics& diag, const LoadInfo& load_info, zx::unowned_vmo vmo) {
return VmarLoader::Load<PartialPagePolicy::kProhibited>(diag, load_info, vmo->borrow());
}
};
// Both possible MapWritable instantiations are defined in vmar-loader.cc.
extern template zx_status_t VmarLoader::MapWritable<false>( //
uintptr_t vmar_offset, zx::unowned_vmo vmo, bool copy_vmo, std::string_view base_name,
uint64_t vmo_offset, size_t size, size_t& num_data_segments);
extern template zx_status_t VmarLoader::MapWritable<true>( //
uintptr_t vmar_offset, zx::unowned_vmo vmo, bool copy_vmo, std::string_view base_name,
uint64_t vmo_offset, size_t size, size_t& num_data_segments);
} // namespace elfldltl
#endif // SRC_LIB_ELFLDLTL_INCLUDE_LIB_ELFLDLTL_VMAR_LOADER_H_