// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. #include "vm/ffi_callback_metadata.h" #if defined(DART_HOST_OS_FUCHSIA) #include #include #include #include #include #include #endif #include "vm/allocation.h" #include "vm/compiler/assembler/disassembler.h" #include "vm/dart_api_state.h" #include "vm/flag_list.h" #include "vm/object.h" #include "vm/runtime_entry.h" #include "vm/stub_code.h" namespace dart { #if defined(SIMULATOR_FFI) && defined(HOST_ARCH_ARM64) extern "C" void SimulatorFfiCallbackTrampoline(); extern "C" void SimulatorFfiCallbackTrampolineEnd(); #endif FfiCallbackMetadata::FfiCallbackMetadata() {} void FfiCallbackMetadata::EnsureStubPageLocked() { ASSERT(lock_.IsOwnedByCurrentThread()); #if defined(DART_HOST_OS_FUCHSIA) if (rx_vmo_ == ZX_HANDLE_INVALID) { int fd = -1; const char* path = "pkg/lib/ffi_callback_stub.bin"; zx_status_t status = fdio_open3_fd(path, static_cast(fuchsia::io::PERM_READABLE | fuchsia::io::PERM_EXECUTABLE), &fd); if (status != ZX_OK) { FATAL("fdio_open3_fd(%s) failed: %s\n", path, zx_status_get_string(status)); } status = fdio_get_vmo_exec(fd, &rx_vmo_); if (status != ZX_OK) { FATAL("fdio_get_vmo_exec failed %s\n", zx_status_get_string(status)); } offset_of_first_trampoline_in_page_ = 0; } return; #endif if (stub_page_ != nullptr) { return; } ASSERT_LESS_OR_EQUAL(VirtualMemory::PageSize(), kPageSize); uword code_start, code_end, code_size; #if defined(SIMULATOR_FFI) && defined(HOST_ARCH_ARM64) if (FLAG_use_simulator) { code_start = reinterpret_cast(SimulatorFfiCallbackTrampoline); code_end = reinterpret_cast(SimulatorFfiCallbackTrampolineEnd); code_size = code_end - code_start; } else { const Code& trampoline_code = StubCode::FfiCallbackTrampoline(); code_start = trampoline_code.EntryPoint(); code_end = code_start + trampoline_code.Size(); code_size = trampoline_code.Size(); } #else const Code& trampoline_code = StubCode::FfiCallbackTrampoline(); code_start = trampoline_code.EntryPoint(); code_end = code_start + trampoline_code.Size(); code_size = trampoline_code.Size(); #endif const uword page_start = code_start & ~(VirtualMemory::PageSize() - 1); ASSERT_LESS_OR_EQUAL((code_start - page_start) + code_size, RXMappingSize()); // Stub page uses a tight (unaligned) bound for the end of the code area. // Otherwise we can read past the end of the code area when doing DuplicateRX. stub_page_ = VirtualMemory::ForImagePage(reinterpret_cast(page_start), code_end - page_start); offset_of_first_trampoline_in_page_ = code_start - page_start; } FfiCallbackMetadata::~FfiCallbackMetadata() { // Unmap all the trampoline pages. 'VirtualMemory's are new-allocated. delete stub_page_; for (intptr_t i = 0; i < trampoline_pages_.length(); ++i) { delete trampoline_pages_[i]; } #if defined(DART_HOST_OS_FUCHSIA) zx_handle_close(rx_vmo_); #endif } namespace { uword RXAreaStart(VirtualMemory* page) { return page->start() + page->OffsetToExecutableAlias(); } } // namespace void FfiCallbackMetadata::FillRuntimeFunction(VirtualMemory* page, uword index, void* function) { void** slot = reinterpret_cast(RXAreaStart(page) + RuntimeFunctionOffset(index)); *slot = function; } FfiCallbackMetadata* FfiCallbackMetadata::Instance(Trampoline trampoline) { const uword start = MappingStart(trampoline); return *reinterpret_cast( start + RuntimeFunctionOffset(kGroupFfiCallbackMetadata)); } VirtualMemory* FfiCallbackMetadata::AllocateTrampolinePage() { #if defined(DART_HOST_OS_FUCHSIA) zx_handle_t vmar = ZX_HANDLE_INVALID; zx_vaddr_t addr = 0; zx_vm_option_t align_flag = Utils::ShiftForPowerOfTwo(MappingAlignment()) << ZX_VM_ALIGN_BASE; ASSERT((ZX_VM_ALIGN_1KB <= align_flag) && (align_flag <= ZX_VM_ALIGN_4GB)); zx_status_t status = zx_vmar_allocate( zx_vmar_root_self(), ZX_VM_CAN_MAP_SPECIFIC | ZX_VM_CAN_MAP_READ | ZX_VM_CAN_MAP_WRITE | ZX_VM_CAN_MAP_EXECUTE | align_flag, 0, MappingSize(), &vmar, &addr); if (status != ZX_OK) { FATAL("zx_vmar_allocate failed: %s", zx_status_get_string(status)); } zx_handle_t rw_vmo = ZX_HANDLE_INVALID; status = zx_vmo_create(RWMappingSize(), 0, &rw_vmo); if (status != ZX_OK) { FATAL("zx_vmo_create failed: %s", zx_status_get_string(status)); } const char* name = "dart-ffi-callback-bss"; zx_object_set_property(rw_vmo, ZX_PROP_NAME, name, strlen(name)); zx_vaddr_t rx_addr = 0; status = zx_vmar_map(vmar, ZX_VM_SPECIFIC | ZX_VM_PERM_READ | ZX_VM_PERM_EXECUTE, /*vmar_offset=*/0, rx_vmo_, /*vmo_offset=*/0, RXMappingSize(), &rx_addr); if (status != ZX_OK) { FATAL("zx_vmar_map failed: %s", zx_status_get_string(status)); } zx_vaddr_t rw_addr = 0; status = zx_vmar_map(vmar, ZX_VM_SPECIFIC | ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, /*vmar_offset=*/RXMappingSize(), rw_vmo, /*vmo_offset=*/0, RWMappingSize(), &rw_addr); if (status != ZX_OK) { FATAL("zx_vmar_map failed: %s", zx_status_get_string(status)); } zx_handle_close(rw_vmo); zx_handle_close(vmar); return VirtualMemory::Adopt(reinterpret_cast(addr), MappingSize()); #endif #if defined(DART_HOST_OS_MACOS) && defined(DART_PRECOMPILED_RUNTIME) const bool should_remap_stub_page = true; #else const bool should_remap_stub_page = false; // No support for remapping. #endif #if defined(DART_HOST_OS_MACOS) // If we are not going to use vm_remap then we need to pass // is_executable=true so that pages get allocated with MAP_JIT flag or // using RX workarounds if necessary. Otherwise OS will kill us with a // codesigning violation if hardened runtime is enabled or we will simply // not be able to execute trampoline code. const bool is_executable = !should_remap_stub_page; #else // On other operating systems we can simply flip RW->RX as necessary. const bool is_executable = false; #endif VirtualMemory* new_page = VirtualMemory::AllocateAligned( MappingSize(), MappingAlignment(), is_executable, /*is_compressed=*/false, "FfiCallbackMetadata::TrampolinePage"); if (new_page == nullptr) { return nullptr; } if (should_remap_stub_page) { #if defined(DART_HOST_OS_MACOS) if (!stub_page_->DuplicateRX(new_page)) { delete new_page; return nullptr; } #else static_assert(!should_remap_stub_page, "Remaping only supported on Mac OS X"); #endif } else { // If we are creating executable mapping then simply fill it with code by // copying the page. const intptr_t aligned_size = Utils::RoundUp(stub_page_->size(), VirtualMemory::PageSize()); ASSERT(new_page->start() >= stub_page_->end() || new_page->end() <= stub_page_->start()); memcpy(new_page->address(), stub_page_->address(), // NOLINT stub_page_->size()); VirtualMemory::WriteProtectCode(new_page->address(), aligned_size); if (VirtualMemory::ShouldDualMapExecutablePages()) { ASSERT(new_page->OffsetToExecutableAlias() != 0); VirtualMemory::Protect( reinterpret_cast(RXAreaStart(new_page) + RXMappingSize()), RWMappingSize(), VirtualMemory::kReadWrite); } } #if !defined(PRODUCT) || defined(FORCE_INCLUDE_DISASSEMBLER) if (FLAG_support_disassembler && FLAG_disassemble_stubs) { DisassembleToStdout formatter; THR_Print("Code for duplicated stub 'FfiCallbackTrampoline' {\n"); const uword code_start = RXAreaStart(new_page) + offset_of_first_trampoline_in_page_; Disassembler::Disassemble(code_start, code_start + kPageSize, &formatter, /*comments=*/nullptr); THR_Print("}\n"); } #endif return new_page; } #if defined(SIMULATOR_FFI) && defined(HOST_ARCH_ARM64) struct CallbackContext; extern "C" void DoRedirectedFfiCallback(CallbackContext* ctxt, uword trampoline); #endif void FfiCallbackMetadata::EnsureFreeListNotEmptyLocked() { ASSERT(lock_.IsOwnedByCurrentThread()); EnsureStubPageLocked(); if (free_list_head_ != nullptr) { return; } VirtualMemory* new_page = AllocateTrampolinePage(); if (new_page == nullptr) { Exceptions::ThrowOOM(); } trampoline_pages_.Add(new_page); // Fill in the runtime functions. FillRuntimeFunction(new_page, kGetFfiCallbackMetadata, reinterpret_cast(DLRT_GetFfiCallbackMetadata)); #if defined(SIMULATOR_FFI) && defined(HOST_ARCH_ARM64) FillRuntimeFunction(new_page, kDoRedirectedFfiCallback, reinterpret_cast(DoRedirectedFfiCallback)); #endif FillRuntimeFunction(new_page, kGroupFfiCallbackMetadata, this); // Add all the trampolines to the free list. const intptr_t trampolines_per_page = NumCallbackTrampolinesPerPage(); MetadataEntry* metadata_entry = reinterpret_cast( RXAreaStart(new_page) + MetadataOffset()); for (intptr_t i = 0; i < trampolines_per_page; ++i) { AddToFreeListLocked(&metadata_entry[i]); } } FfiCallbackMetadata::Trampoline FfiCallbackMetadata::CreateMetadataEntry( Isolate* target_isolate, IsolateGroup* target_isolate_group, TrampolineType trampoline_type, uword target_entry_point, uint64_t context, MetadataEntry** list_head) { MutexLocker locker(&lock_); EnsureFreeListNotEmptyLocked(); ASSERT(free_list_head_ != nullptr); MetadataEntry* entry = free_list_head_; free_list_head_ = entry->free_list_next_; if (free_list_head_ == nullptr) { ASSERT(free_list_tail_ == entry); free_list_tail_ = nullptr; } MetadataEntry* next_entry = *list_head; if (next_entry != nullptr) { ASSERT(next_entry->list_prev_ == nullptr); next_entry->list_prev_ = entry; } if (target_isolate != nullptr) { *entry = MetadataEntry(target_isolate, trampoline_type, target_entry_point, context, nullptr, next_entry); } else { ASSERT(target_isolate_group != nullptr); *entry = MetadataEntry(target_isolate_group, trampoline_type, target_entry_point, context, nullptr, next_entry); } *list_head = entry; return TrampolineOfMetadataEntry(entry); } void FfiCallbackMetadata::AddToFreeListLocked(MetadataEntry* entry) { ASSERT(lock_.IsOwnedByCurrentThread()); if (free_list_tail_ == nullptr) { ASSERT(free_list_head_ == nullptr); free_list_head_ = free_list_tail_ = entry; } else { ASSERT(free_list_head_ != nullptr && free_list_tail_ != nullptr); ASSERT(!free_list_tail_->metadata()->IsLive()); free_list_tail_->free_list_next_ = entry; free_list_tail_ = entry; } entry->metadata()->context_ = 0; entry->metadata()->target_isolate_ = nullptr; entry->free_list_next_ = nullptr; } void FfiCallbackMetadata::DeleteCallbackLocked(MetadataEntry* entry) { ASSERT(lock_.IsOwnedByCurrentThread()); if (entry->metadata()->trampoline_type_ != TrampolineType::kAsync && entry->metadata()->context_ != 0) { ASSERT(entry->metadata()->target_isolate_ != nullptr); entry->metadata()->api_state()->FreePersistentHandle( entry->metadata()->closure_handle()); } AddToFreeListLocked(entry); } void FfiCallbackMetadata::DeleteAllCallbacks(MetadataEntry** list_head) { MutexLocker locker(&lock_); for (MetadataEntry* entry = *list_head; entry != nullptr;) { MetadataEntry* next = entry->list_next(); DeleteCallbackLocked(entry); entry = next; } *list_head = nullptr; } void FfiCallbackMetadata::DeleteCallback(Trampoline trampoline, MetadataEntry** list_head) { MutexLocker locker(&lock_); auto* entry = MetadataEntryOfTrampoline(trampoline); ASSERT(entry->metadata()->IsLive()); auto* prev = entry->list_prev_; auto* next = entry->list_next_; if (prev != nullptr) { prev->list_next_ = next; } else { ASSERT(*list_head == entry); *list_head = next; } if (next != nullptr) { next->list_prev_ = prev; } DeleteCallbackLocked(entry); } uword FfiCallbackMetadata::GetEntryPoint(Zone* zone, const Function& function) { const auto& code = Code::Handle(zone, FLAG_precompiled_mode ? function.CurrentCode() : function.EnsureHasCode()); ASSERT(!code.IsNull()); return code.EntryPoint(); } PersistentHandle* FfiCallbackMetadata::CreatePersistentHandle( IsolateGroup* isolate_group, const Closure& closure) { auto* api_state = isolate_group->api_state(); ASSERT(api_state != nullptr); auto* handle = api_state->AllocatePersistentHandle(); handle->set_ptr(closure); return handle; } FfiCallbackMetadata::Trampoline FfiCallbackMetadata::CreateLocalFfiCallback( Isolate* isolate, IsolateGroup* isolate_group, Zone* zone, const Function& function, const Closure& closure, MetadataEntry** list_head) { PersistentHandle* handle = nullptr; if (closure.IsNull()) { // If the closure is null, it means the target is a static function, so is // baked into the trampoline and is an ordinary sync callback. ASSERT((isolate != nullptr && isolate_group == nullptr && function.GetFfiCallbackKind() == FfiCallbackKind::kIsolateLocalStaticCallback) || (isolate == nullptr && isolate_group != nullptr && function.GetFfiCallbackKind() == FfiCallbackKind::kIsolateGroupBoundStaticCallback)); } else { ASSERT((isolate != nullptr && isolate_group == nullptr && function.GetFfiCallbackKind() == FfiCallbackKind::kIsolateLocalClosureCallback) || (isolate == nullptr && isolate_group != nullptr && function.GetFfiCallbackKind() == FfiCallbackKind::kIsolateGroupBoundClosureCallback)); if (function.GetFfiCallbackKind() == FfiCallbackKind::kIsolateGroupBoundClosureCallback) { closure.EnsureDeeplyImmutable(zone); } handle = CreatePersistentHandle( isolate != nullptr ? isolate->group() : isolate_group, closure); } return CreateSyncFfiCallbackImpl(isolate, isolate_group, zone, function, handle, list_head); } FfiCallbackMetadata::Trampoline FfiCallbackMetadata::CreateSyncFfiCallbackImpl( Isolate* isolate, IsolateGroup* isolate_group, Zone* zone, const Function& function, PersistentHandle* closure, MetadataEntry** list_head) { TrampolineType trampoline_type = isolate != nullptr ? TrampolineType::kSync : TrampolineType::kSyncIsolateGroupBound; #if defined(TARGET_ARCH_IA32) // On ia32, store the stack delta that we need to use when returning. const intptr_t stack_return_delta = function.FfiCSignatureReturnsStruct() && CallingConventions::kUsesRet4 ? compiler::target::kWordSize : 0; if (stack_return_delta != 0) { ASSERT(stack_return_delta == 4); trampoline_type = isolate != nullptr ? TrampolineType::kSyncStackDelta4 : TrampolineType::kSyncIsolateGroupBoundStackDelta4; } #endif return CreateMetadataEntry(isolate, isolate_group, trampoline_type, GetEntryPoint(zone, function), reinterpret_cast(closure), list_head); } FfiCallbackMetadata::Trampoline FfiCallbackMetadata::CreateAsyncFfiCallback( Isolate* isolate, Zone* zone, const Function& send_function, Dart_Port send_port, MetadataEntry** list_head) { ASSERT(send_function.GetFfiCallbackKind() == FfiCallbackKind::kAsyncCallback); return CreateMetadataEntry(isolate, /*target_isolate_group=*/nullptr, TrampolineType::kAsync, GetEntryPoint(zone, send_function), static_cast(send_port), list_head); } FfiCallbackMetadata::Trampoline FfiCallbackMetadata::TrampolineOfMetadataEntry( MetadataEntry* metadata_entry) const { const uword start = MappingStart(reinterpret_cast(metadata_entry)); MetadataEntry* metadata_entries = reinterpret_cast(start + MetadataOffset()); const uword index = metadata_entry - metadata_entries; return start + offset_of_first_trampoline_in_page_ + index * kNativeCallbackTrampolineSize; } FfiCallbackMetadata::MetadataEntry* FfiCallbackMetadata::MetadataEntryOfTrampoline(Trampoline trampoline) const { const uword start = MappingStart(trampoline); MetadataEntry* metadata_entries = reinterpret_cast(start + MetadataOffset()); const uword index = (trampoline - start - offset_of_first_trampoline_in_page_) / kNativeCallbackTrampolineSize; return &metadata_entries[index]; } FfiCallbackMetadata::Metadata FfiCallbackMetadata::LookupMetadataForTrampolineUnlocked( Trampoline trampoline) const { return *MetadataEntryOfTrampoline(trampoline)->metadata(); } ApiState* FfiCallbackMetadata::Metadata::api_state() const { return (is_isolate_group_bound() ? target_isolate_group_ : target_isolate_->group()) ->api_state(); } } // namespace dart