Skip to content

The path jail is not enforced on docker, but the security model says it holds in every mode #120

Description

@Hotragn

SECURITY-MODEL.md lists the path jail first among the controls that are said to apply
everywhere:

These hold in every mode, local included: the path jail, the command deny list, the
environment scrub, output caps, process-tree kill, redact() on every result and
audited() on every MCP call.

On local it holds:

read_file /etc/passwd
-> path is outside the machine's writable area: /etc/passwd
   hint: this computer exposes /work and /tmp; use a path under one of them

On docker the same call returns the file:

read_file /etc/passwd
-> [lines 1-3 of 19]
   root:x:0:0:root:/root:/bin/bash
   daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
   bin:x:2:2:bin:/bin:/usr/sbin/nologin

Writes too. /dev/shm is the only writable path outside /work and /tmp on a read-only
rootfs, and it takes one:

write_file /dev/shm/jail-probe.txt
-> wrote /dev/shm/jail-probe.txt (31B)

I found this through a symlink first and spent a while writing it up as a symlink bypass.
The direct read is what settles it: there is no jail on this provider, rather than a jail
with a hole in it.

Severity, stated plainly

Low, and the design is arguably right. The container is the boundary. Everything
reachable is container-local and disposable, the rootfs is read-only, uid 1000 could read
any of it through shell anyway, and the host is genuinely untouched — no /mnt/c, no
/home, no /var/run/docker.sock, no ~/.ssh, all checked on the same container. A path
jail inside a kernel boundary is mostly redundant, and I would not argue for adding one on
those grounds alone.

The problem is the sentence. "These hold in every mode" is the kind of claim someone builds
a decision on, and the local table spells the jail out further — "every filesystem call
resolves through realpath and is rejected if the target leaves the workspace". A reader
comparing providers would reasonably conclude the jail is the floor and the kernel boundary
is the extra, when on docker it is the only thing there.

So: scope the claim to the providers that implement it, or implement it uniformly. Either
is fine. Leaving both the claim and the gap is the option that misleads.

The part that is not only documentation

If the jail stays local-only, then the local implementation is the only place that logic
ever runs — and it is currently broken there in the other direction, refusing symlinks that
stay inside the workspace (#113). Between the two, the realpath resolution the docs cite
as proof is exercised on exactly one provider and does not work correctly on it. Whichever
way this goes, the jail needs a test that asserts the escape is caught and the in-jail
link is followed, on every provider that claims the control.

Untested, and worth someone checking

A custom computer.image with a bind mount, or any future mount option, would put host
paths inside the container. With no jail, the file tools would follow them. husk's own
docker run uses no -v in the non-persist case and its own named volume otherwise, so
nothing I ran was exposed — but the jail is the thing that would have made that safe by
construction rather than by the current flag set happening not to mount anything.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationpkg: agentTouches packages/agent

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions