From 45343a42609cc16b319493134bf1d27b122ab18d Mon Sep 17 00:00:00 2001 From: sammiller Date: Thu, 8 Oct 2026 21:08:54 +0100 Subject: [PATCH] [GOV.32] Remove the ContentSandbox macOS launch-profile code Implements P2-023 (macOS outside the delivery scope) for the ContentSandbox helper: the macOS App-Sandbox and XPC launch-profile deliverable is removed and the typed fail-closed macOS refusal stays. - Delete src/DesktopHelpers/ArcForges.ContentSandbox/macos/ArcForges.ContentSandbox.entitlements and its row in eng/provenance/files.json (the only provenance change). - ProfileEnforcement.ThisPlatform becomes ContentSandboxProfileKind? and is null off Windows and Linux. HelperEntry compares the frame profile with it unchanged, so every launch frame there, including one carrying MacOsAppSandboxXpc, exits with the isolation-unavailable code (70) before any resource or parser. The Windows and Linux branches are unchanged. - Tests/MacProfileTests.cs is refusal-only: the broker refuses a macOS launch, the reserved value stays wire-stable (3) and is never this platform's profile, the Windows (1) and Linux (2) kinds are unchanged, a helper process given a MacOsAppSandboxXpc frame exits 70, and a helper given its own platform kind passes the profile gate (exit 64 at the bootstrap descriptor check). - Tests/ContractFacadeTests.cs: the non-Windows, non-Linux refusal check applies to every such operating system, not only macOS. No other test changed. - README macOS lines (ArcForges.ContentSandbox and ArcForges.ContentSandbox.Broker) and the doc comments of the reserved MacOsAppSandboxXpc value in ContentSandboxTypes.cs and ContentSandboxLaunchFrame.cs state that macOS is not supported. The value is unchanged. Write scope: the GOV.32 record files only, with no ADP-07 supporting files. Windows and Linux containment is unchanged. ContentSandboxLauncher.cs (the macOS refusal) and HelperEntry.cs are not edited. Validation (local, not CI): - Windows, SDK 10.0.400 from C:/Users/J7Rdm/.dotnet: locked restore, build with 0 warnings and 0 errors, ContentSandbox tests 110 total, 101 passed, 9 opt-in OS checks skipped, 0 failed. The five MacProfileTests pass. - WSL2 Debian 13 on kernel 6.18.40.1-microsoft-standard-WSL2, SDK 10.0.400, Linux-native copy at ~/gov32: the same counts; the five MacProfileTests pass. - Policy gates (python -I, with the repo eng directory added to sys.path): design_policy, dependency_policy, licence_boundary --evaluate-managed (61 projects), runtime_ownership --evaluate-managed (7 repositories), reconciliation, native_provenance, check_provenance --owner DesktopPlatform (973 files), reference_baselines, stage_integration verify (0 findings). Unit suites for these gates and tests/tooling all pass. - Not run locally: architecture_evidence.py secret (RP-09 needs the hosted secret-scan job) and the nine Windows OS-isolation checks, which are opt-in and need a published hostile fixture. - Static scan: App-Sandbox, XPC and entitlements references remain only in the reserved enum values, the broker's macOS refusal (ContentSandboxLauncher.cs), the null ThisPlatform, and the tests. Co-Authored-By: Claude Haiku 5.5 --- eng/provenance/files.json | 1 - .../ContentSandboxTypes.cs | 5 +- .../ArcForges.ContentSandbox.Broker/README.md | 2 +- .../ContentSandboxLaunchFrame.cs | 5 +- .../Host/ProfileEnforcement.cs | 9 +- .../ArcForges.ContentSandbox/README.md | 6 +- .../Tests/ContractFacadeTests.cs | 3 +- .../Tests/MacProfileTests.cs | 133 +++++++++++++++--- .../ArcForges.ContentSandbox.entitlements | 15 -- 9 files changed, 132 insertions(+), 47 deletions(-) delete mode 100644 src/DesktopHelpers/ArcForges.ContentSandbox/macos/ArcForges.ContentSandbox.entitlements diff --git a/eng/provenance/files.json b/eng/provenance/files.json index 4f35881a..735c061c 100644 --- a/eng/provenance/files.json +++ b/eng/provenance/files.json @@ -952,7 +952,6 @@ "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/MacProfileTests.cs", "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/OsIsolationTests.cs", "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/packages.lock.json", - "src/DesktopHelpers/ArcForges.ContentSandbox/macos/ArcForges.ContentSandbox.entitlements", "src/BuildingBlocks/ArcForges.Security/CapabilityEnforcement/ArcForges.Security.CapabilityEnforcement.csproj", "src/BuildingBlocks/ArcForges.Security/CapabilityEnforcement/CapabilityEvidence.cs", "src/BuildingBlocks/ArcForges.Security/CapabilityEnforcement/CapabilityGate.cs", diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox.Broker/ContentSandboxTypes.cs b/src/DesktopHelpers/ArcForges.ContentSandbox.Broker/ContentSandboxTypes.cs index 7aa9415c..01b4a87f 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox.Broker/ContentSandboxTypes.cs +++ b/src/DesktopHelpers/ArcForges.ContentSandbox.Broker/ContentSandboxTypes.cs @@ -17,7 +17,10 @@ public enum ContentSandboxProfile /// Linux Landlock, seccomp and no_new_privs. LinuxLandlockSeccomp = 2, - /// macOS App Sandbox with an XPC descriptor handoff. It is not implemented: a launch for it is refused. + /// + /// Reserved and wire-stable; macOS is not a supported platform. No profile of this family exists, it is never selected, and a launch or a + /// launch frame carrying it is refused fail-closed (security.isolation_unavailable). + /// MacOsAppSandboxXpc = 3, } diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox.Broker/README.md b/src/DesktopHelpers/ArcForges.ContentSandbox.Broker/README.md index d78c2fb3..846fdabc 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox.Broker/README.md +++ b/src/DesktopHelpers/ArcForges.ContentSandbox.Broker/README.md @@ -17,7 +17,7 @@ var image = await invocation.OpenImageAsync(0, 0, 1); using var tile = (await invocation.ReadImageTileAsync(image.Value, 0, 0, 256, 256)).Value!; // bytes verified on a private copy ``` -- **No fallback.** A platform without a verified profile (macOS here, an unknown OS, a failed AppContainer or self-check) fails with +- **No fallback.** A platform without a verified profile (macOS, which is not a supported platform, an unknown OS, a failed AppContainer or self-check) fails with `security.isolation_unavailable`, starts nothing, and never parses in process. - **Windows (`Windows/`).** The helper is opened with writers refused and its SHA-256 compared with the pin; an AppContainer identity is leased from a pool of eight (a lock file per identity, released by the OS if the owner ends); the process is created suspended with a handle list naming diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox.Contracts/ContentSandboxLaunchFrame.cs b/src/DesktopHelpers/ArcForges.ContentSandbox.Contracts/ContentSandboxLaunchFrame.cs index e3ee6e4b..1890281d 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox.Contracts/ContentSandboxLaunchFrame.cs +++ b/src/DesktopHelpers/ArcForges.ContentSandbox.Contracts/ContentSandboxLaunchFrame.cs @@ -14,7 +14,10 @@ internal enum ContentSandboxProfileKind : byte /// Linux Landlock, seccomp and no_new_privs. LinuxLandlockSeccomp = 2, - /// macOS App Sandbox with an XPC descriptor handoff (not implemented here; a launch for it is refused). + /// + /// Reserved and wire-stable; macOS is not a supported platform. No profile of this family exists, it is never selected, and a frame carrying + /// it is refused by the helper with the isolation-unavailable exit on every operating system. + /// MacOsAppSandboxXpc = 3, } diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Host/ProfileEnforcement.cs b/src/DesktopHelpers/ArcForges.ContentSandbox/Host/ProfileEnforcement.cs index 069e305a..3e7ba165 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Host/ProfileEnforcement.cs +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/Host/ProfileEnforcement.cs @@ -16,11 +16,14 @@ namespace ArcForges.ContentSandbox.Host; /// internal static class ProfileEnforcement { - /// The profile family of the operating system this process runs on, or macOS/unsupported values the launch frame never matches. - internal static ContentSandboxProfileKind ThisPlatform => + /// + /// The profile family of the operating system this process runs on. It is null where no restricted profile exists (macOS and every other + /// operating system), so no launch frame matches it and HelperEntry refuses the frame with the isolation-unavailable exit. + /// + internal static ContentSandboxProfileKind? ThisPlatform => OperatingSystem.IsWindows() ? ContentSandboxProfileKind.WindowsAppContainerJob : OperatingSystem.IsLinux() ? ContentSandboxProfileKind.LinuxLandlockSeccomp - : ContentSandboxProfileKind.MacOsAppSandboxXpc; + : null; /// Applies and verifies the profile; false means the helper must not run a parser. [SuppressMessage("Design", "CA1031", Justification = "Any failure of enforcement or verification is a refusal to run.")] diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/README.md b/src/DesktopHelpers/ArcForges.ContentSandbox/README.md index ea19c44f..d9f4da7a 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/README.md +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/README.md @@ -22,8 +22,8 @@ generated `ContentSandboxService` contract. This project is non-packable; packag filter to itself and checks them. Seccomp is synchronised to every thread (TSYNC). Landlock restricts only the calling thread unless its own TSYNC flag is given, so the helper passes that flag, refuses to run if the kernel rejects it (a kernel without multithread Landlock, which is recent), and its self-check observes the denial from the applying thread, from several thread-pool threads and from a new thread. Parent loss is detected by the helper polling its parent id (no parent-death signal: the kernel ties it to the spawning thread, which may retire while the parent lives) and the supervising parent kills the helper on every path. A wedged helper whose parent was killed is not ended by the kernel. No offline test observes the Landlock TSYNC flag or the shrink seal (a mutant that drops either survives offline); both need a Linux kernel. Not covered on Linux: tgkill to another same-user process (the runtime needs it), no PID/user/mount namespaces, and - the profile has never run on a Linux kernel here. macOS: no launcher exists; `macos/ArcForges.ContentSandbox.entitlements` is the declarative input of the - profile that a signed sandboxed bundle and an XPC descriptor handoff would need, and a launch there is refused. + the profile has never run on a Linux kernel here. macOS is not supported: there is no launcher, no profile and no entitlements file; the + reserved `MacOsAppSandboxXpc` value is never selected, and a launch or a frame carrying it is refused (security.isolation_unavailable). - **Fixture (`Fixture/`).** TEST ONLY: the same host with one deliberately hostile first-party test composition (the registered `SUB-hostile-test-parser`). It parses no real format. Its script attacks the boundary (reads a product file, connects to a real listener, sends datagrams, opens other processes, spawns, maps the read-only input writable) or crashes, hangs, exhausts memory or overruns its @@ -51,7 +51,7 @@ Publishing needs the Visual Studio C++ tools on the path of the shell; on a mach ## What is not proven See the Plan ledger record of PLT.45. In short: the Linux profile and launcher are compiled in hosted CI and unit-tested only; they have not been run -on Linux. macOS has no launcher. Windows signature (Authenticode) verification of the helper is not implemented: the launcher pins the +on Linux. macOS is not supported and has no launcher. Windows signature (Authenticode) verification of the helper is not implemented: the launcher pins the SHA-256 of the installed helper from the signed inventory. ## Production composition diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ContractFacadeTests.cs b/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ContractFacadeTests.cs index de0ef688..87d12ee4 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ContractFacadeTests.cs +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ContractFacadeTests.cs @@ -307,8 +307,9 @@ public void TheProfileOfThisPlatformIsReportedWithoutLaunchingAnything() var status = ContentSandboxLauncher.ProbeProfile(); Assert.Equal(OperatingSystem.IsWindows(), status.Profile == ContentSandboxProfile.WindowsAppContainerJob); Assert.Equal(OperatingSystem.IsLinux(), status.Profile == ContentSandboxProfile.LinuxLandlockSeccomp); - if (OperatingSystem.IsMacOS()) + if (!OperatingSystem.IsWindows() && !OperatingSystem.IsLinux()) { + // macOS, and any other operating system: a typed refusal, never an available profile. Assert.False(status.IsAvailable); Assert.NotEmpty(status.Reason); } diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/MacProfileTests.cs b/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/MacProfileTests.cs index 416a1305..c9a172d2 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/MacProfileTests.cs +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/MacProfileTests.cs @@ -1,42 +1,133 @@ // SPDX-License-Identifier: AGPL-3.0-only -using System.Xml.Linq; +using System.Diagnostics; +using System.Security.Cryptography; using ArcForges.ContentSandbox.Broker; +using ArcForges.ContentSandbox.Contracts; +using ArcForges.ContentSandbox.Host; using Xunit; namespace ArcForges.ContentSandbox.Tests; -/// The macOS profile is declarative data and a refusal: no launcher exists, so nothing is ever started on that platform. +/// +/// macOS is not a supported platform, so its restricted profile is a typed refusal and nothing else. The broker launches nothing there, and a +/// helper that receives a frame carrying the reserved macOS value exits with the isolation-unavailable code before any resource or parser exists. +/// public sealed class MacProfileTests { - private static string EntitlementsPath() + private static CancellationToken Ct => TestContext.Current.CancellationToken; + + [Fact] + public async Task ALaunchOnMacOsIsRefusedWithoutStartingAnything() + { + var options = Fixtures.Options(); + await using var launcher = new ContentSandboxLauncher(options, ContentSandboxProfile.MacOsAppSandboxXpc, launcherOverride: null); + var result = await launcher.LaunchAsync("data"u8.ToArray(), Ct); + Assert.False(result.IsSuccess); + Assert.Equal("security.isolation_unavailable", result.Failure!.Code); + Assert.False(Directory.Exists(options.RuntimeRoot)); + } + + [Fact] + public void TheReservedMacOsValueIsWireStableAndIsNeverThisPlatformsProfile() { - var directory = new DirectoryInfo(AppContext.BaseDirectory); - while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "DesktopPlatform.slnx"))) + Assert.Equal((byte)3, (byte)ContentSandboxProfileKind.MacOsAppSandboxXpc); + Assert.NotEqual((ContentSandboxProfileKind?)ContentSandboxProfileKind.MacOsAppSandboxXpc, ProfileEnforcement.ThisPlatform); + } + + [Fact] + public void TheWindowsAndLinuxKindsAreUnchangedAndAreTheOnlyProfilesOfThisPlatform() + { + Assert.Equal((byte)1, (byte)ContentSandboxProfileKind.WindowsAppContainerJob); + Assert.Equal((byte)2, (byte)ContentSandboxProfileKind.LinuxLandlockSeccomp); + if (OperatingSystem.IsWindows()) { - directory = directory.Parent; + Assert.Equal(ContentSandboxProfileKind.WindowsAppContainerJob, ProfileEnforcement.ThisPlatform); + } + else if (OperatingSystem.IsLinux()) + { + Assert.Equal(ContentSandboxProfileKind.LinuxLandlockSeccomp, ProfileEnforcement.ThisPlatform); + } + else + { + Assert.Null(ProfileEnforcement.ThisPlatform); } - - return Path.Combine(directory?.FullName ?? throw new InvalidOperationException("The repository root was not found."), - "src", "DesktopHelpers", "ArcForges.ContentSandbox", "macos", "ArcForges.ContentSandbox.entitlements"); } [Fact] - public void TheEntitlementsEnableTheAppSandboxAndNothingElse() + public async Task AHelperRefusesAFrameCarryingTheMacOsValueWithTheIsolationUnavailableExit() { - var document = XDocument.Load(EntitlementsPath()); - var keys = document.Descendants("key").Select(key => key.Value).ToArray(); - Assert.Equal(["com.apple.security.app-sandbox"], keys); - Assert.Equal("true", document.Descendants("dict").Single().Elements().ElementAt(1).Name.LocalName); - Assert.DoesNotContain(keys, key => key.Contains("network", StringComparison.Ordinal) - || key.Contains("files", StringComparison.Ordinal) || key.Contains("inherit", StringComparison.Ordinal) - || key.Contains("application-groups", StringComparison.Ordinal)); + var exit = await RunHelperAsync(ContentSandboxProfileKind.MacOsAppSandboxXpc, Ct); + Assert.Equal(ContentSandboxContract.ExitIsolationUnavailable, exit); } [Fact] - public async Task ALaunchOnMacOsIsRefusedWithoutStartingAnything() + public async Task AHelperKeepsItsOwnPlatformKindPastTheProfileGate() { - await using var launcher = new ContentSandboxLauncher(Fixtures.Options(), ContentSandboxProfile.MacOsAppSandboxXpc, launcherOverride: null); - var result = await launcher.LaunchAsync("data"u8.ToArray(), TestContext.Current.CancellationToken); - Assert.Equal("security.isolation_unavailable", result.Failure!.Code); + Assert.SkipUnless(OperatingSystem.IsWindows() || OperatingSystem.IsLinux(), "This platform has no restricted profile kind."); + var own = OperatingSystem.IsWindows() ? ContentSandboxProfileKind.WindowsAppContainerJob : ContentSandboxProfileKind.LinuxLandlockSeccomp; + + // The bootstrap resource below is too short to be a launch descriptor, so the helper stops at that check, after the profile gate. + var exit = await RunHelperAsync(own, Ct); + Assert.Equal(ContentSandboxContract.ExitLaunchFrameInvalid, exit); + } + + private static string HelperPath() => + Path.Combine(AppContext.BaseDirectory, "ArcForges.ContentSandbox" + (OperatingSystem.IsWindows() ? ".exe" : string.Empty)); + + private static ContentSandboxLaunchFrame Frame(ContentSandboxProfileKind kind) => + new( + kind, + Guid.NewGuid(), + Guid.NewGuid(), + 1, + Guid.NewGuid(), + 100, + SHA256.HashData("input"u8), + [1024], + new ContentSandboxLimits(), + [ + new(ContentSandboxHandleRole.Control, 10), + new(ContentSandboxHandleRole.Service, 11), + new(ContentSandboxHandleRole.Input, 12), + new(ContentSandboxHandleRole.Slot0, 13), + ], + "hostile-test-parser", + [1, 2, 3, 4]); + + /// Starts the built helper, writes one launch frame of the given kind to its standard input and returns its exit code. + private static async Task RunHelperAsync(ContentSandboxProfileKind kind, CancellationToken cancellationToken) + { + var helper = HelperPath(); + Assert.True(File.Exists(helper), "The helper host was not built next to the tests: " + helper); + using var frame = Frame(kind); + var encoded = frame.Encode(); + var startInfo = new ProcessStartInfo(helper) + { + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("The helper host did not start."); + var output = process.StandardOutput.ReadToEndAsync(cancellationToken); + var diagnostics = process.StandardError.ReadToEndAsync(cancellationToken); + await process.StandardInput.BaseStream.WriteAsync(encoded, cancellationToken); + await process.StandardInput.BaseStream.FlushAsync(cancellationToken); + process.StandardInput.Close(); + using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + deadline.CancelAfter(TimeSpan.FromSeconds(60)); + try + { + await process.WaitForExitAsync(deadline.Token); + } + catch (OperationCanceledException) + { + process.Kill(entireProcessTree: true); + throw; + } + + _ = await output; + _ = await diagnostics; + return process.ExitCode; } } diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/macos/ArcForges.ContentSandbox.entitlements b/src/DesktopHelpers/ArcForges.ContentSandbox/macos/ArcForges.ContentSandbox.entitlements deleted file mode 100644 index dacb2408..00000000 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/macos/ArcForges.ContentSandbox.entitlements +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - com.apple.security.app-sandbox - - -