more things
This commit is contained in:
parent
1cd4394841
commit
6e60709283
15 changed files with 1403 additions and 294 deletions
6
.clang-format
Normal file
6
.clang-format
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
BasedOnStyle: LLVM
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 0
|
||||
SortIncludes: false
|
||||
PointerAlignment: Left
|
||||
ReflowComments: false
|
||||
92
README.md
92
README.md
|
|
@ -15,14 +15,13 @@ format.
|
|||
|
||||
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
|
||||
- `SIGCHLD` crash-window limiting (rate-limited restarts)
|
||||
- property triggers (`property:<k>=<v>`) and `setprop`/`getprop`
|
||||
- per-service logging to files
|
||||
- `reboot`/`poweroff` path with ordered unmount
|
||||
- `SIGHUP` config reload
|
||||
- SELinux
|
||||
- readiness/socket activation
|
||||
|
||||
## building
|
||||
|
||||
|
|
@ -62,17 +61,18 @@ See [`etc/init.rc`](etc/init.rc) for a complete example.
|
|||
|
||||
```rc
|
||||
service NAME /path/to/exe [args...]
|
||||
user root|other # privilege level (drop, planned)
|
||||
group GROUP [GROUP...]
|
||||
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
|
||||
setenv K=V # extra environment (repeatable)
|
||||
cwd /path
|
||||
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
|
||||
```
|
||||
|
||||
### actions
|
||||
|
|
@ -91,6 +91,62 @@ on TRIGGER
|
|||
log message
|
||||
```
|
||||
|
||||
boot triggers fire in order: `early-init`, `init`, `boot`. `shutdown` is
|
||||
reserved (planned wiring to the signal path). property/`service-*` triggers
|
||||
are on the roadmap.
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
66
configure.py
66
configure.py
|
|
@ -5,9 +5,13 @@ usage:
|
|||
python3 configure.py
|
||||
python3 configure.py --asan # enable address/undefined sanitizers
|
||||
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 json
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import sys
|
||||
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()
|
||||
objs = ["obj/" + Path(s).stem + ".o" for s in sources]
|
||||
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\n"
|
||||
|
|
@ -77,22 +90,49 @@ def emit_ninja(cxx, cxxflags, dst):
|
|||
lines.append(rule_cxx)
|
||||
lines.append(rule_link)
|
||||
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("")
|
||||
for o, s in zip(objs, sources):
|
||||
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('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("")
|
||||
|
||||
if write_cc:
|
||||
write_compile_commands(cxx, cxxflags, cc_entries)
|
||||
|
||||
(BUILD / "obj").mkdir(exist_ok=True)
|
||||
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():
|
||||
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("--debug", action="store_true", help="disable -O2, enable -O0")
|
||||
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()
|
||||
|
||||
def main():
|
||||
|
|
@ -106,6 +146,20 @@ def main():
|
|||
print("error: ninja not found in PATH", file=sys.stderr)
|
||||
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)
|
||||
|
||||
cxxflags = list(CXXFLAGS)
|
||||
|
|
@ -117,14 +171,20 @@ def main():
|
|||
if args.asan:
|
||||
cxxflags += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
|
||||
linkflags += ["-fsanitize=address,undefined"]
|
||||
if args.selinux:
|
||||
cxxflags += ["-DBAJIA_SELINUX"]
|
||||
linkflags += ["-lselinux"]
|
||||
|
||||
cxxflags = [f for f in cxxflags if f not in WARNINGS]
|
||||
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()
|
||||
stamp_file.write_text(json.dumps(stamp, indent=1) + "\n", encoding="utf-8")
|
||||
|
||||
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")
|
||||
return 0
|
||||
|
||||
|
|
|
|||
14
etc/init.rc
14
etc/init.rc
|
|
@ -2,7 +2,9 @@
|
|||
#
|
||||
# constructs:
|
||||
# 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
|
||||
# start NAME | stop NAME | restart NAME
|
||||
|
|
@ -39,16 +41,16 @@ on boot
|
|||
|
||||
# the console service: bind stdio to /dev/console, always respawn.
|
||||
service console /sbin/getty -L ttyS0 115200 vt100
|
||||
class core
|
||||
class = core
|
||||
console
|
||||
user root
|
||||
user = root
|
||||
|
||||
# a long-lived example daemon. respawning is the default (always).
|
||||
service watchdog /usr/sbin/watchdog
|
||||
class core
|
||||
respawn always
|
||||
class = core
|
||||
respawn = always
|
||||
|
||||
# a one-shot job: runs once, exits, never respawns.
|
||||
service boot-logo /usr/bin/show-boot-logo
|
||||
class late
|
||||
class = late
|
||||
oneshot
|
||||
|
|
|
|||
93
src/bctl_main.cpp
Normal file
93
src/bctl_main.cpp
Normal 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;
|
||||
}
|
||||
116
src/config.cpp
116
src/config.cpp
|
|
@ -12,7 +12,7 @@ namespace {
|
|||
std::vector<std::string> tokenize(const std::string& line) {
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
|
||||
|
||||
bool in_q = false;
|
||||
bool need_quote_close = false;
|
||||
|
||||
|
|
@ -48,7 +48,8 @@ std::vector<std::string> tokenize(const std::string& line) {
|
|||
}
|
||||
if (in_q) {
|
||||
// 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()) {
|
||||
out.push_back(cur);
|
||||
}
|
||||
|
|
@ -57,8 +58,10 @@ std::vector<std::string> tokenize(const std::string& line) {
|
|||
}
|
||||
|
||||
RespawnPolicy parse_respawn(const std::string& s) {
|
||||
if (s == "always") return RespawnPolicy::Always;
|
||||
if (s == "on-failure") return RespawnPolicy::OnFailure;
|
||||
if (s == "always")
|
||||
return RespawnPolicy::Always;
|
||||
if (s == "on-failure")
|
||||
return RespawnPolicy::OnFailure;
|
||||
return RespawnPolicy::Never;
|
||||
}
|
||||
|
||||
|
|
@ -66,14 +69,16 @@ RespawnPolicy parse_respawn(const std::string& s) {
|
|||
|
||||
Service* Config::find_service(const std::string& name) {
|
||||
for (auto& s : services) {
|
||||
if (s.name == name) return &s;
|
||||
if (s.name == name)
|
||||
return &s;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const Service* Config::find_service(const std::string& name) const {
|
||||
for (auto& s : services) {
|
||||
if (s.name == name) return &s;
|
||||
if (s.name == name)
|
||||
return &s;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
|
@ -81,10 +86,11 @@ const Service* Config::find_service(const std::string& name) const {
|
|||
// public entry point
|
||||
Config parse_config(const std::vector<std::string>& files) {
|
||||
Config cfg;
|
||||
cfg.sources = files;
|
||||
int line = 0;
|
||||
std::string section_kind; // "service" or "action"
|
||||
Service* cur_svc = nullptr; // service being configured
|
||||
Action* cur_act = nullptr; // action being configured
|
||||
std::string section_kind; // "service" or "action"
|
||||
Service* cur_svc = nullptr; // service being configured
|
||||
Action* cur_act = nullptr; // action being configured
|
||||
|
||||
for (const auto& file : files) {
|
||||
std::ifstream in(file);
|
||||
|
|
@ -100,7 +106,8 @@ Config parse_config(const std::vector<std::string>& files) {
|
|||
while (std::getline(in, raw)) {
|
||||
++line;
|
||||
auto toks = tokenize(raw);
|
||||
if (toks.empty()) continue;
|
||||
if (toks.empty())
|
||||
continue;
|
||||
|
||||
std::string first = toks[0];
|
||||
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).
|
||||
// value-taking keywords require `keyword = value` syntax.
|
||||
if (section_kind == "service" && cur_svc) {
|
||||
if (first == "user" && toks.size() >= 2) cur_svc->uid = toks[1];
|
||||
else if (first == "group" && toks.size() >= 2) {
|
||||
cur_svc->gid = toks[1];
|
||||
for (size_t i = 2; i < toks.size(); ++i) cur_svc->groups.push_back(toks[i]);
|
||||
if (first == "oneshot")
|
||||
cur_svc->oneshot = true;
|
||||
else if (first == "disabled")
|
||||
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;
|
||||
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];
|
||||
// unknown option keys are ignored.
|
||||
continue;
|
||||
}
|
||||
|
||||
// action command (must be inside an action section).
|
||||
if (section_kind == "action" && cur_act) {
|
||||
Command cmd;
|
||||
if (first == "start" && toks.size() >= 2) cmd.kind = Command::Kind::Start;
|
||||
else if (first == "stop" && toks.size() >= 2) cmd.kind = Command::Kind::Stop;
|
||||
else if (first == "restart" && toks.size() >= 2) cmd.kind = Command::Kind::Restart;
|
||||
else if (first == "exec") cmd.kind = Command::Kind::Exec;
|
||||
else if (first == "mkdir") cmd.kind = Command::Kind::Mkdir;
|
||||
else if (first == "chmod") 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;
|
||||
if (first == "start" && toks.size() >= 2)
|
||||
cmd.kind = Command::Kind::Start;
|
||||
else if (first == "stop" && toks.size() >= 2)
|
||||
cmd.kind = Command::Kind::Stop;
|
||||
else if (first == "restart" && toks.size() >= 2)
|
||||
cmd.kind = Command::Kind::Restart;
|
||||
else if (first == "exec")
|
||||
cmd.kind = Command::Kind::Exec;
|
||||
else if (first == "mkdir")
|
||||
cmd.kind = Command::Kind::Mkdir;
|
||||
else if (first == "chmod")
|
||||
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 {
|
||||
throw std::runtime_error(file + ":" + std::to_string(line) +
|
||||
": unknown action command '" + first + "'");
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@
|
|||
// Two top-level constructs:
|
||||
//
|
||||
// service NAME /path/to/exec args...
|
||||
// user root
|
||||
// group root
|
||||
// user = root # uid after the privilege drop
|
||||
// group = root [g...] # primary gid + supplementary groups
|
||||
// oneshot
|
||||
// disabled
|
||||
// class main
|
||||
// respawn never|on-failure|always
|
||||
// class = main
|
||||
// respawn = never|on-failure|always
|
||||
// seclabel = CONTEXT
|
||||
// console
|
||||
//
|
||||
// on TRIGGER
|
||||
|
|
@ -30,6 +31,11 @@
|
|||
|
||||
namespace bajia {
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// version
|
||||
// --------------------------------------------------------------------------
|
||||
constexpr const char* kBajiaVersion = "0.1";
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Service
|
||||
// --------------------------------------------------------------------------
|
||||
|
|
@ -41,19 +47,24 @@ enum class RespawnPolicy {
|
|||
|
||||
struct Service {
|
||||
std::string name;
|
||||
std::vector<std::string> args; // executable path + arguments
|
||||
std::vector<std::string> args; // executable path + arguments
|
||||
std::string cwd = "/";
|
||||
std::string uid = "root"; // resolved in supervisor
|
||||
std::string uid = "root"; // resolved in supervisor
|
||||
std::string gid = "root";
|
||||
std::vector<std::string> groups;
|
||||
bool oneshot = false; // run once, don't keep alive
|
||||
bool disabled = false; // not started automatically
|
||||
bool console = false; // bind stdio to the console
|
||||
bool oneshot = false; // run once, don't keep alive
|
||||
bool disabled = false; // not started automatically
|
||||
bool console = false; // bind stdio to the console
|
||||
std::string service_class = "default";
|
||||
RespawnPolicy respawn = RespawnPolicy::Always;
|
||||
int crash_threshold = 4; // max restarts within window
|
||||
int crash_window_secs = 30; // before giving up
|
||||
std::vector<std::string> env; // "K=V" pairs
|
||||
int crash_threshold = 4; // max restarts within window
|
||||
int crash_window_secs = 30; // before giving up
|
||||
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
|
||||
int pid = 0;
|
||||
|
|
@ -69,7 +80,7 @@ struct Command {
|
|||
Start,
|
||||
Stop,
|
||||
Restart,
|
||||
Exec, // run a synchronous command to completion
|
||||
Exec, // run a synchronous command to completion
|
||||
Mkdir,
|
||||
Chmod,
|
||||
Chown,
|
||||
|
|
@ -80,11 +91,11 @@ struct Command {
|
|||
Log,
|
||||
};
|
||||
Kind kind;
|
||||
std::vector<std::string> args; // command-specific arguments
|
||||
std::vector<std::string> args; // command-specific arguments
|
||||
};
|
||||
|
||||
struct Action {
|
||||
std::string trigger; // e.g. "boot", "early-init"
|
||||
std::string trigger; // e.g. "boot", "early-init"
|
||||
std::vector<Command> commands;
|
||||
};
|
||||
|
||||
|
|
@ -95,9 +106,11 @@ struct Config {
|
|||
std::vector<Service> services;
|
||||
std::vector<Action> actions;
|
||||
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);
|
||||
const Service* find_service(const std::string& name) const;
|
||||
Service* find_service(const std::string& name);
|
||||
const Service* find_service(const std::string& name) const;
|
||||
};
|
||||
|
||||
// Parse a set of .rc files into a Config. Throws std::runtime_error on
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <ctime>
|
||||
#include <fcntl.h>
|
||||
#include <mutex>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace bajia {
|
||||
|
|
@ -14,31 +15,75 @@ namespace {
|
|||
|
||||
std::mutex g_lock;
|
||||
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;
|
||||
size_t g_ring_pos = 0;
|
||||
size_t g_ring_count = 0;
|
||||
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) {
|
||||
switch (l) {
|
||||
case LogLevel::Debug: return "DBG";
|
||||
case LogLevel::Info: return "INF";
|
||||
case LogLevel::Warn: return "WRN";
|
||||
case LogLevel::Err: return "ERR";
|
||||
case LogLevel::Debug:
|
||||
return "DBG";
|
||||
case LogLevel::Info:
|
||||
return "INF";
|
||||
case LogLevel::Warn:
|
||||
return "WRN";
|
||||
case LogLevel::Err:
|
||||
return "ERR";
|
||||
}
|
||||
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) {
|
||||
if (g_fd >= 0) {
|
||||
size_t off = 0;
|
||||
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);
|
||||
}
|
||||
raw_write(g_fd, s);
|
||||
} 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_msg(LogLevel level, const std::string& tag, const std::string& msg) {
|
||||
if (level < g_min_level) return;
|
||||
std::string line = "[" + timestamp() + "] [" + level_name(level) + "] " + tag + ": " +
|
||||
msg + "\n";
|
||||
if (level < g_min_level)
|
||||
return;
|
||||
std::string line = "[" + timestamp() + "] [" + level_name(level) + "] " +
|
||||
tag + ": " + msg + "\n";
|
||||
std::lock_guard<std::mutex> lk(g_lock);
|
||||
if (g_fd < 0 && g_ring_count < g_ring.size()) {
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -10,12 +10,23 @@
|
|||
|
||||
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_msg(LogLevel level, const std::string& tag, const std::string& msg);
|
||||
void log_status(LogStatus status, const std::string& msg);
|
||||
|
||||
template <typename... Args>
|
||||
void log_info(const std::string& tag, Args&&... args) {
|
||||
|
|
|
|||
28
src/main.cpp
28
src/main.cpp
|
|
@ -5,6 +5,7 @@
|
|||
// `init=/path/to/bajia`.
|
||||
#include "config.hpp"
|
||||
#include "logger.hpp"
|
||||
#include "selinux_setup.hpp"
|
||||
#include "supervisor.hpp"
|
||||
|
||||
#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.
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
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]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (std::strcmp(argv[i], "--run-as-user") == 0) {
|
||||
run_as_user = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
files.emplace_back(argv[i]);
|
||||
}
|
||||
|
||||
if (::getpid() != 1 && !run_as_user) {
|
||||
::fprintf(stderr,
|
||||
"bajia: refusing to run as pid %d (not PID 1). bajia is an "
|
||||
"init system and would re-fire boot triggers, mount "
|
||||
"filesystems and spawn services on top of a running system. "
|
||||
"Launch it via the kernel (init=...), or pass --run-as-user "
|
||||
"bajia: refusing to run as pid %d (not PID 1). "
|
||||
"launch it via the kernel (init=...), or pass --run-as-user "
|
||||
"for a development run.\n",
|
||||
::getpid());
|
||||
return 1;
|
||||
|
|
@ -64,9 +67,11 @@ int main(int argc, char** argv) {
|
|||
|
||||
if (files.empty()) {
|
||||
// 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()) {
|
||||
::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;
|
||||
}
|
||||
}
|
||||
|
|
@ -79,12 +84,19 @@ int main(int argc, char** argv) {
|
|||
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_status(LogStatus::Banner,
|
||||
std::string("Welcome to bajia ") + kBajiaVersion);
|
||||
log_info("init", "loaded ", std::to_string(files.size()), " config file(s), ",
|
||||
std::to_string(config.services.size()), " services, ",
|
||||
std::to_string(config.actions.size()), " actions");
|
||||
|
||||
if (::getpid() == 1) {
|
||||
selinux_setup();
|
||||
}
|
||||
|
||||
Supervisor supervisor(std::move(config));
|
||||
supervisor.run(); // [[noreturn]]
|
||||
}
|
||||
|
|
|
|||
88
src/selinux_setup.cpp
Normal file
88
src/selinux_setup.cpp
Normal 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
29
src/selinux_setup.hpp
Normal 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
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace bajia {
|
||||
|
||||
|
|
@ -18,7 +19,7 @@ enum class ShutdownKind {
|
|||
};
|
||||
|
||||
class Supervisor {
|
||||
public:
|
||||
public:
|
||||
explicit Supervisor(Config config);
|
||||
~Supervisor();
|
||||
|
||||
|
|
@ -35,7 +36,7 @@ public:
|
|||
// not return normally.
|
||||
[[noreturn]] void run();
|
||||
|
||||
private:
|
||||
private:
|
||||
// shutdown is a state machine driven from the event loop so that SIGKILL
|
||||
// grace periods and child reaping keep working while we wind down.
|
||||
enum class ShutdownState {
|
||||
|
|
@ -49,6 +50,8 @@ private:
|
|||
Config config_;
|
||||
int epfd_ = -1;
|
||||
int sigfd_ = -1;
|
||||
int ctl_fd_ = -1;
|
||||
std::unordered_map<int, std::string> ctl_clients_;
|
||||
bool shutdown_requested_ = false;
|
||||
ShutdownKind shutdown_kind_ = ShutdownKind::PowerOff;
|
||||
ShutdownState shutdown_state_ = ShutdownState::Running;
|
||||
|
|
@ -60,7 +63,18 @@ private:
|
|||
void reap_children();
|
||||
void execute_action(Action& action);
|
||||
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);
|
||||
// step the shutdown state machine; returns true when the loop should exit.
|
||||
|
|
|
|||
151
tools/run_vm.py
151
tools/run_vm.py
|
|
@ -11,12 +11,19 @@ examples:
|
|||
python3 tools/run_vm.py --fetch-busybox --nographic
|
||||
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 --selinux # boot with the host SELinux policy (permissive)
|
||||
|
||||
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.
|
||||
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.
|
||||
--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
|
||||
|
||||
|
|
@ -60,11 +67,11 @@ on shutdown
|
|||
|
||||
service console-serial /bin/getty -L ttyS0 115200 vt100
|
||||
console
|
||||
respawn always
|
||||
respawn = always
|
||||
|
||||
service console-tty1 /bin/getty -L 38400 tty1 vt100
|
||||
console
|
||||
respawn always
|
||||
respawn = always
|
||||
"""
|
||||
|
||||
# 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",
|
||||
"poweroff", "reboot", "mkdir", "mknod", "login"]
|
||||
|
||||
|
||||
def run(cmd, **kw) -> subprocess.CompletedProcess:
|
||||
print("$", " ".join(str(c) for c in cmd))
|
||||
return subprocess.run(cmd, **kw)
|
||||
|
||||
|
||||
def build_bajia(static: bool) -> Path:
|
||||
def build_bajia(static: bool, selinux: bool = False) -> Path:
|
||||
env = dict(os.environ)
|
||||
cfg = ["python3", "configure.py"]
|
||||
if selinux:
|
||||
cfg.append("--selinux")
|
||||
if static:
|
||||
print("building bajia (statically linked)...")
|
||||
env["CXX"] = env.get("CXX", "g++") + " -static"
|
||||
r = run(["python3", "configure.py"], env=env)
|
||||
r = run(cfg, env=env)
|
||||
if r.returncode != 0:
|
||||
sys.exit("configure.py failed")
|
||||
r = run(["ninja", "-C", str(BUILD)])
|
||||
|
|
@ -96,6 +104,64 @@ def build_bajia(static: bool) -> Path:
|
|||
sys.exit("ninja build failed")
|
||||
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:
|
||||
p = Path("/boot/vmlinuz-" + os.uname().release)
|
||||
if p.is_file():
|
||||
|
|
@ -194,8 +260,20 @@ def root_passwd_line(password: str | None) -> str:
|
|||
field = crypt_password(password) if password else ""
|
||||
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,
|
||||
keep: bool) -> Path:
|
||||
selinux: bool, keep: bool) -> Path:
|
||||
if not shutil.which("cpio"):
|
||||
sys.exit("cpio not found (install cpio)")
|
||||
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"):
|
||||
(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")
|
||||
(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")
|
||||
(root / "bin" / "busybox").chmod(0o755)
|
||||
for applet in BUSYBOX_APPLETS:
|
||||
(root / "bin" / applet).symlink_to("busybox")
|
||||
|
||||
(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",
|
||||
"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)
|
||||
if p.returncode != 0:
|
||||
sys.exit("cpio packing failed")
|
||||
|
|
@ -235,6 +346,9 @@ def qemu_command(kernel: Path, initrd: Path, args: argparse.Namespace) -> list[s
|
|||
display = args.display
|
||||
if display is 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 = [
|
||||
qemu,
|
||||
"-M", args.machine,
|
||||
|
|
@ -242,13 +356,15 @@ def qemu_command(kernel: Path, initrd: Path, args: argparse.Namespace) -> list[s
|
|||
"-smp", str(args.smp),
|
||||
"-kernel", str(kernel),
|
||||
"-initrd", str(initrd),
|
||||
"-append",
|
||||
f"console=tty1 console=ttyS0 rdinit=/init loglevel={args.loglevel}",
|
||||
"-append", append,
|
||||
"-display", display,
|
||||
"-serial", "stdio",
|
||||
]
|
||||
if args.nographic:
|
||||
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:
|
||||
cmd += ["-gdb", "tcp::1234", "-S"] if args.wait_gdb else ["-s"]
|
||||
cmd += ["-no-reboot", "-no-shutdown"]
|
||||
|
|
@ -287,12 +403,17 @@ def main() -> int:
|
|||
help="pause the machine until a gdb client attaches")
|
||||
ap.add_argument("--keep-initramfs", action="store_true",
|
||||
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()
|
||||
|
||||
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():
|
||||
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 "
|
||||
"inside the initramfs (error -2). Rebuild with --no-static "
|
||||
"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
|
||||
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)")
|
||||
|
||||
cmd = qemu_command(kernel, initrd, args)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue