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:
co-authored by
Claude Opus 5.5
parent
d97d4313f1
commit
c220d3cdb4
@@ -251,7 +251,13 @@ public sealed class BwrapSandbox : ISandboxRunner
|
||||
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)
|
||||
_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)
|
||||
{
|
||||
int root;
|
||||
try
|
||||
{
|
||||
if (!process.HasExited)
|
||||
process.Kill(entireProcessTree: true);
|
||||
root = process.Id;
|
||||
}
|
||||
catch (InvalidOperationException) { }
|
||||
catch (System.ComponentModel.Win32Exception) { }
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user