Cleanup + security narrowing + manual updates (post-review)

Security (narrow the no-admin grants to one account):
- The service's no-admin start/stop and bin-write grants went to Authenticated Users /
  BUILTIN\Users - together that was a one-step local privilege escalation for ANY
  account (overwrite the SYSTEM-run binary, then stop/start it). Now both grants go to
  the INSTALLING user's SID only (the elevated install runs as that interactive user).
  Same effortless workflow for that user; the any-account escalation surface is gone.
  AddUserStartStopAce takes the SID; self-test asserts it's scoped, not AU.

Dead-code removal:
- --probe-apploopback diagnostic verb + ProbeAppLoopback.cs + the ProcessLoopbackCapture
  .Diagnostic hook (all scaffolding for the now-fixed activation bug).
- --update-service verb + ServiceControl.DoUpdate (the "Update service" menu item is
  gone; auto-update via ServiceUpdate.RestartSelf replaced it). Verb gate now lists five.

Manual (readme.html):
- New "Sending specific applications" section (the How-to-send chooser + the two app
  lists) and the Alt+6/8/9 shortcuts - the whole per-app feature was undocumented.
- Documented the two "Clear remembered ... list" buttons on Preferences > General.

Gate: 42/42.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-17 16:37:13 +01:00
co-authored by Claude Opus 4.8
parent 984bcd042e
commit d002130402
7 changed files with 66 additions and 136 deletions
+12 -9
View File
@@ -760,15 +760,18 @@ internal static class SelfTest
Check(createArgs.Contains("\\\"" + ServiceStore.BinExePath + "\\\" " + ServiceControl.RunVerb),
"the create command must register the ProgramData bin exe as the service binary");
// 2. AddUserStartStopAce inserts the AU start/stop ACE into the DACL, ahead of the SACL, and is idempotent.
// 2. AddUserStartStopAce inserts the user's start/stop ACE into the DACL, ahead of the SACL, idempotently.
const string sample = "D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCLCSWLOCRRC;;;IU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)";
var amended = ServiceControl.AddUserStartStopAce(sample);
Check(amended is not null && amended.Contains(ServiceControl.UserStartStopAce), "the AU start/stop ACE must be added");
Check(amended!.IndexOf(ServiceControl.UserStartStopAce, StringComparison.Ordinal) < amended.IndexOf("S:", StringComparison.Ordinal),
const string sid = "S-1-5-21-111-222-333-1001"; // a specific user SID (the installing user, scoped grant)
var ace = ServiceControl.UserStartStopAceFor(sid);
var amended = ServiceControl.AddUserStartStopAce(sample, sid);
Check(amended is not null && amended.Contains(ace), "the user's start/stop ACE must be added");
Check(amended!.IndexOf(ace, StringComparison.Ordinal) < amended.IndexOf("S:", StringComparison.Ordinal),
"the ACE must sit inside the DACL, before the SACL");
Check(amended.StartsWith("D:", StringComparison.Ordinal), "the result must still be a valid DACL-first SDDL");
Check(ServiceControl.AddUserStartStopAce(amended) == amended, "adding the ACE twice must be a no-op (idempotent)");
Check(ServiceControl.AddUserStartStopAce("garbage") is null, "a non-DACL SDDL must be rejected");
Check(!amended.Contains(";;;AU)"), "the grant must be scoped to the specific user SID, not Authenticated Users");
Check(ServiceControl.AddUserStartStopAce(amended, sid) == amended, "adding the ACE twice must be a no-op (idempotent)");
Check(ServiceControl.AddUserStartStopAce("garbage", sid) is null, "a non-DACL SDDL must be rejected");
// 2b. The app-source path (which the SYSTEM service watches for auto-updates) round-trips, and drives
// the update check: unknown/empty source => no update, so the service never acts on uncertainty.
@@ -811,7 +814,7 @@ internal static class SelfTest
Check(File.Exists(Path.Combine(dst, "default sounds", "connect.wav")), "bundled default sounds must be copied");
Check(!Directory.Exists(Path.Combine(dst, "user settings and logs")), "user settings/logs must NOT be copied");
Check(!Directory.Exists(Path.Combine(dst, "logs")), "stray logs folder must NOT be copied");
return "runs from own ProgramData bin; AU start/stop ACE added idempotently; program copy excludes user state";
return "runs from own ProgramData bin; user-scoped start/stop ACE added idempotently; program copy excludes user state";
}
finally { try { Directory.Delete(root, recursive: true); } catch { /* temp */ } }
}
@@ -1417,7 +1420,7 @@ internal static class SelfTest
foreach (var verb in new[]
{
ServiceControl.RunVerb, ServiceControl.InstallVerb, ServiceControl.UninstallVerb,
ServiceControl.UpdateVerb, ServiceControl.StartVerb, ServiceControl.StopVerb,
ServiceControl.StartVerb, ServiceControl.StopVerb,
})
{
Check(Program.IsServiceInvocation(new[] { verb }), $"'{verb}' must be recognised as a service invocation");
@@ -1435,7 +1438,7 @@ internal static class SelfTest
if (!loadedBefore)
Check(!IsAssemblyLoaded(svcAsm), "deciding a normal launch must not load the Windows-service assembly");
return "normal launches stay load-safe; all six service verbs recognised (case-insensitive)";
return "normal launches stay load-safe; all five service verbs recognised (case-insensitive)";
}
/// <summary>The Service menu's "View service log" opens the newest diagnostic log — the log that says