How private are Erlang private functions?

Roberto Aloi
About Erlang
Published in
3 min readMay 18, 2018

Today a tweet on my Twitter timeline caught my attention:

Private functions in Erlang are private, for Science’s sake! It’s not like in Python where you add a couple of underscores here and there and suddenly, et voilà, what two seconds ago was private is now public. In Erlang we had private functions in a safe for 25 years! There is no way someone can call a function that we declare as private! Or… Is there a way?

I’ll show you

It turns out that there is a not-so-straightforward way to call a private function in Erlang, assuming the code has been compiled with debug_info enabled. Let’s see how things work.

Let’s start by defining a module named test. The module contains two functions, of which only one is exported:

-module(test).-export([public/0]).public() ->
i_am_public.
private() ->
i_am_private.

Let’s compile the module, ensuring we include debug information. Notice how including debug information is fairly common to do in Erlang, since many tools including debuggers, cross-reference tools and cover analysis tools require these information to work:

$ erlc +debug_info test.erl
test.erl:8: Warning: function private/0 is unused

The compiler informs us that the private function is not accessible, which is expected. Let’s now open a shell and try to invoke both functions:

$ erl
Erlang/OTP 19 [erts-8.3.5.3] [...]
Eshell V8.3.5.3 (abort with ^G)
1> l(test).
{module,test}
2> test:public.
i_am_public
3> test:private().
** exception error: undefined function test:private/0

We can invoke the public function, but not the private one. Nothing new. Let’s now define a small anonymous function (I will explain what it does in a second) and let’s bind it to the Open variable:

4> Open = fun(Module) ->
Which = code:which(Module),
{ok,{_,[{_,{_,A}}]}} = beam_lib:chunks(Module, [abstract_code]),
{ok, Module, Binary} = compile:forms(A, [export_all]),
code:load_binary(Module, Which, Binary)
end.
#Fun<erl_eval.6.118419387>

Let’s then invoke the Open function, passing the test module as an argument:

5> Open(test).
{module, test}

Let’s now try to access the private function:

6> test:private().
i_am_private.

The safe is now wide open.

My gosh.

So, what kind of black magic lays behind the Open function? Not much, actually. Let’s look at it again:

fun(Module) ->
Which = code:which(Module),
{ok,{_,[{_,{_,A}}]}} = beam_lib:chunks(Module, [abstract_code]),
{ok, Module, Binary} = compile:forms(A, [export_all]),
code:load_binary(Module, Which, Binary)
end.

After asking the code server for the absolute filename for the test module, we use the powerful beam_lib interface to extract the abstract syntax forms from the debug information contained in the test beam (remember we compiled it with the debug_info option)? We then recompile the module starting from those forms and by adding the notorious export_all
option documented here, which causes all functions defined in the module to be exported. We reload the new version of the module and we’re done.

There are some corner case that, for simplicity, the Open function does not take care of in the above example, but you should get the gist of it.

Happy hacking!

--

--