Threat model
Ralon makes a narrow promise and tries to make it exactly. A tool in this position is only worth as much as the honesty of its limitations, so those are on this page too.
Defends against
A process running with your privileges, started by you through ralon run, that tries to modify a path the policy protects. That covers the ordinary case — an agent editing a file it should not have touched — and the adversarial one: a prompt-injected agent going deliberately after .env, an agent shelling out to sed, python or git checkout, and anything it spawns, including processes that outlive it.
Does not defend against
- Root. Anything that can become root outside the namespace can undo all of it. This is a guardrail for a tool you invited in, not a defence against someone who already has your password.
- Processes you did not start this way. The policy binds the tree under
ralon run. A daemon that was already running — a language server, a file watcher, an editor with a remote API — is not restricted, and a sandboxed process that can ask one of them to write a file gets the write. - Reading. Protected files stay readable, deliberately.
agent.locksays what must not change. A secret an agent must not read does not belong in the project directory. - Exfiltration. Nothing here touches the network.
The guarantee
Inside ralon run, for every protected path, in that process and every descendant:
| Attempt | Result |
|---|---|
| write, append, truncate, cp over it | denied |
| delete it, rename it away | denied |
| replace it by renaming another file over it | denied |
| delete then recreate | denied |
| hard link or symlink over it | denied |
| create anything inside a protected directory | denied |
| rename or remove a directory on the way to it | denied |
| chmod, then write | denied |
| reach the inode through a hard link made inside | denied |
| escape by umount, bind mount, or a nested namespace | denied |
| reach it through another process's /proc/<pid>/root | denied |
| read it | allowed |
| everything else in the project | allowed |
Each row is a test in tests/enforcement.rs. They run a real shell inside a real sandbox and then check the file from outside it, against every backend the kernel provides — so a backend cannot pass by being unavailable.
Why it cannot be undone
- A Landlock domain is one-way. There is no syscall to leave one, and it survives
forkandexecve. - The mount namespace is locked before your command starts. Entering a second user namespace marks every inherited mount
MNT_LOCKED, soumountfails and the kernel refuses any bind mount that would expose what is beneath. no_new_privsis set, so a setuid binary cannot be used to climb out.- Nothing supervises the sandbox, so there is nothing to kill. Ralon becomes the command.
Two properties fall out of the design rather than being checked for. Hard links cannot reach a protected file: under the mount backend the path is itself a mount point, and link() requires source and target on the same mount, so every attempt returns EXDEV. And /proc/<pid>/root is not a way out: following another process's root needs ptrace access, which a process in a nested user namespace does not have over processes in the parent one, even at the same uid.
Where it stops
A second path to the same directory bypasses both backends. This is tested and true. If the project is also visible at another mount point — a bind mount made before the sandbox started, a volume mounted twice into a container, a share exported at two paths — writing through the other path is not restricted. Both backends are path-based and neither can protect a path it was not told about. The sandboxed process cannot create such a mount, so this requires the second path to already exist.
- Landlock alone can be defeated where user namespaces are available. Its rules apply to paths, not inodes, so a process that can create its own mount namespace can bind the project somewhere the carve-out granted. Automatic selection prefers the mount backend, which is available in exactly the environments where that attack is; forcing
--backend landlockthere gives up a real guarantee. - Only paths that exist can be protected. A bind mount needs something to mount.
statusandrunwarn about patterns matching nothing. The landlock backend is stricter here by accident of its design. - The policy is read before the sandbox starts. Nothing races it, but a path created afterwards is not protected for the life of that run. Restart the agent after adding files that need protecting.
- A supervisor is a process, on Windows. Killing it releases the locks.
runhas nothing to kill, which is why it stays the stronger option for an agent you launch yourself. On macOS the failure runs the other way: the flag is on the inode and survives everything, so a supervisor that is killed leaves state behind rather than losing protection —statusreports it andralon guard --stopclears it. - The macOS supervisor is a narrowing, not a sandbox. It enforces with
chflags uchg, which an agent can undo withchflags nouchg— one command, no privileges. Every ordinary write is refused; an agent that goes looking will find a way through. It also does not pin ancestors, so renaming a parent directory moves the protected path out from under the policy while leaving the file itself immutable.ralon runhas neither limitation. - A policy outside every scope does nothing. Ralon honours an
agent.lockonly inside a directory you named withralon scope add— which is what stops one arriving inside a downloaded archive from locking files, and what makesralon statussay policy found, but this project is outside every scope rather than looking protected. Even inside a scope the blast radius is small: patterns are relative to the file that declares them and.., absolute paths and~are rejected, so the most a hostile policy can do is make its own directory read-only.
Verify it yourself
Do not take the test suite's word for it either. Two minutes with a shell is worth more than any table on this page:
$ ralon run -- sh $ echo x > .env # EROFS or EACCES $ rm .env # denied $ echo x > src/App.tsx # fine
If ralon status reports no available backend, run refuses to start the command rather than running it unprotected. A failure to enforce is never silent.
Reporting a bypass
A bypass is anything that modifies a protected path from inside ralon run without root, other than the limitations above. Report it privately through a GitHub security advisory with the policy, the command, the kernel version and the backend. A failing test in the style of tests/enforcement.rs is the most useful report there is.