Authorize AppContainer test hosts on the MTP controller pipe - #10491
Open
Amaury Levé (Evangelink) wants to merge 3 commits into
Conversation
The test host controller pipe is created with the equivalent of PipeOptions.CurrentUserOnly: owned by the creating token's owner SID with a DACL that grants only that SID. A Windows AppContainer host (true UWP, or a WinUI host configured for AppContainer) runs with a restricted token, and Windows grants access only when the normal access check and the restricted-SID check both succeed, so such a DACL denies the host even though it belongs to the same signed-in user. Add a Windows-only, least-privilege authorization path: - NamedPipeServerSecurity composes an explicit SDDL descriptor (owner keeps PipeAccessRights.FullControl, each authorized AppContainer package SID gets ReadWrite|Synchronize only, DACL protected) and creates the pipe through CreateNamedPipeW, additionally rejecting remote clients. The package mask excludes FILE_CREATE_PIPE_INSTANCE, DELETE, WRITE_DAC and WRITE_OWNER. - Only specific AppContainer SIDs may be authorized; ALL APPLICATION PACKAGES, ALL RESTRICTED APPLICATION PACKAGES, users, groups and Everyone are rejected and fail the run instead of widening the pipe. - A new experimental ITestHostControllerPipeAuthorizer lets a launcher contribute the package identity before the pipe is created, which is the only point where it can still be composed. - Microsoft.Testing.Extensions.PackagedApp derives its own package SID and requests it only when the manifest says the app runs in an AppContainer, with a TESTINGPLATFORM_PACKAGEDAPP_PIPEAUTHORIZATION escape hatch. Runs that ask for nothing, and every non-Windows run, keep the existing pipe byte-for-byte. Contributes to #10486. End-to-end AppContainer runs additionally need #10485. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add an end-to-end NamedPipeServer request/reply test over the hardened pipe: it is created from a raw handle rather than by the NamedPipeServerStream name constructor, so the whole connect/request/reply/dispose cycle needs its own coverage. Also read the security descriptor back with LABEL_SECURITY_INFORMATION and assert no mandatory integrity label is ever emitted. Lowering the label was investigated and rejected: a lowbox (AppContainer) token's access check is satisfied by the package-SID ACE, so the label would buy nothing while removing Mandatory Integrity Control as a second gate behind the DACL. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Rebase onto the activation-argument branch and consume its finalized manifest contract instead of duplicating it: the launcher now asks AppxApplicationInfo.IsAppContainer whether the package declares an AppContainer application, which is the same classification that decides whether the host is activated with an activation payload or with plain argv. The AppxManifestInfo/AppxApplicationInfo additions this branch previously carried are dropped. Add the acceptance-level coverage this issue asks for: a live AppContainer process's token is duplicated and impersonated, and used to open three pipes — one authorizing its package, one authorizing a different package, and one authorizing none. Only the first succeeds, which exercises the real restricted-token access check rather than a synthetic stand-in. Candidates are filtered to the shape a real test host has. Measured on Windows: a low-integrity AppContainer (S-1-16-4096, what an ordinary UWP/WinUI app runs at) connects through the package ACE with no mandatory label on the pipe, while an untrusted-integrity one (S-1-16-0, Chromium's hardened renderers) is denied by Mandatory Integrity Control before the DACL is consulted. Selecting an arbitrary AppContainer therefore made the assertion depend on which processes happened to be running. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink)
force-pushed
the
dev/amauryleve/authorize-appcontainer-on-mtp-controller
branch
from
August 6, 2026 17:49
aead3b9 to
c797a7f
Compare
Amaury Levé (Evangelink)
changed the base branch from
main
to
dev/amauryleve/deliver-appcontainer-activation-args
August 6, 2026 17:49
Contributor
There was a problem hiding this comment.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Note
This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #10486.
Related: #10485 (companion issue: delivering MTP activation arguments to AppContainer test hosts), implemented by #10489.
Problem
MTP creates the test-host-controller named pipe with
PipeOptions.CurrentUserOnly. On Windows that means a security descriptor whose owner is the creating token's owner SID and whose DACL contains a single ACE granting that same SID full control — which is what gives the pipe its current-user and elevation protection.A Windows AppContainer host runs with a restricted token. Windows grants access only when the normal access check and the restricted-SID check both succeed, and an AppContainer's restricting SIDs contain the package SID. A DACL that names only the user therefore denies the host even though it belongs to the same signed-in user — knowing the pipe name, or restoring the activation arguments from #10489, changes nothing.
Ordering problem
The pipe has to be listening before the host is launched, so it is created in
TestHostControllersTestHost.InternalRunAsyncwell beforeITestHostLauncher.LaunchTestHostAsyncruns. A launcher cannot contribute the package identity from the launch call.The platform therefore queries a new opt-in interface immediately before creating the pipe:
Alignment with #10489
The launcher decides whether to ask for the grant by reading
AppxApplicationInfo.IsAppContainer— the same manifest classification #10489 uses to decide whether the host is activated with an activation payload or with plainargv. There is exactly one AppContainer classification in the extension, and this PR adds none of its own.Because the pipe is created before the platform names the executable it wants, the specific application cannot be resolved yet. That does not weaken anything: the SID is the package's, shared by every application it declares, so "this package declares an AppContainer application" is the right predicate for authorizing it.
Security model
NamedPipeServerSecuritycomposes an explicit SDDL descriptor and creates the pipe throughCreateNamedPipeW:PipeAccessRights.FullControl(0x1f019f) — byte-for-byte whatPipeOptions.CurrentUserOnlygrantsReadWrite | Synchronize(0x12019b) onlyD:P); the pipe also setsPIPE_REJECT_REMOTE_CLIENTS.FILE_CREATE_PIPE_INSTANCE,DELETE,WRITE_DACandWRITE_OWNER, so an authorized package can talk to the controller but can never create a second instance of the pipe and impersonate it.READ_CONTROLis granted, because the client-sidePipeOptions.CurrentUserOnlyvalidation reads the owner.Everyone, and in particularALL APPLICATION PACKAGES(S-1-15-2-1) andALL RESTRICTED APPLICATION PACKAGES(S-1-15-2-2), failing the run with a localized, actionable error rather than silently widening the pipe.CheckNetIsolation LoopbackExemptapplies to network sockets, not named-pipe DACLs.TESTINGPLATFORM_PACKAGEDAPP_PIPEAUTHORIZATION(auto/always/never) is the escape hatch.Three findings worth reviewing
1. Not
DeriveAppContainerSidFromAppContainerName. That Win32 API looks like the obvious choice, but its result depends on the calling process: from a process that itself has package identity it returns a child AppContainer SID (the caller's package SID plus four extra sub-authorities) instead of the package SID of the requested name. That is precisely our situation — the controller is the packaged test app. Verified: derivation formicrosoft.windowsnotepad_8wekyb3d8bbwefrom packaged PowerShell returnedS-1-15-2-<pwsh package SID>-…, not the SID Windows registered. The package SID is instead computed with the stable derivation Windows uses for its ownAppContainer\Mappingstable (S-1-15-2-+ the first seven little-endianuint32of SHA-256 over the lower-cased package family name in UTF-16LE), and a test cross-checks it against every package mapping registered on the machine.2. Integrity levels. Measured against live AppContainer tokens: a low-integrity AppContainer (
S-1-16-4096— what an ordinary UWP/WinUI app runs at) connects through the package ACE with no mandatory label on the pipe, while an untrusted-integrity one (S-1-16-0— Chromium's hardened renderers) is denied by Mandatory Integrity Control before the DACL is consulted, and no DACL change can admit it. So lowering the pipe's label would buy nothing for a real test host while removing a second gate; the pipe keeps the controller's own label.3. Not
PipeSecurity/NamedPipeServerStreamAcl. The managed ACL types are not in thenetstandard2.0surface this assembly also builds for, andPipeOptions.CurrentUserOnlycannot be combined with an explicit descriptor.CreateNamedPipeW+ SDDL keeps one code path across all TFMs with no new package references.Allowed/denied connection coverage
NamedPipeServerSecurityTests(41 tests, green onnet9.0andnet462):ALL APPLICATION PACKAGES,Everyone,Authenticated Users, another account and another user are all denied; the creating identity is granted full control.GetSecurityInfo(includingLABEL_SECURITY_INFORMATION) and asserted to contain exactly the owner ACE and the one package ACE, a protected DACL, and no mandatory label.UnauthorizedAccessException.NamedPipeServerrequest/reply round-trip completes over the hardened (handle-created) pipe — it is created from a raw handle rather than by theNamedPipeServerStreamname constructor, so the whole cycle needs its own coverage.null/empty/malformed, both catch-all package SIDs,Everyone,Authenticated Users,LocalSystem,BUILTIN\Users, a user SID, a capability SID, a too-short SID, a non-numeric sub-authority and theACalias.NamedPipeServerthrows when handed a non-AppContainer SID (defense in depth behind the caller-side validation).AppContainerSecurityIdentifierTestsandPackagedAppPipeAuthorizationTestscover the SID derivation (including the cross-check against Windows' ownAppContainer\Mappings) and every authorization decision: an AppContainer manifest yields the package SID;runFullTrust, a full-trust entry point, a loose layout, a malformed manifest,never, an unrecognized mode and non-Windows all yield nothing.Full suites pass:
Microsoft.Testing.Platform.UnitTests2163/2163 (net9.0) and 2115/2115 (net462),Microsoft.Testing.Extensions.UnitTests992 passed / 7 skipped.Microsoft.Testing.Platform.slnfbuilds with 0 warnings.Remaining work after this PR
Nothing in either issue is left open in code. The one deliverable deliberately deferred is the combined end-to-end acceptance asset, which cannot exist until both PRs land because it needs argument delivery (#10485/#10489) and pipe authorization (this PR) in the same build:
PackagedAppExtensions.GetTestApplicationArguments, connect back on the controller pipe, and complete a run;Until then the boundary this PR owns is covered by real OS access checks rather than a live end-to-end run — including a duplicated, impersonated AppContainer token that connects only to the pipe authorizing its own package (see above).
Docs
docs/RFCs/017-TestHost-Launcher.mdgains an "Authorizing an AppContainer on the controller pipe" section,docs/winui-testing.mdgains a user-facing "Controller pipe access for AppContainer hosts" section, and the PackagedAppPACKAGE.mddocuments the grant and the escape hatch. The stale "#10486 is the remaining dependency" notes left by #10489 are updated in the same places.