#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include "../rpm-build/lib/set.h" } namespace { using Clock = std::chrono::steady_clock; using Nanoseconds = std::chrono::nanoseconds; using LabelSet = std::set; struct CommandResult { int exit_code; std::string output; }; struct Timings { std::int64_t set_new_ns = 0; std::int64_t set_add_total_ns = 0; std::int64_t set_fini_ns = 0; std::int64_t set_free_ns = 0; std::int64_t total_ns() const { return set_new_ns + set_add_total_ns + set_fini_ns + set_free_ns; } }; struct SetResult { std::string value; Timings timings; }; struct Options { std::filesystem::path input; std::optional bpp; }; std::int64_t elapsed_ns(Clock::time_point start, Clock::time_point finish) { return std::chrono::duration_cast(finish - start).count(); } std::string shortened(const std::string& value, std::size_t limit = 1200) { if (value.size() <= limit) { return value; } return value.substr(0, limit) + "\n... output truncated ..."; } CommandResult run_command(const std::vector& command, const std::map& environment = {}) { if (command.empty()) { throw std::runtime_error("empty command"); } int output_pipe[2]; if (pipe(output_pipe) != 0) { throw std::runtime_error("pipe failed: " + std::string(std::strerror(errno))); } const pid_t child = fork(); if (child < 0) { const int saved_errno = errno; close(output_pipe[0]); close(output_pipe[1]); throw std::runtime_error("fork failed: " + std::string(std::strerror(saved_errno))); } if (child == 0) { close(output_pipe[0]); if (dup2(output_pipe[1], STDOUT_FILENO) < 0 || dup2(output_pipe[1], STDERR_FILENO) < 0) { _exit(126); } close(output_pipe[1]); setenv("LC_ALL", "C", 1); for (const auto& [name, value] : environment) { setenv(name.c_str(), value.c_str(), 1); } std::vector argv; argv.reserve(command.size() + 1); for (const std::string& argument : command) { argv.push_back(const_cast(argument.c_str())); } argv.push_back(nullptr); execvp(argv[0], argv.data()); _exit(127); } close(output_pipe[1]); std::string output; char buffer[16384]; while (true) { const ssize_t count = read(output_pipe[0], buffer, sizeof(buffer)); if (count > 0) { output.append(buffer, static_cast(count)); continue; } if (count < 0 && errno == EINTR) { continue; } if (count < 0) { const int saved_errno = errno; close(output_pipe[0]); waitpid(child, nullptr, 0); throw std::runtime_error("read from child failed: " + std::string(std::strerror(saved_errno))); } break; } close(output_pipe[0]); int status = 0; while (waitpid(child, &status, 0) < 0) { if (errno != EINTR) { throw std::runtime_error("waitpid failed: " + std::string(std::strerror(errno))); } } int exit_code = 128; if (WIFEXITED(status)) { exit_code = WEXITSTATUS(status); } else if (WIFSIGNALED(status)) { exit_code += WTERMSIG(status); } return {exit_code, std::move(output)}; } CommandResult checked_command(const std::vector& command, const std::map& environment = {}) { CommandResult result = run_command(command, environment); if (result.exit_code != 0) { std::ostringstream message; message << command.front() << " exited with " << result.exit_code; if (!result.output.empty()) { message << ":\n" << shortened(result.output); } throw std::runtime_error(message.str()); } return result; } std::string strip_symbol_version(std::string symbol) { const std::size_t at = symbol.find('@'); if (at != std::string::npos) { symbol.resize(at); } return symbol; } bool allowed_symbol_type(const std::string& type) { static const std::unordered_set allowed = { "NOTYPE", "OBJECT", "FUNC", "COMMON", "TLS", "IFUNC", "GNU_IFUNC", }; return allowed.count(type) != 0; } bool allowed_symbol_binding(const std::string& binding) { static const std::unordered_set allowed = { "GLOBAL", "WEAK", "UNIQUE", "GNU_UNIQUE", }; return allowed.count(binding) != 0; } bool allowed_symbol_visibility(const std::string& visibility) { return visibility == "DEFAULT" || visibility == "PROTECTED"; } bool special_symbol(const std::string& symbol) { static const std::unordered_set ignored = { "__bss_start", "_edata", "_end", "_fini", "_init", }; return ignored.count(symbol) != 0; } LabelSet provided_labels(const std::filesystem::path& library) { const CommandResult result = checked_command( {"eu-readelf", "--wide", "--dyn-syms", library.string()}); LabelSet labels; std::istringstream output(result.output); std::string line; while (std::getline(output, line)) { std::istringstream fields(line); std::string number; std::string value; std::string size; std::string type; std::string binding; std::string visibility; std::string section; std::string symbol; if (!(fields >> number >> value >> size >> type >> binding >> visibility >> section >> symbol)) { continue; } if (number.empty() || number.back() != ':') { continue; } std::uint64_t symbol_value = 0; try { symbol_value = std::stoull(value, nullptr, 16); } catch (const std::exception&) { continue; } if (symbol_value == 0 && type != "TLS") { continue; } if (!allowed_symbol_type(type) || !allowed_symbol_binding(binding) || !allowed_symbol_visibility(visibility)) { continue; } symbol = strip_symbol_version(std::move(symbol)); if (symbol.empty() || special_symbol(symbol) || symbol.find_first_of("()@") != std::string::npos) { continue; } labels.insert(std::move(symbol)); } return labels; } LabelSet weak_undefined_labels(const std::filesystem::path& executable) { const CommandResult result = checked_command({"nm", "--dynamic", executable.string()}); LabelSet weak; std::istringstream output(result.output); std::string line; while (std::getline(output, line)) { std::istringstream fields(line); std::vector parts; std::string part; while (fields >> part) { parts.push_back(part); } if (parts.size() != 2 || parts[0].size() != 1 || std::string("wWvV").find(parts[0][0]) == std::string::npos) { continue; } weak.insert(strip_symbol_version(parts[1])); } return weak; } std::string program_interpreter(const std::filesystem::path& executable) { const CommandResult result = checked_command( {"eu-readelf", "--program-headers", executable.string()}); const std::string marker = "[Requesting program interpreter: "; const std::size_t start = result.output.find(marker); if (start == std::string::npos) { throw std::runtime_error("ELF executable has no PT_INTERP entry: " + executable.string()); } const std::size_t value_start = start + marker.size(); const std::size_t end = result.output.find(']', value_start); if (end == std::string::npos || end == value_start) { throw std::runtime_error("cannot parse PT_INTERP for " + executable.string()); } return result.output.substr(value_start, end - value_start); } bool same_path(const std::string& lhs, const std::filesystem::path& rhs) { std::error_code error; if (std::filesystem::equivalent(lhs, rhs, error)) { return true; } return std::filesystem::path(lhs).lexically_normal() == rhs.lexically_normal(); } std::map required_labels_by_provider( const std::filesystem::path& executable) { const std::string interpreter = program_interpreter(executable); const std::map environment = { {"LD_BIND_NOW", "1"}, {"LD_DEBUG", "bindings"}, {"LD_TRACE_LOADED_OBJECTS", "1"}, {"LD_WARN", "1"}, }; const CommandResult result = checked_command( {interpreter, executable.string()}, environment); const LabelSet weak = weak_undefined_labels(executable); std::map labels_by_provider; std::istringstream output(result.output); std::string line; while (std::getline(output, line)) { const std::string binding_marker = "binding file "; const std::size_t binding = line.find(binding_marker); if (binding == std::string::npos) { continue; } const std::size_t source_start = binding + binding_marker.size(); const std::size_t source_end = line.find(" [", source_start); const std::size_t to = source_end == std::string::npos ? std::string::npos : line.find(" to ", source_end); const std::size_t provider_start = to == std::string::npos ? std::string::npos : to + std::string(" to ").size(); const std::size_t provider_end = provider_start == std::string::npos ? std::string::npos : line.find(" [", provider_start); const std::size_t symbol_marker = provider_end == std::string::npos ? std::string::npos : line.find(" symbol ", provider_end); if (source_end == std::string::npos || to == std::string::npos || provider_start == std::string::npos || provider_end == std::string::npos || symbol_marker == std::string::npos) { continue; } const std::string source = line.substr(source_start, source_end - source_start); const std::string provider = line.substr(provider_start, provider_end - provider_start); if (!same_path(source, executable) || source == provider) { continue; } std::size_t symbol_start = symbol_marker + std::string(" symbol ").size(); if (symbol_start < line.size() && (line[symbol_start] == '`' || line[symbol_start] == '\'')) { ++symbol_start; } std::size_t symbol_end = line.find('\'', symbol_start); if (symbol_end == std::string::npos) { symbol_end = line.find_first_of(" \t[", symbol_start); } if (symbol_end == std::string::npos) { symbol_end = line.size(); } std::string symbol = strip_symbol_version( line.substr(symbol_start, symbol_end - symbol_start)); if (symbol.empty() || weak.count(symbol) != 0) { continue; } labels_by_provider[provider].insert(std::move(symbol)); } for (auto it = labels_by_provider.begin(); it != labels_by_provider.end();) { if (it->second.empty()) { it = labels_by_provider.erase(it); } else { ++it; } } return labels_by_provider; } int suggested_bpp(std::size_t provider_label_count) { if (provider_label_count < 1) { provider_label_count = 1; } std::size_t value = provider_label_count - 1; int bits = 0; while (value != 0) { ++bits; value >>= 1; } return std::min(32, bits + 10); } SetResult build_set(const LabelSet& labels, int bpp) { if (labels.empty()) { throw std::runtime_error("cannot build a set from zero labels"); } Timings timings; auto start = Clock::now(); struct set* value = set_new(); auto finish = Clock::now(); timings.set_new_ns = elapsed_ns(start, finish); if (value == nullptr) { throw std::runtime_error("set_new returned NULL"); } start = Clock::now(); for (const std::string& label : labels) { set_add(value, label.c_str()); } finish = Clock::now(); timings.set_add_total_ns = elapsed_ns(start, finish); start = Clock::now(); const char* payload = set_fini(value, bpp); finish = Clock::now(); timings.set_fini_ns = elapsed_ns(start, finish); if (payload == nullptr) { set_free(value); throw std::runtime_error("set_fini returned NULL"); } std::string encoded = "set:" + std::string(payload); std::free(const_cast(payload)); start = Clock::now(); value = set_free(value); finish = Clock::now(); timings.set_free_ns = elapsed_ns(start, finish); (void)value; return {std::move(encoded), timings}; } enum class ElfKind { executable, shared_library, }; ElfKind classify_elf(const std::filesystem::path& input) { const CommandResult executable = run_command( {"eu-elfclassify", "--executable", input.string()}); if (executable.exit_code == 0) { return ElfKind::executable; } if (executable.exit_code == 127) { throw std::runtime_error("eu-elfclassify is required but was not found"); } const CommandResult shared = run_command( {"eu-elfclassify", "--shared", input.string()}); if (shared.exit_code == 0) { return ElfKind::shared_library; } throw std::runtime_error("input is not a supported dynamic ELF executable or shared library: " + input.string()); } int parse_bpp(const std::string& value) { std::size_t parsed = 0; int bpp = 0; try { bpp = std::stoi(value, &parsed); } catch (const std::exception&) { throw std::runtime_error("invalid bpp: " + value); } if (parsed != value.size() || bpp < 10 || bpp > 32) { throw std::runtime_error("bpp must be an integer in [10, 32]"); } return bpp; } void usage(const char* program) { std::cerr << "Usage: " << program << " [--bpp 10..32] ELF_PATH\n"; } Options parse_options(int argc, char** argv) { Options options; for (int index = 1; index < argc; ++index) { const std::string argument = argv[index]; if (argument == "--help" || argument == "-h") { usage(argv[0]); std::exit(0); } if (argument == "--bpp") { if (++index >= argc) { throw std::runtime_error("--bpp requires a value"); } options.bpp = parse_bpp(argv[index]); continue; } if (!argument.empty() && argument.front() == '-') { throw std::runtime_error("unknown option: " + argument); } if (!options.input.empty()) { throw std::runtime_error("exactly one ELF path is required"); } options.input = argument; } if (options.input.empty()) { throw std::runtime_error("ELF path is required"); } return options; } void print_header(const std::filesystem::path& input, ElfKind kind, const std::optional& bpp) { std::cout << "input\t" << input.string() << '\n'; std::cout << "kind\t" << (kind == ElfKind::executable ? "executable" : "shared-library") << '\n'; std::cout << "bpp_mode\t" << (bpp.has_value() ? "override" : "auto") << '\n'; std::cout << "role\tobject\tlabels\tbpp\tset\tset_new_ns\tset_add_total_ns\t" "set_fini_ns\tset_free_ns\tset_api_total_ns\n"; } void print_row(const std::string& role, const std::string& object, std::size_t label_count, int bpp, const SetResult& result) { const Timings& timings = result.timings; std::cout << role << '\t' << object << '\t' << label_count << '\t' << bpp << '\t' << result.value << '\t' << timings.set_new_ns << '\t' << timings.set_add_total_ns << '\t' << timings.set_fini_ns << '\t' << timings.set_free_ns << '\t' << timings.total_ns() << '\n'; } int run(const Options& options) { std::error_code error; const std::filesystem::path input = std::filesystem::canonical(options.input, error); if (error || !std::filesystem::is_regular_file(input)) { throw std::runtime_error("input is not a readable regular file: " + options.input.string()); } if (input.string().find_first_of("\t\r\n") != std::string::npos) { throw std::runtime_error("input path contains characters unsupported by TSV output"); } const ElfKind kind = classify_elf(input); print_header(input, kind, options.bpp); if (kind == ElfKind::shared_library) { const LabelSet labels = provided_labels(input); if (labels.empty()) { throw std::runtime_error("no provided labels found in " + input.string()); } const int bpp = options.bpp.value_or(suggested_bpp(labels.size())); print_row("provided", input.string(), labels.size(), bpp, build_set(labels, bpp)); return 0; } const std::map requirements = required_labels_by_provider(input); if (requirements.empty()) { throw std::runtime_error("no external dynamic symbol bindings found in " + input.string()); } for (const auto& [provider, labels] : requirements) { std::size_t provider_count = 0; try { provider_count = provided_labels(provider).size(); } catch (const std::exception& exception) { std::cerr << "warning: cannot inspect provider " << provider << ": " << exception.what() << '\n'; } const int bpp = options.bpp.value_or( suggested_bpp(provider_count == 0 ? labels.size() : provider_count)); print_row("required", provider, labels.size(), bpp, build_set(labels, bpp)); } return 0; } } // namespace int main(int argc, char** argv) { try { return run(parse_options(argc, argv)); } catch (const std::exception& exception) { std::cerr << "run_set: " << exception.what() << '\n'; usage(argv[0]); return 2; } }