restrictions.dhover×126all
#!/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_restrictions

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.)

io_uring_restrictions
;
import
(module) during

Simple idiomatic dlang wrapper around linux io_uring (see: https://kernel.dk/io_uring.pdf) asynchronous API.

during
;
import
(package) core
core
.
(package) core.sys
sys
.
(package) core.sys.linux
linux
.
(module) core.sys.linux.errno

D header file for GNU/Linux

glibc stdlib/errno.h

errno
:
(alias constant) io_uring_restrictions.EINVAL = int core.stdc.errno.EINVAL = 22
EINVAL
,
(alias constant) io_uring_restrictions.EOPNOTSUPP = int core.stdc.errno.EOPNOTSUPP = 95
EOPNOTSUPP
,
(alias constant) io_uring_restrictions.ENOSYS = int core.stdc.errno.ENOSYS = 38
ENOSYS
,
(alias constant) io_uring_restrictions.EACCES = int core.stdc.errno.EACCES = 13
EACCES
;
import
(package) std
std
.
(module) std.stdio
Category 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:

  1. The lowest layer is the operating system layer. The two main schemes are Windows and Posix.

  2. C's stdio.h which unifies the two operating system schemes.

  3. std.stdio, this module, unifies the various stdio.h implementations into a high level package for D programs.

Source

std/stdio.d

@copyrightCopyright The D Language Foundation 2007-.@licenseBoost License 1.0.@authorsWalter Bright, Andrei Alexandrescu, Alex Rønne Petersen
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 = 41232LU
nopCookie
= 0xA11_0; // user_data for the allowed NOP
enum ulong
(constant) ulong io_uring_restrictions.main.timeoutCookie = 912080LU
timeoutCookie
= 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.Uring

Main 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 io
io
;
const
(local variable) const(int) setupRet
setupRet
=
(local variable) during.Uring io
io
.
int during.setup(ref during.Uring uring, uint entries = 128u, during.io_uring.SetupFlags flags = SetupFlags.NONE) nothrow @nogc @safe

Setup new instance of io_uring into provided Uring structure.

@paramuring Uring structure to be initialized (must not be already initialized)@paramentries Number of entries to initialize uring with@paramflags SetupFlags to use to initialize uring.@returnsOn succes it returns 0, -errno otherwise.
setup
(8,
(enum) during.io_uring.SetupFlags

io_uring_setup() flags

SetupFlags
.
(enum value) during.io_uring.SetupFlags.R_DISABLED = 64u

IORING_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) setupRet
setupRet
== -
(constant) int core.stdc.errno.EINVAL = 22
EINVAL
)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("SKIP: IORING_SETUP_R_DISABLED unsupported (errno %d) — restrictions need Linux 5.10+", -
(local variable) const(int) setupRet
setupRet
);
return 0; } if (
(local variable) const(int) setupRet
setupRet
< 0)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("SKIP: io_uring_setup failed (errno %d) — io_uring unavailable on this host", -
(local variable) const(int) setupRet
setupRet
);
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_restriction
io_uring_restriction
[1]
(local variable) during.io_uring.io_uring_restriction[1] rules
rules
;
(local variable) during.io_uring.io_uring_restriction[1] rules
rules
[0].
(field) during.io_uring.RestrictionOp during.io_uring.io_uring_restriction.opcode
opcode
=
(enum) during.io_uring.RestrictionOp

io_uring_restriction->opcode values

RestrictionOp
.
(enum value) during.io_uring.RestrictionOp.IORING_RESTRICTION_SQE_OP = cast(ushort)1u

Allow an sqe opcode

IORING_RESTRICTION_SQE_OP
;
(local variable) during.io_uring.io_uring_restriction[1] rules
rules
[0].
(field) ubyte during.io_uring.io_uring_restriction.sqe_op

IORING_RESTRICTION_SQE_OP

sqe_op
= cast(ubyte)
(enum) during.io_uring.Operation

Describes the operation to be performed

@seeio_uring_enter(2)
Operation
.
(enum value) during.io_uring.Operation.NOP = cast(ubyte)0u

IORING_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) regRet
regRet
= () @trusted {
return
int during.io_uring.io_uring_register(int fd, during.io_uring.RegisterOpCode opcode, const(void)* arg, uint nr_args) nothrow @nogc @system

Register 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.

@see`io_uring_register(2)@paramfd the file descriptor returned by a call to io_uring_setup(2)@paramopcode code of operation to execute on args@paramarg Args used by specified operation. See RegisterOpCode for usage details.@paramnr_args number of provided arguments@returnsOn success, io_uring_register() returns 0. On error, -1 is returned, and errno is set accordingly.
io_uring_register
(
(local variable) during.Uring io
io
.
int during.Uring.fd() const pure nothrow @nogc @safe

Native io_uring file descriptor

fd
,
(enum) during.io_uring.RegisterOpCode

io_uring_register(2) opcodes and arguments

RegisterOpCode
.
(enum value) during.io_uring.RegisterOpCode.REGISTER_RESTRICTIONS = 11u

IORING_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] rules
rules
.
(constant) during.io_uring.io_uring_restriction* during.io_uring.io_uring_restriction[1].ptr = &rules
ptr
,
cast(uint)
(local variable) during.io_uring.io_uring_restriction[1] rules
rules
.
(constant) ulong during.io_uring.io_uring_restriction[1].length = 1LU
length
,
); }(); if (
(local variable) const(int) regRet
regRet
< 0)
{ const
(local variable) const(int) e
e
= -
(local variable) const(int) regRet
regRet
;
if (
(local variable) const(int) e
e
==
(constant) int core.stdc.errno.EINVAL = 22
EINVAL
||
(local variable) const(int) e
e
==
(constant) int core.stdc.errno.EOPNOTSUPP = 95
EOPNOTSUPP
||
(local variable) const(int) e
e
==
(constant) int core.stdc.errno.ENOSYS = 38
ENOSYS
)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("SKIP: IORING_REGISTER_RESTRICTIONS unsupported (errno %d)",
(local variable) const(int) e
e
);
return 0; } stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("register restrictions failed unexpectedly: errno %d",
(local variable) const(int) e
e
);
return 1; } // (3) Flip the ring on. After this the whitelist is frozen forever. const
(local variable) const(int) enableRet
enableRet
=
(local variable) during.Uring io
io
.
int during.Uring.enableRings() nothrow @nogc @trusted

Enable the "rings" of this Uring if they were previously disabled with IORING_SETUP_R_DISABLED.

Note

Available from Linux 5.10

@returnsOn success, returns 0. On error, -errno is returned.
enableRings
();
if (
(local variable) const(int) enableRet
enableRet
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("enableRings failed: errno %d", -
(local variable) const(int) enableRet
enableRet
);
return 1; } // (4) Allowed op: a plain NOP should sail through.
(local variable) during.Uring io
io
.putWith!((ref SubmissionEntry e) {
e.prepNop(); e.user_data = nopCookie; })(); const
(local variable) const(int) nopSubmitted
nopSubmitted
=
(local variable) during.Uring io
io
.
int during.Uring.submit(uint want) nothrow @nogc @safe

Submits qued SubmissionEntry to be processed by kernel.

@paramwant number of CompletionEntries to wait for. If 0, this just submits queued entries and returns. If > 0, it blocks until at least wanted number of entries were completed.@paramsig See io_uring_enter(2) man page@returnsNumber of submitted entries on success, -errno on error
submit
(1);
if (
(local variable) const(int) nopSubmitted
nopSubmitted
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("submitting the NOP failed: errno %d", -
(local variable) const(int) nopSubmitted
nopSubmitted
);
return 1; }
(local variable) during.Uring io
io
.
int during.Uring.wait(uint want = 1u) nothrow @nogc

Simmilar to submit but with this method we just wait for required number of CompletionEntries.

@returns0 on success, -errno on error
wait
(1);
const
(local variable) const(int) nopRes
nopRes
=
(local variable) during.Uring io
io
.
during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safe

Get first CompletionEntry from cq ring

front
.
(field) int during.io_uring.CompletionEntry.res

result code for this event

res
;
(local variable) during.Uring io
io
.
void during.Uring.popFront() pure nothrow @nogc @safe

Move to next CompletionEntry

popFront
();
if (
(local variable) const(int) nopRes
nopRes
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("whitelisted NOP was rejected (errno %d) — restriction too tight", -
(local variable) const(int) nopRes
nopRes
);
return 1; }
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("ok: whitelisted NOP completed (res=%d)",
(local variable) const(int) nopRes
nopRes
);
// (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.KernelTimespec

Time specification as defined in kernel headers (used by TIMEOUT operations)

KernelTimespec
(local variable) during.io_uring.KernelTimespec ts
ts
= { tv_sec: 0, tv_nsec: 0 };
(local variable) during.Uring io
io
.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 @safe

Adds 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().

@paramFN Function to fill next entry in queue by ref (should be faster). It is expected to be in a form of void function(ARGS)(ref SubmissionEntry, auto ref ARGS). Note that in this case queue entry is cleaned first before function is called.@paramentry Custom built SubmissionEntry to be posted as is. Note that in this case it is copied whole over one in the SubmissionQueue.@paramargs Optional arguments passed to the function@returnsreference to Uring structure so it's possible to chain multiple commands.
ts
);
const
(local variable) const(int) toSubmitted
toSubmitted
=
(local variable) during.Uring io
io
.
int during.Uring.submit(uint want) nothrow @nogc @safe

Submits qued SubmissionEntry to be processed by kernel.

@paramwant number of CompletionEntries to wait for. If 0, this just submits queued entries and returns. If > 0, it blocks until at least wanted number of entries were completed.@paramsig See io_uring_enter(2) man page@returnsNumber of submitted entries on success, -errno on error
submit
(1);
if (
(local variable) const(int) toSubmitted
toSubmitted
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("submitting the TIMEOUT failed: errno %d", -
(local variable) const(int) toSubmitted
toSubmitted
);
return 1; }
(local variable) during.Uring io
io
.
int during.Uring.wait(uint want = 1u) nothrow @nogc

Simmilar to submit but with this method we just wait for required number of CompletionEntries.

@returns0 on success, -errno on error
wait
(1);
const
(local variable) const(int) toRes
toRes
=
(local variable) during.Uring io
io
.
during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safe

Get first CompletionEntry from cq ring

front
.
(field) int during.io_uring.CompletionEntry.res

result code for this event

res
;
const
(local variable) const(ulong) toData
toData
=
(local variable) during.Uring io
io
.
during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safe

Get first CompletionEntry from cq ring

front
.
(field) ulong during.io_uring.CompletionEntry.user_data

sqe->data submission passed back

user_data
;
(local variable) during.Uring io
io
.
void during.Uring.popFront() pure nothrow @nogc @safe

Move to next CompletionEntry

popFront
();
if (
(local variable) const(ulong) toData
toData
!=
(constant) ulong io_uring_restrictions.main.timeoutCookie = 912080LU
timeoutCookie
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("CQE cookie mismatch: expected 0x%X, got 0x%X",
(constant) ulong io_uring_restrictions.main.timeoutCookie = 912080LU
timeoutCookie
,
(local variable) const(ulong) toData
toData
);
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) toRes
toRes
!= -
(constant) int core.stdc.errno.EACCES = 13
EACCES
&&
(local variable) const(int) toRes
toRes
!= -
(constant) int core.stdc.errno.EINVAL = 22
EINVAL
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("expected TIMEOUT to be denied (-EACCES/-EINVAL) but got res=%d",
(local variable) const(int) toRes
toRes
);
return 1; }
void std.stdio.writefln!(char, const(int), const(int))(in char[] fmt, const(int) __param_1, const(int) __param_2) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("ok: non-whitelisted TIMEOUT rejected by the sandbox (res=%d, errno %d)",
(local variable) const(int) toRes
toRes
, -
(local variable) const(int) toRes
toRes
);
void std.stdio.writefln!char(in char[] fmt) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("ok: IORING_REGISTER_RESTRICTIONS sandboxed the ring — NOP allowed, TIMEOUT denied");
return 0; }