ELF packaging without an external linker

Archived MVP specification. For ASPL 0.5.0, see the current reference and native HTTP guide.

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

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:

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

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)

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) 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

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) 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)

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 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)

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) 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:

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)

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 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:

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.