crystal
crystal copied to clipboard
`Box(T).unbox` crashes if `T` is a nilable reference type
class Foo; end
x = nil.as(Foo?)
Box(Foo?).unbox(Box.box(x)) # Invalid memory access (signal 11) at address 0x8
Box.box
casts its argument to Void*
directly if it is reference-like, via the Reference?
parameter restriction. Box(T).unbox
however uses the check T <= Reference || T == Nil
, which fails if T
is a nilable reference type, so the boxed value is treated like a box for a value type instead. The correct check is T <= Reference?
, but this is currently disallowed:
In src/box.cr:30:16
30 | {% if T <= Reference? %}
^--------
Error: can't use Reference as a generic type argument yet, use a more specific type
Nothing actually stops us from permitting Reference
here, as long as we ensure that unions of direct subclasses of Reference
do not merge into Reference
(the way it is now); the same goes for runtime checks, i.e. x.is_a?(Reference?)
. Without this, the workaround would be not using the Void*
cast for nilable references.
Related: #11833
Expanding the subtyping relationship manually, we could probably write T.union_types.all? { |t| t <= Reference || t <= Nil }
.
This can also occur with a union of a value and nil.
x = nil.as(Int32?)
Box(Int32?).unbox(Box.box(x))
@icy-arctic-fox 🤔 Box is meant for reference types... do you have a use-case for a union of int and nil? Or was it an accident?
Ah, nevermind, maybe there's a use case, I don't know... at least there's a spec for values.
My actual use case was a union of a struct and nil. But this issue applies to reference and values types when they are unioned with nil. An example use case of using Int32?
would be for a config file that can have the value omitted.
Int32
breaks because there is a dynamic dispatch between .box(Reference?)
and .box(_)
, so a call like Box.box(1 if rand > 0.5)
actually calls either Box(Int32).box(object)
or Box(Nil).box(r : Reference?)
. There should be no overloading here