Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/ioxide/Native/Native.IoUring.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public static unsafe partial class Native {

public const int EINVAL = 22;
public const int ENOMEM = 12;
public const int EPERM = 1;
public const int ENOSYS = 38;

public const int PROT_READ = 1;
public const int PROT_WRITE = 2;
Expand Down
4 changes: 2 additions & 2 deletions src/ioxide/Reactor/Reactor.RingHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ public T GetService<T>() where T : class

/// <summary>
/// Raised on the reactor's own thread when it is ending because of a fault rather than a
/// <see cref="Stop"/>, after the ring has been torn down. Handle it to log, restart, or bring
/// the process down deliberately.
/// <see cref="Stop"/> - a kernel that refuses to create the ring included - after the ring has
/// been torn down. Handle it to log, restart, or bring the process down deliberately.
/// </summary>
/// <remarks>
/// Without a handler the exception propagates out of <see cref="Run"/>, which on a bare
Expand Down
16 changes: 13 additions & 3 deletions src/ioxide/Reactor/Reactor.Runner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,21 @@ public sealed unsafe partial class Reactor
public void Run()
{
BindReactorThread();
_ring = Ring.Create(_ringEntries);

// A kernel that refuses the ring is a fault the host must hear of too (#270); it gets its own
// catch because there is nothing to tear down until Create returns.
try
{
_ring = Ring.Create(_ringEntries);
}
catch (Exception e) when (OnFault is not null)
{
OnFault(this, e);
return;
}

// Covers setup, not just the loop: OnStart is user code, and a throw from it used to leak
// the listener, the eventfd and both ring mappings. Ring.Create stays outside - nothing to
// tear down until it returns.
// the listener, the eventfd and both ring mappings.
try
{

Expand Down
51 changes: 43 additions & 8 deletions src/ioxide/io_uring/Ring.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,48 @@ private static int SetupWithMemlockRetry(uint entries, IoUringParams* parameters
private static int EstimateRingKib(uint entries)
=> (int)((entries * (64 + 4) + entries * 2 * 16 + 4096) / 1024);

// A failed setup in terms a host can act on: a bare "errno 22" sends people to limits and
// permissions first, when the usual cause is a kernel older than the setup flags (#270).
internal static string DescribeSetupFailure(int errno, uint entries, string kernelRelease)
{
string failed = $"io_uring_setup failed with errno {errno} for a ring of {entries} entries on Linux {kernelRelease}";
return errno switch
{
EINVAL when entries > 32768 => $"{failed}: the kernel allows at most 32768 (ServerConfig.RingEntries).",
EINVAL when KernelOlderThan(kernelRelease, 6, 1)
=> $"{failed}: the kernel does not know SINGLE_ISSUER | DEFER_TASKRUN, and ioxide needs Linux 6.1 or later.",
ENOMEM => $"{failed}: it costs roughly {EstimateRingKib(entries)} KiB of RLIMIT_MEMLOCK, and the kernel "
+ "reclaims a closed ring's memory asynchronously - raise `ulimit -l`, lower "
+ "ServerConfig.RingEntries, or create reactors less abruptly.",
EPERM => $"{failed}: io_uring is disabled here, by the kernel.io_uring_disabled sysctl or by a seccomp "
+ "profile such as a container runtime's default.",
ENOSYS => $"{failed}: this kernel was built without io_uring.",
_ => failed + ".",
};
}

private static string KernelRelease()
{
try
{
return File.ReadAllText("/proc/sys/kernel/osrelease").Trim();
}
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
{
return Environment.OSVersion.Version.ToString();
}
}

// "5.15.167.4-microsoft-standard-WSL2" is older than 6.1; "6.14.0-37-generic" is not.
private static bool KernelOlderThan(string release, int major, int minor)
{
string[] parts = release.Split('.', '-');
return parts.Length >= 2
&& int.TryParse(parts[0], out int a)
&& int.TryParse(parts[1], out int b)
&& (a < major || (a == major && b < minor));
}

public static Ring Create(uint entries)
{
// Prefer NO_SQARRAY (6.6+): the SQ slot index is implicit, dropping one
Expand All @@ -86,14 +128,7 @@ public static Ring Create(uint entries)

if (fd < 0)
{
throw new InvalidOperationException(
$"io_uring_setup failed with errno {-fd}"
+ (fd == -ENOMEM
? $". A ring of {entries} entries costs roughly {EstimateRingKib(entries)} KiB of "
+ "RLIMIT_MEMLOCK, and the kernel reclaims a closed ring's memory "
+ "asynchronously - raise `ulimit -l`, lower ServerConfig.RingEntries, or "
+ "create reactors less abruptly."
: string.Empty));
throw new InvalidOperationException(DescribeSetupFailure(-fd, entries, KernelRelease()));
}

var ring = new Ring
Expand Down
38 changes: 38 additions & 0 deletions tests/Ioxide.Tests.E2E/Core/ReactorSetupTeardownTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,43 @@ public static void Register(Runner runner)
"teardown after the failed bind closed fd 0 - stdin - and the number is now free "
+ "for the next socket the process opens");
});

runner.Test("reactor: a ring the kernel refuses reaches OnFault instead of ending the process", () =>
{
// Every kernel refuses more than 32768 entries with EINVAL - the errno a kernel older than
// 6.1 gives for SINGLE_ISSUER | DEFER_TASKRUN (#270). Ring.Create ran outside the try that
// hands faults to OnFault, so a host that asked to hear of faults lost the process instead.
Exception? reported = null;
var reactor = new Reactor(0, new ServerConfig { ReactorCount = 1, RingEntries = 65_536 })
{
TcpHandle = (_, connection) =>
{
connection.DecRef();
return Task.CompletedTask;
},
OnFault = (_, e) => reported = e,
};

Exception? escaped = null;
var thread = new Thread(() =>
{
try
{
reactor.Run();
}
catch (Exception e)
{
escaped = e;
}
});
thread.Start();

Assert.True(thread.Join(TimeSpan.FromSeconds(10)), "Run should have returned after the ring was refused");
Assert.True(escaped is null,
$"the refused ring was thrown out of Run past OnFault - on a host's thread, that ends the process: {escaped?.Message}");
Assert.True(reported is InvalidOperationException, $"OnFault was not told: {reported}");
Assert.True(reported!.Message.Contains("32768"),
$"the message does not say what the kernel refused: {reported.Message}");
});
}
}
1 change: 1 addition & 0 deletions tests/Ioxide.Tests.Unit/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ private static int Main()

VersionTests.Register(runner);
SyscallErrnoTests.Register(runner);
RingSetupMessageTests.Register(runner);
DemuxParseTests.Register(runner);
MessageTests.Register(runner);
ResponseCapTests.Register(runner);
Expand Down
37 changes: 37 additions & 0 deletions tests/Ioxide.Tests.Unit/RingSetupMessageTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using ioxide;

namespace Ioxide.Tests;

/// <summary>
/// What a refused io_uring_setup says (#270). "errno 22" on its own sent people to limits and
/// permissions, when the usual cause is a kernel older than the setup flags - WSL2's 5.15 refused
/// both attempts with EINVAL.
/// </summary>
internal static class RingSetupMessageTests
{
public static void Register(Runner runner)
{
runner.Test("ring setup: a kernel older than 6.1 is named as the cause", () =>
{
string message = Ring.DescribeSetupFailure(22, 8192, "5.15.167.4-microsoft-standard-WSL2");

Assert.True(message.Contains("5.15.167.4-microsoft-standard-WSL2") && message.Contains("Linux 6.1 or later"),
$"the message does not say the kernel is too old: {message}");
});

runner.Test("ring setup: too many entries is named, not the kernel", () =>
{
string message = Ring.DescribeSetupFailure(22, 65_536, "6.14.0-37-generic");

Assert.True(message.Contains("at most 32768"), $"the message does not name the entry limit: {message}");
Assert.True(!message.Contains("6.1 or later"), $"a 6.14 kernel was blamed for being too old: {message}");
});

runner.Test("ring setup: io_uring disabled by policy says so", () =>
{
string message = Ring.DescribeSetupFailure(1, 8192, "6.14.0-37-generic");

Assert.True(message.Contains("disabled"), $"EPERM was not explained: {message}");
});
}
}
Loading