procfs: let every task read its own /proc/self - #233
Draft
congwang-mk wants to merge 7 commits into
Draft
congwang-mk wants to merge 7 commits into
congwang-mk wants to merge 7 commits into
Conversation
/proc/net is a link to self/net, and every /proc/<pid> and /proc/<pid>/task/<tid> directory carries the same tree. The handler only matched the /proc/net spelling, so with /proc readable a sandbox could read /proc/self/net/dev or /proc/self/net/tcp and see the host's interfaces and sockets that /proc/net/dev and /proc/net/tcp hide. Sandlock creates no network namespace, so all of these name one tree. Rewrite them to /proc/net before matching. Signed-off-by: Cong Wang <cwang@multikernel.io>
/proc/self resolves per caller, but a Landlock rule binds to the inode that exists when the rule is added. A read grant on /proc/self/maps therefore covered only the first process: every child it forked has a different /proc/<pid>/maps and was denied (issue #218). Landlock has no way to say "the caller's own entry", and granting all of /proc instead would widen the policy and lean on a check-then-Continue that a CLONE_VM peer can race. What a task reads under /proc/self describes the task itself, so there is nothing for a grant to protect, while runtimes that read their own maps or status fail in obscure ways without one. Serve these reads for every task instead of asking for a grant: the open handler opens the path inside /proc/<tgid> of the caller with openat2 (RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS) and injects the fd. Whatever it does not serve is left to Landlock, which holds no rule for it, so a write is refused and cwd, root, exe and fd/N are judged by their real target. The net subtree is the host's network namespace rather than the task's own data; it is already spelled /proc/net by the time this runs and keeps needing a /proc grant. fs_deny is honored under the numeric spelling of the caller's directory too, which the string precheck would miss. /proc/self entries in the read list no longer become Landlock rules, so the first process is not treated differently from its children. Entries under net are the exception: nothing else serves them, so they stay rules and behave as before. So does everything when no supervisor runs, since the rule is then all there is. Chroot mode already services /proc on behalf of the child and is left alone. Signed-off-by: Cong Wang <cwang@multikernel.io>
Issue #218 went unnoticed because every existing test read /proc/self from the first process, the one pid the old Landlock rule happened to cover. Exercise forked children and a non-leader thread, and pin the limits: writes, links that lead out of /proc, the net subtree and fs_deny entries stay refused. Each refusal is paired with a read that must succeed, since /dev/null is not writable in these sandboxes and a redirect there fails regardless of what is under test. Signed-off-by: Cong Wang <cwang@multikernel.io>
Every task can now read its own /proc/self without a grant, and such entries no longer become Landlock rules, so recording them only adds dead lines to the profile. Reads under net still need a grant and writes are untouched. Signed-off-by: Cong Wang <cwang@multikernel.io>
congwang-mk
force-pushed
the
proc-self-reads
branch
from
September 20, 2026 21:24
6f666d8 to
edffdb8
Compare
learn kept its own copy of the rule for reads the supervisor serves unasked. The two agree today, but the core's side is about to learn about more entries than net, and a copy that lags would make learned profiles drop a read that needs a grant, or carry grants that do nothing. The core's predicate is now public and learn calls it. Signed-off-by: Cong Wang <cwang@multikernel.io>
The own-entry reads treated everything under /proc/self as the task's own data except net. The mount files are the same kind: they show the host's mount namespace, and sandlock replaces them with a virtual table. Only /proc/mounts, /proc/self/mounts and /proc/self/mountinfo reached that virtualization, so /proc/thread-self/mounts, the numeric and task/<tid> spellings, and all of mountstats were served as they are, to every sandbox and with no /proc grant. One list now names the per-task entries that belong to a namespace rather than to the task, and every spelling of them is rewritten to the name the virtualization matches. mountstats had no virtual form, so it gets one, generated from the same --fs-mount list as the other two. The own-entry reads refuse the whole list, so an entry added to it later cannot reach the real file before it has a virtual one. Signed-off-by: Cong Wang <cwang@multikernel.io>
The file is one of a task's own reads, so every sandbox could open it with no grant, and it names the host's cgroup path: the user slice, the session scope, the terminal that launched the sandbox. None of that describes the task. Every per-task spelling now returns "0::/", which is what a task reads from inside a cgroup namespace of its own. Signed-off-by: Cong Wang <cwang@multikernel.io>
congwang-mk
marked this pull request as draft
September 21, 2026 05:44
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.
Fixes #218.
Problem
/proc/selfresolves per caller, but a Landlock rule binds to the inode that exists when the rule is added. A read grant on/proc/self/mapstherefore covered only the first process; every forked child has a different/proc/<pid>/mapsand was denied. Landlock cannot express "the caller's own entry", and granting all of/procinstead would widen the policy and rely on a check-then-Continue that aCLONE_VMpeer can race.Change
What a task reads under
/proc/selfdescribes the task itself, so there is nothing for a grant to protect, while runtimes that read their ownmapsorstatusfail in obscure ways without one. These reads are now served for every task, with no grant:handle_proc_openopens the path inside/proc/<tgid>of the caller (task/<tid>for/proc/thread-self) withopenat2(RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS)and injects the fd. The kernel never re-reads the child's path string.cwd,root,exeandfd/Nare judged by their real target.fs_denyis honored under the numeric spelling of the caller's own directory too, which the string precheck would miss./proc/selfentries in the read list no longer become Landlock rules when a supervisor runs, so the first process is not treated differently from its children. Withno_supervisorand in-processconfinethe rule stays, since it is then all there is. Chroot mode already services/procon behalf of the child and is unchanged.learnstops recording these reads.Also fixed: /proc/net virtualization bypass
First commit, independent of #218.
/proc/netis a link toself/net, and every/proc/<pid>and/proc/<pid>/task/<tid>carries the same tree, but the handler only matched the/proc/netspelling. With/procreadable,/proc/self/net/devand/proc/self/net/tcpshowed the host's interfaces and sockets that/proc/net/devand/proc/net/tcphide. All per-task spellings are now rewritten to/proc/netbefore matching. This also keeps thenetsubtree (the host's network namespace, not the task's own data) out of the always-allowed reads; it still needs a/procgrant.Not covered
Write grants under
/proc/selfstill only cover the first process: #232.Tests
test_procfs.rs: forked child, the fd describing the caller rather than the first pid, a non-leader thread (/proc/selfvs/proc/thread-self), and the refusals (writes, links out of/proc, thenetsubtree,fs_denyunder both spellings), plusno_supervisorkeeping a listed entry. Each refusal is paired with a read that must succeed, since/dev/nullis not writable in these sandboxes. Disabling the link guard and the deny check each makes the matching test fail.test_proc_net_virt_covers_per_task_spellingsreadsnet/devthrough five spellings and expects loopback only.learntests from fix(learn): agent workload bug fixes #209 that asserted/proc/selfreads are recorded are replaced by one asserting they are not.sandlock-core774 lib and 455 integration tests pass;learn_test39 pass. Python and Go suites were not run locally.🤖 Generated with Claude Code