Launching Win32 Processes in AppContainer: Capability SIDs and Access Boundaries
Launch Win32 work inside AppContainer by separating profile SIDs, explicit capabilities, ACL checks, process creation, and cleanup.
AppContainer is a Windows access-token and resource-isolation mechanism for processes. A controller can create or reuse an AppContainer profile, build a SECURITY_CAPABILITIES descriptor with its AppContainer SID and selected capability SIDs, and pass that descriptor through an extended startup attribute list so Windows creates the child under that identity. This is useful for isolating parsers, previewers, and other workers that handle data outside their trust boundary.
It is not a virtual machine, a complete replacement for a broker, or a guarantee that every resource is inaccessible. The effective access decision still depends on the user’s token, the AppContainer identity, capability grants, object DACLs, mandatory integrity labels, and the APIs exposed to the process. Keep those layers separate when designing or reviewing the boundary.
Keep the identities distinct
An AppContainer token has a package SID that identifies the container and may have capability SIDs that describe specific authority. Those are distinct from the user’s ordinary user and group SIDs. For a protected object, the granted access is constrained by both the user/group access and the AppContainer access; broad rights from one side do not cancel a narrower grant on the other. AppContainers run at low integrity. Microsoft’s AppContainer overview says a Medium-IL-or-lower resource can be accessed according to the DACL grants to both principals, but Microsoft’s separate Mandatory Integrity Control (MIC) guidance says that a lower-integrity principal cannot write to a Medium-IL object under the default NO_WRITE_UP policy, even if its DACL permits the write; unlabeled objects are treated as Medium. The two pages do not fully agree on the Low-to-Medium write case. Do not treat a DACL grant by itself as proof that a Low-IL write will succeed. For a Low-IL AppContainer, treat the DACL grant as necessary, then inspect the target’s mandatory label and policy; under default NO_WRITE_UP, a Medium-IL or unlabeled object remains non-writable. Test the exact resource and supported Windows versions instead of inferring effective access from one SID or one document sentence.
Capabilities are not URL allowlists or resource paths. They are security principals used in Windows access checks and, for some system resources, are part of the platform’s capability policy. Microsoft’s capability table gives internetClient public-Internet access but no local-network access; that built-in scope still is not a per-destination URL allowlist or a substitute for application-level destination policy and input validation. Similarly, an AppContainer package SID does not automatically authorize arbitrary access to a file chosen by the application.
Regular AppContainers and Less Privileged AppContainers (LPAC) have different defaults. LPAC is more restrictive and requires explicit access for resources that a regular AppContainer can reach by default. Do not add LPAC-only attributes to an ordinary AppContainer sample or describe the two as interchangeable configurations.
Create a stable per-user profile
For a desktop process launched directly by a controller, choose a stable, application-specific moniker. CreateAppContainerProfile creates a per-user, per-app profile and returns its package SID. The name is constrained to 64 characters and the documented character set; including both application and publisher identity helps avoid accidental collisions. The API creates the profile for the current user. A service creating one on behalf of another user must impersonate that user before calling it; do not assume the service account’s profile applies.
The profile API can return HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS). That is a normal reuse case, not permission to delete and recreate the profile. Resolve the existing package SID with DeriveAppContainerSidFromAppContainerName and keep the SID’s documented FreeSid ownership. The error only establishes that the per-user app data store already exists; it does not prove that your current installation or policy created the profile with the settings you expect. Treat reuse as an explicit configuration decision, and do not silently fall back to a different name or a non-container process if setup fails. The example below deliberately requests no named capabilities for a minimal worker. Profile creation and the child’s SECURITY_CAPABILITIES attribute are distinct configuration inputs: for a worker that needs capabilities, review the requested profile configuration, explicitly construct the child attribute’s list, and verify the resulting token rather than inferring the child’s effective set from a pre-existing profile.
The code blocks in this article are focused fragments rather than a complete, compilable launcher. They omit helper definitions for derived capability SIDs, application-specific command-line construction, and some surrounding process and handle-management code. Integrate them into a tested program that checks every return value and keeps all input buffers alive for their documented lifetimes.
If this setup runs inside a service that impersonates a client, remember that the profile call’s per-user context and the child-process token are separate decisions. Microsoft documents that CreateProcessW uses the calling process’s primary token even when the calling thread is impersonating. The example below therefore launches in the caller’s primary-token context; to run the worker as the client, convert a duplicated client impersonation token to a primary token (for example, with DuplicateTokenEx using TokenPrimary) before a reviewed CreateProcessAsUserW flow, then verify the resulting token rather than assuming impersonation carries across process creation. That flow has additional prerequisites: the token handle needs TOKEN_QUERY, TOKEN_DUPLICATE, and TOKEN_ASSIGN_PRIMARY; the caller typically needs SE_INCREASE_QUOTA_NAME and may also need SE_ASSIGNPRIMARYTOKEN_NAME when the token is not assignable. Check the API’s complete token, privilege, environment, and session requirements for the service scenario.
PSID appContainerSid = nullptr;
HRESULT hr = CreateAppContainerProfile(
L"com.example.preview.worker",
L"Preview worker",
L"Processes untrusted preview input",
nullptr,
0,
&appContainerSid);
if (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) {
hr = DeriveAppContainerSidFromAppContainerName(
L"com.example.preview.worker", &appContainerSid);
}
if (FAILED(hr)) {
// Do not launch a less-restricted fallback process.
return hr;
}
// Free appContainerSid with FreeSid when its final use is complete.
The profile provides per-user, per-app folders and registry storage; Microsoft documents ACLs on its folders and SHGetKnownFolderPath for locating them. Treat that provisioned profile area separately from arbitrary host paths. For external input and output locations, grant only the exact rights the worker needs, and keep any persistent output channel separate from the untrusted input tree.
Build only the capability set the worker needs
DeriveCapabilitySidsFromName converts a capability name into SID arrays. Its outputs are allocated memory: the caller must LocalFree the individual SIDs and the arrays. For a normal AppContainer, construct the SID_AND_ATTRIBUTES array from the capability SIDs that the workload actually needs, mark each selected SID enabled, and pass the count with the array. If a name cannot be resolved, treat that as a setup failure rather than silently launching with a weaker or different policy. The profile API accepts a PSID_AND_ATTRIBUTES array and count too; if the worker needs capabilities, use the same reviewed set for profile setup and process creation rather than copying an unrelated or broader set.
#include <windows.h>
#include <vector>
// derivedCapabilitySids contains capability PSIDs returned by a checked
// helper. Keep each SID alive through process creation and later free it
// with LocalFree, as required by the derivation API.
std::vector<SID_AND_ATTRIBUTES> capabilities(capabilityCount);
for (DWORD i = 0; i < capabilityCount; ++i) {
capabilities[i].Sid = derivedCapabilitySids[i];
capabilities[i].Attributes = SE_GROUP_ENABLED;
}
SECURITY_CAPABILITIES security = {};
security.AppContainerSid = appContainerSid;
security.Capabilities = capabilities.data();
security.CapabilityCount = capabilityCount;
security.Reserved = 0;
The array and each referenced SID must remain valid while Windows consumes the attribute data. Preserve clear ownership: CreateAppContainerProfile and DeriveAppContainerSidFromAppContainerName return SIDs freed with FreeSid; capability derivation returns arrays and individual SIDs freed with LocalFree; the SID_AND_ATTRIBUTES array itself uses the allocator chosen by the caller.
Attach the security capabilities before process creation
The process-creation path uses STARTUPINFOEX and a PROC_THREAD_ATTRIBUTE_LIST. InitializeProcThreadAttributeList is intentionally called twice: first with a null list to obtain the required byte count (the expected first call fails with ERROR_INSUFFICIENT_BUFFER), then after allocation to initialize the buffer. UpdateProcThreadAttribute adds PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES; Microsoft documents that attribute for Windows 8 and Windows Server 2012 or later. CreateProcess must receive EXTENDED_STARTUPINFO_PRESENT and the extended startup structure. This sample reserves capacity for exactly one attribute. It does not include a handle-list attribute.
SIZE_T attributeBytes = 0;
if (InitializeProcThreadAttributeList(nullptr, 1, 0, &attributeBytes)) {
// The documented size-query call is expected to fail with a null list.
return E_UNEXPECTED;
}
DWORD sizeError = GetLastError();
if (sizeError != ERROR_INSUFFICIENT_BUFFER) {
return HRESULT_FROM_WIN32(sizeError);
}
auto* attributes = static_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(
HeapAlloc(GetProcessHeap(), 0, attributeBytes));
if (attributes == nullptr) return E_OUTOFMEMORY;
if (!InitializeProcThreadAttributeList(attributes, 1, 0, &attributeBytes)) {
DWORD error = GetLastError();
HeapFree(GetProcessHeap(), 0, attributes);
return HRESULT_FROM_WIN32(error);
}
if (!UpdateProcThreadAttribute(
attributes, 0, PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES,
&security, sizeof(security), nullptr, nullptr)) {
DWORD error = GetLastError();
DeleteProcThreadAttributeList(attributes);
HeapFree(GetProcessHeap(), 0, attributes);
return HRESULT_FROM_WIN32(error);
}
STARTUPINFOEXW startup = {};
startup.StartupInfo.cb = sizeof(startup);
startup.lpAttributeList = attributes;
security, the capability array, the package SID, and all referenced SIDs must stay alive until the attribute list is no longer in use. For the single-attribute sample, launch with CreateProcessW, EXTENDED_STARTUPINFO_PRESENT, and bInheritHandles=FALSE. If handles are required, use a separate two-attribute variant: pass the maximum count of 2 to both the size-query and initialization calls, then add both PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES and PROC_THREAD_ATTRIBUTE_HANDLE_LIST. Each listed handle must be inheritable and must not be a pseudo-handle; the CreateProcessW call must set bInheritHandles=TRUE for the handle-list attribute. Do not turn on generic inheritance without a narrowly reviewed need. Keep the command-line buffer writable because CreateProcessW may modify it.
After the create call, capture GetLastError immediately on failure. Regardless of success, delete the initialized attribute list and free its backing buffer; on success, close both handles in PROCESS_INFORMATION when finished. Retain the profile SID and capability SIDs until all process-creation attributes referencing them have been destroyed. A cleanup path should make partial initialization explicit so that each resource is released exactly once.
Access still depends on ACLs and integrity labels
The AppContainer SID and capability SIDs participate in access checks, but they do not replace the DACL on a protected object. If a worker must read a test corpus or write one result file, grant the specific AppContainer identity only the intended rights on those objects. Avoid granting access to a parent directory that also contains secrets, executable tools, or unrelated user data. Windows’ low-integrity mandatory policy is another independent constraint; a DACL grant does not make a low-integrity process able to overwrite a higher-integrity object.
Treat brokered operations as part of the attack surface. If the worker needs a privileged operation, prefer a narrow request/response broker that validates identifiers, sizes, and allowed operations. Do not give the worker a broad file handle, inherited token, named-pipe endpoint, or capability merely because one test case failed without it.
Verify the child token and test denial paths
Start with a disposable executable and a disposable directory. In the child, query its token with GetTokenInformation using TokenIsAppContainer, TokenAppContainerSid, and TokenCapabilities; verify that the expected AppContainer identity is present and that capabilities match the deliberately small grant set. A successful CreateProcessW call alone does not prove that the resulting child is running with the intended security attributes.
Then test both sides of the policy: the worker can perform its one intended operation, and it cannot read a neighboring secret file, write a protected directory, or access an optional resource when its capability is absent. Repeat with malformed input and with capability/profile setup failures. Record Windows version, package state, user context, and ACLs so a passing test is reproducible.
AppContainer reduces the resources available to a compromised process, but the strength of the result depends on the token, ACLs, integrity policy, inherited handles, broker design, and the operating-system surface exposed to the workload. Review those together; do not market the process as “fully sandboxed” based only on the SECURITY_CAPABILITIES attribute.
Related:
- Windows Job Objects: Governing Process Trees, Limits, and Cleanup
- How to Set Up and Use Windows Sandbox
Sources:
- Launch an AppContainer
- CreateAppContainerProfile
- DeriveAppContainerSidFromAppContainerName
- DeriveCapabilitySidsFromName
- SECURITY_CAPABILITIES
- InitializeProcThreadAttributeList
- UpdateProcThreadAttribute
- CreateProcessW
- CreateProcessAsUserW
- GetTokenInformation
- TOKEN_INFORMATION_CLASS
- AppContainer isolation
- Mandatory Integrity Control
- AppContainer for legacy apps
- App capability declarations
- Impersonation Levels (Authorization)
- Processes in the Client Security Context
- DuplicateTokenEx