diff --git a/tests/Makefile b/tests/Makefile index a6c15f1..1d5c036 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -5,3 +5,7 @@ all: ../tiny_gltf.h 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_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 diff --git a/tests/tester_v3_c.c b/tests/tester_v3_c.c index cf58227..6057f49 100644 --- a/tests/tester_v3_c.c +++ b/tests/tester_v3_c.c @@ -129,6 +129,89 @@ static int check_minimal_write_roundtrip(void) { 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) { if (!check_minimal_parse()) { return 1; @@ -136,5 +219,14 @@ int main(void) { if (!check_minimal_write_roundtrip()) { 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; } diff --git a/tests/v3/fuzzer/Makefile b/tests/v3/fuzzer/Makefile index 33ae7d7..e8ccd04 100644 --- a/tests/v3/fuzzer/Makefile +++ b/tests/v3/fuzzer/Makefile @@ -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: -# make — build fuzzer with ASan + UBSan -# make run — run fuzzer with default settings +# make — build both harnesses with ASan + UBSan +# 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 clean — remove binaries and corpus +CC = clang CXX = clang++ +CCFLAGS = -g -O1 -std=c11 CXXFLAGS = -g -O1 -std=c++17 -fno-rtti -fno-exceptions SANITIZE = -fsanitize=fuzzer,address,undefined INCLUDES = -I../../.. FUZZER = fuzz_gltf_v3 +FUZZER_C = fuzz_gltf_v3_c CORPUS = corpus ARTIFACTS = artifacts @@ -21,16 +25,28 @@ ARTIFACTS = artifacts MAX_LEN ?= 65536 JOBS ?= $(shell nproc 2>/dev/null || echo 4) 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 $(CXX) $(CXXFLAGS) $(SANITIZE) $(INCLUDES) -o $@ $< -run: $(FUZZER) | $(CORPUS) $(ARTIFACTS) - ./$(FUZZER) $(CORPUS) \ +$(FUZZER_C): fuzz_gltf_v3_c.c ../../../tiny_gltf_v3.h ../../../tiny_gltf_v3.c ../../../tinygltf_json_c.h + $(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)/ \ -max_len=$(MAX_LEN) \ -jobs=$(JOBS) \ @@ -63,5 +79,5 @@ $(ARTIFACTS): mkdir -p $(ARTIFACTS) clean: - rm -f $(FUZZER) + rm -f $(FUZZER) $(FUZZER_C) rm -rf $(CORPUS) $(ARTIFACTS) diff --git a/tests/v3/fuzzer/fuzz_gltf_v3_c.c b/tests/v3/fuzzer/fuzz_gltf_v3_c.c new file mode 100644 index 0000000..022ac82 --- /dev/null +++ b/tests/v3/fuzzer/fuzz_gltf_v3_c.c @@ -0,0 +1,96 @@ +#include "tiny_gltf_v3.h" + +#include +#include + +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; +} diff --git a/tiny_gltf_v3.c b/tiny_gltf_v3.c index 4c97144..5643b52 100644 --- a/tiny_gltf_v3.c +++ b/tiny_gltf_v3.c @@ -451,6 +451,46 @@ static int tg3__json_is_number(const tg3json_value *v) { 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_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, int32_t *out, int required, const char *parent) { const tg3json_value *it = tg3__json_get(o, key); - int64_t v; if (!it) { if (required) { 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); return 0; } - v = (it->type == TG3JSON_INT) ? it->u.integer : (int64_t)it->u.real; - if (v < INT32_MIN || v > INT32_MAX) { + if (!tg3__json_number_to_int32(it, out)) { 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; } - *out = (int32_t)v; 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); return 0; } - if (it->type == TG3JSON_INT) *out = it->u.integer >= 0 ? (uint64_t)it->u.integer : 0u; - else *out = it->u.real >= 0.0 ? (uint64_t)it->u.real : 0u; + if (!tg3__json_number_to_uint64(it, out)) { + 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; } @@ -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) { 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_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) { 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].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_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) { 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].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; @@ -1611,6 +1669,7 @@ TINYGLTF3_API tg3_error_code tg3_parse(tg3_model *model, tg3_error_stack *errors tg3json_value json_doc; const char *error_pos = NULL; tg3_error_code ret; + int parsed_ok; if (!options) { tg3_parse_options_init(&default_opts); options = &default_opts; } tg3__model_init(model); 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; } 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, 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; } 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; const char *error_pos = NULL; 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); if (err != TG3_OK) return err; 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; } 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, error_pos ? (int64_t)(error_pos - (const char *)json_chunk) : -1); + if (parsed_ok) tg3json_value_free(&json_doc); tg3__arena_destroy(model->arena_); model->arena_ = NULL; 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; if (options) opts = *options; else tg3_parse_options_init(&opts); + tg3__model_init(model); #ifdef TINYGLTF3_ENABLE_FS tg3__set_default_fs(&opts.fs); #endif