Hey all, hoping someone on the Metal team can take a look at this one. It's a crash
inside Metal 4's binary capture, and as far as I can tell there's no way to fix it from
the app side. Filed as FB25478288 (partial repro is link is there too).
The setup that crashes is basically the recommended Metal 4 workflow: one MTL4Compiler
with a MTL4PipelineDataSetSerializer (CaptureBinaries) attached, called from
maximumConcurrentCompilationTaskCount threads, to harvest pipelines for an offline
archive. Lots of pipelines share a vertex function, which I'd guess is true for most
real renderers.
- The crash is 100% inside
Metal.framework, with no app code on the crashing thread. - It's easy to hit: 3 out of 3 harvest runs crashed for us, one of them 2 s in.
- The serializer is attached to the compiler implicitly, so the app can't put a lock
around whatever it does internally. MTL4CompilerisSendable, and the docs say it handles multithreading by default,
so I'd expect capture to be thread-safe too.
- MacBook Pro M2 Max, macOS 27.2, Xcode 27.2, Metal toolchain 32023.922.4
- Also reported on an M3 Pro
- ~33k render pipelines over ~16k vertex functions, up to ~1,000 pipelines in a row
sharing one vertex function MTL4RenderPipelineDescriptorwithMTL4LibraryFunctionDescriptorstages (sometimes
wrapped inMTL4SpecializedFunctionDescriptor), all colour attachmentsUnspecialized- The serializer gets written out with
serializeAsArchiveAndFlushToURL:every so often,
then we start a fresh compiler + serializer
EXC_BAD_ACCESS (SIGSEGV), "possible pointer authentication failure":
objc_msgSend
MultiLevelAirntBinaryCache::getElementAtDestination(MTLBinaryKey*, ...)
-[MTLCompiler compileFunctionRequestInternal:frameworkLinking:linkDataSize:reflectionOnly:compilerTask:completionHandler:]
...
AGX::Compiler::compileProgram<AGX::VertexProgramKey, ...>
It looks like a use-after-free: two compiles capturing the same vertex program at the
same time, and an entry in the destination archive gets freed while the other one is
still using it. It only happens with capture on, and it goes away if we never let two
capture compiles that share a stage function run at once.
A lock per stage function around each capture compile, plus sending runs of pipelines
that share a vertex shader to the same thread. That stopped the crashes, but we had to
put a timeout on the lock, because the compiler occasionally never returns, so in theory
it can still race.
If serializeAsArchiveAndFlushToURL: runs while other threads are still compiling into
that serializer, we get a different crash (null deref at 0x30) in
dispatch_data_create_map ← -[_MTLBinaryArchive newArchiverIdWithBinaryKey:entry:srcArchiverId:functionType:].
Our standalone repro hits this one reliably. The getElementAtDestination crash only
shows up with our real workload so far.
https://github.com/Aeonitis/metal4-capture-race
It's a small Objective-C repro (Metal + Foundation only) with a bunch of modes, the
results so far, and a comparison with how Vulkan (VkPipelineCache) and D3D12 spell out
the thread-safety rules for this same pattern.
- Is capturing from multiple threads into one
MTL4PipelineDataSetSerializermeant to
be supported? If not, could the docs say so? - Is one compiler + one serializer per thread a safe workaround, or is there shared
state between compilers in the same process that would still race? - Is calling
serializeAsArchiveAndFlushToURL:while compiles are still in flight on
that serializer allowed? - Is this already fixed in a seed