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.
SECURITY-MODEL.md lists the path jail first among the controls that are said to apply
everywhere:
On
localit holds:On
dockerthe same call returns the file:Writes too.
/dev/shmis the only writable path outside/workand/tmpon a read-onlyrootfs, and it takes one:
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
shellanyway, 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 pathjail 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
localtable spells the jail out further — "every filesystem callresolves through
realpathand is rejected if the target leaves the workspace". A readercomparing 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
realpathresolution the docs citeas 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.imagewith a bind mount, or any future mount option, would put hostpaths inside the container. With no jail, the file tools would follow them. husk's own
docker runuses no-vin the non-persist case and its own named volume otherwise, sonothing 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.