ld: Avoid overflows in string merging

Michael Matz matz@suse.de
Wed Nov 8 14:31:29 GMT 2023


Hello,

On Wed, 8 Nov 2023, Jan Beulich wrote:

> > --- a/bfd/merge.c
> > +++ b/bfd/merge.c
> > @@ -167,7 +167,7 @@ static bool
> >  sec_merge_maybe_resize (struct sec_merge_hash *table, unsigned added)
> >  {
> >    struct bfd_hash_table *bfdtab = &table->table;
> > -  if (bfdtab->count + added > table->nbuckets * 2 / 3)
> > +  if (bfdtab->count + added > table->nbuckets / 3 * 2)
> >      {
> >        unsigned i;
> >        unsigned long newnb = table->nbuckets * 2;
> > @@ -175,7 +175,7 @@ sec_merge_maybe_resize (struct sec_merge_hash *table, unsigned added)
> >        uint64_t *newl;
> >        unsigned long alloc;
> >  
> > -      while (bfdtab->count + added > newnb * 2 / 3)
> > +      while (bfdtab->count + added > newnb / 3 * 2)
> >  	{
> >  	  newnb *= 2;
> >  	  if (!newnb)
> 
> Isn't this overly aggressive? We want to resize when past two thirds,
> but why would we go by two thirds even within this loop?

Because doubling newnb once doesn't ensure that count+added will fit.  
Consider count==1, oldbuckets==0x2000, and added==1<<24.  The above loop 
seemed more obvious to me than the other method: bit magic to round up 
(count+added)*3/2 to next power of two (which still would need a loop).

> We've doubled once already, and all we care about is that new capacity 
> be enough to cover "added". The more that - as the comment there says - 
> the caller already overestimates heavily.
> 
> Even that estimate could do with tweaking.

Yes.  As you say, there can be max. 255 one-char strings, and 65535 
two-char strings (and so on), making for a better estimation divider of 3 
starting with section sizes of 512 (and so on).  But in the grand scheme, 
and if it weren't for the bug, whatever is over-estimated at one time will 
likely be used up at a later time (of course it will remain waste when it 
so happens to be one of the last input sections).  Fixing the estimation 
would merely mean a reallocation a little later.  Back at the time I 
measured the overall waste and while it could have been better it wasn't 
terrible either.

In the bug report case we hit an extreme: it has a 2GB input string 
section, that turns out to be only adding 750k new strings.  Whatever 
method I had used to estimate, it still would have been grossly off: a 2GB 
section could contain 429m invididual four-byte strings (i.e. 5 bytes with 
the terminator), so that's the lowest number any estimation could validly 
produce, and it would be off by a factor of 1000.

> Otoh aren't we also at risk of underestimating when entsize == 1 and
> we're not dealing with strings?

Hmm, that's true.  I need to fix that.

> > @@ -240,7 +240,7 @@ sec_merge_hash_insert (struct sec_merge_hash *table,
> >    hashp->u.suffix = NULL;
> >    hashp->next = NULL;
> >    // We must not need resizing, otherwise _index is wrong
> > -  BFD_ASSERT (bfdtab->count + 1 <= table->nbuckets * 2 / 3);
> > +  BFD_ASSERT (bfdtab->count + 1 <= table->nbuckets / 3 * 2);
> >    bfdtab->count++;
> >    table->key_lens[_index] = (hash << 32) | (uint32_t)len;
> >    table->values[_index] = hashp;
> 
> I'm puzzled by both comment and assertion here: Afaict we're past
> resizing already, and hence all that matters is that the new index
> is within table boundaries.

But as the resize is based on an estimation this assert makes sure that it 
was in fact a conservative estimation.  E.g. that assert would trigger in 
the case you worry about above: non-strings entsize==1, and 
underestimated.

The comment mentioning _index being wrong is indeed outdated, though.  
There was a time when I did resize the hash table when necessary (instead 
of the assert), eventually I precomputed _index in the caller which would 
have been invalidated if it was in fact resized (because nbuckets would 
have changed); that's what the comment still refers to.  When I removed 
the resizing (and replaced with an assert) I didn't update the comment.  
I'll update it to "We must not need resizing, the estimation has to be 
conservatively correct" or suchlike.

> Further, the same expression occurring three times (and now needing
> updating consistently) pretty clearly calls for putting in a macro
> or function. (Assuming of course the same calculation remains to
> be there three times, which may not be the case as per above.)

Agreed, I'll do that before pushing.

> Finally, is using unsigned int variables / fields actually
> appropriate when BFD64 and hence section sizes can be wider than 32
> bits?

They can be larger, and in that case they won't be part of the whole 
section merging.  That's a conscious decision.  The code is so terribly 
performance sensitive that even cache pressure matters and going from 32 
to 64bit for the offsets has a non-trivial cost that I wasn't willing for 
everyone reasonable to pay to cater for the crazy cases.  At some point in 
time this might need revisiting, with a whole bunch of benchmarking and 
pondering if it's really worth it.  IMHO we aren't there yet.

I'll change the comment and use a macro for the calculation, then push.  
I'm trying to come up with a testcase for the underestimation plus fix 
separately.  Thanks for the review.


Ciao,
Michael.


More information about the Binutils mailing list