more things

This commit is contained in:
Hedy88 2026-08-27 22:50:44 +01:00
commit 6e60709283
No known key found for this signature in database
15 changed files with 1403 additions and 294 deletions

6
.clang-format Normal file
View file

@ -0,0 +1,6 @@
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 0
SortIncludes: false
PointerAlignment: Left
ReflowComments: false

View file

@ -15,14 +15,13 @@ format.
roadmap: roadmap:
- user/group privilege drop (`user`, `group`, supplementary groups) - `SIGCHLD` crash-window limiting (rate-limited restarts; `crash-threshold`/
`crash-window` are parsed but not yet enforced by the reaper)
- dependency ordering between services - dependency ordering between services
- `SIGCHLD` crash-window limiting (rate-limited restarts)
- property triggers (`property:<k>=<v>`) and `setprop`/`getprop` - property triggers (`property:<k>=<v>`) and `setprop`/`getprop`
- per-service logging to files - per-service logging to files
- `reboot`/`poweroff` path with ordered unmount - `reboot`/`poweroff` path with ordered unmount
- `SIGHUP` config reload - readiness/socket activation
- SELinux
## building ## building
@ -62,17 +61,18 @@ See [`etc/init.rc`](etc/init.rc) for a complete example.
```rc ```rc
service NAME /path/to/exe [args...] service NAME /path/to/exe [args...]
user root|other # privilege level (drop, planned) user = root|other # uid after the privilege drop (name or number)
group GROUP [GROUP...] group = GROUP [GROUP...] # primary gid + supplementary groups (names/numbers)
oneshot # run once and exit, never respawn oneshot # run once and exit, never respawn
disabled # not started by the boot sequence disabled # not started by the boot sequence
console # bind stdio to /dev/console console # bind stdio to /dev/console
class NAME # grouping (default "default") class = NAME # grouping (default "default")
respawn never|on-failure|always # restart policy (default always) respawn = never|on-failure|always # restart policy (default always)
crash-threshold N # restarts allowed per window crash-threshold = N # restarts allowed per window
crash-window SECS crash-window = SECS
setenv K=V # extra environment (repeatable) seclabel = CONTEXT # SELinux exec context (--selinux build)
cwd /path setenv = K=V # extra environment (repeatable)
cwd = /path
``` ```
### actions ### actions
@ -91,6 +91,62 @@ on TRIGGER
log message log message
``` ```
boot triggers fire in order: `early-init`, `init`, `boot`. `shutdown` is boot triggers fire in order: `early-init`, `init`, `boot`. `shutdown` triggers
reserved (planned wiring to the signal path). property/`service-*` triggers fire when the system is winding down. property/`service-*` triggers are on the
are on the roadmap. roadmap.
Services run as `root` by default; `user`/`group` trigger a full privilege
drop (supplementary groups, then gid, then uid) before exec.
## 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.

View file

@ -5,9 +5,13 @@ usage:
python3 configure.py python3 configure.py
python3 configure.py --asan # enable address/undefined sanitizers python3 configure.py --asan # enable address/undefined sanitizers
python3 configure.py --debug # -O0 instead of -O2 python3 configure.py --debug # -O0 instead of -O2
python3 configure.py --selinux # enable SELinux integration
python3 configure.py --compile-commands # also write build/compile_commands.json
""" """
import argparse import argparse
import json
import os import os
import shlex
import shutil import shutil
import sys import sys
from pathlib import Path from pathlib import Path
@ -54,10 +58,19 @@ format:
) )
def emit_ninja(cxx, cxxflags, dst): def emit_ninja(cxx, cxxflags, dst, write_cc=False):
sources = discover_sources() sources = discover_sources()
objs = ["obj/" + Path(s).stem + ".o" for s in sources] objs = ["obj/" + Path(s).stem + ".o" for s in sources]
target = "bajia" target = "bajia"
ctl_target = "bctl"
# bctl_main.cpp is a second shipped binary (the control client),
# excluded from the init objects by the *_main.cpp convention.
ctl_source = SRC / "bctl_main.cpp"
ctl_obj = "obj/bctl_main.o"
# (source, output) pairs for compile_commands.json.
cc_entries = [(SRC / s, o) for s, o in zip(sources, objs)]
cc_entries.append((ctl_source, ctl_obj))
rule_cxx = ( rule_cxx = (
"rule cxx\n" "rule cxx\n"
@ -77,22 +90,49 @@ def emit_ninja(cxx, cxxflags, dst):
lines.append(rule_cxx) lines.append(rule_cxx)
lines.append(rule_link) lines.append(rule_link)
lines.append('build {target}: link {objs}'.format(target=target, objs=" ".join(objs))) lines.append('build {target}: link {objs}'.format(target=target, objs=" ".join(objs)))
lines.append('build {ctl}: link {ctl_obj}'.format(ctl=ctl_target, ctl_obj=ctl_obj))
lines.append("") lines.append("")
for o, s in zip(objs, sources): for o, s in zip(objs, sources):
lines.append('build {o}: cxx {src}/{s}'.format(o=o, src=SRC, s=s)) lines.append('build {o}: cxx {src}/{s}'.format(o=o, src=SRC, s=s))
lines.append('build {ctl_obj}: cxx {ctl_src}'.format(ctl_obj=ctl_obj, ctl_src=ctl_source))
lines.append("") lines.append("")
lines.append('build all: phony {target}'.format(target=target)) lines.append(
'build all: phony {target} {ctl}'.format(target=target, ctl=ctl_target))
lines.append("default all") lines.append("default all")
lines.append("") lines.append("")
if write_cc:
write_compile_commands(cxx, cxxflags, cc_entries)
(BUILD / "obj").mkdir(exist_ok=True) (BUILD / "obj").mkdir(exist_ok=True)
dst.write_text("\n".join(lines) + "\n", encoding="utf-8") dst.write_text("\n".join(lines) + "\n", encoding="utf-8")
def write_compile_commands(cxx, cxxflags, src_out_pairs):
"""Write build/compile_commands.json for clangd / clang-tidy."""
entries = []
for src, out in src_out_pairs:
args = [cxx] + list(cxxflags) + ["-c", str(src), "-o", out]
entries.append({
"directory": str(BUILD.resolve()),
"file": str(src.resolve()),
"output": out,
"arguments": args,
"command": shlex.join(args),
})
(BUILD / "compile_commands.json").write_text(
json.dumps(entries, indent=1) + "\n", encoding="utf-8")
def parse_args(): def parse_args():
p = argparse.ArgumentParser(description="Configure bajia build (generates build.ninja)") p = argparse.ArgumentParser(description="Configure bajia build (generates build.ninja)")
p.add_argument("--asan", action="store_true", help="enable address + UB sanitizers") p.add_argument("--asan", action="store_true", help="enable address + UB sanitizers")
p.add_argument("--debug", action="store_true", help="disable -O2, enable -O0") p.add_argument("--debug", action="store_true", help="disable -O2, enable -O0")
p.add_argument("--clean", action="store_true", help="remove build dir") p.add_argument("--clean", action="store_true", help="remove build dir")
p.add_argument("--selinux", action="store_true",
help="enable SELinux integration (needs libselinux-dev; "
"see tools/run_vm.py --selinux for a VM test path)")
p.add_argument("--compile-commands", action="store_true",
help="write build/compile_commands.json (for clangd)")
return p.parse_args() return p.parse_args()
def main(): def main():
@ -106,6 +146,20 @@ def main():
print("error: ninja not found in PATH", file=sys.stderr) print("error: ninja not found in PATH", file=sys.stderr)
return 1 return 1
# Wipe stale objects when compiler flags change: ninja doesn't track
# cflags, so a re-run with a different --selinux/--asan/--debug profile
# would otherwise link a half-stale tree.
stamp = {"selinux": args.selinux, "asan": args.asan, "debug": args.debug}
stamp_file = BUILD / ".configure.json"
if stamp_file.exists():
try:
prev = json.loads(stamp_file.read_text())
except (json.JSONDecodeError, OSError):
prev = None
if prev != stamp:
print("build configuration changed; cleaning build/")
shutil.rmtree(BUILD, ignore_errors=True)
BUILD.mkdir(exist_ok=True) BUILD.mkdir(exist_ok=True)
cxxflags = list(CXXFLAGS) cxxflags = list(CXXFLAGS)
@ -117,14 +171,20 @@ def main():
if args.asan: if args.asan:
cxxflags += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"] cxxflags += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
linkflags += ["-fsanitize=address,undefined"] linkflags += ["-fsanitize=address,undefined"]
if args.selinux:
cxxflags += ["-DBAJIA_SELINUX"]
linkflags += ["-lselinux"]
cxxflags = [f for f in cxxflags if f not in WARNINGS] cxxflags = [f for f in cxxflags if f not in WARNINGS]
cxxflags = WARNINGS + cxxflags cxxflags = WARNINGS + cxxflags
emit_ninja(CXX, cxxflags, BUILD / "build.ninja") emit_ninja(CXX, cxxflags, BUILD / "build.ninja", write_cc=args.compile_commands)
write_makefile_convenience() write_makefile_convenience()
stamp_file.write_text(json.dumps(stamp, indent=1) + "\n", encoding="utf-8")
print(f"configured {len(discover_sources())} sources -> build/build.ninja") print(f"configured {len(discover_sources())} sources -> build/build.ninja")
if args.compile_commands:
print("wrote build/compile_commands.json")
print("run: ninja -C build") print("run: ninja -C build")
return 0 return 0

View file

@ -2,7 +2,9 @@
# #
# constructs: # constructs:
# service NAME /path/to/exe [args...] # service NAME /path/to/exe [args...]
# user|group|oneshot|disabled|console|class|respawn|crash-*|setenv|cwd # user = ... | group = ... | class = ... | respawn = ... | crash-threshold = ...
# crash-window = ... | setenv = ... | cwd = ... | seclabel = ...
# oneshot | disabled | console (flag options, no value)
# #
# on TRIGGER # on TRIGGER
# start NAME | stop NAME | restart NAME # start NAME | stop NAME | restart NAME
@ -39,16 +41,16 @@ on boot
# the console service: bind stdio to /dev/console, always respawn. # the console service: bind stdio to /dev/console, always respawn.
service console /sbin/getty -L ttyS0 115200 vt100 service console /sbin/getty -L ttyS0 115200 vt100
class core class = core
console console
user root user = root
# a long-lived example daemon. respawning is the default (always). # a long-lived example daemon. respawning is the default (always).
service watchdog /usr/sbin/watchdog service watchdog /usr/sbin/watchdog
class core class = core
respawn always respawn = always
# a one-shot job: runs once, exits, never respawns. # a one-shot job: runs once, exits, never respawns.
service boot-logo /usr/bin/show-boot-logo service boot-logo /usr/bin/show-boot-logo
class late class = late
oneshot oneshot

93
src/bctl_main.cpp Normal file
View file

@ -0,0 +1,93 @@
// bctl.cpp - tiny line-based client for the bajia control socket (@bajia).
//
// talks to a running bajia (PID 1) to start/stop/restart services, fire
// triggers, query service status, and request shutdown
//
// wired to the abstract unix socket "@bajia" served by
// Supervisor::open_control_socket().
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <string>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
namespace {
constexpr const char kCtlName[] = "bajia";
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
void usage(const char* argv0) {
std::fprintf(
stderr,
"usage: %s <command> [args...]\n"
" start NAMES start a service\n"
" stop NAME gracefully stop a service (SIGTERM)\n"
" restart NAME restart a service\n"
" trigger EVENT fire a trigger (e.g. boot, shutdown)\n"
" reload re-parse the rc files and reconcile services\n"
" status list services and their state\n"
" shutdown [kind] shut down (kind: poweroff|reboot)\n"
" ping sanity check that init is alive\n",
argv0);
}
} // namespace
int main(int argc, char** argv) {
if (argc < 2) {
usage(argv[0]);
return 2;
}
std::string line;
for (int i = 1; i < argc; ++i) {
if (i > 1)
line += ' ';
line += argv[i];
}
line += '\n';
const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0) {
std::perror("bctl: socket");
return 1;
}
sockaddr_un addr{};
addr.sun_family = AF_UNIX;
std::memcpy(addr.sun_path + 1, kCtlName,
sizeof kCtlName); // leading NUL = abstract
const socklen_t len = static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) +
1 + sizeof kCtlName);
if (::connect(fd, reinterpret_cast<sockaddr*>(&addr), len) != 0) {
std::fprintf(stderr, "bctl: cannot connect to @%s: %s\n", kCtlName,
std::strerror(errno));
::close(fd);
return 1;
}
const ssize_t nw = ::write(fd, line.data(), line.size());
if (nw != static_cast<ssize_t>(line.size())) {
std::perror("bctl: write");
::close(fd);
return 1;
}
::shutdown(fd, SHUT_WR); // tell init no more commands; read the reply
std::string out;
char buf[512];
ssize_t n;
while ((n = ::read(fd, buf, sizeof buf)) > 0) {
out.append(buf, static_cast<size_t>(n));
}
::close(fd);
std::fwrite(out.data(), 1, out.size(), stdout);
// a line starting with "ERR" means failure (status lines end with OK).
const bool failed =
out.find("\nERR ") != std::string::npos || out.rfind("ERR ", 0) == 0;
return failed ? 1 : 0;
}

View file

@ -12,7 +12,7 @@ namespace {
std::vector<std::string> tokenize(const std::string& line) { std::vector<std::string> tokenize(const std::string& line) {
std::vector<std::string> out; std::vector<std::string> out;
std::string cur; std::string cur;
bool in_q = false; bool in_q = false;
bool need_quote_close = false; bool need_quote_close = false;
@ -48,7 +48,8 @@ std::vector<std::string> tokenize(const std::string& line) {
} }
if (in_q) { if (in_q) {
// unterminated quote: best effort, keep what we have. // unterminated quote: best effort, keep what we have.
if (!cur.empty()) out.push_back(cur); if (!cur.empty())
out.push_back(cur);
} else if (!cur.empty()) { } else if (!cur.empty()) {
out.push_back(cur); out.push_back(cur);
} }
@ -57,8 +58,10 @@ std::vector<std::string> tokenize(const std::string& line) {
} }
RespawnPolicy parse_respawn(const std::string& s) { RespawnPolicy parse_respawn(const std::string& s) {
if (s == "always") return RespawnPolicy::Always; if (s == "always")
if (s == "on-failure") return RespawnPolicy::OnFailure; return RespawnPolicy::Always;
if (s == "on-failure")
return RespawnPolicy::OnFailure;
return RespawnPolicy::Never; return RespawnPolicy::Never;
} }
@ -66,14 +69,16 @@ RespawnPolicy parse_respawn(const std::string& s) {
Service* Config::find_service(const std::string& name) { Service* Config::find_service(const std::string& name) {
for (auto& s : services) { for (auto& s : services) {
if (s.name == name) return &s; if (s.name == name)
return &s;
} }
return nullptr; return nullptr;
} }
const Service* Config::find_service(const std::string& name) const { const Service* Config::find_service(const std::string& name) const {
for (auto& s : services) { for (auto& s : services) {
if (s.name == name) return &s; if (s.name == name)
return &s;
} }
return nullptr; return nullptr;
} }
@ -81,10 +86,11 @@ const Service* Config::find_service(const std::string& name) const {
// public entry point // public entry point
Config parse_config(const std::vector<std::string>& files) { Config parse_config(const std::vector<std::string>& files) {
Config cfg; Config cfg;
cfg.sources = files;
int line = 0; int line = 0;
std::string section_kind; // "service" or "action" std::string section_kind; // "service" or "action"
Service* cur_svc = nullptr; // service being configured Service* cur_svc = nullptr; // service being configured
Action* cur_act = nullptr; // action being configured Action* cur_act = nullptr; // action being configured
for (const auto& file : files) { for (const auto& file : files) {
std::ifstream in(file); std::ifstream in(file);
@ -100,7 +106,8 @@ Config parse_config(const std::vector<std::string>& files) {
while (std::getline(in, raw)) { while (std::getline(in, raw)) {
++line; ++line;
auto toks = tokenize(raw); auto toks = tokenize(raw);
if (toks.empty()) continue; if (toks.empty())
continue;
std::string first = toks[0]; std::string first = toks[0];
size_t indent = raw.find_first_not_of(" \t"); size_t indent = raw.find_first_not_of(" \t");
@ -133,42 +140,75 @@ Config parse_config(const std::vector<std::string>& files) {
} }
// service options (must already be inside a service section). // service options (must already be inside a service section).
// value-taking keywords require `keyword = value` syntax.
if (section_kind == "service" && cur_svc) { if (section_kind == "service" && cur_svc) {
if (first == "user" && toks.size() >= 2) cur_svc->uid = toks[1]; if (first == "oneshot")
else if (first == "group" && toks.size() >= 2) { cur_svc->oneshot = true;
cur_svc->gid = toks[1]; else if (first == "disabled")
for (size_t i = 2; i < toks.size(); ++i) cur_svc->groups.push_back(toks[i]); cur_svc->disabled = true;
else if (first == "console")
cur_svc->console = true;
else if (first == "user" || first == "group" || first == "class" ||
first == "respawn" || first == "crash-threshold" ||
first == "crash-window" || first == "setenv" ||
first == "cwd" || first == "seclabel") {
if (toks.size() < 3 || toks[1] != "=") {
throw std::runtime_error(file + ":" + std::to_string(line) +
": option '" + first +
"' requires '= value' syntax");
}
if (first == "user")
cur_svc->uid = toks[2];
else if (first == "group") {
cur_svc->gid = toks[2];
for (size_t i = 3; i < toks.size(); ++i)
cur_svc->groups.push_back(toks[i]);
} else if (first == "class")
cur_svc->service_class = toks[2];
else if (first == "respawn")
cur_svc->respawn = parse_respawn(toks[2]);
else if (first == "crash-threshold")
cur_svc->crash_threshold = std::stoi(toks[2]);
else if (first == "crash-window")
cur_svc->crash_window_secs = std::stoi(toks[2]);
else if (first == "setenv")
cur_svc->env.push_back(toks[2]);
else if (first == "cwd")
cur_svc->cwd = toks[2];
else if (first == "seclabel")
cur_svc->seclabel = toks[2];
} }
else if (first == "oneshot") cur_svc->oneshot = true; // unknown option keys are ignored.
else if (first == "disabled") cur_svc->disabled = true;
else if (first == "console") cur_svc->console = true;
else if (first == "class" && toks.size() >= 2) cur_svc->service_class = toks[1];
else if (first == "respawn" && toks.size() >= 2)
cur_svc->respawn = parse_respawn(toks[1]);
else if (first == "crash-threshold" && toks.size() >= 2)
cur_svc->crash_threshold = std::stoi(toks[1]);
else if (first == "crash-window" && toks.size() >= 2)
cur_svc->crash_window_secs = std::stoi(toks[1]);
else if (first == "setenv" && toks.size() >= 2) cur_svc->env.push_back(toks[1]);
else if (first == "cwd" && toks.size() >= 2) cur_svc->cwd = toks[1];
continue; continue;
} }
// action command (must be inside an action section). // action command (must be inside an action section).
if (section_kind == "action" && cur_act) { if (section_kind == "action" && cur_act) {
Command cmd; Command cmd;
if (first == "start" && toks.size() >= 2) cmd.kind = Command::Kind::Start; if (first == "start" && toks.size() >= 2)
else if (first == "stop" && toks.size() >= 2) cmd.kind = Command::Kind::Stop; cmd.kind = Command::Kind::Start;
else if (first == "restart" && toks.size() >= 2) cmd.kind = Command::Kind::Restart; else if (first == "stop" && toks.size() >= 2)
else if (first == "exec") cmd.kind = Command::Kind::Exec; cmd.kind = Command::Kind::Stop;
else if (first == "mkdir") cmd.kind = Command::Kind::Mkdir; else if (first == "restart" && toks.size() >= 2)
else if (first == "chmod") cmd.kind = Command::Kind::Chmod; cmd.kind = Command::Kind::Restart;
else if (first == "chown") cmd.kind = Command::Kind::Chown; else if (first == "exec")
else if (first == "setenv") cmd.kind = Command::Kind::Setenv; cmd.kind = Command::Kind::Exec;
else if (first == "write") cmd.kind = Command::Kind::Write; else if (first == "mkdir")
else if (first == "symlink") cmd.kind = Command::Kind::Symlink; cmd.kind = Command::Kind::Mkdir;
else if (first == "mount") cmd.kind = Command::Kind::Mount; else if (first == "chmod")
else if (first == "log") cmd.kind = Command::Kind::Log; cmd.kind = Command::Kind::Chmod;
else if (first == "chown")
cmd.kind = Command::Kind::Chown;
else if (first == "setenv")
cmd.kind = Command::Kind::Setenv;
else if (first == "write")
cmd.kind = Command::Kind::Write;
else if (first == "symlink")
cmd.kind = Command::Kind::Symlink;
else if (first == "mount")
cmd.kind = Command::Kind::Mount;
else if (first == "log")
cmd.kind = Command::Kind::Log;
else { else {
throw std::runtime_error(file + ":" + std::to_string(line) + throw std::runtime_error(file + ":" + std::to_string(line) +
": unknown action command '" + first + "'"); ": unknown action command '" + first + "'");

View file

@ -4,12 +4,13 @@
// Two top-level constructs: // Two top-level constructs:
// //
// service NAME /path/to/exec args... // service NAME /path/to/exec args...
// user root // user = root # uid after the privilege drop
// group root // group = root [g...] # primary gid + supplementary groups
// oneshot // oneshot
// disabled // disabled
// class main // class = main
// respawn never|on-failure|always // respawn = never|on-failure|always
// seclabel = CONTEXT
// console // console
// //
// on TRIGGER // on TRIGGER
@ -30,6 +31,11 @@
namespace bajia { namespace bajia {
// --------------------------------------------------------------------------
// version
// --------------------------------------------------------------------------
constexpr const char* kBajiaVersion = "0.1";
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
// Service // Service
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
@ -41,19 +47,24 @@ enum class RespawnPolicy {
struct Service { struct Service {
std::string name; std::string name;
std::vector<std::string> args; // executable path + arguments std::vector<std::string> args; // executable path + arguments
std::string cwd = "/"; std::string cwd = "/";
std::string uid = "root"; // resolved in supervisor std::string uid = "root"; // resolved in supervisor
std::string gid = "root"; std::string gid = "root";
std::vector<std::string> groups; std::vector<std::string> groups;
bool oneshot = false; // run once, don't keep alive bool oneshot = false; // run once, don't keep alive
bool disabled = false; // not started automatically bool disabled = false; // not started automatically
bool console = false; // bind stdio to the console bool console = false; // bind stdio to the console
std::string service_class = "default"; std::string service_class = "default";
RespawnPolicy respawn = RespawnPolicy::Always; RespawnPolicy respawn = RespawnPolicy::Always;
int crash_threshold = 4; // max restarts within window int crash_threshold = 4; // max restarts within window
int crash_window_secs = 30; // before giving up int crash_window_secs = 30; // before giving up
std::vector<std::string> env; // "K=V" pairs std::vector<std::string> env; // "K=V" pairs
std::string seclabel; // SELinux exec context (optional)
// Transient runtime flag: this service was stopped by a config reload
// because its definition changed; respawn it once with the new definition.
bool restart_on_reap = false;
// Runtime state // Runtime state
int pid = 0; int pid = 0;
@ -69,7 +80,7 @@ struct Command {
Start, Start,
Stop, Stop,
Restart, Restart,
Exec, // run a synchronous command to completion Exec, // run a synchronous command to completion
Mkdir, Mkdir,
Chmod, Chmod,
Chown, Chown,
@ -80,11 +91,11 @@ struct Command {
Log, Log,
}; };
Kind kind; Kind kind;
std::vector<std::string> args; // command-specific arguments std::vector<std::string> args; // command-specific arguments
}; };
struct Action { struct Action {
std::string trigger; // e.g. "boot", "early-init" std::string trigger; // e.g. "boot", "early-init"
std::vector<Command> commands; std::vector<Command> commands;
}; };
@ -95,9 +106,11 @@ struct Config {
std::vector<Service> services; std::vector<Service> services;
std::vector<Action> actions; std::vector<Action> actions;
std::string hostname; std::string hostname;
// the .rc files this config was parsed from (used for SIGHUP reload).
std::vector<std::string> sources;
Service* find_service(const std::string& name); Service* find_service(const std::string& name);
const Service* find_service(const std::string& name) const; const Service* find_service(const std::string& name) const;
}; };
// Parse a set of .rc files into a Config. Throws std::runtime_error on // Parse a set of .rc files into a Config. Throws std::runtime_error on

View file

@ -6,6 +6,7 @@
#include <ctime> #include <ctime>
#include <fcntl.h> #include <fcntl.h>
#include <mutex> #include <mutex>
#include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
namespace bajia { namespace bajia {
@ -14,31 +15,75 @@ namespace {
std::mutex g_lock; std::mutex g_lock;
int g_fd = -1; // console fd (>=0 when open) int g_fd = -1; // console fd (>=0 when open)
int g_scr_fd =
-1; // mirrored VT fd (/dev/tty1); systemd-style boot text on the screen
std::array<std::string, 64> g_ring; std::array<std::string, 64> g_ring;
size_t g_ring_pos = 0; size_t g_ring_pos = 0;
size_t g_ring_count = 0; size_t g_ring_count = 0;
LogLevel g_min_level = LogLevel::Info; LogLevel g_min_level = LogLevel::Info;
// /dev/console follows the kernel's *preferred* console; with
// `console=tty1 console=ttyS0` that is ttyS0, so the VGA screen would stay
// empty. Mirror to /dev/tty1 (the screen) whenever it is a different device.
void open_screen() {
if (g_scr_fd >= 0)
return;
int fd = ::open("/dev/tty1", O_WRONLY | O_NOCTTY | O_CLOEXEC);
if (fd < 0)
return; // retried lazily on the next write (devtmpfs may not
// be mounted yet during the very first boot actions)
if (g_fd >= 0) {
struct stat a{}, b{};
if (::fstat(g_fd, &a) == 0 && ::fstat(fd, &b) == 0 &&
a.st_rdev == b.st_rdev) {
::close(fd);
return; // /dev/console is already the VT
}
}
g_scr_fd = fd;
}
const char* level_name(LogLevel l) { const char* level_name(LogLevel l) {
switch (l) { switch (l) {
case LogLevel::Debug: return "DBG"; case LogLevel::Debug:
case LogLevel::Info: return "INF"; return "DBG";
case LogLevel::Warn: return "WRN"; case LogLevel::Info:
case LogLevel::Err: return "ERR"; return "INF";
case LogLevel::Warn:
return "WRN";
case LogLevel::Err:
return "ERR";
} }
return "???"; return "???";
} }
void raw_write(int fd, const std::string& s) {
size_t off = 0;
while (off < s.size()) {
ssize_t n = ::write(fd, s.data() + off, s.size() - off);
if (n < 0)
break;
off += static_cast<size_t>(n);
}
}
void write_all(const std::string& s) { void write_all(const std::string& s) {
if (g_fd >= 0) { if (g_fd >= 0) {
size_t off = 0; raw_write(g_fd, s);
while (off < s.size()) {
ssize_t n = ::write(g_fd, s.data() + off, s.size() - off);
if (n < 0) break;
off += static_cast<size_t>(n);
}
} else { } else {
::write(STDERR_FILENO, s.data(), s.size()); raw_write(STDERR_FILENO, s);
}
}
// status banners (`[ OK ]` / `[FAILED]` / welcome) go to /dev/console and
// are mirrored to the VT screen so a VM with a display shows boot progress;
// the timestamped [INF]/[WRN] log stream (`write_all`) stays console-only.
void write_status(const std::string& s) {
write_all(s);
if (g_fd >= 0) {
open_screen();
if (g_scr_fd >= 0)
raw_write(g_scr_fd, s);
} }
} }
@ -71,9 +116,10 @@ void log_init(const std::string& console_path, LogLevel min_level) {
void log_set_level(LogLevel level) { g_min_level = level; } void log_set_level(LogLevel level) { g_min_level = level; }
void log_msg(LogLevel level, const std::string& tag, const std::string& msg) { void log_msg(LogLevel level, const std::string& tag, const std::string& msg) {
if (level < g_min_level) return; if (level < g_min_level)
std::string line = "[" + timestamp() + "] [" + level_name(level) + "] " + tag + ": " + return;
msg + "\n"; std::string line = "[" + timestamp() + "] [" + level_name(level) + "] " +
tag + ": " + msg + "\n";
std::lock_guard<std::mutex> lk(g_lock); std::lock_guard<std::mutex> lk(g_lock);
if (g_fd < 0 && g_ring_count < g_ring.size()) { if (g_fd < 0 && g_ring_count < g_ring.size()) {
g_ring[g_ring_pos] = line; g_ring[g_ring_pos] = line;
@ -89,4 +135,23 @@ void log_msg(LogLevel level, const std::string& tag, const std::string& msg) {
write_all(line); write_all(line);
} }
void log_status(LogStatus st, const std::string& msg) {
std::string line;
if (st == LogStatus::Banner) {
line = msg + "\n";
} else {
bool tty = g_fd >= 0 && ::isatty(g_fd);
if (st == LogStatus::Ok) {
line = "[" + std::string(tty ? "\x1b[32m" : "") + " OK " +
(tty ? "\x1b[0m" : "") + "] ";
} else {
line = "[" + std::string(tty ? "\x1b[31m" : "") + "FAILED" +
(tty ? "\x1b[0m" : "") + "] ";
}
line += msg + "\n";
}
std::lock_guard<std::mutex> lk(g_lock);
write_status(line);
}
} // namespace bajia } // namespace bajia

View file

@ -10,12 +10,23 @@
namespace bajia { namespace bajia {
enum class LogLevel { Debug = 0, Info = 1, Warn = 2, Err = 3 }; enum class LogLevel { Debug = 0,
Info = 1,
Warn = 2,
Err = 3 };
void log_init(const std::string& console_path = "/dev/console", LogLevel min_level = LogLevel::Info); // high-visibility console status banners in the systemd `[ OK ]` style.
// console-only (not kmsg/ring-buffer), colored when the console is a tty.
enum class LogStatus { Banner,
Ok,
Failed };
void log_init(const std::string& console_path = "/dev/console",
LogLevel min_level = LogLevel::Info);
void log_set_level(LogLevel level); void log_set_level(LogLevel level);
void log_msg(LogLevel level, const std::string& tag, const std::string& msg); void log_msg(LogLevel level, const std::string& tag, const std::string& msg);
void log_status(LogStatus status, const std::string& msg);
template <typename... Args> template <typename... Args>
void log_info(const std::string& tag, Args&&... args) { void log_info(const std::string& tag, Args&&... args) {

View file

@ -5,6 +5,7 @@
// `init=/path/to/bajia`. // `init=/path/to/bajia`.
#include "config.hpp" #include "config.hpp"
#include "logger.hpp" #include "logger.hpp"
#include "selinux_setup.hpp"
#include "supervisor.hpp" #include "supervisor.hpp"
#include <cstdio> #include <cstdio>
@ -37,26 +38,28 @@ int main(int argc, char** argv) {
// PID 1 note: the kernel may pass extra args after the init program name. // PID 1 note: the kernel may pass extra args after the init program name.
for (int i = 1; i < argc; ++i) { for (int i = 1; i < argc; ++i) {
if (argv[i][0] == '-') { if (argv[i][0] == '-') {
if (std::strcmp(argv[i], "-h") == 0 || std::strcmp(argv[i], "--help") == 0) { if (std::strcmp(argv[i], "-h") == 0 ||
std::strcmp(argv[i], "--help") == 0) {
usage(argv[0]); usage(argv[0]);
return 0; return 0;
} }
if (std::strcmp(argv[i], "--run-as-user") == 0) { if (std::strcmp(argv[i], "--run-as-user") == 0) {
run_as_user = true; run_as_user = true;
continue; continue;
} }
usage(argv[0]); usage(argv[0]);
return 2; return 2;
} }
files.emplace_back(argv[i]); files.emplace_back(argv[i]);
} }
if (::getpid() != 1 && !run_as_user) { if (::getpid() != 1 && !run_as_user) {
::fprintf(stderr, ::fprintf(stderr,
"bajia: refusing to run as pid %d (not PID 1). bajia is an " "bajia: refusing to run as pid %d (not PID 1). "
"init system and would re-fire boot triggers, mount " "launch it via the kernel (init=...), or pass --run-as-user "
"filesystems and spawn services on top of a running system. "
"Launch it via the kernel (init=...), or pass --run-as-user "
"for a development run.\n", "for a development run.\n",
::getpid()); ::getpid());
return 1; return 1;
@ -64,9 +67,11 @@ int main(int argc, char** argv) {
if (files.empty()) { if (files.empty()) {
// default: prefer the single init.rc, and also load /etc/bajia.d/*.rc. // default: prefer the single init.rc, and also load /etc/bajia.d/*.rc.
if (::access("/etc/bajia/init.rc", R_OK) == 0) files.emplace_back("/etc/bajia/init.rc"); if (::access("/etc/bajia/init.rc", R_OK) == 0)
files.emplace_back("/etc/bajia/init.rc");
if (files.empty()) { if (files.empty()) {
::fprintf(stderr, "bajia: no config given and /etc/bajia/init.rc not found.\n"); ::fprintf(stderr,
"bajia: no config given and /etc/bajia/init.rc not found.\n");
return 1; return 1;
} }
} }
@ -79,12 +84,19 @@ int main(int argc, char** argv) {
return 1; return 1;
} }
// logger targets the console; falls back to stderr until the console is ready. // logger targets the console; falls back to stderr until the console is
// ready.
log_init("/dev/console", LogLevel::Info); log_init("/dev/console", LogLevel::Info);
log_status(LogStatus::Banner,
std::string("Welcome to bajia ") + kBajiaVersion);
log_info("init", "loaded ", std::to_string(files.size()), " config file(s), ", log_info("init", "loaded ", std::to_string(files.size()), " config file(s), ",
std::to_string(config.services.size()), " services, ", std::to_string(config.services.size()), " services, ",
std::to_string(config.actions.size()), " actions"); std::to_string(config.actions.size()), " actions");
if (::getpid() == 1) {
selinux_setup();
}
Supervisor supervisor(std::move(config)); Supervisor supervisor(std::move(config));
supervisor.run(); // [[noreturn]] supervisor.run(); // [[noreturn]]
} }

88
src/selinux_setup.cpp Normal file
View file

@ -0,0 +1,88 @@
// selinux_setup.cpp - setup SELinux policies (only when built with
// `configure.py --selinux`; otherwise this compiles to an empty TU).
#include "selinux_setup.hpp"
#ifdef BAJIA_SELINUX
#include "logger.hpp"
#include <cerrno>
#include <cstdarg>
#include <cstring>
#include <selinux/restorecon.h>
#include <selinux/selinux.h>
#include <string>
#include <sys/mount.h>
#include <sys/stat.h>
#include <unistd.h>
namespace bajia {
int selinux_log_callback(int type, const char* fmt, ...) {
(void)type;
va_list ap;
va_start(ap, fmt);
va_list copy;
va_copy(copy, ap);
int len = vsnprintf(nullptr, 0, fmt, copy);
va_end(copy);
if (len >= 0) {
std::string msg(static_cast<size_t>(len), '\0');
vsnprintf(msg.data(), msg.size() + 1, fmt, ap);
log_info("selinux", msg.c_str());
}
va_end(ap);
return 0;
}
int selinux_audit_callback(void* auditdata, security_class_t cls, char* msg,
size_t msglen) {
(void)auditdata;
(void)msglen;
log_info("selinux-audit", "class: ", std::to_string(cls), " msg: ", msg);
return 0;
}
void selinux_setup() {
selinux_callback cb{};
cb.func_log = selinux_log_callback;
selinux_set_callback(SELINUX_CB_LOG, cb);
cb.func_audit = selinux_audit_callback;
selinux_set_callback(SELINUX_CB_AUDIT, cb);
// make sure /sys is mounted, then let selinux_init_load_policy() do the
// real work: it mounts selinuxfs, parses /etc/selinux/config + the
// enforcing= cmdline flag and loads the policy. (libselinux's own
// is_selinux_enabled() needs a prior config parse, so we don't gate on it
// here; its return value is authoritative.)
::mkdir("/sys", 0755);
if (::mount("sysfs", "/sys", "sysfs", 0, nullptr) != 0 && errno != EBUSY) {
log_info("selinux", "cannot mount sysfs: ", std::strerror(errno));
return;
}
int enforce = -1;
if (selinux_init_load_policy(&enforce) != 0) {
log_info("selinux",
"policy load failed (no selinuxfs, kernel disabled "
"via selinux=0, and/or no policy in /etc/selinux/<type>/policy)");
return;
}
log_info("selinux",
"policy loaded, enforcing=", std::to_string(enforce == 1));
// relabel the core tree so every path matches the policy's file_contexts.
for (const char* path : {"/dev", "/run", "/etc", "/bin", "/sbin", "/usr"}) {
if (selinux_restorecon(path, SELINUX_RESTORECON_RECURSE) != 0) {
log_info("selinux", "restorecon ", path, ": ", std::strerror(errno));
}
}
log_info("selinux", "setup complete");
}
} // namespace bajia
#endif // BAJIA_SELINUX

29
src/selinux_setup.hpp Normal file
View file

@ -0,0 +1,29 @@
#pragma once
// SELinux integration is optional: it pulls in libselinux, which is only
// useful on a real Android-like system with policies. Compile with
// `configure.py --selinux` to enable; otherwise these are no-ops so the
// embedded/VM builds stay self-contained.
#ifdef BAJIA_SELINUX
#include <selinux/selinux.h>
namespace bajia {
int selinux_log_callback(int type, const char* fmt, ...);
int selinux_audit_callback(void* auditdata, security_class_t cls, char* msg,
size_t msglen);
void selinux_setup();
} // namespace bajia
#else // !BAJIA_SELINUX
namespace bajia {
inline void selinux_setup() {}
} // namespace bajia
#endif // BAJIA_SELINUX

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@
#include <chrono> #include <chrono>
#include <string> #include <string>
#include <unordered_map>
namespace bajia { namespace bajia {
@ -18,7 +19,7 @@ enum class ShutdownKind {
}; };
class Supervisor { class Supervisor {
public: public:
explicit Supervisor(Config config); explicit Supervisor(Config config);
~Supervisor(); ~Supervisor();
@ -35,7 +36,7 @@ public:
// not return normally. // not return normally.
[[noreturn]] void run(); [[noreturn]] void run();
private: private:
// shutdown is a state machine driven from the event loop so that SIGKILL // shutdown is a state machine driven from the event loop so that SIGKILL
// grace periods and child reaping keep working while we wind down. // grace periods and child reaping keep working while we wind down.
enum class ShutdownState { enum class ShutdownState {
@ -49,6 +50,8 @@ private:
Config config_; Config config_;
int epfd_ = -1; int epfd_ = -1;
int sigfd_ = -1; int sigfd_ = -1;
int ctl_fd_ = -1;
std::unordered_map<int, std::string> ctl_clients_;
bool shutdown_requested_ = false; bool shutdown_requested_ = false;
ShutdownKind shutdown_kind_ = ShutdownKind::PowerOff; ShutdownKind shutdown_kind_ = ShutdownKind::PowerOff;
ShutdownState shutdown_state_ = ShutdownState::Running; ShutdownState shutdown_state_ = ShutdownState::Running;
@ -60,7 +63,18 @@ private:
void reap_children(); void reap_children();
void execute_action(Action& action); void execute_action(Action& action);
void run_exec_command(const Command& cmd); void run_exec_command(const Command& cmd);
void run_command(Command& cmd); bool run_command(Command& cmd);
// re-parse the .rc files and reconcile live services: removed services are
// stopped, added ones registered, changed ones restarted. Called from
// SIGHUP (and the `reload` control command).
void reload_config();
// control socket ("@bajia") so userland can drive init: start/stop/restart
// services, fire triggers, query status, request shutdown.
void open_control_socket();
void ctl_accept();
void ctl_handle_client(int fd);
std::string ctl_execute(const std::string& line);
void begin_shutdown(ShutdownKind kind); void begin_shutdown(ShutdownKind kind);
// step the shutdown state machine; returns true when the loop should exit. // step the shutdown state machine; returns true when the loop should exit.

View file

@ -11,12 +11,19 @@ examples:
python3 tools/run_vm.py --fetch-busybox --nographic python3 tools/run_vm.py --fetch-busybox --nographic
python3 tools/run_vm.py --kernel /boot/vmlinuz-$(uname -r) --gdb python3 tools/run_vm.py --kernel /boot/vmlinuz-$(uname -r) --gdb
python3 tools/run_vm.py --config my-init.rc python3 tools/run_vm.py --config my-init.rc
python3 tools/run_vm.py --selinux # boot with the host SELinux policy (permissive)
inside the guest: log in as `root` (passwordless by default, or use inside the guest: log in as `root` (passwordless by default, or use
--root-password), then `kill -TERM 1` -> reboot path, `kill -INT 1` -> --root-password). `bctl status` / `bctl shutdown poweroff` drive the
init over its control socket; `kill -TERM 1` -> reboot path, `kill -INT 1` ->
poweroff path. poweroff path.
bajia is built statically by default: a dynamic binary cannot exec inside the bajia is built statically by default: a dynamic binary cannot exec inside the
initramfs (no libc there). Use --no-static only if you ship the libs too. initramfs (no libc there). Use --no-static only if you ship the libs too.
--selinux flips bajia to a dynamic build (there is no static libselinux on
Fedora), bundles libselinux/libpcre2/glibc + the loader into the initramfs,
and copies the host's /etc/selinux/<type> policy (loaded permissively). This
is the first stage of bootstrapping a real policy: boot, read the `avc:
denied` lines, refine, then flip to enforcing.
""" """
from __future__ import annotations from __future__ import annotations
@ -60,11 +67,11 @@ on shutdown
service console-serial /bin/getty -L ttyS0 115200 vt100 service console-serial /bin/getty -L ttyS0 115200 vt100
console console
respawn always respawn = always
service console-tty1 /bin/getty -L 38400 tty1 vt100 service console-tty1 /bin/getty -L 38400 tty1 vt100
console console
respawn always respawn = always
""" """
# source tarballs of busybox (github.com/mirror/busybox); a pinned tag is # source tarballs of busybox (github.com/mirror/busybox); a pinned tag is
@ -77,18 +84,19 @@ BUSYBOX_URLS = [
BUSYBOX_APPLETS = ["sh", "getty", "mount", "sync", "ls", "cat", "kill", "ps", BUSYBOX_APPLETS = ["sh", "getty", "mount", "sync", "ls", "cat", "kill", "ps",
"poweroff", "reboot", "mkdir", "mknod", "login"] "poweroff", "reboot", "mkdir", "mknod", "login"]
def run(cmd, **kw) -> subprocess.CompletedProcess: def run(cmd, **kw) -> subprocess.CompletedProcess:
print("$", " ".join(str(c) for c in cmd)) print("$", " ".join(str(c) for c in cmd))
return subprocess.run(cmd, **kw) return subprocess.run(cmd, **kw)
def build_bajia(static: bool, selinux: bool = False) -> Path:
def build_bajia(static: bool) -> Path:
env = dict(os.environ) env = dict(os.environ)
cfg = ["python3", "configure.py"]
if selinux:
cfg.append("--selinux")
if static: if static:
print("building bajia (statically linked)...") print("building bajia (statically linked)...")
env["CXX"] = env.get("CXX", "g++") + " -static" env["CXX"] = env.get("CXX", "g++") + " -static"
r = run(["python3", "configure.py"], env=env) r = run(cfg, env=env)
if r.returncode != 0: if r.returncode != 0:
sys.exit("configure.py failed") sys.exit("configure.py failed")
r = run(["ninja", "-C", str(BUILD)]) r = run(["ninja", "-C", str(BUILD)])
@ -96,6 +104,64 @@ def build_bajia(static: bool) -> Path:
sys.exit("ninja build failed") sys.exit("ninja build failed")
return BAJIA return BAJIA
SELINUX_CONFIG = Path("/etc/selinux/config")
def host_selinux_type() -> str:
if not SELINUX_CONFIG.is_file():
return "targeted"
for line in SELINUX_CONFIG.read_text().splitlines():
line = line.strip()
if line.startswith("SELINUXTYPE="):
return line.split("=", 1)[1].strip().strip('"')
return "targeted"
def selinux_policy_files() -> list[tuple[Path, str]]:
"""Return (host_path, initramfs_relative_path) pairs for the policy payload."""
typ = host_selinux_type()
base = Path("/etc/selinux") / typ
pols = sorted((base / "policy").glob("policy.*"))
if not pols:
sys.exit(f"--selinux: no policy under {base / 'policy'}/ "
"(install selinux-policy-targeted, a Fedora SELinux host is assumed)")
# libselinux 3.x looks for the restorecon table at contexts/files/file_contexts
# (older versions used contexts/file_contexts); bundle whichever exists.
fc = next((p for p in (base / "contexts" / "files" / "file_contexts",
base / "contexts" / "file_contexts") if p.is_file()), None)
if not fc:
sys.exit(f"--selinux: missing file_contexts under {base / 'contexts'}/")
fc_rel = "etc/selinux/" + typ + "/" + str(fc.relative_to(base))
prefix = f"etc/selinux/{typ}/"
return [(pols[-1], prefix + f"policy/{pols[-1].name}"),
(fc, fc_rel)]
def bundle_dynamic_libs(init: Path, root: Path) -> None:
"""Copy the dynamic loader + resolved .so deps into the initramfs,
mirroring their absolute paths so the interpreter finds them."""
out = run(["ldd", str(init)], capture_output=True, text=True)
if out.returncode != 0:
sys.exit("ldd failed on " + str(init))
libs: list[str] = []
for line in out.stdout.splitlines():
line = line.strip()
if "=>" in line:
path = line.split("=>", 1)[1].strip().split(" ", 1)[0].strip()
else: # "linux-vdso" or the loader line "/lib64/ld-linux-x86-64.so.2 (0x...)"
path = line.split(" ", 1)[0].strip()
if path.startswith("/") and Path(path).is_file():
libs.append(path)
seen: set[str] = set()
for lib in libs: # preserve first-seen order
if lib in seen:
continue
seen.add(lib)
dst = root / lib.lstrip("/")
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(lib, dst)
dst.chmod(0o755)
print("bundled dynamic libs:", ", ".join(seen))
if not seen:
sys.exit("ldd reported no libraries - unexpected for a dynamic binary")
def find_kernel() -> Path | None: def find_kernel() -> Path | None:
p = Path("/boot/vmlinuz-" + os.uname().release) p = Path("/boot/vmlinuz-" + os.uname().release)
if p.is_file(): if p.is_file():
@ -194,8 +260,20 @@ def root_passwd_line(password: str | None) -> str:
field = crypt_password(password) if password else "" field = crypt_password(password) if password else ""
return f"root:{field}:0:0:root:/:/bin/sh\n" return f"root:{field}:0:0:root:/:/bin/sh\n"
# appended to the test init.rc; started on boot, prints the exec context of a
# seclabel'd service and of init itself, then exits.
SELINUX_RC_PROBE = """
service selinux-probe /bin/sh -c "echo probe-ctx=$(cat /proc/self/attr/current) init-ctx=$(cat /proc/1/attr/current)"
console
seclabel = system_u:system_r:init_t:s0
respawn = never
on boot
start selinux-probe
"""
def build_initramfs(init: Path, busybox: Path, rc_text: str, root_password: str | None, def build_initramfs(init: Path, busybox: Path, rc_text: str, root_password: str | None,
keep: bool) -> Path: selinux: bool, keep: bool) -> Path:
if not shutil.which("cpio"): if not shutil.which("cpio"):
sys.exit("cpio not found (install cpio)") sys.exit("cpio not found (install cpio)")
root = Path(tempfile.mkdtemp(prefix="bajia-root-")) root = Path(tempfile.mkdtemp(prefix="bajia-root-"))
@ -204,19 +282,52 @@ def build_initramfs(init: Path, busybox: Path, rc_text: str, root_password: str
"dev", "proc", "sys", "run", "tmp"): "dev", "proc", "sys", "run", "tmp"):
(root / sub).mkdir(parents=True) (root / sub).mkdir(parents=True)
if selinux:
for src, rel in selinux_policy_files():
dst = root / rel
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(src, dst)
selcfg = root / "etc" / "selinux" / "config"
selcfg.write_text(f"SELINUX=permissive\nSELINUXTYPE={host_selinux_type()}\n")
bundle_dynamic_libs(init, root)
elif not is_static(init):
print("warning: bajia is dynamically linked; /init will fail to exec "
"inside the initramfs (error -2). Rebuild with --no-static "
"unset (static is the default) or drop --no-build.")
if selinux:
rc_text = rc_text + SELINUX_RC_PROBE
shutil.copy(init, root / "init") shutil.copy(init, root / "init")
(root / "init").chmod(0o755) (root / "init").chmod(0o755)
ctl = BUILD / "bctl"
if ctl.is_file():
shutil.copy(ctl, root / "bin" / "bctl")
(root / "bin" / "bctl").chmod(0o755)
shutil.copy(busybox, root / "bin" / "busybox") shutil.copy(busybox, root / "bin" / "busybox")
(root / "bin" / "busybox").chmod(0o755) (root / "bin" / "busybox").chmod(0o755)
for applet in BUSYBOX_APPLETS: for applet in BUSYBOX_APPLETS:
(root / "bin" / applet).symlink_to("busybox") (root / "bin" / applet).symlink_to("busybox")
(root / "etc" / "bajia" / "init.rc").write_text(rc_text) (root / "etc" / "bajia" / "init.rc").write_text(rc_text)
(root / "etc" / "passwd").write_text(root_passwd_line(root_password)) (root / "etc" / "passwd").write_text(
root_passwd_line(root_password) +
"nobody:x:65534:65534:nobody:/:/bin/sh\n")
(root / "etc" / "group").write_text(
"root:x:0:\n"
"nobody:x:65534:\n"
"daemon:x:1:\n")
# The staging tree is owned by the host user and mkdtemp makes the
# top dir 0700; GNU cpio preserves both, so without this the guest's
# "/" would be mode 0700 owned by uid 1000 -- fine for root services,
# but dropped-privilege services couldn't traverse it. Stamp owner
# root:root (no host chown needed) and make the root traversable.
p = run(["bash", "-c", p = run(["bash", "-c",
"cd \"$1\" && find . -print0 | cpio --null -o -H newc", "cd \"$1\" && chmod 0755 . && "
"find . -print0 | cpio --null -o -H newc --owner=0:0",
"bajia-initramfs", str(root)], stdout=subprocess.PIPE) "bajia-initramfs", str(root)], stdout=subprocess.PIPE)
if p.returncode != 0: if p.returncode != 0:
sys.exit("cpio packing failed") sys.exit("cpio packing failed")
@ -235,6 +346,9 @@ def qemu_command(kernel: Path, initrd: Path, args: argparse.Namespace) -> list[s
display = args.display display = args.display
if display is None: if display is None:
display = "gtk" if os.environ.get("DISPLAY") else "none" display = "gtk" if os.environ.get("DISPLAY") else "none"
append = (f"console=tty1 console=ttyS0 rdinit=/init loglevel={args.loglevel}")
if args.selinux:
append += " selinux=1 enforcing=0"
cmd = [ cmd = [
qemu, qemu,
"-M", args.machine, "-M", args.machine,
@ -242,13 +356,15 @@ def qemu_command(kernel: Path, initrd: Path, args: argparse.Namespace) -> list[s
"-smp", str(args.smp), "-smp", str(args.smp),
"-kernel", str(kernel), "-kernel", str(kernel),
"-initrd", str(initrd), "-initrd", str(initrd),
"-append", "-append", append,
f"console=tty1 console=ttyS0 rdinit=/init loglevel={args.loglevel}",
"-display", display, "-display", display,
"-serial", "stdio", "-serial", "stdio",
] ]
if args.nographic: if args.nographic:
cmd[cmd.index("-display") + 1] = "none" cmd[cmd.index("-display") + 1] = "none"
if args.serial_log:
cmd[cmd.index("-display") + 1] = "none"
cmd[cmd.index("-serial") + 1] = f"file:{args.serial_log}"
if args.gdb or args.wait_gdb: if args.gdb or args.wait_gdb:
cmd += ["-gdb", "tcp::1234", "-S"] if args.wait_gdb else ["-s"] cmd += ["-gdb", "tcp::1234", "-S"] if args.wait_gdb else ["-s"]
cmd += ["-no-reboot", "-no-shutdown"] cmd += ["-no-reboot", "-no-shutdown"]
@ -287,12 +403,17 @@ def main() -> int:
help="pause the machine until a gdb client attaches") help="pause the machine until a gdb client attaches")
ap.add_argument("--keep-initramfs", action="store_true", ap.add_argument("--keep-initramfs", action="store_true",
help="don't delete the initramfs staging tree") help="don't delete the initramfs staging tree")
ap.add_argument("--selinux", action="store_true",
help="bundle the host SELinux policy + libs, boot permissive")
ap.add_argument("--serial-log", type=Path,
help="write the serial console to this file (forces -display none)")
args = ap.parse_args() args = ap.parse_args()
init = BAJIA if args.no_build else build_bajia(not args.no_static) static = not args.no_static and not args.selinux
init = BAJIA if args.no_build else build_bajia(static, args.selinux)
if not init.is_file(): if not init.is_file():
sys.exit(f"bajia not built at {init} (drop --no-build)") sys.exit(f"bajia not built at {init} (drop --no-build)")
if not is_static(init): if not static and not args.selinux and not is_static(init):
print("warning: bajia is dynamically linked; /init will fail to exec " print("warning: bajia is dynamically linked; /init will fail to exec "
"inside the initramfs (error -2). Rebuild with --no-static " "inside the initramfs (error -2). Rebuild with --no-static "
"unset (static is the default) or drop --no-build.") "unset (static is the default) or drop --no-build.")
@ -315,7 +436,7 @@ def main() -> int:
rc_text = args.config.read_text() if args.config else DEFAULT_RC rc_text = args.config.read_text() if args.config else DEFAULT_RC
initrd = build_initramfs(init, busybox, rc_text, args.root_password, initrd = build_initramfs(init, busybox, rc_text, args.root_password,
keep=args.keep_initramfs) selinux=args.selinux, keep=args.keep_initramfs)
print("initramfs:", initrd, f"({initrd.stat().st_size / 1024:.0f} KB)") print("initramfs:", initrd, f"({initrd.stat().st_size / 1024:.0f} KB)")
cmd = qemu_command(kernel, initrd, args) cmd = qemu_command(kernel, initrd, args)