Repository navigation
fix(metal): use-after-free in gpu::eval for primitives that synchronize mid-eval - #5
Merged
Merged
Conversation
…ze mid-eval gpu::eval captured the MTL::CommandBuffer* BEFORE eval_gpu, but a primitive that calls CommandEncoder::synchronize() inside eval_gpu (the expert-tile route's sortedness-retract check) commits and REPLACES the encoder's buffer; eval then attached its buffer-liveness completion handler to the dangling pointer (objc_msgSend on freed object). The Gemma E=128 route has been surviving this by allocator luck; the Qwen E=256 shapes crash deterministically. Fetch the buffer from the encoder AFTER eval_gpu — the current buffer holds the tail of the primitive's work, which is exactly what the inputs must outlive.
This was referenced Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
gpu::eval(mlx/backend/metal/eval.cpp) captures theMTL::CommandBuffer*beforeeval_gpuruns, then attaches the buffer-liveness completion handler to that pointer afterwards. Any primitive that callsCommandEncoder::synchronize()mid-eval — e.g. the expert-tile route's sortedness-retract check (Gemma E=128 runs this in production today) — commits and replaces the encoder's buffer, leaving the captured pointer dangling. The handler is then attached to a freed object:objc_msgSend/addCompletedHandleron a staleMTL::CommandBuffer*.Production Gemma has been surviving this by allocator luck. The Qwen E=256 expert-tile shapes (#6) crash deterministically — SIGSEGV in
objc_msgSend/addCompletedHandler(crash logs~/Library/Logs/DiagnosticReports/xctest-2026-08-13-1430*.ips).Change
Fetch the command buffer from the encoder after
eval_gpu. The current buffer holds the tail of the primitive's work, which is exactly what the input buffers must outlive. The debug label call likewise re-fetches instead of using the stale capture.Behavior
flowchart LR subgraph Before A1["capture cbuf ptr"] --> B1[eval_gpu] --> C1{"primitive syncs mid-eval?"} C1 -->|no| D1["handler on cbuf — OK"] C1 -->|yes| E1["encoder committed + replaced buffer<br/>handler on DANGLING ptr → UAF<br/>Gemma: latent · Qwen E=256: deterministic SIGSEGV"] end subgraph After A2[eval_gpu] --> B2["fetch encoder's CURRENT buffer"] --> C2["handler always on live tail buffer<br/>inputs outlive exactly the right work"] endCode
flowchart LR subgraph Before F1["eval(): auto* command_buffer = encoder.get_command_buffer()"] --> G1[eval_gpu] --> H1["command_buffer->addCompletedHandler(...) ← stale after mid-eval commit"] end subgraph After F2["eval(): (no early capture)"] --> G2[eval_gpu] --> H2["encoder.get_command_buffer()->addCompletedHandler(...) ← always current"] endVerification
Same diff running on the
codex/gemma4-autoresearch-v0.8.2line (ab5e1dd): Qwen E=256 route went from deterministic SIGSEGV to 10/10 green GPU kernel tests (SortedGatherQuantizedMMTestsvia mlx-swift) and clean release-server e2e runs; Gemma E=128 suites intact. Cherry-pick ontomainis clean (this PR);libmlxcompiles on this branch (Release, Metal JIT).Stack
Merge order (top to bottom):
gpu::evalUAF fix (basemain) — prod-relevant crash fix, merge first ← this PRcodex/gemma4-autoresearch-v0.8.2, the line mlx-swift pinsSource/Cmlx/mlxto) · perf(metal): E=256 expert-tile route + trust + gpu::eval UAF fix — darkbloom-base mirror #7 — same commits mirrored ontodarkbloom-base(the line d-inference pinslibs/mlxto)main)perf(cbv2-mtp): Qwen3.6 GDN capture-verify + target-prefix acceptance (1.53x vs serial verify) mlx-swift-lm#106— GDN capture-verify — merged (squashd06da67)main)master)perf/qwen36-mtp-capture-verify= Fix typo in CMakeLists.txt ml-explore/mlx#616's head)Chain: mlx UAF fix → mlx tiles (+mirror) → mlx-swift →
mlx-swift-lm#106(merged) → mlx-swift-lm prefill → d-inference#616 → d-inference prefill.