CNA-BUG-130: The .cnj Model reader truncates vertex and index sidecar sizes and never range-checks indices, where the SkinnedModel reader refuses the same input
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
ModelTypeReader and BuildModelMeshPartGeometryEXT derive vertex and index counts by integer division, discarding trailing bytes, and build the index buffer without checking indices against the vertex count; the SkinnedModel reader in the same file throws ContentLoadException for both. A mesh with a non-positive vertexStride is silently skipped, as it is in the SkinnedModel reader.
- Identifier
CNA-BUG-130- Category
- Bug
- Subsystem
- Models & glTF
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Low (a triage suggestion, not a project priority)
- Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- ContentManager::Load<Model> on .cnj Model documents (ModelTypeReader, BuildModelMeshPartGeometryEXT)
Expected behaviour
A malformed Model descriptor should be refused with ContentLoadException. The same file's SkinnedModel reader does this deliberately (Task 11.9): it throws when the vertex data is not a multiple of vertexStride or the index data not a multiple of the index size, and checks every index against the vertex count.
Actual behaviour at TARGET
In ContentManager.cpp the Model reader's mesh loop computes numVertices = size / stride, and BuildModelMeshPartGeometryEXT divides the vertex and index byte counts likewise, dropping any remainder, and builds the index buffer without checking indices against the vertex count, so a corrupt sidecar yields a truncated mesh or indices that reference missing vertices. Both the Model and SkinnedModel readers silently skip a mesh whose vertexStride is zero or negative (and one with no vertices or indices file); that skip is not a divergence between them.
Source locations
modules/content/src/Xna/ContentManager.cpp— ModelTypeReader mesh loop: 'if (stride <= 0) continue' and numVertices by divisionmodules/content/src/Xna/ContentManager.cpp— BuildModelMeshPartGeometryEXT: vertexCount and indexCount by integer divisionmodules/content/src/Xna/ContentManager.cpp— SkinnedModelTypeReader parts loop: the Task 11.9 size and index checks for comparison
Evidence
Checked by reading at 009d40f5; not executed.
Independent re-verification: Checked by reading at 009d40f5; not executed. Correction: the original wording listed the stride skip as something the SkinnedModel reader refuses; it does not, both use `continue`.
Focused reproduction
No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.
Current tests
The .cnj Model tests (and the EasyGL easygl_model_json_reader_* programs) use well-formed sidecars; no test feeds a bad stride or a truncated sidecar.
Regression test
Model .cnj fixtures with vertexStride: 0, a vertex sidecar one byte short and an out-of-range index, each expecting ContentLoadException; the fix is to reuse the SkinnedModel reader's checks.
Blast radius
.cnj Model documents written by hand or by other tools. Files written by gltf_to_cnj are consistent.
Workaround
No workaround is known.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Model loading: glTF to .cnj
- Deep dives
- .cnj toolchain: sidecars · .cnj documents: limits
- Known issues
- Bug index