2026-08-28 01:52:19 +09:00
|
|
|
# bajia
|
|
|
|
|
|
2026-08-27 19:08:08 +01:00
|
|
|
an init system (PID 1) for embedded / VM targets, written in C++20 and
|
|
|
|
|
configured with a small declarative language inspired by Android's init `.rc`
|
|
|
|
|
format.
|
|
|
|
|
|
|
|
|
|
## Status
|
|
|
|
|
|
|
|
|
|
- `.rc` config parser (services + on-trigger action blocks)
|
|
|
|
|
- Supervisor event loop built on `signalfd` + `epoll`
|
|
|
|
|
- service spawn / reap / respawn (per-service restart policy)
|
|
|
|
|
- action commands: `start`, `stop`, `restart`, `exec`, `mkdir`, `chmod`,
|
|
|
|
|
`chown`, `setenv`, `write`, `symlink`, `mount`, `log`
|
|
|
|
|
- logger with a ring buffer that flushes to the console once available
|
|
|
|
|
|
|
|
|
|
roadmap:
|
|
|
|
|
|
2026-08-27 22:50:44 +01:00
|
|
|
- `SIGCHLD` crash-window limiting (rate-limited restarts; `crash-threshold`/
|
|
|
|
|
`crash-window` are parsed but not yet enforced by the reaper)
|
2026-08-27 19:08:08 +01:00
|
|
|
- dependency ordering between services
|
|
|
|
|
- property triggers (`property:<k>=<v>`) and `setprop`/`getprop`
|
|
|
|
|
- per-service logging to files
|
|
|
|
|
- `reboot`/`poweroff` path with ordered unmount
|
2026-08-27 22:50:44 +01:00
|
|
|
- readiness/socket activation
|
2026-08-27 19:08:08 +01:00
|
|
|
|
|
|
|
|
## building
|
|
|
|
|
|
|
|
|
|
requires a C++20 compiler and [Ninja](https://ninja-build.org/)
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
python3 configure.py # generates build/build.ninja
|
|
|
|
|
ninja -C build # produces build/bajia
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`configure.py` also writes a thin `Makefile` convenience wrapper
|
|
|
|
|
(`make`, `make clean`, `make format`, `--asan`, `--debug`).
|
|
|
|
|
|
|
|
|
|
## running
|
|
|
|
|
|
|
|
|
|
As a real init, the kernel must launch it as PID 1:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
init=/path/to/bajia
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Or by hand against a config (useful for development, may not behave like a
|
|
|
|
|
real boot). bajia normally refuses to start unless it is PID 1; pass
|
|
|
|
|
`--run-as-user` to override:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
./build/bajia --run-as-user etc/init.rc
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
If no files are given it looks for `/etc/bajia/init.rc`.
|
|
|
|
|
|
|
|
|
|
## configuration language
|
|
|
|
|
|
|
|
|
|
See [`etc/init.rc`](etc/init.rc) for a complete example.
|
|
|
|
|
|
|
|
|
|
### services
|
|
|
|
|
|
|
|
|
|
```rc
|
|
|
|
|
service NAME /path/to/exe [args...]
|
2026-08-27 22:50:44 +01:00
|
|
|
user = root|other # uid after the privilege drop (name or number)
|
|
|
|
|
group = GROUP [GROUP...] # primary gid + supplementary groups (names/numbers)
|
|
|
|
|
oneshot # run once and exit, never respawn
|
|
|
|
|
disabled # not started by the boot sequence
|
|
|
|
|
console # bind stdio to /dev/console
|
|
|
|
|
class = NAME # grouping (default "default")
|
|
|
|
|
respawn = never|on-failure|always # restart policy (default always)
|
|
|
|
|
crash-threshold = N # restarts allowed per window
|
|
|
|
|
crash-window = SECS
|
|
|
|
|
seclabel = CONTEXT # SELinux exec context (--selinux build)
|
|
|
|
|
setenv = K=V # extra environment (repeatable)
|
|
|
|
|
cwd = /path
|
2026-08-27 19:08:08 +01:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### actions
|
|
|
|
|
|
|
|
|
|
```rc
|
|
|
|
|
on TRIGGER
|
|
|
|
|
start NAME | stop NAME | restart NAME
|
|
|
|
|
exec /cmd args...
|
|
|
|
|
mkdir PATH [mode]
|
|
|
|
|
chmod PATH mode
|
|
|
|
|
chown PATH uid gid
|
|
|
|
|
setenv K V
|
|
|
|
|
write PATH CONTENT
|
|
|
|
|
symlink TARGET LINK
|
|
|
|
|
mount SOURCE TARGET FSTYPE
|
|
|
|
|
log message
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-27 22:50:44 +01:00
|
|
|
boot triggers fire in order: `early-init`, `init`, `boot`. `shutdown` triggers
|
|
|
|
|
fire when the system is winding down. property/`service-*` triggers are on the
|
|
|
|
|
roadmap.
|
|
|
|
|
|
|
|
|
|
Services run as `root` by default; `user`/`group` trigger a full privilege
|
|
|
|
|
drop (supplementary groups, then gid, then uid) before exec.
|
|
|
|
|
|
2026-08-28 00:22:49 +01:00
|
|
|
### imports
|
|
|
|
|
|
|
|
|
|
Configs can be split across files with `@import PATH` (column 0, before any
|
|
|
|
|
section in that file):
|
|
|
|
|
|
|
|
|
|
```rc
|
|
|
|
|
@import extra-services.rc
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The path resolves relative to the importing file's directory (absolute paths
|
|
|
|
|
pass through). A file reached by several imports is only parsed once;
|
|
|
|
|
self/cyclic imports are reported as errors. Imported files may import other
|
|
|
|
|
files and define services and actions like any other rc. `reload` re-parses
|
|
|
|
|
the whole import tree, so imported changes take effect on `bctl reload`.
|
|
|
|
|
|
2026-08-27 22:50:44 +01:00
|
|
|
## control
|
|
|
|
|
|
|
|
|
|
A running init listens on an abstract unix socket (`@bajia`). The bundled
|
|
|
|
|
`bctl` client drives it:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
bctl status # list services + state
|
|
|
|
|
bctl start NAME # start a service
|
|
|
|
|
bctl stop NAME # graceful stop (SIGTERM)
|
|
|
|
|
bctl restart NAME # restart a service
|
|
|
|
|
bctl trigger EVENT # fire an action trigger
|
|
|
|
|
bctl reload # re-parse init.rc and reconcile services
|
|
|
|
|
bctl shutdown [poweroff|reboot]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`reload` (also `kill -HUP 1`) re-parses the rc files: removed services are
|
|
|
|
|
stopped, added services registered, and running services whose definition
|
|
|
|
|
changed are restarted with the new definition. A parse error rejects the
|
|
|
|
|
reload and keeps the live config.
|
|
|
|
|
|
|
|
|
|
## SELinux (experimental)
|
|
|
|
|
|
|
|
|
|
SELinux support is opt-in (`configure.py --selinux`, adds `-DBAJIA_SELINUX`
|
|
|
|
|
+ `-lselinux`). With it enabled, PID 1 mounts selinuxfs, loads the policy
|
|
|
|
|
from `/etc/selinux/config`, calls `selinux_restorecon` on the core tree, and
|
|
|
|
|
applies a per-service exec label via the `seclabel = CONTEXT` service option.
|
|
|
|
|
|
|
|
|
|
To bring up a policy without hand-writing one, reuse the host's installed
|
|
|
|
|
policy (Fedora/SELinux hosts have one at `/etc/selinux/<type>/`) in
|
|
|
|
|
permissive mode:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
python3 tools/run_vm.py --selinux
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This bundles the host `policy.policy.<vers>` and `file_contexts` into the
|
|
|
|
|
initramfs, writes `SELINUX=permissive`, and boots `selinux=1 enforcing=0`.
|
|
|
|
|
Watch the serial console for `selinux: policy loaded, enforcing=0`; a
|
|
|
|
|
`selinux-probe` service prints the runtime exec contexts:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
probe-ctx=system_u:system_r:init_t:s0 init-ctx=system_u:system_r:kernel_t:s0
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The guest kernel must support SELinux and the policy version must match the
|
|
|
|
|
kernel's (`cat /sys/fs/selinux/policyvers`). Once permissive is stable,
|
|
|
|
|
read `avc: denied` lines from dmesg and iterate toward a minimal custom
|
|
|
|
|
policy with `checkpolicy`/`audit2allow`, then flip to `enforcing=1`.
|
|
|
|
|
|
|
|
|
|
Limitations: uses the dynamic libselinux (no static build on Fedora), so the
|
|
|
|
|
`--selinux` init is dynamically linked and the loader + libs (`libselinux`,
|
|
|
|
|
`libpcre2-8`, glibc) are bundled into the initramfs.
|
2026-08-28 00:22:49 +01:00
|
|
|
|
|
|
|
|
## development & testing
|
|
|
|
|
|
|
|
|
|
Host-side unit tests (no framework, no dependencies) cover the rc parser, the
|
|
|
|
|
`@import` machinery, and the pure supervisor helpers:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
make test # builds build/unit_tests and runs it
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The parser is also fuzz-tested with libFuzzer (needs clang):
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
python3 tools/fuzz.py --seconds 300
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This drives random bytes through the same `parse_rc_stream` path the real init
|
|
|
|
|
uses, with `@import` rejected so fuzz input can never open real files (e.g.
|
|
|
|
|
`/dev/zero`). Crashes are saved under `build/fuzz-`; seeds accumulate in
|
|
|
|
|
`build/fuzz-corpus` and grow between runs. A grammar dictionary (auto-seeded
|
|
|
|
|
at `build/fuzz.dict`, overridable via `--dict`, disabled with `--no-dict`)
|
|
|
|
|
guides coverage toward real rc keywords. Leak detection is on by default:
|
|
|
|
|
`tools/lsan.supp` silences the spurious `strdup` that a torsocks `LD_PRELOAD`
|
|
|
|
|
on the dev host allocates at startup, so any real leak in `parse_rc_stream` is
|
|
|
|
|
saved as a `leak-*` artifact; pass `--no-detect-leaks` to disable it on a
|
|
|
|
|
clean host. Peak fuzz RSS is driven mostly by ASan's freed-memory quarantine
|
|
|
|
|
(256MiB default); fuzz.py pins it to 64MiB (`--quarantine-mb N`, 0 to
|
|
|
|
|
disable), which roughly halves peak RSS.
|
|
|
|
|
|
|
|
|
|
To leak-check the host-side unit tests under ASan/LSan instead:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
make test-asan
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
(clang++ and a `leak:tsocks_once` suppression are used automatically; the
|
|
|
|
|
default `make test` runs the same assertions without the sanitizer).
|