NIFs raise Segmentation Fault while loading function has try catch block to handle the exception

I am working on integrating a C++ api with Elixir, by using NIFs. Now whenever I load the NIF, I want to initialize some variables/data which will be persisted across NIF calls. I was trying to achieve the same by load function mentioned in the ERL_NIF_INIT call. But I am observing some weird behavior out of load function. Please have a look at the below mentioned example and then I’ll explain the issue further:

#include <erl_nif.h>

static ERL_NIF_TERM sample(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
  return enif_make_atom(env, "ok");
}

static ErlNifFunc nif_funcs[] = {{"sample", 1, sample}};

static int load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info) {
  try {
    1 / 0; // nif loads if this is the case
    int x = 1 / 0; // float point exception segmentation fault when used this line
  } catch (...) {
    return 1;
  }
  return 0;
}

ERL_NIF_INIT(Elixir.Sample, nif_funcs, load, NULL, NULL, NULL);

Now closely observe the load function containing the try/catch block, when I use 1 / 0; expression in the body of try block, NIF is loaded but when the expression 1 / 0; is replaced by int x = 1 / 0; erlang vm fails to load NIF and crashes with floating point exception segmentation fault even if I have added the try/catch block to handle the exception. Can someone explain this to me? I really need help here.

Here is the elixir code used for interfacing:

defmodule Sample do
  @on_load :load_nifs

  def load_nifs do
    :ok =
      :sample
      |> :code.priv_dir()
      |> Path.join("sample")
      |> :erlang.load_nif("0.0.1")
  end

  def sample(_) do
    raise "sample/1 Function not implemented"
  end
end

Command used to compile NIF g++ -fPIC -shared sample.cpp -o sample.so

P.S. - Thanks for your time to read this, I really appreciate it.

C++ does not have an exception for a divide by zero, rather that is Undefined Behaviour, meaning the C++ compiler is allowed to do whatever it wants from give you back invalid data to crashing to wiping your entire system, everything is allowed, and a try/catch cannot stop it. This is why untrusted input to division should always be checked for zero first in C/C++.

EDIT: If you’re curious the spec defines it as UB in section 5.6.4 of the C++ standard (at least in the version I have here).

3 Likes

The word “exception” in that message refers to a hardware exception, not the C++ kind.

In fact, modern compilers don’t generate any code at all for the catch section in your example because they can determine it will never execute:

Note that if you switch to the 1 / 0; version, the compiler omits the division entirely because it sees the result is completely unused. TBH I’m not sure why assigning the value to a local variable (that immediately goes out-of-scope) doesn’t do this too.

1 Like

Because Undefined Behavior allows for anything, and most of they time they screw with the optimizers in weird ways.

2 Likes