---
title: "This file is a database. It runs anyway."
locale: "en"
url: "https://irz.fr/en/articles/selfdb-executable-sqlite-en"
markdown_url: "https://irz.fr/en/articles/selfdb-executable-sqlite-en.md"
category: "tech"
tags: ["SQLite", "ELF", "Linux", "binfmt_misc", "binary formats"]
published_at: "2026-08-25T19:45:00.000Z"
author: "Arthur Lacoste"
translation: "https://irz.fr/fr/articles/selfdb-executable-sqlite-fr.md"
---

# This file is a database. It runs anyway.

SELF packs an ELF program into a valid SQLite database, registers four bytes at offset 68 with binfmt_misc, and runs it. strip becomes a transaction, ldd a JOIN.

`file hello` answers: SQLite 3.x database, application id 0x53454C46, user version 1. Then the shell runs `./hello` and the program answers too: Hello, world! [1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)

Both answers are correct. The file is a well-formed SQLite database that any `sqlite3` CLI can query, and it is also an executable that Linux runs like any other binary. Farid Zakaria built this format as a prototype called SELF, the Structured Executable & Linkable Format, and published the whole thing as `selfdb` on GitHub, where it gathered over 500 Hacker News comments within a day.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)[2](https://github.com/fzakaria/selfdb)[9](https://news.ycombinator.com/item?id=49415271)

His own framing is exact, and worth quoting:

> "Not 'a database that describes an executable', but the actual file you `chmod +x` and run."[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)

Will SELF replace ELF? No. Its author lists performance parity and kernel upstreaming among his explicit non-goals.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) The interesting question is what falls out when you push a 1989 binary format through a modern database. Quite a lot falls out.

## Four bytes at offset 68

The field has existed since SQLite 3, reserved for exactly this kind of reuse. The file format documentation defines a 4-byte "Application ID" at byte offset 68 of the header, set with `PRAGMA application_id` and "intended for database files used as an application file-format".[5](https://www.sqlite.org/fileformat2.html) SELF stamps those four bytes with the ASCII codes of the letters S, E, L, F: 0x53454C46.[4](https://github.com/fzakaria/selfdb/blob/main/schema/self.sql)

Then the kernel has to care. Linux ships a little-known subsystem called `binfmt_misc` that lets an administrator register an interpreter for any byte pattern found in a file.[6](https://docs.kernel.org/admin-guide/binfmt-misc.html) Its registration format carries one hard constraint: the magic must sit within the first 128 bytes of the file. Offset 68 plus four bytes lands at 72, comfortably inside. That is the entire mechanism: match "SQLite format 3\0" at offset 0 and "SELF" at offset 68, then hand the file to `self-exec`, a small C program linked against libsqlite3 that reads program headers from the database, maps the segments, relocates them and jumps to the entry point.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)

One detail from the project says a lot about how thin the ice is under every executable format: `self-exec` itself must remain an ELF file. An interpreter that also matches its own binfmt registration recurses into itself until the kernel gives up with `-ELOOP`.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)

`self-exec` grew three execution modes along the way. `memfd` rebuilds a minimal ELF from the rows, writes it into an anonymous in-memory file and calls `fexecve`. `native` maps the segments itself, synthesizes the stack and auxiliary vector, then hands control to the real `ld.so`. `selfld` is the SQL linker described further down.[2](https://github.com/fzakaria/selfdb) One deviation between plan and shipped code says a lot: the design proposed three binfmt flags, the shipped version uses none, because the `P` flag makes the kernel inject an extra `argv[0]` operand that strict multi-call programs like GNU hello reject outright.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) For robustness, the registration can also match one 72-byte magic starting at offset 0, covering the SQLite header and the SELF stamp in a single pattern with a mask zeroing the middle bytes.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md)

On NixOS the registration is a few lines of module configuration, and `nix run .#self-vm` boots a virtual machine where `hello` is, factually, a database.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md)

## A database in denial

The argument underneath the stunt is that ELF never had a format problem. It had an honesty problem.

Zakaria, who explored the idea during his PhD, describes ELF as "a hand-rolled, offset-addressed database from 1989".[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) The mapping he lays out is the best part of the project because every line of it can be checked against any systems textbook:

- `.strtab` and `.dynstr`: string interning, reimplemented by hand. In SQLite: a `TEXT` column.
- `.hash` and `.gnu.hash`: a symbol lookup index, hand-rolled as a bloom filter plus bucket chains so `ld.so` can reject a miss without touching the chain. In SQLite: `CREATE INDEX`, a real b-tree.
- the section header table: a table of tables. In SQLite: `sqlite_schema`.
- `st_name` pointing into `.strtab`: a foreign key, done with array offsets.
- "we can't extend ELF, every consumer hardcodes offsets": no schema evolution. In SQLite: `ALTER TABLE ADD COLUMN`.

> **ELF, database in denial**
> - hand-rolled bloom filter → CREATE INDEX: .gnu.hash
> - string interning → TEXT columns: .dynstr
> - a table of tables → sqlite_schema: Section table
> - offset surgery → DELETE + VACUUM: strip(1)
> Mappings as laid out by the selfdb author; IRZ reading.

Anyone who has parsed binaries knows the consequence: every consumer of ELF, from the kernel loader to `readelf` to LIEF, reimplements the same parser, and every producer reimplements the same serializer.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database) The format is terse because it was designed when disk and bandwidth were expensive, and it has no self-describing schema: sections mean something by convention, not by contract.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)

Before SELF there was sqlelf, published as an arXiv paper in May 2024. It attacked the problem from the observation side, exposing ELF as SQL virtual tables, so `SELECT name FROM elf_symbols` replaced `readelf` plus `grep`.[7](https://arxiv.org/abs/2405.03883) The paper, by his account, failed to find a venue.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database) SELF is the inversion: the rows are no longer a view over the file. The rows are the file.

## What falls away

The whole format fits in one readable DDL file, and most of it is optional.[4](https://github.com/fzakaria/selfdb/blob/main/schema/self.sql) Two tables are load-bearing: `self_meta`, the old ELF header as key/value rows, and `segments`, one row per program header with the actual bytes in a BLOB.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database) Add `symbols`, `relocations`, `needed` and `dynamic_entries` and the program links dynamically. Everything else, `sections`, `notes`, even a `docs` table, exists for tooling and can be deleted without the program noticing.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)

Read that last sentence again as a statement about ELF. In SELF, stripping a binary is:

```sql
DELETE FROM sections;
DELETE FROM notes;
VACUUM;
```

The blog demo takes a `hello` binary from 57,344 to 49,152 bytes this way, then runs it, because the deleted tables were never needed at execution time.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database) `patchelf` becomes an `UPDATE`. Symbol versioning, the `.gnu.version_r` contraption, becomes a `version` column. The symbol index is an ordinary b-tree; the schema comments it plainly: "this index IS .gnu.hash".[4](https://github.com/fzakaria/selfdb/blob/main/schema/self.sql)

And the format documents itself. Every SELF file ships `exports`, `imports` and `ldd` as SQL views, so the daily tools collapse into one-liners: `ldd` is `SELECT soname FROM ldd`, `nm -D` is a query on `imports`, `readelf -l` is a query on `segments`.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database) The design doc states it without decoration: `.schema` is the spec.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) There is even a `docs` table in the schema, a place for the format's own manual inside every binary, with the design doc imagining man pages and SBOM tables living there eventually.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md)

The converter, `elf2self`, works post-link in Python with LIEF, so no toolchain changes. The round-trip guarantee is functional rather than byte-exact: `self2elf(elf2self(x))` must produce an ELF with the same segments, symbols and dynamic information.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md)

## Linking, but as a JOIN

Static binaries were the easy part. The database pays off when shared libraries enter, and the project runs two tracks of very different ambition.

The working track keeps glibc's `ld.so` in charge and intercepts only the lookup. glibc's `rtld-audit` interface lets a library intercept every shared-object search, `dlopen` included, before the filesystem is consulted.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database) SELF ships `libself-audit.so`, which answers "which file satisfies this soname?" with a SQL query against the system database. The stock loader then maps and relocates the object as usual, and everything glibc normally does, lazy PLT, IFUNCs, TLS, symbol versioning, keeps working.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database) The repo's test deletes the ELF `libgreet.so.1` from disk entirely, and the application still runs, its library loaded out of SQLite rows.[2](https://github.com/fzakaria/selfdb)

The ambitious track, `self-ld`, replaces `ld.so` outright and resolves each relocation with a query:[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)

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

The honest footnote sits in the design doc rather than in a README boast: `self-ld` handles a freestanding closure with no libc. TLS, IFUNC relocations and glibc's loader handshake are out of scope, so real glibc programs go through the audit route, not the SQL linker.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) Prototype and product stay separated, which is exactly the discipline this kind of project needs.

The design doc also sketches the full system database. A resolver database, `/var/lib/self/system.db`, replaces the `ldconfig` cache with an indexed table of objects, sonames and build IDs. And because every `.self` file shares the same schema, cross-system questions need no new format, only `ATTACH`: attach `hello` and `libc`, then ask which of hello's imports libc does not satisfy. That is `ldd -r`, the mode that reports unresolved symbols, as a single JOIN.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md)

## The honest bill

A format the kernel maps directly does not get replaced for free. The project measures the costs instead of hiding them. "Performance parity" is listed as an explicit non-goal for version zero.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md)

Size first. A single small binary roughly doubles, because SQLite's b-tree pages carry overhead.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database) Most of that overhead lives in the optional tooling tables, though. A stripped coreutils SELF weighs 1,794,048 bytes against the ELF's 1,768,632. That is 1.44% more; the author rounds it to "within 1%", and the raw numbers sit slightly above his rounding. The direction of the result is what matters: once stripped, the database is essentially the same size as the binary it replaced.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md)

Latency is where the bill gets real. The repository benchmark measures a bare exec at 0.42 ms and the SELF loader at about 2.1 ms, roughly five times slower, the price of opening the database and reconstructing the image. The blog post separately quotes a fixed ~5 ms to open SQLite and start the interpreter on its own, larger benchmarks, from a 15 KiB `hello` to a 42 MiB `gdb` linking 47 libraries. Different measurements, same direction.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) The evaluation plan behind those numbers reads like a small methods paper: hyperfine for exec latency, cold and warm; process-shared memory measured with `pss` across concurrent instances; round-trip differential testing; and a `sqldiff` required to come back empty on converted binaries.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md)

The expensive part hides deeper. A normal ELF gets `mmap`-ed, so ten processes running the same binary share the same text pages through the page cache. SELF copies bytes out of the b-tree rather than mapping them, so every process gets its own copy. The author flags this himself as the real performance gap.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) On Hacker News a commenter put the objection sharply: "Isn't this the whole point of executable file formats? To avoid loading everything all at once, share immutable segments with other processes..."[9](https://news.ycombinator.com/item?id=49415271) The criticism is correct, and the project never claims otherwise. In my reading, that kind of objection makes the work more credible, not less. There is also a smaller curiosity in the benchmarks: `curl`, 274 KiB across 27 libraries, starts slower than `git`, 4.6 MiB across 5, because `ld.so` pays per object rather than per byte.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)

> Measured costs
> **The bill, in four numbers**
> - size of a small raw binary vs ELF: ×2
> - stripped coreutils: SELF 1,794,048 B vs ELF 1,768,632 B: +1.44%
> - exec cost of the memfd loader, about 5×: 0.42 → 2.1 ms
> - shared text pages between processes, the real gap: 0
> Author measurements (blog, DESIGN.md bench); IRZ calculation for +1.44%.

## The ambiguity killer

This is where the project stops being a curiosity. A SQLite file does not have to contain one program. `self closure` packs an executable and its transitive dependencies into a single database, and the schema kills a genuinely old ambiguity.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)

`ldd` lists sonames, not files. Which actual `libc.so.6` satisfies a dependency is decided at runtime by search paths, exactly the nondeterminism Nix spends its existence eliminating with `RUNPATH`.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database) SELF does the same with a foreign key: the `needs` table stores, for every dependency edge, the `resolved_path` of the object that satisfies it.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database) Library resolution stops being a search and becomes referential integrity. In the demo, `ls` and its five libraries, six objects, live in one 4.8 MiB file.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)

There is a quiet irony worth naming here. Nix already resolves every edge to a store path; that is what `RUNPATH` is for. SELF adds less determinism than it moves location: the closure file carries its own dependency graph as data, so the answer to "which libc will this program actually use?" travels inside the artifact, together with the artifact.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md)

## 723 programs, one file

Point the same tool at a whole system and the numbers stop being cute. Zakaria ran `self closure` against every ELF binary on his PATH: 723 executables pulling in 400 distinct shared libraries, 1,123 objects, 346,386 symbols and 3,808 dependency edges, all in one SQLite file.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)

The surprise is the size: 611.9 MiB of database against 644.4 MiB of original ELF files. The b-tree overhead that doubled a single `hello` amortises to about 6% across a thousand objects, because libraries and symbols are deduplicated by the schema itself. Under the AppImage model, where every program ships a private closure, the same 723 programs would weigh 5.53 GiB.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database) The database also answers questions the original files cannot. One query shows 345 distinct sonames carried by 399 library objects, a handful of libraries present in several builds at once, the duplication a store like Nix tolerates on purpose. Excluding the dynamic linker itself, four of the 3,808 edges still have no resolved path, and the query that finds them is one `WHERE resolved_path IS NULL` away.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)

> One userland
> **723 programs, one file**
> - executables converted: 723
> - distinct shared libraries: 400
> - symbols indexed: 346,386
> - the database, vs 644.4 MiB of ELF: 611.9 MiB
> Figures from the author's userland experiment, August 2026.

## LD_PRELOAD, transactional

Once the system is rows, intervening on it becomes a matter of transactions. The demo runs in four lines. A program exits with code 13. One row is inserted into a `preload` table, naming a library that interposes a function. The same binary runs again and exits 42. The row is deleted, back to 13. No environment variable, no relink, and the operation is atomic and reversible, `ROLLBACK` included.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)

The author's example is the right one: "interpose a tracing malloc everywhere, then ROLLBACK" becomes a single transaction across an entire userland.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database) Anyone who has ever tried to instrument a fleet of binaries temporarily will recognize how much machinery that sentence deletes.

> System intervention
> **LD_PRELOAD becomes a row**
> - the program exits with 13.: Baseline
> - one preload row names the interposing library.: INSERT
> - the same binary now exits with 42, no env, no relink.: Run
> - the row is gone, back to 13. ROLLBACK works everywhere.: DELETE
> Demo from the selfdb blog post.

## A server inside its own binary

The repository's example project makes the point better than any argument. `self-httpd` is a web server that opens `argv[0]`, its own executable, as a SQLite database, serves pages out of its own `routes` table, and writes visitor counts back into itself.[2](https://github.com/fzakaria/selfdb) The program, its content and its logs are one file. Editing the live site is an `UPDATE` statement. It is deployed and answering at selfdb.exe.xyz.[2](https://github.com/fzakaria/selfdb)[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md)

From there, the format is no longer really about ELF. An artifact that is both code and queryable state invites artifacts that would otherwise be three directories and a deployment script. Building the demo takes two commands from the repo: one script turns the program into a SELF file, and `self-exec` serves it on a port.[2](https://github.com/fzakaria/selfdb)

## What carries over

Take away the ELF replacement and ideas remain that travel.

First, representation. SQLite's own whitepaper on application file formats argues that a documented schema beats a bespoke binary layout, notes that the US Library of Congress recommends SQLite for long-term preservation, and quotes Fred Brooks: "Show me your tables, and I won't usually need your flowcharts."[8](https://sqlite.org/appfileformat.html) SELF is that argument aimed at the most entrenched binary format in computing, and the ELF-to-SQL mapping table is the argument made concrete. SQLite's file format has been backward compatible since version 3.0.0 in June 2004, a longevity claim few executable formats can make.[5](https://www.sqlite.org/fileformat2.html)

Then, tooling economics. Every parser deleted is a class of parser bugs never written. `sqldiff` over two SELF binaries produces a semantic diff of programs; provenance is a row in `self_meta` recording the store path the binary came from; the design doc sketches signing rows instead of signing bytes.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) It also names the general principle, crediting nushell: structured data at the boundary beats bytes plus a bespoke parser at every consumer.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) Small, real wins, available to anyone designing an artifact format today, whatever the kernel ends up executing.

The last one is about method, and it matters most here. Nix let one person rebuild enough of a system to test a heretical format against real glibc programs, in a bootable VM, without asking anyone's permission and without touching the toolchain.[1](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database) The barrier was never kernel expertise. It was having a rebuildable world.

## Where it stops

The honest list, mostly the project's own. No shared text pages, so the memory story stays worse than ELF until someone solves page-aligned blob mapping, which is explicitly future work.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) `self-ld` binds freestanding closures only.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) setuid is declared out of scope.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) The name SELF collides with Sony's Signed ELF format for PlayStation executables, a collision the author decided to accept.[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) The kernel angle gets its own section of the design doc, ranked by effort: do nothing and binfmt_misc already works; a BPF-based matcher could dispatch on `user_version` and route to versioned interpreters; a genuine in-kernel reader, one to two thousand lines of C walking the b-tree without any SQL, would recover demand paging through the page cache. The author's verdict on that last option: "a fantastic talk slide and a terrible patch series".[3](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md) And the repository, at thirteen commits and 307 stars at the time of writing, is a working prototype, not a distribution.[2](https://github.com/fzakaria/selfdb)

None of that weakens the actual result. A program can be a valid database, run on an unmodified Linux kernel through a documented userspace mechanism, and leave behind a system you can query, strip, preload and diff with SQL. ELF is 37 years old and adequate at everything. Ask what it is actually for, and SELF answers in a language databases have spoken since 2004.[5](https://www.sqlite.org/fileformat2.html)

## References

1. [Farid Zakaria, “Your executable is a SQLite database”, August 23, 2026](https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database)
2. [fzakaria/selfdb, README](https://github.com/fzakaria/selfdb)
3. [fzakaria/selfdb, DESIGN.md](https://github.com/fzakaria/selfdb/blob/main/DESIGN.md)
4. [fzakaria/selfdb, schema/self.sql (format v1 DDL)](https://github.com/fzakaria/selfdb/blob/main/schema/self.sql)
5. [SQLite, Database File Format — the database header](https://www.sqlite.org/fileformat2.html)
6. [Linux Kernel Documentation, binfmt_misc](https://docs.kernel.org/admin-guide/binfmt-misc.html)
7. [Zakaria, Chen, Quinn, Scogland, “sqlelf: a SQL-centric Approach to ELF Analysis”, arXiv:2405.03883, May 2024](https://arxiv.org/abs/2405.03883)
8. [SQLite, “SQLite As An Application File Format”](https://sqlite.org/appfileformat.html)
9. [Hacker News discussion, “Executable Is a SQLite Database”, August 24, 2026](https://news.ycombinator.com/item?id=49415271)
