A tiny PID 1 for containers, written in x86-64 NASM and ARM64 GAS.
It spawns your target process as its own process group, forwards signals to the whole group, reaps zombies, and optionally restarts your app on crash. On exit, it returns your app's status code (with configurable signal→exit mapping).
Architectures: x86-64 Linux (native NASM build) and arm64/AArch64 (cross-build via GNU toolchain).
- Problem: many containers still run without a proper PID 1, which breaks signal handling and zombie reaping.
- Solution:
mini-init-asmis a tiny, auditable init that:- creates a new session + process group for your app;
- forwards signals to the entire group;
- reaps zombies (plus optional subreaper mode);
- supports graceful shutdown with configurable timeout and
SIGKILLescalation; - can restart the app on crashes (optional, env-driven);
- is implemented in pure assembly using Linux syscalls only (no libc).
It’s a parallel, assembly-focused alternative to tools like Tini: similar semantics for containers, but a different implementation style and feature focus.
Use mini-init-asm if you:
- Build minimal or
FROM scratchimages, and want a tiny, static PID 1. - Run multi-process containers and need robust process-group signal fan-out.
- Want a small, auditable init written in pure assembly (good for learning & review).
- Need simple restart-on-crash behavior without a full-blown supervisor.
If you just want a battle-tested init with wide distro support, you probably still want Tini. This project is intentionally small and opinionated, and targets PGID-mode container entrypoints.
This project is heavily inspired by the patterns popularized by Tini: spawn one child, forward signals, reap zombies. The difference is mainly in implementation and a few runtime semantics.
| Aspect | mini-init-asm | Tini |
|---|---|---|
| Language | x86-64 NASM + ARM64 GAS | C |
| Architectures | amd64, arm64 | Many (amd64, arm, armhf, i386, etc. – see releases) |
| Binary type | Static, no libc, pure Linux syscalls | Dynamic + static variants; depends on libc |
| Default kill mode | Always kills process group (kill(-pgid, sig)) |
By default kills child only, group-kill via -g / TINI_KILL_PROCESS_GROUP |
| Session / PGID | Always creates new session + PGID for child | Optional group-kill mode; no hard “PGID-mode only” branding |
| Signal handling | signalfd(2) + epoll(7) + timerfd(2) event loop |
traditional signal handlers + wait / reaping loop |
| Subreaper support | EP_SUBREAPER=1 env (uses PR_SET_CHILD_SUBREAPER) |
-s flag or TINI_SUBREAPER env |
| Restart-on-crash | Yes, via EP_RESTART_* env vars (simple supervisor mode) |
No (Tini intentionally does not supervise / restart children) |
| Exit code mapping | EP_EXIT_CODE_BASE (base + signal number) |
-e flags to remap specific exit codes to 0 |
| Config surface | Mostly env vars + minimal flags (-v, -V) |
CLI flags (-v, -s, -g, -e, -p, …) + env vars |
| Ecosystem integration | Standalone binary, Dockerfile provided | Packaged in many distros; integrated into Docker via --init |
| Size (qualitative) | Tiny static binary (pure asm, no libc; tens-of-KB range) | Tiny dynamic binary (~10KB), static version still <1MB (per upstream docs) |
The goal of
mini-init-asmis not to replace Tini everywhere, but to offer:
- a PGID-first, assembly-level implementation;
- an example of a full-featured container init in pure asm;
- a small init with restart-mode for simple setups.
| Feature / Behavior | Plain app as PID 1 | Tini | mini-init-asm (this repo) |
|---|---|---|---|
| Signal forwarding | Depends on app | Yes | Yes (group-wide) |
| Zombie reaping | Depends on app | Yes | Yes |
| Process-group kill | Depends on app | Optional (-g / env) |
Always group-based |
| Subreaper mode | No | Yes (-s / TINI_SUBREAPER) |
Yes (EP_SUBREAPER=1) |
| Restart on crash | Depends on app | No | Yes (EP_RESTART_* envs) |
| Pure-syscall implementation | Rare | No (libc) | Yes |
| Minimal config surface | N/A | CLI + env | Primarily env, very small CLI |
sudo apt-get update
sudo apt-get install -y nasm make binutilsmake./build/mini-init-amd64 -- /bin/sh -c 'echo hello && sleep 5'Install a cross toolchain:
sudo apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
make build-arm64Requires qemu-user-static to run an ARM64 binary on x86:
# Recommended: ensures the second `--` reaches mini-init
qemu-aarch64-static -- ./build/mini-init-arm64 -- /bin/sh -c 'echo hello && sleep 5'
# Alternatively (works in some shells too):
# qemu-aarch64-static ./build/mini-init-arm64 -- /bin/sh -c 'echo hello && sleep 5'If you see a usage message like usage: mini-init-arm64 ..., the -- delimiter was swallowed
by QEMU. Use the extra -- right after qemu-aarch64-static.
# Run and interrupt with Ctrl+C (TERM to group, grace window, optional KILL)
./build/mini-init-amd64 -- bash -c 'trap "echo got TERM; exit 0" TERM; sleep 1000'docker build -t mini-init-asm:dev -f docker/Dockerfile .
docker run --rm -it mini-init-asm:dev -- /bin/sh -c 'sleep 1000'
# In another terminal:
docker kill --signal=TERM <container-id># Build for both platforms
docker buildx build --platform linux/amd64,linux/arm64 \
-t mini-init-asm:latest -f docker/Dockerfile.multiarch .
# Run on specific platform
docker run --rm --platform linux/amd64 -it mini-init-asm:latest -- /bin/sh -c 'echo hello'
docker run --rm --platform linux/arm64 -it mini-init-asm:latest -- /bin/sh -c 'echo hello'mini-init-{amd64|arm64} [--verbose|-v] [--version|-V] -- <command> [args...]-v,--verbose— enable verbose logging (timestamps, fds, signal events).-V,--version— print version string and exit.
-
EP_GRACE_SECONDSGrace period (in seconds) from the first forwarded soft signal toSIGKILLescalation. Default:10. -
EP_SIGNALSCSV of additional signal names to monitor/forward (case-sensitive). Supported names:USR1,USR2,PIPE,WINCH,TTIN,TTOU,CONT,ALRM,RT1,...,RT31(RTN=SIGRTMIN+N, 1–31). These augment the built-in set:HUP,INT,QUIT,TERM,CHLDplus default forwarding ofUSR1,USR2,PIPE,WINCH,TTIN,TTOU,CONT,ALRM. Unknown tokens are ignored with a warning. In verbose mode we only log “EP_SIGNALS parsed” if the variable is present (even if empty). -
EP_SUBREAPERIf set to1, enablesPR_SET_CHILD_SUBREAPERso thatmini-init-asmadopts orphaned grandchildren. Useful when nested processes need proper reaping. Default: disabled. -
EP_EXIT_CODE_BASEBase value for mapping “killed by signal” to exit code:exit_code = EP_EXIT_CODE_BASE + signal_number(default base128, like shells). For example,SIGKILL(9) with base 200 → exit code 209. -
EP_RESTART_ENABLEDIf set to1, enables restart-on-crash: when the child is killed by a signal (non-zero, non-normal exit),mini-init-asmrestarts it. Restarts are disabled during graceful shutdown (after a soft signal likeTERM/INT/HUP/QUIT). Default: disabled. -
EP_MAX_RESTARTSMaximum number of restarts whenEP_RESTART_ENABLED=1. Allows up toNrestarts (N+1total runs: initial + N restarts). If the child crashes more than N times,mini-init-asmexits with the child’s code. Set to0for unlimited restarts. Default:0. -
EP_RESTART_BACKOFF_SECONDSDelay before restarting a crashed child. Helps avoid tight restart loops.0= restart immediately. Default:1. -
EP_ARM64_FALLBACK(ARM64/QEMU only) If set to1, ARM64 builds skip the epoll/signalfd path and use a simplerwait4loop. Intended as a workaround for QEMU user-mode flakiness in CI smoke tests. Default:0(CI jobs typically set this).
# Default behavior: forward TERM/INT/HUP/QUIT to the group, wait 10s, then KILL if needed
./build/mini-init-amd64 -- ./your-app --flag
# Verbose logs
./build/mini-init-amd64 -v -- ./your-app
# Check version
./build/mini-init-amd64 --version
# Custom grace period
EP_GRACE_SECONDS=5 ./build/mini-init-amd64 -- ./your-app
# Add USR1 forwarding
EP_SIGNALS=USR1 ./build/mini-init-amd64 -- ./your-app
# Add RT signals (RT1 = SIGRTMIN+1, RT5 = SIGRTMIN+5)
EP_SIGNALS=RT1,RT5 ./build/mini-init-amd64 -- ./your-app
# Enable subreaper mode (adopt orphaned grandchildren)
EP_SUBREAPER=1 ./build/mini-init-amd64 -- ./your-app
# Custom exit code base (SIGKILL will yield 200+9=209 instead of 128+9=137)
EP_EXIT_CODE_BASE=200 ./build/mini-init-amd64 -- ./your-app
# Restart on crash (up to 5 restarts, 2s backoff)
EP_RESTART_ENABLED=1 EP_MAX_RESTARTS=5 EP_RESTART_BACKOFF_SECONDS=2 \
./build/mini-init-amd64 -- ./your-app
# Unlimited restarts, no backoff
EP_RESTART_ENABLED=1 EP_MAX_RESTARTS=0 EP_RESTART_BACKOFF_SECONDS=0 \
./build/mini-init-amd64 -- ./your-app- Child exits normally →
mini-init-asmreturns the child exit code. - Child dies by signal → returns
EP_EXIT_CODE_BASE + signal_number(default base:128, e.g.SIGTERM= 143). - Child is killed by
SIGKILLafter grace-period expiration → returnsEP_EXIT_CODE_BASE + 9.
High-level algorithm:
-
Block all relevant signals in PID 1.
-
Spawn the child under a new session + process group (PGID = child PID).
-
Create:
- a
signalfdforHUP,INT,QUIT,TERM,CHLDplus anything fromEP_SIGNALS; - a
timerfdfor the grace window; - an
epollinstance watching both.
- a
-
Main loop:
-
Wait on
epoll_wait. -
On
signalfdevents:-
For soft signals (
HUP/INT/QUIT/TERM):- forward to process group via
kill(-pgid, sig); - arm the grace
timerfdif this is the first soft signal.
- forward to process group via
-
On
SIGCHLD:- reap children with
waitpid(-1, WNOHANG); - if the main child exited, propagate its exit code and terminate.
- reap children with
-
-
On timer expiration:
- if the child is still alive, escalate to
SIGKILLfor the whole group.
- if the child is still alive, escalate to
-
Key syscalls: signalfd(2), epoll(7), timerfd_create(2), timerfd_settime(2),
rt_sigprocmask(2), wait4(2), kill(2), setsid(2), setpgid(2).
mini-init-asm/
├─ README.md
├─ ROADMAP.md
├─ LICENSE
├─ Makefile
├─ .gitlab-ci.yml
├─ include/
│ ├─ macros.inc # x86-64 syscall/log helpers
│ ├─ macros_arm64.inc # arm64 syscall/log helpers
│ ├─ syscalls_amd64.inc # syscall numbers for x86-64
│ └─ syscalls_aarch64.inc # syscall numbers for arm64
├─ src/
│ ├─ amd64/ # NASM sources (x86-64 ABI)
│ └─ arm64/ # AArch64 sources (arm64 ABI)
├─ scripts/
│ ├─ test_harness.sh # e2e tests
│ └─ fixtures/
│ ├─ sleeper.sh
│ └─ trap_exit0.sh
└─ docker/
└─ Dockerfile # multi-stage: build -> scratch
make # build/mini-init-amd64
make test # run e2e tests on x86-64 host
make build-arm64 # build/mini-init-arm64 (requires aarch64-linux-gnu toolchain)
make test-arm64 # run ARM64 smoke tests via QEMU (requires qemu-user-static)
make cleanBoth binaries are linked with ld -nostdlib. The code issues syscalls directly:
- x86-64:
rax+rdi/rsi/rdx/r10/r8/r9 - arm64:
x8+x0–x5
Signal numbers and ABI differences are factored into the include/syscalls_*.inc files.
make test # Basic e2e tests
make test-all # e2e + unit + edge cases
# or directly:
bash scripts/test_harness.sh build/mini-init-amd64
bash scripts/test_ep_signals.sh build/mini-init-amd64
bash scripts/test_edge_cases.sh build/mini-init-amd64
bash scripts/test_exit_code_mapping.sh build/mini-init-amd64
bash scripts/test_restart.sh build/mini-init-amd64sudo apt-get install -y qemu-user-static
make test-arm64
# or:
bash scripts/test_harness_arm64.sh build/mini-init-arm64-
Basic e2e tests (
test_harness.sh):- group-wide forwarding (TERM) and graceful exit;
- escalation: app ignores TERM → KILL after grace window;
- custom
EP_SIGNALS=USR1and child reaction.
-
EP_SIGNALS parser tests (
test_ep_signals.sh):- single/multiple token parsing;
- unknown tokens (warnings);
- whitespace handling;
- empty / edge-case inputs.
-
Edge-case integration tests (
test_edge_cases.sh):- rapid signal bursts;
- orphaned process handling (
EP_SUBREAPER=1); - mixed TERM/INT/HUP;
- signals during grace period;
- immediate child exit after signal.
-
Exit code mapping tests (
test_exit_code_mapping.sh):- default base (128);
- custom base;
- normal exits (unaffected);
- signal exits with custom base.
-
Restart functionality tests (
test_restart.sh):- restart on crash (signal);
- restart with backoff;
- max-restart limit;
- no restart on normal exit;
- no restart after shutdown signal;
- unlimited restarts (
EP_MAX_RESTARTS=0).
Note: ARM64 tests run under QEMU user emulation and may differ slightly in timing. The smoke tests verify basic behavior; for full determinism use native ARM64.
- No privilege dropping, seccomp profiles, or capabilities tuning are implemented here.
- Intended as a small, auditable entrypoint that you combine with higher-level policies (cgroups, seccomp, AppArmor/SELinux, etc.) at the orchestrator / image level.
- Inspired by years of using Tini as a tiny init in containers.
- Assembler style: NASM (SysV ABI) and GNU AS (AArch64).