From 7c7fae59c95f35f254079955be2a5d1a79331d7a Mon Sep 17 00:00:00 2001 From: Mark Rowe Date: Wed, 5 Aug 2026 00:50:48 -0700 Subject: [PATCH] Add bn::base::DynamicLibrary, an abstraction over runtime loading of shared libraries `bn::base::DynamicLibrary` opens a shared library at runtime and unloads it when the object is destroyed. The platform handle can be handled off with `Release()` when the library should remain loaded indefinitely. `Open()` returns either the library or the platform's description of why it could not be loaded. `SymbolScope` chooses whether `GetSymbol()` also resolves symbols from the libraries that the loaded library links against. Each platform restricts this differently, so macOS passes `RTLD_FIRST` to `dlopen()`, other Unixes check which image defines each symbol, and Windows only ever searches the module itself. --- base/CMakeLists.txt | 1 + base/dynamic_library.cpp | 229 +++++++++++++++++++++++++++++++++++++++ base/dynamic_library.h | 96 ++++++++++++++++ 3 files changed, 326 insertions(+) create mode 100644 base/dynamic_library.cpp create mode 100644 base/dynamic_library.h diff --git a/base/CMakeLists.txt b/base/CMakeLists.txt index 9460a9ad5..ba404cc3c 100644 --- a/base/CMakeLists.txt +++ b/base/CMakeLists.txt @@ -7,6 +7,7 @@ set_target_properties(binaryninjabase PROPERTIES LINKER_LANGUAGE CXX) get_filename_component(BN_API_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE) target_include_directories(binaryninjabase PUBLIC ${BN_API_DIR}) +target_link_libraries(binaryninjabase PUBLIC ${CMAKE_DL_LIBS}) set_target_properties(binaryninjabase PROPERTIES CXX_STANDARD 20 diff --git a/base/dynamic_library.cpp b/base/dynamic_library.cpp new file mode 100644 index 000000000..305d724b0 --- /dev/null +++ b/base/dynamic_library.cpp @@ -0,0 +1,229 @@ +// Copyright (c) 2026 Vector 35 Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +#include "base/dynamic_library.h" + +#include + +#ifdef WIN32 +#include +#else +#include +#ifndef __APPLE__ +#include +#include +#endif +#endif + +namespace bn::base { + +#ifdef WIN32 + +namespace { + +std::string DescribeLastError() +{ + DWORD error = GetLastError(); + char* text = nullptr; + DWORD length = FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, error, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (char*)&text, 0, nullptr); + if (!length) + return "error code " + std::to_string(error); + + // The system message ends with a newline that is not wanted in the middle of a log line + while (length && (text[length - 1] == '\n' || text[length - 1] == '\r')) + length--; + + std::string result(text, length); + LocalFree(text); + return result; +} + +} // unnamed namespace + + +expected DynamicLibrary::Open( + std::string_view path, SymbolScope scope, [[maybe_unused]] SymbolVisibility visibility) +{ + std::string pathString(path); + HMODULE handle = + LoadLibraryExA(pathString.c_str(), NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); + if (!handle) + return unexpected(DescribeLastError()); + + return DynamicLibrary(std::move(pathString), handle, scope); +} + + +DynamicLibrary::~DynamicLibrary() +{ + if (m_handle) + FreeLibrary((HMODULE)m_handle); +} + + +// GetProcAddress() searches the module it is given and has no way to reach that module's imports, so +// the scope is not consulted: every lookup behaves as LibraryOnly. +void* DynamicLibrary::GetSymbol(const char* name) const +{ + if (!m_handle) + return nullptr; + + return (void*)GetProcAddress((HMODULE)m_handle, name); +} + +#else // !WIN32 + +namespace { + +#ifdef __APPLE__ + +// RTLD_FIRST tags the handle so that dlsym() searches only the image it was opened for +constexpr int LibraryOnlyFlag = RTLD_FIRST; + +// GetSymbol() relies on RTLD_FIRST rather than on the name of the image, so nothing is resolved here +std::string ImagePath(void*, const std::string&) +{ + return std::string(); +} + +#else // !__APPLE__ + +// No dlopen() flag restricts the search on other platforms, so GetSymbol() checks each lookup +constexpr int LibraryOnlyFlag = 0; + +// The name the loader knows the image by, which is what dladdr() reports for the symbols the image +// defines. It is the path dlopen() resolved, which is not the path it was given whenever it searched +// for the library. Falls back to that path when the loader will not report a name. +std::string ImagePath(void* handle, const std::string& path) +{ + link_map* map = nullptr; + if (dlinfo(handle, RTLD_DI_LINKMAP, &map) != 0 || !map || !map->l_name || !map->l_name[0]) + return path; + + return map->l_name; +} + +#endif // !__APPLE__ + +} // unnamed namespace + + +expected DynamicLibrary::Open(std::string_view path, SymbolScope scope, SymbolVisibility visibility) +{ + int flags = RTLD_NOW; + flags |= visibility == SymbolVisibility::Global ? RTLD_GLOBAL : RTLD_LOCAL; + if (scope == SymbolScope::LibraryOnly) + flags |= LibraryOnlyFlag; + + std::string pathString(path); + void* handle = dlopen(pathString.c_str(), flags); + if (!handle) + { + const char* error = dlerror(); + return unexpected(std::string(error ? error : "dlopen failed")); + } + + std::string imagePath = ImagePath(handle, pathString); + return DynamicLibrary(std::move(pathString), handle, scope, std::move(imagePath)); +} + + +DynamicLibrary::~DynamicLibrary() +{ + if (m_handle) + dlclose(m_handle); +} + + +#ifdef __APPLE__ + +// The RTLD_FIRST flag that LibraryOnly adds to dlopen() has already limited the handle to the +// symbols that the library itself defines. +void* DynamicLibrary::GetSymbol(const char* name) const +{ + if (!m_handle) + return nullptr; + + return dlsym(m_handle, name); +} + +#else // !__APPLE__ + +// dlsym() searches the requested image and every image it links against, so a library that links +// against another library resolves that library's symbols as if they were its own. For LibraryOnly, +// resolve a symbol only when the image the library was loaded from is the image that defines it. +void* DynamicLibrary::GetSymbol(const char* name) const +{ + if (!m_handle) + return nullptr; + + void* sym = dlsym(m_handle, name); + if (!sym || m_scope == SymbolScope::LibraryAndDependencies) + return sym; + + Dl_info info; + if (!dladdr(sym, &info) || !info.dli_fname) + return nullptr; + + if (strcmp(info.dli_fname, m_imagePath.c_str()) != 0) + return nullptr; + + return sym; +} + +#endif // !__APPLE__ + +#endif // !WIN32 + + +DynamicLibrary::DynamicLibrary(DynamicLibrary&& other) noexcept + : m_path(std::move(other.m_path)) + , m_imagePath(std::move(other.m_imagePath)) + , m_handle(std::exchange(other.m_handle, nullptr)) + , m_scope(other.m_scope) +{ +} + + +DynamicLibrary& DynamicLibrary::operator=(DynamicLibrary&& other) noexcept +{ + if (this == &other) + return *this; + + // Take over what this object held so that the library is closed at the end of the function. + DynamicLibrary unloaded(std::move(*this)); + m_path = std::move(other.m_path); + m_imagePath = std::move(other.m_imagePath); + m_handle = std::exchange(other.m_handle, nullptr); + m_scope = other.m_scope; + return *this; +} + + +void* DynamicLibrary::Release() +{ + void* handle = m_handle; + m_handle = nullptr; + return handle; +} + +} // namespace bn::base diff --git a/base/dynamic_library.h b/base/dynamic_library.h new file mode 100644 index 000000000..80c7cf43a --- /dev/null +++ b/base/dynamic_library.h @@ -0,0 +1,96 @@ +// Copyright (c) 2026 Vector 35 Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +#pragma once + +#include +#include +#include + +#include "base/expected.h" + +namespace bn::base { + +// A shared library loaded at runtime: a .dll on Windows, a .dylib on macOS, and a .so elsewhere. +// +// The library is unloaded when the object is destroyed unless Release() hands it off, which is +// required whenever code from the library outlives the object, such as a callback that the library +// registered while it was being initialized. +// +// On Windows the library's own directory and the default search path are used to resolve its +// dependencies. +class DynamicLibrary +{ +public: + // Whether symbol lookups reach into the libraries that the loaded library links against. + enum class SymbolScope + { + // GetSymbol() resolves only the symbols that the library itself defines. A library that links + // against another library does not inherit that library's symbols. + LibraryOnly, + // GetSymbol() also resolves the symbols of the libraries that the library links against, which + // is what the platform loaders do by default. Windows resolves imports by module and offers no + // equivalent, so lookups there behave as LibraryOnly whichever scope is requested. + LibraryAndDependencies, + }; + + // Whether the library's own symbols are available to satisfy lookups from other libraries. + // Windows has no process-wide symbol namespace to publish them into, so this has no effect there. + enum class SymbolVisibility + { + Local, + Global, + }; + +private: + std::string m_path; + // The loader's own name for the image, which differs from the path passed to Open() when the + // loader searched for the library. Empty on the platforms whose GetSymbol() does not need it. + std::string m_imagePath; + void* m_handle; + SymbolScope m_scope; + + DynamicLibrary(std::string path, void* handle, SymbolScope scope, std::string imagePath = {}): + m_path(std::move(path)), m_imagePath(std::move(imagePath)), m_handle(handle), m_scope(scope) + {} + +public: + // Load a shared library, or the platform's description of why it could not be loaded + static expected Open(std::string_view path, + SymbolScope scope = SymbolScope::LibraryOnly, SymbolVisibility visibility = SymbolVisibility::Local); + + ~DynamicLibrary(); + + DynamicLibrary(DynamicLibrary&& other) noexcept; + DynamicLibrary& operator=(DynamicLibrary&& other) noexcept; + + DynamicLibrary(const DynamicLibrary&) = delete; + DynamicLibrary& operator=(const DynamicLibrary&) = delete; + + const std::string& GetPath() const { return m_path; } + + // The address of an exported symbol, or nullptr if the library does not export it + void* GetSymbol(const char* name) const; + + // Keep the library loaded after this object is destroyed, returning the platform handle + void* Release(); +}; + +} // namespace bn::base