Skip to content

Add mount operations to the broker core - #1838

Draft
Weidong Cui (wdcui) wants to merge 10 commits into
wdcui/ulitebox/mount-treesfrom
wdcui/ulitebox/mount-ops
Draft

Weidong Cui (wdcui) wants to merge 10 commits into
wdcui/ulitebox/mount-treesfrom
wdcui/ulitebox/mount-ops

Conversation

@wdcui

Copy link
Copy Markdown
Member

This PR adds mount operations to the broker core's mount trees from #1837 (which it is stacked on). A thread can mount a new in-memory file system or a given backend on a directory, bind a file or directory (optionally with the mounts below it) onto another, make a mount read-only (refused while files are open for writing; every modification through a read-only mount then fails), unmount the topmost mount on a node (refused while it has children or users, unless detached, which takes its subtree along while open files keep working), change its root or working directory to any directory handle, and pivot the root of its mount namespace with Linux's checks and errors, which moves every context rooted or working on the old root. Mount points are pinned by backend and path in a registry shared by everything built from the same inode allocators, so a mount point can't be removed or renamed through any alias, and attachments re-check their point after pinning. NamespaceKinds gains mount and file-system-context kinds: unsharing them gives the thread a private context, and a copy of its mount namespace whose mounts share the original backends (open files stay on the old mounts), and a new child can get such a copy too. Mounts and mount namespaces count against new broker-wide, per-job and per-process limits. Tree changes are serialized per namespace and published as copy-on-write snapshots, context copies and pivots are serialized through the core's context registry, and no backend call or final drop happens under a lock.

Weidong Cui (wdcui) and others added 10 commits November 10, 2026 10:56
Threads can now change their mount namespace's tree: mount a new empty in-memory file system or a given backend on a directory, bind a file or directory elsewhere, optionally with the mounts below it, make a mount read-only, and unmount, refusing a busy mount unless detaching it and its subtree. Each change resolves its operands, pins the point it attaches at and checks the node is still there, then commits under the namespace's lock only if the tree hasn't changed meanwhile. Mount points are registered per backend, so removing or renaming one, or a directory above one, fails as busy through any mount, bind or namespace, and read-only mounts refuse writes, while a file open for writing keeps its mount writable. Threads can also change their root and working directory to any directory. Hosts can give guest-mounted backends devices from their composer's allocators.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f5406de6-bab4-4f3b-b232-ae238032c2c2
A thread can now make a mount below its root the root of its mount namespace, as Linux's pivot_root does: the caller's root mount moves to the given directory at or below the new root, the new root's mount takes the old root mount's place, and every root and working directory at the old root, in any process and namespace, moves to the new root. With both operands the caller's working directory at a mount's root, the old root is stacked on the new root's root, which lets runc pivot into a bind of its root file system and then detach the old root. The operands are checked as Linux checks them: the caller's root must be its mount's root, the new root a mount's root below it and not the root mount itself, and the old root's new place at or below the new root.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f5406de6-bab4-4f3b-b232-ae238032c2c2
NamespaceKinds gains a mount namespace kind and a file-system context kind, each a new bit in the namespace byte, which decoding checks strictly. Unsharing the context kind gives a thread a private copy of its context if it shares one. Unsharing the mount kind gives it a private context in a copy of its mount namespace, whose new mounts show the same nodes in the same tree and change independently, with its root and working directory moved onto the copies, while open files stay on the original mounts; moving the initial thread's context moves the process's default context too. A child allocated with the mount kind gets such a copy of its creating thread's context.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f5406de6-bab4-4f3b-b232-ae238032c2c2
Mounts and binds, the copies a recursive bind makes, and the mount namespaces
and mounts made by unsharing or creating a child with a new mount namespace are
now charged to the process that made them, its job and the broker, with new
broker-wide, per-job and per-process limits in BrokerCoreLimits like the PID
namespace ones. Each charge is released when the mount or namespace drops, and
an operation that would exceed a limit fails with ResourceExhausted without
changing anything, so a sandboxed process can't grow the broker's mount state
without bound.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f5406de6-bab4-4f3b-b232-ae238032c2c2
Copying a mount namespace now holds the source namespace's mutation lock from
its tree snapshot through the last pin, re-pinning each point its original
already pins, so no unmount, removal or rename can slip in between and the
copy never fails or pins a point that no longer exists; partial copies drop
after the lock. The registry of mount points now lives with the inode
allocators, so every namespace and file service created from the same
allocators shares it, and this is documented where hosts choose them.
Attaching a mount or pivoting now checks that the pinned point still exists
by walking its path inside the backend, rather than trusting a retained
directory handle that may name a removed directory. Opening a file read-only
with truncation no longer counts as a writer on its mount once the open is
done, so it doesn't stop the mount from being made read-only, and O_PATH
opens never do.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f5406de6-bab4-4f3b-b232-ae238032c2c2
Copying a context with its mount namespace now takes the context's root and
working directory under the source namespace's mutation lock, together with
the copy of its tree, so a concurrent pivot (which moves contexts under the
same lock) can't leave the copy rooted at the old root's copy beneath the new
root. Changing the root or working directory to a directory on a backend that
enforces permissions itself, such as 9P, can't check search permission on it,
as 9P has no way to ask; this is now documented with the other differences on
such backends, rather than probed with a hack.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f5406de6-bab4-4f3b-b232-ae238032c2c2
Copies of file-system contexts are now taken and registered under the core's
context registry lock, which a pivot holds while it moves roots and working
directories, so a fork, a context unshare or a mount namespace copy racing a
pivot can't register a copy still rooted at the old root. A thread's new
context becomes the process default under the threads lock, so process
teardown can't miss it or have it put back afterwards. A mount's read-only
flag and writer count now share one atomic word, so a write starts only while
the mount is writable and the mount becomes read-only only with no writer,
without a refused change briefly failing writes. Pivot now checks its operands
in Linux's order, returning Busy when the new root or the mount on top at
put_old is the caller's root mount before the other invalid-argument checks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f5406de6-bab4-4f3b-b232-ae238032c2c2
A new root process's file-system context now reads its namespace's root and
registers itself under the namespace's mutation lock and then the context
registry lock. A pivot moves the registered contexts and publishes its new
tree under the same mutation lock, so a context created concurrently either
starts at the new root or is moved there, rather than starting at the old root
after it moved under put_old and keeping it busy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f5406de6-bab4-4f3b-b232-ae238032c2c2
Unsharing now prepares every namespace it gives first, then installs the
thread's new file-system context (and the process default, for the initial
thread) and its children's PID namespace under one acquisition of the threads
lock, all or nothing. A thread that exits meanwhile, or whose children went to
another PID namespace meanwhile, makes the unshare fail without changing
anything, and what wasn't installed or was replaced drops outside the lock.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f5406de6-bab4-4f3b-b232-ae238032c2c2
Creating a child for a thread, and unsharing a thread's namespaces, now read
the thread's children PID namespace and its file-system context under one
acquisition of the threads lock, then copy and allocate outside it. A
concurrent unshare naming the same thread, which an untrusted runner can send,
can no longer give a child a PID namespace and a file-system context that the
thread never held together.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f5406de6-bab4-4f3b-b232-ae238032c2c2
@github-actions

Copy link
Copy Markdown

ℹ️ Note: This semver check was run against the wdcui/ulitebox/mount-trees branch, not main or ulitebox.

🤖 SemverChecks 🤖 ⚠️ Potential breaking API changes detected ⚠️

Click for details
--- failure enum_variant_added: enum variant added on exhaustive enum ---

Description:
A publicly-visible enum without #[non_exhaustive] has a new variant.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#enum-variant-new
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.51.0/src/lints/enum_variant_added.ron

Failed in:
  variant UnlinkError:Busy in /home/runner/work/litebox/litebox/litebox_broker_core/src/fs/errors.rs:167

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant