Shell & Coreutils
Technical documentation for Aegis OS shells (sh and stsh) and the coreutils suite
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 executionexec <prog> [args...]in-cmode to replace the shell with another binary (used by Vigil when a serviceruncommand isexec /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_historyon 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:
- Create N-1 pipes for an N-stage pipeline
- Fork N children with
dup2()for pipe wiring - Each child closes all pipe fds after its
dup2()redirects (critical for EOF delivery) - Parent closes all pipe fds after all children are forked
- Register last stage as foreground via
sys_setfg(pids[n-1]) - Wait for all children with
waitpid() - 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 handling –
perror()on failure, non-zero exit status - No flag parsing libraries – hand-rolled argument processing
- Direct syscall wrappers –
shutdownandrebootuse raw syscall numbers - Single-file implementations – each utility is one
main.cwith aMakefile - 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 |