Shell & Coreutils

Aegis ships two shells and a set of coreutils, all written in C with minimal libc dependencies. The basic shell (/bin/sh) handles simple pipeline execution for service scripts, while the secure shell (/bin/stsh) is the interactive login shell with capability-aware builtins, tab completion, history, and a raw-mode line editor.

All shell and coreutil code is v1 software – functional and tested for its intended use cases, but written in C without formal security audit. The parser, line editor, and argument handling all use fixed-size buffers that have been bounds-checked with snprintf/strncpy, but as with any from-scratch C codebase, exploitable memory safety issues are likely present. The planned C-to-Rust migration has begun in the kernel (the capability system is already Rust) and will eventually reach userspace components. Contributions are welcome – file issues or propose changes at exec/aegis.

Shells

sh – Basic Shell

Source: user/bin/shell/main.c

The basic shell is a minimal POSIX-like command interpreter used primarily by Vigil’s start_service() when a service run command contains shell metacharacters. It provides:

  • Pipeline execution (up to 6 stages)
  • I/O redirection: <, >, >>, 2>&1
  • Three builtins: cd, exit, help
  • -c <command> mode for non-interactive execution
  • exec <prog> [args...] in -c mode to replace the shell with another binary (used by Vigil when a service run command is exec /bin/foo)
  • Foreground process tracking via sys_setfg (syscall 360)

The shell uses a static, empty environment (char *g_envp[] = { NULL }) – it does not inherit or manage environment variables. For interactive use, stsh is the intended shell.

Prompt: Fixed # prompt (no username or path display).

Signal handling: SIGCHLD is set to SIG_IGN to prevent zombie accumulation. The sys_setfg syscall registers the last pipeline stage as the foreground process so that Ctrl-C delivers SIGINT to it rather than the shell.

stsh – Secure Shell

Source: user/bin/stsh/ (8 source files)

The Aegis Secure Shell is the primary interactive shell, spawned by /bin/login after successful authentication. It integrates deeply with the Aegis capability model and provides a substantially richer experience than sh.

Module Structure

File Purpose
main.c REPL, prompt building, -c mode, MOTD display
parser.c Tokenizer with redirect extraction, pipeline splitting
exec.c Pipeline execution, builtin dispatch, sys_setfg
env.c Environment variable storage (export, $VAR expansion)
editor.c Raw-mode terminal line editor (emacs keybindings)
history.c Ring buffer history with disk persistence
complete.c Tab completion for commands and file paths
caps.c Capability query, sandbox builtin, caps display
stsh.h Shared types, constants, and function declarations

Constants

#define MAX_PIPELINE    6     /* max pipeline stages */
#define MAX_ARGV        16    /* max args per command */
#define LINE_SIZE       512   /* input line buffer */
#define ENV_MAX         64    /* max environment variables */
#define HIST_SIZE       64    /* history ring buffer entries */
#define CAP_TABLE_SIZE  16    /* max capability slots to query */

Prompt

The prompt reflects the current user, working directory, and privilege level:

user@aegis:~/path$ _     (normal user)
user@aegis:~/path# _     (holds CAP_DELEGATE)

The # vs $ suffix is determined by has_cap_delegate(), which queries the process’s capability table at shell startup via sys_cap_query (syscall 362). The HOME prefix is replaced with ~ for display.

Builtins

Builtin Description
cd [path] Change directory; defaults to $HOME, falls back to /
exit [n] Exit shell with status n (defaults to last exit status); saves history
export VAR=val Set environment variable; no args prints all variables
env Print all environment variables
caps [pid] Display capability table for self (pid=0) or another process (requires CAP_QUERY; returns ENOCAP/errno 130 if denied)
sandbox -allow CAP1,CAP2 -- cmd args... Run command with restricted capability table (requires CAP_DELEGATE)
grant Removed – prints notice that caps are now granted by kernel policy at exec time
help List available builtins

Builtins are only dispatched for single-command lines (not pipelines) without stdin redirection. In addition, stsh recognizes exec <prog> [args...] as a special form in -c mode (not in the interactive REPL), which execves the named program – /bin/<prog> is used if the name is not an absolute path.

Capability Integration

The caps builtin uses sys_cap_query (syscall 362) to enumerate the process’s capability table:

cap_slot_t slots[CAP_TABLE_SIZE];
long ret = syscall(SYS_CAP_QUERY, pid, (long)slots, (long)sizeof(slots));

Each capability is displayed with its human-readable capability kind name and rights bitfield:

VFS_OPEN(rwx) VFS_WRITE(rwx) CAP_DELEGATE(rwx) CAP_QUERY(rwx) POWER(rwx)

The full capability kind table (see the capability model for details):

Kind Name Kind Name
0 NULL 9 THREAD_CREATE
1 VFS_OPEN 10 PROC_READ
2 VFS_WRITE 11 DISK_ADMIN
3 VFS_READ 12 FB
4 AUTH 13 CAP_DELEGATE
5 CAP_GRANT 14 CAP_QUERY
6 SETUID 15 IPC
7 NET_SOCKET 16 POWER
8 NET_ADMIN    

The rights bitfield is a 3-bit value: r (READ=1), w (WRITE=2), x (EXEC=4).

Sandbox Builtin

The sandbox builtin provides capability-restricted process execution using sys_spawn (syscall 514):

sandbox -allow VFS_READ,VFS_OPEN -- cat /etc/passwd

This creates a child process whose capability table holds only the listed capability kinds. Note that the sandbox mechanism enforces capability restrictions at the kernel level via sys_spawn, but stsh itself is v1 C code – the argument parsing and allowlist handling have not been audited for injection or bypass vulnerabilities.

The implementation builds a cap_slot_t mask array from the allowlist, then calls sys_spawn with the mask as the 5th argument:

long pid = syscall(SYS_SPAWN, (long)path, (long)child_argv,
                   (long)envp, (long)-1, (long)mask);

Requires CAP_DELEGATE to use.

Environment Variables

Environment handling is in env.c. Variables are stored in a flat array of 64 entries, each up to 256 bytes in KEY=VALUE format:

static char s_env_store[ENV_MAX][256];
static char *s_env_ptrs[ENV_MAX + 1]; /* NULL-terminated for execve */

The env_expand() function handles three expansion forms:

  • $VAR – alphanumeric + underscore variable names
  • ${VAR} – braced variable names
  • $? – last command exit status

Unset variables expand to the empty string. Command substitution ($(...)) is not supported.

Line Editor

The editor (editor.c) operates in raw terminal mode (disabling ICANON, ECHO, and ISIG via termios) and processes input byte-by-byte. Supported key bindings:

Key Action
Enter Accept line
Ctrl-A Move cursor to beginning
Ctrl-E Move cursor to end
Ctrl-K Kill to end of line
Ctrl-U Kill to beginning of line
Ctrl-W Delete word backward
Ctrl-L Clear screen and redraw
Ctrl-C Discard line, return empty
Ctrl-D EOF on empty line; ignored otherwise
Tab Tab completion
Up/Down History navigation
Left/Right Cursor movement
Home/End Move to line boundaries
Delete Delete character under cursor
Backspace Delete character before cursor

The editor supports cursor positioning at any point in the line with character insertion via memmove().

Tab Completion

Tab completion (complete.c) is context-aware:

  • First token (command position): Completes against builtins (cd, exit, help, export, env, caps, sandbox, grant) and binaries in /bin/
  • Subsequent tokens (argument position): Completes against filesystem entries in the relevant directory

Single-match completions are inserted inline with a trailing space (or / for directories). Multiple matches complete to the longest common prefix and display all candidates below the prompt.

Hidden files (dotfiles) are only shown when the prefix starts with .

History

History (history.c) is a ring buffer of 64 entries with disk persistence:

  • Saved to ~/.stsh_history on shell exit
  • Loaded from disk at startup
  • Consecutive duplicate suppression
  • Navigable with Up/Down arrow keys
  • Privileged mode: When the shell holds CAP_DELEGATE (root session), history is not persisted to disk – this prevents credential leakage in administrative sessions
void hist_init(int privileged)
{
    s_privileged = privileged;
    if (privileged) return;  /* no disk I/O for admin shells */
    /* ... load from disk ... */
}

Pipeline Execution

Both shells implement pipeline execution with the same core pattern:

  1. Create N-1 pipes for an N-stage pipeline
  2. Fork N children with dup2() for pipe wiring
  3. Each child closes all pipe fds after its dup2() redirects (critical for EOF delivery)
  4. Parent closes all pipe fds after all children are forked
  5. Register last stage as foreground via sys_setfg(pids[n-1])
  6. Wait for all children with waitpid()
  7. Clear foreground with sys_setfg(0)

stsh additionally sets each child into its own process group via setpgid() (both in child and parent as a race guard) for proper job control.

Supported redirections:

Syntax Description
< file Redirect stdin from file
> file Redirect stdout to file (truncate)
>> file Redirect stdout to file (append)
2>&1 Redirect stderr to stdout
cmd1 \| cmd2 Pipeline (up to 6 stages)
cmd1 ; cmd2 Sequential execution (stsh only)

Semicolons (stsh only)

stsh supports ; for sequential command execution. The REPL splits the expanded line on ; and processes each segment independently through the parse/builtin/pipeline path.

Coreutils

Aegis includes a set of userspace utilities in individual binaries under /bin/. All are single-file C implementations focused on correctness and minimal size.

File Operations

Utility Description Notes
cat Concatenate files to stdout Reads stdin with no args; 512-byte buffer
cp Copy file Single source to single destination; 4096-byte buffer
mv Move/rename file Wrapper around rename()
rm Remove file Single file; no -r flag
ln Create symlink ln [-s] target linkname; always creates symlinks
touch Create empty file open(O_WRONLY \| O_CREAT) with mode 0644
mkdir Create directory Single directory; mode 0755
readlink Print symlink target Reads and prints the target path

Directory and File Inspection

Utility Description Notes
ls List directory Uses opendir/readdir; defaults to .; no flags
pwd Print working directory Wrapper around getcwd()
chmod Change file mode Octal mode argument: chmod 755 file
chown Change file owner Format: chown UID:GID file

Text Processing

Utility Description Notes
grep Search for literal patterns No regex – literal string matching only; multi-file with filename prefix
sort Sort lines Reads up to 512 lines of up to 512 chars each; strcmp()-based
wc Count lines/words/bytes Supports -l, -w, -c flags; defaults to all three
echo Print arguments Space-separated with trailing newline

System Utilities

Utility Description Notes
uname Print system name Uses utsname struct; prints sysname machine
whoami Print current username getpwuid(getuid()); falls back to root if /etc/passwd unreadable
clear Clear terminal Emits ANSI escape \033[2J\033[H
true Exit with 0 Single-line implementation
false Exit with 1 Single-line implementation
shutdown System shutdown Sends SIGTERM to PID 1 (Vigil)
reboot System reboot Calls sys_reboot(1) (syscall 169) for keyboard reset

Implementation Characteristics

All coreutils share common design principles:

  • No dynamic allocation – fixed-size buffers throughout
  • Minimal error handlingperror() on failure, non-zero exit status
  • No flag parsing libraries – hand-rolled argument processing
  • Direct syscall wrappersshutdown and reboot use raw syscall numbers
  • Single-file implementations – each utility is one main.c with a Makefile
  • v1 maturity – tested for correctness in Aegis’s CI harness, but not fuzz-tested or audited for edge-case input handling; as C programs processing untrusted input (especially grep, sort, cat), they likely contain exploitable buffer handling issues typical of any first-version C codebase

The grep implementation is particularly notable for its simplicity: it performs literal substring matching with memcmp() on each line, with no regular expression support. This is sufficient for the shell test infrastructure and basic text filtering.

The ln command always creates symbolic links (even without -s), as Aegis’s ext2 implementation supports symlinks but the hard link code path defaults to symlink behavior.

Syscalls Referenced

Number Name Used By
169 sys_reboot reboot (arg=1), shutdown (via Vigil)
360 sys_setfg Both shells (foreground process registration)
362 sys_cap_query stsh caps builtin
514 sys_spawn stsh sandbox builtin