3.13.1 A Private Mount Table
A mount namespace is a private copy of the kernel's mount table: the
map from directory paths to the filesystems mounted there. Linux
answers "who may change this table" with CAP_SYS_ADMIN in the user
namespace that owns it, and lets an unprivileged process manufacture
that capability by creating a user namespace first — which is how
bubblewrap, and every unprivileged container tool built the same way,
gets a private root.
On Peios that route leads nowhere. The capability switchboard discards
the target user namespace and answers from the token's privileges
alone (§3.10.2), so being root in a user namespace confers nothing, and
CAP_SYS_ADMIN maps to SeTcbPrivilege. KACS answers the question the
way it answers every other: the mount namespace is an object with a
security descriptor, and changing it is an access check.
3.13.1.1 The object #
Every mount namespace other than the initial one carries a security
descriptor, minted in copy_mnt_ns() from the creating task's effective
token and released when the namespace is. The initial namespace, and the
anonymous namespaces open_tree(OPEN_TREE_CLONE) and fsmount create
for detached trees, carry none.
The minted descriptor names its creator and nobody else: owner and
group come from the token, and the DACL is a single allow ACE granting
the creator's user SID GENERIC_ALL. SYSTEM and
BUILTIN\Administrators, which the SysV IPC and socket defaults name,
are absent on purpose — the privilege rung of the mount gate admits them
before the descriptor is read, so an ACE would only pretend the
descriptor was what let them in.
The rights are those of <pkm/mntns.h>:
| Right | Grants |
|---|---|
KACS_MNTNS_MOUNT | changing the table: a bind mount, an unmount, pivot_root |
KACS_MNTNS_ENTER | joining the namespace with setns — defined so the descriptor's meaning is fixed; no code consults it yet, and setns into a mount namespace still needs SeTcbPrivilege |
READ_CONTROL, WRITE_DAC, WRITE_OWNER | the standard rights, which KACS_MNTNS_ALL_ACCESS includes; nothing exposes a namespace's descriptor to kacs_get_sd or kacs_set_sd yet |
The generic mapping: GENERIC_WRITE is KACS_MNTNS_MOUNT and
WRITE_DAC; GENERIC_EXECUTE is KACS_MNTNS_ENTER and READ_CONTROL;
GENERIC_READ is READ_CONTROL alone, there being nothing to read from
a namespace beyond its descriptor; GENERIC_ALL is
KACS_MNTNS_ALL_ACCESS.
3.13.1.2 Creating one #
unshare(CLONE_NEWNS), and clone with CLONE_NEWNS, need no
privilege when the mount namespace is the only namespace requested.
Every other namespace type — and any combination that includes one —
keeps the CAP_SYS_ADMIN gate in kernel/nsproxy.c, which the
switchboard answers with SeTcbPrivilege. A user namespace may still be
created alongside, as upstream allows; it changes nothing about what
follows.
A table created by a task that does not hold the mount privilege receives its mounts as slaves of the parent table's, exactly as upstream does for a copy owned by a different user namespace: propagation runs parent to child only, so whatever the creator mounts in its own table never appears in the one it copied. A table created with the privilege is copied as upstream copies it, peers and all.
3.13.1.3 The mount gate #
may_mount() in fs/namespace.c is the gate for mount, umount2,
pivot_root, open_tree(OPEN_TREE_CLONE), fsmount, move_mount and
mount_setattr, and for fsopen and fspick in fs/fsopen.c.
mount_capable() in fs/super.c is the second
gate a new filesystem passes, when its superblock is brought into being.
Under KACS both take the operation as an argument, with the filesystem
type where one is being created, and ask pkm_kacs_may_mount_op() in
three rungs, against the caller's effective token and the descriptor of
the caller's current namespace:
- Privilege. A token holding
SeManageVolumePrivilegeorSeTcbPrivilege, enabled, is admitted to every operation in every namespace, and the privilege is marked used. This rung is decided first and alone: a token that holds the privilege never reaches the descriptor, and a token that does not is never asked the privilege question, so an unprivileged caller's routine bind mount records no privilege refusal. - No descriptor. In a namespace with no descriptor — the initial one — nothing else admits anything. This is the whole of the rule that the root table keeps: without the privilege, no process changes it.
- Descriptor. Otherwise, and only for an operation the descriptor
can admit, AccessCheck for
KACS_MNTNS_MOUNTruns against the namespace's descriptor under the caller's PIP context, exactly as it does for a socket or a SysV object.
The operations the descriptor can admit are the ones that put no kernel filesystem parser of an untrusted image in reach of the caller:
mount(2) shape | Gate operation | Admissible by descriptor |
|---|---|---|
MS_BIND, with or without MS_REC | bind | yes |
umount2 without MNT_FORCE | unmount | yes |
pivot_root | pivot_root | yes |
a new tmpfs, proc or stratafs (do_new_mount) | new filesystem | yes |
| any other filesystem type | new filesystem | no |
MS_REMOUNT, including MS_REMOUNT|MS_BIND | other | no |
MS_MOVE; MS_SHARED, MS_PRIVATE, MS_SLAVE, MS_UNBINDABLE | other | no |
open_tree(OPEN_TREE_CLONE), fsopen, fspick, fsmount, move_mount, mount_setattr | other | no |
The filesystem types a descriptor can admit are an allowlist KACS keeps,
and it holds exactly tmpfs, proc and stratafs: all three read
nothing but the caller's own mount options. tmpfs has no backing image,
proc is a view of the kernel's own state, and stratafs is a view of
directories the caller can already traverse, every access through it
decided on the providing object (§4.6.1). A type that parses an image —
ext4, squashfs, iso9660, ntfs3 — is refused before the type is even
looked up. The list is an attack-surface list and not a policy knob:
who may change a table is decided by the table's descriptor alone, and
the same descriptor that admits tmpfs refuses ext4.
A stratafs stack admitted this way is read-only or absent-tolerant.
stratafs's own admission (§4.2.3) refuses a create stratum to any
caller outside the initial user namespace or without CAP_SYS_ADMIN,
which is SeTcbPrivilege, and that test runs after the gate, so an
unprivileged caller that asks for one gets EPERM from stratafs
whatever its table's descriptor grants. Copy-up carries no add-entry
right of its own, which is why a writable stack is authority in itself
and stays with the TCB; a private table's writable paths are tmpfs, or
bind mounts of directories the caller owns. Admitting the type does put
stratafs's option parser and stack validation in reach of unprivileged
input for the first time; both fail closed with EINVAL on a relative
stratum, a duplicate, create combined with ro, an empty stack or
more than sixteen strata, and with ENOENT on a missing stratum not
marked am.
Everything in the no rows needs the privilege whatever the descriptor
grants, and MNT_FORCE additionally keeps its own CAP_SYS_ADMIN test
on the superblock's user namespace. A mount that the gate admits still
goes through every check upstream applies afterwards: the bind source is
looked up through the caller's own table, an unmount needs a mount
point that belongs to it, and pivot_root needs the propagation
conditions it always needed, which the slave copy of an unprivileged
table satisfies.
3.13.1.3.1 An unprivileged tmpfs is stamped for its creator #
A fresh tmpfs is deny-missing under FACS (§3.9.5): nothing on it carries
a descriptor, so nothing on it is reachable until someone sets the
superblock's mount policy, which kacs_set_mount_policy reserves to the
mount privilege. That is right for the system's own overlays, which are
seeded deliberately, and it would leave a tmpfs admitted by a namespace
descriptor permanently empty. So at sb_kern_mount, a tmpfs brought
into being by a token that holds no mount privilege is stamped
synthesize-ephemeral with a template descriptor the kernel mints itself:
owner and group from the mounter's token, one allow ACE granting the
mounter's user SID GENERIC_ALL — the same shape as the namespace
descriptor. The policy generation moves to 1, which retires the
deny-missing default the root inode's creation cached during
fill_super. The mounter cannot choose the template, and a superblock
whose policy kacs_set_mount_policy has already set is left alone. A
privileged mounter's tmpfs is untouched: it keeps the magic default and
the mounter seeds it, as before. proc is unmanaged and needs no
stamp; a second proc mount in the same pid namespace shows the same
processes under the same per-process checks.
So a process whose whole world is a directory of its choosing is:
unshare(CLONE_NEWNS);
mount(dir, dir, NULL, MS_BIND, NULL);
chdir(dir);
pivot_root(".", ".");
umount2(".", MNT_DETACH);
chdir("/");
with no user namespace and no privilege. The bind makes the directory a
mount point, which pivot_root requires; pivoting onto the same path
stacks the old root on the new one, and detaching . drops it.
3.13.1.4 What the object does not change #
The token never changes at exec (§3.10.3), so a program run inside a
private table has exactly the authority of the process that built it,
and a fake /etc fools nobody but its author. Every access to a file
reached through a bind mount is decided on the real object's
descriptor, so a bind mount shows a name and denies at open. The
kernel's own helpers — modprobe and its kin — are executed in the
initial namespace and refused unless TCB-signed (§3.7). A signed
binary's PIP tier is derived from the binary and not from the table it
was found in, and library verification, where a process enables it,
compares signatures at mmap rather than paths.
3.13.1.5 Tracing #
The kacs:kacs_mntns event records a descriptor minted at namespace
creation, each rung of the gate, and a tmpfs stamped for its creator,
with the operation code, the right asked of the descriptor where a rung
reached it, a reason from <pkm/trace.h> — sd-alloc, sd-alloc-fail,
gate-privilege, gate-no-sd, gate-op-not-admitted,
gate-sd-decision, gate-pip-context, gate-fs-not-admitted,
sb-stamp, sb-stamp-fail — and the verdict.