| // Copyright 2017 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 "object/pinned_memory_token_dispatcher.h" |
| |
| #include <align.h> |
| #include <assert.h> |
| #include <lib/counters.h> |
| #include <trace.h> |
| #include <zircon/errors.h> |
| #include <zircon/types.h> |
| |
| #include <new> |
| |
| #include <fbl/auto_lock.h> |
| #include <ktl/algorithm.h> |
| #include <ktl/bit.h> |
| #include <object/bus_transaction_initiator_dispatcher.h> |
| #include <vm/pinned_vm_object.h> |
| #include <vm/vm.h> |
| #include <vm/vm_object.h> |
| |
| #include <ktl/enforce.h> |
| |
| #define LOCAL_TRACE 0 |
| |
| KCOUNTER(dispatcher_pinned_memory_token_create_count, "dispatcher.pinned_memory_token.create") |
| KCOUNTER(dispatcher_pinned_memory_token_destroy_count, "dispatcher.pinned_memory_token.destroy") |
| |
| zx_status_t PinnedMemoryTokenDispatcher::Create(fbl::RefPtr<BusTransactionInitiatorDispatcher> bti, |
| PinnedVmObject pinned_vmo, uint32_t perms, |
| KernelHandle<PinnedMemoryTokenDispatcher>* handle, |
| zx_rights_t* rights) { |
| LTRACE_ENTRY; |
| DEBUG_ASSERT(IS_PAGE_ALIGNED(pinned_vmo.offset()) && IS_PAGE_ALIGNED(pinned_vmo.size())); |
| |
| size_t num_addrs; |
| if (!pinned_vmo.vmo()->is_contiguous()) { |
| const size_t min_contig = bti->minimum_contiguity(); |
| DEBUG_ASSERT(ktl::has_single_bit(min_contig)); |
| |
| num_addrs = ROUNDUP(pinned_vmo.size(), min_contig) / min_contig; |
| } else { |
| num_addrs = 1; |
| } |
| |
| fbl::AllocChecker ac; |
| fbl::Array<dev_vaddr_t> addr_array(new (&ac) dev_vaddr_t[num_addrs], num_addrs); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| KernelHandle new_handle(fbl::AdoptRef(new (&ac) PinnedMemoryTokenDispatcher( |
| ktl::move(bti), ktl::move(pinned_vmo), ktl::move(addr_array)))); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| zx_status_t status = new_handle.dispatcher()->MapIntoIommu(perms); |
| if (status != ZX_OK) { |
| LTRACEF("MapIntoIommu failed: %d\n", status); |
| return status; |
| } |
| |
| // Create must be called with the BTI's lock held, so this is safe to |
| // invoke. |
| const fbl::RefPtr<PinnedMemoryTokenDispatcher>& dispatcher = new_handle.dispatcher(); |
| AssertHeld(*dispatcher->bti_->get_lock()); |
| dispatcher->bti_->AddPmoLocked(new_handle.dispatcher().get()); |
| dispatcher->initialized_ = true; |
| |
| *handle = ktl::move(new_handle); |
| *rights = default_rights(); |
| return ZX_OK; |
| } |
| |
| // Used during initialization to set up the IOMMU state for this PMT. |
| // |
| // We disable thread-safety analysis here, because this is part of the |
| // initialization routine before other threads have access to this dispatcher. |
| zx_status_t PinnedMemoryTokenDispatcher::MapIntoIommu(uint32_t perms) TA_NO_THREAD_SAFETY_ANALYSIS { |
| DEBUG_ASSERT(!initialized_); |
| |
| const uint64_t bti_id = bti_->bti_id(); |
| const size_t min_contig = bti_->minimum_contiguity(); |
| if (pinned_vmo_.vmo()->is_contiguous()) { |
| dev_vaddr_t vaddr; |
| size_t mapped_len; |
| |
| // Usermode drivers assume that if they requested a contiguous buffer in |
| // memory, then the physical addresses will be contiguous. Return an |
| // error if we can't actually map the address contiguously. |
| zx_status_t status = |
| bti_->iommu().MapContiguous(bti_id, pinned_vmo_.vmo(), pinned_vmo_.offset(), |
| pinned_vmo_.size(), perms, &vaddr, &mapped_len); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| DEBUG_ASSERT(vaddr % min_contig == 0); |
| DEBUG_ASSERT(mapped_addrs_.size() == 1); |
| mapped_addrs_[0] = vaddr; |
| return ZX_OK; |
| } |
| |
| size_t remaining = pinned_vmo_.size(); |
| uint64_t curr_offset = pinned_vmo_.offset(); |
| size_t next_addr_idx = 0; |
| while (remaining > 0) { |
| dev_vaddr_t vaddr; |
| size_t mapped_len; |
| zx_status_t status = bti_->iommu().Map(bti_id, pinned_vmo_.vmo(), curr_offset, remaining, perms, |
| &vaddr, &mapped_len); |
| if (status != ZX_OK) { |
| // We'll revert any partial mappings in on_zero_handles(). |
| return status; |
| } |
| |
| // Ensure we don't end up with any non-terminal chunks that are not |min_contig| in |
| // length. |
| DEBUG_ASSERT(mapped_len % min_contig == 0 || remaining == mapped_len); |
| |
| // Break the range up into chunks of length |min_contig| |
| size_t mapped_remaining = mapped_len; |
| while (mapped_remaining > 0) { |
| size_t addr_pages = ktl::min<size_t>(mapped_remaining, min_contig); |
| mapped_addrs_[next_addr_idx] = vaddr; |
| next_addr_idx++; |
| vaddr += addr_pages; |
| mapped_remaining -= addr_pages; |
| } |
| |
| curr_offset += mapped_len; |
| remaining -= ktl::min(mapped_len, remaining); |
| } |
| DEBUG_ASSERT(next_addr_idx == mapped_addrs_.size()); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t PinnedMemoryTokenDispatcher::UnmapFromIommuLocked() { |
| auto& iommu = bti_->iommu(); |
| const uint64_t bus_txn_id = bti_->bti_id(); |
| |
| if (mapped_addrs_[0] == UINT64_MAX) { |
| // No work to do, nothing is mapped. |
| return ZX_OK; |
| } |
| |
| zx_status_t status = ZX_OK; |
| if (pinned_vmo_.vmo()->is_contiguous()) { |
| status = iommu.Unmap(bus_txn_id, mapped_addrs_[0], pinned_vmo_.size()); |
| } else { |
| const size_t min_contig = bti_->minimum_contiguity(); |
| size_t remaining = pinned_vmo_.size(); |
| for (size_t i = 0; i < mapped_addrs_.size(); ++i) { |
| dev_vaddr_t addr = mapped_addrs_[i]; |
| if (addr == UINT64_MAX) { |
| break; |
| } |
| |
| size_t size = ktl::min(remaining, min_contig); |
| DEBUG_ASSERT(size == min_contig || i == mapped_addrs_.size() - 1); |
| // Try to unmap all pages even if we get an error, and return the |
| // first error encountered. |
| zx_status_t err = iommu.Unmap(bus_txn_id, addr, size); |
| DEBUG_ASSERT(err == ZX_OK); |
| if (err != ZX_OK && status == ZX_OK) { |
| status = err; |
| } |
| remaining -= size; |
| } |
| } |
| |
| return status; |
| } |
| |
| void PinnedMemoryTokenDispatcher::Unpin() { |
| Guard<Mutex> guard{get_lock()}; |
| explicitly_unpinned_ = true; |
| |
| // Unmap the memory prior to unpinning to prevent continued access. |
| zx_status_t status = UnmapFromIommuLocked(); |
| ASSERT(status == ZX_OK); |
| |
| // Move the pinned vmo to a temporary and to let its dtor unpin. |
| auto destroy = ktl::move(pinned_vmo_); |
| } |
| |
| void PinnedMemoryTokenDispatcher::InvalidateMappedAddrsLocked() { |
| // Fill with a known invalid address to simplify cleanup of errors during |
| // mapping |
| for (dev_vaddr_t& mapped_addr : mapped_addrs_) { |
| mapped_addr = UINT64_MAX; |
| } |
| } |
| |
| void PinnedMemoryTokenDispatcher::on_zero_handles() { |
| Guard<Mutex> guard{get_lock()}; |
| |
| if (!explicitly_unpinned_ && initialized_) { |
| // The user failed to call zx_pmt_unpin. Unmap the memory to prevent continued access, but leave |
| // the VMO pinned and use the quarantine mechanism to protect against stray DMA. |
| zx_status_t status = UnmapFromIommuLocked(); |
| ASSERT(status == ZX_OK); |
| |
| bti_->Quarantine(fbl::RefPtr(this)); |
| } |
| } |
| |
| PinnedMemoryTokenDispatcher::~PinnedMemoryTokenDispatcher() { |
| kcounter_add(dispatcher_pinned_memory_token_destroy_count, 1); |
| |
| if (initialized_) { |
| bti_->RemovePmo(this); |
| } |
| } |
| |
| PinnedMemoryTokenDispatcher::PinnedMemoryTokenDispatcher( |
| fbl::RefPtr<BusTransactionInitiatorDispatcher> bti, PinnedVmObject pinned_vmo, |
| fbl::Array<dev_vaddr_t> mapped_addrs) |
| : pinned_vmo_(ktl::move(pinned_vmo)), |
| bti_(ktl::move(bti)), |
| mapped_addrs_(ktl::move(mapped_addrs)) { |
| DEBUG_ASSERT(pinned_vmo_.vmo() != nullptr); |
| InvalidateMappedAddrsLocked(); |
| kcounter_add(dispatcher_pinned_memory_token_create_count, 1); |
| } |
| |
| zx_status_t PinnedMemoryTokenDispatcher::EncodeAddrs(bool compress_results, bool contiguous, |
| dev_vaddr_t* mapped_addrs, |
| size_t mapped_addrs_count) { |
| Guard<Mutex> guard{get_lock()}; |
| |
| const fbl::Array<dev_vaddr_t>& pmo_addrs = mapped_addrs_; |
| const size_t found_addrs = pmo_addrs.size(); |
| if (compress_results) { |
| if (pinned_vmo_.vmo()->is_contiguous()) { |
| const size_t min_contig = bti_->minimum_contiguity(); |
| DEBUG_ASSERT(ktl::has_single_bit(min_contig)); |
| DEBUG_ASSERT(found_addrs == 1); |
| |
| uint64_t num_addrs = ROUNDUP(pinned_vmo_.size(), min_contig) / min_contig; |
| if (num_addrs != mapped_addrs_count) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| for (uint64_t i = 0; i < num_addrs; i++) { |
| mapped_addrs[i] = pmo_addrs[0] + min_contig * i; |
| } |
| } else { |
| if (found_addrs != mapped_addrs_count) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| memcpy(mapped_addrs, pmo_addrs.data(), found_addrs * sizeof(dev_vaddr_t)); |
| } |
| } else if (contiguous) { |
| if (mapped_addrs_count != 1 || !pinned_vmo_.vmo()->is_contiguous()) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| *mapped_addrs = pmo_addrs[0]; |
| } else { |
| const size_t num_pages = pinned_vmo_.size() / PAGE_SIZE; |
| if (num_pages != mapped_addrs_count) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| const size_t min_contig = |
| pinned_vmo_.vmo()->is_contiguous() ? pinned_vmo_.size() : bti_->minimum_contiguity(); |
| size_t next_idx = 0; |
| for (size_t i = 0; i < found_addrs; ++i) { |
| dev_vaddr_t extent_base = pmo_addrs[i]; |
| for (dev_vaddr_t addr = extent_base; addr < extent_base + min_contig && next_idx < num_pages; |
| addr += PAGE_SIZE, ++next_idx) { |
| mapped_addrs[next_idx] = addr; |
| } |
| } |
| } |
| return ZX_OK; |
| } |