Repository navigation
fix(fence): reach and grant write roots as checked, never through a planted link - #573
Merged
Merged
Conversation
…lanted link A fenced agent that may write the directory holding one of its write roots (its scratch under $TMPDIR at local=all, a state directory in a written workdir, a root nested in another root it writes) could replace that root with a symlink out of the wall in one turn. The next turn's fence granted the root by name -- Landlock opening it, Seatbelt resolving it -- so it granted writing wherever the link led. Fence.reach now walks a written path from / one name at a time, making what is missing 0700 where asked, and follows a symbolic link only in a directory the fence does not write, that is not beneath one it writes, and that is not /tmp, /var/tmp, $TMPDIR or the scratch, told by device and inode. A link anywhere else refuses the turn. A fence that writes / follows every link. Fence.ready reaches every write root and the scratch where the fence is put up: hmz internal fence and the supervised anchor's walls grant Landlock the descriptors opened, and Seatbelt the paths reached. The agent's and the target's state directories and the serving half's scratch are made through the same walk. Tests: an integration regression of the escape through the wrapper and the supervisor, failing on main (Seatbelt and Landlock), and unit tests of the link rule, of making the roots, and of the serving scratch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A fenced agent that may write the directory holding one of its write roots (its scratch under
$TMPDIRatlocal=all, a state directory in a written workdir, a root nested in another root it writes) could swap that root for a symlink out of the wall in one turn. The next turn's fence granted the root by name: Landlock opened the path, and Seatbelt resolved it. Either way it granted writing wherever the link led, so the agent escaped the fence. Ported from the internal fork's fix (8b1ece81and follow-ups), adapted to main's layout.Fence.reach(path, make=)walks from/one name at a time (O_PATH/O_SEARCH,O_NOFOLLOW) and makes missing directories0700where asked. It follows a symlink only when the link sits in a directory that:/tmp,/var/tmp,$TMPDIRor the scratch.Directories are compared by device and inode, so a different spelling of the same path does not get past the check. Any other link refuses the turn with exit 126:
<path>: it is a link where a fenced program could have made one, which no fence follows to write. A fence that writes/follows every link.Fence.ready()reaches every write root and the scratch where the fence is put up:hmz internal fenceand the supervised anchor's_Wallsgrant Landlock the descriptors they opened, through the newRuleset.held.The agent's
_ready,abroad.readyand the serving half's_scratchnow make directories through the same walk.Docs:
agents.md› Enforcement,remote-execution.md,cli.md.Left out of this port
specs/**is off limits. The refusal follows from 6.4.1 ("confine … to the grant").nosymfollow;fs.protected_symlinks;mutableroots.Test plan
tests/integration/test_anchor_fence_writes.py. The wrapper variant fails on main and passes with the fix, on both macOS (Seatbelt) and Linux (Landlock, in Docker). The supervisor variant was run on Linux in the same way and also fails on main, passes with the fix.Fence.reach/Fence.readyand for the serving scratch.uv run pytestpasses on macOS. On Linux in Docker (without extras), the only failures are the dsh/agy tests that fail on main in the same environment.uv run --all-extras pre-commit run --all-filespasses.