MOOR security

Dieser Eintrag ist auf Englisch verfasst.

Sharing a fence: a handle that knows when it is closed

Entry 043783 of 913 tests pass

D3D12 fences can now be shared through Windows handles inside one process, with the fence kept alive exactly as long as a handle or a fence object refers to it.

What changed

A fence is a counter that the GPU and the CPU use to wait for each other. D3D12 lets a program create a fence as "shared", turn it into a Windows handle with CreateSharedHandle, and turn that handle back into a fence with OpenSharedHandle. Until now the layer returned "not implemented", and one vkd3d-proton test crashed on the missing handle.

Each fence now points at a small reference-counted object holding its Metal counter (a MTLSharedEvent). The handle is a small anonymous or named shared-memory section (a Windows object that can hold a few bytes) recording a marker, the process id and which fence it names. Opening it gives a new fence object over the same counter, with its own reference count, which is what vkd3d-proton's test checks against Windows behaviour.

What fences, handles and the section point at, and when the counter is destroyed.ID3D12Fence (created SHARED) → FenceEvent refcount + MTLSharedEvent; ID3D12Fence (OpenSharedHandle) → FenceEvent refcount + MTLSharedEvent; app's handle → pagefile section magic, process id, fence id, flags; DuplicateHandle copy → pagefile section magic, process id, fence id, flags; our own handle + one reference → pagefile section magic, process id, fence id, flags; our own handle + one reference → FenceEvent refcount + MTLSharedEvent; HandleCount == 1? (NtQueryObject) → our own handle + one reference (yes: drop ours); FenceEvent refcount + MTLSharedEvent → GPU and CPU waits released (last reference: value = max, destroy)ID3D12Fence(created SHARED)FenceEventrefcount +MTLSharedEventID3D12Fence(OpenSharedHandle)pagefile sectionmagic, process id,fence id, flagsapp's handleDuplicateHandle copyour own handle+ one referenceHandleCount == 1?(NtQueryObject)GPU and CPU waitsreleasedyes: drop ourslast reference:value = max, destroy
What fences, handles and the section point at, and when the counter is destroyed. (Mermaid source)
Api/shared_fence's lifetime check: a queue wait stays blocked until the last handle and the last fence are gone.Participants: App, Compute queue, FenceEvent (value). 1. App → FenceEvent (value): CreateFence(SHARED), CreateSharedHandle; 2. App → Compute queue: Wait(fence, 1), then Signal(other, 2); 3. App → FenceEvent (value): Release fence (handle still open); 4. Note (Compute queue, FenceEvent (value)): queue stays blocked: other == 1; 5. App → FenceEvent (value): OpenSharedHandle -> new fence, value 0; 6. App → FenceEvent (value): CloseHandle; 7. Note (Compute queue, FenceEvent (value)): still blocked: a fence is alive; 8. App → FenceEvent (value): Release last fence -> sweep: our handle is the only one; 9. FenceEvent (value) → Compute queue: value = max, wait released; 10. Compute queue → App: other == 2AppCompute queueFenceEvent (value)CreateFence(SHARED), CreateSharedHandleWait(fence, 1), then Signal(other, 2)Release fence (handle still open)queue stays blocked: other == 1OpenSharedHandle -> new fence, value 0CloseHandlestill blocked: a fence is aliveRelease last fence -> sweep: our handle is the only onevalue = max, wait releasedother == 2
Api/shared_fence's lifetime check: a queue wait stays blocked until the last handle and the last fence are gone. (Mermaid source)

The interesting part

The hard part is lifetime, not the handle itself. D3D12 keeps the counter alive while any handle is open, even after the program has released every fence object, and it has to release anything waiting on the counter once the last fence and the last handle are gone. CloseHandle gives a DLL no callback. The layer keeps a handle of its own to each section and asks Wine how many handles the object has (NtQueryObject, which Wine answers from wineserver's own count). When the answer is 1, only our copy is left, so the layer drops it along with its reference.

I first looked at Wine's D3DKMT synchronization objects, which vkd3d-proton uses to share Vulkan semaphores. They give a proper shared handle, but their public API stores no data of ours, so a handle could not lead back to our fence. Sharing across processes is still missing: it would need Metal's cross-process event handle moved between two Unix processes, which wineserver cannot carry.

Numbers

  • Full run: 783 pass, 26 fail, 104 skip of 913; 0 newly failing, 2 newly passing (api/shared_fence, test_fence_wait_robustness_shared).
  • Previous run: 781 pass, 27 fail, 104 skip of 912.
Tests by suite at the end of this entry: 783 of 913 pass (data as CSV)
  • pass
  • fail
  • skip

suite · share of each row's total

  1. vkd3d-proton (+1)460 pass, 17 fail, 100 skip of 577
  2. vkd3d177 pass, 9 fail, 4 skip of 190
  3. apps77 pass, 0 fail, 0 skip of 77
  4. api (+1)65 pass, 0 fail, 0 skip of 65
  5. samples4 pass, 0 fail, 0 skip of 4