dart-sdk/runtime/lib/ffi_dynamic_library.cc
Daco Harkes 9588927faf [vm] Native asset relative path resolution with symlinks
This CL moves native assets resolution to the embedder.

The Dart VM looks up the asset path (for example
`['relative', 'foo.so']`) with the asset id. The embedder defines
callbacks for asset loading, and returns a handle to the dylib.
Then the VM calls the embedder again with `dlsym` to lookup the symbol.

The Dart VM & kernel compiler are responsible for the asset id to
asset path mapping. The kernel compiler compiles it into the snapshot
and the VM looks it up in the snapshot.

Relative paths are resolved relative to the isolate script uri (kernel
snapshot, jit snapshot, aot snapshot, or `dart compile exe` result).
The embedder is responsible for remembering the script uri it set when
spawning the isolate group.

This CL does not add `dlclose` or `dladdr` embedder callbacks yet.
Bug: https://github.com/dart-lang/sdk/issues/55521
Bug: https://github.com/dart-lang/sdk/issues/55966

TEST=pkg/dartdev/test/native_assets/build_test.dart
TEST=tests/ffi/native_assets/asset_relative_test.dart

Bug: https://github.com/dart-lang/sdk/issues/55410
Bug: https://github.com/dart-lang/sdk/issues/55523
Bug: https://github.com/dart-lang/sdk/issues/55207
Change-Id: I75ec4a368c5fb3d2f76b03771e796ff56bcac941
Cq-Include-Trybots: dart/try:vm-aot-linux-debug-x64-try,vm-aot-linux-debug-x64c-try,vm-aot-mac-release-arm64-try,vm-aot-mac-release-x64-try,vm-aot-obfuscate-linux-release-x64-try,vm-aot-optimization-level-linux-release-x64-try,vm-aot-win-debug-arm64-try,vm-aot-win-debug-x64-try,vm-aot-win-debug-x64c-try,pkg-linux-debug-try,pkg-linux-release-arm64-try,pkg-mac-release-try,pkg-mac-release-arm64-try,pkg-win-release-try,pkg-win-release-arm64-try,vm-aot-asan-linux-release-x64-try,vm-asan-linux-release-x64-try,vm-aot-msan-linux-release-x64-try,vm-msan-linux-release-x64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/361881
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Daco Harkes <dacoharkes@google.com>
2024-06-12 16:45:19 +00:00

606 lines
21 KiB
C++

// Copyright (c) 2019, 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 "lib/ffi_dynamic_library.h"
#include "platform/globals.h"
#include "platform/utils.h"
#if defined(DART_HOST_OS_WINDOWS)
#include <Psapi.h>
#include <Windows.h>
#include <combaseapi.h>
#include <stdio.h>
#include <tchar.h>
#endif
#include "platform/uri.h"
#include "vm/bootstrap_natives.h"
#include "vm/dart_api_impl.h"
#include "vm/exceptions.h"
#include "vm/ffi/native_assets.h"
#include "vm/native_entry.h"
#include "vm/symbols.h"
#include "vm/zone_text_buffer.h"
#if defined(DART_HOST_OS_LINUX) || defined(DART_HOST_OS_MACOS) || \
defined(DART_HOST_OS_ANDROID) || defined(DART_HOST_OS_FUCHSIA)
#include <dlfcn.h>
#endif
namespace dart {
#if defined(USING_SIMULATOR) || (defined(DART_PRECOMPILER) && !defined(TESTING))
DART_NORETURN static void SimulatorUnsupported() {
#if defined(USING_SIMULATOR)
Exceptions::ThrowUnsupportedError(
"Not supported on simulated architectures.");
#else
Exceptions::ThrowUnsupportedError("Not supported in precompiler.");
#endif
}
DEFINE_NATIVE_ENTRY(Ffi_dl_open, 0, 1) {
SimulatorUnsupported();
}
DEFINE_NATIVE_ENTRY(Ffi_dl_processLibrary, 0, 0) {
SimulatorUnsupported();
}
DEFINE_NATIVE_ENTRY(Ffi_dl_executableLibrary, 0, 0) {
SimulatorUnsupported();
}
DEFINE_NATIVE_ENTRY(Ffi_dl_lookup, 1, 2) {
SimulatorUnsupported();
}
DEFINE_NATIVE_ENTRY(Ffi_dl_getHandle, 0, 1) {
SimulatorUnsupported();
}
DEFINE_NATIVE_ENTRY(Ffi_dl_close, 0, 1) {
SimulatorUnsupported();
}
DEFINE_NATIVE_ENTRY(Ffi_dl_providesSymbol, 0, 2) {
SimulatorUnsupported();
}
DEFINE_NATIVE_ENTRY(Ffi_GetFfiNativeResolver, 1, 0) {
SimulatorUnsupported();
}
#else // defined(USING_SIMULATOR) || \
// (defined(DART_PRECOMPILER) && !defined(TESTING))
// If an error occurs populates |error| (if provided) with an error message
// (caller must free this message when it is no longer needed).
static void* LoadDynamicLibrary(const char* library_file,
char** error = nullptr) {
char* utils_error = nullptr;
void* handle = Utils::LoadDynamicLibrary(library_file, &utils_error);
if (utils_error != nullptr) {
if (error != nullptr) {
*error = OS::SCreate(
/*use malloc*/ nullptr, "Failed to load dynamic library '%s': %s",
library_file != nullptr ? library_file : "<process>", utils_error);
}
free(utils_error);
}
return handle;
}
#if defined(DART_HOST_OS_WINDOWS)
// On windows, nullptr signals trying a lookup in all loaded modules.
const nullptr_t kWindowsDynamicLibraryProcessPtr = nullptr;
void* co_task_mem_allocated = nullptr;
// If an error occurs populates |error| with an error message
// (caller must free this message when it is no longer needed).
void* LookupSymbolInProcess(const char* symbol, char** error) {
// Force loading ole32.dll.
if (co_task_mem_allocated == nullptr) {
co_task_mem_allocated = CoTaskMemAlloc(sizeof(intptr_t));
CoTaskMemFree(co_task_mem_allocated);
}
HANDLE current_process =
OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE,
GetCurrentProcessId());
if (current_process == nullptr) {
*error = OS::SCreate(nullptr, "Failed to open current process.");
return nullptr;
}
HMODULE modules[1024];
DWORD cb_needed;
if (EnumProcessModules(current_process, modules, sizeof(modules),
&cb_needed) != 0) {
for (intptr_t i = 0; i < (cb_needed / sizeof(HMODULE)); i++) {
if (auto result =
reinterpret_cast<void*>(GetProcAddress(modules[i], symbol))) {
CloseHandle(current_process);
return result;
}
}
}
CloseHandle(current_process);
*error = OS::SCreate(
nullptr, // Use `malloc`.
"None of the loaded modules contained the requested symbol '%s'.",
symbol);
return nullptr;
}
#endif
// If an error occurs populates |error| with an error message
// (caller must free this message when it is no longer needed).
static void* ResolveSymbol(void* handle, const char* symbol, char** error) {
#if defined(DART_HOST_OS_WINDOWS)
if (handle == kWindowsDynamicLibraryProcessPtr) {
return LookupSymbolInProcess(symbol, error);
}
#endif
return Utils::ResolveSymbolInDynamicLibrary(handle, symbol, error);
}
static bool SymbolExists(void* handle, const char* symbol) {
char* error = nullptr;
#if !defined(DART_HOST_OS_WINDOWS)
Utils::ResolveSymbolInDynamicLibrary(handle, symbol, &error);
#else
if (handle == nullptr) {
LookupSymbolInProcess(symbol, &error);
} else {
Utils::ResolveSymbolInDynamicLibrary(handle, symbol, &error);
}
#endif
if (error != nullptr) {
free(error);
return false;
}
return true;
}
DEFINE_NATIVE_ENTRY(Ffi_dl_open, 0, 1) {
GET_NON_NULL_NATIVE_ARGUMENT(String, lib_path, arguments->NativeArgAt(0));
char* error = nullptr;
void* handle = LoadDynamicLibrary(lib_path.ToCString(), &error);
if (error != nullptr) {
const String& msg = String::Handle(String::New(error));
free(error);
Exceptions::ThrowArgumentError(msg);
}
return DynamicLibrary::New(handle, true);
}
DEFINE_NATIVE_ENTRY(Ffi_dl_processLibrary, 0, 0) {
#if defined(DART_HOST_OS_LINUX) || defined(DART_HOST_OS_MACOS) || \
defined(DART_HOST_OS_ANDROID) || defined(DART_HOST_OS_FUCHSIA)
return DynamicLibrary::New(RTLD_DEFAULT, false);
#else
return DynamicLibrary::New(kWindowsDynamicLibraryProcessPtr, false);
#endif
}
DEFINE_NATIVE_ENTRY(Ffi_dl_executableLibrary, 0, 0) {
return DynamicLibrary::New(LoadDynamicLibrary(nullptr), false);
}
DEFINE_NATIVE_ENTRY(Ffi_dl_close, 0, 1) {
GET_NON_NULL_NATIVE_ARGUMENT(DynamicLibrary, dlib, arguments->NativeArgAt(0));
if (dlib.IsClosed()) {
// Already closed, nothing to do
} else if (!dlib.CanBeClosed()) {
const String& msg = String::Handle(
String::New("DynamicLibrary.process() and DynamicLibrary.executable() "
"can't be closed."));
Exceptions::ThrowStateError(msg);
} else {
void* handle = dlib.GetHandle();
char* error = nullptr;
Utils::UnloadDynamicLibrary(handle, &error);
if (error == nullptr) {
dlib.SetClosed(true);
} else {
const String& msg = String::Handle(String::New(error));
free(error);
Exceptions::ThrowStateError(msg);
}
}
return Object::null();
}
DEFINE_NATIVE_ENTRY(Ffi_dl_lookup, 1, 2) {
GET_NON_NULL_NATIVE_ARGUMENT(DynamicLibrary, dlib, arguments->NativeArgAt(0));
GET_NON_NULL_NATIVE_ARGUMENT(String, argSymbolName,
arguments->NativeArgAt(1));
if (dlib.IsClosed()) {
const String& msg =
String::Handle(String::New("Cannot lookup symbols in closed library."));
Exceptions::ThrowStateError(msg);
}
void* handle = dlib.GetHandle();
char* error = nullptr;
const uword pointer = reinterpret_cast<uword>(
ResolveSymbol(handle, argSymbolName.ToCString(), &error));
if (error != nullptr) {
const String& msg = String::Handle(String::NewFormatted(
"Failed to lookup symbol '%s': %s", argSymbolName.ToCString(), error));
free(error);
Exceptions::ThrowArgumentError(msg);
}
return Pointer::New(pointer);
}
DEFINE_NATIVE_ENTRY(Ffi_dl_getHandle, 0, 1) {
GET_NON_NULL_NATIVE_ARGUMENT(DynamicLibrary, dlib, arguments->NativeArgAt(0));
intptr_t handle = reinterpret_cast<intptr_t>(dlib.GetHandle());
return Integer::NewFromUint64(handle);
}
DEFINE_NATIVE_ENTRY(Ffi_dl_providesSymbol, 0, 2) {
GET_NON_NULL_NATIVE_ARGUMENT(DynamicLibrary, dlib, arguments->NativeArgAt(0));
GET_NON_NULL_NATIVE_ARGUMENT(String, argSymbolName,
arguments->NativeArgAt(1));
void* handle = dlib.GetHandle();
return Bool::Get(SymbolExists(handle, argSymbolName.ToCString())).ptr();
}
// nullptr if no native resolver is installed.
static Dart_FfiNativeResolver GetFfiNativeResolver(Thread* const thread,
const String& lib_url_str) {
const Library& lib =
Library::Handle(Library::LookupLibrary(thread, lib_url_str));
if (lib.IsNull()) {
// It is not an error to not have a native resolver installed.
return nullptr;
}
return lib.ffi_native_resolver();
}
// If an error occurs populates |error| with an error message
// (caller must free this message when it is no longer needed).
static void* FfiResolveWithFfiNativeResolver(Thread* const thread,
Dart_FfiNativeResolver resolver,
const String& symbol,
intptr_t args_n,
char** error) {
auto* result = resolver(symbol.ToCString(), args_n);
if (result == nullptr) {
*error = OS::SCreate(/*use malloc*/ nullptr,
"Couldn't resolve function: '%s'", symbol.ToCString());
}
return result;
}
#if defined(DART_TARGET_OS_WINDOWS)
// Replaces back slashes with forward slashes in place.
static void ReplaceBackSlashes(char* cstr) {
const intptr_t length = strlen(cstr);
for (int i = 0; i < length; i++) {
cstr[i] = cstr[i] == '\\' ? '/' : cstr[i];
}
}
#endif
const char* file_schema = "file://";
const int file_schema_length = 7;
// Get a file path with only forward slashes from the script path.
static StringPtr GetPlatformScriptPath(Thread* thread) {
IsolateGroupSource* const source = thread->isolate_group()->source();
#if defined(DART_TARGET_OS_WINDOWS)
// Isolate.spawnUri sets a `source` including the file schema.
// And on Windows we get an extra forward slash in that case.
const char* file_schema_slash = "file:///";
const int file_schema_slash_length = 8;
const char* path = source->script_uri;
if (strlen(source->script_uri) > file_schema_slash_length &&
strncmp(source->script_uri, file_schema_slash,
file_schema_slash_length) == 0) {
path = (source->script_uri + file_schema_slash_length);
}
// Replace backward slashes with forward slashes.
const intptr_t len = strlen(path);
char* path_copy = reinterpret_cast<char*>(malloc(len + 1));
snprintf(path_copy, len + 1, "%s", path);
ReplaceBackSlashes(path_copy);
const auto& result = String::Handle(String::New(path_copy));
free(path_copy);
return result.ptr();
#else
// Isolate.spawnUri sets a `source` including the file schema.
if (strlen(source->script_uri) > file_schema_length &&
strncmp(source->script_uri, file_schema, file_schema_length) == 0) {
const char* path = (source->script_uri + file_schema_length);
return String::New(path);
}
return String::New(source->script_uri);
#endif
}
// Array::null if asset is not in mapping or no mapping.
static ArrayPtr GetAssetLocation(Thread* const thread, const String& asset) {
Zone* const zone = thread->zone();
auto& result = Array::Handle(zone);
const auto& native_assets_map =
Array::Handle(zone, GetNativeAssetsMap(thread));
if (!native_assets_map.IsNull()) {
NativeAssetsMap map(native_assets_map.ptr());
const auto& lookup = Object::Handle(zone, map.GetOrNull(asset));
if (!lookup.IsNull()) {
result = Array::Cast(lookup).ptr();
}
map.Release();
}
return result.ptr();
}
// String is zone allocated.
static char* AvailableAssetsToCString(Thread* const thread) {
Zone* const zone = thread->zone();
const auto& native_assets_map =
Array::Handle(zone, GetNativeAssetsMap(thread));
ZoneTextBuffer buffer(zone, 1024);
if (native_assets_map.IsNull()) {
buffer.Printf("No available native assets.");
} else {
bool first = true;
buffer.Printf("Available native assets: ");
NativeAssetsMap map(native_assets_map.ptr());
NativeAssetsMap::Iterator it(&map);
auto& asset_id = String::Handle(zone);
while (it.MoveNext()) {
if (!first) {
buffer.Printf(" ,");
}
auto entry = it.Current();
asset_id ^= map.GetKey(entry);
buffer.Printf("%s", asset_id.ToCString());
}
buffer.Printf(".");
map.Release();
}
return buffer.buffer();
}
// Fall back to old implementation temporarily to ease the roll into flutter.
// TODO(https://dartbug.com/55523): Remove fallback and throw errors that
// native assets API is not initialized.
static void* FfiResolveAssetFallback(Thread* const thread,
const String& asset_type,
const String& path,
const String& symbol,
char** error) {
Zone* const zone = thread->zone();
void* handle = nullptr;
if (asset_type.Equals(Symbols::absolute())) {
handle = LoadDynamicLibrary(path.ToCString(), error);
} else if (asset_type.Equals(Symbols::relative())) {
const auto& platform_script_uri = String::Handle(
zone,
String::NewFormatted(
"%s%s", file_schema,
String::Handle(zone, GetPlatformScriptPath(thread)).ToCString()));
char* path_cstr = path.ToMallocCString();
#if defined(DART_TARGET_OS_WINDOWS)
ReplaceBackSlashes(path_cstr);
#endif
CStringUniquePtr target_uri =
ResolveUri(path_cstr, platform_script_uri.ToCString());
free(path_cstr);
if (!target_uri) {
*error = OS::SCreate(
/*use malloc*/ nullptr,
"Failed to resolve '%s' relative to "
"'%s'.",
path.ToCString(), platform_script_uri.ToCString());
} else {
const char* target_path = target_uri.get() + file_schema_length;
handle = LoadDynamicLibrary(target_path, error);
}
} else if (asset_type.Equals(Symbols::system())) {
handle = LoadDynamicLibrary(path.ToCString(), error);
} else if (asset_type.Equals(Symbols::process())) {
#if defined(DART_HOST_OS_LINUX) || defined(DART_HOST_OS_MACOS) || \
defined(DART_HOST_OS_ANDROID) || defined(DART_HOST_OS_FUCHSIA)
handle = RTLD_DEFAULT;
#else
handle = kWindowsDynamicLibraryProcessPtr;
#endif
} else if (asset_type.Equals(Symbols::executable())) {
handle = LoadDynamicLibrary(nullptr, error);
} else {
UNREACHABLE();
}
if (*error != nullptr) {
char* inner_error = *error;
*error = OS::SCreate(/*use malloc*/ nullptr,
"Failed to load dynamic library '%s': %s",
path.ToCString(), inner_error);
free(inner_error);
} else {
void* const result = ResolveSymbol(handle, symbol.ToCString(), error);
if (*error != nullptr) {
char* inner_error = *error;
*error = OS::SCreate(/*use malloc*/ nullptr,
"Failed to lookup symbol '%s': %s",
symbol.ToCString(), inner_error);
free(inner_error);
} else {
return result;
}
}
ASSERT(*error != nullptr);
return nullptr;
}
// If an error occurs populates |error| with an error message
// (caller must free this message when it is no longer needed).
//
// The |asset_location| is formatted as follows:
// ['<path_type>', '<path (optional)>']
// The |asset_location| is conform to: pkg/vm/lib/native_assets/validator.dart
static void* FfiResolveAsset(Thread* const thread,
const Array& asset_location,
const String& symbol,
char** error) {
Zone* const zone = thread->zone();
const auto& asset_type =
String::Cast(Object::Handle(zone, asset_location.At(0)));
String& path = String::Handle(zone);
const char* path_cstr = nullptr;
if (asset_type.Equals(Symbols::absolute()) ||
asset_type.Equals(Symbols::relative()) ||
asset_type.Equals(Symbols::system())) {
path = String::RawCast(asset_location.At(1));
path_cstr = path.ToCString();
}
NativeAssetsApi* native_assets_api =
thread->isolate_group()->native_assets_api();
void* handle;
if (asset_type.Equals(Symbols::absolute())) {
if (native_assets_api->dlopen_absolute == nullptr) {
return FfiResolveAssetFallback(thread, asset_type, path, symbol, error);
}
NoActiveIsolateScope no_active_isolate_scope;
handle = native_assets_api->dlopen_absolute(path_cstr, error);
} else if (asset_type.Equals(Symbols::relative())) {
if (native_assets_api->dlopen_relative == nullptr) {
return FfiResolveAssetFallback(thread, asset_type, path, symbol, error);
}
NoActiveIsolateScope no_active_isolate_scope;
handle = native_assets_api->dlopen_relative(path_cstr, error);
} else if (asset_type.Equals(Symbols::system())) {
if (native_assets_api->dlopen_system == nullptr) {
return FfiResolveAssetFallback(thread, asset_type, path, symbol, error);
}
NoActiveIsolateScope no_active_isolate_scope;
handle = native_assets_api->dlopen_system(path_cstr, error);
} else if (asset_type.Equals(Symbols::executable())) {
if (native_assets_api->dlopen_executable == nullptr) {
return FfiResolveAssetFallback(thread, asset_type, path, symbol, error);
}
NoActiveIsolateScope no_active_isolate_scope;
handle = native_assets_api->dlopen_executable(error);
} else {
RELEASE_ASSERT(asset_type.Equals(Symbols::process()));
if (native_assets_api->dlopen_process == nullptr) {
return FfiResolveAssetFallback(thread, asset_type, path, symbol, error);
}
NoActiveIsolateScope no_active_isolate_scope;
handle = native_assets_api->dlopen_process(error);
}
if (*error != nullptr) {
return nullptr;
}
if (native_assets_api->dlsym == nullptr) {
return FfiResolveAssetFallback(thread, asset_type, path, symbol, error);
}
void* const result =
native_assets_api->dlsym(handle, symbol.ToCString(), error);
return result;
}
// Frees |error|.
static void ThrowFfiResolveError(const String& symbol,
const String& asset,
char* error) {
const String& error_message = String::Handle(String::NewFormatted(
"Couldn't resolve native function '%s' in '%s' : %s.\n",
symbol.ToCString(), asset.ToCString(), error));
free(error);
Exceptions::ThrowArgumentError(error_message);
}
intptr_t FfiResolveInternal(const String& asset,
const String& symbol,
uintptr_t args_n,
char** error) {
Thread* thread = Thread::Current();
Zone* zone = thread->zone();
// Resolver resolution.
auto resolver = GetFfiNativeResolver(thread, asset);
if (resolver != nullptr) {
void* ffi_native_result = FfiResolveWithFfiNativeResolver(
thread, resolver, symbol, args_n, error);
return reinterpret_cast<intptr_t>(ffi_native_result);
}
// Native assets resolution.
const auto& asset_location =
Array::Handle(zone, GetAssetLocation(thread, asset));
if (!asset_location.IsNull()) {
void* asset_result = FfiResolveAsset(thread, asset_location, symbol, error);
return reinterpret_cast<intptr_t>(asset_result);
}
// Resolution in current process.
#if !defined(DART_HOST_OS_WINDOWS)
void* const result = Utils::ResolveSymbolInDynamicLibrary(
RTLD_DEFAULT, symbol.ToCString(), error);
#else
void* const result = LookupSymbolInProcess(symbol.ToCString(), error);
#endif
if (*error != nullptr) {
// Process lookup failed, but the user might have tried to use native
// asset lookup. So augment the error message to include native assets info.
char* process_lookup_error = *error;
*error = OS::SCreate(/*use malloc*/ nullptr,
"No asset with id '%s' found. %s "
"Attempted to fallback to process lookup. %s",
asset.ToCString(), AvailableAssetsToCString(thread),
process_lookup_error);
free(process_lookup_error);
}
return reinterpret_cast<intptr_t>(result);
}
// FFI native C function pointer resolver.
static intptr_t FfiResolve(Dart_Handle asset_handle,
Dart_Handle symbol_handle,
uintptr_t args_n) {
auto* const thread = Thread::Current();
DARTSCOPE(thread);
auto* const zone = thread->zone();
const String& asset = Api::UnwrapStringHandle(zone, asset_handle);
const String& symbol = Api::UnwrapStringHandle(zone, symbol_handle);
char* error = nullptr;
const intptr_t result = FfiResolveInternal(asset, symbol, args_n, &error);
if (error != nullptr) {
ThrowFfiResolveError(symbol, asset, error);
}
ASSERT(result != 0x0);
return result;
}
// Bootstrap to get the FFI Native resolver through a `native` call.
DEFINE_NATIVE_ENTRY(Ffi_GetFfiNativeResolver, 1, 0) {
return Pointer::New(reinterpret_cast<intptr_t>(FfiResolve));
}
#endif // defined(USING_SIMULATOR) || \
// (defined(DART_PRECOMPILER) && !defined(TESTING))
} // namespace dart