Windows does not document a general-purpose Win32 mechanism that atomically orders authorization revocation against plaintext becoming visible inside an already-running untrusted process; the broker must instead define a conservative broker-owned commit boundary and treat uncertain delivery as potentially exposed.
Required architecture
Keep protected files, mappings, decryption keys, and unrestricted handles exclusively in the broker. AppContainer can restrict the client to explicitly granted resources, but it does not retract data already supplied to that client. AppContainer was introduced in Windows 8; Win32 app isolation, which builds on AppContainer and brokered access, requires Windows 11 version 24H2 or later.^1^^2^
Use a broker state machine with two independent dimensions:
- Authorization:
AUTHORIZED,RELEASE_RESERVED,REVOKED - Transport result:
NOT_SUBMITTED,PENDING,COMPLETED,CANCELED,FAILED - Disclosure result:
NOT_RELEASED,POTENTIALLY_EXPOSED,INDETERMINATE
A safe ordering protocol is:
- Under a short broker lock, serialize release against revocation and allocate a monotonically increasing operation ID.
- Persist
RELEASE_RESERVEDorRELEASE_COMMITTEDbefore submitting plaintext I/O. If persistence fails, do not submit it. - Release the lock before performing potentially blocking persistence follow-up, IPC, cancellation, or waits. The commit point orders broker decisions only; it does not prove kernel transfer or client visibility.
- Handle every submission result:
- Immediate success: record transport completion and conservatively mark potentially exposed.
-
ERROR_IO_PENDING: retain the buffer and uniqueOVERLAPPEDuntil actual completion. - Immediate failure before pending: record failed/not submitted as applicable.
- On revocation, prevent new reservations immediately, request cancellation of pending operations, and wait only to a policy deadline.
- At the deadline, produce a bounded broker decision such as
INDETERMINATE; continue ownership of the buffer andOVERLAPPEDin a cleanup/reaper path until the real completion arrives. - During crash recovery, classify any committed release without a final result as
INDETERMINATEor conservatively exposed—not recovered.
Illustrative logic:
lock(policy);
if (revoked || !authorized) { unlock(policy); return DENIED; }
id = ReserveRelease(); // broker ordering point
unlock(policy);
if (!PersistReleaseCommitted(id)) // durable before plaintext submission
return NOT_SUBMITTED;
SubmitResult r = SubmitOverlapped(id);
if (r == ImmediateSuccess)
Record(id, COMPLETED, POTENTIALLY_EXPOSED);
else if (r == Pending)
TrackUntilActualCompletion(id); // buffer + OVERLAPPED remain valid
else
Record(id, FAILED, NOT_RELEASED);
Why cancellation is not the boundary
CancelIoEx only requests cancellation of outstanding I/O in the calling process. It does not wait, does not change the handle state, and can race with normal completion. The final operation can complete normally, complete with ERROR_OPERATION_ABORTED, or fail for another reason. Its minimum supported versions are Windows Vista and Windows Server 2008.^3^
Therefore:
-
CancelIoExsuccess means cancellation was requested, not that zero bytes were disclosed. - Final
ERROR_OPERATION_ABORTEDmeans the kernel I/O operation was canceled; it is not general proof that the peer never observed data. - Do not interpret a zero or unavailable byte count on a failed completion as proof of zero delivery unless the specific transport explicitly documents that meaning.
- Do not free or reuse the buffer or
OVERLAPPEDuntil actual completion.
GetOverlappedResultEx supplies a finite wait and reports actual transferred bytes for completed read/write operations. A timeout returns WAIT_TIMEOUT; a zero-duration check of an unfinished operation returns ERROR_IO_INCOMPLETE. It is supported on Windows 8/Windows Server 2012 and later.^4^ A timeout bounds the broker’s wait and audit decision—not kernel cancellation or peer visibility.
IPC and memory limitations
For named pipes, WriteFile may return because the asynchronous write was initiated, because bytes were written, because buffer space became available, or because an error occurred.^5^ FlushFileBuffers on the server end waits until the client has read buffered pipe data, but “read” is still not an exact application-level visibility or processing boundary.^6^ DisconnectNamedPipe discards unread pipe data, but cannot retract bytes already read by the client.^7^
Avoid plaintext shared mappings: UnmapViewOfFile unmaps only the calling process’s view, while shared pages remain until other processes unmap or terminate.^8^
ACL changes govern access checks used to obtain handles; a returned handle already carries granted rights.^9^ Consequently, ACL mutation should block future opens, not serve as revocation of existing capabilities.
Process termination fallback
Termination can contain future execution but cannot prove that earlier plaintext disappeared. TerminateProcess is asynchronous, requests cancellation of pending I/O, and the process cannot finish termination until pending I/O completes or is canceled.^10^
A job object can terminate the client and its descendants, but TerminateJobObject requires JOB_OBJECT_TERMINATE on the job handle and, as documented, PROCESS_TERMINATE rights for associated processes.^11^ Treat termination timeouts or crashes as INDETERMINATE, never as proof of non-exposure.
References
- Launch an AppContainer
- Windows apps: packaging, deployment, and process
- CancelIoEx function (ioapiset.h)
- GetOverlappedResultEx function (ioapiset.h)
- WriteFile function (fileapi.h)
- FlushFileBuffers function (fileapi.h)
- DisconnectNamedPipe function (namedpipeapi.h)
- UnmapViewOfFile function (memoryapi.h)
- Requesting Access Rights to an Object
- TerminateProcess function (processthreadsapi.h)
- TerminateJobObject function (jobapi2.h)