MOOR security

Dieser Eintrag ist auf Englisch verfasst.

Reserved resources on Metal 4, and tiles that remember what they were mapped to

Entry 014501 of 778 tests pass

D3D12 tiled resources now run on Metal 4's placement sparse API, with one workaround: an unmapped tile on Apple's GPU keeps showing the memory it was last mapped to.

What changed

D3D12 has reserved resources (also called tiled resources): buffers and textures with no memory of their own. The application maps their 64 KB tiles onto pieces of heaps, which are large memory blocks, and it can remap them at any time. Streaming engines use them for virtual texturing. macOS 26 added a matching Metal 4 feature, placement sparse resources. mtl12 now implements reserved resources on top of it: creation, GetResourceTiling, UpdateTileMappings, CopyTileMappings and CopyTiles.

Metal matches D3D12 closely. Tiles are 64 KB, and the 2D tile shapes are the ones D3D12 specifies. The gaps were found with small spike programs before the implementation. 3D textures get flat 2D tiles, so there are no 3D reserved textures. The documented tier queries crash with "unrecognized selector". Sparse buffers cannot be viewed as typed texture buffers.

Scene tiled: a reserved texture with the tiles spelling "12" remapped to NULL, which must read zero
Gallery scene tiled: a reserved texture with the tiles spelling "12" remapped to NULL, which must read zero. All scenes in the gallery

The interesting part

In D3D12, a tile can be mapped to NULL. On the better hardware tiers, reads of a NULL tile then return zero. Metal has an unmap operation for this, and a tile that was never mapped does read zero. But on the M1 Max, a tile that had been mapped and was then unmapped mostly kept returning the data of the page it last pointed at. This happened when the GPU read it through a Metal 4 queue, and it did not matter whether I waited on the CPU or put explicit barriers in between. A shader on the older Metal queue read zero. The first unmap sometimes worked and later ones did not, which looks like timing. A small standalone program reproduces it.

So mtl12 never unmaps. Each device has one 64 KB tile of zeros (the kernel zero-fills fresh pages), and a NULL mapping points there. That gives the right reads. A write to a NULL tile would now change the shared zero tile, while D3D12's tier 2 requires such writes to be dropped. Tier 2 also needs shader feedback about whether a sample touched unmapped tiles, and SPIRV-Cross, which translates our shaders to Metal's language, cannot emit that yet. mtl12 therefore reports tier 1, where both of those are undefined.

Tiles mapped to NULL all point at one shared zero tile per device.reserved texture (8×4 tiles of 64 KB): tile 0, tile 1, tile 2, tile 3 → NULL, tile 4 → NULL; heap: page 0, page 1, page 2; tile 0 → page 0; tile 1 → page 1; tile 2 → page 2; tile 3 → NULL → one shared zero tile per device; tile 4 → NULL → one shared zero tile per devicereserved texture (8×4 tiles of 64 KB)heaptile 0tile 1tile 2tile 3 → NULLtile 4 → NULLpage 0page 1page 2one sharedzero tile per device
Tiles mapped to NULL all point at one shared zero tile per device. (Mermaid source)

Numbers

  • Full run: 501 pass, 93 fail, 184 skip of 778, up from 495/93/190. Exactly six tests changed, all from skip to pass, all reserved resource tests. No test got worse.
  • vkd3d-proton's test_get_resource_tiling: 1,298 checks, 0 failures, 8 skipped for tier-3/4 cases.
  • Spike: 952 of the tiles checked read stale data in 100 remap iterations through the Metal 4 queue, 0 through the older queue.
Standalone test: tiles still reading their old page after an unmap, 100 remap iterations (data as CSV)

how the tile was remapped and read · tiles read wrong

  1. separate unmap calls952
  2. one unmap call952
  3. Metal 4 queue shader read966
  4. older queue shader read0
Tests by suite at the end of entry 014: 501 of 778 pass (data as CSV)
  • pass
  • fail
  • skip

suite · share of each row's total

  1. vkd3d-proton (+5)339 pass, 64 fail, 174 skip of 577
  2. vkd3d (+1)151 pass, 29 fail, 10 skip of 190
  3. api8 pass, 0 fail, 0 skip of 8
  4. samples3 pass, 0 fail, 0 skip of 3