From 6f2f09525362cb55dc7fb283b473bacf00ca13cc Mon Sep 17 00:00:00 2001 From: ZUENS2020 <161032866+ZUENS2020@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:32:03 +0800 Subject: [PATCH 1/2] Fix stack-buffer-overflow in parseLine() on a face/line with > TINYOBJ_MAX_FACES_PER_F_LINE vertices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parseLine() stored face/line vertices into the fixed-size stack array `f[TINYOBJ_MAX_FACES_PER_F_LINE]` (16) with no bounds check. An OBJ face is a legal N-gon with an arbitrary vertex count, so a face line with more than 16 vertices wrote past the end of the array — a stack-buffer-overflow WRITE. The guarding assert() is compiled out under -DNDEBUG (release), so there was no runtime protection. With TINYOBJ_FLAG_TRIANGULATE the triangulation loop likewise wrote command->f[3*n+..] (also size 16) unchecked, overflowing for faces with more than 7 vertices. Reachable via the public tinyobj_parse_obj(..., TINYOBJ_FLAG_TRIANGULATE) on a valid OBJ: f 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 => AddressSanitizer: stack-buffer-overflow WRITE in tinyobj_parse_obj (built -O2 -DNDEBUG so the asserts are absent) Fix: bound the writes in both the parse loop and the triangulation loop against TINYOBJ_MAX_FACES_PER_F_LINE (extra vertices are dropped to preserve memory safety). This is a minimal stop-gap; sizing the buffers for the actual face (an N-gon triangulates to 3*(N-2) indices) would be the complete fix and is best done alongside the face/triangulation rework in #60. Normal triangle/quad parsing is unchanged (verified with TINYOBJ_FLAG_TRIANGULATE). Found via libFuzzer + ASan. Co-Authored-By: Claude Opus 4.8 --- tinyobj_loader_c.h | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/tinyobj_loader_c.h b/tinyobj_loader_c.h index c5d307f..ae82098 100644 --- a/tinyobj_loader_c.h +++ b/tinyobj_loader_c.h @@ -1214,8 +1214,14 @@ static int parseLine(Command *command, const char *p, size_t p_len, tinyobj_vertex_index_t vi = parseRawTriple(&token); skip_space_and_cr(&token); - f[num_f] = vi; - num_f++; + /* Bounds-check: an OBJ face/line may list more than + * TINYOBJ_MAX_FACES_PER_F_LINE vertices; without this guard the write + * overflows the fixed-size stack array f[] (the assert below is compiled + * out under NDEBUG). Extra vertices are dropped to preserve memory safety. */ + if (num_f < TINYOBJ_MAX_FACES_PER_F_LINE) { + f[num_f] = vi; + num_f++; + } } assert(num_f == 2); @@ -1241,8 +1247,14 @@ static int parseLine(Command *command, const char *p, size_t p_len, tinyobj_vertex_index_t vi = parseRawTriple(&token); skip_space_and_cr(&token); - f[num_f] = vi; - num_f++; + /* Bounds-check: an OBJ face/line may list more than + * TINYOBJ_MAX_FACES_PER_F_LINE vertices; without this guard the write + * overflows the fixed-size stack array f[] (the assert below is compiled + * out under NDEBUG). Extra vertices are dropped to preserve memory safety. */ + if (num_f < TINYOBJ_MAX_FACES_PER_F_LINE) { + f[num_f] = vi; + num_f++; + } } command->type = COMMAND_F; @@ -1258,6 +1270,11 @@ static int parseLine(Command *command, const char *p, size_t p_len, assert(3 * num_f < TINYOBJ_MAX_FACES_PER_F_LINE); for (k = 2; k < num_f; k++) { + /* Bounds-check the triangulated output: an N-gon produces 3*(N-2) + * indices, which can exceed the fixed-size command->f[]. */ + if (3 * n + 2 >= TINYOBJ_MAX_FACES_PER_F_LINE) { + break; + } i1 = i2; i2 = f[k]; command->f[3 * n + 0] = i0; From aa818b2d2dcad604a9b11cdbf7f6bcd96a2fab15 Mon Sep 17 00:00:00 2001 From: ZUENS2020 <161032866+ZUENS2020@users.noreply.github.com> Date: Tue, 16 Jun 2026 23:13:47 +0800 Subject: [PATCH 2/2] Add regression tests for parseLine() face/line vertex overflow Adds two acutest cases to test/tinyobj_internal_tests.c covering the stack-buffer-overflow this PR fixes: - parseLine_face_overflow: an "f" statement with >TINYOBJ_MAX_FACES_PER_F_LINE vertices (untriangulated and triangulated). - parseLine_line_overflow: an "l" (polyline) statement with >2 vertices. Both reproduce an AddressSanitizer stack-buffer-overflow on the pre-fix code and pass after the fix. Writing the line-statement test exposed a second overflow the original guard missed: the "l" branch keeps two vertices in a size-2 array f[2] but was bounded by TINYOBJ_MAX_FACES_PER_F_LINE (16); the guard is now correctly bounded to 2. Also relaxed two now-incorrect debug asserts that encoded the old (buggy) "the whole n-gon always fits" precondition, which the runtime guards already enforce. Co-Authored-By: Claude Opus 4.8 --- test/tinyobj_internal_tests.c | 48 +++++++++++++++++++++++++++++++++++ tinyobj_loader_c.h | 17 ++++++++----- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/test/tinyobj_internal_tests.c b/test/tinyobj_internal_tests.c index aa89ddc..42475d1 100644 --- a/test/tinyobj_internal_tests.c +++ b/test/tinyobj_internal_tests.c @@ -985,6 +985,52 @@ void test_hash_table_grow(void) destroy_hash_table(&table); } +void test_parseLine_face_overflow(void) +{ + /* Regression for the stack-buffer-overflow fixed in parseLine(): an "f" + * statement that lists far more than TINYOBJ_MAX_FACES_PER_F_LINE vertices + * used to write past the fixed-size stack array f[] / command->f[]. The + * parser must cap the vertices it keeps and must never write out of bounds + * (the latter is what AddressSanitizer catches on the pre-fix code). */ + const char * line = + "f 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 " + "21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40"; + + { + /* Untriangulated: kept vertex count must stay within the array. */ + Command command; + int ret = parseLine(&command, line, strlen(line), /* triangulate */ 0); + TEST_CHECK(ret == 1); + TEST_CHECK(command.type == COMMAND_F); + TEST_CHECK(command.num_f <= TINYOBJ_MAX_FACES_PER_F_LINE); + } + + { + /* Triangulated: the 3*(N-2) emitted indices must also stay within + * command->f[] / command->f_num_verts[]. */ + Command command; + int ret = parseLine(&command, line, strlen(line), /* triangulate */ 1); + TEST_CHECK(ret == 1); + TEST_CHECK(command.type == COMMAND_F); + TEST_CHECK(command.num_f <= TINYOBJ_MAX_FACES_PER_F_LINE); + TEST_CHECK(command.num_f_num_verts <= TINYOBJ_MAX_FACES_PER_F_LINE); + } +} + +void test_parseLine_line_overflow(void) +{ + /* Regression: an "l" (polyline) statement with more than two vertices used + * to overflow the size-2 stack array f[] in parseLine()'s line branch + * (e.g. "l 1 2 3" wrote f[2]). Only the first two vertices are kept and no + * out-of-bounds write may occur. */ + const char * line = "l 1 2 3 4 5 6 7 8 9 10"; + Command command; + int ret = parseLine(&command, line, strlen(line), /* triangulate */ 0); + TEST_CHECK(ret == 1); + TEST_CHECK(command.type == COMMAND_F); + TEST_CHECK(command.num_f == 2); +} + TEST_LIST = { { "skip_space", test_skip_space }, { "skip_space_and_cr", test_skip_space_and_cr }, @@ -1007,5 +1053,7 @@ TEST_LIST = { { "hash_table_exists", test_hash_table_exists }, { "hash_table_get", test_hash_table_get }, { "hash_table_grow", test_hash_table_grow }, + { "parseLine_face_overflow", test_parseLine_face_overflow }, + { "parseLine_line_overflow", test_parseLine_line_overflow }, { 0 } // required by acutest }; diff --git a/tinyobj_loader_c.h b/tinyobj_loader_c.h index ae82098..9884d56 100644 --- a/tinyobj_loader_c.h +++ b/tinyobj_loader_c.h @@ -1214,11 +1214,12 @@ static int parseLine(Command *command, const char *p, size_t p_len, tinyobj_vertex_index_t vi = parseRawTriple(&token); skip_space_and_cr(&token); - /* Bounds-check: an OBJ face/line may list more than - * TINYOBJ_MAX_FACES_PER_F_LINE vertices; without this guard the write - * overflows the fixed-size stack array f[] (the assert below is compiled - * out under NDEBUG). Extra vertices are dropped to preserve memory safety. */ - if (num_f < TINYOBJ_MAX_FACES_PER_F_LINE) { + /* Bounds-check: an OBJ "l" (line) statement may list more than two + * vertices, but this branch only keeps the first two (f[] has size 2, + * see the assert below). Without this guard a polyline such as + * "l 1 2 3" overflows the fixed-size stack array f[] (the assert is + * compiled out under NDEBUG). Extra vertices are dropped. */ + if (num_f < 2) { f[num_f] = vi; num_f++; } @@ -1267,7 +1268,9 @@ static int parseLine(Command *command, const char *p, size_t p_len, tinyobj_vertex_index_t i1; tinyobj_vertex_index_t i2 = f[1]; - assert(3 * num_f < TINYOBJ_MAX_FACES_PER_F_LINE); + /* Note: the triangulated output (3*(num_f-2) indices) may exceed + * command->f[]; the per-iteration guard below enforces the real bound, + * so we no longer assert that the whole n-gon fits. */ for (k = 2; k < num_f; k++) { /* Bounds-check the triangulated output: an N-gon produces 3*(N-2) @@ -1289,7 +1292,7 @@ static int parseLine(Command *command, const char *p, size_t p_len, } else { size_t k = 0; - assert(num_f < TINYOBJ_MAX_FACES_PER_F_LINE); + assert(num_f <= TINYOBJ_MAX_FACES_PER_F_LINE); for (k = 0; k < num_f; k++) { command->f[k] = f[k]; }