* [PATCH RFC v3 00/90] cells: a container as one kernel object
@ 2026-11-05 14:56 Josef Bacik
2026-11-05 14:56 ` [PATCH RFC v3 01/90] Documentation: add a design document for cells Josef Bacik
` (89 more replies)
0 siblings, 90 replies; 93+ messages in thread
From: Josef Bacik @ 2026-11-05 14:56 UTC (permalink / raw)
To: Christian Brauner, James Bottomley, Andrew Halaney,
Jeremy Bingham, Tahera Fahimi, Serge Hallyn
Cc: linux-fsdevel, linux-kernel, Josef Bacik
v1: https://lore.kernel.org/all/20261019-cells-rfc-6f4c85415f29@toxicpanda.com/
v2: https://lore.kernel.org/all/20261026-cells-rfc-v2-b41d7e0c95a3@toxicpanda.com/
v2->v3:
- It's the patches this time and not a link to a branch, 90 of them on
top of v2 of the cred series [2]. Sorry. I tried to post it in pieces,
and every cut either left a first series that wasn't a boundary yet or
needed the core rewritten. So it's one series, grouped so that you can
find your part, see below.
- What a cell may do to its network is a slot, "net-rights", and not
CAP_NET_ADMIN. This is the big one, see below. A network namespace
that the cell owns, which v2 had as an option, is gone.
- No capabilities unless the builder names them in the new "caps" slot,
which is the bounding set as well. uid 0 of a cell comes with nothing.
- File capabilities are ignored in a cell, and a cell can drop from its
bounding set without CAP_SETPCAP.
- A spawn can name the cgroup it starts in, at or below the budget, so
spawning into a cell that runs systemd works. (James, Christian)
- A cell can make cgroups below its budget. It can't touch the limits of
the budget itself.
- The host runs no usermode helper for a task of a cell, so no modprobe
and no request-key, and a core dump doesn't go to a pipe or a socket.
- cell_info() tells the builder which block of ids the cell got, for
netfilter and routing rules that go by uid.
- A uid= or gid= mount option can't name an id of a cell.
- The owner and the group of a tun device only count in the cell that
set them. A cell of one id could attach to the tap devices of its
maker.
- New document with examples, patch 2.
- Rewrote the commit messages, and reordered the series by topic.
- Rebased onto v7.4-rc1.
v1->v2:
- Spawning is an empty pidfd, pidfd_open(cellfd, PIDFD_EMPTY) and
pidfd_configure(). CLONE_EMBRYO and task_configure() are gone and
clone3() is untouched. (Christian)
- cell_kill() is a system call of its own.
- A pod is one cell and not several that share a network namespace.
(Andrew, James)
- The network is set up from the outside and handed to the cell, like
the mount tree.
- No "net" slot means no network, no sockets but AF_UNIX.
- A cell changes nothing in cgroupfs outside of its budget and nothing
in resctrl, which took a hook in kernfs.
- CLONE_NEW* being EPERM in a cell is written down as a position and not
as something that is missing. (Andrew)
- Fixed SO_PEERPIDFD and friends racing with connect(), and files owned
by an id of a cell being left behind outside of the budget.
What it does now:
A cell is a container as one object. A task is born in a cell and never
leaves it, and no existing task can move into one, so there is no
setns() and no unshare(). You build it from the outside by filling named
slots from file descriptors you already hold, you seal it, and then you
spawn tasks into it, at any point while it's running and not just to get
it going. Holding the cellfd is the authority to do all of that, there
is no uid or capability check on it, and making one takes no privilege.
The cell lives exactly as long as somebody holds the cellfd, when the
last one goes the cell is killed.
Whatever a cell has, somebody put there. The mount tree is built from
scratch by the builder and the cell can't mount. The network is set up
by the builder. A new task starts with no mm and no files, so what it
has is what you gave it. It has no capability that the builder didn't
name. Say nothing about the network and there is none.
Spawning is pidfd_open(cellfd, PIDFD_EMPTY), which returns a pidfd that
no task is attached to yet, pidfd_configure() to say what the task is to
be like, and PIDFD_CMD_START to make it.
The branch has the cred series in it, and what v2 described is tagged
cells-rfc-v2 in the same tree.
https://github.com/josefbacik/linux/commits/cells/
git fetch https://github.com/josefbacik/linux.git cells
The network:
Patches 73-85. I expect this to be the part people have opinions about,
so it's all in one place and nothing before it depends on it.
Nearly everything that changes a network namespace is behind
ns_capable(net->user_ns, CAP_NET_ADMIN). That's well over a hundred
checks, for devices, addresses, routes, netfilter, tc, tunnels, IPsec,
socket options and so on, and none of them says which of those it is
for. So a container that needs to add one iptables rule gets all of it,
and to have any of it, it has to own the network namespace, which means
the runtime can't set that namespace up with the tools it has.
Patches 75-82 don't know about cells and change no behaviour. They add
net_may(net, right), where the right says what the check is for: link,
link-new, addr, route, filter, tc, tun, sysctl, sockopt, raw. It comes
down to the same capability over the same user namespace as before.
About fifty checks are converted, the ones behind rtnetlink, the old
ioctls, nfnetlink and the xtables sockopts, tun, the net sysctls, socket
options, and raw and packet sockets.
Then a cell gets the "net-rights" slot (83), and net_may() learns one
rule (84): for a task of a cell it takes the right in the slot, on the
network namespace in the cell's "net" slot and no other, and the
capability in the cell's own user namespace. The slot says what the cell
may do, the capability says who in the cell may do it, so an init
container can add its rules and drop CAP_NET_ADMIN, and LSMs and audit
see the check they have always seen.
The namespace stays the runtime's, CNI sets it up as ever and can go
back in at any time. A check that nobody has converted keeps asking for
the capability over the owner, which no task of a cell has, so what
nobody has thought about is refused. And you can only grant what you
could do yourself.
I have not cc'd netdev on this round, I would like to hear from the
people who have been following along first. What I will be asking them:
- Is naming what a CAP_NET_ADMIN check is for something they would take
at all, cells or no cells? It seems useful to me on its own, an LSM
could go by it for one. If the answer is no I need a different plan.
- Are the rights cut in the right places? They follow the entry points,
because that's where the checks are. "filter" is all of netfilter as
far as it is per namespace, which is a lot of code for one bit.
- rtnetlink. RTM_NEWLINK both changes a device and makes one, so the
gate takes either right and __rtnl_newlink() checks the one that
applies once it has looked. And a request can name another namespace
as its target by nsid, which anybody can read off of a veth, so the
check has to be about the target. It is, and there's a test that
makes a device on the host if it isn't, but I would like somebody who
knows that code to look at it.
What I would like opinions on:
- Sealed means sealed. A runtime that wants the network namespace
later, for CNI DEL say, keeps the file it got while building. A cell
that dies takes its network along, and ptp's DEL without the
namespace succeeds, gives the address back and leaves its masquerade
rules on the host. I think "keep the file" is the right answer, the
alternative is a dead cell that still holds things.
- Pods. The "cgroup" key is in. The "root" key, a directory in the
cell's tree for a spawn to start in, is still not written.
- Capabilities and a cell of one id. Its tasks aren't uid 0 in there,
so they would lose whatever they were given at the first exec. The
seal refuses capabilities for such a cell rather than let them
vanish. That means an unprivileged user's cell can't have any, unless
it has a block.
- Still open, the long versions are in v2: namespaces inside of a cell
for bubblewrap and chrome (3), one static struct net for cells
without a network (4), ucounts rolling up to the maker (5), the id
window versus a separate id space (6), capabilities held in an
ancestor user namespace (7), MNT_LOCKED (8), seccomp and
PIDFD_CMD_START (9), and parking a cellfd somewhere (10).
How this is laid out:
One logical thing per patch, and more than half of them are 50 lines or
less. 5,600 lines of kernel, 1,400 of documentation, 6,900 of tests.
1-2 The design document and a page of examples. Start here.
3-11 The user namespace of a cell and the ids the kernel lends
it. user_namespace, ucount, keys.
12-15 Those ids never end up on a filesystem the cell does not
own. vfs.
16-20 Helpers for the budget. cgroup.
21-23 Helpers for the tree and the namespaces. vfs, nsproxy.
24-33 The cell: the object, cell_create(), cell_kill(),
cell_configure(), the slots it cannot do without, the seal,
cell_open().
34-46 Spawning: empty pidfds and pidfd_configure(). pid, fork,
x86, uprobes, pidfd, ptrace.
47-51 Nobody gets in and nobody gets out. nsproxy, vfs, cgroup.
52-57 A cell runs the cgroups below its budget and touches nothing
else. kernfs, cgroup, resctrl.
58-60 A uid means nothing across cells. cred, ptrace, keys.
61-63 A tree that belongs to the cell, and nothing a cell writes
imparts privilege. vfs.
64-66 Capabilities: none unless the builder names them.
67-70 What does not cross on a socket or through a seccomp
listener. af_unix, net, seccomp.
71-72 The host runs no program on behalf of a cell.
73-85 The network, all of it. net, netfilter, tun. See above.
86-90 cell_info(), Kconfig, the syscall tables, MAINTAINERS and
the tests.
CONFIG_CELLS doesn't exist until near the end, so everything before that
builds as stubs, and every commit builds on its own, with the option off
and with the cells code forced on.
Testing: the selftests are seven programs and 100 tests now. I've run
them under KASAN, lockdep and DEBUG_VFS in qemu, on tmpfs, ext4 and 9p,
and built it with gcc and clang and with the option off. I've booted
systemd 255 in a cell to multi-user.target, with units under other users
and limits of their own, and shut it down in order. I've set up pod
networking with the reference CNI plugins, which work unchanged, and in
a namespace that ptp set up I've run nft and iptables with "filter" and
nothing else, a VPN style tun with tun, link, addr and route, a tap and
a bridge for a VMM with tun, link and link-new, and something like a CNI
agent in the host's namespace. What all of that took is in the examples.
The resctrl patch is compile tested only. It has still not been near
real hardware, a real runtime or a benchmark. Only x86 and the generic
syscall table are wired up. There are no rights on the cellfd and no LSM
hooks of our own. "Not there yet" at the end of the document is the full
list.
[2] https://lore.kernel.org/all/20261104-b4-cred-ids-v2-0-956213608f16@toxicpanda.com/
--- Original email (v1) ---
Hello,
This is the thing Christian and I and a few others have been going back
and forth on, a container as one kernel object instead of a pile of
namespaces that userspace holds together. I have a barebones
implementation of it. It's 62 patches on top of the 13 patch cred series
[1], and I don't think anybody wants that in their inbox for what is at
this point a design discussion, so the code is in a branch, and what's
in this email is the pitch, the things I would like opinions on, and the
design document from the branch in full at the bottom so you can quote
it and yell at specific lines.
https://github.com/josefbacik/linux/commits/cells/
git fetch https://github.com/josefbacik/linux.git cells
That's 6cbfd1952c30 as I write this, based on vfs.all. "Cell" is a
placeholder name, I don't love it, suggestions welcome.
The pitch
---------
A container isn't a thing in the kernel. It's eight kinds of namespace,
a cgroup, a seccomp filter and a pile of conventions that the runtime
holds together. The kernel can't name it, can't kill it in one go, and
can't tell you what's in it. Privilege inside of it hangs off of uids
and user namespaces, and a task can be half way between two sets of
namespaces at any given moment, which is where a depressing number of
our bugs come from.
A cell is a container as one object. A task is born in a cell and never
leaves it, and nothing can enter one, so there is no setns() and no
unshare(). You build it from the outside by filling named slots from
file descriptors you already hold, you seal it, and then you spawn tasks
into it. Holding the cellfd is the authority to do all of that, there is
no uid or capability check on it, and making one takes no privilege. The
cell lives exactly as long as somebody holds the cellfd, when the last
one goes the cell is killed.
Spawning is the other half. clone3(CLONE_EMBRYO) gives you a pidfd on a
task that exists but has never run anything, you set it up with
task_configure() the way you would set up a mount with fsconfig(), and
then you start it and it execs. It starts with no mm and no files, so
what it has is what you gave it. I think this is useful without cells at
all and would like to send it on its own eventually.
A few rules fall out of wanting the boundary to actually mean something.
The kernel allocates the kuids for a cell out of a reserved window, the
builder never names one, and those ids can never be stored on a
filesystem the cell doesn't own. A uid means nothing across cells, not
an equal one and not 0. Files impart no privilege, every task has
no_new_privs, and set-id bits get dropped on anything a cell creates,
chmods or opens for writing. Unix sockets carry bytes between cells and
nothing else, no SCM_RIGHTS and no peer credentials. The mount tree is
built from scratch, nothing in it hides anything, and nothing propagates
into it.
What I would like opinions on
-----------------------------
1. The shape. One object, born in it and never leaves, nothing enters,
built from the outside out of fds, authority is the fd. If you think
this is the wrong shape then the rest doesn't matter, so start here.
2. The id window. Blocks of 65536 kuids come out of the upper half of
the id space, and "nobody else holds an id of this block" is what
replaces the privilege check for mapping a range, so it has to be
enforced. With CONFIG_CELLS on the host can't chown a file to an id
of 2^31 or above anymore, because the check has to go by value. This
is the one I expect to get yelled at for, and I'm open to better
ideas.
3. Capabilities. A capability held in an ancestor user namespace still
applies to what a cell owns, so root on the host can still signal and
ptrace by pid. In the checks that let one task act on another I
closed the branch that goes by a uid, euid 0 included, and left the
capability branch alone. I would like the boundary to be hard in both
directions, you hold the cellfd or you have nothing, but that is a
much bigger conversation. How far do we want to go?
4. MNT_LOCKED (Christian, this one is for you). The seal refuses a tree
with a locked mount in it, since a locked mount hides what it covers.
But for a builder that works in a user namespace of its own, the
kernel locks the tree itself onto the empty root of a namespace made
with FSMOUNT_NAMESPACE or OPEN_TREE_NAMESPACE, so an unprivileged
builder could never seal anything. I exempted exactly that mount, on
the grounds that there is nothing under it to hide. Is that right, or
should it not be locked there to begin with?
5. seccomp. An embryo inherits the filters of whoever made it and they
apply to everything the new program does, but they are not consulted
for what TASK_CMD_START does on the way there, because the exec isn't
a system call of the embryo. So a filter that denies execve() and
execveat() to keep a task from running another program has to learn
about task_configure() as well, same as it once had to learn about
execveat(). This is only about tasks of the host, a task of a cell
can't spawn at all. Documented for now. Good enough, or do we run the
filters at the start?
6. Lifetime. A cell dies with its last cellfd, there is no opt out and
no way to look one up by id. An earlier version had this as a flag,
and without it you ended up with a running cell that nobody held, so
nothing could spawn into it or wait for it, and killing it meant
going around it by pid or cgroup. You can pass the fd to a
supervisor. I would rather add the foot gun later if somebody really
needs it.
7. The budget. Spawning from the outside fails with EBUSY once the cell
enables controllers on its own cgroup, which systemd does right away.
It needs a leaf below the budget for spawns to land in. Does the
kernel make that leaf, or is it a convention for the builder? Also a
cell with a block of ids can't mkdir in cgroupfs until cgroupfs can
be idmapped.
8. procfs, sysfs, devtmpfs and cgroupfs. There are no per-cell instances
yet, the builder assembles those with what exists today and can get
it wrong the same way a runtime can today. Where I want to go is each
of the four taking the cell as a mount parameter and showing only
what the cell holds. That's a lot of work and I wanted this out
first, but I would like to know if anybody hates the direction.
9. The API. Five system calls, cell_create(), cell_configure(),
cell_info(), cell_open() and task_configure(), no ioctls, with string
keys in the style of fsconfig(). The right to spawn is a slot as
well, reserved and always empty for now, which is why cells don't
nest. We do want unprivileged things to be able to build smaller
cells out of what they hold eventually, but it touches ids, cgroups
and mounts all at once and I left it out.
How to read the branch
----------------------
It's one logical thing per patch and more than half of them are 50 lines
or less, so it is less than it sounds like. The first 13 are the cred
series [1]. Read the document, then these are the ones with the design
in them:
3c1f5defa226 user_namespace: let a cell own a user namespace
380bd1bf37a7 fs: seal a mount namespace for a cell
0837cfd6d117 cells: add the cell object and cell_create()
1c42ab4f15a9 cells: add cell_configure()
cef86b491459 cells: lend a cell a block of ids
253d7f9627c0 fork: let copy_process() place a task in a cell
424a90b3e37a fork: add CLONE_EMBRYO
8bef8549476e fork: add task_configure()
The rest are helpers in cgroup, fs and nsproxy ahead of those, and the
rules of the boundary after them, one rule per patch. The uid rule is
the reason for the cred series: with the dozen hand rolled copies of
"the uids match so you can mess with that task" in one place, it is one
small patch,
5372a6763133 cred: let no id count across cells
CONFIG_CELLS doesn't exist until near the end, so everything before that
builds as stubs, and every commit builds on its own. The last patch is
the selftests, 47 of them.
State
-----
I've run the selftests under KASAN, lockdep and DEBUG_VFS in qemu, on
tmpfs, ext4 and 9p, and built it with gcc and clang and with the option
off. It has not been near real hardware, a real runtime or a benchmark.
Only x86 and the generic syscall table are wired up. There are no rights
on the cellfd and no LSM hooks of our own. "Not there yet" at the end of
the document is the full list, and I tried to be honest in it. This is
an RFC in the truest sense, the point is to have code to argue about, so
please argue. Thanks,
Josef
[1] https://patch.msgid.link/20261018-b4-cred-ids-v1-0-7bdf2db4ea9f@toxicpanda.com
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
Josef Bacik (90):
Documentation: add a design document for cells
Documentation: add examples for cells
cells: reserve a window of ids
user_namespace: let a cell own a user namespace
user_namespace: refuse setns() into a cell
capability: give the owner no say over a cell
ucount: count the id blocks a user holds
user_namespace: let the kernel map the ids of a cell
user_namespace: keep legacy maps out of the window
user: add uid_range_in_use()
keys: add key_user_range_in_use()
fs: add kuid_has_fsmapping() and kgid_has_fsmapping()
fs: never store an id that was lent to a cell
fs: take no id of a cell for a mount option
fs: let an idmapping pin the ids of a cell
cgroup: let a cgroup become the budget of a cell
cgroup: add cgroup_cell_chown()
cgroup: give the budget of a cell back
cgroup: add cgroup_cell_kill()
cgroup: root a cgroup namespace at a given cgroup
fs: seal a mount namespace for a cell
nsfs: make open_namespace() available outside of fs/
nsproxy: build the nsproxy the tasks of a cell share
cells: add the cell object and cell_create()
cells: add cell_kill()
cells: add cell_configure()
cells: add the "budget" slot
cells: add the "root" slot
cells: add the "ids" slot, for a single id
cells: lend a cell a block of ids
cells: reserve the "spawn" slot
cells: add CELL_CMD_SEAL
cells: add cell_open()
pid: allocate a struct pid ahead of its numbers
fork: let copy_process() place a task in a cell
cgroup: fork a task of a cell into the budget
x86: give a task without an mm no user state
uprobes: cope with a new task that has no mm
fork: add tasks that start out with nothing
pidfd: add PIDFD_EMPTY
pidfd: add pidfd_configure()
pidfd: let pidfd_configure() place files
pidfd: let pidfd_configure() set credentials
pidfd: let pidfd_configure() name a cgroup to start in
ptrace: keep everybody off an embryo
cells: end a cell when its init exits
cells: refuse a task of a cell a change of namespaces
fs: refuse a task of a cell a new mount namespace
nsproxy: refuse to enter a namespace of a cell
cgroup: keep a task of a cell within the budget
cgroup: let nothing but a spawn into a budget
kernfs: let a filesystem refuse a change to a node
cgroup: let a cell change nothing outside its budget
cgroup: leave the limits of a budget to who is above
fs: let a filesystem hold ids that go with the cell
cgroup: let a cell make cgroups below its budget
resctrl: let a cell change nothing
cred: let no id count across cells
ptrace: refuse PTRACE_TRACEME across cells
keys: ignore an equal uid or gid across cells
fs: accept a cellfd for MOUNT_ATTR_IDMAP
fs: let a cell set no set-id bits
fs: drop privileges from what a cell may write to
cells: add the "caps" slot
capabilities: take nothing from a file in a cell
capabilities: let a cell shrink its bounding sets
af_unix: deliver ancillary data only within a cell
af_unix: tell a sender that control data will not cross
net: do not identify a peer in another cell
seccomp: let a listener put no file into a cell
umh: run no program for a task of a cell
coredump: let no core dump leave a cell
cells: add the "net" slot
net: give a cell without a network no sockets
net: add net_may()
rtnetlink: use net_may() for requests that change state
net: use net_may() in the configuration ioctls
netfilter: use net_may() for configuration
tun: use net_may() to create and attach to devices
net: use net_may() for the net sysctls
net: use net_may() for privileged socket options
net: use net_may() for raw and packet sockets
cells: add the "net-rights" slot
net: honour the rights of a cell in net_may()
tun: ignore an equal uid or gid across cells
cells: add cell_info()
cells: add CONFIG_CELLS
arch: wire up the cell system calls
MAINTAINERS: add an entry for cells
selftests: add tests for cells
Documentation/userspace-api/cells-examples.rst | 551 +++++++
Documentation/userspace-api/cells.rst | 861 +++++++++++
Documentation/userspace-api/index.rst | 2 +
MAINTAINERS | 14 +
arch/x86/entry/syscalls/syscall_32.tbl | 6 +
arch/x86/entry/syscalls/syscall_64.tbl | 6 +
arch/x86/include/asm/shstk.h | 2 +
arch/x86/kernel/process.c | 24 +-
arch/x86/kernel/shstk.c | 14 +
drivers/net/tun.c | 17 +-
fs/Makefile | 2 +
fs/attr.c | 13 +-
fs/cellfs.c | 141 ++
fs/coredump.c | 8 +
fs/fs_parser.c | 7 +
fs/fuse/inode.c | 4 +-
fs/inode.c | 61 +
fs/internal.h | 3 +-
fs/kernfs/file.c | 5 +
fs/kernfs/inode.c | 25 +
fs/kernfs/kernfs-internal.h | 11 +
fs/mnt_idmapping.c | 36 +
fs/mount.h | 3 +
fs/namei.c | 11 +-
fs/namespace.c | 160 +-
fs/nsfs.c | 6 +-
fs/overlayfs/copy_up.c | 4 +-
fs/pidfs.c | 31 +-
fs/posix_acl.c | 4 +-
fs/resctrl/rdtgroup.c | 8 +
include/linux/cell.h | 312 ++++
include/linux/cell_ids.h | 103 ++
include/linux/cgroup-defs.h | 3 +
include/linux/cgroup.h | 8 +
include/linux/cred.h | 3 +-
include/linux/fs.h | 44 +-
include/linux/fs/super_types.h | 1 +
include/linux/kernfs.h | 15 +
include/linux/key.h | 3 +
include/linux/mnt_idmapping.h | 44 +-
include/linux/mnt_namespace.h | 6 +
include/linux/netlink.h | 2 +
include/linux/nsfs.h | 1 +
include/linux/nsproxy.h | 11 +-
include/linux/pid.h | 13 +-
include/linux/sched.h | 6 +
include/linux/sched/task.h | 7 +
include/linux/sched/user.h | 1 +
include/linux/syscalls.h | 14 +
include/linux/user_namespace.h | 18 +
include/net/af_unix.h | 4 +
include/net/net_namespace.h | 25 +
include/net/sock.h | 1 +
include/uapi/asm-generic/unistd.h | 15 +-
include/uapi/linux/cell.h | 81 +
include/uapi/linux/fcntl.h | 1 +
include/uapi/linux/magic.h | 1 +
include/uapi/linux/pidfd.h | 18 +
init/Kconfig | 16 +
kernel/Makefile | 1 +
kernel/bpf/inode.c | 4 +-
kernel/cell/Makefile | 6 +
kernel/cell/boundary.c | 125 ++
kernel/cell/core.c | 481 ++++++
kernel/cell/ids.c | 218 +++
kernel/cell/internal.h | 67 +
kernel/cell/slots.c | 480 ++++++
kernel/cell/spawn.c | 211 +++
kernel/cgroup/cgroup-internal.h | 1 +
kernel/cgroup/cgroup-v1.c | 1 +
kernel/cgroup/cgroup.c | 339 +++++
kernel/cgroup/namespace.c | 44 +
kernel/cred.c | 49 +-
kernel/cred_kunit.c | 5 +
kernel/embryo.c | 764 ++++++++++
kernel/events/uprobes.c | 4 +
kernel/exit.c | 14 +
kernel/fork.c | 88 +-
kernel/nsproxy.c | 107 +-
kernel/pid.c | 98 +-
kernel/ptrace.c | 16 +
kernel/seccomp.c | 12 +
kernel/sys_ni.c | 6 +
kernel/ucount.c | 3 +
kernel/umh.c | 9 +
kernel/user.c | 32 +
kernel/user_namespace.c | 146 +-
mm/shmem.c | 4 +-
net/bridge/netfilter/ebtables.c | 4 +-
net/core/dev_ioctl.c | 4 +-
net/core/net_namespace.c | 66 +
net/core/rtnetlink.c | 60 +-
net/core/sock.c | 83 +-
net/ipv4/af_inet.c | 2 +-
net/ipv4/arp.c | 2 +-
net/ipv4/devinet.c | 6 +-
net/ipv4/fib_frontend.c | 2 +-
net/ipv4/ip_options.c | 8 +-
net/ipv4/ip_sockglue.c | 4 +-
net/ipv4/netfilter/arp_tables.c | 4 +-
net/ipv4/netfilter/ip_tables.c | 4 +-
net/ipv6/addrconf.c | 4 +-
net/ipv6/af_inet6.c | 2 +-
net/ipv6/datagram.c | 6 +-
net/ipv6/ipv6_sockglue.c | 7 +-
net/ipv6/netfilter/ip6_tables.c | 4 +-
net/ipv6/route.c | 2 +-
net/netfilter/ipset/ip_set_core.c | 2 +-
net/netfilter/nfnetlink.c | 2 +-
net/netlink/af_netlink.c | 21 +
net/packet/af_packet.c | 2 +-
net/socket.c | 28 +
net/sysctl_net.c | 2 +-
net/unix/af_unix.c | 113 +-
net/unix/af_unix.h | 14 +-
scripts/syscall.tbl | 6 +
security/commoncap.c | 21 +-
security/keys/key.c | 40 +
security/keys/permission.c | 7 +
tools/testing/selftests/Makefile | 1 +
tools/testing/selftests/cell/.gitignore | 2 +
tools/testing/selftests/cell/Makefile | 14 +
tools/testing/selftests/cell/cell_boundary_test.c | 535 +++++++
tools/testing/selftests/cell/cell_cgroup_test.c | 546 +++++++
tools/testing/selftests/cell/cell_child.c | 1626 +++++++++++++++++++++
tools/testing/selftests/cell/cell_ids_test.c | 762 ++++++++++
tools/testing/selftests/cell/cell_life_test.c | 446 ++++++
tools/testing/selftests/cell/cell_net_test.c | 915 ++++++++++++
tools/testing/selftests/cell/cell_slots_test.c | 748 ++++++++++
tools/testing/selftests/cell/cell_spawn_test.c | 559 +++++++
tools/testing/selftests/cell/cell_test.h | 586 ++++++++
tools/testing/selftests/cell/cell_util.h | 108 ++
tools/testing/selftests/cell/config | 35 +
133 files changed, 13426 insertions(+), 141 deletions(-)
---
base-commit: 6628e7d4c6b25c8d18f1345e3a20d21829aae7e4
change-id: 20261103-b4-cells-1408689c04c3
prerequisite-change-id: 20261018-b4-cred-ids-eb01b2b38524:v2
prerequisite-patch-id: 8c80879ddb5001edd272dcb0c64d7d196cbd967f
prerequisite-patch-id: 5d990d2b1fce14667ea5b7240d82dee380bc9571
prerequisite-patch-id: 71798c0f95cf38c21735affa9479ced238d80db0
prerequisite-patch-id: c506972ae7fdf17afdddbf134c6d3e6328425506
prerequisite-patch-id: d8cc6bbfc2b70fb1b08f6f90080b07dc1fc6e568
prerequisite-patch-id: d6ac288cbce55962a8677270deb9248ea9b865ad
prerequisite-patch-id: a088d829b21a605a678ef6225b8b0d9529a7c719
prerequisite-patch-id: 9d33de3e73545d64039b5ab0020e17d84c29ef31
prerequisite-patch-id: 19e1b4d88b8af7fc24061f3c3becb3480fa33557
prerequisite-patch-id: d6c92fa03be2a3c1511da11d561679aee6a957ea
prerequisite-patch-id: 03b389cf65fe7242391c1e7b55d8714e09662b94
prerequisite-patch-id: ee84977e58369ad065a9e5a0870a9780ad57e6e3
prerequisite-patch-id: f8529903a8bbafdb11208eaf671af625031afd92
^ permalink raw reply [flat|nested] 93+ messages in thread* [PATCH RFC v3 01/90] Documentation: add a design document for cells 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik @ 2026-11-05 14:56 ` Josef Bacik 2026-11-05 17:06 ` Serge E. Hallyn 2026-11-05 14:56 ` [PATCH RFC v3 02/90] Documentation: add examples " Josef Bacik ` (88 subsequent siblings) 89 siblings, 1 reply; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:56 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A container on Linux is not an object. It is eight kinds of namespace, a cgroup, a seccomp filter and a set of conventions that a runtime holds together. The kernel cannot name the result, kill it as a whole or account for it. A cell is a container as a single kernel object. A task is born in a cell and never leaves it, and no existing task can enter one. A cell is built from the outside: the builder fills named slots from file descriptors that it holds, seals the cell, and then spawns tasks into it. Holding the cellfd is the authority to do all of that. Start the series with the design document. It describes what cells are for, which invariants they uphold, how they are used, and what this implementation does not do yet. The following patches refer to it. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- Documentation/userspace-api/cells.rst | 861 ++++++++++++++++++++++++++++++++++ Documentation/userspace-api/index.rst | 1 + 2 files changed, 862 insertions(+) diff --git a/Documentation/userspace-api/cells.rst b/Documentation/userspace-api/cells.rst new file mode 100644 index 000000000000..76ce5267fdb6 --- /dev/null +++ b/Documentation/userspace-api/cells.rst @@ -0,0 +1,861 @@ +.. SPDX-License-Identifier: GPL-2.0 + +===== +Cells +===== + +A cell is a container as one kernel object. A task is born in a cell and never +leaves it, and nothing can enter one. A cell is built from the outside: the +builder fills named slots from file descriptors it holds, seals the cell, and +spawns tasks into it. Holding the file descriptor of the cell, the *cellfd*, is +the authority to do all of that. + +This document explains why cells exist, what they promise, how they are used and +what the first implementation does not do yet. + +.. contents:: :local: + + +Why +=== + +A container on Linux is not an object. It is eight kinds of namespace, a cgroup, +a seccomp filter and a set of conventions that a runtime holds together. A number +of problems follow from that, and they do not go away by fixing bugs one at a +time. + +There is nothing to name. + The kernel cannot say which container a task is in, cannot kill a container in + one operation and cannot tell what a container has access to. + +Membership is a property of a task at a moment. + ``setns()`` and ``unshare()`` let a task change one namespace at a time. A task + that is half way between two sets of namespaces, holding file descriptors, a + working directory or a memory map from one side and credentials from the other, + has been the source of many bugs. + +Setup runs on the inside. + A runtime forks, and the child sheds, one call at a time, what it inherited and + should not have. What it forgets to shed leaks into the container. + +Privilege hangs off ids. + A user namespace gives its creator a full set of capabilities over everything + the namespace owns, which puts a large amount of code that was written for root + within reach of every user. Who may do what to whom is decided by comparing + uids, and whether two uids are the same depends on how several runtimes agreed + to carve up ``/etc/subuid``. + +Hiding is done by mounting over. + What a container must not see is commonly covered by a mount. ``MNT_LOCKED`` + exists to keep such mounts in place, and a way to remove one, or to copy the tree + without it, reveals what it covered. + +A cell turns this around. There is one object with one lifetime. What it holds +was put there explicitly, by somebody who held it. What was not put there is not +there. + + +The model +========= + +Invariants +---------- + +The implementation is meant to uphold the following. A way to break one of them +from the inside of a cell, or as an unprivileged user outside, is a bug. + +#. **A task never changes cell.** There is no way to enter a cell and no way to + leave one. The namespaces of a cell belong to the cell and not to its tasks. +#. **A cell holds what it was given.** Slots are filled from file descriptors the + builder holds. Filling a slot takes what using the object would take. +#. **Authority over a cell is the cellfd.** No uid and no capability is compared + when a cell is configured, spawned into or killed. There is no cell without + somebody who holds one: it dies with the last. +#. **Nothing is inherited that gives access to anything.** A task that is spawned + into a cell starts with no memory map and no open files. What it can reach is + what it was given. `Spawning: empty pidfds`_ lists what it does inherit. +#. **After that nothing crosses.** Once a task has started, no file descriptor and + no identity crosses the boundary of its cell. A cell that was given a network + namespace to share is an exception, see `Not there yet`_. +#. **A cell starts nothing it was not allowed to.** Making a cell and starting a + task in one is a slot like any other, ``spawn``. It cannot be filled yet, so what + runs in a cell comes from a ``fork()`` and ``execve()`` of what was spawned into + it, or from outside. +#. **A uid means nothing across cells.** Not an equal one, and not 0. +#. **Files impart no privilege.** Nothing a cell runs can gain privilege from a + file, and nothing a cell writes can give privilege to anybody else. +#. **Nothing is hidden by a mount.** Whatever is in the tree of a cell, the cell + may see all of it, including what lies under every mount point. +#. **Ids are lent.** The kernel chooses the kuids and kgids of a block, nobody else + can hold one of them while the cell does, and nothing the cell made may still be + owned by one when they are lent again. +#. **DEAD means gone.** When a cell reports that it is dead, no task of it is left, + not even one that is on its way out, and the cell has dropped every reference it + held. What that sets off, such as the release of mounts, of the network + namespace and of the block of ids, completes on its own time. + +Life of a cell +-------------- + +A cell only ever moves down this list. + +================= ============================================================= +``CELL_BUILDING`` Slots may be filled. Nothing runs. +``CELL_SEALED`` Immutable. Nothing has been spawned yet. +``CELL_RUNNING`` Has, or had, its first task. +``CELL_DYING`` Was killed, or its first task has exited. +``CELL_DEAD`` Nothing is left. The exit status can be read. +================= ============================================================= + +The first task that is spawned is the init of the pid namespace of the cell. When +it exits the kernel kills everything else in that namespace, as it does for any pid +namespace, and the cell dies. + +A cell can be killed in every state. + +**A cell lives for as long as somebody holds a cellfd on it, and no longer.** When the +last one is closed, be it because whoever held it was killed, the cell is killed. A +cellfd can be passed on, so a cell can be handed to a supervisor, but a cell that nobody +holds is a cell that nobody could kill, spawn into or wait for. + +Slots +----- + +============== ======================================= ======================= +Slot Filled with If it is left empty +============== ======================================= ======================= +``ids`` a count: 1 or 65536 1; refused for root +``caps`` a set of capabilities no task has any +``root`` a mount namespace nobody has entered the seal fails +``budget`` a cgroup v2 directory the seal fails +``net`` a network namespace, or ``"new"`` no network +``net-rights`` what the cell may do to its network it changes nothing about + it +``hostname`` a string ``(none)`` +``spawn`` nothing yet, the name is reserved no task of the cell can + make a cell or start a + task in one +============== ======================================= ======================= + +The pid, ipc, uts and cgroup namespaces of a cell are always new. The cgroup +namespace is rooted at the budget. The time namespace is that of the builder. + +See `The network`_ for what goes into ``net`` and `Capabilities`_ for ``caps``. + +A file descriptor on a namespace is no authority over it, since anybody who can see a +task can open one. So filling ``root`` or ``net`` from one takes ``CAP_SYS_ADMIN`` over +the user namespace that owns the namespace. That is about what goes into the slot and +not about the cell. It is half of what ``setns(2)`` asks for: the other half is about +the namespaces of the caller, which do not change. + + +Ids +=== + +Every consumer of a kuid in the kernel knows how to deal with a user namespace, so +a cell has one. The kernel makes it together with the cell, and it differs from a +legacy user namespace in the ways that make a legacy one hard to reason about: + +* Nobody can ``setns()`` into it. +* The uid that created it has no capability over it. +* Its map is set once, by the kernel, to ids the kernel chose. + +All translation between the ids a cell sees and the ids on disk stays in the VFS, +in the idmapping of each mount of the tree. See +Documentation/filesystems/idmappings.rst. + +A cell comes in two shapes. + +One id + The cell runs on the uid and gid of whoever made it, under their own numbers, with + the supplementary groups of its maker and without capabilities. It cannot be given + any: who is not uid 0 loses them with the first program it runs. ``setgroups()`` is + denied. The groups are kept because a group can be there to deny something. This + shape is refused to root, since too much goes by the bare value of kuid 0, and to + an id that lies in the window described below. + +A block + The cell sees uids and gids 0 to 65535. They map to a block of 65536 kuids and + kgids that the kernel allocates. **The builder never picks a kuid.** It takes no + privilege: a block that nobody holds gives access to nothing. A user can hold + ``user.max_cell_id_blocks`` of them. ``cell_info()`` tells which block it was, for + where something outside of the cell goes by an id: uid and gid N of the cell are + ``ids_first`` + N to the caller. + +Capabilities +------------ + +A legacy user namespace hands its first task every capability there is. A cell hands +out none. **Being uid 0 of a cell comes with nothing**: it is one id of 65536, which +cannot become another, give a file away or signal a task that runs under another id. + +What the tasks of a cell are to have, the builder names in the ``caps`` slot: a ``__u64`` +with a bit for each capability, by its number in ``<linux/capability.h>``. Every task +that is started in the cell has them permitted and effective. They are its bounding set +too, so that is all anything in the cell will ever have: + +* Every task has ``no_new_privs``. It gains nothing from a file, or from being uid 0 + when it runs one. +* No task of a cell can make a user namespace, to have a full set in. +* A capability that a task has let go of is gone for that task and its children. + +Letting go works as ever. ``capset(2)`` takes nothing, leaving uid 0 behind with +``setuid()`` costs all of them, and ``PR_CAPBSET_DROP`` does not take ``CAP_SETPCAP`` in +a cell. It does elsewhere lest a program that gains from its file runs with less than it +counts on, and in a cell there is no such program. + +The capabilities are over the user namespace of the cell. They are good for the ids of +the cell, for the files that belong to those in a tree that is mapped for the cell, for +its tasks, and for what else that user namespace owns: its uts, ipc and pid namespace. +They are good for nothing else, which is why naming them takes no privilege. The mount +namespace is not the cell's, so no capability lets a cell mount. Nor is the network +namespace, see `Rights over the network`_. + +``CAP_SETFCAP`` cannot be named, see `Files`_. + +Most software that starts as root expects a few. +Documentation/userspace-api/cells-examples.rst says which, and what it looks like when +one is missing. + +The window +---------- + +Blocks come out of a window, the upper half of the id space less its last block: +2147483648 to 4294901759. + +"Nobody else holds an id of this block" stands in for the privilege check that a +legacy user namespace needs before it may map a range. So it is enforced rather +than assumed: + +* A legacy user namespace cannot map into the window. +* **No create, chown or ACL stores an id of the window in a filesystem that the + cell does not own.** Creating a file and ``chown()`` fail with ``EOVERFLOW``, + setting an ACL with ``EINVAL``. Through a mount that is idmapped for the cell the + id has become the on-disk id before the check is made, so that works. The check + goes by the value of the id and not by who is asking, because a stacking + filesystem may create files with the credentials of whoever mounted it. +* A mount option that is read with ``fsparam_uid()`` or ``fsparam_gid()``, as most + are, takes no id of the window: ``uid=`` and the like fail with ``EINVAL``. +* The one filesystem of the host's that holds ids of the window is cgroupfs, below the + budget of the cell they were lent to, and only a task of that cell puts them there. + See `The budget`_. +* A block lives as long as the user namespace object it was lent to, not as long as + the cell. Every credential, and every object that the namespace owns, pins the + namespace. That covers the inodes which the kernel makes on filesystems of its + own and gives to the caller without asking: pipes, sockets, memfds, the slave of + a pty. Each lives no longer than a file that is open on it, and the file pins the + credentials it was opened with. +* The idmapping of a mount holds a copy of the maps. One that was made for a cell + pins the user namespace as well, so a mount that is kept beyond the life of the + cell keeps the block from being lent again. +* What the cell made below its budget is removed when the cell dies, and the budget + returns to its owner. If that fails the block is never lent again. +* Some tables are keyed by a bare kuid and pin nothing: ``struct user_struct`` and + the quota records of keys. A released block is quarantined until they hold + nothing in its range, and a block is checked the same way before it is lent. + +Blocks are handed out round robin, so that reuse comes as late as it can. A block in +quarantine counts as used. A leak therefore shows up as ``ENOSPC`` and never as one +tenant owning what another left behind. + +.. warning:: + + With ``CONFIG_CELLS`` enabled the host itself cannot ``chown()`` a file to an id + of the window, or mount a filesystem with one for an option. This is a change of + behaviour for a system that uses such ids. + Files that already belong to one, seen through a mount that is not idmapped, + look as if they belonged to whichever cell is lent that block. + +What the allocator does not cover is the ids on disk. Through a mount that is +idmapped for it, a cell with a block always uses on-disk ids 0 to 65535, so two cells +that are given the same storage see each other's files. That is sometimes intended, +as with shared images, and remains the responsibility of the builder. + + +The tree +======== + +The tree of a cell is built from scratch, one mount at a time, in a mount namespace +that nobody has entered: see ``fsmount(2)`` with ``FSMOUNT_NAMESPACE`` and +``open_tree(2)`` with ``OPEN_TREE_NAMESPACE``. Sealing the cell makes that namespace +the cell's, or fails without having changed anything about it: + +* No mount may be ``MNT_LOCKED``, since a locked mount hides what it covers. The + tree itself may be: it sits on the root of the namespace, which is empty, and the + kernel locks it there for a builder that works in a user namespace of its own. +* Nobody is in the namespace, or on the way in. +* Every mount is made private. Nothing propagates into a cell. +* In a cell with a block of ids, a writable mount of a filesystem that can be + idmapped must be idmapped for the cell, or belong to a superblock the cell owns. + This is only the early and readable form of the ``EOVERFLOW`` above. + +To idmap a mount for a cell, pass the cellfd in ``mount_attr.userns_fd``. On-disk +ids 0 to 65535 then belong to the ids of the cell, or the one id to itself. + +It is the overlay that is idmapped for the cell, and not what it is made of. An +overlay writes to its upper layer with the credentials of whoever mounted it, and +through a mount that is idmapped for the cell those have no id. + +A device is its number, and not the node in ``/dev`` of the host. Whoever builds a cell +makes a node for it in the tree of the cell, and who in the cell may open that goes by +the owner, group and mode on disk, which the idmapping makes ids of the cell. Several +cells use one device that way, ``/dev/kvm`` say, each by a node of its own with a group +of its own, and no id of the host is mapped into any of them. Or the builder opens the +device and hands the file to a new task. The node of the host itself can be bound into +the tree, but it belongs to ids of the host, which nobody in a cell has, its root +included, so all that counts is what the mode has for everybody. devtmpfs cannot be +idmapped, and as a whole it should not be: every node that belongs to root would belong +to the root of the cell. + +A seal that fails leaves the cell ``CELL_BUILDING``. What it had filled in for slots +that were left empty by then stays filled in. + +The mount namespace stays owned by the user namespace of the builder. That is what +keeps a task of the cell from mounting anything. + + +The network +=========== + +The network of a cell is set up from the outside and handed to the cell, as the tree +is. A task of a cell uses it. It does not configure it: a capability over a network +namespace is one in the user namespace that owns it, and **a cell never owns a network +namespace**. This goes for ``CAP_NET_ADMIN``, ``CAP_NET_RAW`` and +``CAP_NET_BIND_SERVICE`` alike, and for the root of the cell as for anybody else in it. +What a cell is to do to its network all the same, the builder names, see `Rights over the +network`_. + +What the builder puts into the slot decides what the cell gets: + +Nothing + No network. ``socket()`` and ``socketpair()`` fail with ``EAFNOSUPPORT`` for every + family but ``AF_UNIX``, as in a kernel that was built without the rest. An ``ioctl()`` + that is about the network a socket is in and not about the socket, its devices, + addresses and routes, fails with ``ENODEV`` on any socket, and so do those that a + protocol keeps to itself, ``SIOCPROTOPRIVATE``. A socket that is handed + to the cell works, and so does what ``accept()`` makes of it: it is the authority to + use it, and the only network such a cell has. + +A network namespace + That one, the way the builder has set it up: one it made for the purpose, one that + several cells share, or that of the host. + +``"new"`` + One that is made for the cell, for the builder to set up. ``cell_open()`` returns a + file descriptor on it, to enter it or to move a device there. Until then there is a + loopback device in it, which is down. + +Nothing is done for the builder. Whatever is not set up is not there, the address of +the loopback device included. + +The network namespace belongs to the user namespace the cell was made in, or to whoever +made it. Who has ``CAP_SYS_ADMIN`` there can enter it whatever state the cell is in, so a +network can be changed while the cell is running. + +**A cell that dies takes its network along.** It lets go of the namespace, and unless +somebody else holds that, it is the end of whatever is in there. There is no cleaning up +in there after the fact. Nor is the namespace to be had from the cell once it is sealed: +sealed is sealed. Whoever wants to take the network apart in an orderly way keeps the +file that ``cell_open()`` returned while the cell was built. The namespace is there for +as long as that file is, with all that is in it, and once the cell is dead with nobody +left to use it. + +Some of what a container commonly has a capability for is a setting of the network +namespace as well, and the builder can make it: ``net.ipv4.ip_unprivileged_port_start`` +for listening on port 80, ``net.ipv4.ping_group_range`` for ``ping``. + +Rights over the network +----------------------- + +``CAP_NET_ADMIN`` over a network namespace is one capability for well over a hundred +checks: devices, addresses, routes, the packet filter, queueing, tunnels, IPsec and more. +A container that needs one of them gets all of them, and to have any it has to own the +namespace, or to hold the capability on the host. + +A cell is given what it needs by name. The ``net-rights`` slot takes a ``__u64`` with a +bit for each thing that the tasks of the cell may do **to the network namespace in the** +``net`` **slot**: + +====================== ========================================================== +``CELL_NET_LINK`` change a device: up and down, mtu, name, address +``CELL_NET_LINK_NEW`` make and remove devices +``CELL_NET_ADDR`` addresses +``CELL_NET_ROUTE`` routes, routing rules, neighbours, nexthops +``CELL_NET_FILTER`` netfilter: nftables, the iptables family, ipset, conntrack +``CELL_NET_TC`` queueing disciplines, classes, filters, actions +``CELL_NET_TUN`` make tun and tap devices, attach to those of others +``CELL_NET_SYSCTL`` write what is below ``/proc/sys/net`` +``CELL_NET_SOCKOPT`` ``SO_MARK``, ``SO_PRIORITY``, ``IP_TRANSPARENT`` and the like +``CELL_NET_RAW`` IPv4 and IPv6 raw sockets, packet sockets, some IP options +====================== ========================================================== + +The namespace stays the builder's. It sets it up, with the tools it has always used, it +can enter it at any time, and the cell does on top of that what it was given the right +to. + +* **What is not in the table a cell cannot do.** A check in the kernel that has not been + taught to say what it is for goes on asking for the capability over the owner of the + namespace, which no task of a cell has: IPsec, IPVS, whatever is configured by generic + netlink, such as ``ethtool`` settings and the keys and peers of a WireGuard device, the + ``ioctl()`` interfaces of bridges, VLANs and tunnels, and of what takes ``CAP_NET_RAW`` + the sockets of ``AF_XDP``, LLC, NFC and IEEE 802.15.4. +* **The right says what, a capability says who.** Of the tasks of the cell, those may use + a right that have the capability it takes anywhere else, ``CAP_NET_RAW`` for + ``CELL_NET_RAW`` and ``CAP_NET_ADMIN`` for the rest, in the user namespace of the cell. + So it takes both slots. An init can set up a packet filter and let go of the capability + before it runs anything else, and somebody who is not root in the cell cannot undo it. +* **They are rights over that one network namespace.** A socket that was made in another + and handed to the cell comes with none. No device leaves for another, is made in + another or is linked to one in another, whichever way the other is named. +* **Nobody hands out what is not theirs to do.** Whoever fills the slot has to be able to + do all of it to that network namespace itself. So the ``net`` slot is filled first. +* A right is as wide as the subsystem behind it. ``CELL_NET_FILTER`` is all of netfilter + as far as it is per network namespace, which is a lot of code. No module is loaded + because a cell asked, so it is what is built in or loaded already. + +There is no right for ports below ``ip_unprivileged_port_start``, which is a setting of the +network namespace. + + +Spawning: empty pidfds +====================== + +``pidfd_open()`` with ``PIDFD_EMPTY`` takes a cellfd where it otherwise takes a pid, +and returns a pidfd that no task is attached to yet. Whoever has it says with +``pidfd_configure()`` what the task is to be like, and starts it. Only then is there +a task. It never runs an instruction of its creator's, and execs. The creator does +not fork. This is ``fsopen()`` for tasks. + +A cell can be spawned into for as long as it is sealed or running, not only to get it +going. **This is what takes the place of entering a container**: no task that exists +moves in, but whoever holds the cellfd can start a new one in there at any time. + +An empty pidfd is a pidfd like any other, and stays the same pidfd once there is a +task. Until then whatever it is passed to finds no task, which is ``ESRCH``, or +``ECHILD`` from ``waitid(2)``. No task yet is not the same as no task any more: it does +not poll readable, ``PIDFD_GET_INFO`` has no exit to report, and its ``fdinfo`` says +``Pid: 0`` and not ``-1``. + +It reserves nothing in the cell, not even a pid: whichever task is started first is the +init of the cell, one that is closed without having been started leaves no trace, and +one that is left empty keeps no cell from dying. + +The new task has + +* no memory map, from the start, +* an empty file table, +* the root of the tree of the cell as its root and working directory, +* the credentials of the root of the cell, or of its one id, +* default signal dispositions, nothing blocked and nothing pending, +* its own session and no controlling terminal, +* ``no_new_privs``, +* none of its creator's registers. + +It still inherits attributes of whoever started it, as any new task does. Among them are +resource limits, nice value, scheduling policy and affinity, the core scheduling +cookie, ``oom_score_adj``, I/O priority, memory policy, timer slack, the speculation +controls of ``prctl(2)``, perf events that are marked for inheritance, membership in +cgroup v1 hierarchies, the audit ids, **seccomp filters and LSM state**. The list is +not complete. The last two are deliberate: what a confined task spawns is confined as +it is itself. + +For LSMs that covers the start as well. The hooks for receiving a file, for changing +ids and for every step of an exec are called in the context of the new task, with the +state it inherited. + +.. warning:: + + For seccomp it does not. The filters apply to every system call the new program + makes. They are **not** consulted for what ``PIDFD_CMD_START`` does on the way + there, since the new session, the files, the credentials and the exec are not + system calls of the new task. A filter that denies ``execve()`` and ``execveat()`` + to keep a task from running another program does not keep it from starting one in + a cell. Such a filter has to deny ``pidfd_configure()`` too, without which there + never is a task, as it once had to learn about ``execveat()``. + + This is about a task of the host. A task of a cell cannot open an empty pidfd at + all, see the ``spawn`` slot. Evaluating the filters at the start may be added later. + +The listener of a filter with ``SECCOMP_RET_USER_NOTIF`` stays behind, outside of the +cell. It goes on answering for the system calls of what was spawned. It cannot put a +file into it: ``SECCOMP_IOCTL_NOTIF_ADDFD`` fails with ``EPERM`` across cells. + +Only the file that ``pidfd_open()`` returned for ``PIDFD_EMPTY`` can configure. +Another pidfd on the same task cannot. From when the task is made until its exec has +succeeded nobody can ptrace it, or read the files in its ``/proc`` directory that are +gated by ptrace access. + +==================== ================== ======================================= +Command Key Value +==================== ================== ======================================= +``PIDFD_SET_STRING`` ``exec`` the path to execute, in the cell's tree +``PIDFD_SET_BINARY`` ``argv``, ``envp`` NUL terminated strings, back to back +``PIDFD_SET_BINARY`` ``cred`` ``struct pidfd_cred``, in the cell's ids +``PIDFD_SET_FD`` a decimal number the file to have at that number +``PIDFD_SET_FD`` ``cgroup`` the cgroup to start in, within the budget +``PIDFD_CMD_START`` none make the task, apply everything and exec +==================== ================== ======================================= + +The file table of the new task is exactly what was put there. io_uring files, +cellfds, namespace files and what ``pidfd_open()`` returned for ``PIDFD_EMPTY`` are +refused. What was configured is let go of once it has been used, however long the +pidfd is kept. + +A task starts in the budget of the cell, or in the cgroup that was named, which has to +be the budget or lie below it. An init that manages cgroups moves out of the budget and +enables controllers there, and from then on the budget holds no task, by the rule that a +cgroup which passes a controller on has none of its own: ``PIDFD_CMD_START`` fails with +``EBUSY``. Whoever spawns then names a cgroup that can hold a task, the one that the +init is in for one. Whether it is within the budget, ``EPERM``, still there, ``ENODEV``, +and can hold a task, ``EBUSY``, is decided when the task is made, since what is below +the budget is the cell's to change at any time. It takes no permission on the cgroup. +Whoever holds the cellfd can do anything to the cell, and what the cell made in there +belongs to ids that nobody outside of it has. + +Whatever goes wrong is returned by ``PIDFD_CMD_START``, which can be tried only once. +If there was a task by then it is killed, which is what the pidfd reports, and if it was +the first task of the cell, the cell dies with it. + +The pid namespace of the spawner must be an ancestor of that of the cell. Nobody has to +wait for the task: it is reaped on its own, and the pidfd tells how it ended. For now +there is no empty pidfd without a cell. ``-EBADF`` in place of the cellfd is set aside +for that, and fails with ``EOPNOTSUPP``. + + +The boundary +============ + +Namespaces +---------- + +For a task of a cell ``setns()``, ``unshare()`` and ``clone()`` with any +``CLONE_NEW*`` flag fail with ``EPERM``, as do ``OPEN_TREE_NAMESPACE`` and +``FSMOUNT_NAMESPACE``. + +That is on purpose. ``unshare(CLONE_NEWUSER | CLONE_NEWNET)`` hands whoever calls it +every capability over a network stack of its own, and with that a large amount of code +that was written for root. A cell is there to keep such code out of reach, so it does +not come back by default. + +The direction is a slot for the builder to fill: which kinds of namespace the tasks of +the cell may make, which capabilities a user namespace of theirs confers at most, and +which filesystems they may mount. Whatever they make belongs to the cell, nobody +enters or leaves any more than before, and an empty slot is what is described above. + +From the outside ``setns()`` into a namespace that a cell owns fails with ``EPERM``, +and not only once the cell is sealed. Whoever got into the pid namespace while the cell +was being built could become its init, with the credentials of the host. + +Tasks +----- + +A one-id cell runs on the kuid of its maker, and the pids of a cell are visible from +above. So wherever one task acts on another and an equal uid is enough, the same +cell is required as well: signals, ptrace, scheduling attributes, priorities, I/O +priorities, resource limits, ``SIGIO``, moving a task between cgroup v1 or resctrl +groups, and ``KEYCTL_SESSION_TO_PARENT``. + +There used to be a copy of such a check wherever one was needed, and a copy that is +overlooked is a way out of a cell. They have been gathered in one place, see +"Comparing the Ids of Two Tasks" in Documentation/security/credentials.rst, and the +rule about cells is applied there, once, ahead of every one of them. That includes +the two that let euid 0 act on anybody: root on the host can neither have ``SIGIO`` +sent to a task of a cell nor move one between cgroup v1 or resctrl groups. This is a +hard boundary. A cell that is not as it should be is killed and built again. + +The owner and group permissions of a key likewise only count within the cell the key +was made in. + +``PTRACE_TRACEME`` fails across cells. The parent of a task that was spawned into a +cell is the spawner, outside. + +The capability branch of each of these checks is unchanged for now. See +`Not there yet`_. + +Files +----- + +A file can hand a process privilege at exec, by its set-id bits or by its file +capabilities. In a cell both are impossible. + +*Running.* Every task of a cell has ``no_new_privs``, which cannot be unset and is +inherited. The file capabilities of a program are not even looked at, as on a mount with +``nosuid``. If they were, a program that insists on a capability which is not in the +bounding set would not run at all. + +*Writing.* The ids of a cell commonly map to on-disk ids from 0 up, because that is +how images are stored. A set-uid bit that the root of a cell sets would make a +set-uid root file for whoever else looks at that filesystem. So: + +* No task of a cell ever has ``CAP_SETFCAP``, which the ``caps`` slot refuses, so it + cannot write a file capability. +* ``S_ISUID``, and ``S_ISGID`` on anything but a directory, are silently dropped from + the mode of what a task of a cell creates and from the mode it sets. They are + dropped and not refused so that unpacking an archive keeps working. +* The set-id bits and file capabilities of an existing file are dropped as soon as a + task of a cell opens it for writing, or is given it open for writing. A + ``write()`` drops them, a store through a shared mapping does not. + +``sudo``, ``su`` run by anybody but root, ``pkexec``, and tools that rely on a set-gid +bit or a file capability run, and gain nothing. A daemon that starts as root and drops +privileges with ``setuid()`` works as ever, given ``CAP_SETUID`` and ``CAP_SETGID``. + +Programs that the kernel runs +----------------------------- + +There are things a task can do that make the kernel run a program: ``modprobe`` for a +protocol, a cipher or a line discipline that is not there yet, ``/sbin/request-key`` for a +key that is not there yet, whatever ``kernel.core_pattern`` pipes a core dump to. Such a +program runs as root, on the host, in no namespace but the first, with arguments or input +that the task had a say in. + +None is run for a task of a cell, and what was asked for fails: ``socket()`` for a +protocol whose module is not loaded with ``EAFNOSUPPORT``, ``request_key()`` for a key +that somebody would have to make with ``EPERM``. **No module is loaded because a cell +asked.** What a cell is to use has to be there already. + +A core dump is written if ``kernel.core_pattern`` names a file, which is then made where +the task is, as the task. If it names a pipe or a socket there is none. + +Unix sockets +------------ + +A unix socket can span two cells, because both can see the tree it is bound in or +because one end was given to a new task. **Between cells it carries bytes and nothing +else.** + +* Ancillary data is delivered only within a cell. Every message records the cell it + was sent from, and whoever receives it gets the files, the credentials, the pidfd + and the security label only if that is its own cell. This covers a file that was + queued on a socket before the socket was given away. +* Sending control data to a socket that another cell holds fails with ``EPERM``, where + that can be known at the time. +* ``SO_PEERCRED``, ``SO_PEERGROUPS``, ``SO_PEERPIDFD`` and ``SO_PEERSEC`` fail with + ``ENODATA`` if the peer is in another cell. They fail rather than report the + overflow ids: 65534 is a uid like any other inside a cell, and a server that lets + in whoever has its own uid may well be running as it. In a cell ``SO_PEERSEC`` also + fails for a socket that has no peer credentials, such as a TCP connection. + +Peer credentials are not a way to authorise anything between cells. Protocols that +pass file descriptors to a service in another cell do not work. + +The budget +---------- + +The budget of a cell bounds what the cell can use and is how the kernel finds every +task in it. It belongs to one cell: when the slot is filled the cgroup must be empty, +have no children, and neither be nor lie within the budget of another cell. Whoever +fills the slot needs everything that moving a task of its own there would take, and +must own the directory. + +A task of a cell stays within the budget, whoever asks. Nothing but a spawn gets a +task into one, kernel threads included. + +A task of a cell changes nothing in cgroupfs outside of the budget, whatever the modes +say, whoever owns the cgroup and whoever opened the file. It writes to no file, makes, +removes and renames no cgroup, and sets no mode, owner, time or extended attribute. No +cgroup of a v1 hierarchy is within a budget. Nor is anything in resctrl, where the same +goes. + +On a cgroup v1 hierarchy a task of a cell is where its spawner was, and no request +moves it. The kernel itself may: out of a cpuset that lost its last cpu, to the +nearest ancestor that has one. + +Sealing hands the directory, ``cgroup.procs``, ``cgroup.threads`` and +``cgroup.subtree_control`` to the root of the cell. The limits stay with whoever is +above. + +Below the budget the cell does as it likes, the way an init that manages cgroups does: +it makes and removes cgroups, moves its tasks between them, enables controllers, sets +limits, and hands a cgroup to one of its ids. So does whoever built the cell, from the +outside, once the slot is filled. What is below the budget belongs to the ids of the +cell as far as the cell made it, and all of it goes with the cell, whoever made it. + +The budget itself is what bounds the cell. There a task of the cell makes and removes +cgroups and writes to what is written to across a delegation boundary, which is the +three files above and what else the kernel marks so. It writes to nothing else, not to +a limit, ``cgroup.freeze`` or ``cgroup.kill``, and sets no mode, owner, time or extended +attribute on the directory or on any file in it. This is the rule of the ``nsdelegate`` +mount option, whether cgroupfs is mounted with it or not, and it goes by who is asking: +a cell of one id owns what its maker owns, limits included, and a file may have been +opened by somebody else. + + +The system calls +================ + +They are split by what they do, as the mount API is, so that "may look but not touch" +is a system call number: ``cell_info()`` changes nothing. So is "may kill". + +.. code-block:: c + + int cell_create(int parent, unsigned int flags); + int cell_configure(int cfd, unsigned int cmd, const char *key, + const void *value, int aux); + int cell_info(int cfd, struct cell_info *info, size_t size, + unsigned int flags); + int cell_open(int cfd, unsigned int what, const char *key, const void *arg, + unsigned int flags); + int cell_kill(int cfd, unsigned int flags); + int pidfd_open(int cfd, unsigned int flags); /* with PIDFD_EMPTY */ + int pidfd_configure(int pidfd, unsigned int cmd, const char *key, + const void *value, int aux); + +``cell_create()`` + makes a new, empty cell and returns a cellfd, which is always close-on-exec. + ``parent`` is ``CELL_SELF``. It takes no privilege, but it makes a user namespace, + and what applies to those applies here: ``user.max_user_namespaces``, the LSM hook, + and no chrooted caller. + +``cell_configure()`` + fills the slots of a cell that is being built, and seals it. It is shaped like + ``fsconfig(2)`` and data only goes in. + +``cell_info()`` + reads from a cell. The struct can grow: the caller says in ``mask`` what it wants, + the kernel says there what it filled in, and in ``supported_mask`` what it knows of. + With ``CELL_SELF`` for a cellfd it tells a task the id of its own cell. + +``cell_open()`` + gets another file descriptor from a cell. For now that is an nsfs file descriptor + on the network (``"net"``) or pid (``"pids"``) namespace of a cell that is being + built. The namespace is made if the cell does not have it yet, which fills the + slot. + +``cell_kill()`` + kills a cell and everything in it, whatever state it is in. It does not wait. + +``pidfd_open()`` with ``PIDFD_EMPTY`` + returns a pidfd for a task that is yet to be, in the cell. + +``pidfd_configure()`` + says what that task is to be like, and starts it. + +A cellfd polls readable once the cell is dead, and ``read(2)`` then returns the wait +status of its first task as an ``int``, any number of times. A cellfd can be passed on +but cannot be opened again through ``/proc/<pid>/fd``. + +Every cell has a 64 bit id that is never reused. It is the inode number of the +cellfd. + +Example +------- + +Error handling is left out. There is more of this in +Documentation/userspace-api/cells-examples.rst. + +.. code-block:: c + + unsigned int ids = 65536; + __u64 caps = 1ULL << CAP_SETUID | 1ULL << CAP_SETGID; /* it becomes www-data */ + int cfd, tree, pidns, fs, proc, ns, pidfd, status; + + cfd = cell_create(CELL_SELF, 0); + cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)); + cell_configure(cfd, CELL_SET_BINARY, "caps", &caps, sizeof(caps)); + + /* The tree, from scratch. The image becomes "/", for this cell. */ + struct mount_attr attr = { + .attr_set = MOUNT_ATTR_IDMAP, + .userns_fd = cfd, + }; + tree = open_tree_attr(AT_FDCWD, "/images/web", OPEN_TREE_CLONE, + &attr, sizeof(attr)); + + /* A procfs for the pid namespace of the cell. */ + pidns = cell_open(cfd, CELL_OPEN_SLOT, "pids", NULL, 0); + fs = fsopen("proc", 0); + fsconfig(fs, FSCONFIG_SET_FD, "pidns", NULL, pidns); + fsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL, 0); + proc = fsmount(fs, 0, 0); + move_mount(proc, "", tree, "proc", MOVE_MOUNT_F_EMPTY_PATH); + + ns = open_tree(tree, "", AT_EMPTY_PATH | AT_RECURSIVE | OPEN_TREE_NAMESPACE); + + cell_configure(cfd, CELL_SET_FD, "root", NULL, ns); + cell_configure(cfd, CELL_SET_FD, "budget", NULL, cgroup_dirfd); + cell_configure(cfd, CELL_SET_STRING, "hostname", "web-1", 0); + cell_configure(cfd, CELL_CMD_SEAL, NULL, NULL, 0); + + pidfd = pidfd_open(cfd, PIDFD_EMPTY); /* no task yet, the caller does not fork */ + + pidfd_configure(pidfd, PIDFD_SET_STRING, "exec", "/usr/sbin/nginx", 0); + pidfd_configure(pidfd, PIDFD_SET_BINARY, "argv", "nginx\0-g\0daemon off;", 21); + pidfd_configure(pidfd, PIDFD_SET_FD, "1", NULL, log_fd); + pidfd_configure(pidfd, PIDFD_SET_FD, "2", NULL, log_fd); + pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0); + + read(cfd, &status, sizeof(status)); /* until the cell is dead */ + + +Not there yet +============= + +The first implementation is deliberately small. It exists so that there is code to +argue about. + +What a builder can still get wrong +---------------------------------- + +These are mistakes of a privileged builder, of the kind a container runtime can make +today. None of them can be reached from the inside. + +* There are no per-cell instances of procfs, sysfs, devtmpfs and cgroupfs. The + builder assembles those with what exists today, and can mount a procfs that shows + the whole host, or a sysfs that shows the network devices of the host. The direction is for each of the four to take the cell as a mount + parameter, show only what the cell holds, and be refused to a task of any other + cell. +* There is no table of devices a cell may use. The builder can put any device node + into the tree. Devices that create devices, such as ``loop-control``, must not be + given to a cell. +* The list of files for a new task is only lightly filtered. The builder can pass a + plain directory of the host. +* A cellfd that sits in a message on a unix socket counts as held. A builder that + queues the cellfd of a cell on a socket, gives the other end to a task of that cell + and closes the rest is left with a cell that nobody can reach. It is killed when + the message is read, since the file does not cross, or when the socket goes. + +What does not work yet +---------------------- + +* ``EBUSY`` **once the cell has enabled + controllers on its budget**, by the rule that a cgroup with controllers enabled for + its children has no tasks of its own. An init that manages cgroups does that right + away. It needs a leaf below the budget for spawns to land in. +* ``RLIMIT_NPROC`` is not applied to the uid that is set with ``cred``. +* The seccomp filters of a new task are not consulted for its first exec, see + `Spawning: empty pidfds`_. +* There are no rights on a cellfd. Every cellfd can do everything. +* Cells do not nest, since the ``spawn`` slot cannot be filled. +* Software that sandboxes itself with namespaces does not work in a cell, see + `Namespaces`_. What does it with seccomp or Landlock works. +* **Every task of a cell starts at the root of the cell.** Containers that trust each + other, as those of a pod do, belong in one cell: they share a pid namespace, can signal + each other and pass each other files. Each can have a cgroup of its own. For each to + have a root of its own as well, a spawn has to be able to name that too. +* **What is counted per user is charged to whoever made the cell as well**, as with + any user namespace. One cell can use up the inotify instances of its maker, and + with them those of every other cell that has the same maker. +* A cell cannot outlive its last cellfd, and there is no way to find one again by its + id. What is to run for long needs somebody who holds on to it for as long. +* Only x86 and the architectures that use the generic system call table are wired up. +* There are no LSM hooks for the new operations, other than those that come with the + user namespace, the exec and the files. +* **A network namespace that is shared with a cell carries identities.** Only unix + sockets know about cells. ``SCM_CREDENTIALS`` on a netlink socket says who sent a + message, and ``NETLINK_USERSOCK`` lets any two tasks of a network namespace talk. + +What is still decided the old way +--------------------------------- + +* **A capability in an ancestor user namespace still applies to what a cell owns.** + Root on the host can signal and ptrace the tasks of a cell by pid. Only the branch of + each check that goes by a uid has been closed. +* Files in ``/proc/<pid>`` that are protected by nothing but their owner, such as + ``oom_score_adj``, still go by the bare uid. +* A one-id cell shares the ``struct user_struct`` and the key quota of its maker. +* Nothing keeps root on the host from calling ``setresuid()`` with an id of a block + that has been lent. Nor from naming one where a filesystem reads its ids by itself + and not with ``fsparam_uid()`` and ``fsparam_gid()``, such as udf, ntfs, zonefs, + functionfs, hypfs and spufs, ``access=`` of 9p, or the anonymous ids of an nfsd + export. diff --git a/Documentation/userspace-api/index.rst b/Documentation/userspace-api/index.rst index a68b1bea57a8..b9d2e4bef24c 100644 --- a/Documentation/userspace-api/index.rst +++ b/Documentation/userspace-api/index.rst @@ -17,6 +17,7 @@ System calls :maxdepth: 1 unshare + cells futex2 ebpf/index ioctl/index -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* Re: [PATCH RFC v3 01/90] Documentation: add a design document for cells 2026-11-05 14:56 ` [PATCH RFC v3 01/90] Documentation: add a design document for cells Josef Bacik @ 2026-11-05 17:06 ` Serge E. Hallyn 2026-11-05 18:23 ` Josef Bacik 0 siblings, 1 reply; 93+ messages in thread From: Serge E. Hallyn @ 2026-11-05 17:06 UTC (permalink / raw) To: Josef Bacik Cc: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn, linux-fsdevel, linux-kernel On Thu, Nov 05, 2026 at 02:56:52PM +0000, Josef Bacik wrote: > A container on Linux is not an object. It is eight kinds of namespace, a > cgroup, a seccomp filter and a set of conventions that a runtime holds > together. The kernel cannot name the result, kill it as a whole or account > for it. > > A cell is a container as a single kernel object. A task is born in a cell > and never leaves it, and no existing task can enter one. A cell is built > from the outside: the builder fills named slots from file descriptors that > it holds, seals the cell, and then spawns tasks into it. Holding the > cellfd is the authority to do all of that. > > Start the series with the design document. It describes what cells are > for, which invariants they uphold, how they are used, and what this > implementation does not do yet. The following patches refer to it. > > Signed-off-by: Josef Bacik <josef@toxicpanda.com> > --- > Documentation/userspace-api/cells.rst | 861 ++++++++++++++++++++++++++++++++++ > Documentation/userspace-api/index.rst | 1 + > 2 files changed, 862 insertions(+) > > diff --git a/Documentation/userspace-api/cells.rst b/Documentation/userspace-api/cells.rst > new file mode 100644 > index 000000000000..76ce5267fdb6 > --- /dev/null > +++ b/Documentation/userspace-api/cells.rst > @@ -0,0 +1,861 @@ > +.. SPDX-License-Identifier: GPL-2.0 > + > +===== > +Cells > +===== > + > +A cell is a container as one kernel object. A task is born in a cell and never > +leaves it, and nothing can enter one. A cell is built from the outside: the > +builder fills named slots from file descriptors it holds, seals the cell, and > +spawns tasks into it. Holding the file descriptor of the cell, the *cellfd*, is > +the authority to do all of that. > + > +This document explains why cells exist, what they promise, how they are used and > +what the first implementation does not do yet. > + > +.. contents:: :local: > + > + > +Why > +=== > + > +A container on Linux is not an object. It is eight kinds of namespace, a cgroup, > +a seccomp filter and a set of conventions that a runtime holds together. A number > +of problems follow from that, and they do not go away by fixing bugs one at a > +time. > + > +There is nothing to name. > + The kernel cannot say which container a task is in, cannot kill a container in > + one operation and cannot tell what a container has access to. > + > +Membership is a property of a task at a moment. > + ``setns()`` and ``unshare()`` let a task change one namespace at a time. A task > + that is half way between two sets of namespaces, holding file descriptors, a > + working directory or a memory map from one side and credentials from the other, > + has been the source of many bugs. > + > +Setup runs on the inside. > + A runtime forks, and the child sheds, one call at a time, what it inherited and > + should not have. What it forgets to shed leaks into the container. > + > +Privilege hangs off ids. > + A user namespace gives its creator a full set of capabilities over everything > + the namespace owns, Yes this was explicitly one of the 4 user namespace design goals, and I don't really want code running on my system that I can't later control. > which puts a large amount of code that was written for root > + within reach of every user. You imply that cells don't do this, but the capabilities section below suggests otherwise. > Who may do what to whom is decided by comparing > + uids, It's also decided by namespace targeted capabilities (and LSM credentials). > and whether two uids are the same depends on how several runtimes agreed > + to carve up ``/etc/subuid``. Well, you are adding one more runtime (https://xkcd.com/927/ :) This particular problem would be better solved using a subuid NSS module. > +Hiding is done by mounting over. > + What a container must not see is commonly covered by a mount. ``MNT_LOCKED`` > + exists to keep such mounts in place, and a way to remove one, or to copy the tree > + without it, reveals what it covered. > + > +A cell turns this around. There is one object with one lifetime. What it holds > +was put there explicitly, by somebody who held it. What was not put there is not > +there. Of course userspace could do this now - create a userns, create an mntns, set up the mounts, and then create another userns with identity uid mapping, so then the container cannot get ns_capable(mnt_ns->userns, CAP_SYS_ADMIN) to unmount. I can envision this being a good strategy for sandbox style runners. > ... > +Capabilities > +------------ > + > +A legacy user namespace hands its first task every capability there is. A cell hands > +out none. This seems misleading. It hands any that you want, you just have to specify them differently, right? > **Being uid 0 of a cell comes with nothing**: it is one id of 65536, which > +cannot become another, give a file away or signal a task that runs under another id. > + > +What the tasks of a cell are to have, the builder names in the ``caps`` slot: a ``__u64`` > +with a bit for each capability, by its number in ``<linux/capability.h>``. Every task > +that is started in the cell has them permitted and effective. They are its bounding set Every task? But you do say setuid() drops them as usual. Are you saying that the first process is uid 0, and capability inheritance just works as usual? If not, then some examples would be helpful - and in that case it sounds like this would be an unexpected method of operation which would confuse userspace (which can be dangerous). > +too, so that is all anything in the cell will ever have: > + > +* Every task has ``no_new_privs``. It gains nothing from a file, or from being uid 0 > + when it runs one. > +* No task of a cell can make a user namespace, to have a full set in. > +* A capability that a task has let go of is gone for that task and its children. > + > +Letting go works as ever. ``capset(2)`` takes nothing, leaving uid 0 behind with > +``setuid()`` costs all of them, and ``PR_CAPBSET_DROP`` does not take ``CAP_SETPCAP`` in > +a cell. It does elsewhere lest a program that gains from its file runs with less than it > +counts on, and in a cell there is no such program. > + > +The capabilities are over the user namespace of the cell. They are good for the ids of > +the cell, for the files that belong to those in a tree that is mapped for the cell, for > +its tasks, and for what else that user namespace owns: its uts, ipc and pid namespace. It seems to me most of the bugs relating to capabilities in user namespace were not about the privilege leaking, which you seem to suggest here, but rather about kernel code under an ns_capable() check being reachable when, without unprivileged user namespaces, unprivileged users could not have reached them. Since you open quite a few capabilities for instance with net_may(), that code is still reachable. So you are probably reducing the reachable code, which is good, but the above is over-stating it. Also I would continue to be careful about uid mapped filesystems. If the host creates a cell with host uid 0 mapped to uid N in the cell, can a cell process chmod u+s the file, and collude with an unprivileged host process to run it as host-root? > +They are good for nothing else, which is why naming them takes no privilege. The mount > +namespace is not the cell's, so no capability lets a cell mount. Nor is the network > +namespace, see `Rights over the network`_. > + So what is your goal here? You list several properties of the current namespaces implementation, and imply that they are a problem. But what is the final goal: is it to implement a security sandbox that is stronger than what you can currently build from userspace? That's the impression I get. Or is it to avoid bugs in userspace, by simplifying what userspace needs to do? Knowing the goal will help review the patchset.set. Also, how does this jive with criu? Is it introducing any cell identifiers that can't be recreated? I believe that historically that has been the biggest blocker to a "container" like object in the kernel. -serge ^ permalink raw reply [flat|nested] 93+ messages in thread
* Re: [PATCH RFC v3 01/90] Documentation: add a design document for cells 2026-11-05 17:06 ` Serge E. Hallyn @ 2026-11-05 18:23 ` Josef Bacik 0 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 18:23 UTC (permalink / raw) To: Serge E. Hallyn Cc: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn, linux-fsdevel, linux-kernel On Thu, Nov 05, 2026 at 11:06:26AM -0600, Serge E. Hallyn wrote: Thanks for going through this. I'm going to answer your last question first, because the rest follows from it. > So what is your goal here? You list several properties of the current > namespaces implementation, and imply that they are a problem. But what > is the final goal: is it to implement a security sandbox that is > stronger than what you can currently build from userspace? That's the > impression I get. Or is it to avoid bugs in userspace, by simplifying > what userspace needs to do? Knowing the goal will help review the > patchset.set. Both. I want a security sandbox that's stronger than what userspace can build today, and I want the interface to be simple, with fewer footguns and fewer unexplained consequences that turn into holes. They're the same thing to me. You're right that you can get a lot of this out of user namespaces today, you point out one way below. But you get there by taking things away. You start with a copy of everything the parent had and a full set of capabilities, and then you have to know about each thing that needs to be dropped, hidden or locked. A cell starts as a blank space and you add what you want it to have. If you forget something with namespaces you have a hole, and nothing tells you. If you forget something with a cell, the thing doesn't work, and you go add it. The document doesn't say any of that up front, which is a pretty big thing to leave out. I'll fix that. > > +Privilege hangs off ids. > > + A user namespace gives its creator a full set of capabilities over everything > > + the namespace owns, > > Yes this was explicitly one of the 4 user namespace design goals, > and I don't really want code running on my system that I can't > later control. You can still control it. A capability you hold in an ancestor user namespace applies to a cell like it does to any other user namespace, so root on the host can signal and ptrace the tasks of a cell. The user who made the cell controls it with the cellfd, and a cell only lives as long as somebody holds one. When the last one is closed the cell is killed, so you can't lose track of one and have it keep running. The only thing that's gone is the special case in cap_capable() where having the same euid as the creator gets you everything. I don't want a uid to be the authority. > > which puts a large amount of code that was written for root > > + within reach of every user. > > You imply that cells don't do this, but the capabilities section below suggests > otherwise. [...] > It seems to me most of the bugs relating to capabilities in user > namespace were not about the privilege leaking, which you seem to > suggest here, but rather about kernel code under an ns_capable() > check being reachable when, without unprivileged user namespaces, > unprivileged users could not have reached them. Since you open quite > a few capabilities for instance with net_may(), that code is still > reachable. So you are probably reducing the reachable code, which > is good, but the above is over-stating it. I agree that reachable code is the problem, that's what I was trying to say and the document says it badly. I'll rewrite it around that. Here's what is reachable. The user namespace of a cell owns its uts, ipc and pid namespaces and nothing else. It doesn't own the mount namespace, so there's no mount(), and a task of a cell can't make any namespace, so it can't get one that it does own. That's where most of that code is. The network namespace isn't the cell's either, so CAP_NET_ADMIN in the cell gets you nothing by itself. It takes the right in the slot as well, and whoever fills the slot has to pass net_may() for each right on that network namespace themselves. So an unprivileged user can't use a cell to get at any of that code unless they could get at it already. Somebody who is allowed to do it has to hand it over, one right at a time. So it isn't nothing, you're right, but it's a lot less, and it's opt in. > > Who may do what to whom is decided by comparing > > + uids, > > It's also decided by namespace targeted capabilities (and LSM > credentials). Yup, I'll fix the wording. > > and whether two uids are the same depends on how several runtimes agreed > > + to carve up ``/etc/subuid``. > > Well, you are adding one more runtime (https://xkcd.com/927/ :) This > particular problem would be better solved using a subuid NSS module. Fair :). The difference I care about is who enforces it. /etc/subuid is an agreement between the tools that read it. Here the kernel hands out the block, and it makes sure nobody else has those ids: a legacy user namespace can't map into the window, and a filesystem that isn't the cell's won't store an id from it. There's nothing to configure and nothing to agree on. > Of course userspace could do this now - create a userns, create an mntns, > set up the mounts, and then create another userns with identity uid mapping, > so then the container cannot get ns_capable(mnt_ns->userns, CAP_SYS_ADMIN) to > unmount. > > I can envision this being a good strategy for sandbox style runners. Right, and this is the example I'd use for the above. It works, but you have to know to do it, and the inner user namespace still comes with a full set of capabilities, so it can unshare a mount namespace that it does own and we're back to relying on MNT_LOCKED. You then have to take that away as well. > > +A legacy user namespace hands its first task every capability there is. A cell hands > > +out none. > > This seems misleading. It hands any that you want, you just have to specify > them differently, right? Right, none unless the builder names them. I'll reword it. The point is the default. > Every task? But you do say setuid() drops them as usual. Are you saying > that the first process is uid 0, and capability inheritance just works > as usual? If not, then some examples would be helpful - and in that > case it sounds like this would be an unexpected method of operation > which would confuse userspace (which can be dangerous). Yes, it works as usual, the document is sloppy here. A task that is started in a cell is uid 0 of the cell and has what's in the slot as its permitted, effective and bounding sets. If you start it as another uid it loses them, the same as if it had called setresuid(). "Every task" was meant to say that this goes for each task you start in the cell from the outside, and not just for the first one. The two differences are that every task has no_new_privs, and that file capabilities are ignored. So what a task has let go of doesn't come back at the next exec, uid 0 or not. > Also I would continue to be careful about uid mapped filesystems. If > the host creates a cell with host uid 0 mapped to uid N in the > cell, can a cell process chmod u+s the file, and collude with an > unprivileged host process to run it as host-root? No, on both counts. Host uid 0 is never in a cell. A cell either has a block of ids out of the window, or the one id of whoever made it, and root doesn't get to make one of those, that's EPERM. But on disk ids are the real problem, since images are stored with ids from 0 up and the mount is idmapped for the cell, so uid 0 of the cell does own files that are uid 0 on disk. So nothing a cell writes imparts privilege: - S_ISUID, and S_ISGID on anything but a directory, are dropped from the mode of whatever a task of a cell creates, and from any mode it sets. - An existing file loses its set-id bits and its file capabilities as soon as a task of a cell opens it for writing, or is handed it open for writing. write() clears them already, a store through a shared mapping doesn't. - CAP_SETFCAP can't be put into the caps slot, so a cell can't write a file capability. That's the "Files" section of the document and patches 62 to 64, and there are tests for it. Please do poke at it, this is one of the places where I'd really like more eyes. > Also, how does this jive with criu? Is it introducing any cell identifiers > that can't be recreated? I believe that historically that has been the > biggest blocker to a "container" like object in the kernel. Good question, and there is one. cell_info(CELL_SELF) tells a task the id of its own cell, which comes from a counter and is never reused. Nothing needs that as far as I can tell, so I think the answer is to not tell the inside at all. Other than that I don't think so. On the inside the ids are always 0 to 65535, whichever block the cell got. The rest of cell_info() takes a cellfd, which only the outside has, and cells don't nest, so there's no cellfd on the inside to restore. The pid namespace is the cell's, so clone3() with set_tid should work given CAP_CHECKPOINT_RESTORE in the slot. What's missing is a way to pick the pid of a task that's started from the outside. That's all from reading the code though, I haven't tried criu on one. I'll add it to the list of what isn't there yet. Thanks, Josef ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 02/90] Documentation: add examples for cells 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 01/90] Documentation: add a design document for cells Josef Bacik @ 2026-11-05 14:56 ` Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 03/90] cells: reserve a window of ids Josef Bacik ` (87 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:56 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The design document explains what a cell is and why. Add a second page for people who write a builder. It walks through the common cases: running a single program, running a service from an image, devices, a network, an init system, starting another task in a running cell, and containers that belong together. A cell only has what it was given, and it sets up nothing by itself. Software that runs fine in a container often fails at first for that reason. The most common cause is capabilities, because a cell has none unless the builder names them. So the page lists a sensible starting set, says what each capability is for, and shows what the failure looks like when one is missing. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- Documentation/userspace-api/cells-examples.rst | 551 +++++++++++++++++++++++++ Documentation/userspace-api/index.rst | 1 + 2 files changed, 552 insertions(+) diff --git a/Documentation/userspace-api/cells-examples.rst b/Documentation/userspace-api/cells-examples.rst new file mode 100644 index 000000000000..2c0c64dca906 --- /dev/null +++ b/Documentation/userspace-api/cells-examples.rst @@ -0,0 +1,551 @@ +.. SPDX-License-Identifier: GPL-2.0 + +=========== +Using cells +=========== + +Documentation/userspace-api/cells.rst says what a cell is and why it is the way it is. +This page is for whoever writes a builder: a container runtime, a sandbox, a test +harness. It goes through what a builder commonly wants, and through what commonly goes +wrong the first time. + +One rule explains most of it. **A cell has what it was given, and it sets up nothing by +itself.** It mounts nothing, it configures no network, and being its root comes with no +capability. What a container today does for itself on its way up, the builder of a cell +does from the outside, before the first task runs. + +Error handling is left out of the examples. There are no wrappers for the system calls +in the C library yet, so ``cell_create()`` and the others stand for ``syscall(2)``. + +.. contents:: :local: + + +A program and nothing else +========================== + +The smallest cell takes no privilege. It runs one program as the user who builds it, +which can see the tree it was given and reach nothing else: no network, no other task, +no capability. + +.. code-block:: c + + int cfd, tree, ns, pidfd, status; + + cfd = cell_create(CELL_SELF, 0); + + tree = open_tree(AT_FDCWD, "/home/me/sandbox", OPEN_TREE_CLONE); + ns = open_tree(tree, "", AT_EMPTY_PATH | AT_RECURSIVE | OPEN_TREE_NAMESPACE); + cell_configure(cfd, CELL_SET_FD, "root", NULL, ns); + cell_configure(cfd, CELL_SET_FD, "budget", NULL, cgroup_dirfd); + cell_configure(cfd, CELL_CMD_SEAL, NULL, NULL, 0); + + pidfd = pidfd_open(cfd, PIDFD_EMPTY); + pidfd_configure(pidfd, PIDFD_SET_STRING, "exec", "/bin/convert", 0); + pidfd_configure(pidfd, PIDFD_SET_BINARY, "argv", "convert\0-\0png:-", 16); + pidfd_configure(pidfd, PIDFD_SET_FD, "0", NULL, in_fd); + pidfd_configure(pidfd, PIDFD_SET_FD, "1", NULL, out_fd); + pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0); + + read(cfd, &status, sizeof(status)); + +The ``ids`` slot was left empty, so this is a one-id cell. A builder without privilege +does the mounting in a user and mount namespace of its own, since ``open_tree()`` with +``OPEN_TREE_CLONE`` takes ``CAP_SYS_ADMIN`` over the mount namespace of the caller. The +cgroup has to be one that it owns and could move itself into, such as one that +``systemd --user`` has delegated to it. + +The new task has the three files it was given and no others. There is nothing to close +and nothing to forget. + + +Which capabilities +================== + +.. note:: + + If software that runs fine in a container fails in a cell with ``EPERM``, this is the + first place to look. + +A container runtime starts from everything and takes away. Docker leaves fourteen +capabilities by default. A cell starts from nothing, and **uid 0 of a cell is an id like +any other** until the builder says otherwise: + +.. code-block:: c + + __u64 caps = 1ULL << CAP_CHOWN | 1ULL << CAP_DAC_OVERRIDE | 1ULL << CAP_FOWNER | + 1ULL << CAP_SETUID | 1ULL << CAP_SETGID | 1ULL << CAP_KILL; + + cell_configure(cfd, CELL_SET_BINARY, "caps", &caps, sizeof(caps)); + +Those six are what it takes to be root over the ids of the cell, and they are a good +place to start for anything that expects to be started as root: an init system, a +package manager, a daemon that drops privileges. All of them are about the cell's own +ids, files and tasks. None of them reaches anything else. + +====================== ========================================================== +Capability What stops working without it +====================== ========================================================== +``CAP_SETUID``, Becoming another user: ``setuid()``, ``setgid()``, +``CAP_SETGID`` ``setgroups()``. A daemon that drops privileges, ``su`` + and ``runuser`` run by root, ``User=`` in a unit. +``CAP_CHOWN`` Giving a file to another id: unpacking an archive with its + owners, ``install -o``, ``systemd-tmpfiles``, delegating a + cgroup. +``CAP_FOWNER`` ``chmod()``, ``utimes()`` and the like on a file that + belongs to another id, including one that was just given + away. +``CAP_DAC_OVERRIDE`` Reading, writing and creating where the mode says no to + uid 0, such as in the home or the state directory of + another id. +``CAP_KILL`` Signalling a task that runs under another id. An init + cannot stop such a service. +====================== ========================================================== + +A program that runs as one id from start to end needs none of them. That is the common +case for a single service, and the best one: start it as the id it is to run as, with the +``cred`` key of ``pidfd_configure()``, and leave the slot empty. + +.. code-block:: c + + struct pidfd_cred cred = { .uid = 33, .gid = 33 }; /* ids of the cell */ + + pidfd_configure(pidfd, PIDFD_SET_BINARY, "cred", &cred, sizeof(cred)); + +Others that come up: + +``CAP_NET_ADMIN``, ``CAP_NET_RAW`` + Good for nothing by themselves. They say who in the cell may use the rights over its + network that the builder has named, see `A network`_. Listening on port 80 and ``ping`` + do not take a capability at all. + +``CAP_SYS_CHROOT`` + For ``chroot()``, which some daemons use on themselves. + +``CAP_SYS_PTRACE`` + For debugging, and reading ``/proc/<pid>`` of, a task that runs under another id. + +``CAP_SYS_ADMIN`` + Over a cell this is far less than the name suggests. It does not let a cell mount or + make a namespace. It is for ``sethostname()`` and a few more. Give it only to what + asks for it, see `An init system`_. + +``CAP_SETPCAP`` + Not needed to shrink the bounding set, which in a cell takes nothing. + +``CAP_SETFCAP`` + Refused. Nothing that a cell writes imparts privilege. + +``CAP_MKNOD``, ``CAP_SYS_MODULE``, ``CAP_SYS_RAWIO``, ``CAP_SYS_TIME`` ... + Can be named and do nothing. What they guard asks for them over the user namespace of + the host. + +What it looks like when one is missing: + +===================================================== ======================== +Seen Wants +===================================================== ======================== +``setgroups: Operation not permitted``, ``CAP_SETGID``, +systemd: ``status=216/GROUP`` ``CAP_SETUID`` +``chown: Operation not permitted``, ``CAP_CHOWN`` +``fchownat() of ... failed`` +``fchmod() of ... failed: Operation not permitted`` ``CAP_FOWNER`` +right after a ``chown`` +``Permission denied`` for root ``CAP_DAC_OVERRIDE`` +systemd: ``Failed to kill main process`` ``CAP_KILL`` +systemd: ``status=217/USER`` for a unit with ``CAP_SYS_ADMIN``, or +``PrivateTmp=`` and the like drop the options +===================================================== ======================== + +``grep Cap /proc/self/status`` in the cell and ``cell_info()`` with ``CELL_INFO_CAPS`` +outside of it tell what there is. + +The slot is for the whole cell, and it is an upper limit. A task can let go of what it +does not need, for itself and for what it starts, and nothing brings it back. + + +A service from an image +======================= + +An image is stored with on-disk ids from 0 up. A block of ids and an idmapped mount make +those the ids of the cell, for this cell and for as long as it lives. Several cells can +use one image at a time. None of them sees the ids of another, and nothing is written to +the image that says which cell wrote it. + +.. code-block:: c + + unsigned int ids = 65536; + struct mount_attr attr = { .attr_set = MOUNT_ATTR_IDMAP }; + + cfd = cell_create(CELL_SELF, 0); + cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)); + + attr.userns_fd = cfd; /* a cellfd stands for the ids of the cell */ + tree = open_tree_attr(AT_FDCWD, "/images/web", OPEN_TREE_CLONE, &attr, sizeof(attr)); + +The ``ids`` slot has to be filled before a mount can be mapped for the cell. Every +writable mount of a filesystem that can be idmapped has to be, or the seal fails. + +For a read-only image with a writable layer, idmap the overlay and leave the layers as +they are. + +What else goes into the tree is mounted on it before it becomes a mount namespace. A +filesystem for the cell to write to is mapped in the same way: + +.. code-block:: c + + fs = fsopen("tmpfs", 0); + fsconfig(fs, FSCONFIG_SET_STRING, "mode", "1777", 0); + fsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL, 0); + mnt = fsmount(fs, 0, MOUNT_ATTR_NOSUID | MOUNT_ATTR_NODEV); + mount_setattr(mnt, "", AT_EMPTY_PATH, &attr, sizeof(attr)); + move_mount(mnt, "", tree, "tmp", MOVE_MOUNT_F_EMPTY_PATH); + +``/proc`` is one for the pid namespace of the cell, which the builder gets from the cell +while it is being built: + +.. code-block:: c + + pidns = cell_open(cfd, CELL_OPEN_SLOT, "pids", NULL, 0); + fs = fsopen("proc", 0); + fsconfig(fs, FSCONFIG_SET_FD, "pidns", NULL, pidns); + fsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL, 0); + mnt = fsmount(fs, 0, MOUNT_ATTR_NOSUID | MOUNT_ATTR_NODEV | MOUNT_ATTR_NOEXEC); + move_mount(mnt, "", tree, "proc", MOVE_MOUNT_F_EMPTY_PATH); + +.. warning:: + + A procfs that is mounted the plain way shows the tasks of the builder's pid namespace, + and a sysfs the network devices of the network namespace it was mounted from. See + `An init system`_ for sysfs. + + +Devices +======= + +A device is its number. The builder makes a node in a filesystem that is mapped for the +cell, and who in the cell may open it is a matter of the owner, group and mode of that +node, in the ids of the cell. + +.. code-block:: c + + dev = fsmount(fs, 0, MOUNT_ATTR_NOSUID); /* a tmpfs, not yet mapped */ + mknodat(dev, "null", S_IFCHR | 0666, makedev(1, 3)); + fchmodat(dev, "null", 0666, 0); /* mknod() goes by the umask */ + mknodat(dev, "kvm", S_IFCHR | 0660, makedev(10, 232)); + fchownat(dev, "kvm", 0, 36, 0); /* group kvm of the image */ + mount_setattr(dev, "", AT_EMPTY_PATH, &attr, sizeof(attr)); + move_mount(dev, "", tree, "dev", MOVE_MOUNT_F_EMPTY_PATH); + +Binding the node of the host's ``/dev`` does not do it. That one belongs to ids of the +host, which nobody in a cell has, its root included, so what is left is what the mode has +for everybody. A device can also be handed over open, as a file of a new task. + +A cell makes no device nodes, whatever its capabilities. + + +A network +========= + +What is in the ``net`` slot decides, see "The network" in +Documentation/userspace-api/cells.rst. Most builders want ``"new"``: a network namespace +that is made for the cell, belongs to the builder, and is set up by the builder. + +.. code-block:: c + + cell_configure(cfd, CELL_SET_STRING, "net", "new", 0); + net = cell_open(cfd, CELL_OPEN_SLOT, "net", NULL, 0); + + /* + * Move one end of a veth pair there, by IFLA_NET_NS_FD. Then, in a helper that + * has called setns(net, CLONE_NEWNET): addresses, routes, and the links up. + */ + +Nothing is done for the builder. Until it brings ``lo`` up, connecting to 127.0.0.1 fails +with ``ENETUNREACH``. + +No module is loaded because a cell asked for it. A protocol, a tunnel or a packet filter +that the cell is to use and that is built as a module has to be loaded on the host +beforehand, or ``socket()`` fails with ``EAFNOSUPPORT`` in the cell where it works outside. + +What a container commonly has a capability for is a setting of the namespace here, which +the helper makes while it is in there: + +.. code-block:: sh + + sysctl -w net.ipv4.ip_unprivileged_port_start=80 # instead of CAP_NET_BIND_SERVICE + sysctl -w net.ipv4.ping_group_range="0 4294967294" # instead of CAP_NET_RAW, for ping + +.. note:: + + The range is in the gids of whoever writes it, and the gids of a cell are those of its + block, from 2147483648 up. The customary ``0 2147483647`` ends one short of the first + of them, and ``ping`` does not work. Read from inside of the cell the setting + shows as ``65534 65534`` either way. + +The builder can go back in at any time, so the network can change while the cell runs: +devices come and go, addresses and routes change. Once the cell is sealed the way in is +the file that ``cell_open()`` returned, or ``/proc/<pid>/ns/net`` of any of its tasks. + +**CNI plugins** work as they are. They take a path for the namespace, and either will do: + +.. code-block:: sh + + # a namespace that was made and pinned beforehand, as container runtimes do + ip netns add cni-1234 + CNI_COMMAND=ADD CNI_NETNS=/run/netns/cni-1234 CNI_IFNAME=eth0 ... ptp < conf + # then: cell_configure(cfd, CELL_SET_FD, "net", NULL, open("/run/netns/cni-1234")) + + # or the one of the cell, "new", by way of the file that cell_open() returned + CNI_COMMAND=ADD CNI_NETNS=/proc/$builder/fd/$net CNI_IFNAME=eth0 ... ptp < conf + +The second needs no pin, so nothing is left behind if the runtime goes away. **Keep that +file.** ``cell_open()`` returns it only while the cell is being built, and a cell that dies +takes its network along. With the file the network comes down in the customary order, +tasks first: + +.. code-block:: c + + cell_kill(cfd, 0); + poll(&(struct pollfd){ .fd = cfd, .events = POLLIN }, 1, -1); + /* CNI_COMMAND=DEL CNI_NETNS=/proc/$builder/fd/$net ...: all there, nobody in it */ + close(net); + +Without it the namespace is gone by the time anybody notices that the init of the cell has +exited, and a plugin can undo only what it finds without. ``ptp`` then still succeeds and +gives the address back, and the veth pair went with the namespace, but the masquerading +rules that it made on the host stay behind. + +A rule that goes by a uid or a gid means the ids of whoever owns the network namespace, and +that is the builder. To it, uid 1337 of the cell is not 1337. ``cell_info()`` tells what +it is, from when the ``ids`` slot is filled: + +.. code-block:: c + + struct cell_info info = { .mask = CELL_INFO_IDS_FIRST }; + + cell_info(cfd, &info, sizeof(info), 0); + /* nft add rule ... meta skuid (info.ids_first + 1337) ... + * ip rule add uidrange ..., iptables -m owner --uid-owner ... */ + +The bit is not set in ``mask`` on return if the caller has no name for those ids, as in a +user namespace that maps nothing but the caller. To such a builder everybody in the cell +is the overflow uid, and it cannot tell them apart in a rule. + +What a dataplane hangs on the outside applies to a cell as to any container, and the cell +can neither see nor undo it: filters on the host's end of a veth or netkit pair, and BPF +programs on a cgroup. One that is attached to the budget holds for every cgroup that the +cell makes below. + +A device of the host's can be moved in as well, a NIC for one. It goes back to the host +when the namespace goes. With ``"new"`` that is when the cell is dead, provided that +nobody else holds the namespace: no file, no bind mount. A pinned namespace keeps what was +moved there until it is unpinned, whatever has become of the cell. + +A cell that is to change something about its network is given the right to, by name. The +namespace stays the builder's: a CNI plugin sets it up as ever, and the cell does what it +was given the right to on top of that. + +.. code-block:: c + + /* It adds rules to the packet filter, and nothing else. */ + __u64 rights = CELL_NET_FILTER; + __u64 caps = 1ULL << CAP_NET_ADMIN; + + cell_configure(cfd, CELL_SET_STRING, "net", "new", 0); /* first */ + cell_configure(cfd, CELL_SET_BINARY, "net-rights", &rights, sizeof(rights)); + cell_configure(cfd, CELL_SET_BINARY, "caps", &caps, sizeof(caps)); + +It takes both. The right says what the cell may do to its network, and the capability says +who in the cell may do it: ``CAP_NET_RAW`` for ``CELL_NET_RAW``, ``CAP_NET_ADMIN`` for all +others. What sets up the rules can let go of the capability before it runs the rest. + +=============================================== ==================================== +What runs in the cell Rights +=============================================== ==================================== +``iptables``, ``nft`` ``FILTER``; ``iptables-legacy`` also + ``RAW``, for the socket it uses +a VPN client ``TUN``, ``LINK``, ``ADDR``, ``ROUTE`` +a VMM with a TAP device and a bridge ``TUN``, ``LINK``, ``LINK_NEW`` +``tcpdump``, ``ping`` the old way ``RAW`` +a transparent proxy ``SOCKOPT``, with rules and routes that + the builder can just as well make +a DHCP client ``RAW``, ``ADDR``, ``ROUTE`` +what runs the network of the host, given that what it does there, and the builder +for a ``net`` slot needs to be able to do that itself +=============================================== ==================================== + +A request that takes a right which the cell does not have fails as it does for anybody +without the capability, with ``EPERM``, or ``EACCES`` below ``/proc/sys/net``. + +A cell that was given the network namespace of the host can use the network of the host, and +listen on a port from ``ip_unprivileged_port_start`` up. + +A program that wants a TAP device, a VMM for one, needs ``/dev/net/tun`` in its tree and +one of these: + +* ``CELL_NET_TUN``, and it makes the device, or attaches to one that the builder has made; +* the device already open, as a file of the new task. + +Naming an owner for the device does not do it. The owner and the group of a tun device +count in the cell that named them and in no other, so what the builder sets with +``TUNSETOWNER`` means nothing in the cell, whichever id it is. Within the cell they work as +ever: a task that has the right can make a device for another that has not. + + +An init system +============== + +An init expects to find a system, or to put one together. In a cell it cannot do the +latter, so the builder does. This is what systemd 255 takes to boot to +``multi-user.target``, run units under other users with limits of their own, and shut +down in order. + +Mounted on the tree, besides the root itself: + +===================== =============================================================== +Where What +===================== =============================================================== +``/proc`` procfs for the pid namespace of the cell +``/sys`` sysfs, read-only, see below +``/sys/fs/cgroup`` the budget: ``open_tree(AT_FDCWD, budget, OPEN_TREE_CLONE)`` +``/dev`` a tmpfs that is mapped for the cell, with ``null``, ``zero``, + ``full``, ``random``, ``urandom``, ``tty`` and ``console``, and + the links ``fd``, ``stdin``, ``stdout``, ``stderr``, ``ptmx`` +``/dev/pts`` devpts with ``newinstance,ptmxmode=0666`` +``/dev/shm``, a tmpfs each, mapped for the cell +``/run``, ``/tmp`` +``/run/lock`` likewise. Debian and Ubuntu do not boot without it +===================== =============================================================== + +systemd leaves alone what is mounted already. What is not, it tries to mount, and gives +up with "Failed to mount API filesystems". + +A sysfs shows the network devices of the network namespace that its mounter was in. For +``"new"`` the builder mounts it from in there: + +.. code-block:: c + + old = open("/proc/self/ns/net", O_RDONLY); + setns(net, CLONE_NEWNET); + fs = fsopen("sysfs", 0); + fsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL, 0); + mnt = fsmount(fs, 0, MOUNT_ATTR_RDONLY | MOUNT_ATTR_NOSUID | MOUNT_ATTR_NODEV | + MOUNT_ATTR_NOEXEC); + setns(old, CLONE_NEWNET); + +Without a network, ``unshare(CLONE_NEWNET)`` in place of the first ``setns()`` makes for an +empty one. + +The environment says that this is a container, which keeps systemd from what is only for +real hardware: + +.. code-block:: c + + pidfd_configure(pidfd, PIDFD_SET_STRING, "exec", "/usr/lib/systemd/systemd", 0); + pidfd_configure(pidfd, PIDFD_SET_BINARY, "argv", "systemd", 8); + pidfd_configure(pidfd, PIDFD_SET_BINARY, "envp", "container=cell", 15); + +The six capabilities of `Which capabilities`_ are enough. systemd goes by the bounding set +of pid 1 to tell what is worth trying, so it skips debugfs, tracefs and the mqueue +filesystem by itself. + +It manages the cgroups below the budget as it does anywhere: it moves itself to +``init.scope``, enables controllers, and makes a cgroup with limits for every unit. The +limits of the budget itself are not the cell's to change. + +When pid 1 exits, by ``SuccessAction=exit`` of a unit for one, its wait status is what +``read()`` on the cellfd returns. + +.. note:: + + ``PrivateTmp=``, ``ProtectSystem=``, ``ProtectHome=``, ``PrivateDevices=`` and the other + options that are done with a mount namespace do not work, since a cell makes no + namespaces. What systemd makes of that depends on ``CAP_SYS_ADMIN``. With it, it tries, + fails, and **starts the unit without them**, which it logs at debug level only. Without + it, it tries a user namespace first, and the unit fails with ``status=217/USER``. + ``NoNewPrivileges=``, ``SystemCallFilter=`` and what else is done with seccomp work. + The cell is the sandbox. What needs to be kept apart goes into cells of its own. + + +Another task in a running cell +============================== + +There is no entering a cell. What takes the place of ``nsenter`` and ``docker exec`` is +another spawn, by whoever holds the cellfd: + +.. code-block:: c + + pidfd = pidfd_open(cfd, PIDFD_EMPTY); + pidfd_configure(pidfd, PIDFD_SET_STRING, "exec", "/bin/sh", 0); + pidfd_configure(pidfd, PIDFD_SET_FD, "0", NULL, pty); + pidfd_configure(pidfd, PIDFD_SET_FD, "1", NULL, pty); + pidfd_configure(pidfd, PIDFD_SET_FD, "2", NULL, pty); + ret = pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0); + +The task starts in the budget. If the init of the cell manages cgroups that fails with +``EBUSY``: it has enabled controllers on the budget, and a cgroup that passes a controller +on holds no task. The builder then says where, and the cgroup that the init is in will +always do. A start can be tried only once, so this takes a new pidfd: + +.. code-block:: c + + /* "0::/cell0/init.scope", as the host sees it */ + snprintf(path, sizeof(path), "/proc/%d/cgroup", init_pid); + ... + cg = open("/sys/fs/cgroup/cell0/init.scope", O_RDONLY | O_DIRECTORY); + pidfd_configure(pidfd, PIDFD_SET_FD, "cgroup", NULL, cg); + +It has to be the budget or lie below it. It takes no permission on the cgroup, which +belongs to ids that nobody outside of the cell has. Should the cell have removed it in +the meantime the start fails, with ``ENODEV``, and the builder looks again. + +The new task has the capabilities in the ``caps`` slot, as every task that is started in +the cell. Its parent is the spawner, but nobody has to wait for it: the pidfd tells how it +ended. + + +Containers that belong together +=============================== + +Containers that trust each other, as those of a pod do, are tasks of one cell. They share +its pid namespace and network, can signal each other and pass each other files. The +builder makes a cgroup below the budget for each, sets its limits, and starts each where +it belongs: + +.. code-block:: c + + mkdirat(budget, "pause", 0755); + mkdirat(budget, "app", 0755); + mkdirat(budget, "sidecar", 0755); + /* the first task is the init of the cell: when it exits, so does the cell */ + start(cfd, "/pause", openat(budget, "pause", O_RDONLY | O_DIRECTORY)); + write_at(budget, "cgroup.subtree_control", "+memory +pids"); + write_at(budget, "app/memory.max", "1G"); + write_at(budget, "sidecar/memory.max", "128M"); + start(cfd, "/app/run", openat(budget, "app", O_RDONLY | O_DIRECTORY)); + start(cfd, "/sidecar/run", openat(budget, "sidecar", O_RDONLY | O_DIRECTORY)); + +The seal hands the budget to the ids of the cell, so from then on this takes a builder that +is not held back by the owner of a file. They all start at the root of the cell for now. +What does not trust each other goes into cells of its own. + + +The end +======= + +A cell is dead when its first task has exited and everything else in it is gone. The +cellfd then polls readable, and ``read()`` returns the wait status of the first task. + +.. code-block:: c + + struct pollfd p = { .fd = cfd, .events = POLLIN }; + + if (!poll(&p, 1, 30000)) + cell_kill(cfd, 0); /* everything in it, wherever it has got to */ + read(cfd, &status, sizeof(status)); + +``cell_kill()`` does not wait, the ``read()`` does. Closing the last cellfd kills the cell +too, so a builder that dies takes its cells along. + +The cgroups that the cell made below the budget are removed with it, and the budget is +given back to whoever owned it. diff --git a/Documentation/userspace-api/index.rst b/Documentation/userspace-api/index.rst index b9d2e4bef24c..f11307aa8970 100644 --- a/Documentation/userspace-api/index.rst +++ b/Documentation/userspace-api/index.rst @@ -18,6 +18,7 @@ System calls unshare cells + cells-examples futex2 ebpf/index ioctl/index -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 03/90] cells: reserve a window of ids 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 01/90] Documentation: add a design document for cells Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 02/90] Documentation: add examples " Josef Bacik @ 2026-11-05 14:56 ` Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 04/90] user_namespace: let a cell own a user namespace Josef Bacik ` (86 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:56 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A cell needs uids and gids of its own. The builder should not have to be trusted to pick ids that nobody else uses, so the kernel picks them. Reserve the upper half of the uid and gid space, minus its last block, as a window. The kernel will hand out blocks of 65536 ids from this window to cells. Add helpers that tell whether an id, or a range of ids, lies in the window. None of this is compiled yet. CONFIG_CELLS is added at the end of the series. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cell_ids.h | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/include/linux/cell_ids.h b/include/linux/cell_ids.h new file mode 100644 index 000000000000..df0b874d5788 --- /dev/null +++ b/include/linux/cell_ids.h @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _LINUX_CELL_IDS_H +#define _LINUX_CELL_IDS_H + +#include <linux/compiler.h> +#include <linux/limits.h> +#include <linux/uidgid.h> + +struct user_namespace; + +/* + * The kernel hands every multi-id cell a block of kuids and kgids out of this + * window. Nobody else may hold an id in it: a legacy user namespace cannot map + * into it, and no create, chown or ACL stores one in a filesystem that the cell + * it was lent to does not own. + * + * The last block is left out so that (uid_t)-1 stays clear of it. + */ +#define CELL_ID_BLOCK_SIZE 65536U +#define CELL_ID_WINDOW_FIRST 0x80000000U +#define CELL_ID_WINDOW_LAST 0xfffeffffU +#define CELL_ID_NR_BLOCKS \ + ((CELL_ID_WINDOW_LAST - CELL_ID_WINDOW_FIRST + 1) / CELL_ID_BLOCK_SIZE) + +#ifdef CONFIG_CELLS + +/** + * cell_id_in_window - is an id one of those the kernel lends to cells? + * @id: the value of a kuid or kgid + * + * Return: true if it is. + */ +static inline bool cell_id_in_window(u32 id) +{ + return id >= CELL_ID_WINDOW_FIRST && id <= CELL_ID_WINDOW_LAST; +} + +/** + * cell_range_in_window - does a range of ids touch the window? + * @first: the value of the first kuid or kgid of the range + * @count: how many there are + * + * Return: true if any id of the range lies in the window. + */ +static inline bool cell_range_in_window(u32 first, u32 count) +{ + u32 last = first + count - 1; + + if (!count) + return false; + if (last < first) + last = U32_MAX; + return first <= CELL_ID_WINDOW_LAST && last >= CELL_ID_WINDOW_FIRST; +} + +#else + +static inline bool cell_id_in_window(u32 id) { return false; } +static inline bool cell_range_in_window(u32 first, u32 count) { return false; } +#endif +#endif /* _LINUX_CELL_IDS_H */ -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 04/90] user_namespace: let a cell own a user namespace 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (2 preceding siblings ...) 2026-11-05 14:56 ` [PATCH RFC v3 03/90] cells: reserve a window of ids Josef Bacik @ 2026-11-05 14:56 ` Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 05/90] user_namespace: refuse setns() into a cell Josef Bacik ` (85 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:56 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Every user of a kuid in the kernel already knows how to deal with user namespaces. So give each cell one user namespace, which the kernel creates together with the cell. Add create_cell_user_ns(). The namespace holds a reference on its cell for as long as the namespace exists. The pointer to the cell is set before anybody else can find the namespace. This makes the user namespace the link from a credential to its cell. Add the accessors cred_cell(), current_cell(), task_cell() and a few more. They return NULL for a task that is in no cell. Because the cell is found through the credential, override_creds() and file->f_cred work without any extra code. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cell.h | 148 +++++++++++++++++++++++++++++++++++++++++ include/linux/cell_ids.h | 4 ++ include/linux/user_namespace.h | 7 ++ kernel/user_namespace.c | 56 +++++++++++++++- 4 files changed, 214 insertions(+), 1 deletion(-) diff --git a/include/linux/cell.h b/include/linux/cell.h new file mode 100644 index 000000000000..6a65d035ffca --- /dev/null +++ b/include/linux/cell.h @@ -0,0 +1,148 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _LINUX_CELL_H +#define _LINUX_CELL_H + +/* + * Cells, see Documentation/userspace-api/cells.rst. + * + * Each group of declarations below comes with its own stubs for a kernel + * without CONFIG_CELLS. + */ + +#include <linux/cell_ids.h> +#include <linux/cred.h> +#include <linux/refcount.h> +#include <linux/sched.h> +#include <linux/stat.h> +#include <linux/uidgid.h> +#include <linux/user_namespace.h> + +struct cell; +struct cgroup; +struct file; +struct kernel_clone_args; +struct nsproxy; +struct path; +struct pid; +struct task_struct; + +/* Who is in which cell. */ +#ifdef CONFIG_CELLS + +/** + * userns_cell - the cell a user namespace belongs to + * @ns: a user namespace + * + * Return: the cell, or NULL if @ns is a legacy user namespace. No reference is + * taken: @ns holds one for as long as it lives. + */ +static inline struct cell *userns_cell(const struct user_namespace *ns) +{ + return ns->cell; +} + +/** + * cred_cell - the cell a credential belongs to + * @cred: a credential + * + * The cell is reached through the user namespace of the credential, so it + * follows override_creds() and file->f_cred without any help. + * + * Return: the cell, or NULL for the host. No reference is taken: @cred pins its + * user namespace, which pins the cell. + */ +static inline struct cell *cred_cell(const struct cred *cred) +{ + return userns_cell(cred->user_ns); +} + +/** + * current_cell - the cell the caller acts in + * + * Goes by the subjective credentials, so a caller that has overridden its + * credentials acts in the cell of those. + * + * Return: the cell, or NULL for the host. + */ +static inline struct cell *current_cell(void) +{ + return cred_cell(current_cred()); +} + +/** + * task_cell - the cell a task is in + * @task: a task + * + * A task is born in a cell and never changes cell. + * + * Context: The caller holds rcu_read_lock(), or otherwise keeps the objective + * credentials of @task from going away. + * Return: the cell, or NULL for the host. + */ +static inline struct cell *task_cell(const struct task_struct *task) +{ + return cred_cell(__task_cred(task)); +} + +/** + * cell_same - do two credentials belong to the same cell? + * @a: a credential + * @b: another + * + * Across cells an equal uid means nothing: a one-id cell runs on the kuid of + * whoever made it. A check that lets one task act on another because their uids + * are equal asks this as well, and otherwise falls back to whatever capability + * check it has. + * + * Return: true if both belong to the same cell, or both to the host. + */ +static inline bool cell_same(const struct cred *a, const struct cred *b) +{ + return cred_cell(a) == cred_cell(b); +} + +u64 cell_id(const struct cell *cell); + +/** + * cred_cell_id - the id of the cell a credential belongs to + * @cred: a credential + * + * For where a cell has to be remembered beyond the life of anything that pins + * it. Ids are never reused, so they can be compared at any later time. + * + * Return: the id of the cell, or 0 for the host. + */ +static inline u64 cred_cell_id(const struct cred *cred) +{ + struct cell *cell = cred_cell(cred); + + return cell ? cell_id(cell) : 0; +} + +/** + * current_cell_id - the id of the cell the caller acts in + * + * Return: the id of the cell, or 0 for the host. + */ +static inline u64 current_cell_id(void) +{ + return cred_cell_id(current_cred()); +} + +struct cell *cell_get(struct cell *cell); +void cell_put(struct cell *cell); + +#else + +static inline struct cell *userns_cell(const struct user_namespace *ns) { return NULL; } +static inline struct cell *cred_cell(const struct cred *cred) { return NULL; } +static inline struct cell *current_cell(void) { return NULL; } +static inline struct cell *task_cell(const struct task_struct *t) { return NULL; } +static inline bool cell_same(const struct cred *a, const struct cred *b) { return true; } +static inline u64 cred_cell_id(const struct cred *cred) { return 0; } +static inline u64 current_cell_id(void) { return 0; } +static inline void cell_put(struct cell *cell) { } + +#endif + +#endif /* _LINUX_CELL_H */ diff --git a/include/linux/cell_ids.h b/include/linux/cell_ids.h index df0b874d5788..dcb0a559d0e9 100644 --- a/include/linux/cell_ids.h +++ b/include/linux/cell_ids.h @@ -53,9 +53,13 @@ static inline bool cell_range_in_window(u32 first, u32 count) return first <= CELL_ID_WINDOW_LAST && last >= CELL_ID_WINDOW_FIRST; } +bool userns_is_cell(const struct user_namespace *ns); + #else static inline bool cell_id_in_window(u32 id) { return false; } static inline bool cell_range_in_window(u32 first, u32 count) { return false; } +static inline bool userns_is_cell(const struct user_namespace *ns) { return false; } + #endif #endif /* _LINUX_CELL_IDS_H */ diff --git a/include/linux/user_namespace.h b/include/linux/user_namespace.h index 91232053775d..5c8cf31ab9d4 100644 --- a/include/linux/user_namespace.h +++ b/include/linux/user_namespace.h @@ -40,6 +40,7 @@ struct uid_gid_map { /* 64 bytes -- 1 cache line */ #define USERNS_INIT_FLAGS USERNS_SETGROUPS_ALLOWED struct ucounts; +struct cell; enum ucount_type { UCOUNT_USER_NAMESPACES, @@ -117,6 +118,9 @@ struct user_namespace { #if IS_ENABLED(CONFIG_BINFMT_MISC) struct binfmt_misc *binfmt_misc; #endif +#ifdef CONFIG_CELLS + struct cell *cell; /* whose namespace this is, counted */ +#endif } __randomize_layout; struct ucounts { @@ -184,6 +188,9 @@ static inline struct user_namespace *get_user_ns(struct user_namespace *ns) } extern int create_user_ns(struct cred *new); +#ifdef CONFIG_CELLS +int create_cell_user_ns(struct cred *new, struct cell *cell); +#endif extern int unshare_userns(unsigned long unshare_flags, struct cred **new_cred); extern void __put_user_ns(struct user_namespace *ns); diff --git a/kernel/user_namespace.c b/kernel/user_namespace.c index cd748e1cbace..9301d0d41cb9 100644 --- a/kernel/user_namespace.c +++ b/kernel/user_namespace.c @@ -2,6 +2,7 @@ #include <kunit/visibility.h> #include <linux/export.h> +#include <linux/cell.h> #include <linux/nsproxy.h> #include <linux/slab.h> #include <linux/sched/signal.h> @@ -64,6 +65,21 @@ static void set_cred_user_ns(struct cred *cred, struct user_namespace *user_ns) cred->user_ns = user_ns; } +#ifdef CONFIG_CELLS +/** + * userns_is_cell - is this the user namespace of a cell? + * @ns: a user namespace + * + * Out of line for the headers that cannot see into struct user_namespace. + * + * Return: true if it is, false for a legacy user namespace. + */ +bool userns_is_cell(const struct user_namespace *ns) +{ + return ns->cell; +} +#endif + static unsigned long enforced_nproc_rlimit(void) { unsigned long limit = RLIM_INFINITY; @@ -83,8 +99,10 @@ static unsigned long enforced_nproc_rlimit(void) * * This is called by copy_creds(), which will finish setting the target task's * credentials. + * + * @cell is the cell the namespace is made for, or NULL for a legacy one. */ -int create_user_ns(struct cred *new) +static int __create_user_ns(struct cred *new, struct cell *cell) { struct user_namespace *ns, *parent_ns = new->user_ns; kuid_t owner = new->euid; @@ -162,6 +180,11 @@ int create_user_ns(struct cred *new) if (!setup_userns_sysctls(ns)) goto fail_keyring; +#ifdef CONFIG_CELLS + /* Before anybody can find it. */ + if (cell) + ns->cell = cell_get(cell); +#endif set_cred_user_ns(new, ns); ns_tree_add(ns); return 0; @@ -175,6 +198,34 @@ int create_user_ns(struct cred *new) return ret; } +/** + * create_user_ns - create a new, legacy, user namespace + * @new: the credentials to derive the creator from, and to put into it + * + * Return: 0 or a negative error. + */ +int create_user_ns(struct cred *new) +{ + return __create_user_ns(new, NULL); +} + +#ifdef CONFIG_CELLS +/** + * create_cell_user_ns - create the user namespace of a cell + * @new: a scratch credential, as for create_user_ns() + * @cell: the cell + * + * The namespace holds a reference on @cell for as long as it lives, and knows + * its cell before anybody can find it. + * + * Return: 0 or a negative error. + */ +int create_cell_user_ns(struct cred *new, struct cell *cell) +{ + return __create_user_ns(new, cell); +} +#endif + int unshare_userns(unsigned long unshare_flags, struct cred **new_cred) { struct cred *cred; @@ -221,6 +272,9 @@ static void free_user_ns(struct work_struct *work) #endif retire_userns_sysctls(ns); key_free_user_ns(ns); +#ifdef CONFIG_CELLS + cell_put(ns->cell); +#endif ns_common_free(ns); /* Concurrent nstree traversal depends on a grace period. */ kfree_rcu(ns, ns.ns_rcu); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 05/90] user_namespace: refuse setns() into a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (3 preceding siblings ...) 2026-11-05 14:56 ` [PATCH RFC v3 04/90] user_namespace: let a cell own a user namespace Josef Bacik @ 2026-11-05 14:56 ` Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 06/90] capability: give the owner no say over " Josef Bacik ` (84 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:56 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A task that enters a user namespace with setns() gets a full set of capabilities over everything that the namespace owns. A task is born in a cell, and no existing task can enter one. Refuse setns() into the user namespace of a cell. This also applies while the cell is still being built. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/user_namespace.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kernel/user_namespace.c b/kernel/user_namespace.c index 9301d0d41cb9..45699a0a0bfd 100644 --- a/kernel/user_namespace.c +++ b/kernel/user_namespace.c @@ -1437,6 +1437,10 @@ static int userns_install(struct nsset *nsset, struct ns_common *ns) if (user_ns == current_user_ns()) return -EINVAL; + /* Nobody enters a cell, not even while it is being built. */ + if (userns_is_cell(user_ns)) + return -EPERM; + /* Tasks that share a thread group must share a user namespace */ if (!thread_group_empty(current)) return -EINVAL; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 06/90] capability: give the owner no say over a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (4 preceding siblings ...) 2026-11-05 14:56 ` [PATCH RFC v3 05/90] user_namespace: refuse setns() into a cell Josef Bacik @ 2026-11-05 14:56 ` Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 07/90] ucount: count the id blocks a user holds Josef Bacik ` (83 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:56 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik cap_capable() has a special case for the owner of a user namespace. A task in the parent namespace whose euid is the uid that created the namespace has every capability over it. That makes a uid the authority over whatever the namespace owns. The authority over a cell is the cellfd, not a uid. Skip the special case for the user namespace of a cell. A capability that a task holds in an ancestor user namespace still applies to the cell. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- security/commoncap.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/security/commoncap.c b/security/commoncap.c index d47ab3022343..8af343c78d35 100644 --- a/security/commoncap.c +++ b/security/commoncap.c @@ -3,6 +3,7 @@ */ #include <linux/capability.h> +#include <linux/cell_ids.h> #include <linux/audit.h> #include <linux/init.h> #include <linux/kernel.h> @@ -90,9 +91,11 @@ static inline int cap_capable_helper(const struct cred *cred, /* * The owner of the user namespace in the parent of the - * user namespace has all caps. + * user namespace has all caps. Not over a cell: authority + * over a cell is a handle, not a uid. */ - if ((ns->parent == cred_ns) && uid_eq(ns->owner, cred->euid)) + if ((ns->parent == cred_ns) && uid_eq(ns->owner, cred->euid) && + !userns_is_cell(ns)) return 0; /* -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 07/90] ucount: count the id blocks a user holds 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (5 preceding siblings ...) 2026-11-05 14:56 ` [PATCH RFC v3 06/90] capability: give the owner no say over " Josef Bacik @ 2026-11-05 14:56 ` Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 08/90] user_namespace: let the kernel map the ids of a cell Josef Bacik ` (82 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:56 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A block of ids gives access to nothing, so taking one requires no privilege. But the number of blocks is limited. Add a ucount, user.max_cell_id_blocks, so that a single user cannot use up the window. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/user_namespace.h | 3 +++ kernel/ucount.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/include/linux/user_namespace.h b/include/linux/user_namespace.h index 5c8cf31ab9d4..f0b0fe12c2fd 100644 --- a/include/linux/user_namespace.h +++ b/include/linux/user_namespace.h @@ -61,6 +61,9 @@ enum ucount_type { #endif #if IS_ENABLED(CONFIG_BINFMT_MISC) UCOUNT_BINFMT_MISC_INTERPRETERS, +#endif +#ifdef CONFIG_CELLS + UCOUNT_CELL_ID_BLOCKS, #endif UCOUNT_COUNTS, }; diff --git a/kernel/ucount.c b/kernel/ucount.c index 94b70502dc9d..7eab985121f6 100644 --- a/kernel/ucount.c +++ b/kernel/ucount.c @@ -93,6 +93,9 @@ static const struct ctl_table user_table[] = { #if IS_ENABLED(CONFIG_BINFMT_MISC) UCOUNT_ENTRY("max_binfmt_misc_interpreters"), #endif +#ifdef CONFIG_CELLS + UCOUNT_ENTRY("max_cell_id_blocks"), +#endif }; #endif /* CONFIG_SYSCTL */ -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 08/90] user_namespace: let the kernel map the ids of a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (6 preceding siblings ...) 2026-11-05 14:56 ` [PATCH RFC v3 07/90] ucount: count the id blocks a user holds Josef Bacik @ 2026-11-05 14:56 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 09/90] user_namespace: keep legacy maps out of the window Josef Bacik ` (81 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:56 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The id maps of an ordinary user namespace are written by a task, and map_write() makes many checks on that task before it accepts the map. The maps of the user namespace of a cell are set once, by the kernel, to ids that the kernel chose. Add userns_set_cell_map() for that. It takes one extent for the uid map and one for the gid map. The checks in map_write() are about the writer, so they do not apply here. If the cell has a single gid there are no groups to drop or to add, so setgroups() is denied permanently. The namespace records which block it was lent and which ucount the block was charged to. Both are released when the namespace is freed, not when the cell dies. Every credential and every object that the namespace owns pins the namespace, and may still name one of its ids. Add userns_retire_cell_block() for the case where something that is owned by those ids could not be cleaned up. The block is then never handed out again. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cell.h | 7 +++++ include/linux/user_namespace.h | 7 +++++ kernel/user_namespace.c | 65 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/include/linux/cell.h b/include/linux/cell.h index 6a65d035ffca..37fde4bfea44 100644 --- a/include/linux/cell.h +++ b/include/linux/cell.h @@ -145,4 +145,11 @@ static inline void cell_put(struct cell *cell) { } #endif +/* The id block allocator. */ +#ifdef CONFIG_CELLS +void cell_block_release(u32 block); +#else +static inline void cell_block_release(u32 block) { } +#endif + #endif /* _LINUX_CELL_H */ diff --git a/include/linux/user_namespace.h b/include/linux/user_namespace.h index f0b0fe12c2fd..4a48ead45820 100644 --- a/include/linux/user_namespace.h +++ b/include/linux/user_namespace.h @@ -123,6 +123,8 @@ struct user_namespace { #endif #ifdef CONFIG_CELLS struct cell *cell; /* whose namespace this is, counted */ + u32 cell_block; /* kuid block + 1, 0 for none */ + struct ucounts *cell_block_ucounts; #endif } __randomize_layout; @@ -193,6 +195,11 @@ static inline struct user_namespace *get_user_ns(struct user_namespace *ns) extern int create_user_ns(struct cred *new); #ifdef CONFIG_CELLS int create_cell_user_ns(struct cred *new, struct cell *cell); +int userns_set_cell_map(struct user_namespace *ns, + const struct uid_gid_extent *uids, + const struct uid_gid_extent *gids, + u32 block, struct ucounts *ucounts); +void userns_retire_cell_block(struct user_namespace *ns); #endif extern int unshare_userns(unsigned long unshare_flags, struct cred **new_cred); extern void __put_user_ns(struct user_namespace *ns); diff --git a/kernel/user_namespace.c b/kernel/user_namespace.c index 45699a0a0bfd..4155e2e77497 100644 --- a/kernel/user_namespace.c +++ b/kernel/user_namespace.c @@ -78,6 +78,66 @@ bool userns_is_cell(const struct user_namespace *ns) { return ns->cell; } + +/** + * userns_set_cell_map - give the user namespace of a cell its one and only map + * @ns: the user namespace of a cell, which has no map yet + * @uids: the uids: .first as seen from the inside, .lower_first the kuid + * @gids: the gids likewise + * @block: the number of the block the ids are from plus 1, or 0 for none + * @ucounts: what the block was charged to, or NULL + * + * The kernel chose the ids, so none of what map_write() has to ask about the + * writer applies. With a single gid there are no groups to be dropped or taken + * up, so setgroups() is denied for good. + * + * @ns takes over @block and @ucounts and gives them back when it is freed. + * + * Return: 0, -EBUSY if @ns has a map, -EINVAL if what either starts at is not an + * id. + */ +int userns_set_cell_map(struct user_namespace *ns, + const struct uid_gid_extent *uids, + const struct uid_gid_extent *gids, + u32 block, struct ucounts *ucounts) +{ + guard(mutex)(&userns_state_mutex); + + if (ns->uid_map.nr_extents || ns->gid_map.nr_extents) + return -EBUSY; + if (uids->first == (u32)-1 || gids->first == (u32)-1) + return -EINVAL; + + ns->uid_map.extent[0] = *uids; + ns->gid_map.extent[0] = *gids; + /* With a single gid there are no groups to be dropped or taken up. */ + if (gids->count == 1) + ns->flags &= ~USERNS_SETGROUPS_ALLOWED; + ns->cell_block = block; + ns->cell_block_ucounts = ucounts; + /* Pairs with the smp_rmb() of the lookups, as in map_write(). */ + smp_wmb(); + ns->uid_map.nr_extents = 1; + ns->gid_map.nr_extents = 1; + return 0; +} + +/** + * userns_retire_cell_block - never hand the block of a user namespace out again + * @ns: the user namespace of a cell + * + * For when something that is owned by the ids of @ns could not be got rid of. + * The block stays marked as used for good, so what shows is -ENOSPC and never + * one tenant owning what another left behind. + */ +void userns_retire_cell_block(struct user_namespace *ns) +{ + guard(mutex)(&userns_state_mutex); + + if (ns->cell_block) + pr_warn_ratelimited("cell: kuid block %u retired\n", ns->cell_block - 1); + ns->cell_block = 0; +} #endif static unsigned long enforced_nproc_rlimit(void) @@ -273,6 +333,11 @@ static void free_user_ns(struct work_struct *work) retire_userns_sysctls(ns); key_free_user_ns(ns); #ifdef CONFIG_CELLS + /* Nothing that pins the namespace is left to name its ids. */ + if (ns->cell_block) + cell_block_release(ns->cell_block - 1); + if (ns->cell_block_ucounts) + dec_ucount(ns->cell_block_ucounts, UCOUNT_CELL_ID_BLOCKS); cell_put(ns->cell); #endif ns_common_free(ns); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 09/90] user_namespace: keep legacy maps out of the window 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (7 preceding siblings ...) 2026-11-05 14:56 ` [PATCH RFC v3 08/90] user_namespace: let the kernel map the ids of a cell Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 10/90] user: add uid_range_in_use() Josef Bacik ` (80 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik It is safe to hand a block of ids to an unprivileged user only because nobody else can hold an id of that block. Refuse to map an ordinary user namespace into the window, no matter who writes the map. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/user_namespace.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/kernel/user_namespace.c b/kernel/user_namespace.c index 4155e2e77497..5c2cf3dc02c1 100644 --- a/kernel/user_namespace.c +++ b/kernel/user_namespace.c @@ -1230,6 +1230,11 @@ static ssize_t map_write(struct file *file, const char __user *buf, if (lower_first == (u32) -1) goto out; + /* Those ids are the kernel's to hand out, to cells. */ + if (cap_valid(cap_setid) && + cell_range_in_window(lower_first, e->count)) + goto out; + e->lower_first = lower_first; } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 10/90] user: add uid_range_in_use() 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (8 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 09/90] user_namespace: keep legacy maps out of the window Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 11/90] keys: add key_user_range_in_use() Josef Bacik ` (79 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A user_struct is looked up by a bare kuid and does not pin a user namespace. So it can still exist after the cell that used the kuid is gone. If the block is then lent to another cell, that cell would find the old user_struct. Add uid_range_in_use(), so that the block allocator can ask whether a user_struct exists for any kuid in a range. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/sched/user.h | 1 + kernel/user.c | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/include/linux/sched/user.h b/include/linux/sched/user.h index ec262b605437..5966e355db9f 100644 --- a/include/linux/sched/user.h +++ b/include/linux/sched/user.h @@ -55,5 +55,6 @@ static inline struct user_struct *get_uid(struct user_struct *u) return u; } extern void free_uid(struct user_struct *); +bool uid_range_in_use(kuid_t first, kuid_t last); #endif /* _LINUX_SCHED_USER_H */ diff --git a/kernel/user.c b/kernel/user.c index 21bafdc11379..b69cab2a5a3b 100644 --- a/kernel/user.c +++ b/kernel/user.c @@ -154,6 +154,38 @@ static void user_epoll_free(struct user_struct *up) #endif } +#ifdef CONFIG_CELLS +/** + * uid_range_in_use - is there a user_struct for any uid of a range? + * @first: the first uid of the range + * @last: the last + * + * A user_struct is keyed by a bare kuid and pins no user namespace. The id block + * allocator asks before it lends a block, and before it takes one back. + * + * Return: true if there is one. + */ +bool uid_range_in_use(kuid_t first, kuid_t last) +{ + struct user_struct *user; + unsigned long flags; + bool ret = false; + int i; + + spin_lock_irqsave(&uidhash_lock, flags); + for (i = 0; i < UIDHASH_SZ && !ret; i++) { + hlist_for_each_entry(user, &uidhash_table[i], uidhash_node) { + if (uid_gte(user->uid, first) && uid_lte(user->uid, last)) { + ret = true; + break; + } + } + } + spin_unlock_irqrestore(&uidhash_lock, flags); + return ret; +} +#endif + /* IRQs are disabled and uidhash_lock is held upon function entry. * IRQ state (as stored in flags) is restored and uidhash_lock released * upon function exit. -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 11/90] keys: add key_user_range_in_use() 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (9 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 10/90] user: add uid_range_in_use() Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 12/90] fs: add kuid_has_fsmapping() and kgid_has_fsmapping() Josef Bacik ` (78 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The key quota record of a user is looked up by a bare kuid and does not pin a user namespace. The record, and possibly keys that are charged to it, can still exist after the cell that used the kuid is gone. If the block is then lent to another cell, that cell would find them. Add key_user_range_in_use(), so that the block allocator can ask whether a quota record exists for any kuid in a range. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/key.h | 2 ++ security/keys/key.c | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/include/linux/key.h b/include/linux/key.h index 115db6e60254..0bd77fcab4db 100644 --- a/include/linux/key.h +++ b/include/linux/key.h @@ -461,6 +461,7 @@ extern void key_set_timeout(struct key *, unsigned); extern key_ref_t lookup_user_key(key_serial_t id, unsigned long flags, enum key_need_perm need_perm); extern void key_free_user_ns(struct user_namespace *); +bool key_user_range_in_use(kuid_t first, kuid_t last); static inline short key_read_state(const struct key *key) { @@ -521,6 +522,7 @@ extern void key_init(void); #define key_fsgid_changed(c) do { } while(0) #define key_init() do { } while(0) #define key_free_user_ns(ns) do { } while(0) +static inline bool key_user_range_in_use(kuid_t first, kuid_t last) { return false; } #define key_remove_domain(d) do { } while(0) #define key_lookup(k) NULL diff --git a/security/keys/key.c b/security/keys/key.c index 6ebb8691551f..0a6e741805d6 100644 --- a/security/keys/key.c +++ b/security/keys/key.c @@ -113,6 +113,44 @@ struct key_user *key_user_lookup(kuid_t uid) return user; } +#ifdef CONFIG_CELLS +struct key_user_range { + kuid_t first; + kuid_t last; +}; + +static int key_user_range_cmp(const void *key, const struct rb_node *n) +{ + const struct key_user *user = rb_entry(n, struct key_user, node); + const struct key_user_range *range = key; + + if (uid_lt(range->last, user->uid)) + return -1; + if (uid_gt(range->first, user->uid)) + return 1; + return 0; +} + +/** + * key_user_range_in_use - is there a quota record for any uid of a range? + * @first: the first uid of the range + * @last: the last + * + * A quota record, and so possibly a key, is keyed by a bare kuid and pins no + * user namespace. The id block allocator asks before it lends a block, and + * before it takes one back. + * + * Return: true if there is one. + */ +bool key_user_range_in_use(kuid_t first, kuid_t last) +{ + struct key_user_range range = { .first = first, .last = last }; + + guard(spinlock)(&key_user_lock); + return rb_find(&range, &key_user_tree, key_user_range_cmp); +} +#endif + /* * Dispose of a user structure */ -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 12/90] fs: add kuid_has_fsmapping() and kgid_has_fsmapping() 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (10 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 11/90] keys: add key_user_range_in_use() Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 13/90] fs: never store an id that was lent to a cell Josef Bacik ` (77 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Several places ask whether a filesystem can hold a given id. Each of them calls kuid_has_mapping() or kgid_has_mapping() on the user namespace of the filesystem: - fsuidgid_has_mapping(), when a file is created - posix_acl_valid(), for an ACL entry - the uid and gid mount options of tmpfs, fuse and bpffs - ovl_copy_up_one(), which runs with the credentials of the mounter, so current_user_ns() there is the user namespace of the overlay The same two helpers are also used for many unrelated questions. Nothing distinguishes the callers that ask about a filesystem, so there is no place for a rule that should apply to filesystems only. Add kuid_has_fsmapping() and kgid_has_fsmapping(), next to vfsuid_has_fsmapping() and vfsgid_has_fsmapping(), and use them in the places listed above. No functional change. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/fuse/inode.c | 4 ++-- fs/overlayfs/copy_up.c | 4 ++-- fs/posix_acl.c | 4 ++-- include/linux/fs.h | 4 ++-- include/linux/mnt_idmapping.h | 31 +++++++++++++++++++++++++++++++ kernel/bpf/inode.c | 4 ++-- mm/shmem.c | 4 ++-- 7 files changed, 43 insertions(+), 12 deletions(-) diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 1341b681d0ed..37712fce905b 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -888,7 +888,7 @@ static int fuse_parse_param(struct fs_context *fsc, struct fs_parameter *param) * The requested uid must be representable in the * filesystem's idmapping. */ - if (!kuid_has_mapping(fsc->user_ns, kuid)) + if (!kuid_has_fsmapping(fsc->user_ns, kuid)) return invalfc(fsc, "Invalid user_id"); ctx->user_id = kuid; ctx->user_id_present = true; @@ -900,7 +900,7 @@ static int fuse_parse_param(struct fs_context *fsc, struct fs_parameter *param) * The requested gid must be representable in the * filesystem's idmapping. */ - if (!kgid_has_mapping(fsc->user_ns, kgid)) + if (!kgid_has_fsmapping(fsc->user_ns, kgid)) return invalfc(fsc, "Invalid group_id"); ctx->group_id = kgid; ctx->group_id_present = true; diff --git a/fs/overlayfs/copy_up.c b/fs/overlayfs/copy_up.c index c147ad155b79..48cd09a2b69b 100644 --- a/fs/overlayfs/copy_up.c +++ b/fs/overlayfs/copy_up.c @@ -1173,8 +1173,8 @@ static int ovl_copy_up_one(struct dentry *parent, struct dentry *dentry, if (err) return err; - if (!kuid_has_mapping(current_user_ns(), ctx.stat.uid) || - !kgid_has_mapping(current_user_ns(), ctx.stat.gid)) + if (!kuid_has_fsmapping(current_user_ns(), ctx.stat.uid) || + !kgid_has_fsmapping(current_user_ns(), ctx.stat.gid)) return -EOVERFLOW; /* diff --git a/fs/posix_acl.c b/fs/posix_acl.c index fe77934ea8f2..a2e2ca984d95 100644 --- a/fs/posix_acl.c +++ b/fs/posix_acl.c @@ -260,7 +260,7 @@ posix_acl_valid(struct user_namespace *user_ns, const struct posix_acl *acl) case ACL_USER: if (state != ACL_USER) return -EINVAL; - if (!kuid_has_mapping(user_ns, pa->e_uid)) + if (!kuid_has_fsmapping(user_ns, pa->e_uid)) return -EINVAL; needs_mask = 1; break; @@ -275,7 +275,7 @@ posix_acl_valid(struct user_namespace *user_ns, const struct posix_acl *acl) case ACL_GROUP: if (state != ACL_GROUP) return -EINVAL; - if (!kgid_has_mapping(user_ns, pa->e_gid)) + if (!kgid_has_fsmapping(user_ns, pa->e_gid)) return -EINVAL; needs_mask = 1; break; diff --git a/include/linux/fs.h b/include/linux/fs.h index 88748be03505..ea9b9d5a667d 100644 --- a/include/linux/fs.h +++ b/include/linux/fs.h @@ -1589,8 +1589,8 @@ static inline bool fsuidgid_has_mapping(struct super_block *sb, kgid = mapped_fsgid(idmap, fs_userns); if (!gid_valid(kgid)) return false; - return kuid_has_mapping(fs_userns, kuid) && - kgid_has_mapping(fs_userns, kgid); + return kuid_has_fsmapping(fs_userns, kuid) && + kgid_has_fsmapping(fs_userns, kgid); } struct timespec64 current_time(struct inode *inode); diff --git a/include/linux/mnt_idmapping.h b/include/linux/mnt_idmapping.h index 78eeef4c2996..b5e0772aa190 100644 --- a/include/linux/mnt_idmapping.h +++ b/include/linux/mnt_idmapping.h @@ -136,6 +136,37 @@ kuid_t from_vfsuid(const struct mnt_idmap *idmap, kgid_t from_vfsgid(const struct mnt_idmap *idmap, struct user_namespace *fs_userns, vfsgid_t vfsgid); +/** + * kuid_has_fsmapping - check whether a filesystem can hold a kuid + * @fs_userns: the filesystem's idmapping + * @kuid: the kuid, after the idmapping of the mount has been applied + * + * Use this function, and not kuid_has_mapping(), for a kuid that is about to end + * up in a filesystem: as the owner of a file, in an ACL, by a mount option. + * + * Return: true if @kuid has a mapping in the filesystem, false if not. + */ +static inline bool kuid_has_fsmapping(struct user_namespace *fs_userns, + kuid_t kuid) +{ + return kuid_has_mapping(fs_userns, kuid); +} + +/** + * kgid_has_fsmapping - check whether a filesystem can hold a kgid + * @fs_userns: the filesystem's idmapping + * @kgid: the kgid, after the idmapping of the mount has been applied + * + * See kuid_has_fsmapping(). + * + * Return: true if @kgid has a mapping in the filesystem, false if not. + */ +static inline bool kgid_has_fsmapping(struct user_namespace *fs_userns, + kgid_t kgid) +{ + return kgid_has_mapping(fs_userns, kgid); +} + /** * vfsuid_has_fsmapping - check whether a vfsuid maps into the filesystem * @idmap: the mount's idmapping diff --git a/kernel/bpf/inode.c b/kernel/bpf/inode.c index c6f328e4752e..504bb7accb39 100644 --- a/kernel/bpf/inode.c +++ b/kernel/bpf/inode.c @@ -1026,7 +1026,7 @@ static int bpf_parse_param(struct fs_context *fc, struct fs_parameter *param) * The requested uid must be representable in the * filesystem's idmapping. */ - if (!kuid_has_mapping(fc->user_ns, uid)) + if (!kuid_has_fsmapping(fc->user_ns, uid)) goto bad_value; opts->uid = uid; @@ -1040,7 +1040,7 @@ static int bpf_parse_param(struct fs_context *fc, struct fs_parameter *param) * The requested gid must be representable in the * filesystem's idmapping. */ - if (!kgid_has_mapping(fc->user_ns, gid)) + if (!kgid_has_fsmapping(fc->user_ns, gid)) goto bad_value; opts->gid = gid; diff --git a/mm/shmem.c b/mm/shmem.c index af21a89abb0f..8e7b51797ab8 100644 --- a/mm/shmem.c +++ b/mm/shmem.c @@ -4793,7 +4793,7 @@ static int shmem_parse_one(struct fs_context *fc, struct fs_parameter *param) * The requested uid must be representable in the * filesystem's idmapping. */ - if (!kuid_has_mapping(fc->user_ns, kuid)) + if (!kuid_has_fsmapping(fc->user_ns, kuid)) goto bad_value; ctx->uid = kuid; @@ -4805,7 +4805,7 @@ static int shmem_parse_one(struct fs_context *fc, struct fs_parameter *param) * The requested gid must be representable in the * filesystem's idmapping. */ - if (!kgid_has_mapping(fc->user_ns, kgid)) + if (!kgid_has_fsmapping(fc->user_ns, kgid)) goto bad_value; ctx->gid = kgid; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 13/90] fs: never store an id that was lent to a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (11 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 12/90] fs: add kuid_has_fsmapping() and kgid_has_fsmapping() Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 14/90] fs: take no id of a cell for a mount option Josef Bacik ` (76 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A block of ids is lent to a cell for as long as the cell exists, and is later lent to another cell. Anything that is still owned by one of those ids at that point would belong to the new cell. Today a task in a user namespace can create a file on a host filesystem that is not idmapped. fsuidgid_has_mapping() only asks whether the user namespace of the superblock can represent the kuid, and the initial user namespace can represent every kuid. The raw kuid ends up on disk. Refuse to represent an id of the window in a filesystem that the cell does not own. Do this in the helpers that decide whether an id can be represented: kuid_has_fsmapping(), kgid_has_fsmapping(), vfsuid_has_fsmapping() and vfsgid_has_fsmapping(). That covers creating a file, chown, ACL entries, copy up in overlayfs, and the uid and gid mount options of tmpfs, fuse and bpffs. A mount that is idmapped for the cell keeps working. There the id has already been translated to the on-disk id before it reaches these helpers. Some details: - The vfsuid and vfsgid helpers do not call the kuid and kgid helpers. On a mount that is not idmapped they do not check for a mapping in the user namespace of the filesystem today, and this patch does not change that. - The check looks at the value of the id, not at the caller. overlayfs creates files in its upper layer with the credentials of the mounter and the fsuid of the caller. - The check is not in from_vfsuid(). That would give the inodes of the kernel internal shmem mount an invalid owner, and notify_change() would then fail every ftruncate() of a memfd. With CONFIG_CELLS enabled, the host can no longer chown a file to an id of 2147483648 or above. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cell_ids.h | 38 ++++++++++++++++++++++++++++++++++++++ include/linux/mnt_idmapping.h | 13 +++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/include/linux/cell_ids.h b/include/linux/cell_ids.h index dcb0a559d0e9..9bdd98e73a9d 100644 --- a/include/linux/cell_ids.h +++ b/include/linux/cell_ids.h @@ -62,4 +62,42 @@ static inline bool cell_range_in_window(u32 first, u32 count) { return false; } static inline bool userns_is_cell(const struct user_namespace *ns) { return false; } #endif + +/** + * kuid_storable - may a filesystem store a kuid? + * @fs_userns: the user namespace of the filesystem, sb->s_user_ns + * @kuid: the kuid, after the idmapping of the mount has been applied + * + * An id that was lent to a cell must not outlive the loan, so it never reaches + * a filesystem that the cell does not own. Through a mount that is idmapped for + * the cell the id has become the on-disk one long before it gets here. + * + * That @kuid is an id of this cell and not of another is for kuid_has_mapping() + * to say. Nobody but kuid_has_fsmapping() and vfsuid_has_fsmapping() has to ask. + * + * Return: true if @kuid is outside of the window, or @fs_userns is the user + * namespace of a cell. + */ +static inline bool kuid_storable(const struct user_namespace *fs_userns, kuid_t kuid) +{ + return likely(!cell_id_in_window(__kuid_val(kuid))) || + userns_is_cell(fs_userns); +} + +/** + * kgid_storable - may a filesystem store a kgid? + * @fs_userns: the user namespace of the filesystem, sb->s_user_ns + * @kgid: the kgid, after the idmapping of the mount has been applied + * + * See kuid_storable(). + * + * Return: true if @kgid is outside of the window, or @fs_userns is the user + * namespace of a cell. + */ +static inline bool kgid_storable(const struct user_namespace *fs_userns, kgid_t kgid) +{ + return likely(!cell_id_in_window(__kgid_val(kgid))) || + userns_is_cell(fs_userns); +} + #endif /* _LINUX_CELL_IDS_H */ diff --git a/include/linux/mnt_idmapping.h b/include/linux/mnt_idmapping.h index b5e0772aa190..dfe35b570317 100644 --- a/include/linux/mnt_idmapping.h +++ b/include/linux/mnt_idmapping.h @@ -4,6 +4,7 @@ #include <linux/types.h> #include <linux/uidgid.h> +#include <linux/cell_ids.h> struct mnt_idmap; struct user_namespace; @@ -149,6 +150,8 @@ kgid_t from_vfsgid(const struct mnt_idmap *idmap, static inline bool kuid_has_fsmapping(struct user_namespace *fs_userns, kuid_t kuid) { + if (!kuid_storable(fs_userns, kuid)) + return false; return kuid_has_mapping(fs_userns, kuid); } @@ -164,6 +167,8 @@ static inline bool kuid_has_fsmapping(struct user_namespace *fs_userns, static inline bool kgid_has_fsmapping(struct user_namespace *fs_userns, kgid_t kgid) { + if (!kgid_storable(fs_userns, kgid)) + return false; return kgid_has_mapping(fs_userns, kgid); } @@ -183,7 +188,9 @@ static inline bool vfsuid_has_fsmapping(const struct mnt_idmap *idmap, struct user_namespace *fs_userns, vfsuid_t vfsuid) { - return uid_valid(from_vfsuid(idmap, fs_userns, vfsuid)); + kuid_t kuid = from_vfsuid(idmap, fs_userns, vfsuid); + + return uid_valid(kuid) && kuid_storable(fs_userns, kuid); } static inline bool vfsuid_has_mapping(struct user_namespace *userns, @@ -221,7 +228,9 @@ static inline bool vfsgid_has_fsmapping(const struct mnt_idmap *idmap, struct user_namespace *fs_userns, vfsgid_t vfsgid) { - return gid_valid(from_vfsgid(idmap, fs_userns, vfsgid)); + kgid_t kgid = from_vfsgid(idmap, fs_userns, vfsgid); + + return gid_valid(kgid) && kgid_storable(fs_userns, kgid); } static inline bool vfsgid_has_mapping(struct user_namespace *userns, -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 14/90] fs: take no id of a cell for a mount option 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (12 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 13/90] fs: never store an id that was lent to a cell Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 15/90] fs: let an idmapping pin the ids of a cell Josef Bacik ` (75 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Most filesystems with a uid= or gid= mount option assign that id to some or all of their files, without checking whether they may hold it. For an id of the window this has the same effect as a chown to it: the files would belong to whichever cell is lent the block next. Only root in the initial user namespace can name such an id, because no other user namespace maps it. But the rule is about the value of the id, not about the caller. Nobody is supposed to name the ids of a cell, not even its builder. Refuse ids of the window in fs_param_is_uid() and fs_param_is_gid(). About two dozen filesystems parse their options with these. Not covered are filesystems that parse the number themselves, such as udf, ntfs, zonefs, functionfs, hypfs and spufs, the access= option of 9p, and the anonymous ids of an nfsd export. The design document lists them. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/fs_parser.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fs/fs_parser.c b/fs/fs_parser.c index b4cc4cce518a..0e8c84459c45 100644 --- a/fs/fs_parser.c +++ b/fs/fs_parser.c @@ -5,6 +5,7 @@ * Written by David Howells (dhowells@redhat.com) */ +#include <linux/cell_ids.h> #include <linux/export.h> #include <linux/fs_context.h> #include <linux/fs_parser.h> @@ -331,6 +332,9 @@ int fs_param_is_uid(struct p_log *log, const struct fs_parameter_spec *p, uid = make_kuid(current_user_ns(), result->uint_32); if (!uid_valid(uid)) return inval_plog(log, "Invalid uid '%s'", param->string); + /* Nobody names an id that is lent to a cell, see kuid_storable(). */ + if (cell_id_in_window(__kuid_val(uid))) + return inval_plog(log, "Invalid uid '%s'", param->string); result->uid = uid; return 0; @@ -348,6 +352,9 @@ int fs_param_is_gid(struct p_log *log, const struct fs_parameter_spec *p, gid = make_kgid(current_user_ns(), result->uint_32); if (!gid_valid(gid)) return inval_plog(log, "Invalid gid '%s'", param->string); + /* See fs_param_is_uid(). */ + if (cell_id_in_window(__kgid_val(gid))) + return inval_plog(log, "Invalid gid '%s'", param->string); result->gid = gid; return 0; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 15/90] fs: let an idmapping pin the ids of a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (13 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 14/90] fs: take no id of a cell for a mount option Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 16/90] cgroup: let a cgroup become the budget " Josef Bacik ` (74 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A struct mnt_idmap holds a copy of the id maps of the user namespace it was created from. The copy does not pin the namespace. So a mount that was idmapped for a cell can outlive the cell. It would keep translating on-disk ids into the ids of the block after the block has been lent to another cell. A set-uid file on that mount would then run as the root of the new cell. Let an idmapping that was created from the user namespace of a cell hold a reference on that namespace. The block is released when the namespace is freed, so it cannot be lent again while such a mount exists. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/mnt_idmapping.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fs/mnt_idmapping.c b/fs/mnt_idmapping.c index bed57094cef0..93a34bc93efd 100644 --- a/fs/mnt_idmapping.c +++ b/fs/mnt_idmapping.c @@ -21,6 +21,14 @@ struct mnt_idmap { struct uid_gid_map uid_map; struct uid_gid_map gid_map; refcount_t count; +#ifdef CONFIG_CELLS + /* + * The maps are a copy, and a copy pins nothing. The ids of a cell are + * only lent to it, until its user namespace is freed, so an idmapping + * that names them holds on to that. + */ + struct user_namespace *cell_ns; +#endif }; /* @@ -281,6 +289,9 @@ static void free_mnt_idmap(struct mnt_idmap *idmap) kfree(idmap->gid_map.forward); kfree(idmap->gid_map.reverse); } +#ifdef CONFIG_CELLS + put_user_ns(idmap->cell_ns); +#endif kfree(idmap); } @@ -294,6 +305,10 @@ struct mnt_idmap *alloc_mnt_idmap(struct user_namespace *mnt_userns) return ERR_PTR(-ENOMEM); refcount_set(&idmap->count, 1); +#ifdef CONFIG_CELLS + if (userns_is_cell(mnt_userns)) + idmap->cell_ns = get_user_ns(mnt_userns); +#endif ret = copy_mnt_idmap(&mnt_userns->uid_map, &idmap->uid_map); if (!ret) ret = copy_mnt_idmap(&mnt_userns->gid_map, &idmap->gid_map); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 16/90] cgroup: let a cgroup become the budget of a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (14 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 15/90] fs: let an idmapping pin the ids of a cell Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 17/90] cgroup: add cgroup_cell_chown() Josef Bacik ` (73 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A cell needs a limit on the resources it can use, and a way to find every task in it, including forks that are in flight. A cgroup provides both. Add cgroup_cell_budget_get(). It takes a cgroup2 directory as the budget of a cell and marks it with CGRP_CELL_BUDGET. From then on, holding the cellfd is enough to put a task into that cgroup. So the caller is checked here instead. It must pass every check that moving one of its own tasks into the cgroup would require, and it must own the directory. Killing a cell kills everything in its budget, so a budget can only belong to one cell. The cgroup must be empty, must have no children, and must not be the budget of another cell or lie inside of one. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cgroup-defs.h | 3 ++ include/linux/cgroup.h | 2 ++ kernel/cgroup/cgroup.c | 82 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/include/linux/cgroup-defs.h b/include/linux/cgroup-defs.h index cbfe8937647c..4626adbf45d6 100644 --- a/include/linux/cgroup-defs.h +++ b/include/linux/cgroup-defs.h @@ -72,6 +72,9 @@ enum { /* Cgroup is frozen. */ CGRP_FROZEN, + + /* Cgroup is the budget of a cell. */ + CGRP_CELL_BUDGET, }; /* cgroup_root->flags */ diff --git a/include/linux/cgroup.h b/include/linux/cgroup.h index 2afb4cb2bb4f..a5ef01688e7f 100644 --- a/include/linux/cgroup.h +++ b/include/linux/cgroup.h @@ -152,6 +152,8 @@ struct cgroup_subsys_state *css_tryget_online_from_dir(struct dentry *dentry, struct cgroup *cgroup_get_from_path(const char *path); struct cgroup *cgroup_get_from_fd(int fd); + +struct cgroup *cgroup_cell_budget_get(struct file *file, kuid_t *uid, kgid_t *gid); struct cgroup *cgroup_v1v2_get_from_fd(int fd); /* Was this controller blocked from v1 hierarchies by cgroup_no_v1= ? */ diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c index a284225f8dc9..c4bbb70f490f 100644 --- a/kernel/cgroup/cgroup.c +++ b/kernel/cgroup/cgroup.c @@ -7044,6 +7044,88 @@ static void cgroup_css_set_put_fork(struct kernel_clone_args *kargs) } } +#ifdef CONFIG_CELLS +/** + * cgroup_cell_budget_get - take a cgroup as the budget of a cell + * @file: a cgroup2 directory + * @uid: returns who owns it now + * @gid: likewise + * + * From here on holding the cellfd is all it takes to put a task into the + * cgroup, and sealing the cell hands the cgroup to the cell's root. So whoever + * fills the slot must be allowed both: everything moving a task of its own + * there would take, and ownership of the directory. + * + * Killing the cell kills everything in the cgroup. So it must be empty, have no + * children, and neither be nor lie within the budget of another cell. + * + * Return: the cgroup, marked as a budget and with a reference, both for + * cgroup_cell_budget_put() to undo, or an ERR_PTR(). Among the errors are + * -EBADF if @file is no cgroup2 directory, -EPERM or -EACCES if the caller may + * not, -EBUSY if the cgroup will not do and -ENODEV if it is dead. + */ +struct cgroup *cgroup_cell_budget_get(struct file *file, kuid_t *uid, kgid_t *gid) +{ + struct super_block *sb = file->f_path.dentry->d_sb; + struct cgroup *cgrp, *anc; + struct css_set *cset; + struct kstat stat; + int ret; + + cgrp = cgroup_get_from_file(file); + if (IS_ERR(cgrp)) + return cgrp; + + /* Also brings the inode up to date with the kernfs node. */ + ret = vfs_getattr_nosec(&file->f_path, &stat, STATX_UID | STATX_GID, 0); + if (ret) { + cgroup_put(cgrp); + return ERR_PTR(ret); + } + + cgroup_lock(); + + spin_lock_irq(&css_set_lock); + cset = task_css_set(current); + get_css_set(cset); + spin_unlock_irq(&css_set_lock); + + ret = -ENODEV; + if (cgroup_is_dead(cgrp)) + goto out; + ret = -EPERM; + if (!inode_owner_or_capable(&nop_mnt_idmap, file_inode(file))) + goto out; + ret = cgroup_may_write(cgrp, sb); + if (ret) + goto out; + ret = cgroup_attach_permissions(cset->dfl_cgrp, cgrp, sb, true, + current->nsproxy->cgroup_ns); + if (ret) + goto out; + ret = -EBUSY; + if (cgroup_is_populated(cgrp) || css_has_online_children(&cgrp->self)) + goto out; + for (anc = cgrp; anc; anc = cgroup_parent(anc)) { + if (test_bit(CGRP_CELL_BUDGET, &anc->flags)) + goto out; + } + + set_bit(CGRP_CELL_BUDGET, &cgrp->flags); + *uid = stat.uid; + *gid = stat.gid; + ret = 0; +out: + put_css_set(cset); + cgroup_unlock(); + if (ret) { + cgroup_put(cgrp); + return ERR_PTR(ret); + } + return cgrp; +} +#endif /* CONFIG_CELLS */ + /** * cgroup_can_fork - called on a new task before the process is exposed * @child: the child process -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 17/90] cgroup: add cgroup_cell_chown() 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (15 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 16/90] cgroup: let a cgroup become the budget " Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 18/90] cgroup: give the budget of a cell back Josef Bacik ` (72 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A cgroup is delegated by giving somebody ownership of its directory and of cgroup.procs, cgroup.threads and cgroup.subtree_control. The builder of a cell cannot do that for the root user of the cell. It cannot name the kuid, and cgroupfs could not store that kuid through a chown anyway. Add cgroup_cell_chown(), which changes the owner directly on the kernfs nodes. The files that hold the limits keep their owner. The function can fail half way if it runs out of memory. The caller undoes a partial change by calling it again with the previous owner. That second call cannot fail: kernfs_setattr() only allocates memory the first time the attributes of a node are set, and the nodes in question have already been changed once. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cgroup.h | 1 + kernel/cgroup/cgroup.c | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/include/linux/cgroup.h b/include/linux/cgroup.h index a5ef01688e7f..0e45a5a1afea 100644 --- a/include/linux/cgroup.h +++ b/include/linux/cgroup.h @@ -154,6 +154,7 @@ struct cgroup *cgroup_get_from_path(const char *path); struct cgroup *cgroup_get_from_fd(int fd); struct cgroup *cgroup_cell_budget_get(struct file *file, kuid_t *uid, kgid_t *gid); +int cgroup_cell_chown(struct cgroup *cgrp, kuid_t uid, kgid_t gid); struct cgroup *cgroup_v1v2_get_from_fd(int fd); /* Was this controller blocked from v1 hierarchies by cgroup_no_v1= ? */ diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c index c4bbb70f490f..fb2775006087 100644 --- a/kernel/cgroup/cgroup.c +++ b/kernel/cgroup/cgroup.c @@ -7124,6 +7124,43 @@ struct cgroup *cgroup_cell_budget_get(struct file *file, kuid_t *uid, kgid_t *gi } return cgrp; } + +/** + * cgroup_cell_chown - hand a cgroup to somebody + * @cgrp: the cgroup + * @uid: the new owner + * @gid: the new group + * + * Changes what delegating a cgroup is about: the directory, cgroup.procs, + * cgroup.threads and cgroup.subtree_control. The limits stay with whoever is + * above. + * + * Return: 0 or a negative error, in which case some of it may have changed. To + * undo that call this again with what it was. All that kernfs_setattr() can fail + * for is the memory for the attributes of a node that never had any set, so it + * does not fail for a node that has been changed. + */ +int cgroup_cell_chown(struct cgroup *cgrp, kuid_t uid, kgid_t gid) +{ + static const char * const files[] = { + "cgroup.procs", "cgroup.threads", "cgroup.subtree_control", + }; + struct iattr iattr = { + .ia_valid = ATTR_UID | ATTR_GID, .ia_uid = uid, .ia_gid = gid, + }; + int i, ret; + + ret = kernfs_setattr(cgrp->kn, &iattr); + for (i = 0; !ret && i < ARRAY_SIZE(files); i++) { + struct kernfs_node *kn = kernfs_find_and_get(cgrp->kn, files[i]); + + if (!kn) + continue; + ret = kernfs_setattr(kn, &iattr); + kernfs_put(kn); + } + return ret; +} #endif /* CONFIG_CELLS */ /** -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 18/90] cgroup: give the budget of a cell back 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (16 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 17/90] cgroup: add cgroup_cell_chown() Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 19/90] cgroup: add cgroup_cell_kill() Josef Bacik ` (71 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The ids of a cell are only lent to it. The cgroups that the cell created below its budget are owned by those ids. So is the budget itself, once it has been handed to the root user of the cell. Add cgroup_cell_budget_put(). It removes everything below the budget, returns the budget to its previous owner and clears CGRP_CELL_BUDGET. It reports whether all of that worked, because the ids must not be lent to another cell if it did not. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cgroup.h | 1 + kernel/cgroup/cgroup.c | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/include/linux/cgroup.h b/include/linux/cgroup.h index 0e45a5a1afea..4d9baf195f1e 100644 --- a/include/linux/cgroup.h +++ b/include/linux/cgroup.h @@ -155,6 +155,7 @@ struct cgroup *cgroup_get_from_fd(int fd); struct cgroup *cgroup_cell_budget_get(struct file *file, kuid_t *uid, kgid_t *gid); int cgroup_cell_chown(struct cgroup *cgrp, kuid_t uid, kgid_t gid); +int cgroup_cell_budget_put(struct cgroup *cgrp, bool handed, kuid_t uid, kgid_t gid); struct cgroup *cgroup_v1v2_get_from_fd(int fd); /* Was this controller blocked from v1 hierarchies by cgroup_no_v1= ? */ diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c index fb2775006087..e5e6761522eb 100644 --- a/kernel/cgroup/cgroup.c +++ b/kernel/cgroup/cgroup.c @@ -7125,6 +7125,44 @@ struct cgroup *cgroup_cell_budget_get(struct file *file, kuid_t *uid, kgid_t *gi return cgrp; } +/** + * cgroup_cell_budget_put - the cell is gone, give its budget back + * @cgrp: the budget. The reference is consumed + * @handed: the cgroup had been handed to the cell's root + * @uid: who owned it before + * @gid: likewise + * + * The cgroup is empty. Whatever the cell made below it is owned by ids that + * were only lent to the cell, so it goes, and the cgroup itself returns to its + * owner. + * + * Return: 0, or an error if something is left that is owned by the cell's ids. + */ +int cgroup_cell_budget_put(struct cgroup *cgrp, bool handed, kuid_t uid, kgid_t gid) +{ + struct cgroup_subsys_state *css; + struct cgroup *dsct; + int ret = 0; + + cgroup_lock(); + clear_bit(CGRP_CELL_BUDGET, &cgrp->flags); + if (handed && !cgroup_is_dead(cgrp)) { + cgroup_for_each_live_descendant_post(dsct, css, cgrp) { + if (dsct == cgrp) + continue; + ret = cgroup_destroy_locked(dsct); + if (ret) + break; + } + } + cgroup_unlock(); + + if (handed && !ret) + ret = cgroup_cell_chown(cgrp, uid, gid); + cgroup_put(cgrp); + return ret; +} + /** * cgroup_cell_chown - hand a cgroup to somebody * @cgrp: the cgroup -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 19/90] cgroup: add cgroup_cell_kill() 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (17 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 18/90] cgroup: give the budget of a cell back Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 20/90] cgroup: root a cgroup namespace at a given cgroup Josef Bacik ` (70 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Killing a cell means killing everything in its budget. Add cgroup_cell_kill(). It does what a write to cgroup.kill does, which includes forks that are in flight. It also reports whether the cgroup is still populated, so that a cell is only reported dead once nothing is left in it. It uses the populated state of the cgroup rather than a task iterator. An iterator skips a task that is exiting, but such a task keeps its cgroup populated until it has been scheduled out for the last time. cgroup_destroy_locked() uses the populated state too, and it is called next, for the cgroups that the cell created below its budget. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cgroup.h | 1 + kernel/cgroup/cgroup.c | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/include/linux/cgroup.h b/include/linux/cgroup.h index 4d9baf195f1e..57a4037671c9 100644 --- a/include/linux/cgroup.h +++ b/include/linux/cgroup.h @@ -155,6 +155,7 @@ struct cgroup *cgroup_get_from_fd(int fd); struct cgroup *cgroup_cell_budget_get(struct file *file, kuid_t *uid, kgid_t *gid); int cgroup_cell_chown(struct cgroup *cgrp, kuid_t uid, kgid_t gid); +bool cgroup_cell_kill(struct cgroup *cgrp); int cgroup_cell_budget_put(struct cgroup *cgrp, bool handed, kuid_t uid, kgid_t gid); struct cgroup *cgroup_v1v2_get_from_fd(int fd); diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c index e5e6761522eb..75f8367271aa 100644 --- a/kernel/cgroup/cgroup.c +++ b/kernel/cgroup/cgroup.c @@ -7199,6 +7199,30 @@ int cgroup_cell_chown(struct cgroup *cgrp, kuid_t uid, kgid_t gid) } return ret; } + +/** + * cgroup_cell_kill - kill everything in a cgroup and below + * @cgrp: the cgroup + * + * As a write to cgroup.kill. It closes against forks in flight by kill_seq. + * + * Return: whether the cgroup is still populated, as cgroup.events has it. A + * task counts until it has been switched out for the last time, see + * cgroup_task_dead(). That is longer than a task iterator shows it, and it is + * what cgroup_destroy_locked() goes by. + */ +bool cgroup_cell_kill(struct cgroup *cgrp) +{ + bool busy = false; + + cgroup_lock(); + if (!cgroup_is_dead(cgrp)) { + cgroup_kill(cgrp); + busy = cgroup_is_populated(cgrp); + } + cgroup_unlock(); + return busy; +} #endif /* CONFIG_CELLS */ /** -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 20/90] cgroup: root a cgroup namespace at a given cgroup 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (18 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 19/90] cgroup: add cgroup_cell_kill() Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 21/90] fs: seal a mount namespace for a cell Josef Bacik ` (69 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik copy_cgroup_ns() roots the new cgroup namespace at the cgroup of the caller. The builder of a cell is not in the budget of the cell and never will be. Add cgroup_cell_ns(), which creates a cgroup namespace that is rooted at a given cgroup, and cgroup_cell_cset(), which finds the css_set that it needs. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cgroup.h | 3 +++ kernel/cgroup/cgroup.c | 17 +++++++++++++++++ kernel/cgroup/namespace.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/include/linux/cgroup.h b/include/linux/cgroup.h index 57a4037671c9..af467b402448 100644 --- a/include/linux/cgroup.h +++ b/include/linux/cgroup.h @@ -154,6 +154,9 @@ struct cgroup *cgroup_get_from_path(const char *path); struct cgroup *cgroup_get_from_fd(int fd); struct cgroup *cgroup_cell_budget_get(struct file *file, kuid_t *uid, kgid_t *gid); +struct css_set *cgroup_cell_cset(struct cgroup *cgrp); +struct cgroup_namespace *cgroup_cell_ns(struct user_namespace *user_ns, + struct cgroup *cgrp); int cgroup_cell_chown(struct cgroup *cgrp, kuid_t uid, kgid_t gid); bool cgroup_cell_kill(struct cgroup *cgrp); int cgroup_cell_budget_put(struct cgroup *cgrp, bool handed, kuid_t uid, kgid_t gid); diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c index 75f8367271aa..051b220e2850 100644 --- a/kernel/cgroup/cgroup.c +++ b/kernel/cgroup/cgroup.c @@ -7163,6 +7163,23 @@ int cgroup_cell_budget_put(struct cgroup *cgrp, bool handed, kuid_t uid, kgid_t return ret; } +/** + * cgroup_cell_cset - the css_set a cgroup namespace rooted at a cgroup needs + * @cgrp: a cgroup on the default hierarchy + * + * Return: a css_set that has @cgrp on the default hierarchy and the root on + * every other, with a reference, or NULL if @cgrp is dead or memory is short. + */ +struct css_set *cgroup_cell_cset(struct cgroup *cgrp) +{ + struct css_set *cset; + + cgroup_lock(); + cset = cgroup_is_dead(cgrp) ? NULL : find_css_set(&init_css_set, cgrp); + cgroup_unlock(); + return cset; +} + /** * cgroup_cell_chown - hand a cgroup to somebody * @cgrp: the cgroup diff --git a/kernel/cgroup/namespace.c b/kernel/cgroup/namespace.c index 76660332ec64..987ec8cd5db6 100644 --- a/kernel/cgroup/namespace.c +++ b/kernel/cgroup/namespace.c @@ -87,6 +87,50 @@ struct cgroup_namespace *copy_cgroup_ns(u64 flags, return new_ns; } +#ifdef CONFIG_CELLS +/** + * cgroup_cell_ns - make a cgroup namespace rooted at a given cgroup + * @user_ns: the user namespace that will own it + * @cgrp: the cgroup, on the default hierarchy + * + * copy_cgroup_ns() roots the namespace wherever the caller happens to be. This + * is for a cell, whose builder is not in the budget. + * + * Return: the namespace with a reference, or an ERR_PTR(). + */ +struct cgroup_namespace *cgroup_cell_ns(struct user_namespace *user_ns, + struct cgroup *cgrp) +{ + struct cgroup_namespace *new_ns; + struct ucounts *ucounts; + struct css_set *cset; + + ucounts = inc_cgroup_namespaces(user_ns); + if (!ucounts) + return ERR_PTR(-ENOSPC); + + cset = cgroup_cell_cset(cgrp); + if (!cset) { + dec_cgroup_namespaces(ucounts); + return ERR_PTR(-ENOMEM); + } + + new_ns = alloc_cgroup_ns(); + if (IS_ERR(new_ns)) { + put_css_set(cset); + dec_cgroup_namespaces(ucounts); + return new_ns; + } + + new_ns->user_ns = get_user_ns(user_ns); + new_ns->ucounts = ucounts; + new_ns->root_cset = cset; + + ns_tree_add(new_ns); + return new_ns; +} +#endif + static int cgroupns_install(struct nsset *nsset, struct ns_common *ns) { struct nsproxy *nsproxy = nsset->nsproxy; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 21/90] fs: seal a mount namespace for a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (19 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 20/90] cgroup: root a cgroup namespace at a given cgroup Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 22/90] nsfs: make open_namespace() available outside of fs/ Josef Bacik ` (68 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The mount tree of a container is usually a copy of a tree of the host, with mounts placed on top of whatever the container must not see. MNT_LOCKED exists to keep those mounts in place. A bug that allows one of them to be removed, or the tree to be copied without it, reveals what was underneath. The tree of a cell is built from scratch instead. The builder assembles it one mount at a time, in a mount namespace that nobody has entered. The cell may see everything in that tree, including what lies under each mount point. No mount is ever used to hide something from a cell. Add mnt_ns_seal_for_cell(). It turns such a namespace into the namespace of a cell, or fails without changing anything. It requires and does the following: - No mount may be MNT_LOCKED, with one exception that hides nothing: the tree itself, mounted on the root of the namespace, which is an empty nullfs. create_new_namespace() locks that mount if the root of the caller is locked, which is the case for a caller that created a user namespace to do the mount work in. Without the exception only root could build a cell. - Nobody is in the namespace or about to enter it. mntns_install() takes its reference under namespace_sem, so either it sees that the namespace is sealed, or the seal sees the reference. - Every mount is made private, so nothing propagates into a cell. - For a cell with a block of ids, a writable mount of a filesystem that supports idmapping must be idmapped for the cell, or its superblock must be owned by the cell. Otherwise every create and chown would fail later. This check only reports the problem early and clearly. "Idmapped for the cell" is decided by identity: the idmapping holds a reference on the user namespace that it was created from. A sealed namespace cannot be entered. It is still owned by the user namespace of the builder, which is what prevents a task of the cell from mounting anything. Also add mnt_ns_may_claim() and mnt_ns_get_from_common(), for the code outside of fs/ that gets the namespace from a file descriptor. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/internal.h | 2 + fs/mnt_idmapping.c | 21 +++++++ fs/mount.h | 3 + fs/namespace.c | 141 +++++++++++++++++++++++++++++++++++++++++- include/linux/mnt_namespace.h | 6 ++ 5 files changed, 172 insertions(+), 1 deletion(-) diff --git a/fs/internal.h b/fs/internal.h index 967c8556c17f..2ce49dff6ee1 100644 --- a/fs/internal.h +++ b/fs/internal.h @@ -341,6 +341,8 @@ int path_from_stashed(struct dentry **stashed, struct vfsmount *mnt, void *data, void stashed_dentry_prune(struct dentry *dentry); struct dentry *stash_dentry(struct dentry **stashed, struct dentry *dentry); struct dentry *stashed_dentry_get(struct dentry **stashed); +bool mnt_idmap_is_for_cell(const struct mnt_idmap *idmap, + const struct user_namespace *cell_ns); /** * path_mounted - check whether path is mounted * @path: path to check diff --git a/fs/mnt_idmapping.c b/fs/mnt_idmapping.c index 93a34bc93efd..5a813c8d86fe 100644 --- a/fs/mnt_idmapping.c +++ b/fs/mnt_idmapping.c @@ -407,3 +407,24 @@ int statmount_mnt_idmap(const struct mnt_idmap *idmap, struct seq_file *seq, boo return nr_mappings; } + +#ifdef CONFIG_CELLS +/** + * mnt_idmap_is_for_cell - was an idmapping made for this cell? + * @idmap: idmapping of a mount + * @cell_ns: user namespace of a cell + * + * By identity and not by the value of the ids: nothing else can name an id of + * the window, and an idmapping that was made for an earlier holder of the same + * block keeps that block from being handed out again. + * + * Return: true if @idmap was made from @cell_ns. + */ +bool mnt_idmap_is_for_cell(const struct mnt_idmap *idmap, + const struct user_namespace *cell_ns) +{ + if (idmap == &nop_mnt_idmap || idmap == &invalid_mnt_idmap) + return false; + return idmap->cell_ns == cell_ns; +} +#endif diff --git a/fs/mount.h b/fs/mount.h index 4d23cade9f6c..3e6b30a5dee0 100644 --- a/fs/mount.h +++ b/fs/mount.h @@ -32,6 +32,9 @@ struct mnt_namespace { unsigned int pending_mounts; refcount_t passive; /* number references not pinning @mounts */ bool is_anon; +#ifdef CONFIG_CELLS + bool cell_sealed; /* a cell's: nobody enters. namespace_sem */ +#endif } __randomize_layout; struct mnt_pcp { diff --git a/fs/namespace.c b/fs/namespace.c index 25e196c05126..7298b589e10e 100644 --- a/fs/namespace.c +++ b/fs/namespace.c @@ -9,6 +9,7 @@ */ #include <linux/syscalls.h> +#include <linux/cell.h> #include <linux/export.h> #include <linux/capability.h> #include <linux/mnt_namespace.h> @@ -6710,6 +6711,136 @@ static void mntns_put(struct ns_common *ns) put_mnt_ns(to_mnt_ns(ns)); } +#ifdef CONFIG_CELLS +static inline bool mnt_ns_cell_sealed(const struct mnt_namespace *ns) +{ + return ns->cell_sealed; +} + +/** + * mnt_ns_get_from_common - take a reference on a mount namespace + * @ns: the ns_common of a mount namespace + * + * For callers outside of fs/, which cannot see into struct mnt_namespace. + * + * Return: the mount namespace, with a reference for put_mnt_ns(). + */ +struct mnt_namespace *mnt_ns_get_from_common(struct ns_common *ns) +{ + struct mnt_namespace *mnt_ns = to_mnt_ns(ns); + + get_mnt_ns(mnt_ns); + return mnt_ns; +} + +/** + * mnt_ns_may_claim - may the caller make a mount namespace a cell's? + * @ns: the ns_common of a mount namespace + * + * Return: true if the caller has CAP_SYS_ADMIN over the owner of @ns, which is + * what mounting in it takes. + */ +bool mnt_ns_may_claim(struct ns_common *ns) +{ + return ns_capable(to_mnt_ns(ns)->user_ns, CAP_SYS_ADMIN); +} + +/** + * mnt_ns_seal_for_cell - make a mount namespace a cell's + * @ns: the namespace. The caller holds @nr_refs references on it + * @nr_refs: see above + * @cell_ns: the cell's user namespace + * @ranged: the cell has a block of ids of its own + * @root: where to return the root of the tree + * + * The tree was built from scratch by whoever builds the cell, one mount at a + * time. Whatever is in it the cell may see all of, including what lies under + * every mount point: a mount is never what keeps something from a cell. So no + * mount may be locked, other than the tree itself on the empty root of @ns. + * Nothing propagates into a cell either, so the tree is cut loose from every + * peer and master. + * + * Nothing is changed unless everything checks out. + * + * Return: 0, -EBUSY if somebody is in @ns, on the way in, or it is a cell's + * already, -EINVAL if a mount is locked or @ns will not do, -EOVERFLOW if a + * writable mount is not for the ids of the cell. + */ +int mnt_ns_seal_for_cell(struct mnt_namespace *ns, unsigned int nr_refs, + struct user_namespace *cell_ns, bool ranged, + struct path *root) +{ + struct mount *m, *top = ns->root; + int err; + + if (is_anon_ns(ns) || ns == current->nsproxy->mnt_ns) + return -EINVAL; + + scoped_guard(namespace_excl) { + err = -EBUSY; + if (ns->cell_sealed) + goto out; + /* + * Nobody is in it and nobody is on the way in: besides the + * caller only an nsfs inode may hold on to it. + */ + if (ns_ref_read(ns) != nr_refs + !!READ_ONCE(ns->ns.stashed)) + goto out; + + for (m = ns->root; m; m = next_mnt(m, ns->root)) { + struct super_block *sb = m->mnt.mnt_sb; + + /* + * A locked mount hides what it covers. The exception + * covers nothing: create_new_namespace() locks the tree + * it puts on top of the root of the namespace if the + * caller's own is locked, which it is for whoever made + * a user namespace to do the mount work in. That root is + * an empty nullfs. + */ + err = -EINVAL; + if ((m->mnt.mnt_flags & MNT_LOCKED) && + !(m->mnt_parent == ns->root && + ns->root->mnt.mnt_sb->s_type == &nullfs_fs_type)) + goto out; + + /* + * Only the early and readable form of an error that + * kuid_storable() would make of every create and chown. + */ + err = -EOVERFLOW; + if (!ranged || __mnt_is_readonly(&m->mnt) || + !(sb->s_type->fs_flags & FS_ALLOW_IDMAP) || + sb->s_user_ns == cell_ns) + continue; + if (!mnt_idmap_is_for_cell(mnt_idmap(&m->mnt), cell_ns)) + goto out; + } + + for (m = ns->root; m; m = next_mnt(m, ns->root)) + change_mnt_propagation(m, MS_PRIVATE); + ns->cell_sealed = true; + scoped_guard(mount_locked_reader) { + touch_mnt_namespace(ns); + /* Whatever is on top, now that it can no longer change. */ + while ((m = __lookup_mnt(&top->mnt, top->mnt.mnt_root))) + top = m; + } + root->mnt = &top->mnt; + root->dentry = top->mnt.mnt_root; + path_get(root); + err = 0; + } +out: + return err; +} +#else +static inline bool mnt_ns_cell_sealed(const struct mnt_namespace *ns) +{ + return false; +} +#endif + static int mntns_install(struct nsset *nsset, struct ns_common *ns) { struct nsproxy *nsproxy = nsset->nsproxy; @@ -6730,7 +6861,15 @@ static int mntns_install(struct nsset *nsset, struct ns_common *ns) if (current->fs->users != 1) return -EINVAL; - get_mnt_ns(mnt_ns); + /* + * Either this sees that the namespace has become a cell's, or the + * sealing of the cell sees the reference. + */ + scoped_guard(namespace_shared) { + if (mnt_ns_cell_sealed(mnt_ns)) + return -EPERM; + get_mnt_ns(mnt_ns); + } old_mnt_ns = nsproxy->mnt_ns; nsproxy->mnt_ns = mnt_ns; diff --git a/include/linux/mnt_namespace.h b/include/linux/mnt_namespace.h index 0acd1089d149..ed7e227f9e3d 100644 --- a/include/linux/mnt_namespace.h +++ b/include/linux/mnt_namespace.h @@ -10,6 +10,7 @@ struct mnt_namespace; struct fs_struct; struct user_namespace; struct ns_common; +struct path; extern struct mnt_namespace init_mnt_ns; @@ -18,6 +19,11 @@ extern struct mnt_namespace *copy_mnt_ns(u64, struct mnt_namespace *, extern void put_mnt_ns(struct mnt_namespace *ns); DEFINE_FREE(put_mnt_ns, struct mnt_namespace *, if (!IS_ERR_OR_NULL(_T)) put_mnt_ns(_T)) extern struct ns_common *from_mnt_ns(struct mnt_namespace *); +struct mnt_namespace *mnt_ns_get_from_common(struct ns_common *ns); +bool mnt_ns_may_claim(struct ns_common *ns); +int mnt_ns_seal_for_cell(struct mnt_namespace *ns, unsigned int nr_refs, + struct user_namespace *cell_ns, bool ranged, + struct path *root); extern const struct file_operations proc_mounts_operations; extern const struct file_operations proc_mountinfo_operations; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 22/90] nsfs: make open_namespace() available outside of fs/ 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (20 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 21/90] fs: seal a mount namespace for a cell Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 23/90] nsproxy: build the nsproxy the tasks of a cell share Josef Bacik ` (67 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik While a cell is being built it hands out nsfs file descriptors for its network and pid namespaces. That code lives in kernel/cell/ and needs open_namespace(). Move the declaration from fs/internal.h to linux/nsfs.h. The name is too generic for a function that is visible everywhere. Rename it to ns_open_namespace(), in line with the other functions that are declared in that header. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/internal.h | 1 - fs/nsfs.c | 6 +++--- fs/pidfs.c | 4 ++-- include/linux/nsfs.h | 1 + 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fs/internal.h b/fs/internal.h index 2ce49dff6ee1..8446aed78466 100644 --- a/fs/internal.h +++ b/fs/internal.h @@ -251,7 +251,6 @@ extern void mnt_pin_kill(struct mount *m); * fs/nsfs.c */ extern const struct dentry_operations ns_dentry_operations; -int open_namespace(struct ns_common *ns); struct file *open_namespace_file(struct ns_common *ns); /* diff --git a/fs/nsfs.c b/fs/nsfs.c index 8179d69da038..2c7165bb6739 100644 --- a/fs/nsfs.c +++ b/fs/nsfs.c @@ -113,14 +113,14 @@ struct file *open_namespace_file(struct ns_common *ns) } /** - * open_namespace - open a namespace + * ns_open_namespace - open a namespace * @ns: the namespace to open * * This will consume a reference to @ns indendent of success or failure. * * Return: A file descriptor on success or a negative error code on failure. */ -int open_namespace(struct ns_common *ns) +int ns_open_namespace(struct ns_common *ns) { struct path path __free(path_put) = {}; int err; @@ -142,7 +142,7 @@ int open_related_ns(struct ns_common *ns, if (IS_ERR(relative)) return PTR_ERR(relative); - return open_namespace(relative); + return ns_open_namespace(relative); } EXPORT_SYMBOL_GPL(open_related_ns); diff --git a/fs/pidfs.c b/fs/pidfs.c index 114d2713a79e..ef305cc359eb 100644 --- a/fs/pidfs.c +++ b/fs/pidfs.c @@ -709,8 +709,8 @@ static long pidfd_ioctl(struct file *file, unsigned int cmd, unsigned long arg) if (error) return error; - /* open_namespace() unconditionally consumes the reference */ - return open_namespace(ns_common); + /* ns_open_namespace() unconditionally consumes the reference */ + return ns_open_namespace(ns_common); } #ifdef CONFIG_COMPAT diff --git a/include/linux/nsfs.h b/include/linux/nsfs.h index 731b67fc2fec..4cb87c7644a2 100644 --- a/include/linux/nsfs.h +++ b/include/linux/nsfs.h @@ -23,6 +23,7 @@ bool ns_match(const struct ns_common *ns, dev_t dev, ino_t ino); int ns_get_name(char *buf, size_t size, struct task_struct *task, const struct proc_ns_operations *ns_ops); void nsfs_init(void); +int ns_open_namespace(struct ns_common *ns); #define __current_namespace_from_type(__ns) \ _Generic((__ns), \ -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 23/90] nsproxy: build the nsproxy the tasks of a cell share 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (21 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 22/90] nsfs: make open_namespace() available outside of fs/ Josef Bacik @ 2026-11-05 14:57 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 24/90] cells: add the cell object and cell_create() Josef Bacik ` (66 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 14:57 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The namespaces of a cell are a property of the cell, not of its tasks. A task of a cell can call neither unshare() nor setns(). So a single nsproxy, created when the cell is sealed, can serve every task that the cell will ever have. Add cell_create_nsproxy(), which builds that nsproxy from the namespaces in the slots of the cell. The uts and ipc namespaces are always new. Objects in a shared ipc namespace would be owned by ids of the cell and could outlive the cell. The time namespace is that of the builder. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/nsproxy.h | 9 +++++++ kernel/nsproxy.c | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/include/linux/nsproxy.h b/include/linux/nsproxy.h index dc2447f5f092..02101d96e4dd 100644 --- a/include/linux/nsproxy.h +++ b/include/linux/nsproxy.h @@ -12,6 +12,9 @@ struct ipc_namespace; struct pid_namespace; struct cgroup_namespace; struct fs_struct; +struct cell; +struct net; +struct user_namespace; /* * A structure to contain pointers to all per-process @@ -101,6 +104,12 @@ void switch_task_namespaces(struct task_struct *tsk, struct nsproxy *new); int exec_task_namespaces(void); void deactivate_nsproxy(struct nsproxy *ns); void nsproxy_free(struct nsproxy *ns); +struct nsproxy *cell_create_nsproxy(struct user_namespace *user_ns, + struct mnt_namespace *mnt_ns, + struct pid_namespace *pid_ns, + struct net *net, + struct cgroup_namespace *cgroup_ns, + const char *hostname); int unshare_nsproxy_namespaces(unsigned long, struct nsproxy **, struct cred *, struct fs_struct *); int __init nsproxy_cache_init(void); diff --git a/kernel/nsproxy.c b/kernel/nsproxy.c index 2dcf105e8ee2..bfdd392f4007 100644 --- a/kernel/nsproxy.c +++ b/kernel/nsproxy.c @@ -28,6 +28,7 @@ #include <linux/cgroup.h> #include <linux/perf_event.h> #include <linux/nstree.h> +#include <linux/cell.h> static struct kmem_cache *nsproxy_cachep; @@ -242,6 +243,72 @@ int unshare_nsproxy_namespaces(unsigned long unshare_flags, return err; } +#ifdef CONFIG_CELLS +/** + * cell_create_nsproxy - make the nsproxy every task of a cell shares + * @user_ns: the user namespace of the cell + * @mnt_ns: its mount namespace + * @pid_ns: its pid namespace + * @net: its network namespace + * @cgroup_ns: its cgroup namespace + * @hostname: its hostname + * + * The nsproxy never changes: a task of a cell can neither unshare() nor + * setns(). It takes its own references. The uts and ipc namespaces are always + * fresh, the time namespace is the caller's. + * + * Return: the nsproxy, active and with a reference for put_nsproxy(), or an + * ERR_PTR(). + */ +struct nsproxy *cell_create_nsproxy(struct user_namespace *user_ns, + struct mnt_namespace *mnt_ns, + struct pid_namespace *pid_ns, + struct net *net, + struct cgroup_namespace *cgroup_ns, + const char *hostname) +{ + struct nsproxy *cur = current->nsproxy; + struct nsproxy *nsp; + int err; + + nsp = create_nsproxy(); + if (!nsp) + return ERR_PTR(-ENOMEM); + + nsp->uts_ns = copy_utsname(CLONE_NEWUTS, user_ns, cur->uts_ns); + if (IS_ERR(nsp->uts_ns)) { + err = PTR_ERR(nsp->uts_ns); + goto out_free; + } + /* Nobody else can see it yet. */ + strscpy_pad(nsp->uts_ns->name.nodename, hostname); + strscpy_pad(nsp->uts_ns->name.domainname, "(none)"); + + nsp->ipc_ns = copy_ipcs(CLONE_NEWIPC, user_ns, cur->ipc_ns); + if (IS_ERR(nsp->ipc_ns)) { + err = PTR_ERR(nsp->ipc_ns); + goto out_uts; + } + + nsp->mnt_ns = mnt_ns_get_from_common(from_mnt_ns(mnt_ns)); + nsp->pid_ns_for_children = get_pid_ns(pid_ns); + nsp->net_ns = get_net(net); + get_cgroup_ns(cgroup_ns); + nsp->cgroup_ns = cgroup_ns; + nsp->time_ns = get_time_ns(cur->time_ns); + nsp->time_ns_for_children = get_time_ns(cur->time_ns); + + nsproxy_ns_active_get(nsp); + return nsp; + +out_uts: + put_uts_ns(nsp->uts_ns); +out_free: + kmem_cache_free(nsproxy_cachep, nsp); + return ERR_PTR(err); +} +#endif + void switch_task_namespaces(struct task_struct *p, struct nsproxy *new) { struct nsproxy *ns; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 24/90] cells: add the cell object and cell_create() 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (22 preceding siblings ...) 2026-11-05 14:57 ` [PATCH RFC v3 23/90] nsproxy: build the nsproxy the tasks of a cell share Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 25/90] cells: add cell_kill() Josef Bacik ` (65 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Add the cell object in kernel/cell/, a small filesystem called cellfs that backs its file descriptor, and the system call cell_create(). cell_create() creates a new, empty cell and returns a cellfd. It requires no privilege. A task of a cell cannot call it, see cell_may_spawn(). A cell goes through the states BUILDING, SEALED, RUNNING, DYING and DEAD, in that order and never backwards. This patch implements the two ends: a cell that holds only its user namespace, and the teardown that drops whatever a cell holds. The cellfd becomes readable for poll() once the cell is DEAD. read() then returns the wait status of the first task of the cell. DEAD is only reported after the budget is empty and the cell has dropped all of its references. A cellfd can be passed to another process, but it cannot be reopened through /proc/<pid>/fd. So when the last cellfd is closed nobody can reach the cell any more, and the cell is killed. If the process that created the cell is killed, the cell dies with it, unless the cellfd was passed on. A cell without a cellfd could not be killed, spawned into or waited for. Every cell has a 64 bit id that is never reused. It is meant for code that has to remember a cell for longer than anything pins it. It is also the inode number of the cellfd. cell_id() is exported, because some of that code can be built as a module. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/cellfs.c | 141 ++++++++++++++++++++ include/linux/cell.h | 23 ++++ include/uapi/linux/cell.h | 24 ++++ include/uapi/linux/fcntl.h | 1 + include/uapi/linux/magic.h | 1 + kernel/cell/Makefile | 2 + kernel/cell/core.c | 312 +++++++++++++++++++++++++++++++++++++++++++++ kernel/cell/internal.h | 62 +++++++++ 8 files changed, 566 insertions(+) diff --git a/fs/cellfs.c b/fs/cellfs.c new file mode 100644 index 000000000000..744ed39abbba --- /dev/null +++ b/fs/cellfs.c @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * cellfs: the pseudo filesystem a cellfd lives on. + * + * Modelled on pidfs. One inode per cell, numbered by the cell's id, which is + * never reused. The inode holds a reference on the cell. + */ +#include <linux/cell.h> +#include <linux/file.h> +#include <linux/fs.h> +#include <linux/magic.h> +#include <linux/mount.h> +#include <linux/pseudo_fs.h> + +#include "internal.h" + +static struct vfsmount *cellfs_mnt __ro_after_init; + +static void cellfs_evict_inode(struct inode *inode) +{ + struct cell *cell = inode->i_private; + + clear_inode(inode); + cell_put(cell); +} + +static const struct super_operations cellfs_sops = { + .drop_inode = inode_just_drop, + .evict_inode = cellfs_evict_inode, + .statfs = simple_statfs, +}; + +static char *cellfs_dname(struct dentry *dentry, char *buffer, int buflen) +{ + return dynamic_dname(buffer, buflen, "cell:[%llu]", + d_inode(dentry)->i_ino); +} + +static const struct dentry_operations cellfs_dentry_operations = { + .d_dname = cellfs_dname, + .d_prune = stashed_dentry_prune, +}; + +static int cellfs_setattr(const struct mnt_idmap *idmap, struct dentry *dentry, + struct iattr *attr) +{ + return -EOPNOTSUPP; +} + +static const struct inode_operations cellfs_inode_operations = { + .setattr = cellfs_setattr, +}; + +static int cellfs_init_inode(struct inode *inode, void *data) +{ + struct cell *cell = data; + + inode->i_private = data; + inode->i_flags |= S_PRIVATE | S_ANON_INODE; + inode->i_mode |= 0700; + inode->i_op = &cellfs_inode_operations; + inode->i_fop = &cell_file_operations; + inode->i_ino = cell_id(cell); + return 0; +} + +static void cellfs_put_data(void *data) +{ + cell_put(data); +} + +static const struct stashed_operations cellfs_stashed_ops = { + .init_inode = cellfs_init_inode, + .put_data = cellfs_put_data, +}; + +static int cellfs_init_fs_context(struct fs_context *fc) +{ + struct pseudo_fs_context *ctx; + + ctx = init_pseudo(fc, CELL_FS_MAGIC); + if (!ctx) + return -ENOMEM; + + ctx->s_d_flags |= DCACHE_DONTCACHE; + ctx->ops = &cellfs_sops; + ctx->dops = &cellfs_dentry_operations; + fc->s_fs_info = (void *)&cellfs_stashed_ops; + return 0; +} + +static struct file_system_type cellfs_type = { + .name = "cellfs", + .init_fs_context = cellfs_init_fs_context, + .kill_sb = kill_anon_super, +}; + +/** + * cell_from_file - the cell a file is a handle on + * @file: any file + * + * Return: the cell, or NULL if @file is no cellfd. No reference is taken: @file + * pins the inode, which holds one. + */ +struct cell *cell_from_file(const struct file *file) +{ + if (file->f_op != &cell_file_operations) + return NULL; + return file_inode(file)->i_private; +} + +/** + * cellfs_alloc_file - make a new handle on a cell + * @cell: the cell. The caller keeps its own reference + * + * This is the only way to a handle. A cellfd cannot be opened again through + * /proc/<pid>/fd. + * + * Return: a file open for reading and writing, or an ERR_PTR(). + */ +struct file *cellfs_alloc_file(struct cell *cell) +{ + struct path path __free(path_put) = {}; + int ret; + + ret = path_from_stashed(cell_stashed(cell), cellfs_mnt, cell_get(cell), + &path); + if (ret < 0) + return ERR_PTR(ret); + + return dentry_open(&path, O_RDWR, current_cred()); +} + +static int __init cellfs_init(void) +{ + cellfs_mnt = kern_mount(&cellfs_type); + if (IS_ERR(cellfs_mnt)) + panic("Failed to mount cellfs pseudo filesystem"); + return 0; +} +fs_initcall(cellfs_init); diff --git a/include/linux/cell.h b/include/linux/cell.h index 37fde4bfea44..75dc59518c91 100644 --- a/include/linux/cell.h +++ b/include/linux/cell.h @@ -69,6 +69,20 @@ static inline struct cell *current_cell(void) return cred_cell(current_cred()); } +/** + * cell_may_spawn - may the caller make cells and start tasks in them? + * + * This is the "spawn" slot of the cell the caller acts in. There is nothing to + * fill it with yet, so it is always empty, and the host holds everything. That + * no cellfd gets into a cell is not what this rests on. + * + * Return: true if it may. + */ +static inline bool cell_may_spawn(void) +{ + return !current_cell(); +} + /** * task_cell - the cell a task is in * @task: a task @@ -137,6 +151,7 @@ void cell_put(struct cell *cell); static inline struct cell *userns_cell(const struct user_namespace *ns) { return NULL; } static inline struct cell *cred_cell(const struct cred *cred) { return NULL; } static inline struct cell *current_cell(void) { return NULL; } +static inline bool cell_may_spawn(void) { return true; } static inline struct cell *task_cell(const struct task_struct *t) { return NULL; } static inline bool cell_same(const struct cred *a, const struct cred *b) { return true; } static inline u64 cred_cell_id(const struct cred *cred) { return 0; } @@ -152,4 +167,12 @@ void cell_block_release(u32 block); static inline void cell_block_release(u32 block) { } #endif +/* The cellfd. */ +#ifdef CONFIG_CELLS +extern const struct file_operations cell_file_operations; +struct file *cellfs_alloc_file(struct cell *cell); +struct cell *cell_from_file(const struct file *file); +struct dentry **cell_stashed(struct cell *cell); +#endif + #endif /* _LINUX_CELL_H */ diff --git a/include/uapi/linux/cell.h b/include/uapi/linux/cell.h new file mode 100644 index 000000000000..f3589a23a596 --- /dev/null +++ b/include/uapi/linux/cell.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +#ifndef _UAPI_LINUX_CELL_H +#define _UAPI_LINUX_CELL_H + +#include <linux/types.h> + +/* + * A cell is a container: one kernel object that a task is born into and never + * leaves. It is built from the outside, by filling named slots from file + * descriptors the builder holds, and then sealed. + */ + +/* cell_create(): @parent is CELL_SELF, see <linux/fcntl.h>. */ + +/* Life of a cell. It only ever moves down this list. */ +enum cell_state { + CELL_BUILDING = 0, /* Slots may be filled */ + CELL_SEALED = 1, /* Immutable, nothing spawned yet */ + CELL_RUNNING = 2, /* Has, or had, its first task */ + CELL_DYING = 3, /* Killed, or its init exited */ + CELL_DEAD = 4, /* Nothing left. The status can be read */ +}; + +#endif /* _UAPI_LINUX_CELL_H */ diff --git a/include/uapi/linux/fcntl.h b/include/uapi/linux/fcntl.h index e43e3de3e9ee..951c97e47e99 100644 --- a/include/uapi/linux/fcntl.h +++ b/include/uapi/linux/fcntl.h @@ -125,6 +125,7 @@ struct delegation { #define FD_PIDFS_ROOT -10002 /* Root of the pidfs filesystem */ #define FD_NSFS_ROOT -10003 /* Root of the nsfs filesystem */ #define FD_FAILFS_ROOT -10004 /* Root of the failfs filesystem */ +#define CELL_SELF -10005 /* The caller's own cell */ #define FD_INVALID -10009 /* Invalid file descriptor: -10000 - EBADF = -10009 */ /* Generic flags for the *at(2) family of syscalls. */ diff --git a/include/uapi/linux/magic.h b/include/uapi/linux/magic.h index 66a91c8b1cb9..093e68790b47 100644 --- a/include/uapi/linux/magic.h +++ b/include/uapi/linux/magic.h @@ -107,5 +107,6 @@ #define GUEST_MEMFD_MAGIC 0x474d454d /* "GMEM" */ #define NULL_FS_MAGIC 0x4E554C4C /* "NULL" */ #define FAIL_FS_MAGIC 0x4641494C /* "FAIL" */ +#define CELL_FS_MAGIC 0x43454C4C /* "CELL" */ #endif /* __LINUX_MAGIC_H__ */ diff --git a/kernel/cell/Makefile b/kernel/cell/Makefile new file mode 100644 index 000000000000..e343e0fa72b0 --- /dev/null +++ b/kernel/cell/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +obj-y += core.o diff --git a/kernel/cell/core.c b/kernel/cell/core.c new file mode 100644 index 000000000000..d0243779af19 --- /dev/null +++ b/kernel/cell/core.c @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Cells: a container as one kernel object. + * + * A task is born in a cell and never leaves it. There is no way to enter one. + * A cell is built from the outside: the builder fills named slots from file + * descriptors it holds, seals the cell, and spawns the first task into it. + * Holding the cellfd is the authority to do all of that. + * + * Life: BUILDING -> SEALED -> RUNNING -> DYING -> DEAD + * + * References. The cell owns a reference on its user namespace until it is torn + * down. The user namespace owns a reference on the cell until it is freed, so + * that current_cell() can be followed from any credential. Teardown breaks the + * cycle, and every cell is torn down: by a kill, by the exit of its first task, + * or by the last cellfd going away before anything ran. + * + * Locking. cell->lock serialises configuration, sealing, spawning and killing. + * It is held across copy_process(), which is what closes a kill against a spawn + * in flight. It nests outside cgroup_mutex and namespace_sem. + */ +#include <linux/cgroup.h> +#include <linux/fs.h> +#include <linux/mnt_namespace.h> +#include <linux/nsproxy.h> +#include <linux/pid_namespace.h> +#include <linux/poll.h> +#include <linux/sched/task.h> +#include <linux/slab.h> +#include <linux/syscalls.h> +#include <net/net_namespace.h> +#include <uapi/linux/cell.h> + +#include "internal.h" + +static atomic64_t cell_next_id = ATOMIC64_INIT(0); + +/** + * cell_id - the id of a cell + * @cell: a cell + * + * Return: the id. It is never 0, which stands for the host, and never reused. + */ +u64 cell_id(const struct cell *cell) +{ + return cell->id; +} +EXPORT_SYMBOL_GPL(cell_id); + +/** + * cell_stashed - where cellfs keeps the dentry of a cell + * @cell: a cell + * + * Return: the location to hand to path_from_stashed(). + */ +struct dentry **cell_stashed(struct cell *cell) +{ + return &cell->stashed; +} + +/** + * cell_get - take a reference on a cell + * @cell: a cell the caller already holds a reference on + * + * A reference keeps the memory of the cell around and nothing else. It neither + * keeps the cell alive nor is it a handle on it. + * + * Return: @cell. + */ +struct cell *cell_get(struct cell *cell) +{ + refcount_inc(&cell->ref); + return cell; +} + +/** + * cell_put - drop a reference on a cell + * @cell: a cell, or NULL + * + * Context: Process context. The cell is freed after a grace period. + */ +void cell_put(struct cell *cell) +{ + if (!cell || !refcount_dec_and_test(&cell->ref)) + return; + if (cell->groups) + put_group_info(cell->groups); + kfree_rcu(cell, rcu); +} + +/* + * Teardown. Drops every reference the cell holds and only then says DEAD. What + * that sets off, the release of mounts, of the network namespace and of the + * block of ids, completes on its own time. + */ +static void cell_teardown(struct cell *cell) +{ + struct cgroup *budget = cell->budget; + + lockdep_assert_held(&cell->lock); + + /* cell_may_migrate() looks at this under nothing but RCU. */ + WRITE_ONCE(cell->budget, NULL); + + if (cell->nsproxy) + put_nsproxy(cell->nsproxy); + cell->nsproxy = NULL; + path_put(&cell->root); + memset(&cell->root, 0, sizeof(cell->root)); + if (cell->mnt_ns) + put_mnt_ns(cell->mnt_ns); + cell->mnt_ns = NULL; + if (cell->pid_ns) + put_pid_ns(cell->pid_ns); + cell->pid_ns = NULL; + if (cell->net) + put_net(cell->net); + cell->net = NULL; + if (budget) { + /* + * The ids of the cell were only lent, so nothing may go on + * being owned by them. If that cannot be had, the block is + * never handed out again. + */ + if (cgroup_cell_budget_put(budget, cell->sealed, cell->budget_uid, + cell->budget_gid)) + userns_retire_cell_block(cell->user_ns); + } + put_user_ns(cell->user_ns); + cell->user_ns = NULL; + + /* Pairs with the acquire of whoever goes on to read the status. */ + smp_store_release(&cell->state, CELL_DEAD); + wake_up_all(&cell->wait); +} + +static void cell_kill_locked(struct cell *cell) +{ + lockdep_assert_held(&cell->lock); + + switch (cell->state) { + case CELL_BUILDING: + case CELL_SEALED: + /* Nothing ever ran. */ + cell->exit_status = SIGKILL; + cell_teardown(cell); + break; + case CELL_RUNNING: + WRITE_ONCE(cell->state, CELL_DYING); + fallthrough; + case CELL_DYING: + /* The exit of the cell's init finishes the job. */ + cgroup_cell_kill(cell->budget); + break; + case CELL_DEAD: + break; + } +} + +static void cell_kill(struct cell *cell) +{ + guard(mutex)(&cell->lock); + cell_kill_locked(cell); +} + +/* The last cellfd is gone, and there is no way to get another. */ +static void cell_kill_workfn(struct work_struct *work) +{ + struct cell *cell = container_of(work, struct cell, kill_work); + + cell_kill(cell); + cell_put(cell); +} + +/* The cellfd. */ + +static int cell_file_open(struct inode *inode, struct file *file) +{ + /* + * A handle is handed out, by cellfs_alloc_file(), and passed on. It is + * not opened again through /proc/<pid>/fd: the inode is immutable, so + * may_open() refuses that for writing, and this refuses the rest. + */ + if (!(file->f_mode & FMODE_WRITE)) + return -ENXIO; + + return 0; +} + +static int cell_file_release(struct inode *inode, struct file *file) +{ + struct cell *cell = inode->i_private; + + /* + * A cell has the one file that cell_create() made, however many file + * descriptors refer to it. Nobody can reach the cell any more, and there + * is no way to get another handle, so it goes: a cell lives for as long + * as somebody holds it. The last fput() may come from anywhere, so only + * owe the kill here. + */ + cell_get(cell); + if (!queue_work(system_dfl_wq, &cell->kill_work)) + cell_put(cell); + return 0; +} + +static bool cell_is_dead(struct cell *cell) +{ + /* Pairs with the release in cell_teardown(): the status is there. */ + return smp_load_acquire(&cell->state) == CELL_DEAD; +} + +static __poll_t cell_file_poll(struct file *file, poll_table *pts) +{ + struct cell *cell = cell_from_file(file); + + poll_wait(file, &cell->wait, pts); + if (cell_is_dead(cell)) + return EPOLLIN | EPOLLRDNORM; + return 0; +} + +/* Reads the wait status of the cell's first task. Repeatable. */ +static ssize_t cell_file_read(struct file *file, char __user *buf, size_t count, + loff_t *ppos) +{ + struct cell *cell = cell_from_file(file); + __s32 status; + int ret; + + if (count < sizeof(status)) + return -EINVAL; + + if (!cell_is_dead(cell)) { + if (file->f_flags & O_NONBLOCK) + return -EAGAIN; + ret = wait_event_interruptible(cell->wait, cell_is_dead(cell)); + if (ret) + return ret; + } + + status = cell->exit_status; + if (copy_to_user(buf, &status, sizeof(status))) + return -EFAULT; + return sizeof(status); +} + +const struct file_operations cell_file_operations = { + .open = cell_file_open, + .release = cell_file_release, + .poll = cell_file_poll, + .read = cell_file_read, + .llseek = noop_llseek, +}; + +/** + * sys_cell_create - make a new, empty cell + * @parent: CELL_SELF + * @flags: none yet, must be 0 + * + * The cell holds nothing, is in state CELL_BUILDING and can already be killed. + * It takes no privilege beyond what making a user namespace takes. A task of a + * cell cannot make one. + * + * Return: a cellfd, which is always close-on-exec, or a negative error. + */ +SYSCALL_DEFINE2(cell_create, int, parent, unsigned int, flags) +{ + struct cell *cell; + struct cred *cred; + int ret; + + if (flags) + return -EINVAL; + if (parent != CELL_SELF) + return -EINVAL; + if (!cell_may_spawn()) + return -EPERM; + + cell = kzalloc_obj(struct cell, GFP_KERNEL_ACCOUNT); + if (!cell) + return -ENOMEM; + + refcount_set(&cell->ref, 1); + cell->id = atomic64_inc_return(&cell_next_id); + mutex_init(&cell->lock); + init_waitqueue_head(&cell->wait); + INIT_WORK(&cell->kill_work, cell_kill_workfn); + strscpy(cell->hostname, "(none)"); + + /* The scratch credential is only here to carry the new namespace. */ + cred = prepare_creds(); + if (!cred) { + kfree(cell); + return -ENOMEM; + } + ret = create_cell_user_ns(cred, cell); + if (ret) { + abort_creds(cred); + kfree(cell); + return ret; + } + cell->user_ns = get_user_ns(cred->user_ns); + cell->groups = get_group_info(cred->group_info); + abort_creds(cred); + + ret = FD_ADD(O_CLOEXEC, cellfs_alloc_file(cell)); + if (ret < 0) + cell_kill(cell); + cell_put(cell); + return ret; +} diff --git a/kernel/cell/internal.h b/kernel/cell/internal.h new file mode 100644 index 000000000000..01a7a8581ff7 --- /dev/null +++ b/kernel/cell/internal.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef _KERNEL_CELL_INTERNAL_H +#define _KERNEL_CELL_INTERNAL_H + +#include <linux/capability.h> +#include <linux/cell.h> +#include <linux/file.h> +#include <linux/mutex.h> +#include <linux/path.h> +#include <linux/rcupdate.h> +#include <linux/refcount.h> +#include <linux/uidgid.h> +#include <linux/utsname.h> +#include <linux/wait.h> +#include <linux/workqueue.h> + +#define CELL_KEY_MAX 64 + +struct cell { + refcount_t ref; + u64 id; + struct dentry *stashed; + + struct mutex lock; + unsigned int state; /* enum cell_state */ + + /* Gone once the cell is DEAD. */ + struct user_namespace *user_ns; + u32 ids_count; + struct group_info *groups; /* of whoever made the cell */ + + /* Slots. */ + struct mnt_namespace *mnt_ns; + struct pid_namespace *pid_ns; + struct net *net; + struct cgroup *budget; + u64 budget_id; + kuid_t budget_uid; /* who owned it before */ + kgid_t budget_gid; + bool sealed; /* the budget was handed over */ + char hostname[__NEW_UTS_LEN + 1]; + kernel_cap_t caps; /* what a task starts with */ + + /* What sealing makes of them. */ + struct nsproxy *nsproxy; + struct path root; + + int exit_status; + wait_queue_head_t wait; + struct work_struct kill_work; + struct delayed_work exit_work; + struct rcu_head rcu; +}; + +static inline struct cell *cell_from_fd(const struct fd *f) +{ + if (fd_empty(*f)) + return ERR_PTR(-EBADF); + return cell_from_file(fd_file(*f)) ?: ERR_PTR(-EINVAL); +} + +#endif /* _KERNEL_CELL_INTERNAL_H */ -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 25/90] cells: add cell_kill() 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (23 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 24/90] cells: add the cell object and cell_create() Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 26/90] cells: add cell_configure() Josef Bacik ` (64 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik cell_kill() kills a cell and everything in it, in whatever state the cell is. A cell that has not run anything yet is torn down immediately. Otherwise every task in the budget is killed, and the teardown happens when the init of the cell exits. cell_kill() does not wait for that. The cellfd becomes readable once the cell is dead. This is a separate system call rather than a command of cell_configure(), which only applies to a cell that is being built. It also means that a seccomp filter can allow or deny killing by system call number, the same way cell_info() separates looking from changing. Holding the cellfd is the only requirement. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cell/core.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/kernel/cell/core.c b/kernel/cell/core.c index d0243779af19..b9494b5fd7b9 100644 --- a/kernel/cell/core.c +++ b/kernel/cell/core.c @@ -310,3 +310,31 @@ SYSCALL_DEFINE2(cell_create, int, parent, unsigned int, flags) cell_put(cell); return ret; } + +/** + * sys_cell_kill - kill a cell and everything in it + * @cfd: the cell, in whatever state + * @flags: none yet, must be 0 + * + * Kills every task in the budget. A cell that has run nothing yet is torn down + * on the spot. For one that has, that is for the exit of its init to set off, so + * this does not wait: the cellfd polls readable once the cell is dead. Killing a + * cell that is dying or dead already changes nothing. + * + * Return: 0, -EINVAL for a flag or a file that is no cellfd, -EBADF. + */ +SYSCALL_DEFINE2(cell_kill, int, cfd, unsigned int, flags) +{ + struct cell *cell; + + if (flags) + return -EINVAL; + + CLASS(fd, f)(cfd); + cell = cell_from_fd(&f); + if (IS_ERR(cell)) + return PTR_ERR(cell); + + cell_kill(cell); + return 0; +} -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 26/90] cells: add cell_configure() 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (24 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 25/90] cells: add cell_kill() Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 27/90] cells: add the "budget" slot Josef Bacik ` (63 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Add the system call that changes a cell. It is modelled on fsconfig(): it takes a command, a key and a value, and data only flows into the kernel. Reading from a cell is a separate system call, so that a seccomp filter can allow a monitor to read and deny it to change anything. Holding the cellfd is the only authority that is needed over the cell. This patch adds the framework and the one key that depends on nothing else, the "hostname" string. There are no flags for CELL_SET_FLAG yet. Every command requires the cell to be in the BUILDING state. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/uapi/linux/cell.h | 9 +++ kernel/cell/Makefile | 1 + kernel/cell/slots.c | 136 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) diff --git a/include/uapi/linux/cell.h b/include/uapi/linux/cell.h index f3589a23a596..22337cab3da1 100644 --- a/include/uapi/linux/cell.h +++ b/include/uapi/linux/cell.h @@ -12,6 +12,15 @@ /* cell_create(): @parent is CELL_SELF, see <linux/fcntl.h>. */ +/* cell_configure(): @cmd. Data only ever goes in. */ +enum cell_configure_cmd { + CELL_SET_FLAG = 0, /* Set the flag named by @key. There are none yet */ + CELL_SET_STRING = 1, /* Set @key to the string in @value */ + CELL_SET_BINARY = 2, /* Set @key to the @aux bytes at @value */ + CELL_SET_FD = 3, /* Fill the slot named by @key from fd @aux */ + CELL_CMD_SEAL = 4, /* Validate everything and seal */ +}; + /* Life of a cell. It only ever moves down this list. */ enum cell_state { CELL_BUILDING = 0, /* Slots may be filled */ diff --git a/kernel/cell/Makefile b/kernel/cell/Makefile index e343e0fa72b0..ac48bfb289d0 100644 --- a/kernel/cell/Makefile +++ b/kernel/cell/Makefile @@ -1,2 +1,3 @@ # SPDX-License-Identifier: GPL-2.0-only obj-y += core.o +obj-y += slots.o diff --git a/kernel/cell/slots.c b/kernel/cell/slots.c new file mode 100644 index 000000000000..905672db9936 --- /dev/null +++ b/kernel/cell/slots.c @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Cells: filling the slots of a cell, and sealing it. + */ +#include <linux/cgroup.h> +#include <linux/fs.h> +#include <linux/mnt_namespace.h> +#include <linux/nsfs.h> +#include <linux/nsproxy.h> +#include <linux/pid_namespace.h> +#include <linux/proc_fs.h> +#include <linux/proc_ns.h> +#include <linux/slab.h> +#include <linux/syscalls.h> +#include <net/net_namespace.h> +#include <uapi/linux/cell.h> + +#include "internal.h" + +/* + * Slots. All of these run with cell->lock held and the cell BUILDING. Filling a + * slot takes what using the object that goes into it would take: holding the + * cellfd is authority over the cell, not over what is put into it. + */ + +static int cell_set_hostname(struct cell *cell, const char *val) +{ + if (strlen(val) > __NEW_UTS_LEN) + return -ENAMETOOLONG; + strscpy(cell->hostname, val); + return 0; +} + +static int cell_set_string(struct cell *cell, const char *key, const char *val) +{ + if (!strcmp(key, "hostname")) + return cell_set_hostname(cell, val); + return -EINVAL; +} + +static int cell_set_binary(struct cell *cell, const char *key, + const void __user *uvalue, size_t len) +{ + return -EINVAL; +} + +static int cell_set_fd(struct cell *cell, const char *key, struct file *file) +{ + return -EINVAL; +} + +/** + * sys_cell_configure - change a cell + * @cfd: a cellfd + * @cmd: one of enum cell_configure_cmd + * @ukey: the name of what is set, or NULL for the commands that take none + * @uvalue: a string for CELL_SET_STRING, @aux bytes for CELL_SET_BINARY + * @aux: a length for CELL_SET_BINARY, a file descriptor for CELL_SET_FD + * + * Shaped like fsconfig(). Data only goes in. Holding @cfd is all the authority + * it takes over the cell. Filling a slot also takes what using the object that + * goes into it would take. + * + * All of it needs the cell to be in state CELL_BUILDING. + * + * Return: 0, -EBUSY if the cell is past CELL_BUILDING or the slot is taken, + * -EOPNOTSUPP for an unknown @cmd or a reserved key, -EINVAL for an unknown key + * or arguments that do not go with @cmd, or what the slot has to say. + */ +SYSCALL_DEFINE5(cell_configure, int, cfd, unsigned int, cmd, + const char __user *, ukey, const void __user *, uvalue, int, aux) +{ + char *key __free(kfree) = NULL; + char *str __free(kfree) = NULL; + struct cell *cell; + + CLASS(fd, f)(cfd); + cell = cell_from_fd(&f); + if (IS_ERR(cell)) + return PTR_ERR(cell); + + switch (cmd) { + case CELL_SET_FLAG: + if (!ukey || uvalue || aux) + return -EINVAL; + break; + case CELL_SET_STRING: + if (!ukey || !uvalue || aux) + return -EINVAL; + break; + case CELL_SET_BINARY: + if (!ukey || !uvalue || aux <= 0) + return -EINVAL; + break; + case CELL_SET_FD: + if (!ukey || uvalue || aux < 0) + return -EINVAL; + break; + default: + return -EOPNOTSUPP; + } + + if (ukey) { + key = strndup_user(ukey, CELL_KEY_MAX); + if (IS_ERR(key)) + return PTR_ERR(no_free_ptr(key)); + } + if (cmd == CELL_SET_STRING) { + str = strndup_user(uvalue, 256); + if (IS_ERR(str)) + return PTR_ERR(no_free_ptr(str)); + } + + guard(mutex)(&cell->lock); + + if (cell->state != CELL_BUILDING) + return -EBUSY; + + switch (cmd) { + case CELL_SET_FLAG: + /* There are none yet. */ + return -EINVAL; + case CELL_SET_STRING: + return cell_set_string(cell, key, str); + case CELL_SET_BINARY: + return cell_set_binary(cell, key, uvalue, aux); + case CELL_SET_FD: { + CLASS(fd_raw, slot)(aux); + + if (fd_empty(slot)) + return -EBADF; + return cell_set_fd(cell, key, fd_file(slot)); + } + } + return -EOPNOTSUPP; +} -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 27/90] cells: add the "budget" slot 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (25 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 26/90] cells: add cell_configure() Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 28/90] cells: add the "root" slot Josef Bacik ` (62 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik CELL_SET_FD with the key "budget" takes a file descriptor for a cgroup2 directory and makes that cgroup the budget of the cell. Holding the cellfd gives authority over the cell, but not over the things that are put into it. So cgroup_cell_budget_get() checks at this point whether the caller may use the cgroup. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cell/slots.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/kernel/cell/slots.c b/kernel/cell/slots.c index 905672db9936..3f74b4640334 100644 --- a/kernel/cell/slots.c +++ b/kernel/cell/slots.c @@ -31,6 +31,21 @@ static int cell_set_hostname(struct cell *cell, const char *val) return 0; } +/* A cgroup2 directory. Judged against whoever fills the slot, here and now. */ +static int cell_set_budget(struct cell *cell, struct file *file) +{ + struct cgroup *cgrp; + + if (cell->budget) + return -EBUSY; + cgrp = cgroup_cell_budget_get(file, &cell->budget_uid, &cell->budget_gid); + if (IS_ERR(cgrp)) + return PTR_ERR(cgrp); + cell->budget = cgrp; + cell->budget_id = cgroup_id(cgrp); + return 0; +} + static int cell_set_string(struct cell *cell, const char *key, const char *val) { if (!strcmp(key, "hostname")) @@ -46,6 +61,8 @@ static int cell_set_binary(struct cell *cell, const char *key, static int cell_set_fd(struct cell *cell, const char *key, struct file *file) { + if (!strcmp(key, "budget")) + return cell_set_budget(cell, file); return -EINVAL; } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 28/90] cells: add the "root" slot 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (26 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 27/90] cells: add the "budget" slot Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 29/90] cells: add the "ids" slot, for a single id Josef Bacik ` (61 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik CELL_SET_FD with the key "root" takes a file descriptor for a mount namespace and makes it the mount tree of the cell. Holding a file descriptor for a namespace gives no authority over it. So the caller needs CAP_SYS_ADMIN over the user namespace that owns the mount namespace. That is one half of what setns() requires. The other half concerns the namespaces of the caller, which do not change here. Whether the mount namespace is suitable is checked later, when the cell is sealed. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cell/slots.c | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/kernel/cell/slots.c b/kernel/cell/slots.c index 3f74b4640334..b3dfb32b1345 100644 --- a/kernel/cell/slots.c +++ b/kernel/cell/slots.c @@ -46,6 +46,33 @@ static int cell_set_budget(struct cell *cell, struct file *file) return 0; } +static struct ns_common *cell_ns_from_file(struct file *file, int type) +{ + struct ns_common *ns; + + if (!proc_ns_file(file)) + return ERR_PTR(-EINVAL); + ns = get_proc_ns(file_inode(file)); + if (ns->ns_type != type) + return ERR_PTR(-EINVAL); + return ns; +} + +/* A mount namespace that nobody has entered, which sealing sees to. */ +static int cell_set_root(struct cell *cell, struct file *file) +{ + struct ns_common *ns = cell_ns_from_file(file, CLONE_NEWNS); + + if (IS_ERR(ns)) + return PTR_ERR(ns); + if (cell->mnt_ns) + return -EBUSY; + if (!mnt_ns_may_claim(ns)) + return -EPERM; + cell->mnt_ns = mnt_ns_get_from_common(ns); + return 0; +} + static int cell_set_string(struct cell *cell, const char *key, const char *val) { if (!strcmp(key, "hostname")) @@ -63,6 +90,8 @@ static int cell_set_fd(struct cell *cell, const char *key, struct file *file) { if (!strcmp(key, "budget")) return cell_set_budget(cell, file); + if (!strcmp(key, "root")) + return cell_set_root(cell, file); return -EINVAL; } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 29/90] cells: add the "ids" slot, for a single id 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (27 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 28/90] cells: add the "root" slot Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 30/90] cells: lend a cell a block of ids Josef Bacik ` (60 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik CELL_SET_BINARY with the key "ids" sets how many ids the cell has. Start with the value 1. Such a cell runs with the uid and gid of the user who created it, and its tasks see those ids under the same numbers. This is refused for root, because too many checks in the kernel look at the bare value of kuid 0, with or without capabilities. It is also refused for an id of the window, as map_write() refuses to map one into an ordinary user namespace. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cell/Makefile | 1 + kernel/cell/ids.c | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++ kernel/cell/internal.h | 3 +++ kernel/cell/slots.c | 14 ++++++++++++ 4 files changed, 80 insertions(+) diff --git a/kernel/cell/Makefile b/kernel/cell/Makefile index ac48bfb289d0..61ca8e9cb216 100644 --- a/kernel/cell/Makefile +++ b/kernel/cell/Makefile @@ -1,3 +1,4 @@ # SPDX-License-Identifier: GPL-2.0-only obj-y += core.o obj-y += slots.o +obj-y += ids.o diff --git a/kernel/cell/ids.c b/kernel/cell/ids.c new file mode 100644 index 000000000000..aaf7e7ed9826 --- /dev/null +++ b/kernel/cell/ids.c @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Cells: the ids of a cell. + */ +#include <linux/user_namespace.h> + +#include "internal.h" + +/* The id of whoever made the cell, under its own number. */ +static int cell_set_one_id(struct cell *cell) +{ + struct user_namespace *ns = cell->user_ns; + struct uid_gid_extent uids = { + .first = from_kuid(ns->parent, ns->owner), + .lower_first = __kuid_val(ns->owner), + .count = 1, + }; + struct uid_gid_extent gids = { + .first = from_kgid(ns->parent, ns->group), + .lower_first = __kgid_val(ns->group), + .count = 1, + }; + + /* Too much goes by the bare value of kuid 0, capabilities or none. */ + if (uid_eq(ns->owner, GLOBAL_ROOT_UID)) + return -EPERM; + /* Nor an id of the window, as for a legacy user namespace. */ + if (cell_id_in_window(__kuid_val(ns->owner)) || + cell_id_in_window(__kgid_val(ns->group))) + return -EPERM; + return userns_set_cell_map(ns, &uids, &gids, 0, NULL); +} + +/** + * cell_set_ids - fill the "ids" slot + * @cell: a cell that is being built + * @count: how many ids it is to have, 1 or CELL_ID_BLOCK_SIZE + * + * Context: The caller holds the lock of the cell. + * Return: 0, -EBUSY if the slot is taken, -EINVAL for any other @count, -EPERM + * if whoever made the cell cannot have a cell of one id, -ENOSPC if there is no + * block to be had. + */ +int cell_set_ids(struct cell *cell, u32 count) +{ + int ret; + + if (cell->ids_count) + return -EBUSY; + + switch (count) { + case 1: + ret = cell_set_one_id(cell); + break; + default: + return -EINVAL; + } + + if (!ret) + cell->ids_count = count; + return ret; +} diff --git a/kernel/cell/internal.h b/kernel/cell/internal.h index 01a7a8581ff7..c7a1895eadd4 100644 --- a/kernel/cell/internal.h +++ b/kernel/cell/internal.h @@ -59,4 +59,7 @@ static inline struct cell *cell_from_fd(const struct fd *f) return cell_from_file(fd_file(*f)) ?: ERR_PTR(-EINVAL); } +/* ids.c */ +int cell_set_ids(struct cell *cell, u32 count); + #endif /* _KERNEL_CELL_INTERNAL_H */ diff --git a/kernel/cell/slots.c b/kernel/cell/slots.c index b3dfb32b1345..cc732c4e09f9 100644 --- a/kernel/cell/slots.c +++ b/kernel/cell/slots.c @@ -73,6 +73,18 @@ static int cell_set_root(struct cell *cell, struct file *file) return 0; } +static int cell_set_ids_binary(struct cell *cell, const void __user *uvalue, + size_t len) +{ + u32 count; + + if (len != sizeof(count)) + return -EINVAL; + if (copy_from_user(&count, uvalue, sizeof(count))) + return -EFAULT; + return cell_set_ids(cell, count); +} + static int cell_set_string(struct cell *cell, const char *key, const char *val) { if (!strcmp(key, "hostname")) @@ -83,6 +95,8 @@ static int cell_set_string(struct cell *cell, const char *key, const char *val) static int cell_set_binary(struct cell *cell, const char *key, const void __user *uvalue, size_t len) { + if (!strcmp(key, "ids")) + return cell_set_ids_binary(cell, uvalue, len); return -EINVAL; } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 30/90] cells: lend a cell a block of ids 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (28 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 29/90] cells: add the "ids" slot, for a single id Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 31/90] cells: reserve the "spawn" slot Josef Bacik ` (59 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik With the value 65536 for "ids", the tasks of the cell see uids and gids 0 to 65535. These map to a block of the window that the kernel allocates. The builder never names a kuid. An ordinary user namespace needs a privilege check before it may map a range of ids. Here the check is replaced by the guarantee that nobody else holds an id of the block. The allocator enforces that guarantee rather than assuming it: - A block stays allocated for as long as the user namespace that it was lent to exists. - A released block is quarantined. It returns to the pool only once the tables that are indexed by a bare kuid hold nothing in its range. - The same tables are checked again before a block is handed out, because nothing stops a task of the host from running with an id of the window. - Blocks are handed out round robin, so that a block is reused as late as possible. - A quarantined block counts as used. A block whose ids could not be removed from the budget is retired permanently. So a leak shows up as ENOSPC, and never as one cell owning what another cell left behind. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cell/ids.c | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/kernel/cell/ids.c b/kernel/cell/ids.c index aaf7e7ed9826..edf625fa03a1 100644 --- a/kernel/cell/ids.c +++ b/kernel/cell/ids.c @@ -2,10 +2,137 @@ /* * Cells: the ids of a cell. */ +#include <linux/bitmap.h> +#include <linux/key.h> +#include <linux/sched/user.h> +#include <linux/spinlock.h> #include <linux/user_namespace.h> #include "internal.h" +/* + * The kuid block allocator. + * + * "Nobody else holds an id of this block" is what stands in for the privilege + * check a legacy user namespace needs before it may map a range. So it is + * enforced rather than assumed: + * + * - a block lives as long as the user namespace object it was given to, which + * every credential, and every object owned by the namespace, pins; + * - no create, chown or ACL stores an id of the window in a filesystem the cell + * does not own, see kuid_storable(); + * - a legacy user namespace cannot map into the window, see map_write(); + * - an idmapping of a mount that was made for the cell pins the namespace too, + * see alloc_mnt_idmap(); + * - a released block is quarantined, and goes back to the pool only once the + * tables that are keyed by bare kuid hold nothing in its range. The same is + * asked before a block is handed out. + * + * Blocks are handed out round robin so that reuse comes as late as it can. A + * quarantined block counts as used: a leak shows up as -ENOSPC, never as one + * tenant owning what another left behind. + */ +static DEFINE_SPINLOCK(cell_block_lock); +static DECLARE_BITMAP(cell_block_used, CELL_ID_NR_BLOCKS); +static DECLARE_BITMAP(cell_block_quarantine, CELL_ID_NR_BLOCKS); +static unsigned int cell_block_cursor; + +static void cell_block_sweep(struct work_struct *work); +static DECLARE_DELAYED_WORK(cell_block_sweep_work, cell_block_sweep); + +static inline u32 cell_block_first(u32 block) +{ + return CELL_ID_WINDOW_FIRST + block * CELL_ID_BLOCK_SIZE; +} + +static int cell_block_take(void) +{ + unsigned int block; + + guard(spinlock)(&cell_block_lock); + + block = find_next_zero_bit(cell_block_used, CELL_ID_NR_BLOCKS, + cell_block_cursor); + if (block >= CELL_ID_NR_BLOCKS) + block = find_first_zero_bit(cell_block_used, CELL_ID_NR_BLOCKS); + if (block >= CELL_ID_NR_BLOCKS) + return -ENOSPC; + + __set_bit(block, cell_block_used); + cell_block_cursor = (block + 1) % CELL_ID_NR_BLOCKS; + return block; +} + +/* Does anything that is keyed by a bare kuid still name an id of @block? */ +static bool cell_block_busy(u32 block) +{ + kuid_t first = KUIDT_INIT(cell_block_first(block)); + kuid_t last = KUIDT_INIT(cell_block_first(block) + CELL_ID_BLOCK_SIZE - 1); + + return uid_range_in_use(first, last) || key_user_range_in_use(first, last); +} + +/* Returns a block number, or a negative error. */ +static int cell_block_alloc(void) +{ + int block, tries; + + for (tries = 0; tries < 64; tries++) { + block = cell_block_take(); + if (block < 0 || !cell_block_busy(block)) + return block; + /* + * Nothing keeps a task of the host from running on an id of the + * window. A block it does that in is not for lending. + */ + cell_block_release(block); + } + return -ENOSPC; +} + +static void cell_block_sweep(struct work_struct *work) +{ + bool again = false; + unsigned int block; + + for_each_set_bit(block, cell_block_quarantine, CELL_ID_NR_BLOCKS) { + if (cell_block_busy(block)) { + again = true; + continue; + } + guard(spinlock)(&cell_block_lock); + __clear_bit(block, cell_block_quarantine); + __clear_bit(block, cell_block_used); + } + + if (again) + schedule_delayed_work(&cell_block_sweep_work, 10 * HZ); +} + +/** + * cell_block_release - the user namespace that was lent a block is gone + * @block: the number of the block + * + * The block goes into quarantine. It returns to the pool once the sweep finds + * nothing left that names one of its ids. + * + * Context: Process context. + */ +void cell_block_release(u32 block) +{ + scoped_guard(spinlock, &cell_block_lock) + __set_bit(block, cell_block_quarantine); + schedule_delayed_work(&cell_block_sweep_work, 0); +} + +static int __init cell_init(void) +{ + /* No one user gets to use up the window. */ + init_user_ns.ucount_max[UCOUNT_CELL_ID_BLOCKS] = CELL_ID_NR_BLOCKS / 8; + return 0; +} +late_initcall(cell_init); + /* The id of whoever made the cell, under its own number. */ static int cell_set_one_id(struct cell *cell) { @@ -31,6 +158,32 @@ static int cell_set_one_id(struct cell *cell) return userns_set_cell_map(ns, &uids, &gids, 0, NULL); } +/* A block gives access to nothing, but there are only so many. */ +static int cell_set_id_block(struct cell *cell) +{ + struct user_namespace *ns = cell->user_ns; + struct uid_gid_extent ids = { .count = CELL_ID_BLOCK_SIZE }; + struct ucounts *ucounts; + int block, ret; + + ucounts = inc_ucount(ns->parent, ns->owner, UCOUNT_CELL_ID_BLOCKS); + if (!ucounts) + return -ENOSPC; + block = cell_block_alloc(); + if (block < 0) { + dec_ucount(ucounts, UCOUNT_CELL_ID_BLOCKS); + return block; + } + /* From 0 up on the inside, uids and gids alike. */ + ids.lower_first = cell_block_first(block); + ret = userns_set_cell_map(ns, &ids, &ids, block + 1, ucounts); + if (ret) { + cell_block_release(block); + dec_ucount(ucounts, UCOUNT_CELL_ID_BLOCKS); + } + return ret; +} + /** * cell_set_ids - fill the "ids" slot * @cell: a cell that is being built @@ -52,6 +205,9 @@ int cell_set_ids(struct cell *cell, u32 count) case 1: ret = cell_set_one_id(cell); break; + case CELL_ID_BLOCK_SIZE: + ret = cell_set_id_block(cell); + break; default: return -EINVAL; } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 31/90] cells: reserve the "spawn" slot 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (29 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 30/90] cells: lend a cell a block of ids Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 32/90] cells: add CELL_CMD_SEAL Josef Bacik ` (58 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A task of a cell can neither create a cell nor start a task in one. So far that looks like a side effect of the fact that no cellfd ever gets into a cell. Make it an explicit rule that does not depend on that. The rule fits the model: a cell can do what its slots hold, and the right to spawn is a slot like any other. cell_may_spawn() checks it where a cell is created and where a task is started in one. There is nothing to put into the slot yet, so it is always empty for a cell, and a task that is in no cell may always spawn. This patch only reserves the name. Nested cells can later be enabled by filling the slot. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cell/slots.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kernel/cell/slots.c b/kernel/cell/slots.c index cc732c4e09f9..6ce8892f7908 100644 --- a/kernel/cell/slots.c +++ b/kernel/cell/slots.c @@ -165,6 +165,9 @@ SYSCALL_DEFINE5(cell_configure, int, cfd, unsigned int, cmd, if (IS_ERR(key)) return PTR_ERR(no_free_ptr(key)); } + /* Reserved. See cell_may_spawn(). */ + if (key && !strcmp(key, "spawn")) + return -EOPNOTSUPP; if (cmd == CELL_SET_STRING) { str = strndup_user(uvalue, 256); if (IS_ERR(str)) -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 32/90] cells: add CELL_CMD_SEAL 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (30 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 31/90] cells: reserve the "spawn" slot Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 33/90] cells: add cell_open() Josef Bacik ` (57 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Sealing turns the contents of the slots into a cell that can run tasks. It validates everything or fails. All the steps that can fail come before the mount namespace is sealed, because that step changes the namespace in place and cannot be undone. If the seal fails, the cell stays in the BUILDING state, and keeps whatever the seal had filled into empty slots by then. A cell must have a root and a budget. For the rest there are defaults: - If no ids were requested, the cell gets a single id. - It gets a new network namespace that nobody has set up. The namespace belongs to the user namespace that the cell was created in. - Its pid, ipc, uts and cgroup namespaces are always new. The cgroup namespace is rooted at the budget. In a cell with a block of ids, ownership of the budget is given to the root user of the cell. Once sealed, the cell cannot be changed any more. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cell/slots.c | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/kernel/cell/slots.c b/kernel/cell/slots.c index 6ce8892f7908..81f43744e858 100644 --- a/kernel/cell/slots.c +++ b/kernel/cell/slots.c @@ -73,6 +73,26 @@ static int cell_set_root(struct cell *cell, struct file *file) return 0; } +/* + * The network is set up from the outside and handed to the cell, as the tree is. + * So a network namespace that is made for a cell belongs to the user namespace + * the cell was made in, as its mount namespace does, and no capability of a task + * of the cell counts for it, see cell_set_net_rights(). Nothing is done to it here: + * until somebody sets it up there is a loopback device that is down. + */ +static int cell_make_net(struct cell *cell) +{ + struct net *net; + + if (cell->net) + return 0; + net = copy_net_ns(CLONE_NEWNET, cell->user_ns->parent, current->nsproxy->net_ns); + if (IS_ERR(net)) + return PTR_ERR(net); + cell->net = net; + return 0; +} + static int cell_set_ids_binary(struct cell *cell, const void __user *uvalue, size_t len) { @@ -109,6 +129,85 @@ static int cell_set_fd(struct cell *cell, const char *key, struct file *file) return -EINVAL; } +static int cell_make_pid_ns(struct cell *cell) +{ + struct pid_namespace *ns; + + if (cell->pid_ns) + return 0; + ns = copy_pid_ns(CLONE_NEWPID, cell->user_ns, task_active_pid_ns(current)); + if (IS_ERR(ns)) + return PTR_ERR(ns); + cell->pid_ns = ns; + return 0; +} + +/* + * Everything that can fail comes before the mount namespace, which is changed + * in place and cannot be changed back. If it fails the cell is still BUILDING. + * What was filled in for slots that were left empty stays filled in. + */ +static int cell_seal(struct cell *cell) +{ + struct cgroup_namespace *cgroup_ns; + struct nsproxy *nsproxy; + kuid_t root_uid; + kgid_t root_gid; + int ret; + + if (!cell->budget || !cell->mnt_ns) + return -EINVAL; + + if (!cell->ids_count) { + ret = cell_set_ids(cell, 1); + if (ret) + return ret; + } + ret = cell_make_pid_ns(cell); + if (ret) + return ret; + ret = cell_make_net(cell); + if (ret) + return ret; + cgroup_ns = cgroup_cell_ns(cell->user_ns, cell->budget); + if (IS_ERR(cgroup_ns)) + return PTR_ERR(cgroup_ns); + + nsproxy = cell_create_nsproxy(cell->user_ns, cell->mnt_ns, cell->pid_ns, + cell->net, cgroup_ns, cell->hostname); + put_cgroup_ns(cgroup_ns); + if (IS_ERR(nsproxy)) + return PTR_ERR(nsproxy); + + /* The builder cannot name the cell's ids, so it cannot do this. */ + if (cell->ids_count == 1) { + root_uid = cell->user_ns->owner; + root_gid = cell->user_ns->group; + } else { + root_uid = make_kuid(cell->user_ns, 0); + root_gid = make_kgid(cell->user_ns, 0); + } + ret = cgroup_cell_chown(cell->budget, root_uid, root_gid); + if (ret) + goto out_chown; + + /* Two references: the slot's and the nsproxy's. */ + ret = mnt_ns_seal_for_cell(cell->mnt_ns, 2, cell->user_ns, + cell->ids_count > 1, &cell->root); + if (ret) + goto out_chown; + + cell->nsproxy = nsproxy; + cell->sealed = true; + WRITE_ONCE(cell->state, CELL_SEALED); + return 0; + +out_chown: + cgroup_cell_chown(cell->budget, cell->budget_uid, cell->budget_gid); + put_nsproxy(nsproxy); + return ret; +} + /** * sys_cell_configure - change a cell * @cfd: a cellfd @@ -156,6 +255,10 @@ SYSCALL_DEFINE5(cell_configure, int, cfd, unsigned int, cmd, if (!ukey || uvalue || aux < 0) return -EINVAL; break; + case CELL_CMD_SEAL: + if (ukey || uvalue || aux) + return -EINVAL; + break; default: return -EOPNOTSUPP; } @@ -194,6 +297,8 @@ SYSCALL_DEFINE5(cell_configure, int, cfd, unsigned int, cmd, return -EBADF; return cell_set_fd(cell, key, fd_file(slot)); } + case CELL_CMD_SEAL: + return cell_seal(cell); } return -EOPNOTSUPP; } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 33/90] cells: add cell_open() 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (31 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 32/90] cells: add CELL_CMD_SEAL Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 34/90] pid: allocate a struct pid ahead of its numbers Josef Bacik ` (56 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Add the system call that returns another file descriptor from a cell. For now it returns an nsfs file descriptor for the network or the pid namespace of a cell that is still being built. The builder needs these to move a network device into the cell and to mount a procfs for it, as it would for any container. The namespace is created if the cell does not have one yet. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/uapi/linux/cell.h | 5 ++++ kernel/cell/slots.c | 61 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/include/uapi/linux/cell.h b/include/uapi/linux/cell.h index 22337cab3da1..3c10c9179289 100644 --- a/include/uapi/linux/cell.h +++ b/include/uapi/linux/cell.h @@ -30,4 +30,9 @@ enum cell_state { CELL_DEAD = 4, /* Nothing left. The status can be read */ }; +/* cell_open(): @what. */ +enum cell_open_what { + CELL_OPEN_SLOT = 0, /* An fd on the object in the slot named by @key */ +}; + #endif /* _UAPI_LINUX_CELL_H */ diff --git a/kernel/cell/slots.c b/kernel/cell/slots.c index 81f43744e858..0a406422ce34 100644 --- a/kernel/cell/slots.c +++ b/kernel/cell/slots.c @@ -302,3 +302,64 @@ SYSCALL_DEFINE5(cell_configure, int, cfd, unsigned int, cmd, } return -EOPNOTSUPP; } + +/** + * sys_cell_open - get another file descriptor from a cell + * @cfd: a cellfd + * @what: one of enum cell_open_what + * @ukey: for CELL_OPEN_SLOT the name of the slot, "net" or "pids" + * @arg: nothing yet, must be NULL + * @flags: none yet, must be 0 + * + * For now only this: an nsfs file descriptor on the network or pid namespace of + * a cell that is still being built, so that the builder can move a device in or + * mount a procfs for it, the way it would for any container. The namespace is + * made if the cell does not have it yet. + * + * Return: a file descriptor, which is close-on-exec, or a negative error. + */ +SYSCALL_DEFINE5(cell_open, int, cfd, unsigned int, what, + const char __user *, ukey, const void __user *, arg, + unsigned int, flags) +{ + char *key __free(kfree) = NULL; + struct ns_common *ns; + struct cell *cell; + int ret; + + if (what != CELL_OPEN_SLOT) + return -EOPNOTSUPP; + if (!ukey || arg || flags) + return -EINVAL; + + CLASS(fd, f)(cfd); + cell = cell_from_fd(&f); + if (IS_ERR(cell)) + return PTR_ERR(cell); + + key = strndup_user(ukey, CELL_KEY_MAX); + if (IS_ERR(key)) + return PTR_ERR(no_free_ptr(key)); + + scoped_guard(mutex, &cell->lock) { + if (cell->state != CELL_BUILDING) + return -EBUSY; + + if (!strcmp(key, "net")) { + ret = cell_make_net(cell); + if (ret) + return ret; + ns = &get_net(cell->net)->ns; + } else if (!strcmp(key, "pids")) { + ret = cell_make_pid_ns(cell); + if (ret) + return ret; + ns = &get_pid_ns(cell->pid_ns)->ns; + } else { + return -EINVAL; + } + } + + /* Consumes the reference, whatever happens. */ + return ns_open_namespace(ns); +} -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 34/90] pid: allocate a struct pid ahead of its numbers 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (32 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 33/90] cells: add cell_open() Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 35/90] fork: let copy_process() place a task in a cell Josef Bacik ` (55 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik alloc_pid() does two things. It allocates and initialises a struct pid, and it assigns the pid a number in its pid namespace and in every ancestor of that namespace. Make the first step available on its own: - alloc_pid_struct() returns a struct pid that can be referenced, waited on and added to pidfs. It has no task and no numbers. So nothing can find it, it uses up no pid, and zap_pid_ns_processes() does not count it. - alloc_pid() can take such a struct pid and assign its numbers. - kernel_clone_args.pid passes one to copy_process(). - free_pid_struct() frees one that never got its numbers. This makes it possible to create a pidfd before there is a task. No functional change. Nothing passes a struct pid to alloc_pid() yet. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/pid.h | 13 ++++++- include/linux/sched/task.h | 1 + kernel/fork.c | 2 +- kernel/pid.c | 93 ++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 91 insertions(+), 18 deletions(-) diff --git a/include/linux/pid.h b/include/linux/pid.h index 185bf0cde2d1..d590baf155d7 100644 --- a/include/linux/pid.h +++ b/include/linux/pid.h @@ -183,8 +183,10 @@ extern struct pid *find_vpid(int nr); extern struct pid *find_get_pid(int nr); extern struct pid *find_ge_pid(int nr, struct pid_namespace *); +struct pid *alloc_pid_struct(struct pid_namespace *ns); +void free_pid_struct(struct pid *pid); extern struct pid *alloc_pid(struct pid_namespace *ns, pid_t *set_tid, - size_t set_tid_size); + size_t set_tid_size, struct pid *pid); extern void free_pid(struct pid *pid); void free_pids(struct pid **pids); extern void disable_pid_allocation(struct pid_namespace *ns); @@ -207,6 +209,15 @@ static inline struct pid_namespace *ns_of_pid(struct pid *pid) return ns; } +/* + * Has alloc_pid() given it its numbers? It keeps them for good, free_pid() only + * gives them back to the pid namespaces. + */ +static inline bool pid_has_numbers(const struct pid *pid) +{ + return pid->numbers[pid->level].nr; +} + /* * is_child_reaper returns true if the pid is the init process * of the current namespace. As this one could be checked before diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h index 90ed5bc3c7af..aca22f08c8f0 100644 --- a/include/linux/sched/task.h +++ b/include/linux/sched/task.h @@ -45,6 +45,7 @@ struct kernel_clone_args { struct cgroup *cgrp; struct css_set *cset; unsigned int kill_seq; + struct pid *pid; }; /* diff --git a/kernel/fork.c b/kernel/fork.c index 8b108ed37152..7937052def66 100644 --- a/kernel/fork.c +++ b/kernel/fork.c @@ -2334,7 +2334,7 @@ __latent_entropy struct task_struct *copy_process( if (pid != &init_struct_pid) { pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid, - args->set_tid_size); + args->set_tid_size, args->pid); if (IS_ERR(pid)) { retval = PTR_ERR(pid); goto bad_fork_cleanup_thread; diff --git a/kernel/pid.c b/kernel/pid.c index adf721606768..0611406e9a9a 100644 --- a/kernel/pid.c +++ b/kernel/pid.c @@ -156,13 +156,75 @@ void free_pids(struct pid **pids) free_pid(pids[tmp]); } +/** + * alloc_pid_struct - allocate a struct pid that has no numbers yet + * @ns: the pid namespace it is going to get its numbers in + * + * What comes back can be referred to, waited on and added to pidfs. It + * has no task and no number in any pid namespace, so nothing finds it and it + * keeps no pid namespace from dying. Pass it to alloc_pid() for the numbers. + * + * Return: the struct pid, to be passed to alloc_pid() or free_pid_struct(), or + * NULL. + */ +struct pid *alloc_pid_struct(struct pid_namespace *ns) +{ + struct pid_namespace *tmp; + enum pid_type type; + struct pid *pid; + int i; + + pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL); + if (!pid) + return NULL; + + get_pid_ns(ns); + pid->level = ns->level; + refcount_set(&pid->count, 1); + spin_lock_init(&pid->lock); + for (type = 0; type < PIDTYPE_MAX; ++type) + INIT_HLIST_HEAD(&pid->tasks[type]); + init_waitqueue_head(&pid->wait_pidfd); + INIT_HLIST_HEAD(&pid->inodes); + pidfs_prepare_pid(pid); + for (tmp = ns, i = ns->level; i >= 0; tmp = tmp->parent, i--) { + pid->numbers[i].nr = 0; + pid->numbers[i].ns = tmp; + } + return pid; +} + +/** + * free_pid_struct - free a struct pid that never got its numbers + * @pid: what alloc_pid_struct() returned, and no alloc_pid() succeeded on + * + * Drops the reference that alloc_pid_struct() returned. + */ +void free_pid_struct(struct pid *pid) +{ + WARN_ON_ONCE(pid_has_numbers(pid)); + pidfs_remove_pid(pid); + call_rcu(&pid->rcu, delayed_put_pid); +} + +/** + * alloc_pid - allocate a struct pid with a number in @ns and all its ancestors + * @ns: the pid namespace + * @arg_set_tid: the numbers to use, the most nested pid namespace first + * @arg_set_tid_size: how many of them there are + * @pid: what alloc_pid_struct() returned for @ns, or NULL + * + * A @pid that is passed in has been added to pidfs already. If this fails it is + * as it was and still the caller's. + * + * Return: the struct pid, to be passed to free_pid(), or an ERR_PTR(). + */ struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid, - size_t arg_set_tid_size) + size_t arg_set_tid_size, struct pid *pid) { int set_tid[MAX_PID_NS_LEVEL + 1] = {}; int pid_max[MAX_PID_NS_LEVEL + 1] = {}; - struct pid *pid; - enum pid_type type; + const bool prepared = pid; int i, nr; struct pid_namespace *tmp; struct upid *upid; @@ -179,26 +241,19 @@ struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid, */ if (arg_set_tid_size > ns->level + 1) return ERR_PTR(-EINVAL); + if (prepared && (ns_of_pid(pid) != ns || pid_has_numbers(pid) || !pid->ino)) + return ERR_PTR(-EINVAL); /* * Prep before we take locks: * * 1. allocate and fill in pid struct */ - pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL); + if (!prepared) + pid = alloc_pid_struct(ns); if (!pid) return ERR_PTR(retval); - get_pid_ns(ns); - pid->level = ns->level; - refcount_set(&pid->count, 1); - spin_lock_init(&pid->lock); - for (type = 0; type < PIDTYPE_MAX; ++type) - INIT_HLIST_HEAD(&pid->tasks[type]); - init_waitqueue_head(&pid->wait_pidfd); - INIT_HLIST_HEAD(&pid->inodes); - pidfs_prepare_pid(pid); - /* * 2. perm check checkpoint_restore_ns_capable() * @@ -337,6 +392,9 @@ struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid, idr_preload_end(); ns_ref_active_get(ns); + if (prepared) + return pid; + retval = pidfs_add_pid(pid); if (unlikely(retval)) { free_pid(pid); @@ -349,14 +407,17 @@ struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid, while (++i <= ns->level) { upid = pid->numbers + i; idr_remove(&upid->ns->idr, upid->nr); + upid->nr = 0; } spin_unlock(&pidmap_lock); idr_preload_end(); out_abort: - put_pid_ns(ns); - kmem_cache_free(ns->pid_cachep, pid); + if (!prepared) { + put_pid_ns(ns); + kmem_cache_free(ns->pid_cachep, pid); + } return ERR_PTR(retval); } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 35/90] fork: let copy_process() place a task in a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (33 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 34/90] pid: allocate a struct pid ahead of its numbers Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 36/90] cgroup: fork a task of a cell into the budget Josef Bacik ` (54 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A task is born in a cell. Add kernel_clone_args.cell, and teach the helpers of copy_process() what to do when it is set: - copy_creds() creates credentials that belong to the cell. Everything that identifies the spawner is replaced: ids, groups, capabilities, keyrings and the user namespace. The LSM state is kept, so that a spawner cannot escape its confinement by spawning. - copy_namespaces() uses the nsproxy of the cell. - copy_fs() starts at the root of the mount tree of the cell. - copy_exec_state() creates a new one that is owned by the user namespace of the cell. The task starts as uid 0 of the cell. In a cell with a single id it starts with that id, and with the supplementary groups of the user who created the cell. The groups are kept because a group can be used to deny access. Neither identity comes with any privilege. The permitted, effective and bounding sets of the task are the capabilities in the "caps" slot, which apply to the user namespace of the cell. Add cell_fork_prepare() and cell_fork_finish(), which hold cell->lock across copy_process(). A kill therefore either happens first, and the spawn fails, or it waits for the lock and then finds the new task in the budget. Nothing sets kernel_clone_args.cell yet. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cell.h | 18 ++++ include/linux/cred.h | 3 +- include/linux/nsproxy.h | 2 +- include/linux/sched/task.h | 2 + include/linux/user_namespace.h | 1 + kernel/cell/Makefile | 1 + kernel/cell/spawn.c | 211 +++++++++++++++++++++++++++++++++++++++++ kernel/cred.c | 21 +++- kernel/fork.c | 39 ++++++-- kernel/nsproxy.c | 23 ++++- kernel/user_namespace.c | 16 ++++ 11 files changed, 321 insertions(+), 16 deletions(-) diff --git a/include/linux/cell.h b/include/linux/cell.h index 75dc59518c91..564c3e86f3bd 100644 --- a/include/linux/cell.h +++ b/include/linux/cell.h @@ -175,4 +175,22 @@ struct cell *cell_from_file(const struct file *file); struct dentry **cell_stashed(struct cell *cell); #endif +/* A task is born in a cell. */ +#ifdef CONFIG_CELLS +int cell_fork_prepare(struct cell *cell); +void cell_fork_finish(struct cell *cell); +void cell_task_spawned(struct cell *cell); +int cell_copy_creds(struct cred *new, struct cell *cell); +struct user_namespace *cell_user_ns(struct cell *cell); +struct nsproxy *cell_nsproxy(struct cell *cell); +void cell_root(struct cell *cell, struct path *root); +struct cgroup *cell_budget(struct cell *cell); +#else +static inline int cell_copy_creds(struct cred *n, struct cell *c) { return -EINVAL; } +static inline struct user_namespace *cell_user_ns(struct cell *cell) { return NULL; } +static inline struct nsproxy *cell_nsproxy(struct cell *cell) { return NULL; } +static inline void cell_root(struct cell *cell, struct path *root) { } +static inline struct cgroup *cell_budget(struct cell *cell) { return NULL; } +#endif + #endif /* _LINUX_CELL_H */ diff --git a/include/linux/cred.h b/include/linux/cred.h index 7fb1663a5fc6..cabde3c1ba31 100644 --- a/include/linux/cred.h +++ b/include/linux/cred.h @@ -152,7 +152,8 @@ struct cred { extern void __put_cred(struct cred *); extern void exit_creds(struct task_struct *); -extern int copy_creds(struct task_struct *, u64); +struct cell; +int copy_creds(struct task_struct *p, u64 clone_flags, struct cell *cell); extern const struct cred *get_task_cred(struct task_struct *); extern struct cred *cred_alloc_blank(void); extern struct cred *prepare_creds(void); diff --git a/include/linux/nsproxy.h b/include/linux/nsproxy.h index 02101d96e4dd..baa7c1acafde 100644 --- a/include/linux/nsproxy.h +++ b/include/linux/nsproxy.h @@ -95,7 +95,7 @@ static inline struct cred *nsset_cred(struct nsset *set) * */ -int copy_namespaces(u64 flags, struct task_struct *tsk); +int copy_namespaces(u64 flags, struct task_struct *tsk, struct cell *cell); void switch_cred_namespaces(const struct cred *old, const struct cred *new); void exit_nsproxy_namespaces(struct task_struct *tsk); void get_cred_namespaces(struct task_struct *tsk); diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h index aca22f08c8f0..eaf939ac4bef 100644 --- a/include/linux/sched/task.h +++ b/include/linux/sched/task.h @@ -20,6 +20,7 @@ struct css_set; /* All the bits taken by the old clone syscall. */ #define CLONE_LEGACY_FLAGS 0xffffffffULL +struct cell; struct kernel_clone_args { u64 flags; int __user *pidfd; @@ -46,6 +47,7 @@ struct kernel_clone_args { struct css_set *cset; unsigned int kill_seq; struct pid *pid; + struct cell *cell; }; /* diff --git a/include/linux/user_namespace.h b/include/linux/user_namespace.h index 4a48ead45820..59a081b9aaf6 100644 --- a/include/linux/user_namespace.h +++ b/include/linux/user_namespace.h @@ -199,6 +199,7 @@ int userns_set_cell_map(struct user_namespace *ns, const struct uid_gid_extent *uids, const struct uid_gid_extent *gids, u32 block, struct ucounts *ucounts); +void userns_enter_cell(struct cred *cred, struct user_namespace *ns); void userns_retire_cell_block(struct user_namespace *ns); #endif extern int unshare_userns(unsigned long unshare_flags, struct cred **new_cred); diff --git a/kernel/cell/Makefile b/kernel/cell/Makefile index 61ca8e9cb216..1a2dcd469d10 100644 --- a/kernel/cell/Makefile +++ b/kernel/cell/Makefile @@ -2,3 +2,4 @@ obj-y += core.o obj-y += slots.o obj-y += ids.o +obj-y += spawn.o diff --git a/kernel/cell/spawn.c b/kernel/cell/spawn.c new file mode 100644 index 000000000000..70073eff9005 --- /dev/null +++ b/kernel/cell/spawn.c @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Cells: a task is born in a cell. + */ +#include <linux/cgroup.h> +#include <linux/fs_struct.h> +#include <linux/key.h> +#include <linux/pid_namespace.h> +#include <linux/sched/task.h> +#include <linux/security.h> +#include <uapi/linux/cell.h> +#include <uapi/linux/keyctl.h> + +#include "internal.h" + +/** + * cell_fork_prepare - get ready to create a task in a cell + * @cell: the cell + * + * On success the cell is locked, until cell_fork_finish(). A kill therefore + * either comes first, and this fails, or waits and finds the new task in the + * budget. + * + * The pid namespace of the caller must be an ancestor of that of the cell: a + * parent can see its children. + * + * Return: 0, -EPERM if the caller is in a cell, whose "spawn" slot is empty, + * -EINVAL if the cell is not sealed or out of the caller's sight, + * -ESRCH if it is dying or dead, -EINTR on a fatal signal. + */ +int cell_fork_prepare(struct cell *cell) +{ + int ret; + + if (!cell_may_spawn()) + return -EPERM; + + if (mutex_lock_killable(&cell->lock)) + return -EINTR; + + ret = cell->state == CELL_BUILDING ? -EINVAL : -ESRCH; + if (cell->state != CELL_SEALED && cell->state != CELL_RUNNING) + goto err; + /* The caller must be able to see what it spawns. */ + ret = -EINVAL; + if (!pidns_is_ancestor(cell->pid_ns, task_active_pid_ns(current))) + goto err; + + return 0; +err: + mutex_unlock(&cell->lock); + return ret; +} + +/** + * cell_fork_finish - done creating a task in a cell, one way or the other + * @cell: what cell_fork_prepare() succeeded on + * + * Unlocks the cell. + */ +void cell_fork_finish(struct cell *cell) +{ + mutex_unlock(&cell->lock); +} + +/** + * cell_task_spawned - a task was created in a cell + * @cell: the cell + * + * Context: Between cell_fork_prepare() and cell_fork_finish(), once + * copy_process() has succeeded. + */ +void cell_task_spawned(struct cell *cell) +{ + lockdep_assert_held(&cell->lock); + + if (cell->state == CELL_SEALED) + WRITE_ONCE(cell->state, CELL_RUNNING); +} + +/** + * cell_user_ns - the user namespace of a cell + * @cell: a cell that has not been torn down + * + * Context: The caller holds the lock of the cell, as between + * cell_fork_prepare() and cell_fork_finish(). + * Return: the user namespace. No reference is taken. + */ +struct user_namespace *cell_user_ns(struct cell *cell) +{ + return cell->user_ns; +} + +/** + * cell_nsproxy - the nsproxy every task of a cell shares + * @cell: a sealed cell that has not been torn down + * + * Context: Between cell_fork_prepare() and cell_fork_finish(). + * Return: the nsproxy. No reference is taken. + */ +struct nsproxy *cell_nsproxy(struct cell *cell) +{ + return cell->nsproxy; +} + +/** + * cell_root - the root of the tree of a cell + * @cell: a sealed cell that has not been torn down + * @root: where to return it, with a reference + * + * Context: Between cell_fork_prepare() and cell_fork_finish(). + */ +void cell_root(struct cell *cell, struct path *root) +{ + *root = cell->root; + path_get(root); +} + +/** + * cell_budget - the cgroup that is the budget of a cell + * @cell: a sealed cell that has not been torn down + * + * Context: Between cell_fork_prepare() and cell_fork_finish(). + * Return: the cgroup. No reference is taken. + */ +struct cgroup *cell_budget(struct cell *cell) +{ + return cell->budget; +} + +/** + * cell_copy_creds - make the credentials of a new task of a cell + * @new: a copy of the credentials of the spawner that nobody else can see yet + * @cell: the cell + * + * Nothing that says who the spawner is survives: ids, groups, capabilities, + * keyrings and user namespace become those of the cell. The LSM state does + * survive, since a spawner must not shed a confinement by spawning. + * + * The task starts as the root of the cell, or in a one-id cell as the one id, with + * the groups of whoever made the cell. Neither comes with anything: it has the + * capabilities in the "caps" slot, over the user namespace of the cell, and that + * is its bounding set too. In a one-id cell the slot is empty. + * + * Context: Between cell_fork_prepare() and cell_fork_finish(). + * Return: 0 or a negative error, in which case the caller discards @new. + */ +int cell_copy_creds(struct cred *new, struct cell *cell) +{ + struct user_namespace *ns = cell->user_ns; + struct group_info *groups; + struct user_struct *user; + kuid_t uid; + kgid_t gid; + + if (cell->ids_count == 1) { + uid = ns->owner; + gid = ns->group; + } else { + uid = make_kuid(ns, 0); + gid = make_kgid(ns, 0); + } + if (WARN_ON_ONCE(!uid_valid(uid) || !gid_valid(gid))) + return -EINVAL; + + /* + * A one-id cell is its maker as far as files go, and that includes the + * groups that are there to deny it something. It cannot change them. + */ + if (cell->ids_count == 1) + groups = get_group_info(cell->groups); + else + groups = groups_alloc(0); + if (!groups) + return -ENOMEM; + user = alloc_uid(uid); + if (!user) { + put_group_info(groups); + return -EAGAIN; + } + free_uid(new->user); + new->user = user; + + new->uid = new->euid = new->suid = new->fsuid = uid; + new->gid = new->egid = new->sgid = new->fsgid = gid; + put_group_info(new->group_info); + new->group_info = groups; + + userns_enter_cell(new, ns); + + /* + * What the builder named, for good. The task gets no_new_privs, so it + * gains nothing from a file, or from being uid 0 when it runs one, and + * it cannot make a user namespace to have a full set in. + */ + new->cap_bset = cell->caps; + new->cap_permitted = cell->caps; + new->cap_effective = cell->caps; + +#ifdef CONFIG_KEYS + key_put(new->session_keyring); + new->session_keyring = NULL; + key_put(new->process_keyring); + new->process_keyring = NULL; + key_put(new->thread_keyring); + new->thread_keyring = NULL; + new->jit_keyring = KEY_REQKEY_DEFL_THREAD_KEYRING; +#endif + + return set_cred_ucounts(new); +} diff --git a/kernel/cred.c b/kernel/cred.c index 5e10e0bdd11e..3aa1b1e3eade 100644 --- a/kernel/cred.c +++ b/kernel/cred.c @@ -8,6 +8,7 @@ #define pr_fmt(fmt) "CRED: " fmt #include <linux/export.h> +#include <linux/cell.h> #include <linux/cred.h> #include <linux/slab.h> #include <linux/sched.h> @@ -252,16 +253,22 @@ struct cred *prepare_exec_creds(void) return new; } -/* - * Copy credentials for the new process created by fork() +/** + * copy_creds - copy credentials for the new process created by fork() + * @p: the new task + * @clone_flags: the flags of the clone + * @cell: the cell @p is born in, or NULL if it stays where its parent is * * We share if we can, but under some circumstances we have to generate a new * set. * * The new process gets the current process's subjective credentials as its - * objective and subjective credentials + * objective and subjective credentials. One that is born in a cell gets + * credentials of that cell instead, see cell_copy_creds(). + * + * Return: 0 or a negative error. */ -int copy_creds(struct task_struct *p, u64 clone_flags) +int copy_creds(struct task_struct *p, u64 clone_flags, struct cell *cell) { struct cred *new; int ret; @@ -297,6 +304,12 @@ int copy_creds(struct task_struct *p, u64 clone_flags) goto error_put; } + if (cell) { + ret = cell_copy_creds(new, cell); + if (ret < 0) + goto error_put; + } + #ifdef CONFIG_KEYS /* new threads get their own thread keyrings if their parent already * had one */ diff --git a/kernel/fork.c b/kernel/fork.c index 7937052def66..4db060281448 100644 --- a/kernel/fork.c +++ b/kernel/fork.c @@ -14,6 +14,7 @@ #include <linux/anon_inodes.h> #include <linux/slab.h> +#include <linux/cell.h> #include <linux/sched/autogroup.h> #include <linux/sched/mm.h> #include <linux/sched/user.h> @@ -1602,10 +1603,20 @@ static int copy_mm(u64 clone_flags, struct task_struct *tsk) return 0; } -static int copy_exec_state(u64 clone_flags, struct task_struct *tsk) +static int copy_exec_state(u64 clone_flags, struct task_struct *tsk, + struct cell *cell) { struct task_exec_state *exec_state; + /* Born in a cell: nothing of the parent's, and not dumpable. */ + if (cell) { + exec_state = alloc_task_exec_state(cell_user_ns(cell)); + if (!exec_state) + return -ENOMEM; + rcu_assign_pointer(tsk->exec_state, exec_state); + return 0; + } + /* CLONE_VM siblings refcount-share the parent's exec_state. */ if (clone_flags & CLONE_VM) { exec_state = rcu_dereference_protected(current->exec_state, true); @@ -1624,10 +1635,26 @@ enum copy_fs_flags_t { }; static int copy_fs(u64 clone_flags, struct task_struct *tsk, - enum copy_fs_flags_t flags) + enum copy_fs_flags_t flags, struct cell *cell) { struct fs_struct *fs; + /* Born in a cell: at the root of the cell's tree. */ + if (cell) { + struct path root; + + fs = copy_fs_struct(current->fs); + if (!fs) + return -ENOMEM; + cell_root(cell, &root); + set_fs_root(fs, &root); + set_fs_pwd(fs, &root); + path_put(&root); + fs->umask = 0022; + tsk->real_fs = tsk->fs = fs; + return 0; + } + /* * Usermodehelper may copy userspace_init_fs filesystem state but * they don't get to create mount namespaces, share the @@ -2147,7 +2174,7 @@ __latent_entropy struct task_struct *copy_process( * free the parent's ret_stack. */ ftrace_graph_init_task(p); - retval = copy_exec_state(clone_flags, p); + retval = copy_exec_state(clone_flags, p, args->cell); if (retval) goto bad_fork_free; p->flags &= ~PF_KTHREAD; @@ -2177,7 +2204,7 @@ __latent_entropy struct task_struct *copy_process( #ifdef CONFIG_PROVE_LOCKING DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled); #endif - retval = copy_creds(p, clone_flags); + retval = copy_creds(p, clone_flags, args->cell); if (retval < 0) goto bad_fork_free; @@ -2308,7 +2335,7 @@ __latent_entropy struct task_struct *copy_process( goto bad_fork_cleanup_semundo; retval = copy_fs(clone_flags, p, (args->umh ? COPY_FS_UMH : 0) | - (args->idle ? COPY_FS_IDLE : 0)); + (args->idle ? COPY_FS_IDLE : 0), args->cell); if (retval) goto bad_fork_cleanup_semundo; retval = copy_sighand(clone_flags, p); @@ -2320,7 +2347,7 @@ __latent_entropy struct task_struct *copy_process( retval = copy_mm(clone_flags, p); if (retval) goto bad_fork_cleanup_signal; - retval = copy_namespaces(clone_flags, p); + retval = copy_namespaces(clone_flags, p, args->cell); if (retval) goto bad_fork_cleanup_mm; retval = copy_io(clone_flags, p); diff --git a/kernel/nsproxy.c b/kernel/nsproxy.c index bfdd392f4007..dfe47a235cb5 100644 --- a/kernel/nsproxy.c +++ b/kernel/nsproxy.c @@ -163,16 +163,31 @@ static struct nsproxy *create_new_namespaces(u64 flags, return ERR_PTR(err); } -/* - * called from clone. This now handles copy for nsproxy and all - * namespaces therein. +/** + * copy_namespaces - give a new task its namespaces + * @flags: the flags of the clone + * @tsk: the new task + * @cell: the cell @tsk is born in, or NULL if it stays where its parent is + * + * Shares the nsproxy of the parent, or makes a new one with new namespaces for + * the CLONE_NEW* in @flags. A task that is born in a cell shares the nsproxy of + * the cell. + * + * Return: 0 or a negative error. */ -int copy_namespaces(u64 flags, struct task_struct *tsk) +int copy_namespaces(u64 flags, struct task_struct *tsk, struct cell *cell) { struct nsproxy *old_ns = tsk->nsproxy; struct user_namespace *user_ns = task_cred_xxx(tsk, user_ns); struct nsproxy *new_ns; + /* Born in a cell: every task of a cell shares the one nsproxy. */ + if (cell) { + tsk->nsproxy = cell_nsproxy(cell); + get_nsproxy(tsk->nsproxy); + return 0; + } + if (likely(!(flags & (CLONE_NS_ALL & ~CLONE_NEWUSER)))) { if ((flags & CLONE_VM) || likely(old_ns->time_ns_for_children == old_ns->time_ns)) { diff --git a/kernel/user_namespace.c b/kernel/user_namespace.c index 5c2cf3dc02c1..e9543bd91329 100644 --- a/kernel/user_namespace.c +++ b/kernel/user_namespace.c @@ -138,6 +138,22 @@ void userns_retire_cell_block(struct user_namespace *ns) pr_warn_ratelimited("cell: kuid block %u retired\n", ns->cell_block - 1); ns->cell_block = 0; } + +/** + * userns_enter_cell - make a credential one of the user namespace of a cell + * @cred: a credential that nobody else can see yet + * @ns: the user namespace of a cell + * + * As for a task that enters a user namespace: a full set of capabilities, which + * are good for nothing outside of @ns, and default securebits. The caller sets + * the ids and cuts the capabilities down to what the cell is to have. Only a new + * task gets credentials this way, nobody enters a cell. + */ +void userns_enter_cell(struct cred *cred, struct user_namespace *ns) +{ + put_user_ns(cred->user_ns); + set_cred_user_ns(cred, get_user_ns(ns)); +} #endif static unsigned long enforced_nproc_rlimit(void) -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 36/90] cgroup: fork a task of a cell into the budget 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (34 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 35/90] fork: let copy_process() place a task in a cell Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 37/90] x86: give a task without an mm no user state Josef Bacik ` (53 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A task that is born in a cell starts in the budget of the cell. The spawner is not checked for permission to put a task into that cgroup. That check was made when the "budget" slot was filled, and holding the cellfd is the authority to spawn. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cgroup/cgroup.c | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c index 051b220e2850..3869cfa6af50 100644 --- a/kernel/cgroup/cgroup.c +++ b/kernel/cgroup/cgroup.c @@ -28,6 +28,7 @@ #include "cgroup-internal.h" #include <linux/bpf-cgroup.h> +#include <linux/cell.h> #include <linux/cred.h> #include <linux/errno.h> #include <linux/init_task.h> @@ -6905,6 +6906,41 @@ static void cgroup_fork_child_unlock(struct task_struct *child) up_read(&child->signal->cgroup_threadgroup_rwsem); } +/* + * Fork into the budget of a cell. The cellfd is the authority: whoever gave the + * cell this budget was asked everything cgroup_css_set_fork() asks of a caller + * that names a cgroup. Consumes @cset. + */ +static int cgroup_css_set_fork_cell(struct kernel_clone_args *kargs, + struct css_set *cset) +{ + struct cgroup *dst_cgrp = cell_budget(kargs->cell); + int ret; + + ret = -ENODEV; + if (cgroup_is_dead(dst_cgrp)) + goto err; + ret = cgroup_migrate_vet_dst(dst_cgrp); + if (ret) + goto err; + ret = -ENOMEM; + kargs->cset = find_css_set(cset, dst_cgrp); + if (!kargs->cset) + goto err; + + put_css_set(cset); + cgroup_get(dst_cgrp); + kargs->cgrp = dst_cgrp; + kargs->kill_seq = dst_cgrp->kill_seq; + return 0; + +err: + cgroup_threadgroup_change_end(current); + cgroup_unlock(); + put_css_set(cset); + return ret; +} + /** * cgroup_css_set_fork - find or create a css_set for a child process * @kargs: the arguments passed to create the child process @@ -6945,6 +6981,9 @@ static int cgroup_css_set_fork(struct kernel_clone_args *kargs) return 0; } + if (kargs->cell) + return cgroup_css_set_fork_cell(kargs, cset); + CLASS(fd_raw, f)(kargs->cgroup); if (fd_empty(f)) { ret = -EBADF; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 37/90] x86: give a task without an mm no user state 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (35 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 36/90] cgroup: fork a task of a cell into the budget Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 38/90] uprobes: cope with a new task that has no mm Josef Bacik ` (52 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A later patch creates tasks that have no mm and whose first action is to exec. Such a task has no use for the user state of its parent, and none of that state should be left in it. A kernel thread can keep the state because it never returns to user mode, but these tasks do. In copy_thread(), clear the segment bases and selectors, the I/O bitmap and the shadow stack state for a task without an mm. The registers are already cleared, as for every task that starts in a kernel function. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- arch/x86/include/asm/shstk.h | 2 ++ arch/x86/kernel/process.c | 24 +++++++++++++++++++++--- arch/x86/kernel/shstk.c | 14 ++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/arch/x86/include/asm/shstk.h b/arch/x86/include/asm/shstk.h index fc7dcec58fd4..fdf43119c2bb 100644 --- a/arch/x86/include/asm/shstk.h +++ b/arch/x86/include/asm/shstk.h @@ -16,6 +16,7 @@ struct thread_shstk { long shstk_prctl(struct task_struct *task, int option, unsigned long arg2); void reset_thread_features(void); +void shstk_clear_thread(struct task_struct *p); unsigned long shstk_alloc_thread_stack(struct task_struct *p, u64 clone_flags, unsigned long stack_size); void shstk_free(struct task_struct *p); @@ -29,6 +30,7 @@ int shstk_push(u64 val); static inline long shstk_prctl(struct task_struct *task, int option, unsigned long arg2) { return -EINVAL; } static inline void reset_thread_features(void) {} +static inline void shstk_clear_thread(struct task_struct *p) {} static inline unsigned long shstk_alloc_thread_stack(struct task_struct *p, u64 clone_flags, unsigned long stack_size) { return 0; } diff --git a/arch/x86/kernel/process.c b/arch/x86/kernel/process.c index 346c438ac880..d2be7a9647ce 100644 --- a/arch/x86/kernel/process.c +++ b/arch/x86/kernel/process.c @@ -219,9 +219,23 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args) * is disabled, new_ssp will remain 0, and fpu_clone() will know not to * update it. */ - new_ssp = shstk_alloc_thread_stack(p, clone_flags, args->stack_size); - if (IS_ERR_VALUE(new_ssp)) - return PTR_ERR((void *)new_ssp); + if (unlikely(args->fn && !p->mm)) { + /* + * It has no address space to have a shadow stack in, and what + * it will run it has yet to exec. Nothing of the user state of + * whoever created it is any of its business. + */ + new_ssp = 0; + shstk_clear_thread(p); +#ifdef CONFIG_X86_64 + p->thread.fsindex = p->thread.gsindex = 0; + p->thread.fsbase = p->thread.gsbase = 0; +#endif + } else { + new_ssp = shstk_alloc_thread_stack(p, clone_flags, args->stack_size); + if (IS_ERR_VALUE(new_ssp)) + return PTR_ERR((void *)new_ssp); + } fpu_clone(p, clone_flags, args->fn, new_ssp); @@ -258,6 +272,10 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args) */ childregs->sp = 0; childregs->ip = 0; + if (!p->mm) { + memset(childregs, 0, sizeof(struct pt_regs)); + p->thread.pkru = pkru_get_init_value(); + } kthread_frame_init(frame, args->fn, args->fn_arg); return 0; } diff --git a/arch/x86/kernel/shstk.c b/arch/x86/kernel/shstk.c index eb690ba90180..41d77efd7952 100644 --- a/arch/x86/kernel/shstk.c +++ b/arch/x86/kernel/shstk.c @@ -183,6 +183,20 @@ void reset_thread_features(void) current->thread.features_locked = 0; } +/** + * shstk_clear_thread - a new task takes no shadow stack state from its parent + * @p: a task that is being created + * + * For a task without an mm, which has no address space to have a shadow stack + * in. What reset_thread_features() does for the caller at exec. + */ +void shstk_clear_thread(struct task_struct *p) +{ + memset(&p->thread.shstk, 0, sizeof(struct thread_shstk)); + p->thread.features = 0; + p->thread.features_locked = 0; +} + unsigned long shstk_alloc_thread_stack(struct task_struct *tsk, u64 clone_flags, unsigned long stack_size) { -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 38/90] uprobes: cope with a new task that has no mm 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (36 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 37/90] x86: give a task without an mm no user state Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 39/90] fork: add tasks that start out with nothing Josef Bacik ` (51 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik If the parent has pending return probes and the child does not share its mm, uprobe_copy_process() copies the probes to the child and queues work that sets up a probe area in the mm of the child. A task that is created without an mm does not share the mm of its parent, so it would take that path. It has no use for any of this. Return early for such a task. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/events/uprobes.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kernel/events/uprobes.c b/kernel/events/uprobes.c index caf444762a27..af378f228d5c 100644 --- a/kernel/events/uprobes.c +++ b/kernel/events/uprobes.c @@ -2185,6 +2185,10 @@ void uprobe_copy_process(struct task_struct *t, u64 flags) t->utask = NULL; + /* Nothing of the parent's mm matters to a task that has none. */ + if (!t->mm) + return; + if (!utask || !utask->return_instances) return; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 39/90] fork: add tasks that start out with nothing 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (37 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 38/90] uprobes: cope with a new task that has no mm Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 40/90] pidfd: add PIDFD_EMPTY Josef Bacik ` (50 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik To start a program in a container today, a process forks. The child then gets rid of everything it inherited and should not keep, one call at a time: file descriptors, the working directory, the controlling terminal, credentials. Whatever it forgets leaks into the container. Until it is done, it runs code of the parent while it is partly inside of the container and partly outside. Add kernel_clone_args.embryo, which makes copy_process() create a task that has nothing to get rid of. The task works like a usermode helper: it runs a kernel function and returns to user mode once kernel_execve() has succeeded. From the start it has no mm, an empty file table, no blocked or pending signals, and no controlling terminal. It never executes an instruction of its creator. The task is called an embryo until its exec has succeeded. struct embryo describes what it will become, and task_struct.embryo is set for as long as it is an embryo. An embryo still inherits resource limits, scheduling attributes, seccomp filters and LSM state. For the last two this is deliberate: what a confined task spawns is confined in the same way. Nothing sets kernel_clone_args.embryo yet. The following patches add the interface. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cell.h | 24 +++++++++ include/linux/sched.h | 6 +++ include/linux/sched/task.h | 3 ++ kernel/embryo.c | 127 +++++++++++++++++++++++++++++++++++++++++++++ kernel/fork.c | 39 +++++++++++--- 5 files changed, 192 insertions(+), 7 deletions(-) diff --git a/include/linux/cell.h b/include/linux/cell.h index 564c3e86f3bd..f9975b0fb65c 100644 --- a/include/linux/cell.h +++ b/include/linux/cell.h @@ -193,4 +193,28 @@ static inline void cell_root(struct cell *cell, struct path *root) { } static inline struct cgroup *cell_budget(struct cell *cell) { return NULL; } #endif +/* Embryo tasks. */ +#ifdef CONFIG_CELLS + +/** + * task_is_embryo - is a task still on its way to its first exec? + * @task: a task + * + * Return: true from the time PIDFD_CMD_START made @task until its exec has + * succeeded or it is on its way out. + */ +static inline bool task_is_embryo(const struct task_struct *task) +{ + return READ_ONCE(task->embryo); +} + +void embryo_fork_finish(struct kernel_clone_args *args, struct task_struct *p); + +#else + +static inline bool task_is_embryo(const struct task_struct *task) { return false; } +static inline void embryo_fork_finish(struct kernel_clone_args *a, struct task_struct *p) { } + +#endif + #endif /* _LINUX_CELL_H */ diff --git a/include/linux/sched.h b/include/linux/sched.h index 1c93da75226e..3dbe7c13da3b 100644 --- a/include/linux/sched.h +++ b/include/linux/sched.h @@ -85,6 +85,7 @@ struct sighand_struct; struct signal_struct; struct task_delay_info; struct task_exec_state; +struct embryo; struct task_group; struct task_struct; struct timespec64; @@ -983,6 +984,11 @@ struct task_struct { struct task_exec_state __rcu *exec_state; +#ifdef CONFIG_CELLS + /* Set from PIDFD_CMD_START until the first exec has succeeded. */ + struct embryo *embryo; +#endif + int exit_state; int exit_code; int exit_signal; diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h index eaf939ac4bef..97e3675101bd 100644 --- a/include/linux/sched/task.h +++ b/include/linux/sched/task.h @@ -21,6 +21,8 @@ struct css_set; #define CLONE_LEGACY_FLAGS 0xffffffffULL struct cell; +struct embryo; + struct kernel_clone_args { u64 flags; int __user *pidfd; @@ -48,6 +50,7 @@ struct kernel_clone_args { unsigned int kill_seq; struct pid *pid; struct cell *cell; + struct embryo *embryo; }; /* diff --git a/kernel/embryo.c b/kernel/embryo.c new file mode 100644 index 000000000000..163115c56ab4 --- /dev/null +++ b/kernel/embryo.c @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Empty pidfds: fsopen() for processes. + * + * pidfd_open() with PIDFD_EMPTY returns a pidfd that no task is attached to yet. + * It is a pidfd like any other, on a struct pid that has no numbers, so whatever + * it is passed to finds no task. Whoever has it says with pidfd_configure() what + * the task is to be like and starts it. Only then is there a task, which never + * runs a single instruction of its creator's and execs. The creator does not + * fork. + * + * The task is an embryo until its exec has succeeded. It has no mm and an empty + * file table. What it is to reach, it is given. It does inherit attributes of + * whoever started it, as any new task does, see + * Documentation/userspace-api/cells.rst. + * + * It takes the shape of a usermode helper: a task with a kernel function to + * run, that returns to user mode once kernel_execve() has succeeded. + */ +#include <linux/binfmts.h> +#include <linux/cell.h> +#include <linux/completion.h> +#include <linux/cred.h> +#include <linux/fdtable.h> +#include <linux/file.h> +#include <linux/fs.h> +#include <linux/io_uring.h> +#include <linux/personality.h> +#include <linux/nsproxy.h> +#include <linux/pid.h> +#include <linux/pidfs.h> +#include <linux/proc_fs.h> +#include <linux/sched/signal.h> +#include <linux/sched/task.h> +#include <linux/sched/user.h> +#include <linux/security.h> +#include <linux/slab.h> +#include <linux/syscalls.h> +#include <linux/user_namespace.h> +#include <net/af_unix.h> +#include <uapi/linux/pidfd.h> + +struct embryo_strings { + char *block; /* NUL terminated strings, back to back */ + const char **vec; /* NULL terminated, points into @block */ +}; + +struct embryo { + struct rcu_head rcu; + refcount_t ref; + struct mutex lock; /* everything down to @result */ + bool started; + bool settled; /* there is a task, or never will be */ + + char *exec; + struct embryo_strings argv; + struct embryo_strings envp; + + struct pid *pid; /* the reference alloc_pid_struct() returned */ + struct cell *cell; /* where the task is going to be */ + + int result; + struct completion done; /* embryo -> starter */ +}; + +static void embryo_strings_free(struct embryo_strings *s) +{ + kvfree(s->block); + kvfree(s->vec); + s->block = NULL; + s->vec = NULL; +} + +/* + * What was configured has been used, or never will be. The pidfd may be around + * for long, and must not keep the other end of a pipe from seeing the last + * writer go. + */ +static void embryo_drop_config(struct embryo *e) +{ + kfree(e->exec); + e->exec = NULL; + embryo_strings_free(&e->argv); + embryo_strings_free(&e->envp); +} + +static void embryo_put(struct embryo *e) +{ + if (!e || !refcount_dec_and_test(&e->ref)) + return; + + embryo_drop_config(e); + cell_put(e->cell); + kfree_rcu(e, rcu); +} + +/** + * embryo_fork_finish - done creating an embryo, one way or the other + * @args: the arguments of the clone, with @args->embryo set + * @p: what copy_process() returned, a task or an ERR_PTR() + * + * Unlocks the cell, which embryo_start() had locked. + * + * Context: kernel_clone(), before the new task is woken. + */ +void embryo_fork_finish(struct kernel_clone_args *args, struct task_struct *p) +{ + struct embryo *e = args->embryo; + + if (!IS_ERR(p)) + cell_task_spawned(args->cell); + cell_fork_finish(args->cell); + + /* Nothing has happened yet, see embryo_start(). */ + if (p == ERR_PTR(-ERESTARTNOINTR)) { + embryo_put(e); + return; + } + + /* From here on the pidfd polls like any other. */ + WRITE_ONCE(e->settled, true); + if (IS_ERR(p)) { + wake_up_all(&e->pid->wait_pidfd); + /* The reference that was to be the task's. */ + embryo_put(e); + } +} diff --git a/kernel/fork.c b/kernel/fork.c index 4db060281448..119b59f30f4d 100644 --- a/kernel/fork.c +++ b/kernel/fork.c @@ -1565,7 +1565,7 @@ static struct mm_struct *dup_mm(struct task_struct *tsk, return NULL; } -static int copy_mm(u64 clone_flags, struct task_struct *tsk) +static int copy_mm(u64 clone_flags, struct task_struct *tsk, bool embryo) { struct mm_struct *mm, *oldmm; @@ -1583,9 +1583,12 @@ static int copy_mm(u64 clone_flags, struct task_struct *tsk) * Are we cloning a kernel thread? * * We need to steal a active VM for that.. + * + * An embryo is in the same position until it execs. It must never + * get to see the mm of whoever created it. */ oldmm = current->mm; - if (!oldmm) + if (!oldmm || embryo) return 0; if (clone_flags & CLONE_VM) { @@ -1693,10 +1696,19 @@ static int copy_fs(u64 clone_flags, struct task_struct *tsk, } static int copy_files(u64 clone_flags, struct task_struct *tsk, - int no_files) + int no_files, bool embryo) { struct files_struct *oldf, *newf; + /* An empty table. What pidfd_configure() was given goes in there. */ + if (embryo) { + newf = dup_fd(&init_files, NULL); + if (IS_ERR(newf)) + return PTR_ERR(newf); + tsk->files = newf; + return 0; + } + /* * A background process may not have any files ... */ @@ -2187,6 +2199,14 @@ __latent_entropy struct task_struct *copy_process( } if (args->io_thread) p->flags |= PF_IO_WORKER; +#ifdef CONFIG_CELLS + p->embryo = args->embryo; + if (args->embryo) { + sigemptyset(&p->blocked); + sigemptyset(&p->saved_sigmask); + clear_tsk_restore_sigmask(p); + } +#endif if (args->name) strscpy_pad(p->comm, args->name, sizeof(p->comm)); @@ -2330,7 +2350,7 @@ __latent_entropy struct task_struct *copy_process( retval = copy_semundo(clone_flags, p); if (retval) goto bad_fork_cleanup_security; - retval = copy_files(clone_flags, p, args->no_files); + retval = copy_files(clone_flags, p, args->no_files, args->embryo); if (retval) goto bad_fork_cleanup_semundo; retval = copy_fs(clone_flags, p, @@ -2344,7 +2364,7 @@ __latent_entropy struct task_struct *copy_process( retval = copy_signal(clone_flags, p); if (retval) goto bad_fork_cleanup_sighand; - retval = copy_mm(clone_flags, p); + retval = copy_mm(clone_flags, p, args->embryo); if (retval) goto bad_fork_cleanup_signal; retval = copy_namespaces(clone_flags, p, args->cell); @@ -2568,8 +2588,10 @@ __latent_entropy struct task_struct *copy_process( WRITE_ONCE(ns->child_reaper, p); p->signal->flags |= SIGNAL_UNKILLABLE; } - p->signal->shared_pending.signal = delayed.signal; - p->signal->tty = tty_kref_get(current->signal->tty); + if (!args->embryo) + p->signal->shared_pending.signal = delayed.signal; + if (!args->embryo) + p->signal->tty = tty_kref_get(current->signal->tty); /* * Inherit has_child_subreaper flag under the same * tasklist_lock with adding child to the process tree @@ -2817,6 +2839,9 @@ pid_t kernel_clone(struct kernel_clone_args *args) p = copy_process(NULL, trace, NUMA_NO_NODE, args); add_latent_entropy(); + if (args->embryo) + embryo_fork_finish(args, p); + if (IS_ERR(p)) return PTR_ERR(p); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 40/90] pidfd: add PIDFD_EMPTY 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (38 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 39/90] fork: add tasks that start out with nothing Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 41/90] pidfd: add pidfd_configure() Josef Bacik ` (49 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Do for tasks what fsopen() did for mounts. pidfd_open() with PIDFD_EMPTY returns a pidfd that has no task yet. The caller then describes the task and starts it. It is an ordinary pidfd, for a struct pid that has no task and no numbers. Everything that takes a pidfd already handles a pidfd without a task: it returns ESRCH, or ECHILD in the case of waitid(). Once the task exists it is still the same pidfd on the same inode, and pidfd_open() on the pid of the task finds that inode. pidfs itself needs changes, because it takes a missing task to mean that the task has exited. Teach it the difference between "no task yet" and "no task any more" in three places: - poll() must not report an exit. - PIDFD_GET_INFO has no exit status to report. - fdinfo shows "Pid: 0" instead of -1. The namespace ioctls already return ESRCH, and a struct pid without numbers cannot be found by file handle. Nothing is reserved ahead of time. The struct pid gets its numbers when the task is created. So the task that is started first becomes the init of its pid namespace, an empty pidfd that is closed leaves nothing behind, and one that is never started does not keep zap_pid_ns_processes() waiting. The size of a struct pid depends on the level of its pid namespace, so the namespace must be known when the pidfd is created. With PIDFD_EMPTY the first argument of pidfd_open() is therefore a cellfd instead of a pid. Such a task would be useful outside of a cell as well. That is not implemented, and -EBADF in place of the cellfd is reserved for it. The configuration is attached to the file. So only the file that pidfd_open() returned can be used to configure the task, no matter who else can open a pidfd for it. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/pidfs.c | 27 ++++++++--- include/linux/cell.h | 11 +++++ include/uapi/linux/pidfd.h | 1 + kernel/embryo.c | 117 +++++++++++++++++++++++++++++++++++++++++++++ kernel/pid.c | 5 ++ 5 files changed, 155 insertions(+), 6 deletions(-) diff --git a/fs/pidfs.c b/fs/pidfs.c index ef305cc359eb..b1c1e0ea1e4a 100644 --- a/fs/pidfs.c +++ b/fs/pidfs.c @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-2.0 #include <linux/anon_inodes.h> +#include <linux/cell.h> #include <linux/compat.h> #include <linux/exportfs.h> #include <linux/file.h> @@ -278,6 +279,9 @@ static void pidfd_show_fdinfo(struct seq_file *m, struct file *f) if (likely(pid_has_task(pid, PIDTYPE_PID))) { ns = proc_pid_ns(file_inode(m->file)->i_sb); nr = pid_nr_ns(pid, ns); + } else if (embryo_pidfd_is_empty(f)) { + /* No task yet is not the same as no task any more. */ + nr = 0; } seq_put_decimal_ll(m, "Pid:\t", nr); @@ -310,6 +314,9 @@ static __poll_t pidfd_poll(struct file *file, struct poll_table_struct *pts) __poll_t poll_flags = 0; poll_wait(file, &pid->wait_pidfd, pts); + /* No task yet is not the same as no task any more. */ + if (embryo_pidfd_is_empty(file)) + return 0; /* * Don't wake waiters if the thread-group leader exited * prematurely. They either get notified when the last subthread @@ -437,6 +444,10 @@ static long pidfd_info(struct file *file, unsigned int cmd, unsigned long arg) */ if (!(mask & PIDFD_INFO_EXIT)) return -ESRCH; + /* What was opened empty may never have had a task to exit. */ + if ((file->f_flags & PIDFD_EMPTY) && + !test_bit(PIDFS_ATTR_BIT_EXIT, &attr->attr_mask)) + return -ESRCH; goto copy_out; } @@ -729,6 +740,9 @@ static int pidfs_file_release(struct inode *inode, struct file *file) struct pid *pid = inode->i_private; struct task_struct *task; + if (file->f_flags & PIDFD_EMPTY) + embryo_pidfd_release(file); + if (!(file->f_flags & PIDFD_AUTOKILL)) return 0; @@ -1004,7 +1018,8 @@ static struct file *pidfs_dentry_open(const struct path *path, file = dentry_open(path, flags | O_RDWR, cred); /* do_dentry_open() strips O_EXCL and O_TRUNC. */ if (!IS_ERR(file)) - file->f_flags |= flags & (PIDFD_THREAD | PIDFD_AUTOKILL); + file->f_flags |= flags & (PIDFD_THREAD | PIDFD_AUTOKILL | + PIDFD_EMPTY); return file; } @@ -1033,10 +1048,10 @@ static int pidfs_export_permission(struct handle_to_path_ctx *ctx, static struct file *pidfs_export_open(const struct path *path, unsigned int oflags) { /* - * Opening via file handle may never raise PIDFD_AUTOKILL. That can - * only be done at task creation! + * Opening via file handle may never raise PIDFD_AUTOKILL or + * PIDFD_EMPTY. That can only be done at task creation, or ahead of it! */ - if (WARN_ON_ONCE(oflags & PIDFD_AUTOKILL)) + if (WARN_ON_ONCE(oflags & (PIDFD_AUTOKILL | PIDFD_EMPTY))) return ERR_PTR(-EINVAL); /* Clear O_LARGEFILE as open_by_handle_at() forces it. */ return pidfs_dentry_open(path, oflags & ~O_LARGEFILE, current_cred()); @@ -1226,8 +1241,8 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags) * Ensure that internal pidfd flags don't overlap with each * other or with uapi pidfd flags. */ - BUILD_BUG_ON(hweight32(PIDFD_THREAD | PIDFD_NONBLOCK | - PIDFD_STALE | PIDFD_AUTOKILL) != 4); + BUILD_BUG_ON(hweight32(PIDFD_THREAD | PIDFD_NONBLOCK | PIDFD_STALE | + PIDFD_AUTOKILL | PIDFD_EMPTY) != 5); ret = path_from_stashed(&pid->stashed, pidfs_mnt, get_pid(pid), &path); if (ret < 0) diff --git a/include/linux/cell.h b/include/linux/cell.h index f9975b0fb65c..a68638b7319d 100644 --- a/include/linux/cell.h +++ b/include/linux/cell.h @@ -217,4 +217,15 @@ static inline void embryo_fork_finish(struct kernel_clone_args *a, struct task_s #endif +/* An empty pidfd. */ +#ifdef CONFIG_CELLS +int embryo_pidfd_open(int cfd, unsigned int flags); +bool embryo_pidfd_is_empty(const struct file *file); +void embryo_pidfd_release(struct file *file); +#else +static inline int embryo_pidfd_open(int cfd, unsigned int flags) { return -EINVAL; } +static inline bool embryo_pidfd_is_empty(const struct file *file) { return false; } +static inline void embryo_pidfd_release(struct file *file) { } +#endif + #endif /* _LINUX_CELL_H */ diff --git a/include/uapi/linux/pidfd.h b/include/uapi/linux/pidfd.h index 0919246a1611..eb3704687c04 100644 --- a/include/uapi/linux/pidfd.h +++ b/include/uapi/linux/pidfd.h @@ -10,6 +10,7 @@ /* Flags for pidfd_open(). */ #define PIDFD_NONBLOCK O_NONBLOCK #define PIDFD_THREAD O_EXCL +#define PIDFD_EMPTY O_NOCTTY /* No task yet. See pidfd_configure(). */ #ifdef __KERNEL__ #include <linux/sched.h> #define PIDFD_STALE CLONE_PIDFD diff --git a/kernel/embryo.c b/kernel/embryo.c index 163115c56ab4..e052c09f9192 100644 --- a/kernel/embryo.c +++ b/kernel/embryo.c @@ -125,3 +125,120 @@ void embryo_fork_finish(struct kernel_clone_args *args, struct task_struct *p) embryo_put(e); } } + +static int embryo_pidfd_create(struct embryo *e, unsigned int flags) +{ + struct file *file; + + CLASS(get_unused_fd, fd)(O_CLOEXEC); + if (fd < 0) + return fd; + file = pidfs_alloc_file(e->pid, flags | O_RDWR); + if (IS_ERR(file)) + return PTR_ERR(file); + + file->private_data = e; + fd_install(fd, file); + return take_fd(fd); +} + +/** + * embryo_pidfd_open - pidfd_open() with PIDFD_EMPTY + * @cfd: the cell the task is going to be in + * @flags: PIDFD_EMPTY, and PIDFD_NONBLOCK if wanted + * + * The struct pid is made here and gets no numbers before the task is started. + * Its size goes by the pid namespace, which is why the cell has to be known by + * now. It reserves nothing in there: whichever task is started first is the init + * of the cell, and a pidfd that is left empty keeps no cell from dying. + * + * Return: the pidfd, which is always close-on-exec, -EPERM if the caller is in a + * cell, -EOPNOTSUPP without a cell, what cell_fork_prepare() has to say about the + * cell, or another negative error. + */ +int embryo_pidfd_open(int cfd, unsigned int flags) +{ + struct embryo *e; + struct cell *cell; + int ret; + + if (flags & ~(PIDFD_EMPTY | PIDFD_NONBLOCK)) + return -EINVAL; + /* Whatever the file descriptor is. */ + if (!cell_may_spawn()) + return -EPERM; + /* A task like this outside of a cell makes sense. It is not done yet. */ + if (cfd == -EBADF) + return -EOPNOTSUPP; + + CLASS(fd, f)(cfd); + if (fd_empty(f)) + return -EBADF; + cell = cell_from_file(fd_file(f)); + if (!cell) + return -EINVAL; + + e = kzalloc_obj(struct embryo, GFP_KERNEL_ACCOUNT); + if (!e) + return -ENOMEM; + refcount_set(&e->ref, 1); + mutex_init(&e->lock); + init_completion(&e->done); + + ret = cell_fork_prepare(cell); + if (ret) { + kfree(e); + return ret; + } + e->pid = alloc_pid_struct(cell_nsproxy(cell)->pid_ns_for_children); + e->cell = cell_get(cell); + cell_fork_finish(cell); + + ret = -ENOMEM; + if (!e->pid) + goto err_put; + ret = pidfs_add_pid(e->pid); + if (ret) + goto err_pid; + ret = embryo_pidfd_create(e, flags); + if (ret >= 0) + return ret; +err_pid: + free_pid_struct(e->pid); +err_put: + embryo_put(e); + return ret; +} + +/** + * embryo_pidfd_is_empty - is there no task yet, rather than no task any more? + * @file: a pidfd + * + * Return: true for what pidfd_open() returned for PIDFD_EMPTY, until there is a + * task or it is known that there never will be. + */ +bool embryo_pidfd_is_empty(const struct file *file) +{ + const struct embryo *e = file->private_data; + + return (file->f_flags & PIDFD_EMPTY) && !READ_ONCE(e->settled); +} + +/** + * embryo_pidfd_release - what pidfd_open() returned for PIDFD_EMPTY was closed + * @file: the pidfd + * + * A task that was started is left alone. If there never was one, neither is + * there anything to kill. + * + * Context: The ->release() of the file. + */ +void embryo_pidfd_release(struct file *file) +{ + struct embryo *e = file->private_data; + + /* Otherwise free_pid() has dropped, or is going to drop, that reference. */ + if (!pid_has_numbers(e->pid)) + free_pid_struct(e->pid); + embryo_put(e); +} diff --git a/kernel/pid.c b/kernel/pid.c index 0611406e9a9a..9b75daf407ca 100644 --- a/kernel/pid.c +++ b/kernel/pid.c @@ -39,6 +39,7 @@ #include <linux/proc_ns.h> #include <linux/refcount.h> #include <linux/anon_inodes.h> +#include <linux/cell.h> #include <linux/sched/signal.h> #include <linux/sched/task.h> #include <linux/idr.h> @@ -760,6 +761,10 @@ SYSCALL_DEFINE2(pidfd_open, pid_t, pid, unsigned int, flags) int fd; struct pid *p; + /* There is no task to name yet. @pid says where it is going to be. */ + if (flags & PIDFD_EMPTY) + return embryo_pidfd_open(pid, flags); + if (flags & ~(PIDFD_NONBLOCK | PIDFD_THREAD)) return -EINVAL; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 41/90] pidfd: add pidfd_configure() 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (39 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 40/90] pidfd: add PIDFD_EMPTY Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 42/90] pidfd: let pidfd_configure() place files Josef Bacik ` (48 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Add the system call that describes the task of an empty pidfd and starts it. It is modelled on fsconfig(): it takes a command, a key and a value, and data only flows into the kernel. pidfd = pidfd_open(cellfd, PIDFD_EMPTY); pidfd_configure(pidfd, PIDFD_SET_STRING, "exec", "/sbin/init", 0); pidfd_configure(pidfd, PIDFD_SET_BINARY, "argv", "init\0-v", 8); pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0); This patch adds the keys "exec", "argv" and "envp", and PIDFD_CMD_START. The start command creates the task, applies the configuration in the context of the task, lets it exec, and waits for the exec to finish. If anything fails, PIDFD_CMD_START returns the error, and the task is killed if it already exists. A pidfd can only be started once. The task gets default signal dispositions and its own session. CLONE_CLEAR_SIGHAND would keep signals ignored that the caller ignores, so the handlers are flushed the way they are for a usermode helper. Nobody has to wait for the task. It is reaped automatically, and the pidfd reports how it exited. The pidfd may be kept open for a long time after the start, so the configuration is freed as soon as it has been used. The LSM hooks for exec run in the context of the new task, as for any exec. Seccomp filters apply to the system calls of the new program. But the work that the start command does is not a system call of the new task, so the filters are not consulted for it. A filter that denies execve() to stop a task from running another program must also deny pidfd_configure(), just as such filters once had to learn about execveat(). Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/uapi/linux/pidfd.h | 7 ++ kernel/embryo.c | 258 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) diff --git a/include/uapi/linux/pidfd.h b/include/uapi/linux/pidfd.h index eb3704687c04..c01bce5e17ad 100644 --- a/include/uapi/linux/pidfd.h +++ b/include/uapi/linux/pidfd.h @@ -17,6 +17,13 @@ #define PIDFD_AUTOKILL O_TRUNC #endif +/* pidfd_configure(): @cmd. Data only ever goes in. */ +enum pidfd_configure_cmd { + PIDFD_SET_STRING = 0, /* Set @key to the string in @value */ + PIDFD_SET_BINARY = 1, /* Set @key to the @aux bytes at @value */ + PIDFD_SET_FD = 2, /* The file to have at number @key, from fd @aux */ + PIDFD_CMD_START = 3, /* Make the task, apply everything and exec */ +}; /* Flags for pidfd_send_signal(). */ #define PIDFD_SIGNAL_THREAD (1UL << 0) #define PIDFD_SIGNAL_THREAD_GROUP (1UL << 1) diff --git a/kernel/embryo.c b/kernel/embryo.c index e052c09f9192..f405efb195c7 100644 --- a/kernel/embryo.c +++ b/kernel/embryo.c @@ -40,6 +40,10 @@ #include <net/af_unix.h> #include <uapi/linux/pidfd.h> +#define EMBRYO_KEY_MAX 32 +#define EMBRYO_MAX_STRINGS 4096 +#define EMBRYO_MAX_BLOCK (256 * 1024) + struct embryo_strings { char *block; /* NUL terminated strings, back to back */ const char **vec; /* NULL terminated, points into @block */ @@ -94,6 +98,53 @@ static void embryo_put(struct embryo *e) kfree_rcu(e, rcu); } +/* The task is done being an embryo, one way or the other. */ +static void embryo_detach(struct embryo *e) +{ + WRITE_ONCE(current->embryo, NULL); + embryo_put(e); +} + +static int embryo_exec(struct embryo *e) +{ + static const char * const no_strings[] = { NULL }; + const char *argv0[] = { e->exec, NULL }; + + return kernel_execve(e->exec, e->argv.vec ?: argv0, + e->envp.vec ?: no_strings); +} + +static int embryo_main(void *arg) +{ + struct embryo *e = arg; + int ret; + + /* CLONE_CLEAR_SIGHAND leaves what the starter ignores ignored. */ + flush_signal_handlers(current, 1); + + /* The starter's session, process group and terminal are not ours. */ + ksys_setsid(); + set_personality(PER_LINUX); + + /* @started is set, so nobody changes anything any more. */ + ret = embryo_exec(e); + embryo_drop_config(e); + /* + * A signal to the embryo is no signal to whoever started it. Whatever + * interrupted the exec, the embryo is about to be killed. + */ + if (ret && (fatal_signal_pending(current) || ret <= -ERESTARTSYS)) + ret = -ESRCH; + e->result = ret; + complete_all(&e->done); + embryo_detach(e); + if (ret) + do_group_exit(SIGKILL); + + /* Off to user mode. */ + return 0; +} + /** * embryo_fork_finish - done creating an embryo, one way or the other * @args: the arguments of the clone, with @args->embryo set @@ -242,3 +293,210 @@ void embryo_pidfd_release(struct file *file) free_pid_struct(e->pid); embryo_put(e); } + +static struct embryo *embryo_from_file(struct file *file) +{ + struct pid *pid; + + pid = pidfd_pid(file); + if (IS_ERR(pid)) + return ERR_CAST(pid); + /* Only the file pidfd_open() returned will do. Nothing else ever has this. */ + if (!(file->f_flags & PIDFD_EMPTY)) + return ERR_PTR(-EPERM); + return file->private_data; +} + +static int embryo_set_strings(struct embryo_strings *s, const void __user *ublock, + size_t len) +{ + struct embryo_strings new = {}; + unsigned int nr = 0, i; + size_t off; + + if (!len || len > EMBRYO_MAX_BLOCK) + return -E2BIG; + + new.block = vmemdup_user(ublock, len); + if (IS_ERR(new.block)) + return PTR_ERR(new.block); + if (new.block[len - 1]) { + kvfree(new.block); + return -EINVAL; + } + + for (off = 0; off < len; off += strlen(new.block + off) + 1) + nr++; + if (nr > EMBRYO_MAX_STRINGS) { + kvfree(new.block); + return -E2BIG; + } + + new.vec = kvmalloc_array(nr + 1, sizeof(*new.vec), GFP_KERNEL_ACCOUNT); + if (!new.vec) { + kvfree(new.block); + return -ENOMEM; + } + for (off = 0, i = 0; off < len; off += strlen(new.block + off) + 1) + new.vec[i++] = new.block + off; + new.vec[i] = NULL; + + embryo_strings_free(s); + *s = new; + return 0; +} + +static int embryo_start(struct embryo *e) +{ + struct kernel_clone_args args = { + /* + * CLONE_VM only says "do not copy the mm": there is none, + * copy_mm() sees to that. Nobody has to wait for the task, the + * pidfd tells. The rest is what every task of a cell gets. + */ + .flags = CLONE_VM | CLONE_UNTRACED | CLONE_AUTOREAP | + CLONE_INTO_CGROUP | CLONE_NNP | CLONE_CLEAR_SIGHAND, + .fn = embryo_main, + .fn_arg = e, + .name = "embryo", + .pid = e->pid, + .cell = e->cell, + .embryo = e, + }; + pid_t nr; + int ret; + + /* Until embryo_fork_finish(). */ + ret = cell_fork_prepare(e->cell); + if (ret) + return ret; + + scoped_guard(mutex, &e->lock) { + ret = e->started ? -EBUSY : e->exec ? 0 : -EINVAL; + if (!ret) + e->started = true; + } + if (ret) { + cell_fork_finish(e->cell); + return ret; + } + + /* For the task. */ + refcount_inc(&e->ref); + nr = kernel_clone(&args); + /* + * A signal is pending for the caller, so the fork is to be started over + * once that has been seen to, as any fork is. It has not been tried. + */ + if (nr == -ERESTARTNOINTR) { + scoped_guard(mutex, &e->lock) + e->started = false; + return nr; + } + if (nr < 0) { + scoped_guard(mutex, &e->lock) + embryo_drop_config(e); + return nr; + } + + /* + * Everything up to the point of no return of the exec is reported + * here. Past that it is a fatal signal, for the pidfd to tell. + */ + if (wait_for_completion_killable(&e->done)) + return -EINTR; + return e->result; +} + +/** + * sys_pidfd_configure - say what the task of an empty pidfd is to be, and start it + * @pidfd: what pidfd_open() returned for PIDFD_EMPTY, and no other pidfd + * @cmd: one of enum pidfd_configure_cmd + * @ukey: the name of what is set, or NULL for PIDFD_CMD_START + * @uvalue: a string for PIDFD_SET_STRING, @aux bytes for PIDFD_SET_BINARY + * @aux: a length for PIDFD_SET_BINARY, a file descriptor for PIDFD_SET_FD + * + * Shaped like fsconfig(). Data only goes in. + * + * PIDFD_CMD_START makes the task, applies everything in its context, has it exec + * and waits for that. Whatever goes wrong is returned here, and the task, if it + * got that far, is killed. It can be tried only once. Being started over because + * a signal is pending for the caller, as a fork is, does not count. + * + * Return: 0, -EPERM for any other pidfd, -EBUSY once it was started, -ESRCH if + * the task or the cell was killed, -EOPNOTSUPP for an unknown @cmd, -EINVAL for + * an unknown key or arguments that do not go with @cmd, or what went wrong. + */ +SYSCALL_DEFINE5(pidfd_configure, int, pidfd, unsigned int, cmd, + const char __user *, ukey, const void __user *, uvalue, int, aux) +{ + char *key __free(kfree) = NULL; + char *str __free(kfree) = NULL; + struct embryo *e; + int ret; + + switch (cmd) { + case PIDFD_SET_STRING: + if (!ukey || !uvalue || aux) + return -EINVAL; + break; + case PIDFD_SET_BINARY: + if (!ukey || !uvalue || aux <= 0) + return -EINVAL; + break; + case PIDFD_CMD_START: + if (ukey || uvalue || aux) + return -EINVAL; + break; + default: + return -EOPNOTSUPP; + } + + if (ukey) { + key = strndup_user(ukey, EMBRYO_KEY_MAX); + if (IS_ERR(key)) + return PTR_ERR(no_free_ptr(key)); + } + if (cmd == PIDFD_SET_STRING) { + str = strndup_user(uvalue, PATH_MAX); + if (IS_ERR(str)) + return PTR_ERR(no_free_ptr(str)); + } + + CLASS(fd, f)(pidfd); + if (fd_empty(f)) + return -EBADF; + e = embryo_from_file(fd_file(f)); + if (IS_ERR(e)) + return PTR_ERR(e); + + if (cmd == PIDFD_CMD_START) + return embryo_start(e); + + mutex_lock(&e->lock); + ret = -EBUSY; + if (e->started) + goto out; + + switch (cmd) { + case PIDFD_SET_STRING: + ret = -EINVAL; + if (strcmp(key, "exec")) + break; + kfree(e->exec); + e->exec = no_free_ptr(str); + ret = 0; + break; + case PIDFD_SET_BINARY: + if (!strcmp(key, "argv")) + ret = embryo_set_strings(&e->argv, uvalue, aux); + else if (!strcmp(key, "envp")) + ret = embryo_set_strings(&e->envp, uvalue, aux); + else + ret = -EINVAL; + break; + } +out: + mutex_unlock(&e->lock); + return ret; +} -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 42/90] pidfd: let pidfd_configure() place files 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (40 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 41/90] pidfd: add pidfd_configure() Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 43/90] pidfd: let pidfd_configure() set credentials Josef Bacik ` (47 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik PIDFD_SET_FD with a decimal number as the key places a file at that descriptor number in the new task. The file table of the task starts out empty, so it ends up containing exactly the files that were placed. The files are installed in the context of the new task, after security_file_receive() has been called for each of them. Some kinds of file are refused, because each of them is a way to act on something outside of the cell: io_uring files, cellfds, namespace files, and empty pidfds. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/embryo.c | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/kernel/embryo.c b/kernel/embryo.c index f405efb195c7..2c65a99eaeea 100644 --- a/kernel/embryo.c +++ b/kernel/embryo.c @@ -41,9 +41,16 @@ #include <uapi/linux/pidfd.h> #define EMBRYO_KEY_MAX 32 +#define EMBRYO_MAX_FDS 256 +#define EMBRYO_MAX_FD 1024 #define EMBRYO_MAX_STRINGS 4096 #define EMBRYO_MAX_BLOCK (256 * 1024) +struct embryo_fd { + unsigned int fd; + struct file *file; +}; + struct embryo_strings { char *block; /* NUL terminated strings, back to back */ const char **vec; /* NULL terminated, points into @block */ @@ -59,6 +66,8 @@ struct embryo { char *exec; struct embryo_strings argv; struct embryo_strings envp; + struct embryo_fd fds[EMBRYO_MAX_FDS]; + unsigned int nr_fds; struct pid *pid; /* the reference alloc_pid_struct() returned */ struct cell *cell; /* where the task is going to be */ @@ -75,6 +84,34 @@ static void embryo_strings_free(struct embryo_strings *s) s->vec = NULL; } +static void embryo_put_fds(struct embryo *e) +{ + unsigned int i; + + for (i = 0; i < e->nr_fds; i++) + fput(e->fds[i].file); + e->nr_fds = 0; +} + +/* In the context of the embryo, whose file table is empty. */ +static int embryo_install_fds(struct embryo *e) +{ + unsigned int i; + int ret; + + for (i = 0; i < e->nr_fds; i++) { + struct file *file = e->fds[i].file; + + ret = security_file_receive(file); + if (ret) + return ret; + ret = replace_fd(e->fds[i].fd, file, 0); + if (ret < 0) + return ret; + } + return 0; +} + /* * What was configured has been used, or never will be. The pidfd may be around * for long, and must not keep the other end of a pipe from seeing the last @@ -86,6 +123,7 @@ static void embryo_drop_config(struct embryo *e) e->exec = NULL; embryo_strings_free(&e->argv); embryo_strings_free(&e->envp); + embryo_put_fds(e); } static void embryo_put(struct embryo *e) @@ -109,6 +147,11 @@ static int embryo_exec(struct embryo *e) { static const char * const no_strings[] = { NULL }; const char *argv0[] = { e->exec, NULL }; + int ret; + + ret = embryo_install_fds(e); + if (ret) + return ret; return kernel_execve(e->exec, e->argv.vec ?: argv0, e->envp.vec ?: no_strings); @@ -346,6 +389,43 @@ static int embryo_set_strings(struct embryo_strings *s, const void __user *ubloc return 0; } +/* + * The file table of the new task is exactly what is put here. After this + * nothing crosses the boundary of the cell any more, so this is where it is + * decided what may. + */ +static int embryo_set_fd(struct embryo *e, const char *key, struct file *file) +{ + unsigned int fd, i; + int ret; + + ret = kstrtouint(key, 10, &fd); + if (ret) + return -EINVAL; + if (fd >= EMBRYO_MAX_FD) + return -EBADF; + + /* Handles on what is outside, or means to get at it. */ + if (io_is_uring_fops(file) || cell_from_file(file) || proc_ns_file(file)) + return -EPERM; + if (!IS_ERR(pidfd_pid(file)) && (file->f_flags & PIDFD_EMPTY)) + return -EPERM; + + for (i = 0; i < e->nr_fds; i++) { + if (e->fds[i].fd == fd) { + fput(e->fds[i].file); + e->fds[i].file = get_file(file); + return 0; + } + } + if (e->nr_fds == EMBRYO_MAX_FDS) + return -EMFILE; + e->fds[e->nr_fds].fd = fd; + e->fds[e->nr_fds].file = get_file(file); + e->nr_fds++; + return 0; +} + static int embryo_start(struct embryo *e) { struct kernel_clone_args args = { @@ -444,6 +524,10 @@ SYSCALL_DEFINE5(pidfd_configure, int, pidfd, unsigned int, cmd, if (!ukey || !uvalue || aux <= 0) return -EINVAL; break; + case PIDFD_SET_FD: + if (!ukey || uvalue || aux < 0) + return -EINVAL; + break; case PIDFD_CMD_START: if (ukey || uvalue || aux) return -EINVAL; @@ -495,6 +579,14 @@ SYSCALL_DEFINE5(pidfd_configure, int, pidfd, unsigned int, cmd, else ret = -EINVAL; break; + case PIDFD_SET_FD: { + CLASS(fd, slot)(aux); + + ret = -EBADF; + if (!fd_empty(slot)) + ret = embryo_set_fd(e, key, fd_file(slot)); + break; + } } out: mutex_unlock(&e->lock); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 43/90] pidfd: let pidfd_configure() set credentials 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (41 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 42/90] pidfd: let pidfd_configure() place files Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 44/90] pidfd: let pidfd_configure() name a cgroup to start in Josef Bacik ` (46 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A task that is spawned into a cell starts as uid 0 of the cell. Add a way to start it as another user. PIDFD_SET_BINARY with the key "cred" takes a struct pidfd_cred that holds a uid, a gid and supplementary groups, all given as ids of the cell. The credentials are applied in the context of the new task. The task loses its capabilities in the process, as it would with a setresuid() away from uid 0. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/uapi/linux/pidfd.h | 10 ++++ kernel/embryo.c | 116 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/include/uapi/linux/pidfd.h b/include/uapi/linux/pidfd.h index c01bce5e17ad..e1f6d27bb220 100644 --- a/include/uapi/linux/pidfd.h +++ b/include/uapi/linux/pidfd.h @@ -24,6 +24,16 @@ enum pidfd_configure_cmd { PIDFD_SET_FD = 2, /* The file to have at number @key, from fd @aux */ PIDFD_CMD_START = 3, /* Make the task, apply everything and exec */ }; + +/* pidfd_configure(PIDFD_SET_BINARY, "cred"). Ids are those of the new task. */ +struct pidfd_cred { + __u32 uid; + __u32 gid; + __u32 ngroups; + __u32 __spare; + __aligned_u64 groups; /* pointer to @ngroups __u32 gids */ +}; + /* Flags for pidfd_send_signal(). */ #define PIDFD_SIGNAL_THREAD (1UL << 0) #define PIDFD_SIGNAL_THREAD_GROUP (1UL << 1) diff --git a/kernel/embryo.c b/kernel/embryo.c index 2c65a99eaeea..0ad85cd2b865 100644 --- a/kernel/embryo.c +++ b/kernel/embryo.c @@ -69,8 +69,14 @@ struct embryo { struct embryo_fd fds[EMBRYO_MAX_FDS]; unsigned int nr_fds; + bool have_cred; + kuid_t uid; + kgid_t gid; + struct group_info *groups; + struct pid *pid; /* the reference alloc_pid_struct() returned */ struct cell *cell; /* where the task is going to be */ + struct user_namespace *user_ns; /* likewise */ int result; struct completion done; /* embryo -> starter */ @@ -124,6 +130,9 @@ static void embryo_drop_config(struct embryo *e) embryo_strings_free(&e->argv); embryo_strings_free(&e->envp); embryo_put_fds(e); + if (e->groups) + put_group_info(e->groups); + e->groups = NULL; } static void embryo_put(struct embryo *e) @@ -132,6 +141,7 @@ static void embryo_put(struct embryo *e) return; embryo_drop_config(e); + put_user_ns(e->user_ns); cell_put(e->cell); kfree_rcu(e, rcu); } @@ -143,6 +153,52 @@ static void embryo_detach(struct embryo *e) embryo_put(e); } +static int embryo_apply_cred(struct embryo *e) +{ + const struct cred *old = current_cred(); + struct user_struct *user; + struct cred *new; + int ret; + + if (!e->have_cred) + return 0; + + new = prepare_creds(); + if (!new) + return -ENOMEM; + + new->uid = new->euid = new->suid = new->fsuid = e->uid; + new->gid = new->egid = new->sgid = new->fsgid = e->gid; + if (e->groups) + set_groups(new, e->groups); + + ret = -EAGAIN; + user = alloc_uid(e->uid); + if (!user) + goto err; + free_uid(new->user); + new->user = user; + + /* Leaving uid 0 behind costs the capabilities, as setresuid() would. */ + ret = security_task_fix_setuid(new, old, LSM_SETID_RES); + if (ret) + goto err; + ret = security_task_fix_setgid(new, old, LSM_SETID_RES); + if (ret) + goto err; + ret = security_task_fix_setgroups(new, old); + if (ret) + goto err; + ret = set_cred_ucounts(new); + if (ret) + goto err; + return commit_creds(new); + +err: + abort_creds(new); + return ret; +} + static int embryo_exec(struct embryo *e) { static const char * const no_strings[] = { NULL }; @@ -150,6 +206,9 @@ static int embryo_exec(struct embryo *e) int ret; ret = embryo_install_fds(e); + if (ret) + return ret; + ret = embryo_apply_cred(e); if (ret) return ret; @@ -286,6 +345,7 @@ int embryo_pidfd_open(int cfd, unsigned int flags) } e->pid = alloc_pid_struct(cell_nsproxy(cell)->pid_ns_for_children); e->cell = cell_get(cell); + e->user_ns = get_user_ns(cell_user_ns(cell)); cell_fork_finish(cell); ret = -ENOMEM; @@ -389,6 +449,60 @@ static int embryo_set_strings(struct embryo_strings *s, const void __user *ubloc return 0; } +static int embryo_set_cred(struct embryo *e, const void __user *uvalue, size_t len) +{ + struct group_info *groups = NULL; + struct pidfd_cred tc; + kuid_t uid; + kgid_t gid; + u32 i; + + if (len != sizeof(tc)) + return -EINVAL; + if (copy_from_user(&tc, uvalue, sizeof(tc))) + return -EFAULT; + if (tc.__spare || tc.ngroups > NGROUPS_MAX) + return -EINVAL; + + /* The ids are the cell's own. One it does not have cannot be named. */ + uid = make_kuid(e->user_ns, tc.uid); + gid = make_kgid(e->user_ns, tc.gid); + if (!uid_valid(uid) || !gid_valid(gid)) + return -EINVAL; + + if (tc.ngroups) { + const u32 __user *ugroups = u64_to_user_ptr(tc.groups); + + if (!userns_may_setgroups(e->user_ns)) + return -EPERM; + groups = groups_alloc(tc.ngroups); + if (!groups) + return -ENOMEM; + for (i = 0; i < tc.ngroups; i++) { + u32 g; + + if (get_user(g, ugroups + i)) { + put_group_info(groups); + return -EFAULT; + } + groups->gid[i] = make_kgid(e->user_ns, g); + if (!gid_valid(groups->gid[i])) { + put_group_info(groups); + return -EINVAL; + } + } + groups_sort(groups); + } + + if (e->groups) + put_group_info(e->groups); + e->groups = groups; + e->uid = uid; + e->gid = gid; + e->have_cred = true; + return 0; +} + /* * The file table of the new task is exactly what is put here. After this * nothing crosses the boundary of the cell any more, so this is where it is @@ -576,6 +690,8 @@ SYSCALL_DEFINE5(pidfd_configure, int, pidfd, unsigned int, cmd, ret = embryo_set_strings(&e->argv, uvalue, aux); else if (!strcmp(key, "envp")) ret = embryo_set_strings(&e->envp, uvalue, aux); + else if (!strcmp(key, "cred")) + ret = embryo_set_cred(e, uvalue, aux); else ret = -EINVAL; break; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 44/90] pidfd: let pidfd_configure() name a cgroup to start in 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (42 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 43/90] pidfd: let pidfd_configure() set credentials Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 45/90] ptrace: keep everybody off an embryo Josef Bacik ` (45 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A new task of a cell starts in the budget of the cell. That stops working once the init of the cell manages cgroups. It moves itself out of the budget and enables controllers there, which systemd does right at the start. A cgroup that distributes a controller to its children cannot contain tasks, so from then on every start fails with EBUSY. Spawning is the replacement for entering a container, so this means that nothing can be added to a running cell. Container runtimes have the same problem today and solve it themselves. runc exec accepts a cgroup for the new process. If it is given none and gets EBUSY, it looks up the cgroup of the init of the container and uses that. So let the spawner choose. PIDFD_SET_FD with the key "cgroup" names the cgroup that the task starts in. Which cgroup to use is a policy decision, and it stays with the runtime. For containers that share a cell, like those of a pod, this is also how each of them gets its own cgroup. The cgroup must be the budget or lie below it, because a kill finds the tasks of the cell through the budget. The fork checks this with cgroup_mutex held, along with whether the cgroup still exists and can contain tasks. It has to be checked that late because the cell can change what is below its budget at any time. The task is created in the cgroup rather than moved there, so it is never anywhere else. No permission on the cgroup is required. Whoever holds the cellfd can already do anything to the cell, including killing it, and the permission to put tasks into the budget was checked when the slot was filled. Requiring write access to cgroup.procs would not work in practice: the cgroups that the init of a cell creates are owned by ids that nobody outside of the cell has, so only a builder with CAP_DAC_OVERRIDE would pass. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/sched/task.h | 1 + kernel/cgroup/cgroup.c | 8 ++++++++ kernel/embryo.c | 31 +++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h index 97e3675101bd..83bc58598ea0 100644 --- a/include/linux/sched/task.h +++ b/include/linux/sched/task.h @@ -50,6 +50,7 @@ struct kernel_clone_args { unsigned int kill_seq; struct pid *pid; struct cell *cell; + struct cgroup *cell_cgrp; struct embryo *embryo; }; diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c index 3869cfa6af50..d3a36b586cd3 100644 --- a/kernel/cgroup/cgroup.c +++ b/kernel/cgroup/cgroup.c @@ -6917,6 +6917,14 @@ static int cgroup_css_set_fork_cell(struct kernel_clone_args *kargs, struct cgroup *dst_cgrp = cell_budget(kargs->cell); int ret; + /* Somewhere below, if whoever spawns says so. Nowhere else: a kill goes by the budget. */ + if (kargs->cell_cgrp) { + ret = -EPERM; + if (!cgroup_is_descendant(kargs->cell_cgrp, dst_cgrp)) + goto err; + dst_cgrp = kargs->cell_cgrp; + } + ret = -ENODEV; if (cgroup_is_dead(dst_cgrp)) goto err; diff --git a/kernel/embryo.c b/kernel/embryo.c index 0ad85cd2b865..0a04f39fa37a 100644 --- a/kernel/embryo.c +++ b/kernel/embryo.c @@ -19,6 +19,7 @@ */ #include <linux/binfmts.h> #include <linux/cell.h> +#include <linux/cgroup.h> #include <linux/completion.h> #include <linux/cred.h> #include <linux/fdtable.h> @@ -73,6 +74,7 @@ struct embryo { kuid_t uid; kgid_t gid; struct group_info *groups; + struct cgroup *cgrp; /* where in the budget, or NULL for the budget */ struct pid *pid; /* the reference alloc_pid_struct() returned */ struct cell *cell; /* where the task is going to be */ @@ -133,6 +135,9 @@ static void embryo_drop_config(struct embryo *e) if (e->groups) put_group_info(e->groups); e->groups = NULL; + if (e->cgrp) + cgroup_put(e->cgrp); + e->cgrp = NULL; } static void embryo_put(struct embryo *e) @@ -540,6 +545,26 @@ static int embryo_set_fd(struct embryo *e, const char *key, struct file *file) return 0; } +/* + * Where in the budget of the cell the task starts. Whether it is in there, still + * there and able to hold a task is for the fork to say, with cgroup_mutex held: + * what is below the budget is the cell's to change at any time. + * + * It takes no permission. Whoever holds the cellfd can do anything to the cell, and + * what the cell made in there belongs to ids that nobody outside of it has. + */ +static int embryo_set_cgroup(struct embryo *e, int fd) +{ + struct cgroup *cgrp = cgroup_get_from_fd(fd); + + if (IS_ERR(cgrp)) + return PTR_ERR(cgrp); + if (e->cgrp) + cgroup_put(e->cgrp); + e->cgrp = cgrp; + return 0; +} + static int embryo_start(struct embryo *e) { struct kernel_clone_args args = { @@ -569,6 +594,8 @@ static int embryo_start(struct embryo *e) ret = e->started ? -EBUSY : e->exec ? 0 : -EINVAL; if (!ret) e->started = true; + /* Nobody changes it from here on. */ + args.cell_cgrp = e->cgrp; } if (ret) { cell_fork_finish(e->cell); @@ -698,6 +725,10 @@ SYSCALL_DEFINE5(pidfd_configure, int, pidfd, unsigned int, cmd, case PIDFD_SET_FD: { CLASS(fd, slot)(aux); + if (!strcmp(key, "cgroup")) { + ret = embryo_set_cgroup(e, aux); + break; + } ret = -EBADF; if (!fd_empty(slot)) ret = embryo_set_fd(e, key, fd_file(slot)); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 45/90] ptrace: keep everybody off an embryo 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (43 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 44/90] pidfd: let pidfd_configure() name a cgroup to start in Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 46/90] cells: end a cell when its init exits Josef Bacik ` (44 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Between its creation and the end of its exec, an embryo is in an intermediate state. It already has the credentials of the cell, and it holds files that were placed from the outside. But it has not yet gone through the exec that turns it into something the cell is meant to see. Refuse all ptrace access to an embryo. That covers PTRACE_ATTACH and PTRACE_SEIZE, and the files in /proc that are guarded by a ptrace access check. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/ptrace.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kernel/ptrace.c b/kernel/ptrace.c index 8024d80f82e3..c30e15d39c41 100644 --- a/kernel/ptrace.c +++ b/kernel/ptrace.c @@ -9,6 +9,7 @@ */ #include <linux/capability.h> +#include <linux/cell.h> #include <linux/export.h> #include <linux/sched.h> #include <linux/sched/mm.h> @@ -324,6 +325,9 @@ static int __ptrace_may_access(struct task_struct *task, unsigned int mode) /* Don't let security modules deny introspection */ if (same_thread_group(task, current)) return 0; + /* It is not yet what it will be. Nobody has any business with it. */ + if (task_is_embryo(task)) + return -EPERM; rcu_read_lock(); if (mode & PTRACE_MODE_FSCREDS) { caller_ids = CRED_IDS_FS; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 46/90] cells: end a cell when its init exits 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (44 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 45/90] ptrace: keep everybody off an embryo Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 47/90] cells: refuse a task of a cell a change of namespaces Josef Bacik ` (43 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The first task that is spawned into a cell is the init of its pid namespace. When it exits, the kernel kills everything else in that namespace, as it does for every pid namespace. That is also the end of the cell. Notify the cell from exit_notify(), after zap_pid_ns_processes(). The cell records the exit status and queues a work item for the rest. The work item kills whatever may still be in the budget, waits until the budget is no longer populated, tears the cell down, and only then sets the state to DEAD. The waiting is needed because the init itself keeps the budget populated until it has been scheduled out for the last time. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cell.h | 7 +++++++ kernel/cell/core.c | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ kernel/exit.c | 14 ++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/include/linux/cell.h b/include/linux/cell.h index a68638b7319d..86999efc083d 100644 --- a/include/linux/cell.h +++ b/include/linux/cell.h @@ -228,4 +228,11 @@ static inline bool embryo_pidfd_is_empty(const struct file *file) { return false static inline void embryo_pidfd_release(struct file *file) { } #endif +/* The end of a cell. */ +#ifdef CONFIG_CELLS +void cell_init_exited(struct cell *cell, struct task_struct *tsk); +#else +static inline void cell_init_exited(struct cell *cell, struct task_struct *tsk) { } +#endif + #endif /* _LINUX_CELL_H */ diff --git a/kernel/cell/core.c b/kernel/cell/core.c index b9494b5fd7b9..c780094ad794 100644 --- a/kernel/cell/core.c +++ b/kernel/cell/core.c @@ -172,6 +172,53 @@ static void cell_kill_workfn(struct work_struct *work) cell_put(cell); } +static void cell_exit_workfn(struct work_struct *work) +{ + struct cell *cell = container_of(to_delayed_work(work), struct cell, + exit_work); + + scoped_guard(mutex, &cell->lock) { + if (cell->state == CELL_DEAD) + break; + WRITE_ONCE(cell->state, CELL_DYING); + /* + * DEAD means gone. zap_pid_ns_processes() has reaped everything + * else in the pid namespace, but the init itself is still on + * its way out. It populates the budget until it is off the cpu + * for good. Come back until then. + */ + if (cgroup_cell_kill(cell->budget)) { + queue_delayed_work(system_dfl_wq, &cell->exit_work, + max(HZ / 100, 1)); + return; + } + cell_teardown(cell); + } + cell_put(cell); +} + +/** + * cell_init_exited - the init of the pid namespace of a cell is gone + * @cell: the cell + * @tsk: the last thread of that init, which is the caller + * + * With its init everything else in the pid namespace is gone, and so the cell. + * This notes the exit status and owes the teardown to a work item. + * + * Context: The exit path of @tsk, after zap_pid_ns_processes(), with no locks + * held. The cell has not been torn down, since only this sets that off once a + * cell has run anything. + */ +void cell_init_exited(struct cell *cell, struct task_struct *tsk) +{ + cell->exit_status = (tsk->signal->flags & SIGNAL_GROUP_EXIT) ? + tsk->signal->group_exit_code : + tsk->group_leader->exit_code; + cell_get(cell); + if (!queue_delayed_work(system_dfl_wq, &cell->exit_work, 0)) + cell_put(cell); +} + /* The cellfd. */ static int cell_file_open(struct inode *inode, struct file *file) @@ -286,6 +333,7 @@ SYSCALL_DEFINE2(cell_create, int, parent, unsigned int, flags) mutex_init(&cell->lock); init_waitqueue_head(&cell->wait); INIT_WORK(&cell->kill_work, cell_kill_workfn); + INIT_DELAYED_WORK(&cell->exit_work, cell_exit_workfn); strscpy(cell->hostname, "(none)"); /* The scratch credential is only here to carry the new namespace. */ diff --git a/kernel/exit.c b/kernel/exit.c index d5e7bc2af804..1ae82e7ac29f 100644 --- a/kernel/exit.c +++ b/kernel/exit.c @@ -6,6 +6,7 @@ */ #include <linux/mm.h> +#include <linux/cell.h> #include <linux/slab.h> #include <linux/sched/autogroup.h> #include <linux/sched/mm.h> @@ -741,11 +742,20 @@ static void forget_original_parent(struct task_struct *father, */ static void exit_notify(struct task_struct *tsk, int group_dead) { + struct cell *cell = NULL; bool autoreap; struct task_struct *p, *n; LIST_HEAD(dead); write_lock_irq(&tasklist_lock); + /* + * Is this the end of the init of a cell? Asked of the pid namespace + * and not of the task, and asked now: once the lock is dropped the + * parent may reap us, pids and all. The cell holds on to its pid + * namespace until it is torn down, which only this sets off. + */ + if (group_dead && is_child_reaper(task_tgid(tsk))) + cell = userns_cell(ns_of_pid(task_tgid(tsk))->user_ns); forget_original_parent(tsk, &dead); if (group_dead) @@ -776,6 +786,10 @@ static void exit_notify(struct task_struct *tsk, int group_dead) wake_up_process(tsk->signal->group_exec_task); write_unlock_irq(&tasklist_lock); + /* With its init, and so everything else in it, gone, so is a cell. */ + if (cell) + cell_init_exited(cell, tsk); + list_for_each_entry_safe(p, n, &dead, ptrace_entry) { list_del_init(&p->ptrace_entry); release_task(p); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 47/90] cells: refuse a task of a cell a change of namespaces 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (45 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 46/90] cells: end a cell when its init exits Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 48/90] fs: refuse a task of a cell a new mount namespace Josef Bacik ` (42 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The point of a cell is that inside and outside are fixed once and for all. setns() and unshare() turn that into a property of each task at each moment. Tasks that are half way between two sets of namespaces have been the source of many bugs. For a task of a cell, refuse setns(), unshare(), and clone() with any CLONE_NEW* flag. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/fork.c | 8 ++++++++ kernel/nsproxy.c | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/kernel/fork.c b/kernel/fork.c index 119b59f30f4d..134409910215 100644 --- a/kernel/fork.c +++ b/kernel/fork.c @@ -2080,6 +2080,10 @@ __latent_entropy struct task_struct *copy_process( if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS)) return ERR_PTR(-EINVAL); + /* The namespaces of a cell are the cell's. A task has no say in them. */ + if ((clone_flags & CLONE_NS_ALL) && current_cell()) + return ERR_PTR(-EPERM); + /* * Thread groups must share signals as well, and detached threads * can only be started up within the thread group. @@ -3300,6 +3304,10 @@ int ksys_unshare(unsigned long unshare_flags) int do_sysvsem = 0; int err; + /* The namespaces of a cell are the cell's. A task has no say in them. */ + if ((unshare_flags & (CLONE_NS_ALL | UNSHARE_EMPTY_MNTNS)) && current_cell()) + return -EPERM; + /* * If unsharing a user namespace must also unshare the thread group * and unshare the filesystem root and working directories. diff --git a/kernel/nsproxy.c b/kernel/nsproxy.c index dfe47a235cb5..5d6cec2dc4ce 100644 --- a/kernel/nsproxy.c +++ b/kernel/nsproxy.c @@ -664,6 +664,10 @@ SYSCALL_DEFINE2(setns, int, fd, int, flags) if (fd_empty(f)) return -EBADF; + /* The namespaces of a cell are the cell's. A task has no say in them. */ + if (current_cell()) + return -EPERM; + if (proc_ns_file(fd_file(f))) { ns = get_proc_ns(file_inode(fd_file(f))); if (flags && (ns->ns_type != flags)) -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 48/90] fs: refuse a task of a cell a new mount namespace 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (46 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 47/90] cells: refuse a task of a cell a change of namespaces Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 49/90] nsproxy: refuse to enter a namespace of a cell Josef Bacik ` (41 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik OPEN_TREE_NAMESPACE and FSMOUNT_NAMESPACE create a mount namespace without going through unshare(). A task of a cell could not enter such a namespace, but it should not be able to create namespaces in the first place. Refuse both flags for a task of a cell. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/namespace.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fs/namespace.c b/fs/namespace.c index 7298b589e10e..30167f8863bf 100644 --- a/fs/namespace.c +++ b/fs/namespace.c @@ -3353,6 +3353,9 @@ static struct file *vfs_open_tree(int dfd, const char __user *filename, unsigned * just care about being privileged over our current user namespace. * The new mount namespace will be owned by it. */ + if ((flags & OPEN_TREE_NAMESPACE) && current_cell()) + return ERR_PTR(-EPERM); + if ((flags & OPEN_TREE_NAMESPACE) && !ns_capable(current_user_ns(), CAP_SYS_ADMIN)) return ERR_PTR(-EPERM); @@ -4622,6 +4625,9 @@ SYSCALL_DEFINE3(fsmount, int, fs_fd, unsigned int, flags, if ((flags & ~(FSMOUNT_CLOEXEC | FSMOUNT_NAMESPACE)) != 0) return -EINVAL; + if ((flags & FSMOUNT_NAMESPACE) && current_cell()) + return -EPERM; + if ((flags & FSMOUNT_NAMESPACE) && !ns_capable(current_user_ns(), CAP_SYS_ADMIN)) return -EPERM; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 49/90] nsproxy: refuse to enter a namespace of a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (47 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 48/90] fs: refuse a task of a cell a new mount namespace Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 50/90] cgroup: keep a task of a cell within the budget Josef Bacik ` (40 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik No existing task can enter a cell. Refuse setns() into any namespace that is owned by the user namespace of a cell. This also applies while the cell is still being built. A task that entered the pid namespace at that point could become the init of the cell while it has the credentials of the host. Or it could hold on to the namespace and fork into the cell later. The mount namespace of a cell is owned by the user namespace of the builder, and has its own protection, see mnt_ns_seal_for_cell(). The network namespace is also owned by the user namespace of the builder. It stays accessible to whoever set it up. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/nsproxy.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/kernel/nsproxy.c b/kernel/nsproxy.c index 5d6cec2dc4ce..6b9b220a2bae 100644 --- a/kernel/nsproxy.c +++ b/kernel/nsproxy.c @@ -467,8 +467,21 @@ static int prepare_nsset(unsigned flags, struct nsset *nsset) static inline int validate_ns(struct nsset *nsset, struct ns_common *ns) { + struct user_namespace *owner = ns->ops->owner ? ns->ops->owner(ns) : NULL; int ret; + /* + * Nobody enters a cell, and not while it is being built either. Whoever + * got into the pid namespace then could become the init of the cell, or + * fork into it later. + * + * The mount namespace belongs to the user namespace of whoever built the + * cell and looks after itself, see mnt_ns_seal_for_cell(). So does the + * network namespace, and whoever set that up may go on doing so. + */ + if (owner && userns_is_cell(owner)) + return -EPERM; + ret = security_namespace_install(nsset, ns); if (ret) return ret; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 50/90] cgroup: keep a task of a cell within the budget 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (48 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 49/90] nsproxy: refuse to enter a namespace of a cell Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 51/90] cgroup: let nothing but a spawn into a budget Josef Bacik ` (39 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The kernel finds the tasks of a cell through the budget. A task that got out of the budget would survive a kill, and the cell would be reported dead while that task is still running. Refuse to migrate a task of a cell out of the budget, no matter who asks. Refuse CLONE_INTO_CGROUP by a task of a cell if the target is outside of the budget. On every other hierarchy, refuse to move the task away from the cgroup it was born in. The kernel itself may still move such a task. cgroup_transfer_tasks() empties a legacy cpuset that has lost its last cpu, and those tasks have to go somewhere. It does not operate on the default hierarchy, so it never touches a budget. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cell.h | 7 +++++++ kernel/cell/Makefile | 1 + kernel/cell/boundary.c | 36 ++++++++++++++++++++++++++++++++++++ kernel/cgroup/cgroup.c | 13 +++++++++++++ 4 files changed, 57 insertions(+) diff --git a/include/linux/cell.h b/include/linux/cell.h index 86999efc083d..0c3c65b93f2b 100644 --- a/include/linux/cell.h +++ b/include/linux/cell.h @@ -235,4 +235,11 @@ void cell_init_exited(struct cell *cell, struct task_struct *tsk); static inline void cell_init_exited(struct cell *cell, struct task_struct *tsk) { } #endif +/* A task of a cell stays within the budget. */ +#ifdef CONFIG_CELLS +bool cell_may_migrate(struct task_struct *task, struct cgroup *dst); +#else +static inline bool cell_may_migrate(struct task_struct *t, struct cgroup *d) { return true; } +#endif + #endif /* _LINUX_CELL_H */ diff --git a/kernel/cell/Makefile b/kernel/cell/Makefile index 1a2dcd469d10..0e5bc34808db 100644 --- a/kernel/cell/Makefile +++ b/kernel/cell/Makefile @@ -3,3 +3,4 @@ obj-y += core.o obj-y += slots.o obj-y += ids.o obj-y += spawn.o +obj-y += boundary.o diff --git a/kernel/cell/boundary.c b/kernel/cell/boundary.c new file mode 100644 index 000000000000..251a8516e76a --- /dev/null +++ b/kernel/cell/boundary.c @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Cells: what other subsystems ask about the boundary of a cell. + */ +#include <linux/cgroup.h> +#include <linux/user_namespace.h> +#include <uapi/linux/cell.h> + +#include "internal.h" + +/** + * cell_may_migrate - may a task be moved to a cgroup? + * @task: the task + * @dst: the cgroup + * + * A task of a cell stays within the budget of the cell, whoever asks. On a + * hierarchy other than that of the budget no request moves it from where it was + * born. This is asked for every request. It is not asked when the kernel itself + * empties a legacy cpuset that lost its last cpu, see cgroup_transfer_tasks(), + * which works on legacy hierarchies only and so never touches a budget. + * + * Return: true if @task is in no cell, or @dst is the budget of its cell or + * lies below it. + */ +bool cell_may_migrate(struct task_struct *task, struct cgroup *dst) +{ + struct cgroup *budget; + struct cell *cell; + + guard(rcu)(); + cell = task_cell(task); + if (!cell) + return true; + budget = READ_ONCE(cell->budget); + return budget && cgroup_is_descendant(dst, budget); +} diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c index d3a36b586cd3..1286f5d318b0 100644 --- a/kernel/cgroup/cgroup.c +++ b/kernel/cgroup/cgroup.c @@ -3102,6 +3102,14 @@ int cgroup_attach_task(struct cgroup *dst_cgrp, struct task_struct *leader, struct task_struct *task; int ret = 0; + /* + * A task of a cell stays within the cell's budget, whoever asks. On + * any other hierarchy no request moves it from where it was born. The + * kernel itself may, with cgroup_transfer_tasks(), out of a legacy + * cpuset that lost its last cpu. That never touches a budget. + */ + if (!cell_may_migrate(leader, dst_cgrp)) + return -EPERM; /* look up all src csets */ spin_lock_irq(&css_set_lock); task = leader; @@ -7011,6 +7019,11 @@ static int cgroup_css_set_fork(struct kernel_clone_args *kargs) goto err; } + /* A task of a cell stays within the cell's budget, children and all. */ + if (!cell_may_migrate(current, dst_cgrp)) { + ret = -EPERM; + goto err; + } /* * Verify that we the target cgroup is writable for us. This is * usually done by the vfs layer but since we're not going through -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 51/90] cgroup: let nothing but a spawn into a budget 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (49 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 50/90] cgroup: keep a task of a cell within the budget Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 52/90] kernfs: let a filesystem refuse a change to a node Josef Bacik ` (38 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A kill has to remove everything from the budget, and the cell is reported dead once the budget is empty. In its kthread mode, vhost attaches a kernel thread to the cgroups of the owner of the device with cgroup_attach_task_all(). A kill does not remove a kernel thread. So a cell that was given a vhost device could prevent itself from ever being reported dead. Refuse to move a task that is in no cell into a budget or below one. Do this in cgroup_attach_task(), which every way of attaching a task goes through, and for CLONE_INTO_CGROUP. Budgets do not nest and a task of a cell stays within its own budget, so no other case needs a check. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cgroup/cgroup.c | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c index 1286f5d318b0..3fc1f9ac2268 100644 --- a/kernel/cgroup/cgroup.c +++ b/kernel/cgroup/cgroup.c @@ -3087,6 +3087,30 @@ int cgroup_migrate(struct task_struct *leader, bool threadgroup, return cgroup_migrate_execute(mgctx); } +/* + * May @task, which cell_may_migrate() has no objection to, be put into @dst? + * Budgets do not nest, so a task of a cell is within its own. + */ +static bool cgroup_cell_may_enter(struct task_struct *task, struct cgroup *dst) +{ + bool in_cell; + + if (!IS_ENABLED(CONFIG_CELLS)) + return true; + + rcu_read_lock(); + in_cell = task_cell(task); + rcu_read_unlock(); + if (in_cell) + return true; + + for (; dst; dst = cgroup_parent(dst)) { + if (test_bit(CGRP_CELL_BUDGET, &dst->flags)) + return false; + } + return true; +} + /** * cgroup_attach_task - attach a task or a whole threadgroup to a cgroup * @dst_cgrp: the cgroup to attach to @@ -3110,6 +3134,14 @@ int cgroup_attach_task(struct cgroup *dst_cgrp, struct task_struct *leader, */ if (!cell_may_migrate(leader, dst_cgrp)) return -EPERM; + + /* + * Nothing else gets into a budget. A kill is to take everything there + * with it, which it could not do to a kernel thread. + */ + if (!cgroup_cell_may_enter(leader, dst_cgrp)) + return -EPERM; + /* look up all src csets */ spin_lock_irq(&css_set_lock); task = leader; @@ -7024,6 +7056,13 @@ static int cgroup_css_set_fork(struct kernel_clone_args *kargs) ret = -EPERM; goto err; } + + /* And nobody else forks into one. */ + if (!cgroup_cell_may_enter(current, dst_cgrp)) { + ret = -EPERM; + goto err; + } + /* * Verify that we the target cgroup is writable for us. This is * usually done by the vfs layer but since we're not going through -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 52/90] kernfs: let a filesystem refuse a change to a node 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (50 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 51/90] cgroup: let nothing but a spawn into a budget Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 53/90] cgroup: let a cell change nothing outside its budget Josef Bacik ` (37 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Whether a node of a kernfs based filesystem may be changed is decided by its mode and owner, in the VFS and in kernfs. The filesystem on top of kernfs sees some of the changes, such as a write to a file or a mkdir. It never sees others: chmod, chown, a timestamp update, an extended attribute. If it has a rule that is based on something other than mode and owner, it can only enforce that rule for the first kind. Add a may_change() callback to kernfs_syscall_ops. It is called in addition to the mode check, everywhere a change can come from: - when write permission is checked, which covers opening a file for writing and creating, removing or renaming a directory entry - on every write to a file, because the writer may not be the task that opened it - before an attribute or an extended attribute is set or removed The callback is told whether the change is to the content of the node or to its attributes. A filesystem may want to let a task create entries in a directory without letting it change the mode of that directory. Truncating a file counts as a change to its content. That is what an open with O_TRUNC does, which every shell redirection uses, and kernfs ignores the size anyway. No filesystem implements the callback yet. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/kernfs/file.c | 5 +++++ fs/kernfs/inode.c | 25 +++++++++++++++++++++++++ fs/kernfs/kernfs-internal.h | 11 +++++++++++ include/linux/kernfs.h | 15 +++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/fs/kernfs/file.c b/fs/kernfs/file.c index cca9f83fc9b5..27e234c4aea0 100644 --- a/fs/kernfs/file.c +++ b/fs/kernfs/file.c @@ -307,6 +307,11 @@ static ssize_t kernfs_fop_write_iter(struct kiocb *iocb, struct iov_iter *iter) ssize_t len = iov_iter_count(iter); const struct kernfs_ops *ops; char *buf; + int ret; + + ret = kernfs_may_change(of->kn, KERNFS_CHANGE_CONTENT); + if (ret) + return ret; if (of->atomic_write_len) { if (len > of->atomic_write_len) diff --git a/fs/kernfs/inode.c b/fs/kernfs/inode.c index 6630b29d7c07..efefd40161d6 100644 --- a/fs/kernfs/inode.c +++ b/fs/kernfs/inode.c @@ -112,12 +112,21 @@ int kernfs_iop_setattr(const struct mnt_idmap *idmap, struct dentry *dentry, { struct inode *inode = d_inode(dentry); struct kernfs_node *kn = inode->i_private; + enum kernfs_change what = KERNFS_CHANGE_ATTRS; struct kernfs_root *root; int error; if (!kn) return -EINVAL; + /* Truncating a file is writing to it, which is what O_TRUNC is for. */ + if ((iattr->ia_valid & ATTR_SIZE) && + !(iattr->ia_valid & (ATTR_MODE | ATTR_UID | ATTR_GID))) + what = KERNFS_CHANGE_CONTENT; + error = kernfs_may_change(kn, what); + if (error) + return error; + root = kernfs_root(kn); down_write(&root->kernfs_iattr_rwsem); error = setattr_prepare(&nop_mnt_idmap, dentry, iattr); @@ -283,6 +292,12 @@ int kernfs_iop_permission(const struct mnt_idmap *idmap, kn = inode->i_private; root = kernfs_root(kn); + if (mask & MAY_WRITE) { + ret = kernfs_may_change(kn, KERNFS_CHANGE_CONTENT); + if (ret) + return ret; + } + down_read(&root->kernfs_iattr_rwsem); kernfs_refresh_inode(kn, inode); ret = generic_permission(&nop_mnt_idmap, inode, mask); @@ -349,6 +364,11 @@ static int kernfs_vfs_xattr_set(const struct xattr_handler *handler, { const char *name = xattr_full_name(handler, suffix); struct kernfs_node *kn = inode->i_private; + int ret; + + ret = kernfs_may_change(kn, KERNFS_CHANGE_ATTRS); + if (ret) + return ret; return kernfs_xattr_set(kn, name, value, size, flags); } @@ -362,10 +382,15 @@ static int kernfs_vfs_user_xattr_set(const struct xattr_handler *handler, const char *full_name = xattr_full_name(handler, suffix); struct kernfs_node *kn = inode->i_private; struct kernfs_iattrs *attrs; + int ret; if (!(kernfs_root(kn)->flags & KERNFS_ROOT_SUPPORT_USER_XATTR)) return -EOPNOTSUPP; + ret = kernfs_may_change(kn, KERNFS_CHANGE_ATTRS); + if (ret) + return ret; + attrs = kernfs_iattrs(kn); if (!attrs) return -ENOMEM; diff --git a/fs/kernfs/kernfs-internal.h b/fs/kernfs/kernfs-internal.h index f1e93b09e27d..5d1d4e282556 100644 --- a/fs/kernfs/kernfs-internal.h +++ b/fs/kernfs/kernfs-internal.h @@ -127,6 +127,17 @@ static inline const char *kernfs_rcu_name(const struct kernfs_node *kn) kernfs_rename_is_locked(kn)); } +/* See kernfs_syscall_ops::may_change. */ +static inline int kernfs_may_change(struct kernfs_node *kn, + enum kernfs_change what) +{ + struct kernfs_syscall_ops *scops = kernfs_root(kn)->syscall_ops; + + if (scops && scops->may_change) + return scops->may_change(kn, what); + return 0; +} + static inline struct kernfs_node *kernfs_parent(const struct kernfs_node *kn) { /* diff --git a/include/linux/kernfs.h b/include/linux/kernfs.h index 73786b567ccd..62bdb77961e3 100644 --- a/include/linux/kernfs.h +++ b/include/linux/kernfs.h @@ -243,6 +243,12 @@ struct kernfs_node { * the exact operation requested. An active reference is held for each * kernfs_node parameter. */ +/* What kernfs_syscall_ops::may_change is asked about. */ +enum kernfs_change { + KERNFS_CHANGE_CONTENT, /* what a file holds, size included, the entries of a directory */ + KERNFS_CHANGE_ATTRS, /* mode, owner, times, extended attributes */ +}; + struct kernfs_syscall_ops { int (*show_options)(struct seq_file *sf, struct kernfs_root *root); @@ -253,6 +259,15 @@ struct kernfs_syscall_ops { const char *new_name); int (*show_path)(struct seq_file *sf, struct kernfs_node *kn, struct kernfs_root *root); + /* + * May the caller change @kn? Asked on top of what the mode says, and + * before it: when permission to write is asked for, which is what + * opening a file for writing and making, removing or renaming an entry + * of a directory take, for every write to a file, whoever opened it, and + * before an attribute or an extended attribute is set or removed. No + * active reference is held on @kn. Returns 0 or a negative error. + */ + int (*may_change)(struct kernfs_node *kn, enum kernfs_change what); }; struct kernfs_node *kernfs_root_to_node(struct kernfs_root *root); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 53/90] cgroup: let a cell change nothing outside its budget 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (51 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 52/90] kernfs: let a filesystem refuse a change to a node Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 54/90] cgroup: leave the limits of a budget to who is above Josef Bacik ` (36 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Whatever a task of a cell creates is owned by ids that are only lent to the cell, and must not outlive the cell. The filesystem helpers that decide whether an id can be stored take care of that, but not everything in cgroupfs goes through them. Enabling a controller in cgroup.subtree_control adds files to every child of the cgroup. cgroup_add_file() assigns those files to the writer, directly through kernfs. Below the budget that is fine, because cgroup_cell_budget_put() removes everything there. Anywhere else the files remain, and would belong to the next cell that is lent the block. For that to happen, a builder has to expose more of cgroupfs than the budget, and there has to be a cgroup.subtree_control out there that the cell can write to. Neither should be the case. But the rule that a lent id is never left behind should not depend on nobody making a mistake. Modes and owners do not help with a cell that has a single id either. It runs with the id of the user who created it, so it owns every cgroup that was delegated to that user. It could rename cgroups of a legacy hierarchy, change their modes, and replace or exhaust the extended attributes that a service manager keeps on them. Implement may_change() for cgroupfs. Refuse every change by a task of a cell to a cgroup that is not within its budget, whatever the mode and the owner are. The check looks at the task that makes the change, not at the task that opened the file, because the file may have been passed to the cell. No cgroup of a legacy hierarchy is within a budget, so a task of a cell cannot change anything there. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cell.h | 7 +++++++ kernel/cell/boundary.c | 26 ++++++++++++++++++++++++++ kernel/cgroup/cgroup-internal.h | 1 + kernel/cgroup/cgroup-v1.c | 1 + kernel/cgroup/cgroup.c | 19 +++++++++++++++++++ 5 files changed, 54 insertions(+) diff --git a/include/linux/cell.h b/include/linux/cell.h index 0c3c65b93f2b..2107cd3c994b 100644 --- a/include/linux/cell.h +++ b/include/linux/cell.h @@ -242,4 +242,11 @@ bool cell_may_migrate(struct task_struct *task, struct cgroup *dst); static inline bool cell_may_migrate(struct task_struct *t, struct cgroup *d) { return true; } #endif +/* A task of a cell changes nothing outside of the budget. */ +#ifdef CONFIG_CELLS +bool cell_may_change_cgroup(struct cgroup *cgrp); +#else +static inline bool cell_may_change_cgroup(struct cgroup *cgrp) { return true; } +#endif + #endif /* _LINUX_CELL_H */ diff --git a/kernel/cell/boundary.c b/kernel/cell/boundary.c index 251a8516e76a..3f5cc898dc48 100644 --- a/kernel/cell/boundary.c +++ b/kernel/cell/boundary.c @@ -34,3 +34,29 @@ bool cell_may_migrate(struct task_struct *task, struct cgroup *dst) budget = READ_ONCE(cell->budget); return budget && cgroup_is_descendant(dst, budget); } + +/** + * cell_may_change_cgroup - may the caller change a cgroup? + * @cgrp: the cgroup + * + * A task of a cell changes nothing outside of the budget of the cell, whatever + * the modes say and whoever owns the cgroup, which a cell of one id does if its + * maker does. It writes to no file, makes, removes and renames no cgroup, and + * sets no mode, owner, time or extended attribute. See cgroup_may_change() for + * where this is asked. + * + * What it makes is owned by ids that were only lent to the cell. Some of that is + * made without the filesystem being asked whether it can store the id, such as + * the files that enabling a controller adds to the children of a cgroup. So it + * has to be where it goes with the cell, see cgroup_cell_budget_put(). + * + * It goes by who is asking and not by who opened the file, which may have been + * handed to the cell. + * + * Return: true if the caller is in no cell, or @cgrp is the budget of its cell or + * lies below it. + */ +bool cell_may_change_cgroup(struct cgroup *cgrp) +{ + return cell_may_migrate(current, cgrp); +} diff --git a/kernel/cgroup/cgroup-internal.h b/kernel/cgroup/cgroup-internal.h index 233fb8b45d2c..71744b453df5 100644 --- a/kernel/cgroup/cgroup-internal.h +++ b/kernel/cgroup/cgroup-internal.h @@ -258,6 +258,7 @@ int cgroup_lock_and_drain_offline(struct cgroup *cgrp); int cgroup_mkdir(struct kernfs_node *parent_kn, const char *name, umode_t mode); int cgroup_rmdir(struct kernfs_node *kn); +int cgroup_may_change(struct kernfs_node *kn, enum kernfs_change what); int cgroup_show_path(struct seq_file *sf, struct kernfs_node *kf_node, struct kernfs_root *kf_root); diff --git a/kernel/cgroup/cgroup-v1.c b/kernel/cgroup/cgroup-v1.c index 216a2dcdde86..4c27e63774e6 100644 --- a/kernel/cgroup/cgroup-v1.c +++ b/kernel/cgroup/cgroup-v1.c @@ -1145,6 +1145,7 @@ struct kernfs_syscall_ops cgroup1_kf_syscall_ops = { .mkdir = cgroup_mkdir, .rmdir = cgroup_rmdir, .show_path = cgroup_show_path, + .may_change = cgroup_may_change, }; /* diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c index 3fc1f9ac2268..accae30a901d 100644 --- a/kernel/cgroup/cgroup.c +++ b/kernel/cgroup/cgroup.c @@ -6488,11 +6488,30 @@ int cgroup_rmdir(struct kernfs_node *kn) return ret; } +/* Whatever the modes say, and whoever owns it. */ +int cgroup_may_change(struct kernfs_node *kn, enum kernfs_change what) +{ + struct cgroup *cgrp; + + if (likely(!current_cell())) + return 0; + + /* @kn may have been removed, and the cgroup may be on its way out. */ + guard(rcu)(); + if (kernfs_type(kn) != KERNFS_DIR) + kn = rcu_dereference(kn->__parent); + cgrp = rcu_dereference(*(void __rcu __force **)&kn->priv); + if (!cgrp || !cell_may_change_cgroup(cgrp)) + return -EPERM; + return 0; +} + static struct kernfs_syscall_ops cgroup_kf_syscall_ops = { .show_options = cgroup_show_options, .mkdir = cgroup_mkdir, .rmdir = cgroup_rmdir, .show_path = cgroup_show_path, + .may_change = cgroup_may_change, }; static void __init cgroup_init_subsys(struct cgroup_subsys *ss, bool early) -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 54/90] cgroup: leave the limits of a budget to who is above 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (52 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 53/90] cgroup: let a cell change nothing outside its budget Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 55/90] fs: let a filesystem hold ids that go with the cell Josef Bacik ` (35 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The budget limits what a cell can use. The limits are set by the builder. The seal gives the root user of the cell ownership of the directory, cgroup.procs, cgroup.threads and cgroup.subtree_control, and of nothing else. That relies on modes and owners, which is not enough: - The files of a cgroup belong to whoever created it. An unprivileged builder creates the budget itself, so it owns the limits. A cell with a single id runs with the id of that builder, so it can write whatever it likes to its own pids.max and memory.max. - A cell with a block of ids can do the same through a file that somebody outside of the cell has opened and passed to it. - The cell owns the directory, so it can make it accessible to everybody on the host and set extended attributes on it. cgroup already has the right rule, for cgroup namespaces with the nsdelegate mount option. From inside of the namespace, the only files in its root that can be written are those marked CFTYPE_NS_DELEGATABLE. The cgroup namespace of a cell is rooted at the budget. But that rule depends on how cgroupfs was mounted, it looks at the task that opened the file, and it does not cover attributes. So apply the rule to every task of a cell, in cgroup_may_change(). In the budget itself such a task can create and remove cgroups and write to the delegatable files. It cannot write to any other file there, and it cannot set attributes on the directory or on the files in it. Below the budget there are no restrictions. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cgroup/cgroup.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c index accae30a901d..a0836b115c79 100644 --- a/kernel/cgroup/cgroup.c +++ b/kernel/cgroup/cgroup.c @@ -6488,9 +6488,25 @@ int cgroup_rmdir(struct kernfs_node *kn) return ret; } +/* + * Is this what a cgroup is handed to somebody for? Making cgroups in it, and what + * is written to across a delegation boundary: see CGRP_ROOT_NS_DELEGATE, which + * goes by who opened the file and leaves the attributes to their owner. + */ +static bool cgroup_change_is_delegated(struct kernfs_node *kn, + enum kernfs_change what) +{ + const struct cftype *cft = kn->priv; + + if (what != KERNFS_CHANGE_CONTENT) + return false; + return kernfs_type(kn) == KERNFS_DIR || (cft->flags & CFTYPE_NS_DELEGATABLE); +} + /* Whatever the modes say, and whoever owns it. */ int cgroup_may_change(struct kernfs_node *kn, enum kernfs_change what) { + struct kernfs_node *asked = kn; struct cgroup *cgrp; if (likely(!current_cell())) @@ -6503,6 +6519,10 @@ int cgroup_may_change(struct kernfs_node *kn, enum kernfs_change what) cgrp = rcu_dereference(*(void __rcu __force **)&kn->priv); if (!cgrp || !cell_may_change_cgroup(cgrp)) return -EPERM; + /* The budget is what bounds the cell. Its limits are not the cell's. */ + if (test_bit(CGRP_CELL_BUDGET, &cgrp->flags) && + !cgroup_change_is_delegated(asked, what)) + return -EPERM; return 0; } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 55/90] fs: let a filesystem hold ids that go with the cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (53 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 54/90] cgroup: leave the limits of a budget to who is above Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 56/90] cgroup: let a cell make cgroups below its budget Josef Bacik ` (34 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A filesystem that a cell does not own cannot hold an id that was lent to the cell, because the id would outlive the cell. That is the right default. It is wrong for a filesystem whose objects do not outlive the cell either, because the kernel removes them together with the cell. Add the superblock flag SB_I_LENT_IDS, with which a filesystem declares that this is the case. The filesystem promises two things: whatever a cell owns there is removed with the cell, and the filesystem itself controls where a task of a cell can create anything. In return, a task of a cell can create objects there and can chown them to another id of its cell. Nobody else can. sb_takes_lent_kuid() and sb_takes_lent_kgid() require that the caller is in a cell and that the id belongs to that cell. So root on the host still cannot name such an id, and neither can a task of another cell. The helpers are called where a superblock is at hand: in fsuidgid_has_mapping() for a create, and in notify_change() for a chown. No filesystem sets the flag yet. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/attr.c | 8 ++++++-- include/linux/fs.h | 40 ++++++++++++++++++++++++++++++++++++++++ include/linux/fs/super_types.h | 1 + 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/fs/attr.c b/fs/attr.c index 9706d370f34a..10cf1b3827c6 100644 --- a/fs/attr.c +++ b/fs/attr.c @@ -520,11 +520,15 @@ int notify_change(const struct mnt_idmap *idmap, struct dentry *dentry, */ if (ia_valid & ATTR_UID && !vfsuid_has_fsmapping(idmap, inode->i_sb->s_user_ns, - attr->ia_vfsuid)) + attr->ia_vfsuid) && + !sb_takes_lent_kuid(inode->i_sb, + from_vfsuid(idmap, i_user_ns(inode), attr->ia_vfsuid))) return -EOVERFLOW; if (ia_valid & ATTR_GID && !vfsgid_has_fsmapping(idmap, inode->i_sb->s_user_ns, - attr->ia_vfsgid)) + attr->ia_vfsgid) && + !sb_takes_lent_kgid(inode->i_sb, + from_vfsgid(idmap, i_user_ns(inode), attr->ia_vfsgid))) return -EOVERFLOW; /* Don't allow modifications of files with invalid uids or diff --git a/include/linux/fs.h b/include/linux/fs.h index ea9b9d5a667d..e1933649e885 100644 --- a/include/linux/fs.h +++ b/include/linux/fs.h @@ -1565,6 +1565,44 @@ static inline void inode_fsgid_set(struct inode *inode, inode->i_gid = mapped_fsgid(idmap, i_user_ns(inode)); } +/** + * sb_takes_lent_kuid - may the caller have a filesystem hold a kuid of its cell? + * @sb: the superblock + * @kuid: the kuid, after the idmapping of the mount has been applied + * + * The ids of a cell are only lent to it, so a filesystem that the cell does not + * own does not hold them, see kuid_storable(). One with SB_I_LENT_IDS sees to it + * that what a cell owns there goes with the cell, and to where a task of a cell + * makes anything. So a task of a cell can have it hold the ids of that cell. + * Nobody else can: whoever is not in the cell is not held to any place. + * + * Return: true if it may. + */ +static inline bool sb_takes_lent_kuid(const struct super_block *sb, kuid_t kuid) +{ + return (sb->s_iflags & SB_I_LENT_IDS) && + userns_is_cell(current_user_ns()) && + kuid_has_mapping(current_user_ns(), kuid) && + kuid_has_mapping(sb->s_user_ns, kuid); +} + +/** + * sb_takes_lent_kgid - may the caller have a filesystem hold a kgid of its cell? + * @sb: the superblock + * @kgid: the kgid, after the idmapping of the mount has been applied + * + * See sb_takes_lent_kuid(). + * + * Return: true if it may. + */ +static inline bool sb_takes_lent_kgid(const struct super_block *sb, kgid_t kgid) +{ + return (sb->s_iflags & SB_I_LENT_IDS) && + userns_is_cell(current_user_ns()) && + kgid_has_mapping(current_user_ns(), kgid) && + kgid_has_mapping(sb->s_user_ns, kgid); +} + /** * fsuidgid_has_mapping() - check whether caller's fsuid/fsgid is mapped * @sb: the superblock we want a mapping in @@ -1587,6 +1625,8 @@ static inline bool fsuidgid_has_mapping(struct super_block *sb, if (!uid_valid(kuid)) return false; kgid = mapped_fsgid(idmap, fs_userns); + if (sb_takes_lent_kuid(sb, kuid) && sb_takes_lent_kgid(sb, kgid)) + return true; if (!gid_valid(kgid)) return false; return kuid_has_fsmapping(fs_userns, kuid) && diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h index 72f4288e16c5..727a3c0a72dd 100644 --- a/include/linux/fs/super_types.h +++ b/include/linux/fs/super_types.h @@ -359,5 +359,6 @@ struct super_block { #define SB_I_NOIDMAP 0x00002000 /* No idmapped mounts on this superblock */ #define SB_I_ALLOW_HSM 0x00004000 /* Allow HSM events on this superblock */ #define SB_I_NO_DATA_INTEGRITY 0x00008000 /* fs cannot guarantee data persistence on sync */ +#define SB_I_LENT_IDS 0x00010000 /* What a cell owns here goes with the cell */ #endif /* _LINUX_FS_SUPER_TYPES_H */ -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 56/90] cgroup: let a cell make cgroups below its budget 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (54 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 55/90] fs: let a filesystem hold ids that go with the cell Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 57/90] resctrl: let a cell change nothing Josef Bacik ` (33 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik An init that manages cgroups moves itself out of the root of its hierarchy, enables controllers there, and creates one cgroup per service. In a cell with a block of ids this fails at the first mkdir, with EOVERFLOW. cgroupfs is a filesystem of the host, so it cannot hold the ids of a cell. The init therefore never leaves the budget, and it can never enable a controller, because a cgroup that distributes a controller cannot contain tasks. The builder cannot create the cgroups and hand them over either, because nobody can name the ids of a cell. cgroupfs meets the conditions for holding those ids: - cgroup_cell_budget_put() removes everything below the budget when the cell is gone. If that fails, the block is never lent again. - cgroup_may_change() stops a task of a cell from creating or changing anything outside of its budget. - The seal already stores an id of the cell on the budget itself. So set SB_I_LENT_IDS on the superblock of the default hierarchy. The alternative would be to make cgroupfs idmappable. The ids that are stored would then be 0 to 65535. The limits of the budget, which belong to root on the host, would appear to belong to the root user of the cell. And uid 1000 on the host would own what uid 1000 of the cell created. Legacy hierarchies do not get the flag, because none of their cgroups is within a budget. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cgroup/cgroup.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c index a0836b115c79..3f439ae0e3b7 100644 --- a/kernel/cgroup/cgroup.c +++ b/kernel/cgroup/cgroup.c @@ -2331,6 +2331,9 @@ static int cgroup_get_tree(struct fs_context *fc) ret = cgroup_do_get_tree(fc); if (!ret) apply_cgroup_root_flags(ctx->flags); + /* See cgroup_may_change() for where, cgroup_cell_budget_put() for how. */ + if (!ret) + fc->root->d_sb->s_iflags |= SB_I_LENT_IDS; return ret; } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 57/90] resctrl: let a cell change nothing 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (55 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 56/90] cgroup: let a cell make cgroups below its budget Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 58/90] cred: let no id count across cells Josef Bacik ` (32 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik resctrl is delegated the same way as cgroupfs. A group belongs to whoever created it, and the owner can write to its files, create groups below it, and remove and rename them. A cell with a single id runs with the id of the user who created it, so it owns every group that this user owns. Moving a task to another group through the tasks file is already refused across cells. But other operations affect tasks without checking them: - Removing a group moves its tasks to the parent. - Renaming a monitoring group moves its tasks to another control group. - A write to schemata, cpus or mode changes the conditions that the tasks of the group run under. Creating groups also uses up hardware ids, of which there are only a few for the whole machine. All of this requires a builder that gives the cell access to resctrl. It should not do that, but the boundary of a cell should not depend on nobody making a mistake. No part of resctrl can belong to a cell. So implement may_change() and refuse every change by a task of a cell. Besides cgroupfs, resctrl is the only kernfs based filesystem that is delegated by changing ownership. Compile tested only. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/resctrl/rdtgroup.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fs/resctrl/rdtgroup.c b/fs/resctrl/rdtgroup.c index 6b7b7daa9fd3..e7f2ec56f371 100644 --- a/fs/resctrl/rdtgroup.c +++ b/fs/resctrl/rdtgroup.c @@ -12,6 +12,7 @@ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt +#include <linux/cell.h> #include <linux/cpu.h> #include <linux/debugfs.h> #include <linux/fs.h> @@ -4451,11 +4452,18 @@ static int rdtgroup_show_options(struct seq_file *seq, struct kernfs_root *kf) return 0; } +/* Nothing here is a cell's, whatever the modes say and whoever owns it. */ +static int rdtgroup_may_change(struct kernfs_node *kn, enum kernfs_change what) +{ + return current_cell() ? -EPERM : 0; +} + static struct kernfs_syscall_ops rdtgroup_kf_syscall_ops = { .mkdir = rdtgroup_mkdir, .rmdir = rdtgroup_rmdir, .rename = rdtgroup_rename, .show_options = rdtgroup_show_options, + .may_change = rdtgroup_may_change, }; static int rdtgroup_setup_root(struct rdt_fs_context *ctx) -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 58/90] cred: let no id count across cells 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (56 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 57/90] resctrl: let a cell change nothing Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 59/90] ptrace: refuse PTRACE_TRACEME " Josef Bacik ` (31 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A cell with a single id runs with the kuid of the user who created it. That causes problems in both directions: - The pid namespace prevents the cell from naming tasks outside of it. But the pids of the cell are visible from above. So every other task of that user passes the checks that compare uids, and can signal, renice or limit the tasks of the cell. The authority over a cell is supposed to be the cellfd. - The parent of a task that was spawned into a cell is the spawner, which is outside of the cell. With KEYCTL_SESSION_TO_PARENT the task could replace the session keyring of the spawner with one that it controls. The ids of two credentials can only be compared if both belong to the same cell. All the rules that let one task act on another because of its ids are now in one place, so the restriction can be added once. Add cred_ids_comparable(), and make every rule call it first. That covers signals and SIGIO, scheduling attributes and nice values, I/O priorities, resource limits, ptrace, moving a task in a cgroup v1 hierarchy or between resctrl groups, and KEYCTL_SESSION_TO_PARENT. It also applies to euid 0. Some of these rules let a task with euid 0 send SIGIO to any task, or move any task in a cgroup v1 hierarchy or between resctrl groups. A cell is a hard boundary for root as well. A cell that misbehaves can be killed and built again. The capability checks that sit next to most of these rules are not changed. A capability that is held in an ancestor user namespace still applies to a task of a cell for now. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cred.c | 28 ++++++++++++++++++++++++++++ kernel/cred_kunit.c | 5 +++++ 2 files changed, 33 insertions(+) diff --git a/kernel/cred.c b/kernel/cred.c index 3aa1b1e3eade..72df625f8c99 100644 --- a/kernel/cred.c +++ b/kernel/cred.c @@ -577,6 +577,10 @@ void __init cred_init(void) * Where it says so, a subject with euid 0 may act on anybody. That is a * capability by another name, from before there were any. * + * It is also the one place to say when the ids of two credentials are not to be + * compared at all, in which case every rule says no. They are not across cells, + * see Documentation/userspace-api/cells.rst. + * * These functions only speak for the ids. A capability that does as well, and * the LSM hook that goes with the operation, remain with the caller. * @@ -588,6 +592,18 @@ void __init cred_init(void) #define CRED_EUID BIT(1) #define CRED_SUID BIT(2) +/* + * Are the ids of two credentials to be compared at all? Every rule below asks + * this first. + * + * Across cells an id means nothing, 0 included. A one-id cell runs on the ids + * of whoever made it, and authority over a cell is a handle. + */ +static bool cred_ids_comparable(const struct cred *subj, const struct cred *obj) +{ + return cell_same(subj, obj); +} + static bool cred_euid_is_root(const struct cred *subj) { return uid_eq(subj->euid, GLOBAL_ROOT_UID); @@ -607,6 +623,9 @@ static bool cred_has_uid(const struct cred *cred, unsigned int ids, kuid_t uid) static bool cred_uid_match_any(const struct cred *subj, unsigned int subj_ids, const struct cred *obj, unsigned int obj_ids) { + if (!cred_ids_comparable(subj, obj)) + return false; + return ((subj_ids & CRED_RUID) && cred_has_uid(obj, obj_ids, subj->uid)) || ((subj_ids & CRED_EUID) && cred_has_uid(obj, obj_ids, subj->euid)); } @@ -633,6 +652,9 @@ bool cred_ids_allow_signal(const struct cred *subj, const struct cred *obj) */ bool cred_ids_allow_sigio(const struct cred *subj, const struct cred *obj) { + if (!cred_ids_comparable(subj, obj)) + return false; + return cred_euid_is_root(subj) || cred_ids_allow_signal(subj, obj); } @@ -671,6 +693,9 @@ bool cred_ids_allow_ioprio(const struct cred *subj, const struct cred *obj) */ bool cred_ids_allow_move(const struct cred *subj, const struct cred *obj) { + if (!cred_ids_comparable(subj, obj)) + return false; + return cred_euid_is_root(subj) || cred_uid_match_any(subj, CRED_EUID, obj, CRED_RUID | CRED_SUID); } @@ -693,6 +718,9 @@ bool cred_ids_allow_control(const struct cred *subj, enum cred_ids which, kuid_t uid; kgid_t gid; + if (!cred_ids_comparable(subj, obj)) + return false; + switch (which) { case CRED_IDS_REAL: uid = subj->uid; diff --git a/kernel/cred_kunit.c b/kernel/cred_kunit.c index b3a54e8c565f..3921e8f2716f 100644 --- a/kernel/cred_kunit.c +++ b/kernel/cred_kunit.c @@ -9,6 +9,7 @@ #include <kunit/test.h> #include <linux/cred.h> #include <linux/uidgid.h> +#include <linux/user_namespace.h> /* Root, since two of the rules know it, and two others. */ static const uid_t ids[] = { 0, 1000, 1001 }; @@ -31,6 +32,8 @@ static void uid_combination(unsigned int n, struct cred *cred, struct cred *tcre { memset(cred, 0, sizeof(*cred)); memset(tcred, 0, sizeof(*tcred)); + cred->user_ns = &init_user_ns; + tcred->user_ns = &init_user_ns; set_uids(cred, &n); set_uids(tcred, &n); } @@ -169,6 +172,8 @@ static void cred_ids_control_rule(struct kunit *test) tcred = kunit_kzalloc(test, sizeof(*tcred), GFP_KERNEL); KUNIT_ASSERT_NOT_NULL(test, cred); KUNIT_ASSERT_NOT_NULL(test, tcred); + cred->user_ns = &init_user_ns; + tcred->user_ns = &init_user_ns; for (n = 0; n < NR_CONTROL_COMBINATIONS; n++) { cred->uid = KUIDT_INIT(1000 + !!(n & BIT(0))); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 59/90] ptrace: refuse PTRACE_TRACEME across cells 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (57 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 58/90] cred: let no id count across cells Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 60/90] keys: ignore an equal uid or gid " Josef Bacik ` (30 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The parent of a task that was spawned into a cell is the spawner, which is outside of the cell. PTRACE_TRACEME would turn the spawner into a tracer without its consent. A traced task is not reaped automatically. It would remain a zombie, and zap_pid_ns_processes() would wait for it. A hostile cell could use that to prevent itself from ever being reported dead. Refuse PTRACE_TRACEME if the parent is not in the same cell. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/ptrace.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/kernel/ptrace.c b/kernel/ptrace.c index c30e15d39c41..c6115922ea2e 100644 --- a/kernel/ptrace.c +++ b/kernel/ptrace.c @@ -508,8 +508,20 @@ static int ptrace_attach(struct task_struct *task, long request, static int ptrace_traceme(void) { int ret = -EPERM; + bool same; write_lock_irq(&tasklist_lock); + /* + * The parent of a task that was spawned into a cell is outside of it, + * never asked to be a tracer and will not behave like one. + */ + rcu_read_lock(); + same = cell_same(current_cred(), __task_cred(current->real_parent)); + rcu_read_unlock(); + if (!same) { + write_unlock_irq(&tasklist_lock); + return -EPERM; + } /* Are we already being traced? */ if (!current->ptrace) { ret = security_ptrace_traceme(current->parent); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 60/90] keys: ignore an equal uid or gid across cells 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (58 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 59/90] ptrace: refuse PTRACE_TRACEME " Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 61/90] fs: accept a cellfd for MOUNT_ATTR_IDMAP Josef Bacik ` (29 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik key_task_permission() grants the permissions of the owner to any credential whose fsuid equals the uid of the key. Key serial numbers are global. A cell with a single id runs with the kuid of the user who created it. So a task of the cell can link the user keyring of that user into one of its own keyrings and read the keys in it. Record in each key the id of the cell that its uid and gid belong to. Grant the owner and group permissions only to credentials of that cell. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/key.h | 1 + security/keys/key.c | 2 ++ security/keys/permission.c | 7 +++++++ 3 files changed, 10 insertions(+) diff --git a/include/linux/key.h b/include/linux/key.h index 0bd77fcab4db..c291165e9096 100644 --- a/include/linux/key.h +++ b/include/linux/key.h @@ -215,6 +215,7 @@ struct key { time64_t last_used_at; /* last time used for LRU keyring discard */ kuid_t uid; kgid_t gid; + u64 cell_id; /* the cell those ids are to be read in */ key_perm_t perm; /* access permissions */ unsigned short quotalen; /* length added to quota */ unsigned short datalen; /* payload data length diff --git a/security/keys/key.c b/security/keys/key.c index 0a6e741805d6..bbc594a0cb21 100644 --- a/security/keys/key.c +++ b/security/keys/key.c @@ -6,6 +6,7 @@ */ #include <linux/export.h> +#include <linux/cell.h> #include <linux/init.h> #include <linux/poison.h> #include <linux/sched.h> @@ -331,6 +332,7 @@ struct key *key_alloc(struct key_type *type, const char *desc, key->datalen = type->def_datalen; key->uid = uid; key->gid = gid; + key->cell_id = cred_cell_id(cred); key->perm = perm; key->expiry = TIME64_MAX; key->restrict_link = restrict_link; diff --git a/security/keys/permission.c b/security/keys/permission.c index 4a61f804e80f..7b367c59cd3f 100644 --- a/security/keys/permission.c +++ b/security/keys/permission.c @@ -6,6 +6,7 @@ */ #include <linux/export.h> +#include <linux/cell.h> #include <linux/security.h> #include "internal.h" @@ -50,6 +51,12 @@ int key_task_permission(const key_ref_t key_ref, const struct cred *cred, key = key_ref_to_ptr(key_ref); + /* Across cells an equal uid, or gid, means nothing. */ + if (key->cell_id != cred_cell_id(cred)) { + kperm = key->perm; + goto use_these_perms; + } + /* use the second 8-bits of permissions for keys the caller owns */ if (uid_eq(key->uid, cred->fsuid)) { kperm = key->perm >> 16; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 61/90] fs: accept a cellfd for MOUNT_ATTR_IDMAP 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (59 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 60/90] keys: ignore an equal uid or gid " Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 62/90] fs: let a cell set no set-id bits Josef Bacik ` (28 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The builder of a cell has to idmap the mounts that it puts into the tree of the cell. But it cannot name the ids that the kernel chose for the cell. Accept a cellfd in mount_attr.userns_fd. It means "idmap this mount for the cell": on-disk ids from 0 upwards map to the ids of the cell. This works while the cell is being built, once its ids have been set. mount_setattr() normally requires CAP_SYS_ADMIN over the user namespace. That check stops a caller from mapping file ownership to ids that it does not control. Here the ids belong to the cell and nobody else can hold them, so the check is not needed. The checks that can_idmap_mount() makes on the filesystem are unchanged. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/namespace.c | 13 +++++++++++++ include/linux/cell.h | 7 +++++++ kernel/cell/boundary.c | 25 +++++++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/fs/namespace.c b/fs/namespace.c index 30167f8863bf..e2406d7f7bcc 100644 --- a/fs/namespace.c +++ b/fs/namespace.c @@ -5207,6 +5207,19 @@ static int build_mount_idmapped(const struct mount_attr *attr, size_t usize, if (fd_empty(f)) return -EBADF; + /* + * "For this cell." The ids on the caller's side of the mapping are + * the cell's own, so there is nobody to ask about them. What is asked + * about the filesystem is the same as ever. + */ + mnt_userns = cell_idmap_userns(fd_file(f)); + if (mnt_userns) { + if (IS_ERR(mnt_userns)) + return PTR_ERR(mnt_userns); + kattr->mnt_userns = mnt_userns; + return 0; + } + if (!proc_ns_file(fd_file(f))) return -EINVAL; diff --git a/include/linux/cell.h b/include/linux/cell.h index 2107cd3c994b..f204c47ee783 100644 --- a/include/linux/cell.h +++ b/include/linux/cell.h @@ -249,4 +249,11 @@ bool cell_may_change_cgroup(struct cgroup *cgrp); static inline bool cell_may_change_cgroup(struct cgroup *cgrp) { return true; } #endif +/* A mount is idmapped for a cell. */ +#ifdef CONFIG_CELLS +struct user_namespace *cell_idmap_userns(const struct file *file); +#else +static inline struct user_namespace *cell_idmap_userns(const struct file *f) { return NULL; } +#endif + #endif /* _LINUX_CELL_H */ diff --git a/kernel/cell/boundary.c b/kernel/cell/boundary.c index 3f5cc898dc48..3891dfe899f2 100644 --- a/kernel/cell/boundary.c +++ b/kernel/cell/boundary.c @@ -8,6 +8,31 @@ #include "internal.h" +/** + * cell_idmap_userns - the user namespace to idmap a mount with, by cellfd + * @file: what was passed in mount_attr.userns_fd + * + * A cellfd there means "for this cell". It works while the cell is being built + * and once its ids have been set. + * + * Return: the user namespace of the cell with a reference, NULL if @file is no + * cellfd, or an ERR_PTR(). + */ +struct user_namespace *cell_idmap_userns(const struct file *file) +{ + struct cell *cell = cell_from_file(file); + + if (!cell) + return NULL; + + guard(mutex)(&cell->lock); + if (cell->state != CELL_BUILDING) + return ERR_PTR(-EBUSY); + if (!cell->ids_count) + return ERR_PTR(-EINVAL); + return get_user_ns(cell->user_ns); +} + /** * cell_may_migrate - may a task be moved to a cgroup? * @task: the task -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 62/90] fs: let a cell set no set-id bits 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (60 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 61/90] fs: accept a cellfd for MOUNT_ATTR_IDMAP Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 63/90] fs: drop privileges from what a cell may write to Josef Bacik ` (27 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A file can give a process privilege at exec, through its set-id bits or its file capabilities. Every task of a cell has no_new_privs, so nothing that runs in a cell can gain privilege from a file. But that is only half of the problem. The ids of a cell usually map to on-disk ids from 0 upwards, because that is how container images are stored. If the root user of a cell sets the set-uid bit on a file, the result is a set-uid root file for anybody else who can reach that filesystem. Only directory permissions, and a nosuid mount option that the host has to remember, stand in the way. File capabilities cannot be written without CAP_SETFCAP, which a task of a cell never has. That leaves the mode bits. Clear S_ISUID, and S_ISGID on anything that is not a directory, in two places: in vfs_prepare_mode(), for a file that a task of a cell creates, and in notify_change(), for a mode that it sets. The bits are cleared silently instead of failing the call, so that unpacking an archive keeps working. setattr_prepare() does the same with S_ISGID for a caller that is not in the group of the file. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/attr.c | 5 +++++ fs/namei.c | 3 ++- include/linux/cell.h | 28 ++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/fs/attr.c b/fs/attr.c index 10cf1b3827c6..946fba92b5c8 100644 --- a/fs/attr.c +++ b/fs/attr.c @@ -7,6 +7,7 @@ */ #include <linux/export.h> +#include <linux/cell.h> #include <linux/time.h> #include <linux/mm.h> #include <linux/string.h> @@ -457,6 +458,10 @@ int notify_change(const struct mnt_idmap *idmap, struct dentry *dentry, if (S_ISLNK(inode->i_mode)) return -EOPNOTSUPP; + /* Nothing a cell writes imparts privilege. */ + attr->ia_mode = cell_strip_setid((attr->ia_mode & ~S_IFMT) | + (inode->i_mode & S_IFMT)); + /* Flag setting protected by i_rwsem */ if (is_sxid(attr->ia_mode)) inode->i_flags &= ~S_NOSEC; diff --git a/fs/namei.c b/fs/namei.c index 925aeb63bfa1..49995c353292 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -16,6 +16,7 @@ */ #include <linux/init.h> +#include <linux/cell.h> #include <linux/export.h> #include <linux/slab.h> #include <linux/wordpart.h> @@ -4162,7 +4163,7 @@ static inline umode_t vfs_prepare_mode(const struct mnt_idmap *idmap, mode &= (mask_perms & ~S_IFMT); mode |= (type & S_IFMT); - return mode; + return cell_strip_setid(mode); } /** diff --git a/include/linux/cell.h b/include/linux/cell.h index f204c47ee783..1d0eec34acb9 100644 --- a/include/linux/cell.h +++ b/include/linux/cell.h @@ -256,4 +256,32 @@ struct user_namespace *cell_idmap_userns(const struct file *file); static inline struct user_namespace *cell_idmap_userns(const struct file *f) { return NULL; } #endif +/* Nothing a cell writes imparts privilege: new files and new modes. */ + +/** + * cell_strip_setid - take from a mode what a task of a cell may not set + * @mode: a file mode, file type included + * + * Nothing a cell runs can gain from a file, since every task of a cell has + * no_new_privs. This is the other half: nothing a cell writes can make anybody + * else gain. The ids of a cell commonly map to on-disk ids from 0 up, so a + * set-uid bit set from the inside would make a set-uid root file for whoever + * else looks at that filesystem. + * + * The bits are dropped, not refused, as setattr_prepare() drops S_ISGID for a + * caller that is not in the group of the file: unpacking an archive keeps + * working. On a directory S_ISGID is about inheritance and stays. + * + * Return: @mode, without S_ISUID and S_ISGID if the caller is in a cell. + */ +static inline umode_t cell_strip_setid(umode_t mode) +{ + if (likely(!(mode & (S_ISUID | S_ISGID))) || !current_cell()) + return mode; + mode &= ~S_ISUID; + if (!S_ISDIR(mode)) + mode &= ~S_ISGID; + return mode; +} + #endif /* _LINUX_CELL_H */ -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 63/90] fs: drop privileges from what a cell may write to 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (61 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 62/90] fs: let a cell set no set-id bits Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 64/90] cells: add the "caps" slot Josef Bacik ` (26 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A write() clears the set-id bits and the file capabilities of a file. A store through a shared mapping does not. So a task of a cell could replace the contents of an existing set-uid file while the file keeps the bit. Clear them as soon as a task of a cell opens a file for writing, and when pidfd_configure() places a file that is open for writing. This changes the inode at open time, like O_TRUNC, so it is done in the same place as the truncation, in do_open(). The directory lock has been dropped at that point, which is not the case in ->atomic_open(). The change needs write access to the mount, so it waits if the filesystem is frozen, but only if there is something to clear. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/inode.c | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ fs/namei.c | 8 +++++++ include/linux/cell.h | 7 ++++++ kernel/embryo.c | 6 ++++++ 4 files changed, 82 insertions(+) diff --git a/fs/inode.c b/fs/inode.c index f63535ee3629..d32c14217420 100644 --- a/fs/inode.c +++ b/fs/inode.c @@ -4,6 +4,7 @@ * (C) 1999 Andrea Arcangeli <andrea@suse.de> (dynamic inode allocation) */ #include <linux/export.h> +#include <linux/cell.h> #include <linux/fs.h> #include <linux/filelock.h> #include <linux/mm.h> @@ -2379,6 +2380,66 @@ static int __remove_privs(const struct mnt_idmap *idmap, return notify_change(idmap, dentry, &newattrs, NULL); } +#ifdef CONFIG_CELLS +static int cell_privs_to_kill(struct dentry *dentry) +{ + umode_t mode = d_inode(dentry)->i_mode; + int kill = 0, ret; + + if (mode & S_ISUID) + kill |= ATTR_KILL_SUID; + if (mode & S_ISGID) + kill |= ATTR_KILL_SGID; + ret = security_inode_need_killpriv(dentry); + if (ret < 0) + return ret; + if (ret) + kill |= ATTR_KILL_PRIV; + return kill; +} + +/** + * cell_file_drop_privs - a task of a cell is about to be able to write @file + * @file: a file open for writing + * + * Nothing a cell writes imparts privilege. A write() drops the set-id bits and + * the file capabilities of what it writes to, a store through a shared mapping + * does not. So for a cell they go as soon as the file can be written at all, + * and they all go, whatever capabilities the caller has. + * + * This changes the inode, so the caller must hold no inode lock and no write + * access to the superblock. It waits for a frozen filesystem, but only if there + * is something to drop. + * + * Return: 0 or a negative error, in which case @file must not be handed out. + */ +int cell_file_drop_privs(struct file *file) +{ + struct dentry *dentry = file_dentry(file); + struct inode *inode = file_inode(file); + int kill; + + if (!S_ISREG(inode->i_mode) || IS_NOSEC(inode)) + return 0; + + /* A look without the lock, as do_truncate() takes one. */ + kill = cell_privs_to_kill(dentry); + if (kill <= 0) + return kill; + + kill = mnt_want_write_file(file); + if (kill) + return kill; + inode_lock(inode); + kill = cell_privs_to_kill(dentry); + if (kill > 0) + kill = __remove_privs(file_mnt_idmap(file), dentry, kill); + inode_unlock(inode); + mnt_drop_write_file(file); + return kill; +} +#endif + static int file_remove_privs_flags(struct file *file, unsigned int flags) { struct dentry *dentry = file_dentry(file); diff --git a/fs/namei.c b/fs/namei.c index 49995c353292..f36c1a385d80 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -4862,6 +4862,14 @@ static int do_open(struct nameidata *nd, mnt_drop_write(mnt); mntput(mnt); } + /* + * Nothing a cell writes imparts privilege. Here, like the truncation, + * because it changes the inode: the lock of the directory has been + * dropped, which is not so in ->atomic_open(). + */ + if (!error && unlikely(current_cell()) && + (file->f_mode & (FMODE_WRITE | FMODE_CREATED)) == FMODE_WRITE) + error = cell_file_drop_privs(file); return error; } diff --git a/include/linux/cell.h b/include/linux/cell.h index 1d0eec34acb9..0451055f74fc 100644 --- a/include/linux/cell.h +++ b/include/linux/cell.h @@ -284,4 +284,11 @@ static inline umode_t cell_strip_setid(umode_t mode) return mode; } +/* Nothing a cell writes imparts privilege: files that are already there. */ +#ifdef CONFIG_CELLS +int cell_file_drop_privs(struct file *file); +#else +static inline int cell_file_drop_privs(struct file *file) { return 0; } +#endif + #endif /* _LINUX_CELL_H */ diff --git a/kernel/embryo.c b/kernel/embryo.c index 0a04f39fa37a..657ce07441e0 100644 --- a/kernel/embryo.c +++ b/kernel/embryo.c @@ -113,6 +113,12 @@ static int embryo_install_fds(struct embryo *e) ret = security_file_receive(file); if (ret) return ret; + /* As if the cell had opened it. */ + if (file->f_mode & FMODE_WRITE) { + ret = cell_file_drop_privs(file); + if (ret) + return ret; + } ret = replace_fd(e->fds[i].fd, file, 0); if (ret < 0) return ret; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 64/90] cells: add the "caps" slot 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (62 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 63/90] fs: drop privileges from what a cell may write to Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 65/90] capabilities: take nothing from a file in a cell Josef Bacik ` (25 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The first task in a new user namespace has every capability, over everything that the namespace owns. That puts a large amount of code that was written for root within reach of every user. A container runtime then spends its time taking most of those capabilities away again. A cell only has what it is given, so start from the other end. Being uid 0 in a cell comes with no privilege. It is one id out of 65536. It cannot switch to another id, give a file away, or signal a task that runs under another id. Let the builder name the capabilities that the tasks of the cell have. The "caps" slot takes a __u64 with one bit per capability. An empty slot means none. The value is used as the bounding set as well as the initial permitted and effective sets, so nothing in the cell can ever have more. Every task of a cell has no_new_privs, so it gains nothing from a file, or from being uid 0 when it runs one. And no task of a cell can create a user namespace to get a full set in. Filling the slot requires no privilege. The capabilities apply to the user namespace of the cell, so they cover its ids, its tasks and whatever else that namespace owns. That is no more than anybody gets from unshare(CLONE_NEWUSER). CAP_SETFCAP is refused, because nothing that a cell writes may confer privilege. A task of a cell with a single id is not uid 0 in the cell, so it would lose its capabilities as soon as it runs a program. The seal refuses capabilities for such a cell instead of letting them disappear silently. For reference: systemd 255 boots in a cell without any capability and runs units that stay root. To run units as other users, give files and cgroups to those users, and stop the units again, it needs CAP_SETUID, CAP_SETGID, CAP_CHOWN, CAP_FOWNER, CAP_DAC_OVERRIDE and CAP_KILL. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cell/slots.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/kernel/cell/slots.c b/kernel/cell/slots.c index 0a406422ce34..a96d2f262aa1 100644 --- a/kernel/cell/slots.c +++ b/kernel/cell/slots.c @@ -105,6 +105,28 @@ static int cell_set_ids_binary(struct cell *cell, const void __user *uvalue, return cell_set_ids(cell, count); } +/* + * The capabilities that the tasks of the cell start with, and can never have more + * than. They are over the user namespace of the cell, so they are good for what + * that owns and for the ids of the cell, which is why naming them takes nothing. + */ +static int cell_set_caps(struct cell *cell, const void __user *uvalue, size_t len) +{ + u64 caps; + + if (len != sizeof(caps)) + return -EINVAL; + if (copy_from_user(&caps, uvalue, sizeof(caps))) + return -EFAULT; + if (caps & ~CAP_VALID_MASK) + return -EINVAL; + /* Nothing a cell writes imparts privilege, so it writes no file capability. */ + if (caps & BIT_ULL(CAP_SETFCAP)) + return -EINVAL; + cell->caps.val = caps; + return 0; +} + static int cell_set_string(struct cell *cell, const char *key, const char *val) { if (!strcmp(key, "hostname")) @@ -117,6 +139,8 @@ static int cell_set_binary(struct cell *cell, const char *key, { if (!strcmp(key, "ids")) return cell_set_ids_binary(cell, uvalue, len); + if (!strcmp(key, "caps")) + return cell_set_caps(cell, uvalue, len); return -EINVAL; } @@ -163,6 +187,9 @@ static int cell_seal(struct cell *cell) if (ret) return ret; } + /* Who is not uid 0 loses them as soon as it runs a program. */ + if (cell->ids_count == 1 && !cap_isclear(cell->caps)) + return -EINVAL; ret = cell_make_pid_ns(cell); if (ret) return ret; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 65/90] capabilities: take nothing from a file in a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (63 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 64/90] cells: add the "caps" slot Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 66/90] capabilities: let a cell shrink its bounding sets Josef Bacik ` (24 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik File capabilities with the effective bit set mark a program that does not know about capabilities and relies on having them. If one of those capabilities is missing from the bounding set, the program is not run at all, and execve() fails with EPERM. This is why ping does not start in a container that was created with --cap-drop=ALL. The bounding set of a cell is what the builder named, which is nothing by default. Every task of a cell also has no_new_privs, so whatever the file offers is discarded later in the exec anyway. The only effect that reading the file capabilities can have is to stop the program from running. Do not read them for a task of a cell, as is already the case for a file on a nosuid mount. The program runs and gains nothing, which is also what happens with a set-uid bit in a cell. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- security/commoncap.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/security/commoncap.c b/security/commoncap.c index 8af343c78d35..02e25ab18f56 100644 --- a/security/commoncap.c +++ b/security/commoncap.c @@ -777,6 +777,13 @@ static int get_file_caps(struct linux_binprm *bprm, const struct file *file, if (!mnt_may_suid(file->f_path.mnt)) return 0; + /* + * A task of a cell gains nothing from a file, so do not look. A file that + * insists on a capability which is not in the bounding set would not run. + */ + if (userns_is_cell(bprm->cred->user_ns)) + return 0; + /* * This check is redundant with mnt_may_suid() but is kept to make * explicit that capability bits are limited to s_user_ns and its -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 66/90] capabilities: let a cell shrink its bounding sets 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (64 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 65/90] capabilities: take nothing from a file in a cell Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 67/90] af_unix: deliver ancillary data only within a cell Josef Bacik ` (23 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik PR_CAPBSET_DROP requires CAP_SETPCAP. Without that requirement, a user could run a set-uid root program with a capability removed that the program relies on. A program that does not notice that it failed to drop its privileges would then carry on with them. In a cell no program gains anything from its file, so there is no such program to trick. A task that wants to give less to the programs it starts should not need a capability for that. This matters in practice. systemd removes every capability from the bounding set of a service that the unit does not list, and fails the service if it cannot. In a cell that has CAP_KILL but not CAP_SETPCAP, journald does not start. Let a task of a cell drop capabilities from its bounding set without CAP_SETPCAP. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- security/commoncap.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/security/commoncap.c b/security/commoncap.c index 02e25ab18f56..1e0ef7465766 100644 --- a/security/commoncap.c +++ b/security/commoncap.c @@ -1281,7 +1281,12 @@ static int cap_prctl_drop(unsigned long cap) { struct cred *new; - if (!ns_capable(current_user_ns(), CAP_SETPCAP)) + /* + * It takes a capability lest a program that gains from its file runs with + * less than it counts on. In a cell no program gains anything from a file. + */ + if (!ns_capable(current_user_ns(), CAP_SETPCAP) && + !userns_is_cell(current_user_ns())) return -EPERM; if (!cap_valid(cap)) return -EINVAL; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 67/90] af_unix: deliver ancillary data only within a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (65 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 66/90] capabilities: let a cell shrink its bounding sets Josef Bacik @ 2026-11-05 15:02 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 68/90] af_unix: tell a sender that control data will not cross Josef Bacik ` (22 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:02 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Once a cell has started, no file descriptor and no identity should cross its boundary. A unix socket can connect two cells, either because both can see the directory that it is bound in, or because one end was given to a new task. Such a socket can carry both. Record in every skb the cell that it was sent from. The receiver gets the files, the credentials, the pidfd and the security label only if it is in the same cell. This rule needs no state on the socket. So there is no marking that could be forgotten, and no race with setsockopt(). It also covers a file that was queued on a socket before the socket was given away. Messages from different cells are never merged into a single read. Otherwise the ancillary data of one message could be lost, or delivered, because of the other. The cell id fills skb->cb completely. Move the small members of struct unix_skb_parms next to each other to make room. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- net/unix/af_unix.c | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- net/unix/af_unix.h | 14 ++++++++++---- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/net/unix/af_unix.c b/net/unix/af_unix.c index 98fcfd7301f6..58fef56c0f7e 100644 --- a/net/unix/af_unix.c +++ b/net/unix/af_unix.c @@ -78,6 +78,7 @@ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include <linux/bpf-cgroup.h> +#include <linux/cell.h> #include <linux/btf_ids.h> #include <linux/dcache.h> #include <linux/errno.h> @@ -797,6 +798,37 @@ static void copy_peercred(struct sock *sk, struct sock *peersk) spin_unlock(&sk->sk_peer_lock); } +/* + * Between cells a socket carries bytes and nothing else: no file, no + * credential, no pidfd, no label. + * + * The rule is enforced where ancillary data is received, and it keeps no state: + * every message says which cell it was sent from, and whoever receives it gets + * the ancillary data only if that is its own cell. However a socket came to + * span two cells, by a connect() through a tree both can see or by being given + * to a new task, there is nothing to forget to mark. + */ +#ifdef CONFIG_CELLS +static inline void unix_skb_set_cell(struct sk_buff *skb) +{ + UNIXCB(skb).cell_id = current_cell_id(); +} + +static inline u64 unix_skb_cell(const struct sk_buff *skb) +{ + return UNIXCB(skb).cell_id; +} + +static inline bool unix_skb_foreign(const struct sk_buff *skb) +{ + return unix_skb_cell(skb) != current_cell_id(); +} +#else +static inline void unix_skb_set_cell(struct sk_buff *skb) { } +static inline u64 unix_skb_cell(const struct sk_buff *skb) { return 0; } +static inline bool unix_skb_foreign(const struct sk_buff *skb) { return false; } +#endif + static bool unix_may_passcred(const struct sock *sk) { return sk->sk_scm_credentials || sk->sk_scm_pidfd || @@ -1993,6 +2025,7 @@ static int unix_scm_to_skb(struct scm_cookie *scm, struct sk_buff *skb, bool sen UNIXCB(skb).uid = scm->creds.uid; UNIXCB(skb).gid = scm->creds.gid; UNIXCB(skb).fp = NULL; + unix_skb_set_cell(skb); unix_get_secdata(scm, skb); if (scm->fp && send_fds) err = unix_attach_fds(scm, skb); @@ -2670,7 +2703,10 @@ int __unix_dgram_recvmsg(struct sock *sk, struct msghdr *msg, size_t size, } err = (flags & MSG_TRUNC) ? skb->len - skip : size; - scm_recv_unix(sock, msg, &scm, flags); + if (unix_skb_foreign(skb)) + scm_destroy(&scm); + else + scm_recv_unix(sock, msg, &scm, flags); out_free: skb_free_datagram(sk, skb); @@ -2929,6 +2965,8 @@ static int unix_stream_read_generic(struct unix_stream_read_state *state, size_t size = state->size; int flags = state->flags; bool check_creds = false; + bool foreign = false, glued = false; + u64 from = 0; struct scm_cookie scm; struct unix_sock *u; int copied = 0; @@ -3032,6 +3070,18 @@ static int unix_stream_read_generic(struct unix_stream_read_state *state, unix_state_unlock(sk); + /* + * Never glue messages from different cells: what comes with + * one is not to be lost, or let through, on account of another. + */ + if (!glued) { + glued = true; + from = unix_skb_cell(skb); + foreign = unix_skb_foreign(skb); + } else if (unix_skb_cell(skb) != from) { + break; + } + if (check_creds) { /* Never glue messages from different writers */ if (!unix_skb_scm_eq(sk, skb, &scm)) @@ -3113,7 +3163,10 @@ static int unix_stream_read_generic(struct unix_stream_read_state *state, if (msg) { bool do_cmsg = READ_ONCE(u->recvmsg_inq); - scm_recv_unix(sock, msg, &scm, flags); + if (foreign) + scm_destroy(&scm); + else + scm_recv_unix(sock, msg, &scm, flags); if ((do_cmsg | msg->msg_get_inq) && (copied ?: err) >= 0) { msg->msg_inq = READ_ONCE(u->inq_len); diff --git a/net/unix/af_unix.h b/net/unix/af_unix.h index 402742895acc..d9c393ac58cd 100644 --- a/net/unix/af_unix.h +++ b/net/unix/af_unix.h @@ -13,13 +13,19 @@ struct sock *unix_peer_get(struct sock *sk); struct unix_skb_parms { DECLARE_PIDS(pid, PIDTYPE_TGID); /* skb credentials by pid type */ - kuid_t uid; - kgid_t gid; struct scm_fp_list *fp; /* Passed files */ +#ifdef CONFIG_CELLS + u64 cell_id; /* of the cell it was sent from */ +#endif + /* This fills skb->cb to the last byte. Keep the small ones together. */ + struct { + kuid_t uid; + kgid_t gid; #ifdef CONFIG_SECURITY_NETWORK - u32 secid; /* Security ID */ + u32 secid; /* Security ID */ #endif - u32 consumed; + u32 consumed; + }; } __randomize_layout; #define UNIXCB(skb) (*(struct unix_skb_parms *)&((skb)->cb)) -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 68/90] af_unix: tell a sender that control data will not cross 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (66 preceding siblings ...) 2026-11-05 15:02 ` [PATCH RFC v3 67/90] af_unix: deliver ancillary data only within a cell Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 69/90] net: do not identify a peer in another cell Josef Bacik ` (21 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Control data that is sent to another cell is dropped on the receiving side. It is better for the sender to find out. Record in each socket the cell that holds it: - For a new socket, the cell of its creator. - For an accepted socket, the cell of the listener. - For a socket that pidfd_configure() gives to a new task, the cell of that task, once its exec has succeeded. If the task never starts, the socket stays with its current holder. Sending control data to a socket that is held by another cell fails with EPERM. This is only a convenience for the sender. What is delivered is still decided on the receiving side. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/net/af_unix.h | 4 ++++ kernel/embryo.c | 17 ++++++++++++++++ net/unix/af_unix.c | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/include/net/af_unix.h b/include/net/af_unix.h index bb1b3dee02e8..10c1613b4d42 100644 --- a/include/net/af_unix.h +++ b/include/net/af_unix.h @@ -14,6 +14,7 @@ #if IS_ENABLED(CONFIG_UNIX) struct unix_sock *unix_get_socket(struct file *filp); +void unix_file_cross_cell(struct file *file, u64 cell_id); #else static inline struct unix_sock *unix_get_socket(struct file *filp) { @@ -50,6 +51,9 @@ struct unix_sock { int inq_len; bool recvmsg_inq; bool scm_rights_notrunc; +#ifdef CONFIG_CELLS + u64 cell_id; /* of the cell that holds it */ +#endif #if IS_ENABLED(CONFIG_AF_UNIX_OOB) struct sk_buff *oob_skb; #endif diff --git a/kernel/embryo.c b/kernel/embryo.c index 657ce07441e0..1f8d0b14e6a4 100644 --- a/kernel/embryo.c +++ b/kernel/embryo.c @@ -78,6 +78,7 @@ struct embryo { struct pid *pid; /* the reference alloc_pid_struct() returned */ struct cell *cell; /* where the task is going to be */ + u64 cell_id; /* of that cell */ struct user_namespace *user_ns; /* likewise */ int result; @@ -126,6 +127,19 @@ static int embryo_install_fds(struct embryo *e) return 0; } +/* + * The exec went through, so these ends are the cell's from here on. Not any + * earlier: a socket that was to go to a task which never started stays with + * whoever has it. + */ +static void embryo_fds_crossed(struct embryo *e) +{ + unsigned int i; + + for (i = 0; i < e->nr_fds; i++) + unix_file_cross_cell(e->fds[i].file, e->cell_id); +} + /* * What was configured has been used, or never will be. The pidfd may be around * for long, and must not keep the other end of a pipe from seeing the last @@ -241,6 +255,8 @@ static int embryo_main(void *arg) /* @started is set, so nobody changes anything any more. */ ret = embryo_exec(e); + if (!ret) + embryo_fds_crossed(e); embryo_drop_config(e); /* * A signal to the embryo is no signal to whoever started it. Whatever @@ -356,6 +372,7 @@ int embryo_pidfd_open(int cfd, unsigned int flags) } e->pid = alloc_pid_struct(cell_nsproxy(cell)->pid_ns_for_children); e->cell = cell_get(cell); + e->cell_id = cell_id(cell); e->user_ns = get_user_ns(cell_user_ns(cell)); cell_fork_finish(cell); diff --git a/net/unix/af_unix.c b/net/unix/af_unix.c index 58fef56c0f7e..198898b86c1c 100644 --- a/net/unix/af_unix.c +++ b/net/unix/af_unix.c @@ -829,6 +829,49 @@ static inline u64 unix_skb_cell(const struct sk_buff *skb) { return 0; } static inline bool unix_skb_foreign(const struct sk_buff *skb) { return false; } #endif +/* + * A sender is told right away that control data will not arrive, where that can + * be known: each socket records the cell that holds it. That is a courtesy. The + * rule above is what counts. + */ +#ifdef CONFIG_CELLS +static inline bool unix_sk_foreign(const struct sock *other) +{ + return READ_ONCE(unix_sk(other)->cell_id) != current_cell_id(); +} + +static inline void unix_sk_set_cell(struct sock *sk, u64 cell_id) +{ + WRITE_ONCE(unix_sk(sk)->cell_id, cell_id); +} + +static inline u64 unix_sk_cell(const struct sock *sk) +{ + return READ_ONCE(unix_sk(sk)->cell_id); +} + +/** + * unix_file_cross_cell - a file is being given to a task of another cell + * @file: any file + * @cell_id: the id of that cell + * + * If @file is a unix socket, it is held by that cell from here on. This only + * serves to tell a sender early that control data will not arrive. Whether it + * does is decided where it is received. + */ +void unix_file_cross_cell(struct file *file, u64 cell_id) +{ + struct unix_sock *u = unix_get_socket(file); + + if (u) + unix_sk_set_cell(&u->sk, cell_id); +} +#else +static inline bool unix_sk_foreign(const struct sock *other) { return false; } +static inline void unix_sk_set_cell(struct sock *sk, u64 cell_id) { } +static inline u64 unix_sk_cell(const struct sock *sk) { return 0; } +#endif + static bool unix_may_passcred(const struct sock *sk) { return sk->sk_scm_credentials || sk->sk_scm_pidfd || @@ -1146,6 +1189,7 @@ static struct sock *unix_create1(struct net *net, struct socket *sock, int kern, sock_init_data(sock, sk); sk->sk_scm_rights = 1; + unix_sk_set_cell(sk, current_cell_id()); sk->sk_hash = unix_unbound_hash(sk); sk->sk_allocation = GFP_KERNEL_ACCOUNT; sk->sk_write_space = unix_write_space; @@ -1777,6 +1821,8 @@ static int unix_stream_connect(struct socket *sock, struct sockaddr_unsized *uad newsk->sk_state = TCP_ESTABLISHED; newsk->sk_type = sk->sk_type; newsk->sk_scm_recv_flags = other->sk_scm_recv_flags; + /* It is the listener's end, whoever made it. */ + unix_sk_set_cell(newsk, unix_sk_cell(other)); init_peercred(newsk, &peercred); newu = unix_sk(newsk); @@ -2284,6 +2330,11 @@ static int unix_dgram_sendmsg(struct socket *sock, struct msghdr *msg, goto out_unlock; } + if (msg->msg_controllen && unix_sk_foreign(other)) { + err = -EPERM; + goto out_unlock; + } + if (sk->sk_type != SOCK_SEQPACKET) { err = security_unix_may_send(sk->sk_socket, other->sk_socket); if (err) @@ -2456,6 +2507,11 @@ static int unix_stream_sendmsg(struct socket *sock, struct msghdr *msg, otheru = unix_sk(other); + if (msg->msg_controllen && unix_sk_foreign(other)) { + err = -EPERM; + goto out_err; + } + if (READ_ONCE(sk->sk_shutdown) & SEND_SHUTDOWN) goto out_pipe; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 69/90] net: do not identify a peer in another cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (67 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 68/90] af_unix: tell a sender that control data will not cross Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 70/90] seccomp: let a listener put no file into a cell Josef Bacik ` (20 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik SO_PEERCRED, SO_PEERGROUPS, SO_PEERPIDFD and SO_PEERSEC report who is at the other end of a socket. No identity should cross the boundary of a cell. Fail with ENODATA if the credentials of the peer belong to another cell. Reporting the overflow ids instead would be wrong: 65534 is an ordinary uid inside of a cell, and a server that accepts peers with its own uid may well be running with it. The peer can change while the option is being read, because a connect() on another thread sets it under sk_peer_lock. So the check is made in the same critical section that reads the value, or on the very reference that was taken. With a separate check ahead of the read, a connect() that was waiting for the lock got in between about one time in five. The label that SO_PEERSEC reports is not protected by that lock, and connect() lets the LSM set it before the peer credentials are set. So a task of a cell only gets the label once the peer is known to be in its cell. After that point the peer of a stream socket does not change. As a consequence, SO_PEERSEC does not work in a cell for a socket that has no peer credentials at all, such as a labelled TCP connection. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- net/core/sock.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/net/core/sock.c b/net/core/sock.c index e7405be8941f..cac93cba3c84 100644 --- a/net/core/sock.c +++ b/net/core/sock.c @@ -87,6 +87,7 @@ #include <linux/unaligned.h> #include <linux/capability.h> +#include <linux/cell.h> #include <linux/errno.h> #include <linux/errqueue.h> #include <linux/types.h> @@ -1737,20 +1738,37 @@ static int groups_to_user(sockptr_t dst, const struct group_info *src) return 0; } +/* + * Who the peer is, is nobody's business in another cell. The question fails + * rather than be answered with a nobody: 65534 is a uid like any other in a + * cell, and a server that lets in whoever has its own uid may well run as it. + * + * The caller holds sk_peer_lock and goes on holding it until it has read what it + * is going to hand out. A connect() on another thread may be waiting for that + * very lock to say who the peer is. + */ +static bool sk_peer_foreign(const struct sock *sk) +{ + lockdep_assert_held(&sk->sk_peer_lock); + + return sk->sk_peer_cred && cred_cell(sk->sk_peer_cred) != current_cell(); +} + /* Hand out a pidfd for @type of the socket's peer via SO_PEERPIDFD*. */ static int sk_getsockopt_peerpidfd(struct sock *sk, enum pid_type type, sockptr_t optval, sockptr_t optlen, int len) { struct file *pidfd_file = NULL; unsigned int flags = 0; - struct pid *peer_pid; + struct pid *peer_pid = NULL; int pidfd; if (len > sizeof(pidfd)) len = sizeof(pidfd); spin_lock(&sk->sk_peer_lock); - peer_pid = get_pid(sk->sk_peer_pid[type]); + if (!sk_peer_foreign(sk)) + peer_pid = get_pid(sk->sk_peer_pid[type]); spin_unlock(&sk->sk_peer_lock); if (!peer_pid) @@ -1962,13 +1980,20 @@ int sk_getsockopt(struct sock *sk, int level, int optname, case SO_PEERCRED: { struct ucred peercred; + bool foreign; + if (len > sizeof(peercred)) len = sizeof(peercred); spin_lock(&sk->sk_peer_lock); - cred_to_ucred(sk->sk_peer_pid[PIDTYPE_TGID], sk->sk_peer_cred, - &peercred); + foreign = sk_peer_foreign(sk); + if (!foreign) { + cred_to_ucred(sk->sk_peer_pid[PIDTYPE_TGID], + sk->sk_peer_cred, &peercred); + } spin_unlock(&sk->sk_peer_lock); + if (foreign) + return -ENODATA; if (copy_to_sockptr(optval, &peercred, len)) return -EFAULT; @@ -1993,6 +2018,11 @@ int sk_getsockopt(struct sock *sk, int level, int optname, cred = sk_get_peer_cred(sk); if (!cred) return -ENODATA; + /* What was taken is what is looked at. */ + if (cred_cell(cred) != current_cell()) { + put_cred(cred); + return -ENODATA; + } n = cred->group_info->ngroups; if (len < n * sizeof(gid_t)) { @@ -2038,8 +2068,24 @@ int sk_getsockopt(struct sock *sk, int level, int optname, break; case SO_PEERSEC: + { + bool unknown, foreign; + + spin_lock(&sk->sk_peer_lock); + unknown = !sk->sk_peer_cred; + foreign = sk_peer_foreign(sk); + spin_unlock(&sk->sk_peer_lock); + /* + * The label is not under sk_peer_lock, and a connect() has the + * LSM note it before it says who the peer is. So a task of a + * cell gets it only once the peer is known to be of its cell. + * From there on the peer of a stream socket does not change. + */ + if (foreign || (unknown && current_cell())) + return -ENODATA; return security_socket_getpeersec_stream(sock, optval, optlen, len); + } case SO_MARK: v.val = READ_ONCE(sk->sk_mark); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 70/90] seccomp: let a listener put no file into a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (68 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 69/90] net: do not identify a peer in another cell Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 71/90] umh: run no program for a task of " Josef Bacik ` (19 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Seccomp filters are inherited by tasks that are spawned into a cell. That is intentional. The listener of a filter that uses SECCOMP_RET_USER_NOTIF stays outside of the cell, with the spawner or with whoever confined the spawner. SECCOMP_IOCTL_NOTIF_ADDFD lets the listener install any file that it holds into the task that is waiting for an answer. But once a task of a cell has started, no file should cross the boundary. The files that pidfd_configure() refuses to place, such as a cellfd, a namespace file or an io_uring of the host, could be installed this way a moment later. Fail the ioctl with EPERM if the task is in a different cell than the caller. The listener can still answer the system calls of the task. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/seccomp.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/kernel/seccomp.c b/kernel/seccomp.c index 3eecba5ccdef..b8ade37dfcae 100644 --- a/kernel/seccomp.c +++ b/kernel/seccomp.c @@ -17,6 +17,7 @@ #include <linux/refcount.h> #include <linux/audit.h> +#include <linux/cell.h> #include <linux/compat.h> #include <linux/coredump.h> #include <linux/kmemleak.h> @@ -1764,6 +1765,17 @@ static long seccomp_notify_addfd(struct seccomp_filter *filter, goto out_unlock; } + /* + * Filters are inherited by what is spawned into a cell, and the + * listener stays behind. No file crosses the boundary of a cell. + */ + scoped_guard(rcu) { + if (!cell_same(current_cred(), __task_cred(knotif->task))) + ret = -EPERM; + } + if (ret) + goto out_unlock; + /* * We do not want to allow for FD injection to occur before the * notification has been picked up by a userspace handler, or after -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 71/90] umh: run no program for a task of a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (69 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 70/90] seccomp: let a listener put no file into a cell Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 72/90] coredump: let no core dump leave " Josef Bacik ` (18 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Some things that a task does make the kernel run a program: - socket() for a protocol family that is not loaded runs modprobe, and so do several other lookups by name. - request_key() for a key that does not exist runs /sbin/request-key. - A crash runs the program that core_pattern pipes core dumps to. The program runs as root in the initial namespaces, and the task has influence over its arguments or its input. None of this requires a capability. For containers it has long been a way to attack the host: any module on disk can be loaded into the kernel, with whatever bugs it has, and the helper programs have had bugs of their own. A cell only has what it is given, and here the host acts on its behalf. All of these cases end up in call_usermodehelper_exec(), in the context of the task that caused them. Refuse there, so that cases which are added later are covered too. The original request then fails. socket() returns EAFNOSUPPORT, as for any protocol that is not available, and request_key() returns EPERM. A module that a cell is supposed to use has to be loaded beforehand. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/umh.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/kernel/umh.c b/kernel/umh.c index 512dc018a50b..e812a0008cfc 100644 --- a/kernel/umh.c +++ b/kernel/umh.c @@ -6,6 +6,7 @@ #include <linux/sched.h> #include <linux/sched/task.h> #include <linux/binfmts.h> +#include <linux/cell_ids.h> #include <linux/syscalls.h> #include <linux/unistd.h> #include <linux/umh.h> @@ -406,6 +407,14 @@ int call_usermodehelper_exec(struct subprocess_info *sub_info, int wait) call_usermodehelper_freeinfo(sub_info); return -EINVAL; } + /* + * The helper runs as root, on the host, in no namespace but the first. That + * is not for a task of a cell to bring about, whatever it is it asked for. + */ + if (userns_is_cell(current_user_ns())) { + call_usermodehelper_freeinfo(sub_info); + return -EPERM; + } helper_lock(); if (usermodehelper_disabled) { retval = -EBUSY; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 72/90] coredump: let no core dump leave a cell 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (70 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 71/90] umh: run no program for a task of " Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 73/90] cells: add the "net" slot Josef Bacik ` (17 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A core dump that goes to a file is written in the context of the crashing task, with its credentials. A core dump that goes to a pipe or a socket is handed to a program of the host. That program runs as root and parses what was in the memory of the crashed task. After the previous patch, the helper at the end of a pipe is no longer run for a task of a cell. But that is treated as a failure and logged every time. A socket is still connected to, and the task then waits for the other end. If the listener does not answer, a cell whose init has crashed never dies. Skip both kinds of core dump silently for a task of a cell. Whoever set core_pattern may not have had cells in mind, and what a cell hands to the host is for its builder to decide. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/coredump.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fs/coredump.c b/fs/coredump.c index 82ef9d115999..6b091807aef1 100644 --- a/fs/coredump.c +++ b/fs/coredump.c @@ -17,6 +17,7 @@ #include <linux/key.h> #include <linux/personality.h> #include <linux/binfmts.h> +#include <linux/cell_ids.h> #include <linux/coredump.h> #include <linux/sort.h> #include <linux/sched/coredump.h> @@ -1210,6 +1211,13 @@ static noinline_for_stack void do_coredump(struct core_name *cn, return; } + /* + * A file is written where the task is, as the task. Anything else hands what + * was in its memory to a program of the host, which is root there. + */ + if (cn->core_type != COREDUMP_FILE && userns_is_cell(current_user_ns())) + return; + switch (cn->core_type) { case COREDUMP_FILE: if (!coredump_file(cn, cprm, binfmt)) -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 73/90] cells: add the "net" slot 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (71 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 72/90] coredump: let no core dump leave " Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 74/90] net: give a cell without a network no sockets Josef Bacik ` (16 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The network of a cell is set up from the outside and handed to the cell, like its mount tree. A capability over a network namespace has to be held in the user namespace that owns it, and that is never the user namespace of the cell. So no task of the cell can configure the network, not even its root user. Compare unshare(CLONE_NEWUSER | CLONE_NEWNET), which gives any caller CAP_NET_ADMIN over a network stack, and with it access to a large amount of code that was written for root. A cell does not get that. There are two ways to fill the slot: - CELL_SET_FD with the key "net" gives the cell an existing network namespace, set up however the builder wants. It requires CAP_SYS_ADMIN over the user namespace that owns the network namespace, as for the "root" slot. - CELL_SET_STRING with the value "new" creates a network namespace for the cell. It is owned by the user namespace that the cell was created in, so the builder can set it up. Until then it only contains a loopback device that is down. For now a cell whose slot is left empty also gets such a new namespace. The next patch changes that. A later patch adds a slot that lets the cell make selected changes to its network. A network namespace that is shared between cells can carry identities from one to the other. Only unix sockets know about cells. The builder has to take that into account. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- kernel/cell/slots.c | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/kernel/cell/slots.c b/kernel/cell/slots.c index a96d2f262aa1..75855f2ee020 100644 --- a/kernel/cell/slots.c +++ b/kernel/cell/slots.c @@ -93,6 +93,36 @@ static int cell_make_net(struct cell *cell) return 0; } +/* "new": one that is made for the cell, for whoever builds the cell to set up. */ +static int cell_set_net_string(struct cell *cell, const char *val) +{ + if (strcmp(val, "new")) + return -EINVAL; + if (cell->net) + return -EBUSY; + return cell_make_net(cell); +} + +/* + * A network namespace to share. A file descriptor on one is no authority over + * it, so it takes CAP_SYS_ADMIN over its owner. That is half of what setns() + * asks for. The other half is about the namespaces of the caller, which do not + * change here. It goes on belonging to whoever it belongs to. + */ +static int cell_set_net(struct cell *cell, struct file *file) +{ + struct ns_common *ns = cell_ns_from_file(file, CLONE_NEWNET); + + if (IS_ERR(ns)) + return PTR_ERR(ns); + if (cell->net) + return -EBUSY; + if (!ns_capable(to_net_ns(ns)->user_ns, CAP_SYS_ADMIN)) + return -EPERM; + cell->net = get_net(to_net_ns(ns)); + return 0; +} + static int cell_set_ids_binary(struct cell *cell, const void __user *uvalue, size_t len) { @@ -131,6 +161,8 @@ static int cell_set_string(struct cell *cell, const char *key, const char *val) { if (!strcmp(key, "hostname")) return cell_set_hostname(cell, val); + if (!strcmp(key, "net")) + return cell_set_net_string(cell, val); return -EINVAL; } @@ -150,6 +182,8 @@ static int cell_set_fd(struct cell *cell, const char *key, struct file *file) return cell_set_budget(cell, file); if (!strcmp(key, "root")) return cell_set_root(cell, file); + if (!strcmp(key, "net")) + return cell_set_net(cell, file); return -EINVAL; } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 74/90] net: give a cell without a network no sockets 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (72 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 73/90] cells: add the "net" slot Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 75/90] net: add net_may() Josef Bacik ` (15 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A cell whose "net" slot is left empty should have no network. So far it has a network namespace that nobody has set up, which is not the same thing. It is an empty network, and all the code that operates on one is within reach. The tasks can create sockets of every family and talk to rtnetlink and every other netlink family, although none of that is of any use to them. There is no equivalent of failfs for the network. Refuse every socket except a unix socket for a task of such a cell. The error is EAFNOSUPPORT, which is what a kernel returns that was built without the family. The check is in __sock_create(), which socket(), socketpair() and the socket operation of io_uring all go through. A socket that is handed to the cell works, and so do the sockets that accept() returns for it. Holding the socket is the authority to use it, and it is the only network that such a cell has. Holding the socket is not the authority to inspect the network that it belongs to, though. Many ioctls are about the network rather than the socket, and they work on any socket. Through a single socket of the host, a cell could list the devices of the host and their addresses. Refuse those ioctls with ENODEV, by number, before anything else looks at them. That covers every ioctl from SIOCADDRT upwards except SIOCOUTQNSD. The protocol private range can contain either kind, so it is refused as well. The check looks at the cell of the caller, not at a mark on the network namespace. So it holds no matter which network namespace a socket belongs to. The decision is recorded once the seal has created the network namespace. If the seal fails before that point, the slot can still be filled. If it fails later and the builder then asks for the namespace with cell_open(), the decision is reverted. The network namespace still exists, because unix sockets need one. A single static namespace for all such cells would avoid creating and destroying a whole network namespace per cell. That requires moving the unix socket tables out of struct net and is left for later. It would make no difference from inside of the cell. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cell.h | 7 +++++++ kernel/cell/boundary.c | 19 +++++++++++++++++++ kernel/cell/internal.h | 1 + kernel/cell/slots.c | 9 +++++++++ net/socket.c | 28 ++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+) diff --git a/include/linux/cell.h b/include/linux/cell.h index 0451055f74fc..b12e2f124769 100644 --- a/include/linux/cell.h +++ b/include/linux/cell.h @@ -249,6 +249,13 @@ bool cell_may_change_cgroup(struct cgroup *cgrp); static inline bool cell_may_change_cgroup(struct cgroup *cgrp) { return true; } #endif +/* A cell without a network. */ +#ifdef CONFIG_CELLS +bool cell_no_net(void); +#else +static inline bool cell_no_net(void) { return false; } +#endif + /* A mount is idmapped for a cell. */ #ifdef CONFIG_CELLS struct user_namespace *cell_idmap_userns(const struct file *file); diff --git a/kernel/cell/boundary.c b/kernel/cell/boundary.c index 3891dfe899f2..af67ef68d4a2 100644 --- a/kernel/cell/boundary.c +++ b/kernel/cell/boundary.c @@ -85,3 +85,22 @@ bool cell_may_change_cgroup(struct cgroup *cgrp) { return cell_may_migrate(current, cgrp); } + +/** + * cell_no_net - is the caller in a cell that was built without a network? + * + * A cell whose "net" slot was left empty has no network. Its tasks make no + * socket but a unix socket, and learn nothing about a network through any socket. + * What they are handed works: a socket is the authority to use it. + * + * It goes by the cell of the caller and not by the network namespace it is in, + * so that it holds whatever that is. + * + * Return: true if so, false for a task of any other cell and of the host. + */ +bool cell_no_net(void) +{ + struct cell *cell = current_cell(); + + return cell && cell->no_net; +} diff --git a/kernel/cell/internal.h b/kernel/cell/internal.h index c7a1895eadd4..4eafa19daa25 100644 --- a/kernel/cell/internal.h +++ b/kernel/cell/internal.h @@ -33,6 +33,7 @@ struct cell { struct mnt_namespace *mnt_ns; struct pid_namespace *pid_ns; struct net *net; + bool no_net; /* the slot was left empty */ struct cgroup *budget; u64 budget_id; kuid_t budget_uid; /* who owned it before */ diff --git a/kernel/cell/slots.c b/kernel/cell/slots.c index 75855f2ee020..2273d9ce733f 100644 --- a/kernel/cell/slots.c +++ b/kernel/cell/slots.c @@ -211,6 +211,7 @@ static int cell_seal(struct cell *cell) struct nsproxy *nsproxy; kuid_t root_uid; kgid_t root_gid; + bool no_net; int ret; if (!cell->budget || !cell->mnt_ns) @@ -227,9 +228,15 @@ static int cell_seal(struct cell *cell) ret = cell_make_pid_ns(cell); if (ret) return ret; + /* No network, see cell_no_net(). Unix sockets need a namespace to live in all the same. */ + no_net = !cell->net; ret = cell_make_net(cell); if (ret) return ret; + /* Only now. Had that failed, the slot would still be there to be filled. */ + if (no_net) + cell->no_net = true; + cgroup_ns = cgroup_cell_ns(cell->user_ns, cell->budget); if (IS_ERR(cgroup_ns)) return PTR_ERR(cgroup_ns); @@ -410,6 +417,8 @@ SYSCALL_DEFINE5(cell_open, int, cfd, unsigned int, what, ret = cell_make_net(cell); if (ret) return ret; + /* A seal that failed may have made it. Whoever asks means to set it up. */ + cell->no_net = false; ns = &get_net(cell->net)->ns; } else if (!strcmp(key, "pids")) { ret = cell_make_pid_ns(cell); diff --git a/net/socket.c b/net/socket.c index 2e3b5737c481..c9659f7370ef 100644 --- a/net/socket.c +++ b/net/socket.c @@ -106,6 +106,7 @@ #include <linux/route.h> #include <linux/termios.h> #include <linux/sockios.h> +#include <linux/cell.h> #include <net/busy_poll.h> #include <linux/errqueue.h> #include <linux/ptp_clock_kernel.h> @@ -1352,6 +1353,22 @@ static long sock_do_ioctl(struct net *net, struct socket *sock, * what to do with it - that's up to the protocol still. */ +/* + * Is @cmd about the network that a socket is in, its devices, addresses and + * routes, and not about the socket? Those are served for any socket, whatever + * its family, some of them by the family itself. From SIOCADDRT up that is all + * of them but one. What a protocol keeps to itself, SIOCPROTOPRIVATE, may be + * either, the counters of multicast routing are in there, so it counts. + */ +static bool sock_ioctl_about_net(unsigned int cmd) +{ + if (cmd >= SIOCIWFIRST && cmd <= SIOCIWLAST) + return true; + if (!is_socket_ioctl_cmd(cmd) || _IOC_NR(cmd) < _IOC_NR(SIOCADDRT)) + return false; + return cmd != SIOCOUTQNSD; +} + static long sock_ioctl(struct file *file, unsigned cmd, unsigned long arg) { const struct proto_ops *ops; @@ -1361,6 +1378,10 @@ static long sock_ioctl(struct file *file, unsigned cmd, unsigned long arg) int pid, err; struct net *net; + /* There is no network, so there are no devices. */ + if (sock_ioctl_about_net(cmd) && cell_no_net()) + return -ENODEV; + sock = file->private_data; ops = READ_ONCE(sock->ops); sk = sock->sk; @@ -1620,6 +1641,10 @@ int __sock_create(struct net *net, int family, int type, int protocol, if (type < 0 || type >= SOCK_MAX) return -EINVAL; + /* As in a kernel that was built without the rest of them. */ + if (!kern && family != PF_UNIX && cell_no_net()) + return -EAFNOSUPPORT; + /* Compatibility. This uglymoron is moved from INET layer to here to avoid @@ -3642,6 +3667,9 @@ static long compat_sock_ioctl(struct file *file, unsigned int cmd, struct sock *sk; struct net *net; + if (sock_ioctl_about_net(cmd) && cell_no_net()) + return -ENODEV; + sk = sock->sk; net = sock_net(sk); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 75/90] net: add net_may() 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (73 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 74/90] net: give a cell without a network no sockets Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 76/90] rtnetlink: use net_may() for requests that change state Josef Bacik ` (14 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Almost every operation that changes a network namespace is guarded by the same check: ns_capable(net->user_ns, CAP_NET_ADMIN). There are well over a hundred of these checks. They cover devices, addresses, routes, netfilter, traffic control, tunnels, IPsec, socket options and more. The check does not say which of these it is guarding. So there is no way to allow one kind of operation without allowing all of them. A program that only needs to add a netfilter rule must be given the power to create tunnels as well. Add net_may(net, right). It asks whether the caller may do one particular kind of thing to a network namespace, and enum net_right names the kinds. For now the answer is the same as before: NET_RIGHT_RAW requires CAP_NET_RAW over the owner of the namespace, and every other right requires CAP_NET_ADMIN. A single function, net_right_ns(), makes that decision, so a later patch can change it in one place. Add a variant for each form the existing checks take: net_may_noaudit() like ns_capable_noaudit() file_net_may() like file_ns_capable(), for the opener of a file sockopt_net_may() like sockopt_ns_capable(), which lets BPF set options netlink_net_may() like netlink_ns_capable(), which checks both the opener of the socket and the sender of the message The rights follow the entry points, because most of the code in question sits behind a handful of them. A right can be split later if its entry point learns to distinguish more cases. There are no users yet. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/netlink.h | 2 ++ include/net/net_namespace.h | 25 +++++++++++++++++++ include/net/sock.h | 1 + net/core/net_namespace.c | 58 +++++++++++++++++++++++++++++++++++++++++++++ net/core/sock.c | 13 ++++++++++ net/netlink/af_netlink.c | 21 ++++++++++++++++ 6 files changed, 120 insertions(+) diff --git a/include/linux/netlink.h b/include/linux/netlink.h index 882e9c1b6c1d..16ecb55e9f12 100644 --- a/include/linux/netlink.h +++ b/include/linux/netlink.h @@ -10,6 +10,7 @@ #include <uapi/linux/netlink.h> struct net; +enum net_right; void do_trace_netlink_extack(const char *msg); @@ -356,6 +357,7 @@ bool netlink_ns_capable(const struct sk_buff *skb, struct user_namespace *ns, int cap); bool netlink_capable(const struct sk_buff *skb, int cap); bool netlink_net_capable(const struct sk_buff *skb, int cap); +bool netlink_net_may(const struct sk_buff *skb, const struct net *net, enum net_right right); struct sk_buff *netlink_alloc_large_skb(unsigned int size, int broadcast); #endif /* __LINUX_NETLINK_H */ diff --git a/include/net/net_namespace.h b/include/net/net_namespace.h index c29a22ca178f..badee0bbc567 100644 --- a/include/net/net_namespace.h +++ b/include/net/net_namespace.h @@ -459,6 +459,31 @@ static inline struct net *read_pnet_rcu(const possible_net_t *pnet) #define __net_initconst __initconst #endif +/** + * enum net_right - what somebody wants to do to a network namespace + * + * Changing anything about a network namespace takes a capability over the user + * namespace that owns it, and nearly always that is CAP_NET_ADMIN, whatever it is + * that is to be changed. A check that goes by way of net_may() says what it is for. + */ +enum net_right { + NET_RIGHT_LINK, /* change a device: up, down, mtu, name */ + NET_RIGHT_LINK_NEW, /* make and remove devices */ + NET_RIGHT_ADDR, /* addresses */ + NET_RIGHT_ROUTE, /* routes, rules, neighbours */ + NET_RIGHT_FILTER, /* netfilter */ + NET_RIGHT_TC, /* queueing disciplines, classes, filters */ + NET_RIGHT_TUN, /* make tun and tap devices */ + NET_RIGHT_SYSCTL, /* /proc/sys/net */ + NET_RIGHT_SOCKOPT, /* socket options that reach beyond the socket */ + NET_RIGHT_RAW, /* raw and packet sockets. This one is CAP_NET_RAW */ + NET_RIGHT_NR +}; + +bool net_may(const struct net *net, enum net_right right); +bool net_may_noaudit(const struct net *net, enum net_right right); +bool file_net_may(const struct file *file, const struct net *net, enum net_right right); + int peernet2id_alloc(struct net *net, struct net *peer, gfp_t gfp); int peernet2id(const struct net *net, struct net *peer); bool peernet_has_id(const struct net *net, struct net *peer); diff --git a/include/net/sock.h b/include/net/sock.h index 173b37b2b8bb..7562a73ac29d 100644 --- a/include/net/sock.h +++ b/include/net/sock.h @@ -1781,6 +1781,7 @@ static inline void unlock_sock_fast(struct sock *sk, bool slow) void sockopt_lock_sock(struct sock *sk); void sockopt_release_sock(struct sock *sk); bool sockopt_ns_capable(struct user_namespace *ns, int cap); +bool sockopt_net_may(const struct sock *sk, enum net_right right); bool sockopt_capable(int cap); /* Used by processes to "lock" a socket state, so that diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c index 52c2bd44778a..d211631f8c42 100644 --- a/net/core/net_namespace.c +++ b/net/core/net_namespace.c @@ -362,6 +362,64 @@ int peernet2id(const struct net *net, struct net *peer) } EXPORT_SYMBOL(peernet2id); +static int net_right_cap(enum net_right right) +{ + return right == NET_RIGHT_RAW ? CAP_NET_RAW : CAP_NET_ADMIN; +} + +/* The user namespace over which @cred needs the capability. NULL: none will do. */ +static struct user_namespace *net_right_ns(const struct cred *cred, const struct net *net, + enum net_right right) +{ + return net->user_ns; +} + +/** + * net_may - may the caller do this to a network namespace? + * @net: the network namespace + * @right: what it is that the caller wants to do + * + * Return: true if it may, which is noted as a use of privilege. + */ +bool net_may(const struct net *net, enum net_right right) +{ + struct user_namespace *ns = net_right_ns(current_cred(), net, right); + + return ns && ns_capable(ns, net_right_cap(right)); +} +EXPORT_SYMBOL_GPL(net_may); + +/** + * net_may_noaudit - the same, where a no is nothing to write home about + * @net: the network namespace + * @right: what it is that the caller wants to do + * + * Return: true if it may. + */ +bool net_may_noaudit(const struct net *net, enum net_right right) +{ + struct user_namespace *ns = net_right_ns(current_cred(), net, right); + + return ns && ns_capable_noaudit(ns, net_right_cap(right)); +} +EXPORT_SYMBOL_GPL(net_may_noaudit); + +/** + * file_net_may - might whoever opened a file do this to a network namespace? + * @file: the file + * @net: the network namespace + * @right: what is to be done + * + * Return: true if it might, going by what it was when it opened @file. + */ +bool file_net_may(const struct file *file, const struct net *net, enum net_right right) +{ + struct user_namespace *ns = net_right_ns(file->f_cred, net, right); + + return ns && file_ns_capable(file, ns, net_right_cap(right)); +} +EXPORT_SYMBOL_GPL(file_net_may); + /* This function returns true is the peer netns has an id assigned into the * current netns. */ diff --git a/net/core/sock.c b/net/core/sock.c index cac93cba3c84..352310066e35 100644 --- a/net/core/sock.c +++ b/net/core/sock.c @@ -1172,6 +1172,19 @@ bool sockopt_ns_capable(struct user_namespace *ns, int cap) } EXPORT_SYMBOL(sockopt_ns_capable); +/** + * sockopt_net_may - net_may() over the network namespace of a socket, for an option + * @sk: the socket + * @right: what the option comes to + * + * Return: true if the caller may, or if it is a BPF program that sets the option. + */ +bool sockopt_net_may(const struct sock *sk, enum net_right right) +{ + return has_current_bpf_ctx() || net_may(sock_net(sk), right); +} +EXPORT_SYMBOL(sockopt_net_may); + bool sockopt_capable(int cap) { return has_current_bpf_ctx() || capable(cap); diff --git a/net/netlink/af_netlink.c b/net/netlink/af_netlink.c index 1281c42a3456..69c92fad0a32 100644 --- a/net/netlink/af_netlink.c +++ b/net/netlink/af_netlink.c @@ -899,6 +899,27 @@ bool netlink_net_capable(const struct sk_buff *skb, int cap) } EXPORT_SYMBOL(netlink_net_capable); +/** + * netlink_net_may - net_may() for a request that came in by netlink + * @skb: socket buffer holding a netlink command from userspace + * @net: the network namespace that the command is about + * @right: what the command wants to do to it + * + * As for netlink_ns_capable(), it takes both whoever opened the socket and whoever + * sent the message. + * + * Return: true if both may. + */ +bool netlink_net_may(const struct sk_buff *skb, const struct net *net, enum net_right right) +{ + const struct netlink_skb_parms *nsp = &NETLINK_CB(skb); + + return ((nsp->flags & NETLINK_SKB_DST) || + file_net_may(nsp->sk->sk_socket->file, net, right)) && + net_may(net, right); +} +EXPORT_SYMBOL(netlink_net_may); + static inline int netlink_allowed(const struct socket *sock, unsigned int flag) { return (nl_table[sock->sk->sk_protocol].flags & flag) || -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 76/90] rtnetlink: use net_may() for requests that change state 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (74 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 75/90] net: add net_may() Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 77/90] net: use net_may() in the configuration ioctls Josef Bacik ` (13 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik rtnetlink_rcv_msg() has a single CAP_NET_ADMIN check for every request that is not a GET. Pick the right from the message type instead: NET_RIGHT_LINK SETLINK, NEWLINKPROP, DELLINKPROP NET_RIGHT_LINK_NEW DELLINK NET_RIGHT_ADDR NEWADDR, DELADDR NET_RIGHT_ROUTE routes, rules, neighbours, nexthops NET_RIGHT_TC qdiscs, classes, filters, actions, chains All other message types keep the plain capability check. RTM_NEWLINK either changes an existing device or creates a new one, and that is only known after __rtnl_newlink() has looked the device up. So rtnetlink_rcv_msg() accepts either NET_RIGHT_LINK or NET_RIGHT_LINK_NEW for it, and __rtnl_newlink() then checks the one that applies. rtnl_link_get_net_capable() checks the network namespace that the request names as its target. That is the namespace of the sender if the request names none. Convert it as well, and keep checking the target namespace. All of its callers pass CAP_NET_ADMIN, so drop that argument. No functional change. Every right used here maps to CAP_NET_ADMIN over the owner of the same network namespace as before. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- net/core/rtnetlink.c | 60 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/net/core/rtnetlink.c b/net/core/rtnetlink.c index e141340172ce..df97309a9622 100644 --- a/net/core/rtnetlink.c +++ b/net/core/rtnetlink.c @@ -2737,7 +2737,7 @@ static struct net *rtnl_link_get_net_by_nlattr(struct net *src_net, static struct net *rtnl_link_get_net_capable(const struct sk_buff *skb, struct net *src_net, - struct nlattr *tb[], int cap) + struct nlattr *tb[]) { struct net *net; @@ -2745,7 +2745,9 @@ static struct net *rtnl_link_get_net_capable(const struct sk_buff *skb, if (IS_ERR(net)) return net; - if (!netlink_ns_capable(skb, net->user_ns, cap)) { + /* Where the device is, or where it is to go. */ + if (!netlink_net_may(skb, net, NET_RIGHT_LINK) && + !netlink_net_may(skb, net, NET_RIGHT_LINK_NEW)) { put_net(net); return ERR_PTR(-EPERM); } @@ -3610,7 +3612,7 @@ static int rtnl_setlink(struct sk_buff *skb, struct nlmsghdr *nlh, if (err < 0) goto errout; - tgt_net = rtnl_link_get_net_capable(skb, net, tb, CAP_NET_ADMIN); + tgt_net = rtnl_link_get_net_capable(skb, net, tb); if (IS_ERR(tgt_net)) { err = PTR_ERR(tgt_net); goto errout; @@ -4109,6 +4111,11 @@ static int __rtnl_newlink(struct sk_buff *skb, struct nlmsghdr *nlh, dev = NULL; } + /* A device that is there is changed. One that is not is made. */ + if (!netlink_net_may(skb, net, dev || !(nlh->nlmsg_flags & NLM_F_CREATE) ? + NET_RIGHT_LINK : NET_RIGHT_LINK_NEW)) + return -EPERM; + if (dev) return rtnl_changelink(skb, nlh, ops, dev, tgt_net, tbs, data, extack); @@ -4220,7 +4227,7 @@ static int rtnl_newlink(struct sk_buff *skb, struct nlmsghdr *nlh, } } - tgt_net = rtnl_link_get_net_capable(skb, sock_net(skb->sk), tb, CAP_NET_ADMIN); + tgt_net = rtnl_link_get_net_capable(skb, sock_net(skb->sk), tb); if (IS_ERR(tgt_net)) { ret = PTR_ERR(tgt_net); goto put_net; @@ -7067,6 +7074,49 @@ static int rtnetlink_dump_start(struct sock *ssk, struct sk_buff *skb, return netlink_dump_start(ssk, skb, nlh, control); } +/* May whoever sent a request that changes something do so? */ +static bool rtnl_may(const struct sk_buff *skb, int msgtype) +{ + const struct net *net = sock_net(skb->sk); + + switch (msgtype) { + case RTM_NEWLINK: + /* Which of the two it takes, __rtnl_newlink() finds out. */ + return netlink_net_may(skb, net, NET_RIGHT_LINK) || + netlink_net_may(skb, net, NET_RIGHT_LINK_NEW); + case RTM_SETLINK: + case RTM_NEWLINKPROP: + case RTM_DELLINKPROP: + return netlink_net_may(skb, net, NET_RIGHT_LINK); + case RTM_DELLINK: + return netlink_net_may(skb, net, NET_RIGHT_LINK_NEW); + case RTM_NEWADDR: + case RTM_DELADDR: + return netlink_net_may(skb, net, NET_RIGHT_ADDR); + case RTM_NEWROUTE: + case RTM_DELROUTE: + case RTM_NEWNEIGH: + case RTM_DELNEIGH: + case RTM_NEWRULE: + case RTM_DELRULE: + case RTM_NEWNEXTHOP: + case RTM_DELNEXTHOP: + return netlink_net_may(skb, net, NET_RIGHT_ROUTE); + case RTM_NEWQDISC: + case RTM_DELQDISC: + case RTM_NEWTCLASS: + case RTM_DELTCLASS: + case RTM_NEWTFILTER: + case RTM_DELTFILTER: + case RTM_NEWACTION: + case RTM_DELACTION: + case RTM_NEWCHAIN: + case RTM_DELCHAIN: + return netlink_net_may(skb, net, NET_RIGHT_TC); + } + return netlink_net_capable(skb, CAP_NET_ADMIN); +} + static int rtnetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh, struct netlink_ext_ack *extack) { @@ -7093,7 +7143,7 @@ static int rtnetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh, family = ((struct rtgenmsg *)nlmsg_data(nlh))->rtgen_family; kind = rtnl_msgtype_kind(type); - if (kind != RTNL_KIND_GET && !netlink_net_capable(skb, CAP_NET_ADMIN)) + if (kind != RTNL_KIND_GET && !rtnl_may(skb, nlh->nlmsg_type)) return -EPERM; rcu_read_lock(); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 77/90] net: use net_may() in the configuration ioctls 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (75 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 76/90] rtnetlink: use net_may() for requests that change state Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 78/90] netfilter: use net_may() for configuration Josef Bacik ` (12 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The ioctls that predate rtnetlink have their own CAP_NET_ADMIN checks. Convert them: NET_RIGHT_LINK the device ioctls in dev_ioctl(), and SIOCSIFFLAGS in devinet_ioctl() NET_RIGHT_ADDR the address ioctls in devinet_ioctl(), and addrconf_add_ifaddr() and addrconf_del_ifaddr() NET_RIGHT_ROUTE SIOCADDRT and SIOCDELRT for IPv4 and IPv6, and SIOCSARP and SIOCDARP No functional change. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- net/core/dev_ioctl.c | 4 ++-- net/ipv4/arp.c | 2 +- net/ipv4/devinet.c | 4 ++-- net/ipv4/fib_frontend.c | 2 +- net/ipv6/addrconf.c | 4 ++-- net/ipv6/route.c | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/net/core/dev_ioctl.c b/net/core/dev_ioctl.c index f6029f60c1dc..caec8909b0c2 100644 --- a/net/core/dev_ioctl.c +++ b/net/core/dev_ioctl.c @@ -757,7 +757,7 @@ int dev_ioctl(struct net *net, unsigned int cmd, struct ifreq *ifr, case SIOCGMIIREG: case SIOCSIFNAME: dev_load(net, ifr->ifr_name); - if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (!net_may(net, NET_RIGHT_LINK)) return -EPERM; rtnl_net_lock(net); @@ -799,7 +799,7 @@ int dev_ioctl(struct net *net, unsigned int cmd, struct ifreq *ifr, case SIOCBONDSETHWADDR: case SIOCBONDCHANGEACTIVE: case SIOCSHWTSTAMP: - if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (!net_may(net, NET_RIGHT_LINK)) return -EPERM; fallthrough; case SIOCBONDSLAVEINFOQUERY: diff --git a/net/ipv4/arp.c b/net/ipv4/arp.c index 1ca4ced280ae..934eead1dc51 100644 --- a/net/ipv4/arp.c +++ b/net/ipv4/arp.c @@ -1285,7 +1285,7 @@ int arp_ioctl(struct net *net, unsigned int cmd, void __user *arg) switch (cmd) { case SIOCDARP: case SIOCSARP: - if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (!net_may(net, NET_RIGHT_ROUTE)) return -EPERM; fallthrough; case SIOCGARP: diff --git a/net/ipv4/devinet.c b/net/ipv4/devinet.c index 1e9d988849c9..ec57948ffbb3 100644 --- a/net/ipv4/devinet.c +++ b/net/ipv4/devinet.c @@ -1088,7 +1088,7 @@ int devinet_ioctl(struct net *net, unsigned int cmd, struct ifreq *ifr) case SIOCSIFFLAGS: ret = -EPERM; - if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (!net_may(net, NET_RIGHT_LINK)) goto out; break; case SIOCSIFADDR: /* Set interface address (and family) */ @@ -1096,7 +1096,7 @@ int devinet_ioctl(struct net *net, unsigned int cmd, struct ifreq *ifr) case SIOCSIFDSTADDR: /* Set the destination address */ case SIOCSIFNETMASK: /* Set the netmask for the interface */ ret = -EPERM; - if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (!net_may(net, NET_RIGHT_ADDR)) goto out; ret = -EINVAL; if (sin->sin_family != AF_INET) diff --git a/net/ipv4/fib_frontend.c b/net/ipv4/fib_frontend.c index 8c123b111b3d..67d1eb7f8b7a 100644 --- a/net/ipv4/fib_frontend.c +++ b/net/ipv4/fib_frontend.c @@ -651,7 +651,7 @@ int ip_rt_ioctl(struct net *net, unsigned int cmd, struct rtentry *rt) switch (cmd) { case SIOCADDRT: /* Add a route */ case SIOCDELRT: /* Delete a route */ - if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (!net_may(net, NET_RIGHT_ROUTE)) return -EPERM; rtnl_net_lock(net); diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c index c8c0dd04982d..3699e5410992 100644 --- a/net/ipv6/addrconf.c +++ b/net/ipv6/addrconf.c @@ -3185,7 +3185,7 @@ int addrconf_add_ifaddr(struct net *net, void __user *arg) struct in6_ifreq ireq; int err; - if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (!net_may(net, NET_RIGHT_ADDR)) return -EPERM; if (copy_from_user(&ireq, arg, sizeof(struct in6_ifreq))) @@ -3212,7 +3212,7 @@ int addrconf_del_ifaddr(struct net *net, void __user *arg) struct in6_ifreq ireq; int err; - if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (!net_may(net, NET_RIGHT_ADDR)) return -EPERM; if (copy_from_user(&ireq, arg, sizeof(struct in6_ifreq))) diff --git a/net/ipv6/route.c b/net/ipv6/route.c index 616b86f89f63..f458a3cae13a 100644 --- a/net/ipv6/route.c +++ b/net/ipv6/route.c @@ -4578,7 +4578,7 @@ int ipv6_route_ioctl(struct net *net, unsigned int cmd, struct in6_rtmsg *rtmsg) if (cmd != SIOCADDRT && cmd != SIOCDELRT) return -EINVAL; - if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (!net_may(net, NET_RIGHT_ROUTE)) return -EPERM; rtmsg_to_fib6_config(net, rtmsg, &cfg); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 78/90] netfilter: use net_may() for configuration 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (76 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 77/90] net: use net_may() in the configuration ioctls Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 79/90] tun: use net_may() to create and attach to devices Josef Bacik ` (11 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Userspace configures netfilter in two ways. One is nfnetlink, which has a single CAP_NET_ADMIN check in nfnetlink_rcv() for all of its subsystems. The other is the set of socket options of ip_tables, ip6_tables, arp_tables, ebtables and ipset. Convert all of these checks to NET_RIGHT_FILTER. Two nfnetlink subsystems keep state that is global rather than per network namespace: the fingerprints of nfnetlink_osf and the helpers of nfnetlink_cthelper. Both also call capable(CAP_NET_ADMIN), which requires the capability in the initial user namespace. Those calls are not changed. IPVS is not converted. No functional change. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- net/bridge/netfilter/ebtables.c | 4 ++-- net/ipv4/netfilter/arp_tables.c | 4 ++-- net/ipv4/netfilter/ip_tables.c | 4 ++-- net/ipv6/netfilter/ip6_tables.c | 4 ++-- net/netfilter/ipset/ip_set_core.c | 2 +- net/netfilter/nfnetlink.c | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/net/bridge/netfilter/ebtables.c b/net/bridge/netfilter/ebtables.c index 0bf541936f60..10a9d206a613 100644 --- a/net/bridge/netfilter/ebtables.c +++ b/net/bridge/netfilter/ebtables.c @@ -2491,7 +2491,7 @@ static int do_ebt_get_ctl(struct sock *sk, int cmd, void __user *user, int *len) struct ebt_table *t; int ret; - if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (!net_may(net, NET_RIGHT_FILTER)) return -EPERM; if (!xt_compat_check()) return -EPERM; @@ -2558,7 +2558,7 @@ static int do_ebt_set_ctl(struct sock *sk, int cmd, sockptr_t arg, struct net *net = sock_net(sk); int ret; - if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (!net_may(net, NET_RIGHT_FILTER)) return -EPERM; if (!xt_compat_check()) return -EPERM; diff --git a/net/ipv4/netfilter/arp_tables.c b/net/ipv4/netfilter/arp_tables.c index cf747e1e17a3..22fe0706b003 100644 --- a/net/ipv4/netfilter/arp_tables.c +++ b/net/ipv4/netfilter/arp_tables.c @@ -974,7 +974,7 @@ static int do_arpt_set_ctl(struct sock *sk, int cmd, sockptr_t arg, { int ret; - if (!ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN)) + if (!net_may(sock_net(sk), NET_RIGHT_FILTER)) return -EPERM; if (!xt_compat_check()) return -EPERM; @@ -999,7 +999,7 @@ static int do_arpt_get_ctl(struct sock *sk, int cmd, void __user *user, int *len { int ret; - if (!ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN)) + if (!net_may(sock_net(sk), NET_RIGHT_FILTER)) return -EPERM; if (!xt_compat_check()) return -EPERM; diff --git a/net/ipv4/netfilter/ip_tables.c b/net/ipv4/netfilter/ip_tables.c index 809441cedced..40a5e3cb7abd 100644 --- a/net/ipv4/netfilter/ip_tables.c +++ b/net/ipv4/netfilter/ip_tables.c @@ -1619,7 +1619,7 @@ do_ipt_set_ctl(struct sock *sk, int cmd, sockptr_t arg, unsigned int len) { int ret; - if (!ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN)) + if (!net_may(sock_net(sk), NET_RIGHT_FILTER)) return -EPERM; if (!xt_compat_check()) return -EPERM; @@ -1650,7 +1650,7 @@ do_ipt_get_ctl(struct sock *sk, int cmd, void __user *user, int *len) { int ret; - if (!ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN)) + if (!net_may(sock_net(sk), NET_RIGHT_FILTER)) return -EPERM; if (!xt_compat_check()) return -EPERM; diff --git a/net/ipv6/netfilter/ip6_tables.c b/net/ipv6/netfilter/ip6_tables.c index 313c4aac377a..5339e35eb1a5 100644 --- a/net/ipv6/netfilter/ip6_tables.c +++ b/net/ipv6/netfilter/ip6_tables.c @@ -1633,7 +1633,7 @@ do_ip6t_set_ctl(struct sock *sk, int cmd, sockptr_t arg, unsigned int len) { int ret; - if (!ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN)) + if (!net_may(sock_net(sk), NET_RIGHT_FILTER)) return -EPERM; if (!xt_compat_check()) return -EPERM; @@ -1664,7 +1664,7 @@ do_ip6t_get_ctl(struct sock *sk, int cmd, void __user *user, int *len) { int ret; - if (!ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN)) + if (!net_may(sock_net(sk), NET_RIGHT_FILTER)) return -EPERM; if (!xt_compat_check()) return -EPERM; diff --git a/net/netfilter/ipset/ip_set_core.c b/net/netfilter/ipset/ip_set_core.c index 4aeb767088cf..dfe2c8ef1054 100644 --- a/net/netfilter/ipset/ip_set_core.c +++ b/net/netfilter/ipset/ip_set_core.c @@ -2267,7 +2267,7 @@ ip_set_sockfn_get(struct sock *sk, int optval, void __user *user, int *len) struct net *net = sock_net(sk); struct ip_set_net *inst = ip_set_pernet(net); - if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (!net_may(net, NET_RIGHT_FILTER)) return -EPERM; if (optval != SO_IP_SET) return -EBADF; diff --git a/net/netfilter/nfnetlink.c b/net/netfilter/nfnetlink.c index 47f3ed441f64..524941691e29 100644 --- a/net/netfilter/nfnetlink.c +++ b/net/netfilter/nfnetlink.c @@ -656,7 +656,7 @@ static void nfnetlink_rcv(struct sk_buff *skb) skb->len < nlh->nlmsg_len) return; - if (!netlink_net_capable(skb, CAP_NET_ADMIN)) { + if (!netlink_net_may(skb, sock_net(skb->sk), NET_RIGHT_FILTER)) { netlink_ack(skb, nlh, -EPERM, NULL); return; } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 79/90] tun: use net_may() to create and attach to devices 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (77 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 78/90] netfilter: use net_may() for configuration Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 80/90] net: use net_may() for the net sysctls Josef Bacik ` (10 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Creating a tun or tap device requires CAP_NET_ADMIN. So does attaching to a device unless the caller is its owner or a member of its group. Convert both checks to NET_RIGHT_TUN. SIOCGSKNS and TUNGETDEVNETNS return a file descriptor for a network namespace. They keep the plain capability check. No functional change. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- drivers/net/tun.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/net/tun.c b/drivers/net/tun.c index dcec7311e601..0a2f864a253e 100644 --- a/drivers/net/tun.c +++ b/drivers/net/tun.c @@ -524,7 +524,7 @@ static inline bool tun_permitted(struct tun_struct *tun) (!gid_valid(tun->group) || in_egroup_p(tun->group))) return true; - return ns_capable(net->user_ns, CAP_NET_ADMIN); + return net_may(net, NET_RIGHT_TUN); } static void tun_set_real_num_queues(struct tun_struct *tun) @@ -2924,7 +2924,7 @@ static int tun_set_iff(struct net *net, struct file *file, struct ifreq *ifr) int queues = ifr->ifr_flags & IFF_MULTI_QUEUE ? MAX_TAP_QUEUES : 1; - if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (!net_may(net, NET_RIGHT_TUN)) return -EPERM; err = security_tun_dev_create(); if (err < 0) -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 80/90] net: use net_may() for the net sysctls 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (78 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 79/90] tun: use net_may() to create and attach to devices Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 81/90] net: use net_may() for privileged socket options Josef Bacik ` (9 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik net_ctl_permissions() gives a task with CAP_NET_ADMIN over the owner of a network namespace the same access to the sysctls of that namespace as their owner has. The handler of the IPv4 forwarding sysctls checks the capability a second time on a write. Convert both checks to NET_RIGHT_SYSCTL. No functional change. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- net/ipv4/devinet.c | 2 +- net/sysctl_net.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/net/ipv4/devinet.c b/net/ipv4/devinet.c index ec57948ffbb3..44baea38bec4 100644 --- a/net/ipv4/devinet.c +++ b/net/ipv4/devinet.c @@ -2602,7 +2602,7 @@ static int devinet_sysctl_forward(const struct ctl_table *ctl, int write, int val = *valp; int ret; - if (write && !ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (write && !net_may(net, NET_RIGHT_SYSCTL)) return -EPERM; ret = proc_dointvec(ctl, write, buffer, lenp, ppos); diff --git a/net/sysctl_net.c b/net/sysctl_net.c index e190a639eef2..1a79b8a3674d 100644 --- a/net/sysctl_net.c +++ b/net/sysctl_net.c @@ -45,7 +45,7 @@ static int net_ctl_permissions(struct ctl_table_header *head, struct net *net = container_of(head->set, struct net, sysctls); /* Allow network administrator to have same access as root. */ - if (ns_capable_noaudit(net->user_ns, CAP_NET_ADMIN)) { + if (net_may_noaudit(net, NET_RIGHT_SYSCTL)) { int mode = (table->mode >> 6) & 7; return (mode << 6) | (mode << 3) | mode; } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 81/90] net: use net_may() for privileged socket options 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (79 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 80/90] net: use net_may() for the net sysctls Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 82/90] net: use net_may() for raw and packet sockets Josef Bacik ` (8 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Some socket options affect how the network namespace treats packets, not just the socket, and require a capability. Convert their checks. These accept CAP_NET_ADMIN or CAP_NET_RAW today, and now accept NET_RIGHT_SOCKOPT or NET_RIGHT_RAW: SO_MARK, both as an option and as a control message, SO_PRIORITY outside of the unprivileged range, IP_TRANSPARENT and IPV6_TRANSPARENT. This one requires CAP_NET_ADMIN, and now NET_RIGHT_SOCKOPT: SO_TXTIME with a clock other than CLOCK_MONOTONIC. These require CAP_NET_RAW, and now NET_RIGHT_RAW: SO_BINDTODEVICE on a socket that is already bound to a device, and the IPv6 hop-by-hop and destination options. Not converted: IP_IPSEC_POLICY, IP_XFRM_POLICY and their IPv6 counterparts, and SO_ATTACH_FILTER on a TCP socket. No functional change. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- net/core/sock.c | 16 ++++++++-------- net/ipv4/ip_sockglue.c | 4 ++-- net/ipv6/ipv6_sockglue.c | 7 +++---- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/net/core/sock.c b/net/core/sock.c index 352310066e35..ba870aa6b967 100644 --- a/net/core/sock.c +++ b/net/core/sock.c @@ -462,8 +462,8 @@ static int sock_set_timeout(long *timeo_p, sockptr_t optval, int optlen, static bool sk_set_prio_allowed(const struct sock *sk, int val) { return ((val >= TC_PRIO_BESTEFFORT && val <= TC_PRIO_INTERACTIVE) || - sockopt_ns_capable(sock_net(sk)->user_ns, CAP_NET_RAW) || - sockopt_ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN)); + sockopt_net_may(sk, NET_RIGHT_RAW) || + sockopt_net_may(sk, NET_RIGHT_SOCKOPT)); } static bool sock_needs_netstamp(const struct sock *sk) @@ -642,7 +642,7 @@ static int sock_bindtoindex_locked(struct sock *sk, int ifindex) /* Sorry... */ ret = -EPERM; - if (sk->sk_bound_dev_if && !ns_capable(net->user_ns, CAP_NET_RAW)) + if (sk->sk_bound_dev_if && !net_may(net, NET_RIGHT_RAW)) goto out; ret = -EINVAL; @@ -1540,8 +1540,8 @@ int sk_setsockopt(struct sock *sk, int level, int optname, break; case SO_MARK: - if (!sockopt_ns_capable(sock_net(sk)->user_ns, CAP_NET_RAW) && - !sockopt_ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN)) { + if (!sockopt_net_may(sk, NET_RIGHT_RAW) && + !sockopt_net_may(sk, NET_RIGHT_SOCKOPT)) { ret = -EPERM; break; } @@ -1649,7 +1649,7 @@ int sk_setsockopt(struct sock *sk, int level, int optname, * scheduler has enough safe guards. */ if (sk_txtime.clockid != CLOCK_MONOTONIC && - !sockopt_ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN)) { + !sockopt_net_may(sk, NET_RIGHT_SOCKOPT)) { ret = -EPERM; break; } @@ -3172,8 +3172,8 @@ int __sock_cmsg_send(struct sock *sk, struct cmsghdr *cmsg, switch (cmsg->cmsg_type) { case SO_MARK: - if (!ns_capable(sock_net(sk)->user_ns, CAP_NET_RAW) && - !ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN)) + if (!net_may(sock_net(sk), NET_RIGHT_RAW) && + !net_may(sock_net(sk), NET_RIGHT_SOCKOPT)) return -EPERM; if (cmsg->cmsg_len != CMSG_LEN(sizeof(u32))) return -EINVAL; diff --git a/net/ipv4/ip_sockglue.c b/net/ipv4/ip_sockglue.c index e06c1f48ecad..cc9e0bc952f2 100644 --- a/net/ipv4/ip_sockglue.c +++ b/net/ipv4/ip_sockglue.c @@ -1008,8 +1008,8 @@ int do_ip_setsockopt(struct sock *sk, int level, int optname, inet_assign_bit(MC_ALL, sk, val); return 0; case IP_TRANSPARENT: - if (!!val && !sockopt_ns_capable(sock_net(sk)->user_ns, CAP_NET_RAW) && - !sockopt_ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN)) + if (!!val && !sockopt_net_may(sk, NET_RIGHT_RAW) && + !sockopt_net_may(sk, NET_RIGHT_SOCKOPT)) return -EPERM; if (optlen < 1) return -EINVAL; diff --git a/net/ipv6/ipv6_sockglue.c b/net/ipv6/ipv6_sockglue.c index 4b3536571c98..d2880493d33b 100644 --- a/net/ipv6/ipv6_sockglue.c +++ b/net/ipv6/ipv6_sockglue.c @@ -304,12 +304,11 @@ static int ipv6_set_opt_hdr(struct sock *sk, int optname, sockptr_t optval, { struct ipv6_pinfo *np = inet6_sk(sk); struct ipv6_opt_hdr *new = NULL; - struct net *net = sock_net(sk); struct ipv6_txoptions *opt; int err; /* hop-by-hop / destination options are privileged option */ - if (optname != IPV6_RTHDR && !sockopt_ns_capable(net->user_ns, CAP_NET_RAW)) + if (optname != IPV6_RTHDR && !sockopt_net_may(sk, NET_RIGHT_RAW)) return -EPERM; /* remove any sticky options header with a zero option @@ -405,8 +404,8 @@ int do_ipv6_setsockopt(struct sock *sk, int level, int optname, inet_assign_bit(FREEBIND, sk, valbool); return 0; case IPV6_TRANSPARENT: - if (valbool && !sockopt_ns_capable(net->user_ns, CAP_NET_RAW) && - !sockopt_ns_capable(net->user_ns, CAP_NET_ADMIN)) + if (valbool && !sockopt_net_may(sk, NET_RIGHT_RAW) && + !sockopt_net_may(sk, NET_RIGHT_SOCKOPT)) return -EPERM; if (optlen < sizeof(int)) return -EINVAL; -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 82/90] net: use net_may() for raw and packet sockets 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (80 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 81/90] net: use net_may() for privileged socket options Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 83/90] cells: add the "net-rights" slot Josef Bacik ` (7 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Creating an IPv4 or IPv6 raw socket or a packet socket requires CAP_NET_RAW over the owner of the network namespace. So does sending certain IP options and IPv6 extension headers from an ordinary socket. Convert these checks to NET_RIGHT_RAW. AF_XDP, LLC, NFC and IEEE 802.15.4 sockets are not converted. No functional change. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- net/ipv4/af_inet.c | 2 +- net/ipv4/ip_options.c | 8 ++++---- net/ipv6/af_inet6.c | 2 +- net/ipv6/datagram.c | 6 +++--- net/packet/af_packet.c | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/net/ipv4/af_inet.c b/net/ipv4/af_inet.c index 7677b0343abd..b24b50e0098e 100644 --- a/net/ipv4/af_inet.c +++ b/net/ipv4/af_inet.c @@ -320,7 +320,7 @@ static int inet_create(struct net *net, struct socket *sock, int protocol, err = -EPERM; if (sock->type == SOCK_RAW && !kern && - !ns_capable(net->user_ns, CAP_NET_RAW)) + !net_may(net, NET_RIGHT_RAW)) goto out_rcu_unlock; sock->ops = answer->ops; diff --git a/net/ipv4/ip_options.c b/net/ipv4/ip_options.c index 09d745112c15..c9ab57a6f1a8 100644 --- a/net/ipv4/ip_options.c +++ b/net/ipv4/ip_options.c @@ -393,7 +393,7 @@ int __ip_options_compile(struct net *net, optptr[2] += 8; break; default: - if (!skb && !ns_capable(net->user_ns, CAP_NET_RAW)) { + if (!skb && !net_may(net, NET_RIGHT_RAW)) { pp_ptr = optptr + 3; goto error; } @@ -428,7 +428,7 @@ int __ip_options_compile(struct net *net, opt->router_alert = optptr - iph; break; case IPOPT_CIPSO: - if ((!skb && !ns_capable(net->user_ns, CAP_NET_RAW)) || opt->cipso) { + if ((!skb && !net_may(net, NET_RIGHT_RAW)) || opt->cipso) { pp_ptr = optptr; goto error; } @@ -441,7 +441,7 @@ int __ip_options_compile(struct net *net, case IPOPT_SEC: case IPOPT_SID: default: - if (!skb && !ns_capable(net->user_ns, CAP_NET_RAW)) { + if (!skb && !net_may(net, NET_RIGHT_RAW)) { pp_ptr = optptr; goto error; } @@ -530,7 +530,7 @@ int ip_options_get(struct net *net, struct ip_options_rcu **optp, kfree(opt); return -EINVAL; } - if (opt->opt.srr && !ns_capable(net->user_ns, CAP_NET_RAW)) { + if (opt->opt.srr && !net_may(net, NET_RIGHT_RAW)) { kfree(opt); return -EPERM; } diff --git a/net/ipv6/af_inet6.c b/net/ipv6/af_inet6.c index a9b8b4b42a1a..03a630b92680 100644 --- a/net/ipv6/af_inet6.c +++ b/net/ipv6/af_inet6.c @@ -164,7 +164,7 @@ static int inet6_create(struct net *net, struct socket *sock, int protocol, err = -EPERM; if (sock->type == SOCK_RAW && !kern && - !ns_capable(net->user_ns, CAP_NET_RAW)) + !net_may(net, NET_RIGHT_RAW)) goto out_rcu_unlock; sock->ops = answer->ops; diff --git a/net/ipv6/datagram.c b/net/ipv6/datagram.c index 191c9733ff9f..3d3e9fd4443a 100644 --- a/net/ipv6/datagram.c +++ b/net/ipv6/datagram.c @@ -912,7 +912,7 @@ int ip6_datagram_send_ctl(struct net *net, struct sock *sk, err = -EINVAL; goto exit_f; } - if (!ns_capable(net->user_ns, CAP_NET_RAW)) { + if (!net_may(net, NET_RIGHT_RAW)) { err = -EPERM; goto exit_f; } @@ -932,7 +932,7 @@ int ip6_datagram_send_ctl(struct net *net, struct sock *sk, err = -EINVAL; goto exit_f; } - if (!ns_capable(net->user_ns, CAP_NET_RAW)) { + if (!net_may(net, NET_RIGHT_RAW)) { err = -EPERM; goto exit_f; } @@ -957,7 +957,7 @@ int ip6_datagram_send_ctl(struct net *net, struct sock *sk, err = -EINVAL; goto exit_f; } - if (!ns_capable(net->user_ns, CAP_NET_RAW)) { + if (!net_may(net, NET_RIGHT_RAW)) { err = -EPERM; goto exit_f; } diff --git a/net/packet/af_packet.c b/net/packet/af_packet.c index cf6429f09477..006349f214e6 100644 --- a/net/packet/af_packet.c +++ b/net/packet/af_packet.c @@ -3417,7 +3417,7 @@ static int packet_create(struct net *net, struct socket *sock, int protocol, __be16 proto = (__force __be16)protocol; /* weird, but documented */ int err; - if (!ns_capable(net->user_ns, CAP_NET_RAW)) + if (!net_may(net, NET_RIGHT_RAW)) return -EPERM; if (sock->type != SOCK_DGRAM && sock->type != SOCK_RAW && sock->type != SOCK_PACKET) -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 83/90] cells: add the "net-rights" slot 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (81 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 82/90] net: use net_may() for raw and packet sockets Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 84/90] net: honour the rights of a cell in net_may() Josef Bacik ` (6 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A cell never owns its network namespace. The capabilities of its tasks are over the user namespace of the cell, so they do not apply to the network namespace, and the tasks cannot change anything about their network. That is right for most workloads but not for all. Some need to add netfilter rules, run a VPN client, or create a tap device for a VM. A container gets CAP_NET_ADMIN for any of these, and with it everything else that the capability allows. Let the builder list what the tasks of the cell may do to the network namespace in the "net" slot. The new "net-rights" slot takes a __u64 with one bit for each enum net_right. An empty slot grants nothing, which is the behaviour so far. The caller can only grant what it could do itself. It must pass net_may() on that network namespace for every right it names. This is why the "net" slot has to be filled first. For a builder this means having the capability over the owner of the namespace. Once cells can nest, the same check will let a task of a cell pass on the rights of its own cell. This patch only stores the rights. A later patch makes net_may() use them. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/uapi/linux/cell.h | 15 +++++++++++++++ kernel/cell/internal.h | 1 + kernel/cell/slots.c | 45 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/include/uapi/linux/cell.h b/include/uapi/linux/cell.h index 3c10c9179289..c0d17c890552 100644 --- a/include/uapi/linux/cell.h +++ b/include/uapi/linux/cell.h @@ -21,6 +21,21 @@ enum cell_configure_cmd { CELL_CMD_SEAL = 4, /* Validate everything and seal */ }; +/* + * The "net-rights" slot: what the tasks of a cell may do to the network of the cell. + * What is not here they may not do. + */ +#define CELL_NET_LINK (1ULL << 0) /* change a device: up, down, mtu, name */ +#define CELL_NET_LINK_NEW (1ULL << 1) /* make and remove devices */ +#define CELL_NET_ADDR (1ULL << 2) /* addresses */ +#define CELL_NET_ROUTE (1ULL << 3) /* routes, rules, neighbours */ +#define CELL_NET_FILTER (1ULL << 4) /* netfilter */ +#define CELL_NET_TC (1ULL << 5) /* queueing disciplines, classes, filters */ +#define CELL_NET_TUN (1ULL << 6) /* make tun and tap devices */ +#define CELL_NET_SYSCTL (1ULL << 7) /* /proc/sys/net */ +#define CELL_NET_SOCKOPT (1ULL << 8) /* SO_MARK, IP_TRANSPARENT and the like */ +#define CELL_NET_RAW (1ULL << 9) /* raw and packet sockets */ + /* Life of a cell. It only ever moves down this list. */ enum cell_state { CELL_BUILDING = 0, /* Slots may be filled */ diff --git a/kernel/cell/internal.h b/kernel/cell/internal.h index 4eafa19daa25..469842ebee95 100644 --- a/kernel/cell/internal.h +++ b/kernel/cell/internal.h @@ -34,6 +34,7 @@ struct cell { struct pid_namespace *pid_ns; struct net *net; bool no_net; /* the slot was left empty */ + u64 net_rights; /* what its tasks may do to @net */ struct cgroup *budget; u64 budget_id; kuid_t budget_uid; /* who owned it before */ diff --git a/kernel/cell/slots.c b/kernel/cell/slots.c index 2273d9ce733f..5f5378ddfe15 100644 --- a/kernel/cell/slots.c +++ b/kernel/cell/slots.c @@ -157,6 +157,49 @@ static int cell_set_caps(struct cell *cell, const void __user *uvalue, size_t le return 0; } +/* + * What the tasks of the cell may do to its network. The network namespace is never + * the cell's, so no capability of theirs counts for it, see net_may(). + * + * Nobody hands out what is not theirs to do: whoever fills the slot has to be able + * to do all of it to that network namespace itself. + */ +static int cell_set_net_rights(struct cell *cell, const void __user *uvalue, size_t len) +{ + unsigned long rights; + unsigned int right; + u64 val; + + BUILD_BUG_ON(BIT_ULL(NET_RIGHT_LINK) != CELL_NET_LINK || + BIT_ULL(NET_RIGHT_LINK_NEW) != CELL_NET_LINK_NEW || + BIT_ULL(NET_RIGHT_ADDR) != CELL_NET_ADDR || + BIT_ULL(NET_RIGHT_ROUTE) != CELL_NET_ROUTE || + BIT_ULL(NET_RIGHT_FILTER) != CELL_NET_FILTER || + BIT_ULL(NET_RIGHT_TC) != CELL_NET_TC || + BIT_ULL(NET_RIGHT_TUN) != CELL_NET_TUN || + BIT_ULL(NET_RIGHT_SYSCTL) != CELL_NET_SYSCTL || + BIT_ULL(NET_RIGHT_SOCKOPT) != CELL_NET_SOCKOPT || + BIT_ULL(NET_RIGHT_RAW) != CELL_NET_RAW || + NET_RIGHT_NR > BITS_PER_LONG); + + if (len != sizeof(val)) + return -EINVAL; + if (copy_from_user(&val, uvalue, sizeof(val))) + return -EFAULT; + if (val & ~(BIT_ULL(NET_RIGHT_NR) - 1)) + return -EINVAL; + /* Rights over what? */ + if (!cell->net) + return -EINVAL; + rights = val; + for_each_set_bit(right, &rights, NET_RIGHT_NR) { + if (!net_may(cell->net, right)) + return -EPERM; + } + cell->net_rights = val; + return 0; +} + static int cell_set_string(struct cell *cell, const char *key, const char *val) { if (!strcmp(key, "hostname")) @@ -173,6 +216,8 @@ static int cell_set_binary(struct cell *cell, const char *key, return cell_set_ids_binary(cell, uvalue, len); if (!strcmp(key, "caps")) return cell_set_caps(cell, uvalue, len); + if (!strcmp(key, "net-rights")) + return cell_set_net_rights(cell, uvalue, len); return -EINVAL; } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 84/90] net: honour the rights of a cell in net_may() 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (82 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 83/90] cells: add the "net-rights" slot Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 85/90] tun: ignore an equal uid or gid across cells Josef Bacik ` (5 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik net_may() requires a capability over the user namespace that owns the network namespace. A task of a cell never has that, because a cell does not own a network namespace. For a task of a cell, use the "net-rights" slot instead. net_may() succeeds if all of these hold: - the network namespace is the one in the "net" slot of the cell - the builder granted the right in the "net-rights" slot - the task has the capability that the right maps to, in the user namespace of the cell The slot decides what the cell may do, and the capability decides which tasks of the cell may do it. An init can set up netfilter rules and then drop CAP_NET_ADMIN before it starts anything else. A task that is not root in the cell cannot undo those rules. LSMs and audit still see a capability check. The network namespace still belongs to whoever built the cell. The builder sets it up with the usual tools and can enter it at any time. A check that has not been converted to net_may() still requires the capability over the owner of the namespace, so it fails for every task of a cell. Anything that nobody has looked at is therefore refused, and each conversion adds one thing that a builder can allow. The rights apply to the network namespace of the cell only: - A socket that was created in another network namespace and passed to the cell carries no rights. - A request that names another network namespace as the place to move a device to, or to create one in, is checked against that namespace and fails. This matters because such a namespace can be named from inside a cell: a veth device whose peer is in another namespace gives its own namespace an id for the other one, and any task can read that id. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/linux/cell.h | 11 +++++++++++ kernel/cell/boundary.c | 19 +++++++++++++++++++ net/core/net_namespace.c | 8 ++++++++ 3 files changed, 38 insertions(+) diff --git a/include/linux/cell.h b/include/linux/cell.h index b12e2f124769..e79ac7e9ec60 100644 --- a/include/linux/cell.h +++ b/include/linux/cell.h @@ -256,6 +256,17 @@ bool cell_no_net(void); static inline bool cell_no_net(void) { return false; } #endif +/* What a cell may do to its network. */ +#ifdef CONFIG_CELLS +bool cell_has_net_right(const struct cred *cred, const struct net *net, unsigned int right); +#else +static inline bool cell_has_net_right(const struct cred *cred, const struct net *net, + unsigned int right) +{ + return false; +} +#endif + /* A mount is idmapped for a cell. */ #ifdef CONFIG_CELLS struct user_namespace *cell_idmap_userns(const struct file *file); diff --git a/kernel/cell/boundary.c b/kernel/cell/boundary.c index af67ef68d4a2..dd49a6e4b39c 100644 --- a/kernel/cell/boundary.c +++ b/kernel/cell/boundary.c @@ -104,3 +104,22 @@ bool cell_no_net(void) return cell && cell->no_net; } + +/** + * cell_has_net_right - has a cell been given a right over a network namespace? + * @cred: credentials of a task of the cell + * @net: the network namespace + * @right: an enum net_right + * + * The rights of a cell are over the network namespace in its "net" slot and over no + * other, so a socket that was made elsewhere and handed to the cell comes with none. + * + * Return: true if @net is that of the cell of @cred and @right is in its + * "net-rights" slot. + */ +bool cell_has_net_right(const struct cred *cred, const struct net *net, unsigned int right) +{ + const struct cell *cell = cred_cell(cred); + + return cell && READ_ONCE(cell->net) == net && (cell->net_rights & BIT_ULL(right)); +} diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c index d211631f8c42..be29eddaa9a2 100644 --- a/net/core/net_namespace.c +++ b/net/core/net_namespace.c @@ -16,6 +16,7 @@ #include <linux/file.h> #include <linux/export.h> #include <linux/user_namespace.h> +#include <linux/cell.h> #include <linux/net_namespace.h> #include <linux/sched/task.h> #include <linux/uidgid.h> @@ -371,6 +372,13 @@ static int net_right_cap(enum net_right right) static struct user_namespace *net_right_ns(const struct cred *cred, const struct net *net, enum net_right right) { + /* + * A cell owns no network namespace. What its tasks may do to the one it was + * given is what its builder named, and who among them may do it is whoever + * has the capability in there. + */ + if (userns_is_cell(cred->user_ns)) + return cell_has_net_right(cred, net, right) ? cred->user_ns : NULL; return net->user_ns; } -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 85/90] tun: ignore an equal uid or gid across cells 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (83 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 84/90] net: honour the rights of a cell in net_may() Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 86/90] cells: add cell_info() Josef Bacik ` (4 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik A tun device can have an owner and a group, set with TUNSETOWNER and TUNSETGROUP. tun_permitted() lets a task attach to the device without CAP_NET_ADMIN if its euid is the owner, or it is a member of the group, or both if both are set. That check does not know about cells. A one-id cell runs with the uid and gid of the user who built it. If the cell has /dev/net/tun in its tree and uses a network namespace in which that user owns tun devices, its tasks pass the check for all of them. They can attach, read the packets and inject their own, with no capability and no right over the network. A cell with a block of ids has a related problem. Blocks are reused, and a persistent tun device can outlive the cell that its owner belonged to. A later cell that is lent the same block would pass the check. Fix both the way it is done for keys. Record the id of the cell that the caller of TUNSETOWNER or TUNSETGROUP is in, and honour the owner and the group only for tasks of that cell. Cell ids are never reused. Any other task needs NET_RIGHT_TUN, as it does for a device that belongs to somebody else. As a consequence, an owner that a builder sets from outside of the cell no longer lets a task of the cell attach. The builder can hand the device to the task already open, or give the cell CELL_NET_TUN. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- drivers/net/tun.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/drivers/net/tun.c b/drivers/net/tun.c index 0a2f864a253e..ef1cc5617812 100644 --- a/drivers/net/tun.c +++ b/drivers/net/tun.c @@ -47,6 +47,7 @@ #include <linux/miscdevice.h> #include <linux/ethtool.h> #include <linux/rtnetlink.h> +#include <linux/cell.h> #include <linux/compat.h> #include <linux/if.h> #include <linux/if_arp.h> @@ -185,6 +186,8 @@ struct tun_struct { unsigned int flags; kuid_t owner; kgid_t group; + u64 owner_cell; /* the cell that @owner is to be read in */ + u64 group_cell; /* and @group */ struct net_device *dev; netdev_features_t set_features; @@ -518,6 +521,14 @@ static inline bool tun_permitted(struct tun_struct *tun) const struct cred *cred = current_cred(); struct net *net = dev_net(tun->dev); + /* + * Across cells an equal uid, or gid, means nothing: a cell of one id runs on + * those of its maker, and a block of ids is lent to one cell after another. + */ + if ((uid_valid(tun->owner) && tun->owner_cell != cred_cell_id(cred)) || + (gid_valid(tun->group) && tun->group_cell != cred_cell_id(cred))) + return net_may(net, NET_RIGHT_TUN); + /* If both uid and gid are set, *both* must match */ if ((uid_valid(tun->owner) || gid_valid(tun->group)) && (!uid_valid(tun->owner) || uid_eq(cred->euid, tun->owner)) && @@ -3338,6 +3349,7 @@ static long __tun_chr_ioctl(struct file *file, unsigned int cmd, break; } WRITE_ONCE(tun->owner, owner); + tun->owner_cell = current_cell_id(); do_notify = true; netif_info(tun, drv, tun->dev, "owner set to %u\n", from_kuid(&init_user_ns, owner)); @@ -3351,6 +3363,7 @@ static long __tun_chr_ioctl(struct file *file, unsigned int cmd, break; } WRITE_ONCE(tun->group, group); + tun->group_cell = current_cell_id(); do_notify = true; netif_info(tun, drv, tun->dev, "group set to %u\n", from_kgid(&init_user_ns, group)); -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 86/90] cells: add cell_info() 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (84 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 85/90] tun: ignore an equal uid or gid across cells Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 87/90] cells: add CONFIG_CELLS Josef Bacik ` (3 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Add the system call that reads from a cell. It reports the id, state, flags, exit status, budget, capabilities, ids and network rights of the cell. It comes after the slots in this series so that it can report all of them. The kernel chooses the block of ids that a cell gets. cell_info() tells the builder which block it was, as soon as one has been allocated. The builder needs to know, because some things outside of the cell are configured by id, such as a netfilter rule that matches the owner of a socket, or a routing rule for a range of uids. These are set up together with the network of the cell, before anything runs in it. The first id of the block is reported as the caller sees it. It is not reported at all if the user namespace of the caller has no mapping for it. The struct can grow. The caller sets mask to what it wants. The kernel sets mask to what it filled in, and supported_mask to what it knows about. With CELL_SELF in place of a cellfd, a task can ask for the id of its own cell and nothing else. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- include/uapi/linux/cell.h | 28 ++++++++++++++ kernel/cell/core.c | 93 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/include/uapi/linux/cell.h b/include/uapi/linux/cell.h index c0d17c890552..22cd5e1f8f24 100644 --- a/include/uapi/linux/cell.h +++ b/include/uapi/linux/cell.h @@ -45,6 +45,34 @@ enum cell_state { CELL_DEAD = 4, /* Nothing left. The status can be read */ }; +/* cell_info(): bits for @mask. */ +#define CELL_INFO_ID (1ULL << 0) /* id, parent_id */ +#define CELL_INFO_STATE (1ULL << 1) /* state, flags */ +#define CELL_INFO_EXIT (1ULL << 2) /* exit_status, once CELL_DEAD */ +#define CELL_INFO_BUDGET (1ULL << 3) /* budget_cgroupid */ +#define CELL_INFO_IDS (1ULL << 4) /* ids_count */ +#define CELL_INFO_CAPS (1ULL << 5) /* caps */ +#define CELL_INFO_IDS_FIRST (1ULL << 6) /* ids_first, if the caller has a name for it */ +#define CELL_INFO_NET_RIGHTS (1ULL << 7) /* net_rights */ + +struct cell_info { + __u64 mask; /* in: what is wanted. out: what was filled in */ + __u64 supported_mask; /* out: everything this kernel can fill in */ + __u64 id; /* never reused */ + __u64 parent_id; /* 0: the host */ + __u32 state; /* enum cell_state */ + __u32 flags; /* none yet */ + __s32 exit_status; /* as wait(2) reports it, of the cell's first task */ + __u32 ids_count; /* how many uids and gids the cell has */ + __u64 budget_cgroupid; + __u64 caps; /* the capabilities its tasks start with, bit by number */ + __u32 ids_first; /* what uid 0 and gid 0 of a block are to the caller */ + __u32 __spare; + __u64 net_rights; /* CELL_NET_* */ +}; + +#define CELL_INFO_SIZE_VER0 80 + /* cell_open(): @what. */ enum cell_open_what { CELL_OPEN_SLOT = 0, /* An fd on the object in the slot named by @key */ diff --git a/kernel/cell/core.c b/kernel/cell/core.c index c780094ad794..12773f16d188 100644 --- a/kernel/cell/core.c +++ b/kernel/cell/core.c @@ -386,3 +386,96 @@ SYSCALL_DEFINE2(cell_kill, int, cfd, unsigned int, flags) cell_kill(cell); return 0; } + +#define CELL_INFO_SUPPORTED (CELL_INFO_ID | CELL_INFO_STATE | CELL_INFO_EXIT | \ + CELL_INFO_BUDGET | CELL_INFO_IDS | \ + CELL_INFO_CAPS | CELL_INFO_IDS_FIRST | \ + CELL_INFO_NET_RIGHTS) + +/** + * sys_cell_info - read from a cell + * @cfd: a cellfd, or CELL_SELF, which tells a task the id of its own cell + * @uinfo: where to return it. Only @uinfo->mask is read + * @usize: the size of *@uinfo as the caller knows it + * @flags: none yet, must be 0 + * + * Fills in what @uinfo->mask asks for and the kernel has, and says in the mask + * what that was. @uinfo->supported_mask is everything this kernel knows of. + * + * Return: 0 or a negative error. + */ +SYSCALL_DEFINE4(cell_info, int, cfd, struct cell_info __user *, uinfo, + size_t, usize, unsigned int, flags) +{ + struct cell_info info = {}; + struct cell *cell; + u64 want; + + BUILD_BUG_ON(sizeof(struct cell_info) != CELL_INFO_SIZE_VER0); + + if (flags) + return -EINVAL; + if (usize < CELL_INFO_SIZE_VER0) + return -EINVAL; + /* Only the mask goes in. The rest of the struct is the kernel's to fill. */ + if (get_user(want, &uinfo->mask)) + return -EFAULT; + info.supported_mask = CELL_INFO_SUPPORTED; + + if (cfd == CELL_SELF) { + /* A name, and nothing that a handle would be needed for. */ + info.id = current_cell_id(); + info.mask = want & CELL_INFO_ID; + return copy_struct_to_user(uinfo, usize, &info, sizeof(info), NULL); + } + + CLASS(fd, f)(cfd); + cell = cell_from_fd(&f); + if (IS_ERR(cell)) + return PTR_ERR(cell); + + scoped_guard(mutex, &cell->lock) { + if (want & CELL_INFO_ID) { + info.id = cell->id; + /* Cells do not nest yet. */ + info.parent_id = 0; + info.mask |= CELL_INFO_ID; + } + if (want & CELL_INFO_STATE) { + info.state = cell->state; + /* There are none yet. */ + info.flags = 0; + info.mask |= CELL_INFO_STATE; + } + if ((want & CELL_INFO_EXIT) && cell->state == CELL_DEAD) { + info.exit_status = cell->exit_status; + info.mask |= CELL_INFO_EXIT; + } + if ((want & CELL_INFO_BUDGET) && cell->budget_id) { + info.budget_cgroupid = cell->budget_id; + info.mask |= CELL_INFO_BUDGET; + } + if ((want & CELL_INFO_IDS) && cell->ids_count) { + info.ids_count = cell->ids_count; + info.mask |= CELL_INFO_IDS; + } + if (want & CELL_INFO_CAPS) { + info.caps = cell->caps.val; + info.mask |= CELL_INFO_CAPS; + } + if (want & CELL_INFO_NET_RIGHTS) { + info.net_rights = cell->net_rights; + info.mask |= CELL_INFO_NET_RIGHTS; + } + /* Uids and gids alike. Not everybody has a name for an id of a block. */ + if ((want & CELL_INFO_IDS_FIRST) && cell->ids_count > 1 && cell->user_ns) { + info.ids_first = from_kuid(current_user_ns(), + make_kuid(cell->user_ns, 0)); + if (info.ids_first != (uid_t)-1) + info.mask |= CELL_INFO_IDS_FIRST; + else + info.ids_first = 0; + } + } + return copy_struct_to_user(uinfo, usize, &info, sizeof(info), NULL); +} -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 87/90] cells: add CONFIG_CELLS 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (85 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 86/90] cells: add cell_info() Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 88/90] arch: wire up the cell system calls Josef Bacik ` (2 subsequent siblings) 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Everything is in place now. Add the config symbol, and build kernel/cell/, kernel/embryo.c and fs/cellfs.c if it is set. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- fs/Makefile | 2 ++ init/Kconfig | 16 ++++++++++++++++ kernel/Makefile | 1 + 3 files changed, 19 insertions(+) diff --git a/fs/Makefile b/fs/Makefile index 16f1108b64e1..6a15893003e3 100644 --- a/fs/Makefile +++ b/fs/Makefile @@ -127,3 +127,5 @@ obj-$(CONFIG_VBOXSF_FS) += vboxsf/ obj-$(CONFIG_ZONEFS_FS) += zonefs/ obj-$(CONFIG_BPF_LSM) += bpf_fs_kfuncs.o obj-$(CONFIG_RESCTRL_FS) += resctrl/ + +obj-$(CONFIG_CELLS) += cellfs.o diff --git a/init/Kconfig b/init/Kconfig index 4013781e0a02..78690b74e6cb 100644 --- a/init/Kconfig +++ b/init/Kconfig @@ -1264,6 +1264,22 @@ config USER_NS_MAP_KUNIT_TEST If unsure, say N. +config CELLS + bool "Cells" + depends on USER_NS && PID_NS && NET_NS && IPC_NS && UTS_NS && CGROUPS + depends on UNIX + help + A cell is a container as one kernel object. It is built from the + outside by filling slots from file descriptors, sealed, and has + tasks spawned into it. A task never changes cell and nothing can + enter one. + + This reserves the upper half of the uid and gid space for the + kernel to hand out to cells: with it enabled no file on a filesystem + of the host can be given to an id from 2147483648 to 4294901759. + + If unsure, say N. + config PID_NS bool "PID Namespaces" default y diff --git a/kernel/Makefile b/kernel/Makefile index da712b4399ff..ba60039affdb 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -90,6 +90,7 @@ obj-$(CONFIG_COMPAT) += compat.o obj-$(CONFIG_CGROUPS) += cgroup/ obj-$(CONFIG_UTS_NS) += utsname.o obj-$(CONFIG_USER_NS) += user_namespace.o +obj-$(CONFIG_CELLS) += cell/ embryo.o obj-$(CONFIG_PID_NS) += pid_namespace.o obj-$(CONFIG_IKCONFIG) += configs.o obj-$(CONFIG_IKHEADERS) += kheaders.o -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 88/90] arch: wire up the cell system calls 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (86 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 87/90] cells: add CONFIG_CELLS Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 89/90] MAINTAINERS: add an entry for cells Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 90/90] selftests: add tests " Josef Bacik 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Wire up cell_create(), cell_configure(), cell_info(), cell_open(), cell_kill() and pidfd_configure() on x86 and in the generic system call table. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- arch/x86/entry/syscalls/syscall_32.tbl | 6 ++++++ arch/x86/entry/syscalls/syscall_64.tbl | 6 ++++++ include/linux/syscalls.h | 14 ++++++++++++++ include/uapi/asm-generic/unistd.h | 15 ++++++++++++++- kernel/sys_ni.c | 6 ++++++ scripts/syscall.tbl | 6 ++++++ 6 files changed, 52 insertions(+), 1 deletion(-) diff --git a/arch/x86/entry/syscalls/syscall_32.tbl b/arch/x86/entry/syscalls/syscall_32.tbl index 2c172ef48dfd..4ed6bf999b53 100644 --- a/arch/x86/entry/syscalls/syscall_32.tbl +++ b/arch/x86/entry/syscalls/syscall_32.tbl @@ -478,3 +478,9 @@ 470 i386 listns sys_listns 471 i386 rseq_slice_yield sys_rseq_slice_yield 472 i386 fchroot sys_fchroot +473 i386 cell_create sys_cell_create +474 i386 cell_configure sys_cell_configure +475 i386 cell_info sys_cell_info +476 i386 cell_open sys_cell_open +477 i386 cell_kill sys_cell_kill +478 i386 pidfd_configure sys_pidfd_configure diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl index d5b6045b0090..bdf0c48b5682 100644 --- a/arch/x86/entry/syscalls/syscall_64.tbl +++ b/arch/x86/entry/syscalls/syscall_64.tbl @@ -397,6 +397,12 @@ 470 common listns sys_listns 471 common rseq_slice_yield sys_rseq_slice_yield 472 common fchroot sys_fchroot +473 common cell_create sys_cell_create +474 common cell_configure sys_cell_configure +475 common cell_info sys_cell_info +476 common cell_open sys_cell_open +477 common cell_kill sys_cell_kill +478 common pidfd_configure sys_pidfd_configure # # Due to a historical design error, certain syscalls are numbered differently diff --git a/include/linux/syscalls.h b/include/linux/syscalls.h index 552d9710a0d5..f70bfe5b028b 100644 --- a/include/linux/syscalls.h +++ b/include/linux/syscalls.h @@ -77,6 +77,7 @@ struct cachestat; struct statmount; struct mnt_id_req; struct ns_id_req; +struct cell_info; struct xattr_args; struct file_attr; @@ -442,6 +443,19 @@ asmlinkage long sys_statmount(const struct mnt_id_req __user *req, asmlinkage long sys_listmount(const struct mnt_id_req __user *req, u64 __user *mnt_ids, size_t nr_mnt_ids, unsigned int flags); +asmlinkage long sys_cell_create(int parent, unsigned int flags); +asmlinkage long sys_cell_configure(int cfd, unsigned int cmd, + const char __user *key, + const void __user *value, int aux); +asmlinkage long sys_cell_info(int cfd, struct cell_info __user *info, + size_t size, unsigned int flags); +asmlinkage long sys_cell_open(int cfd, unsigned int what, + const char __user *key, const void __user *arg, + unsigned int flags); +asmlinkage long sys_cell_kill(int cfd, unsigned int flags); +asmlinkage long sys_pidfd_configure(int pidfd, unsigned int cmd, + const char __user *key, + const void __user *value, int aux); asmlinkage long sys_listns(const struct ns_id_req __user *req, u64 __user *ns_ids, size_t nr_ns_ids, unsigned int flags); diff --git a/include/uapi/asm-generic/unistd.h b/include/uapi/asm-generic/unistd.h index 5b7e77a7c736..aa7bcaa8348f 100644 --- a/include/uapi/asm-generic/unistd.h +++ b/include/uapi/asm-generic/unistd.h @@ -867,8 +867,21 @@ __SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield) #define __NR_fchroot 472 __SYSCALL(__NR_fchroot, sys_fchroot) +#define __NR_cell_create 473 +__SYSCALL(__NR_cell_create, sys_cell_create) +#define __NR_cell_configure 474 +__SYSCALL(__NR_cell_configure, sys_cell_configure) +#define __NR_cell_info 475 +__SYSCALL(__NR_cell_info, sys_cell_info) +#define __NR_cell_open 476 +__SYSCALL(__NR_cell_open, sys_cell_open) +#define __NR_cell_kill 477 +__SYSCALL(__NR_cell_kill, sys_cell_kill) +#define __NR_pidfd_configure 478 +__SYSCALL(__NR_pidfd_configure, sys_pidfd_configure) + #undef __NR_syscalls -#define __NR_syscalls 473 +#define __NR_syscalls 479 /* * 32 bit systems traditionally used different diff --git a/kernel/sys_ni.c b/kernel/sys_ni.c index c8be0abaa407..e0ff9a6909fb 100644 --- a/kernel/sys_ni.c +++ b/kernel/sys_ni.c @@ -167,6 +167,12 @@ COND_SYSCALL_COMPAT(keyctl); COND_SYSCALL(landlock_create_ruleset); COND_SYSCALL(landlock_add_rule); COND_SYSCALL(landlock_restrict_self); +COND_SYSCALL(cell_create); +COND_SYSCALL(cell_configure); +COND_SYSCALL(cell_info); +COND_SYSCALL(cell_open); +COND_SYSCALL(cell_kill); +COND_SYSCALL(pidfd_configure); COND_SYSCALL(fadvise64_64); COND_SYSCALL_COMPAT(fadvise64_64); COND_SYSCALL(lsm_get_self_attr); diff --git a/scripts/syscall.tbl b/scripts/syscall.tbl index 0ab531605120..cff7843e2677 100644 --- a/scripts/syscall.tbl +++ b/scripts/syscall.tbl @@ -413,3 +413,9 @@ 470 common listns sys_listns 471 common rseq_slice_yield sys_rseq_slice_yield 472 common fchroot sys_fchroot +473 common cell_create sys_cell_create +474 common cell_configure sys_cell_configure +475 common cell_info sys_cell_info +476 common cell_open sys_cell_open +477 common cell_kill sys_cell_kill +478 common pidfd_configure sys_pidfd_configure -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 89/90] MAINTAINERS: add an entry for cells 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (87 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 88/90] arch: wire up the cell system calls Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 90/90] selftests: add tests " Josef Bacik 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik Add an entry for the new files. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- MAINTAINERS | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 0bc2c39eb417..faa9f1cc720c 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -6173,6 +6173,20 @@ F: arch/powerpc/include/asm/spu*.h F: arch/powerpc/include/uapi/asm/spu*.h F: arch/powerpc/platforms/cell/ +CELLS +M: Josef Bacik <josef@toxicpanda.com> +L: linux-fsdevel@vger.kernel.org +L: linux-kernel@vger.kernel.org +S: Maintained +F: Documentation/userspace-api/cells*.rst +F: fs/cellfs.c +F: include/linux/cell.h +F: include/linux/cell_ids.h +F: include/uapi/linux/cell.h +F: kernel/cell/ +F: kernel/embryo.c +F: tools/testing/selftests/cell/ + CELLWISE CW2015 BATTERY DRIVER M: Tobias Schrammm <t.schramm@manjaro.org> S: Maintained -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
* [PATCH RFC v3 90/90] selftests: add tests for cells 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik ` (88 preceding siblings ...) 2026-11-05 15:03 ` [PATCH RFC v3 89/90] MAINTAINERS: add an entry for cells Josef Bacik @ 2026-11-05 15:03 ` Josef Bacik 89 siblings, 0 replies; 93+ messages in thread From: Josef Bacik @ 2026-11-05 15:03 UTC (permalink / raw) To: Christian Brauner, James Bottomley, Andrew Halaney, Jeremy Bingham, Tahera Fahimi, Serge Hallyn Cc: linux-fsdevel, linux-kernel, Josef Bacik The tests build cells, run programs in them, kill them, and try the things that are not supposed to work. There are seven test programs, one per topic: the life of a cell, spawning, the slots, ids, the boundary, the network and cgroups. Each of them acts as a builder, and cell_test.h holds the code that they share. cell_child is a statically linked program that is copied into the otherwise empty tree of each cell. It reports what it finds from the inside. The tests cover: - the states of a cell, and that it is not reported dead early - empty pidfds, and what a new task does and does not inherit - capabilities: a task has what the builder named and never gains more - setns(), unshare() and related calls, refused from both sides - set-id bits and file capabilities - unix sockets between a cell and the host - the id window, ids that must not reach a filesystem of the host, mount options, overlayfs, and that the builder is told the block - device nodes and supplementary groups - the permission checks on the slots, and every allocation in the seal failing in turn - what can and cannot get into a budget, what a cell and its builder can do below it, and what a cell cannot do to the budget itself - a network that is set up from the outside, no network at all, the rights of a cell over its network, and that the network goes away with the cell - what seccomp does and does not cover - that the host runs no program and takes no core dump for a cell - an unprivileged builder, and a pod made of several cells - a kill, a close and a start racing with spawns in flight The tests need root, cgroup2 with the memory and pids controllers, and tmpfs. Some need more and are skipped otherwise. CELL_TEST_EXT4_IMG names an ext4 image that can be frozen. CELL_TEST_9P_TAG names a writable 9p share, for ->atomic_open(). /dev/vhost-net is used if it exists. Signed-off-by: Josef Bacik <josef@toxicpanda.com> --- tools/testing/selftests/Makefile | 1 + tools/testing/selftests/cell/.gitignore | 2 + tools/testing/selftests/cell/Makefile | 14 + tools/testing/selftests/cell/cell_boundary_test.c | 535 +++++++ tools/testing/selftests/cell/cell_cgroup_test.c | 546 +++++++ tools/testing/selftests/cell/cell_child.c | 1626 +++++++++++++++++++++ tools/testing/selftests/cell/cell_ids_test.c | 762 ++++++++++ tools/testing/selftests/cell/cell_life_test.c | 446 ++++++ tools/testing/selftests/cell/cell_net_test.c | 915 ++++++++++++ tools/testing/selftests/cell/cell_slots_test.c | 748 ++++++++++ tools/testing/selftests/cell/cell_spawn_test.c | 559 +++++++ tools/testing/selftests/cell/cell_test.h | 586 ++++++++ tools/testing/selftests/cell/cell_util.h | 108 ++ tools/testing/selftests/cell/config | 35 + 14 files changed, 6883 insertions(+) diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile index 07c3719f8b04..45b78a73fa2e 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -8,6 +8,7 @@ TARGETS += bpf TARGETS += breakpoints TARGETS += cachestat TARGETS += capabilities +TARGETS += cell TARGETS += cgroup TARGETS += clone3 TARGETS += connector diff --git a/tools/testing/selftests/cell/.gitignore b/tools/testing/selftests/cell/.gitignore new file mode 100644 index 000000000000..585ed8e41c1c --- /dev/null +++ b/tools/testing/selftests/cell/.gitignore @@ -0,0 +1,2 @@ +cell_*_test +cell_child diff --git a/tools/testing/selftests/cell/Makefile b/tools/testing/selftests/cell/Makefile new file mode 100644 index 000000000000..ccf1fa3c6565 --- /dev/null +++ b/tools/testing/selftests/cell/Makefile @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0 +CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) + +TEST_GEN_PROGS := cell_life_test cell_spawn_test cell_slots_test cell_ids_test +TEST_GEN_PROGS += cell_boundary_test cell_net_test cell_cgroup_test +TEST_GEN_FILES := cell_child + +include ../lib.mk + +# Runs inside a cell, whose tree holds nothing else. +$(OUTPUT)/cell_child: cell_child.c cell_util.h + $(CC) $(CFLAGS) -static -o $@ $< + +$(TEST_GEN_PROGS): cell_test.h cell_util.h diff --git a/tools/testing/selftests/cell/cell_boundary_test.c b/tools/testing/selftests/cell/cell_boundary_test.c new file mode 100644 index 000000000000..483a5e642ebf --- /dev/null +++ b/tools/testing/selftests/cell/cell_boundary_test.c @@ -0,0 +1,535 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Tests for cells, the boundary: nobody enters, a uid means nothing across it, + * and sockets carry bytes. + */ +#include "cell_test.h" + +TEST_F(cell, membership) +{ + RUN_IN_CELL("membership"); +} + +TEST_F(cell, nobody_enters) +{ + static const char * const ns[] = { "user", "mnt", "pid", "ipc", "uts", "cgroup" }; + char path[64]; + unsigned int i; + pid_t pid; + int pidfd, fd; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + pidfd = spawn(self->c.cfd, &pid, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + + for (i = 0; i < ARRAY_SIZE(ns); i++) { + snprintf(path, sizeof(path), "/proc/%d/ns/%s", pid, ns[i]); + fd = open(path, O_RDONLY); + ASSERT_GE(fd, 0); + EXPECT_EQ(setns(fd, 0), -1) TH_LOG("entered %s", ns[i]); + EXPECT_EQ(errno, EPERM) TH_LOG("%s", ns[i]); + close(fd); + } + EXPECT_EQ(setns(pidfd, CLONE_NEWIPC), -1); + EXPECT_EQ(errno, EPERM); + close(pidfd); +} + +/* Not while it is built either. Whoever got in could become the init of the cell. */ +TEST_F(cell, nobody_enters_while_it_is_built) +{ + int fd; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0); + fd = cell_open(self->c.cfd, CELL_OPEN_SLOT, "pids", NULL, 0); + ASSERT_GE(fd, 0); + EXPECT_EQ(setns(fd, CLONE_NEWPID), -1); + EXPECT_EQ(errno, EPERM); + close(fd); +} + +static int drop_caps(void) +{ + struct __user_cap_header_struct hdr = { .version = _LINUX_CAPABILITY_VERSION_3 }; + struct __user_cap_data_struct data[2] = {}; + + return syscall(SYS_capset, &hdr, data); +} + +/* A one-id cell runs on the kuid of whoever made it. That must not count. */ +/* Have the kernel send @pid a SIGUSR1 on behalf of the caller. */ +static int sigio_to(pid_t pid) +{ + int sp[2], ret = -1; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) + return -1; + if (!fcntl(sp[0], F_SETOWN, pid) && !fcntl(sp[0], F_SETSIG, SIGUSR1) && + !fcntl(sp[0], F_SETFL, O_ASYNC) && write(sp[1], "x", 1) == 1) + ret = 0; + close(sp[0]); + close(sp[1]); + return ret; +} + +#define V1ROOT "/tmp/cell-test-v1" + +TEST_F(cell, equal_uid_means_nothing) +{ + int pidfd, status; + pid_t pid, plain, prober; + bool v1; + + /* A cgroup v1 hierarchy with a group that uid 1000 may write to. */ + mkdir(V1ROOT, 0755); + v1 = !mount("none", V1ROOT, "cgroup", 0, "none,name=celltest"); + if (v1) { + ASSERT_EQ(mkdir(V1ROOT "/sub", 0755), 0); + ASSERT_EQ(chown(V1ROOT "/sub/cgroup.procs", 1000, 1000), 0); + } else { + TH_LOG("no cgroup v1, that part is skipped"); + } + + /* Made by uid 1000, with the privilege the mount work takes. */ + ASSERT_EQ(prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0), 0); + plain = fork(); + ASSERT_GE(plain, 0); + if (!plain) { + if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000) || + drop_caps() || prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)) + _exit(1); + pause(); + _exit(0); + } + + ASSERT_EQ(setresgid(-1, 1000, -1), 0); + ASSERT_EQ(setresuid(-1, 1000, -1), 0); + self->c.cfd = cell_create(CELL_SELF, 0); + ASSERT_EQ(setresuid(-1, 0, -1), 0); + ASSERT_EQ(setresgid(-1, 0, -1), 0); + ASSERT_GE(self->c.cfd, 0); + close(self->c.cfd); + self->c.cfd = -1; + + /* cell_build() makes its own: do all of it as euid 1000. */ + prober = fork(); + ASSERT_GE(prober, 0); + if (!prober) { + struct __user_cap_header_struct hdr = { .version = _LINUX_CAPABILITY_VERSION_3 }; + struct __user_cap_data_struct data[2]; + struct pollfd pfd = { .events = POLLIN }; + struct sched_param sp = { 0 }; + struct rlimit rl; + struct cellctx c; + cpu_set_t cpus; + pid_t victim; + char buf[32]; + int step; + + if (chown(CGROOT, 1000, 1000) || chown(CGROOT "/cgroup.procs", 1000, 1000)) + _exit(10); + if (setgroups(2, (gid_t[]){ 2000, 2001 })) + _exit(11); + if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000)) + _exit(11); + if (syscall(SYS_capget, &hdr, data)) + _exit(12); + data[0].effective = data[0].permitted; + data[1].effective = data[1].permitted; + if (syscall(SYS_capset, &hdr, data)) + _exit(13); + + step = cell_build(&c, &(struct build){ .ids = 1 }); + if (step) + _exit(20 + step); + pidfd = spawn(c.cfd, &pid, "oneid", "1000", NULL); + if (pidfd < 0) + _exit(14); + if (cell_wait(c.cfd)) + _exit(15); + close(pidfd); + cell_cleanup(&c); + + /* It would lose a capability with its first exec, so it gets none. */ + step = cell_build(&c, &(struct build){ .ids = 1, .caps = CAP(CHOWN), + .no_seal = true }); + if (step) + _exit(80 + step); + if (!cell_configure(c.cfd, CELL_CMD_SEAL, NULL, NULL, 0) || errno != EINVAL) + _exit(16); + cell_cleanup(&c); + + step = cell_build(&c, &(struct build){ .ids = 1 }); + if (step) + _exit(50 + step); + pidfd = spawn(c.cfd, &pid, "usr1", NULL, NULL); + if (pidfd < 0) + _exit(16); + if (host_uid(pid) != 1000) + _exit(17); + + /* Now be nobody in particular, with the very same uid. */ + if (drop_caps()) + _exit(18); + if (kill(plain, 0)) + _exit(19); /* an equal uid at home counts */ + if (kill(pid, 0) != -1 || errno != EPERM) + _exit(2); + if (kill(pid, SIGKILL) != -1 || errno != EPERM) + _exit(3); + if (setpriority(PRIO_PROCESS, pid, 5) != -1 || errno != EPERM) + _exit(4); + /* IOPRIO_WHO_PROCESS, IOPRIO_CLASS_IDLE */ + if (syscall(SYS_ioprio_set, 1, plain, 3 << 13)) + _exit(7); + if (syscall(SYS_ioprio_set, 1, pid, 3 << 13) != -1 || errno != EPERM) + _exit(8); + CPU_ZERO(&cpus); + CPU_SET(0, &cpus); + if (sched_setaffinity(plain, sizeof(cpus), &cpus)) + _exit(100); + if (sched_setaffinity(pid, sizeof(cpus), &cpus) != -1 || errno != EPERM) + _exit(101); + if (sched_setscheduler(plain, SCHED_BATCH, &sp)) + _exit(102); + if (sched_setscheduler(pid, SCHED_BATCH, &sp) != -1 || errno != EPERM) + _exit(103); + if (prlimit(plain, RLIMIT_NOFILE, NULL, &rl)) + _exit(104); + if (prlimit(pid, RLIMIT_NOFILE, NULL, &rl) != -1 || errno != EPERM) + _exit(105); + if (v1) { + snprintf(buf, sizeof(buf), "%d", plain); + if (write_file(V1ROOT "/sub/cgroup.procs", buf)) + _exit(106); + snprintf(buf, sizeof(buf), "%d", pid); + if (write_file(V1ROOT "/sub/cgroup.procs", buf) != -1 || + errno != EACCES) + _exit(107); + } + /* SIGIO: fatal at home, never sent across. It would end the cell. */ + victim = fork(); + if (victim < 0) + _exit(108); + if (!victim) { + pause(); + _exit(0); + } + if (sigio_to(victim) || waitpid(victim, &step, 0) != victim || + !WIFSIGNALED(step) || WTERMSIG(step) != SIGUSR1) + _exit(109); + if (sigio_to(pid)) + _exit(110); + pfd.fd = pidfd; + if (poll(&pfd, 1, 200) != 0) + _exit(111); + /* + * Last, since a traced task takes its signals differently. The + * capability LSM refuses this one too, for the user namespace. + */ + if (ptrace(PTRACE_SEIZE, plain, 0, 0)) + _exit(112); + if (ptrace(PTRACE_SEIZE, pid, 0, 0) != -1 || errno != EPERM) + _exit(113); + /* The handle is what counts. */ + if (cell_kill(c.cfd, 0)) + _exit(5); + if (cell_wait(c.cfd) != SIGKILL) + _exit(6); + _exit(0); + } + ASSERT_EQ(waitpid(prober, &status, 0), prober); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); + + kill(plain, SIGKILL); + waitpid(plain, NULL, 0); + if (v1) { + EXPECT_EQ(rmdir(V1ROOT "/sub"), 0); + EXPECT_EQ(umount(V1ROOT), 0); + } + rmdir(V1ROOT); + EXPECT_EQ(chown(CGROOT, 0, 0), 0); + EXPECT_EQ(chown(CGROOT "/cgroup.procs", 0, 0), 0); +} + +/* A hard boundary: the uid of root on the host counts for no more than any other. */ +TEST_F(cell, uid_0_means_nothing_either) +{ + struct pollfd pfd = { .events = POLLIN }; + char buf[32]; + int ready[2]; + pid_t pid; + bool v1; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + ASSERT_EQ(pipe(ready), 0); + pfd.fd = prepare(self->c.cfd, "usr1", NULL, NULL); + ASSERT_GE(pfd.fd, 0); + ASSERT_EQ(pidfd_configure(pfd.fd, PIDFD_SET_FD, "3", NULL, ready[1]), 0); + ASSERT_EQ(pidfd_configure(pfd.fd, PIDFD_CMD_START, NULL, NULL, 0), 0); + pid = pidfd_nr(pfd.fd); + ASSERT_GT(pid, 0); + close(ready[1]); + /* Until it has its handler an init does not get the signal anyway. */ + ASSERT_EQ(read(ready[0], buf, 1), 1); + close(ready[0]); + + /* It would end the cell. */ + ASSERT_EQ(sigio_to(pid), 0); + EXPECT_EQ(poll(&pfd, 1, 200), 0); + + mkdir(V1ROOT, 0755); + v1 = !mount("none", V1ROOT, "cgroup", 0, "none,name=celltest"); + if (v1) { + ASSERT_EQ(mkdir(V1ROOT "/sub", 0755), 0); + snprintf(buf, sizeof(buf), "%d", pid); + EXPECT_EQ(write_file(V1ROOT "/sub/cgroup.procs", buf), -1); + EXPECT_EQ(errno, EACCES); + EXPECT_EQ(rmdir(V1ROOT "/sub"), 0); + EXPECT_EQ(umount(V1ROOT), 0); + } else { + TH_LOG("no cgroup v1, that part is skipped"); + } + rmdir(V1ROOT); + close(pfd.fd); +} + +TEST_F(cell, sockets_carry_bytes) +{ + struct sockaddr_un addr = { .sun_family = AF_UNIX }; + char ctl[CMSG_SPACE(sizeof(int))] = {}, c = 'h'; + struct iovec iov = { .iov_base = &c, .iov_len = 1 }; + struct msghdr msg = { + .msg_iov = &iov, .msg_iovlen = 1, + .msg_control = ctl, .msg_controllen = sizeof(ctl), + }; + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + int pidfd, sv[2], lsock, conn, zero = 0, cwd; + struct ucred uc; + socklen_t len; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0); + + /* A listener of the host's that the cell can reach by name. */ + lsock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + cwd = open(".", O_RDONLY | O_DIRECTORY); + ASSERT_EQ(fchdir(self->c.raw), 0); + strcpy(addr.sun_path, "tmp/sock"); + ASSERT_EQ(bind(lsock, (struct sockaddr *)&addr, sizeof(addr)), 0); + ASSERT_EQ(chmod("tmp/sock", 0777), 0); + ASSERT_EQ(fchdir(cwd), 0); + ASSERT_EQ(listen(lsock, 1), 0); + ASSERT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), 0); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv), 0); + pidfd = prepare(self->c.cfd, "unix", NULL, NULL); + ASSERT_GE(pidfd, 0); + + /* A file in flight from before the socket is given away. */ + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + memcpy(CMSG_DATA(cmsg), &zero, sizeof(int)); + c = 'q'; + ASSERT_EQ(sendmsg(sv[0], &msg, 0), 1); + c = 'h'; + + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, sv[1]), 0); + close(sv[1]); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0); + + /* From the moment it was given away. */ + EXPECT_EQ(sendmsg(sv[0], &msg, 0), -1); + EXPECT_EQ(errno, EPERM); + + ASSERT_EQ(read(sv[0], &c, 1), 1); + EXPECT_EQ(c, 'a'); + ASSERT_EQ(read(sv[0], &c, 1), 1); + EXPECT_EQ(c, 'b'); + ASSERT_EQ(write(sv[0], "h", 1), 1); + + ASSERT_EQ(wait_for(lsock, pidfd), 0); + conn = accept(lsock, NULL, NULL); + ASSERT_GE(conn, 0); + len = sizeof(uc); + EXPECT_EQ(getsockopt(conn, SOL_SOCKET, SO_PEERCRED, &uc, &len), -1); + EXPECT_EQ(errno, ENODATA); + c = 'h'; + EXPECT_EQ(sendmsg(conn, &msg, 0), -1); + EXPECT_EQ(errno, EPERM); + ASSERT_EQ(read(conn, &c, 1), 1); + EXPECT_EQ(c, 'c'); + + EXPECT_EQ(cell_wait(self->c.cfd), 0); + close(conn); + close(lsock); + close(sv[0]); + close(pidfd); + close(cwd); +} + +/* See check_peer_race(). */ +TEST_F(cell, peer_is_checked_and_read_at_once) +{ + struct sockaddr_un addr = { .sun_family = AF_UNIX }; + int lsock, cwd, pidfd, status; + pid_t pid; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0); + lsock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + ASSERT_GE(lsock, 0); + cwd = open(".", O_RDONLY | O_DIRECTORY); + ASSERT_EQ(fchdir(self->c.raw), 0); + strcpy(addr.sun_path, "tmp/sock"); + ASSERT_EQ(bind(lsock, (struct sockaddr *)&addr, sizeof(addr)), 0); + ASSERT_EQ(chmod("tmp/sock", 0777), 0); + ASSERT_EQ(fchdir(cwd), 0); + close(cwd); + ASSERT_EQ(listen(lsock, 128), 0); + ASSERT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), 0); + + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + for (;;) { + int conn = accept(lsock, NULL, NULL); + + if (conn >= 0) + close(conn); + } + } + pidfd = spawn(self->c.cfd, NULL, "peer-race", NULL, NULL); + ASSERT_GE(pidfd, 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + close(pidfd); + kill(pid, SIGKILL); + waitpid(pid, &status, 0); + close(lsock); +} + +#define HELPER_RAN "/tmp/cell_test_helper_ran" +#define CORE_SOCK "/tmp/cell_test_core.sock" + +static int read_line(const char *path, char *buf, size_t len) +{ + int fd = open(path, O_RDONLY | O_CLOEXEC); + ssize_t n = fd < 0 ? -1 : read(fd, buf, len - 1); + + close(fd); + if (n < 0) + return -1; + buf[n] = 0; + buf[strcspn(buf, "\n")] = 0; + return 0; +} + +/* Has the kernel run the helper with @arg among its arguments? It may take a moment. */ +static char helper_log[4096]; + +static bool helper_ran(const char *arg) +{ + char *buf = helper_log; + int i, fd; + + for (i = 0; i < 20; i++) { + fd = open(HELPER_RAN, O_RDONLY | O_CLOEXEC); + memset(buf, 0, sizeof(helper_log)); + if (fd >= 0 && read(fd, buf, sizeof(helper_log) - 1) > 0 && strstr(buf, arg)) { + close(fd); + return true; + } + close(fd); + usleep(50000); + } + return false; +} + +/* + * There are things a task can do that make the kernel run a program: as root, on the + * host, in no namespace but the first. Not for a task of a cell. + */ +TEST_F(cell, host_runs_no_program_for_a_cell) +{ + char self_path[256] = {}, child[300], pattern[340], modprobe[256], core[256]; + struct sockaddr_un addr = { .sun_family = AF_UNIX, .sun_path = CORE_SOCK }; + bool request_key = false; + int pidfd, status, lsock; + struct pollfd pfd; + pid_t pid; + + if (read_line("/proc/sys/kernel/modprobe", modprobe, sizeof(modprobe)) || + read_line("/proc/sys/kernel/core_pattern", core, sizeof(core))) + SKIP(return, "no modules or no core dumps"); + ASSERT_GT(readlink("/proc/self/exe", self_path, sizeof(self_path) - 1), 0); + *strrchr(self_path, '/') = 0; + snprintf(child, sizeof(child), "%s/cell_child", self_path); + snprintf(pattern, sizeof(pattern), "|%s core %%p", child); + ASSERT_EQ(write_file("/proc/sys/kernel/modprobe", child), 0); + ASSERT_EQ(write_file("/proc/sys/kernel/core_pattern", pattern), 0); + /* The path of this one is fixed. Leave a real one alone. */ + mkdir("/sbin", 0755); + request_key = !symlink(child, "/sbin/request-key"); + + /* They do tell: this is what it looks like for a task of the host. */ + unlink(HELPER_RAN); + EXPECT_EQ(socket(AF_ECONET, SOCK_DGRAM, 0), -1); + EXPECT_TRUE(helper_ran("net-pf-19")); + if (request_key) { + syscall(SYS_request_key, "user", "cell:test", "make it", KEY_SPEC_PROCESS_KEYRING); + EXPECT_TRUE(helper_ran("request-key")); + } + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) + raise(SIGSEGV); + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_TRUE(helper_ran(" core ")); + + unlink(HELPER_RAN); + EXPECT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net_str = "new" }), 0); + pidfd = spawn(self->c.cfd, NULL, "helpers", NULL, NULL); + EXPECT_GE(pidfd, 0); + /* Killed by the signal, and nothing was dumped. */ + EXPECT_EQ(cell_wait(self->c.cfd), SIGILL); + close(pidfd); + usleep(300000); + EXPECT_EQ(access(HELPER_RAN, F_OK), -1) { + helper_ran("\n\n"); + TH_LOG("it ran: %s", helper_log); + } + cell_cleanup(&self->c); + + /* A core dump can go to a socket as well, where a daemon of the host listens. */ + unlink(CORE_SOCK); + lsock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + ASSERT_GE(lsock, 0); + ASSERT_EQ(bind(lsock, (struct sockaddr *)&addr, sizeof(addr)), 0); + ASSERT_EQ(listen(lsock, 4), 0); + pfd = (struct pollfd){ .fd = lsock, .events = POLLIN }; + if (!write_file("/proc/sys/kernel/core_pattern", "@" CORE_SOCK)) { + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) + raise(SIGSEGV); + EXPECT_EQ(poll(&pfd, 1, 5000), 1); + close(accept(lsock, NULL, NULL)); + ASSERT_EQ(waitpid(pid, &status, 0), pid); + + EXPECT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + pidfd = spawn(self->c.cfd, NULL, "helpers", NULL, NULL); + EXPECT_GE(pidfd, 0); + EXPECT_EQ(cell_wait(self->c.cfd), SIGILL); + close(pidfd); + EXPECT_EQ(poll(&pfd, 1, 300), 0); + } + close(lsock); + unlink(CORE_SOCK); + + unlink(HELPER_RAN); + if (request_key) + unlink("/sbin/request-key"); + EXPECT_EQ(write_file("/proc/sys/kernel/modprobe", modprobe), 0); + EXPECT_EQ(write_file("/proc/sys/kernel/core_pattern", core), 0); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/cell/cell_cgroup_test.c b/tools/testing/selftests/cell/cell_cgroup_test.c new file mode 100644 index 000000000000..6bd4f21a3fec --- /dev/null +++ b/tools/testing/selftests/cell/cell_cgroup_test.c @@ -0,0 +1,546 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Cells: cgroups below the budget. Whoever built the cell lays them out, and so + * does the cell itself, as an init that manages cgroups will. The budget is what + * bounds the cell, so its own limits are not the cell's. + */ +#include "cell_test.h" + +static int at(const struct cellctx *c, const char *name, char *path, size_t len) +{ + return snprintf(path, len, "%s/%s", c->cg, name); +} + +static int cg_mkdir(const struct cellctx *c, const char *name) +{ + char path[256]; + + at(c, name, path, sizeof(path)); + return mkdir(path, 0755); +} + +static int cg_write(const struct cellctx *c, const char *name, const char *val) +{ + char path[256]; + + at(c, name, path, sizeof(path)); + return write_file(path, val); +} + +static int cg_move(const struct cellctx *c, const char *name, pid_t pid) +{ + char path[256], nr[16]; + + snprintf(path, sizeof(path), "%s/%s/cgroup.procs", c->cg, name); + snprintf(nr, sizeof(nr), "%d", pid); + return write_file(path, nr); +} + +/* Where a task is, below the budget of @c: "" for the budget itself. */ +static const char *cg_of(const struct cellctx *c, pid_t pid) +{ + static char buf[256]; + const char *budget = c->cg + strlen(CGROOT); + char path[64]; + FILE *f; + + snprintf(path, sizeof(path), "/proc/%d/cgroup", pid); + f = fopen(path, "r"); + if (!f) + return "?"; + /* There may be legacy hierarchies about. */ + while (fgets(buf, sizeof(buf), f)) { + if (!strncmp(buf, "0::", 3)) + break; + } + fclose(f); + buf[strcspn(buf, "\n")] = 0; + if (strncmp(buf, "0::", 3) || strncmp(buf + 3, budget, strlen(budget))) + return buf; + return buf + 3 + strlen(budget); +} + +/* How many cgroups there are below the budget. */ +static int cg_children(const struct cellctx *c) +{ + struct dirent *e; + int n = 0; + DIR *d; + + d = opendir(c->cg); + if (!d) + return -1; + while ((e = readdir(d))) + n += e->d_type == DT_DIR && e->d_name[0] != '.'; + closedir(d); + return n; +} + +/* Has all of a cgroup come to a halt? */ +static int cg_frozen(const struct cellctx *c, const char *name) +{ + char path[256], buf[128] = {}; + int fd, i; + + snprintf(path, sizeof(path), "%s/%s/cgroup.events", c->cg, name); + for (i = 0; i < 100; i++) { + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0 || read(fd, buf, sizeof(buf) - 1) <= 0) + return -1; + close(fd); + if (strstr(buf, "populated 1") && strstr(buf, "frozen 1")) + return 1; + usleep(20000); + } + return 0; +} + +/* Controllers for the budgets to pass on. */ +static int controllers(void) +{ + return write_file(CGROOT "/cgroup.subtree_control", "+memory +pids"); +} + +struct inside { + int pidfd; + int ready; /* the task writes to this */ + int go; /* and then waits for this */ +}; + +/* Run @what with the budget as fd 3. @fd4, or else a pipe each way as fd 4 and 5. */ +static int start_inside(struct cellctx *c, struct inside *in, const char *what, + const char *arg, int fd4) +{ + int up[2] = { -1, -1 }, down[2] = { -1, -1 }; + + in->ready = in->go = -1; + in->pidfd = prepare(c->cfd, what, arg, NULL); + if (in->pidfd < 0 || pidfd_configure(in->pidfd, PIDFD_SET_FD, "3", NULL, c->cgfd)) + return -1; + if (fd4 >= 0) { + if (pidfd_configure(in->pidfd, PIDFD_SET_FD, "4", NULL, fd4)) + return -1; + } else { + if (pipe2(up, O_CLOEXEC) || pipe2(down, O_CLOEXEC) || + pidfd_configure(in->pidfd, PIDFD_SET_FD, "4", NULL, up[1]) || + pidfd_configure(in->pidfd, PIDFD_SET_FD, "5", NULL, down[0])) + return -1; + close(up[1]); + close(down[0]); + in->ready = up[0]; + in->go = down[1]; + } + return pidfd_configure(in->pidfd, PIDFD_CMD_START, NULL, NULL, 0); +} + +static int wait_ready(struct inside *in) +{ + char c; + + return read(in->ready, &c, 1) == 1 ? 0 : -1; +} + +static void done_inside(struct inside *in) +{ + close(in->pidfd); + if (in->ready >= 0) + close(in->ready); + if (in->go >= 0) + close(in->go); +} + +/* Run @fn as uid 1000, in a user namespace of its own, as who builds a one-id cell. */ +static int unprivileged(int (*fn)(void)) +{ + int status, ret = -1; + pid_t pid; + + if (chown(CGROOT, 1000, 1000) || chown(CGROOT "/cgroup.procs", 1000, 1000)) + return -1; + pid = fork(); + if (!pid) { + if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) || + setresuid(1000, 1000, 1000) || prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)) + _exit(101); + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) + _exit(102); + if (write_file("/proc/self/setgroups", "deny") || + write_file("/proc/self/uid_map", "1000 1000 1") || + write_file("/proc/self/gid_map", "1000 1000 1")) + _exit(103); + _exit(fn()); + } + if (pid > 0 && waitpid(pid, &status, 0) == pid) + ret = WIFEXITED(status) ? WEXITSTATUS(status) : -1; + if (chown(CGROOT, 0, 0) || chown(CGROOT "/cgroup.procs", 0, 0)) + return -1; + return ret; +} + +/* + * The budget has no children when the slot is filled. From then on whoever built + * the cell makes what it likes in there and moves the tasks of the cell about. + */ +TEST_F(cell, builder_lays_out_the_budget) +{ + pid_t pid; + int p; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0); + EXPECT_EQ(cg_mkdir(&self->c, "a"), 0); + ASSERT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), 0); + EXPECT_EQ(cg_mkdir(&self->c, "b"), 0); + + p = spawn(self->c.cfd, &pid, "pause", NULL, NULL); + ASSERT_GE(p, 0); + EXPECT_STREQ(cg_of(&self->c, pid), ""); + EXPECT_EQ(cg_move(&self->c, "a", pid), 0); + EXPECT_STREQ(cg_of(&self->c, pid), "/a"); + EXPECT_EQ(cg_move(&self->c, "b", pid), 0); + EXPECT_STREQ(cg_of(&self->c, pid), "/b"); + + /* Within the budget, and only what is the cell's. */ + EXPECT_EQ(cg_move(&self->c, "..", pid), -1); + EXPECT_EQ(errno, EPERM); + EXPECT_EQ(cg_move(&self->c, "a", getpid()), -1); + EXPECT_EQ(errno, EPERM); + EXPECT_STREQ(cg_of(&self->c, pid), "/b"); + + /* All of it goes with the cell, whoever made it. */ + EXPECT_EQ(cell_kill(self->c.cfd, 0), 0); + cell_wait(self->c.cfd); + EXPECT_EQ(cg_children(&self->c), 0); + close(p); +} + +/* Have a task of the cell say where it is, started in the cgroup @name. */ +static int run_in_cgroup(struct cellctx *c, const char *name, const char *seen) +{ + int cgfd = openat(c->cgfd, name, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + int pidfd = prepare(c->cfd, "in-cgroup", seen, NULL), st = -99; + + if (cgfd >= 0 && pidfd >= 0 && + !pidfd_configure(pidfd, PIDFD_SET_FD, "cgroup", NULL, cgfd) && + !pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0)) + st = task_wait(pidfd); + close(pidfd); + close(cgfd); + return st; +} + +/* One cgroup with a limit for each of what runs in the cell. */ +TEST_F(cell, builder_sets_limits_below_the_budget) +{ + pid_t pid; + int p; + + ASSERT_EQ(controllers(), 0); + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + ASSERT_EQ(cg_mkdir(&self->c, "a"), 0); + p = spawn(self->c.cfd, &pid, "pause", NULL, NULL); + ASSERT_GE(p, 0); + + /* A cgroup that passes a controller on has no tasks of its own. */ + EXPECT_EQ(cg_write(&self->c, "cgroup.subtree_control", "+memory"), -1); + EXPECT_EQ(errno, EBUSY); + EXPECT_EQ(cg_move(&self->c, "a", pid), 0); + EXPECT_EQ(cg_write(&self->c, "cgroup.subtree_control", "+memory +pids"), 0); + EXPECT_EQ(cg_write(&self->c, "a/pids.max", "5"), 0); + EXPECT_EQ(cg_write(&self->c, "a/memory.max", "67108864"), 0); + + /* That goes for a new task as well, which starts in the budget. */ + EXPECT_EQ(spawn(self->c.cfd, NULL, "pause", NULL, NULL), -1); + EXPECT_EQ(errno, EBUSY); + /* Unless it is told where else. */ + EXPECT_EQ(run_in_cgroup(&self->c, "a", "/a"), 0); + close(p); +} + +/* Start @what in the cgroup @cgfd. Returns a pidfd, or -1 with what START had to say. */ +static int spawn_in(struct cellctx *c, int cgfd, const char *what, const char *arg) +{ + int pidfd = prepare(c->cfd, what, arg, NULL), e; + + if (pidfd < 0) + return -1; + if (pidfd_configure(pidfd, PIDFD_SET_FD, "cgroup", NULL, cgfd) || + pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0)) { + e = errno; + close(pidfd); + errno = e; + return -1; + } + return pidfd; +} + +/* It is within the budget of the cell, there, and can hold a task. Or there is no task. */ +TEST_F(cell, spawn_names_no_other_cgroup) +{ + struct cellctx other; + int init, fd, pidfd; + + ASSERT_EQ(controllers(), 0); + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + ASSERT_EQ(cell_build(&other, &(struct build){ .ids = 65536 }), 0); + ASSERT_EQ(cg_mkdir(&self->c, "a"), 0); + ASSERT_EQ(cg_mkdir(&self->c, "a/leaf"), 0); + ASSERT_EQ(cg_mkdir(&self->c, "gone"), 0); + ASSERT_EQ(cg_mkdir(&other, "theirs"), 0); + fd = openat(self->c.cgfd, "a/leaf", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(fd, 0); + init = spawn_in(&self->c, fd, "pause", NULL); + ASSERT_GE(init, 0); + close(fd); + + /* Above the budget, beside it, and in that of another cell. */ + fd = open(CGROOT, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + EXPECT_EQ(spawn_in(&self->c, fd, "pause", NULL), -1); + EXPECT_EQ(errno, EPERM); + close(fd); + EXPECT_EQ(spawn_in(&self->c, other.cgfd, "pause", NULL), -1); + EXPECT_EQ(errno, EPERM); + fd = openat(other.cgfd, "theirs", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + EXPECT_EQ(spawn_in(&self->c, fd, "pause", NULL), -1); + EXPECT_EQ(errno, EPERM); + close(fd); + + /* Removed since it was named, and since it was opened. */ + fd = openat(self->c.cgfd, "gone", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + pidfd = prepare(self->c.cfd, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "cgroup", NULL, fd), 0); + ASSERT_EQ(unlinkat(self->c.cgfd, "gone", AT_REMOVEDIR), 0); + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), -1); + EXPECT_EQ(errno, ENODEV); + close(pidfd); + EXPECT_EQ(spawn_in(&self->c, fd, "pause", NULL), -1); + EXPECT_EQ(errno, ENOENT); + close(fd); + + /* Passes a controller on. */ + ASSERT_EQ(cg_write(&self->c, "cgroup.subtree_control", "+memory"), 0); + ASSERT_EQ(cg_write(&self->c, "a/cgroup.subtree_control", "+memory"), 0); + fd = openat(self->c.cgfd, "a", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + EXPECT_EQ(spawn_in(&self->c, fd, "pause", NULL), -1); + EXPECT_EQ(errno, EBUSY); + close(fd); + + /* No cgroup at all. */ + pidfd = prepare(self->c.cfd, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "cgroup", NULL, fd), -1); + EXPECT_EQ(errno, EBADF); + close(fd); + /* The last word counts, and an O_PATH file will do. */ + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "cgroup", NULL, other.cgfd), 0); + fd = openat(self->c.cgfd, "a/leaf", O_PATH | O_CLOEXEC); + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "cgroup", NULL, fd), 0); + close(fd); + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0); + EXPECT_STREQ(cg_of(&self->c, pidfd_nr(pidfd)), "/a/leaf"); + close(pidfd); + + /* A kill finds them wherever they are. */ + EXPECT_EQ(cell_kill(self->c.cfd, 0), 0); + cell_wait(self->c.cfd); + EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD); + EXPECT_EQ(cg_children(&self->c), 0); + close(init); + cell_cleanup(&other); +} + +/* What the ids of the cell own below the budget, as the host sees it. */ +TEST_F(cell, manages_its_cgroups) +{ + struct inside in; + struct stat st; + char path[256]; + long first; + + ASSERT_EQ(controllers(), 0); + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .caps = CAP(CHOWN) }), 0); + ASSERT_EQ(fstat(self->c.cgfd, &st), 0); + first = st.st_uid; + ASSERT_GE(first, (long)CELL_WINDOW_FIRST); + + ASSERT_EQ(start_inside(&self->c, &in, "cg-manage", "block", -1), 0); + ASSERT_EQ(wait_ready(&in), 0); + + ASSERT_EQ(fstatat(self->c.cgfd, "init.scope", &st, 0), 0); + EXPECT_EQ(st.st_uid, first); + ASSERT_EQ(fstatat(self->c.cgfd, "svc", &st, 0), 0); + EXPECT_EQ(st.st_uid, first + 1000); + EXPECT_EQ(st.st_gid, first + 1000); + ASSERT_EQ(fstatat(self->c.cgfd, "svc/pids.max", &st, 0), 0); + EXPECT_EQ(st.st_uid, first); + + /* That is for a task of the cell to do. Nobody else names its ids. */ + at(&self->c, "svc", path, sizeof(path)); + EXPECT_EQ(chown(path, first + 5, first + 5), -1); + EXPECT_EQ(errno, EOVERFLOW); + EXPECT_EQ(chown(CGROOT, first, first), -1); + EXPECT_EQ(errno, EOVERFLOW); + + /* The init has taken over. Another task goes where it made room for one. */ + EXPECT_EQ(spawn(self->c.cfd, NULL, "pause", NULL, NULL), -1); + EXPECT_EQ(errno, EBUSY); + EXPECT_EQ(run_in_cgroup(&self->c, "init.scope", "/init.scope"), 0); + EXPECT_EQ(run_in_cgroup(&self->c, "svc", "/svc"), 0); + + ASSERT_EQ(write(in.go, "g", 1), 1); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + done_inside(&in); + + /* Nothing is left that belongs to them. */ + EXPECT_EQ(cg_children(&self->c), 0); + ASSERT_EQ(fstat(self->c.cgfd, &st), 0); + EXPECT_EQ(st.st_uid, 0); +} + +static int one_id_manages(void) +{ + struct build b = { .ids = 1 }; + struct inside in; + struct cellctx c; + int st; + + if (cell_build(&c, &b)) + return 1; + if (start_inside(&c, &in, "cg-manage", "one", -1) || wait_ready(&in)) + return 2; + if (write(in.go, "g", 1) != 1) + return 3; + st = cell_wait(c.cfd); + done_inside(&in); + cell_cleanup(&c); + return st ? 4 : 0; +} + +TEST_F(cell, one_id_cell_manages_its_cgroups) +{ + ASSERT_EQ(controllers(), 0); + EXPECT_EQ(unprivileged(one_id_manages), 0); +} + +/* + * Holding the cellfd is all it takes. What the init of the cell made belongs to + * ids that nobody outside of the cell has, whoever built it included. + */ +static int spawns_where_it_owns_nothing(void) +{ + struct build b = { .ids = 65536, .caps = CAP(CHOWN) }; + struct inside in; + struct cellctx c; + struct stat st; + int ret = 0; + + if (cell_build(&c, &b)) + return 1; + if (start_inside(&c, &in, "cg-manage", "block", -1) || wait_ready(&in)) + return 2; + /* It has not even a name for them. */ + if (fstatat(c.cgfd, "svc", &st, 0) || st.st_uid != 65534) + ret = 3; + else if (!faccessat(c.cgfd, "svc/cgroup.procs", W_OK, 0)) + ret = 4; + else if (run_in_cgroup(&c, "svc", "/svc")) + ret = 5; + if (write(in.go, "g", 1) != 1 || cell_wait(c.cfd)) + ret = ret ?: 6; + done_inside(&in); + cell_cleanup(&c); + return ret; +} + +TEST_F(cell, unprivileged_builder_spawns_where_the_cell_made_room) +{ + ASSERT_EQ(controllers(), 0); + EXPECT_EQ(unprivileged(spawns_where_it_owns_nothing), 0); +} + +static int own_limits(unsigned int ids) +{ + struct build b = { .ids = ids }; + struct inside in; + struct cellctx c; + char buf[16] = {}; + int max, st; + + if (cell_build(&c, &b)) + return 1; + if (cg_write(&c, "pids.max", "7")) + return 2; + max = openat(c.cgfd, "pids.max", O_RDWR | O_CLOEXEC); + if (max < 0 || start_inside(&c, &in, "cg-own", NULL, max)) + return 3; + st = cell_wait(c.cfd); + done_inside(&in); + if (pread(max, buf, sizeof(buf) - 1, 0) <= 0 || strcmp(buf, "7\n")) + st = 5; + close(max); + cell_cleanup(&c); + return st ? (st == 5 ? 5 : 4) : 0; +} + +static int own_limits_one_id(void) +{ + return own_limits(1); +} + +/* The limits of the budget stay with whoever is above. */ +TEST_F(cell, limits_of_the_budget_are_not_the_cells) +{ + ASSERT_EQ(controllers(), 0); + EXPECT_EQ(own_limits(65536), 0); +} + +/* A cell of one id owns what its maker owns, and its maker made the budget. */ +TEST_F(cell, limits_of_the_budget_are_not_a_one_id_cells) +{ + ASSERT_EQ(controllers(), 0); + EXPECT_EQ(unprivileged(own_limits_one_id), 0); +} + +/* What it is in the middle of making goes as well. */ +TEST_F_TIMEOUT(cell, killed_while_it_makes_cgroups, 300) +{ + struct inside in; + struct stat st; + int i; + + for (i = 0; i < 10; i++) { + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + ASSERT_EQ(start_inside(&self->c, &in, "cg-churn", NULL, -1), 0); + ASSERT_EQ(wait_ready(&in), 0); + usleep(50000); + EXPECT_EQ(cell_kill(self->c.cfd, 0), 0); + cell_wait(self->c.cfd); + EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD); + EXPECT_EQ(cg_children(&self->c), 0); + ASSERT_EQ(fstat(self->c.cgfd, &st), 0); + EXPECT_EQ(st.st_uid, 0); + done_inside(&in); + cell_cleanup(&self->c); + self->c.cfd = self->c.tree = self->c.raw = self->c.cgfd = -1; + self->c.rootfd = self->c.layers = -1; + } +} + +/* A task that the cell has frozen is killed like any other. */ +TEST_F(cell, kill_takes_what_the_cell_has_frozen) +{ + struct inside in; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + ASSERT_EQ(start_inside(&self->c, &in, "cg-freeze", NULL, -1), 0); + ASSERT_EQ(wait_ready(&in), 0); + EXPECT_EQ(cg_frozen(&self->c, "cold"), 1); + EXPECT_EQ(cell_kill(self->c.cfd, 0), 0); + cell_wait(self->c.cfd); + EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD); + EXPECT_EQ(cg_children(&self->c), 0); + done_inside(&in); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/cell/cell_child.c b/tools/testing/selftests/cell/cell_child.c new file mode 100644 index 000000000000..2f2d205ce9d3 --- /dev/null +++ b/tools/testing/selftests/cell/cell_child.c @@ -0,0 +1,1626 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * What runs inside a cell for cell_test. Statically linked: the tree of the + * cell holds this file and nothing else. + * + * Usage: cell_child <check>. Complains on stderr, exits 0 if all is well. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <signal.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <grp.h> +#include <netinet/in.h> +#include <pthread.h> +#include <time.h> +#include <sys/ioctl.h> +#include <sys/mount.h> +#include <sys/resource.h> +#include <sys/prctl.h> +#include <sys/ptrace.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/sysmacros.h> +#include <sys/un.h> +#include <sys/wait.h> +#include <sys/xattr.h> +#include <linux/capability.h> +#include <linux/keyctl.h> +#include <linux/if_tun.h> +#include <linux/netfilter/nfnetlink.h> +#include <linux/netfilter/nf_tables.h> +#include <linux/netlink.h> +#include <linux/rtnetlink.h> +#include <linux/sockios.h> +#include <net/if.h> +#include <net/route.h> + +#include "cell_util.h" + +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) + +extern char **environ; +static int failures; + +#define CHECK(cond, fmt, ...) do { \ + if (!(cond)) { \ + fprintf(stderr, "cell_child: %s:%d: " fmt "\n", __func__, \ + __LINE__, ##__VA_ARGS__); \ + failures++; \ + } \ +} while (0) + +#define CHECK_ERRNO(call, err) do { \ + errno = 0; \ + long __r = (long)(call); \ + CHECK(__r == -1 && errno == (err), "%s: returned %ld, errno %d, wanted %d", \ + #call, __r, errno, (err)); \ +} while (0) + +static unsigned long long status_caps(const char *field) +{ + unsigned long long val = ~0ULL; + char line[256]; + FILE *f = fopen("/proc/self/status", "r"); + + while (f && fgets(line, sizeof(line), f)) { + if (!strncmp(line, field, strlen(field))) + val = strtoull(line + strlen(field) + 1, NULL, 16); + } + if (f) + fclose(f); + return val; +} + +static void check_caps_are(unsigned long long has, unsigned long long bound) +{ + CHECK(status_caps("CapPrm") == has, "permitted %llx", status_caps("CapPrm")); + CHECK(status_caps("CapEff") == has, "effective %llx", status_caps("CapEff")); + CHECK(status_caps("CapBnd") == bound, "bounding set %llx", status_caps("CapBnd")); + CHECK(status_caps("CapInh") == 0, "inheritable %llx", status_caps("CapInh")); + CHECK(status_caps("CapAmb") == 0, "ambient %llx", status_caps("CapAmb")); +} + +static int set_caps_to(unsigned long long effective, unsigned long long permitted) +{ + struct __user_cap_header_struct hdr = { .version = _LINUX_CAPABILITY_VERSION_3 }; + struct __user_cap_data_struct data[2] = { + { .effective = effective, .permitted = permitted }, + { .effective = effective >> 32, .permitted = permitted >> 32 }, + }; + + return syscall(SYS_capset, &hdr, data); +} + +static int set_caps(unsigned long long caps) +{ + return set_caps_to(caps, caps); +} + +/* Run @fn in a task of its own, which may do to itself what it likes. */ +static void in_a_fork(void (*fn)(unsigned long long), unsigned long long caps) +{ + int status = -1; + pid_t pid = fork(); + + if (!pid) { + failures = 0; + fn(caps); + _exit(!!failures); + } + CHECK(waitpid(pid, &status, 0) == pid && !status, "status %x", status); +} + +#define CAP(x) (1ULL << CAP_##x) + +static void become_somebody(unsigned long long caps) +{ + if (!(caps & CAP(SETGID))) { + CHECK_ERRNO(setgroups(0, NULL), EPERM); + CHECK_ERRNO(setgid(33), EPERM); + } else { + CHECK(!setgroups(0, NULL) && !setgid(33), "setgid: %d", errno); + } + if (!(caps & CAP(SETUID))) { + CHECK_ERRNO(setuid(33), EPERM); + return; + } + CHECK(!setuid(33), "setuid: %d", errno); + /* Which costs them, as ever. */ + check_caps_are(0, caps); +} + +/* Letting go takes nothing, CAP_SETPCAP included. */ +static void shrink_the_bounding_set(unsigned long long caps) +{ + CHECK(!prctl(PR_CAPBSET_DROP, CAP_CHOWN, 0, 0, 0), "drop: %d", errno); + check_caps_are(caps, caps & ~CAP(CHOWN)); + CHECK(!prctl(PR_CAPBSET_DROP, CAP_SYS_ADMIN, 0, 0, 0), "drop: %d", errno); + check_caps_are(caps, caps & ~(CAP(CHOWN) | CAP(SYS_ADMIN))); + CHECK_ERRNO(prctl(PR_CAPBSET_DROP, 63, 0, 0, 0), EINVAL); +} + +/* + * What the builder named and nothing else, through an exec and for good: a task + * can let go of a capability, and nothing brings one back. + */ +static void check_caps(char **argv) +{ + unsigned long long caps = strtoull(argv[2], NULL, 16); + const char *stage = argv[3] ?: "first"; + int fd; + + if (!strcmp(stage, "first")) { + check_caps_are(caps, caps); + + fd = open("/tmp/c", O_CREAT | O_WRONLY, 0644); + CHECK(fd >= 0, "create: %d", errno); + if (caps & CAP(CHOWN)) + CHECK(!fchown(fd, 33, 33), "chown: %d", errno); + else + CHECK_ERRNO(fchown(fd, 33, 33), EPERM); + close(fd); + /* What goes by a capability over the host stays out of reach. */ + CHECK_ERRNO(mknod("/tmp/n", S_IFCHR | 0600, makedev(1, 3)), EPERM); + in_a_fork(become_somebody, caps); + in_a_fork(shrink_the_bounding_set, caps); + CHECK_ERRNO(set_caps(caps | CAP(SETFCAP)), EPERM); + stage = "again"; + } else if (!strcmp(stage, "again")) { + /* Uid 0 ran a program. */ + check_caps_are(caps, caps); + CHECK(!set_caps(0), "capset: %d", errno); + check_caps_are(0, caps); + stage = "dropped"; + } else if (!strcmp(stage, "dropped")) { + /* It did so again, and has nothing to show for it. */ + check_caps_are(0, caps); + CHECK_ERRNO(set_caps(caps ?: CAP(CHOWN)), EPERM); + stage = "gain"; + } else { + /* Set-uid root, with a file capability. */ + check_caps_are(0, caps); + return; + } + if (failures) + return; + execl(strcmp(stage, "gain") ? "/cell_child" : "/gain", "cell_child", "caps", argv[2], + stage, NULL); + CHECK(0, "exec for %s: %d", stage, errno); +} + +/* Nothing but what was given. */ +static void check_basic(void) +{ + struct cell_info info = { .mask = CELL_INFO_ID }; + char buf[256]; + int fd, nr = 0; + + CHECK(getpid() == 1, "pid %d", getpid()); + CHECK(getppid() == 0, "ppid %d", getppid()); + CHECK(getuid() == 0 && geteuid() == 0, "uid %d euid %d", getuid(), geteuid()); + CHECK(getgid() == 0 && getegid() == 0, "gid %d egid %d", getgid(), getegid()); + CHECK(getgroups(0, NULL) == 0, "%d groups", getgroups(0, NULL)); + CHECK(getsid(0) == 1, "sid %d", getsid(0)); + CHECK(getpgrp() == 1, "pgrp %d", getpgrp()); + CHECK(getcwd(buf, sizeof(buf)) && !strcmp(buf, "/"), "cwd %s", buf); + CHECK(!gethostname(buf, sizeof(buf)) && !strcmp(buf, "celltest"), + "hostname %s", buf); + CHECK(umask(0) == 022, "umask"); + CHECK(open("/dev/tty", O_RDWR) == -1, "has a controlling terminal"); + + for (fd = 0; fd < 1024; fd++) { + if (fcntl(fd, F_GETFD) != -1) + nr++; + } + CHECK(nr == 3, "%d open files", nr); + + CHECK(environ[0] && !strcmp(environ[0], "CELL=yes") && !environ[1], + "environment"); + + CHECK(prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) == 1, "no_new_privs is off"); + check_caps_are(0, 0); + + CHECK(!cell_info(CELL_SELF, &info, sizeof(info), 0) && info.id != 0, + "own cell id %llu", (unsigned long long)info.id); + + /* The "spawn" slot is empty, whatever the file descriptor is. */ + CHECK_ERRNO(cell_create(CELL_SELF, 0), EPERM); + for (fd = -1; fd < 8; fd++) + CHECK_ERRNO(pidfd_open_empty(fd, 0), EPERM); + CHECK_ERRNO(pidfd_open_empty(-EBADF, 0), EPERM); +} + +/* A task has no say in its namespaces. */ +static void check_membership(void) +{ + static const int flags[] = { + CLONE_NEWUSER, CLONE_NEWNS, CLONE_NEWPID, CLONE_NEWNET, + CLONE_NEWIPC, CLONE_NEWUTS, CLONE_NEWCGROUP, 0x80 /* NEWTIME */, + }; + static const char * const ns[] = { + "user", "mnt", "pid", "net", "ipc", "uts", "cgroup", "time", + }; + char path[64]; + unsigned int i; + int fd; + + for (i = 0; i < ARRAY_SIZE(flags); i++) + CHECK_ERRNO(unshare(flags[i]), EPERM); + + for (i = 0; i < ARRAY_SIZE(ns); i++) { + snprintf(path, sizeof(path), "/proc/self/ns/%s", ns[i]); + fd = open(path, O_RDONLY); + CHECK(fd >= 0, "open %s: %d", path, errno); + if (fd < 0) + continue; + CHECK_ERRNO(setns(fd, 0), EPERM); + close(fd); + } + + CHECK_ERRNO(syscall(SYS_clone, CLONE_NEWUSER | SIGCHLD, 0, 0, 0, 0), EPERM); + CHECK_ERRNO(syscall(SYS_clone, CLONE_NEWNET | SIGCHLD, 0, 0, 0, 0), EPERM); + CHECK_ERRNO(syscall(SYS_open_tree, AT_FDCWD, "/", 2 /* OPEN_TREE_NAMESPACE */), + EPERM); + CHECK_ERRNO(mount("none", "/tmp", "tmpfs", 0, NULL), EPERM); + CHECK_ERRNO(umount2("/proc", MNT_DETACH), EPERM); +} + +/* Nothing a cell writes imparts privilege. */ +static void check_setid(void) +{ + /* VFS_CAP_REVISION_2, cap_net_raw permitted and effective. */ + static const unsigned char cap[20] = { + 0x01, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0x00, + }; + /* VFS_CAP_REVISION_3, the same, with uid 0 for the root. */ + static const unsigned char cap3[24] = { + 0x01, 0x00, 0x00, 0x03, 0x00, 0x20, 0x00, 0x00, + }; + struct stat st; + char buf[64]; + int fd; + + fd = open("/tmp/a", O_CREAT | O_WRONLY, 06755); + CHECK(fd >= 0, "create: %d", errno); + CHECK(!fstat(fd, &st) && !(st.st_mode & (S_ISUID | S_ISGID)), + "created with mode %o", st.st_mode); + CHECK(st.st_uid == 0 && st.st_gid == 0, "owned by %d:%d", st.st_uid, st.st_gid); + + CHECK(!fchmod(fd, 06755), "fchmod: %d", errno); + CHECK(!fstat(fd, &st) && (st.st_mode & 07777) == 0755, + "chmod gave mode %o", st.st_mode); + CHECK(!chmod("/tmp/a", 04711), "chmod: %d", errno); + CHECK(!stat("/tmp/a", &st) && (st.st_mode & 07777) == 0711, + "chmod gave mode %o", st.st_mode); + + CHECK_ERRNO(fsetxattr(fd, "security.capability", cap, sizeof(cap), 0), EPERM); + /* + * Nor one for the root of the cell only. On disk that is uid 0, so whoever + * else looks at the filesystem would take it for one of its own. + */ + CHECK_ERRNO(fsetxattr(fd, "security.capability", cap3, sizeof(cap3), 0), EPERM); + close(fd); + + CHECK(!mknod("/tmp/f", S_IFIFO | 06644, 0), "mknod: %d", errno); + CHECK(!stat("/tmp/f", &st) && !(st.st_mode & (S_ISUID | S_ISGID)), + "mknod gave mode %o", st.st_mode); + + /* + * A set-uid file that came with the tree. A store through a shared + * mapping would leave the bits alone, so being able to write is enough + * to lose them. + */ + CHECK(!stat("/su", &st) && (st.st_mode & 07777) == 06755, "mode %o", st.st_mode); + fd = open("/su", O_RDONLY); + CHECK(fd >= 0 && !fstat(fd, &st) && (st.st_mode & 07777) == 06755, + "reading cost the bits: %o", st.st_mode); + CHECK(fgetxattr(fd, "security.capability", buf, sizeof(buf)) > 0, + "reading cost the file capability: %d", errno); + close(fd); + fd = open("/su", O_RDWR); + CHECK(fd >= 0 && !fstat(fd, &st) && (st.st_mode & 07777) == 0755, + "still mode %o when open for writing", st.st_mode); + CHECK_ERRNO(fgetxattr(fd, "security.capability", buf, sizeof(buf)), ENODATA); + close(fd); + + /* On a directory setgid is about inheritance, not privilege. */ + CHECK(!mkdir("/tmp/d", 0755) && !chmod("/tmp/d", 02755), "mkdir: %d", errno); + CHECK(!stat("/tmp/d", &st) && (st.st_mode & S_ISGID), "mode %o", st.st_mode); + + /* Ids work as ever otherwise. */ + CHECK(!chown("/tmp/a", 33, 33), "chown: %d", errno); + CHECK(!stat("/tmp/a", &st) && st.st_uid == 33 && st.st_gid == 33, + "owned by %d:%d", st.st_uid, st.st_gid); +} + +static int send_fd(int sock, int fd) +{ + char data = 'x', ctl[CMSG_SPACE(sizeof(int))] = {}; + struct iovec iov = { .iov_base = &data, .iov_len = 1 }; + struct msghdr msg = { + .msg_iov = &iov, .msg_iovlen = 1, + .msg_control = ctl, .msg_controllen = sizeof(ctl), + }; + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + memcpy(CMSG_DATA(cmsg), &fd, sizeof(int)); + return sendmsg(sock, &msg, 0); +} + +static void check_peer_queries(int sock) +{ + struct ucred uc; + socklen_t len; + gid_t groups[8]; + int pidfd; + + len = sizeof(uc); + CHECK_ERRNO(getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &uc, &len), ENODATA); + len = sizeof(groups); + CHECK_ERRNO(getsockopt(sock, SOL_SOCKET, SO_PEERGROUPS, groups, &len), ENODATA); + len = sizeof(pidfd); + CHECK_ERRNO(getsockopt(sock, SOL_SOCKET, 77 /* SO_PEERPIDFD */, &pidfd, &len), + ENODATA); +} + +/* + * Between cells a socket carries bytes. fd 3 is one end of a socketpair the + * host made, /tmp/sock a listener of the host's. + */ +static void check_unix(void) +{ + struct sockaddr_un addr = { .sun_family = AF_UNIX, .sun_path = "/tmp/sock" }; + char ctl[256], c; + struct iovec iov = { .iov_base = &c, .iov_len = 1 }; + struct msghdr msg = { + .msg_iov = &iov, .msg_iovlen = 1, + .msg_control = ctl, .msg_controllen = sizeof(ctl), + }; + int one = 1, sock, sv[2]; + struct ucred uc; + socklen_t len; + + /* + * The host queued a file on the socket while both ends were still its + * own. Nobody could have refused that. It must not arrive. + */ + CHECK(recvmsg(3, &msg, MSG_PEEK) == 1 && c == 'q', "peek: %d", errno); + CHECK(msg.msg_controllen == 0, "peeked %zu bytes of ancillary data", + (size_t)msg.msg_controllen); + msg.msg_controllen = sizeof(ctl); + CHECK(recvmsg(3, &msg, 0) == 1 && c == 'q', "recvmsg: %d", errno); + CHECK(msg.msg_controllen == 0, "got %zu bytes of ancillary data", + (size_t)msg.msg_controllen); + CHECK(fcntl(4, F_GETFD) == -1, "a file arrived"); + msg.msg_controllen = sizeof(ctl); + + check_peer_queries(3); + CHECK_ERRNO(send_fd(3, 0), EPERM); + CHECK(write(3, "a", 1) == 1, "plain bytes: %d", errno); + + /* The host sends a byte, with SO_PASSCRED on at this end. */ + CHECK(!setsockopt(3, SOL_SOCKET, SO_PASSCRED, &one, sizeof(one)), "passcred"); + CHECK(write(3, "b", 1) == 1, "plain bytes: %d", errno); + CHECK(recvmsg(3, &msg, 0) == 1 && c == 'h', "recvmsg: %d", errno); + CHECK(msg.msg_controllen == 0, "got %zu bytes of ancillary data", + (size_t)msg.msg_controllen); + + sock = socket(AF_UNIX, SOCK_STREAM, 0); + CHECK(!connect(sock, (struct sockaddr *)&addr, sizeof(addr)), "connect: %d", errno); + check_peer_queries(sock); + CHECK_ERRNO(send_fd(sock, 0), EPERM); + CHECK(write(sock, "c", 1) == 1, "plain bytes: %d", errno); + close(sock); + + /* Within the cell everything is as it always was. */ + CHECK(!socketpair(AF_UNIX, SOCK_STREAM, 0, sv), "socketpair"); + len = sizeof(uc); + CHECK(!getsockopt(sv[0], SOL_SOCKET, SO_PEERCRED, &uc, &len) && + uc.pid == getpid() && uc.uid == 0, "SO_PEERCRED at home: %d", errno); + CHECK(send_fd(sv[0], 0) == 1, "SCM_RIGHTS at home: %d", errno); + msg.msg_controllen = sizeof(ctl); + CHECK(recvmsg(sv[1], &msg, 0) == 1 && msg.msg_controllen >= CMSG_LEN(sizeof(int)), + "no fd arrived at home"); +} + +/* fd 3 is a directory of a filesystem of the host that is not idmapped. */ +static void check_dirfd(void) +{ + CHECK_ERRNO(openat(3, "x", O_CREAT | O_WRONLY, 0644), EOVERFLOW); + CHECK_ERRNO(mkdirat(3, "d", 0755), EOVERFLOW); + CHECK_ERRNO(fchownat(3, "world", 0, 0, 0), EOVERFLOW); +} + +/* /data is a real filesystem, with a set-uid file that came with it. */ +static void check_data(const char *create) +{ + struct stat st; + int fd; + + fd = open("/data/su", O_RDWR); + CHECK(fd >= 0, "open: %d", errno); + CHECK(!fstat(fd, &st) && !(st.st_mode & (S_ISUID | S_ISGID)), + "still mode %o when open for writing", st.st_mode); + close(fd); + + if (!create) + return; + /* With ->atomic_open() this opens under the lock of the directory. */ + fd = open("/data/new", O_CREAT | O_EXCL | O_RDWR, 06755); + CHECK(fd >= 0, "create: %d", errno); + CHECK(!fstat(fd, &st) && !(st.st_mode & (S_ISUID | S_ISGID)), + "created with mode %o", st.st_mode); + CHECK(write(fd, "x", 1) == 1, "write: %d", errno); + close(fd); + CHECK(!unlink("/data/new"), "unlink: %d", errno); +} + +/* /data is an overlay, and writing to a file of the lower layer copies it up. */ +static void check_copy_up(void) +{ + struct stat st; + int fd; + + CHECK(!stat("/data/file", &st) && !st.st_uid && !st.st_gid, + "belongs to %u:%u", st.st_uid, st.st_gid); + fd = open("/data/file", O_WRONLY); + CHECK(fd >= 0, "open: %d", errno); + CHECK(write(fd, "upper", 5) == 5, "write: %d", errno); + fd = open("/data/new", O_CREAT | O_EXCL | O_WRONLY, 0644); + CHECK(fd >= 0, "create: %d", errno); +} + +/* + * fd 3 is /dev/vhost-net. In its kthread mode vhost puts a kernel thread into + * the cgroups of the owner. A kill could not take that with it. + */ +static void check_vhost(void) +{ + unsigned char mode = 0; /* VHOST_FORK_OWNER_KTHREAD */ + + CHECK(!ioctl(3, _IOW(0xAF, 0x84, unsigned char), &mode), + "VHOST_SET_FORK_FROM_OWNER: %d", errno); + /* vhost makes ENOMEM of whatever went wrong with its worker. */ + CHECK(ioctl(3, _IO(0xAF, 0x01) /* VHOST_SET_OWNER */) == -1, + "VHOST_SET_OWNER worked"); +} + +/* The spawner ignores, handles and blocks signals. None of that is ours. */ +static void check_signals(void) +{ + struct sigaction sa; + sigset_t set; + int sig; + + for (sig = 1; sig < _NSIG; sig++) { + /* The C library keeps a few to itself. */ + if (sigaction(sig, NULL, &sa)) + continue; + CHECK(sa.sa_handler == SIG_DFL, "signal %d is not at its default", sig); + } + CHECK(!sigprocmask(SIG_BLOCK, NULL, &set), "sigprocmask: %d", errno); + for (sig = 1; sig < _NSIG; sig++) + CHECK(sigismember(&set, sig) != 1, "signal %d is blocked", sig); + CHECK(!sigpending(&set), "sigpending: %d", errno); + for (sig = 1; sig < _NSIG; sig++) + CHECK(sigismember(&set, sig) != 1, "signal %d is pending", sig); +} + +/* The spawner has a filter that fails getrusage() with EACCES. */ +static void check_seccomp(void) +{ + struct rusage ru; + + CHECK_ERRNO(getrusage(RUSAGE_SELF, &ru), EACCES); +} + +static void on_usr1(int sig) +{ + _exit(0); +} + +/* + * An init only gets the signals it has a handler for, so without one nobody + * could tell a signal that was refused from one that was dropped. + */ +static void wait_for_usr1(void) +{ + signal(SIGUSR1, on_usr1); + /* Whoever wants to know that we are ready put a pipe there. */ + if (write(3, "x", 1) < 0) + ; + for (;;) + pause(); +} + +/* Spawned into a running cell: the parent is somebody outside. */ +static void check_second(void) +{ + CHECK(getpid() != 1 && getppid() == 0, "pid %d ppid %d", getpid(), getppid()); + CHECK_ERRNO(ptrace(PTRACE_TRACEME, 0, 0, 0), EPERM); +} + +static void check_oneid(const char *uid) +{ + CHECK((int)getuid() == atoi(uid), "uid %d", getuid()); + check_caps_are(0, 0); + CHECK_ERRNO(setuid(getuid() + 1), EINVAL); + CHECK_ERRNO(setgroups(0, NULL), EPERM); + /* The groups of whoever made the cell, denials and all. */ + CHECK(getgroups(0, NULL) == 2, "%d groups", getgroups(0, NULL)); + + /* The parent has the very same uid. It is not in this cell. */ + CHECK(syscall(SYS_keyctl, 1 /* JOIN_SESSION_KEYRING */, NULL) > 0, + "join session keyring: %d", errno); + CHECK_ERRNO(syscall(SYS_keyctl, 18 /* SESSION_TO_PARENT */), EPERM); + CHECK_ERRNO(ptrace(PTRACE_TRACEME, 0, 0, 0), EPERM); +} + +static int write_str(const char *path, const char *val) +{ + int fd = open(path, O_WRONLY), ret; + + if (fd < 0) + return -1; + ret = write(fd, val, strlen(val)) < 0 ? -1 : 0; + close(fd); + return ret; +} + +/* As "echo val > path" does it, which truncates the file. */ +static int write_at(int dir, const char *path, const char *val) +{ + int fd = openat(dir, path, O_WRONLY | O_TRUNC), ret; + + if (fd < 0) + return -1; + ret = write(fd, val, strlen(val)) < 0 ? -1 : 0; + close(fd); + return ret; +} + +static void check_in_cgroup(const char *want) +{ + char buf[512] = {}, line[256]; + int fd = open("/proc/self/cgroup", O_RDONLY); + + /* There may be legacy hierarchies about. */ + snprintf(line, sizeof(line), "0::%s\n", want); + CHECK(fd >= 0 && read(fd, buf, sizeof(buf) - 1) > 0 && strstr(buf, line), + "in %s and not in %s", buf, want); + close(fd); +} + +/* Tell whoever is outside, on fd 4, and wait for it to have had a look, on fd 5. */ +static void show_the_host(void) +{ + char c; + + CHECK(write(4, "r", 1) == 1 && read(5, &c, 1) == 1, "the host went away"); +} + +/* + * fd 3 is the budget. What an init that manages cgroups does: get out of the + * root, enable controllers there, and make a cgroup with a limit per service. + */ +static void check_cg_manage(const char *ids) +{ + struct stat st; + int status; + pid_t pid; + + CHECK(!mkdirat(3, "init.scope", 0755), "mkdir: %d", errno); + CHECK(!write_at(3, "init.scope/cgroup.procs", "0"), "move: %d", errno); + check_in_cgroup("/init.scope"); + CHECK(!write_at(3, "cgroup.subtree_control", "+memory +pids"), "enable: %d", errno); + + CHECK(!mkdirat(3, "svc", 0755), "mkdir: %d", errno); + CHECK(!write_at(3, "svc/pids.max", "5"), "pids.max: %d", errno); + CHECK(!write_at(3, "svc/memory.max", "67108864"), "memory.max: %d", errno); + pid = fork(); + if (!pid) { + failures = 0; + CHECK(!write_at(3, "svc/cgroup.procs", "0"), "move: %d", errno); + check_in_cgroup("/svc"); + _exit(!!failures); + } + CHECK(waitpid(pid, &status, 0) == pid && !status, "the service: %x", status); + + CHECK(!mkdirat(3, "svc/deeper", 0755), "mkdir: %d", errno); + CHECK(!unlinkat(3, "svc/deeper", AT_REMOVEDIR), "rmdir: %d", errno); + + /* Hand a cgroup to somebody in here. */ + if (!strcmp(ids, "block")) { + CHECK(!fchownat(3, "svc", 1000, 1000, 0), "chown: %d", errno); + CHECK(!fchownat(3, "svc/cgroup.procs", 1000, 1000, 0), "chown: %d", errno); + CHECK(!fstatat(3, "svc", &st, 0) && st.st_uid == 1000 && st.st_gid == 1000, + "belongs to %u:%u", st.st_uid, st.st_gid); + CHECK_ERRNO(fchownat(3, "svc", 70000, 70000, 0), EINVAL); + } + show_the_host(); +} + +/* + * fd 3 is the budget, fd 4 its pids.max, which somebody outside opened. The cell + * makes cgroups in the budget and puts tasks there. The rest of it is not the + * cell's, whoever it belongs to: that is what bounds the cell. + */ +static void check_cg_own(void) +{ + static const char * const not_ours[] = { + "pids.max", "memory.max", "memory.high", "cgroup.freeze", "cgroup.kill", + "cgroup.type", "cgroup.max.depth", "cgroup.max.descendants", + }; + static const char * const ours[] = { + "cgroup.procs", "cgroup.threads", "cgroup.subtree_control", + }; + struct timespec now[2] = { { .tv_nsec = UTIME_NOW }, { .tv_nsec = UTIME_NOW } }; + unsigned int i; + int fd; + + for (i = 0; i < ARRAY_SIZE(not_ours); i++) { + fd = openat(3, not_ours[i], O_WRONLY); + CHECK(fd == -1 && errno == EPERM, "%s: %d, %d", not_ours[i], fd, errno); + fd = openat(3, not_ours[i], O_WRONLY | O_TRUNC); + CHECK(fd == -1 && errno == EPERM, "%s: %d, %d", not_ours[i], fd, errno); + } + CHECK_ERRNO(ftruncate(4, 0), EPERM); + CHECK_ERRNO(write(4, "max", 3), EPERM); + + for (i = 0; i < ARRAY_SIZE(ours); i++) { + fd = openat(3, ours[i], O_WRONLY); + CHECK(fd >= 0, "%s: %d", ours[i], errno); + close(fd); + /* Truncating a file is writing to it, and a shell does. */ + fd = openat(3, ours[i], O_WRONLY | O_TRUNC); + CHECK(fd >= 0, "%s, truncating: %d", ours[i], errno); + close(fd); + /* To write to, and not to hand on. */ + CHECK_ERRNO(fchmodat(3, ours[i], 0666, 0), EPERM); + } + + CHECK_ERRNO(fchmodat(3, "pids.max", 0666, 0), EPERM); + CHECK_ERRNO(fchownat(3, "pids.max", getuid(), getgid(), 0), EPERM); + CHECK_ERRNO(utimensat(3, "pids.max", now, 0), EPERM); + CHECK_ERRNO(fchmod(3, 0777), EPERM); + CHECK_ERRNO(fchown(3, getuid(), getgid()), EPERM); + CHECK_ERRNO(fsetxattr(3, "user.cell", "c", 1, 0), EPERM); + + /* What it makes is its own, limits and all. */ + CHECK(!mkdirat(3, "mine", 0755), "mkdir: %d", errno); + CHECK(!write_at(3, "mine/cgroup.max.depth", "3"), "a limit below: %d", errno); + CHECK(!fchmodat(3, "mine", 0700, 0), "chmod below: %d", errno); + CHECK(!unlinkat(3, "mine", AT_REMOVEDIR), "rmdir: %d", errno); +} + +/* fd 3 is the budget. Make and remove cgroups until killed. */ +static void cg_churn(void) +{ + char a[32], b[48]; + int i; + + for (i = 0; i < 3; i++) { + if (!fork()) + break; + } + snprintf(a, sizeof(a), "c%d", getpid()); + snprintf(b, sizeof(b), "%s/x", a); + /* Not before it is known to work. */ + if (!i && (mkdirat(3, a, 0755) || write(4, "r", 1) != 1)) + _exit(1); + for (;;) { + mkdirat(3, a, 0755); + mkdirat(3, b, 0755); + unlinkat(3, b, AT_REMOVEDIR); + unlinkat(3, a, AT_REMOVEDIR); + } +} + +/* fd 3 is the budget. Freeze a cgroup with a task in it, and wait to be killed. */ +static void cg_freeze(void) +{ + CHECK(!mkdirat(3, "cold", 0755), "mkdir: %d", errno); + if (!fork()) { + write_at(3, "cold/cgroup.procs", "0"); + for (;;) + pause(); + } + usleep(100000); + CHECK(!write_at(3, "cold/cgroup.freeze", "1"), "freeze: %d", errno); + show_the_host(); +} + +/* + * All of cgroupfs is at /data, and /data/open and its files are there for + * anybody to write. It is not within the budget. + */ +static void check_outside_the_budget(void) +{ + CHECK_ERRNO(write_str("/data/open/cgroup.subtree_control", "+pids"), EPERM); + CHECK_ERRNO(write_str("/data/open/cgroup.procs", "0"), EPERM); + CHECK_ERRNO(write_str("/data/open/pids.max", "1"), EPERM); + CHECK_ERRNO(rmdir("/data/open/empty"), EPERM); + CHECK_ERRNO(chmod("/data/open", 0700), EPERM); + /* These may not get that far: the id of the caller cannot be stored here. */ + CHECK(mkdir("/data/open/mine", 0755) && (errno == EPERM || errno == EOVERFLOW), + "mkdir: %d", errno); + CHECK(rename("/data/open/empty", "/data/open/other") && + (errno == EPERM || errno == EOVERFLOW), "rename: %d", errno); + CHECK(chown("/data/open", 0, 0) && (errno == EPERM || errno == EOVERFLOW), + "chown: %d", errno); + /* Neither of these asks cgroup anything. */ + CHECK_ERRNO(setxattr("/data/open", "user.cell", "x", 1, 0), EPERM); + CHECK_ERRNO(setxattr("/data/open", "user.host", "x", 1, 0), EPERM); + CHECK_ERRNO(removexattr("/data/open", "user.host"), EPERM); + CHECK_ERRNO(utimensat(AT_FDCWD, "/data/open", NULL, 0), EPERM); + CHECK_ERRNO(open("/data/open/cgroup.procs", O_WRONLY), EPERM); + CHECK(open("/data/open/cgroup.procs", O_RDONLY) >= 0, "open to read: %d", errno); +} + +/* A legacy hierarchy has no budget on it, and it can rename. */ +static void check_legacy_hierarchy(void) +{ + CHECK_ERRNO(rename("/data/open/empty", "/data/open/other"), EPERM); + CHECK_ERRNO(rmdir("/data/open/empty"), EPERM); + CHECK_ERRNO(write_str("/data/open/cgroup.procs", "0"), EPERM); + CHECK_ERRNO(write_str("/data/open/notify_on_release", "1"), EPERM); + CHECK_ERRNO(setxattr("/data/open", "user.cell", "x", 1, 0), EPERM); + CHECK(mkdir("/data/open/mine", 0755) && (errno == EPERM || errno == EOVERFLOW), + "mkdir: %d", errno); +} + +/* + * A cell of one id runs on the id of its maker, so it owns what its maker owns. + * File descriptor 3 is on a cgroup of the maker's that is not within the budget. + */ +static void check_outside_but_its_own(void) +{ + struct stat st; + + CHECK(!fstat(3, &st) && st.st_uid == getuid(), "owner %u", st.st_uid); + CHECK_ERRNO(fchmod(3, 0700), EPERM); + CHECK_ERRNO(fchown(3, -1, getgid()), EPERM); + CHECK_ERRNO(futimens(3, NULL), EPERM); + CHECK_ERRNO(fsetxattr(3, "user.cell", "x", 1, 0), EPERM); + CHECK_ERRNO(fremovexattr(3, "user.host"), EPERM); + CHECK_ERRNO(mkdirat(3, "mine", 0755), EPERM); + CHECK_ERRNO(unlinkat(3, "empty", AT_REMOVEDIR), EPERM); + CHECK_ERRNO(openat(3, "cgroup.procs", O_WRONLY), EPERM); + CHECK_ERRNO(fchmodat(3, "cgroup.procs", 0666, 0), EPERM); + /* File descriptor 4 was opened for writing by the maker. */ + CHECK_ERRNO(write(4, "5", 1), EPERM); + /* File descriptor 5, if there is one, is the same on a legacy hierarchy. */ + if (fstat(5, &st)) + return; + CHECK(st.st_uid == getuid(), "owner %u", st.st_uid); + CHECK_ERRNO(renameat(5, "empty", 5, "other"), EPERM); + CHECK_ERRNO(mkdirat(5, "mine", 0755), EPERM); + CHECK_ERRNO(unlinkat(5, "empty", AT_REMOVEDIR), EPERM); + CHECK_ERRNO(fchmod(5, 0700), EPERM); + CHECK_ERRNO(fsetxattr(5, "user.cell", "x", 1, 0), EPERM); +} + +#ifndef SO_PEERPIDFD +#define SO_PEERPIDFD 77 +#endif + +static int race_sock = -1, race_stop, race_hits; + +static void *peer_race_ask(void *arg) +{ + while (!__atomic_load_n(&race_stop, __ATOMIC_RELAXED)) { + int s = __atomic_load_n(&race_sock, __ATOMIC_RELAXED), fd = -1; + socklen_t len = sizeof(int); + + if (s >= 0 && !getsockopt(s, SOL_SOCKET, SO_PEERPIDFD, &fd, &len)) { + __atomic_fetch_add(&race_hits, 1, __ATOMIC_RELAXED); + close(fd); + } + } + return NULL; +} + +/* + * Who the peer is must be checked and read in one go. One thread connects a + * fresh socket to a listener of the host's, over and over, while another asks + * each of them for a pidfd on its peer. There is no moment at which it gets one. + */ +static void check_peer_race(void) +{ + struct sockaddr_un addr = { .sun_family = AF_UNIX, .sun_path = "/tmp/sock" }; + time_t end = time(NULL) + 8; + pthread_t ask[2]; + int i, s, connected = 0; + + for (i = 0; i < 2; i++) + CHECK(!pthread_create(&ask[i], NULL, peer_race_ask, NULL), "thread"); + while (time(NULL) < end) { + s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (s < 0) + break; + __atomic_store_n(&race_sock, s, __ATOMIC_RELAXED); + if (!connect(s, (struct sockaddr *)&addr, sizeof(addr))) + connected++; + __atomic_store_n(&race_sock, -1, __ATOMIC_RELAXED); + close(s); + } + __atomic_store_n(&race_stop, 1, __ATOMIC_RELAXED); + for (i = 0; i < 2; i++) + pthread_join(ask[i], NULL); + CHECK(connected > 100, "only %d connected", connected); + CHECK(!race_hits, "%d pidfds on a task of the host in %d connects", + race_hits, connected); +} + +static const struct sockaddr_in pod_addr = { + .sin_family = AF_INET, + .sin_port = __builtin_bswap16(7777), + .sin_addr = { __builtin_bswap32(INADDR_LOOPBACK) }, +}; + +/* One container of a pod: leaves a file on the volume and serves on localhost. */ +static void pod_serve(void) +{ + int one = 1, fd, lsock, conn; + char c = 0; + + fd = open("/data/from-a", O_CREAT | O_WRONLY, 0600); + CHECK(fd >= 0, "create on the volume: %d", errno); + close(fd); + lsock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + CHECK(lsock >= 0, "socket: %d", errno); + setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + CHECK(!bind(lsock, (const struct sockaddr *)&pod_addr, sizeof(pod_addr)), + "bind: %d", errno); + CHECK(!listen(lsock, 1), "listen: %d", errno); + /* Ready. */ + CHECK(write(3, "r", 1) == 1, "ready: %d", errno); + conn = accept(lsock, NULL, NULL); + CHECK(conn >= 0, "accept: %d", errno); + CHECK(read(conn, &c, 1) == 1 && c == 'p', "read '%c': %d", c, errno); + CHECK(write(conn, "q", 1) == 1, "write: %d", errno); +} + +/* Another one: finds the file, as its own root's, and talks to the first. */ +static void pod_connect(void) +{ + struct stat st; + char c = 0; + int sock; + + CHECK(!stat("/data/from-a", &st), "stat on the volume: %d", errno); + CHECK(st.st_uid == 0 && st.st_gid == 0, "owner %u:%u", st.st_uid, st.st_gid); + sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + CHECK(sock >= 0, "socket: %d", errno); + CHECK(!connect(sock, (const struct sockaddr *)&pod_addr, sizeof(pod_addr)), + "connect: %d", errno); + CHECK(write(sock, "p", 1) == 1, "write: %d", errno); + CHECK(read(sock, &c, 1) == 1 && c == 'q', "read '%c': %d", c, errno); +} + +/* The network. */ + +static bool has_cap(int cap) +{ + struct __user_cap_header_struct hdr = { .version = _LINUX_CAPABILITY_VERSION_3 }; + struct __user_cap_data_struct data[2] = {}; + + return !syscall(SYS_capget, &hdr, data) && (data[cap / 32].effective & (1U << (cap % 32))); +} + +static int dev_flags(const char *dev, int set, int clear) +{ + struct ifreq ifr = {}; + int s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0), was, r; + + strcpy(ifr.ifr_name, dev); + r = ioctl(s, SIOCGIFFLAGS, &ifr); + was = (unsigned short)ifr.ifr_flags; + if (!r && (set || clear)) { + ifr.ifr_flags = (was | set) & ~clear; + r = ioctl(s, SIOCSIFFLAGS, &ifr); + } + r = r ? -errno : was; + close(s); + return r; +} + +static int dev_set_addr(const char *dev, unsigned int ip) +{ + struct ifreq ifr = {}; + struct sockaddr_in *sin = (void *)&ifr.ifr_addr; + int s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0), r; + + strcpy(ifr.ifr_name, dev); + sin->sin_family = AF_INET; + sin->sin_addr.s_addr = __builtin_bswap32(ip); + r = ioctl(s, SIOCSIFADDR, &ifr) ? -errno : 0; + close(s); + return r; +} + +static int try_bind(int port) +{ + struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = __builtin_bswap16(port) }; + int s = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0), r; + + r = bind(s, (void *)&a, sizeof(a)) ? -errno : 0; + close(s); + return r; +} + +static int try_connect(unsigned int ip, int port) +{ + struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = __builtin_bswap16(port), + .sin_addr = { __builtin_bswap32(ip) } }; + int s = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + + if (connect(s, (void *)&a, sizeof(a))) { + int err = errno; + + close(s); + return -err; + } + return s; +} + +/* + * One request on a netlink socket. Returns what the kernel made of it. NLM_F_ECHO, which + * means nothing here, stands for a plain write(): whoever says where a message is to go + * knows what it is writing to, and whoever opened the socket is then not asked about. + */ +static int nl_poke_on(int s, int type, int flags, const void *body, size_t len) +{ + struct sockaddr_nl sa = { .nl_family = AF_NETLINK }; + bool plain = flags & NLM_F_ECHO; + struct { + struct nlmsghdr h; + char body[64]; + } req = { .h = { sizeof(req), type, NLM_F_REQUEST | NLM_F_ACK | (flags & ~NLM_F_ECHO) } }; + char buf[4096]; + struct nlmsghdr *h = (void *)buf; + int n; + + if (body) { + memcpy(req.body, body, len); + req.h.nlmsg_len = NLMSG_LENGTH(len); + } + if ((plain ? write(s, &req, req.h.nlmsg_len) : + sendto(s, &req, req.h.nlmsg_len, 0, (void *)&sa, sizeof(sa))) < 0) + return -errno; + n = recv(s, buf, sizeof(buf), 0); + if (n < (int)sizeof(*h)) + return -EPROTO; + return h->nlmsg_type == NLMSG_ERROR ? ((struct nlmsgerr *)NLMSG_DATA(h))->error : 0; +} + +static int nl_poke(int proto, int type, const void *body, size_t len) +{ + int s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, proto), r; + + if (s < 0) + return -errno; + r = nl_poke_on(s, type, 0, body, len); + close(s); + return r; +} + +/* Allowed or not is all that is asked: a request that is let through may well be nonsense. */ +#define MAY(right, r, refused, fmt, ...) \ + CHECK((rights & (right)) ? (r) != (refused) : (r) == (refused), \ + "%s " fmt ": %d", (rights & (right)) ? "refused" : "allowed", ##__VA_ARGS__, (int)(r)) + +/* + * What the cell may do to its network is what the builder named, @granted, and who in + * the cell may do it is whoever has the capability that it takes anywhere else. + */ +static void check_net_config(const char *dev, unsigned long long granted) +{ + static const struct { int type; unsigned long long right; } rtnl[] = { + { RTM_SETLINK, CELL_NET_LINK }, { RTM_DELLINK, CELL_NET_LINK_NEW }, + { RTM_NEWADDR, CELL_NET_ADDR }, { RTM_DELADDR, CELL_NET_ADDR }, + { RTM_NEWROUTE, CELL_NET_ROUTE }, { RTM_DELROUTE, CELL_NET_ROUTE }, + { RTM_NEWNEIGH, CELL_NET_ROUTE }, { RTM_NEWRULE, CELL_NET_ROUTE }, + { RTM_NEWQDISC, CELL_NET_TC }, { RTM_NEWTFILTER, CELL_NET_TC }, + /* Nobody has said what these are for. */ + { RTM_NEWNSID, 0 }, { RTM_NEWNETCONF, 0 }, { RTM_SETDCB, 0 }, + }; + struct ifinfomsg lo = { .ifi_index = 1 }, none = {}; + struct nfgenmsg nfg = { .nfgen_family = AF_INET }; + struct ifreq ifr = { .ifr_flags = IFF_TAP | IFF_NO_PI }; + unsigned long long rights = 0; + struct rtentry rt = {}; + socklen_t len = 84; + char info[84] = "filter"; + int one = 1, r, s; + unsigned int i; + + if (has_cap(CAP_NET_ADMIN)) + rights |= granted & ~CELL_NET_RAW; + if (has_cap(CAP_NET_RAW)) + rights |= granted & CELL_NET_RAW; + + for (i = 0; i < ARRAY_SIZE(rtnl); i++) { + r = nl_poke(NETLINK_ROUTE, rtnl[i].type, NULL, 0); + MAY(rtnl[i].right, r, -EPERM, "rtnetlink %d", rtnl[i].type); + } + /* The one request is for a device that is there and for one that is to be. */ + r = nl_poke(NETLINK_ROUTE, RTM_NEWLINK, &lo, sizeof(lo)); + MAY(CELL_NET_LINK, r, -EPERM, "RTM_NEWLINK for lo"); + s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + r = nl_poke_on(s, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL, &none, sizeof(none)); + MAY(CELL_NET_LINK_NEW, r, -EPERM, "RTM_NEWLINK for a new device"); + close(s); + + r = dev_flags(dev, IFF_UP, 0); + MAY(CELL_NET_LINK, r, -EPERM, "%s up", dev); + r = dev_flags(dev, 0, IFF_UP); + MAY(CELL_NET_LINK, r, -EPERM, "%s down", dev); + r = dev_set_addr(dev, 0x0a0900fe); + MAY(CELL_NET_ADDR, r, -EPERM, "address on %s", dev); + s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + r = ioctl(s, SIOCADDRT, &rt) ? -errno : 0; + MAY(CELL_NET_ROUTE, r, -EPERM, "SIOCADDRT"); + + r = setsockopt(s, SOL_SOCKET, SO_MARK, &one, sizeof(one)) ? -errno : 0; + MAY(CELL_NET_SOCKOPT | CELL_NET_RAW, r, -EPERM, "SO_MARK"); + r = setsockopt(s, IPPROTO_IP, IP_TRANSPARENT, &one, sizeof(one)) ? -errno : 0; + MAY(CELL_NET_SOCKOPT | CELL_NET_RAW, r, -EPERM, "IP_TRANSPARENT"); + + /* Both ways into netfilter, where it is built. */ + r = nl_poke(NETLINK_NETFILTER, NFNL_SUBSYS_NFTABLES << 8 | NFT_MSG_GETGEN, &nfg, + sizeof(nfg)); + if (r != -EPROTONOSUPPORT) { + MAY(CELL_NET_FILTER, r, -EPERM, "nftables"); + /* What these keep is not per network namespace. Not with any right. */ + r = nl_poke(NETLINK_NETFILTER, NFNL_SUBSYS_OSF << 8 | 0 /* OSF_MSG_ADD */, &nfg, + sizeof(nfg)); + CHECK(r == -EPERM || ((rights & CELL_NET_FILTER) && r == -EINVAL), "osf: %d", r); + r = nl_poke(NETLINK_NETFILTER, NFNL_SUBSYS_CTHELPER << 8 | 0 /* NEW */, &nfg, + sizeof(nfg)); + CHECK(r == -EPERM || ((rights & CELL_NET_FILTER) && r == -EINVAL), + "cthelper: %d", r); + } + r = getsockopt(s, IPPROTO_IP, 64 /* IPT_SO_GET_INFO */, info, &len) ? -errno : 0; + if (r != -ENOPROTOOPT) + MAY(CELL_NET_FILTER, r, -EPERM, "iptables"); + close(s); + + s = socket(AF_INET, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_ICMP); + MAY(CELL_NET_RAW, s < 0 ? -errno : 0, -EPERM, "raw socket"); + close(s); + s = socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC, 0); + MAY(CELL_NET_RAW, s < 0 ? -errno : 0, -EPERM, "packet socket"); + close(s); + + r = write_str("/proc/sys/net/ipv4/ip_forward", "1") ? -errno : 0; + MAY(CELL_NET_SYSCTL, r, -EACCES, "net sysctl"); + + s = open("/tun", O_RDWR | O_CLOEXEC); + if (s >= 0) { + strcpy(ifr.ifr_name, "tap%d"); + r = ioctl(s, TUNSETIFF, &ifr) ? -errno : 0; + MAY(CELL_NET_TUN, r, -EPERM, "TUNSETIFF"); + close(s); + } + + /* There is no right for these, so far or at all. */ + r = try_bind(80); + CHECK(r == -EACCES, "port 80: %d", r); + r = nl_poke(NETLINK_XFRM, 0x10 /* XFRM_MSG_NEWSA */, NULL, 0); + CHECK(r == -EPERM || r == -EPROTONOSUPPORT, "xfrm: %d", r); +} + +/* @arg: the rights in hex, then a device if it is not to be lo. */ +static void check_net_rights(const char *hex, const char *dev) +{ + unsigned long long granted = strtoull(hex, NULL, 16), caps = status_caps("CapPrm"); + int s; + + check_net_config(dev ?: "lo", granted); + + /* + * A request by netlink is also held against whoever opened the socket, lest + * somebody has another write to a socket of its own making. + */ + CHECK(!set_caps_to(0, caps), "capset: %d", errno); + s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + CHECK(!set_caps_to(caps, caps), "capset: %d", errno); + CHECK(nl_poke_on(s, RTM_SETLINK, NLM_F_ECHO, NULL, 0) == -EPERM, "a socket opened without"); + close(s); + /* It does tell: the same on a socket of its own. */ + if ((granted & CELL_NET_LINK) && has_cap(CAP_NET_ADMIN)) { + s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + CHECK(nl_poke_on(s, RTM_SETLINK, NLM_F_ECHO, NULL, 0) != -EPERM, "its own socket"); + close(s); + } + + /* Who lets go of the capability is like anybody else in here. */ + CHECK(!set_caps(0), "capset: %d", errno); + check_net_config(dev ?: "lo", granted); + CHECK(!has_cap(CAP_NET_ADMIN) && !has_cap(CAP_NET_RAW), "still has them"); +} + +/* + * The rights are over the network of the cell. Files 3 and 4 are an inet and a + * rtnetlink socket that somebody made in another one. + */ +static void check_net_foreign(void) +{ + struct ifreq ifr = { .ifr_name = "lo", .ifr_flags = IFF_UP }; + int one = 1; + + CHECK_ERRNO(setsockopt(3, SOL_SOCKET, SO_MARK, &one, sizeof(one)), EPERM); + CHECK_ERRNO(ioctl(3, SIOCSIFFLAGS, &ifr), EPERM); + CHECK(nl_poke_on(4, RTM_SETLINK, 0, NULL, 0) == -EPERM, "RTM_SETLINK elsewhere"); + CHECK(nl_poke_on(4, RTM_NEWADDR, 0, NULL, 0) == -EPERM, "RTM_NEWADDR elsewhere"); + /* They do work at home. */ + check_net_config("lo", ~0ULL); +} + +/* Nobody has set it up: there is a loopback device that is down, and that is that. */ +static void check_net_bare(void) +{ + struct if_nameindex *ni = if_nameindex(); + int r; + + CHECK(ni && ni[0].if_name && !strcmp(ni[0].if_name, "lo") && !ni[1].if_index, + "more than lo"); + r = dev_flags("lo", 0, 0); + CHECK(r >= 0 && !(r & IFF_UP), "lo: %x", r); + r = try_connect(INADDR_LOOPBACK, 7777); + CHECK(r == -ENETUNREACH, "connect: %d", r); + check_net_config("lo", 0); +} + +/* + * The slot was left empty, so there is no network: no socket but a unix socket, + * and nothing to be learned about a network through any socket. File descriptor + * 3 is a socket of the host's that listens on its loopback device. What is handed + * to the cell works. + */ +static void check_net_none(void) +{ + static const int families[] = { + AF_INET, AF_INET6, AF_NETLINK, AF_PACKET, AF_ALG, AF_VSOCK, AF_KEY, AF_XDP, + AF_BLUETOOTH, AF_CAN, AF_TIPC, + }; + static const unsigned long about_the_net[] = { + SIOCGIFCONF, SIOCGIFFLAGS, SIOCSIFFLAGS, SIOCGIFADDR, SIOCSIFADDR, SIOCGIFINDEX, + SIOCGIFNAME, SIOCGIFHWADDR, SIOCADDRT, SIOCDELRT, SIOCGARP, SIOCGIFBR, + SIOCBRADDBR, SIOCGIFVLAN, SIOCDEVPRIVATE, SIOCGSKNS, 0x8B01 /* SIOCGIWNAME */, + }; + struct sockaddr_un addr = { .sun_family = AF_UNIX, .sun_path = "\0cell-test" }; + int sp[2], socks[2], s, l, n, pending; + unsigned int i, j; + char c = 0; + + for (i = 0; i < ARRAY_SIZE(families); i++) { + CHECK_ERRNO(socket(families[i], SOCK_DGRAM | SOCK_CLOEXEC, 0), EAFNOSUPPORT); + CHECK_ERRNO(socket(families[i], SOCK_STREAM | SOCK_CLOEXEC, 0), EAFNOSUPPORT); + CHECK_ERRNO(socket(families[i], SOCK_RAW | SOCK_CLOEXEC, 0), EAFNOSUPPORT); + CHECK_ERRNO(socketpair(families[i], SOCK_STREAM, 0, sp), EAFNOSUPPORT); + } + CHECK(!if_nameindex(), "there are devices to be listed"); + + /* Unix sockets are not the network. */ + CHECK(!socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sp), "socketpair: %d", errno); + l = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + CHECK(l >= 0 && !bind(l, (void *)&addr, sizeof(sa_family_t) + 10) && !listen(l, 1), + "an abstract unix socket: %d", errno); + s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + CHECK(!connect(s, (void *)&addr, sizeof(sa_family_t) + 10), "connect to it: %d", errno); + + /* What was handed over works, new sockets that come of it included. */ + n = accept4(3, NULL, NULL, SOCK_CLOEXEC); + CHECK(n >= 0, "accept: %d", errno); + CHECK(read(n, &c, 1) == 1 && c == 'h' && write(n, "c", 1) == 1, + "talk to the host: '%c', %d", c, errno); + + /* About the socket: fine. About the network it is in: there is none. */ + socks[0] = s; + socks[1] = n; + for (j = 0; j < ARRAY_SIZE(socks); j++) { + CHECK(!ioctl(socks[j], FIONREAD, &pending), "FIONREAD on %u: %d", j, errno); + CHECK(!ioctl(socks[j], SIOCGPGRP, &pending), "SIOCGPGRP on %u: %d", j, errno); + /* In among those that are about devices, by its number. */ + if (j) + CHECK(!ioctl(socks[j], SIOCOUTQNSD, &pending), "SIOCOUTQNSD: %d", errno); + for (i = 0; i < ARRAY_SIZE(about_the_net); i++) { + struct ifreq ifr = { .ifr_name = "lo" }; + + errno = 0; + CHECK(ioctl(socks[j], about_the_net[i], &ifr) == -1 && errno == ENODEV, + "ioctl %#lx on %u: %d", about_the_net[i], j, errno); + } + } +} + +/* "new", and whoever built the cell has brought lo up before sealing it. */ +static void check_net_lo(void) +{ + int r = dev_flags("lo", 0, 0); + + CHECK(r >= 0 && (r & IFF_UP), "lo: %x", r); + r = try_connect(INADDR_LOOPBACK, 7777); + CHECK(r == -ECONNREFUSED, "connect: %d", r); + check_net_config("lo", 0); +} + +/* + * One end of a veth pair, with an address. The host listens at the other end. + * File descriptors 3 and 4 are for telling the host to add an address meanwhile. + */ +static void check_net_given(void) +{ + char c = 0; + int s; + + s = try_connect(0x0a090001, 7777); + CHECK(s >= 0, "connect to the host: %d", s); + CHECK(write(s, "c", 1) == 1 && read(s, &c, 1) == 1 && c == 'h', + "talk to the host: '%c', %d", c, errno); + close(s); + CHECK(!try_bind(8080), "port 8080"); + + CHECK(try_connect(0x0a090003, 9) == -ENETUNREACH || + try_connect(0x0a090003, 9) == -EHOSTUNREACH, "the address is there already"); + CHECK(write(3, "a", 1) == 1 && read(4, &c, 1) == 1, "handshake: %d", errno); + s = try_connect(0x0a090003, 9); + CHECK(s == -ECONNREFUSED, "the address that was added from the outside: %d", s); + + check_net_config("vc0", 0); +} + +/* Low ports and ping are settings of the network namespace. */ +static void check_net_settings(const char *when) +{ + struct sockaddr_in lo = { .sin_family = AF_INET, + .sin_addr = { __builtin_bswap32(INADDR_LOOPBACK) } }; + struct { + unsigned char type, code; + unsigned short sum, id, seq; + } echo = { .type = 8 }, reply = { .type = 0xff }; + bool after = !strcmp(when, "after"); + int r, s; + + r = try_bind(80); + CHECK(after ? !r : r == -EACCES, "port 80: %d", r); + s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, IPPROTO_ICMP); + CHECK(after ? s >= 0 : (s == -1 && errno == EACCES), "ping socket: %d, %d", s, errno); + if (s >= 0) { + CHECK(sendto(s, &echo, sizeof(echo), 0, (void *)&lo, sizeof(lo)) == sizeof(echo), + "echo: %d", errno); + CHECK(recv(s, &reply, sizeof(reply), 0) == sizeof(reply) && !reply.type, + "echo reply: type %d, %d", reply.type, errno); + close(s); + } + /* They are not the cell's to change back. */ + CHECK_ERRNO(write_str("/proc/sys/net/ipv4/ip_unprivileged_port_start", "1024"), EACCES); + CHECK_ERRNO(write_str("/proc/sys/net/ipv4/ping_group_range", "1 0"), EACCES); +} + +/* Is there a network at all? */ +static void check_net_at_all(const char *want) +{ + int s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + + if (!strcmp(want, "yes")) + CHECK(s >= 0, "socket: %d", errno); + else + CHECK(s == -1 && errno == EAFNOSUPPORT, "socket: %d, %d", s, errno); +} + +/* The id that the network namespace of @dev has for the one its other end is in. */ +static int link_netnsid(const char *dev) +{ + struct sockaddr_nl sa = { .nl_family = AF_NETLINK }; + struct { + struct nlmsghdr h; + struct ifinfomsg ifi; + } req = { + .h = { sizeof(req), RTM_GETLINK, NLM_F_REQUEST }, + .ifi = { .ifi_index = if_nametoindex(dev) }, + }; + int s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE), n, left; + struct nlmsghdr *h; + struct rtattr *a; + char buf[8192]; + + if (s < 0 || sendto(s, &req, sizeof(req), 0, (void *)&sa, sizeof(sa)) < 0) + return -errno; + n = recv(s, buf, sizeof(buf), 0); + close(s); + h = (void *)buf; + if (n < (int)sizeof(*h) || h->nlmsg_type != RTM_NEWLINK) + return -EPROTO; + left = h->nlmsg_len - NLMSG_LENGTH(sizeof(req.ifi)); + for (a = IFLA_RTA(NLMSG_DATA(h)); RTA_OK(a, left); a = RTA_NEXT(a, left)) { + if (a->rta_type == IFLA_LINK_NETNSID) + return *(int *)RTA_DATA(a); + } + return -ENOENT; +} + +/* + * A veth device whose peer is elsewhere gives the network namespace it is in an id for + * the other one, and a request can name that as the place for a device to go to, to be + * made in, or to be linked to. @dev is such a device, and the other end is the host's. + */ +static void check_net_no_way_out(const char *dev) +{ + struct { + struct ifinfomsg ifi; + struct nlattr a; + int nsid; + struct nlattr n; + char name[8]; + struct nlattr li; + struct nlattr k; + char kind[8]; + } req = { + .a = { NLA_HDRLEN + 4, IFLA_TARGET_NETNSID }, + .n = { NLA_HDRLEN + 5, IFLA_IFNAME }, .name = "esc0", + .li = { 2 * NLA_HDRLEN + 8, IFLA_LINKINFO | NLA_F_NESTED }, + .k = { NLA_HDRLEN + 5, IFLA_INFO_KIND }, .kind = "veth", + }; + int s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE), r, nsid; + + /* Anybody can look it up. Without one, what follows would say nothing. */ + nsid = link_netnsid(dev); + CHECK(nsid >= 0, "no id for where the other end of %s is: %d", dev, nsid); + if (failures) + return; + + req.nsid = nsid; + r = nl_poke_on(s, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL, &req, sizeof(req)); + CHECK(r == -EPERM, "a device made elsewhere: %d", r); + req.nsid = nsid + 77; + r = nl_poke_on(s, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL, &req, sizeof(req)); + CHECK(r == -EINVAL, "an id that there is not: %d", r); + req.nsid = nsid; + + req.a.nla_type = IFLA_LINK_NETNSID; + r = nl_poke_on(s, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL, &req, sizeof(req)); + CHECK(r == -EPERM, "a device linked to elsewhere: %d", r); + + req.a.nla_type = IFLA_TARGET_NETNSID; + req.ifi.ifi_index = if_nametoindex(dev); + r = nl_poke_on(s, RTM_SETLINK, 0, &req, offsetof(typeof(req), n)); + CHECK(r == -EPERM, "%s moved elsewhere: %d", dev, r); + CHECK(if_nametoindex(dev), "%s is gone", dev); + close(s); + + /* At home all of that works. */ + req.a.nla_type = IFLA_UNSPEC; + req.ifi.ifi_index = 0; + s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + r = nl_poke_on(s, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL, &req, sizeof(req)); + CHECK(!r && if_nametoindex("esc0"), "a device made at home: %d", r); + close(s); +} + +/* Attaches to a tap device, or makes it. With an owner or a group it is made to stay. */ +static int tun_attach_as(const char *dev, int owner, int group) +{ + struct ifreq ifr = { .ifr_flags = IFF_TAP | IFF_NO_PI }; + int fd = open("/bound", O_RDWR | O_CLOEXEC), r; + + if (fd < 0) + fd = open("/tun", O_RDWR | O_CLOEXEC); + if (fd < 0) + return -errno; + strcpy(ifr.ifr_name, dev); + r = ioctl(fd, TUNSETIFF, &ifr) ? -errno : 0; + if (!r && owner >= 0 && ioctl(fd, TUNSETOWNER, owner)) + r = -errno; + if (!r && group >= 0 && ioctl(fd, TUNSETGROUP, group)) + r = -errno; + if (!r && (owner >= 0 || group >= 0) && ioctl(fd, TUNSETPERSIST, 1)) + r = -errno; + close(fd); + return r; +} + +static int tun_attach(const char *dev) +{ + return tun_attach_as(dev, -1, -1); +} + +/* + * A tun device can have an owner and a group, who may attach to it without further ado. + * Those of tapu and tapg are the uid and the gid of whoever made this cell, which are also + * what a cell of one id runs on. They mean the maker. tapn has neither, which takes what + * it takes to make one. + */ +static void check_tun_owner(void) +{ + CHECK(tun_attach("tapu") == -EPERM, "the maker's, by uid: %d", tun_attach("tapu")); + CHECK(tun_attach("tapg") == -EPERM, "the maker's, by gid: %d", tun_attach("tapg")); + CHECK(tun_attach("tapn") == -EPERM, "nobody's: %d", tun_attach("tapn")); +} + +/* + * Within a cell an owner is what it is anywhere. This one may make tun devices, and makes + * tapc for its uid 1000 and tapd for its gid 1000. taph was made on the outside, for the + * very same kuid, and that means nothing in here: it takes the right, as for a device of + * anybody else. + */ +static void check_tun_owner_within(void) +{ + int status; + pid_t pid; + + CHECK(!tun_attach_as("tapc", 1000, -1), "making one for uid 1000: %d", errno); + CHECK(!tun_attach_as("tapd", -1, 1000), "making one for gid 1000: %d", errno); + CHECK(!tun_attach("taph"), "with the right"); + if (failures) + return; + + pid = fork(); + if (!pid) { + if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000)) + _exit(1); + if (has_cap(CAP_NET_ADMIN)) + _exit(2); + if (tun_attach("tapc")) + _exit(3); + if (tun_attach("tapd")) + _exit(5); + _exit(tun_attach("taph") == -EPERM ? 0 : 4); + } + CHECK(waitpid(pid, &status, 0) == pid && WIFEXITED(status) && !WEXITSTATUS(status), + "as uid 1000: %x", status); +} + +static void pause_for_good(void) +{ + for (;;) + pause(); +} + +static void fork_for_good(void) +{ + for (;;) + fork(); +} + +#define HELPER_RAN "/tmp/cell_test_helper_ran" + +/* Run by the kernel, on the host: as modprobe, as request-key, or for a core dump. */ +static int note_helper(int argc, char **argv) +{ + int fd = open(HELPER_RAN, O_CREAT | O_WRONLY | O_APPEND, 0600), i; + + for (i = 0; i < argc; i++) + dprintf(fd, "%s ", argv[i]); + dprintf(fd, "\n"); + close(fd); + return 1; +} + +/* What makes the kernel run a program, as root and outside of every namespace. */ +static void check_helpers(void) +{ + /* A protocol that nobody has: the kernel goes looking for a module. */ + CHECK_ERRNO(socket(AF_ECONET, SOCK_DGRAM, 0), EAFNOSUPPORT); + /* A key that is not there, and something to tell whoever is to make it. */ + CHECK_ERRNO(syscall(SYS_request_key, "user", "cell:test", "make it", + KEY_SPEC_PROCESS_KEYRING), EPERM); + if (failures) + return; + /* And a crash. A real one: an init does not take a signal from itself. */ + __builtin_trap(); +} + +int main(int argc, char **argv) +{ + const char *what = argc > 1 ? argv[1] : ""; + + if (!strcmp(what, "-q") || !strcmp(what, "core") || strstr(argv[0], "request-key")) + return note_helper(argc, argv); + if (!strcmp(what, "basic")) + check_basic(); + else if (!strcmp(what, "helpers")) + check_helpers(); + else if (!strcmp(what, "caps")) + check_caps(argv); + else if (!strcmp(what, "membership")) + check_membership(); + else if (!strcmp(what, "setid")) + check_setid(); + else if (!strcmp(what, "unix")) + check_unix(); + else if (!strcmp(what, "dirfd")) + check_dirfd(); + else if (!strcmp(what, "oneid")) + check_oneid(argv[2]); + else if (!strcmp(what, "pause")) + pause_for_good(); + else if (!strcmp(what, "usr1")) + wait_for_usr1(); + else if (!strcmp(what, "seccomp")) + check_seccomp(); + else if (!strcmp(what, "signals")) + check_signals(); + else if (!strcmp(what, "forkbomb")) + fork_for_good(); + else if (!strcmp(what, "exit")) + return atoi(argv[2]); + else if (!strcmp(what, "in-cgroup")) + check_in_cgroup(argv[2]); + else if (!strcmp(what, "cg-manage")) + check_cg_manage(argv[2]); + else if (!strcmp(what, "cg-own")) + check_cg_own(); + else if (!strcmp(what, "cg-churn")) + cg_churn(); + else if (!strcmp(what, "cg-freeze")) + cg_freeze(); + else if (!strcmp(what, "opens")) { + int fd = open(argv[2], O_RDWR); + + if (!strcmp(argv[3], "yes")) + CHECK(fd >= 0, "%s: %d", argv[2], errno); + else + CHECK(fd == -1 && errno == EACCES, "%s: %d, %d", argv[2], fd, errno); + } else if (!strcmp(what, "exists")) + CHECK(!access(argv[2], F_OK), "%s: %d", argv[2], errno); + else if (!strcmp(what, "copy-up")) + check_copy_up(); + else if (!strcmp(what, "vhost")) + check_vhost(); + else if (!strcmp(what, "data")) + check_data(argv[2]); + else if (!strcmp(what, "second")) + check_second(); + else if (!strcmp(what, "outside")) + check_outside_the_budget(); + else if (!strcmp(what, "net-none")) + check_net_none(); + else if (!strcmp(what, "net-at-all")) + check_net_at_all(argv[2]); + else if (!strcmp(what, "net-bare")) + check_net_bare(); + else if (!strcmp(what, "net-lo")) + check_net_lo(); + else if (!strcmp(what, "net-given")) + check_net_given(); + else if (!strcmp(what, "net-settings")) + check_net_settings(argv[2]); + else if (!strcmp(what, "net-rights")) + check_net_rights(argv[2], argv[3]); + else if (!strcmp(what, "net-foreign")) + check_net_foreign(); + else if (!strcmp(what, "tun-owner")) + check_tun_owner(); + else if (!strcmp(what, "tun-owner-within")) + check_tun_owner_within(); + else if (!strcmp(what, "net-no-way-out")) + check_net_no_way_out(argv[2]); + else if (!strcmp(what, "legacy")) + check_legacy_hierarchy(); + else if (!strcmp(what, "outside-own")) + check_outside_but_its_own(); + else if (!strcmp(what, "peer-race")) + check_peer_race(); + else if (!strcmp(what, "pod-serve")) + pod_serve(); + else if (!strcmp(what, "pod-connect")) + pod_connect(); + else if (!strcmp(what, "uid")) + return ((int)getuid() == atoi(argv[2]) && (int)getgid() == atoi(argv[3])) ? 0 : 1; + else + return 99; + + return failures ? 1 : 0; +} diff --git a/tools/testing/selftests/cell/cell_ids_test.c b/tools/testing/selftests/cell/cell_ids_test.c new file mode 100644 index 000000000000..4178c80f8f63 --- /dev/null +++ b/tools/testing/selftests/cell/cell_ids_test.c @@ -0,0 +1,762 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Tests for cells, ids: the window, the blocks, and that nothing a cell writes + * imparts privilege. + */ +#include "cell_test.h" + +/* Root does not get a cell on kuid 0, least of all by default. */ +TEST_F(cell, no_cell_on_uid_0) +{ + EXPECT_EQ(cell_build(&self->c, &(struct build){ .ids = 1 }), 4); + EXPECT_EQ(errno, EPERM); + cell_cleanup(&self->c); + EXPECT_EQ(cell_build(&self->c, &(struct build){ .ids = 0 }), 18); + EXPECT_EQ(errno, EPERM); +} + +/* The window is the kernel's. A one-id cell is not made of an id in it. */ +TEST_F(cell, no_cell_on_an_id_of_the_window) +{ + int status; + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + unsigned int ids = 1; + int cfd; + + if (setresuid(CELL_WINDOW_FIRST + 0x1234, CELL_WINDOW_FIRST + 0x1234, -1)) + _exit(1); + cfd = cell_create(CELL_SELF, 0); + if (cfd < 0) + _exit(2); + if (cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)) != -1 || + errno != EPERM) + _exit(3); + _exit(0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); +} + +TEST_F(cell, window_is_the_kernels) +{ + char path[64]; + int sync[2]; + pid_t pid; + char c; + + ASSERT_EQ(pipe(sync), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + if (unshare(CLONE_NEWUSER)) + _exit(1); + if (write(sync[1], "x", 1) != 1) + _exit(1); + pause(); + _exit(0); + } + ASSERT_EQ(read(sync[0], &c, 1), 1); + + snprintf(path, sizeof(path), "/proc/%d/uid_map", pid); + EXPECT_EQ(write_file(path, "0 2147483648 1"), -1); + EXPECT_EQ(errno, EPERM); + EXPECT_EQ(write_file(path, "0 2147483000 1000"), -1); + EXPECT_EQ(errno, EPERM); + EXPECT_EQ(write_file(path, "0 4294900000 10"), -1); + EXPECT_EQ(errno, EPERM); + EXPECT_EQ(write_file(path, "0 2147483000 648"), 0); + snprintf(path, sizeof(path), "/proc/%d/gid_map", pid); + EXPECT_EQ(write_file(path, "0 3000000000 1"), -1); + EXPECT_EQ(errno, EPERM); + + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); +} + +static int set_option(int fs, const char *key, const char *val) +{ + return sys_fsconfig(fs, FSCONFIG_SET_STRING, key, val, 0); +} + +/* Nor is it for a mount option to put one on the files of a filesystem. */ +TEST_F(cell, no_mount_option_names_an_id_of_the_window) +{ + static const char * const types[] = { "tmpfs", "bpf", "devpts", "hugetlbfs" }; + static const char * const keys[] = { "uid", "gid" }; + unsigned int t, k, seen = 0; + + for (t = 0; t < ARRAY_SIZE(types); t++) { + int fs = sys_fsopen(types[t], FSOPEN_CLOEXEC); + + if (fs < 0) + continue; + seen++; + for (k = 0; k < ARRAY_SIZE(keys); k++) { + TH_LOG("%s %s", types[t], keys[k]); + EXPECT_EQ(set_option(fs, keys[k], "2147483648"), -1); + EXPECT_EQ(errno, EINVAL); + EXPECT_EQ(set_option(fs, keys[k], "4294901759"), -1); + EXPECT_EQ(errno, EINVAL); + EXPECT_EQ(set_option(fs, keys[k], "2147483647"), 0); + } + close(fs); + } + EXPECT_GE(seen, 2); +} + +/* Is anything below @dirfd owned by an id of the window? */ +static int window_ids_below(int dirfd) +{ + struct dirent *e; + struct stat st; + int n = 0, sub; + DIR *d; + + d = fdopendir(openat(dirfd, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC)); + if (!d) + return -1; + while ((e = readdir(d))) { + if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) + continue; + if (fstatat(dirfd, e->d_name, &st, AT_SYMLINK_NOFOLLOW)) + continue; + if (st.st_uid >= CELL_WINDOW_FIRST || st.st_gid >= CELL_WINDOW_FIRST) + n++; + if (!S_ISDIR(st.st_mode)) + continue; + sub = openat(dirfd, e->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (sub >= 0) { + n += window_ids_below(sub); + close(sub); + } + } + closedir(d); + return n; +} + +static int run_in(struct cellctx *c, const char *what, const char *arg) +{ + int pidfd = spawn(c->cfd, NULL, what, arg, NULL), st; + + if (pidfd < 0) + return -99; + st = cell_wait(c->cfd); + close(pidfd); + return st; +} + +/* An overlay that is idmapped for the cell stores the ids on disk, copying up included. */ +TEST_F(cell, overlay_for_a_cell) +{ + struct stat st; + int r; + + r = cell_build(&self->c, &(struct build){ .ids = 65536, .overlay = OVL_IDMAPPED }); + if (r == 22 && sys_fsopen("overlay", FSOPEN_CLOEXEC) < 0) + SKIP(return, "no overlayfs"); + ASSERT_EQ(r, 0); + EXPECT_EQ(run_in(&self->c, "copy-up", NULL), 0); + ASSERT_EQ(fstatat(self->c.layers, "rw/upper/file", &st, 0), 0); + EXPECT_EQ(st.st_uid, 0); + EXPECT_EQ(st.st_gid, 0); + ASSERT_EQ(fstatat(self->c.layers, "rw/upper/new", &st, 0), 0); + EXPECT_EQ(st.st_uid, 0); + EXPECT_EQ(window_ids_below(self->c.layers), 0); +} + +/* + * A stacking filesystem stores with the credentials of whoever mounted it, so the + * rule goes by the value of the id. Here a file of the lower layer belongs to an + * id of the window, and the upper layer is as the host has it. + */ +TEST_F(cell, copy_up_stores_no_id_of_the_window) +{ + struct stat st; + int r, fd; + + r = cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true, + .overlay = OVL_LOWER_IDMAPPED }); + if (r == 22 && sys_fsopen("overlay", FSOPEN_CLOEXEC) < 0) + SKIP(return, "no overlayfs"); + ASSERT_EQ(r, 0); + ASSERT_EQ(fstatat(self->c.tree, "data/file", &st, 0), 0); + ASSERT_GE(st.st_uid, CELL_WINDOW_FIRST); + + fd = openat(self->c.tree, "data/file", O_WRONLY | O_CLOEXEC); + EXPECT_EQ(fd, -1); + EXPECT_EQ(errno, EOVERFLOW); + EXPECT_EQ(fstatat(self->c.layers, "rw/upper/file", &st, 0), -1); + EXPECT_EQ(window_ids_below(self->c.layers), 0); + + /* No cell is made of that to begin with. */ + EXPECT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), -1); + EXPECT_EQ(errno, EOVERFLOW); +} + +/* Does a task of the cell with these ids get to open @path? */ +static int opens(struct cellctx *c, const char *path, unsigned int id, unsigned int group, + const char *want) +{ + struct pidfd_cred cred = { .uid = id, .gid = id, .ngroups = !!group, + .groups = (unsigned long)&group }; + int pidfd = prepare(c->cfd, "opens", path, want), st = -99; + + if (pidfd >= 0 && + !pidfd_configure(pidfd, PIDFD_SET_BINARY, "cred", &cred, sizeof(cred)) && + !pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0)) + st = task_wait(pidfd); + close(pidfd); + return st; +} + +#define KVM_GID 36 + +/* + * A device is its number. Whoever builds a cell makes a node for it in the tree of + * the cell, and who in the cell may open that is a matter of the ids on disk, which + * the idmapping of the tree makes the ids of the cell. So several cells use one + * device, each by a node of its own, and none of them has an id of the host's. + */ +TEST_F(cell, device_by_a_node_of_its_own) +{ + struct cellctx cells[2]; + struct stat st; + int init[2], i; + + for (i = 0; i < 2; i++) { + ASSERT_EQ(cell_build(&cells[i], &(struct build){ .ids = 65536 }), 0); + /* /dev/null, for want of a /dev/kvm. */ + ASSERT_EQ(mknodat(cells[i].raw, "kvm", S_IFCHR | 0660, makedev(1, 3)), 0); + ASSERT_EQ(fchownat(cells[i].raw, "kvm", 0, KVM_GID, 0), 0); + ASSERT_EQ(fchmodat(cells[i].raw, "kvm", 0660, 0), 0); + init[i] = spawn(cells[i].cfd, NULL, "pause", NULL, NULL); + ASSERT_GE(init[i], 0); + } + for (i = 0; i < 2; i++) { + EXPECT_EQ(opens(&cells[i], "/kvm", 0, 0, "yes"), 0); + EXPECT_EQ(opens(&cells[i], "/kvm", 1000, KVM_GID, "yes"), 0); + EXPECT_EQ(opens(&cells[i], "/kvm", 1000, 0, "no"), 0); + EXPECT_EQ(opens(&cells[i], "/kvm", 1000, KVM_GID + 1, "no"), 0); + /* Nothing of the window is on disk. */ + ASSERT_EQ(fstatat(cells[i].raw, "kvm", &st, 0), 0); + EXPECT_EQ(st.st_gid, KVM_GID); + close(init[i]); + cell_cleanup(&cells[i]); + } +} + +/* + * The node of the host's own /dev belongs to ids of the host, which nobody in a + * cell has, its root included. What is left is what the mode has for everybody. + */ +TEST_F(cell, device_by_the_node_of_the_host) +{ + struct stat was; + int init; + + ASSERT_EQ(stat("/dev/full", &was), 0); + ASSERT_EQ(chown("/dev/full", 0, KVM_GID), 0); + ASSERT_EQ(chmod("/dev/full", 0660), 0); + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, + .bind_file = "/dev/full" }), 0); + init = spawn(self->c.cfd, NULL, "pause", NULL, NULL); + ASSERT_GE(init, 0); + + EXPECT_EQ(opens(&self->c, "/bound", 0, 0, "no"), 0); + EXPECT_EQ(opens(&self->c, "/bound", 1000, KVM_GID, "no"), 0); + ASSERT_EQ(chmod("/dev/full", 0666), 0); + EXPECT_EQ(opens(&self->c, "/bound", 1000, 0, "yes"), 0); + + close(init); + EXPECT_EQ(chown("/dev/full", was.st_uid, was.st_gid), 0); + EXPECT_EQ(chmod("/dev/full", was.st_mode & 07777), 0); +} + +TEST_F(cell, blocks_do_not_overlap) +{ + struct cellctx other; + long a, b; + pid_t pid; + int p1, p2; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + ASSERT_EQ(cell_build(&other, &(struct build){ .ids = 65536 }), 0); + p1 = spawn(self->c.cfd, &pid, "pause", NULL, NULL); + ASSERT_GE(p1, 0); + a = host_uid(pid); + p2 = spawn(other.cfd, &pid, "pause", NULL, NULL); + ASSERT_GE(p2, 0); + b = host_uid(pid); + + EXPECT_GE(a, (long)CELL_WINDOW_FIRST); + EXPECT_LE(a, (long)CELL_WINDOW_LAST); + EXPECT_EQ(a % 65536, 0); + EXPECT_EQ(b % 65536, 0); + EXPECT_NE(a, b); + + close(p1); + close(p2); + cell_cleanup(&other); +} + +/* What cell_info() has for the first id of the block, or -1 if it has nothing. */ +static long ids_first(int cfd) +{ + struct cell_info info = { .mask = CELL_INFO_IDS_FIRST }; + + if (cell_info(cfd, &info, sizeof(info), 0)) + return -2; + return info.mask & CELL_INFO_IDS_FIRST ? (long)info.ids_first : -1; +} + +/* + * The kernel picks the block. Whoever builds the cell can ask which one it was, as + * soon as there is one, to say who in the cell is meant where a rule goes by an id: + * in a packet filter, in a routing rule. + */ +TEST_F(cell, builder_is_told_the_block) +{ + unsigned int ids = 65536, one = 1; + struct stat st; + int cfd, pidfd, status; + long first; + pid_t pid; + + cfd = cell_create(CELL_SELF, 0); + ASSERT_GE(cfd, 0); + EXPECT_EQ(ids_first(cfd), -1); + ASSERT_EQ(cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)), 0); + first = ids_first(cfd); + EXPECT_GE(first, (long)CELL_WINDOW_FIRST); + EXPECT_LE(first, (long)CELL_WINDOW_LAST); + EXPECT_EQ(first % 65536, 0); + + /* It is what everything else says, later on. */ + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + EXPECT_NE(ids_first(self->c.cfd), first); + first = ids_first(self->c.cfd); + ASSERT_EQ(stat(self->c.cg, &st), 0); + EXPECT_EQ((long)st.st_uid, first); + EXPECT_EQ((long)st.st_gid, first); + pidfd = spawn(self->c.cfd, &pid, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + EXPECT_EQ(host_uid(pid), first); + close(pidfd); + close(cfd); + + /* The block goes back with the cell. */ + ASSERT_EQ(cell_kill(self->c.cfd, 0), 0); + cell_wait(self->c.cfd); + EXPECT_EQ(ids_first(self->c.cfd), -1); + + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) || + setresuid(1000, 1000, 1000) || prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)) + _exit(1); + /* One id is no block, and the builder knows its own. */ + cfd = cell_create(CELL_SELF, 0); + if (cfd < 0 || cell_configure(cfd, CELL_SET_BINARY, "ids", &one, sizeof(one))) + _exit(2); + if (ids_first(cfd) != -1) + _exit(3); + close(cfd); + /* No privilege is needed to be told. */ + cfd = cell_create(CELL_SELF, 0); + if (cfd < 0 || cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids))) + _exit(4); + if (ids_first(cfd) < (long)CELL_WINDOW_FIRST) + _exit(5); + close(cfd); + /* But a name for it is: in here there is one for uid 1000 and no other. */ + if (unshare(CLONE_NEWUSER) || write_file("/proc/self/setgroups", "deny") || + write_file("/proc/self/uid_map", "1000 1000 1") || + write_file("/proc/self/gid_map", "1000 1000 1")) + _exit(6); + cfd = cell_create(CELL_SELF, 0); + if (cfd < 0 || cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids))) + _exit(7); + _exit(ids_first(cfd) == -1 ? 0 : 8); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); +} + +/* There are only so many blocks, and no one user gets them all. */ +TEST_F(cell, blocks_are_counted) +{ + unsigned int ids = 65536; + int status; + pid_t pid; + + ASSERT_EQ(write_file("/proc/sys/user/max_cell_id_blocks", "1"), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + int a, b; + + /* + * They are counted by user, and a block goes on counting until + * the user namespace it was lent to is freed, some time after + * the cell is dead. So as a user that never had one. + */ + if (setresgid(54321, 54321, 54321) || setresuid(54321, 54321, 54321)) + _exit(1); + a = cell_create(CELL_SELF, 0); + b = cell_create(CELL_SELF, 0); + if (a < 0 || b < 0) + _exit(2); + if (cell_configure(a, CELL_SET_BINARY, "ids", &ids, sizeof(ids))) + _exit(3); + if (cell_configure(b, CELL_SET_BINARY, "ids", &ids, sizeof(ids)) != -1 || + errno != ENOSPC) + _exit(4); + _exit(0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); + ASSERT_EQ(write_file("/proc/sys/user/max_cell_id_blocks", "4095"), 0); +} + +TEST_F(cell, ids_never_reach_a_disk) +{ + int pidfd, host, fd; + + /* A filesystem of the host's, wide open, and a way to it past the tree. */ + fd = new_tmpfs(); + ASSERT_GE(fd, 0); + host = openat(fd, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(host, 0); + close(fd); + ASSERT_EQ(fchmod(host, 0777), 0); + fd = openat(host, "world", O_CREAT | O_WRONLY, 0666); + ASSERT_GE(fd, 0); + close(fd); + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + pidfd = prepare(self->c.cfd, "dirfd", NULL, NULL); + ASSERT_GE(pidfd, 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, host), 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + + /* Nor by the hand of the host. */ + EXPECT_EQ(fchownat(host, "world", CELL_WINDOW_FIRST + 5, -1, 0), -1); + EXPECT_EQ(errno, EOVERFLOW); + EXPECT_EQ(fchownat(host, "world", -1, CELL_WINDOW_LAST, 0), -1); + EXPECT_EQ(errno, EOVERFLOW); + EXPECT_EQ(fchownat(host, "world", CELL_WINDOW_FIRST - 1, CELL_WINDOW_FIRST - 1, 0), 0); + close(host); + close(pidfd); +} + +/* A mount that was idmapped for a cell names its ids, so it holds on to them. */ +TEST_F(cell, idmapped_mount_pins_the_block) +{ + struct mount_attr attr = { .attr_set = MOUNT_ATTR_IDMAP }; + unsigned int ids = 65536; + int a, b, mnt, i, ret = -1; + + ASSERT_EQ(write_file("/proc/sys/user/max_cell_id_blocks", "1"), 0); + a = cell_create(CELL_SELF, 0); + ASSERT_GE(a, 0); + ASSERT_EQ(cell_configure(a, CELL_SET_BINARY, "ids", &ids, sizeof(ids)), 0); + mnt = new_tmpfs(); + ASSERT_GE(mnt, 0); + attr.userns_fd = a; + ASSERT_EQ(sys_mount_setattr(mnt, "", AT_EMPTY_PATH, &attr, sizeof(attr)), 0); + + /* The cell is gone. The mount is not. */ + ASSERT_EQ(cell_kill(a, 0), 0); + close(a); + usleep(500000); + b = cell_create(CELL_SELF, 0); + ASSERT_GE(b, 0); + EXPECT_EQ(cell_configure(b, CELL_SET_BINARY, "ids", &ids, sizeof(ids)), -1); + EXPECT_EQ(errno, ENOSPC); + + close(mnt); + for (i = 0; i < 100 && ret; i++) { + usleep(50000); + ret = cell_configure(b, CELL_SET_BINARY, "ids", &ids, sizeof(ids)); + } + EXPECT_EQ(ret, 0); + close(b); + ASSERT_EQ(write_file("/proc/sys/user/max_cell_id_blocks", "4095"), 0); +} + +/* Every capability there is but the one that no cell can have. */ +static __u64 all_caps(void) +{ + char buf[16] = {}; + int fd = open("/proc/sys/kernel/cap_last_cap", O_RDONLY); + + if (fd < 0 || read(fd, buf, sizeof(buf) - 1) <= 0) + return 0; + close(fd); + return ((2ULL << atoi(buf)) - 1) & ~CAP(SETFCAP); +} + +/* A program that is set-uid root and comes with cap_net_raw, for the cell to run. */ +static int add_gain(struct cellctx *c) +{ + int fd; + + if (copy_child(c->raw, "gain")) + return -1; + fd = openat(c->raw, "gain", O_RDONLY); + if (fd < 0 || fchmod(fd, 06755) || + fsetxattr(fd, "security.capability", fscap_v2, sizeof(fscap_v2), 0)) + return -1; + return close(fd); +} + +/* Being uid 0 of a cell comes with nothing. The builder names what it comes with. */ +TEST_F(cell, capabilities_are_what_the_builder_named) +{ + __u64 caps[] = { + 0, CAP(CHOWN), CAP(SETUID) | CAP(SETGID), CAP(SETPCAP) | CAP(CHOWN), + CAP(NET_RAW), all_caps(), + }; + struct cellctx c; + char hex[32]; + unsigned int i; + int init, pidfd; + + ASSERT_NE(caps[ARRAY_SIZE(caps) - 1], 0); + for (i = 0; i < ARRAY_SIZE(caps); i++) { + snprintf(hex, sizeof(hex), "%llx", caps[i]); + ASSERT_EQ(cell_build(&c, &(struct build){ .ids = 65536, .caps = caps[i] }), 0); + ASSERT_EQ(add_gain(&c), 0); + pidfd = spawn(c.cfd, NULL, "caps", hex, NULL); + ASSERT_GE(pidfd, 0); + EXPECT_EQ(cell_wait(c.cfd), 0) { + TH_LOG("with %s", hex); + } + close(pidfd); + cell_cleanup(&c); + } + + /* For every task that is started, not just the first. */ + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .caps = CAP(KILL) }), 0); + ASSERT_EQ(add_gain(&self->c), 0); + init = spawn(self->c.cfd, NULL, "pause", NULL, NULL); + ASSERT_GE(init, 0); + pidfd = spawn(self->c.cfd, NULL, "caps", "20", NULL); + ASSERT_GE(pidfd, 0); + EXPECT_EQ(task_wait(pidfd), 0); + close(pidfd); + close(init); +} + +TEST_F(cell, files_impart_no_privilege) +{ + struct stat st; + char buf[64]; + int fd; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .caps = CAP(CHOWN) }), 0); + fd = spawn(self->c.cfd, NULL, "setid", NULL, NULL); + ASSERT_GE(fd, 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + close(fd); + + /* And this is what the host finds. */ + ASSERT_EQ(fstatat(self->c.raw, "tmp/a", &st, 0), 0); + EXPECT_EQ(st.st_mode & (S_ISUID | S_ISGID), 0); + /* On disk the ids of the cell count from 0. No kuid of its block got there. */ + EXPECT_EQ(st.st_uid, 33); + ASSERT_EQ(fstatat(self->c.raw, "su", &st, 0), 0); + EXPECT_EQ(st.st_mode & 07777, 0755); + EXPECT_EQ(st.st_uid, 0); + fd = openat(self->c.raw, "su", O_RDONLY | O_CLOEXEC); + ASSERT_GE(fd, 0); + EXPECT_EQ(fgetxattr(fd, "security.capability", buf, sizeof(buf)), -1); + EXPECT_EQ(errno, ENODATA); + close(fd); + fd = openat(self->c.raw, "tmp/a", O_RDONLY | O_CLOEXEC); + ASSERT_GE(fd, 0); + EXPECT_EQ(fgetxattr(fd, "security.capability", buf, sizeof(buf)), -1); + EXPECT_EQ(errno, ENODATA); + close(fd); +} + +/* The kernel wrote the maps of a cell, once. Nobody writes them again. */ +TEST_F(cell, maps_are_the_kernels) +{ + static const char * const maps[] = { "uid_map", "gid_map" }; + char path[64]; + unsigned int i; + pid_t pid; + int p; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + p = spawn(self->c.cfd, &pid, "pause", NULL, NULL); + ASSERT_GE(p, 0); + for (i = 0; i < ARRAY_SIZE(maps); i++) { + snprintf(path, sizeof(path), "/proc/%d/%s", pid, maps[i]); + EXPECT_EQ(write_file(path, "0 0 1"), -1); + EXPECT_EQ(errno, EPERM); + EXPECT_EQ(write_file(path, "0 100000 65536"), -1); + EXPECT_EQ(errno, EPERM); + } + close(p); +} + +static int mount_source(const char *fstype, const char *source, const char *opt, + const char *val) +{ + int fs, mnt; + + fs = sys_fsopen(fstype, FSOPEN_CLOEXEC); + if (fs < 0) + return -1; + if (sys_fsconfig(fs, FSCONFIG_SET_STRING, "source", source, 0) || + (opt && sys_fsconfig(fs, FSCONFIG_SET_STRING, opt, val, 0)) || + sys_fsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) { + close(fs); + return -1; + } + mnt = sys_fsmount(fs, FSMOUNT_CLOEXEC, 0); + close(fs); + return mnt; +} + +static int make_su(int dirfd) +{ + int fd = openat(dirfd, "su", O_CREAT | O_WRONLY, 0755); + + if (fd < 0 || fchmod(fd, 06755)) + return -1; + close(fd); + return 0; +} + +/* + * Dropping the bits changes the inode, so on a frozen filesystem the open has to + * wait. Needs CELL_TEST_EXT4_IMG to name an ext4 image. + */ +TEST_F(cell, drop_privs_waits_for_thaw) +{ + const char *img = getenv("CELL_TEST_EXT4_IMG"); + int ctl, nr, loop, back, mnt, raw, pidfd; + struct pollfd pfd; + struct stat st; + char dev[32]; + + if (!img) + SKIP(return, "CELL_TEST_EXT4_IMG is not set"); + ctl = open("/dev/loop-control", O_RDWR | O_CLOEXEC); + if (ctl < 0) + SKIP(return, "no loop devices"); + nr = ioctl(ctl, LOOP_CTL_GET_FREE); + ASSERT_GE(nr, 0); + snprintf(dev, sizeof(dev), "/dev/loop%d", nr); + loop = open(dev, O_RDWR | O_CLOEXEC); + back = open(img, O_RDWR | O_CLOEXEC); + ASSERT_GE(loop, 0); + ASSERT_GE(back, 0); + ASSERT_EQ(ioctl(loop, LOOP_SET_FD, back), 0); + + mnt = mount_source("ext4", dev, NULL, NULL); + ASSERT_GE(mnt, 0); + ASSERT_EQ(make_su(mnt), 0); + raw = sys_open_tree(mnt, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(raw, 0); + raw = openat(raw, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(raw, 0); + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .data = mnt }), 0); + ASSERT_EQ(ioctl(raw, FIFREEZE, 0), 0); + + pidfd = spawn(self->c.cfd, NULL, "data", NULL, NULL); + ASSERT_GE(pidfd, 0); + pfd = (struct pollfd){ .fd = self->c.cfd, .events = POLLIN }; + EXPECT_EQ(poll(&pfd, 1, 1500), 0); + ASSERT_EQ(fstatat(raw, "su", &st, 0), 0); + EXPECT_EQ(st.st_mode & 07777, 06755); + + ASSERT_EQ(ioctl(raw, FITHAW, 0), 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + ASSERT_EQ(fstatat(raw, "su", &st, 0), 0); + EXPECT_EQ(st.st_mode & 07777, 0755); + + close(pidfd); + close(raw); + close(mnt); + cell_cleanup(&self->c); + self->c.cfd = self->c.tree = self->c.raw = self->c.cgfd = -1; + ioctl(loop, LOOP_CLR_FD, 0); + close(loop); + close(back); + close(ctl); +} + +/* + * A filesystem with ->atomic_open() opens a file it creates with the directory + * locked, a path tmpfs does not have. Needs CELL_TEST_9P_TAG to name a writable + * 9p share over virtio. + */ +TEST_F(cell, drop_privs_with_atomic_open) +{ + const char *tag = getenv("CELL_TEST_9P_TAG"); + int status; + pid_t pid; + + if (!tag) + SKIP(return, "CELL_TEST_9P_TAG is not set"); + + /* 9p cannot be idmapped, so a one-id cell, so not as root. */ + ASSERT_EQ(prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + struct __user_cap_header_struct hdr = { .version = _LINUX_CAPABILITY_VERSION_3 }; + struct __user_cap_data_struct data[2]; + struct cellctx c; + int mnt, pidfd, step; + + mnt = mount_source("9p", tag, "trans", "virtio"); + if (mnt < 0) + _exit(77); + if (fchmodat(mnt, "", 0777, AT_EMPTY_PATH) && errno != ENOTSUP && errno != EINVAL) + _exit(9); + if (chown(CGROOT, 1000, 1000) || chown(CGROOT "/cgroup.procs", 1000, 1000)) + _exit(10); + if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000)) + _exit(11); + if (syscall(SYS_capget, &hdr, data)) + _exit(12); + data[0].effective = data[0].permitted; + data[1].effective = data[1].permitted; + if (syscall(SYS_capset, &hdr, data)) + _exit(13); + unlinkat(mnt, "su", 0); + if (make_su(mnt)) + _exit(14); + + step = cell_build(&c, &(struct build){ .ids = 1, .data = mnt }); + if (step) + _exit(20 + step); + pidfd = spawn(c.cfd, NULL, "data", "create", NULL); + if (pidfd < 0) + _exit(15); + if (cell_wait(c.cfd)) + _exit(16); + cell_cleanup(&c); + _exit(0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(chown(CGROOT, 0, 0), 0); + EXPECT_EQ(chown(CGROOT "/cgroup.procs", 0, 0), 0); + if (WIFEXITED(status) && WEXITSTATUS(status) == 77) + SKIP(return, "cannot mount the 9p share"); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/cell/cell_life_test.c b/tools/testing/selftests/cell/cell_life_test.c new file mode 100644 index 000000000000..e00cbacebc49 --- /dev/null +++ b/tools/testing/selftests/cell/cell_life_test.c @@ -0,0 +1,446 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Tests for cells, the life of a cell: building one, killing one, handles, and + * the races between them. + */ +#include "cell_test.h" + +TEST_F(cell, life) +{ + struct cell_info info = { .mask = ~0ULL }; + struct pollfd pfd; + pid_t pid; + int pidfd; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0); + EXPECT_EQ(cell_state(self->c.cfd), CELL_BUILDING); + + /* Not before it is sealed. */ + EXPECT_EQ(pidfd_open_empty(self->c.cfd, 0), -1); + EXPECT_EQ(errno, EINVAL); + + ASSERT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), 0); + EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED); + + /* Sealed is sealed. */ + EXPECT_EQ(cell_configure(self->c.cfd, CELL_SET_STRING, "hostname", "x", 0), -1); + EXPECT_EQ(errno, EBUSY); + EXPECT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), -1); + EXPECT_EQ(errno, EBUSY); + EXPECT_EQ(cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0), -1); + EXPECT_EQ(errno, EBUSY); + + pidfd = spawn(self->c.cfd, &pid, "exit", "7", NULL); + ASSERT_GE(pidfd, 0); + + pfd = (struct pollfd){ .fd = self->c.cfd, .events = POLLIN }; + EXPECT_EQ(poll(&pfd, 1, 10000), 1); + EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD); + EXPECT_EQ(cell_wait(self->c.cfd), 7 << 8); + /* Repeatable. */ + EXPECT_EQ(cell_wait(self->c.cfd), 7 << 8); + + ASSERT_EQ(cell_info(self->c.cfd, &info, sizeof(info), 0), 0); + EXPECT_NE(info.id, 0); + EXPECT_EQ(info.parent_id, 0); + EXPECT_EQ(info.ids_count, 65536); + EXPECT_EQ(info.exit_status, 7 << 8); + EXPECT_NE(info.budget_cgroupid, 0); + /* All there is to know, but for the block of ids, which has gone back. */ + EXPECT_EQ(info.mask, info.supported_mask & ~CELL_INFO_IDS_FIRST); + + /* Nothing goes into a dead cell. */ + EXPECT_EQ(pidfd_open_empty(self->c.cfd, 0), -1); + EXPECT_EQ(errno, ESRCH); + EXPECT_EQ(cg_populated(self->c.cg), 0); + close(pidfd); +} + +/* A read of the cellfd waits for the end of the cell, if it may. */ +TEST_F(cell, read_waits_for_the_end) +{ + int pidfd, status = -1, fl; + pid_t pid; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + pidfd = spawn(self->c.cfd, NULL, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + + fl = fcntl(self->c.cfd, F_GETFL); + ASSERT_EQ(fcntl(self->c.cfd, F_SETFL, fl | O_NONBLOCK), 0); + EXPECT_EQ(read(self->c.cfd, &status, sizeof(status)), -1); + EXPECT_EQ(errno, EAGAIN); + ASSERT_EQ(fcntl(self->c.cfd, F_SETFL, fl), 0); + + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + usleep(200000); + _exit(!!cell_kill(self->c.cfd, 0)); + } + /* Not for ever, should it not come. */ + alarm(20); + EXPECT_EQ(read(self->c.cfd, &status, sizeof(status)), sizeof(status)); + alarm(0); + EXPECT_EQ(status, SIGKILL); + EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD); + waitpid(pid, NULL, 0); + close(pidfd); +} + +TEST_F(cell, bad_arguments) +{ + unsigned int ids; + char path[64]; + int cfd; + + EXPECT_EQ(cell_create(CELL_SELF, 1), -1); + EXPECT_EQ(errno, EINVAL); + EXPECT_EQ(cell_create(0, 0), -1); + EXPECT_EQ(errno, EINVAL); + + cfd = cell_create(CELL_SELF, 0); + ASSERT_GE(cfd, 0); + EXPECT_EQ(fcntl(cfd, F_GETFD) & FD_CLOEXEC, FD_CLOEXEC); + + /* A handle is passed on, not opened again. */ + snprintf(path, sizeof(path), "/proc/self/fd/%d", cfd); + EXPECT_EQ(open(path, O_RDONLY), -1); + EXPECT_EQ(errno, ENXIO); + EXPECT_EQ(open(path, O_RDWR), -1); + EXPECT_EQ(errno, EPERM); + + EXPECT_EQ(cell_configure(cfd, 99, NULL, NULL, 0), -1); + EXPECT_EQ(errno, EOPNOTSUPP); + EXPECT_EQ(cell_configure(cfd, CELL_SET_STRING, "nonsense", "x", 0), -1); + EXPECT_EQ(errno, EINVAL); + EXPECT_EQ(cell_configure(cfd, CELL_SET_FD, "root", NULL, cfd), -1); + EXPECT_EQ(errno, EINVAL); + EXPECT_EQ(cell_configure(0, CELL_CMD_SEAL, NULL, NULL, 0), -1); + EXPECT_EQ(errno, EINVAL); + ids = 1000; + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)), -1); + EXPECT_EQ(errno, EINVAL); + ids = 65536; + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)), 0); + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)), -1); + EXPECT_EQ(errno, EBUSY); + /* No root, no budget. */ + EXPECT_EQ(cell_configure(cfd, CELL_CMD_SEAL, NULL, NULL, 0), -1); + EXPECT_EQ(errno, EINVAL); + close(cfd); +} + +TEST_F(cell, kill_while_building) +{ + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0); + ASSERT_EQ(cell_kill(self->c.cfd, 0), 0); + EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD); + EXPECT_EQ(cell_wait(self->c.cfd), SIGKILL); + EXPECT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), -1); + EXPECT_EQ(errno, EBUSY); + /* Idempotent. */ + EXPECT_EQ(cell_kill(self->c.cfd, 0), 0); +} + +/* What cell_kill() takes, and that it works whatever state the cell is in. */ +TEST_F(cell, kill_in_every_state) +{ + struct pollfd pfd; + int fd, pidfd; + + EXPECT_EQ(cell_kill(-1, 0), -1); + EXPECT_EQ(errno, EBADF); + fd = open("/dev/null", O_RDONLY | O_CLOEXEC); + ASSERT_GE(fd, 0); + EXPECT_EQ(cell_kill(fd, 0), -1); + EXPECT_EQ(errno, EINVAL); + close(fd); + + /* Sealed, nothing spawned. */ + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED); + EXPECT_EQ(cell_kill(self->c.cfd, 1), -1); + EXPECT_EQ(errno, EINVAL); + EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED); + /* It is a system call of its own and no command of cell_configure(). */ + EXPECT_EQ(cell_configure(self->c.cfd, 5, NULL, NULL, 0), -1); + EXPECT_EQ(errno, EOPNOTSUPP); + EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED); + EXPECT_EQ(cell_kill(self->c.cfd, 0), 0); + EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD); + /* Dead. */ + EXPECT_EQ(cell_kill(self->c.cfd, 0), 0); + EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD); + cell_cleanup(&self->c); + + /* Running. */ + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + pidfd = spawn(self->c.cfd, NULL, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + EXPECT_EQ(cell_state(self->c.cfd), CELL_RUNNING); + EXPECT_EQ(cell_kill(self->c.cfd, 0), 0); + /* Dying, or dead already. It does not wait. */ + EXPECT_EQ(cell_kill(self->c.cfd, 0), 0); + pfd = (struct pollfd){ .fd = self->c.cfd, .events = POLLIN }; + EXPECT_EQ(poll(&pfd, 1, 10000), 1); + EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD); + close(pidfd); +} + +TEST_F(cell, kill_fork_bomb) +{ + int pidfd; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + ASSERT_EQ(write_file(({ static char p[192]; + snprintf(p, sizeof(p), "%s/pids.max", self->c.cg); p; }), + "200") == 0 || errno == ENOENT, 1); + pidfd = spawn(self->c.cfd, NULL, "forkbomb", NULL, NULL); + ASSERT_GE(pidfd, 0); + usleep(300000); + EXPECT_EQ(cell_state(self->c.cfd), CELL_RUNNING); + + ASSERT_EQ(cell_kill(self->c.cfd, 0), 0); + EXPECT_EQ(cell_wait(self->c.cfd), SIGKILL); + /* DEAD means gone. */ + EXPECT_EQ(cg_populated(self->c.cg), 0); + close(pidfd); +} + +/* A cell lives for as long as somebody holds a handle on it, and no longer. */ +TEST_F(cell, dies_with_its_last_handle) +{ + struct pollfd pfd; + int pidfd, second; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + pidfd = spawn(self->c.cfd, NULL, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + pfd = (struct pollfd){ .fd = pidfd, .events = POLLIN }; + + /* Any handle will do, so a cell can be handed on. */ + second = dup(self->c.cfd); + ASSERT_GE(second, 0); + close(self->c.cfd); + self->c.cfd = -1; + EXPECT_EQ(poll(&pfd, 1, 300), 0); + + close(second); + EXPECT_EQ(poll(&pfd, 1, 10000), 1); + close(pidfd); +} + +/* Whoever made a cell is killed, and had told nobody about it. */ +TEST_F(cell, dies_with_its_maker) +{ + char cg[sizeof(self->c.cg)] = {}; + int p[2], i, populated = -1; + pid_t pid; + + ASSERT_EQ(pipe(p), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + struct build b = { .ids = 65536 }; + struct cellctx c; + + if (cell_build(&c, &b) || + spawn(c.cfd, NULL, "pause", NULL, NULL) < 0 || + write(p[1], c.cg, sizeof(c.cg)) != sizeof(c.cg)) + _exit(1); + for (;;) + pause(); + } + close(p[1]); + ASSERT_EQ(read(p[0], cg, sizeof(cg)), sizeof(cg)); + close(p[0]); + EXPECT_EQ(cg_populated(cg), 1); + + ASSERT_EQ(kill(pid, SIGKILL), 0); + ASSERT_EQ(waitpid(pid, NULL, 0), pid); + for (i = 0; i < 200 && populated; i++) { + usleep(50000); + populated = cg_populated(cg); + } + EXPECT_EQ(populated, 0); + for (i = 0; i < 100 && rmdir(cg) && errno == EBUSY; i++) + usleep(20000); +} + +/* The name is taken, and there is nothing to fill the slot with yet. */ +TEST_F(cell, spawn_slot_is_reserved) +{ + unsigned int one = 1; + int cfd; + + cfd = cell_create(CELL_SELF, 0); + ASSERT_GE(cfd, 0); + EXPECT_EQ(cell_configure(cfd, CELL_SET_FLAG, "spawn", NULL, 0), -1); + EXPECT_EQ(errno, EOPNOTSUPP); + EXPECT_EQ(cell_configure(cfd, CELL_SET_STRING, "spawn", "yes", 0), -1); + EXPECT_EQ(errno, EOPNOTSUPP); + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "spawn", &one, sizeof(one)), -1); + EXPECT_EQ(errno, EOPNOTSUPP); + EXPECT_EQ(cell_configure(cfd, CELL_SET_FD, "spawn", NULL, cfd), -1); + EXPECT_EQ(errno, EOPNOTSUPP); + close(cfd); +} + +/* + * DEAD means gone. The init of a cell is still on its way out when the kernel + * learns of its exit, and the cell must not be reported dead before it is off + * the cpu for good: that is when it stops counting for cgroup.events. + */ +TEST_F(cell, dead_means_gone) +{ + int i, pidfd, early = 0; + + for (i = 0; i < 60; i++) { + struct cellctx c; + + ASSERT_EQ(cell_build(&c, &(struct build){ .ids = 65536 }), 0); + pidfd = spawn(c.cfd, NULL, "exit", "0", NULL); + ASSERT_GE(pidfd, 0); + ASSERT_EQ(cell_wait(c.cfd), 0); + if (cg_populated(c.cg)) + early++; + close(pidfd); + cell_cleanup(&c); + } + EXPECT_EQ(early, 0); +} + +/* Make and break a good many, so that leaks and races get a chance. */ +TEST_F(cell, churn) +{ + int i; + + for (i = 0; i < 40; i++) { + struct cellctx c; + int pidfd; + + ASSERT_EQ(cell_build(&c, &(struct build){ .ids = 65536 }), 0); + pidfd = spawn(c.cfd, NULL, i % 3 ? "pause" : "exit", "0", NULL); + ASSERT_GE(pidfd, 0); + if (i % 5 == 0) { + int second = prepare(c.cfd, "pause", NULL, NULL); + + if (second >= 0) + close(second); + } + close(pidfd); + cell_cleanup(&c); + } +} + +/* A kill against spawns in flight: whatever got in is gone when it says DEAD. */ +TEST_F(cell, kill_races_with_spawn) +{ + int i, j; + + for (i = 0; i < 25; i++) { + pid_t spawners[3]; + struct cellctx c; + int pidfd; + + ASSERT_EQ(cell_build(&c, &(struct build){ .ids = 65536 }), 0); + pidfd = spawn(c.cfd, NULL, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + + for (j = 0; j < 3; j++) { + spawners[j] = fork(); + ASSERT_GE(spawners[j], 0); + if (!spawners[j]) { + for (;;) { + int p = j ? spawn(c.cfd, NULL, "pause", NULL, NULL) : + prepare(c.cfd, "pause", NULL, NULL); + + if (p < 0) { + _exit(errno == ESRCH || errno == ENOMEM || + errno == EINTR || errno == EBUSY ? + 0 : 1); + } + close(p); + } + } + } + usleep(1000 * (i % 7) * (i % 5)); + ASSERT_EQ(cell_kill(c.cfd, 0), 0); + EXPECT_EQ(cell_wait(c.cfd), SIGKILL); + EXPECT_EQ(cg_populated(c.cg), 0); + for (j = 0; j < 3; j++) { + int status; + + ASSERT_EQ(waitpid(spawners[j], &status, 0), spawners[j]); + EXPECT_EQ(status, 0); + } + EXPECT_EQ(cg_populated(c.cg), 0); + close(pidfd); + cell_cleanup(&c); + } +} + +/* Starting an embryo while its handle goes away, from two processes. */ +TEST_F(cell, start_races_with_close) +{ + int i; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + ASSERT_GE(spawn(self->c.cfd, NULL, "pause", NULL, NULL), 0); + + for (i = 0; i < 60; i++) { + int pidfd = prepare(self->c.cfd, "exit", "0", NULL); + int status; + pid_t pid; + + ASSERT_GE(pidfd, 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + /* Both hold the one file. Only one start can win. */ + int r = pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0); + + _exit(!r || errno == EBUSY || errno == ESRCH ? 0 : 1); + } + if (i & 1) + pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0); + close(pidfd); + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(status, 0); + } + EXPECT_EQ(cell_state(self->c.cfd), CELL_RUNNING); +} + +/* The last handle going away against everything else. */ +TEST_F(cell, close_races) +{ + int i; + + for (i = 0; i < 40; i++) { + struct cellctx c; + int status; + pid_t pid; + + ASSERT_EQ(cell_build(&c, &(struct build){ .ids = 65536, + .no_seal = i % 4 == 0 }), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + int p = spawn(c.cfd, NULL, i % 3 ? "pause" : "exit", "0", NULL); + + if (p >= 0) + close(p); + _exit(0); /* drops its copy of the handle */ + } + if (i % 2) + usleep(500 * (i % 9)); + close(c.cfd); + c.cfd = -1; + ASSERT_EQ(waitpid(pid, &status, 0), pid); + cell_cleanup(&c); + EXPECT_EQ(access(c.cg, F_OK), -1); + } +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/cell/cell_net_test.c b/tools/testing/selftests/cell/cell_net_test.c new file mode 100644 index 000000000000..297469260cba --- /dev/null +++ b/tools/testing/selftests/cell/cell_net_test.c @@ -0,0 +1,915 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Cells: the network. It is set up from the outside and handed to the cell, as + * the tree is. A cell configures nothing unless it was built to. + */ +#include "cell_test.h" +#include <netinet/in.h> +#include <linux/if_tun.h> +#include <linux/if_link.h> +#include <linux/netlink.h> +#include <linux/rtnetlink.h> +#include <linux/veth.h> + +#define HOST_IP 0x0a090001 /* 10.9.0.1 */ +#define CELL_IP 0x0a090002 +#define LATER_IP 0x0a090003 + +static int set_cred(int pidfd, unsigned int id) +{ + struct pidfd_cred cred = { .uid = id, .gid = id }; + + return pidfd_configure(pidfd, PIDFD_SET_BINARY, "cred", &cred, sizeof(cred)); +} + +/* Build a cell, run one thing in it as @id to its end. Returns its status. */ +static int run(const struct build *b, unsigned int id, const char *what, const char *arg) +{ + struct cellctx c; + int pidfd, st; + + st = cell_build(&c, b); + if (st) { + cell_cleanup(&c); + return -100 - st; + } + pidfd = prepare(c.cfd, what, arg, NULL); + if (pidfd < 0 || (id && set_cred(pidfd, id)) || + pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0)) + st = -99; + else + st = cell_wait(c.cfd); + close(pidfd); + cell_cleanup(&c); + return st; +} + +static int dev_up(const char *dev, unsigned int ip) +{ + struct ifreq ifr = {}; + struct sockaddr_in *sin = (void *)&ifr.ifr_addr; + int s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0), r; + + strcpy(ifr.ifr_name, dev); + sin->sin_family = AF_INET; + sin->sin_addr.s_addr = htonl(ip); + r = ioctl(s, SIOCSIFADDR, &ifr); + sin->sin_addr.s_addr = htonl(0xffffff00); + r = r ?: ioctl(s, SIOCSIFNETMASK, &ifr); + r = r ?: ioctl(s, SIOCGIFFLAGS, &ifr); + ifr.ifr_flags |= IFF_UP; + r = r ?: ioctl(s, SIOCSIFFLAGS, &ifr); + r = r ? -errno : 0; + close(s); + return r; +} + +/* The same in the network namespace @net. Comes back. */ +static int dev_up_in(int net, const char *dev, unsigned int ip) +{ + int old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC), r; + + if (setns(net, CLONE_NEWNET)) + return -errno; + r = dev_up(dev, ip); + if (setns(old, CLONE_NEWNET)) + return -errno; + close(old); + return r; +} + +static int sysctl_in(int net, const char *path, const char *val) +{ + int old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC), r; + + if (setns(net, CLONE_NEWNET)) + return -errno; + r = write_file(path, val) ? -errno : 0; + if (setns(old, CLONE_NEWNET)) + return -errno; + close(old); + return r; +} + +/* A network namespace of the caller's own making, with nothing done to it. */ +static int make_netns(void) +{ + int old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC), net; + + if (old < 0 || unshare(CLONE_NEWNET)) + return -1; + net = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC); + if (setns(old, CLONE_NEWNET)) + return -1; + close(old); + return net; +} + +static struct rtattr *put_attr(struct nlmsghdr *h, int type, const void *data, int len) +{ + struct rtattr *a = (void *)((char *)h + NLMSG_ALIGN(h->nlmsg_len)); + + a->rta_type = type; + a->rta_len = RTA_LENGTH(len); + if (data) + memcpy(RTA_DATA(a), data, len); + h->nlmsg_len = NLMSG_ALIGN(h->nlmsg_len) + RTA_ALIGN(a->rta_len); + return a; +} + +static void end_nest(struct nlmsghdr *h, struct rtattr *a) +{ + a->rta_len = (char *)h + NLMSG_ALIGN(h->nlmsg_len) - (char *)a; +} + +/* vh0 here, vc0 in the network namespace @net. */ +static int make_veth(int net) +{ + struct sockaddr_nl sa = { .nl_family = AF_NETLINK }; + struct { + struct nlmsghdr h; + struct ifinfomsg i; + char attrs[256]; + } req = {}; + struct rtattr *info, *data, *peer; + struct nlmsghdr *h; + char buf[1024]; + int s, n; + + req.h.nlmsg_type = RTM_NEWLINK; + req.h.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL; + req.h.nlmsg_len = NLMSG_LENGTH(sizeof(req.i)); + put_attr(&req.h, IFLA_IFNAME, "vh0", 4); + info = put_attr(&req.h, IFLA_LINKINFO, NULL, 0); + put_attr(&req.h, IFLA_INFO_KIND, "veth", 5); + data = put_attr(&req.h, IFLA_INFO_DATA, NULL, 0); + peer = put_attr(&req.h, VETH_INFO_PEER, NULL, sizeof(struct ifinfomsg)); + put_attr(&req.h, IFLA_IFNAME, "vc0", 4); + put_attr(&req.h, IFLA_NET_NS_FD, &net, sizeof(net)); + end_nest(&req.h, peer); + end_nest(&req.h, data); + end_nest(&req.h, info); + + s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + if (s < 0 || sendto(s, &req, req.h.nlmsg_len, 0, (void *)&sa, sizeof(sa)) < 0) + return -errno; + n = recv(s, buf, sizeof(buf), 0); + close(s); + h = (void *)buf; + if (n < (int)sizeof(*h) || h->nlmsg_type != NLMSG_ERROR) + return -EPROTO; + return ((struct nlmsgerr *)NLMSG_DATA(h))->error; +} + +/* + * Nothing was said about the network, so there is none. What the cell is handed + * works all the same: here a socket that listens in the network of the host. + */ +TEST_F(cell, no_net_slot_no_network) +{ + static const unsigned int ids[] = { 0, 1000 }; + struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(7778), + .sin_addr = { htonl(INADDR_LOOPBACK) } }; + int lsock, conn, pidfd, one = 1; + unsigned int i; + char c = 0; + + ASSERT_EQ(dev_up("lo", INADDR_LOOPBACK), 0); + lsock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + ASSERT_EQ(bind(lsock, (void *)&a, sizeof(a)), 0); + ASSERT_EQ(listen(lsock, 1), 0); + + for (i = 0; i < ARRAY_SIZE(ids); i++) { + struct cellctx cell; + + ASSERT_EQ(cell_build(&cell, &(struct build){ .ids = 65536 }), 0); + pidfd = prepare(cell.cfd, "net-none", NULL, NULL); + ASSERT_GE(pidfd, 0); + if (ids[i]) + ASSERT_EQ(set_cred(pidfd, ids[i]), 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, lsock), 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0); + + conn = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + ASSERT_EQ(connect(conn, (void *)&a, sizeof(a)), 0); + EXPECT_EQ(write(conn, "h", 1), 1); + ASSERT_EQ(wait_for(conn, pidfd), 0); + EXPECT_EQ(read(conn, &c, 1), 1); + EXPECT_EQ(c, 'c'); + close(conn); + + EXPECT_EQ(cell_wait(cell.cfd), 0); + close(pidfd); + cell_cleanup(&cell); + } + close(lsock); +} + +static int seal(int cfd) +{ + return cell_configure(cfd, CELL_CMD_SEAL, NULL, NULL, 0); +} + +static int run_sealed(struct cellctx *c, const char *what, const char *arg) +{ + int pidfd = spawn(c->cfd, NULL, what, arg, NULL), st; + + if (pidfd < 0) + return -99; + st = cell_wait(c->cfd); + close(pidfd); + return st; +} + +/* A seal that fails before there is a network namespace has decided nothing. */ +TEST_F(cell, failed_seal_leaves_the_net_slot_open) +{ + char max[32] = {}; + int fd; + + fd = open("/proc/sys/user/max_net_namespaces", O_RDONLY); + ASSERT_GE(fd, 0); + ASSERT_GT(read(fd, max, sizeof(max) - 1), 0); + close(fd); + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0); + ASSERT_EQ(write_file("/proc/sys/user/max_net_namespaces", "0"), 0); + EXPECT_EQ(seal(self->c.cfd), -1); + EXPECT_EQ(errno, ENOSPC); + ASSERT_EQ(write_file("/proc/sys/user/max_net_namespaces", max), 0); + + ASSERT_EQ(cell_configure(self->c.cfd, CELL_SET_STRING, "net", "new", 0), 0); + ASSERT_EQ(seal(self->c.cfd), 0); + EXPECT_EQ(run_sealed(&self->c, "net-at-all", "yes"), 0); +} + +/* Makes the next seal fail late, with EBUSY: somebody is in the mount namespace. */ +static pid_t sit_in(int rootfd) +{ + int ready[2]; + pid_t pid; + char c; + + if (pipe2(ready, O_CLOEXEC)) + return -1; + pid = fork(); + if (!pid) { + if (setns(rootfd, CLONE_NEWNS) || write(ready[1], "r", 1) != 1) + _exit(1); + for (;;) + pause(); + } + close(ready[1]); + if (read(ready[0], &c, 1) != 1) + pid = -1; + close(ready[0]); + return pid; +} + +static int seal_once_free(int cfd, pid_t sitter) +{ + int i, r = -1; + + kill(sitter, SIGKILL); + waitpid(sitter, NULL, 0); + for (i = 0; i < 100 && (r = seal(cfd)) && errno == EBUSY; i++) + usleep(20000); + return r; +} + +/* One that fails later has filled the slot in. No network is what it stays. */ +TEST_F(cell, failed_seal_then_still_no_network) +{ + pid_t sitter; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true, + .keep_root = true }), 0); + sitter = sit_in(self->c.rootfd); + ASSERT_GT(sitter, 0); + EXPECT_EQ(seal(self->c.cfd), -1); + EXPECT_EQ(errno, EBUSY); + EXPECT_EQ(cell_configure(self->c.cfd, CELL_SET_STRING, "net", "new", 0), -1); + EXPECT_EQ(errno, EBUSY); + ASSERT_EQ(seal_once_free(self->c.cfd, sitter), 0); + EXPECT_EQ(run_sealed(&self->c, "net-at-all", "no"), 0); +} + +/* Unless the builder asks for it after all, to set it up. */ +TEST_F(cell, failed_seal_then_a_network_after_all) +{ + pid_t sitter; + int net; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true, + .keep_root = true }), 0); + sitter = sit_in(self->c.rootfd); + ASSERT_GT(sitter, 0); + EXPECT_EQ(seal(self->c.cfd), -1); + EXPECT_EQ(errno, EBUSY); + net = cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0); + ASSERT_GE(net, 0); + ASSERT_EQ(dev_up_in(net, "lo", INADDR_LOOPBACK), 0); + close(net); + ASSERT_EQ(seal_once_free(self->c.cfd, sitter), 0); + EXPECT_EQ(run_sealed(&self->c, "net-lo", NULL), 0); +} + +/* "new" is one for whoever builds the cell to set up. Until then there is little to it. */ +TEST_F(cell, new_net_is_the_builders_to_set_up) +{ + int net, pidfd; + + EXPECT_EQ(run(&(struct build){ .ids = 65536, .net_str = "new" }, 0, "net-bare", NULL), 0); + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net_str = "new", + .no_seal = true }), 0); + net = cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0); + ASSERT_GE(net, 0); + ASSERT_EQ(dev_up_in(net, "lo", INADDR_LOOPBACK), 0); + ASSERT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), 0); + pidfd = spawn(self->c.cfd, NULL, "net-lo", NULL, NULL); + ASSERT_GE(pidfd, 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + close(pidfd); + close(net); +} + +/* + * What an orchestrator does: it makes the network namespace, puts one end of a + * veth pair in there, gives it an address, and goes on owning all of it. The cell + * uses it, and its root can change nothing about it. + */ +TEST_F(cell, net_is_set_up_from_the_outside) +{ + struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(7777), + .sin_addr = { htonl(HOST_IP) } }; + int net, lsock, conn, up[2], down[2], pidfd, one = 1, r; + char c = 0; + + net = make_netns(); + ASSERT_GE(net, 0); + r = make_veth(net); + if (r == -EOPNOTSUPP) { + close(net); + SKIP(return, "no veth"); + } + ASSERT_EQ(r, 0); + ASSERT_EQ(dev_up("vh0", HOST_IP), 0); + ASSERT_EQ(dev_up_in(net, "lo", INADDR_LOOPBACK), 0); + ASSERT_EQ(dev_up_in(net, "vc0", CELL_IP), 0); + + lsock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + ASSERT_EQ(bind(lsock, (void *)&a, sizeof(a)), 0); + ASSERT_EQ(listen(lsock, 1), 0); + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net = net }), 0); + ASSERT_EQ(pipe2(up, O_CLOEXEC), 0); + ASSERT_EQ(pipe2(down, O_CLOEXEC), 0); + pidfd = prepare(self->c.cfd, "net-given", NULL, NULL); + ASSERT_GE(pidfd, 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, up[1]), 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "4", NULL, down[0]), 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0); + + ASSERT_EQ(wait_for(lsock, pidfd), 0); + conn = accept(lsock, NULL, NULL); + ASSERT_GE(conn, 0); + ASSERT_EQ(wait_for(conn, pidfd), 0); + EXPECT_EQ(read(conn, &c, 1), 1); + EXPECT_EQ(c, 'c'); + EXPECT_EQ(write(conn, "h", 1), 1); + + /* The cell is running. Whoever owns the namespace still gets at it. */ + ASSERT_EQ(wait_for(up[0], pidfd), 0); + ASSERT_EQ(read(up[0], &c, 1), 1); + EXPECT_EQ(cell_state(self->c.cfd), CELL_RUNNING); + EXPECT_EQ(dev_up_in(net, "lo", LATER_IP), 0); + ASSERT_EQ(write(down[1], "g", 1), 1); + + EXPECT_EQ(cell_wait(self->c.cfd), 0); + close(pidfd); + close(conn); + close(lsock); + close(net); +} + +/* That of the host is no different: it is there to be used. */ +TEST_F(cell, host_net_is_used_and_not_configured) +{ + int net = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC); + + ASSERT_GE(net, 0); + ASSERT_EQ(dev_up("lo", INADDR_LOOPBACK), 0); + EXPECT_EQ(run(&(struct build){ .ids = 65536, .net = net }, 0, "net-lo", NULL), 0); + close(net); +} + +/* What a pod expects takes no capability: it is set on the network namespace. */ +TEST_F(cell, low_ports_and_ping_are_settings) +{ + static const unsigned int ids[] = { 0, 1000 }; + unsigned int i; + int net; + + net = make_netns(); + ASSERT_GE(net, 0); + ASSERT_EQ(dev_up_in(net, "lo", INADDR_LOOPBACK), 0); + for (i = 0; i < ARRAY_SIZE(ids); i++) { + EXPECT_EQ(run(&(struct build){ .ids = 65536, .net = net }, ids[i], + "net-settings", "before"), 0); + } + + ASSERT_EQ(sysctl_in(net, "/proc/sys/net/ipv4/ip_unprivileged_port_start", "0"), 0); + ASSERT_EQ(sysctl_in(net, "/proc/sys/net/ipv4/ping_group_range", "0 4294967294"), 0); + for (i = 0; i < ARRAY_SIZE(ids); i++) { + EXPECT_EQ(run(&(struct build){ .ids = 65536, .net = net }, ids[i], + "net-settings", "after"), 0); + } + close(net); +} + +/* Is there a device of that name in the network namespace? -1 if it cannot be entered. */ +static int dev_in(int net, const char *dev) +{ + int status; + pid_t pid; + + pid = fork(); + if (!pid) { + if (setns(net, CLONE_NEWNET)) + _exit(2); + _exit(!!if_nametoindex(dev)); + } + if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status) || + WEXITSTATUS(status) > 1) + return -1; + return WEXITSTATUS(status); +} + +/* A network namespace is taken apart in its own time. */ +static bool dev_goes(const char *dev) +{ + int i; + + for (i = 0; i < 100 && if_nametoindex(dev); i++) + usleep(100000); + return !if_nametoindex(dev); +} + +/* + * A cell that dies takes its network along: it lets go of the namespace, and that is + * the end of whatever is in there. + * + * Sealed is sealed, so the namespace is not to be had from the cell later on. Whoever + * wants to take the network apart in an orderly way keeps the file from when the cell + * was built. The namespace is then there for as long as that file is, with all that is + * in it and nobody left to use it. + */ +TEST_F(cell, net_goes_with_the_cell) +{ + struct cellctx c; + int net, pidfd, r; + + /* Nobody has kept anything. */ + ASSERT_EQ(cell_build(&c, &(struct build){ .ids = 65536, .no_seal = true, + .net_str = "new" }), 0); + net = cell_open(c.cfd, CELL_OPEN_SLOT, "net", NULL, 0); + ASSERT_GE(net, 0); + r = make_veth(net); + close(net); + if (r == -EOPNOTSUPP) { + cell_cleanup(&c); + SKIP(return, "no veth"); + } + ASSERT_EQ(r, 0); + ASSERT_EQ(seal(c.cfd), 0); + pidfd = spawn(c.cfd, NULL, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + EXPECT_NE(if_nametoindex("vh0"), 0); + ASSERT_EQ(cell_kill(c.cfd, 0), 0); + EXPECT_EQ(cell_wait(c.cfd), SIGKILL); + close(pidfd); + /* The cellfd is still open. It does not hold on to anything. */ + EXPECT_TRUE(dev_goes("vh0")); + cell_cleanup(&c); + + /* Somebody has kept the file. */ + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true, + .net_str = "new" }), 0); + net = cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0); + ASSERT_GE(net, 0); + ASSERT_EQ(make_veth(net), 0); + ASSERT_EQ(seal(self->c.cfd), 0); + EXPECT_EQ(cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0), -1); + EXPECT_EQ(errno, EBUSY); + pidfd = spawn(self->c.cfd, NULL, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + EXPECT_EQ(cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0), -1); + EXPECT_EQ(errno, EBUSY); + ASSERT_EQ(cell_kill(self->c.cfd, 0), 0); + EXPECT_EQ(cell_wait(self->c.cfd), SIGKILL); + close(pidfd); + EXPECT_EQ(cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0), -1); + EXPECT_EQ(errno, EBUSY); + + /* Long enough for it to have gone, if it were going to. */ + usleep(500000); + EXPECT_EQ(dev_in(net, "vc0"), 1); + EXPECT_NE(if_nametoindex("vh0"), 0); + + close(net); + EXPECT_TRUE(dev_goes("vh0")); +} + +#define NET_CAPS (CAP(NET_ADMIN) | CAP(NET_RAW) | CAP(NET_BIND_SERVICE)) +#define ALL_RIGHTS ((CELL_NET_RAW << 1) - 1) + +static int run_rights(__u64 rights, __u64 caps, unsigned int id) +{ + struct build b = { .ids = 65536, .net_str = "new", .caps = caps, .net_rights = rights, + .tun = true }; + char hex[32]; + + snprintf(hex, sizeof(hex), "%llx", rights); + return run(&b, id, "net-rights", hex); +} + +/* + * The network namespace of a cell is never the cell's, so no capability of a task of + * the cell counts for it. What such a task may do to it all the same, the builder + * names, one thing at a time. + */ +TEST_F(cell, net_rights_are_what_the_builder_named) +{ + __u64 right; + + EXPECT_EQ(run_rights(0, NET_CAPS, 0), 0); + for (right = 1; right <= CELL_NET_RAW; right <<= 1) { + EXPECT_EQ(run_rights(right, NET_CAPS, 0), 0) { + TH_LOG("with %llx", right); + } + } + EXPECT_EQ(run_rights(ALL_RIGHTS, NET_CAPS, 0), 0); +} + +/* The right says what, the capability says who in the cell. It takes both. */ +TEST_F(cell, net_rights_are_for_who_has_the_capability) +{ + EXPECT_EQ(run_rights(ALL_RIGHTS, 0, 0), 0); + EXPECT_EQ(run_rights(ALL_RIGHTS, CAP(NET_ADMIN), 0), 0); + EXPECT_EQ(run_rights(ALL_RIGHTS, CAP(NET_RAW), 0), 0); + /* Somebody else in there, who has left them behind with uid 0. */ + EXPECT_EQ(run_rights(ALL_RIGHTS, NET_CAPS, 1000), 0); +} + +/* What the "net-rights" slot takes, and from whom. */ +TEST_F(cell, net_rights_slot) +{ + struct cell_info info = { .mask = CELL_INFO_NET_RIGHTS }; + __u64 rights[2] = { CELL_NET_LINK | CELL_NET_ADDR, 0 }; + int cfd, status; + pid_t pid; + + cfd = cell_create(CELL_SELF, 0); + ASSERT_GE(cfd, 0); + /* Rights over what? */ + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", rights, 8), -1); + EXPECT_EQ(errno, EINVAL); + ASSERT_EQ(cell_configure(cfd, CELL_SET_STRING, "net", "new", 0), 0); + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", rights, 4), -1); + EXPECT_EQ(errno, EINVAL); + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", rights, 16), -1); + EXPECT_EQ(errno, EINVAL); + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", (void *)1, 8), -1); + EXPECT_EQ(errno, EFAULT); + rights[1] = CELL_NET_RAW << 1; + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", &rights[1], 8), -1); + EXPECT_EQ(errno, EINVAL); + ASSERT_EQ(cell_info(cfd, &info, sizeof(info), 0), 0); + EXPECT_EQ(info.mask, CELL_INFO_NET_RIGHTS); + EXPECT_EQ(info.net_rights, 0); + + /* The last word counts. */ + rights[1] = ALL_RIGHTS; + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", &rights[1], 8), 0); + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", rights, 8), 0); + ASSERT_EQ(cell_info(cfd, &info, sizeof(info), 0), 0); + EXPECT_EQ(info.net_rights, rights[0]); + close(cfd); + + /* Until the seal. */ + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net_str = "new", + .net_rights = rights[0] }), 0); + EXPECT_EQ(cell_configure(self->c.cfd, CELL_SET_BINARY, "net-rights", &rights[1], 8), -1); + EXPECT_EQ(errno, EBUSY); + + /* There used to be a network namespace that the cell owns. */ + cfd = cell_create(CELL_SELF, 0); + ASSERT_GE(cfd, 0); + EXPECT_EQ(cell_configure(cfd, CELL_SET_STRING, "net", "own", 0), -1); + EXPECT_EQ(errno, EINVAL); + close(cfd); + + /* Nobody hands out what is not theirs to do. */ + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + __u64 raw = CELL_NET_RAW, none = 0; + + if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) || + setresuid(1000, 1000, 1000) || prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)) + _exit(1); + /* This one belongs to the user namespace of the host, where it is nobody. */ + cfd = cell_create(CELL_SELF, 0); + if (cfd < 0 || cell_configure(cfd, CELL_SET_STRING, "net", "new", 0)) + _exit(2); + if (!cell_configure(cfd, CELL_SET_BINARY, "net-rights", rights, 8) || + errno != EPERM) + _exit(3); + if (!cell_configure(cfd, CELL_SET_BINARY, "net-rights", &raw, 8) || errno != EPERM) + _exit(4); + if (cell_configure(cfd, CELL_SET_BINARY, "net-rights", &none, 8)) + _exit(5); + close(cfd); + /* And this one to a user namespace of its own. */ + if (unshare(CLONE_NEWUSER) || write_file("/proc/self/setgroups", "deny") || + write_file("/proc/self/uid_map", "1000 1000 1") || + write_file("/proc/self/gid_map", "1000 1000 1")) + _exit(6); + cfd = cell_create(CELL_SELF, 0); + if (cfd < 0 || cell_configure(cfd, CELL_SET_STRING, "net", "new", 0)) + _exit(7); + _exit(cell_configure(cfd, CELL_SET_BINARY, "net-rights", &rights[1], 8) ? 8 : 0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); +} + +/* They are rights over the network of the cell, and over no other. */ +TEST_F(cell, net_rights_end_at_the_network_of_the_cell) +{ + int inet, rtnl, pidfd; + + inet = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + rtnl = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + ASSERT_GE(inet, 0); + ASSERT_GE(rtnl, 0); + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net_str = "new", + .caps = NET_CAPS, .tun = true, + .net_rights = ALL_RIGHTS }), 0); + pidfd = prepare(self->c.cfd, "net-foreign", NULL, NULL); + ASSERT_GE(pidfd, 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, inet), 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "4", NULL, rtnl), 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + close(pidfd); + close(inet); + close(rtnl); +} + +/* Nor does a device leave for another, or come to be there. */ +TEST_F(cell, net_rights_move_nothing_out) +{ + int net, r; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true, + .net_str = "new", .caps = NET_CAPS, + .net_rights = ALL_RIGHTS }), 0); + net = cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0); + ASSERT_GE(net, 0); + r = make_veth(net); + close(net); + if (r == -EOPNOTSUPP) + SKIP(return, "no veth"); + ASSERT_EQ(r, 0); + ASSERT_EQ(seal(self->c.cfd), 0); + r = spawn(self->c.cfd, NULL, "net-no-way-out", "vc0", NULL); + ASSERT_GE(r, 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + close(r); + EXPECT_EQ(if_nametoindex("esc0"), 0); + EXPECT_EQ(if_nametoindex("vc0"), 0); +} + +/* The builder hands it a device and the right to run it. The rest is up to the cell. */ +TEST_F(cell, net_rights_run_a_device_that_was_handed_over) +{ + char hex[32]; + int net, r; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true, + .net_str = "new", .caps = NET_CAPS, + .net_rights = CELL_NET_LINK | + CELL_NET_ADDR }), 0); + net = cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0); + ASSERT_GE(net, 0); + r = make_veth(net); + close(net); + if (r == -EOPNOTSUPP) + SKIP(return, "no veth"); + ASSERT_EQ(r, 0); + ASSERT_EQ(seal(self->c.cfd), 0); + snprintf(hex, sizeof(hex), "%llx", CELL_NET_LINK | CELL_NET_ADDR); + r = spawn(self->c.cfd, NULL, "net-rights", hex, "vc0"); + ASSERT_GE(r, 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + close(r); +} + +/* + * A network namespace that was made for a cell is not the cell's, so who owns it + * may enter it whatever the cell is up to, and may share it with another cell. + */ +TEST_F(cell, new_net_belongs_to_the_builder) +{ + int net, old, other, pidfd; + char path[64]; + pid_t pid; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net_str = "new" }), 0); + pidfd = spawn(self->c.cfd, &pid, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + snprintf(path, sizeof(path), "/proc/%d/ns/net", pid); + net = open(path, O_RDONLY | O_CLOEXEC); + ASSERT_GE(net, 0); + + old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC); + EXPECT_EQ(setns(net, CLONE_NEWNET), 0); + ASSERT_EQ(setns(old, CLONE_NEWNET), 0); + close(old); + + other = cell_create(CELL_SELF, 0); + ASSERT_GE(other, 0); + EXPECT_EQ(cell_configure(other, CELL_SET_FD, "net", NULL, net), 0); + close(other); + close(net); + close(pidfd); +} + +/* A builder without privilege works in a user namespace of its own, which owns it. */ +TEST_F(cell, unprivileged_builder_sets_up_the_net) +{ + int status; + pid_t pid; + + ASSERT_EQ(chown(CGROOT, 1000, 1000), 0); + ASSERT_EQ(chown(CGROOT "/cgroup.procs", 1000, 1000), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + struct cellctx c; + pid_t helper; + int step, net; + + if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) || + setresuid(1000, 1000, 1000) || prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)) + _exit(1); + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) + _exit(2); + if (write_file("/proc/self/setgroups", "deny") || + write_file("/proc/self/uid_map", "1000 1000 1") || + write_file("/proc/self/gid_map", "1000 1000 1")) + _exit(3); + step = cell_build(&c, &(struct build){ .ids = 1, .no_seal = true }); + if (step) + _exit(20 + step); + net = cell_open(c.cfd, CELL_OPEN_SLOT, "net", NULL, 0); + if (net < 0) + _exit(4); + /* There is no way back to the network namespace of the host. */ + helper = fork(); + if (!helper) + _exit(setns(net, CLONE_NEWNET) || dev_up("lo", INADDR_LOOPBACK) ? 1 : 0); + if (waitpid(helper, &step, 0) != helper || step) + _exit(5); + if (cell_configure(c.cfd, CELL_CMD_SEAL, NULL, NULL, 0)) + _exit(6); + if (spawn(c.cfd, NULL, "net-lo", NULL, NULL) < 0) + _exit(7); + _exit(cell_wait(c.cfd) ? 8 : 0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); + EXPECT_EQ(chown(CGROOT, 0, 0), 0); + EXPECT_EQ(chown(CGROOT "/cgroup.procs", 0, 0), 0); +} + +#define TUN_NODE "/tmp/cell-test-tun" + +/* A persistent tap device in the network namespace of the caller. -1: nobody's. */ +static int make_tap(const char *dev, long long uid, long long gid) +{ + struct ifreq ifr = { .ifr_flags = IFF_TAP | IFF_NO_PI }; + int fd = open(TUN_NODE, O_RDWR | O_CLOEXEC), r = 0; + + if (fd < 0) + return -errno; + strcpy(ifr.ifr_name, dev); + if (ioctl(fd, TUNSETIFF, &ifr) || + (uid >= 0 && ioctl(fd, TUNSETOWNER, (unsigned long)uid)) || + (gid >= 0 && ioctl(fd, TUNSETGROUP, (unsigned long)gid)) || + ioctl(fd, TUNSETPERSIST, 1)) + r = -errno; + close(fd); + return r; +} + +/* A cell of one id runs on the uid of its maker. What is the maker's is not the cell's. */ +TEST_F(cell, tun_device_of_the_maker_is_not_the_cells) +{ + int status; + pid_t pid; + + unlink(TUN_NODE); + ASSERT_EQ(mknod(TUN_NODE, S_IFCHR | 0666, makedev(10, 200)), 0); + ASSERT_EQ(chmod(TUN_NODE, 0666), 0); + status = open(TUN_NODE, O_RDWR | O_CLOEXEC); + if (status < 0) { + unlink(TUN_NODE); + SKIP(return, "no tun"); + } + close(status); + ASSERT_EQ(chown(CGROOT, 1000, 1000), 0); + ASSERT_EQ(chown(CGROOT "/cgroup.procs", 1000, 1000), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + struct cellctx c; + pid_t helper; + int step, net; + + if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) || + setresuid(1000, 1000, 1000) || prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)) + _exit(1); + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) + _exit(2); + if (write_file("/proc/self/setgroups", "deny") || + write_file("/proc/self/uid_map", "1000 1000 1") || + write_file("/proc/self/gid_map", "1000 1000 1")) + _exit(3); + step = cell_build(&c, &(struct build){ .ids = 1, .no_seal = true, + .bind_file = TUN_NODE }); + if (step) + _exit(20 + step); + net = cell_open(c.cfd, CELL_OPEN_SLOT, "net", NULL, 0); + if (net < 0) + _exit(4); + helper = fork(); + if (!helper) + _exit(setns(net, CLONE_NEWNET) || make_tap("tapu", 1000, -1) || + make_tap("tapg", -1, 1000) || make_tap("tapn", -1, -1) ? 1 : 0); + if (waitpid(helper, &step, 0) != helper || step) + _exit(5); + if (cell_configure(c.cfd, CELL_CMD_SEAL, NULL, NULL, 0)) + _exit(6); + if (spawn(c.cfd, NULL, "tun-owner", NULL, NULL) < 0) + _exit(7); + _exit(cell_wait(c.cfd) ? 8 : 0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); + EXPECT_EQ(chown(CGROOT, 0, 0), 0); + EXPECT_EQ(chown(CGROOT "/cgroup.procs", 0, 0), 0); + unlink(TUN_NODE); +} + +/* An owner counts in the cell that named it, and in no other. Not even if it was meant to. */ +TEST_F(cell, tun_owner_counts_within_a_cell) +{ + struct cell_info info = { .mask = CELL_INFO_IDS_FIRST }; + int net, old, r; + + unlink(TUN_NODE); + ASSERT_EQ(mknod(TUN_NODE, S_IFCHR | 0666, makedev(10, 200)), 0); + r = open(TUN_NODE, O_RDWR | O_CLOEXEC); + if (r < 0) { + unlink(TUN_NODE); + SKIP(return, "no tun"); + } + close(r); + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true, .tun = true, + .net_str = "new", + .caps = CAP(NET_ADMIN) | CAP(SETUID) | + CAP(SETGID), + .net_rights = CELL_NET_TUN }), 0); + ASSERT_EQ(cell_info(self->c.cfd, &info, sizeof(info), 0), 0); + ASSERT_EQ(info.mask, CELL_INFO_IDS_FIRST); + net = cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0); + ASSERT_GE(net, 0); + old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC); + ASSERT_GE(old, 0); + ASSERT_EQ(setns(net, CLONE_NEWNET), 0); + r = make_tap("taph", (long long)info.ids_first + 1000, -1); + ASSERT_EQ(setns(old, CLONE_NEWNET), 0); + close(old); + close(net); + unlink(TUN_NODE); + ASSERT_EQ(r, 0); + ASSERT_EQ(seal(self->c.cfd), 0); + EXPECT_EQ(run_sealed(&self->c, "tun-owner-within", NULL), 0); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/cell/cell_slots_test.c b/tools/testing/selftests/cell/cell_slots_test.c new file mode 100644 index 000000000000..58e57e8c1642 --- /dev/null +++ b/tools/testing/selftests/cell/cell_slots_test.c @@ -0,0 +1,748 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Tests for cells, the slots: what may fill them, who may fill them, and what + * sealing makes of them. + */ +#include "cell_test.h" + +TEST_F(cell, tree_must_be_for_the_cell) +{ + /* A writable tmpfs of the host's, not idmapped. */ + EXPECT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_idmap = true }), 18); + EXPECT_EQ(errno, EOVERFLOW); + EXPECT_EQ(cell_state(self->c.cfd), CELL_BUILDING); +} + +TEST_F(cell, nothing_propagates_in) +{ + char path[64], line[512]; + pid_t pid; + int pidfd; + FILE *f; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .shared = true }), 0); + pidfd = spawn(self->c.cfd, &pid, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + + snprintf(path, sizeof(path), "/proc/%d/mountinfo", pid); + f = fopen(path, "r"); + ASSERT_NE(f, NULL); + while (fgets(line, sizeof(line), f)) { + EXPECT_EQ(strstr(line, "shared:"), NULL) TH_LOG("%s", line); + EXPECT_EQ(strstr(line, "master:"), NULL) TH_LOG("%s", line); + } + fclose(f); + close(pidfd); +} + +/* Making a mount namespace a cell's takes CAP_SYS_ADMIN over its owner. */ +/* + * Somebody who is still in the mount namespace puts another tree in the place of + * the one that is there, and leaves, while the cell is being sealed. The tree that + * the seal has looked at is the one that the tasks of the cell are in. + */ +TEST_F(cell, root_is_what_the_seal_looked_at) +{ + struct mount_attr attr = { .attr_set = MOUNT_ATTR_IDMAP }; + int other, fd, p, i, r = -1, go[2], status; + pid_t pid; + char c; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true, + .keep_root = true }), 0); + other = new_tmpfs(); + ASSERT_GE(other, 0); + ASSERT_EQ(copy_child(other, "cell_child"), 0); + fd = openat(other, "other", O_CREAT | O_WRONLY, 0644); + ASSERT_GE(fd, 0); + close(fd); + attr.userns_fd = self->c.cfd; + ASSERT_EQ(sys_mount_setattr(other, "", AT_EMPTY_PATH, &attr, sizeof(attr)), 0); + + ASSERT_EQ(pipe2(go, O_CLOEXEC), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + if (setns(self->c.rootfd, CLONE_NEWNS)) + _exit(1); + if (read(go[0], &c, 1) != 1) + _exit(2); + usleep(100000); + if (umount2("/", MNT_DETACH)) + _exit(3); + /* Once more, for the root of the namespace that this has laid bare. */ + if (setns(self->c.rootfd, CLONE_NEWNS)) + _exit(4); + if (sys_move_mount(other, "", AT_FDCWD, "/", MOVE_MOUNT_F_EMPTY_PATH)) + _exit(5); + _exit(0); + } + usleep(50000); + ASSERT_EQ(write(go[1], "g", 1), 1); + for (i = 0; i < 200; i++) { + r = cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0); + if (!r || errno != EBUSY) + break; + usleep(10000); + } + EXPECT_EQ(r, 0); + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); + + p = spawn(self->c.cfd, NULL, "exists", "/other", NULL); + ASSERT_GE(p, 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + close(p); + close(other); +} + +TEST_F(cell, root_takes_permission) +{ + int nsfd, status; + pid_t pid; + + nsfd = sys_open_tree(AT_FDCWD, "/tmp", OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC); + ASSERT_GE(nsfd, 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + int cfd; + + if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000)) + _exit(1); + cfd = cell_create(CELL_SELF, 0); + if (cfd < 0) + _exit(2); + if (cell_configure(cfd, CELL_SET_FD, "root", NULL, nsfd) != -1 || errno != EPERM) + _exit(3); + _exit(0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); + close(nsfd); +} + +/* + * Sharing a network namespace takes CAP_SYS_ADMIN over its owner, which is half + * of what setns() asks for. The other half is about the namespaces of the + * caller, and those do not change. So a caller without any privilege can share + * what a child of its made in a user namespace of its own, and cannot enter it. + */ +TEST_F(cell, net_takes_half_of_setns) +{ + int status; + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + int ready[2], hold[2], cfd, nsfd, host; + char path[64], c; + pid_t child; + + host = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC); + if (host < 0 || setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000)) + _exit(1); + /* /proc/<pid>/ns of the child is gated by ptrace access. */ + if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) || pipe(ready) || pipe(hold)) + _exit(2); + child = fork(); + if (child < 0) + _exit(3); + if (!child) { + close(hold[1]); + if (unshare(CLONE_NEWUSER | CLONE_NEWNET) || write(ready[1], "r", 1) != 1) + _exit(1); + _exit(read(hold[0], &c, 1)); + } + if (read(ready[0], &c, 1) != 1) + _exit(4); + snprintf(path, sizeof(path), "/proc/%d/ns/net", child); + nsfd = open(path, O_RDONLY | O_CLOEXEC); + if (nsfd < 0) + _exit(5); + if (setns(nsfd, CLONE_NEWNET) != -1 || errno != EPERM) + _exit(6); + cfd = cell_create(CELL_SELF, 0); + if (cfd < 0) + _exit(7); + /* Not the host's, over whose owner it has nothing. */ + if (cell_configure(cfd, CELL_SET_FD, "net", NULL, host) != -1 || errno != EPERM) + _exit(8); + if (cell_configure(cfd, CELL_SET_FD, "net", NULL, nsfd)) + _exit(9); + close(hold[1]); + _exit(waitpid(child, &status, 0) == child && !status ? 0 : 10); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); +} + +/* A network namespace of the caller's own making, with lo up. */ +static int new_netns(void) +{ + struct ifreq ifr = { .ifr_name = "lo", .ifr_flags = IFF_UP }; + int old, new = -1, sock; + + old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC); + if (old < 0 || unshare(CLONE_NEWNET)) + return -1; + sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (sock >= 0 && !ioctl(sock, SIOCSIFFLAGS, &ifr)) + new = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC); + close(sock); + if (setns(old, CLONE_NEWNET)) + return -1; + close(old); + return new; +} + +/* + * A pod is several cells and not one. Each has a tree, ids and a budget of its + * own. What they share is filled in from the same file descriptors: a network + * namespace that the builder made and still owns, and a volume. One that shows + * up later is one more cell. Nothing enters anything. + */ +TEST_F(cell, pod) +{ + int net, vol, vol_a, vol_b, old, ready[2], pidfd; + struct cellctx b; + char c; + + net = new_netns(); + ASSERT_GE(net, 0); + vol = new_tmpfs(); + ASSERT_GE(vol, 0); + vol_a = sys_open_tree(vol, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + vol_b = sys_open_tree(vol, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(vol_a, 0); + ASSERT_GE(vol_b, 0); + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net = net, + .data = vol_a }), 0); + ASSERT_EQ(pipe2(ready, O_CLOEXEC), 0); + pidfd = prepare(self->c.cfd, "pod-serve", NULL, NULL); + ASSERT_GE(pidfd, 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, ready[1]), 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0); + close(pidfd); + close(ready[1]); + ASSERT_EQ(read(ready[0], &c, 1), 1); + + /* The second one is made while the first is running. */ + ASSERT_EQ(cell_build(&b, &(struct build){ .ids = 65536, .net = net, + .data = vol_b }), 0); + + /* The builder owns the network namespace, so it can still get in there. */ + old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC); + ASSERT_GE(old, 0); + EXPECT_EQ(setns(net, CLONE_NEWNET), 0); + ASSERT_EQ(setns(old, CLONE_NEWNET), 0); + close(old); + + pidfd = spawn(b.cfd, NULL, "pod-connect", NULL, NULL); + ASSERT_GE(pidfd, 0); + close(pidfd); + EXPECT_EQ(cell_wait(b.cfd), 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + + cell_cleanup(&b); + close(ready[0]); + close(net); + close(vol); +} + +/* The budget is judged against whoever fills the slot. */ +TEST_F(cell, budget_takes_permission) +{ + int status; + pid_t pid; + + ASSERT_EQ(mkdir(CGROOT "/notyours", 0755), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + int cfd, cg; + + if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000)) + _exit(1); + cfd = cell_create(CELL_SELF, 0); + if (cfd < 0) + _exit(2); + cg = open(CGROOT "/notyours", O_RDONLY | O_DIRECTORY); + if (cg < 0) + _exit(3); + if (cell_configure(cfd, CELL_SET_FD, "budget", NULL, cg) != -1 || + (errno != EPERM && errno != EACCES)) + _exit(4); + _exit(0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); + rmdir(CGROOT "/notyours"); +} + +/* A kill takes everything in the budget with it, so a budget is one cell's. */ +TEST_F(cell, budget_is_exclusive) +{ + char path[192], buf[32]; + int cfd, cg, status; + struct stat st; + pid_t pid; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + + cfd = cell_create(CELL_SELF, 0); + ASSERT_GE(cfd, 0); + EXPECT_EQ(cell_configure(cfd, CELL_SET_FD, "budget", NULL, self->c.cgfd), -1); + EXPECT_EQ(errno, EBUSY); + + /* Nor one that lies within. */ + snprintf(path, sizeof(path), "%s/sub", self->c.cg); + ASSERT_EQ(mkdir(path, 0755), 0); + cg = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(cg, 0); + EXPECT_EQ(cell_configure(cfd, CELL_SET_FD, "budget", NULL, cg), -1); + EXPECT_EQ(errno, EBUSY); + close(cg); + + /* Nor one that somebody is in. */ + ASSERT_EQ(mkdir(CGROOT "/busy", 0755), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + pause(); + _exit(0); + } + snprintf(buf, sizeof(buf), "%d", pid); + ASSERT_EQ(write_file(CGROOT "/busy/cgroup.procs", buf), 0); + cg = open(CGROOT "/busy", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(cg, 0); + EXPECT_EQ(cell_configure(cfd, CELL_SET_FD, "budget", NULL, cg), -1); + EXPECT_EQ(errno, EBUSY); + kill(pid, SIGKILL); + waitpid(pid, &status, 0); + close(cg); + close(cfd); + rmdir(CGROOT "/busy"); + + /* It comes back when the cell is gone, with nothing of the cell's left. */ + ASSERT_EQ(cell_kill(self->c.cfd, 0), 0); + EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD); + ASSERT_EQ(fstat(self->c.cgfd, &st), 0); + EXPECT_EQ(st.st_uid, 0); + ASSERT_EQ(fstatat(self->c.cgfd, "cgroup.procs", &st, 0), 0); + EXPECT_EQ(st.st_uid, 0); + EXPECT_EQ(access(path, F_OK), -1); +} + +TEST_F(cell, budget_is_handed_over) +{ + struct stat st; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + ASSERT_EQ(fstat(self->c.cgfd, &st), 0); + EXPECT_GE(st.st_uid, CELL_WINDOW_FIRST); + ASSERT_EQ(fstatat(self->c.cgfd, "cgroup.procs", &st, 0), 0); + EXPECT_GE(st.st_uid, CELL_WINDOW_FIRST); + /* Limits stay with whoever is above. */ + ASSERT_EQ(fstatat(self->c.cgfd, "cgroup.max.depth", &st, 0), 0); + EXPECT_EQ(st.st_uid, 0); +} + +/* Who the budget and what is handed over with it belong to, or -1 if not to one. */ +static long budget_owner(const struct cellctx *c) +{ + static const char * const names[] = { + ".", "cgroup.procs", "cgroup.threads", "cgroup.subtree_control", + }; + struct stat st; + long uid = -1; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(names); i++) { + if (fstatat(c->cgfd, names[i], &st, 0)) + return -2; + if (i && (long)st.st_uid != uid) + return -1; + uid = st.st_uid; + } + return uid; +} + +#define FAIL_NTH "/proc/thread-self/fail-nth" + +/* + * Have one allocation after the other fail in a seal. It leaves the budget with + * whoever it belongs to, all of it, and the cell to be sealed another time. + */ +TEST_F_TIMEOUT(cell, seal_that_runs_out_of_memory, 900) +{ + char nth[16], left[16] = {}; + int n, r, e, fd, failed = 0; + + mkdir("/tmp/cell_test.debugfs", 0700); + mount("none", "/tmp/cell_test.debugfs", "debugfs", 0, NULL); + r = write_file("/tmp/cell_test.debugfs/failslab/ignore-gfp-wait", "N"); + umount2("/tmp/cell_test.debugfs", MNT_DETACH); + rmdir("/tmp/cell_test.debugfs"); + /* Opening it takes memory as well, so that is done with for good. */ + fd = open(FAIL_NTH, O_RDWR | O_CLOEXEC); + if (r || fd < 0) + SKIP(return, "no fault injection"); + + for (n = 1; n < 2000; n++) { + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true, + .net_str = "new" }), 0); + ASSERT_EQ(budget_owner(&self->c), 0); + + snprintf(nth, sizeof(nth), "%d", n); + ASSERT_GT(pwrite(fd, nth, strlen(nth), 0), 0); + r = cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0); + e = errno; + ASSERT_GT(pread(fd, left, sizeof(left) - 1, 0), 0); + ASSERT_GT(pwrite(fd, "0", 1, 0), 0); + + if (r) { + failed++; + EXPECT_EQ(e, ENOMEM) TH_LOG("allocation %d", n); + EXPECT_EQ(budget_owner(&self->c), 0) TH_LOG("allocation %d", n); + EXPECT_EQ(cell_state(self->c.cfd), CELL_BUILDING); + EXPECT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), 0) + TH_LOG("allocation %d", n); + } + EXPECT_GE(budget_owner(&self->c), (long)CELL_WINDOW_FIRST) + TH_LOG("allocation %d", n); + cell_cleanup(&self->c); + self->c.cfd = self->c.tree = self->c.raw = self->c.cgfd = -1; + self->c.rootfd = self->c.layers = -1; + /* It got through without reaching the one that was to fail. */ + if (!r && atoi(left) > 0) + break; + } + close(fd); + TH_LOG("%d allocations, %d failed seals", n - 1, failed); + EXPECT_GT(failed, 4); + EXPECT_LT(n, 2000); +} + +TEST_F(cell, stays_in_its_budget) +{ + char buf[32]; + pid_t pid; + int pidfd; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + pidfd = spawn(self->c.cfd, &pid, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + + snprintf(buf, sizeof(buf), "%d", pid); + EXPECT_EQ(write_file(CGROOT "/cgroup.procs", buf), -1); + EXPECT_EQ(errno, EPERM); + close(pidfd); +} + +/* Nothing but a spawn gets a task into a budget. */ +TEST_F(cell, nothing_enters_the_budget) +{ + struct cell_clone_args ca = { .flags = CLONE_INTO_CGROUP, .exit_signal = SIGCHLD }; + char path[192], buf[32]; + int pidfd, vhost; + pid_t pid; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + + snprintf(path, sizeof(path), "%s/cgroup.procs", self->c.cg); + snprintf(buf, sizeof(buf), "%d", getpid()); + EXPECT_EQ(write_file(path, buf), -1); + EXPECT_EQ(errno, EPERM); + + ca.cgroup = self->c.cgfd; + pid = syscall(SYS_clone3, &ca, sizeof(ca)); + if (!pid) + _exit(0); + EXPECT_EQ(pid, -1); + EXPECT_EQ(errno, EPERM); + + /* Nor the kernel on behalf of a task of the cell. */ + vhost = open("/dev/vhost-net", O_RDWR | O_CLOEXEC); + if (vhost < 0) { + TH_LOG("no /dev/vhost-net, that part is skipped"); + return; + } + pidfd = prepare(self->c.cfd, "vhost", NULL, NULL); + ASSERT_GE(pidfd, 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, vhost), 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0); + /* With the device still open out here, the cell gets to be DEAD. */ + EXPECT_EQ(cell_wait(self->c.cfd), 0); + EXPECT_EQ(cg_populated(self->c.cg), 0); + close(vhost); + close(pidfd); +} + +/* + * Nobody in particular builds a cell. The mount work takes a user namespace and + * a mount namespace of one's own, and nothing else. + */ +/* + * A task of a cell changes nothing in cgroupfs outside of its budget, even if the + * builder gave it all of cgroupfs and somebody left a cgroup open to anybody. + * Enabling a controller makes files in the children that belong to whoever asked, + * and nothing asks whether their id can be stored: it would outlive the cell. + */ +TEST_F(cell, changes_nothing_outside_its_budget) +{ + struct stat st; + int pidfd; + char c = 0; + + write_file(CGROOT "/cgroup.subtree_control", "+pids"); + ASSERT_EQ(mkdir(CGROOT "/open", 0777), 0); + ASSERT_EQ(chmod(CGROOT "/open", 0777), 0); + ASSERT_EQ(chmod(CGROOT "/open/cgroup.subtree_control", 0666), 0); + ASSERT_EQ(chmod(CGROOT "/open/cgroup.procs", 0666), 0); + chmod(CGROOT "/open/pids.max", 0666); + ASSERT_EQ(mkdir(CGROOT "/open/kid", 0755), 0); + ASSERT_EQ(mkdir(CGROOT "/open/empty", 0755), 0); + ASSERT_EQ(setxattr(CGROOT "/open", "user.host", "h", 1, 0), 0); + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .cgroupfs = true }), 0); + pidfd = spawn(self->c.cfd, NULL, "outside", NULL, NULL); + ASSERT_GE(pidfd, 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + close(pidfd); + + /* Nothing was made, so nothing is left that is owned by an id of the cell. */ + EXPECT_EQ(stat(CGROOT "/open/kid/pids.max", &st), -1); + EXPECT_EQ(errno, ENOENT); + EXPECT_EQ(stat(CGROOT "/open/empty", &st), 0); + EXPECT_EQ(getxattr(CGROOT "/open", "user.host", &c, 1), 1); + EXPECT_EQ(c, 'h'); + EXPECT_EQ(getxattr(CGROOT "/open", "user.cell", &c, 1), -1); + + write_file(CGROOT "/open/cgroup.subtree_control", "-pids"); + rmdir(CGROOT "/open/kid"); + rmdir(CGROOT "/open/empty"); + rmdir(CGROOT "/open"); +} + +#define V1ROOT "/tmp/cell-test-v1" + +TEST_F(cell, changes_nothing_on_a_legacy_hierarchy) +{ + struct stat st; + int pidfd; + + mkdir(V1ROOT, 0755); + if (mount("none", V1ROOT, "cgroup", 0, "none,name=celltest")) { + rmdir(V1ROOT); + SKIP(return, "no legacy hierarchies"); + } + ASSERT_EQ(mkdir(V1ROOT "/open", 0777), 0); + ASSERT_EQ(chmod(V1ROOT "/open", 0777), 0); + ASSERT_EQ(chmod(V1ROOT "/open/cgroup.procs", 0666), 0); + ASSERT_EQ(chmod(V1ROOT "/open/notify_on_release", 0666), 0); + ASSERT_EQ(mkdir(V1ROOT "/open/empty", 0777), 0); + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .bind = V1ROOT }), 0); + pidfd = spawn(self->c.cfd, NULL, "legacy", NULL, NULL); + ASSERT_GE(pidfd, 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + close(pidfd); + EXPECT_EQ(stat(V1ROOT "/open/empty", &st), 0); + + cell_cleanup(&self->c); + self->c.cfd = self->c.tree = self->c.raw = self->c.cgfd = -1; + rmdir(V1ROOT "/open/other"); + rmdir(V1ROOT "/open/empty"); + EXPECT_EQ(rmdir(V1ROOT "/open"), 0); + EXPECT_EQ(umount(V1ROOT), 0); + rmdir(V1ROOT); +} + +/* Nor does it help to own the cgroup, as a cell of one id does what its maker owns. */ +TEST_F(cell, changes_nothing_outside_its_budget_that_it_owns) +{ + struct stat st; + int status; + pid_t pid; + char c = 0; + bool v1; + + ASSERT_EQ(chown(CGROOT, 1000, 1000), 0); + ASSERT_EQ(chown(CGROOT "/cgroup.procs", 1000, 1000), 0); + ASSERT_EQ(mkdir(CGROOT "/theirs", 0755), 0); + ASSERT_EQ(mkdir(CGROOT "/theirs/empty", 0755), 0); + ASSERT_EQ(chown(CGROOT "/theirs", 1000, 1000), 0); + ASSERT_EQ(chown(CGROOT "/theirs/empty", 1000, 1000), 0); + ASSERT_EQ(chown(CGROOT "/theirs/cgroup.procs", 1000, 1000), 0); + ASSERT_EQ(chown(CGROOT "/theirs/cgroup.max.depth", 1000, 1000), 0); + ASSERT_EQ(setxattr(CGROOT "/theirs", "user.host", "h", 1, 0), 0); + mkdir(V1ROOT, 0755); + v1 = !mount("none", V1ROOT, "cgroup", 0, "none,name=celltest"); + if (v1) { + ASSERT_EQ(mkdir(V1ROOT "/theirs", 0755), 0); + ASSERT_EQ(mkdir(V1ROOT "/theirs/empty", 0755), 0); + ASSERT_EQ(chown(V1ROOT "/theirs", 1000, 1000), 0); + ASSERT_EQ(chown(V1ROOT "/theirs/empty", 1000, 1000), 0); + } + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + struct cellctx c; + int step, pidfd, dir, procs, legacy; + + if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) || + setresuid(1000, 1000, 1000) || + prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)) + _exit(1); + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) + _exit(2); + if (write_file("/proc/self/setgroups", "deny") || + write_file("/proc/self/uid_map", "1000 1000 1") || + write_file("/proc/self/gid_map", "1000 1000 1")) + _exit(3); + step = cell_build(&c, &(struct build){ .ids = 1 }); + if (step) + _exit(20 + step); + dir = open(CGROOT "/theirs", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + /* Not cgroup.procs, which has more reasons to refuse. */ + procs = open(CGROOT "/theirs/cgroup.max.depth", O_WRONLY | O_CLOEXEC); + pidfd = prepare(c.cfd, "outside-own", NULL, NULL); + if (dir < 0 || procs < 0 || pidfd < 0 || + pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, dir) || + pidfd_configure(pidfd, PIDFD_SET_FD, "4", NULL, procs)) + _exit(4); + legacy = open(V1ROOT "/theirs", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (v1 && (legacy < 0 || + pidfd_configure(pidfd, PIDFD_SET_FD, "5", NULL, legacy))) + _exit(6); + if (pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0)) + _exit(7); + _exit(cell_wait(c.cfd) ? 5 : 0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); + + EXPECT_EQ(stat(CGROOT "/theirs", &st), 0); + EXPECT_EQ(st.st_mode & 07777, 0755); + EXPECT_EQ(stat(CGROOT "/theirs/empty", &st), 0); + EXPECT_EQ(getxattr(CGROOT "/theirs", "user.host", &c, 1), 1); + EXPECT_EQ(getxattr(CGROOT "/theirs", "user.cell", &c, 1), -1); + + if (v1) { + EXPECT_EQ(stat(V1ROOT "/theirs/empty", &st), 0); + rmdir(V1ROOT "/theirs/other"); + rmdir(V1ROOT "/theirs/mine"); + rmdir(V1ROOT "/theirs/empty"); + EXPECT_EQ(rmdir(V1ROOT "/theirs"), 0); + EXPECT_EQ(umount(V1ROOT), 0); + } + rmdir(V1ROOT); + rmdir(CGROOT "/theirs/mine"); + rmdir(CGROOT "/theirs/empty"); + EXPECT_EQ(rmdir(CGROOT "/theirs"), 0); + EXPECT_EQ(chown(CGROOT, 0, 0), 0); + EXPECT_EQ(chown(CGROOT "/cgroup.procs", 0, 0), 0); +} + +TEST_F(cell, unprivileged_builder) +{ + int status; + pid_t pid; + + ASSERT_EQ(chown(CGROOT, 1000, 1000), 0); + ASSERT_EQ(chown(CGROOT "/cgroup.procs", 1000, 1000), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + struct cellctx c; + int step; + + /* Dumpable, or /proc/self is root's. */ + if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) || + setresuid(1000, 1000, 1000) || + prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)) + _exit(1); + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) + _exit(2); + if (write_file("/proc/self/setgroups", "deny") || + write_file("/proc/self/uid_map", "1000 1000 1") || + write_file("/proc/self/gid_map", "1000 1000 1")) + _exit(3); + step = cell_build(&c, &(struct build){ .ids = 1 }); + if (step) + _exit(20 + step); + if (spawn(c.cfd, NULL, "exit", "0", NULL) < 0) + _exit(4); + _exit(cell_wait(c.cfd) ? 5 : 0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); + EXPECT_EQ(chown(CGROOT, 0, 0), 0); + EXPECT_EQ(chown(CGROOT "/cgroup.procs", 0, 0), 0); +} + +/* What the "caps" slot takes. */ +TEST_F(cell, caps_slot) +{ + struct cell_info info = { .mask = CELL_INFO_CAPS }; + __u64 caps[2] = { CAP(CHOWN) | CAP(KILL), 0 }; + char buf[16] = {}; + int cfd, fd, last; + + fd = open("/proc/sys/kernel/cap_last_cap", O_RDONLY); + ASSERT_GE(fd, 0); + ASSERT_GT(read(fd, buf, sizeof(buf) - 1), 0); + close(fd); + last = atoi(buf); + ASSERT_LT(last, 63); + + cfd = cell_create(CELL_SELF, 0); + ASSERT_GE(cfd, 0); + ASSERT_EQ(cell_info(cfd, &info, sizeof(info), 0), 0); + EXPECT_EQ(info.mask & CELL_INFO_CAPS, CELL_INFO_CAPS); + EXPECT_EQ(info.caps, 0); + + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", caps, 4), -1); + EXPECT_EQ(errno, EINVAL); + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", caps, 16), -1); + EXPECT_EQ(errno, EINVAL); + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", (void *)1, 8), -1); + EXPECT_EQ(errno, EFAULT); + EXPECT_EQ(cell_configure(cfd, CELL_SET_STRING, "caps", "chown", 0), -1); + EXPECT_EQ(errno, EINVAL); + + /* Nothing a cell writes imparts privilege, whatever the builder thinks of it. */ + caps[1] = caps[0] | CAP(SETFCAP); + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", &caps[1], 8), -1); + EXPECT_EQ(errno, EINVAL); + /* There is no such capability. */ + caps[1] = caps[0] | 2ULL << last; + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", &caps[1], 8), -1); + EXPECT_EQ(errno, EINVAL); + caps[1] = caps[0] | 1ULL << 63; + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", &caps[1], 8), -1); + EXPECT_EQ(errno, EINVAL); + ASSERT_EQ(cell_info(cfd, &info, sizeof(info), 0), 0); + EXPECT_EQ(info.caps, 0); + + /* The last word counts. */ + caps[1] = 1ULL << last; + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", &caps[1], 8), 0); + EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", caps, 8), 0); + ASSERT_EQ(cell_info(cfd, &info, sizeof(info), 0), 0); + EXPECT_EQ(info.caps, caps[0]); + close(cfd); + + /* Until the seal. */ + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .caps = caps[0] }), 0); + caps[1] = 0; + EXPECT_EQ(cell_configure(self->c.cfd, CELL_SET_BINARY, "caps", &caps[1], 8), -1); + EXPECT_EQ(errno, EBUSY); + ASSERT_EQ(cell_info(self->c.cfd, &info, sizeof(info), 0), 0); + EXPECT_EQ(info.caps, caps[0]); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/cell/cell_spawn_test.c b/tools/testing/selftests/cell/cell_spawn_test.c new file mode 100644 index 000000000000..e5f0250be4db --- /dev/null +++ b/tools/testing/selftests/cell/cell_spawn_test.c @@ -0,0 +1,559 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Tests for cells, spawning: empty pidfds, and what a new task does and does + * not start out with. + */ +#include "cell_test.h" + +TEST_F(cell, nothing_inherited) +{ + /* Things a fork would have passed on. */ + int junk = open("/etc/passwd", O_RDONLY); + + ASSERT_GE(junk, 0); + ASSERT_EQ(chdir("/etc"), 0); + setenv("SECRET", "hunter2", 1); + RUN_IN_CELL("basic"); + close(junk); +} + +/* What pidfd_open() takes with PIDFD_EMPTY. */ +TEST_F(cell, empty_pidfd_open) +{ + int fd; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + + /* Outside of a cell it makes sense, and is not done yet. */ + EXPECT_EQ(pidfd_open_empty(-EBADF, 0), -1); + EXPECT_EQ(errno, EOPNOTSUPP); + EXPECT_EQ(pidfd_open_empty(-1, 0), -1); + EXPECT_EQ(errno, EBADF); + fd = open("/dev/null", O_RDONLY | O_CLOEXEC); + ASSERT_GE(fd, 0); + EXPECT_EQ(pidfd_open_empty(fd, 0), -1); + EXPECT_EQ(errno, EINVAL); + close(fd); + EXPECT_EQ(pidfd_open_empty(self->c.cfd, O_EXCL /* PIDFD_THREAD */), -1); + EXPECT_EQ(errno, EINVAL); + + fd = pidfd_open_empty(self->c.cfd, 0); + ASSERT_GE(fd, 0); + EXPECT_EQ(fcntl(fd, F_GETFD), FD_CLOEXEC); + close(fd); + /* Nothing came of it, and the cell is none the worse. */ + EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED); +} + +/* The "Pid:" line of the fdinfo of a pidfd, or -2. */ +static int pidfd_fdinfo_pid(int pidfd) +{ + char path[64], line[128]; + int nr = -2; + FILE *f; + + snprintf(path, sizeof(path), "/proc/self/fdinfo/%d", pidfd); + f = fopen(path, "r"); + if (!f) + return -2; + while (fgets(line, sizeof(line), f)) { + if (sscanf(line, "Pid:\t%d", &nr) == 1) + break; + } + fclose(f); + return nr; +} + +/* It is a pidfd like any other. There just is no task yet. */ +TEST_F(cell, empty_pidfd) +{ + struct pidfd_info_v0 info = {}; + struct stat st, st_other; + struct pollfd pfd; + int pidfd, other; + siginfo_t si; + pid_t pid; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + + pidfd = prepare(self->c.cfd, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + + /* Nothing exists, and nothing has happened to the cell. */ + EXPECT_EQ(cg_populated(self->c.cg), 0); + EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED); + + /* Whatever it is passed to finds no task. */ + EXPECT_EQ(syscall(SYS_pidfd_send_signal, pidfd, 0, NULL, 0), -1); + EXPECT_EQ(errno, ESRCH); + EXPECT_EQ(syscall(SYS_pidfd_getfd, pidfd, 0, 0), -1); + EXPECT_EQ(errno, ESRCH); + EXPECT_EQ(ioctl(pidfd, PIDFD_GET_INFO_V0, &info), -1); + EXPECT_EQ(errno, ESRCH); + /* Nor the record of an exit: nothing has exited. */ + info.mask = PIDFD_INFO_EXIT_V0; + EXPECT_EQ(ioctl(pidfd, PIDFD_GET_INFO_V0, &info), -1); + EXPECT_EQ(errno, ESRCH); + info.mask = 0; + /* -1 is for a task that is gone. */ + EXPECT_EQ(pidfd_fdinfo_pid(pidfd), 0); + EXPECT_EQ(setns(pidfd, CLONE_NEWNET), -1); + EXPECT_EQ(errno, ESRCH); + EXPECT_EQ(waitid(P_PIDFD, pidfd, &si, WEXITED | WNOHANG), -1); + EXPECT_EQ(errno, ECHILD); + /* No task yet is not the same as no task any more. */ + pfd = (struct pollfd){ .fd = pidfd, .events = POLLIN }; + EXPECT_EQ(poll(&pfd, 1, 100), 0); + ASSERT_EQ(fstat(pidfd, &st), 0); + + /* What may not be given. */ + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, self->c.cfd), -1); + EXPECT_EQ(errno, EPERM); + other = open("/proc/self/ns/net", O_RDONLY); + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, other), -1); + EXPECT_EQ(errno, EPERM); + close(other); + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, pidfd), -1); + EXPECT_EQ(errno, EPERM); + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "nonsense", NULL, 0), -1); + EXPECT_EQ(errno, EINVAL); + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_BINARY, "argv", "unterminated", 12), -1); + EXPECT_EQ(errno, EINVAL); + /* An id the cell does not have. */ + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_BINARY, "cred", + &(struct pidfd_cred){ .uid = 70000 }, + sizeof(struct pidfd_cred)), -1); + EXPECT_EQ(errno, EINVAL); + + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0); + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), -1); + EXPECT_EQ(errno, EBUSY); + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_STRING, "exec", "/x", 0), -1); + EXPECT_EQ(errno, EBUSY); + + /* Now there is one: in the cell, as the cell's root. */ + pid = pidfd_nr(pidfd); + ASSERT_GT(pid, 0); + EXPECT_GE(host_uid(pid), (long)CELL_WINDOW_FIRST); + EXPECT_EQ(cg_populated(self->c.cg), 1); + EXPECT_EQ(cell_state(self->c.cfd), CELL_RUNNING); + EXPECT_EQ(syscall(SYS_pidfd_send_signal, pidfd, 0, NULL, 0), 0); + EXPECT_EQ(poll(&pfd, 1, 100), 0); + EXPECT_EQ(pidfd_fdinfo_pid(pidfd), pid); + + /* Another pidfd on it is the same inode, which did not change either. */ + other = sys_pidfd_open(pid, 0); + ASSERT_GE(other, 0); + ASSERT_EQ(fstat(other, &st_other), 0); + EXPECT_EQ(st.st_ino, st_other.st_ino); + ASSERT_EQ(fstat(pidfd, &st_other), 0); + EXPECT_EQ(st.st_ino, st_other.st_ino); + /* But only the file pidfd_open() returned for PIDFD_EMPTY configures. */ + EXPECT_EQ(pidfd_configure(other, PIDFD_CMD_START, NULL, NULL, 0), -1); + EXPECT_EQ(errno, EPERM); + close(other); + close(pidfd); + + /* A second one, into the running cell. */ + pidfd = spawn(self->c.cfd, NULL, "second", NULL, NULL); + ASSERT_GE(pidfd, 0); + EXPECT_EQ(task_wait(pidfd), 0); + close(pidfd); + EXPECT_EQ(cell_state(self->c.cfd), CELL_RUNNING); +} + +/* + * An empty pidfd reserves nothing in the cell. Whichever is started first is + * the init, and one that is left empty keeps the cell from nothing. + */ +TEST_F(cell, empty_pidfd_reserves_nothing) +{ + int a, b, c; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + + /* "basic" insists on being pid 1, "second" on not being it. */ + a = prepare(self->c.cfd, "second", NULL, NULL); + b = prepare(self->c.cfd, "pause", NULL, NULL); + c = prepare(self->c.cfd, "pause", NULL, NULL); + ASSERT_GE(a, 0); + ASSERT_GE(b, 0); + ASSERT_GE(c, 0); + ASSERT_EQ(pidfd_configure(b, PIDFD_CMD_START, NULL, NULL, 0), 0); + ASSERT_EQ(pidfd_configure(a, PIDFD_CMD_START, NULL, NULL, 0), 0); + EXPECT_EQ(task_wait(a), 0); + + /* The cell dies with @c still there, and then nothing comes of @c. */ + ASSERT_EQ(cell_kill(self->c.cfd, 0), 0); + EXPECT_EQ(cell_wait(self->c.cfd), SIGKILL); + EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD); + EXPECT_EQ(pidfd_configure(c, PIDFD_CMD_START, NULL, NULL, 0), -1); + EXPECT_EQ(errno, ESRCH); + close(a); + close(b); + close(c); +} + +/* + * A start that fails before there is a task. The pidfd then says that there is + * none and never will be, every way it can be asked, and the cell is as it was. + */ +TEST_F(cell, start_fails_without_a_task) +{ + struct pidfd_info_v0 info = { .mask = PIDFD_INFO_EXIT_V0 }; + struct pollfd pfd; + char path[192]; + int pidfd; + + write_file(CGROOT "/cgroup.subtree_control", "+pids"); + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + snprintf(path, sizeof(path), "%s/pids.max", self->c.cg); + if (write_file(path, "0")) { + TH_LOG("no pids controller, skipped"); + return; + } + + pidfd = prepare(self->c.cfd, "pause", NULL, NULL); + ASSERT_GE(pidfd, 0); + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), -1); + EXPECT_EQ(errno, EAGAIN); + EXPECT_EQ(cg_populated(self->c.cg), 0); + + /* It can be tried only once. */ + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), -1); + EXPECT_EQ(errno, EBUSY); + pfd = (struct pollfd){ .fd = pidfd, .events = POLLIN }; + EXPECT_EQ(poll(&pfd, 1, 1000), 1); + EXPECT_EQ(ioctl(pidfd, PIDFD_GET_INFO_V0, &info), -1); + EXPECT_EQ(errno, ESRCH); + EXPECT_EQ(pidfd_fdinfo_pid(pidfd), -1); + EXPECT_EQ(syscall(SYS_pidfd_send_signal, pidfd, 0, NULL, 0), -1); + EXPECT_EQ(errno, ESRCH); + close(pidfd); + + /* Nothing ever ran, so the next one is the init of the cell. */ + EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED); + ASSERT_EQ(write_file(path, "max"), 0); + pidfd = spawn(self->c.cfd, NULL, "basic", NULL, NULL); + ASSERT_GE(pidfd, 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + close(pidfd); +} + +/* The pidfd may be kept for long. What was configured is not kept with it. */ +static int usr1_seen; + +static void on_usr1(int sig) +{ + __atomic_fetch_add(&usr1_seen, 1, __ATOMIC_RELAXED); +} + +/* + * A fork is started over if a signal is pending for whoever forks, and nobody gets + * to see that. A start is no different, and it has not been tried by then. + */ +TEST_F_TIMEOUT(cell, start_is_started_over_after_a_signal, 120) +{ + struct sigaction sa = { .sa_handler = on_usr1, .sa_flags = SA_RESTART }; + pid_t me = getpid(), pest; + int init, p, i, seen, failed = 0; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + init = spawn(self->c.cfd, NULL, "pause", NULL, NULL); + ASSERT_GE(init, 0); + ASSERT_EQ(sigaction(SIGUSR1, &sa, NULL), 0); + pest = fork(); + ASSERT_GE(pest, 0); + if (!pest) { + for (;;) { + kill(me, SIGUSR1); + usleep(300); + } + } + + for (i = 0; i < 300; i++) { + p = prepare(self->c.cfd, "exit", "0", NULL); + ASSERT_GE(p, 0); + if (pidfd_configure(p, PIDFD_CMD_START, NULL, NULL, 0)) { + failed++; + TH_LOG("start %d: errno %d", i, errno); + } + close(p); + } + kill(pest, SIGKILL); + waitpid(pest, NULL, 0); + signal(SIGUSR1, SIG_DFL); + + seen = __atomic_load_n(&usr1_seen, __ATOMIC_RELAXED); + TH_LOG("%d signals", seen); + EXPECT_GT(seen, 100); + EXPECT_EQ(failed, 0); + close(init); +} + +TEST_F(cell, pidfd_keeps_no_file) +{ + int p[2], pidfd; + char c; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + ASSERT_EQ(pipe2(p, O_CLOEXEC), 0); + pidfd = prepare(self->c.cfd, "exit", "0", NULL); + ASSERT_GE(pidfd, 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, p[1]), 0); + close(p[1]); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + /* The last writer is gone, though the pidfd is not. */ + ASSERT_EQ(fcntl(p[0], F_SETFL, O_NONBLOCK), 0); + EXPECT_EQ(read(p[0], &c, 1), 0); + close(p[0]); + close(pidfd); +} + +TEST_F(cell, exec_failure_is_reported) +{ + int pidfd; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + pidfd = prepare(self->c.cfd, "basic", NULL, NULL); + ASSERT_GE(pidfd, 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_STRING, "exec", "/no/such/file", 0), 0); + EXPECT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), -1); + EXPECT_EQ(errno, ENOENT); + /* That was the cell's init. */ + EXPECT_EQ(cell_wait(self->c.cfd), SIGKILL); + close(pidfd); +} + +TEST_F(cell, cred) +{ + int pidfd; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + pidfd = prepare(self->c.cfd, "uid", "33", "44"); + ASSERT_GE(pidfd, 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_BINARY, "cred", + &(struct pidfd_cred){ .uid = 33, .gid = 44 }, + sizeof(struct pidfd_cred)), 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0); + EXPECT_EQ(cell_wait(self->c.cfd), 0); + close(pidfd); +} + +/* Send @fd over @sock, by SCM_RIGHTS. */ +static int send_one_fd(int sock, int fd) +{ + char buf[CMSG_SPACE(sizeof(int))] = {}; + struct iovec iov = { .iov_base = "x", .iov_len = 1 }; + struct msghdr msg = { + .msg_iov = &iov, .msg_iovlen = 1, + .msg_control = buf, .msg_controllen = sizeof(buf), + }; + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + memcpy(CMSG_DATA(cmsg), &fd, sizeof(int)); + return sendmsg(sock, &msg, 0) == 1 ? 0 : -1; +} + +/* + * A socket that was to go to a task which then never started is nobody's but + * the host's, and goes on working as it did. + */ +TEST_F(cell, failed_start_leaves_a_socket_alone) +{ + int sp[2], pidfd; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sp), 0); + ASSERT_EQ(send_one_fd(sp[0], sp[0]), 0); + + pidfd = prepare(self->c.cfd, "exit", "0", NULL); + ASSERT_GE(pidfd, 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, sp[1]), 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_STRING, "exec", "/no-such-file", 0), 0); + ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), -1); + ASSERT_EQ(errno, ENOENT); + close(pidfd); + + EXPECT_EQ(send_one_fd(sp[0], sp[0]), 0); + close(sp[0]); + close(sp[1]); +} + +static void no_handler(int sig) +{ +} + +/* Nothing is inherited: not what the spawner ignores, handles or blocks. */ +TEST_F(cell, signals_start_from_scratch) +{ + int status; + pid_t pid; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + sigset_t set; + + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + signal(SIGCHLD, SIG_IGN); + signal(SIGUSR2, no_handler); + sigemptyset(&set); + sigaddset(&set, SIGUSR1); + sigaddset(&set, SIGTERM); + if (sigprocmask(SIG_BLOCK, &set, NULL) || raise(SIGUSR1)) + _exit(1); + if (spawn(self->c.cfd, NULL, "signals", NULL, NULL) < 0) + _exit(2); + _exit(cell_wait(self->c.cfd) ? 3 : 0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); +} + +/* From here on system call @nr fails with EACCES, for the caller and its issue. */ +static int seccomp_deny(int nr) +{ + struct sock_filter insns[] = { + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, nr, 0, 1), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EACCES), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), + }; + struct sock_fprog prog = { .len = ARRAY_SIZE(insns), .filter = insns }; + + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) + return -1; + return prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog); +} + +/* What a confined task spawns is confined as it is itself. */ +TEST_F(cell, seccomp_filters_are_inherited) +{ + int status; + pid_t pid; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + if (seccomp_deny(SYS_getrusage)) + _exit(1); + if (spawn(self->c.cfd, NULL, "seccomp", NULL, NULL) < 0) + _exit(2); + _exit(cell_wait(self->c.cfd) ? 3 : 0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); +} + +/* + * A listener on a filter stays with the spawner, outside. It can answer for the + * system calls of a task of the cell. It cannot put a file into it. + */ +TEST_F(cell, seccomp_listener_adds_no_file) +{ + int status; + pid_t pid; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + struct sock_filter insns[] = { + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_getrusage, 0, 1), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_USER_NOTIF), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), + }; + struct sock_fprog prog = { .len = ARRAY_SIZE(insns), .filter = insns }; + struct seccomp_notif_addfd addfd = {}; + struct seccomp_notif_resp resp = {}; + struct seccomp_notif req = {}; + int listener, round, task; + pid_t home; + + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) + _exit(1); + listener = syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, + SECCOMP_FILTER_FLAG_NEW_LISTENER, &prog); + if (listener < 0) + _exit(2); + + /* First at home, where it works, then into the cell. */ + for (round = 0; round < 2; round++) { + if (!round) { + home = fork(); + if (home < 0) + _exit(3); + if (!home) { + struct rusage ru; + + _exit(getrusage(RUSAGE_SELF, &ru) == -1 && + errno == EACCES ? 0 : 1); + } + task = syscall(SYS_pidfd_open, home, 0); + } else { + task = spawn(self->c.cfd, NULL, "seccomp", NULL, NULL); + } + if (task < 0) + _exit(4); + + /* Nothing shows on a listener when the task is gone. */ + if (wait_for(listener, task)) + _exit(11); + close(task); + memset(&req, 0, sizeof(req)); + if (ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req)) + _exit(5); + addfd.id = req.id; + addfd.srcfd = self->c.cfd; + if (!round) { + if (ioctl(listener, SECCOMP_IOCTL_NOTIF_ADDFD, &addfd) < 0) + _exit(6); + } else if (ioctl(listener, SECCOMP_IOCTL_NOTIF_ADDFD, &addfd) != -1 || + errno != EPERM) { + _exit(7); + } + resp.id = req.id; + resp.error = -EACCES; + if (ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp)) + _exit(8); + if (!round && (waitpid(home, &status, 0) != home || status)) + _exit(9); + } + _exit(cell_wait(self->c.cfd) ? 10 : 0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); +} + +/* The exec of an embryo is no system call. This is how a filter prevents it. */ +TEST_F(cell, seccomp_can_deny_a_start) +{ + int status; + pid_t pid; + + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); + pid = fork(); + ASSERT_GE(pid, 0); + if (!pid) { + if (seccomp_deny(__NR_pidfd_configure)) + _exit(1); + if (spawn(self->c.cfd, NULL, "exit", "0", NULL) >= 0) + _exit(2); + _exit(errno == EACCES ? 0 : 3); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0); + /* Nothing ever ran. */ + EXPECT_EQ(cg_populated(self->c.cg), 0); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/cell/cell_test.h b/tools/testing/selftests/cell/cell_test.h new file mode 100644 index 000000000000..fdd3a55358dd --- /dev/null +++ b/tools/testing/selftests/cell/cell_test.h @@ -0,0 +1,586 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Cells: build them, run things in them, kill them, and try everything that is + * not supposed to work. This is what the tests share: how to build a cell, how + * to spawn into one, and the fixture. + * + * They need root, cgroup2 and tmpfs. cell_child must sit next to the binaries. + */ +#ifndef CELL_TEST_H +#define CELL_TEST_H + +#define _GNU_SOURCE +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <poll.h> +#include <sched.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/auxv.h> +#include <sys/mount.h> +#include <grp.h> +#include <net/if.h> +#include <sys/ioctl.h> +#include <sys/prctl.h> +#include <sys/ptrace.h> +#include <sys/resource.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/sysmacros.h> +#include <sys/un.h> +#include <sys/wait.h> +#include <sys/xattr.h> +#include <linux/capability.h> +#include <linux/keyctl.h> +#include <linux/filter.h> +#include <linux/fs.h> +#include <linux/loop.h> +#include <linux/mount.h> +#include <linux/seccomp.h> + +#include "../kselftest_harness.h" +#include "cell_util.h" + +#ifndef OPEN_TREE_NAMESPACE +#define OPEN_TREE_NAMESPACE (1 << 1) +#endif +#ifndef MOUNT_ATTR_IDMAP +#define MOUNT_ATTR_IDMAP 0x00100000 +#endif + +static inline int sys_fsopen(const char *fs, unsigned int flags) +{ + return syscall(SYS_fsopen, fs, flags); +} + +static inline int sys_fsconfig(int fd, unsigned int cmd, const char *key, const void *val, int aux) +{ + return syscall(SYS_fsconfig, fd, cmd, key, val, aux); +} + +static inline int sys_fsmount(int fd, unsigned int flags, unsigned int attr) +{ + return syscall(SYS_fsmount, fd, flags, attr); +} + +static inline int sys_move_mount(int from, const char *fp, int to, const char *tp, + unsigned int flags) +{ + return syscall(SYS_move_mount, from, fp, to, tp, flags); +} + +static inline int sys_open_tree(int dfd, const char *path, unsigned int flags) +{ + return syscall(SYS_open_tree, dfd, path, flags); +} + +static inline int sys_mount_setattr(int dfd, const char *path, unsigned int flags, + struct mount_attr *attr, size_t size) +{ + return syscall(SYS_mount_setattr, dfd, path, flags, attr, size); +} + +static inline int sys_pidfd_open(pid_t pid, unsigned int flags) +{ + return syscall(SYS_pidfd_open, pid, flags); +} + +#define CGROOT "/tmp/cell_test.cgroup" + +static int child_fd = -1; +static int cg_seq; + +/* What goes into a cell. */ +#define CAP(x) (1ULL << CAP_##x) + +struct build { + unsigned int ids; /* 0: leave alone */ + __u64 caps; /* likewise */ + bool no_idmap; /* leave the tree as it is */ + bool shared; /* make the tree a peer of something */ + bool no_seal; + int data; /* a detached mount to put at /data, or 0 */ + int net; /* a network namespace to share, or 0 */ + const char *net_str; /* or "new" */ + __u64 net_rights; /* CELL_NET_* */ + bool tun; /* a node for /dev/net/tun, at /tun */ + bool keep_root; /* leave a file descriptor on the mount namespace */ + bool cgroupfs; /* all of cgroupfs at /data, not just the budget */ + const char *bind; /* or this, as it is */ + int overlay; /* or an overlay, see OVL_* */ + const char *bind_file; /* a file of the host's, as it is, at /bound */ +}; + +#define OVL_IDMAPPED 1 /* the overlay is idmapped for the cell, as it should be */ +#define OVL_LOWER_IDMAPPED 2 /* its lower layer is, and nothing else */ + +struct cellctx { + int cfd; + int tree; /* the detached tmpfs that becomes "/" */ + int raw; /* the same filesystem as the host sees it */ + int cgfd; + int rootfd; /* see keep_root */ + int layers; /* what an overlay is made of, as the host sees it */ + char cg[128]; +}; + +static inline int copy_child(int dirfd, const char *name) +{ + static char buf[65536]; + off_t off = 0; + ssize_t n; + int out; + + out = openat(dirfd, name, O_CREAT | O_WRONLY, 0755); + if (out < 0) + return -1; + while ((n = pread(child_fd, buf, sizeof(buf), off)) > 0) { + if (write(out, buf, n) != n) + return -1; + off += n; + } + close(out); + return n; +} + +static inline int new_tmpfs(void) +{ + int fs, mnt; + + fs = sys_fsopen("tmpfs", FSOPEN_CLOEXEC); + if (fs < 0) + return -1; + if (sys_fsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) + return -1; + mnt = sys_fsmount(fs, FSMOUNT_CLOEXEC, 0); + close(fs); + return mnt; +} + +/* Returns 0, or the step that failed with errno set. */ +/* VFS_CAP_REVISION_2, cap_net_raw permitted and effective. */ +static const unsigned char fscap_v2[20] = { + 0x01, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0x00, +}; + +static inline int put_file(int dirfd, const char *name) +{ + int fd = openat(dirfd, name, O_CREAT | O_WRONLY, 0644); + + if (fd < 0 || write(fd, "lower", 5) != 5) + return -1; + return close(fd); +} + +/* An overlay at /data. Its layers are in a tmpfs of the host's, c->layers. */ +static inline int add_overlay(struct cellctx *c, int how) +{ + struct mount_attr attr = { .attr_set = MOUNT_ATTR_IDMAP, .userns_fd = c->cfd }; + int lower, rw, upper, work, fs, mnt; + + c->layers = new_tmpfs(); + if (c->layers < 0 || mkdirat(c->layers, "lower", 0755) || mkdirat(c->layers, "rw", 0755) || + mkdirat(c->layers, "rw/upper", 0755) || mkdirat(c->layers, "rw/work", 0755) || + put_file(c->layers, "lower/file")) + return -1; + lower = sys_open_tree(c->layers, "lower", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + rw = sys_open_tree(c->layers, "rw", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + if (lower < 0 || rw < 0) + return -2; + if (how == OVL_LOWER_IDMAPPED && + sys_mount_setattr(lower, "", AT_EMPTY_PATH, &attr, sizeof(attr))) + return -3; + upper = openat(rw, "upper", O_PATH | O_DIRECTORY | O_CLOEXEC); + work = openat(rw, "work", O_PATH | O_DIRECTORY | O_CLOEXEC); + fs = sys_fsopen("overlay", FSOPEN_CLOEXEC); + if (fs < 0) + return -ENODEV; + if (sys_fsconfig(fs, FSCONFIG_SET_FD, "lowerdir+", NULL, lower) || + sys_fsconfig(fs, FSCONFIG_SET_FD, "upperdir", NULL, upper) || + sys_fsconfig(fs, FSCONFIG_SET_FD, "workdir", NULL, work) || + sys_fsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) + return -4; + mnt = sys_fsmount(fs, FSMOUNT_CLOEXEC, 0); + if (mnt < 0) + return -5; + if (how == OVL_IDMAPPED && + sys_mount_setattr(mnt, "", AT_EMPTY_PATH, &attr, sizeof(attr))) + return -6; + if (sys_move_mount(mnt, "", c->tree, "data", MOVE_MOUNT_F_EMPTY_PATH)) + return -7; + close(mnt); + close(fs); + close(work); + close(upper); + close(rw); + close(lower); + return 0; +} + +static inline int cell_build(struct cellctx *c, const struct build *b) +{ + struct mount_attr attr = { .attr_set = MOUNT_ATTR_IDMAP }; + int pidns, fs, proc, nsfd; + + memset(c, 0, sizeof(*c)); + c->cfd = c->tree = c->raw = c->cgfd = c->rootfd = c->layers = -1; + + snprintf(c->cg, sizeof(c->cg), CGROOT "/cell.%d.%d", getpid(), cg_seq++); + if (mkdir(c->cg, 0755)) + return 1; + c->cgfd = open(c->cg, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (c->cgfd < 0) + return 2; + + c->cfd = cell_create(CELL_SELF, 0); + if (c->cfd < 0) + return 3; + if (b->ids && cell_configure(c->cfd, CELL_SET_BINARY, "ids", &b->ids, sizeof(b->ids))) + return 4; + if (b->caps && cell_configure(c->cfd, CELL_SET_BINARY, "caps", &b->caps, sizeof(b->caps))) + return 4; + + /* The tree, from scratch, one mount at a time. */ + c->tree = new_tmpfs(); + if (c->tree < 0) + return 5; + if (copy_child(c->tree, "cell_child") || mkdirat(c->tree, "proc", 0755) || + mkdirat(c->tree, "data", 0755) || + mkdirat(c->tree, "tmp", 01777) || fchmodat(c->tree, "tmp", 01777, 0)) + return 6; + c->raw = sys_open_tree(c->tree, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + if (c->raw < 0) + return 6; + if (b->bind_file && put_file(c->tree, "bound")) + return 6; + if (b->tun && (mknodat(c->tree, "tun", S_IFCHR | 0666, makedev(10, 200)) || + fchmodat(c->tree, "tun", 0666, 0))) + return 6; + pidns = openat(c->tree, "su", O_CREAT | O_WRONLY, 0755); + if (pidns < 0 || fchmod(pidns, 06755) || + fsetxattr(pidns, "security.capability", fscap_v2, sizeof(fscap_v2), 0)) + return 6; + close(pidns); + if (!b->no_idmap && b->ids > 1) { + attr.userns_fd = c->cfd; + if (sys_mount_setattr(c->tree, "", AT_EMPTY_PATH, &attr, sizeof(attr))) + return 7; + } + + pidns = cell_open(c->cfd, CELL_OPEN_SLOT, "pids", NULL, 0); + if (pidns < 0) + return 8; + fs = sys_fsopen("proc", FSOPEN_CLOEXEC); + if (fs < 0 || sys_fsconfig(fs, FSCONFIG_SET_FD, "pidns", NULL, pidns) || + sys_fsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) + return 9; + proc = sys_fsmount(fs, FSMOUNT_CLOEXEC, 0); + if (proc < 0) + return 10; + if (sys_move_mount(proc, "", c->tree, "proc", MOVE_MOUNT_F_EMPTY_PATH)) + return 11; + close(pidns); + close(fs); + close(proc); + + if (b->data) { + if (b->ids > 1 && + sys_mount_setattr(b->data, "", AT_EMPTY_PATH, &attr, sizeof(attr))) + return 19; + if (sys_move_mount(b->data, "", c->tree, "data", MOVE_MOUNT_F_EMPTY_PATH)) + return 20; + } + + if (b->cgroupfs || b->bind) { + int dir = open(b->bind ?: CGROOT, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + int mnt = sys_open_tree(dir, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | + OPEN_TREE_CLOEXEC); + + if (mnt < 0 || + sys_move_mount(mnt, "", c->tree, "data", MOVE_MOUNT_F_EMPTY_PATH)) + return 21; + close(mnt); + close(dir); + } + + if (b->overlay && add_overlay(c, b->overlay)) + return 22; + + if (b->bind_file) { + int mnt = sys_open_tree(AT_FDCWD, b->bind_file, + OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + + if (mnt < 0 || + sys_move_mount(mnt, "", c->tree, "bound", MOVE_MOUNT_F_EMPTY_PATH)) + return 23; + close(mnt); + } + + if (b->shared) { + struct mount_attr sh = { .propagation = MS_SHARED }; + + if (sys_mount_setattr(c->tree, "", AT_EMPTY_PATH | AT_RECURSIVE, &sh, sizeof(sh))) + return 12; + } + + nsfd = sys_open_tree(c->tree, "", AT_EMPTY_PATH | AT_RECURSIVE | + OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC); + if (nsfd < 0) + return 13; + + if (cell_configure(c->cfd, CELL_SET_FD, "root", NULL, nsfd)) + return 14; + if (b->keep_root) + c->rootfd = nsfd; + else + close(nsfd); + if (cell_configure(c->cfd, CELL_SET_FD, "budget", NULL, c->cgfd)) + return 15; + if (cell_configure(c->cfd, CELL_SET_STRING, "hostname", "celltest", 0)) + return 16; + if (b->net && cell_configure(c->cfd, CELL_SET_FD, "net", NULL, b->net)) + return 17; + if (b->net_str && cell_configure(c->cfd, CELL_SET_STRING, "net", b->net_str, 0)) + return 17; + if (b->net_rights && cell_configure(c->cfd, CELL_SET_BINARY, "net-rights", &b->net_rights, + sizeof(b->net_rights))) + return 23; + if (!b->no_seal && cell_configure(c->cfd, CELL_CMD_SEAL, NULL, NULL, 0)) + return 18; + return 0; +} + +static inline void cell_cleanup(struct cellctx *c) +{ + int i; + + if (c->cfd >= 0) { + cell_kill(c->cfd, 0); + close(c->cfd); + } + if (c->tree >= 0) + close(c->tree); + if (c->raw >= 0) + close(c->raw); + if (c->cgfd >= 0) + close(c->cgfd); + if (c->rootfd > 0) + close(c->rootfd); + if (c->layers > 0) + close(c->layers); + for (i = 0; i < 100 && rmdir(c->cg) && errno == EBUSY; i++) + usleep(20000); +} + +/* <linux/pidfd.h> does not get along with <fcntl.h>. */ +struct pidfd_info_v0 { + __u64 mask, cgroupid; + __u32 pid, tgid, ppid, ruid, rgid, euid, egid, suid, sgid, fsuid, fsgid; + __s32 exit_code; +}; +#define PIDFD_GET_INFO_V0 _IOWR(0xFF, 11, struct pidfd_info_v0) +#define PIDFD_INFO_EXIT_V0 (1 << 3) + +/* The pid of what a pidfd is on, as the caller sees it, or -1. */ +static inline pid_t pidfd_nr(int pidfd) +{ + struct pidfd_info_v0 info = {}; + + if (ioctl(pidfd, PIDFD_GET_INFO_V0, &info)) + return -1; + return info.pid; +} + +/* argv as a block of NUL terminated strings: "cell_child\0<a>\0<b>\0<c>\0" */ +static inline int set_argv(int pidfd, const char *a, const char *b, const char *c) +{ + char blk[256]; + int len = 0; + + len += sprintf(blk + len, "cell_child") + 1; + if (a) + len += sprintf(blk + len, "%s", a) + 1; + if (b) + len += sprintf(blk + len, "%s", b) + 1; + if (c) + len += sprintf(blk + len, "%s", c) + 1; + return pidfd_configure(pidfd, PIDFD_SET_BINARY, "argv", blk, len); +} + +/* There is no task, and so no pid, before it is started. */ +static inline int prepare(int cfd, const char *a, const char *b, const char *c) +{ + int pidfd = pidfd_open_empty(cfd, 0); + int null; + + if (pidfd < 0) + return -1; + null = open("/dev/null", O_RDWR | O_CLOEXEC); + if (pidfd_configure(pidfd, PIDFD_SET_STRING, "exec", "/cell_child", 0) || + set_argv(pidfd, a, b, c) || + pidfd_configure(pidfd, PIDFD_SET_BINARY, "envp", "CELL=yes", 9) || + pidfd_configure(pidfd, PIDFD_SET_FD, "0", NULL, null) || + pidfd_configure(pidfd, PIDFD_SET_FD, "1", NULL, STDERR_FILENO) || + pidfd_configure(pidfd, PIDFD_SET_FD, "2", NULL, STDERR_FILENO)) { + close(pidfd); + return -1; + } + close(null); + return pidfd; +} + +static inline int spawn(int cfd, pid_t *pid, const char *a, const char *b, const char *c) +{ + int pidfd = prepare(cfd, a, b, c); + + if (pidfd >= 0 && pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0)) { + close(pidfd); + return -1; + } + if (pidfd >= 0 && pid) + *pid = pidfd_nr(pidfd); + return pidfd; +} + +/* + * Wait for there to be something to read or to accept on @fd. Not for longer than + * there is the task of @pidfd to provide it: its death need not show on @fd. + */ +static inline int wait_for(int fd, int pidfd) +{ + struct pollfd p[2] = { + { .fd = fd, .events = POLLIN }, { .fd = pidfd, .events = POLLIN }, + }; + + if (poll(p, 2, 20000) <= 0) + return -1; + return p[0].revents & POLLIN ? 0 : -1; +} + +/* + * How the task of @pidfd ended, or -99. A pidfd is readable once the task has + * exited. How it did is on record only once it has been reaped, which nobody has to + * do for a task that was spawned, and which is when the pidfd hangs up. + */ +static inline int task_wait(int pidfd) +{ + struct pidfd_info_v0 info = { .mask = PIDFD_INFO_EXIT_V0 }; + struct pollfd p = { .fd = pidfd }; + + if (poll(&p, 1, 20000) != 1 || !(p.revents & POLLHUP)) + return -99; + if (ioctl(pidfd, PIDFD_GET_INFO_V0, &info) || !(info.mask & PIDFD_INFO_EXIT_V0)) + return -99; + return info.exit_code; +} + +/* How the cell ended, once it is dead, or -2 if that takes too long. */ +static inline int cell_wait(int cfd) +{ + struct pollfd pfd = { .fd = cfd, .events = POLLIN }; + int status = -1; + + if (poll(&pfd, 1, 20000) != 1) + return -2; + if (read(cfd, &status, sizeof(status)) != sizeof(status)) + return -1; + return status; +} + +static inline unsigned int cell_state(int cfd) +{ + struct cell_info info = { .mask = CELL_INFO_STATE }; + + if (cell_info(cfd, &info, sizeof(info), 0) || !(info.mask & CELL_INFO_STATE)) + return ~0U; + return info.state; +} + +/* Not by cgroup.procs, which does not list a task that is on its way out. */ +static inline int cg_populated(const char *cg) +{ + char path[192], buf[256] = {}; + int fd; + + snprintf(path, sizeof(path), "%s/cgroup.events", cg); + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + if (read(fd, buf, sizeof(buf) - 1) < 0) + buf[0] = 0; + close(fd); + if (strstr(buf, "populated 0")) + return 0; + return strstr(buf, "populated 1") ? 1 : -1; +} + +/* The kuid the host sees @pid run as. */ +static inline long host_uid(pid_t pid) +{ + char path[64]; + struct stat st; + + snprintf(path, sizeof(path), "/proc/%d", pid); + if (stat(path, &st)) + return -1; + return st.st_uid; +} + +static inline int write_file(const char *path, const char *val) +{ + int fd = open(path, O_WRONLY), ret; + + if (fd < 0) + return -1; + ret = write(fd, val, strlen(val)) < 0 ? -1 : 0; + close(fd); + return ret; +} + +FIXTURE(cell) { + struct cellctx c; +}; + +FIXTURE_SETUP(cell) +{ + char path[PATH_MAX], *slash; + + if (geteuid()) + SKIP(return, "needs root"); + if (cell_create(-1, 0) == -1 && errno == ENOSYS) + SKIP(return, "no cells in this kernel"); + + /* Next to this binary. */ + snprintf(path, sizeof(path) - 16, "%s", (char *)getauxval(AT_EXECFN)); + slash = strrchr(path, '/'); + strcpy(slash ? slash + 1 : path, "cell_child"); + child_fd = open(path, O_RDONLY | O_CLOEXEC); + ASSERT_GE(child_fd, 0); + + mkdir(CGROOT, 0755); + if (mount("none", CGROOT, "cgroup2", 0, NULL) && errno != EBUSY) + SKIP(return, "no cgroup2"); + self->c.cfd = self->c.tree = self->c.raw = self->c.cgfd = -1; +} + +FIXTURE_TEARDOWN(cell) +{ + cell_cleanup(&self->c); + umount2(CGROOT, MNT_DETACH); + close(child_fd); +} + +#define RUN_IN_CELL(what) do { \ + int pidfd; \ + ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0); \ + pidfd = spawn(self->c.cfd, NULL, what, NULL, NULL); \ + ASSERT_GE(pidfd, 0); \ + EXPECT_EQ(cell_wait(self->c.cfd), 0); \ + close(pidfd); \ +} while (0) + +#endif /* CELL_TEST_H */ diff --git a/tools/testing/selftests/cell/cell_util.h b/tools/testing/selftests/cell/cell_util.h new file mode 100644 index 000000000000..7a0946d87e5a --- /dev/null +++ b/tools/testing/selftests/cell/cell_util.h @@ -0,0 +1,108 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef CELL_UTIL_H +#define CELL_UTIL_H + +#include <fcntl.h> +#include <stdint.h> +#include <stddef.h> +#include <unistd.h> +#include <sys/syscall.h> +#include <linux/types.h> + +#include "../../../../include/uapi/linux/cell.h" + +#ifndef CELL_SELF +#define CELL_SELF -10005 +#endif + +#ifndef __NR_cell_create +#define __NR_cell_create 473 +#define __NR_cell_configure 474 +#define __NR_cell_info 475 +#define __NR_cell_open 476 +#define __NR_cell_kill 477 +#define __NR_pidfd_configure 478 +#endif + +/* <linux/pidfd.h> does not get along with <fcntl.h>. */ +#ifndef PIDFD_EMPTY +#define PIDFD_EMPTY O_NOCTTY + +enum pidfd_configure_cmd { + PIDFD_SET_STRING = 0, + PIDFD_SET_BINARY = 1, + PIDFD_SET_FD = 2, + PIDFD_CMD_START = 3, +}; + +struct pidfd_cred { + __u32 uid; + __u32 gid; + __u32 ngroups; + __u32 __spare; + __aligned_u64 groups; +}; +#endif + +#ifndef CLONE_INTO_CGROUP +#define CLONE_INTO_CGROUP (1ULL << 33) +#endif + +#define CELL_WINDOW_FIRST 0x80000000U +#define CELL_WINDOW_LAST 0xfffeffffU + +struct cell_clone_args { + __aligned_u64 flags; + __aligned_u64 pidfd; + __aligned_u64 child_tid; + __aligned_u64 parent_tid; + __aligned_u64 exit_signal; + __aligned_u64 stack; + __aligned_u64 stack_size; + __aligned_u64 tls; + __aligned_u64 set_tid; + __aligned_u64 set_tid_size; + __aligned_u64 cgroup; +}; + +static inline int cell_create(int parent, unsigned int flags) +{ + return syscall(__NR_cell_create, parent, flags); +} + +static inline int cell_configure(int cfd, unsigned int cmd, const char *key, + const void *value, int aux) +{ + return syscall(__NR_cell_configure, cfd, cmd, key, value, aux); +} + +static inline int cell_info(int cfd, struct cell_info *info, size_t size, + unsigned int flags) +{ + return syscall(__NR_cell_info, cfd, info, size, flags); +} + +static inline int cell_open(int cfd, unsigned int what, const char *key, + const void *arg, unsigned int flags) +{ + return syscall(__NR_cell_open, cfd, what, key, arg, flags); +} + +static inline int cell_kill(int cfd, unsigned int flags) +{ + return syscall(__NR_cell_kill, cfd, flags); +} + +/* A pidfd that no task is attached to yet, for one in the cell @cfd. */ +static inline int pidfd_open_empty(int cfd, unsigned int flags) +{ + return syscall(SYS_pidfd_open, cfd, PIDFD_EMPTY | flags); +} + +static inline int pidfd_configure(int pidfd, unsigned int cmd, const char *key, + const void *value, int aux) +{ + return syscall(__NR_pidfd_configure, pidfd, cmd, key, value, aux); +} + +#endif /* CELL_UTIL_H */ diff --git a/tools/testing/selftests/cell/config b/tools/testing/selftests/cell/config new file mode 100644 index 000000000000..262912e589d6 --- /dev/null +++ b/tools/testing/selftests/cell/config @@ -0,0 +1,35 @@ +CONFIG_CELLS=y +CONFIG_USER_NS=y +CONFIG_PID_NS=y +CONFIG_NET_NS=y +CONFIG_VETH=y +CONFIG_IPC_NS=y +CONFIG_UTS_NS=y +CONFIG_CGROUPS=y +CONFIG_TMPFS=y +CONFIG_TMPFS_XATTR=y +CONFIG_OVERLAY_FS=y +CONFIG_DEBUG_FS=y +CONFIG_FAULT_INJECTION=y +CONFIG_FAULT_INJECTION_DEBUG_FS=y +CONFIG_FAILSLAB=y +CONFIG_MEMCG=y +CONFIG_CGROUP_PIDS=y +CONFIG_IPV6=y +CONFIG_NETFILTER=y +CONFIG_NETFILTER_ADVANCED=y +CONFIG_NETFILTER_NETLINK=y +CONFIG_NETFILTER_NETLINK_OSF=y +CONFIG_NETFILTER_XTABLES=y +CONFIG_NF_CONNTRACK=y +CONFIG_NF_CT_NETLINK=y +CONFIG_NETFILTER_NETLINK_QUEUE=y +CONFIG_NETFILTER_NETLINK_GLUE_CT=y +CONFIG_NF_CT_NETLINK_HELPER=y +CONFIG_NF_TABLES=y +CONFIG_NF_TABLES_INET=y +CONFIG_IP_NF_IPTABLES=y +CONFIG_IP_NF_FILTER=y +CONFIG_PACKET=y +CONFIG_TUN=y +CONFIG_XFRM_USER=y -- 2.55.0 ^ permalink raw reply [flat|nested] 93+ messages in thread
end of thread, other threads:[~2026-11-05 18:23 UTC | newest] Thread overview: 93+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 01/90] Documentation: add a design document for cells Josef Bacik 2026-11-05 17:06 ` Serge E. Hallyn 2026-11-05 18:23 ` Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 02/90] Documentation: add examples " Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 03/90] cells: reserve a window of ids Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 04/90] user_namespace: let a cell own a user namespace Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 05/90] user_namespace: refuse setns() into a cell Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 06/90] capability: give the owner no say over " Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 07/90] ucount: count the id blocks a user holds Josef Bacik 2026-11-05 14:56 ` [PATCH RFC v3 08/90] user_namespace: let the kernel map the ids of a cell Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 09/90] user_namespace: keep legacy maps out of the window Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 10/90] user: add uid_range_in_use() Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 11/90] keys: add key_user_range_in_use() Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 12/90] fs: add kuid_has_fsmapping() and kgid_has_fsmapping() Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 13/90] fs: never store an id that was lent to a cell Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 14/90] fs: take no id of a cell for a mount option Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 15/90] fs: let an idmapping pin the ids of a cell Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 16/90] cgroup: let a cgroup become the budget " Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 17/90] cgroup: add cgroup_cell_chown() Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 18/90] cgroup: give the budget of a cell back Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 19/90] cgroup: add cgroup_cell_kill() Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 20/90] cgroup: root a cgroup namespace at a given cgroup Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 21/90] fs: seal a mount namespace for a cell Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 22/90] nsfs: make open_namespace() available outside of fs/ Josef Bacik 2026-11-05 14:57 ` [PATCH RFC v3 23/90] nsproxy: build the nsproxy the tasks of a cell share Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 24/90] cells: add the cell object and cell_create() Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 25/90] cells: add cell_kill() Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 26/90] cells: add cell_configure() Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 27/90] cells: add the "budget" slot Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 28/90] cells: add the "root" slot Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 29/90] cells: add the "ids" slot, for a single id Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 30/90] cells: lend a cell a block of ids Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 31/90] cells: reserve the "spawn" slot Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 32/90] cells: add CELL_CMD_SEAL Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 33/90] cells: add cell_open() Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 34/90] pid: allocate a struct pid ahead of its numbers Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 35/90] fork: let copy_process() place a task in a cell Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 36/90] cgroup: fork a task of a cell into the budget Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 37/90] x86: give a task without an mm no user state Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 38/90] uprobes: cope with a new task that has no mm Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 39/90] fork: add tasks that start out with nothing Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 40/90] pidfd: add PIDFD_EMPTY Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 41/90] pidfd: add pidfd_configure() Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 42/90] pidfd: let pidfd_configure() place files Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 43/90] pidfd: let pidfd_configure() set credentials Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 44/90] pidfd: let pidfd_configure() name a cgroup to start in Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 45/90] ptrace: keep everybody off an embryo Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 46/90] cells: end a cell when its init exits Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 47/90] cells: refuse a task of a cell a change of namespaces Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 48/90] fs: refuse a task of a cell a new mount namespace Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 49/90] nsproxy: refuse to enter a namespace of a cell Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 50/90] cgroup: keep a task of a cell within the budget Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 51/90] cgroup: let nothing but a spawn into a budget Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 52/90] kernfs: let a filesystem refuse a change to a node Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 53/90] cgroup: let a cell change nothing outside its budget Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 54/90] cgroup: leave the limits of a budget to who is above Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 55/90] fs: let a filesystem hold ids that go with the cell Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 56/90] cgroup: let a cell make cgroups below its budget Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 57/90] resctrl: let a cell change nothing Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 58/90] cred: let no id count across cells Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 59/90] ptrace: refuse PTRACE_TRACEME " Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 60/90] keys: ignore an equal uid or gid " Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 61/90] fs: accept a cellfd for MOUNT_ATTR_IDMAP Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 62/90] fs: let a cell set no set-id bits Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 63/90] fs: drop privileges from what a cell may write to Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 64/90] cells: add the "caps" slot Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 65/90] capabilities: take nothing from a file in a cell Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 66/90] capabilities: let a cell shrink its bounding sets Josef Bacik 2026-11-05 15:02 ` [PATCH RFC v3 67/90] af_unix: deliver ancillary data only within a cell Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 68/90] af_unix: tell a sender that control data will not cross Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 69/90] net: do not identify a peer in another cell Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 70/90] seccomp: let a listener put no file into a cell Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 71/90] umh: run no program for a task of " Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 72/90] coredump: let no core dump leave " Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 73/90] cells: add the "net" slot Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 74/90] net: give a cell without a network no sockets Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 75/90] net: add net_may() Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 76/90] rtnetlink: use net_may() for requests that change state Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 77/90] net: use net_may() in the configuration ioctls Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 78/90] netfilter: use net_may() for configuration Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 79/90] tun: use net_may() to create and attach to devices Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 80/90] net: use net_may() for the net sysctls Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 81/90] net: use net_may() for privileged socket options Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 82/90] net: use net_may() for raw and packet sockets Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 83/90] cells: add the "net-rights" slot Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 84/90] net: honour the rights of a cell in net_may() Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 85/90] tun: ignore an equal uid or gid across cells Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 86/90] cells: add cell_info() Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 87/90] cells: add CONFIG_CELLS Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 88/90] arch: wire up the cell system calls Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 89/90] MAINTAINERS: add an entry for cells Josef Bacik 2026-11-05 15:03 ` [PATCH RFC v3 90/90] selftests: add tests " Josef Bacik
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®