Note that you already have a global storage for the atoms, the atom table! You only need a deterministic algorithm for generating new atoms. No need to store them anywhere. E.g:
I don’t think that this will contribute much. It is unlikely that, for example, '1' will trigger something that '2'…'10000' won’t. In general, it is questionable that generating totally random atoms is of much value, ie likely to trigger anything.
The interesting atoms, as I see it, are:
well-known atoms such as ok, error, true, false, undefined, infinity, etc
“weird” atoms like '', or containing weird characters like '{}', '\'' or '😱', or very long atom names, etc
Anyway, I think what would help (and what it would have to boil down to) is to have a reference implementation which can be tried against some reasonably large system to actually see the impact (or not).
Quoting from EEP 20:
The change is simple in concept, …"
… but affects several atoms in the core of the system.
… the extent and impact of which would be good to know, too. Because…
Theory meets practice.
“Hi, I’m Theory How do you do? ”
“I’m Practice Now go sit over there, and watch ”
One class of atoms in Erlang that’s worth remembering is
Internet domain names like snap.stanford.edu .
Erlang lexical structure was specifically bent to allow
such atoms to be used freely without quoting.
Indeed node names like logger@demo.example.org do not need
quoting, precisely because it was expected that such names
would be used often.
IMO you should fork PropEr and work on making the atom generator less dangerous without unduly worrying about “upstream”.
There is a trick (due to Roberto Aloi if I’m not mistaken) to retrieve the set of atoms known to the VM. One could limit the atom generator to that set plus perhaps a small set of random ones.
Well, if I wanted this for a private or work-related project, I might do just that. However, what brought the issue up for us was writing property tests for OTP, namely the lists module.
Now, on the one hand, the few existing property tests in OTP will only work with PropEr. They use features that the alternatives, Eqc and Triq, don’t have. Consequently, the automated tests are run with PropEr.
On the other hand, while PropEr does have that questionable atom generator that ultimately puts a tight lid on the number of property tests that can be in OTP, I think it unlikely that they will switch to a spur-of-the-moment fork of it, whatever its benefits. Even if they did, I can’t say for certain that we would be willing to maintain it for all eternity
For those reasons, we would like to see the change being done in PropEr
I see that PropEr upstream don’t seem to want to change their atom generator. You could suggest adding a new “safe_atom” generator (for some definition of “safe”). There is a definite use-case for it, so upstream should IMO be willing to consider it (modulo implementation nits). That way users can opt-in on the new behaviour.
Yes that looks like the trick. Perhaps Roberto was the one who mentioned and used it locally (while he was at Klarna) so that’s why I associated his name with it.
Way back in 1980-something, Quintus lifted the
size limit on atoms from 512 characters to 1024
so that any BSD file name could be represented
as a Prolog atom. I find it surprising that
Erlang limits atoms to 255 characters. Even
so, if an atom name is 0 to 255 Latin-1
characters, there are
126733357142396107061626967406548831217427853606727388361295472264802569172030944679204711417736915543877325336948619684290664937845062435667012362299797188708356381832552925850000775491724125909004147601506147326575655759382822573183678801327357531257182924909946294285833630947219865916108048624015413276556526797606542519296803822692410076450416376609122831600305120232670690951797883584902974873977165163631151387131222970029797455499222140801583127895032498945988728175336549842059439614615416954783053108159438031117827440268050975051825588887801715041347459354640116096648307759235106463504140602396312142081
possible atoms. That’s a bit over 10**614.
So we’re never going to be able to sample more than
an infinitesimally tiny fraction of possible atoms.
That suggests to me taking a leaf from simulation
and specifically from Deterministic Monte Carlo.
I’m thinking that a carefully crafted fixed set
of say 200 atoms, ranging from say ‘’ to
‘\0(repeated 255 times)’ might do BETTER at finding
bugs than ‘any possible atom’.
It is worth noting that the high estimate of the number of atoms in the observable universe is 1089, pun intended
Just my opinion, only one addition: the universally used atoms (undefined, ok, error, true, false etc) should be contained in that set and emphasized, meaning that they should be generated more often than the other “weird” ones.
I did some prototyping this summer to see if I could solve the problem with atoms and the thing that I bumped against the most was that making atoms immediate and non-immediate made it so that c-code such as this (lists:member/2):
term = BIF_ARG_1;
non_immed_key = is_not_immed(term);
list = BIF_ARG_2;
while (is_list(list)) {
item = CAR(list_val(list));
if ((item == term) || (non_immed_key && eq(item, term))) {
BIF_RET(am_true);
}
list = CDR(list_val(list));
}
which assumes that a term cannot be both immiate and non-immediate at the same time needs fixing for atoms. And this pattern pops up in a lot of places in the code. It’s used in many of the lists bifs, in match specs, in distribution caches, it kind of leaks into the NIF api, in the iteration order of map keys etc etc. 30 years of assuming that a term cannot be both immediate and boxed has cemented that notion into the VM.
Maybe I will make an attempt at making all atoms dynamic, that is they basically become the same as small binaries, but with a hash as proposed by EEP-20 to make not equal comparison faster.
Anyway, a thought that occurred to me while doing all of this is that what we really are trying to do is to mitigate un-intentional usage of dynamic atoms. So maybe what we should do is to instead of shutting down the VM, we should be throwing an exception when the atom table is full.
That way if there is an attacker trying to fill the atom table, most likely only its connections will be affected. If the atom creation happens in a critical process, the system will go down anyways as the application supervisor dies and then the node is shut down, it will just be delayed a bit.
So it takes the DoS attack from “guaranteed to bring the node down”, to “it might bring the node down”, which might be a good mitigating step. What do you think?
My take on this is that we should instead eliminate the need for dynamic atoms in the first place. We should allow other types than atoms to name dynamically created resources.
So we should remove list_to_atom, binary_to_atom and force binary_to_term to always be safe? The only way to create atoms would be via distribution messages and code loading?
Replying to myself to clarity a bit instead of editing.
I’m not saying that allowing different terms than atoms for ets tables and registered processes is a bad idea. It’s just that none of the DoS atom-leak related CVEs I can think of have been because of such legitimate usages of list_to_atom. They have been instances where the application takes some input from the an untrusted user and assumes that it is POST or GET while in practice it can be anything.
So while fixing this would be nice, it would not have prevented most (any?) or the CVEs discovered so far.
I’m relying on my memory with this claim, so it could be that I’m wrong.
So we should remove list_to_atom, binary_to_atom and force binary_to_term to always be safe? The only way to create atoms would be via distribution messages and code loading?
No, that’s not what I meant. Things like processes and ETS tables should allow non-atoms as names. In my experience that’s where the majority of dynamically created atoms come from. Some come from configuration, but those are under the system owner’s control.
Maybe I will make an attempt at making all atoms dynamic, that is they basically become the same as small binaries, but with a hash as proposed by EEP-20 to make not equal comparison faster.
So on-heap, as opposed to a shared, garbage-collected, off-heap structure? That should work, but EQ/NE comparisons would (often) need additional memory loads.
yes, exactly. EQ/NE will be slower, but maybe not slow enough to notice in real systems. Would have to write a prototype of it to see if it is even worth going through all the code that assumes an atom is an immediate.
I think an API that would allow to convert a binary/list into one of the set of known atoms would help in places like these. Something like list_to_existing_atom(String, ['POST', 'GET', ...]), though ideally without a linear search through a list. This API also statically guarantees atoms do actually exist before it’s called, so there’s no risk of depending on weird cross-module load order dependencies that normal list_to_existing_atom is unfortunately prone to. This would also potentially allow type systems like dialyzer or eqWAlizer to better type such calls into specific atom types rather than the generic atom() helping further detect issues at dev-time.