Repository navigation
config-linux: add linux.ioUring - #1328
gvozdetsky wants to merge 3 commits into
Conversation
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>
There was a problem hiding this comment.
🟡 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.ioUringand 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.
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>
cacd22a to
8c70401
Compare
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>
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>
2538391 to
76517a2
Compare
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>
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>
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>
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>
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>
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
left a comment
There was a problem hiding this comment.
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).
| 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. |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
|
|
||
| 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. |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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>
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>
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>
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>

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,
defaultActionanddeniedSqeFlagsare dropped and register opcodes are out of scope. All of them can be added later without breaking existing configs.socketFamiliescan't be empty, and it requiresIORING_OP_SOCKETinops.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.