From 7167d82972f83b1a7cee3b757e194701ed9ba360 Mon Sep 17 00:00:00 2001 From: Ziya Kapoor Date: Fri, 6 Nov 2026 14:49:49 +0530 Subject: [PATCH] txt: bound schema rows and columns in readstat_parse_txt readstat_parse_txt allocates line_lens with rows_per_observation entries and then indexes it with each schema entry's row, storing col + len there. Neither value is checked, and both come from the dictionary file. In a Stata .dct, _line(N) is independent of _lines(), so _line(3) with the default single line writes past line_lens, and _line(0) or a SAS INPUT #0 wraps the row to 0xFFFFFFFF. _column(0) wraps col to UINT32_MAX, so col + len wraps small, the line buffer is sized from that, and txt_parse_fixed_width copies from &line_buffer[col]. Reject rows outside rows_per_observation and col + len that overflows before line_lens is touched. --- src/test/test_readstat.c | 45 +++++++++++++++++++++++++++++++++++++ src/txt/readstat_txt_read.c | 5 +++++ 2 files changed, 50 insertions(+) diff --git a/src/test/test_readstat.c b/src/test/test_readstat.c index 5b89b0a5..96584dde 100644 --- a/src/test/test_readstat.c +++ b/src/test/test_readstat.c @@ -15,6 +15,7 @@ #include "../stata/readstat_dta.h" #include "test_buffer.h" +#include "test_buffer_io.h" #include "test_types.h" #include "test_error.h" #include "test_readstat.h" @@ -41,6 +42,47 @@ static rt_test_args_t _test_args[] = { } }; +static int test_txt_schema_bounds(void) { + char data[] = "hello\nworld\n"; + rt_buffer_t buffer = { .bytes = data, .size = sizeof(data)-1, .used = sizeof(data)-1 }; + rt_buffer_ctx_t buffer_ctx = { .buffer = &buffer }; + readstat_schema_entry_t entry = { .row = 1, .col = 0, .len = 5 }; + readstat_schema_t schema = { .rows_per_observation = 1, .entry_count = 1, .entries = &entry }; + readstat_error_t error = READSTAT_OK; + + readstat_parser_t *parser = readstat_parser_init(); + readstat_set_open_handler(parser, rt_open_handler); + readstat_set_close_handler(parser, rt_close_handler); + readstat_set_seek_handler(parser, rt_seek_handler); + readstat_set_read_handler(parser, rt_read_handler); + readstat_set_update_handler(parser, rt_update_handler); + readstat_set_io_ctx(parser, &buffer_ctx); + + /* Row past the declared number of lines per observation */ + error = readstat_parse_txt(parser, NULL, &schema, NULL); + if (error != READSTAT_ERROR_PARSE) { + printf("Expected a parse error for an out-of-range row, got: %s\n", + readstat_error_message(error)); + readstat_parser_free(parser); + return 1; + } + + /* Column whose end does not fit in 32 bits */ + entry.row = 0; + entry.col = UINT32_MAX; + buffer_ctx.pos = 0; + error = readstat_parse_txt(parser, NULL, &schema, NULL); + if (error != READSTAT_ERROR_PARSE) { + printf("Expected a parse error for an out-of-range column, got: %s\n", + readstat_error_message(error)); + readstat_parser_free(parser); + return 1; + } + + readstat_parser_free(parser); + return 0; +} + static void dump_buffer(rt_buffer_t *buffer, long format) { char filename[128]; #if !defined _MSC_VER @@ -143,5 +185,8 @@ int main(int argc, char *argv[]) { if (test_sas7bcat_large_label_sets() != 0) return 1; + if (test_txt_schema_bounds() != 0) + return 1; + return 0; } diff --git a/src/txt/readstat_txt_read.c b/src/txt/readstat_txt_read.c index e48f687a..e6a77bfe 100644 --- a/src/txt/readstat_txt_read.c +++ b/src/txt/readstat_txt_read.c @@ -200,6 +200,11 @@ readstat_error_t readstat_parse_txt(readstat_parser_t *parser, const char *filen for (i=0; ientry_count; i++) { readstat_schema_entry_t *entry = &schema->entries[i]; + if (entry->row >= schema->rows_per_observation || + entry->len > UINT32_MAX - entry->col) { + retval = READSTAT_ERROR_PARSE; + goto cleanup; + } if (line_lens[entry->row] < entry->col + entry->len) line_lens[entry->row] = entry->col + entry->len; }