qhwa

qhwa

Hi, all,

I just published Formular package. It is a tiny library that evaluates a piece of Elixir code.

Online documentation

On the shoulder of Elixir’s Code module

Given a piece of Elixir code (as a string, or AST), Formular runs it with Elixir’s Code module under some security limitations.

So far, the limitations are:

  • No calling module functions;
  • No calling exit;
  • No sending messages.

Indeed, the whole library consists of only one thin module, thanks to the power Elixir has already shipped out of the box.

Motivation

Formular was developed to support some dynamic configuration scenarios. For example, in a scene of an online book store, the discount of a book can be dynamically configured as a piece of code, then evaluated by Formular:

iex> discount_formula = ~s"
...>   case order do
...>     # old books get a big promotion
...>     %{book: %{year: year}} when year < 2000 ->
...>       0.5
...>   
...>     %{book: %{tags: tags}} ->
...>       # Elixir books!
...>       if ~s{elixir} in tags do
...>         0.9
...>       else
...>         1.0
...>       end
...>
...>     _ ->
...>       1.0
...>   end
...> "
...>
...> book_order = %{
...>   book: %{
...>     title: "Elixir in Action", year: 2019, tags: ["elixir"]
...>   }
...> }
...>
...> Formular.eval(discount_formula, [order: book_order])
{:ok, 0.9}

In such a way, the discount calculation code, which changes frequently, is separated away from the stable business flow, and the primary code is probably more generic and flexible.

I’ve been using it in production for a while so I publish it today in case others may find it useful too.

Cheers!

Showing Posts 1 to 10

mat-hek

mat-hek

Membrane Core Team

Nice, though I’m curious if you tried using Sand?

hauleth

hauleth

It is very unsafe implementation. Super simple example of how you can run arbitrary code with it:

Formular.eval(~S{
  import Kernel
  apply(IO, :puts, ["Hi"])
}, [])

And as soon as you have access to apply/3 (or any of the spawn_*/3 family) then you can run any code you want. In general as soon as you have access to import then you can do anything, and you do not prevent import in any way (it is imported by default as it is part of Kernel.SpecialForms).

If you want something like that, it is better to use any embedded language that is distinct from the Elixir and give it access only to needed primitives. You can take a look on Luerl or Erlog for example.

mat-hek

mat-hek

Membrane Core Team

I suppose it wasn’t meant to be safe, meaning resistant to malicious input, but rather to impose some restrictions on the code that’s changing frequently to limit its impact on the system. Although I have doubts if that’s the correct approach, that’s why I mentioned Sand, that aims to be an actual sandbox.

qhwa

qhwa OP

Nice! I forgot that import is in Kernel.SpecialForms. It could be prevented after parsing.

qhwa

qhwa OP

Yes, the purpose was to separate complex configurations from code. Thanks for sharing Sand. I’ll give it a try!

qhwa

qhwa OP

Thanks for pointing out Luerl and Erlong which are very solid and good references. However, what I want to achieve is to compile the config into Elixir code which can be sent to and used in some Elixir applications.

Ideally, there can be a service with some UI to manage the configuration rules. On update of any rule, the change is synchronized to some services who are interested in the config. The configuration would be compiled into BEAM code so that it can be directly called in the code.

Lua, or Prolog also works in such scenario, but I prefer Elixir because:

a) Elixir has a more friendly syntax IMHO (personal taste?)
b) I think compiling to BEAM code instead of running in a sandbox is more performant. But I haven’t benchmarked it yet. Will try to see how different approaches work.

I built a configuration management system in Elixir years ago but the data format was a little lispy formatted JSON. It worked very well but I think compiling rules to Elixir code would be more fun! :smiley:

qhwa

qhwa OP

Forumlar 0.2.1 released

  • import and require are now disallowed in the code. Thank @hauleth for pointing it out. :slight_smile:
hauleth

hauleth

DoS (atom exhaustion):

Formular.eval(~S|for a <- %Range{first: 0, last: 100_000, step: 1}, do: :"#{a}"|, [])

I needed to create range manually, as you do not export ../2 operator.

qhwa

qhwa OP

I played with Sand as @mat-hek shared and it does what I wanted. Not implying by the name, under the scene, Sand runs the code with Code.eval_quoted/3 too. Only in a separated process which can be limited in reductions & memory usage. I think that is the right way to go.

Also, I did some benchmarks, and the result was surprising at first glance.

Code:

code = """
  squares = %{3 => 9, 4 => 16, 5 => 25}
  squares[3]
"""

ast = Code.string_to_quoted!(code)

Benchee.run(%{
  eval: fn -> {:ok, 9} = Formular.eval(code, []) end,
  eval_ast: fn -> {:ok, 9} = Formular.eval(ast, []) end,
  sand_run: fn -> {:ok, 9, _} = Sand.run(code) end,
  sand_run_without_cpu_memory_monitoring: fn ->
    {9, _} = Sand.run_without_cpu_memory_monitoring(code)
  end
})

(Sand doesn’t accept AST at this moment)

Result:

Name                                             ips        average  deviation         median         99th %
sand_run_without_cpu_memory_monitoring       23.91 K       41.82 μs    ±20.35%       39.04 μs       70.77 μs
sand_run                                      3.50 K      285.99 μs    ±28.04%      269.01 μs      509.55 μs
eval_ast                                      3.35 K      298.88 μs     ±9.51%      293.91 μs      437.12 μs
eval                                          3.09 K      323.70 μs     ±9.67%      317.00 μs      467.09 μs

Comparison: 
sand_run_without_cpu_memory_monitoring       23.91 K
sand_run                                      3.50 K - 6.84x slower +244.18 μs
eval_ast                                      3.35 K - 7.15x slower +257.06 μs
eval                                          3.09 K - 7.74x slower +281.88 μs

I figured out the reason after some research and it is very interesting. I’m excited about the work ahead.

Thank you @hauleth , really appreciate it. I know the next direction now.

collegeimprovements

collegeimprovements

We use it in production now :slight_smile:
Thanks a lot for this package. We replaced Expreso with Formular and so far it’s been great.

Where Next? Top

Trending in Announcing Top

wojtekmach
Hey everyone! Req is an HTTP client for Elixir that I’ve been working on for quite some time. There is already a lot of HTTP clients out...
New
handnot2
Samly can be used to enable SAML 2.0 Single Sign On in a Plug/Phoenix application. This library uses Erlang esaml to provide plug enabl...
New
woylie
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries. offset-based pagination with...
New
restlessronin
The repo is at GitHub - cyberchitta/openai_ex: Community maintained Elixir library for OpenAI API · GitHub. Docs are at OpenaiEx User Gu...
152 11030 135
New
shahryarjb
The Chelekom project is a library of Phoenix and LiveView components generated via Mix tasks to fit developer needs seamlessly. One of i...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
fuelen
Hi all! I want to present a small library which provides a mix task for generating an Entity-Relationship Diagram for Ecto schemas. You...
New

Other Trending Topics Top

mudasobwa
I am seeing a lot of aplications of Argumentum ad Vericundiam in software discussions. They do link some piece of writing and point us to...
New
bartblast
Hey folks, I just published a post about Hologram’s funding and where the project goes next - the short version: Curiosum as Main Spons...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New
Herve37
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using. We’re particularly inte...
New
sorenone
Today we’re releasing Oban for Python. Not an Oban client in Python. Not a pythonx wrapper embedded in Elixir. Nope, it’s a fully operati...
New
lawik
I was thinking since Goatmire Elixir turned out pretty good I should maybe do another one. 30th of Sep - 2nd of Oct this year./ The firs...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews