DEV Community

Vincent Tran
Vincent Tran

Posted on Originally published at 0xgosu.dev on

What Changes When Your Executable Is a SQLite Database

Linux programs usually arrive as ELF files: compact binary containers whose headers, segments, symbols, relocations, string tables, and dependency records tell the kernel and dynamic linker how to create a process. The format is dependable and universal, but it is not pleasant to inspect or change. Its relationships are encoded as offsets, indexes, conventions, and specialized structures. Every serious tool needs an ELF parser, and every modification risks moving data that some other field points toward.

What if the executable were a relational database instead?

That is the premise of SELF , the Structured Executable & Linkable Format. A SELF file is a valid SQLite database containing executable segments as BLOBs and linking metadata as ordinary rows. After chmod +x, Linux can launch it through a registered interpreter. The same file can also be opened with sqlite3, queried with joins, changed in a transaction, or packed with its shared-library closure.

This is not merely an ELF catalog stored beside a program. The database is the executable format. The experiment works because an executable is already structured data; ELF simply expresses that structure with a highly specialized physical layout.

ELF already behaves like a small database

An ELF file starts with a header that points to other tables. Program headers describe the segments needed at runtime. Section headers organize material used by linkers, debuggers, and analysis tools. Symbol entries refer to names in separate string tables. Dynamic-linking records identify required libraries, relocations, versions, and lookup structures.

Those mechanisms have close relational equivalents:

ELF mechanism Relational equivalent
section and program-header tables schema tables describing stored records
offsets into string tables foreign keys to shared text values
.hash and .gnu.hash indexes over symbol names
symbol-version sections version columns and relationships
readelf, nm, and ldd traversals projections, filters, and joins
removing optional debug sections deleting rows and reclaiming pages

ELF’s design is not a historical mistake. Loadable segments can be mapped efficiently, the format has no database engine in its startup path, and decades of operating-system and toolchain work understand it. Its dense representation is exactly why it remains such an effective execution format.

The cost appears when people need to understand or alter it. Tools such as the kernel loader, ld.so, binutils, debuggers, security scanners, package systems, and language libraries all implement overlapping parts of the same parser. Extending a packed format also demands great care because byte positions are part of the meaning.

SELF asks whether a stable, self-describing storage engine can absorb that complexity and make the logical model explicit.

The minimum runnable schema is surprisingly small

At its core, a SELF executable needs metadata describing the target and a segments table describing the memory image. Each segment row carries the familiar loader facts: its type, virtual address, file and memory sizes, alignment, read/write/execute flags, and the bytes to load.

Conceptually, the essential table looks like this:

CREATE TABLE segments (
  id INTEGER PRIMARY KEY,
  type TEXT NOT NULL,
  vaddr INTEGER NOT NULL,
  filesz INTEGER NOT NULL,
  memsz INTEGER NOT NULL,
  r INTEGER NOT NULL,
  w INTEGER NOT NULL,
  x INTEGER NOT NULL,
  align INTEGER NOT NULL,
  content BLOB
);

Enter fullscreen mode Exit fullscreen mode

The symbol table becomes just as direct. A symbol name is text rather than an offset into another byte array. Binding, visibility, version, size, and whether the symbol is defined are columns. A normal SQLite index over (name, version) replaces ELF-specific lookup machinery.

Views restore familiar questions without cloning familiar tools:

CREATE VIEW imports AS
SELECT name, version
FROM symbols
WHERE defined = 0;

CREATE VIEW exports AS
SELECT name, version, type, size
FROM symbols
WHERE exported = 1;

CREATE VIEW ldd AS
SELECT ord, soname
FROM needed
ORDER BY ord;

Enter fullscreen mode Exit fullscreen mode

This changes the unit of extension. Adding metadata no longer requires allocating a new binary section type and updating every parser. A producer can add a table or column while older consumers keep reading the parts they understand. Constraints can make invalid relationships harder to create, while indexes can be added for the queries that matter.

It also separates runtime truth from optional tooling data. Segment bytes and required linking records make the program run. Sections, notes, and richer analysis metadata can remain queryable without being mandatory. Removing those optional rows is the database equivalent of stripping a binary.

How Linux launches a database

SQLite reserves four bytes at offset 68 of its 100-byte header for an application ID. SELF stores the bytes SELF there. The file therefore remains recognizable as SQLite while carrying an unambiguous subtype marker.

Linux already has a mechanism for launching non-native formats. binfmt_misc associates a filename extension or a magic-byte pattern with an interpreter. It is commonly used for formats that need a user-space runtime. SELF registers the SQLite header plus its application ID and points the match at self-exec.

“A
The kernel recognizes the format; a user-space interpreter performs the database-backed loading work.

The launch sequence is straightforward:

  1. A process calls execve on the SELF file.
  2. The kernel sees the registered magic bytes and starts self-exec with the file path.
  3. The interpreter opens SQLite and validates the SELF metadata.
  4. It reads segment rows, creates the required memory mappings, and copies segment BLOBs into them.
  5. It resolves dynamic symbols and applies relocations.
  6. It prepares process state and transfers control to the recorded entry address.

The interpreter itself must remain an ordinary ELF executable. If it matched its own binfmt_misc rule, launching it would recurse until the kernel rejected the loop.

SELF’s repository contains three loader strategies, each useful for a different stage of the experiment. A compatibility path reconstructs an ELF image in a memory-backed file and invokes it. A native path maps SELF segments and hands dynamic work to the system linker. The most ambitious path implements the dynamic linker itself, with symbol binding driven by SQL. Together they let the project test the format without requiring every hard loader feature to be replaced at once.

Dynamic linking becomes a query problem

Starting a statically linked hello proves that bytes can be loaded. Dynamic linking is where the relational model earns its keep.

A dynamic linker must load objects in a defined order, find the symbol that satisfies each relocation, respect binding and version rules, calculate addresses using each object’s load bias, and patch the right location. ELF represents the required information across several connected structures. SELF represents the same search as relationships among objects, symbols, and relocations.

A simplified binding query looks like this:

SELECT s.value + o.load_bias
FROM relocations AS r
JOIN symbols AS s ON s.id = r.symbol
JOIN objects AS o ON o.id = s.object
WHERE r.id = ?
ORDER BY o.load_order
LIMIT 1;

Enter fullscreen mode Exit fullscreen mode

SQL does not eliminate linker semantics. The loader still has to implement TLS, symbol versions, weak bindings, IFUNC behavior, relocation types, memory protection, and architecture-specific details. What changes is how the state is represented and explored. Lookup order is visible data. Missing definitions can be queried. Dependency resolution can be checked before execution. An investigator can ask the file questions without decoding several layers of offsets first.

The project also demonstrates a hybrid approach using glibc’s runtime-linker audit interface. An audit library intercepts shared-object searches and answers them from a SQLite system database, while stock ld.so retains responsibility for mapping and relocation. That is a practical experimental pattern: replace the lookup policy while borrowing the mature loader for everything else.

Tooling becomes data manipulation

Once executable structure is rows, traditional binary utilities collapse into familiar database operations.

To list required libraries:

sqlite3 app.self 'SELECT soname FROM ldd'

Enter fullscreen mode Exit fullscreen mode

To inspect imported functions:

sqlite3 app.self \
  'SELECT name, version FROM imports ORDER BY name'

Enter fullscreen mode Exit fullscreen mode

To strip optional records:

sqlite3 app.self \
  'DELETE FROM sections; DELETE FROM notes; VACUUM;'

Enter fullscreen mode Exit fullscreen mode

A patchelf-style change becomes an UPDATE. Adding an index can speed up a new analysis workload without redesigning the file format. Multiple related edits can happen inside one transaction, so readers see the state before or after a change rather than a half-rewritten binary.

That power also changes the security model. Ordinary code-signing and package-verification workflows assume that the executable is an immutable byte sequence. A database invites mutation. Transactions provide atomicity, not authenticity: they prevent torn logical updates but do not prove who authorized a change. A production design would still need signed snapshots, read-only deployment, policy around write access, reproducible conversion, and clear handling of journals or write-ahead logs. Queryability does not replace trust.

A database can contain the whole dependency closure

The larger idea is not one database per executable. A SELF database can contain a root program plus every shared object it needs.

In a normal Linux environment, a dependency record names a shared object such as libc.so.6. The dynamic linker then searches configured paths to discover which file supplies it. That name alone does not identify one immutable artifact. Nix improves the situation by embedding resolved store paths in runtime search paths.

SELF can record the resolved edge directly:

CREATE TABLE needs (
  object_id INTEGER REFERENCES objects(id),
  ord INTEGER NOT NULL,
  soname TEXT NOT NULL,
  resolved_path TEXT REFERENCES objects(path)
);

Enter fullscreen mode Exit fullscreen mode

Now the dependency graph is explicit. A library relationship is a foreign key, and ldd is a join. Packaging a closure produces one portable database containing the root, its dependency identities, metadata, and segment bytes.

“Nested
Moving from one executable to many lets shared libraries and indexes amortize the database overhead.

The prototype pushes this to a whole userland. Its published experiment packs 723 executables and 400 distinct shared libraries—1,123 objects in total—into one SQLite file. The database records 346,386 symbols and 3,808 dependency edges. Because common libraries and metadata structures are shared instead of copied into private application bundles, the result is 611.9 MiB compared with 644.4 MiB for the source ELF files.

That result is more important than the exact numbers. A relational schema makes deduplication a natural consequence of identity and references. The database can represent many roots sharing one dependency object, while a bundle-per-application model tends to repeat the same libraries.

It also creates new system-wide operations. A preload policy can be a row instead of an environment variable. Enabling a tracing library across many programs can be one transaction, and rolling it back can restore the previous dependency graph atomically. Whether that is desirable in production is a separate question, but the experiment reveals operations that are awkward when executable state is scattered across thousands of files.

The performance bill is real

ELF’s physical layout serves execution. SQLite’s serves queries and transactional updates. Trading one for the other has measurable costs.

For a small unstripped program, SQLite page structure and optional metadata can make a SELF file roughly twice the size of its ELF source. Much of that overhead disappears after deleting tooling tables: in the published coreutils comparison, a stripped SELF file is 1,794,048 bytes versus 1,768,632 bytes for ELF, a difference of about one percent.

Startup is the harder problem. The prototype reports a fixed cost of roughly 5 ms to start the interpreter and open SQLite, plus work proportional to the image being loaded. ELF permits loadable file pages to be memory-mapped directly. SELF currently reads BLOB content from SQLite’s b-tree pages and copies it into executable mappings. Two processes running the same SELF program therefore do not automatically share the original file-backed text pages as effectively as two processes mapping the same ELF.

Dynamic linking adds another dimension: startup work depends not only on bytes but also on the number of libraries, symbols, and relocations traversed. An index can improve lookup, but a general database engine and an extra interpreter remain in the critical path.

These are not implementation footnotes. They define where the design could make sense:

  • an experimental or research system where inspectability matters more than startup latency;
  • reproducible environments that value explicit dependency closure;
  • offline analysis and rewriting, followed by conversion back to ELF;
  • appliance-style bundles where one database replaces a forest of files;
  • development tools that benefit from SQL even if deployment retains ELF.

For latency-sensitive command-line tools or dense multi-process servers, the inability to directly share mapped code pages is a serious disadvantage until the storage and loader design changes.

The best near-term use may be as an intermediate format

A new executable format does not have to replace ELF everywhere to be valuable.

SELF can act as an editable intermediate representation. Convert ELF into a relational form, inspect and transform it with transactions, validate invariants with queries, then emit a conventional ELF for deployment. That workflow keeps compatibility with kernels, debuggers, signatures, containers, and distribution systems while giving transformation tools a higher-level model.

It can also serve as an analysis format. The earlier sqlelf project exposed ELF through SQLite virtual tables, allowing SQL queries without changing the underlying file. SELF takes the bolder step of making rows canonical, but both approaches demonstrate the same insight: binary tooling improves when format relationships become directly queryable.

The experiment is especially useful as a design probe. It separates properties that are essential to execution from properties inherited from one storage layout. Segments, permissions, relocations, symbol resolution, and entry points are essential. String-table offsets and bespoke hash sections are implementation choices. Replacing the representation makes that boundary visible.

A radical format succeeds by exposing trade-offs

SELF is a working prototype, not a claim that every Linux distribution should replace ELF. Its value comes from being complete enough to encounter the difficult parts: kernel dispatch, dynamic linking, dependency identity, size, startup time, memory sharing, mutation, and trust.

The database version is dramatically easier to interrogate. It can bundle resolved closures, deduplicate shared objects, and turn structural edits into transactions. The ELF version starts faster, maps code directly, works everywhere, and participates in a vast mature ecosystem.

That contrast is productive. It suggests concrete improvements for both worlds: richer query layers over existing binaries, relational intermediate forms for safe rewriting, explicit dependency graphs, and loader-aware application databases. It also reminds us that a file format is not just bytes on disk. It is the set of questions, mutations, guarantees, and execution paths those bytes make possible.

Further reading

Top comments (0)