1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
using System.Runtime.CompilerServices;
using System.Security.AccessControl;
using System.Diagnostics;
using Agent.Windows.Network;
using Agent.Windows.Recovery;
using Agent.Windows.Security;
namespace Agent.Windows.Tests;
internal static class CrashScenarioBootstrap
{
[ModuleInitializer]
internal static void RunIfRequested()
{
string? scenario = Environment.GetEnvironmentVariable("AGENT_MCP_CRASH_SCENARIO");
if (string.IsNullOrEmpty(scenario)) return;
string root = Environment.GetEnvironmentVariable("AGENT_MCP_CRASH_ROOT")
?? throw new InvalidOperationException("Crash root is required.");
Guid taskId = Guid.Parse(Environment.GetEnvironmentVariable("AGENT_MCP_CRASH_TASK")!);
try
{
TaskResourceJournal journal = TaskResourceJournal.Create(Path.Combine(root, "journals"), taskId);
switch (scenario)
{
case "acl":
var acl = new TaskAclManager(AppContainerProfile.EnsureFixed().Sid, new MemorySecurityAuditSink(), journal);
_ = acl.Grant(taskId, Path.Combine(root, "resource"), FileCapability.Read | FileCapability.Write);
break;
case "acl-inheritance":
var inheritedAcl = new TaskAclManager(
new System.Security.Principal.SecurityIdentifier("S-1-15-2-330000001-330000002-330000003-330000004-330000005-330000006-330000007"),
new MemorySecurityAuditSink(), journal);
_ = inheritedAcl.Grant(taskId, Path.Combine(root, "resource"), FileCapability.Read | FileCapability.Write);
break;
case "acl-before":
byte[] descriptor = FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(Path.Combine(root, "resource")), AccessControlSections.Access)
.GetSecurityDescriptorBinaryForm();
_ = journal.Prepare("acl", Path.Combine(root, "resource"),
new Dictionary<string, string> { ["security_descriptor"] = Convert.ToBase64String(descriptor) });
break;
case "file-before":
string temporary = Path.Combine(root, "resource.txt.agent-mcp-prepared.tmp");
string backup = Path.Combine(root, "backups", taskId.ToString("N"), "prepared.backup");
Directory.CreateDirectory(Path.GetDirectoryName(backup)!);
File.WriteAllText(temporary, "temporary");
File.WriteAllText(backup, "before-crash");
_ = journal.Prepare("file.replace", Path.Combine(root, "resource.txt"), new Dictionary<string, string>
{
["existed"] = bool.TrueString, ["original_sha256"] = string.Empty, ["original_base64"] = string.Empty,
["backup_path"] = backup, ["temporary_path"] = temporary
});
break;
case "file-replace":
_ = new TransactionalFileEditor(journal, Path.Combine(root, "backups"), 1)
.Replace(Path.Combine(root, "resource.txt"), "after-crash");
break;
case "process-after":
string powershell = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "WindowsPowerShell", "v1.0", "powershell.exe");
string auditPath = Path.Combine(root, "crash-audit.jsonl");
var processAudit = new JsonLinesSecurityAuditSink(auditPath);
var launcher = new RestrictedProcessLauncher(
new ExecutablePolicy([new ExecutableApproval(powershell, ExecutablePolicy.ComputeSha256(powershell))], true), processAudit);
string escapedAudit = auditPath.Replace("'", "''", StringComparison.Ordinal);
string crashError = Path.Combine(root, "crash-error.txt").Replace("'", "''", StringComparison.Ordinal);
string watchdogScript = $"$d=[DateTime]::UtcNow.AddSeconds(10); while([DateTime]::UtcNow -lt $d){{ if((Test-Path -LiteralPath '{escapedAudit}') -and (Select-String -LiteralPath '{escapedAudit}' -SimpleMatch '\"Event\":\"process.start\"' -Quiet)){{ Stop-Process -Id {Environment.ProcessId} -Force; exit 0 }}; Start-Sleep -Milliseconds 25 }}; Set-Content -LiteralPath '{crashError}' -Value 'Sandbox process did not start before crash'; Stop-Process -Id {Environment.ProcessId} -Force";
_ = Process.Start(new ProcessStartInfo
{
FileName = powershell, UseShellExecute = false, CreateNoWindow = true,
ArgumentList = { "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", watchdogScript }
});
_ = launcher.Run(taskId, powershell,
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "Start-Sleep -Seconds 30"],
Environment.SystemDirectory, TimeSpan.FromMinutes(1));
break;
case "cleanup-mid":
var cleanupAcl = new TaskAclManager(AppContainerProfile.EnsureFixed().Sid, new MemorySecurityAuditSink(), journal);
_ = cleanupAcl.Grant(taskId, Path.Combine(root, "resource"), FileCapability.Read | FileCapability.Write);
FileEditTransaction edit = new TransactionalFileEditor(journal, Path.Combine(root, "backups"), 1)
.Replace(Path.Combine(root, "resource.txt"), "during-cleanup");
edit.Rollback();
break;
case "wfp":
_ = TaskNetworkPolicy.Apply(journal, AppContainerProfile.EnsureFixed().Sid, [], new MemorySecurityAuditSink());
break;
default:
throw new InvalidOperationException($"Unknown crash scenario '{scenario}'.");
}
}
catch (Exception ex)
{
File.WriteAllText(Path.Combine(root, "crash-error.txt"), ex.ToString());
}
Environment.FailFast($"Intentional security recovery crash point: {scenario}");
}
}
|