blob: 2afef4ceba8ffb27d9d2721823c2e1f035fb5df5 [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.
#include <lib/elfldltl/self.h>
#include "dl-iterate-phdr-tests.h"
#include "dl-load-tests.h"
namespace {
using dl::testing::DlTests;
TYPED_TEST_SUITE(DlTests, dl::testing::TestTypes);
using ::testing::Contains;
using ::testing::Not;
using ::testing::Property;
using dl::testing::CollectModulePhdrInfo;
using dl::testing::GetGlobalCounters;
using dl::testing::ModuleInfoList;
using dl::testing::ModulePhdrInfo;
using dl::testing::RunFunction;
using dl::testing::TestModule;
using dl::testing::TestShlib;
using dl::testing::TestSym;
// Weak symbol resolution tests conditionalize on `ld::kResolverPolicy` to
// verify the symbol chosen based on the resolution strategy (e.g., strict
// link-order vs. strong-over-weak).
static_assert(ld::kResolverPolicy == elfldltl::ResolverPolicy::kStrongOverWeak);
// Test that the module data structure uses its own copy of the module's name,
// so that there is no dependency on the memory backing the original string
// pointer.
TYPED_TEST(DlTests, ModuleNameOwnership) {
std::string file = TestModule("ret17");
this->ExpectRootModule(file);
auto open_first = this->DlOpen(file.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(open_first.is_ok()) << open_first.error_value();
EXPECT_TRUE(open_first.value());
// Changing the contents for this pointer should not affect the dynamic
// linker's state.
file = "FOO";
// A lookup for the original filename should succeed.
auto open_second = this->DlOpen(TestModule("ret17").c_str(), RTLD_NOW | RTLD_LOCAL | RTLD_NOLOAD);
ASSERT_TRUE(open_second.is_ok()) << open_second.error_value();
EXPECT_TRUE(open_second.value());
EXPECT_EQ(open_first.value(), open_second.value());
ASSERT_TRUE(this->DlClose(open_first.value()).is_ok());
ASSERT_TRUE(this->DlClose(open_second.value()).is_ok());
}
// Load a basic file with no dependencies.
TYPED_TEST(DlTests, Basic) {
constexpr int64_t kReturnValue = 17;
const std::string kRet17File = TestModule("ret17");
this->ExpectRootModule(kRet17File);
auto open = this->DlOpen(kRet17File.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(open.is_ok()) << open.error_value();
EXPECT_TRUE(open.value());
// Look up the TestSym("TestStart").c_str() function and call it, expecting it to return 17.
auto sym = this->DlSym(open.value(), TestSym("TestStart").c_str());
ASSERT_TRUE(sym.is_ok()) << sym.error_value();
ASSERT_TRUE(sym.value());
EXPECT_EQ(RunFunction<int64_t>(sym.value()), kReturnValue);
ASSERT_TRUE(this->DlClose(open.value()).is_ok());
}
// Load a file that performs relative relocations against itself. The TestStart
// function's return value is derived from the resolved symbols.
TYPED_TEST(DlTests, Relative) {
constexpr int64_t kReturnValue = 17;
const std::string kRelativeRelocFile = TestModule("relative-reloc");
this->ExpectRootModule(kRelativeRelocFile);
auto open = this->DlOpen(kRelativeRelocFile.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(open.is_ok()) << open.error_value();
EXPECT_TRUE(open.value());
auto sym = this->DlSym(open.value(), TestSym("TestStart").c_str());
ASSERT_TRUE(sym.is_ok()) << sym.error_value();
ASSERT_TRUE(sym.value());
EXPECT_EQ(RunFunction<int64_t>(sym.value()), kReturnValue);
ASSERT_TRUE(this->DlClose(open.value()).is_ok());
}
// Load a file that performs symbolic relocations against itself. The TestStart
// functions' return value is derived from the resolved symbols.
TYPED_TEST(DlTests, Symbolic) {
constexpr int64_t kReturnValue = 17;
const std::string kSymbolicFile = TestModule("symbolic-reloc");
this->ExpectRootModule(kSymbolicFile);
auto open = this->DlOpen(kSymbolicFile.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(open.is_ok()) << open.error_value();
EXPECT_TRUE(open.value());
auto sym = this->DlSym(open.value(), TestSym("TestStart").c_str());
ASSERT_TRUE(sym.is_ok()) << sym.error_value();
ASSERT_TRUE(sym.value());
EXPECT_EQ(RunFunction<int64_t>(sym.value()), kReturnValue);
ASSERT_TRUE(this->DlClose(open.value()).is_ok());
}
// Tests how symbols are resolved when presented with a weak and strong
// definition.
// dlopen one-weak-symbol:
// - foo-v1 -> [[gnu::weak]] foo() returns 2
// - foo-v2 -> foo() returns 7
// call call_foo from one-weak-symbol and expect it to resolve its foo symbol to
// 7 from foo-v2 for musl (which uses strong symbols over weak), or 2
// from from foo-v1 (for glibc which uses the first symbol encountered).
TYPED_TEST(DlTests, OneWeakSymbol) {
const std::string kOneWeakSymbolFile = TestModule("one-weak-symbol");
const std::string kFooV1File = TestShlib("libld-dep-foo-v1-weak");
const std::string kFooV2File = TestShlib("libld-dep-foo-v2");
constexpr int64_t kFooV1ReturnValue = 2;
constexpr int64_t kFooV2ReturnValue = 7;
this->ExpectRootModule(kOneWeakSymbolFile);
this->Needed({kFooV1File, kFooV2File});
auto open = this->DlOpen(kOneWeakSymbolFile.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(open.is_ok()) << open.error_value();
EXPECT_TRUE(open.value());
auto sym = this->DlSym(open.value(), TestSym("call_foo").c_str());
ASSERT_TRUE(sym.is_ok()) << sym.error_value();
ASSERT_TRUE(sym.value());
switch (TestFixture::kResolverPolicy) {
case elfldltl::ResolverPolicy::kStrictLinkOrder: {
EXPECT_EQ(RunFunction<int64_t>(sym.value()), kFooV1ReturnValue);
break;
}
case elfldltl::ResolverPolicy::kStrongOverWeak: {
EXPECT_EQ(RunFunction<int64_t>(sym.value()), kFooV2ReturnValue);
break;
}
}
ASSERT_TRUE(this->DlClose(open.value()).is_ok());
}
// Tests how symbols are resolved when all definitions are weak.
// dlopen all-weak-symbols:
// - foo-v1 -> [[gnu::weak]] foo() returns 2
// - foo-v2 -> [[gnu::weak]] foo() returns 7
// call call_foo from one-weak-symbol and expect it to resolve its foo symbol to
// from foo-v1. All implementations will use the first weak definition found in
// absence of any strong definitions.
TYPED_TEST(DlTests, AllWeakSymbols) {
const std::string kOneWeakSymbolFile = TestModule("all-weak-symbols");
const std::string kFooV1File = TestShlib("libld-dep-foo-v1-weak");
const std::string kFooV2File = TestShlib("libld-dep-foo-v2-weak");
constexpr int64_t kFooV1ReturnValue = 2;
this->ExpectRootModule(kOneWeakSymbolFile);
this->Needed({kFooV1File, kFooV2File});
auto open = this->DlOpen(kOneWeakSymbolFile.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(open.is_ok()) << open.error_value();
EXPECT_TRUE(open.value());
auto sym = this->DlSym(open.value(), TestSym("call_foo").c_str());
ASSERT_TRUE(sym.is_ok()) << sym.error_value();
ASSERT_TRUE(sym.value());
EXPECT_EQ(RunFunction<int64_t>(sym.value()), kFooV1ReturnValue);
ASSERT_TRUE(this->DlClose(open.value()).is_ok());
}
// Test loading a module with relro protections.
TYPED_TEST(DlTests, Relro) {
const std::string kRelroFile = TestModule("relro");
this->ExpectRootModule(kRelroFile);
auto open = this->DlOpen(kRelroFile.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(open.is_ok()) << open.error_value();
ASSERT_TRUE(open.value());
auto sym = this->DlSym(open.value(), TestSym("TestStart").c_str());
ASSERT_TRUE(sym.is_ok()) << sym.error_value();
ASSERT_TRUE(sym.value());
EXPECT_DEATH(RunFunction<int64_t>(sym.value()), "");
ASSERT_TRUE(this->DlClose(open.value()).is_ok());
}
// Test that calling dlopen twice on a file will return the same pointer,
// indicating that the dynamic linker is storing the module in its bookkeeping.
// dlsym() should return a pointer to the same symbol from the same module as
// well.
TYPED_TEST(DlTests, BasicModuleReuse) {
const std::string kRet17File = TestModule("ret17");
this->ExpectRootModule(kRet17File);
auto first_open_ret17 = this->DlOpen(kRet17File.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(first_open_ret17.is_ok()) << first_open_ret17.error_value();
EXPECT_TRUE(first_open_ret17.value());
auto second_open_ret17 = this->DlOpen(kRet17File.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(second_open_ret17.is_ok()) << second_open_ret17.error_value();
EXPECT_TRUE(second_open_ret17.value());
EXPECT_EQ(first_open_ret17.value(), second_open_ret17.value());
auto first_ret17_test_start = this->DlSym(first_open_ret17.value(), TestSym("TestStart").c_str());
ASSERT_TRUE(first_ret17_test_start.is_ok()) << first_ret17_test_start.error_value();
EXPECT_TRUE(first_ret17_test_start.value());
auto second_ret17_test_start =
this->DlSym(second_open_ret17.value(), TestSym("TestStart").c_str());
ASSERT_TRUE(second_ret17_test_start.is_ok()) << second_ret17_test_start.error_value();
EXPECT_TRUE(second_ret17_test_start.value());
EXPECT_EQ(first_ret17_test_start.value(), second_ret17_test_start.value());
ASSERT_TRUE(this->DlClose(first_open_ret17.value()).is_ok());
ASSERT_TRUE(this->DlClose(second_open_ret17.value()).is_ok());
}
// Test that different mutually-exclusive kFiles that were dlopen-ed do not share
// pointers or resolved symbols.
TYPED_TEST(DlTests, UniqueModules) {
const std::string kRet17File = TestModule("ret17");
const std::string kRet23File = TestModule("ret23");
this->ExpectRootModule(kRet17File);
auto open_ret17 = this->DlOpen(kRet17File.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(open_ret17.is_ok()) << open_ret17.error_value();
EXPECT_TRUE(open_ret17.value());
this->ExpectRootModule(kRet23File);
auto open_ret23 = this->DlOpen(kRet23File.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(open_ret23.is_ok()) << open_ret23.error_value();
EXPECT_TRUE(open_ret23.value());
EXPECT_NE(open_ret17.value(), open_ret23.value());
auto ret17_test_start = this->DlSym(open_ret17.value(), TestSym("TestStart").c_str());
ASSERT_TRUE(ret17_test_start.is_ok()) << ret17_test_start.error_value();
EXPECT_TRUE(ret17_test_start.value());
auto sym23_test_start = this->DlSym(open_ret23.value(), TestSym("TestStart").c_str());
ASSERT_TRUE(sym23_test_start.is_ok()) << sym23_test_start.error_value();
EXPECT_TRUE(sym23_test_start.value());
EXPECT_NE(ret17_test_start.value(), sym23_test_start.value());
EXPECT_EQ(RunFunction<int64_t>(ret17_test_start.value()), 17);
EXPECT_EQ(RunFunction<int64_t>(sym23_test_start.value()), 23);
ASSERT_TRUE(this->DlClose(open_ret17.value()).is_ok());
ASSERT_TRUE(this->DlClose(open_ret23.value()).is_ok());
}
// Test that the same module can be located by either its DT_SONAME or filename.
// dlopen libfoo-filename (with DT_SONAME libbar-soname)
// dlopen libbar-soname and expect the same module handle for libfoo-filename.
// dlopen soname-filename-match:
// - libbar-soname
// Expect dlopen for soname-filename-match to reuse libfoo-filename module.
// Expect only 2 modules are loaded: libfoo-filename, soname-filename-match.
TYPED_TEST(DlTests, SonameFilenameMatch) {
const std::string kFilename = TestShlib("libfoo-filename");
const std::string kSoname = TestShlib("libbar-soname");
const std::string kParentFile = TestShlib("libsoname-filename-match");
const size_t initial_loaded_count = GetGlobalCounters(this).adds;
this->Needed({kFilename});
// Open the module by its filename.
auto open_filename = this->DlOpen(kFilename.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(open_filename.is_ok()) << open_filename.error_value();
ASSERT_TRUE(open_filename.value());
// Open the module by its DT_SONAME, expecting it to already be loaded and is
// pointing to the same module as `open_filename`.
auto open_soname = this->DlOpen(kSoname.c_str(), RTLD_NOW | RTLD_LOCAL | RTLD_NOLOAD);
ASSERT_TRUE(open_soname.is_ok()) << open_soname.error_value();
ASSERT_TRUE(open_soname.value());
EXPECT_EQ(open_filename.value(), open_soname.value());
// Expect only the parent-file to be fetched from the filesystem; its dep
// should already be loaded.
this->Needed({kParentFile});
auto open_parent = this->DlOpen(kParentFile.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(open_parent.is_ok()) << open_parent.error_value();
ASSERT_TRUE(open_parent.value());
const size_t updated_loaded_count = GetGlobalCounters(this).adds;
if constexpr (TestFixture::kInaccurateLoadCountAfterSonameMatch) {
EXPECT_EQ(updated_loaded_count, initial_loaded_count + 3);
} else {
EXPECT_EQ(updated_loaded_count, initial_loaded_count + 2);
}
// Expect a file named libbar-soname to be absent from loaded modules.
ModuleInfoList info_list;
EXPECT_EQ(this->DlIteratePhdr(CollectModulePhdrInfo, &info_list), 0);
EXPECT_THAT(info_list,
Not(Contains(Property(&ModulePhdrInfo::name, TestShlib("libbar-soname")))));
ASSERT_TRUE(this->DlClose(open_parent.value()).is_ok());
ASSERT_TRUE(this->DlClose(open_filename.value()).is_ok());
ASSERT_TRUE(this->DlClose(open_soname.value()).is_ok());
}
// Test a linking session will match the DT_SONAME of a module that is in the
// loading queue but has not yet been loaded.
// dlopen soname-filename-dep:
// - soname-filename-match
// - libbar-soname
// - libfoo-filename (with DT_SONAME libbar-soname)
// Since libbar-soname was enqueued before libfoo-filename was loaded, this
// tests if the loading logic can match libbar-soname with libfoo-filename's
// DT_SONAME. Glibc does perform this matching, but Musl/Libdl does not.
// Check there should be 3 modules that were loaded after this dlopen:
// soname-filename-loaded-dep, soname-filename-match, and libfoo-filename.
TYPED_TEST(DlTests, SonameFilenameDep) {
const std::string kLibFooFile = TestShlib("libfoo-filename");
const std::string kHasLibBarFile = TestShlib("libsoname-filename-match");
const std::string kHasDepsFile = TestModule("soname-filename-loaded-dep");
if constexpr (!TestFixture::kSonameLookupInPendingDeps) {
GTEST_SKIP()
<< "Fixture should be able to look up DT_SONAME in pending deps in a linking session";
}
const size_t initial_loaded_count = GetGlobalCounters(this).adds;
this->ExpectRootModule(kHasDepsFile);
this->Needed({kHasLibBarFile, kLibFooFile});
auto open_has_deps = this->DlOpen(kHasDepsFile.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(open_has_deps.is_ok()) << open_has_deps.error_value();
ASSERT_TRUE(open_has_deps.value());
const size_t updated_loaded_count = GetGlobalCounters(this).adds;
EXPECT_EQ(updated_loaded_count, initial_loaded_count + 3);
// Expect a file named libar-soname to be absent from loaded modules.
ModuleInfoList info_list;
EXPECT_EQ(this->DlIteratePhdr(CollectModulePhdrInfo, &info_list), 0);
EXPECT_THAT(info_list,
Not(Contains(Property(&ModulePhdrInfo::name, TestShlib("libbar-soname")))));
ASSERT_TRUE(this->DlClose(open_has_deps.value()).is_ok());
}
// Test a linking session will match the DT_SONAME of a module that has already
// been loaded in the same linking session. This test is similar to above,
// except the module with the matching DT_SONAME has been loaded by the time the
// pending module is looking for a match.
// dlopen soname-filename-loaded-dep:
// - libfoo-filename (with DT_SONAME libbar-soname)
// - soname-filename-match
// - libbar-soname
// Expect that libbar-soname will reuse the module for libfoo-filename, because
// libfoo-filename was loaded/decoded by the time libbar-soname is added to the
// loading queue.
// Check there should be 3 modules that were loaded after this dlopen:
// soname-filename-loaded-dep, libfoo-filename, and
// soname-filename-match
TYPED_TEST(DlTests, SonameFilenameLoadedDep) {
const std::string kLibFooFile = TestShlib("libfoo-filename");
const std::string kHasLibBarFile = TestShlib("libsoname-filename-match");
const std::string kHasDepsFile = TestModule("soname-filename-loaded-dep");
if constexpr (!TestFixture::kSonameLookupInLoadedDeps) {
GTEST_SKIP()
<< "Fixture should be able to look up DT_SONAME in loaded deps in a linking session.";
}
const size_t initial_loaded_count = GetGlobalCounters(this).adds;
this->ExpectRootModule(kHasDepsFile);
this->Needed({kLibFooFile, kHasLibBarFile});
auto open_has_deps = this->DlOpen(kHasDepsFile.c_str(), RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(open_has_deps.is_ok()) << open_has_deps.error_value();
ASSERT_TRUE(open_has_deps.value());
const size_t updated_loaded_count = GetGlobalCounters(this).adds;
if constexpr (TestFixture::kInaccurateLoadCountAfterSonameMatch) {
// For some reason, Musl only counts one additional module.
EXPECT_EQ(updated_loaded_count, initial_loaded_count + 1);
} else {
EXPECT_EQ(updated_loaded_count, initial_loaded_count + 3);
}
// Expect a file named libar-soname to be absent from loaded modules.
ModuleInfoList info_list;
EXPECT_EQ(this->DlIteratePhdr(CollectModulePhdrInfo, &info_list), 0);
EXPECT_THAT(info_list,
Not(Contains(Property(&ModulePhdrInfo::name, TestShlib("libbar-soname")))));
ASSERT_TRUE(this->DlClose(open_has_deps.value()).is_ok());
}
// Test that passing a NULL or an empty string as the file arg will return a
// handle to the executable.
TYPED_TEST(DlTests, NullFileArgBasic) {
const std::string kEmpty;
auto null_open = this->DlOpen(NULL, RTLD_NOW | RTLD_LOCAL);
ASSERT_TRUE(null_open.is_ok()) << null_open.error_value();
ASSERT_TRUE(null_open.value());
// To verify the module handle dlopen() returned is for the executable, we
// that the module's link_map.l_ld points to dynamic section of this test
// binary.
EXPECT_EQ(this->ModuleLinkMap(null_open.value())->l_ld,
reinterpret_cast<const Elf64_Dyn*>(elfldltl::Self<>::Dynamic().data()));
auto empty_str_open = this->DlOpen(kEmpty.c_str(), RTLD_NOW | RTLD_LOCAL | RTLD_NOLOAD);
ASSERT_TRUE(empty_str_open.is_ok()) << empty_str_open.error_value();
EXPECT_TRUE(empty_str_open.value());
// Expect that the handle for an empty string is the same as the handle for
// a NULL argument, which is the executable.
EXPECT_EQ(empty_str_open.value(), null_open.value());
// TODO(https://fxbug.dev/339294119): TODO test that dlsym() will resolve
// symbols correctly with this handle.
ASSERT_TRUE(this->DlClose(null_open.value()).is_ok());
ASSERT_TRUE(this->DlClose(empty_str_open.value()).is_ok());
}
} // namespace