Ruby で Segmentation fault の発生を検知する

# 問題

Rails アプリケーションのとある処理で [Segmentation fault](https://ja.wikipedia.org/wiki/%E3%82%BB%E3%82%B0%E3%83%A1%E3%83%B3%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E9%81%95%E5%8F%8D) がランダムで発生してしまいます :cry: この場合、Rails アプリケーションのプロセスごと終了 (abort) してしまうため、エラーハンドリングができません。

# やりたいこと

Segmentation fault が発生する可能性がある処理だけ別プロセスで実行し、かつ Segmentation fault の発生を検知したいです :pray:

# バージョン情報

```shell-session
$ gcc -v
Apple clang version 14.0.0 (clang-1400.0.29.202)
Target: arm64-apple-darwin22.3.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

$ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin22]
```

# 方法

まず Ruby で Segmentation fault を再現させるためのコードを用意します。実行すると必ず Segmentation fault を発生させる C 言語のファイルを用意して、それをもとに共有ライブラリを作成してから Ruby の [fiddle](https://docs.ruby-lang.org/ja/latest/library/fiddle.html) で実行します。

```produce_segmentation_fault.c
#include <stdlib.h>

void produce_segmentation_fault() {
    int *p = NULL;
    *p = 1; // raise segmentation fault
}
```

```shell-session
$ gcc -shared -o produce_segmentation_fault.so produce_segmentation_fault.c
```

```rb
require 'fiddle'

handler = Fiddle.dlopen('./produce_segmentation_fault.so')
produce_segmentation_fault = Fiddle::Function.new(handler['produce_segmentation_fault'], [], Fiddle::TYPE_VOID)
produce_segmentation_fault.call # Segmentation fault が発生して、Ruby プロセスが終了する。
```

次に fork メソッドで子プロセスを生成し、そこで特定の処理を実行する手段を用意します。このとき、専用の IO オブジェクトを用意して、子プロセスで処理を実行した際の結果を親プロセスで受け取れるようにしています。ただし、オブジェクトのシリアライズ・デシリアライズに [Marshal.dump](https://docs.ruby-lang.org/ja/latest/method/Marshal/m/dump.html), [Marshal.#load](https://docs.ruby-lang.org/ja/latest/method/Marshal/m/load.html) メソッドを使っているため、Proc オブジェクトなどシリアライズできないオブジェクトは受け渡しできないことにご注意ください。なお、この実装は以下の記事を参考にしました。

- [ruby - Returning data from forked processes - Stack Overflow](https://stackoverflow.com/questions/1076257/returning-data-from-forked-processes)

```rb
class ChildProcessError < StandardError; end

def execute_in_child_process
  read_io, write_io = IO.pipe

  pid = fork do
    read_io.close
    result = yield
    Marshal.dump(result, write_io)
    exit!(0)
  end

  write_io.close
  result = read_io.read
  Process.wait(pid)
  raise(ChildProcessError) if result.empty?
  Marshal.load(result)
end
```

以上を組み合わせると、

- 子プロセスで特定の処理を実行して結果を受け取る。
- 子プロセスで Segmentation fault が発生してプロセスが終了した場合に、親プロセスでそれを検知する。

ということができます :muscle::sparkles:

```rb
require 'fiddle'

handler = Fiddle.dlopen('./produce_segmentation_fault.so')
produce_segmentation_fault = Fiddle::Function.new(handler['produce_segmentation_fault'], [], Fiddle::TYPE_VOID)

execute_in_child_process { 1 + 1 }
#=> 2
execute_in_child_process { produce_segmentation_fault.call }
# ChildProcessError
```

# 参考

- [ruby - Returning data from forked processes - Stack Overflow](https://stackoverflow.com/questions/1076257/returning-data-from-forked-processes)
- [library fiddle (Ruby 3.2 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/library/fiddle.html)
- [IO.pipe (Ruby 3.2 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/method/IO/s/pipe.html)
- [Kernel.#fork (Ruby 3.2 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/method/Kernel/m/fork.html)