blob: ada4d3c30ffe7992c12024187f26b97b7cc1adf0 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#include <assert.h>
#include <lib/counters.h>
#include <lib/iob/blob-id-allocator.h>
#include <lib/user_copy/user_ptr.h>
#include <lib/zx/result.h>
#include <string.h>
#include <zircon/errors.h>
#include <zircon/rights.h>
#include <zircon/syscalls/iob.h>
#include <zircon/syscalls/object.h>
#include <zircon/types.h>
#include <cstdint>
#include <fbl/alloc_checker.h>
#include <fbl/array.h>
#include <fbl/ref_ptr.h>
#include <kernel/koid.h>
#include <kernel/lockdep.h>
#include <kernel/mutex.h>
#include <ktl/move.h>
#include <object/dispatcher.h>
#include <object/handle.h>
#include <object/io_buffer_dispatcher.h>
#include <object/vm_object_dispatcher.h>
#include <vm/pinned_vm_object.h>
#include <vm/pmm.h>
#include <vm/vm_address_region.h>
#include <vm/vm_object.h>
#include <ktl/enforce.h>
#define LOCAL_TRACE 0
KCOUNTER(dispatcher_iob_create_count, "dispatcher.iob.create")
KCOUNTER(dispatcher_iob_destroy_count, "dispatcher.iob.destroy")
// static
zx::result<fbl::Array<IoBufferDispatcher::IobRegionVariant>> IoBufferDispatcher::CreateRegions(
const IoBufferDispatcher::RegionArray& region_configs, VmObjectChildObserver* ep0,
VmObjectChildObserver* ep1) {
fbl::AllocChecker ac;
fbl::Array<IobRegionVariant> region_inner =
fbl::MakeArray<IobRegionVariant>(&ac, region_configs.size());
if (!ac.check()) {
return zx::error(ZX_ERR_NO_MEMORY);
}
for (unsigned i = 0; i < region_configs.size(); i++) {
zx_iob_region_t region_config = region_configs[i];
if (region_config.type != ZX_IOB_REGION_TYPE_PRIVATE) {
return zx::error(ZX_ERR_INVALID_ARGS);
}
if (region_config.access == 0) {
return zx::error(ZX_ERR_INVALID_ARGS);
}
// A note on resource management:
//
// Everything we allocate in this loop ultimately gets owned by the SharedIobState and will be
// cleaned up when both PeeredDispatchers are destroyed and drop their reference to the
// SharedIobState.
//
// However, there is a complication. The VmoChildObservers have a reference to the dispatchers
// which creates a cycle: IoBufferDispatcher -> SharedIobState -> IobRegion -> VmObject ->
// VmoChildObserver -> IoBufferDispatcher.
//
// Since the VmoChildObservers keep a raw pointers to the dispatchers, we need to be sure to
// reset the the corresponding pointers when we destroy an IoBufferDispatcher. Otherwise when an
// IoBufferDispatcher maps a region, it could try to update a destroyed peer.
//
// See: IoBufferDispatcher~IoBufferDispatcher.
// We effectively duplicate the logic from sys_vmo_create here, but instead of creating a
// kernel handle and dispatcher, we keep ownership of it and assign it to a region.
zx::result<VmObjectDispatcher::CreateStats> parse_result =
VmObjectDispatcher::parse_create_syscall_flags(region_config.private_region.options,
region_config.size);
if (parse_result.is_error()) {
return zx::error(parse_result.error_value());
}
VmObjectDispatcher::CreateStats stats = parse_result.value();
fbl::RefPtr<VmObjectPaged> vmo;
if (zx_status_t status =
VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, stats.flags, stats.size, &vmo);
status != ZX_OK) {
return zx::error(status);
}
// parse_create_syscall_flags will round up the size to the nearest page, or set the size to the
// maximum possible VMO size if ZX_VMO_UNBOUNDED is used. We need to know the actual size of the
// vmo to later return if asked.
region_config.size = stats.size;
zx_koid_t koid = KernelObjectId::Generate();
vmo->set_user_id(koid);
// In order to track mappings and unmappings separately for each endpoint, we give each
// endpoint a child reference instead of the created vmo.
fbl::RefPtr<VmObject> ep0_reference;
fbl::RefPtr<VmObject> ep1_reference;
Resizability resizability =
vmo->is_resizable() ? Resizability::Resizable : Resizability::NonResizable;
if (zx_status_t status =
vmo->CreateChildReference(resizability, 0, 0, true, nullptr, &ep0_reference);
status != ZX_OK) {
return zx::error(status);
}
if (zx_status_t status =
vmo->CreateChildReference(resizability, 0, 0, true, nullptr, &ep1_reference);
status != ZX_OK) {
return zx::error(status);
}
ep0_reference->set_user_id(koid);
ep1_reference->set_user_id(koid);
// Now each endpoint can observe the mappings created by the other
ep0_reference->SetChildObserver(ep1);
ep1_reference->SetChildObserver(ep0);
if (zx::result result =
CreateIobRegionVariant(ktl::move(ep0_reference), ktl::move(ep1_reference),
ktl::move(vmo), region_config, koid);
result.is_error()) {
return result.take_error();
} else {
region_inner[i] = ktl::move(result.value());
}
}
return zx::ok(ktl::move(region_inner));
}
// static
zx_status_t IoBufferDispatcher::Create(uint64_t options,
const IoBufferDispatcher::RegionArray& region_configs,
KernelHandle<IoBufferDispatcher>* handle0,
KernelHandle<IoBufferDispatcher>* handle1,
zx_rights_t* rights) {
if (region_configs.size() == 0) {
return ZX_ERR_INVALID_ARGS;
}
fbl::AllocChecker ac;
auto holder0 = fbl::AdoptRef(new (&ac) PeerHolder<IoBufferDispatcher>());
if (!ac.check()) {
return ZX_ERR_NO_MEMORY;
}
auto holder1 = holder0;
fbl::RefPtr<SharedIobState> shared_regions = fbl::AdoptRef(new (&ac) SharedIobState{
.regions = nullptr,
});
if (!ac.check()) {
return ZX_ERR_NO_MEMORY;
}
KernelHandle new_handle0(fbl::AdoptRef(
new (&ac) IoBufferDispatcher(ktl::move(holder0), IobEndpointId::Ep0, shared_regions)));
if (!ac.check()) {
return ZX_ERR_NO_MEMORY;
}
KernelHandle new_handle1(fbl::AdoptRef(
new (&ac) IoBufferDispatcher(ktl::move(holder1), IobEndpointId::Ep1, shared_regions)));
if (!ac.check()) {
return ZX_ERR_NO_MEMORY;
}
zx::result<fbl::Array<IobRegionVariant>> regions =
CreateRegions(region_configs, new_handle0.dispatcher().get(), new_handle1.dispatcher().get());
if (regions.is_error()) {
return regions.error_value();
}
{
Guard<CriticalMutex> guard{&shared_regions->state_lock};
shared_regions->regions = *ktl::move(regions);
}
new_handle0.dispatcher()->InitPeer(new_handle1.dispatcher());
new_handle1.dispatcher()->InitPeer(new_handle0.dispatcher());
*rights = default_rights();
*handle0 = ktl::move(new_handle0);
*handle1 = ktl::move(new_handle1);
return ZX_OK;
}
IoBufferDispatcher::IoBufferDispatcher(fbl::RefPtr<PeerHolder<IoBufferDispatcher>> holder,
IobEndpointId endpoint_id,
fbl::RefPtr<SharedIobState> shared_state)
: PeeredDispatcher(ktl::move(holder)),
shared_state_(ktl::move(shared_state)),
endpoint_id_(endpoint_id) {
kcounter_add(dispatcher_iob_create_count, 1);
}
zx_rights_t IoBufferDispatcher::GetMapRights(zx_rights_t iob_rights, size_t region_index) const {
zx_rights_t region_rights =
shared_state_->GetRegion<>(region_index)->GetMapRights(GetEndpointId());
region_rights &= (iob_rights | ZX_RIGHT_MAP);
return region_rights;
}
zx_rights_t IoBufferDispatcher::GetMediatedRights(size_t region_index) const {
return shared_state_->GetRegion<>(region_index)->GetMediatedRights(GetEndpointId());
}
const fbl::RefPtr<VmObject>& IoBufferDispatcher::GetVmo(size_t region_index) const {
return shared_state_->GetRegion<>(region_index)->GetVmo(GetEndpointId());
}
zx_iob_region_t IoBufferDispatcher::GetRegion(size_t region_index) const {
return shared_state_->GetRegion<>(region_index)->region();
}
size_t IoBufferDispatcher::RegionCount() const {
canary_.Assert();
Guard<CriticalMutex> guard{&shared_state_->state_lock};
return shared_state_->regions.size();
}
void IoBufferDispatcher::on_zero_handles_locked() { canary_.Assert(); }
void IoBufferDispatcher::OnPeerZeroHandlesLocked() {
canary_.Assert();
peer_zero_handles_ = true;
if (peer_mapped_regions_ == 0) {
UpdateStateLocked(0, ZX_IOB_PEER_CLOSED);
}
}
void IoBufferDispatcher::OnZeroChild() {
Guard<CriticalMutex> guard{get_lock()};
// OnZeroChildren gets called every time the number of children is equal to zero. Due to races, by
// the time this method is called we could already have children again. This is fine, since all
// we need to do is ensure that every increment of peer_mapped_regions_ is paired with a
// decrement.
DEBUG_ASSERT(peer_mapped_regions_ > 0);
peer_mapped_regions_--;
if (peer_mapped_regions_ == 0 && peer_zero_handles_) {
UpdateStateLocked(0, ZX_IOB_PEER_CLOSED);
}
}
IoBufferDispatcher::~IoBufferDispatcher() {
// The other endpoint's vmos are set up to notify this endpoint when they map regions via a raw
// pointer. Since we're about to be destroyed, we need to unregister.
//
// VMObject unregisters in when the last handle is closed, but we do it a bit differently here.
//
// If we unregister in OnPeerZeroHandlesLocked, then we update the ChildObserver, we create the
// dependency get_lock() -> child_observer_lock_. Then when we OnZeroChild is called, we first get
// the child_observer_lock_, then try to update the state, locking get_lock establishing
// child_observer_lock_ -> get_lock() which could lead to some lock ordering issues.
//
// However, cleaning up in the destructor means that we could be notified while we are being
// destroyed. This should be okay because:
// - Each vmo's child observer is protected by its child_observer_lock_.
// - While we are in this destructor, a notification may attempt to get a vmo's.
// child_observer_lock_.
// - If we have already destroyed updated the observer, it will see a nullptr and not access our
// state.
// - If we have not already destroyed the observer, it will notify us while continuing to hold the
// child observer lock.
// - If we attempt to reset the observer that is notifying us, we will block until it is completed
// and releases the lock.
// - Thus we cannot continue to the Destruction Sequence until there are no observers in progress
// accessing our state, and no observer can start accessing our state.
Guard<CriticalMutex> guard{&shared_state_->state_lock};
IobEndpointId other_id =
endpoint_id_ == IobEndpointId::Ep0 ? IobEndpointId::Ep1 : IobEndpointId::Ep0;
for (size_t i = 0; i < shared_state_->regions.size(); ++i) {
if (const fbl::RefPtr<VmObject>& vmo = shared_state_->GetRegionLocked<>(i)->GetVmo(other_id);
vmo != nullptr) {
vmo->SetChildObserver(nullptr);
}
}
kcounter_add(dispatcher_iob_destroy_count, 1);
}
zx_info_iob_t IoBufferDispatcher::GetInfo() const {
canary_.Assert();
Guard<CriticalMutex> guard{&shared_state_->state_lock};
return {.options = 0, .region_count = static_cast<uint32_t>(shared_state_->regions.size())};
}
zx_iob_region_info_t IoBufferDispatcher::GetRegionInfo(size_t index) const {
canary_.Assert();
return shared_state_->GetRegion<>(index)->GetRegionInfo(endpoint_id_ == IobEndpointId::Ep1);
}
zx_status_t IoBufferDispatcher::get_name(char (&out_name)[ZX_MAX_NAME_LEN]) const {
shared_state_->GetRegion<>(0)->GetVmo(IobEndpointId::Ep0)->get_name(out_name, ZX_MAX_NAME_LEN);
return ZX_OK;
}
zx_status_t IoBufferDispatcher::set_name(const char* name, size_t len) {
canary_.Assert();
Guard<CriticalMutex> guard{&shared_state_->state_lock};
for (size_t i = 0; i < shared_state_->regions.size(); ++i) {
const IobRegion* region = shared_state_->GetRegionLocked<>(i);
switch (region->region().type) {
case ZX_IOB_REGION_TYPE_PRIVATE: {
zx_status_t region_result = region->GetVmo(IobEndpointId::Ep0)->set_name(name, len);
if (region_result != ZX_OK) {
return region_result;
}
region_result = region->GetVmo(IobEndpointId::Ep1)->set_name(name, len);
if (region_result != ZX_OK) {
return region_result;
}
break;
}
default:
continue;
}
}
return ZX_OK;
}
zx::result<fbl::RefPtr<VmObject>> IoBufferDispatcher::CreateMappableVmoForRegion(
size_t region_index) {
fbl::RefPtr<VmObject> child_reference;
const fbl::RefPtr<VmObject>& vmo = GetVmo(region_index);
Resizability resizability =
vmo->is_resizable() ? Resizability::Resizable : Resizability::NonResizable;
{
bool first_child = false;
zx_status_t status =
vmo->CreateChildReference(resizability, 0, 0, true, &first_child, &child_reference);
if (status != ZX_OK) {
return zx::error(status);
}
if (first_child) {
// Need to record 0->1 transition. As we are holding a refptr to the child we know that
// OnZeroChildren cannot get called to decrement before we can increment. In this case the
// lock is not attempting to synchronize anything beyond the raw access of
// peer_mapped_regions_.
Guard<CriticalMutex> guard{get_lock()};
const fbl::RefPtr<IoBufferDispatcher>& p = peer();
AssertHeld(*p->get_lock());
p->peer_mapped_regions_++;
}
}
child_reference->set_user_id(vmo->user_id());
return zx::ok(child_reference);
}
zx::result<uint32_t> IoBufferDispatcher::AllocateId(size_t region_index,
user_in_ptr<const ktl::byte> blob_ptr,
size_t blob_size) {
if (region_index >= RegionCount()) {
return zx::error{ZX_ERR_OUT_OF_RANGE};
}
if (auto* region = shared_state_->GetRegion<IobRegionIdAllocator>(region_index)) {
return region->AllocateId(GetEndpointId(), blob_ptr, blob_size);
}
return zx::error{ZX_ERR_WRONG_TYPE};
}
// static
zx::result<IoBufferDispatcher::IobRegionVariant> IoBufferDispatcher::CreateIobRegionVariant(
fbl::RefPtr<VmObject> ep0_vmo, //
fbl::RefPtr<VmObject> ep1_vmo, //
fbl::RefPtr<VmObject> kernel_vmo, //
const zx_iob_region_t& region, //
zx_koid_t koid) {
constexpr zx_iob_access_t kEp0MedR = ZX_IOB_ACCESS_EP0_CAN_MEDIATED_READ;
constexpr zx_iob_access_t kEp0MedW = ZX_IOB_ACCESS_EP0_CAN_MEDIATED_WRITE;
constexpr zx_iob_access_t kEp0MapW = ZX_IOB_ACCESS_EP0_CAN_MAP_WRITE;
constexpr zx_iob_access_t kEp1MedR = ZX_IOB_ACCESS_EP1_CAN_MEDIATED_READ;
constexpr zx_iob_access_t kEp1MedW = ZX_IOB_ACCESS_EP1_CAN_MEDIATED_WRITE;
constexpr zx_iob_access_t kEp1MapW = ZX_IOB_ACCESS_EP1_CAN_MAP_WRITE;
zx_iob_access_t mediated0 = region.access & (kEp0MedR | kEp0MedW);
zx_iob_access_t mediated1 = region.access & (kEp1MedR | kEp1MedW);
bool mediated = mediated0 != 0 || mediated1 != 0;
bool map_writable = (region.access & kEp0MapW) != 0 || (region.access & kEp1MapW) != 0;
// The region memory must be directly accessible by userspace or the kernel.
if (!map_writable && !mediated) {
return zx::error{ZX_ERR_INVALID_ARGS};
}
fbl::RefPtr<VmMapping> mapping;
zx_vaddr_t base = 0;
if (mediated) {
const char* mapping_name;
switch (region.discipline.type) {
case ZX_IOB_DISCIPLINE_TYPE_ID_ALLOCATOR:
// If an ID allocator endpoint requests mediated access, that access must at
// least admit mediated writes. Equivalently, no ID allocator endpoint can
// be read-only in terms of mediated access.
if (mediated0 == kEp0MedR || mediated1 == kEp1MedR) {
return zx::error{ZX_ERR_INVALID_ARGS};
}
mapping_name = "IOBuffer region (ID allocator)";
break;
case ZX_IOB_DISCIPLINE_TYPE_NONE:
// NONE type discipline does not support mediated access.
[[fallthrough]];
default:
// Unknown discipline.
return zx::error{ZX_ERR_INVALID_ARGS};
}
// Create a mapping of this VMO in the kernel aspace. It is convenient to
// create the mapping object first before any discipline-specific pinning
// and mapping is actually performed, allowing that to be done in
// subclasses, so we pass `VMAR_FLAG_DEBUG_DYNAMIC_KERNEL_MAPPING`.
fbl::RefPtr<VmAddressRegion> kernel_vmar =
VmAspace::kernel_aspace()->RootVmar()->as_vm_address_region();
zx::result<VmAddressRegion::MapResult> result = kernel_vmar->CreateVmMapping(
0, region.size, 0, VMAR_FLAG_DEBUG_DYNAMIC_KERNEL_MAPPING, ktl::move(kernel_vmo), 0,
ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE, mapping_name);
if (result.is_error()) {
return result.take_error();
}
mapping = ktl::move(result.value().mapping);
base = result.value().base;
}
IobRegionVariant variant;
switch (region.discipline.type) {
case ZX_IOB_DISCIPLINE_TYPE_NONE:
variant = IobRegionNone{
ktl::move(ep0_vmo), ktl::move(ep1_vmo), ktl::move(mapping), base, region, koid};
break;
case ZX_IOB_DISCIPLINE_TYPE_ID_ALLOCATOR: {
IobRegionIdAllocator allocator{
ktl::move(ep0_vmo), ktl::move(ep1_vmo), ktl::move(mapping), base, region, koid};
if (zx::result result = allocator.Init(); result.is_error()) {
return result.take_error();
}
variant = ktl::move(allocator);
break;
}
default:
// Unknown discipline.
return zx::error{ZX_ERR_INVALID_ARGS};
}
return zx::ok(ktl::move(variant));
}
zx::result<> IoBufferDispatcher::IobRegionIdAllocator::Init() {
AssertDiscipline(ZX_IOB_DISCIPLINE_TYPE_ID_ALLOCATOR);
if (!mapping()) {
return zx::ok();
}
zx_status_t status =
PinnedVmObject::Create(mapping()->vmo(), 0, region().size, /*write=*/true, &pin_);
if (status != ZX_OK) {
return zx::error{status};
}
status = mapping()->MapRange(0, region().size, /*commit=*/true);
if (status != ZX_OK) {
return zx::error{status};
}
iob::BlobIdAllocator allocator(mediated_bytes());
allocator.Init(/*zero_fill=*/false); // A fresh VMO, so already zero-filled.
return zx::ok();
}
zx::result<uint32_t> IoBufferDispatcher::IobRegionIdAllocator::AllocateId(
IobEndpointId id, user_in_ptr<const ktl::byte> blob_ptr, size_t blob_size) {
AssertDiscipline(ZX_IOB_DISCIPLINE_TYPE_ID_ALLOCATOR);
zx_rights_t mediated_rights = GetMediatedRights(id);
if ((mediated_rights & ZX_RIGHT_WRITE) == 0) {
return zx::error{ZX_ERR_ACCESS_DENIED};
}
iob::BlobIdAllocator allocator(mediated_bytes());
zx_status_t copy_status = ZX_OK;
auto result = allocator.Allocate(
[&copy_status](auto blob_ptr, ktl::span<ktl::byte> dest) {
copy_status = blob_ptr.copy_array_from_user(dest.data(), dest.size());
},
blob_ptr, blob_size);
if (copy_status != ZX_OK) {
return zx::error{copy_status};
}
if (result.is_error()) {
switch (result.error_value()) {
case iob::BlobIdAllocator::AllocateError::kOutOfMemory:
return zx::error{ZX_ERR_NO_MEMORY};
default:
return zx::error{ZX_ERR_IO_DATA_INTEGRITY};
}
}
return zx::ok(result.value());
}