Vigil Init System
Technical documentation for Vigil, the PID 1 init system and service supervisor in Aegis OS
Vigil Init System
Vigil is the PID 1 process for Aegis. It serves as both the init system and a lightweight service supervisor, responsible for scanning service declarations at boot, starting and monitoring supervised processes, handling IPC commands from vigictl, and performing graceful shutdown with filesystem sync.
Vigil is implemented in a single C file (user/bin/vigil/main.c) with no external library dependencies beyond the Aegis libc. Like all of Aegis’s userspace, Vigil is v1 software – functional and tested, but not yet hardened against adversarial input or privilege escalation. As a C program running as PID 1 with full system privileges, it represents a high-value attack surface that has not undergone formal security audit. The planned gradual migration from C to Rust (which has already begun in the kernel’s capability system) will eventually reach the userspace service layer. Contributions are welcome – file issues or propose changes at exec/aegis.
Architecture Overview
+-----------+
| vigil | PID 1
| (init) |
+-----+-----+
|
+---------------+---------------+
| | |
scan_services main event loop shutdown_all
| | |
/etc/vigil/services/ waitpid() SIGTERM children
| SIGUSR1 IPC 5s grace period
load_service() process_cmd() SIGKILL stragglers
| sync()
start_service() sys_reboot(0)
Service Declaration Format
Each service is defined by a directory under /etc/vigil/services/:
/etc/vigil/services/<name>/
run -- command string (absolute path or shell command)
policy -- "respawn" or "oneshot"; optional: max_restarts=N
mode -- boot mode filter: "graphical", "text", or absent (always)
All files are plain text, newline-terminated. Vigil reads them with a simple read_file() helper that strips trailing whitespace. The shipped rootfs also includes user files under each service directory (all currently containing root) as placeholders for future per-user execution – Vigil does not parse them yet, and all services run as root.
Policy Types
| Policy | Behavior |
|---|---|
respawn |
Vigil restarts the service when it exits, up to max_restarts (default 5) |
oneshot |
Vigil starts the service once and does not restart it on exit |
When a respawn service exceeds its restart limit, Vigil deactivates it and logs a message.
Boot Mode Filtering
Vigil reads the kernel command line from /proc/cmdline to determine the boot mode:
char cmdline[256] = "";
read_file("/proc/cmdline", cmdline, sizeof(cmdline));
if (strstr(cmdline, "boot=graphical"))
memcpy(s_boot_mode, "graphical", 10);
else if (strstr(cmdline, "boot=text"))
memcpy(s_boot_mode, "text", 5);
/* else keep default "text" */
Services with a mode file are only started when the boot mode matches. This enables mutually exclusive service configurations:
- getty (
mode: text) – runs/bin/loginfor text-mode console authentication - bastion (
mode: graphical) – runs/bin/bastionfor graphical display manager login
Services without a mode file start regardless of boot mode (e.g., dhcp, httpd, chronos).
Quiet Mode
If the kernel command line contains quiet, Vigil suppresses its startup log messages. This prevents service chatter from interfering with serial console test pattern matching in the CI harness.
In quiet mode, stdout is also closed for non-interactive services (those whose run_cmd is not /bin/login or /bin/bastion), keeping the serial stream clean:
if (s_quiet &&
strcmp(s->run_cmd, "/bin/login") != 0 &&
strcmp(s->run_cmd, "/bin/bastion") != 0)
close(1);
Internal Data Structures
Service Table
#define VIGIL_MAX_SERVICES 16
typedef enum { POLICY_RESPAWN, POLICY_ONESHOT } policy_t;
typedef struct {
char name[64];
char run_cmd[256];
char mode[16]; /* "graphical", "text", or "" (always) */
policy_t policy;
int max_restarts;
pid_t pid;
int restarts;
int active;
} service_t;
static service_t s_svcs[VIGIL_MAX_SERVICES];
static int s_nsvc = 0;
The service table is a static array of 16 entries. Services are loaded in readdir() order – there is no dependency graph or ordering mechanism.
Service Lifecycle
Scanning and Loading
At boot, scan_services() opens /etc/vigil/services using the raw getdents64 syscall (number 217) and calls load_service() for each subdirectory entry:
while ((n = syscall(217, fd, buf, sizeof(buf))) > 0) {
long pos = 0;
while (pos < n) {
struct linux_dirent64 *de = (struct linux_dirent64 *)(buf + pos);
if (de->d_name[0] != '.' && de->d_type == 4)
load_service(de->d_name);
pos += de->d_reclen;
}
}
Fallback Getty
If no services are found (e.g., the services directory is empty or missing), Vigil registers a built-in getty that runs /bin/login with a respawn policy:
if (s_nsvc == 0) {
vigil_log("no services, starting getty");
service_t *s = &s_svcs[s_nsvc++];
memcpy(s->name, "getty", 5);
memcpy(s->run_cmd, "/bin/login", 10);
s->policy = POLICY_RESPAWN;
s->max_restarts = 5;
}
Process Execution
start_service() forks a child process and either calls execv() directly (for absolute paths) or goes through /bin/sh -c (for shell commands with metacharacters):
if (s->run_cmd[0] == '/') {
char *argv[] = { s->run_cmd, NULL };
execv(s->run_cmd, argv);
} else {
char *argv[] = { "/bin/sh", "-c", s->run_cmd, NULL };
execv("/bin/sh", argv);
}
Direct execution is preferred because it ensures that exec-time capabilities (set via sys_cap_grant_exec) are applied to the target binary rather than being consumed by the shell intermediary.
Main Event Loop
Vigil’s main loop runs on a 1-second tick via nanosleep():
- Check for
SIGUSR1(IPC command pending) and callprocess_cmd() - Reap dead children with
waitpid(-1, &status, WNOHANG) - For respawn services: increment restart counter and call
start_service()if under the limit - For oneshot services: mark as inactive on exit
- Check
s_got_termflag for shutdown signal
while (!s_got_term) {
if (s_got_usr1) {
s_got_usr1 = 0;
process_cmd();
}
int status;
pid_t dead;
while ((dead = waitpid(-1, &status, WNOHANG)) > 0) {
for (i = 0; i < s_nsvc; i++) {
if (s_svcs[i].pid != dead) continue;
s_svcs[i].pid = -1;
if (!s_svcs[i].active || s_svcs[i].policy == POLICY_ONESHOT)
break;
if (s_svcs[i].restarts >= s_svcs[i].max_restarts) {
s_svcs[i].active = 0;
break;
}
s_svcs[i].restarts++;
start_service(&s_svcs[i]);
break;
}
}
struct timespec ts = { 1, 0 };
nanosleep(&ts, NULL);
}
IPC Protocol
Vigil uses a file-based IPC mechanism (AF_UNIX socket IPC is deferred):
- vigictl writes a command string to
/run/vigil.cmd - vigictl sends
SIGUSR1to Vigil (PID read from/run/vigil.pid) - Vigil reads the command file, processes it, and unlinks the file
Commands
| Command | Description |
|---|---|
status |
Print all services with PID and restart count |
start <svc> |
Mark service as active and start it |
stop <svc> |
Send SIGTERM to service, mark inactive |
restart <svc> |
Send SIGTERM to service, keep active (auto-restarts) |
shutdown |
Sent via kill(pid, SIGTERM) directly (no cmd file) |
vigictl Implementation
vigictl is a minimal client (user/bin/vigictl/main.c) that reads the PID file and dispatches commands:
static int
write_cmd(const char *cmd)
{
int fd = open(VIGIL_CMD_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd < 0) return -1;
write(fd, cmd, strlen(cmd));
write(fd, "\n", 1);
close(fd);
return 0;
}
/* For shutdown, vigictl sends SIGTERM directly: */
if (strcmp(cmd, "shutdown") == 0) {
kill(pid, SIGTERM);
return 0;
}
Shutdown Sequence
When Vigil receives SIGTERM or SIGINT:
- Send
SIGTERMto all services with active PIDs - Enter a 5-second grace period, polling
waitpid()every second - Send
SIGKILLto any remaining processes - Call
sync()to flush the ext2 block cache (syscall 162) - Log “powering off” and call
sys_reboot(0)(syscall 169) for ACPI S5 power-off
shutdown_all();
sync();
vigil_log("powering off");
syscall(169, 0L); /* sys_reboot(0) = ACPI S5 power off */
for (;;) pause(); /* should not return */
If PID 1 exits by any other path, the kernel triggers arch_request_shutdown (QEMU isa-debug-exit).
Capability Delegation
Vigil does not itself grant capabilities to services. Instead, services requiring elevated rights receive them from the kernel’s policy capabilities mechanism (/etc/aegis/caps.d/), which matches on the binary path at exec time and applies the configured capability kinds to the freshly exec’d process. Vigil’s only role in the pipeline is to fork() + execv() the service binary with the expected argv – the kernel’s policy engine takes over from there.
A per-service caps file has been sketched as a future extension that would let Vigil pre-register capability kinds via sys_cap_grant_exec (syscall 361) before fork/exec, but the current implementation does not read such a file. For a full description of the capability kind enum and the rights bitfield, see the capability model documentation.
Shipped Service Definitions
The default rootfs ships with six services:
| Service | Binary | Policy | Mode | Description |
|---|---|---|---|---|
getty |
/bin/login |
respawn (max 5) | text | Text-mode login prompt |
bastion |
/bin/bastion |
respawn (max 5) | graphical | Graphical display manager |
dhcp |
/bin/dhcp |
oneshot | – | DHCP client (RFC 2131) |
httpd |
/bin/httpd |
respawn (max 5) | – | Minimal HTTP server (port 80) |
chronos |
/bin/chronos |
oneshot | – | SNTP time sync daemon |
nettest |
/bin/nettest |
oneshot | – | Network connectivity test |
All services currently run as root. Per-user service execution is planned but not yet implemented.
Logging
Vigil logs to stdout with the format vigil: <message>. The vigil: prefix (without brackets) is intentional – [VIGIL] would be captured by the test harness’s grep ^[ filter, which expects only structured test output lines starting with [.
Constraints and Limitations
- Maximum 16 services (
VIGIL_MAX_SERVICES) – static array, no dynamic allocation - No dependency ordering – services start in
readdir()order - No AF_UNIX IPC – file + signal mechanism only (socket IPC deferred)
- No cgroup isolation – services share the root cgroup
- No per-user execution – all services run as root
- 1-second event loop granularity –
nanosleep(1s)between iterations - CLOCK_REALTIME = CLOCK_MONOTONIC – RTC not implemented; wall clock starts at boot
Security Considerations
Vigil is v1 software. The file-based IPC mechanism (/run/vigil.cmd) has no authentication – any process that can write to /run/ can issue commands to Vigil, including stopping services or triggering shutdown. The command parsing uses fixed-size buffers with snprintf bounds checking, but the code has not been audited for memory safety issues. As with any C codebase of this scale, exploitable vulnerabilities are likely present. None of the security concerns identified to date have been demonstrated as real exploits – they remain hypothetical threats – but the absence of known exploits should not be mistaken for the absence of vulnerabilities.
Syscalls Used
| Number | Name | Purpose |
|---|---|---|
| 162 | sys_sync |
Flush ext2 block cache during shutdown |
| 169 | sys_reboot |
ACPI S5 power off (arg=0) or keyboard reset (arg=1) |
| 217 | sys_getdents64 |
Read directory entries for service scanning |
| 228 | sys_clock_gettime |
POSIX timespec from 100 Hz PIT ticks |
Testing
python3 tests/test_vigil.py boots the system with INIT=vigil on a q35+NVMe configuration. The test verifies that capabilities are correctly granted and that the getty/login service starts. The test is skipped if build/disk.img is absent.