All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Mickaël Salaün" <mic@digikod.net>
To: Vivek Parikh <viv0411.parikh@gmail.com>
Cc: "Günther Noack" <gnoack@google.com>,
	"Paul Moore" <paul@paul-moore.com>,
	"Jens Axboe" <axboe@kernel.dk>,
	linux-security-module@vger.kernel.org, io-uring@vger.kernel.org
Subject: Re: Landlock: LANDLOCK_ACCESS_FS_IOCTL_DEV is bypassable via io_uring IORING_OP_URING_CMD (confirmed on real NVMe hardware)
Date: Sat, 18 Jul 2026 17:01:56 +0200	[thread overview]
Message-ID: <20260718.pie0Adeerohx@digikod.net> (raw)
In-Reply-To: <20260718135650.380643-1-viv0411.parikh@gmail.com>

Hi Vivek,

Similar reports were already sent:
https://lore.kernel.org/all/20260616201633.275067-1-hexlabsecurity@proton.me/

Please take a look at the Landlock threat model:
https://lore.kernel.org/all/20260707210336.2060040-1-mic@digikod.net/

This is not a bypass.

Regards,
 Mickaël

On Sat, Jul 18, 2026 at 07:26:48PM +0530, Vivek Parikh wrote:
> Hi Mickaël,
> 
> Note: this was found with AI assistance, so I am treating it as public per
> Documentation/process/security-bugs.
> 
> While continuing the LSM-mediation audit I found that Landlock's
> LANDLOCK_ACCESS_FS_IOCTL_DEV right can be bypassed with io_uring's
> IORING_OP_URING_CMD. Unlike the mount_setattr(2) gap I reported earlier,
> this one is fully unprivileged and squarely inside Landlock's documented
> model. I have verified it on real NVMe hardware (output below).
> 
> The mechanism
> -------------
> Landlock enforces IOCTL_DEV through the file_ioctl / file_ioctl_compat LSM
> hooks (security/landlock/fs.c: LSM_HOOK_INIT(file_ioctl, ...) /
> file_ioctl_compat -> hook_file_ioctl_common ->
> LANDLOCK_ACCESS_FS_IOCTL_DEV, security/landlock/fs.c:1854/1865).
> 
> io_uring's IORING_OP_URING_CMD dispatches driver passthrough commands
> through a *different* hook, security_uring_cmd(ioucmd)
> (io_uring/uring_cmd.c:249), before calling file->f_op->uring_cmd. Landlock
> implements no uring_cmd hook -- it has no io_uring hooks at all (only
> SELinux and Smack implement security_uring_cmd). So for the same device
> fd:
> 
>   ioctl(devfd, CMD, arg)      -> security_file_ioctl -> Landlock: DENIED
>                                                         (IOCTL_DEV not granted)
>   IORING_OP_URING_CMD(devfd)  -> security_uring_cmd  -> Landlock: NO HOOK
>                                                         -> f_op->uring_cmd runs
> 
> uring_cmd is the async twin of the device ioctl. NVMe makes the
> equivalence explicit (drivers/nvme/host/ioctl.c): the ioctl path handles
> NVME_IOCTL_ADMIN_CMD and the uring_cmd path handles NVME_URING_CMD_ADMIN
> -- the same admin/IO passthrough commands. Both the controller char dev
> (nvme_dev_uring_cmd, core.c:3841) and the namespace char dev /dev/ngX
> (nvme_ns_chr_uring_cmd, core.c:3946) implement ->uring_cmd, as do ublk
> and drivers/char/mem.c.
> 
> Impact
> ------
> A Landlock-sandboxed task that is denied IOCTL_DEV on a device but holds
> an fd to it (opened under a granted fs right, or inherited) can issue the
> equivalent device commands via IORING_OP_URING_CMD, defeating exactly what
> IOCTL_DEV exists to gate. On NVMe -- the most common storage device on
> Linux systems -- this means arbitrary admin passthrough: the same code
> path carries FORMAT NVM, SANITIZE, and firmware-download commands, not
> just reads/writes. The realistic scenario: a sandboxed storage workload
> is given an NVMe namespace fd for fast IO with the expectation "it can do
> IO but cannot send device-control commands" -- the expectation
> LANDLOCK_ACCESS_FS_IOCTL_DEV was added (ABI 5) to express. It is void.
> 
> For ublk devices the gap is total: ublk's control plane is uring_cmd-only
> (no ioctl equivalent), so on ublk char devices IOCTL_DEV currently
> mediates nothing at all.
> 
> Documentation/userspace-api/landlock.rst presents IOCTL_DEV as the control
> over device ioctls; a sandbox author reasonably expects withholding it to
> stop device control commands. io_uring is unprivileged, Landlock is
> unprivileged, and no capability is required anywhere.
> 
> Affected versions
> -----------------
> This is not a regression -- it is a coverage gap that shipped with the
> IOCTL_DEV right. LANDLOCK_ACCESS_FS_IOCTL_DEV was added in v6.10
> (b25f7415eb41, May 2024); NVMe/ublk ->uring_cmd predate it (v6.0), so the
> first kernel to support IOCTL_DEV was already bypassable. It is present in
> every kernel from v6.10 through v7.2-rc3, including 6.12 LTS. (SELinux and
> Smack implement security_uring_cmd and are unaffected; Landlock and AppArmor
> do not, but only Landlock exposes IOCTL_DEV as a user-facing right.)
> 
> For completeness on scope: exploitation requires a Landlock policy that
> handles IOCTL_DEV, a sandboxee that holds/opens a ->uring_cmd-capable device
> fd, and io_uring not otherwise blocked. Sandboxers that also seccomp-filter
> io_uring (e.g. Chromium) are not affected via this path; the exposure is for
> the growing set of tools that adopt IOCTL_DEV without blocking io_uring.
> 
> Reproducer (confirmed on real hardware)
> ---------------------------------------
> Three self-contained PoCs (raw io_uring, no liburing) are available on
> request; I am not inlining them.
> 
> PoC 1 targets /dev/null (null_fops has no ->unlocked_ioctl but has
> .uring_cmd = uring_cmd_null) and needs no special hardware:
> 
>   [*] Landlock enforced: READ/WRITE granted on /, IOCTL_DEV denied
>   [1] ioctl(/dev/null, dev-cmd) = -1 (Permission denied)  <- Landlock DENIED
>   [2] io_uring URING_CMD(/dev/null) res = 0 (OK)          <- Landlock did NOT mediate
> 
> PoC 2 targets a real NVMe controller (/dev/nvme0, WD PC SN740) with
> IDENTIFY CONTROLLER (admin opcode 0x06, read-only), on
> 7.0.0-27-generic:
> 
>   [*] Landlock enforced: READ/WRITE granted on /, IOCTL_DEV denied
>   [1] ioctl(NVME_IOCTL_ADMIN_CMD identify) = -1 (Permission denied)  <- Landlock DENIED
>   [2] URING_CMD(NVME_URING_CMD_ADMIN identify) res = 0 (OK)
>       IDENTIFY data: vid=0xb715 model="WD PC SN740 SDDPMQD-512G-1101"
> 
> The same PoC was also confirmed on AWS EC2 (Ubuntu 7.0.0-1008-aws, stock
> cloud image, default settings, real NVMe EBS volume):
> 
>   [1] ioctl(NVME_IOCTL_ADMIN_CMD identify) = -1 (Permission denied)  <- Landlock DENIED
>   [2] URING_CMD(NVME_URING_CMD_ADMIN identify) res = 0 (OK)
>       IDENTIFY data: vid=0x0f1d model="Amazon Elastic Block Store"
> 
> PoC 3 targets /dev/fuse (world-accessible, 0666) as a fully unprivileged
> user (fresh uid, no group memberships, Landlock ABI 8; also reproduced in
> a Docker container with seccomp relaxed). No fuse module parameter is
> needed for this signal: fuse_uring_cmd() (fs/fuse/dev_uring.c:1217) calls
> fuse_get_dev() *before* its enable_uring check, so a never-mounted fd
> returns -EPERM even with enable_uring=N (the default). The full
> queue-registration scenario does need enable_uring=1; NVMe and ublk have
> no such gate at all.
> 
>   [*] Landlock enforced: READ/WRITE granted on /, IOCTL_DEV denied
>   [1] ioctl(/dev/fuse, FUSE_DEV_IOC_CLONE) = -1 (Permission denied)  <- Landlock DENIED
>   [2] URING_CMD(/dev/fuse, FUSE_IO_URING_CMD_REGISTER) res = -1 (EPERM)
>       <- reached fuse_uring_cmd; Landlock did NOT mediate
> 
>   (-EPERM can only originate inside fuse_uring_cmd on a never-mounted
>    fd, proving the call passed security_uring_cmd into the driver.)
> 
> The ioctl admin passthrough is denied while the identical admin command
> executes via io_uring. (Note: NVMe uring passthrough requires
> SQE128+CQE32, per nvme_uring_cmd_checks.)
> 
> Mitigations / not affected
> --------------------------
> The bypass is neutralised anywhere io_uring cannot be reached:
> 
> - kernel.io_uring_disabled = 2 makes io_uring_setup() return -EPERM for all
>   callers (io_uring_allowed(), io_uring/io_uring.c); value 1 restricts it to
>   io_uring_group / CAP_SYS_ADMIN. Where an admin has set either, this path is
>   blocked. This is a hardening knob, not a universal default: RHEL 9.3 / Rocky
>   9.3 ship io_uring *enabled* on the host (Red Hat documents the syscalls as
>   succeeding or returning EPERM per configuration).
> - Many container runtimes block the io_uring syscalls in their default seccomp
>   profile (e.g. Docker/Podman -> io_uring_setup fails with EPERM/ENOSYS inside
>   the container, verified with Docker 29 on Fedora 42), so a sandboxee confined
>   by such a runtime is protected. Likewise application sandboxers that
>   seccomp-filter io_uring (e.g. Chromium) are not affected via this path.
>   Additionally, on SELinux-enforcing hosts, containers without a relaxed label
>   are denied io_uring_setup by selinux_uring_allowed -- a second independent
>   gate (verified: the same container run fails with EACCES until
>   --security-opt label=disable is given).
> 
> So the exposed population is: io_uring-enabled kernels (the desktop default,
> and RHEL/Rocky on a bare host) running a Landlock sandbox that handles
> IOCTL_DEV without an io_uring seccomp block.
> 
> Fix direction
> -------------
> security_uring_cmd(ioucmd) gives the LSM the io_uring_cmd (and thus the
> struct file). Landlock should implement a uring_cmd hook that, for a
> device file, requires LANDLOCK_ACCESS_FS_IOCTL_DEV -- mirroring
> hook_file_ioctl. The uring_cmd command is driver-specific rather than a
> standard ioctl cmd number, so the is_masked_device_ioctl() allow-list
> (FIONREAD etc.) does not apply; the safe behavior is to require IOCTL_DEV
> for any uring_cmd on a device file. I am happy to prepare that patch
> (LSM_HOOK_INIT(uring_cmd, ...) + a tools/testing/selftests/landlock test)
> if you agree with the direction.
> 
> CCing Jens and io-uring, as the io_uring side is involved (a new LSM hook
> consumer, no io_uring behavior change expected).
> 
> Thanks,
> Vivek
> 

      reply	other threads:[~2026-07-18 15:02 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-18 13:56 Landlock: LANDLOCK_ACCESS_FS_IOCTL_DEV is bypassable via io_uring IORING_OP_URING_CMD (confirmed on real NVMe hardware) Vivek Parikh
2026-07-18 15:01 ` Mickaël Salaün [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260718.pie0Adeerohx@digikod.net \
    --to=mic@digikod.net \
    --cc=axboe@kernel.dk \
    --cc=gnoack@google.com \
    --cc=io-uring@vger.kernel.org \
    --cc=linux-security-module@vger.kernel.org \
    --cc=paul@paul-moore.com \
    --cc=viv0411.parikh@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.