Skip to content

Commit

Permalink
Properly guard UpsilonNode unboxed store
Browse files Browse the repository at this point in the history
In #51852, we are coercing a boxed `Union{@NamedTuple{progress::String}, @NamedTuple{progress::Float64}}`
to `@NamedTuple{progress::String}` via convert_julia_type. This results in a jl_cgval_t that has
a Vboxed that points to a boxed `@NamedTuple{progress::Float64}` but with a `@NamedTuple{progress::String}`
type tag that the up upsilonnode code then tries to unbox into the typed PhiC slot. This ends up treating
the Float64 as a pointer and crashing in GC. Avoid this by adding a runtime check that the converted value
is actually compatible (we already had this kind of check for the non-boxed cases) and then making the
unboxing runtime-conditional on the type of the union matching the type of the phic.

Fixes #51852
  • Loading branch information
Keno committed Nov 1, 2023
1 parent 072896d commit 47ed081
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 84 deletions.
82 changes: 43 additions & 39 deletions base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2230,31 +2230,31 @@ end

function abstract_eval_special_value(interp::AbstractInterpreter, @nospecialize(e), vtypes::Union{VarTable,Nothing}, sv::AbsIntState)
if isa(e, QuoteNode)
return Const(e.value)
return RTEffects(Const(e.value), EFFECTS_TOTAL)
elseif isa(e, SSAValue)
return abstract_eval_ssavalue(e, sv)
return RTEffects(abstract_eval_ssavalue(e, sv), EFFECTS_TOTAL)
elseif isa(e, SlotNumber)
effects = EFFECTS_THROWS
if vtypes !== nothing
vtyp = vtypes[slot_id(e)]
if vtyp.undef
merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; nothrow=false))
if !vtyp.undef
effects = EFFECTS_TOTAL
end
return vtyp.typ
return RTEffects(vtyp.typ, effects)
end
merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; nothrow=false))
return Any
return RTEffects(Any, effects)
elseif isa(e, Argument)
if vtypes !== nothing
return vtypes[slot_id(e)].typ
return RTEffects(vtypes[slot_id(e)].typ, EFFECTS_TOTAL)
else
@assert isa(sv, IRInterpretationState)
return sv.ir.argtypes[e.n] # TODO frame_argtypes(sv)[e.n] and remove the assertion
return RTEffects(sv.ir.argtypes[e.n], EFFECTS_TOTAL) # TODO frame_argtypes(sv)[e.n] and remove the assertion
end
elseif isa(e, GlobalRef)
return abstract_eval_globalref(interp, e, sv)
end

return Const(e)
return RTEffects(Const(e), EFFECTS_TOTAL)
end

function abstract_eval_value_expr(interp::AbstractInterpreter, e::Expr, vtypes::Union{VarTable,Nothing}, sv::AbsIntState)
Expand Down Expand Up @@ -2288,8 +2288,9 @@ function abstract_eval_value(interp::AbstractInterpreter, @nospecialize(e), vtyp
if isa(e, Expr)
return abstract_eval_value_expr(interp, e, vtypes, sv)
else
typ = abstract_eval_special_value(interp, e, vtypes, sv)
return collect_limitations!(typ, sv)
(;rt, effects) = abstract_eval_special_value(interp, e, vtypes, sv)
merge_effects!(interp, sv, effects)
return collect_limitations!(rt, sv)
end
end

Expand Down Expand Up @@ -2615,7 +2616,9 @@ function abstract_eval_phi(interp::AbstractInterpreter, phi::PhiNode, vtypes::Un
for i in 1:length(phi.values)
isassigned(phi.values, i) || continue
val = phi.values[i]
rt = tmerge(typeinf_lattice(interp), rt, abstract_eval_special_value(interp, val, vtypes, sv))
# N.B.: Phi arguments are restricted to not have effects, so we can drop
# them here safely.
rt = tmerge(typeinf_lattice(interp), rt, abstract_eval_special_value(interp, val, vtypes, sv).rt)
end
return rt
end
Expand All @@ -2631,53 +2634,55 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e),
add_curr_ssaflag!(sv, IR_FLAG_EFFECT_FREE | IR_FLAG_NOTHROW)
return abstract_eval_phi(interp, e, vtypes, sv)
end
return abstract_eval_special_value(interp, e, vtypes, sv)
end
(; rt, effects) = abstract_eval_statement_expr(interp, e, vtypes, sv)
if effects.noub === NOUB_IF_NOINBOUNDS
if !iszero(get_curr_ssaflag(sv) & IR_FLAG_INBOUNDS)
effects = Effects(effects; noub=ALWAYS_FALSE)
elseif !propagate_inbounds(sv)
# The callee read our inbounds flag, but unless we propagate inbounds,
# we ourselves don't read our parent's inbounds.
effects = Effects(effects; noub=ALWAYS_TRUE)
(; rt, effects) = abstract_eval_special_value(interp, e, vtypes, sv)
else
(; rt, effects) = abstract_eval_statement_expr(interp, e, vtypes, sv)
if effects.noub === NOUB_IF_NOINBOUNDS
if !iszero(get_curr_ssaflag(sv) & IR_FLAG_INBOUNDS)
effects = Effects(effects; noub=ALWAYS_FALSE)
elseif !propagate_inbounds(sv)
# The callee read our inbounds flag, but unless we propagate inbounds,
# we ourselves don't read our parent's inbounds.
effects = Effects(effects; noub=ALWAYS_TRUE)
end
end
e = e::Expr
@assert !isa(rt, TypeVar) "unhandled TypeVar"
rt = maybe_singleton_const(rt)
if !isempty(sv.pclimitations)
if rt isa Const || rt === Union{}
empty!(sv.pclimitations)
else
rt = LimitedAccuracy(rt, sv.pclimitations)
sv.pclimitations = IdSet{InferenceState}()
end
end
end
# N.B.: This only applies to the effects of the statement itself.
# It is possible for arguments (GlobalRef/:static_parameter) to throw,
# but these will be recomputed during SSA construction later.
set_curr_ssaflag!(sv, flags_for_effects(effects), IR_FLAGS_EFFECTS)
merge_effects!(interp, sv, effects)
e = e::Expr
@assert !isa(rt, TypeVar) "unhandled TypeVar"
rt = maybe_singleton_const(rt)
if !isempty(sv.pclimitations)
if rt isa Const || rt === Union{}
empty!(sv.pclimitations)
else
rt = LimitedAccuracy(rt, sv.pclimitations)
sv.pclimitations = IdSet{InferenceState}()
end
end

return rt
end

function isdefined_globalref(g::GlobalRef)
return ccall(:jl_globalref_boundp, Cint, (Any,), g) != 0
end

function abstract_eval_globalref(g::GlobalRef)
function abstract_eval_globalref_type(g::GlobalRef)
if isdefined_globalref(g) && isconst(g)
return Const(ccall(:jl_get_globalref_value, Any, (Any,), g))
end
ty = ccall(:jl_get_binding_type, Any, (Any, Any), g.mod, g.name)
ty === nothing && return Any
return ty
end
abstract_eval_global(M::Module, s::Symbol) = abstract_eval_globalref(GlobalRef(M, s))
abstract_eval_global(M::Module, s::Symbol) = abstract_eval_globalref_type(GlobalRef(M, s))

function abstract_eval_globalref(interp::AbstractInterpreter, g::GlobalRef, sv::AbsIntState)
rt = abstract_eval_globalref(g)
rt = abstract_eval_globalref_type(g)
consistent = inaccessiblememonly = ALWAYS_FALSE
nothrow = false
if isa(rt, Const)
Expand All @@ -2692,8 +2697,7 @@ function abstract_eval_globalref(interp::AbstractInterpreter, g::GlobalRef, sv::
consistent = inaccessiblememonly = ALWAYS_TRUE
rt = Union{}
end
merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; consistent, nothrow, inaccessiblememonly))
return rt
return RTEffects(rt, Effects(EFFECTS_TOTAL; consistent, nothrow, inaccessiblememonly))
end

function handle_global_assignment!(interp::AbstractInterpreter, frame::InferenceState, lhs::GlobalRef, @nospecialize(newty))
Expand Down
2 changes: 1 addition & 1 deletion base/compiler/optimize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ function argextype(
elseif isa(x, QuoteNode)
return Const(x.value)
elseif isa(x, GlobalRef)
return abstract_eval_globalref(x)
return abstract_eval_globalref_type(x)
elseif isa(x, PhiNode)
return Any
elseif isa(x, PiNode)
Expand Down
2 changes: 1 addition & 1 deletion base/compiler/ssair/slot2ssa.jl
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ function typ_for_val(@nospecialize(x), ci::CodeInfo, ir::IRCode, idx::Int, slott
end
return (ci.ssavaluetypes::Vector{Any})[idx]
end
isa(x, GlobalRef) && return abstract_eval_globalref(x)
isa(x, GlobalRef) && return abstract_eval_globalref_type(x)
isa(x, SSAValue) && return (ci.ssavaluetypes::Vector{Any})[x.id]
isa(x, Argument) && return slottypes[x.n]
isa(x, NewSSAValue) && return types(ir)[new_to_regular(x, length(ir.stmts))]
Expand Down
2 changes: 1 addition & 1 deletion base/compiler/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ function is_throw_call(e::Expr)
if e.head === :call
f = e.args[1]
if isa(f, GlobalRef)
ff = abstract_eval_globalref(f)
ff = abstract_eval_globalref_type(f)
if isa(ff, Const) && ff.val === Core.throw
return true
end
Expand Down
22 changes: 18 additions & 4 deletions src/cgutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,9 @@ static void null_pointer_check(jl_codectx_t &ctx, Value *v, Value **nullcheck =
template<typename Func>
static Value *emit_guarded_test(jl_codectx_t &ctx, Value *ifnot, Value *defval, Func &&func)
{
if (!ifnot) {
return func();
}
if (auto Cond = dyn_cast<ConstantInt>(ifnot)) {
if (Cond->isZero())
return defval;
Expand Down Expand Up @@ -1544,7 +1547,7 @@ static bool can_optimize_isa_union(jl_uniontype_t *type)
}

// a simple case of emit_isa that is obvious not to include a safe-point
static Value *emit_exactly_isa(jl_codectx_t &ctx, const jl_cgval_t &arg, jl_datatype_t *dt)
static Value *emit_exactly_isa(jl_codectx_t &ctx, const jl_cgval_t &arg, jl_datatype_t *dt, bool could_be_null=false)
{
assert(jl_is_concrete_type((jl_value_t*)dt));
if (arg.TIndex) {
Expand All @@ -1559,6 +1562,10 @@ static Value *emit_exactly_isa(jl_codectx_t &ctx, const jl_cgval_t &arg, jl_data
else if (arg.Vboxed) {
// test for (arg.TIndex == UNION_BOX_MARKER && typeof(arg.V) == type)
Value *isboxed = ctx.builder.CreateICmpEQ(arg.TIndex, ConstantInt::get(getInt8Ty(ctx.builder.getContext()), UNION_BOX_MARKER));
if (could_be_null) {
isboxed = ctx.builder.CreateAnd(isboxed,
ctx.builder.CreateNot(null_pointer_cmp(ctx, arg.Vboxed)));
}
setName(ctx.emission_context, isboxed, "isboxed");
BasicBlock *currBB = ctx.builder.GetInsertBlock();
BasicBlock *isaBB = BasicBlock::Create(ctx.builder.getContext(), "isa", ctx.f);
Expand All @@ -1579,9 +1586,16 @@ static Value *emit_exactly_isa(jl_codectx_t &ctx, const jl_cgval_t &arg, jl_data
return ConstantInt::get(getInt1Ty(ctx.builder.getContext()), 0);
}
}
auto isa = ctx.builder.CreateICmpEQ(emit_typeof(ctx, arg, false, true), emit_tagfrom(ctx, dt));
setName(ctx.emission_context, isa, "exactly_isa");
return isa;
Value *isnull = NULL;
if (could_be_null && arg.isboxed) {
isnull = null_pointer_cmp(ctx, arg.Vboxed);
}
Constant *Vfalse = ConstantInt::get(getInt1Ty(ctx.builder.getContext()), 0);
return emit_guarded_test(ctx, isnull, Vfalse, [&]{
auto isa = ctx.builder.CreateICmpEQ(emit_typeof(ctx, arg, false, true), emit_tagfrom(ctx, dt));
setName(ctx.emission_context, isa, "exactly_isa");
return isa;
});
}

static std::pair<Value*, bool> emit_isa(jl_codectx_t &ctx, const jl_cgval_t &x,
Expand Down
78 changes: 49 additions & 29 deletions src/codegen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1590,8 +1590,17 @@ struct jl_cgval_t {
// runtime values) if `(TIndex | UNION_BOX_MARKER) != 0`, then `Vboxed == V` (by value).
// For convenience, we also set this value of isboxed values, in which case
// it is equal (at compile time) to V.
// If this is non-NULL, it is always of type `T_prjlvalue`

// If this is non-NULL (at compile time), it is always of type `T_prjlvalue`.
// N.B.: In general we expect this to always be a dereferenceable pointer at runtime.
// However, there are situations where this value may be a runtime NULL
// (PhiNodes with undef predecessors or PhiC with undef UpsilonNode).
// The middle-end arranges appropriate error checks before any use
// of this value that may read a non-dereferenceable Vboxed, with two
// exceptions: PhiNode and UpsilonNode arguments which need special
// handling to account for the possibility that this may be NULL.
Value *Vboxed;

Value *TIndex; // if `V` is an unboxed (tagged) Union described by `typ`, this gives the DataType index (1-based, small int) as an i8
jl_value_t *constant; // constant value (rooted in linfo.def.roots)
jl_value_t *typ; // the original type of V, never NULL
Expand Down Expand Up @@ -2408,29 +2417,28 @@ static jl_cgval_t convert_julia_type(jl_codectx_t &ctx, const jl_cgval_t &v, jl_
return ghostValue(ctx, typ);
Value *new_tindex = NULL;
if (jl_is_concrete_type(typ)) {
if (v.TIndex && !jl_is_pointerfree(typ)) {
// discovered that this union-split type must actually be isboxed
if (v.Vboxed) {
return jl_cgval_t(v.Vboxed, true, typ, NULL, best_tbaa(ctx.tbaa(), typ));
}
else {
// type mismatch: there weren't any boxed values in the union
if (skip)
*skip = ConstantInt::get(getInt1Ty(ctx.builder.getContext()), 1);
else
CreateTrap(ctx.builder);
return jl_cgval_t();
}
}
if (jl_is_concrete_type(v.typ)) {
if (jl_is_concrete_type(typ)) {
// type mismatch: changing from one leaftype to another
if (skip)
*skip = ConstantInt::get(getInt1Ty(ctx.builder.getContext()), 1);
else
CreateTrap(ctx.builder);
return jl_cgval_t();
// type mismatch: changing from one leaftype to another
if (skip)
*skip = ConstantInt::get(getInt1Ty(ctx.builder.getContext()), 1);
else
CreateTrap(ctx.builder);
return jl_cgval_t();
}
bool mustbox_union = v.TIndex && !jl_is_pointerfree(typ);
if (v.Vboxed && (v.isboxed || mustbox_union)) {
if (skip) {
*skip = ctx.builder.CreateNot(emit_exactly_isa(ctx, v, (jl_datatype_t*)typ, true));
}
return jl_cgval_t(v.Vboxed, true, typ, NULL, best_tbaa(ctx.tbaa(), typ));
}
if (mustbox_union) {
// type mismatch: there weren't any boxed values in the union
if (skip)
*skip = ConstantInt::get(getInt1Ty(ctx.builder.getContext()), 1);
else
CreateTrap(ctx.builder);
return jl_cgval_t();
}
}
else {
Expand Down Expand Up @@ -5302,9 +5310,13 @@ static void emit_varinfo_assign(jl_codectx_t &ctx, jl_varinfo_t &vi, jl_cgval_t
// If allow_mismatch is set, type mismatches will not result in traps.
// This is used for upsilon nodes, where the destination can have a narrower
// type than the store, if inference determines that the store is never read.
Value *dummy = NULL;
Value **skip = allow_mismatch ? &dummy : NULL;
rval_info = convert_julia_type(ctx, rval_info, slot_type, skip);
Value *skip = NULL;
rval_info = convert_julia_type(ctx, rval_info, slot_type, &skip);
if (!allow_mismatch && skip) {
CreateTrap(ctx.builder);
return;
}

if (rval_info.typ == jl_bottom_type)
return;

Expand Down Expand Up @@ -5349,8 +5361,13 @@ static void emit_varinfo_assign(jl_codectx_t &ctx, jl_varinfo_t &vi, jl_cgval_t

// store unboxed variables
if (!vi.boxroot || (vi.pTIndex && rval_info.TIndex)) {
emit_vi_assignment_unboxed(ctx, vi, isboxed, rval_info);
emit_guarded_test(ctx, skip ? ctx.builder.CreateNot(skip) : nullptr, nullptr, [&]{
emit_vi_assignment_unboxed(ctx, vi, isboxed, rval_info);
return nullptr;
});
}

return;
}

static void emit_assignment(jl_codectx_t &ctx, jl_value_t *l, jl_value_t *r, ssize_t ssaval)
Expand All @@ -5362,7 +5379,8 @@ static void emit_assignment(jl_codectx_t &ctx, jl_value_t *l, jl_value_t *r, ssi
int sl = jl_slot_number(l) - 1;
// it's a local variable
jl_varinfo_t &vi = ctx.slots[sl];
return emit_varinfo_assign(ctx, vi, rval_info, l);
emit_varinfo_assign(ctx, vi, rval_info, l);
return;
}

jl_module_t *mod;
Expand Down Expand Up @@ -5393,15 +5411,17 @@ static void emit_upsilonnode(jl_codectx_t &ctx, ssize_t phic, jl_value_t *val)
// upsilon node is not dynamically observed.
if (val) {
jl_cgval_t rval_info = emit_expr(ctx, val);
if (rval_info.typ == jl_bottom_type)
if (rval_info.typ == jl_bottom_type) {
// as a special case, PhiC nodes are allowed to use undefined
// values, since they are just copy operations, so we need to
// ignore the store (it will not by dynamically observed), while
// normally, for any other operation result, we'd assume this store
// was unreachable and dead
val = NULL;
else
}
else {
emit_varinfo_assign(ctx, vi, rval_info, NULL, true);
}
}
if (!val) {
if (vi.boxroot) {
Expand Down
4 changes: 2 additions & 2 deletions stdlib/REPL/src/REPLCompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,9 @@ function CC.abstract_eval_globalref(interp::REPLInterpreter, g::GlobalRef,
sv::CC.InferenceState)
if (interp.limit_aggressive_inference ? is_repl_frame(sv) : is_call_graph_uncached(sv))
if CC.isdefined_globalref(g)
return Const(ccall(:jl_get_globalref_value, Any, (Any,), g))
return CC.RTEffects(Const(ccall(:jl_get_globalref_value, Any, (Any,), g)), CC.EFFECTS_TOTAL)
end
return Union{}
return CC.RTEffects(Union{}, CC.EFFECTS_THROWS)
end
return @invoke CC.abstract_eval_globalref(interp::CC.AbstractInterpreter, g::GlobalRef,
sv::CC.InferenceState)
Expand Down
2 changes: 2 additions & 0 deletions test/compiler/EscapeAnalysis/EscapeAnalysis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ end
let # try/catch
result = code_escapes((Any,)) do a
try
println("prevent ConstABI")
nothing
catch err
return a # return escape
Expand All @@ -210,6 +211,7 @@ end
end
let result = code_escapes((Any,)) do a
try
println("prevent ConstABI")
nothing
finally
return a # return escape
Expand Down
Loading

0 comments on commit 47ed081

Please sign in to comment.