Harden installer activation against ACC file locks

This commit is contained in:
2026-08-11 15:37:52 +02:00
parent b32cc61e43
commit 08626197be
15 changed files with 389 additions and 23 deletions
@@ -29,6 +29,9 @@ namespace BizTalkPlatformManagementTool.Tests
Run("PackageManifestRejectsTampering", PackageManifestRejectsTampering);
Run("PackageManifestRejectsUndeclaredAndTraversalFiles", PackageManifestRejectsUndeclaredAndTraversalFiles);
Run("InstallerActivatesValidatedPayload", InstallerActivatesValidatedPayload);
Run("InstallerRetriesTransientActivationMove", InstallerRetriesTransientActivationMove);
Run("InstallerUsesVerifiedCopyFallbackForNewInstall", InstallerUsesVerifiedCopyFallbackForNewInstall);
Run("InstallerStopsAfterBoundedUpdateMoveRetries", InstallerStopsAfterBoundedUpdateMoveRetries);
Run("InstallerDoesNotMutateOnStagingFailure", InstallerDoesNotMutateOnStagingFailure);
Run("InstallerRollsBackFailedActivatedSelfTest", InstallerRollsBackFailedActivatedSelfTest);
Run("InstallerUninstallRemovesProgramDirectory", InstallerUninstallRemovesProgramDirectory);
@@ -147,6 +150,118 @@ namespace BizTalkPlatformManagementTool.Tests
});
}
/// <summary>Prüft die erfolgreiche Aktivierung nach einer kurzzeitigen Rename-Sperre.</summary>
private static void InstallerRetriesTransientActivationMove()
{
InTemp(directory =>
{
var package = CreatePackage(directory, "new");
var install = Path.Combine(directory, "install");
var data = Path.Combine(directory, "data");
var moveCalls = 0;
var delays = new List<int>();
var engine = new InstallerEngine(
package,
install,
data,
false,
path => true,
(source, target) =>
{
moveCalls++;
if (moveCalls == 1) throw new IOException("simulated transient scanner lock");
Directory.Move(source, target);
},
delays.Add);
engine.Install(false, null);
Assert(moveCalls == 2, "transient activation move was not retried exactly once");
Assert(delays.SequenceEqual(new[] { 250 }), "unexpected retry delay for transient activation move");
Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "new", "payload was not activated after retry");
var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single());
Assert(log.Contains("event=directory_move_retry role=activate failed_attempt=1"), "transient move retry was not diagnosed");
Assert(log.Contains("event=directory_move_recovered role=activate attempt=2"), "move recovery was not diagnosed");
});
}
/// <summary>Prüft den verifizierten Kopierfallback einer durchgehend gesperrten Neuinstallation.</summary>
private static void InstallerUsesVerifiedCopyFallbackForNewInstall()
{
InTemp(directory =>
{
var package = CreatePackage(directory, "new");
var install = Path.Combine(directory, "install");
var data = Path.Combine(directory, "data");
var moveCalls = 0;
var delays = new List<int>();
var engine = new InstallerEngine(
package,
install,
data,
false,
path => true,
(source, target) =>
{
moveCalls++;
throw new UnauthorizedAccessException("simulated permanent policy denial");
},
delays.Add);
engine.Install(false, null);
Assert(moveCalls == 8, "permanent move failure did not stop after eight attempts");
Assert(delays.SequenceEqual(new[] { 250, 500, 1000, 2000, 3000, 5000, 8000 }), "bounded retry schedule changed unexpectedly");
Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "new", "verified copy fallback did not activate payload");
Assert(!Directory.GetDirectories(directory, "install.staging.*").Any(), "staging remained after copy fallback");
var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single());
Assert(log.Contains("event=directory_move_retry role=activate failed_attempt=7"), "final scheduled retry was not diagnosed");
Assert(log.Contains("exception_type=System.UnauthorizedAccessException"), "ACL failure type missing from retry diagnostics");
Assert(log.Contains("event=activation_fallback_complete method=verified_copy"), "copy fallback completion was not diagnosed");
Assert(log.Contains("activation_method=verified_copy_fallback"), "copy fallback missing from setup summary");
});
}
/// <summary>Prüft, dass ein Update bei dauerhaft gesperrtem Backup atomar und unverändert abbricht.</summary>
private static void InstallerStopsAfterBoundedUpdateMoveRetries()
{
InTemp(directory =>
{
var package = CreatePackage(directory, "new");
var install = Path.Combine(directory, "install");
Directory.CreateDirectory(install);
File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName), "old");
File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName + ".config"), "old-config");
var data = Path.Combine(directory, "data");
var moveCalls = 0;
var delays = new List<int>();
var engine = new InstallerEngine(
package,
install,
data,
false,
path => true,
(source, target) =>
{
moveCalls++;
throw new UnauthorizedAccessException("simulated permanent update policy denial");
},
delays.Add);
var exception = Capture<InvalidOperationException>(() => engine.Install(false, null));
Assert(moveCalls == 8, "permanent update move failure did not stop after eight attempts");
Assert(delays.SequenceEqual(new[] { 250, 500, 1000, 2000, 3000, 5000, 8000 }), "bounded update retry schedule changed unexpectedly");
Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "old", "failed update modified the active installation");
Assert(!Directory.GetDirectories(directory, "install.staging.*").Any(), "staging remained after permanent update failure");
Assert(exception.Message.Contains("Fehlercode=SETUP-ACTIVATION"), "activation error code missing after update retries");
Assert(exception.Message.Contains("Kein Rollback erforderlich"), "pre-mutation update denial reported a rollback");
var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single());
Assert(log.Contains("event=directory_move_retry role=backup failed_attempt=7"), "final update retry was not diagnosed");
Assert(!log.Contains("event=activation_fallback_started"), "update incorrectly used the new-install copy fallback");
});
}
/// <summary>Prüft die Ablehnung nicht deklarierter Dateien und ausbrechender Manifestpfade.</summary>
private static void PackageManifestRejectsUndeclaredAndTraversalFiles()
{