Sandbox: Beenden sammelt den ganzen Baum ein und wartet nicht ewig auf Rohre (#654)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-25 20:42:59 +02:00
co-authored by Claude Opus 5.5
parent d97d4313f1
commit c220d3cdb4
+71 -6
View File
@@ -251,7 +251,13 @@ public sealed class BwrapSandbox : ISandboxRunner
throw; throw;
} }
await Task.WhenAll(stdout, stderr); // After a stop, the streams end with the processes — but a reader must never hang on a
// pipe something still holds. Bounded, then the result is what arrived.
var pumps = Task.WhenAll(stdout, stderr);
if (timedOut || output.Truncated)
await Task.WhenAny(pumps, Task.Delay(TimeSpan.FromSeconds(5), CancellationToken.None));
else
await pumps;
if (timedOut) if (timedOut)
_logger.LogWarning("Sandbox run of {Program} stopped after {Timeout}", request.Executable, request.Limits.Timeout); _logger.LogWarning("Sandbox run of {Program} stopped after {Timeout}", request.Executable, request.Limits.Timeout);
@@ -280,16 +286,75 @@ public sealed class BwrapSandbox : ISandboxRunner
} }
} }
/// <summary>bwrap dies, and with it — die-with-parent, own PID namespace — everything inside.</summary> /// <summary>
/// Stops the run: everything below bwrap is collected first, then all of it is killed.
///
/// <para><b>Not trusting <c>--die-with-parent</c>.</b> The kernel clears the parent-death
/// signal when a process changes its credentials — and bwrap does exactly that while it
/// builds the user namespace. Seen on the test machine: bwrap's monitor was gone, the
/// sandbox lived on under PID 1 and kept the output pipe open. Killing the sandbox's own
/// PID 1 (the init of its PID namespace) takes every process inside with it.</para>
/// </summary>
private static void Kill(Process process) private static void Kill(Process process)
{ {
int root;
try try
{ {
if (!process.HasExited) root = process.Id;
process.Kill(entireProcessTree: true);
} }
catch (InvalidOperationException) { } catch (InvalidOperationException)
catch (System.ComponentModel.Win32Exception) { } {
return;
}
foreach (var pid in Descendants(root).Append(root))
{
try
{
using var victim = Process.GetProcessById(pid);
victim.Kill();
}
catch (ArgumentException) { } // already gone
catch (InvalidOperationException) { }
catch (System.ComponentModel.Win32Exception) { }
}
}
/// <summary>All processes below <paramref name="root"/>, from /proc — collected before anything is killed.</summary>
private static IEnumerable<int> Descendants(int root)
{
var children = new Dictionary<int, List<int>>();
foreach (var dir in Directory.EnumerateDirectories("/proc"))
{
if (!int.TryParse(Path.GetFileName(dir), out var pid))
continue;
try
{
// /proc/<pid>/stat: "pid (comm) state ppid …" — comm may hold spaces and parentheses.
var stat = File.ReadAllText(Path.Combine(dir, "stat"));
var fields = stat[(stat.LastIndexOf(')') + 2)..].Split(' ');
if (int.TryParse(fields[1], out var parent))
{
if (!children.TryGetValue(parent, out var list))
children[parent] = list = [];
list.Add(pid);
}
}
catch (IOException) { } // it ended meanwhile
catch (UnauthorizedAccessException) { }
}
var result = new List<int>();
var queue = new Queue<int>([root]);
while (queue.Count > 0 && result.Count < 10_000)
{
foreach (var child in children.GetValueOrDefault(queue.Dequeue()) ?? [])
{
result.Add(child);
queue.Enqueue(child);
}
}
return result;
} }
private static string Kilobytes(long bytes) => Math.Max(1, bytes / 1024).ToString(CultureInfo.InvariantCulture); private static string Kilobytes(long bytes) => Math.Max(1, bytes / 1024).ToString(CultureInfo.InvariantCulture);