Skip to content

Check realloc results before using the grown buffer - #430

Open
bmryggdrasil wants to merge 1 commit into
WizardMac:devfrom
bmryggdrasil:fix-unchecked-realloc
Open

bmryggdrasil wants to merge 1 commit into
WizardMac:devfrom
bmryggdrasil:fix-unchecked-realloc

Conversation

@bmryggdrasil

Copy link
Copy Markdown

Summary

Four places still assign realloc's result straight back to the only pointer to the old block. When realloc fails it returns NULL without freeing the old block, so each site leaks it and then writes through NULL. This continues #373, which fixed the same pattern in the por, dta, xport and writer code.

Site What happens when realloc fails
CKHashTable.c ck_str_n_hash_insert memcpy to table->keys + keys_used, an offset from NULL (keys_used is the size of all earlier keys); count was already incremented
txt/readstat_txt_read.c txt_getdelim keeps reading into value_buffer[i], an offset from NULL; value_buffer_len was already doubled
sas/readstat_sas7bcat_read.c sas7bcat_parse_value_labels passes the NULL label to readstat_convert
spss/readstat_sav_write.c sav_emit_long_string_value_labels_record memset on the NULL space_buffer

Changes

  • Keep the old pointer until realloc succeeds, then return READSTAT_ERROR_MALLOC so the existing cleanup paths free it.
  • ck_str_n_hash_insert returns 0 (callers already treat this as failure) and only updates keys, keys_capacity and count after the growth succeeds. The doubling loop also refuses to go past SIZE_MAX.
  • txt_getdelim returns -2 for an allocation failure, so txt_parse_delimited can report READSTAT_ERROR_MALLOC instead of READSTAT_ERROR_READ. It leaves *linep/*linecapp pointing at the old, still-valid buffer, which the caller frees.

Testing

  • test_readstat passes before and after the change (clang via zig cc, -std=c99 -Wall -Werror -pedantic-errors, zlib enabled; Windows x86_64).
  • Fault injection (harness not part of this PR): realloc is made to return NULL during the growth step.
    • ck_str_hash_insert with a key larger than the key buffer: before, segfault; after, returns 0, the table is unchanged and a later insert and lookup work.
    • txt_getdelim on a line longer than its buffer: before, segfault; after, returns -2 and keeps the original buffer and capacity.

Not included

readstat_schema_find_or_create_entry and the entry array in the Stata dictionary reader have the same pattern, but their callers are Ragel actions in src/txt/*.rl that dereference the result, so fixing them means regenerating the .c files. I left those for a separate change. The same pattern also appears in src/bin/ tools (extract_metadata.c, read_csv.c).

🤖 Generated with Claude Code

Four places still assign realloc's result straight back to the only
pointer to the old block, so a failed realloc leaks the old block and
the code then writes through NULL:

- ck_str_n_hash_insert doubles keys_capacity and then copies the new
  key to table->keys + keys_used. Since keys_used is the size of all
  earlier keys, the write lands at an offset from NULL rather than at
  NULL itself. count was also bumped before the copy.
- txt_getdelim doubles value_buffer_len inside the realloc call and
  keeps reading into value_buffer[i], again at an offset from NULL.
- sas7bcat_parse_value_labels passes the NULL label buffer to
  readstat_convert.
- sav_emit_long_string_value_labels_record memsets the NULL
  space_buffer.

Keep the old pointer until realloc succeeds and return
READSTAT_ERROR_MALLOC (or 0 from the hash insert, which callers already
treat as failure) so the existing cleanup paths free it. txt_getdelim
returns -2 for allocation failure so txt_parse_delimited can tell it
apart from a read error. The key-buffer growth in the hash table also
refuses to double past SIZE_MAX.

This continues WizardMac#373, which fixed the same pattern in other readers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

1 participant