oci-builder 0.1.1

CLI for building and pushing OCI images with embedded Buildah
oci-builder-0.1.1 is not a library.

buildah-ffi

Rust library and oci-builder CLI that embed Buildah in-process. There is no Buildah daemon and no buildah binary on PATH.

Linux runs the engine. Other targets, including macOS, link a stub with the same ABI so the crate still compiles. The stub returns unsupported.

Install from crates.io

Library:

cargo add buildah-ffi
buildah-ffi = "0.1"

CLI:

cargo install oci-builder

Both commands compile the crate on your machine. On Linux, build.rs builds the Buildah Go shim and links it into the binary, so the build needs Go 1.26 or newer, a C compiler, and pkg-config. libseccomp is optional: when pkg-config finds it, the seccomp build tag is turned on. On macOS and other non-Linux hosts the stub is linked instead. startup() and oci-builder then exit with code 8 and a message that Buildah is available on Linux only.

An image build also needs the host prerequisites under Build requirements. Rootless use needs user namespaces. A pull needs a signature policy. A RUN instruction with oci or rootless isolation needs runc or crun on PATH. chroot isolation and a scratch image that only uses COPY do not.

Library

Call startup() before spawning threads or parsing arguments. Buildah may re-exec this process for a rootless user namespace, and that child has to reach startup before anything else reads argv.

use buildah_ffi::{startup, BuildRequest, Builder, Config, StorageDriver};

fn main() -> Result<(), buildah_ffi::Error> {
    startup()?;
    let builder = Builder::open(Config {
        storage_driver: Some(StorageDriver::Vfs),
        ..Config::default()
    })?;
    let info = builder.build(BuildRequest::new("Dockerfile", "."))?;
    println!("image_id={}", info.image_id);
    builder.shutdown()?;
    Ok(())
}

shutdown() releases the storage mounts. Dropping the Builder does not.

CLI

oci-builder diagnose

oci-builder --root /tmp/graph --runroot /tmp/run --storage-driver vfs \
  --signature-policy policy.json \
  build -f Dockerfile -t localhost/app:latest --pull never --isolation chroot

oci-builder --root /tmp/graph --runroot /tmp/run --storage-driver vfs \
  push localhost/app:latest localhost:5000/app:latest --insecure

policy.json for a local store that does not verify signatures:

{"default":[{"type":"insecureAcceptAnything"}]}

Stdout of build and push is image_id=, digest=, and reference=. Logs go to stderr. The exit code is the ErrorCode value. Clap's own usage errors stay exit code 2. diagnose prints the prerequisite report and exits 0 when the host is ready.

The sample Dockerfiles in examples/ live in the source repository. They are not part of the published crates.

Architecture

oci-builder  -->  buildah-ffi (safe Rust)
                      |
                      | C ABI (rob_*.h): borrowed C strings in, malloc'd buffers out
                      v
                 Go c-archive
                      |
                      +-- buildah.InitReexec / rootless re-exec
                      +-- containers/storage (one store per process)
                      +-- imagebuildah.BuildDockerfiles
                      +-- libimage tag
                      +-- buildah.Push

Buildah-specific types stay behind the C ABI. The Rust API (Builder::build, Builder::push, Builder::tag) does not expose Go structs, so Buildah's internals can change without an ABI break.

The archive is linked with static:+whole-archive so the Go runtime's .init_array constructor is not dropped. startup is a normal call from main. A Rust constructor would race that Go constructor and can deadlock inside cgo.

Safety

  • Call startup() as the first thing in main, before spawning threads and before parsing arguments. Storage helpers re-exec this binary and dispatch on argv[0]. If startup has not run, those children do not reach their handlers.
  • Rootless startup may re-exec. The parent waits and exits with the child's status. The child's argv[0] is the original name plus -in-a-user-namespace, and the rest of the arguments are unchanged. startup returns only in the child (or when no re-exec was required).
  • Do not call unshare or startup while holding a lock you expect to survive a fork. The re-exec path runs /proc/self/exe, then the parent exits.
  • Input strings are borrowed for the call. The shim copies them with C.GoString.
  • Output buffers are malloc'd, NUL-terminated, and owned by the caller. len does not include the NUL. A NULL pointer with len == 0 is empty. The safe API frees them before returning.
  • rob_buildah_version is process-lifetime storage. Do not free it.
  • Log callbacks run on a Go thread. The bytes are valid only for that call; the safe API copies them first. The callback must not call back into Builder (the engine lock is held) and must not unwind. Panics in the callback are caught and discarded.
  • Cancel tokens are integers, not Go pointers. cancel does not take the engine lock. Drop frees the token and does not cancel. A token that is already freed when an operation starts is reported as cancelled, so the token has to outlive the call. Clone shares one token.
  • One store is open per process. Builder is not cloned. Drop does not shut the store down; call shutdown so the graph driver can release mounts. A second open fails until shutdown returns.
  • Passwords are copied into the Go heap for the push and are not written to logs. Debug for PushRequest redacts the password.
  • The Go runtime installs its own signal handlers. Cancellation is the supported way to interrupt a build or push.

MaybeReexecUsingUserNamespace can still os.Exit if user-namespace setup fails after the pre-check. startup checks newuidmap, newgidmap, subordinate IDs, and the user-namespace sysctls first so the common failures return ErrorCode::Prerequisite instead of exiting.

Build requirements

Requirement Why
Linux, 64-bit The engine uses cgo, namespaces, and /proc/self/exe.
Go >= 1.26 Buildah v1.45.1 sets go 1.26.0.
gcc, libc headers, pkg-config cgo.
libseccomp (optional) Enables the seccomp tag. Without it, RUN that installs a seccomp profile fails. The library is linked dynamically when the tag is on.
libapparmor (optional) Enables the apparmor tag when pkg-config finds it.
uidmap (newuidmap, newgidmap) Rootless user namespaces. The binaries must be setuid or carry the matching file capability.
/etc/subuid and /etc/subgid Subordinate ranges for the calling user.
fuse-overlayfs Rootless overlay. vfs needs neither overlay nor a mount helper.
runc or crun RUN with oci or rootless isolation. chroot, and a scratch image that only uses COPY, do not need them.
policy.json Pulls fail closed when neither /etc/containers/policy.json nor ~/.config/containers/policy.json exists.

startup / oci-builder diagnose prints status: ready or status: blocked with [ok], [warn], and [fail] lines. Blocked startup returns exit code 4 and includes that report. Missing overlay, a missing policy, and a missing runc are warnings. A missing newuidmap, a zero user.max_user_namespaces, kernel.unprivileged_userns_clone=0, or kernel.apparmor_restrict_unprivileged_userns=1 is a failure for non-root.

Build tags always include exclude_graphdriver_btrfs, exclude_graphdriver_devicemapper, and containers_image_openpgp (pure-Go signature verification). Image signing is not supported. Extra tags can be added with ROB_GO_TAGS.

Cross-compiling the archive is refused unless CC and ROB_ALLOW_CROSS=1 are set. build.rs runs go build -mod=readonly, so crates/buildah-ffi/shim/go.sum has to be present. Refresh it on Linux (or in a Linux container) with GOOS=linux:

cd crates/buildah-ffi/shim
go mod tidy

Versions

Direct module requirement:

  • go.podman.io/buildah v1.45.1 (module go.podman.io/buildah, repository podman-container-tools/buildah)

Versions recorded in crates/buildah-ffi/shim/go.mod after go mod tidy on Linux:

  • go.podman.io/storage v1.64.1
  • go.podman.io/image/v5 v5.41.2
  • go.podman.io/common v0.69.2

Older github.com/containers/buildah 1.43.x releases are affected by GO-2026-5116 (build breakout via a malicious Containerfile or Git HTTP server). This tree tracks v1.45.1, not that line.

go.sum is the lock. If go mod tidy moves a transitive pin, update this list to match.

Limitations

  • Real builds are Linux-only. The macOS and other non-Linux binaries link the stub. oci-builder calls startup before clap, so on those hosts --help exits 8 with a message that Buildah is available on Linux only.
  • RUN under oci or rootless isolation needs runc or crun on PATH. They are not linked in.
  • Signing is not implemented. Verification uses the pure-Go OpenPGP tag.
  • One store per process.
  • The Go runtime takes signals such as SIGURG. Cancel through CancelToken instead of relying on a Rust ctrl-c hook inside a build.
  • Rootless re-exec changes argv[0] and the original process does not continue after the child exits.
  • When the seccomp tag is enabled, libseccomp is a dynamic runtime dependency.
  • cargo test of the library does not drive the engine. The test harness is already multithreaded, so a Buildah re-exec from inside it would restart the harness. crates/oci-builder/tests/build_image.rs runs the oci-builder binary. It builds FROM scratch plus COPY with the vfs driver and chroot isolation, and it pushes only when ROB_TEST_REGISTRY is set (HTTP, TLS verification skipped).

Release

Pushing a tag vX.Y.Z runs .github/workflows/release.yml. The workflow runs the CI tests, then publishes buildah-ffi and oci-builder with crates.io trusted publishing. The tag must match workspace.package.version.

For each crate, the GitHub trusted publisher is workflow filename release.yml and environment release. Trusted publishing updates a crate that already exists, so the first upload of a crate still uses an API token. Later versions do not store a crates.io token in this repository.

License

Apache-2.0, the same license as Buildah. See LICENSE.