Executables
How the kernel finds, loads, and runs programs. The relationship between files, functions, and the executable bit.
The Tension
fishbowl has two kinds of executables, mirroring real Unix:
- Native functions —
BinFunctionclosures. Our platform is JavaScript, so our “machine code” is a function reference, not ELF bytes. These can’t be meaningfully serialized to bytes any more than you can serialize compiled x86 to source. - Scripts — shell scripts, hypothetical python scripts, whatever. Text files stored as bytes in any fileserver. They need an interpreter.
The current implementation (BinFS) conflates these by wrapping native functions in a virtual filesystem. BinFS stores function references in a Map<string, BinFunction> and pretends they’re files — readdir() lists them, stat() reports them, read() serializes their source. A special resolve() method provides a fast path for the kernel to get the function back out.
This works, but it’s an inversion of the real Unix model. In real Unix, the filesystem doesn’t know what an executable is. It just stores bytes. The kernel interprets those bytes at exec time (ELF magic? shebang? error). BinFS puts the intelligence in the filesystem instead of the kernel.
What’s a File?
In our system, a “file” is whatever a Fileserver says it is:
- MemoryFS files —
Uint8Arraystored in memory. Ephemeral. No disk. - ProcFS files — projections of kernel state. Read-only views of live data.
- DevFS files — streams (stdin, stdout, stderr). Reads/writes go to streams, not storage.
Files are the Fileserver protocol’s abstraction — named data accessible through open/read/write/stat/readdir. Nothing more.
A BinFunction is not byte content. But it can be a property of a file node — like how an ELF binary is bytes but also “something the kernel knows how to load.” In real Unix, a file’s bytes happen to be executable machine code that the kernel interprets. In fishbowl, a file node can carry a function reference that the kernel calls directly. The file still has byte content (for cat), a name (for ls), permissions (for chmod) — it just also has a bonus field that the kernel knows about.
This is not the same as BinFS, which pretended functions were files. Here, functions are attached to files. The file is real. The function is an extra property the kernel inspects at exec time.
What’s an Executable?
In real Unix, the delta between a regular file and an executable is one bit: the executable permission. chmod +x script.sh turns a text file into a program. The kernel checks the bit at exec time, then reads the file to figure out what kind of program it is (ELF header? shebang? error).
The same model works for fishbowl, with one addition for native functions:
- A file with
+xand anexecfield — the kernel calls the function directly. This is how native commands work. Theexecfield is our equivalent of ELF bytes that the kernel knows how to load. - A file with
+xand noexecfield — the kernel reads the content. A shebang line (#!/bin/sh) tells it which interpreter to use. No shebang → ENOEXEC.
One resolution path. No fallback tiers. No separate lookup table.
# Script — lives in the filesystem as contentecho '#!/bin/shecho hello world' > /usr/local/bin/greetchmod +x /usr/local/bin/greetgreet # kernel reads file, finds shebang, spawns sh
# Native function — lives in the filesystem as an exec nodeecho # kernel finds file node at /bin/echo, sees exec field, calls itBoth coexist on the same PATH. The kernel dispatches based on what it finds on the file node.
The Real Unix Exec Model
In real Unix, the kernel’s exec path:
- Resolve the path (via PATH for bare names)
- Check permissions — is the file executable?
- Read the file header:
- ELF magic bytes → native binary, load into memory and jump
#!shebang → read interpreter path, re-exec with interpreter- Neither → error (ENOEXEC)
The filesystem is uninvolved in this decision. It stores bytes. The kernel interprets them.
fishbowl Exec Model
How the Builder Registers Bins
When the builder processes Extension.bins, it writes file nodes into memoryFS at /bin/<name>. Each node is a real file with:
exec: theFunction— the native function referencemode: 0o755— executable permissionscontent: Uint8Array— metadata comment (name, source extension) forcat
Builder: 1. Collects bins from all .use() calls 2. For each bin, creates a file node in memoryFS at /bin/<name> - exec: the BinFunction reference - mode: 0o755 (executable) - content: metadata comment (for cat /bin/echo) 3. Done. No separate table.These are real files with a bonus field. ls /bin lists them. stat /bin/echo reports them. cat /bin/echo reads their content. The kernel checks their exec field at exec time.
PATH Resolution
When the kernel resolves a command, there is one path — walk the filesystem:
exec("greet", argv): 1. For each directory in $PATH: a. Stat the file at <dir>/<name> → not found? Continue to next PATH entry b. Check permissions — is the file +x? (mode & 0o111) → not executable? Continue to next PATH entry c. Check exec field on the file node → has exec? Call the function. Done. d. Read file content, check for shebang (#!) → has shebang? Parse interpreter path, re-exec with interpreter → no shebang? Continue to next PATH entry 2. Nothing found → command not foundOne resolution path. The filesystem is the single source of truth. No exec table, no two-tier lookup, no fallback.
Decision: no exec table — the filesystem IS the source of truth. In real Unix, a file’s bytes are the executable. In fishbowl, a file node’s
execfield is the executable. Both are properties of the file, checked by the kernel at exec time. There is no separate registry, no dual-awareness problem, no stubs. The exec field is our platform’s equivalent of “these bytes are ELF machine code” — a kernel-level concern attached to a filesystem entity.
Decision:
execfield is a kernel concern, not exposed through Stat. Theexecfield lives on the MemoryFS node, not on theStatprotocol type. User-facing tools seemode: 0o755and file content. The kernel sees theexecfield because it has access to the underlying node. This keeps the Fileserver protocol clean —Statdoesn’t need to know about function references.
File Permissions
The executable bit is the gate for all execution — both native functions and scripts. A file without +x is just data. A file with +x is a candidate for execution.
This requires the Fileserver protocol to support permissions:
interface Stat { name: string type: NodeType size: number mtime: number mode?: number // permission bits (rwxrwxrwx), default: 0o644 uid?: number // owner, default: 0 gid?: number // group, default: 0 ctime?: number // metadata change time, default: mtime}The mode field follows Unix conventions. 0o755 means owner rwx, group rx, others rx — a typical executable. 0o644 means owner rw, group r, others r — a typical data file. The kernel checks mode & 0o111 (any execute bit set) during PATH resolution. When mode is absent (fileservers that don’t track permissions), the kernel defaults to 0o644 for files — not executable. See the inodes spec for the full Stat interface and INode data model.
Decision:
modeon Stat, Unix-convention permission bits. Not just anexecutable: boolean— the full mode field gives uschmod 755,chmod 644,ls -lpermission display, and future user/group permissions. We don’t need to enforce user/group semantics in v1, but the data model should support them. Default mode for new files is0o644(not executable). Default mode for new directories is0o755.
Script Execution
Scripts are regular files with the executable bit set and no exec field. The kernel reads the first line to check for a shebang:
#!/bin/shecho "I am a shell script"The kernel reads #!/bin/sh, resolves sh through the normal exec path (which finds it in /bin/sh as a node with an exec field), and spawns it with the script file path as an argument. This is exactly how real Unix handles shebangs.
This means any interpreter can be used — shell, a hypothetical python interpreter, anything available through the exec path. The shebang is the universal dispatch mechanism.
Decision: shebang recursion limit of 4. If a shebang points to another script with a shebang, the kernel follows the chain up to 4 levels deep (matching Linux’s
BINPRM_BUF_SIZEbehavior). Beyond that → ENOEXEC. This prevents infinite loops from circular shebangs.
Files Without Shebangs
What happens when the kernel encounters an executable file (+x) with no exec field and no shebang?
- In real Unix: ENOEXEC. The shell may retry with
/bin/shas a fallback (POSIX behavior), but the kernel itself rejects it. - In fishbowl: same — ENOEXEC. No silent eval of raw JS source.
Decision: no implicit eval of filesystem content. An executable file must have a shebang (or an
execfield). The kernel does not guess. This eliminates a security surface (arbitrary eval from filesystem content) and matches POSIX behavior. If a developer wants a JS-source executable, they write#!/usr/bin/env node-style shebangs pointing to a JS interpreter — or more practically in our system, they register native functions via the Extension API.
Write Semantics for /bin
What happens when a user writes to a path where a native function node exists?
echo '#!/bin/shecho "custom echo"' > /bin/echochmod +x /bin/echoThe write replaces the node entirely. The old node — including its exec field — is gone. The new node has content (the script) and mode (set by chmod), but no exec field. On next exec, the kernel finds the file, sees +x, finds no exec field, reads content, finds the shebang, dispatches to the interpreter.
The native echo is gone. Replaced by a script. Clean, predictable, exactly how Unix works — overwriting /bin/echo replaces the binary.
# Remove a native command entirelyrm /bin/echo # node is gone, exec field and allecho hello # command not found
# Replace a native command with a scriptecho '#!/bin/shecho "custom: $*"' > /bin/echochmod +x /bin/echoecho hello # → custom: helloDecision: overwriting a file replaces the entire node, including
exec. A file node’sexecfield is not sticky — it’s part of the node, not the path. When the node is replaced (by write or rm + create), theexecfield is gone unless the new writer explicitly sets it. This is natural: in real Unix,cp script.sh /bin/echoreplaces the ELF binary with a script. The old binary is gone. Same here.
Consumers of the Exec Model
Shell builtins that introspect command resolution need no special awareness — they just use filesystem operations:
| Builtin | Behavior |
|---|---|
which | Walk PATH, stat files, check +x. Report first match path. |
type | Same resolution. Report the kind based on what’s found: native function (has exec) vs script (has shebang). |
hash | Future — cache resolved paths to skip PATH walking. Bash does this. |
| Tab completion | Scan directories in PATH, list executable files. Pure filesystem operation. |
No dual awareness needed. No “check exec table then check filesystem.” Just check the filesystem.
What This Replaces
BinFS as a separate fileserver type goes away. Its responsibilities all live in the filesystem:
| BinFS responsibility | New home |
|---|---|
| Store function references | exec field on file nodes in memoryFS |
resolve(name) → BinFunction | Kernel checks exec field on the file node during PATH resolution |
readdir() for ls /bin | Real directory entries in memoryFS |
stat() for which/type | Real file nodes in memoryFS |
read() for cat /bin/echo | Real file content (metadata) on the node |
write() for dynamic bin creation | Write script + chmod +x, or pkg install writes exec node |
No stubs. No projection. No separate table. The filesystem is the single source of truth.
Dynamic Bin Creation
Two paths for creating new commands at runtime:
Scripts (primary)
Write a script file with a shebang and set +x. No special mechanism needed — this is how Unix works.
cat > /usr/local/bin/greet << 'EOF'#!/bin/shecho "hello, $1"EOFchmod +x /usr/local/bin/greetgreet world # → hello, worldPackage Installation
pkg install can write exec nodes into memoryFS (via the Extension API) — file nodes with exec fields and mode: 0o755. This is how new native commands enter the system at runtime — through the same mechanism the builder uses at boot. The builder writes exec nodes; package install writes exec nodes. Same operation.
Summary
The exec model unifies what BinFS split across two abstractions:
- Storage — native functions live as
execfields on file nodes in memoryFS. Scripts live as byte content on file nodes in memoryFS. Both are properties of real files in real directories. - Resolution — PATH resolution walks directories, stats files, checks +x, checks the
execfield, checks for shebangs. One path, one source of truth. - Inspection —
ls,which,type, tab completion, globs — all pure filesystem operations. No dual awareness, no special-casing for native functions.
The executable bit (chmod +x) is the universal gate. The exec field is the native function indicator. The shebang is the script dispatch mechanism. These three concepts, all on file nodes in the filesystem, cover the full exec model with no separate tables, no stubs, and no projection layers.