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/login for text-mode console authentication
  • bastion (mode: graphical) – runs /bin/bastion for 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():

  1. Check for SIGUSR1 (IPC command pending) and call process_cmd()
  2. Reap dead children with waitpid(-1, &status, WNOHANG)
  3. For respawn services: increment restart counter and call start_service() if under the limit
  4. For oneshot services: mark as inactive on exit
  5. Check s_got_term flag 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):

  1. vigictl writes a command string to /run/vigil.cmd
  2. vigictl sends SIGUSR1 to Vigil (PID read from /run/vigil.pid)
  3. 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:

  1. Send SIGTERM to all services with active PIDs
  2. Enter a 5-second grace period, polling waitpid() every second
  3. Send SIGKILL to any remaining processes
  4. Call sync() to flush the ext2 block cache (syscall 162)
  5. 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 granularitynanosleep(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.