(fix): Scope-aware runtime escape detection#36
Conversation
…outer-scope arrays _check_pointer_overlap and _check_bitchunks_overlap previously iterated ALL pool vectors, causing false PoolRuntimeEscapeError when an array acquired in an outer scope was used/returned from an inner scope. Now uses checkpoint stack boundary (_scope_boundary) to only check vectors acquired in the current scope depth.
…s in CI - Reorder direct-rewind @with_pool expansion: leaked inner scope cleanup now runs BEFORE _validate_pool_return, ensuring _scope_boundary uses the correct (normalized) depth instead of a leaked inner depth. Applies to both block-form and function-form code paths. - Register test_scope_depth_validation.jl in runtests.jl for both Julia 1.11+ and legacy branches so CI exercises the scope-depth tests. - Add edge case tests: ReshapedArray parent-chain propagation (scenarios 16a-c), sentinel depth-0 (scenario 17), and leaked inner scope regression (scenarios 18a-b for Array and BitArray). - Update test header comment to past tense for the fixed bug description.
Apply _scope_boundary to _check_tp_cuda_overlap and _check_tp_metal_overlap so GPU backends only check vectors acquired in the current scope, matching the CPU fix from 761a3c9. Previously both GPU overlap functions iterated all tp.vectors, causing false-positive escape errors for outer-scope GPU arrays used inside nested @with_pool scopes. Add scope-depth validation tests for both backends (7 scenarios each: outer/inner, 3-level nesting, mixed types, lazy path, container wrapping, leaked scope). Metal tests verified locally.
_poison_fill! now handles three cases: - Known types (Float, Int, Complex): dispatch to _poison_value (NaN, typemax) - Custom isbits structs without zero(T): duck-type fallback via 0 * first(v) - Non-isbits reference types: skip poison entirely (resize! handles invalidation) Previously, _poison_fill! called _poison_value(T) unconditionally, which fell through to zero(T) for unknown types — causing MethodError during rewind for custom structs that don't define zero(). Add test scenarios for custom isbits struct (TestPoint) and mutable non-isbits struct (TestParticle) validating both scope-aware escape detection and graceful poison/skip behavior.
There was a problem hiding this comment.
Pull request overview
This PR fixes false-positive runtime escape detection in nested @with_pool scopes by making overlap checks scope-aware (only checking vectors acquired in the current scope), reorders direct-rewind macro cleanup to normalize leaked inner scopes before validation, and improves poisoning behavior for custom element types.
Changes:
- Add
_scope_boundaryand apply it to CPU/CUDA/Metal overlap checks so outer-scope arrays aren’t flagged in inner scopes. - Reorder direct-rewind
@with_poolexpansion to clean leaked inner scopes before calling_validate_pool_return. - Extend
_poison_fill!to handle some custom isbits structs and skip poisoning for non-isbits element types; add comprehensive CPU/CUDA/Metal tests for scope-depth validation.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/debug.jl |
Adds _scope_boundary, updates pointer-overlap to be scope-aware, and changes _poison_fill! fallback behavior. |
src/macros.jl |
Reorders leaked-scope cleanup to occur before runtime validation in direct-rewind macro paths. |
src/bitarray.jl |
Makes BitArray chunk overlap checks scope-aware (Julia ≥ 1.11 path). |
ext/AdaptiveArrayPoolsCUDAExt/debug.jl |
Adds scope-aware boundary to CUDA overlap checks by threading current_depth. |
ext/AdaptiveArrayPoolsMetalExt/debug.jl |
Adds scope-aware boundary to Metal overlap checks by threading current_depth. |
test/test_scope_depth_validation.jl |
New CPU test suite covering nested scopes, containers, views/reshape parent chains, leaked scopes, and poison behavior. |
test/runtests.jl |
Includes the new CPU scope-depth validation test file. |
test/cuda/test_scope_depth_validation.jl |
New CUDA test suite mirroring key CPU scope-depth validation scenarios. |
test/cuda/runtests.jl |
Includes the new CUDA scope-depth validation test file. |
test/metal/test_scope_depth_validation.jl |
New Metal test suite mirroring key CPU scope-depth validation scenarios. |
test/metal/runtests.jl |
Includes the new Metal scope-depth validation test file. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #36 +/- ##
==========================================
+ Coverage 96.55% 96.61% +0.06%
==========================================
Files 14 14
Lines 2726 2748 +22
==========================================
+ Hits 2632 2655 +23
+ Misses 94 93 -1
🚀 New features to boost your workflow:
|
Port _scope_boundary to src/legacy/bitarray.jl — the Julia <1.11 code path still iterated all pool.bits.vectors, causing false-positive PoolRuntimeEscapeError on LTS. Fixes CI failure on julia-lts. Harden _poison_fill! so neither zero(T) nor 0*first(v) failure can throw during rewind: both attempts are individually try/catch guarded. If neither produces a value, poisoning is skipped gracefully.
Problem
Runtime escape detection (
RUNTIME_CHECK=1) iterated all pool vectors whenchecking for escaping arrays. This caused false-positive
PoolRuntimeEscapeErrorfor arrays acquired in an outer
@with_poolscope and used inside a nestedinner scope — a valid and common pattern.
Additionally, the direct-rewind
@with_poolmacro validated before cleaningup leaked inner scopes, causing
_scope_boundaryto use an incorrect (leaked)depth when an inner scope threw without rewind.
Solution
1.
_scope_boundary— scope-aware overlap checkNew
@inlinefunction that uses the existing checkpoint stack to compute theboundary index: vectors at
1:boundarybelong to outer scopes and are skipped.Applied to all three backends:
_check_pointer_overlap,_check_bitchunks_overlap_check_tp_cuda_overlap_check_tp_metal_overlap2. Macro reorder — leaked scope cleanup before validation
In the direct-rewind
@with_poolexpansion (both block-form and function-form),leaked inner scope cleanup now runs before
_validate_pool_return:This ensures
_scope_boundaryalways sees the correct_current_depth.3. Robust poison for custom isbits types
_poison_fill!now handles custom isbits structs via duck-type zero(
0 * first(v)) whenzero(T)is not defined, and skips poisoning entirelyfor non-isbits reference types (where
resize!(v, 0)handles invalidation).