[net.bugs.4bsd] bug in realloc in 4.2 version of malloc

jpl@allegra.UUCP (John P. Linderman) (03/03/84)

To quote the manual page for malloc:

    `Realloc' changes the size of the block pointed to by `ptr' to `size'
    bytes and returns a pointer to the (possibly moved) block.  The
    contents will be unchanged up to the lesser of the new and old sizes.

    In order to be compatible with older versions, `realloc' also works
    if `ptr' points to a block freed since the last call of `malloc',
    `realloc' or `calloc'; sequences of `free', `malloc' and `realloc'
    were previously used to attempt storage compaction.  This procedure
    is no longer recommended.

It certainly isn't recommended.  It doesn't work if there have been too
many calls to free() before the call to realloc(), as the following test
program demonstrates.

Repeat by:
    Run this under 4.2:

    #include <stdio.h>

    main()
    {
	char *q, *p[20], *malloc(), *realloc();
	int i;

	for (i = 0; i < 20; i++) p[i] = malloc(10);
	q = p[0];
	for (i = 0; i < 10; *q++ = ++i);
	q = p[0];
	for (i = 0; i < 10; i++) printf("%d ", *q++);
	printf("\n");
	for (i = 0; i < 20; i++) free(p[i]);
	q = realloc(p[0], 20);
	for (i = 0; i < 10; i++) printf("%d ", *q++);
	printf("\n");
    }

    The output will look like

    1 2 3 4 5 6 7 8 9 10 
    1 2 3 4 0 0 0 0 0 0 

    Only the first four characters of p[0] are unchanged, not the original
    10 bytes, as advertised.

Fix by:
    The simplest fix is to change the initialization of realloc_srchlen in
    /usr/src/lib/lic/gen/malloc.c from
 
    int realloc_srchlen = 4;	/* 4 should be plenty, -1 =>'s whole list */

    to

    int realloc_srchlen = -1;	/* Let's be right, not fast: look everywhere */

    The problems arise because realloc() tells findbucket() to look for
    the freed block only among the first realloc_srchlen items in the
    free chains, and when findbucket() fails to find it, realloc()
    assumes the freed block was of minimal size, and ends up copying
    only the first few bytes, regardless of the original size.  The
    suggested fix will cause findbucket() to find the free block and
    use the correct size if it is found, defaulting to the minimum
    size only if the block doesn't appear on the free list at all.

Comments (exercising great restraint to avoid undue sarcasm):
    The new malloc is very conservative of cycles and very profligate
    with space.  If you allocate blocks whose sizes are exact powers
    of 2, you'll get approximately 50% storage efficiency.  If you
    allocate a megabyte for temporary storage, then free it when you
    are done, you'll never use that space unless you allocate another
    area of comparable size.

    There is a tolerable amount of dead code in the routine, considering
    the preoccupation with efficiency.  For example, in morecore(), one finds

  	rnu = (bucket <= 8) ? 11 : bucket + 3;
  	nblks = 1 << (rnu - (bucket + 3));  /* how many blocks to get */
  	if (rnu < bucket)
		rnu = bucket;
    
    Now,
	(bucket <= 8) => (rnu == 11) => (rnu > bucket)
	(bucket > 8) => (rnu == (bucket + 3)) => (rnu > bucket)
    so the `if' is always false.

    A little later in the same routine, one finds
	
	/*
	 * Round up to minimum allocation size boundary
	 * and deduct from block count to reflect.
	 */
  	if ((int)op & 7) {
  		op = (union overhead *)(((int)op + 8) &~ 7);
  		nblks--;
  	}
    
    Fortunately, the code earlier in the routine ensures that this
    conditional is also always false.  If it were true, the size of
    the area at op might no longer be large enough after rounding op
    up, and an area of insufficient size could be returned.

    On the other hand, morecore should (but doesn't) end with a

	op->ov_next = NULL;

    since sbrk() doesn't promise to clear the new space to 0's,
    but malloc relies on the free chains being terminated by
    NULL pointers.

John P. Linderman  Space Cadet  allegra!jpl

guido@mcvax.UUCP (Guido van Rossum) (03/05/84)

I generally agree with your evaluation of 4.2 malloc.  However I think it is
not fair to blame the author for the fact that realloc_srchlen is
initialized to 4 rather than -1.  After all it is a global variable, so
those programs which use memory compaction (there can't be many, given the
obscure description of this feature in the V7 manual) can be fixed quite
easily to set it to -1 at initialization time.  Of course, the length of
your article probably reflects the time you spent to find the bug --
maybe you had been better off when realloc returned (char*)NULL if it can't
find the original pointer.  This should be a minor change.

Guido (oh, and change the manual page, too) van Rossum, CWI, Amsterdam
	guido @ mcvax