From dfa899128afa34ee925767429f204827bcf30d4e Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Wed, 22 Jul 2026 15:02:40 +0200 Subject: [PATCH] Fix derived array lifetime race Keep the parent oneArray alive while deriving views so its finalizer cannot mark the shared GPUArrays DataRef as freed before the new array retains it. Add a GC-stress regression for nested ephemeral views, which frequently triggered "Attempt to copy a freed reference" in AcceleratedKernels. --- src/array.jl | 5 ++++- test/array.jl | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/array.jl b/src/array.jl index 569da017..3fd539b1 100644 --- a/src/array.jl +++ b/src/array.jl @@ -570,7 +570,10 @@ function GPUArrays.derive(::Type{T}, a::oneArray, dims::Dims{N}, offset::Int) wh Base.elsize(a) == 0 || error("Cannot derive a singleton array from non-singleton inputs") end offset = a.offset + offset * sizeof(T) - oneArray{T,N}(a.data, dims; a.maxsize, offset) + # The derived array constructor copies `a.data`, but merely loading that field does not + # keep `a` alive. Without this preserve, `a` may be finalized between the field load and + # the DataRef copy, causing its finalizer to mark the reference as freed. + GC.@preserve a oneArray{T,N}(a.data, dims; a.maxsize, offset) end diff --git a/test/array.jl b/test/array.jl index aadc8928..1ca66453 100644 --- a/test/array.jl +++ b/test/array.jl @@ -43,6 +43,41 @@ end @test Array(xs) == [0,1,0] end +@testset "derived array lifetime" begin + parent = oneArray{UInt8}(undef, 1) + + # Construct through an ephemeral derived array, whose finalizer shares the same + # reference-counted allocation with the returned view. + function ephemeral_derived_view(parent) + intermediate = @view parent[:] + @view intermediate[:] + end + + derived = ephemeral_derived_view(parent) + @test derived isa oneArray{UInt8} + + # Exercise finalization while deriving. Without preserving the immediate parent in + # GPUArrays.derive, its finalizer can mark the DataRef as freed before it is copied. + if Threads.nthreads() > 1 + stop_gc = Threads.Atomic{Bool}(false) + gc_task = Threads.@spawn while !stop_gc[] + GC.gc(false) + yield() + end + try + Threads.@threads for _ in 1:min(Threads.nthreads(), 4) + for _ in 1:100 + a = ephemeral_derived_view(parent) + oneAPI.unsafe_free!(a) + end + end + finally + stop_gc[] = true + wait(gc_task) + end + end +end + @testset "reinterpret of view with non-aligned offset" begin # reinterpreting a view to a larger element type where the byte offset # is not a multiple of the new element size