Skip to content

config-linux: add linux.ioUring - #1328

Open
gvozdetsky wants to merge 3 commits into
opencontainers:mainfrom
gvozdetsky:io-uring-restrictions
Open

gvozdetsky wants to merge 3 commits into
opencontainers:mainfrom
gvozdetsky:io-uring-restrictions

Conversation

@gvozdetsky

@gvozdetsky gvozdetsky commented Oct 13, 2026 •

Copy link
Copy Markdown

Adds linux.ioUring, an allowlist of io_uring operations for the container, based on the task restrictions added in Linux 7.0. Closes #1323.

Compared to the issue, defaultAction and deniedSqeFlags are dropped and register opcodes are out of scope. All of them can be added later without breaking existing configs. socketFamilies can't be empty, and it requires IORING_OP_SOCKET in ops.

Implementations, both behind an annotation until this is merged:

Unrecognized names are ignored, as they can only stay denied. On 7.0 and 7.1 without bc0e8faf90e7, restrictions are lost on exec; the runtime must refuse the config if it can't ensure they are kept. crun and youki check it by re-executing themselves from a restricted child that has used io_uring.

gvozdetsky added a commit to gvozdetsky/youki that referenced this pull request Oct 13, 2026
The proposed linux.ioUring says that whether an operation is allowed
doesn't depend on its submission flags (opencontainers/runtime-spec#1328).
Check it: a read with IOSQE_BUFFER_SELECT must be allowed. No buffers
are registered, so the kernel answers -ENOBUFS, not -EACCES. A runtime
that builds the allowlist with IORING_REGISTER_RESTRICTIONS would deny
it, since that also denies every flag that isn't listed.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
@AkihiroSuda
AkihiroSuda requested a balanced review from Copilot October 13, 2026 13:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Go serialization currently converts an empty socket-family allowlist into an omitted, unrestricted policy.

1 open finding
What changed in this PR

Adds OCI configuration and feature reporting for Linux io_uring operation allowlists.

Changes:

  • Defines linux.ioUring and corresponding Go types.
  • Adds schemas, validation fixtures, and feature reporting.
  • Documents enforcement semantics and examples.
File Description
specs-go/​features/​features.go Adds io_uring feature reporting types.
specs-go/​config.go Adds io_uring configuration types.
schema/​test/​config/​good/​linux-io-uring.json Adds valid configuration fixture.
schema/​test/​config/​bad/​linux-io-uring.json Tests required ops.
schema/​test/​config/​bad/​linux-io-uring-invalid-op.json Tests operation-name validation.
schema/​features-linux.json Defines feature schema.
schema/​defs-linux.json Defines operation and family patterns.
schema/​config-linux.json Defines configuration schema.
features.md Extends the complete feature example.
features-linux.md Documents feature reporting.
config-linux.md Documents io_uring restrictions.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread specs-go/config.go Resolved
gvozdetsky added a commit to gvozdetsky/youki that referenced this pull request Oct 13, 2026
The proposed linux.ioUring no longer allows an empty socketFamilies
(opencontainers/runtime-spec#1328). To deny all sockets, leave
IORING_OP_SOCKET out of ops.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
@gvozdetsky
gvozdetsky force-pushed the io-uring-restrictions branch from cacd22a to 8c70401 Compare October 13, 2026 14:25
gvozdetsky added a commit to gvozdetsky/youki that referenced this pull request Oct 20, 2026
The proposed linux.ioUring now requires IORING_OP_SOCKET in ops when
socketFamilies is set (opencontainers/runtime-spec#1328): a policy that
limits the families of sockets it doesn't allow is most likely a
mistake.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
gvozdetsky added a commit to gvozdetsky/crun that referenced this pull request Oct 20, 2026
linux: add experimental io_uring restrictions

seccomp cannot filter io_uring operations, as they are submitted
through the ring and not as syscalls, so today the only option is to
block io_uring entirely.  Linux 7.0 added task-level io_uring BPF
filters, which are inherited across fork and exec and can only be
tightened, like seccomp.

Add the run.oci.io_uring annotation, in the shape proposed for
linux.ioUring in opencontainers/runtime-spec#1328:

  {"ops": [...], "socketFamilies": [...]}

ops is an allowlist of IORING_OP_* names, every other operation is
denied.  socketFamilies, when present, limits IORING_OP_SOCKET to the
listed address families.  It must not be empty, and ops must contain
IORING_OP_SOCKET.

The policy is validated, and kernel support checked, before the
container is created or restored, and before a process is exec'ed in
it.  Registering a filter needs either no_new_privs or CAP_SYS_ADMIN,
so, like seccomp, the restrictions are applied to the container init
and to every exec'ed process before the capabilities are dropped, or
with noNewPrivileges once no_new_privs is set.  In both cases they are
applied before seccomp, which may deny io_uring_register.  CRIU does
not restore them, so on restore crun applies them to itself before
running CRIU, and the restored processes inherit them.

Only BPF filters are used, never IORING_REGISTER_RESTRICTIONS, so the
SQE flags are not restricted and the workload can still add its own
filters.

The annotation will be replaced by linux.ioUring once it is in the
runtime spec.

Assisted-by: Claude Code (claude-opus-5-5)
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
Restrict the io_uring operations a container can submit, using the
io_uring task restrictions added in Linux 7.0. io_uring operations are
not system calls, so seccomp cannot filter them.

ops is an allowlist that doesn't depend on submission flags, so runtimes
behave the same whether they use BPF filters or the task allowlist. An
empty socketFamilies is not allowed: Go's omitempty would turn it into
"any family". To deny all sockets, leave IORING_OP_SOCKET out of ops.

Closes opencontainers#1323.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
@gvozdetsky
gvozdetsky force-pushed the io-uring-restrictions branch from 2538391 to 76517a2 Compare October 23, 2026 12:57
gvozdetsky added a commit to gvozdetsky/crun that referenced this pull request Oct 23, 2026
seccomp cannot filter io_uring operations, as they are submitted
through the ring and not as syscalls, so today the only option is to
block io_uring entirely.  Linux 7.0 added task-level io_uring BPF
filters, which are inherited across fork and exec and can only be
tightened, like seccomp.

Add the run.oci.io_uring annotation, in the shape proposed for
linux.ioUring in opencontainers/runtime-spec#1328:

  {"ops": [...], "socketFamilies": [...]}

ops is an allowlist of IORING_OP_* names, every other operation is
denied.  socketFamilies, when present, limits IORING_OP_SOCKET to the
listed address families.  It must not be empty, and ops must contain
IORING_OP_SOCKET.

The policy is validated, and kernel support checked, before the
container is created or restored, and before a process is exec'ed in
it.  Registering a filter needs either no_new_privs or CAP_SYS_ADMIN,
so, like seccomp, the restrictions are applied to the container init
and to every exec'ed process before the capabilities are dropped, or
with noNewPrivileges once no_new_privs is set.  In both cases they are
applied before seccomp, which may deny io_uring_register.  CRIU does
not restore them, so on restore crun applies them to itself before
running CRIU, and the restored processes inherit them.

Only BPF filters are used, never IORING_REGISTER_RESTRICTIONS, so the
SQE flags are not restricted and the workload can still add its own
filters.

The annotation will be replaced by linux.ioUring once it is in the
runtime spec.

Assisted-by: Claude Code (claude-opus-5-5)
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
gvozdetsky added a commit to gvozdetsky/youki that referenced this pull request Oct 23, 2026
The proposed linux.ioUring says that whether an operation is allowed
doesn't depend on its submission flags (opencontainers/runtime-spec#1328).
Check it: a read with IOSQE_BUFFER_SELECT must be allowed. No buffers
are registered, so the kernel answers -ENOBUFS, not -EACCES. A runtime
that builds the allowlist with IORING_REGISTER_RESTRICTIONS would deny
it, since that also denies every flag that isn't listed.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
gvozdetsky added a commit to gvozdetsky/youki that referenced this pull request Oct 23, 2026
The proposed linux.ioUring no longer allows an empty socketFamilies
(opencontainers/runtime-spec#1328). To deny all sockets, leave
IORING_OP_SOCKET out of ops.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
gvozdetsky added a commit to gvozdetsky/youki that referenced this pull request Oct 23, 2026
The proposed linux.ioUring now requires IORING_OP_SOCKET in ops when
socketFamilies is set (opencontainers/runtime-spec#1328): a policy that
limits the families of sockets it doesn't allow is most likely a
mistake.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
gvozdetsky added a commit to gvozdetsky/youki that referenced this pull request Oct 26, 2026
On Linux 7.0 and 7.1 without bc0e8faf90e7, a task that has used
io_uring loses its restrictions when it execs. supported() can't
detect that: distributions backport the fix without a version change,
and a reliable check would have to exec a helper program. State the
requirement in the module documentation, as the proposed linux.ioUring
leaves it to runtimes (opencontainers/runtime-spec#1328).

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
gvozdetsky added a commit to gvozdetsky/crun that referenced this pull request Oct 26, 2026
seccomp cannot filter io_uring operations, as they are submitted
through the ring and not as syscalls, so today the only option is to
block io_uring entirely.  Linux 7.0 added task-level io_uring BPF
filters, which are inherited across fork and exec and can only be
tightened, like seccomp.

Add the run.oci.io_uring annotation, in the shape proposed for
linux.ioUring in opencontainers/runtime-spec#1328:

  {"ops": [...], "socketFamilies": [...]}

ops is an allowlist of IORING_OP_* names, every other operation is
denied.  socketFamilies, when present, limits IORING_OP_SOCKET to the
listed address families.  It must not be empty, and ops must contain
IORING_OP_SOCKET.

The policy is validated, and kernel support checked, before the
container is created or restored, and before a process is exec'ed in
it.  Registering a filter needs either no_new_privs or CAP_SYS_ADMIN,
so, like seccomp, the restrictions are applied to the container init
and to every exec'ed process before the capabilities are dropped, or
with noNewPrivileges once no_new_privs is set.  In both cases they are
applied before seccomp, which may deny io_uring_register.  CRIU does
not restore them, so on restore crun applies them to itself before
running CRIU, and the restored processes inherit them.

Only BPF filters are used, never IORING_REGISTER_RESTRICTIONS, so the
SQE flags are not restricted and the workload can still add its own
filters.

The annotation will be replaced by linux.ioUring once it is in the
runtime spec.

Assisted-by: Claude Code (claude-opus-5-5)
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>

@kolyshkin kolyshkin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few questions on the semantics, checked against the kernel source (io_uring/bpf_filter.c, io_uring/register.c, 7.3-rc7).

This comment was written by an AI (Claude Code).

Comment thread config-linux.md
The runtime MUST generate an error if this property is set and `ops` does not contain `IORING_OP_SOCKET`.

The runtime MUST apply the restrictions before the container process is executed, so that they apply to the container process and all of its descendants.
If the runtime cannot apply the restrictions, it MUST generate an error.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the kernel bug mentioned in the description (restrictions lost on exec on 7.0/7.1 without bc0e8faf90e7, fixed in v7.2-rc6, Cc stable 7.1+): per the commit, if a task has used io_uring, the exec cancellation path frees its per-task restrictions, so a ring created after exec is unrestricted. The container init itself probably never creates a ring before exec, but any process inside the container that uses io_uring and then execs gets an unrestricted image, i.e. a trivial bypass of the policy.

On such a kernel, registering the filters succeeds, so with the current wording the runtime "applies" the restrictions and doesn't error out, yet the requirement that they apply "to the container process and all of its descendants" is silently violated. Leaving the detection to runtimes means in practice nobody does it (neither the crun nor the youki implementation checks for it, as far as I can see).

Maybe the spec should say explicitly that if the runtime cannot ensure the restrictions are preserved across exec, it MUST (or at least SHOULD) generate an error, so runtimes have to do a functional check (or a kernel version check) instead of only probing for IORING_REGISTER_BPF_FILTER.

This comment was written by an AI (Claude Code).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, added: "If the runtime cannot ensure that a process keeps the restrictions when it calls execve(2) after using io_uring, it MUST generate an error", with 7.0/7.1 without bc0e8faf90e7 as the example. crun and youki now check it on a child that restricts itself, creates a ring and re-executes the runtime. They refuse the policy on mainline 7.0, and accept it on 7.2.9 and on Ubuntu's 7.0.0-42, which has the backport.

Comment thread config-linux.md Outdated

The runtime MUST apply the restrictions before the container process is executed, so that they apply to the container process and all of its descendants.
If the runtime cannot apply the restrictions, it MUST generate an error.
Applying `ioUring` SHOULD NOT prevent the container process from registering further io_uring restrictions of its own.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be achievable in full with the current kernel API. Registering a task-level BPF filter creates current->io_uring_restrict, and after that IORING_REGISTER_RESTRICTIONS for the task (io_register_restrictions_task) fails with -EPERM ("Disallow if task already has registered restrictions"). So the container process can add more BPF filters, but can no longer register classic task restrictions.

Maybe narrow it to "further io_uring BPF filters" (or document that task-level IORING_REGISTER_RESTRICTIONS is not available once ioUring is applied).

This comment was written by an AI (Claude Code).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Narrowed to "adding io_uring BPF filters of its own, or restrictions to the io_uring instances it creates": per-ring IORING_REGISTER_RESTRICTIONS still works, as a ring that inherits only BPF filters has no registered restrictions.

Comment thread config-linux.md Outdated
All other operations are denied, and an empty array denies all operations.
Whether an operation is allowed does not depend on its submission flags (`IOSQE_*`).
Values are names of the `IORING_OP_*` constants from `<linux/io_uring.h>`, for example `IORING_OP_READ`.
The runtime MUST generate an error if it does not recognize a value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since ops (and socketFamilies) is an allowlist, ignoring an unrecognized value (perhaps with a warning) would still be fail-closed: the operation stays denied. Making it an error is not fail-safer, but it is forward-incompatible: a config produced by a newer higher-level runtime that lists an opcode added in a newer kernel makes the container fail to start with an older runtime, even though the same opcode is simply ignored if the kernel doesn't know it (the MUST ignore above).

If the error is kept, then every config producer effectively has to intersect its list with ops/socketFamilies from the features document before generating the config; that's probably worth stating explicitly. Otherwise, I'd suggest making unrecognized values ignored, consistent with the "recognized but not supported by the kernel" case.

This comment was written by an AI (Claude Code).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed: unrecognized values are now ignored in both ops and socketFamilies; if none of the families is recognized, IORING_OP_SOCKET is denied. crun and youki do the same now.

- Unrecognized ops and socketFamilies values are ignored, not an error:
  they stay denied, and a config for a newer runtime still works.
- The runtime must fail if a process would lose the restrictions on
  execve(2) after using io_uring, as on Linux 7.0 and 7.1 without
  bc0e8faf90e7.
- The container process can add BPF filters and restrict its own rings;
  registering task-level IORING_REGISTER_RESTRICTIONS is not possible
  once BPF filters are registered.
- specs-go: SocketFamilies is a pointer, so that an empty list is not
  turned into "any family" by omitempty.
- schema: allow lowercase in family names (AF_DECnet).

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
config-linux: add linux.ioUring

Restrict the io_uring operations a container can submit, using the
io_uring task restrictions added in Linux 7.0. io_uring operations are
not system calls, so seccomp cannot filter them.

ops is an allowlist that doesn't depend on submission flags, so runtimes
behave the same whether they use BPF filters or the task allowlist.
Unrecognized names are ignored: they can only stay denied, and a config
written for a newer runtime or kernel still works. The runtime must fail
if the restrictions would be lost on execve(2), as on Linux 7.0 and 7.1
without bc0e8faf90e7.

An empty socketFamilies is not allowed; to deny all sockets, leave
IORING_OP_SOCKET out of ops. In specs-go it is a pointer, so that an
empty list is not omitted and turned into "any family".

Closes opencontainers#1323.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
gvozdetsky added a commit to gvozdetsky/crun that referenced this pull request Oct 30, 2026
linux: add experimental io_uring restrictions

seccomp cannot filter io_uring operations, as they are submitted
through the ring and not as syscalls, so today the only option is to
block io_uring entirely.  Linux 7.0 added task-level io_uring BPF
filters, which are inherited across fork and exec and can only be
tightened, like seccomp.

Add the run.oci.io_uring annotation, in the shape proposed for
linux.ioUring in opencontainers/runtime-spec#1328:

  {"ops": [...], "socketFamilies": [...]}

ops is an allowlist of IORING_OP_* names, every other operation is
denied.  socketFamilies, when present, limits IORING_OP_SOCKET to the
listed address families.  It must not be empty, and ops must contain
IORING_OP_SOCKET.  Unknown names are ignored with a warning: what they
would allow stays denied.

Every opcode the kernel knows gets a filter, an allow or a deny one:
with DENY_REST alone, an opcode that a filter inherited by crun allows
would stay allowed.

The policy is validated, and kernel support checked, before the
container is created or restored, and before a process is exec'ed in
it.  Linux 7.0 and 7.1 without bc0e8faf90e7 drop the restrictions on
exec for a task that has used io_uring, so crun also checks that on a
child that restricts itself, creates a ring and re-executes itself.

Registering a filter needs either no_new_privs or CAP_SYS_ADMIN, so,
like seccomp, the restrictions are applied to the container init and
to every exec'ed process before the capabilities are dropped, or with
noNewPrivileges once no_new_privs is set.  In both cases they are
applied before seccomp, which may deny io_uring_register.  CRIU does
not restore them, so on restore crun applies them to itself before
running CRIU, and the restored processes inherit them.

Only BPF filters are used, never IORING_REGISTER_RESTRICTIONS, so the
SQE flags are not restricted.  The workload can add its own BPF filters
and restrict its own rings, but it can't register task restrictions
with IORING_REGISTER_RESTRICTIONS.

crun features reports run.oci.crun.io_uring.  The annotation will be
replaced by linux.ioUring once it is in the runtime spec.

Assisted-by: Claude Code (claude-opus-5-5)
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
gvozdetsky added a commit to gvozdetsky/crun that referenced this pull request Oct 30, 2026
seccomp cannot filter io_uring operations, as they are submitted
through the ring and not as syscalls, so today the only option is to
block io_uring entirely.  Linux 7.0 added task-level io_uring BPF
filters, which are inherited across fork and exec and can only be
tightened, like seccomp.

Add the run.oci.io_uring annotation, in the shape proposed for
linux.ioUring in opencontainers/runtime-spec#1328:

  {"ops": [...], "socketFamilies": [...]}

ops is an allowlist of IORING_OP_* names, every other operation is
denied.  socketFamilies, when present, limits IORING_OP_SOCKET to the
listed address families.  It must not be empty, and ops must contain
IORING_OP_SOCKET.

The policy is validated, and kernel support checked, before the
container is created or restored, and before a process is exec'ed in
it.  Registering a filter needs either no_new_privs or CAP_SYS_ADMIN,
so, like seccomp, the restrictions are applied to the container init
and to every exec'ed process before the capabilities are dropped, or
with noNewPrivileges once no_new_privs is set.  In both cases they are
applied before seccomp, which may deny io_uring_register.  CRIU does
not restore them, so on restore crun applies them to itself before
running CRIU, and the restored processes inherit them.

Only BPF filters are used, never IORING_REGISTER_RESTRICTIONS, so the
SQE flags are not restricted and the workload can still add its own
filters.

The annotation will be replaced by linux.ioUring once it is in the
runtime spec.

Assisted-by: Claude Code (claude-opus-5-5)
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
gvozdetsky added a commit to gvozdetsky/crun that referenced this pull request Oct 30, 2026
linux: add experimental io_uring restrictions

seccomp cannot filter io_uring operations, as they are submitted
through the ring and not as syscalls, so today the only option is to
block io_uring entirely.  Linux 7.0 added task-level io_uring BPF
filters, which are inherited across fork and exec and can only be
tightened, like seccomp.

Add the run.oci.io_uring annotation, in the shape proposed for
linux.ioUring in opencontainers/runtime-spec#1328:

  {"ops": [...], "socketFamilies": [...]}

ops is an allowlist of IORING_OP_* names, every other operation is
denied.  socketFamilies, when present, limits IORING_OP_SOCKET to the
listed address families.  It must not be empty, and ops must contain
IORING_OP_SOCKET.  Unknown names are ignored with a warning: what they
would allow stays denied.

Every opcode the kernel knows gets a filter, an allow or a deny one:
with DENY_REST alone, an opcode that a filter inherited by crun allows
would stay allowed.

The policy is validated, and kernel support checked, before the
container is created or restored, and before a process is exec'ed in
it.  Linux 7.0 and 7.1 without bc0e8faf90e7 drop the restrictions on
exec for a task that has used io_uring, so crun also checks that on a
child that restricts itself, creates a ring and re-executes itself.

Registering a filter needs either no_new_privs or CAP_SYS_ADMIN, so,
like seccomp, the restrictions are applied to the container init and
to every exec'ed process before the capabilities are dropped, or with
noNewPrivileges once no_new_privs is set.  In both cases they are
applied before seccomp, which may deny io_uring_register.  CRIU does
not restore them, so on restore crun applies them to itself before
running CRIU, and the restored processes inherit them.

Only BPF filters are used, never IORING_REGISTER_RESTRICTIONS, so the
SQE flags are not restricted.  The workload can add its own BPF filters
and restrict its own rings, but it can't register task restrictions
with IORING_REGISTER_RESTRICTIONS.

crun features reports run.oci.crun.io_uring.  The annotation will be
replaced by linux.ioUring once it is in the runtime spec.

Assisted-by: Claude Code (claude-opus-5-5)
Signed-off-by: Eugene Gvozdetsky <gvozdet@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: linux.ioUring to restrict io_uring operations (Linux 7.0+)

3 participants