Re: branch-1_4 allocation overflow
Eric Blake <[email protected]>
| Newsgroups | gmane.comp.gnu.m4.patches |
|---|---|
| Message-ID | <[email protected]> |
Gary V. Vaughan <gary <at> gnu.org> writes: > > > > Here's the two-part patch I am using for head. First I optimized > > the data > > representation, and got rid of a number of potential size_t-to-int > > bugs on 64- > > bit platforms. The second part pulls in a gnulib list structure, > > to convert > > the sparse array into an efficient AVL tree. > > Nice! I remember thinking about doing something similar in the pre- > gnulib > days, but was exhausted after writing the hash ADT and forgot about it. I liked this so much (in the interest of removing arbitrary limits) that I ported it to the branch. In the process, I discovered that gl_oset is lighter- weight than gl_list, fixed a bug in gnulib's oset, and found that I still had a bug in head when hitting the EMFILE limit on my platform (calling fclose on a bogus char* is never a good idea :). Tripped on cygwin, which apparently cannot handle more than 1024 fd's (even though <limits.h> claims otherwise), by doing: $ build/src/m4 include(examples/forloop.m4)dnl forloop(i,1,2200,`divert(i)divnum ') build/src/m4:stdin:2: cannot create temporary file for diversion: Too many open files Aborted (core dumped) So I'll have to re-port this to head. But I'm not quite sure how to add it to the head testsuite, since not all platforms have a puny OPEN_MAX limit. And on head, maybe it would be worth the extra complexity of closing and reopening temp files, to get around even that arbitrary limit. 2006-11-13 Eric Blake <[email protected]> Backport sparse diversion handling from head. * m4/gnulib-cache.m4: Augment with 'gnulib-tool --import avltree-oset'. * src/output.c (struct m4_diversion): Rename from struct diversion, and update members. All users changed. (diversion_table): Change to an ordered set, instead of an array. (div0): New storage for diversion 0. (diversions): No longer needed. (free_list): New list to allow recycling diversion storage. (diversion_storage): New storage to reduce malloc overhead. (cmp_diversion_CB, threshold_diversion_CB): New callbacks. (output_init, output_exit, cleanup_tmpfile, make_room_for) (make_diversion): Handle new diversion storage scheme. (insert_diversion_helper): New function. (insert_diversion, undivert_all, freeze_diversions): Use it. * doc/m4.texinfo (Divert, Diversions): Move hidden test of memory exhaustion to visible test of large diversion numbers. * NEWS: Document this fix. Index: NEWS =================================================================== RCS file: /sources/m4/m4/NEWS,v retrieving revision 1.1.1.1.2.80 diff -u -r1.1.1.1.2.80 NEWS --- NEWS 11 Nov 2006 12:54:46 -0000 1.1.1.1.2.80 +++ NEWS 13 Nov 2006 18:48:48 -0000 @@ -5,7 +5,10 @@ Version 1.4.8 - ?? ??? 2006, by ?? (CVS version 1.4.7a) * The `divert' macro and `-H'/`--hashsize' command line option no longer - cause a core dump when handed extra large values. + cause a core dump when handed extra large values. Also, `divert' now + uses memory proportional to the number of diversions in use, rather than + to the maximum diversion number encountered, so that large diversion + numbers are less likely to exhaust system memory. * The `--help' and `--version' command line options now consistently override all earlier options. For example, `m4 --debugfile=trace --help' now no longer accidentally creates an empty file `trace'. Index: doc/m4.texinfo =================================================================== RCS file: /sources/m4/m4/doc/m4.texinfo,v retrieving revision 1.1.1.1.2.100 diff -u -r1.1.1.1.2.100 m4.texinfo --- doc/m4.texinfo 11 Nov 2006 12:54:46 -0000 1.1.1.1.2.100 +++ doc/m4.texinfo 13 Nov 2006 18:48:48 -0000 @@ -3652,20 +3652,6 @@ f m4exit @end example - -@comment We also need to test allocation overflow. On 32-bit -@comment platforms, this should fail outright. But on 64-bit platforms -@comment with enough memory, the allocation might succeed (hopefully -@comment testers don't mind the memory thrashing), so fake the same -@comment output for test success. - -@example -divert(eval(`1<<28')) -divert(`2') -errprint(__program__`: memory exhausted -')m4exit(`1') -@error{}m4: memory exhausted -@end example @end ignore @c FIXME: need some explanation here why this is a useful feature, not @@ -3725,10 +3711,10 @@ @result{}Wrapped TEXT preceeds diverted text. @end example -If output is diverted to a non-existent diversion, it is simply -discarded. This can be used to suppress unwanted output. A common -example of unwanted output is the trailing newlines after macro -definitions. Here is how to avoid them. +If output is diverted to a negative diversion, it is simply discarded. +This can be used to suppress unwanted output. A common example of +unwanted output is the trailing newlines after macro definitions. Here +is a common programming idiom in @code{m4} for avoiding them. @example divert(`-1') @@ -3738,7 +3724,19 @@ @result{} @end example -This is a common programming idiom in @code{m4}. +@cindex @acronym{GNU} extensions +Traditional implementations only supported ten diversions. But as a +@acronym{GNU} extension, diversion numbers can be as large as positive +integers will allow, rather than treating a multi-digit diversion number +as a request to discard text. + +@example +divert(eval(`1<<28'))world +divert(`2')hello +^D +@result{}hello +@result{}world +@end example Note that @code{divert} is an English word, but also an active macro without arguments. When processing plain text, the word might appear in Index: m4/gnulib-cache.m4 =================================================================== RCS file: /sources/m4/m4/m4/Attic/gnulib-cache.m4,v retrieving revision 1.1.2.19 diff -u -r1.1.2.19 gnulib-cache.m4 --- m4/gnulib-cache.m4 1 Nov 2006 22:29:08 -0000 1.1.2.19 +++ m4/gnulib-cache.m4 13 Nov 2006 18:48:48 -0000 @@ -15,11 +15,11 @@ # Specification in the form of a command-line invocation: -# gnulib-tool --import --dir=. --lib=libm4 --source-base=lib --m4-base=m4 -- doc-base=doc --aux-dir=. --no-libtool --macro-prefix=M4 binary-io clean-temp cloexec close-stream closeout config-h error fdl fopen-safer free gendocs getopt gnupload mkstemp obstack regex stdbool stdlib-safer strstr strtol unlocked-io verror xalloc xvasprintf +# gnulib-tool --import --dir=. --lib=libm4 --source-base=lib --m4-base=m4 -- doc-base=doc --aux-dir=. --no-libtool --macro-prefix=M4 avltree-oset binary-io clean-temp cloexec close-stream closeout config-h error fdl fopen-safer free gendocs getopt gnupload mkstemp obstack regex stdbool stdlib-safer strstr strtol unlocked-io verror xalloc xvasprintf # Specification in the form of a few gnulib-tool.m4 macro invocations: gl_LOCAL_DIR([]) -gl_MODULES([binary-io clean-temp cloexec close-stream closeout config-h error fdl fopen-safer free gendocs getopt gnupload mkstemp obstack regex stdbool stdlib-safer strstr strtol unlocked-io verror xalloc xvasprintf]) +gl_MODULES([avltree-oset binary-io clean-temp cloexec close-stream closeout config-h error fdl fopen-safer free gendocs getopt gnupload mkstemp obstack regex stdbool stdlib-safer strstr strtol unlocked-io verror xalloc xvasprintf]) gl_AVOID([]) gl_SOURCE_BASE([lib]) gl_M4_BASE([m4]) Index: src/output.c =================================================================== RCS file: /sources/m4/m4/src/Attic/output.c,v retrieving revision 1.1.1.1.2.15 diff -u -r1.1.1.1.2.15 output.c --- src/output.c 8 Nov 2006 05:08:26 -0000 1.1.1.1.2.15 +++ src/output.c 13 Nov 2006 18:48:48 -0000 @@ -23,6 +23,8 @@ #include <sys/stat.h> +#include "gl_avltree_oset.h" + /* Size of initial in-memory buffer size for diversions. Small diversions would usually fit in. */ #define INITIAL_BUFFER_SIZE 512 @@ -40,24 +42,40 @@ This code is fairly entangled with the code in input.c, and maybe it belongs there? */ -/* In a struct diversion, only one of file or buffer be may non-NULL, - depending on the fact output is diverted to a file or in memory - buffer. Further, if buffer is NULL, then pointer is NULL, size and - unused are zero. */ +typedef struct temp_dir m4_temp_dir; + +/* When part of diversion_table, each struct m4_diversion either + represents an open file (zero size, non-NULL u.file), an in-memory + buffer (non-zero size, non-NULL u.buffer), or an unused placeholder + diversion (zero size, u is NULL). When not part of + diversion_table, u.next is a pointer to the free_list chain. */ -struct diversion +typedef struct m4_diversion m4_diversion; + +struct m4_diversion { - FILE *file; /* diversion file on disk */ - char *buffer; /* in-memory diversion buffer */ + union + { + FILE *file; /* diversion file on disk */ + char *buffer; /* in-memory diversion buffer */ + m4_diversion *next; /* free-list pointer */ + } u; + int divnum; /* which diversion this represents */ int size; /* usable size before reallocation */ int used; /* used length in characters */ }; -/* Table of diversions. */ -static struct diversion *diversion_table; +/* Table of diversions 1 through INT_MAX. */ +static gl_oset_t diversion_table; + +/* Diversion 0 (not part of diversion_table). */ +static m4_diversion div0; -/* Number of entries in diversion table. */ -static int diversions; +/* Linked list of reclaimed diversion storage. */ +static m4_diversion *free_list; + +/* Obstack from which diversion storage is allocated. */ +static struct obstack diversion_storage; /* Total size of all in-memory buffer sizes. */ static int total_buffer_size; @@ -67,7 +85,7 @@ int current_diversion; /* Current output diversion, NULL if output is being currently discarded. */ -static struct diversion *output_diversion; +static m4_diversion *output_diversion; /* Values of some output_diversion fields, cached out for speed. */ static FILE *output_file; /* current value of (file) */ @@ -77,40 +95,56 @@ /* Number of input line we are generating output for. */ int output_current_line; -typedef struct temp_dir m4_temp_dir; - /* Temporary directory holding all spilled diversion files. */ static m4_temp_dir *output_temp_dir; /*------------------------. -| Output initialisation. | +| Output initialization. | `------------------------*/ +/* Callback for comparing list elements ELT1 and ELT2 for order in + diversion_table. */ +static int +cmp_diversion_CB (const void *elt1, const void *elt2) +{ + const m4_diversion *d1 = (const m4_diversion *) elt1; + const m4_diversion *d2 = (const m4_diversion *) elt2; + /* No need to worry about overflow, since we don't create diversions + with negative divnum. */ + return d1->divnum - d2->divnum; +} + +/* Callback for comparing list element ELT against THRESHOLD. */ +static bool +threshold_diversion_CB (const void *elt, const void *threshold) +{ + const m4_diversion *div = (const m4_diversion *) elt; + /* No need to worry about overflow, since we don't create diversions + with negative divnum. */ + return div->divnum >= *(const int *) threshold; +} + void output_init (void) { - diversion_table = (struct diversion *) xmalloc (sizeof (struct diversion)); - diversions = 1; - diversion_table[0].file = stdout; - diversion_table[0].buffer = NULL; - diversion_table[0].size = 0; - diversion_table[0].used = 0; - - total_buffer_size = 0; - current_diversion = 0; - output_diversion = diversion_table; + diversion_table = gl_oset_create_empty (GL_AVLTREE_OSET, cmp_diversion_CB); + div0.u.file = stdout; + output_diversion = &div0; output_file = stdout; - output_cursor = NULL; - output_unused = 0; + obstack_init (&diversion_storage); } void output_exit (void) { - free (diversion_table); + /* Order is important, since we may have registered cleanup_tmpfile + as an atexit handler, and it must not traverse stale memory. */ + gl_oset_t table = diversion_table; diversion_table = NULL; + gl_oset_free (table); + obstack_free (&diversion_storage, NULL); } /* Clean up any temporary directory. Designed for use as an atexit @@ -120,20 +154,24 @@ cleanup_tmpfile (void) { /* Close any open diversions. */ - int divnum; - struct diversion *diversion; bool fail = false; if (diversion_table) - for (divnum = 1; divnum < diversions; divnum++) - { - diversion = diversion_table + divnum; - if (diversion->file && close_stream_temp (diversion->file) != 0) - { - M4ERROR ((0, errno, "cannot clean temporary file for diversion")); - fail = true; - } - } + { + const void *elt; + gl_oset_iterator_t iter = gl_oset_iterator (diversion_table); + while (gl_oset_iterator_next (&iter, &elt)) + { + m4_diversion *diversion = (m4_diversion *) elt; + if (!diversion->size && diversion->u.file + && close_stream_temp (diversion->u.file) != 0) + { + M4ERROR ((0, errno, + "cannot clean temporary file for diversion")); + fail = true; + } + } + } /* Clean up the temporary directory. */ if (cleanup_temp_dir (output_temp_dir) != 0) @@ -187,6 +225,7 @@ make_room_for (int length) { int wanted_size; + m4_diversion *selected_diversion = NULL; /* Compute needed size for in-memory buffer. Diversions in-memory buffers start at 0 bytes, then 512, then keep doubling until it is @@ -204,10 +243,12 @@ if (total_buffer_size - output_diversion->size + wanted_size > MAXIMUM_TOTAL_SIZE) { - struct diversion *selected_diversion; int selected_used; - struct diversion *diversion; + char *selected_buffer; + m4_diversion *diversion; int count; + gl_oset_iterator_t iter; + const void *elt; /* Find out the buffer having most data, in view of flushing it to disk. Fake the current buffer as having already received the @@ -217,29 +258,38 @@ selected_diversion = output_diversion; selected_used = output_diversion->used + length; - for (diversion = diversion_table + 1; - diversion < diversion_table + diversions; - diversion++) - if (diversion->used > selected_used) - { - selected_diversion = diversion; - selected_used = diversion->used; - } + iter = gl_oset_iterator (diversion_table); + while (gl_oset_iterator_next (&iter, &elt)) + { + diversion = (m4_diversion *) elt; + if (diversion->used > selected_used) + { + selected_diversion = diversion; + selected_used = diversion->used; + } + } + gl_oset_iterator_free (&iter); /* Create a temporary file, write the in-memory buffer of the - diversion to this file, then release the buffer. */ + diversion to this file, then release the buffer. Zero the + diversion before doing anything that can exit () (including + m4_tmpfile), so that the atexit handler doesn't try to close + a garbage pointer as a file. */ - selected_diversion->file = m4_tmpfile (); - if (set_cloexec_flag (fileno (selected_diversion->file), true) != 0) + selected_buffer = selected_diversion->u.buffer; + total_buffer_size -= selected_diversion->size; + selected_diversion->size = 0; + selected_diversion->u.file = NULL; + selected_diversion->u.file = m4_tmpfile (); + + if (set_cloexec_flag (fileno (selected_diversion->u.file), true) != 0) M4ERROR ((warning_status, errno, "Warning: cannot protect diversion across forks")); if (selected_diversion->used > 0) { - count = fwrite (selected_diversion->buffer, - (size_t) selected_diversion->used, - 1, - selected_diversion->file); + count = fwrite (selected_buffer, (size_t) selected_diversion->used, + 1, selected_diversion->u.file); if (count != 1) M4ERROR ((EXIT_FAILURE, errno, "ERROR: cannot flush diversion to temporary file")); @@ -247,37 +297,31 @@ /* Reclaim the buffer space for other diversions. */ - free (selected_diversion->buffer); - total_buffer_size -= selected_diversion->size; - - selected_diversion->buffer = NULL; - selected_diversion->size = 0; + free (selected_buffer); selected_diversion->used = 0; } /* Reload output_file, just in case the flushed diversion was current. */ - output_file = output_diversion->file; - if (output_file) + if (output_diversion == selected_diversion) { - /* The flushed diversion was current indeed. */ + output_file = output_diversion->u.file; output_cursor = NULL; output_unused = 0; } else { - /* The buffer may be safely reallocated. */ - output_diversion->buffer - = xrealloc (output_diversion->buffer, (size_t) wanted_size); + output_diversion->u.buffer + = xrealloc (output_diversion->u.buffer, (size_t) wanted_size); total_buffer_size += wanted_size - output_diversion->size; output_diversion->size = wanted_size; - output_cursor = output_diversion->buffer + output_diversion->used; + output_cursor = output_diversion->u.buffer + output_diversion->used; output_unused = wanted_size - output_diversion->used; } } @@ -445,12 +489,22 @@ void make_diversion (int divnum) { - struct diversion *diversion; + m4_diversion *diversion = NULL; + + if (current_diversion == divnum) + return; if (output_diversion) { - output_diversion->file = output_file; - output_diversion->used = output_diversion->size - output_unused; + if (!output_diversion->size && !output_diversion->u.file) + { + if (!gl_oset_remove (diversion_table, output_diversion)) + error (EXIT_FAILURE, 0, "INTERNAL ERROR: make_diversion failed"); + output_diversion->u.next = free_list; + free_list = output_diversion; + } + else + output_diversion->used = output_diversion->size - output_unused; output_diversion = NULL; output_file = NULL; output_cursor = NULL; @@ -462,26 +516,47 @@ if (divnum < 0) return; - if (divnum >= diversions) + if (divnum == 0) + diversion = &div0; + else + { + const void *elt; + if (gl_oset_search_atleast (diversion_table, threshold_diversion_CB, + &divnum, &elt)) + { + m4_diversion *temp = (m4_diversion *) elt; + if (temp->divnum == divnum) + diversion = temp; + } + } + if (diversion == NULL) { - diversion_table = (struct diversion *) - xnrealloc (diversion_table, divnum + 1, sizeof (struct diversion)); - for (diversion = diversion_table + diversions; - diversion <= diversion_table + divnum; - diversion++) + /* First time visiting this diversion. */ + if (free_list) { - diversion->file = NULL; - diversion->buffer = NULL; + diversion = free_list; + free_list = diversion->u.next; + } + else + { + diversion = (m4_diversion *) obstack_alloc (&diversion_storage, + sizeof *diversion); diversion->size = 0; diversion->used = 0; } - diversions = divnum + 1; + diversion->u.file = NULL; + diversion->divnum = divnum; + gl_oset_add (diversion_table, diversion); } - output_diversion = diversion_table + divnum; - output_file = output_diversion->file; - output_cursor = output_diversion->buffer + output_diversion->used; - output_unused = output_diversion->size - output_diversion->used; + output_diversion = diversion; + if (output_diversion->size) + { + output_cursor = output_diversion->u.buffer + output_diversion->used; + output_unused = output_diversion->size - output_diversion->used; + } + else + output_file = output_diversion->u.file; output_current_line = -1; } @@ -515,59 +590,66 @@ } } -/*-------------------------------------------------------------------------. -| Insert diversion number DIVNUM into the current output file. The | -| diversion is NOT placed on the expansion obstack, because it must not be | -| rescanned. When the file is closed, it is deleted by the system. | -`-------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------. +| Insert DIVERSION (but not div0) into the current output file. The | +| diversion is NOT placed on the expansion obstack, because it must | +| not be rescanned. When the file is closed, it is deleted by the | +| system. | +`-------------------------------------------------------------------*/ -void -insert_diversion (int divnum) +static void +insert_diversion_helper (m4_diversion *diversion) { - struct diversion *diversion; - - /* Do not care about unexisting diversions. Also, diversion 0 is stdout, - which is effectively always empty. */ - - if (divnum <= 0 || divnum >= diversions) - return; - - /* Also avoid undiverting into self. */ - - diversion = diversion_table + divnum; - if (diversion == output_diversion) - return; - /* Effectively undivert only if an output stream is active. */ - if (output_diversion) { - if (diversion->file) + if (diversion->size) + output_text (diversion->u.buffer, diversion->used); + else if (diversion->u.file) { - rewind (diversion->file); - insert_file (diversion->file); + rewind (diversion->u.file); + insert_file (diversion->u.file); } - else if (diversion->buffer) - output_text (diversion->buffer, diversion->used); output_current_line = -1; } /* Return all space used by the diversion. */ - - if (diversion->file) - { - if (close_stream_temp (diversion->file) != 0) - M4ERROR ((0, errno, "cannot clean temporary file for diversion")); - diversion->file = NULL; - } - else if (diversion->buffer) + if (diversion->size) { - free (diversion->buffer); - diversion->buffer = NULL; + free (diversion->u.buffer); diversion->size = 0; diversion->used = 0; } + else if (diversion->u.file && close_stream_temp (diversion->u.file) != 0) + M4ERROR ((0, errno, "cannot clean temporary file for diversion")); + gl_oset_remove (diversion_table, diversion); + diversion->u.next = free_list; + free_list = diversion; +} + +/*-------------------------------------------------------------------------. +| Insert diversion number DIVNUM into the current output file. The | +| diversion is NOT placed on the expansion obstack, because it must not be | +| rescanned. When the file is closed, it is deleted by the system. | +`-------------------------------------------------------------------------*/ + +void +insert_diversion (int divnum) +{ + const void *elt; + + /* Do not care about nonexistent diversions, and undiverting stdout + or self is a no-op. */ + if (divnum <= 0 || current_diversion == divnum) + return; + if (gl_oset_search_atleast (diversion_table, threshold_diversion_CB, + &divnum, &elt)) + { + m4_diversion *diversion = (m4_diversion *) elt; + if (diversion->divnum == divnum) + insert_diversion_helper (diversion); + } } /*-------------------------------------------------------------------------. @@ -578,10 +660,15 @@ void undivert_all (void) { - int divnum; - - for (divnum = 1; divnum < diversions; divnum++) - insert_diversion (divnum); + const void *elt; + gl_oset_iterator_t iter = gl_oset_iterator (diversion_table); + while (gl_oset_iterator_next (&iter, &elt)) + { + m4_diversion *diversion = (m4_diversion *) elt; + if (diversion->divnum != current_diversion) + insert_diversion_helper (diversion); + } + gl_oset_iterator_free (&iter); } /*-------------------------------------------------------------. @@ -593,38 +680,39 @@ { int saved_number; int last_inserted; - int divnum; - struct diversion *diversion; - struct stat file_stat; + gl_oset_iterator_t iter; + const void *elt; saved_number = current_diversion; last_inserted = 0; make_diversion (0); output_file = file; /* kludge in the frozen file */ - for (divnum = 1; divnum < diversions; divnum++) + iter = gl_oset_iterator (diversion_table); + while (gl_oset_iterator_next (&iter, &elt)) { - diversion = diversion_table + divnum; - if (diversion->file || diversion->buffer) + m4_diversion *diversion = (m4_diversion *) elt;; + if (diversion->size || diversion->u.file) { - if (diversion->file) + if (diversion->size) + fprintf (file, "D%d,%d\n", diversion->divnum, diversion->used); + else { - fflush (diversion->file); - if (fstat (fileno (diversion->file), &file_stat) < 0) + struct stat file_stat; + fflush (diversion->u.file); + if (fstat (fileno (diversion->u.file), &file_stat) < 0) M4ERROR ((EXIT_FAILURE, errno, "cannot stat diversion")); if (file_stat.st_size < 0 - || file_stat.st_size != (unsigned long) file_stat.st_size) + || file_stat.st_size != (unsigned long int) file_stat.st_size) M4ERROR ((EXIT_FAILURE, 0, "diversion too large")); - fprintf (file, "D%d,%lu", divnum, + fprintf (file, "D%d,%lu", diversion->divnum, (unsigned long int) file_stat.st_size); } - else - fprintf (file, "D%d,%d\n", divnum, diversion->used); - insert_diversion (divnum); + insert_diversion_helper (diversion); putc ('\n', file); - last_inserted = divnum; + last_inserted = diversion->divnum; } }