Files
systemd/units/systemd-nsresourced.service.in
T
Daan De Meyer e50ec930c2 nsresourced: refuse writable overlayfs whose upper outlives the namespace
A write through an overlay mount does not create the object on the overlay
itself: overlayfs creates it on the upper layer via vfs_create(), which
reaches only the *inode* LSM hooks, not the *path* hooks the transient
range policy attaches to. The one path hook that does fire sees the overlay
mount, whose superblock belongs to the (managed) mounting namespace and so
looks like it dies with it -- while the upper may be a persistent file
system on which the transient IDs then persist.

  Regular file system -- caught by the path hook:

    openat(dir_on_persistent_fs, "f", O_CREAT)
      path_openat -> ... -> security_path_mknod(path{mnt = persistent mount})   << path hook (attached)
        userns_restrict_path_mknod -> validate_mount()
          sb->s_user_ns = init   -> not below managed   (outlives the ns)
          mnt_idmap does not remap the transient id      (reaches disk)
        => -EPERM                                        BLOCKED

  Overlay write -- slips past the path hook (this is the hole):

    openat(overlay_mnt/"f", O_CREAT)
      path_openat -> ... -> security_path_mknod(path{mnt = OVERLAY mount})       << path hook (attached)
        validate_mount(): overlay sb->s_user_ns == managed
        => "dies with the namespace"                     ALLOW
      ovl_create_or_link -> vfs_create(UPPER dentry)
        security_inode_create(UPPER inode)               << inode hook (NOT attached)
        => no check
      inode created on the UPPER fs, owned by the transient uid   PERSISTS

Judge the upper at mount time instead, in lsm/sb_kern_mount:

    mount("overlay", ..., upperdir=<X>, workdir=<Y>)
      do_new_mount -> vfs_get_tree
        ovl_fill_super
          stamps work/index onto the upper via vfs_mkdir, owned by the
          transient mounter -> security_inode_mkdir only (not a path hook)
        security_sb_kern_mount(sb)                        << hook (attached)
          userns_restrict_sb_kern_mount:
            sb->s_magic != OVERLAYFS_SUPER_MAGIC       -> return 0   (not an overlay)
            find_managed_userns(sb->s_user_ns) == none -> return 0   (unmanaged mounter)
            upper = ((struct ovl_fs*)sb->s_fs_info)->layers[0].mnt
            upper == NULL                              -> return 0   (read-only, no upper)
            fs_userns = upper->mnt_sb->s_user_ns
              upper dies with the managed ns (e.g. a tmpfs)         => return 0  ALLOW
              upper persistent, idmap records no transient id       => return 0  ALLOW
              upper persistent, an idmap extent records a transient => -EPERM   REFUSE

The upper has to be judged at mount time because overlayfs performs the
actual upper creations (both the mount-time work/index dirs and every later
copy-up) with internal vfs_create()/vfs_mkdir() calls that reach only the
inode hooks, on the upper's own superblock, where neither the overlay's
ovl_fs nor the mount idmapping is available. Refusing the mount stops every
such write. It cannot undo the work/index dirs ovl_fill_super() already
stamped onto the upper before this hook runs. Reaching those takes an
upperdir and a workdir that already exist on the same persistent,
non-idmapped file system and that the transient IDs may write to: the client
cannot create them itself, since that is what the path hooks deny.

The idmap check walks the upper mount's uid_map and gid_map and permits the
overlay only when no extent records a transient id on disk (ids the idmap
does not map fail the write, so they are fine too). This is what lets a
client back a writable overlay with a persistent but id-mapped upper, e.g.
one handed out by mountfsd, while a plain persistent upper stays refused.

The check keys on whether a writable upper exists (layers[0].mnt is
non-NULL), which is fixed for the superblock's lifetime, not on the mutable
SB_RDONLY flag: an overlay mounted read-only but with an upper can be
remounted read-write later without re-entering sb_kern_mount (remount uses a
different LSM hook), so it must be judged now. A genuinely read-only overlay
has no upper and is left alone. Reading the upper layer needs overlayfs'
private struct ovl_fs/ovl_layer, which are absent from vmlinux.h when
overlayfs is built as a module; they are declared as CO-RE type flavors and
guarded by bpf_core_type_exists(), and the service now orders itself after
modprobe@overlay.service so the module's BTF is present by the time the
program is loaded.

A managed (nsresourced-provisioned) user namespace can therefore only mount
a writable overlay whose upper dies with it (e.g. a tmpfs it set up
itself). Callers that need a writable, persistent (non-tmpfs) upperdir --
mkosi, for one -- should use a regular unprivileged user namespace for that
rather than a transient nsresourced range, since such an overlay is refused
for a managed namespace by design.

test-userns-restrict now mounts real overlays: a read-only overlay, a
writable overlay with a tmpfs upper, and a writable overlay whose upper is
an init-owned but idmapped mount are all allowed, while a writable overlay
with a plain init-owned upper is refused with EPERM. The kernel permits that
last mount without the LSM loaded, so the EPERM is the policy's doing.
2026-08-12 10:49:34 +02:00

50 lines
1.5 KiB
Plaintext

# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
[Unit]
Description=Namespace Resource Manager
Documentation=man:systemd-nsresourced.service(8)
Requires=systemd-nsresourced.socket
Conflicts=shutdown.target
Before=sysinit.target shutdown.target
Wants=modprobe@tun.service modprobe@overlay.service
After=modprobe@tun.service modprobe@overlay.service
DefaultDependencies=no
[Service]
CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_SYS_RESOURCE CAP_BPF CAP_PERFMON CAP_SETGID CAP_SETUID CAP_SYS_ADMIN CAP_CHOWN CAP_FOWNER CAP_NET_ADMIN CAP_SETFCAP
ExecStart={{LIBEXECDIR}}/systemd-nsresourced
IPAddressDeny=any
LimitNOFILE={{HIGH_RLIMIT_NOFILE}}
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
DevicePolicy=closed
DeviceAllow=/dev/net/tun rwm
ProtectProc=invisible
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectSystem=strict
RestrictAddressFamilies=AF_UNIX AF_NETLINK
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
SystemCallFilter=@system-service bpf perf_event_open open_by_handle_at
Type=notify
NotifyAccess=all
FileDescriptorStoreMax=4096
{{SERVICE_WATCHDOG}}
[Install]
Also=systemd-nsresourced.socket