Static Zig rootfs engine: verified ALT RPM bootstrap from caller-supplied metadata
  • Zig 99.2%
  • C 0.8%
Find a file
hermes 01696a6d45 fix: remove prebuilt rootfs importer (#1)
Reviewed independently for Kanban task t_58fbdd50 at exact head aa870ab96b.
2026-09-09 10:29:37 +00:00
docs fix: remove prebuilt rootfs importer (#1) 2026-09-09 10:29:37 +00:00
examples feat: add locked Crucible build profile API 2026-09-08 23:22:10 +00:00
include feat: add locked Crucible build profile API 2026-09-08 23:22:10 +00:00
src fix: remove prebuilt rootfs importer (#1) 2026-09-09 10:29:37 +00:00
tests fix: remove prebuilt rootfs importer (#1) 2026-09-09 10:29:37 +00:00
.editorconfig docs: prepare repository for public release 2026-09-02 10:08:56 +00:00
.gitignore feat: build ALT P11 LXC rootfs profile 2026-09-01 11:38:30 +00:00
build.zig fix: remove prebuilt rootfs importer (#1) 2026-09-09 10:29:37 +00:00
build.zig.zon fix: remove prebuilt rootfs importer (#1) 2026-09-09 10:29:37 +00:00
CHANGELOG.md fix: remove prebuilt rootfs importer (#1) 2026-09-09 10:29:37 +00:00
CONTRIBUTING.md docs: prepare repository for public release 2026-09-02 10:08:56 +00:00
LICENSE feat: initial Ember rootfs assembler 2026-09-01 11:07:14 +00:00
README.md fix: remove prebuilt rootfs importer (#1) 2026-09-09 10:29:37 +00:00
SECURITY.md feat: verify embedded RPM signatures 2026-09-08 19:41:54 +00:00

Ember

Ember builds Linux root filesystems from explicit inputs.

It is a dependency-light rootfs engine written in Zig 0.16.0 and shipped as a statically linked CLI, a static PIC library, and a stable C ABI. Its primary working pipeline starts with caller-supplied ALT APT-RPM metadata, raw-bootstraps only the target package tools, then installs the final closure through target rpmi with scriptlets and triggers enabled.

Project status: early development (0.2.0). The ALT bootstrap and normal target RPM transaction work end to end, but Ember is not yet a general-purpose distribution image builder. See Current limitations.

Why Ember?

Traditional image builders often rely on a host package manager, shell pipelines, container runtimes, or a prebuilt rootfs. Ember instead makes the artifact and trust boundaries explicit:

caller-supplied metadata and package requests
  -> appended OpenPGP Release signature + identity verification
  -> pkglist size and BLAKE2b verification
  -> dependency resolution
  -> selected individual RPM downloads
  -> size and BLAKE2b verification
  -> embedded RSA/OpenPGP header and payload signature verification
  -> native RPM + LZMA + CPIO extraction
  -> empty staging rootfs
  -> target RPM database bootstrap
  -> normal target rpmi transaction with scriptlets and triggers
  -> ownership/mode normalization and rpm -qa/-Va validation
  -> atomic publication

Ember does not download repository metadata, signing keys, profiles, scripts, OCI layers, or ready-made root filesystems. The only network objects fetched by the ALT bootstrap are individual RPMs already selected from metadata supplied by the caller.

Working features

  • ALT pkglist.classic.xz parsing, including concatenated XZ streams;
  • ALT appended detached Release signature verification against a caller-pinned fingerprint;
  • signed Release identity plus pkglist size/BLAKE2b verification before solving;
  • package, capability, absolute-path, and ordinary RPM EVR dependency resolution;
  • exact-size and BLAKE2b verification of downloaded RPMs;
  • embedded RPMSIGTAG_RSA/RPMSIGTAG_GPG verification with exact signer provenance before extraction;
  • native RPM header parsing and bounded LZMA payload decompression;
  • safe CPIO newc extraction with traversal and symlink-parent protection;
  • usr-merge aliases, hardlinks, modes, numeric ownership, and mtimes;
  • atomic ALT initial-root construction from an empty staging directory;
  • target rpm --initdb and dependency-ordered --justdb registration;
  • normal target rpmi installation of the non-bootstrap closure with scriptlets and post-transaction file triggers enabled;
  • ordered transaction diagnostics, target package-set lock comparison, mode/ownership normalization, and strict rpm -Va allowlisting;
  • canonical multi-alt.repository-lock v1 output with repository snapshot, policy, NEVRA, origin, size, SHA-256 and ALT BLAKE2b-512 for every selected RPM;
  • fail-closed build --locked verification with field-specific mutable-snapshot and package drift diagnostics;
  • normalized rootfs comparison over sorted paths, types, modes, hardlink counts, sizes, file bytes, and symlink targets, with an explicit derived-cache/RPM-index policy;
  • a shared verified-RPM provider API with direct network, network-backed CAS, and strictly offline CAS implementations;
  • a BLAKE2b-512 content-addressed RPM cache with atomic object publication, per-object advisory locking, verified reads, and explicit digest-scoped eviction;
  • low-level JSON tree assembly;
  • typed package-origin alt:p11/base, alt:p11/crucible-build, and alt:p11/live-system rootfs profiles; the live profile composes a TTY installer, network/storage tools, an installer target/unit tree, and ordered systemd configuration steps without an embedded target offline pool;
  • reusable native namespace runner with explicit mount graphs, bounded output, progress/cancellation callbacks, process-group cancellation, and namespace-scoped cleanup;
  • static CLI and PIC static library with C ABI version 3, including native and embedding-owned external runner boundaries plus a locked built-in profile request.

A real ALT P11 x86_64 run currently raw-bootstraps a 39-package closure for setup, filesystem, rpm, and fakeroot, then installs diffutils and tzdata normally for a 41-package verified result.

Requirements

  • Zig 0.16.0 to build Ember;
  • Linux x86_64 for the currently tested ALT bootstrap;
  • root privileges for ember bootstrap alt-rpm, because the bootstrap preserves numeric ownership and invokes target RPM through chroot;
  • caller-provided ALT Release/pkglist snapshot, pinned Release fingerprint, and separately identified trusted package-key keyring;
  • gpgv for detached OpenPGP verification (or an embedding-provided release.Verifier callback);
  • HTTPS access to the configured RPM repository for the direct/cached network providers; the offline provider performs no HTTP and requires every selected RPM in its caller-opened CAS pool.

No external Zig packages are used. Runtime extraction does not call host rpm, rpm2cpio, cpio, xz, tar, Docker, Podman, or a host package manager. The default trust adapter invokes gpgv with only the caller-supplied keyring; the typed Zig API permits a different verifier implementation.

Build

zig build test
zig build -Doptimize=ReleaseSmall

Artifacts:

zig-out/bin/ember          static CLI
zig-out/lib/libember.a     static PIC library
zig-out/include/ember.h    public C header

Verify the CLI:

./zig-out/bin/ember --version
./zig-out/bin/ember --help

Quick start: assemble a tiny rootfs tree

This unprivileged example demonstrates deterministic tree assembly and source paths relative to the manifest:

zig build -Doptimize=ReleaseSmall
rm -rf /tmp/ember-example
./zig-out/bin/ember assemble examples/assemble/rootfs.json /tmp/ember-example
find /tmp/ember-example -maxdepth 3 -printf '%y %p -> %l\n'
/tmp/ember-example/bin/hello

Expected final command output:

hello from an Ember-assembled rootfs

The result is a filesystem tree, not a bootable distribution. See examples/assemble/README.md.

Build an ALT P11 transaction rootfs

  1. Put caller-supplied x86_64/noarch release and pkglist.classic.xz files plus the P11 public-key keyring in examples/alt-p11/metadata/.
  2. Review the repository URLs and requested packages in examples/alt-p11/plan.json.
  3. Run:
sudo ./zig-out/bin/ember bootstrap alt-rpm \
  examples/alt-p11/plan.json \
  /var/tmp/ember-alt-p11-rootfs

sudo chroot /var/tmp/ember-alt-p11-rootfs /bin/rpm -qa | sort
sudo chroot /var/tmp/ember-alt-p11-rootfs /bin/bash -c \
  'printf "rpm=%s bash=%s\n" "$(rpm --version)" "$BASH_VERSION"'

The destination must not exist. Ember builds under <destination>.work, removes temporary RPMs, and publishes the rootfs by rename only after bootstrap succeeds. Failure removes staging and leaves an existing destination untouched.

The example builds a bootstrap plus a small normal target RPM transaction, not yet a complete bootable ALT container. Full instructions and expected checks are in examples/alt-p11/README.md.

CLI reference

ember verify alt-release <release> <index> <keyring> <fingerprint> <architecture> <component>
ember verify rpm <file.rpm> <keyring> <fingerprint> <trust-store-identity>
ember inspect apt-rpm-pkglist <pkglist.xz> [package]
ember inspect rpm <file.rpm>
ember compare rootfs <first> <second>
ember solve apt-rpm <pkglist.xz>... -- <package>...
ember fetch rpm <base-url> <pkglist.xz> <package> --output <file.rpm>
ember extract rpm <file.rpm> <destination> <keyring> <fingerprint> <trust-store-identity>
ember bootstrap alt-rpm <plan.json> <destination>
ember bootstrap alt-rpm <plan.json> <destination> --lock <repository-lock.json>
ember build --locked <plan.json> <repository-lock.json> <destination>
ember profile <alt:p11/base|alt:p11/crucible-build|alt:p11/live-system> <plan.json> <destination> --lock <repository-lock.json>
ember profile <alt:p11/base|alt:p11/crucible-build|alt:p11/live-system> --locked <plan.json> <repository-lock.json> <destination>
ember assemble <manifest.json> <destination>

Run ember --help for the authoritative command summary.

compare rootfs omits mtimes, derived APT/ldconfig cache files, and raw Berkeley DB RPM index files. The latter are transaction-layout state rather than a canonical package inventory; acceptance must additionally compare sorted rpm -qa, verify each RPM database, and compare the repository/profile locks. Numeric ownership is verified separately when accepting a system rootfs because Zig's portable file-stat surface intentionally does not expose uid/gid.

C API

ABI version 3 exposes the low-level manifest assembler, ember_run_step, and ember_build_locked_profile. The locked-profile request contains only profile, caller-supplied plan path, canonical repository-lock path, and destination; unknown profiles, null fields, drift, and existing destinations fail closed. ember_run_step accepts a command, working directory, network-isolation policy, declarative mounts, cancellation/progress callbacks, and either the native backend or an embedding-owned external runner callback.

#include <stdio.h>
#include <ember.h>

int main(int argc, char **argv) {
    if (argc != 3) {
        fprintf(stderr, "usage: %s <manifest.json> <destination>\n", argv[0]);
        return 2;
    }

    ember_status status = ember_build(argv[1], argv[2]);
    if (status != EMBER_OK) {
        fprintf(stderr, "ember_build: %s\n", ember_status_string(status));
        return 1;
    }
    return 0;
}

Compile with a normal C compiler:

cc -I zig-out/include examples/c-api/assemble.c \
  zig-out/lib/libember.a -pthread -o /tmp/ember-c-example
cc -I zig-out/include examples/c-api/run-step.c \
  zig-out/lib/libember.a -pthread -o /tmp/ember-c-run-step
rm -rf /tmp/ember-c-rootfs
/tmp/ember-c-example examples/assemble/rootfs.json /tmp/ember-c-rootfs
/tmp/ember-c-run-step
/tmp/ember-c-run-step --mount
/tmp/ember-c-run-step --cancel
/tmp/ember-c-run-step --external

The ALT build API will later expose opaque plans and package providers. CLI and C interfaces already use the same Zig runner core; the C external callback lets an embedding process own a cross-architecture or stronger sandbox.

Safety model

  • Existing destinations are never overwritten.
  • Every build uses a private staging directory and atomic publication.
  • Archive paths and manifest paths reject traversal and unsafe components.
  • CPIO extraction rejects writes through symlink parents created by an archive.
  • Downloads use identity encoding and are accepted only after metadata-derived size and BLAKE2b checks.
  • Cache objects use objects/blake2b-512/<first-two-hex>/<full-hex>; writes and materialization are atomic, reads are reverified, and per-object locks serialize insertion against eviction.
  • The offline provider rejects remote sources before creating a staging output and resolves cache sources only by their trusted size and BLAKE2b-512 identity.
  • ALT catalogs enter the solver only after their exact Release prefix is authenticated and their size/BLAKE2b record matches.
  • Locked builds compare authenticated Release/pkglist identities before RPM download and compare the complete canonical lock before publication.
  • Target package tools are executed from the constructed rootfs, not from the host.
  • Runtime mounts are modeled separately and must never leak into published artifacts.

Current limitations

  • ALT set: symbol-set requirements use decoded rpmsetcmp subset semantics; malformed encodings fail closed with deterministic diagnostics.
  • The bootstrap plan accepts signed ALT Release-chain repositories plus a caller-approved Forgejo manifest whose exact RPMs may be realized from local paths for unpublished builds. Local transport is digest-verified, explicitly records unsigned-own provenance, and never implies that the RPM was published; remote Forgejo acquisition is not wired yet.
  • Bootstrap packages are registered with --justdb; every non-bootstrap package is installed by a normal target rpmi transaction.
  • %ghost paths can make RPM normalization print missing-file warnings; unexplained nonzero ownership normalization is fatal, and publication still requires an empty or explicitly allowlisted rpm -Va result.
  • Symlink ownership remains root-owned due to a Zig 0.16 Dir.setFileOwner error-set defect.
  • Automatic cache-size policy and broader build provenance are not complete. The native runner currently targets privileged Linux/native-architecture execution; user namespaces, seccomp, cgroups, and cross-architecture emulation remain caller-owned external-runner concerns. Cache eviction is intentionally an explicit digest-scoped API boundary; no implicit LRU/TTL policy exists yet.
  • alt:p11/live-system requires the package-origin multi-alt-installer RPM. For local-only validation it can consume the digest-bound RPM through an approved Forgejo manifest; production builds still require a published, trusted Forgejo source. Ember does not substitute an unowned binary or prebuilt rootfs.
  • LXC, OCI, Crucible, and LiveCD emitters over the package-origin rootfs core are not implemented. Ember has no prebuilt-rootfs import path; those formats are output-only boundaries.

Do not use the current output as a production or security-hardened base image without independent validation.

Roadmap

The intended order is:

  1. Trust and solver correctness — RPM signatures, conflicts/obsoletes, deterministic providers, and lock/provenance output.
  2. Built-in ALT profiles — complete alt:p11/base, then Crucible build-root and systemd-networkd LXC profiles.
  3. Artifact emitters — deterministic LXC and OCI output from the same validated rootfs core.
  4. Higher-level systems — complete live-system roots and LiveCD stages.
  5. Public APIs — extend the Zig and C APIs with opaque build plans, input providers, runner callbacks, diagnostics, and progress/cancellation.

Near-term development is focused on stages 12. The project will not label package-origin LXC/OCI output complete until a disposable artifact has been built and boot-tested.

Documentation

License

MIT © 2026 WetGrape.