mirror of
https://github.com/syoyo/tinygltf.git
synced 2026-06-08 03:03:50 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
96
tests/v3/fuzzer/fuzz_gltf_v3_c.c
Normal file
96
tests/v3/fuzzer/fuzz_gltf_v3_c.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user