Executor field order is load-bearing for teardown safety and undocumented #14
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
The field declaration order of
struct Executoris load-bearing for memory safety: it determines the order in which GPU buffers, the Metal context, and the mmap'd model are torn down. Reordering those three fields — a change that looks purely cosmetic and that no reviewer or lint would flag — introduces a use-after-free. There is no comment saying so.This is not currently a bug. It is a correct arrangement with no documentation protecting it, in a file that was recently split and reorganised (
4292e0d Split Rust code into domain modules).Evidence
src/engine/metal.rs:561-571:Rust drops fields in declaration order, so the current sequence is: buffers freed →
ds4_gpu_cleanup()→ mmap unmapped. Both edges matter:sessionbefore_context:Session/LayerState/CompressionState(src/engine/metal.rs:463-484) ownBuffers whoseDropcallsds4_gpu_tensor_free(src/engine/metal/gpu.rs:922). Those must run before the device teardown inds4_gpu_cleanup()._contextbeforemodel: this is the sharp one.Context::openpasses the raw mmap pointer to the C side (src/engine/metal/gpu.rs:783-789,ds4_gpu_set_model_map_range(model.main.map_ptr().cast(), ...)), and the Objective-C side wraps that memory in Metal buffers without copying it:native/metal/ds4_metal.m:10329and:1791—[g_device newBufferWithBytesNoCopy:(void *)(base + page_offset) ...]So live
MTLBuffers alias the mapping directly. The mapping must outlive them, which is exactly what the current order guarantees.How ds4 handles this — and why DS4Server deliberately differs
The C reference tears down in the opposite order:
ds4.c:56282-56289:model_close()atds4.c:2310callsmunmap((void *)m->map, m->size).weights_free()atds4.c:6564ismemset(w, 0, sizeof(*w))— it does not callds4_gpu_model_views_clear(). The only callers of that helper are insideds4_metal.mitself (:9283,:10138, and the re-map paths).So ds4 unmaps the model and only then releases the no-copy Metal buffers that alias it. In practice this is likely benign — an
MTLBuffercreated withnewBufferWithBytesNoCopyand no deallocator block does not dereference the pages on release — but it is the weaker ordering, and DS4Server's is the defensible one.This is the actionable part: because the two codebases disagree, someone comparing them later could "fix" DS4Server to match the C reference and introduce a real use-after-free. The divergence needs to be recorded as intentional.
Suggested fix
No code change. Add a comment above the last three fields of
Executorstating:session'sBuffers must be freed beforeds4_gpu_cleanup();model's mmap must outliveds4_gpu_cleanup()becauseds4_metal.mwraps it withnewBufferWithBytesNoCopy(citenative/metal/ds4_metal.m:10329);../ds4/ds4.c:56287-56288, which unmaps first — do not "align" it.Optional belt-and-braces: a
Drop for Executorthat explicitly documents/enforces the sequence, so the invariant survives even a field reorder. Weigh that against the extra code; the comment may well be enough.Acceptance criteria
Dropimpl is added instead,make bundleand a real generation run still succeed with no Metal validation errors (MTL_DEBUG_LAYER=1is a useful check here).Fixed in
3708afb. Executor now documents its safety-critical declaration/drop order: session buffers must precede Metal context teardown, and the model mmap must outlive context cleanup because native/metal/ds4_metal.m:10329 wraps it with newBufferWithBytesNoCopy. The comment also records the intentional divergence from ../ds4/ds4.c:56287-56288. Verified with cargo fmt, Clippy (-D warnings), make bundle, and cargo test --all-features.