Skip to content

Commit ea86a62

Browse files
vtjnashKristofferC
authored andcommitted
process: ensure uvfinalize and _uv_close_cb are synchronized (#44476)
There appeared to be a possibility they could race and the data field might already be destroyed before we reached the close callback, from looking at the state of the program when reproducing #44460. This is because the uv_return_spawn set the handle to NULL, which later can cause the uvfinalize to exit early (if the finalizer gets run on another thread, since we have disabled finalizers on our thread). Then the GC can reap the julia Process object prior to uv_close cleaning up the object. We solve this by calling disassociate_julia_struct before dropping the reference to the handle. But then we also fully address any remaining race condition by having uvfinalize acquire a lock also. The uv_return_spawn callback also needs to be synchronized with the constructor, since we might have arrived there before we finished allocating the Process struct here, leading to missed exit events. Fixes #44460 (cherry picked from commit c591bf2)
1 parent 6e5b031 commit ea86a62

File tree

2 files changed

+44
-28
lines changed

2 files changed

+44
-28
lines changed

base/process.jl

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ pipe_writer(p::ProcessChain) = p.in
3838
# release ownership of the libuv handle
3939
function uvfinalize(proc::Process)
4040
if proc.handle != C_NULL
41-
disassociate_julia_struct(proc.handle)
42-
ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), proc.handle)
43-
proc.handle = C_NULL
41+
iolock_begin()
42+
if proc.handle != C_NULL
43+
disassociate_julia_struct(proc.handle)
44+
ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), proc.handle)
45+
proc.handle = C_NULL
46+
end
47+
iolock_end()
4448
end
4549
nothing
4650
end
@@ -52,6 +56,7 @@ function uv_return_spawn(p::Ptr{Cvoid}, exit_status::Int64, termsignal::Int32)
5256
proc = unsafe_pointer_to_objref(data)::Process
5357
proc.exitcode = exit_status
5458
proc.termsignal = termsignal
59+
disassociate_julia_struct(proc) # ensure that data field is set to C_NULL
5560
ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), proc.handle)
5661
proc.handle = C_NULL
5762
lock(proc.exitnotify)
@@ -74,33 +79,44 @@ const SpawnIOs = Vector{Any} # convenience name for readability
7479
# handle marshalling of `Cmd` arguments from Julia to C
7580
@noinline function _spawn_primitive(file, cmd::Cmd, stdio::SpawnIOs)
7681
loop = eventloop()
77-
iohandles = Tuple{Cint, UInt}[ # assuming little-endian layout
78-
let h = rawhandle(io)
79-
h === C_NULL ? (0x00, UInt(0)) :
80-
h isa OS_HANDLE ? (0x02, UInt(cconvert(@static(Sys.iswindows() ? Ptr{Cvoid} : Cint), h))) :
81-
h isa Ptr{Cvoid} ? (0x04, UInt(h)) :
82-
error("invalid spawn handle $h from $io")
82+
cpumask = cmd.cpus
83+
cpumask === nothing || (cpumask = as_cpumask(cmd.cpus))
84+
GC.@preserve stdio begin
85+
iohandles = Tuple{Cint, UInt}[ # assuming little-endian layout
86+
let h = rawhandle(io)
87+
h === C_NULL ? (0x00, UInt(0)) :
88+
h isa OS_HANDLE ? (0x02, UInt(cconvert(@static(Sys.iswindows() ? Ptr{Cvoid} : Cint), h))) :
89+
h isa Ptr{Cvoid} ? (0x04, UInt(h)) :
90+
error("invalid spawn handle $h from $io")
91+
end
92+
for io in stdio]
93+
handle = Libc.malloc(_sizeof_uv_process)
94+
disassociate_julia_struct(handle)
95+
(; exec, flags, env, dir) = cmd
96+
iolock_begin()
97+
err = ccall(:jl_spawn, Int32,
98+
(Cstring, Ptr{Cstring}, Ptr{Cvoid}, Ptr{Cvoid},
99+
Ptr{Tuple{Cint, UInt}}, Int,
100+
UInt32, Ptr{Cstring}, Cstring, Ptr{Bool}, Csize_t, Ptr{Cvoid}),
101+
file, exec, loop, handle,
102+
iohandles, length(iohandles),
103+
flags,
104+
env === nothing ? C_NULL : env,
105+
isempty(dir) ? C_NULL : dir,
106+
cpumask === nothing ? C_NULL : cpumask,
107+
cpumask === nothing ? 0 : length(cpumask),
108+
@cfunction(uv_return_spawn, Cvoid, (Ptr{Cvoid}, Int64, Int32)))
109+
if err == 0
110+
pp = Process(cmd, handle)
111+
associate_julia_struct(handle, pp)
112+
else
113+
ccall(:jl_forceclose_uv, Cvoid, (Ptr{Cvoid},), handle) # will call free on handle eventually
83114
end
84-
for io in stdio]
85-
handle = Libc.malloc(_sizeof_uv_process)
86-
disassociate_julia_struct(handle) # ensure that data field is set to C_NULL
87-
(; exec, flags, env, dir) = cmd
88-
err = ccall(:jl_spawn, Int32,
89-
(Cstring, Ptr{Cstring}, Ptr{Cvoid}, Ptr{Cvoid},
90-
Ptr{Tuple{Cint, UInt}}, Int,
91-
UInt32, Ptr{Cstring}, Cstring, Ptr{Cvoid}),
92-
file, exec, loop, handle,
93-
iohandles, length(iohandles),
94-
flags,
95-
env === nothing ? C_NULL : env,
96-
isempty(dir) ? C_NULL : dir,
97-
@cfunction(uv_return_spawn, Cvoid, (Ptr{Cvoid}, Int64, Int32)))
115+
iolock_end()
116+
end
98117
if err != 0
99-
ccall(:jl_forceclose_uv, Cvoid, (Ptr{Cvoid},), handle) # will call free on handle eventually
100118
throw(_UVError("could not spawn " * repr(cmd), err))
101119
end
102-
pp = Process(cmd, handle)
103-
associate_julia_struct(handle, pp)
104120
return pp
105121
end
106122

src/jl_uv.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ JL_DLLEXPORT void jl_uv_disassociate_julia_struct(uv_handle_t *handle)
285285
handle->data = NULL;
286286
}
287287

288-
#define UV_CLOSED 0x02 // UV_HANDLE_CLOSED on Windows (same value)
288+
#define UV_HANDLE_CLOSED 0x02
289289

290290
JL_DLLEXPORT int jl_spawn(char *name, char **argv,
291291
uv_loop_t *loop, uv_process_t *proc,
@@ -310,7 +310,7 @@ JL_DLLEXPORT int jl_spawn(char *name, char **argv,
310310
if (!(flags == UV_INHERIT_FD || flags == UV_INHERIT_STREAM || flags == UV_IGNORE)) {
311311
proc->type = UV_PROCESS;
312312
proc->loop = loop;
313-
proc->flags = UV_CLOSED;
313+
proc->flags = UV_HANDLE_CLOSED;
314314
return UV_EINVAL;
315315
}
316316
}

0 commit comments

Comments
 (0)