Commit Graph

5 Commits

Author SHA1 Message Date
Syoyo Fujita
0e3043f3e9 Harden and optimize v3 C parser 2026-05-31 22:20:46 +09:00
Syoyo Fujita
a8fb48fa91 Harden v3 C parser against untrusted glTF input
Threat model: parser is intended for server-side processing of attacker-
supplied glTF/GLB. Two adversarial review rounds plus a 1-hour libFuzzer
run (4 workers, ASan+UBSan, ~420M execs total, zero new artifacts) drove
this set of fixes. Concrete PoCs in tests/v3/security/ confirmed each
issue was exploitable on the prior code.

Path traversal (CRITICAL): tg3__load_external_file concatenated base_dir
with the JSON-supplied URI verbatim. A glTF with
"uri":"../../../tmp/secret" successfully loaded the file from outside
base_dir (verified by FNV64 match). New tg3__uri_is_safe rejects empty,
NUL, leading / or \\, Windows drive prefixes, and any '..' segment.
Path-buffer length checks switched to saturating subtraction so 32-bit
size_t cannot wrap.

Sign-coercion in byteStride: int32_t -1 was cast directly to uint32_t,
producing 0xFFFFFFFF and propagating into downstream count*stride math.
Restrict to glTF spec range: 0 (tightly packed) or [4, 252].

Index validation: parsed int32 index fields (accessor.bufferView,
primitive.indices/material/attributes, node.mesh/skin/camera/light,
scene.nodes[], skin.joints[], animation channel/sampler refs, MSFT_lod
ids, KHR_audio emitter/source refs, etc.) were stored unchecked. New
tg3__validate_indices walks every index field and returns
TG3_ERR_INVALID_INDEX on out-of-range. Gated by
tg3_parse_options.validate_indices, defaulting to 1.

Use-after-free on parse failure (PRE-EXISTING, surfaced by ASan during
fix verification): tg3_parse and tg3_parse_glb destroyed model->arena_
on error paths, but error messages on the user-facing tg3_error_stack
were arena-allocated. Any caller reading errors.entries[i].message
after parse failure read freed memory. tg3_model_free is now sole arena
owner; arena lives across error paths so messages stay valid until the
caller frees the model.

Other fixes:
- tg3_parse_glb: hoist tg3__model_init before header parse so callers
  can safely tg3_model_free on header failure.
- tg3__parse_primitive morph targets: when arena alloc returns NULL,
  pair with target_counts[ti]=0 so validators do not deref.
- Defensive 'if (!tarr) continue' in the morph-target validator loop.
- New Security Considerations block in tiny_gltf_v3.h documents the
  threat model, default-on validation, fs-callback contract, and error
  message lifetime.

Verification: 13 internal tests in tester_v3_c (incl. 7 new security
regressions covering path traversal absolute and relative, fs-callback
no-call assertion, byteStride wrap, OOB index, opt-in raw mode, ext
fields, and arena-message lifetime), 134/134 Khronos sample models
match v1 ground truth digest, 1-hour ASan+UBSan fuzz on the final code
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:02:34 +09:00
Syoyo Fujita
188d7b257b Cross-version verifier comparing v3 C parser against v1 ground truth
Adds a structured DIGEST block (asset, buffers w/ FNV-1a hash, bufferViews,
accessors w/ min/max, mesh primitives w/ sorted attribute maps, nodes w/
normalized TRS+matrix, materials, textures/samplers/images, skins,
animations, cameras, scenes) emitted by both loader_example (v1) and
tester_v3_c (v3 C, now accepting a file arg). test_runner.py runs both,
diffs the digests, and reports counts/digest mismatches with v1 as truth.

Also rolls in /simplify follow-ups on top of 7f736d1: a shared
tg3__json_number_to_double helper to dedupe inline number coercions, a
collapsed fuzz_gltf_v3_c harness using a single tg3_fuzz_run dispatcher,
a rewritten max_safe_uint64_real comment explaining the 53-bit mantissa
constraint, and a tests/Makefile fix so tester_v3_c is a real prerequisite
of `all` (built once via the dedicated rule, not duplicated).

Verifier passes 134/134 on the Khronos glTF-Sample-Models/2.0 suite.
bufferView.target and image.mime_type/uri are intentionally excluded from
the digest: v1 infers target from accessor usage and rewrites image
URIs/mime via stb_image, neither of which is a parse-fidelity concern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:50:28 +09:00
Syoyo Fujita
7f736d19db 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>
2026-05-09 04:10:32 +09:00
Syoyo Fujita
85441bbe19 Add pure-C TinyGLTF v3 runtime
Introduce a C-first TinyGLTF v3 runtime in tiny_gltf_v3.c with a pure-C JSON backend, hook the public header to the new implementation, and add CMake/test coverage for parse and write round-trips.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 04:28:41 +09:00