#!/usr/bin/env dub
/+ dub.sdl:
name "io_uring_restrictions"
dependency "during" version="~>0.5.0"
platforms "linux"
targetPath "build"
+/
/**
* `io_uring` — sandboxing a ring with `IORING_REGISTER_RESTRICTIONS` (Linux 5.10).
*
* The 5.10 "deferred setup" trio lets you create a ring that starts *disabled*,
* lock down exactly which operations it may ever perform, and only then turn it
* on. The whitelist is one-shot — once installed it can never be widened — so a
* privileged process can hand a tightly-scoped ring to untrusted code.
*
* This demo:
* 1. sets up a ring with `IORING_SETUP_R_DISABLED` (no SQEs accepted yet);
* 2. registers a restriction whitelisting only `IORING_OP_NOP` as a legal SQE
* opcode (`IORING_RESTRICTION_SQE_OP`);
* 3. enables the ring with `IORING_REGISTER_ENABLE_RINGS`;
* 4. proves an allowed op (`NOP`) completes normally;
* 5. proves a *non*-whitelisted op (`TIMEOUT`) is rejected — the kernel hands
* back a CQE with `-EACCES` instead of running it.
*
* Companion to the io_uring chronology:
* see docs/research/async-io/io-uring/timeline.md
* § "5.10 — Restrictions and deferred setup (December 2020)".
*
* Run with: `dub run --single restrictions.d`
*
* Portability: prints a `SKIP:` line and exits 0 if io_uring is unavailable or
* the restrictions API is not supported on the running kernel, so it stays green
* in CI regardless of host. (Restrictions are supported on this 6.18 box.)
*/
module (module) io_uring_restrictionsio_uring — sandboxing a ring with IORING_REGISTER_RESTRICTIONS (Linux 5.10).
The 5.10 "deferred setup" trio lets you create a ring that starts disabled,
lock down exactly which operations it may ever perform, and only then turn it
on. The whitelist is one-shot — once installed it can never be widened — so a
privileged process can hand a tightly-scoped ring to untrusted code.
This demo:
sets up a ring with IORING_SETUP_R_DISABLED (no SQEs accepted yet);
registers a restriction whitelisting only IORING_OP_NOP as a legal SQE
opcode (IORING_RESTRICTION_SQE_OP);
enables the ring with IORING_REGISTER_ENABLE_RINGS;
proves an allowed op (NOP) completes normally;
proves a non-whitelisted op (TIMEOUT) is rejected — the kernel hands
back a CQE with -EACCES instead of running it.
Companion to the io_uring chronology:
see docs/research/async-io/io-uring/timeline.md
§ "5.10 — Restrictions and deferred setup (December 2020)".
Run with: dub run --single restrictions.d
Portability
prints a SKIP: line and exits 0 if io_uring is unavailable or
the restrictions API is not supported on the running kernel, so it stays green
in CI regardless of host. (Restrictions are supported on this 6.18 box.)
io_uring_restrictions;
import (module) duringSimple idiomatic dlang wrapper around linux io_uring
(see: https://kernel.dk/io_uring.pdf) asynchronous API.
during;
import (package) corecore.(package) core.syssys.(package) core.sys.linuxlinux.(module) core.sys.linux.errnoD header file for GNU/Linux
errno : (alias constant) io_uring_restrictions.EINVAL = int core.stdc.errno.EINVAL = 22EINVAL, (alias constant) io_uring_restrictions.EOPNOTSUPP = int core.stdc.errno.EOPNOTSUPP = 95EOPNOTSUPP, (alias constant) io_uring_restrictions.ENOSYS = int core.stdc.errno.ENOSYS = 38ENOSYS, (alias constant) io_uring_restrictions.EACCES = int core.stdc.errno.EACCES = 13EACCES;
import (package) stdstd.(module) std.stdioCategory Symbols File handles _popen File isFileHandle openNetwork stderr stdin stdout Reading chunks lines readf readfln readln Writing toFile write writef writefln writeln Misc KeepTerminator LockType StdioException
Standard I/O functions that extend core.stdc.stdio. core.stdc.stdio
is publically imported when importing std.stdio.
There are three layers of I/O:
The lowest layer is the operating system layer. The two main schemes are Windows and Posix.
C's stdio.h which unifies the two operating system schemes.
std.stdio, this module, unifies the various stdio.h implementations into
a high level package for D programs.
Source
std/stdio.d
stdio : (alias template) io_uring_restrictions.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))Equivalent to writef(fmt, args, '\n').
writefln, stderr;
int int D main()main()
{
enum ulong (constant) ulong io_uring_restrictions.main.nopCookie = 41232LUnopCookie = 0xA11_0; // user_data for the allowed NOP
enum ulong (constant) ulong io_uring_restrictions.main.timeoutCookie = 912080LUtimeoutCookie = 0xDEAD_0; // user_data for the forbidden TIMEOUT
// (1) Create the ring in the *disabled* state. While disabled it accepts no
// SQEs; the only thing we may do is register restrictions/files/buffers and
// then enable it. Old kernels without R_DISABLED fail setup with -EINVAL.
(struct) during.UringMain entry point to work with io_uring.
It hides SubmissionQueue and CompletionQueue behind standard range interface.
We put in SubmissionEntry entries and take out CompletionEntry entries.
Use predefined prepXX methods to fill required fields of SubmissionEntry before put or during putWith.
Note
prepXX functions doesn't touch previous entry state, just fills in operation properties. This is because for
less error prone interface it is cleared automatically when prepared using putWith. So when using on own SubmissionEntry
(outside submission queue), that would be added to the submission queue using put, be sure its cleared if it's
reused for multiple operations.
Uring (local variable) during.Uring ioio;
const (local variable) const(int) setupRetsetupRet = (local variable) during.Uring ioio.int during.setup(ref during.Uring uring, uint entries = 128u, during.io_uring.SetupFlags flags = SetupFlags.NONE) nothrow @nogc @safeSetup new instance of io_uring into provided Uring structure.
setup(8, (enum) during.io_uring.SetupFlagsio_uring_setup() flags
SetupFlags.(enum value) during.io_uring.SetupFlags.R_DISABLED = 64uIORING_SETUP_R_DISABLED flag to start the rings disabled, allowing the user to register
restrictions, buffers, files, before to start processing SQEs.
When IORING_SETUP_R_DISABLED is set, SQE are not processed and SQPOLL kthread is not started.
The restrictions registration are allowed only when the rings are disable to prevent
concurrency issue while processing SQEs.
The rings can be enabled using IORING_REGISTER_ENABLE_RINGS opcode with io_uring_register(2).
Note
Available from Linux 5.10
R_DISABLED);
if ((local variable) const(int) setupRetsetupRet == -(constant) int core.stdc.errno.EINVAL = 22EINVAL)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln("SKIP: IORING_SETUP_R_DISABLED unsupported (errno %d) — restrictions need Linux 5.10+", -(local variable) const(int) setupRetsetupRet);
return 0;
}
if ((local variable) const(int) setupRetsetupRet < 0)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln("SKIP: io_uring_setup failed (errno %d) — io_uring unavailable on this host", -(local variable) const(int) setupRetsetupRet);
return 0;
}
// (2) Build the whitelist. A single `SQE_OP` entry naming NOP is enough to
// prove the point: as soon as *any* SQE-opcode allow-rule is registered, the
// kernel switches that dimension to deny-by-default, so every opcode not on
// the list (e.g. TIMEOUT) is refused.
(struct) during.io_uring.io_uring_restrictionio_uring_restriction[1] (local variable) during.io_uring.io_uring_restriction[1] rulesrules;
(local variable) during.io_uring.io_uring_restriction[1] rulesrules[0].(field) during.io_uring.RestrictionOp during.io_uring.io_uring_restriction.opcodeopcode = (enum) during.io_uring.RestrictionOpio_uring_restriction->opcode values
RestrictionOp.(enum value) during.io_uring.RestrictionOp.IORING_RESTRICTION_SQE_OP = cast(ushort)1uAllow an sqe opcode
IORING_RESTRICTION_SQE_OP;
(local variable) during.io_uring.io_uring_restriction[1] rulesrules[0].(field) ubyte during.io_uring.io_uring_restriction.sqe_opIORING_RESTRICTION_SQE_OP
sqe_op = cast(ubyte) (enum) during.io_uring.OperationDescribes the operation to be performed
Operation.(enum value) during.io_uring.Operation.NOP = cast(ubyte)0uIORING_OP_NOP
NOP;
// `Uring.registerRestrictions` only forwards a single entry, so call the raw
// syscall wrapper directly to pass the array + count. (Here it's one entry,
// but this is the shape you'd use to whitelist several opcodes at once.)
const (local variable) const(int) regRetregRet = () @trusted {
return int during.io_uring.io_uring_register(int fd, during.io_uring.RegisterOpCode opcode, const(void)* arg, uint nr_args) nothrow @nogc @systemRegister files or user buffers for asynchronous I/O.
The ``io_uring_register() system call registers user buffers or files for use in an io_uring(7)
instance referenced by fd. Registering files or user buffers allows the kernel to take long term
references to internal data structures or create long term mappings of application memory,
greatly reducing per-I/O overhead.
io_uring_register(
(local variable) during.Uring ioio.int during.Uring.fd() const pure nothrow @nogc @safeNative io_uring file descriptor
fd,
(enum) during.io_uring.RegisterOpCodeio_uring_register(2) opcodes and arguments
RegisterOpCode.(enum value) during.io_uring.RegisterOpCode.REGISTER_RESTRICTIONS = 11uIORING_REGISTER_RESTRICTIONS (from Linux 5.10)
Permanently installs a feature allowlist on an io_ring_ctx. The io_ring_ctx can then be
passed to untrusted code with the knowledge that only operations present in the allowlist can
be executed.
The allowlist approach ensures that new features added to io_uring do not accidentally become
available when an existing application is launched on a newer kernel version.
Currently it's possible to restrict sqe opcodes, sqe flags, and register opcodes.
IOURING_REGISTER_RESTRICTIONS can only be made once. Afterwards it is not possible to
change restrictions anymore. This prevents untrusted code from removing restrictions.
REGISTER_RESTRICTIONS,
(local variable) during.io_uring.io_uring_restriction[1] rulesrules.(constant) during.io_uring.io_uring_restriction* during.io_uring.io_uring_restriction[1].ptr = &rulesptr,
cast(uint) (local variable) during.io_uring.io_uring_restriction[1] rulesrules.(constant) ulong during.io_uring.io_uring_restriction[1].length = 1LUlength,
);
}();
if ((local variable) const(int) regRetregRet < 0)
{
const (local variable) const(int) ee = -(local variable) const(int) regRetregRet;
if ((local variable) const(int) ee == (constant) int core.stdc.errno.EINVAL = 22EINVAL || (local variable) const(int) ee == (constant) int core.stdc.errno.EOPNOTSUPP = 95EOPNOTSUPP || (local variable) const(int) ee == (constant) int core.stdc.errno.ENOSYS = 38ENOSYS)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln("SKIP: IORING_REGISTER_RESTRICTIONS unsupported (errno %d)", (local variable) const(int) ee);
return 0;
}
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("register restrictions failed unexpectedly: errno %d", (local variable) const(int) ee);
return 1;
}
// (3) Flip the ring on. After this the whitelist is frozen forever.
const (local variable) const(int) enableRetenableRet = (local variable) during.Uring ioio.int during.Uring.enableRings() nothrow @nogc @trustedEnable the "rings" of this Uring if they were previously disabled with
IORING_SETUP_R_DISABLED.
Note
Available from Linux 5.10
enableRings();
if ((local variable) const(int) enableRetenableRet < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("enableRings failed: errno %d", -(local variable) const(int) enableRetenableRet);
return 1;
}
// (4) Allowed op: a plain NOP should sail through.
(local variable) during.Uring ioio.putWith!((ref SubmissionEntry e) {
e.prepNop();
e.user_data = nopCookie;
})();
const (local variable) const(int) nopSubmittednopSubmitted = (local variable) during.Uring ioio.int during.Uring.submit(uint want) nothrow @nogc @safeSubmits qued SubmissionEntry to be processed by kernel.
submit(1);
if ((local variable) const(int) nopSubmittednopSubmitted < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("submitting the NOP failed: errno %d", -(local variable) const(int) nopSubmittednopSubmitted);
return 1;
}
(local variable) during.Uring ioio.int during.Uring.wait(uint want = 1u) nothrow @nogcSimmilar to submit but with this method we just wait for required number
of CompletionEntries.
wait(1);
const (local variable) const(int) nopResnopRes = (local variable) during.Uring ioio.during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safeGet first CompletionEntry from cq ring
front.(field) int during.io_uring.CompletionEntry.resresult code for this event
res;
(local variable) during.Uring ioio.void during.Uring.popFront() pure nothrow @nogc @safeMove to next CompletionEntry
popFront();
if ((local variable) const(int) nopResnopRes < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("whitelisted NOP was rejected (errno %d) — restriction too tight", -(local variable) const(int) nopResnopRes);
return 1;
}
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln("ok: whitelisted NOP completed (res=%d)", (local variable) const(int) nopResnopRes);
// (5) Forbidden op: TIMEOUT is not on the whitelist. The kernel doesn't run
// it — instead it posts a completion carrying -EACCES. (We give it a 0-second
// relative timeout so that on the off chance it *were* allowed, it would fire
// immediately rather than block.)
(struct) during.io_uring.KernelTimespecTime specification as defined in kernel headers (used by TIMEOUT operations)
KernelTimespec (local variable) during.io_uring.KernelTimespec tsts = { tv_sec: 0, tv_nsec: 0 };
(local variable) during.Uring ioio.putWith!((ref SubmissionEntry e, ref KernelTimespec t) {
e.prepTimeout(t);
e.user_data = timeoutCookie;
})(during.Uring during.Uring.putWith!(function (ref during.io_uring.SubmissionEntry e, ref during.io_uring.KernelTimespec t) nothrow @nogc @safe
{
prepTimeout(e, t, 0LU, TimeoutFlags.REL);
e.user_data = 912080LU;
}
, during.io_uring.KernelTimespec)(ref during.io_uring.KernelTimespec __param_0) nothrow @nogc return ref @safeAdds new entry to the SubmissionQueue.
Note that this just adds entry to the queue and doesn't advance the tail
marker kernel sees. For that finishSq() is needed to be called next.
Also note that to actually enter new entries to kernel,
it's needed to call submit().
ts);
const (local variable) const(int) toSubmittedtoSubmitted = (local variable) during.Uring ioio.int during.Uring.submit(uint want) nothrow @nogc @safeSubmits qued SubmissionEntry to be processed by kernel.
submit(1);
if ((local variable) const(int) toSubmittedtoSubmitted < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("submitting the TIMEOUT failed: errno %d", -(local variable) const(int) toSubmittedtoSubmitted);
return 1;
}
(local variable) during.Uring ioio.int during.Uring.wait(uint want = 1u) nothrow @nogcSimmilar to submit but with this method we just wait for required number
of CompletionEntries.
wait(1);
const (local variable) const(int) toRestoRes = (local variable) during.Uring ioio.during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safeGet first CompletionEntry from cq ring
front.(field) int during.io_uring.CompletionEntry.resresult code for this event
res;
const (local variable) const(ulong) toDatatoData = (local variable) during.Uring ioio.during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safeGet first CompletionEntry from cq ring
front.(field) ulong during.io_uring.CompletionEntry.user_datasqe->data submission passed back
user_data;
(local variable) during.Uring ioio.void during.Uring.popFront() pure nothrow @nogc @safeMove to next CompletionEntry
popFront();
if ((local variable) const(ulong) toDatatoData != (constant) ulong io_uring_restrictions.main.timeoutCookie = 912080LUtimeoutCookie)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("CQE cookie mismatch: expected 0x%X, got 0x%X", (constant) ulong io_uring_restrictions.main.timeoutCookie = 912080LUtimeoutCookie, (local variable) const(ulong) toDatatoData);
return 1;
}
// The kernel returns -EACCES for a denied opcode; some versions surface
// -EINVAL instead. Either proves the whitelist blocked the op.
if ((local variable) const(int) toRestoRes != -(constant) int core.stdc.errno.EACCES = 13EACCES && (local variable) const(int) toRestoRes != -(constant) int core.stdc.errno.EINVAL = 22EINVAL)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("expected TIMEOUT to be denied (-EACCES/-EINVAL) but got res=%d", (local variable) const(int) toRestoRes);
return 1;
}
void std.stdio.writefln!(char, const(int), const(int))(in char[] fmt, const(int) __param_1, const(int) __param_2) @safeEquivalent to writef(fmt, args, '\n').
writefln("ok: non-whitelisted TIMEOUT rejected by the sandbox (res=%d, errno %d)", (local variable) const(int) toRestoRes, -(local variable) const(int) toRestoRes);
void std.stdio.writefln!char(in char[] fmt) @safeEquivalent to writef(fmt, args, '\n').
writefln("ok: IORING_REGISTER_RESTRICTIONS sandboxed the ring — NOP allowed, TIMEOUT denied");
return 0;
}