Skip to content

Fix heap overflow uncompressing tables with variable-length string columns (#134) - #175

Open
cruzzil wants to merge 1 commit into
heasarc:developfrom
cruzzil:fix-134-vla-string-table
Open

Fix heap overflow uncompressing tables with variable-length string columns (#134)#175
cruzzil wants to merge 1 commit into
heasarc:developfrom
cruzzil:fix-134-vla-string-table

Conversation

@cruzzil

@cruzzil cruzzil commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #134: funpack aborts with realloc(): invalid next size on its own
fpack -table output when the table has a 1PA column.

Cause

fits_uncompress_table sizes each column's slice of the decompression buffer
from ZFORMn, but compares the datatype code with abs():

if (abs(coltype[ii]) == TBIT) {
} else if (abs(coltype[ii]) == TSTRING) {
        width = 1;
} else if (coltype[ii] < 0) {    /* variable length array - never reached for 1PA */

ffbnfm returns a negative code for a variable-length array, so 1PA has
coltype = -TSTRING and took the string branch: 1 byte per row instead of an
8-byte P descriptor, and the extra 16 bytes per row for the second descriptor
set were never added to cm_size. The gunzipped blob needs
(8 + 16) * rowspertile bytes and got 1 * rowspertile. fits_compress_table
compares without abs(), so the compressed file is fine — only reading it back
overruns the buffer. The same comparisons also mis-size variable-length bit
columns.

Nothing catches the overrun because the destination is an interior pointer of
cm_buffer and uncompress2mem_from_mem is given realloc, which then
reallocs a pointer that is not the start of a heap block.

Change

  • Drop the two abs() calls, so the tests match fits_compress_table.
  • Pass NULL instead of realloc for the four in-place gunzip calls, so an
    over-long stream fails with DATA_DECOMPRESSION_ERR instead of reallocating
    an interior pointer, and bail out of the tile loop when it does.
  • Reject the table if the ZFORMn widths do not sum to ZNAXIS1; the buffers
    are sized from one and indexed with the other, so this would have caught the
    bug above as a clean error.

Tests

New test_compress_table_vla() in tests/test_imcompress.c (this fork's unit
suite) round-trips a 600-row table with 1PA, 1PE, 1PJ and 8A columns
through fits_compress_table/fits_uncompress_table and checks every value.
It aborts on develop and passes with this change.

Also verified: testprog output and .fit unchanged; the issue's reproducer
now funpacks with all 3000 strings intact; fpack -table/funpack round trip
of testprog.fit OK; valgrind and ASan clean on the new test and on funpack.

🤖 Generated with Claude Code

…lumns

fits_uncompress_table sizes each column's slice of the decompression
buffer from the ZFORMn keyword, but compared the datatype code using
abs():

    if (abs(coltype[ii]) == TBIT) {
    } else if (abs(coltype[ii]) == TSTRING) {
            width = 1;
    } else if (coltype[ii] < 0) {   /* variable length array */

ffbnfm returns a negative code for a variable length array, so a
variable length string column ('1PA') has coltype = -TSTRING and matched
the TSTRING case: its field was sized as 1 byte per row instead of the 8
bytes of a 'P' descriptor, and the extra 16 bytes per row that a VLA
column needs for the second set of descriptors were never added to the
buffer.  fits_uncompress_table then gunzipped the descriptors into a
buffer that was far too small.  The overflow is not caught, because the
destination is an interior pointer of the shared buffer and the realloc
callback handed to uncompress2mem_from_mem reallocs it, so funpack dies
with "realloc(): invalid next size" or corrupts the heap.  The same
comparisons also mis-sized variable length bit columns.

Compare the datatype code without abs(), so that the tests match those
in fits_compress_table, and harden the decompression of an untrusted
table:

  - pass NULL rather than realloc for the four in-place gunzip calls, so
    a stream that expands beyond the space reserved for the column fails
    with DATA_DECOMPRESSION_ERR instead of reallocating an interior
    pointer, and bail out of the tile loop when it does;

  - reject the table if the ZFORMn column widths do not add up to
    ZNAXIS1, since the buffers are sized from one and indexed with the
    other.

Fixes heasarc#134.
@cruzzil
cruzzil force-pushed the fix-134-vla-string-table branch from 643a241 to ec1a183 Compare August 14, 2026 15:06
Comment thread imcompress.c
/* compressed table is corrupt. */
if (*status > 0) {
ffpmsg("Error uncompressing a column of the compressed table");
free(rm_buffer); free(cm_buffer);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nitpick: Please put the two free()s on separate lines.

Otherwise, it all looks good time. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

funpack crashes (heap corruption) on tile-compressed tables with variable-length string (1PA) columns

2 participants