Skip to content

Commit db74dc0

Browse files
vchuravyNHDalyKeno
authored andcommitted
Preserve the scope across the exception handler (JuliaLang#60647)
We store the previous scope in the eh_state and thus hide it from GC. This means we need to manually preserve that scope across the `try ... catch`, instead fo the new scope that we switch to. --------- Co-authored-by: Nathan Daly <[email protected]> Co-authored-by: Keno Fischer <[email protected]>
1 parent 254bc2b commit db74dc0

File tree

6 files changed

+94
-11
lines changed

6 files changed

+94
-11
lines changed

src/codegen.cpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6159,6 +6159,7 @@ static void emit_stmtpos(jl_codectx_t &ctx, jl_value_t *expr, int ssaval_result)
61596159
Value *scope_ptr = get_scope_field(ctx);
61606160
jl_aliasinfo_t::fromTBAA(ctx, ctx.tbaa().tbaa_gcframe).decorateInst(
61616161
ctx.builder.CreateAlignedStore(scope_to_restore, scope_ptr, ctx.types().alignof_ptr));
6162+
// NOTE: wb not needed here, due to store to current_task (see jl_gc_wb_current_task)
61626163
}
61636164
}
61646165
else if (head == jl_pop_exception_sym) {
@@ -9334,12 +9335,12 @@ static jl_llvm_functions_t
93349335
Value *scope_ptr = get_scope_field(ctx);
93359336
LoadInst *current_scope = ctx.builder.CreateAlignedLoad(ctx.types().T_prjlvalue, scope_ptr, ctx.types().alignof_ptr);
93369337
StoreInst *scope_store = ctx.builder.CreateAlignedStore(scope_boxed, scope_ptr, ctx.types().alignof_ptr);
9338+
// NOTE: wb not needed here, due to store to current_task (see jl_gc_wb_current_task)
93379339
jl_aliasinfo_t::fromTBAA(ctx, ctx.tbaa().tbaa_gcframe).decorateInst(current_scope);
93389340
jl_aliasinfo_t::fromTBAA(ctx, ctx.tbaa().tbaa_gcframe).decorateInst(scope_store);
9339-
// GC preserve the scope, since it is not rooted in the `jl_handler_t *`
9340-
// and may be removed from jl_current_task by any nested block and then
9341-
// replaced later
9342-
Value *scope_token = ctx.builder.CreateCall(prepare_call(gc_preserve_begin_func), {scope_boxed});
9341+
// GC preserve the current_scope, since it is not rooted in the `jl_handler_t *`,
9342+
// the newly entered scope is preserved through the current_task.
9343+
Value *scope_token = ctx.builder.CreateCall(prepare_call(gc_preserve_begin_func), {current_scope});
93439344
ctx.scope_restore[cursor] = std::make_pair(scope_token, current_scope);
93449345
}
93459346
}

src/gc-interface.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,14 @@ STATIC_INLINE void jl_gc_wb(const void *parent, const void *ptr) JL_NOTSAFEPOINT
240240
// so write barriers can be omitted until the next allocation. This function is a no-op that
241241
// can be used to annotate that a write barrier would be required were it not for this property
242242
// (as opposed to somebody just having forgotten to think about write barriers).
243-
STATIC_INLINE void jl_gc_wb_fresh(const void *parent, const void *ptr) JL_NOTSAFEPOINT {}
243+
STATIC_INLINE void jl_gc_wb_fresh(const void *parent JL_UNUSED, const void *ptr JL_UNUSED) JL_NOTSAFEPOINT {}
244+
// As an optimization, the current_task is explicitly added to the remset while it is running.
245+
// Upon deschedule, we conservatively move the write barrier into the young generation.
246+
// This allows the omission of write barriers for all GC roots on the current task stack (JL_GC_PUSH_*),
247+
// as well as the Task's explicit fields (but only for the current task).
248+
// This function is a no-op that can be used to annotate that a write barrier would be required were
249+
// it not for this property (as opposed to somebody just having forgotten to think about write barriers).
250+
STATIC_INLINE void jl_gc_wb_current_task(const void *parent JL_UNUSED, const void *ptr JL_UNUSED) JL_NOTSAFEPOINT {}
244251
// Used to annotate that a write barrier would be required, but may be omitted because `ptr`
245252
// is known to be an old object.
246253
STATIC_INLINE void jl_gc_wb_knownold(const void *parent, const void *ptr) JL_NOTSAFEPOINT {}

src/interpreter.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -539,12 +539,12 @@ static jl_value_t *eval_body(jl_array_t *stmts, interpreter_state *s, size_t ip,
539539
}
540540
s->locals[jl_source_nslots(s->src) + ip] = jl_box_ulong(jl_excstack_state(ct));
541541
if (jl_enternode_scope(stmt)) {
542-
jl_value_t *scope = eval_value(jl_enternode_scope(stmt), s);
543-
// GC preserve the scope, since it is not rooted in the `jl_handler_t *`
544-
// and may be removed from jl_current_task by any nested block and then
545-
// replaced later
546-
JL_GC_PUSH1(&scope);
547-
ct->scope = scope;
542+
jl_value_t *old_scope = ct->scope; // Identical to __eh.scope
543+
// GC preserve the old_scope, since it is not rooted in the `jl_handler_t *`,
544+
// the newly entered scope is preserved through the current_task.
545+
JL_GC_PUSH1(&old_scope);
546+
ct->scope = eval_value(jl_enternode_scope(stmt), s);
547+
jl_gc_wb_current_task(ct, ct->scope);
548548
if (!jl_setjmp(__eh.eh_ctx, 0)) {
549549
ct->eh = &__eh;
550550
eval_body(stmts, s, next_ip, toplevel);

src/rtutils.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ JL_DLLEXPORT void jl_eh_restore_state(jl_task_t *ct, jl_handler_t *eh)
296296
ct->eh = eh->prev;
297297
ct->gcstack = eh->gcstack;
298298
ct->scope = eh->scope;
299+
jl_gc_wb_current_task(ct, ct->scope);
299300
small_arraylist_t *locks = &ptls->locks;
300301
int unlocks = locks->len > eh->locks_len;
301302
if (unlocks) {
@@ -335,6 +336,7 @@ JL_DLLEXPORT void jl_eh_restore_state_noexcept(jl_task_t *ct, jl_handler_t *eh)
335336
{
336337
assert(ct->gcstack == eh->gcstack && "Incorrect GC usage under try catch");
337338
ct->scope = eh->scope;
339+
jl_gc_wb_current_task(ct, ct->scope);
338340
ct->eh = eh->prev;
339341
ct->ptls->defer_signal = eh->defer_signal; // optional, but certain try-finally (in stream.jl) may be slightly harder to write without this
340342
}

src/task.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,7 @@ JL_DLLEXPORT jl_task_t *jl_new_task(jl_function_t *start, jl_value_t *completion
11081108
jl_atomic_store_relaxed(&t->_isexception, 0);
11091109
// Inherit scope from parent task
11101110
t->scope = ct->scope;
1111+
jl_gc_wb_fresh(t, t->scope);
11111112
// Fork task-local random state from parent
11121113
jl_rng_split(t->rngState, ct->rngState);
11131114
// there is no active exception handler available on this stack yet
@@ -1573,6 +1574,7 @@ jl_task_t *jl_init_root_task(jl_ptls_t ptls, void *stack_lo, void *stack_hi)
15731574
ct->donenotify = jl_nothing;
15741575
jl_atomic_store_relaxed(&ct->_isexception, 0);
15751576
ct->scope = jl_nothing;
1577+
jl_gc_wb_knownold(ct, ct->scope);
15761578
ct->eh = NULL;
15771579
ct->gcstack = NULL;
15781580
ct->excstack = NULL;

test/scopedvalues.jl

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,74 @@ nothrow_scope()
195195
push!(ts, 2)
196196
end
197197
end
198+
199+
# LazyScopedValue
200+
global lsv_ncalled = 0
201+
const lsv = LazyScopedValue{Int}(OncePerProcess(() -> (global lsv_ncalled; lsv_ncalled += 1; 1)))
202+
@testset "LazyScopedValue" begin
203+
@test (@with lsv=>2 lsv[]) == 2
204+
@test lsv_ncalled == 0
205+
@test lsv[] == 1
206+
@test lsv_ncalled == 1
207+
@test lsv[] == 1
208+
@test lsv_ncalled == 1
209+
end
210+
211+
@testset "ScopedThunk" begin
212+
function check_svals()
213+
@test sval[] == 8
214+
@test sval_float[] == 8.0
215+
end
216+
sf = nothing
217+
@with sval=>8 sval_float=>8.0 begin
218+
sf = ScopedThunk(check_svals)
219+
end
220+
sf()
221+
@with sval=>8 sval_float=>8.0 begin
222+
sf2 = ScopedThunk{Function}(check_svals)
223+
end
224+
sf2()
225+
end
226+
227+
using Base.ScopedValues: ScopedValue, with
228+
@noinline function test_59483()
229+
sv = ScopedValue([])
230+
ch = Channel{Bool}()
231+
232+
# Spawn a child task, which inherits the parent's Scope
233+
@noinline function inner_function()
234+
# Block until the parent task has left the scope.
235+
take!(ch)
236+
# Now, per issue 59483, this task's scope is not rooted, except by the task itself.
237+
238+
# Now switch to an inner scope, leaving the current scope possibly unrooted.
239+
val = with(sv=>Any[2]) do
240+
# Inside this new scope, when we perform GC, the parent scope can be freed.
241+
# The fix for this issue made sure that the first scope in this task remains
242+
# rooted.
243+
GC.gc()
244+
GC.gc()
245+
sv[]
246+
end
247+
@test val == Any[2]
248+
# Finally, we've returned to the original scope, but that could be a dangling
249+
# pointer if the scope itself was freed by the above GCs. So these GCs could crash:
250+
GC.gc()
251+
GC.gc()
252+
end
253+
@noinline function spawn_inner()
254+
# Set a new Scope in the parent task - this is the scope that could be freed.
255+
with(sv=>Any[1]) do
256+
return @async inner_function()
257+
end
258+
end
259+
260+
# RUN THE TEST:
261+
t = spawn_inner()
262+
# Exit the scope, and let the child task proceed
263+
put!(ch, true)
264+
wait(t)
265+
end
266+
@testset "issue 59483" begin
267+
test_59483()
268+
end

0 commit comments

Comments
 (0)