razuf

razuf

Hi everybody,

I found a bug or structural problem in Decimal what could have some big impact:
wrong calculation (maybe with money!!!) or even infinite loop ;-(

My suggestion for a fix is on pull request:
https://github.com/ericmj/decimal/pull/157#issue-436023781

Does anybody have another idea - there are some possible ways to handle validation for structs - but what would you suggest is the best way?
Any suggestions are welcome! :wink:

Here are one solution example for division:

defmodule Decimal do
  @type coefficient :: non_neg_integer | :NaN | :inf
  @type exponent :: integer
  @type sign :: 1 | -1

  @type t :: %__MODULE__{
          sign: sign,
          coef: coefficient,
          exp: exponent
        }
  defstruct sign: 1, coef: 0, exp: 0

...

  def div(%Decimal{coef: coef1}, %Decimal{})
      when coef1 < 0,
      do: error(:invalid_operation, "dividend (#{coef1}) must be > 0", %Decimal{coef: :NaN})

  def div(%Decimal{}, %Decimal{coef: coef2})
      when coef2 < 0,
      do: error(:invalid_operation, "divisor (#{coef2}) must be > 0", %Decimal{coef: :NaN})

the orig pull request text - if don’t want to follow the link:

Hi,

I found a bug or structural problem in Decimal what could have some big impact under special circumstances. Here an example from the Money lib where Decimal is used as main backbone:


iex(15)> m = Money.new(:USD, Decimal.new(%Decimal{sign: -1, coef: -12000000000}))

#Money<:USD, --12000000000>

iex(16)> Money.mult(m, 3)

{:ok, #Money<:USD, --36000000000>}

iex(17)> Money.mult!(m, 3 )|> Money.to_string

{:ok, "$--*,000,000,000.00"}

Of course it’s a special edge case … normally the best practice is:


iex(18)> m = Money.new(:USD, "-12000000000")

#Money<:USD, -12000000000>

but you never know … maybe someone using it …it’s possible! it’s valid Code ! NO compiler warning! NO runtime error!

And the worst case, if you are using any division with this kind of Decimal number like this:


iex(19)> m = Money.new(:USD, Decimal.new(%Decimal{sign: -1, coef: -12000000000}))

#Money<:USD, --12000000000>

iex(20)> Money.mult!(m, 3) |> Money.to_decimal |> Decimal.to_float

→ you end up in an infinite loop!!!

The reason for this is clear: the %Decimal{} struct - with no validation for input → so you can input a negative coefficient.

The type spec and docu are perfect - but do not prevent any misuse:

Type spec : @type coefficient :: non_neg_integer | :NaN | :inf

Documentation: The coefficient of the power of 10. Non-negative because the sign is stored separately in sign.

So my suggestion for a solution you can find in this pull request: mostly checks for this special edge case.

And this time I included the corresponding tests. :wink:

Kind regards and I hope it helps anybody to use Decimal in a safer way!

Ralph

Showing Posts 1 to 10

bglusman

bglusman

I think the intended use here and for most structs is to not construct them yourself with raw internals but to use the library as the authority on validating/constructing… in this case, Decimal.new handles this by setting coef to an absolute value decimal/lib/decimal.ex at main · ericmj/decimal · GitHub

I’ll let @ericmj or other maintainers decide if they’re also interested in this additional safeguard, and I’ve been burned by constructing a struct incorrectly myself like this before, but, worth knowing as a general rule in elixir.

NobbZ

NobbZ

The coefficient type is clearly typed as non negative integer. Even though I’d not necessarily expect the library to crash on malformed data, I’d at least not expect it to behave correctly.

So any PR “fixing” this should do so by crashing because of function clause errors or badarg at least.

razuf

razuf OP

yes - the fix does exact that - anyway I was interested on your opinions and ideas. Thanks.

razuf

razuf OP

al2o3cr

al2o3cr

Nitpick: that specific code

defmodule Derp do
  @spec bad_decimal() :: Decimal.t()
  def bad_decimal() do
    Decimal.new(%Decimal{sign: -1, coef: -12000000000})
  end
end

will be detected by Dialyzer:

lib/derp.ex:3:no_return
Function bad_decimal/0 has no local return.
________________________________________________________________________________
lib/derp.ex:4:call
The function call will not succeed.

Decimal.new(%Decimal{:coef => -12_000_000_000, :exp => 0, :sign => -1})

breaks the contract
(decimal()) :: t()

because Dialyzer can be 100% certain that -12_000_000_000 is not a non_neg_integer().

However, using a variable gets it to pass:

defmodule Derp do
  @spec bad_decimal(pos_integer()) :: Decimal.t()
  def bad_decimal(n) do
    Decimal.new(%Decimal{sign: -1, coef: -n})
  end
end

BUT I’m not sure how far library code should go to defend its invariants against intentional manipulation.

For instance, it’s possible to produce a MapSet with strange behavior at runtime:

iex(28)> x = MapSet.new([:a, :b])                        
#MapSet<[:a, :b]>

# NOTE: writing this will trigger a Dialyzer error because `MapSet` is opaque, but works at runtime
iex(29)> y = %MapSet{map: %{c: "wat"}}
#MapSet<[:c]>
iex(30)> z = MapSet.union(x, y)       
#MapSet<[:a, :b, :c]>
iex(31)> z2 = MapSet.new([:a, :b, :c])
#MapSet<[:a, :b, :c]>
iex(32)> MapSet.equal?(z, z2)         
false

but that code will always cause a Dialyzer error because of the @opaque setting on MapSet.

There are some gotchas with @opaque, notably in pattern matching and module attributes which may explain why it isn’t used in Decimal.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Yes exactly. Elixir is dynamically typed. If you abuse that to create malformed types they won’t work properly. This is true of literally every struct.

razuf

razuf OP

Thanks all for commenting.

@benwilson512 : Of course! I know. But does that mean we should not fix error edge cases? → I mean NO. And I think you agree with me and if we can we should make it easier to use and more safe for everybody.

As NobbZ wrote : So any PR “fixing” this should do so by crashing because of function clause errors or bad arg at least.

And thats exactly what it does. So back to the focus :

I think too the best bevavior for the existing Decimal lib and all the depending libs are: let it crash if wrong inputs:

  # wrong sign
  assert_raise Decimal.Error, fn ->
    Decimal.new(%Decimal{sign: 2, coef: 1, exp: 2})
  end

  # wrong coef
  assert_raise Decimal.Error, fn ->
    Decimal.new(%Decimal{sign: 1, coef: -1, exp: 2})
  end
iex(1)> Decimal.new(%Decimal{sign: 1, coef: 1, exp: 2})  
#Decimal<1E+2>
iex(2)> Decimal.new(%Decimal{sign: 2, coef: 1, exp: 2})
** (Decimal.Error) : wrong decimal number: (%Inspect.Error{message: "got ArgumentError with message \"argument error\" while inspecting %{__struct__: Decimal, coef: 1, exp: 2, sign: 2}"}) 
    (decimal 2.0.0-rc.0) lib/decimal.ex:1151: Decimal.new/1
iex(3)> Decimal.new(%Decimal{sign: 1, coef: -1, exp: 2})
** (Decimal.Error) : wrong decimal number: (%Inspect.Error{message: "got ArgumentError with message \"argument error\" while inspecting %{__struct__: Decimal, coef: -1, exp: 2, sign: 1}"})
    (decimal 2.0.0-rc.0) lib/decimal.ex:1151: Decimal.new/1 ```

all other wrong cases are handled by function clauses

So I think better then the current behavior with all the consequences of prolonging the error (see above):

iex(1)> Decimal.new(%Decimal{sign: 1, coef: 1, exp: 2})  
#Decimal<1E+2>
iex(2)> Decimal.new(%Decimal{sign: 2, coef: 1, exp: 2})
#Decimal<1E+2>
iex(3)> Decimal.new(%Decimal{sign: 1, coef: -1, exp: 2})
#Decimal<-.1E+3>
michallepicki

michallepicki

I think in a dynamically typed language, it is possible for a library to detect many misuses and edge cases at runtime, but there is a runtime cost to doing that. Documenting proper usage and data types allows to lower that runtime cost (assuming the documentation and type specifications are being followed)

razuf

razuf OP

yes something like this…

I think as much as possible without breaking the target. And it’s possible in this case.

Thanks for your thoughts!

razuf

razuf OP

yes you are right! There is a cost for it. I will try to measure it …

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 94592 917
New
cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
heathen
Quite interesting article Google brought me. Didn’t find any mentions about it here. What do you think in general? Would you use togethe...
New
AstonJ
Since we have deprecated our Erlang sections (as we have dedicated Erlang Forums now) let’s add this thread for those who’d like to post ...
New
maennchen
:warning: Security advisory: Decimal DoS vulnerability A vulnerability has been published for decimal where very large exponents can cau...
New
marciol
It would be helpful to have a list of companies worldwide that hire engineers without prior experience in Elixir. Often, it can be quite ...
New

Other Trending Topics Top

marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New
webofbits
Aludel - LLM Evaluation Workbench Aludel is an embeddable Phoenix LiveView dashboard for evaluating and comparing LLM prompts across mult...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews