Last update: 1 Feb 2026
Copyright (C) 2026 Trukhin Maksim Aleksandrovich
- Clone the repo:
git clone https://github.com/maksimphono/Simple-esh.git- Compile and run the script:
cd Simple-esh
make
./esh- GNU Make 4.4.1 or later
- gcc 14.3.1 or later
Bugs discovered so far:
- Program can't process empty strings.
My implementation actively relies on tokenization to separate string into sequence of tokens. It’s useful in different ways: most importantly to separate different commands in the pipeline, separate command itself and it’s arguments for every command, read contents of rules file and also it provides convenient way of passing arguments to the underlying execvp function, that is used to execute executable program. Each token is simply just a vanilla C-style string (char*), I’m using strtok() function, but copying tokens into a dedicated array.
For build-in commands I’ve created handler functions: one per command (cd, export, exit). When executing the command, if the command is a build-in function – corresponding handler is executed. Note, that build-in functions are only executed alone, if build-in function is encountered in pipeline it is ignored.
Example:
cd /home
cd ../If the command is not build-in, it is treated as path to executable program, in that case the Shell creates a child process with fork(), and wait for it to finish, corresponding executable file is located and executed using execvp() syscall, in my implementation I also create wrapper for that syscall execvp_ignore_redirect which works exactly like execvp() but ignoring every output redirection, since it’s being processed separately. After process is finished the Shell will react to it’s return code and print Execution Error if necessary.
Example:
ls .
ls "./my favorite directory"When raw input string is processed, it first is tokenized by chatacter ‘|’ to separate different commands in the pipeline. When commands are chained into a pipeline it is discovered by the fact, that the array of such tokens contains more than one element. In that case logic changes pid of every created child process is stored in the array, then Shell waits for all these processes to finish, Shell also creates one pipe per each process, child process’s stdin is redirected onto read end of the previous process’s pipe’s write end using dup2() syscall, and stdout is redirected onto the write end of the pipe, after process finishes execution pipe is closed, next process will again redirect it’s stdin and stdout to a dedicated pipe ends and the loop will continue. The first process binds it’s stdin to the Shell’s stdin, last process binds it’s stdout to Shell’s stdout. This chain of pipes allows processes to pass their out puts to the next process, implementing correct logic of piping in Unix systems.
Example:
ls "./my favorite directory" | grep Pictures | wc -lOnly output can be redirected, any attempt to redirect stdin or stderr will result in Invalid syntax. When output redirection is present it’s handled by opening all streams specified by the redirection command (most common streams are files), and redirecting process’s stdout onto these streams instead of the pipe, pipe is closed, therefore next process in the pipeline will not receive the output of that process, since it was redirected, which is a correct behavior for output redirections in Unix systems. As was stated earlier redirection is not performed by the execvp().
Example:
ls "./my favorite directory" | grep Pictures | wc -l > words.txtWhen the first token in the raw command is a sandbox keyword, it sets global flag SANDBOX, which mean, that every command, that will be executed next will be traced (discussed later). First Shell opens rules file (if it present) for every specified rule in the file it will write entry to the rules table. Then if only one command is executed – tracer process is created by Shell with fork(), then Shell waits for the tracer process to finish, tracer process in turn creates tracee (child) process, sets ptrace() to trace it’s syscalls and wait for it to finish every encountered syscall is checked against the corresponding entry in rules table. Shell keeps lookup table, where argument types of every syscall are stored, when examining the encountered syscall, tracer will read argument types from that lookup table using ptrace(PTRACE_PEEK_DATA, ...), encoding every argument’s value from the registers and collecting their string representations, then it will compare these string representations to the arguments specified in rules table (if any), if blocked syscall is identified – it is replaced by exit_group(0) syscall using ptrace(PTRACE_SETREGS) and executed using ptrace(CONT), which will stop the child process and notify tracer, that blocked syscall was found. Tracer then will notify Shell and exit.
If sanbox is performed to a pipeline – process is way more complicated: for every child process one tracer process is created, Shell waits for all tracers to finish or terminate, each tracer creates and waits for it’s designated tracee (child) process to finish or terminate. Along with tracers and children processes one additional leader process is created, it’s executing syscall sleep all the time, so doesn’t use CPU too much, it’s role is to be the leader of a process group, every created process then is added to that process group, this is done so Shell can effectively terminate all the processes if blocked syscall was encountered. Redirection logic with pipes is the same: pipe ends are passed from one child to another, output redirection also remains the same. A tracer then inspects every syscall done by a tracee (child) in the same way, but if unwanted syscall is identified – tracer will print information about the syscall and send SIGKILL to the process group, that will effectively terminate all processes without executing bad syscall. Rules table is cleared after every sandbox execution, so new rules can be passed with the next command.
Example: First create a rules.txt file and specify syscalls and arguments you want to block from execution
- rules.txt:
deny:write arg0=1 arg2="qwerty"
deny:open arg0=3
deny:mkdirThis rules will block syscall write to stdout with content "qwerty", will block any attempt to open a file by descriptor 3 as well as any attempt to create new directory, no matter with what arguments.
Then just use sandbox with these rules:
sandbox rules.txt ./my_favorite_programIf your program will try to use forbidden syscall, it will be blocked before actually executing this syscall.