Ralonv0.1.6
[01]

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.lock says what must not change. A secret an agent must not read does not belong in the project directory.
  • Exfiltration. Nothing here touches the network.
[02]

The guarantee

Inside ralon run, for every protected path, in that process and every descendant:

AttemptResult
write, append, truncate, cp over itdenied
delete it, rename it awaydenied
replace it by renaming another file over itdenied
delete then recreatedenied
hard link or symlink over itdenied
create anything inside a protected directorydenied
rename or remove a directory on the way to itdenied
chmod, then writedenied
reach the inode through a hard link made insidedenied
escape by umount, bind mount, or a nested namespacedenied
reach it through another process's /proc/<pid>/rootdenied
read itallowed
everything else in the projectallowed

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.

[03]

Why it cannot be undone

  • A Landlock domain is one-way. There is no syscall to leave one, and it survives fork and execve.
  • The mount namespace is locked before your command starts. Entering a second user namespace marks every inherited mount MNT_LOCKED, so umount fails and the kernel refuses any bind mount that would expose what is beneath.
  • no_new_privs is 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.

[04]

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 landlock there gives up a real guarantee.
  • Only paths that exist can be protected. A bind mount needs something to mount. status and run warn 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. run has 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 — status reports it and ralon guard --stop clears it.
  • The macOS supervisor is a narrowing, not a sandbox. It enforces with chflags uchg, which an agent can undo with chflags 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 run has neither limitation.
  • A policy outside every scope does nothing. Ralon honours an agent.lock only inside a directory you named with ralon scope add — which is what stops one arriving inside a downloaded archive from locking files, and what makes ralon status say 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.
[05]

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.

[06]

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.