blob: 4afa614083f2e5ed3cb06976254f86f64dff38ae [file] [log] [blame]
// Copyright 2024 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef LIB_DL_RUNTIME_DYNAMIC_LINKER_H_
#define LIB_DL_RUNTIME_DYNAMIC_LINKER_H_
#include <dlfcn.h> // for RTLD_* macros
#include <lib/elfldltl/soname.h>
#include <lib/fit/result.h>
#include <fbl/intrusive_double_list.h>
#include "diagnostics.h"
#include "error.h"
#include "module.h"
namespace dl {
enum OpenSymbolScope : int {
kLocal = RTLD_LOCAL,
kGlobal = RTLD_GLOBAL,
};
enum OpenBindingMode : int {
kNow = RTLD_NOW,
// RTLD_LAZY functionality is not supported, but keep the flag definition
// because it's a legitimate flag that can be passed in.
kLazy = RTLD_LAZY,
};
enum OpenFlags : int {
kNoload = RTLD_NOLOAD,
kNodelete = RTLD_NODELETE,
// TODO(https://fxbug.dev/323425900): support glibc's RTLD_DEEPBIND flag.
// kDEEPBIND = RTLD_DEEPBIND,
};
// Masks used to validate flag values.
inline constexpr int kOpenSymbolScopeMask = OpenSymbolScope::kLocal | OpenSymbolScope::kGlobal;
inline constexpr int kOpenBindingModeMask = OpenBindingMode::kLazy | OpenBindingMode::kNow;
inline constexpr int kOpenFlagsMask = OpenFlags::kNoload | OpenFlags::kNodelete;
class RuntimeDynamicLinker {
public:
using Soname = elfldltl::Soname<>;
// Not copyable, not movable
RuntimeDynamicLinker() = default;
RuntimeDynamicLinker(const RuntimeDynamicLinker&) = delete;
RuntimeDynamicLinker(RuntimeDynamicLinker&&) = delete;
// Attempt to find the loaded module with the given name, returning a nullptr
// if the module was not found.
ModuleHandle* FindModule(Soname name);
// Lookup a symbol from the given module, returning a pointer to it in memory,
// or an error if not found (ie undefined symbol).
fit::result<Error, void*> LookupSymbol(ModuleHandle* module, const char* ref);
// TODO(https://fxbug.dev/339037138): Add a test exercising the system error
// case and include it as an example for the fit::error{Error} description.
// Open `file` with the given `mode`, returning a pointer to the loaded module
// for the file. The `retrieve_file` argument is passed on to LoadModule.Load
// and is called as a
// `fit::result<std::optional<Error>, File>(Diagnostics&, std::string_view)`
// with the following semantics:
// - fit::error{std::nullopt} is a not found error
// - fit::error{Error} is an error type that can be passed to
// Diagnostics::SystemError (see <lib/elfldltl/diagnostics.h>) to give
// more context to the error message.
// - fit::ok{File} is the found elfldltl File API type for the module
// (see <lib/elfldltl/memory.h>).
// The Diagnostics reference passed to `retrieve_file` is not used by the
// function itself to report its errors, but is plumbed into the created File
// API object that will use it for reporting file read errors.
template <class Loader, typename RetrieveFile>
fit::result<Error, void*> Open(const char* file, int mode, RetrieveFile&& retrieve_file) {
auto already_loaded = CheckOpen(file, mode);
if (already_loaded.is_error()) [[unlikely]] {
return already_loaded.take_error();
}
// If the module for `file` was found, return a reference to it.
if (already_loaded.value()) {
return fit::ok(already_loaded.value());
}
// A ModuleHandle for `file` does not yet exist; proceed to loading the
// file and all its dependencies.
// Use a non-scoped diagnostics object for the main module. Because errors
// are generated on this module directly, it's name does not need to be
// prefixed to the error, as is the case using ld::ScopedModuleDiagnostics.
dl::Diagnostics diag;
auto [pending_modules, pending_load_modules] =
Load<Loader>(diag, Soname{file}, std::forward<RetrieveFile>(retrieve_file));
if (pending_load_modules.is_empty()) [[unlikely]] {
assert(pending_modules.is_empty() == pending_load_modules.is_empty());
return diag.take_error();
}
// TODO(https://fxbug.dev/324136831): This does not include global modules
// yet.
if (!Relocate(diag, pending_load_modules)) {
return diag.take_error();
}
// Obtain a reference to the root module for the dlopen-ed file to return
// back to the caller.
ModuleHandle& root_module = pending_modules.front();
// TODO(https://fxbug.dev/333573264): this assumes that all pending modules
// are not already in modules_.
// After successful loading and relocation, append the new permanent modules
// created by this dlopen session to the dynamic linker's module list.
modules_.splice(modules_.end(), pending_modules);
return diag.ok(&root_module);
}
private:
// Perform basic argument checking and check whether a module for `file` was
// already loaded. An error is returned if bad input was given. Otherwise,
// return a reference to the module if it was already loaded, or nullptr if
// a module for `file` was not found.
fit::result<Error, ModuleHandle*> CheckOpen(const char* file, int mode);
// TODO(https://fxbug.dev/333573264): Talk about how previously-loaded modules
// that happen to be a dependency in this dlopen session are represented in
// these lists.
// Load the root module and all its dependencies, constructing two lists of
// module data structures in the process:
// - List of ModuleHandles: This is the list of permanent module data
// structures that will eventually be installed in the runtime dynamic
// linker's module list and managed by the runtime dynamic linker.
// - List of LoadModules: This is the list of temporary load module data
// structures needed to perform loading, decoding, relocations, etc. The
// elements in this list live only as long as the current dlopen session.
// The `retrieve_file` argument is a callable passed down from `Open` and is
// invoked to retrieve the module's file from the file system for processing.
template <class Loader, typename RetrieveFile>
std::pair<ModuleHandleList, LoadModuleList<Loader>> Load(Diagnostics& diag, Soname soname,
RetrieveFile&& retrieve_file) {
static_assert(std::is_invocable_v<RetrieveFile, Diagnostics&, std::string_view>);
LoadModuleList<Loader> load_modules;
ModuleHandleList modules;
// This lambda will retrieve the module's file, load the module into the
// system image, and then create new modules for each of its dependencies
// to enqueue onto load_modules list for future processing. A
// fit::result<bool> is returned to the caller where the boolean indicates
// if the file was found, so that the caller can handle the "not-found"
// error case.
auto load_and_enqueue_deps = [&](auto& module) -> fit::result<bool> {
auto file = retrieve_file(diag, module.name().str());
if (file.is_error()) [[unlikely]] {
// Check if the error is a not-found error or a system error.
if (auto error = file.error_value()) {
// If a general system error occurred, emit the error for the module.
diag.SystemError("cannot open ", module.name().str(), ": ", *error);
return fit::error(false);
}
// A "not-found" error occurred, and the caller is responsible for
// emitting the error message for the module.
return fit::error(true);
}
if (auto result = module.Load(diag, *std::move(file))) {
// Create a module for each dependency from the LoadModule.Load result
// and enqueue it onto `load_modules` to be processed and loaded in the
// future.
auto enqueue_dep = [this, &diag, &modules, &load_modules](const Soname& name) {
return EnqueueModule(diag, name, modules, load_modules);
};
if (std::all_of(std::begin(*result), std::end(*result), enqueue_dep)) {
return fit::ok();
}
}
return fit::error(false);
};
if (!EnqueueModule(diag, soname, modules, load_modules)) {
return {};
}
// Load the root module and enqueue all its dependencies.
if (auto result = load_and_enqueue_deps(load_modules.front()); result.is_error()) {
if (result.error_value()) {
diag.SystemError(load_modules.front().name().str(), " not found");
}
return {};
}
// Proceed to load and enqueue the root module's dependencies and their
// dependencies in a breadth-first order.
for (auto it = std::next(load_modules.begin()); it != load_modules.end(); it++) {
if (auto result = load_and_enqueue_deps(*it); result.is_error()) {
if (result.error_value()) {
// TODO(https://fxbug.dev/336633049): harmonize this error message
// with musl, which appends a "(needed by <depending module>)" to the
// message.
diag.MissingDependency(it->name().str());
}
return {};
}
}
return std::make_pair(std::move(modules), std::move(load_modules));
}
// Create new ModuleHandle and LoadModule data structures for `soname` and
// enqueue these data structures to the `modules` and `load_modules` list.
template <class Loader>
bool EnqueueModule(Diagnostics& diag, Soname soname, ModuleHandleList& modules,
LoadModuleList<Loader>& load_modules) {
if (std::find(load_modules.begin(), load_modules.end(), soname) != load_modules.end()) {
// The module was already added to the load_modules list in this dlopen
// session.
return true;
}
// TODO(https://fxbug.dev/333573264): Check if the module was already
// loaded by a previous dlopen call or at startup and use that reference
// instead.
// TODO(https://fxbug.dev/338229987): This is just to make sure we're not
// exercising deps from modules already loaded yet.
assert(!FindModule(soname));
fbl::AllocChecker module_ac;
auto module = ModuleHandle::Create(module_ac, soname);
if (!module_ac.check()) [[unlikely]] {
diag.OutOfMemory("permanent module data structure", sizeof(ModuleHandle));
return false;
}
fbl::AllocChecker load_module_ac;
auto load_module = LoadModule<Loader>::Create(load_module_ac, *module);
if (!load_module_ac.check()) [[unlikely]] {
diag.OutOfMemory("temporary module data structure", sizeof(LoadModule<Loader>));
return false;
}
modules.push_back(std::move(module));
load_modules.push_back(std::move(load_module));
return true;
}
// TODO(https://fxbug.dev/324136831): Include global modules in `modules` (and
// remember to skip relocating previously-loaded modules).
// Perform relocations on all pending modules to be loaded. Return a boolean
// if relocations succeeded on all modules.
template <class Loader>
bool Relocate(Diagnostics& diag, LoadModuleList<Loader>& modules) {
// Scope diagnostics to the root module so that its name will prefix error
// messages.
auto relocate = [&](auto& module) -> bool {
ld::ScopedModuleDiagnostics root_module_diag{diag, module.name().str()};
return module.Relocate(diag, modules);
};
return std::all_of(std::begin(modules), std::end(modules), relocate);
}
// The RuntimeDynamicLinker owns the list of all 'live' modules that have been
// loaded into the system image.
// TODO(https://fxbug.dev/324136831): support startup modules
ModuleHandleList modules_;
};
} // namespace dl
#endif // LIB_DL_RUNTIME_DYNAMIC_LINKER_H_