libspectrum: fix NULL/short symbol-table crashes in TZX 0x19 blocks

A malformed Generalised Data Block (TZX ID 0x19) can drive the tape state
machine into DATA1 with data_table.symbols == NULL, after which
set_tstates_and_flags() dereferences symbol->lengths and faults on the
near-NULL address 0x8 (the offset of the lengths pointer in
libspectrum_tape_generalised_data_symbol on LP64). Two independent paths
reach this:

  1. Empty data table (TOTD == 0): read_symbol_table() allocates nothing,
     but generalised_data_edge()'s PILOT->DATA1 transition is taken
     unconditionally once the pilot run is exhausted. The subsequent
     get_generalised_data_symbol() then also walks block->data with a
     bits_per_symbol derived from log(0), overrunning the buffer.

  2. Truncated data table (TOTD > 0 but body too short):
     tzx_read_generalised_data() ignored the data-table
     read_symbol_table() return, so the block was appended with
     symbols_in_block > 0 and symbols == NULL. Playback then indexes
     table->symbols[current_symbol] and faults at 0x8.

Fixes:
  - Check the data-table read_symbol_table() return, mirroring the pilot
    table, so a corrupt table rejects the file at parse time (path 2).
  - In the PILOT->DATA1 transition, fall through to PAUSE when the data
    table / data buffer is absent instead of entering DATA1 (path 1).
  - Guard the DATA1 edge itself against a NULL data symbol table as a
    belt-and-suspenders backstop.

Reproduced and verified fixed under ASan/UBSan with two crafted fixtures
(the exact 0x8 SEGV for path 2, a heap-overflow for path 1); both are
clean after the fix.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-08-01 10:21:55 +02:00
1 parent ecfee0006d
commit 24fe44eace
2 files changed
+23 -7

No files matched your search

+21 -6
View File
@@ -1012,12 +1012,19 @@ generalised_data_edge( libspectrum_tape_generalised_data_block *block,
if( ++state->symbols_through_run == block->pilot_repeats[ state->run ] ) {
state->symbols_through_run = 0;
if( ++state->run == table->symbols_in_block ) {
state->state = LIBSPECTRUM_TAPE_STATE_DATA1;
state->bits_through_byte = 0;
state->bytes_through_stream = 0;
state->symbols_through_stream = 0;
state->current_byte = block->data[ 0 ];
state->current_symbol = get_generalised_data_symbol( block, state );
/* A data table with no symbols leaves data_table.symbols == NULL;
entering DATA1 would then dereference a NULL symbol table (and
block->data). Skip straight to the tail pause instead. */
if( block->data_table.symbols == NULL || block->data == NULL ) {
state->state = LIBSPECTRUM_TAPE_STATE_PAUSE;
} else {
state->state = LIBSPECTRUM_TAPE_STATE_DATA1;
state->bits_through_byte = 0;
state->bytes_through_stream = 0;
state->symbols_through_stream = 0;
state->current_byte = block->data[ 0 ];
state->current_symbol = get_generalised_data_symbol( block, state );
}
}
}
}
@@ -1025,6 +1032,14 @@ generalised_data_edge( libspectrum_tape_generalised_data_block *block,
case LIBSPECTRUM_TAPE_STATE_DATA1:
table = &( block->data_table );
/* Defend against a malformed block that reached DATA1 without a data
symbol table (see the PILOT transition above). */
if( table->symbols == NULL ) {
state->state = LIBSPECTRUM_TAPE_STATE_PAUSE;
*tstates = block->pause_tstates;
do_tail_pause( tstates, end_of_block, flags );
break;
}
symbol = &( table->symbols[ state->current_symbol ] );
set_tstates_and_flags( symbol, state->edges_through_symbol, tstates,
+2 -1
View File
@@ -628,7 +628,8 @@ tzx_read_generalised_data( libspectrum_tape *tape,
ptr2 = *ptr;
table = libspectrum_tape_block_data_table( block );
libspectrum_tape_block_read_symbol_table( table, ptr, length );
error = libspectrum_tape_block_read_symbol_table( table, ptr, length );
if( error ) { libspectrum_tape_block_free( block ); return error; }
length -= ptr2 - *ptr;