App::Ariza::ELF

NAME

App::Ariza::ELF - read what an ELF file asks the loader for, with no toolchain installed

SYNOPSIS


use App::Ariza::ELF;

my %d = elf-dynamic('native/sqlcipher/libsqlcipher.so.0');

say %d<class>;      # 64
say %d<endian>;     # little
say %d<needed>;     # (libcrypto.so.3 libc.so.6)
say %d<runpath>;    # $ORIGIN
say %d<rpath>;      # (Str) -- this file has no DT_RPATH

# What the loader will search, which is what `patchelf --print-rpath`
# prints: DT_RUNPATH where there is one, DT_RPATH otherwise.
say elf-loader-rpath(%d);                   # $ORIGIN

# A file built with the old tag, pointing at the machine that built it:
say elf-loader-rpath(elf-dynamic('libfoo.so'));   # /opt/build/lib:$ORIGIN

# A static executable has nothing to ask for, and says so:
my %s = elf-dynamic('static-tool');
say %s<dynamic>;    # False
say %s<needed>;     # ()

# Anything that does not parse is a death, never a guess:
elf-dynamic('truncated.so');
# ariza: cannot read the dynamic section of /…/truncated.so: reading its
# dynamic section needs 400 bytes at offset 600, and the file is 700
# bytes long

DESCRIPTION

Everything App::Ariza::Native needs to know about an ELF shared library before it will let one into a bundle is in three entries of its dynamic section: the DT_NEEDED sonames it asks the loader for, and the DT_RPATH or DT_RUNPATH the loader is told to search for them. This module reads those, and nothing else, out of the file's bytes.

It exists because the tools that otherwise answer the question are not on the machines that need the answer. readelf comes with binutils, which a stock Linux install, a slim container image and every Mac lack; patchelf is a package almost nothing installs by default. An audit that dies because a tool is missing cannot run where it is wanted, and one that answers "no dependencies" when the tool is missing passes the bundle it exists to stop. Reading the format directly has neither failure, and gives the same answer on every host — an ELF bundle can be checked from a Mac or a Windows box exactly as it is checked on Linux.

What is read, and how

The walk is the loader's, not the static linker's:

  • The identification bytes: the \x7FELF magic, EI_CLASS (32- or 64-bit), EI_DATA (little- or big-endian) and EI_VERSION. Every field after them is read at the width and in the byte order they name, so one reader covers x86_64 and i686, aarch64 and s390x, PowerPC both ways round.

  • The ELF header, for where the program headers are, how many there are, and how big each one is.

  • The program headers: the PT_LOAD segments, which are the map between the addresses a dynamic section records and the bytes in the file, and the one PT_DYNAMIC segment, which is the dynamic section.

  • The dynamic section's entries, up to the DT_NULL that ends it.

  • The string table, found through DT_STRTAB — an address, mapped to a file offset through the PT_LOAD segment that holds it — and bounded by DT_STRSZ.

The section table is not read. It is the static linker's account of the file, it can be stripped without changing what loads, and it can disagree with the segments; the loader reads only the segments, so this does too. The one exception is the format's own: when e_phnum is PN_XNUM (0xFFFF), the real program-header count is in section header 0, and is read from there.

$ORIGIN is not expanded and no search is performed. This reports what the file asks for; where the asking would end up on a given machine is ldd's question, and only that machine can answer it.

What counts as malformed

Each of these is a die that names the file and the part of it that was wrong, rather than an answer:

  • a file too short for the structure being read — the commonest case, and the message says which structure wanted how many bytes where;

  • no ELF magic number, or an EI_CLASS, EI_DATA or EI_VERSION byte that is not one the format defines;

  • a program header size that is not the one the class defines, or program headers that overlap the ELF header;

  • an executable or shared object with no program headers, which no loader could map;

  • more than one PT_DYNAMIC;

  • a dynamic section with no DT_NULL inside it;

  • more than one DT_RPATH, DT_RUNPATH, DT_STRTAB or DT_STRSZ, which no linker writes, and between which the loader's choice is not something to guess at;

  • DT_NEEDED, DT_RPATH or DT_RUNPATH entries and no DT_STRTAB;

  • a DT_STRTAB address that is in no PT_LOAD, or in the part of one that has no bytes in the file, or a DT_STRSZ that runs past its segment;

  • a string offset past the end of the string table, a string with no terminating NUL inside it, or a DT_NEEDED naming the empty string.

Two things that look odd are not malformed, and are answered rather than refused: a file with no PT_DYNAMIC (a static executable) or no program headers at all (an object file) has dynamic False and needs nothing; and a file with both DT_RPATH and DT_RUNPATH — which older GNU linkers wrote for --enable-new-dtags — reports both, with elf-loader-rpath choosing as the loader does.

FUNCTIONS

elf-dynamic(IO() $file --> Hash)

{ class, endian, dynamic, needed, rpath, runpath }, as described above; or a die. needed is a List in file order, duplicates and all; rpath and runpath are Str, undefined where the tag is absent and the empty string where the tag is present and empty. Strings are decoded as utf8-c8, so a path that is not valid UTF-8 survives the round trip byte for byte. Exported.

elf-loader-rpath(%dynamic --> Str)

The search path the loader uses, from an elf-dynamic result: runpath if defined, else rpath if defined, else the empty string. That is the loader's precedence and patchelf --print-rpath's output. Exported.

SEE ALSO

App::Ariza::Native, whose Linux audit and staging pass read ELF files through this module, and t/fixtures/elf/, whose build.sh builds the fixtures the tests read and records what readelf -d and patchelf --print-rpath say about each one.

AUTHOR

Matt Doughty

COPYRIGHT AND LICENSE

Copyright 2026 Matt Doughty

This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.

App::Ariza v0.3.1

bundler and distribution tool for Raku terminal apps

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

Config::TOML:ver<0.1.3+>:auth<zef:raku-community-modules>JSON::Fast:ver<0.21.0+>:auth<zef:timo>Template::Jinja2:ver<0.3.0+>:auth<zef:apogee>

Test Dependencies

Provides

  • App::Ariza
  • App::Ariza::Bundle
  • App::Ariza::CI
  • App::Ariza::Config
  • App::Ariza::ELF
  • App::Ariza::Installer
  • App::Ariza::Launcher
  • App::Ariza::Licensing
  • App::Ariza::Native
  • App::Ariza::Platform
  • App::Ariza::Rakudo
  • App::Ariza::Resources
  • App::Ariza::Runner
  • App::Ariza::Site
  • App::Ariza::Smoke
  • App::Ariza::Tools
  • App::Ariza::Update
  • App::Ariza::Versions

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite — the markup and publishing tools behind this site.