Shaking the Mesh: Four Memory Corruption Bugs in VTK’s GLTF Loader
Table of Contents
“The future is already here — it’s just not evenly distributed.” — William Gibson
How I Got Here#
In mid-2025 I scoped out an engagement for Radically Open Security targeting F3D — a fast, minimalist 3D viewer that supports dozens of file formats — and its library counterpart, libf3d. The proposal focused on three attack surfaces:
Code audit and pentesting of
f3dandlibf3d.Our primary target is the
libf3dsince its API is used by third-party projects and security issues are more critical.
The scope broke down into three targets: the build system and dependency chain, the plugin/thumbnail subsystem, and file parsing. Of these, file parsing was the most interesting — F3D handles glTF, USD, STL, STEP, PLY, OBJ, FBX, and more. Each format has its own parser, and most of the heavy lifting is delegated to VTK (Visualization Toolkit), a massive C++ library originally developed at Kitware for scientific visualization.
The proposal explicitly called out fuzzing campaigns for glTF, USD, and FBX under “Target 3: libf3d: File Parsing and API Security.” glTF came first — it’s the most widely used format in the set, JSON-based (which makes dictionary fuzzing effective), and VTK’s implementation handles a complex web of interlinked structures that practically invites validation bugs.
Six days of audit, one day of reporting. The fuzzing started on day one.
Background#
F3D and VTK#
F3D is a desktop 3D viewer: you throw a file at it and it renders. Under the hood, the actual file parsing and rendering pipeline is VTK — a library used far beyond F3D. VTK powers ParaView (used at Los Alamos National Lab, NASA, and across the energy sector), medical imaging applications, and academic visualization tools. Any memory corruption in VTK’s file parsers affects every downstream consumer.
On Linux, F3D also registers as a thumbnailer — meaning your file manager will automatically parse 3D files to generate preview thumbnails when you browse a directory. No click required. A malicious .gltf file dropped in a shared folder gets parsed the moment someone opens that directory in their file manager.
The glTF Format#
glTF 2.0 (GL Transmission Format) is the “JPEG of 3D” — a JSON-based format for transmitting 3D scenes. A glTF file describes a hierarchy of interlinked objects:
| |
Each layer references the next by integer index. An Accessor says “read 2549 floats starting at byte offset 20392 from BufferView 1.” A BufferView says “I’m a window into Buffer 0, starting at byte 20392, length 30588.” A Buffer points to the raw binary data.
This indirection chain is the attack surface. If any of those integer references are wrong — pointing to a BufferView that’s too small, an Accessor that’s already been freed, or a Mesh that doesn’t exist — the parser will read or write memory it shouldn’t. VTK’s GLTF loader trusts these references.
The Import Pipeline#
When F3D loads a glTF file, the call chain runs deep through VTK:
| |
Two phases, two bug classes. The buffer overflows (CVE-2025-57106, CVE-2025-57107) live in phase 1 — data extraction from buffers with invalid offsets and counts. The use-after-frees (CVE-2025-57108, CVE-2025-57109) live in phase 2 — the importer accesses model data that was freed between the two phases. In both cases the JSON is valid enough to parse, but the internal references it describes are broken. VTK doesn’t catch this until it’s already copying memory.
Building the Fuzzer#
The cgltf Approach#
I didn’t start from scratch. The cgltf project — a single-header C glTF parser — already had a well-documented fuzzing setup using AFL with a dictionary of glTF keywords. The insight is simple: glTF is structured JSON. A fuzzer that generates random bytes will spend most of its time being rejected by the JSON parser. But a fuzzer armed with a dictionary of valid glTF keywords ("accessors", "bufferViews", "byteOffset", 5126) can mutate structurally valid files into semantically broken ones — files that parse as JSON but describe impossible geometry.
I adapted cgltf’s AFL dictionary and extended it for VTK’s specific extension support:
| |
The dictionary had 224 entries covering JSON structure, glTF core keywords, component types, accessor types, and extension-specific tokens.
The Harness#
The working harness (fuzz_f3d_direct.cpp) is 48 lines. It uses F3D’s createNone() engine — headless, no GPU — so the fuzzer runs fast without a display server:
| |
The build uses Clang with libFuzzer and AddressSanitizer:
| |
Seed Corpus#
The corpus started with four valid glTF files of increasing complexity:
TriangleWithoutIndices.gltf— minimal: 1 buffer, 1 accessor, 1 meshBox.glb— binary glTF containerWaterBottle.gltf— 5 accessors, 5 buffer views, 4 textures, full PBR materialBadBasisU.gltf— intentionally malformed, exercises the KHR_texture_basisu extension path
The fuzzer mutations focused on the integer fields — accessor indices, byte offsets, counts, component types — while the dictionary ensured the JSON structure stayed parseable. ASAN caught the rest.
What Fell Out#
Within minutes of the first run, ASAN started flagging crashes. After deduplication and triage, four distinct bug classes emerged — each in a different part of VTK’s GLTF import pipeline. All four are triggered by a single malformed .gltf file with crafted integer references.
CVE-2025-57106 — Buffer Overflow in Data Extraction#
Component: vtkGLTFDocumentLoader::AccessorLoadingWorker::ExecuteBufferDataExtractionWorker
The ExecuteBufferDataExtractionWorker template function sets up a worker to extract typed data from a buffer:
| |
The worker reads accessor.Count elements starting at bufferView.ByteOffset + accessor.ByteOffset from the buffer. But nothing validates that the buffer actually has that many bytes. A crafted glTF with a byteLength of 56 bytes in the BufferView but an accessor claiming 2549 elements reads straight off the end of the heap.
ASAN:
| |
CWE: CWE-120 (Buffer Copy without Checking Size of Input)
CVE-2025-57107 — Heap Buffer Overflow in Accessor Copy#
Component: vtkGLTFDocumentLoader::Accessor copy constructor
When ExtractPrimitiveAttributes processes mesh attributes, it copies Accessor objects by value:
| |
The Accessor struct (defined at vtkGLTFDocumentLoader.h:125) contains fields that reference buffer data. When the accessor’s metadata points to memory beyond the allocated region — because the glTF file’s accessor indices were mutated to reference an undersized buffer — the compiler-generated copy constructor triggers a memcpy that reads past the heap allocation.
ASAN:
| |
544 bytes past the end of the allocation — the accessor’s index was pointing deep into unrelated heap memory.
CWE: CWE-122 (Heap-based Buffer Overflow)
CVE-2025-57108 — Use-After-Free in Mesh Copy#
Component: vtkGLTFDocumentLoader::Mesh copy constructor
The Mesh struct holds a vector of Primitive objects:
| |
When ImportActors copies a mesh from the internal model (line 636: auto mesh = model->Meshes[node.Mesh]), the copy constructor needs to read the Primitives vector’s size. But by this point, the JSON parse tree that owned the mesh data has already been destroyed — vtknlohmann::json’s destructor chain (_Rb_tree::_M_erase) deallocated the nodes, and the mesh’s memory is gone.
ASAN:
| |
The mesh struct thinks it’s alive. The heap knows otherwise.
CWE: CWE-416 (Use After Free)
CVE-2025-57109 — Use-After-Free in ImportActors#
Component: vtkGLTFImporter::ImportActors
During scene graph construction, ImportActors iterates over nodes and accesses their Name member:
| |
The node.Name.empty() call at line 623 dereferences the string’s internal pointer to check its size. But the node’s memory has already been freed — the freed-by trace shows deallocation through the font rasterization path (stbtt__rasterize in imgui’s imstb_truetype.h), which ran during scene setup and reclaimed the heap region that the node data occupied.
ASAN:
| |
A string that looks valid, pointing at memory that’s been recycled.
CWE: CWE-416 (Use After Free)
Impact#
All four vulnerabilities are in VTK’s core GLTF import pipeline. Any application that opens untrusted glTF files through VTK is affected — that includes F3D, ParaView, and any custom application built on libf3d or VTK’s IO modules.
The attack surface is broader than “open a file”:
- Desktop thumbnailers: F3D registers as a Linux thumbnailer. Browsing a directory containing a malicious
.gltftriggers the parser with no user interaction beyond opening the file manager. - Web-to-desktop pipelines: glTF is the standard interchange format for 3D content on the web. Files downloaded from model repositories, embedded in emails, or shared via collaboration tools all pass through the same parser.
- Scientific computing: ParaView installations at national labs and research institutions process externally-sourced data files routinely.
The buffer overflows (CVE-2025-57106, CVE-2025-57107) are heap reads — information disclosure and crash are guaranteed, code execution is possible with heap grooming but non-trivial. The use-after-frees (CVE-2025-57108, CVE-2025-57109) access freed heap memory that may have been reallocated to attacker-influenced data, making them more directly exploitable in theory.
All four are triggerable with a single malformed .gltf file.
Affected versions: VTK <= 9.5.0
The Fix (or Lack Thereof)#
I reported all four bugs to Kitware through responsible disclosure. After some back-and-forth, they asked me to open the issues publicly on VTK’s GitLab. I did — all five issues (one CVE maps to two separate code paths) were opened on July 17, 2025:
- Issue #19732 — CVE-2025-57107 (heap buffer overflow in Accessor copy)
- Issue #19733 — CVE-2025-57106 (buffer overflow in data extraction, path 1)
- Issue #19734 — CVE-2025-57106 (buffer overflow in data extraction, path 2)
- Issue #19735 — CVE-2025-57109 (use-after-free in ImportActors)
- Issue #19736 — CVE-2025-57108 (use-after-free in Mesh copy)
I proposed fixes and root cause analysis on each issue. The buffer overflows need bounds validation before buffer reads — checking that bufferView.ByteOffset + accessor.ByteOffset + (accessor.Count * componentSize) doesn’t exceed the buffer’s byteLength. The use-after-frees need lifetime management fixes to ensure model data isn’t freed while still referenced by the import pipeline.
As of this writing — nine months after disclosure — none of the fixes have been merged. The issues remain open. The CVEs remain unpatched in every released version of VTK.
Timeline#
| Date | Event |
|---|---|
| 2025-06 | F3D engagement begins for Radically Open Security |
| 2025-06/07 | Fuzzer built, crashes discovered and triaged into 4 bug classes |
| 2025-07 | Reported to Kitware via responsible disclosure |
| 2025-07-17 | Issues opened publicly on VTK GitLab at Kitware’s request (#19732, #19733, #19734, #19735, #19736) |
| 2025-10 | CVE-2025-57106, CVE-2025-57107, CVE-2025-57108, CVE-2025-57109 assigned |
| 2026-04 | Fixes still pending — issues open ~9 months |
References#
- VTK GitLab Issue #19732 — CVE-2025-57107, heap buffer overflow in Accessor copy constructor
- VTK GitLab Issue #19733 — CVE-2025-57106, buffer overflow in data extraction (path 1)
- VTK GitLab Issue #19734 — CVE-2025-57106, buffer overflow in data extraction (path 2)
- VTK GitLab Issue #19735 — CVE-2025-57109, use-after-free in ImportActors
- VTK GitLab Issue #19736 — CVE-2025-57108, use-after-free in Mesh copy
- CVE-2025-57106 — Buffer Overflow
- CVE-2025-57107 — Heap Buffer Overflow
- CVE-2025-57108 — Use-After-Free
- CVE-2025-57109 — Use-After-Free
- Snyk — VTK Vulnerabilities
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-122: Heap-based Buffer Overflow
- CWE-416: Use After Free
- cgltf Fuzzing — the dictionary-based fuzzing approach that inspired this work
- glTF 2.0 Specification
- GitHub Advisory GHSA-5pfc-43r5-qrmg (CVE-2025-57106)
Responsible disclosure was followed throughout. All issues were reported privately to Kitware before public disclosure.
Thanks to Radically Open Security for the engagement.