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++ -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

View File

@@ -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;
}

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:
# 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)

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;
}