freetype/src/base/ftdbgmem.c
David Turner 264f307e66 * include/freetype/fterrdef.h, include/freetype/config/ftconfig.h,
include/freetype/internal/ftmemory.h, src/base/ftdbgmem.c,
    src/base/ftutil.c: udpating the memory management functions and
    macros to safely deal with array size buffer overflows, this
    corresponds to attemps to allocate arrays that are too large. For
    an example, consider the following code:

         count = read_uint32_from_file();
         array = malloc( sizeof(Item) * count );
         for ( nn = 0; nn < count; nn++ )
           array[nn] = read_item_from_file();

    if 'count' is larger than FT_UINT_MAX/sizeof(Item), the multiplication
    will overflow and the array allocated will be smaller than the data
    read from the file. In this case, the heap will be trashed, and this
    can be used as a denial-of-service, or make the engine crash later.

    the FT_ARRAY_NEW and FT_ARRAY_RENEW macro now check that the new
    count is no more than FT_INT_MAX/item_size, otherwise, a new error,
    named 'FT_Err_Array_Too_Large' will be returned.

    note that the memory debugger now works again when FT_DEBUG_MEMORY
    is defined, and FT_STRICT_ALIASING has disappeared, the corresponding
    code being now the default.
2006-05-02 06:34:27 +00:00

994 lines
24 KiB
C

/***************************************************************************/
/* */
/* ftdbgmem.c */
/* */
/* Memory debugger (body). */
/* */
/* Copyright 2001, 2002, 2003, 2004, 2005, 2006 by */
/* David Turner, Robert Wilhelm, and Werner Lemberg. */
/* */
/* This file is part of the FreeType project, and may only be used, */
/* modified, and distributed under the terms of the FreeType project */
/* license, LICENSE.TXT. By continuing to use, modify, or distribute */
/* this file you indicate that you have read the license and */
/* understand and accept it fully. */
/* */
/***************************************************************************/
#include <ft2build.h>
#include FT_CONFIG_CONFIG_H
#include FT_INTERNAL_DEBUG_H
#include FT_INTERNAL_MEMORY_H
#include FT_SYSTEM_H
#include FT_ERRORS_H
#include FT_TYPES_H
#ifdef FT_DEBUG_MEMORY
#define KEEPALIVE /* `Keep alive' means that freed blocks aren't released
* to the heap. This is useful to detect double-frees
* or weird heap corruption, but it uses large amounts of
* memory, however.
*/
#include <stdio.h>
#include <stdlib.h>
FT_BASE_DEF( const char* ) _ft_debug_file = 0;
FT_BASE_DEF( long ) _ft_debug_lineno = 0;
extern void
FT_DumpMemory( FT_Memory memory );
typedef struct FT_MemSourceRec_* FT_MemSource;
typedef struct FT_MemNodeRec_* FT_MemNode;
typedef struct FT_MemTableRec_* FT_MemTable;
#define FT_MEM_VAL( addr ) ((FT_ULong)(FT_Pointer)( addr ))
/* this structure holds statistics for a single allocation/release
* site. This is useful to know where memory operations happen the
* most.
*/
typedef struct FT_MemSourceRec_
{
const char* file_name;
long line_no;
FT_Long cur_blocks; /* current number of allocated blocks */
FT_Long max_blocks; /* max. number of allocated blocks */
FT_Long all_blocks; /* total number of blocks allocated */
FT_Long cur_size; /* current cumulative allocated size */
FT_Long max_size; /* maximum cumulative allocated size */
FT_Long all_size; /* total cumulative allocated size */
FT_Long cur_max; /* current maximum allocated size */
FT_UInt32 hash;
FT_MemSource link;
} FT_MemSourceRec;
/*
* We don't need a resizable array for the memory sources, because
* their number is pretty limited within FreeType.
*/
#define FT_MEM_SOURCE_BUCKETS 128
/* this structure holds information related to a single allocated
* memory block. if KEEPALIVE is defined, blocks that are freed by
* FreeType are never released to the system. Instead, their 'size'
* field is set to -size. This is mainly useful to detect double frees,
* at the price of large memory footprint during execution !!
*/
typedef struct FT_MemNodeRec_
{
FT_Byte* address;
FT_Long size; /* < 0 if the block was freed */
FT_MemSource source;
#ifdef KEEPALIVE
const char* free_file_name;
FT_Long free_line_no;
#endif
FT_MemNode link;
} FT_MemNodeRec;
/* the global structure, containing compound statistics and all hash
* tables
*/
typedef struct FT_MemTableRec_
{
FT_ULong size;
FT_ULong nodes;
FT_MemNode* buckets;
FT_ULong alloc_total;
FT_ULong alloc_current;
FT_ULong alloc_max;
FT_ULong alloc_count;
FT_Bool bound_total;
FT_ULong alloc_total_max;
FT_Bool bound_count;
FT_ULong alloc_count_max;
FT_MemSource sources[FT_MEM_SOURCE_BUCKETS];
FT_Bool keep_alive;
FT_Memory memory;
FT_Pointer memory_user;
FT_Alloc_Func alloc;
FT_Free_Func free;
FT_Realloc_Func realloc;
} FT_MemTableRec;
#define FT_MEM_SIZE_MIN 7
#define FT_MEM_SIZE_MAX 13845163
#define FT_FILENAME( x ) ((x) ? (x) : "unknown file")
/*
* Prime numbers are ugly to handle. It would be better to implement
* L-Hashing, which is 10% faster and doesn't require divisions.
*/
static const FT_UInt ft_mem_primes[] =
{
7,
11,
19,
37,
73,
109,
163,
251,
367,
557,
823,
1237,
1861,
2777,
4177,
6247,
9371,
14057,
21089,
31627,
47431,
71143,
106721,
160073,
240101,
360163,
540217,
810343,
1215497,
1823231,
2734867,
4102283,
6153409,
9230113,
13845163,
};
static FT_ULong
ft_mem_closest_prime( FT_ULong num )
{
FT_UInt i;
for ( i = 0;
i < sizeof ( ft_mem_primes ) / sizeof ( ft_mem_primes[0] ); i++ )
if ( ft_mem_primes[i] > num )
return ft_mem_primes[i];
return FT_MEM_SIZE_MAX;
}
extern void
ft_mem_debug_panic( const char* fmt,
... )
{
va_list ap;
printf( "FreeType.Debug: " );
va_start( ap, fmt );
vprintf( fmt, ap );
va_end( ap );
printf( "\n" );
exit( EXIT_FAILURE );
}
static FT_Pointer
ft_mem_table_alloc( FT_MemTable table,
FT_Long size )
{
FT_Memory memory = table->memory;
FT_Pointer block;
memory->user = table->memory_user;
block = table->alloc( memory, size );
memory->user = table;
return block;
}
static void
ft_mem_table_free( FT_MemTable table,
FT_Pointer block )
{
FT_Memory memory = table->memory;
memory->user = table->memory_user;
table->free( memory, block );
memory->user = table;
}
static void
ft_mem_table_resize( FT_MemTable table )
{
FT_ULong new_size;
new_size = ft_mem_closest_prime( table->nodes );
if ( new_size != table->size )
{
FT_MemNode* new_buckets;
FT_ULong i;
new_buckets = (FT_MemNode *)
ft_mem_table_alloc( table,
new_size * sizeof ( FT_MemNode ) );
if ( new_buckets == NULL )
return;
FT_ARRAY_ZERO( new_buckets, new_size );
for ( i = 0; i < table->size; i++ )
{
FT_MemNode node, next, *pnode;
FT_ULong hash;
node = table->buckets[i];
while ( node )
{
next = node->link;
hash = FT_MEM_VAL( node->address ) % new_size;
pnode = new_buckets + hash;
node->link = pnode[0];
pnode[0] = node;
node = next;
}
}
if ( table->buckets )
ft_mem_table_free( table, table->buckets );
table->buckets = new_buckets;
table->size = new_size;
}
}
static FT_MemTable
ft_mem_table_new( FT_Memory memory )
{
FT_MemTable table;
table = (FT_MemTable)memory->alloc( memory, sizeof ( *table ) );
if ( table == NULL )
goto Exit;
FT_ZERO( table );
table->size = FT_MEM_SIZE_MIN;
table->nodes = 0;
table->memory = memory;
table->memory_user = memory->user;
table->alloc = memory->alloc;
table->realloc = memory->realloc;
table->free = memory->free;
table->buckets = (FT_MemNode *)
memory->alloc( memory,
table->size * sizeof ( FT_MemNode ) );
if ( table->buckets )
FT_ARRAY_ZERO( table->buckets, table->size );
else
{
memory->free( memory, table );
table = NULL;
}
Exit:
return table;
}
static void
ft_mem_table_destroy( FT_MemTable table )
{
FT_ULong i;
FT_DumpMemory( table->memory );
if ( table )
{
FT_Long leak_count = 0;
FT_ULong leaks = 0;
/* remove all blocks from the table, revealing leaked ones */
for ( i = 0; i < table->size; i++ )
{
FT_MemNode *pnode = table->buckets + i, next, node = *pnode;
while ( node )
{
next = node->link;
node->link = 0;
if ( node->size > 0 )
{
printf(
"leaked memory block at address %p, size %8ld in (%s:%ld)\n",
node->address, node->size,
FT_FILENAME( node->source->file_name ),
node->source->line_no );
leak_count++;
leaks += node->size;
ft_mem_table_free( table, node->address );
}
node->address = NULL;
node->size = 0;
ft_mem_table_free( table, node );
node = next;
}
table->buckets[i] = 0;
}
ft_mem_table_free( table, table->buckets );
table->buckets = NULL;
table->size = 0;
table->nodes = 0;
/* remove all sources */
for ( i = 0; i < FT_MEM_SOURCE_BUCKETS; i++ )
{
FT_MemSource source, next;
for ( source = table->sources[i]; source != NULL; source = next )
{
next = source->link;
ft_mem_table_free( table, source );
}
table->sources[i] = NULL;
}
printf(
"FreeType: total memory allocations = %ld\n", table->alloc_total );
printf(
"FreeType: maximum memory footprint = %ld\n", table->alloc_max );
ft_mem_table_free( table, table );
if ( leak_count > 0 )
ft_mem_debug_panic(
"FreeType: %ld bytes of memory leaked in %ld blocks\n",
leaks, leak_count );
printf( "FreeType: No memory leaks detected!\n" );
}
}
static FT_MemNode*
ft_mem_table_get_nodep( FT_MemTable table,
FT_Byte* address )
{
FT_ULong hash;
FT_MemNode *pnode, node;
hash = FT_MEM_VAL( address );
pnode = table->buckets + ( hash % table->size );
for (;;)
{
node = pnode[0];
if ( !node )
break;
if ( node->address == address )
break;
pnode = &node->link;
}
return pnode;
}
static FT_MemSource
ft_mem_table_get_source( FT_MemTable table )
{
FT_UInt32 hash;
FT_MemSource node, *pnode;
hash = (FT_UInt32)(void*)_ft_debug_file +
(FT_UInt32)( 5 * _ft_debug_lineno );
pnode = &table->sources[hash % FT_MEM_SOURCE_BUCKETS];
for ( ;; )
{
node = *pnode;
if ( node == NULL )
break;
if ( node->file_name == _ft_debug_file &&
node->line_no == _ft_debug_lineno )
goto Exit;
pnode = &node->link;
}
node = (FT_MemSource)ft_mem_table_alloc( table, sizeof ( *node ) );
if ( node == NULL )
ft_mem_debug_panic(
"not enough memory to perform memory debugging\n" );
node->file_name = _ft_debug_file;
node->line_no = _ft_debug_lineno;
node->cur_blocks = 0;
node->max_blocks = 0;
node->all_blocks = 0;
node->cur_size = 0;
node->max_size = 0;
node->all_size = 0;
node->cur_max = 0;
node->link = NULL;
node->hash = hash;
*pnode = node;
Exit:
return node;
}
static void
ft_mem_table_set( FT_MemTable table,
FT_Byte* address,
FT_ULong size,
FT_Long delta )
{
FT_MemNode *pnode, node;
if ( table )
{
FT_MemSource source;
pnode = ft_mem_table_get_nodep( table, address );
node = *pnode;
if ( node )
{
if ( node->size < 0 )
{
/* This block was already freed. Our memory is now completely */
/* corrupted! */
/* This can only happen in keep-alive mode. */
ft_mem_debug_panic(
"memory heap corrupted (allocating freed block)" );
}
else
{
/* This block was already allocated. This means that our memory */
/* is also corrupted! */
ft_mem_debug_panic(
"memory heap corrupted (re-allocating allocated block at"
" %p, of size %ld)\n"
"org=%s:%d new=%s:%d\n",
node->address, node->size,
FT_FILENAME( node->source->file_name ), node->source->line_no,
FT_FILENAME( _ft_debug_file ), _ft_debug_lineno );
}
}
/* we need to create a new node in this table */
node = (FT_MemNode)ft_mem_table_alloc( table, sizeof ( *node ) );
if ( node == NULL )
ft_mem_debug_panic( "not enough memory to run memory tests" );
node->address = address;
node->size = size;
node->source = source = ft_mem_table_get_source( table );
if ( delta == 0 )
{
/* this is an allocation */
source->all_blocks++;
source->cur_blocks++;
if ( source->cur_blocks > source->max_blocks )
source->max_blocks = source->cur_blocks;
}
if ( size > (FT_ULong)source->cur_max )
source->cur_max = size;
if ( delta != 0 )
{
/* we are growing or shrinking a reallocated block */
source->cur_size += delta;
table->alloc_current += delta;
}
else
{
/* we are allocating a new block */
source->cur_size += size;
table->alloc_current += size;
}
source->all_size += size;
if ( source->cur_size > source->max_size )
source->max_size = source->cur_size;
node->free_file_name = NULL;
node->free_line_no = 0;
node->link = pnode[0];
pnode[0] = node;
table->nodes++;
table->alloc_total += size;
if ( table->alloc_current > table->alloc_max )
table->alloc_max = table->alloc_current;
if ( table->nodes * 3 < table->size ||
table->size * 3 < table->nodes )
ft_mem_table_resize( table );
}
}
static void
ft_mem_table_remove( FT_MemTable table,
FT_Byte* address,
FT_Long delta )
{
if ( table )
{
FT_MemNode *pnode, node;
pnode = ft_mem_table_get_nodep( table, address );
node = *pnode;
if ( node )
{
FT_MemSource source;
if ( node->size < 0 )
ft_mem_debug_panic(
"freeing memory block at %p more than once at (%s:%ld)\n"
"block allocated at (%s:%ld) and released at (%s:%ld)",
address,
FT_FILENAME( _ft_debug_file ), _ft_debug_lineno,
FT_FILENAME( node->source->file_name ), node->source->line_no,
FT_FILENAME( node->free_file_name ), node->free_line_no );
/* scramble the node's content for additional safety */
FT_MEM_SET( address, 0xF3, node->size );
if ( delta == 0 )
{
source = node->source;
source->cur_blocks--;
source->cur_size -= node->size;
table->alloc_current -= node->size;
}
if ( table->keep_alive )
{
/* we simply invert the node's size to indicate that the node */
/* was freed. */
node->size = -node->size;
node->free_file_name = _ft_debug_file;
node->free_line_no = _ft_debug_lineno;
}
else
{
table->nodes--;
*pnode = node->link;
node->size = 0;
node->source = NULL;
ft_mem_table_free( table, node );
if ( table->nodes * 3 < table->size ||
table->size * 3 < table->nodes )
ft_mem_table_resize( table );
}
}
else
ft_mem_debug_panic(
"trying to free unknown block at %p in (%s:%ld)\n",
address,
FT_FILENAME( _ft_debug_file ), _ft_debug_lineno );
}
}
extern FT_Pointer
ft_mem_debug_alloc( FT_Memory memory,
FT_Long size )
{
FT_MemTable table = (FT_MemTable)memory->user;
FT_Byte* block;
if ( size <= 0 )
ft_mem_debug_panic( "negative block size allocation (%ld)", size );
/* return NULL if the maximum number of allocations was reached */
if ( table->bound_count &&
table->alloc_count >= table->alloc_count_max )
return NULL;
/* return NULL if this allocation would overflow the maximum heap size */
if ( table->bound_total &&
table->alloc_total_max - table->alloc_current > (FT_ULong)size )
return NULL;
block = (FT_Byte *)ft_mem_table_alloc( table, size );
if ( block )
{
ft_mem_table_set( table, block, (FT_ULong)size, 0 );
table->alloc_count++;
}
_ft_debug_file = "<unknown>";
_ft_debug_lineno = 0;
return (FT_Pointer)block;
}
extern void
ft_mem_debug_free( FT_Memory memory,
FT_Pointer block )
{
FT_MemTable table = (FT_MemTable)memory->user;
if ( block == NULL )
ft_mem_debug_panic( "trying to free NULL in (%s:%ld)",
FT_FILENAME( _ft_debug_file ),
_ft_debug_lineno );
ft_mem_table_remove( table, (FT_Byte*)block, 0 );
if ( !table->keep_alive )
ft_mem_table_free( table, block );
table->alloc_count--;
_ft_debug_file = "<unknown>";
_ft_debug_lineno = 0;
}
extern FT_Pointer
ft_mem_debug_realloc( FT_Memory memory,
FT_Long cur_size,
FT_Long new_size,
FT_Pointer block )
{
FT_MemTable table = (FT_MemTable)memory->user;
FT_MemNode node, *pnode;
FT_Pointer new_block;
FT_Long delta;
const char* file_name = FT_FILENAME( _ft_debug_file );
FT_Long line_no = _ft_debug_lineno;
/* unlikely, but possible */
if ( new_size == cur_size )
return block;
/* the following is valid according to ANSI C */
#if 0
if ( block == NULL || cur_size == 0 )
ft_mem_debug_panic( "trying to reallocate NULL in (%s:%ld)",
file_name, line_no );
#endif
/* while the following is allowed in ANSI C also, we abort since */
/* such case should be handled by FreeType. */
if ( new_size <= 0 )
ft_mem_debug_panic(
"trying to reallocate %p to size 0 (current is %ld) in (%s:%ld)",
block, cur_size, file_name, line_no );
/* check `cur_size' value */
pnode = ft_mem_table_get_nodep( table, (FT_Byte*)block );
node = *pnode;
if ( !node )
ft_mem_debug_panic(
"trying to reallocate unknown block at %p in (%s:%ld)",
block, file_name, line_no );
if ( node->size <= 0 )
ft_mem_debug_panic(
"trying to reallocate freed block at %p in (%s:%ld)",
block, file_name, line_no );
if ( node->size != cur_size )
ft_mem_debug_panic( "invalid ft_realloc request for %p. cur_size is "
"%ld instead of %ld in (%s:%ld)",
block, cur_size, node->size, file_name, line_no );
/* return NULL if the maximum number of allocations was reached */
if ( table->bound_count &&
table->alloc_count >= table->alloc_count_max )
return NULL;
delta = (FT_Long)( new_size - cur_size );
/* return NULL if this allocation would overflow the maximum heap size */
if ( delta > 0 &&
table->bound_total &&
table->alloc_current + (FT_ULong)delta > table->alloc_total_max )
return NULL;
new_block = (FT_Byte *)ft_mem_table_alloc( table, new_size );
if ( new_block == NULL )
return NULL;
ft_mem_table_set( table, (FT_Byte*)new_block, new_size, delta );
ft_memcpy( new_block, block, cur_size < new_size ? cur_size : new_size );
ft_mem_table_remove( table, (FT_Byte*)block, delta );
_ft_debug_file = "<unknown>";
_ft_debug_lineno = 0;
if ( !table->keep_alive )
ft_mem_table_free( table, block );
return new_block;
}
extern FT_Int
ft_mem_debug_init( FT_Memory memory )
{
FT_MemTable table;
FT_Int result = 0;
if ( getenv( "FT2_DEBUG_MEMORY" ) )
{
table = ft_mem_table_new( memory );
if ( table )
{
const char* p;
memory->user = table;
memory->alloc = ft_mem_debug_alloc;
memory->realloc = ft_mem_debug_realloc;
memory->free = ft_mem_debug_free;
p = getenv( "FT2_ALLOC_TOTAL_MAX" );
if ( p != NULL )
{
FT_Long total_max = ft_atol( p );
if ( total_max > 0 )
{
table->bound_total = 1;
table->alloc_total_max = (FT_ULong)total_max;
}
}
p = getenv( "FT2_ALLOC_COUNT_MAX" );
if ( p != NULL )
{
FT_Long total_count = ft_atol( p );
if ( total_count > 0 )
{
table->bound_count = 1;
table->alloc_count_max = (FT_ULong)total_count;
}
}
p = getenv( "FT2_KEEP_ALIVE" );
if ( p != NULL )
{
FT_Long keep_alive = ft_atol( p );
if ( keep_alive > 0 )
table->keep_alive = 1;
}
result = 1;
}
}
return result;
}
extern void
ft_mem_debug_done( FT_Memory memory )
{
FT_MemTable table = (FT_MemTable)memory->user;
if ( table )
{
memory->free = table->free;
memory->realloc = table->realloc;
memory->alloc = table->alloc;
ft_mem_table_destroy( table );
memory->user = NULL;
}
}
static int
ft_mem_source_compare( const void* p1,
const void* p2 )
{
FT_MemSource s1 = *(FT_MemSource*)p1;
FT_MemSource s2 = *(FT_MemSource*)p2;
if ( s2->max_size > s1->max_size )
return 1;
else if ( s2->max_size < s1->max_size )
return -1;
else
return 0;
}
extern void
FT_DumpMemory( FT_Memory memory )
{
FT_MemTable table = (FT_MemTable)memory->user;
if ( table )
{
FT_MemSource* bucket = table->sources;
FT_MemSource* limit = bucket + FT_MEM_SOURCE_BUCKETS;
FT_MemSource* sources;
FT_UInt nn, count;
const char* fmt;
count = 0;
for ( ; bucket < limit; bucket++ )
{
FT_MemSource source = *bucket;
for ( ; source; source = source->link )
count++;
}
sources = (FT_MemSource*)ft_mem_table_alloc(
table, sizeof ( *sources ) * count );
count = 0;
for ( bucket = table->sources; bucket < limit; bucket++ )
{
FT_MemSource source = *bucket;
for ( ; source; source = source->link )
sources[count++] = source;
}
ft_qsort( sources, count, sizeof ( *sources ), ft_mem_source_compare );
printf( "FreeType Memory Dump: "
"current=%ld max=%ld total=%ld count=%ld\n",
table->alloc_current, table->alloc_max,
table->alloc_total, table->alloc_count );
printf( " block block sizes sizes sizes source\n" );
printf( " count high sum highsum max location\n" );
printf( "-------------------------------------------------\n" );
fmt = "%6ld %6ld %8ld %8ld %8ld %s:%d\n";
for ( nn = 0; nn < count; nn++ )
{
FT_MemSource source = sources[nn];
printf( fmt,
source->cur_blocks, source->max_blocks,
source->cur_size, source->max_size, source->cur_max,
FT_FILENAME( source->file_name ),
source->line_no );
}
printf( "------------------------------------------------\n" );
ft_mem_table_free( table, sources );
}
}
#else /* !FT_DEBUG_MEMORY */
/* ANSI C doesn't like empty source files */
const FT_Byte _debug_mem_dummy = 0;
#endif /* !FT_DEBUG_MEMORY */
/* END */