Supercolliding a PHP array (inserting 65536 elements takes 30 seconds)
nikic.github.com
nikic.github.com
On a side note, Ruby does not appear to have that same problem as similar code executes in .02 seconds. I think it is probably because Ruby's hash function is superior.
This is a problem that hashtables have in general (unless they are randomized).
_ In the case of the Ruby language, the 1.9.x branch is not affected by the predictable collision condition since this version includes a randomization of the hashing function._
So there is some merit to what the commenter is saying, though I doubt he knew the above.
Actual ruby arrays (which are arrays and not hashes) will obviously not exhibit this problem though.
I think for all practical purposes, unless you're doing something really weird, the likelyhood of hash function collisions is rare enough that we don't need to think too much about it.
I think for all practical purposes, unless you're doing
something really weird, the likelyhood of hash function
collisions is rare enough that we don't need to think too
much about it.
Except that, like with PHP, the worrying part is that someone can stuff rack.request.form_hash or rack.request.query_hash (a la PHP's $_POST and $_GET).(Unlike PHP, though, the Ruby community can head off these particular attacks by releasing a new version of Rack, while waiting for a new 1.8.x release containing a security patch.)
Meanwhile PHP will presumably do a similar set of fixes, though I haven't seen the announcement yet. There are workarounds for web apps that involve using extensions to limit the size of POST requests and/or the acceptable number of parameters in a POST request.
I would like to know if PHP's object implementation, which was introduced after their associative arrays, suffers from the same hash collision issues. I've read CPython's hash algorithm (it's quite beautiful) and I wonder if PHP internally represents objects as glorified hash maps also.
I was curious myself, so I went digging. Most of this stuff appears in `Zend/zend_object_handlers.c`. For objects with std_object_handlers set up on them (which I assume is most of them), zend_std_read_property can do quite a bit of work: it tries a search of the property info hash zobj->ce, then the zobj->properties hash, and then calling `__get()` with protection against __get loops, then giving up and returning null. zend_hash_quick_find(), as used for these lookups, takes a HashTable* as its first argument, which is also the type of a hash table in zvalue_value.
Thus, object properties are associative arrays, though I don't think there's an equivalent severity of attack compared to causing collisions with crafted GET/POST data, as the latter is automatically parsed. You'd have to be able to upload code to make evil objects.
Going through this exercise was fun, though:
<?php $x=new stdClass();
$x->{"b\0ar"} = 12;
$x->{"b\0"} = 14;
$x->{"\0bar"} = 18; // fatal error ?>
The "property starts '\0'" case is specially checked by the standard property handler, with no explanation...Wait, where did we establish that less user input = less array insertions?
I imagine it would actually be fairly difficult to accidentally recreate this issue, or let it slip through testing. No amount of patches in the world will protect you from idiots with access to your codebase
An ini setting seems like a terrible and incomplete fix to the problem.
Why? It solves the problem entirely.
The true issue is that their hashing algorithm sucks. Any patch that doesn't fix the hashing algorithm is a band-aid and not a true fix.
This doesn't seem correct, or I'm missing something.
Upon the first insertion, PHP doesn't know that you intend to insert 63 more elements. It shouldn't allocate a 2^6-element underlying array until it exceeds 2^5 elements, right? So the first 2^5 insertions would be constant time, and only the next 2^5 would be linear.
I'm not sure how PHP performs the reallocation to increase an array's capacity. Maybe it allocates a blank array, and then inserts the existing elements using the standard insertion algorithm. In that case 2^6 linear-time insertions would occur -- half during the reallocation, and half afterwords. But it still bears mentioning that performance wouldn't tank until you inserted half+1 of the values.
http://www.cs.rice.edu/~scrosby/hash/CrosbyWallach_UsenixSec...
How is any modern programming language still vulnerable to this?!
Consider this:
$foo = array();
$foo['bar'] = 'x';
$foo['bar'] = 'y';
In this case the second set of the 'bar' index modifies an existing element in the linked list and is not appended as a new one.In C you don't need a pointer to the last element though, you can just replace the pointer to the first element with the new element and put the old pointer in the "next" field of the new element. I usually implement stacks this way.
What version of PHP are you looking at? PHP 5.3 and up seem to have a sensible linked list implementation in zend_hash.[ch]. The buckets only have a pointer to the list head, but items are inserted at the head. The hash has a separate list for ordered traversal of all items in the "array" and that has pointers to both the head (for traversal) and tail (for insertion). In both cases, list insertion is O(1).
Of course, as you said, the search for our worst case is necessarily O(N). Depending on your perspective, we can say that's a trade-off with hashes, a fault with the hash algorithm, or a fault with the collision resolution strategy.
Not being at all familiar with the underlying mechanisms, does anybody understand why it would take so much longer on {PHP 5.3.6, Lion, my MacBook Air, the PHP CLI}?
I don't know what a MacBook Air uses, but given that it's a notebook probably something slower :)
My Core Duo MacBook gave the following results:
Inserting 32768 evil elements took 19.348465919495 seconds
Inserting 32768 good elements took 0.0174241065979 seconds
And running from the command-line for the full 2^16: Inserting 65536 evil elements took 123.00568199158 seconds
Inserting 65536 good elements took 0.038620948791504 seconds
I'm running php 5.3.6, which is the current default version on 10.6.8 I believe.Thanks for the pointer!
But there is hope! PHP already landed a change (which
will ship with PHP 5.3.9) ...
As far as I can tell, this isn't being backported.This is going to be bad. I've never had the privilege of using PHP 5.3 during work hours; everything has always been stuck on 5.1.6 and 5.2.x.
Predictions:
a) People are going to jump to 5.3 in a hurry, or
b) RedHat will release a backport for RHEL (and Centos will release the patch in six months).
Either way, I think this will go unpatched in a large number of systems until DoS attacks become so common that a) and b) will need to happen.
Didn't check in details yet but it might be interesting for the curious kind.
I shall, arrogant as I am, scoff at people who are surprised when this happens. I mean, honestly, what kind of developer doesn't know the basic properties of their data structures?
See http://www.ocert.org/advisories/ocert-2011-003.html for a listing of vulnerable languages (and yes, Python is on the list).
Inserting 65535 evil elements took 0.030333042144775 seconds Inserting 65535 good elements took 0.020994901657104 seconds
Did you read the article? The reason for the bad performance is that the way that PHP hashes integers is just taking the integer mod the size of the hash table. So for values which are 0 mod the size of the hash table, they will all go into the same bucket. If your hash table has a size that's a power of 2, then any sum of powers of two equal or larger than the size of the hash table will hash to 0, and go into the same bucket. So if you insert a whole bunch of values that increase by a large power of two, then they will all hash to 0 and give you O(n^2) performance.
Note that the size referred to there is being use not only as the size of the array, but also the amount to iterate by. By changing that to something other than a power of 2, you are no longer inserting "evil" elements.
The point of this article is that it's really, really easy to do a denial of service attack on PHP arrays by picking array indices that are large powers of two. Of course you can fix the problem by not using large powers of two.
Thanks again for helping me understand and learn something new today :)