Android system — 链接器命名空间(linker namespace)源码分析
1. 源码分析
在前一篇文章Android system — Android链接器命名空间(Android 11后)中我们介绍了Android linker namespace的概念、作用及相关配置属性,这一篇我们会跟踪Anroid linker namespace的源码实现。
应用程序对应的可执行文件app_process会创建一个类加载器classloader并调用System.loadLibrary加载so库,最终会调用至android_dlopen_ext;而native的二进制bin文件是通过dlopen获取so库的handle,但是无论是dlopen还是android_dlopen_ext最后都是调用do_dlopen函数,所以从do_dlopen开始分析。
2. do_dlopen
源码位置:bionic/linker/linker.cpp
- 调用find_containing_library根据solist获取dlopen调用地址对应的so文件的soinfo指针。
- 调用get_caller_namespace获取调用者对应的命名空间,调用者so文件对应的soinfo的primary_namespace_成员就是其对应的命名空间。
- 调用find_library寻找待加载so文件的soinfo指针,如果找到了就获取其对应的handle信息,否则就返回失败。这里还可以得到一个信息,android早期版本中dlopen加载so文件返回的handle就是so文件对应的soinfo结构体指针,现在获取的handle并不是简单的soinfo结构体指针
//bionic/linker/linker.cpp
void* do_dlopen(const char* name, int flags,
const android_dlextinfo* extinfo,
const void* caller_addr) {
std::string trace_prefix = std::string("dlopen: ") + (name == nullptr ? "(nullptr)" : name);
ScopedTrace trace(trace_prefix.c_str());
ScopedTrace loading_trace((trace_prefix + " - loading and linking").c_str());
//获取caller_addr调用地址对应的so文件的soinfo指针
soinfo* const caller = find_containing_library(caller_addr);
//获取caller调用者的命名空间
android_namespace_t* ns = get_caller_namespace(caller);
LD_LOG(kLogDlopen,
"dlopen(name=\"%s\", flags=0x%x, extinfo=%s, caller=\"%s\", caller_ns=%s@%p, targetSdkVersion=%i) ...",
name,
flags,
android_dlextinfo_to_string(extinfo).c_str(),
caller == nullptr ? "(null)" : caller->get_realpath(),
ns == nullptr ? "(null)" : ns->get_name(),
ns,
get_application_target_sdk_version());
......
ProtectedDataGuard guard;
//得到此so文件对应的soinfo指针
soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);
loading_trace.End();
if (si != nullptr) {
//通过soinfo指针得到对应的handle
void* handle = si->to_handle();
LD_LOG(kLogDlopen,
"... dlopen calling constructors: realpath=\"%s\", soname=\"%s\", handle=%p",
si->get_realpath(), si->get_soname(), handle);
si->call_constructors();
failure_guard.Disable();
LD_LOG(kLogDlopen,
"... dlopen successful: realpath=\"%s\", soname=\"%s\", handle=%p",
si->get_realpath(), si->get_soname(), handle);
return handle;
}
return nullptr;
}
3. find_library_internal
刚刚do_dlopen中,最后我们发现最后会调用find_library寻找待加载so文件的soinfo指针,而根据代码来看,后续实际调用为find_library-->find_libraries-->find_library_internal
//bionic/linker/linker.cpp
static soinfo* find_library(android_namespace_t* ns,
const char* name, int rtld_flags,
const android_dlextinfo* extinfo,
soinfo* needed_by) {
soinfo* si = nullptr;
if (name == nullptr) {
si = solist_get_somain();
} else if (!find_libraries(ns,
needed_by,
&name,
1,
&si,
nullptr,
0,
rtld_flags,
extinfo,
false /* add_as_children */)/*实际调用find_libraries */) {
if (si != nullptr) {
soinfo_unload(si);
}
return nullptr;
}
si->increment_ref_count();
return si;
}
// add_as_children - add first-level loaded libraries (i.e. library_names[], but
// not their transitive dependencies) as children of the start_with library.
// This is false when find_libraries is called for dlopen(), when newly loaded
// libraries must form a disjoint tree.
bool find_libraries(android_namespace_t* ns,
soinfo* start_with,
const char* const library_names[],
size_t library_names_count,
soinfo* soinfos[],
std::vector<soinfo*>* ld_preloads,
size_t ld_preloads_count,
int rtld_flags,
const android_dlextinfo* extinfo,
bool add_as_children,
std::vector<android_namespace_t*>* namespaces) {
......
// Step 1: expand the list of load_tasks to include
// all DT_NEEDED libraries (do not load them just yet)
for (size_t i = 0; i<load_tasks.size(); ++i) {
LoadTask* task = load_tasks[i];
soinfo* needed_by = task->get_needed_by();
bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children);
task->set_extinfo(is_dt_needed ? nullptr : extinfo);
task->set_dt_needed(is_dt_needed);
// Note: start from the namespace that is stored in the LoadTask. This namespace
// is different from the current namespace when the LoadTask is for a transitive
// dependency and the lib that created the LoadTask is not found in the
// current namespace but in one of the linked namespaces.
android_namespace_t* start_ns = const_cast<android_namespace_t*>(task->get_start_from());
LD_LOG(kLogDlopen, "find_library_internal(ns=%s@%p): task=%s, is_dt_needed=%d",
start_ns->get_name(), start_ns, task->get_name(), is_dt_needed);
if (!find_library_internal(start_ns, task, &zip_archive_cache, &load_tasks, rtld_flags)/* 调用find_library_internal,从namespace find library*/) {
return false;
}
......
return true;
}
因此我们直接分析find_library_internal函数。
- 调用find_loaded_library_by_soname()在caller命名空间和链接的命名空间中查找库是否已经加载。
- 如果上一步未找到就调用load_library()尝试在caller命名空间中加载库。
- 如果前两步都没成功就尝试调用find_library_in_linked_namespace()在caller命名空间的所有链接命名空间中搜索库,并尝试调用load_library在链接命名空间中加载库。
static bool find_library_internal(android_namespace_t* ns,
LoadTask* task,
ZipArchiveCache* zip_archive_cache,
LoadTaskList* load_tasks,
int rtld_flags) {
soinfo* candidate;
//在caller命名空间和链接的命名空间中查找库是否已经加载
if (find_loaded_library_by_soname(ns, task->get_name(), true /* search_linked_namespaces */,
&candidate)) {
LD_LOG(kLogDlopen,
"find_library_internal(ns=%s, task=%s): Already loaded (by soname): %s",
ns->get_name(), task->get_name(), candidate->get_realpath());
task->set_soinfo(candidate);
return true;
}
// Library might still be loaded, the accurate detection
// of this fact is done by load_library.
TRACE("[ \"%s\" find_loaded_library_by_soname failed (*candidate=%s@%p). Trying harder... ]",
task->get_name(), candidate == nullptr ? "n/a" : candidate->get_realpath(), candidate);
//如果上一步未找到就调用load_library尝试在caller命名空间中加载库
if (load_library(ns, task, zip_archive_cache, load_tasks, rtld_flags,
true /* search_linked_namespaces */)) {
return true;
}
// workaround 方案:Android Nougat 7.0以后不会生效
// TODO(dimitry): workaround for http://b/26394120 (the exempt-list)
if (ns->is_exempt_list_enabled() && is_exempt_lib(ns, task->get_name(), task->get_needed_by())) {
// For the libs in the exempt-list, switch to the default namespace and then
// try the load again from there. The library could be loaded from the
// default namespace or from another namespace (e.g. runtime) that is linked
// from the default namespace.
LD_LOG(kLogDlopen,
"find_library_internal(ns=%s, task=%s): Exempt system library - trying namespace %s",
ns->get_name(), task->get_name(), g_default_namespace.get_name());
ns = &g_default_namespace;
if (load_library(ns, task, zip_archive_cache, load_tasks, rtld_flags,
true /* search_linked_namespaces */)) {
return true;
}
}
// END OF WORKAROUND
//如果前两步都没成功就尝试在caller命名空间的所有链接命名空间中搜索库并尝试在链接命名空间中加载
// if a library was not found - look into linked namespaces
// preserve current dlerror in the case it fails.
DlErrorRestorer dlerror_restorer;
LD_LOG(kLogDlopen, "find_library_internal(ns=%s, task=%s): Trying %zu linked namespaces",
ns->get_name(), task->get_name(), ns->linked_namespaces().size());
for (auto& linked_namespace : ns->linked_namespaces()) {
if (find_library_in_linked_namespace(linked_namespace, task)) {
// Library is already loaded.
if (task->get_soinfo() != nullptr) {
// n.b. This code path runs when find_library_in_linked_namespace found an already-loaded
// library by soname. That should only be possible with a exempt-list lookup, where we
// switch the namespace, because otherwise, find_library_in_linked_namespace is duplicating
// the soname scan done in this function's first call to find_loaded_library_by_soname.
return true;
}
if (load_library(linked_namespace.linked_namespace(), task, zip_archive_cache, load_tasks,
rtld_flags, false /* search_linked_namespaces */)) {
LD_LOG(kLogDlopen, "find_library_internal(ns=%s, task=%s): Found in linked namespace %s",
ns->get_name(), task->get_name(), linked_namespace.linked_namespace()->get_name());
return true;
}
}
}
return false;
}
3.1 find_loaded_library_by_soname
- 首先会判断加载的库名称(也就是dlopen传入的so名称)是否为绝对路径,如果是直接返回false。
- 如果不是绝对路径就调用find_loaded_library_by_soname()在caller命名空间已经加载的so库列表中找是否有待加载的so库。
- 如果上一步没找到就遍历caller命名空间所有链接的命名空间,并调用 android_namespace_link_t::is_accessible()进行权限判断。权限判断失败直接返回false,权限判断成功就调用find_loaded_library_by_soname在链接命名空间已经加载的so库列表中找是否有待加载的so库。
// Returns true if library was found and false otherwise
static bool find_loaded_library_by_soname(android_namespace_t* ns,
const char* name,
bool search_linked_namespaces,
soinfo** candidate) {
*candidate = nullptr;
// Ignore filename with path.
//如果是绝对路径就直接返回false
if (strchr(name, '/') != nullptr) {
return false;
}
//在caller命名空间已经加载的so库列表中找是否有待加载的so库
bool found = find_loaded_library_by_soname(ns, name, candidate);
//如果没找到就遍历caller命名空间所有链接命名空间
if (!found && search_linked_namespaces) {
// if a library was not found - look into linked namespaces
for (auto& link : ns->linked_namespaces()) {
//首先判断权限
if (!link.is_accessible(name)) {
continue;
}
android_namespace_t* linked_ns = link.linked_namespace();
//权限判断通过就在链接命名空间中已经加载的so库列表中找是否有待加载的so库
if (find_loaded_library_by_soname(linked_ns, name, candidate)) {
return true;
}
}
}
return found;
}
android_namespace_link_t::is_accessible()进行权限判断。
- 先判断此链接的allow_all_shared_libs_ 是否为true,如果为true说明当前命名空间中无法加载的so库都可以在链接命名空间中搜索加载。
- 如果allow_all_shared_libs_为false就需要进一步判断待加载的so库是否在此链接命名空间的shared_lib_sonames_列表中。
// bionic/linker/linker_namespaces.h
bool is_accessible(const char* soname) const {
return allow_all_shared_libs_ || shared_lib_sonames_.find(soname) != shared_lib_sonames_.end();
}
3.2 load_library
load_library先判断extinfo是否为空,只有java层调用System.loadLibrary时extinfo才不为空。对于在native层中直接调用dlopen和android_dlopen_ext而言extinfo都为空。
- extinfo不为空,直接调用重载的load_library
- extinfo为空,需要先调用open_library将so文件从磁盘加载到内存中,在调用重载的load_library
static bool load_library(android_namespace_t* ns,
LoadTask* task,
ZipArchiveCache* zip_archive_cache,
LoadTaskList* load_tasks,
int rtld_flags,
bool search_linked_namespaces) {
const char* name = task->get_name();
soinfo* needed_by = task->get_needed_by();
const android_dlextinfo* extinfo = task->get_extinfo();
//判断extinfo是否为空,只有从java层调用System.loadLibrary来到者才extinfo才不为空
if (extinfo != nullptr && (extinfo->flags & ANDROID_DLEXT_USE_LIBRARY_FD) != 0) {
off64_t file_offset = 0;
if ((extinfo->flags & ANDROID_DLEXT_USE_LIBRARY_FD_OFFSET) != 0) {
file_offset = extinfo->library_fd_offset;
}
std::string realpath;
if (!realpath_fd(extinfo->library_fd, &realpath)) {
if (!is_first_stage_init()) {
PRINT(
"warning: unable to get realpath for the library \"%s\" by extinfo->library_fd. "
"Will use given name.",
name);
}
realpath = name;
}
task->set_fd(extinfo->library_fd, false);
task->set_file_offset(file_offset);
return load_library(ns, task, load_tasks, rtld_flags, realpath, search_linked_namespaces);
}
LD_LOG(kLogDlopen,
"load_library(ns=%s, task=%s, flags=0x%x, search_linked_namespaces=%d): calling "
"open_library",
ns->get_name(), name, rtld_flags, search_linked_namespaces);
// Open the file.
off64_t file_offset;
std::string realpath;
//对于直接native调用dlopen而言先调用open_library把so文件载入内存。
int fd = open_library(ns, zip_archive_cache, name, needed_by, &file_offset, &realpath);
if (fd == -1) {
if (task->is_dt_needed()) {
if (needed_by->is_main_executable()) {
DL_OPEN_ERR("library \"%s\" not found: needed by main executable", name);
} else {
DL_OPEN_ERR("library \"%s\" not found: needed by %s in namespace %s", name,
needed_by->get_realpath(), task->get_start_from()->get_name());
}
} else {
DL_OPEN_ERR("library \"%s\" not found", name);
}
return false;
}
task->set_fd(fd, true);
task->set_file_offset(file_offset);
//调用重载的load_library加载so文件获取soinfo结构
return load_library(ns, task, load_tasks, rtld_flags, realpath, search_linked_namespaces);
}
open_library会尝试从磁盘中加载so文件
- 判断是否为绝对路径,是的话直接打开。
- 尝试从LD_LIBRARY_PATH设置的路径中打开文件(ld_library_paths)
- 尝试从DT_RUNPATH打开
- 尝试从default_library_paths中加载
static int open_library(android_namespace_t* ns,
ZipArchiveCache* zip_archive_cache,
const char* name, soinfo *needed_by,
off64_t* file_offset, std::string* realpath) {
TRACE("[ opening %s from namespace %s ]", name, ns->get_name());
// If the name contains a slash, we should attempt to open it directly and not search the paths.
//如果是绝对路径直接打开文件
if (strchr(name, '/') != nullptr) {
return open_library_at_path(zip_archive_cache, name, file_offset, realpath);
}
// LD_LIBRARY_PATH has the highest priority. We don't have to check accessibility when searching
// the namespace's path lists, because anything found on a namespace path list should always be
// accessible.
//尝试从LD_LIBRARY_PATH设置的路径中打开(ld_library_paths)
int fd = open_library_on_paths(zip_archive_cache, name, file_offset, ns->get_ld_library_paths(), realpath);
// Try the DT_RUNPATH, and verify that the library is accessible.
//尝试从DT_RUNPATH打开,从代码可以看到LD_LIBRARY_PATH并没有校验is_accessible,但是DT_RUNPATH会校验,因此向通过编译设置DT_RUNPATH和直接export LD_LIBRARY_PATH环境变量效果并不一样
if (fd == -1 && needed_by != nullptr) {
fd = open_library_on_paths(zip_archive_cache, name, file_offset, needed_by->get_dt_runpath(), realpath);
if (fd != -1 && !ns->is_accessible(*realpath)) {
close(fd);
fd = -1;
}
}
// Finally search the namespace's main search path list.
//尝试从default_library_paths中加载
if (fd == -1) {
fd = open_library_on_paths(zip_archive_cache, name, file_offset, ns->get_default_library_paths(), realpath);
}
return fd;
}
open_library如果成功加载文件后就调用重载的load_library。
- 调用is_accessible进行权限检查,检查通过直接调用soinfo_alloc为so文件申请soinfo结构体。
- is_accessible权限检查未通过调用is_greylisted判断是否在灰名单中,如果在就调用soinfo_alloc为so文件申请soinfo结构体。
- is_accessible权限检查未通过,也不在灰名单中则此caller命名空间无权限加载此so文件,打印错误日志。(我的测试bin就是在这里报错的)
static bool load_library(android_namespace_t* ns,
LoadTask* task,
LoadTaskList* load_tasks,
int rtld_flags,
const std::string& realpath,
bool search_linked_namespaces) {
off64_t file_offset = task->get_file_offset();
const char* name = task->get_name();
const android_dlextinfo* extinfo = task->get_extinfo();
LD_LOG(kLogDlopen,
"load_library(ns=%s, task=%s, flags=0x%x, realpath=%s, search_linked_namespaces=%d)",
ns->get_name(), name, rtld_flags, realpath.c_str(), search_linked_namespaces);
......
//文件不在临时文件系统,is_accessible权限检查没有通过
// do not check accessibility using realpath if fd is located on tmpfs
// this enables use of memfd_create() for apps
if ((fs_stat.f_type != TMPFS_MAGIC) && (!ns->is_accessible(realpath))) {
// TODO(dimitry): workaround for http://b/26394120 - the exempt-list
const soinfo* needed_by = task->is_dt_needed() ? task->get_needed_by() : nullptr;
//是否在exempt-list中
if (is_exempt_lib(ns, name, needed_by)) {
// print warning only if needed by non-system library
if (needed_by == nullptr || !is_system_library(needed_by->get_realpath())) {
const soinfo* needed_or_dlopened_by = task->get_needed_by();
const char* sopath = needed_or_dlopened_by == nullptr ? "(unknown)" :
needed_or_dlopened_by->get_realpath();
DL_WARN_documented_change(24,
"private-api-enforced-for-api-level-24",
"library \"%s\" (\"%s\") needed or dlopened by \"%s\" "
"is not accessible by namespace \"%s\"",
name, realpath.c_str(), sopath, ns->get_name());
add_dlwarning(sopath, "unauthorized access to", name);
}
} else {
//如果权限检查未通过,也不在exempt-list中则证明当前caller命名空间无权限加载此库文件。(app报错就是在此报错的)
// do not load libraries if they are not accessible for the specified namespace.
const char* needed_or_dlopened_by = task->get_needed_by() == nullptr ?
"(unknown)" :
task->get_needed_by()->get_realpath();
DL_OPEN_ERR("library \"%s\" needed or dlopened by \"%s\" is not accessible for the namespace \"%s\"",
name, needed_or_dlopened_by, ns->get_name());
// do not print this if a library is in the list of shared libraries for linked namespaces
if (!maybe_accessible_via_namespace_links(ns, name)) {
PRINT("library \"%s\" (\"%s\") needed or dlopened by \"%s\" is not accessible for the"
" namespace: [name=\"%s\", ld_library_paths=\"%s\", default_library_paths=\"%s\","
" permitted_paths=\"%s\"]",
name, realpath.c_str(),
needed_or_dlopened_by,
ns->get_name(),
android::base::Join(ns->get_ld_library_paths(), ':').c_str(),
android::base::Join(ns->get_default_library_paths(), ':').c_str(),
android::base::Join(ns->get_permitted_paths(), ':').c_str());
}
return false;
}
}
//如果上述检查都通过就为加载的so文件申请一个soinfo结构
soinfo* si = soinfo_alloc(ns, realpath.c_str(), &file_stat, file_offset, rtld_flags);
task->set_soinfo(si);
// Read the ELF header and some of the segments.
if (!task->read(realpath.c_str(), file_stat.st_size)) {
task->remove_cached_elf_reader();
task->set_soinfo(nullptr);
soinfo_free(si);
return false;
}
......
return true;
}
is_accessible进行权限检查,依次进行如下检查,有一项检查通过则直接返回true。
- 判断命名空间的is_isolated_是否为true,如果不是则证明不是严格隔离,权限检查通过。(这就是通常说的如果命名空间不是严格隔离的则可以加载任意绝对路径的库文件的原理)
- 判断是否在白名单中
- 判断是否在ld_library_paths中(LD_LIBRARY_PATH设置)
- 判断是否在default_library_paths中
- 判断是否在特权路径permitted_paths中
// bionic/linker/linker_namespaces.cpp
// Given an absolute path, can this library be loaded into this namespace?
bool android_namespace_t::is_accessible(const std::string& file) {
//判断命名空间的is_isolated_,即是否为严格隔离,如果不是则权限检查通过。
if (!is_isolated_) {
return true;
}
//判断是否在允许名单中
if (!allowed_libs_.empty()) {
const char *lib_name = basename(file.c_str());
if (std::find(allowed_libs_.begin(), allowed_libs_.end(), lib_name) == allowed_libs_.end()) {
return false;
}
}
//判断是否在ld_library_paths中(LD_LIBRARY_PATH设置)
for (const auto& dir : ld_library_paths_) {
if (file_is_in_dir(file, dir)) {
return true;
}
}
//判断是否在default_library_paths中
for (const auto& dir : default_library_paths_) {
if (file_is_in_dir(file, dir)) {
return true;
}
}
//判断是否在特权路径permitted_paths中
for (const auto& dir : permitted_paths_) {
if (file_is_under_dir(file, dir)) {
return true;
}
}
return false;
}
3.3 find_library_in_linked_namespace
如果第一步和第二步都失败了就遍历caller命名空间的链接命名空间并调用find_library_in_linked_namespace。
- 调用find_loaded_library_by_soname在链接命名空间中已经加载的so库列表中查找是否存在待加载so文件,如果是绝对路径直接返回false。(这次传入false所以不会调用链接命名空间的链接命名空间,这也是链接命名空间不具有传递性的原理)
- 无论找没找到都调用is_accessible权限检查(检查 链接命名空间的allow_all_shared_libs_是否为True或者so文件是否在链接命名空间的共享库列表shared_lib_sonames_中。)
static bool find_library_in_linked_namespace(const android_namespace_link_t& namespace_link,
LoadTask* task) {
android_namespace_t* ns = namespace_link.linked_namespace();
soinfo* candidate;
bool loaded = false;
std::string soname;
//检查so库在链接命名空间中是否加载,传入false不会去检查链接命名空间的链接命名空间(这也是链接命名空间不具有传递性的原理)
if (find_loaded_library_by_soname(ns, task->get_name(), false, &candidate)) {
loaded = true;
soname = candidate->get_soname();
} else {
soname = resolve_soname(task->get_name());
}
//进行权限检查
if (!namespace_link.is_accessible(soname.c_str())) {
// the library is not accessible via namespace_link
LD_LOG(kLogDlopen,
"find_library_in_linked_namespace(ns=%s, task=%s): Not accessible (soname=%s)",
ns->get_name(), task->get_name(), soname.c_str());
return false;
}
// if library is already loaded - return it
//如果已经加载就返回
if (loaded) {
LD_LOG(kLogDlopen, "find_library_in_linked_namespace(ns=%s, task=%s): Already loaded",
ns->get_name(), task->get_name());
task->set_soinfo(candidate);
return true;
}
// returning true with empty soinfo means that the library is okay to be
// loaded in the namespace but has not yet been loaded there before.
LD_LOG(kLogDlopen, "find_library_in_linked_namespace(ns=%s, task=%s): Ok to load", ns->get_name(),
task->get_name());
task->set_soinfo(nullptr);
return true;
}