Files
Playa/tests/Volcanic.AgenticCore.Tests/Processes/HostProcessHardeningTests.cs
EugeneTes 76bda6cf55 fix(processes): address review findings
- Fill TimeZoneId only for a configured session/host zone with a resolvable IANA id
  (IExtensionHost.TimeZoneId); otherwise children inherit the host's TZ instead of TZ=Local/UTC.
- Host hardening logs a warning instead of throwing when libc/prctl can't be resolved.
- A hook or process_spawn handler returning a null or incomplete context fails the spawn with
  InvalidOperationException, so bash reports "process environment hook failed".
- Move tool-result redaction into the Agent slice with a behavioural no-change check.
- ProcessSpawnContext.ApplyTo(psi) sets cwd and replaces the environment; used by bash and ExecAsync.
- Split the Telegram process environment types into their own files; move the Extensions.Tests
  NoHostHardening under Support/; check the Claude Code assembly never references the spawn policy.
- Spec and docs: TZ inheritance, incomplete contexts, ApplyTo, async spawn handlers, root skip.
2026-09-26 19:32:47 +02:00

138 lines
5.8 KiB
C#

using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Volcanic.AgenticCore.Features.Processes;
using Volcanic.AgenticCore.Tests.Support;
namespace Volcanic.AgenticCore.Tests.Processes;
public class HostProcessHardeningTests
{
private static readonly ProcessEnvironmentOptions On = new();
private static readonly ProcessEnvironmentOptions Off = new() { HardenHostProcess = false };
private static Func<string, string?> Env(string? diagnostics = "0", string? ipc = null) =>
name => name switch { "DOTNET_EnableDiagnostics" => diagnostics, "DOTNET_EnableDiagnostics_IPC" => ipc, _ => null };
[Fact]
public void Linux_with_the_flag_on_calls_prctl_with_zero()
{
var calls = new List<int>();
var logger = new RecordingLogger();
new HostProcessHardening.Hardener(v => { calls.Add(v); return 0; }, isLinux: true, Env()).Apply(On, logger);
Assert.Equal([0], calls);
Assert.True(logger.Has(LogLevel.Information, "Host process hardened: not dumpable"));
}
[Fact]
public void A_second_apply_does_not_call_prctl_again()
{
var calls = 0;
var hardener = new HostProcessHardening.Hardener(_ => { calls++; return 0; }, isLinux: true, Env());
hardener.Apply(On, new RecordingLogger());
hardener.Apply(On, new RecordingLogger());
Assert.Equal(1, calls);
}
[Fact]
public void A_failing_prctl_logs_a_warning_and_does_not_throw()
{
var logger = new RecordingLogger();
new HostProcessHardening.Hardener(_ => 22, isLinux: true, Env()).Apply(On, logger);
Assert.True(logger.Has(LogLevel.Warning, "prctl(PR_SET_DUMPABLE, 0) failed"));
Assert.True(logger.Has(LogLevel.Warning, "22"));
}
[Fact]
public void An_unresolvable_libc_logs_a_warning_and_does_not_throw()
{
var logger = new RecordingLogger();
new HostProcessHardening.Hardener(_ => throw new DllNotFoundException("libc"), isLinux: true, Env()).Apply(On, logger);
Assert.True(logger.Has(LogLevel.Warning, "Host process hardening failed"));
Assert.False(logger.Has(LogLevel.Information, "Host process hardened"));
}
[Fact]
public void Non_linux_does_not_call_prctl()
{
var calls = 0;
new HostProcessHardening.Hardener(_ => { calls++; return 0; }, isLinux: false, Env(null)).Apply(On, new RecordingLogger());
Assert.Equal(0, calls);
}
[Fact]
public void Flag_off_does_not_call_prctl_and_logs_a_warning()
{
var calls = 0;
var logger = new RecordingLogger();
new HostProcessHardening.Hardener(_ => { calls++; return 0; }, isLinux: true, Env()).Apply(Off, logger);
Assert.Equal(0, calls);
Assert.True(logger.Has(LogLevel.Warning, "Host process hardening disabled"));
}
[Fact]
public void Missing_DOTNET_EnableDiagnostics_logs_a_warning()
{
var logger = new RecordingLogger();
new HostProcessHardening.Hardener(_ => 0, isLinux: true, Env(diagnostics: null)).Apply(On, logger);
Assert.True(logger.Has(LogLevel.Warning, "DOTNET_EnableDiagnostics=0"));
}
[Theory]
[InlineData("0", null)]
[InlineData(null, "0")]
public void DOTNET_EnableDiagnostics_zero_suppresses_the_warning(string? diagnostics, string? ipc)
{
var logger = new RecordingLogger();
new HostProcessHardening.Hardener(_ => 0, isLinux: true, Env(diagnostics, ipc)).Apply(On, logger);
Assert.False(logger.Has(LogLevel.Warning, "DOTNET_EnableDiagnostics"));
}
[Fact]
public async Task Hardened_process_environ_is_unreadable_to_the_same_user()
{
if (!OperatingSystem.IsLinux()) return;
if (Environment.UserName == "root") return; // root reads every /proc entry; the guarantee is for the bot's own user
var (hardenedExit, _, hardenedErr) = await ReadProbeEnvironAsync(harden: true);
Assert.NotEqual(0, hardenedExit);
Assert.Contains("Permission denied", hardenedErr);
var (plainExit, plainOut, _) = await ReadProbeEnvironAsync(harden: false);
Assert.Equal(0, plainExit);
Assert.Contains("PROBE_MARKER=probe-marker-value", plainOut);
}
private static async Task<(int Exit, string Stdout, string Stderr)> ReadProbeEnvironAsync(bool harden)
{
var dir = AppContext.BaseDirectory;
var dll = Path.Combine(dir, "Volcanic.AgenticCore.HardeningProbe.dll");
var psi = new ProcessStartInfo("dotnet") { RedirectStandardInput = true, RedirectStandardOutput = true, UseShellExecute = false };
if (!File.Exists(Path.Combine(dir, "Volcanic.AgenticCore.HardeningProbe.runtimeconfig.json")))
{
psi.ArgumentList.Add("exec");
psi.ArgumentList.Add("--runtimeconfig");
psi.ArgumentList.Add(Path.Combine(dir, "Volcanic.AgenticCore.Tests.runtimeconfig.json"));
}
psi.ArgumentList.Add(dll);
psi.ArgumentList.Add(harden ? "harden" : "plain");
psi.Environment["PROBE_MARKER"] = "probe-marker-value";
using var probe = Process.Start(psi)!;
try
{
Assert.Equal("ok", await probe.StandardOutput.ReadLineAsync().WaitAsync(TimeSpan.FromSeconds(30)));
var cat = new ProcessStartInfo("cat") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false };
cat.ArgumentList.Add($"/proc/{probe.Id}/environ");
using var reader = Process.Start(cat)!;
var stdoutTask = reader.StandardOutput.ReadToEndAsync();
var stderrTask = reader.StandardError.ReadToEndAsync();
await reader.WaitForExitAsync();
return (reader.ExitCode, (await stdoutTask).Replace('\0', '\n'), await stderrTask);
}
finally
{
probe.StandardInput.Close();
if (!probe.WaitForExit(10_000)) probe.Kill();
}
}
}