Skip to content

Commit 1307803

Browse files
NHDalyvchuravyKenovtjnashJeffBezanson
authored andcommitted
Fix GC corruption via two upstream PRs. (#272)
* 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]> * add wb_back on all task switch paths (JuliaLang#60617) Since this task's stack or scope field could have been modified after it was marked by an incremental collection (and not just for copy stacks), move the barrier back unconditionally here. --------- Co-authored-by: Valentin Churavy <[email protected]> Co-authored-by: Jeff Bezanson <[email protected]> --------- Co-authored-by: Valentin Churavy <[email protected]> Co-authored-by: Keno Fischer <[email protected]> Co-authored-by: Jameson Nash <[email protected]> Co-authored-by: Jeff Bezanson <[email protected]>
1 parent f8a775b commit 1307803

File tree

6 files changed

+100
-15
lines changed

6 files changed

+100
-15
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: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,6 @@ static void NOINLINE save_stack(jl_ptls_t ptls, jl_task_t *lastt, jl_task_t **pt
203203
lastt->ctx.copy_stack = nb;
204204
lastt->sticky = 1;
205205
memcpy_stack_a16((uint64_t*)buf, (uint64_t*)frame_addr, nb);
206-
// this task's stack could have been modified after
207-
// it was marked by an incremental collection
208-
// move the barrier back instead of walking it again here
209-
jl_gc_wb_back(lastt);
210206
}
211207

212208
JL_NO_ASAN static void NOINLINE JL_NORETURN restore_stack(jl_ucontext_t *t, jl_ptls_t ptls, char *p)
@@ -504,6 +500,12 @@ JL_NO_ASAN static void ctx_switch(jl_task_t *lastt)
504500
lastt->ctx.ctx = &lasttstate.ctx;
505501
}
506502
}
503+
// this task's stack or scope field could have been modified after
504+
// it was marked by an incremental collection
505+
// move the barrier back instead of walking the shadow stack again here to check if that is required
506+
// even if killed (dropping the stack) and just the scope field matters,
507+
// let the gc figure that out next time it does a quick mark
508+
jl_gc_wb_back(lastt);
507509

508510
// set up global state for new task and clear global state for old task
509511
t->ptls = ptls;
@@ -1108,6 +1110,7 @@ JL_DLLEXPORT jl_task_t *jl_new_task(jl_function_t *start, jl_value_t *completion
11081110
jl_atomic_store_relaxed(&t->_isexception, 0);
11091111
// Inherit scope from parent task
11101112
t->scope = ct->scope;
1113+
jl_gc_wb_fresh(t, t->scope);
11111114
// Fork task-local random state from parent
11121115
jl_rng_split(t->rngState, ct->rngState);
11131116
// there is no active exception handler available on this stack yet
@@ -1573,6 +1576,7 @@ jl_task_t *jl_init_root_task(jl_ptls_t ptls, void *stack_lo, void *stack_hi)
15731576
ct->donenotify = jl_nothing;
15741577
jl_atomic_store_relaxed(&ct->_isexception, 0);
15751578
ct->scope = jl_nothing;
1579+
jl_gc_wb_knownold(ct, ct->scope);
15761580
ct->eh = NULL;
15771581
ct->gcstack = NULL;
15781582
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)