Sandbox: Start aus einem langlebigen Thread - das Elternsterbe-Signal haengt am Thread (#654)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-25 21:20:27 +02:00
co-authored by Claude Opus 5.5
parent 6204f1247c
commit f97dfa06d6
+47 -1
View File
@@ -219,7 +219,7 @@ public sealed class BwrapSandbox : ISandboxRunner
private async Task<SandboxResult> RunProcessAsync(ProcessStartInfo start, SandboxRequest request, CancellationToken ct)
{
using var process = Process.Start(start) ?? throw new InvalidOperationException("The sandbox did not start.");
using var process = await Spawner.StartAsync(start);
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeout.CancelAfter(request.Limits.Timeout);
@@ -411,6 +411,52 @@ public sealed class BwrapSandbox : ISandboxRunner
}
}
/// <summary>
/// #654: Starts every sandbox from one thread that lives as long as the process.
///
/// <para><b>Why.</b> <c>--die-with-parent</c> uses the kernel's parent-death signal, and that
/// is bound to the <i>thread</i> that forked, not to the process. .NET forks on the calling
/// thread — a thread-pool thread, and the pool retires idle threads. Seen on the test machine
/// under load: the pool dropped a thread, bwrap got SIGKILL (exit 137), and when it happened
/// while bwrap was still building the sandbox, the inside lived on as an orphan holding the
/// output pipes. Started from a thread that never ends, the signal fires only when the
/// instance itself dies — which is what it is for.</para>
/// </summary>
internal static class Spawner
{
private static readonly System.Collections.Concurrent.BlockingCollection<(ProcessStartInfo Start, TaskCompletionSource<Process> Done)> Queue = new();
static Spawner()
{
var thread = new Thread(() =>
{
foreach (var (start, done) in Queue.GetConsumingEnumerable())
{
try
{
done.SetResult(Process.Start(start) ?? throw new InvalidOperationException("The sandbox did not start."));
}
catch (Exception ex)
{
done.SetException(ex);
}
}
})
{
IsBackground = true,
Name = "ClawdDotNet sandbox spawner"
};
thread.Start();
}
public static Task<Process> StartAsync(ProcessStartInfo start)
{
var done = new TaskCompletionSource<Process>(TaskCreationOptions.RunContinuationsAsynchronously);
Queue.Add((start, done));
return done.Task;
}
}
/// <summary>#654: What the installed bwrap can do beyond the basics.</summary>
public sealed record BwrapFeatures(bool DisableUserns, bool ClearEnv)
{