Harden v3 numeric parsing and add C fuzz harness

Reject non-finite/out-of-range JSON numbers in int32/uint64 fields and
array/attribute elements instead of silently truncating, initialize the
model on parse-file failure, and free the partial JSON document when the
root is not an object. Adds a pure-C libFuzzer harness (fuzz_gltf_v3_c)
alongside the existing C++ one and tests covering the new failure modes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Syoyo Fujita
2026-05-09 04:10:32 +09:00
parent af09ec3405
commit 7f736d19db
5 changed files with 296 additions and 21 deletions

View File

@@ -5,3 +5,7 @@ all: ../tiny_gltf.h
clang++ -I../ $(EXTRA_CXXFLAGS) -std=c++11 -g -O0 -o tester tester.cc clang++ -I../ $(EXTRA_CXXFLAGS) -std=c++11 -g -O0 -o tester tester.cc
clang++ -DTINYGLTF_NOEXCEPTION -I../ $(EXTRA_CXXFLAGS) -std=c++11 -g -O0 -o tester_noexcept tester.cc clang++ -DTINYGLTF_NOEXCEPTION -I../ $(EXTRA_CXXFLAGS) -std=c++11 -g -O0 -o tester_noexcept tester.cc
clang++ -DTINYGLTF_USE_CUSTOM_JSON -I../ $(EXTRA_CXXFLAGS) -std=c++11 -g -O0 -o tester_intensive_customjson tester_intensive_customjson.cc clang++ -DTINYGLTF_USE_CUSTOM_JSON -I../ $(EXTRA_CXXFLAGS) -std=c++11 -g -O0 -o tester_intensive_customjson tester_intensive_customjson.cc
clang -I../ -std=c11 -g -O0 -o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c
tester_v3_c: tester_v3_c.c ../tiny_gltf_v3.h ../tiny_gltf_v3.c ../tinygltf_json_c.h
clang -I../ -std=c11 -g -O0 -o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c

View File

@@ -129,6 +129,89 @@ static int check_minimal_write_roundtrip(void) {
return 1; return 1;
} }
static int check_parse_file_failure_initializes_model(void) {
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
memset(&model, 0xA5, sizeof(model));
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse_file(&model, &errors, "scene.gltf", 10, &opts);
if (err != TG3_ERR_FS_NOT_AVAILABLE) {
fprintf(stderr, "tg3_parse_file unexpected error: %d\n", (int)err);
tg3_error_stack_free(&errors);
return 0;
}
if (model.default_scene != -1) {
fprintf(stderr, "tg3_parse_file did not initialize model on failure\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_non_object_root_rejected(void) {
static const uint8_t json[] = "\"not an object\"";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_ERR_JSON_PARSE) {
fprintf(stderr, "non-object root returned unexpected error: %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
if (model.default_scene != -1) {
fprintf(stderr, "non-object root left model in unexpected state\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_huge_integer_field_rejected(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},\"scene\":6.66667e+70}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_ERR_JSON_PARSE) {
fprintf(stderr, "huge integer-like field returned unexpected error: %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
int main(void) { int main(void) {
if (!check_minimal_parse()) { if (!check_minimal_parse()) {
return 1; return 1;
@@ -136,5 +219,14 @@ int main(void) {
if (!check_minimal_write_roundtrip()) { if (!check_minimal_write_roundtrip()) {
return 1; return 1;
} }
if (!check_parse_file_failure_initializes_model()) {
return 1;
}
if (!check_non_object_root_rejected()) {
return 1;
}
if (!check_huge_integer_field_rejected()) {
return 1;
}
return 0; return 0;
} }

View File

@@ -1,19 +1,23 @@
# tests/v3/fuzzer/Makefile — Build libFuzzer harness for tinygltf v3 # tests/v3/fuzzer/Makefile — Build libFuzzer harnesses for tinygltf v3
# #
# Requires: clang++ with libFuzzer support # Requires: clang/clang++ with libFuzzer support
# #
# Targets: # Targets:
# make — build fuzzer with ASan + UBSan # make — build both harnesses with ASan + UBSan
# make run — run fuzzer with default settings # make run — run the dedicated pure-C v3 harness
# make run-cpp — run the legacy header-implementation harness
# make seed — generate seed corpus from test models # make seed — generate seed corpus from test models
# make clean — remove binaries and corpus # make clean — remove binaries and corpus
CC = clang
CXX = clang++ CXX = clang++
CCFLAGS = -g -O1 -std=c11
CXXFLAGS = -g -O1 -std=c++17 -fno-rtti -fno-exceptions CXXFLAGS = -g -O1 -std=c++17 -fno-rtti -fno-exceptions
SANITIZE = -fsanitize=fuzzer,address,undefined SANITIZE = -fsanitize=fuzzer,address,undefined
INCLUDES = -I../../.. INCLUDES = -I../../..
FUZZER = fuzz_gltf_v3 FUZZER = fuzz_gltf_v3
FUZZER_C = fuzz_gltf_v3_c
CORPUS = corpus CORPUS = corpus
ARTIFACTS = artifacts ARTIFACTS = artifacts
@@ -21,16 +25,28 @@ ARTIFACTS = artifacts
MAX_LEN ?= 65536 MAX_LEN ?= 65536
JOBS ?= $(shell nproc 2>/dev/null || echo 4) JOBS ?= $(shell nproc 2>/dev/null || echo 4)
MAX_TIME ?= 0 MAX_TIME ?= 0
FUZZ_ENV ?= LSAN_OPTIONS=detect_leaks=0
.PHONY: all run seed clean .PHONY: all run run-cpp seed clean
all: $(FUZZER) all: $(FUZZER) $(FUZZER_C)
$(FUZZER): fuzz_gltf_v3.cc ../../../tiny_gltf_v3.h ../../../tiny_gltf_v3.c ../../../tinygltf_json_c.h $(FUZZER): fuzz_gltf_v3.cc ../../../tiny_gltf_v3.h ../../../tiny_gltf_v3.c ../../../tinygltf_json_c.h
$(CXX) $(CXXFLAGS) $(SANITIZE) $(INCLUDES) -o $@ $< $(CXX) $(CXXFLAGS) $(SANITIZE) $(INCLUDES) -o $@ $<
run: $(FUZZER) | $(CORPUS) $(ARTIFACTS) $(FUZZER_C): fuzz_gltf_v3_c.c ../../../tiny_gltf_v3.h ../../../tiny_gltf_v3.c ../../../tinygltf_json_c.h
./$(FUZZER) $(CORPUS) \ $(CC) $(CCFLAGS) $(SANITIZE) $(INCLUDES) -o $@ $< ../../../tiny_gltf_v3.c
run: $(FUZZER_C) | $(CORPUS) $(ARTIFACTS)
$(FUZZ_ENV) ./$(FUZZER_C) $(CORPUS) \
-artifact_prefix=$(ARTIFACTS)/ \
-max_len=$(MAX_LEN) \
-jobs=$(JOBS) \
-workers=$(JOBS) \
$(if $(filter-out 0,$(MAX_TIME)),-max_total_time=$(MAX_TIME))
run-cpp: $(FUZZER) | $(CORPUS) $(ARTIFACTS)
$(FUZZ_ENV) ./$(FUZZER) $(CORPUS) \
-artifact_prefix=$(ARTIFACTS)/ \ -artifact_prefix=$(ARTIFACTS)/ \
-max_len=$(MAX_LEN) \ -max_len=$(MAX_LEN) \
-jobs=$(JOBS) \ -jobs=$(JOBS) \
@@ -63,5 +79,5 @@ $(ARTIFACTS):
mkdir -p $(ARTIFACTS) mkdir -p $(ARTIFACTS)
clean: clean:
rm -f $(FUZZER) rm -f $(FUZZER) $(FUZZER_C)
rm -rf $(CORPUS) $(ARTIFACTS) rm -rf $(CORPUS) $(ARTIFACTS)

View File

@@ -0,0 +1,96 @@
#include "tiny_gltf_v3.h"
#include <stddef.h>
#include <stdint.h>
static const uint64_t FUZZ_MEMORY_BUDGET = 64ULL * 1024 * 1024;
static void tg3_fuzz_parse_auto(const uint8_t *data, size_t size) {
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
opts.memory.memory_budget = FUZZ_MEMORY_BUDGET;
tg3_parse_auto(&model, &errors, data, (uint64_t)size, "", 0, &opts);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
static void tg3_fuzz_parse_json(const uint8_t *data, size_t size) {
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
opts.memory.memory_budget = FUZZ_MEMORY_BUDGET;
tg3_parse(&model, &errors, data, (uint64_t)size, "", 0, &opts);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
static void tg3_fuzz_parse_glb(const uint8_t *data, size_t size) {
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
opts.memory.memory_budget = FUZZ_MEMORY_BUDGET;
tg3_parse_glb(&model, &errors, data, (uint64_t)size, "", 0, &opts);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
static void tg3_fuzz_parse_float32(const uint8_t *data, size_t size) {
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
opts.memory.memory_budget = FUZZ_MEMORY_BUDGET;
opts.parse_float32 = 1;
tg3_parse_auto(&model, &errors, data, (uint64_t)size, "", 0, &opts);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
uint8_t selector;
const uint8_t *payload;
size_t payload_size;
if (size == 0) return 0;
selector = data[0] % 4;
payload = data + 1;
payload_size = size - 1;
switch (selector) {
case 0:
tg3_fuzz_parse_auto(payload, payload_size);
break;
case 1:
tg3_fuzz_parse_json(payload, payload_size);
break;
case 2:
tg3_fuzz_parse_glb(payload, payload_size);
break;
case 3:
tg3_fuzz_parse_float32(payload, payload_size);
break;
}
return 0;
}

View File

@@ -451,6 +451,46 @@ static int tg3__json_is_number(const tg3json_value *v) {
return v && (v->type == TG3JSON_INT || v->type == TG3JSON_REAL); return v && (v->type == TG3JSON_INT || v->type == TG3JSON_REAL);
} }
static int tg3__json_number_to_int32(const tg3json_value *v, int32_t *out) {
double real;
int32_t converted;
if (!tg3__json_is_number(v) || !out) return 0;
if (v->type == TG3JSON_INT) {
if (v->u.integer < INT32_MIN || v->u.integer > INT32_MAX) return 0;
*out = (int32_t)v->u.integer;
return 1;
}
real = v->u.real;
if (!isfinite(real) || real < (double)INT32_MIN || real > (double)INT32_MAX) {
return 0;
}
converted = (int32_t)real;
if ((double)converted != real) return 0;
*out = converted;
return 1;
}
static int tg3__json_number_to_uint64(const tg3json_value *v, uint64_t *out) {
double real;
uint64_t converted;
/* Largest safely castable integer expressible as double below 2^64. */
const double max_safe_uint64_real = 18446744073709547520.0;
if (!tg3__json_is_number(v) || !out) return 0;
if (v->type == TG3JSON_INT) {
if (v->u.integer < 0) return 0;
*out = (uint64_t)v->u.integer;
return 1;
}
real = v->u.real;
if (!isfinite(real) || real < 0.0 || real > max_safe_uint64_real) {
return 0;
}
converted = (uint64_t)real;
if ((double)converted != real) return 0;
*out = converted;
return 1;
}
static int tg3__json_is_object(const tg3json_value *v) { return v && v->type == TG3JSON_OBJECT; } static int tg3__json_is_object(const tg3json_value *v) { return v && v->type == TG3JSON_OBJECT; }
static int tg3__json_is_array(const tg3json_value *v) { return v && v->type == TG3JSON_ARRAY; } static int tg3__json_is_array(const tg3json_value *v) { return v && v->type == TG3JSON_ARRAY; }
@@ -487,7 +527,6 @@ static int tg3__parse_string(tg3__parse_ctx *ctx, const tg3json_value *o, const
static int tg3__parse_int(tg3__parse_ctx *ctx, const tg3json_value *o, const char *key, static int tg3__parse_int(tg3__parse_ctx *ctx, const tg3json_value *o, const char *key,
int32_t *out, int required, const char *parent) { int32_t *out, int required, const char *parent) {
const tg3json_value *it = tg3__json_get(o, key); const tg3json_value *it = tg3__json_get(o, key);
int64_t v;
if (!it) { if (!it) {
if (required) { if (required) {
tg3__error_pushf(ctx->errors, ctx->arena, TG3_SEVERITY_ERROR, TG3_ERR_JSON_MISSING_FIELD, tg3__error_pushf(ctx->errors, ctx->arena, TG3_SEVERITY_ERROR, TG3_ERR_JSON_MISSING_FIELD,
@@ -501,13 +540,11 @@ static int tg3__parse_int(tg3__parse_ctx *ctx, const tg3json_value *o, const cha
parent, "Field '%s' must be a number", key); parent, "Field '%s' must be a number", key);
return 0; return 0;
} }
v = (it->type == TG3JSON_INT) ? it->u.integer : (int64_t)it->u.real; if (!tg3__json_number_to_int32(it, out)) {
if (v < INT32_MIN || v > INT32_MAX) {
tg3__error_pushf(ctx->errors, ctx->arena, TG3_SEVERITY_ERROR, TG3_ERR_JSON_TYPE_MISMATCH, tg3__error_pushf(ctx->errors, ctx->arena, TG3_SEVERITY_ERROR, TG3_ERR_JSON_TYPE_MISMATCH,
parent, "Field '%s' value %" PRId64 " is out of range for int32", key, v); parent, "Field '%s' must be a finite int32-valued number", key);
return 0; return 0;
} }
*out = (int32_t)v;
return 1; return 1;
} }
@@ -527,8 +564,11 @@ static int tg3__parse_uint64(tg3__parse_ctx *ctx, const tg3json_value *o, const
parent, "Field '%s' must be a number", key); parent, "Field '%s' must be a number", key);
return 0; return 0;
} }
if (it->type == TG3JSON_INT) *out = it->u.integer >= 0 ? (uint64_t)it->u.integer : 0u; if (!tg3__json_number_to_uint64(it, out)) {
else *out = it->u.real >= 0.0 ? (uint64_t)it->u.real : 0u; tg3__error_pushf(ctx->errors, ctx->arena, TG3_SEVERITY_ERROR, TG3_ERR_JSON_TYPE_MISMATCH,
parent, "Field '%s' must be a finite uint64-valued number", key);
return 0;
}
return 1; return 1;
} }
@@ -651,7 +691,13 @@ static int tg3__parse_int_array(tg3__parse_ctx *ctx, const tg3json_value *o, con
} }
for (i = 0; i < count; ++i) { for (i = 0; i < count; ++i) {
const tg3json_value *item = tg3json_array_get(it, i); const tg3json_value *item = tg3json_array_get(it, i);
arr[i] = item ? (int32_t)((item->type == TG3JSON_REAL) ? item->u.real : item->u.integer) : 0; if (!item || !tg3__json_number_to_int32(item, &arr[i])) {
tg3__error_pushf(ctx->errors, ctx->arena, TG3_SEVERITY_ERROR,
TG3_ERR_JSON_TYPE_MISMATCH, parent,
"Field '%s' element %u must be a finite int32-valued number",
key, (unsigned)i);
return 0;
}
} }
*out = arr; *out = arr;
*out_count = (uint32_t)count; *out_count = (uint32_t)count;
@@ -1155,7 +1201,13 @@ static int tg3__parse_primitive(tg3__parse_ctx *ctx, const tg3json_value *o, tg3
for (i = 0; i < count; ++i) { for (i = 0; i < count; ++i) {
const tg3json_object_entry *entry = tg3json_object_at(attr_it, i); const tg3json_object_entry *entry = tg3json_object_at(attr_it, i);
attrs[i].key = tg3__arena_str(ctx->arena, entry->key, (uint32_t)entry->key_len); attrs[i].key = tg3__arena_str(ctx->arena, entry->key, (uint32_t)entry->key_len);
attrs[i].value = (int32_t)((entry->value->type == TG3JSON_REAL) ? entry->value->u.real : entry->value->u.integer); if (!tg3__json_number_to_int32(entry->value, &attrs[i].value)) {
tg3__error_pushf(ctx->errors, ctx->arena, TG3_SEVERITY_ERROR,
TG3_ERR_JSON_TYPE_MISMATCH, "/primitive/attributes",
"Attribute '%s' must be a finite int32-valued number",
entry->key ? entry->key : "");
attrs[i].value = 0;
}
} }
prim->attributes = attrs; prim->attributes = attrs;
prim->attributes_count = (uint32_t)count; prim->attributes_count = (uint32_t)count;
@@ -1186,7 +1238,13 @@ static int tg3__parse_primitive(tg3__parse_ctx *ctx, const tg3json_value *o, tg3
for (ai = 0; ai < acount; ++ai) { for (ai = 0; ai < acount; ++ai) {
const tg3json_object_entry *entry = tg3json_object_at(target_obj, ai); const tg3json_object_entry *entry = tg3json_object_at(target_obj, ai);
tattrs[ai].key = tg3__arena_str(ctx->arena, entry->key, (uint32_t)entry->key_len); tattrs[ai].key = tg3__arena_str(ctx->arena, entry->key, (uint32_t)entry->key_len);
tattrs[ai].value = (int32_t)((entry->value->type == TG3JSON_REAL) ? entry->value->u.real : entry->value->u.integer); if (!tg3__json_number_to_int32(entry->value, &tattrs[ai].value)) {
tg3__error_pushf(ctx->errors, ctx->arena, TG3_SEVERITY_ERROR,
TG3_ERR_JSON_TYPE_MISMATCH, "/primitive/targets",
"Target attribute '%s' must be a finite int32-valued number",
entry->key ? entry->key : "");
tattrs[ai].value = 0;
}
} }
} }
target_arrays[ti] = tattrs; target_arrays[ti] = tattrs;
@@ -1611,6 +1669,7 @@ TINYGLTF3_API tg3_error_code tg3_parse(tg3_model *model, tg3_error_stack *errors
tg3json_value json_doc; tg3json_value json_doc;
const char *error_pos = NULL; const char *error_pos = NULL;
tg3_error_code ret; tg3_error_code ret;
int parsed_ok;
if (!options) { tg3_parse_options_init(&default_opts); options = &default_opts; } if (!options) { tg3_parse_options_init(&default_opts); options = &default_opts; }
tg3__model_init(model); tg3__model_init(model);
arena = tg3__arena_create(&options->memory); arena = tg3__arena_create(&options->memory);
@@ -1619,9 +1678,12 @@ TINYGLTF3_API tg3_error_code tg3_parse(tg3_model *model, tg3_error_stack *errors
return TG3_ERR_OUT_OF_MEMORY; return TG3_ERR_OUT_OF_MEMORY;
} }
model->arena_ = arena; model->arena_ = arena;
if (!tg3json_parse_n((const char *)json_data, (size_t)json_size, TINYGLTF3_MAX_NESTING_DEPTH, &json_doc, &error_pos) || json_doc.type != TG3JSON_OBJECT) { parsed_ok = tg3json_parse_n((const char *)json_data, (size_t)json_size,
TINYGLTF3_MAX_NESTING_DEPTH, &json_doc, &error_pos);
if (!parsed_ok || json_doc.type != TG3JSON_OBJECT) {
tg3__error_push(errors, TG3_SEVERITY_ERROR, TG3_ERR_JSON_PARSE, "Failed to parse JSON", NULL, tg3__error_push(errors, TG3_SEVERITY_ERROR, TG3_ERR_JSON_PARSE, "Failed to parse JSON", NULL,
error_pos ? (int64_t)(error_pos - (const char *)json_data) : -1); error_pos ? (int64_t)(error_pos - (const char *)json_data) : -1);
if (parsed_ok) tg3json_value_free(&json_doc);
if (model->arena_) { tg3__arena_destroy(model->arena_); model->arena_ = NULL; } if (model->arena_) { tg3__arena_destroy(model->arena_); model->arena_ = NULL; }
return TG3_ERR_JSON_PARSE; return TG3_ERR_JSON_PARSE;
} }
@@ -1657,6 +1719,7 @@ TINYGLTF3_API tg3_error_code tg3_parse_glb(tg3_model *model, tg3_error_stack *er
tg3json_value json_doc; tg3json_value json_doc;
const char *error_pos = NULL; const char *error_pos = NULL;
tg3_error_code err; tg3_error_code err;
int parsed_ok;
err = tg3__parse_glb_header(glb_data, glb_size, &json_chunk, &json_chunk_size, &bin_chunk, &bin_chunk_size, errors); err = tg3__parse_glb_header(glb_data, glb_size, &json_chunk, &json_chunk_size, &bin_chunk, &bin_chunk_size, errors);
if (err != TG3_OK) return err; if (err != TG3_OK) return err;
if (!options) { tg3_parse_options_init(&default_opts); options = &default_opts; } if (!options) { tg3_parse_options_init(&default_opts); options = &default_opts; }
@@ -1667,9 +1730,12 @@ TINYGLTF3_API tg3_error_code tg3_parse_glb(tg3_model *model, tg3_error_stack *er
return TG3_ERR_OUT_OF_MEMORY; return TG3_ERR_OUT_OF_MEMORY;
} }
model->arena_ = arena; model->arena_ = arena;
if (!tg3json_parse_n((const char *)json_chunk, (size_t)json_chunk_size, TINYGLTF3_MAX_NESTING_DEPTH, &json_doc, &error_pos) || json_doc.type != TG3JSON_OBJECT) { parsed_ok = tg3json_parse_n((const char *)json_chunk, (size_t)json_chunk_size,
TINYGLTF3_MAX_NESTING_DEPTH, &json_doc, &error_pos);
if (!parsed_ok || json_doc.type != TG3JSON_OBJECT) {
tg3__error_push(errors, TG3_SEVERITY_ERROR, TG3_ERR_JSON_PARSE, "Failed to parse GLB JSON chunk", NULL, tg3__error_push(errors, TG3_SEVERITY_ERROR, TG3_ERR_JSON_PARSE, "Failed to parse GLB JSON chunk", NULL,
error_pos ? (int64_t)(error_pos - (const char *)json_chunk) : -1); error_pos ? (int64_t)(error_pos - (const char *)json_chunk) : -1);
if (parsed_ok) tg3json_value_free(&json_doc);
tg3__arena_destroy(model->arena_); tg3__arena_destroy(model->arena_);
model->arena_ = NULL; model->arena_ = NULL;
return TG3_ERR_JSON_PARSE; return TG3_ERR_JSON_PARSE;
@@ -1717,6 +1783,7 @@ TINYGLTF3_API tg3_error_code tg3_parse_file(tg3_model *model, tg3_error_stack *e
uint32_t i; uint32_t i;
if (options) opts = *options; if (options) opts = *options;
else tg3_parse_options_init(&opts); else tg3_parse_options_init(&opts);
tg3__model_init(model);
#ifdef TINYGLTF3_ENABLE_FS #ifdef TINYGLTF3_ENABLE_FS
tg3__set_default_fs(&opts.fs); tg3__set_default_fs(&opts.fs);
#endif #endif