Crystal: segmentation fault using procs in crystal 0.35.1

Created on 19 Nov 2020  路  8Comments  路  Source: crystal-lang/crystal

Rather interestingly, deleting some of the code here (block for bug0, bug1) will cause the crash to disappear.

Crystal 0.35.1 [5999ae29b] (2020-06-19)

LLVM: 8.0.0
Default target: x86_64-unknown-linux-gnu

Fedora 33

require "option_parser"

thunk = nil
parser = OptionParser.new do |parser|
  parser.on("bug0", "") do
    parser.unknown_args do |args|
      thunk = ->do
        Process::Status.new(0)
      end
    end
  end

  parser.on("bug1", "") do
    parser.unknown_args do |args|
      thunk = ->do
      end
    end
  end

  parser.on("bug", "") do
    parser.unknown_args do |_, after_dash|
      thunk = ->do
        Process.exec("ls", after_dash)
      end
      puts thunk
      puts "never get here"
    end
  end
end

parser.parse
$ crystal run bug.cr -- bug -- hi
Invalid memory access (signal 11) at address 0x39a00000001
[0x4614e6] *Exception::CallStack::print_backtrace:Int32 +118
[0x4543c5] __crystal_sigfault_handler +341
bug

Most helpful comment

Further reduced:

puts typeof(true ? ->{ raise "" } : true ? ->{ 1 } : ->{ 1.0 }) # => Proc(Float64) | Proc(Int32)
puts true ? ->{ raise "" } : true ? ->{ 1 } : ->{ 1.0 }

It seems

  • Crystal considers Proc(*T, NoReturn) to be a subtype of Proc(*T, R) for any other R;
  • Proc(NoReturn) < Proc(Float64) and Proc(NoReturn) < Proc(Int32), therefore Proc(NoReturn) < Union(Proc(Float64), Proc(Int32));
  • Thus Crystal assumes a Proc(NoReturn) has the same binary layout as a Union(Proc(Float64), Proc(Int32)), which cannot be true because only the latter has a type ID.

(On a side note I thought Pointer(Void) would be subject to the same issues, but luckily that doesn't happen.)

All 8 comments

Umm if this contains Process.exec, it'd be more surprising if it didn't crash. Any way to exclude that?

Reduced:

def capture(&block)
  block
end

thunk = ->{ 1 }

a = capture do
  thunk = ->{ 1.0 }
end

thunk = ->{ raise "OH NO" }

puts thunk

No idea why it happens.

@asterite 's example for me always crashes at the specific address 0x23500000235, same on carc.in.
So weird.

Umm if this contains Process.exec, it'd be more surprising if it didn't crash. Any way to exclude that?

I rather thought the sometimes-NoReturn aspect of the Proc would be the most interesting part, but I guess not given @asterite's reduction

That 0x235 is 100% the typeid.

I reduced it a bit more (and changed the snippet). Yeah, I'm almost sure that number is a type ID or something.

Further reduced:

puts typeof(true ? ->{ raise "" } : true ? ->{ 1 } : ->{ 1.0 }) # => Proc(Float64) | Proc(Int32)
puts true ? ->{ raise "" } : true ? ->{ 1 } : ->{ 1.0 }

It seems

  • Crystal considers Proc(*T, NoReturn) to be a subtype of Proc(*T, R) for any other R;
  • Proc(NoReturn) < Proc(Float64) and Proc(NoReturn) < Proc(Int32), therefore Proc(NoReturn) < Union(Proc(Float64), Proc(Int32));
  • Thus Crystal assumes a Proc(NoReturn) has the same binary layout as a Union(Proc(Float64), Proc(Int32)), which cannot be true because only the latter has a type ID.

(On a side note I thought Pointer(Void) would be subject to the same issues, but luckily that doesn't happen.)

@HertzDevil Thanks for the explanation! I think I know how to fix it, then. We shouldn't be forming unions here. Instead, one should be able to pass a Proc that returns NoReturn to something that expect a Proc that returns T (with the same argument types). We already do something similar for Procs that returns Nil.

I'll fix this soon.

Was this page helpful?
0 / 5 - 0 ratings