All of lore.kernel.org
 help / color / mirror / Atom feed
* Landlock: LANDLOCK_ACCESS_FS_IOCTL_DEV is bypassable via io_uring IORING_OP_URING_CMD (confirmed on real NVMe hardware)
@ 2026-07-18 13:56 Vivek Parikh
  2026-07-18 15:01 ` Mickaël Salaün
  0 siblings, 1 reply; 2+ messages in thread
From: Vivek Parikh @ 2026-07-18 13:56 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Günther Noack, Paul Moore, Jens Axboe, linux-security-module,
	io-uring, viv0411.parikh

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

^ permalink raw reply	[flat|nested] 2+ messages in thread

* Re: Landlock: LANDLOCK_ACCESS_FS_IOCTL_DEV is bypassable via io_uring IORING_OP_URING_CMD (confirmed on real NVMe hardware)
  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
  0 siblings, 0 replies; 2+ messages in thread
From: Mickaël Salaün @ 2026-07-18 15:01 UTC (permalink / raw)
  To: Vivek Parikh
  Cc: Günther Noack, Paul Moore, Jens Axboe, linux-security-module,
	io-uring

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
> 

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-07-18 15:02 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 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.