# ELF packaging without an external linker ## Recommendation Use a prelinked x86-64 PIE VM template with a dedicated final, read-only `PT_LOAD` segment for the ASPL payload. Build and link that template once when producing an ASPL release. Embed the normalized template and a small manifest inside the `aspl` executable. An end-user build then does four things in-process: 1. Copy the template to a temporary output file. 2. Write a versioned payload envelope and the program bytecode into the final segment. 3. Patch that segment's `p_filesz` and `p_memsz` to the exact payload size. 4. Make the completed file executable and publish it under the requested name. This meets the narrow promise that `aspl build` does not invoke a compiler, assembler, or static linker. The result contains the native VM and the program bytecode, so it does not need ASPL installed to run. It still depends on the Linux ELF loader, the glibc dynamic loader named by the template, glibc itself, and any shared libraries the program requests. Calling the file "standalone" must not imply static linkage. The arbitrary C-library feature should use `dlopen` and `dlsym` from the prelinked VM. It should not edit `DT_NEEDED`, relocation tables, symbol tables, or PLT entries for every program. Editing those structures would turn the MVP emitter into a linker. ## Why the mapped payload works ELF execution follows the program header table. The generic ABI says each `PT_LOAD` entry maps `p_filesz` bytes from the file, requires `p_filesz <= p_memsz`, and zero-fills any remaining memory up to `p_memsz`. It also requires load segments to appear in ascending virtual-address order. File offsets and virtual addresses must be congruent modulo the segment alignment. These are the rules the template and emitter must preserve. [ELF gABI, program loading](https://gabi.xinuos.com/elf/07-pheader.html) The release build should place a small payload-envelope placeholder in its own final `PT_LOAD` segment. A linker-defined symbol such as `__aspl_payload_start` lets the C VM refer to the start directly. The segment must have the largest virtual address, have read permission only, and end the executable's meaningful file image. Since the segment already exists, the emitter only extends it. It does not add a program header or relocate native code. The final segment should contain: ```text ASPL payload envelope magic envelope version bytecode format version bytecode length VM compatibility identifier integrity value bytecode bytes ``` Set `p_filesz` and `p_memsz` to the envelope size plus bytecode size. The VM needs no writable bytes in this segment, so `PF_R` is enough. A writable and executable payload segment would be needless and conflicts with glibc's hardening advice to avoid `LOAD` segments that are writable and executable at the same time. The template should also retain a non-executable `GNU_STACK` header. [glibc dynamic-linker hardening](https://sourceware.org/glibc/manual/latest/html_node/Dynamic-Linker-Hardening.html) Section headers are not needed for execution. Linux documents that an ELF executable may have a program header table, a section header table, or both. It also defines zero `e_shoff` and `e_shnum` values for a file without a section table. The release process can therefore normalize the runtime template so its file ends after the payload-envelope placeholder and it has no trailing section table. [Linux `elf(5)`](https://man7.org/linux/man-pages/man5/elf.5.html) The emitter must use the template's recorded `p_align`, then pad the payload file offset so it remains congruent with `p_vaddr`. It should not assume every linker uses the same page alignment. The ASPL release owns this template, so the compiler can store the exact program-header index, payload offset, virtual address, original size, and a hash of the immutable template bytes in an internal manifest. ## Process startup Keep the template's startup path unchanged. A dynamically linked ELF file has one `PT_INTERP` entry. During `execve`, Linux runs the interpreter named there, which loads the required shared objects before control reaches the program. [Linux `execve(2)`](https://man7.org/linux/man-pages/man2/execve.2.html) The ELF ABI describes the same handoff and says the dynamic linker maps dependencies, performs relocations, and transfers control to the program. [ELF gABI, dynamic linking](https://gabi.xinuos.com/elf/08-dynamic.html) The template should be an `ET_DYN` position-independent executable so Linux can apply address-space randomization. Native startup reaches the template's usual C entry path and then `main`. The VM validates the ASPL envelope at `__aspl_payload_start`, checks the bytecode version and length, and starts the bytecode entry function. No child process, temporary bytecode file, or self-reopen is required. The fixed `PT_INTERP` path is part of the platform contract. So are the glibc symbol versions and `DT_NEEDED` entries recorded when the VM template is linked. The release specification must name a minimum glibc baseline, build the template against that baseline, and test it on a clean system at that version. Copying the template does not make a binary built against a newer glibc run on an older one. ## Dynamic C dependencies The template's link-time dependencies stay fixed. They should contain only what the VM itself needs. Library declarations stored in ASPL bytecode can name an ABI-compatible shared object and symbols. At VM startup or first use, the runtime calls `dlopen`, obtains symbols with `dlsym`, and invokes them through the separately specified x86-64 C ABI bridge. `dlopen` accepts an absolute or relative path when the name contains a slash. Otherwise, the glibc loader searches its configured paths, cache, and default library directories. It also loads the selected object's own dependencies recursively. [Linux `dlopen(3)`](https://man7.org/linux/man-pages/man3/dlopen.3.html) The MVP should use `RTLD_NOW | RTLD_LOCAL`: `RTLD_NOW` reports unresolved dependencies before execution continues, while `RTLD_LOCAL` avoids exporting a library's symbols into later loads unless ASPL gains an explicit reason to do that. This gives "link arbitrary C libraries" a precise boundary. It means load compatible Linux shared objects present on the target system and call symbols covered by ASPL's supported ABI types. It does not mean static archives, C header parsing, link-time symbol resolution, or embedding each library into the output. The system dynamic loader remains a runtime dependency. Its search rules for `DT_NEEDED`, `DT_RUNPATH`, the loader cache, and default paths still apply. [Linux `ld.so(8)`](https://man7.org/linux/man-pages/man8/ld.so.8.html) ## Feasible alternatives | Mechanism | Bytecode placement | Runtime constraint | Dynamic dependencies | Integrity cost | Verdict | | --- | --- | --- | --- | --- | --- | | Dedicated final `PT_LOAD` segment | Mapped read-only beside the VM | Ordinary ELF and glibc startup | Fixed VM dependencies plus runtime `dlopen` | Bounded header patching and payload validation | Use for the MVP | | Fixed-size array in the template | Preallocated in `.rodata` | Ordinary startup and direct symbol access | Same as the recommended design | No ELF patch, but unused capacity ships in every file | Useful only for an early spike | | Unmapped file overlay | Bytes appended after all ELF segments | VM must reopen its own executable and read the trailer | Same as the recommended design | Simple ELF output, but a second file parser and I/O path | Keep as a fallback, not the release format | | Rebuild dynamic ELF metadata | New sections, segments, symbols, relocations, and dependencies | Ordinary startup if every link rule is correct | Can encode per-program `DT_NEEDED` entries | Very large validation burden | Reject for the MVP | The overlay is technically workable on Linux. The kernel's ELF loader maps the ranges described by `PT_LOAD`; its upstream implementation iterates those headers and maps `p_filesz` bytes for each one. [Linux `fs/binfmt_elf.c`](https://github.com/torvalds/linux/blob/master/fs/binfmt_elf.c) A VM can open `/proc/self/exe`, which Linux exposes as a link to the executed file, then locate a trailer from the end of the file. [Linux `proc_pid_exe(5)`](https://man7.org/linux/man-pages/man5/proc_pid_exe.5.html) That simplicity is deceptive. The overlay depends on procfs being mounted and accessible, performs file I/O during startup, and has special behavior after unlinking or when access checks reject `/proc/self/exe`. `argv[0]` is not a sound substitute because the caller controls it. `AT_EXECFN` only records the pathname passed to execution, so it does not remove pathname and replacement races. [Linux `getauxval(3)`](https://man7.org/linux/man-pages/man3/getauxval.3.html) A mapped segment avoids all of those conditions. The fixed-size-array option is even simpler to implement, but it forces an arbitrary maximum bytecode size and pads every executable to that maximum. It is acceptable for a prototype that proves the template-copy idea. It is a poor public format. ## Emitter and integrity contract The emitter should parse and write ELF fields as explicit little-endian integers. It should validate the template before writing anything: - ELF magic, `ELFCLASS64`, little-endian encoding, `EM_X86_64`, and expected `ET_DYN` type. - Exact program-header entry size and count. - The manifest's payload header index, offset, virtual address, flags, and alignment. - `p_filesz <= p_memsz` for every load segment, ascending load addresses, non-overlap after page rounding, and overflow-free offset and size arithmetic. - A hash of every immutable template byte. The compiler should reject bytecode lengths that overflow ELF fields, the process address space, or an implementation limit. It should write the output with no execute bits set, validate the finished file, then add the requested execute permissions. Publishing through a same-directory temporary file and `rename` prevents another process from observing a half-written replacement because Linux replaces an existing destination atomically. [Linux `rename(2)`](https://man7.org/linux/man-pages/man2/rename.2.html) The payload envelope needs a corruption check over the bytecode and all fields that control parsing. A cryptographic digest is preferable if ASPL already contains a suitable implementation. A simpler checksum detects accidental truncation but does not establish authenticity. Neither choice protects against an attacker who can rewrite both payload and checksum. Release signatures and download checksums are a separate distribution concern. Do not treat `.note.gnu.build-id` as an output checksum. GNU `ld` says a build ID identifies the originally linked file, is not intended as a content checksum, and remains unchanged if another tool later modifies the file. [GNU `ld` build-ID documentation](https://sourceware.org/binutils/docs/ld/Options.html) Retaining the template build ID is useful for identifying native VM code in crash reports. Add a separate ASPL content identifier for the payload, or omit the GNU build ID if downstream tooling would mistake it for the identity of the complete generated program. Reproducible output is straightforward. For a fixed ASPL release, identical bytecode and build options should produce identical bytes. The envelope must not contain the current time, random padding, absolute source paths, or host-specific data unless a user explicitly requests them. ## Consequences for the MVP specification The native packaging decision can now state the following constraints: - `aspl build` copies and patches a release-owned VM template in-process. It never invokes a compiler, assembler, or static linker. - The generated ELF is x86-64, little-endian, PIE, glibc-linked, and contains a final read-only load segment with a versioned bytecode envelope. - The template fixes the ELF interpreter, minimum glibc baseline, and native VM dependencies for that ASPL release. - Program-specific C libraries are loaded by name at runtime through `dlopen` and resolved through `dlsym`; they are not added as ELF link-time dependencies. - The VM validates payload bounds, version, compatibility, and corruption metadata before interpreting any instruction. - The release build may use a C compiler and linker to create the template. The end-user build path may not. This design leaves one deliberate maintenance cost. Each supported native platform needs its own tested runtime template and manifest. That is acceptable for the Linux x86-64 glibc MVP and scales more honestly than hiding a partial linker inside `aspl build`.