crystal icon indicating copy to clipboard operation
crystal copied to clipboard

`Box(T).unbox` crashes if `T` is a nilable reference type

Open HertzDevil opened this issue 3 years ago • 5 comments

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

HertzDevil avatar Feb 17 '22 14:02 HertzDevil

Expanding the subtyping relationship manually, we could probably write T.union_types.all? { |t| t <= Reference || t <= Nil }.

HertzDevil avatar Feb 18 '22 13:02 HertzDevil

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 avatar Jul 19 '22 01:07 icy-arctic-fox

@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?

asterite avatar Jul 25 '22 12:07 asterite

Ah, nevermind, maybe there's a use case, I don't know... at least there's a spec for values.

asterite avatar Jul 25 '22 12:07 asterite

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.

icy-arctic-fox avatar Jul 25 '22 19:07 icy-arctic-fox

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

HertzDevil avatar Oct 16 '23 21:10 HertzDevil