How private are Erlang private functions?
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?
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.
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!