Skip to content

Fix/validate mesh face indices - #3565

Open
aWarmWalrus wants to merge 3 commits into
google-deepmind:mainfrom
aWarmWalrus:fix/validate-mesh-face-indices
Open

aWarmWalrus wants to merge 3 commits into
google-deepmind:mainfrom
aWarmWalrus:fix/validate-mesh-face-indices

Conversation

@aWarmWalrus

@aWarmWalrus aWarmWalrus commented Sep 9, 2026

Copy link
Copy Markdown

Validate mesh index arrays in mj_validateReferences

Problem

When loading a binary model (.mjb), mj_loadModelBuffer reads the whole
mjModel buffer verbatim from the file and validates it only through
mj_validateReferences. That function checks the *adr/*num arrays but not the
contents of the mesh index arrays mesh_face, mesh_polyvert and mesh_polymap.

At runtime these are used as unchecked offsets, e.g.
mesh_vert + 3*(mesh_face[...] + mesh_vertadr[meshid])
in engine_ray.c, render_context.c, engine_collision_sdf.c,
engine_support.c and engine_collision_convex.c.

The XML compiler already rejects out-of-range indices
(mjCMesh::CheckMesh, user_mesh.cc), but that path is bypassed for binary models.
As a result a crafted .mjb with an out-of-bounds vertex index loads successfully
and then causes an out-of-bounds read (crash, or silent adjacent-heap read for small
overruns) as soon as the mesh is ray-cast, rendered or collided.

Repro (before this change)

A .mjb identical to a valid model except mesh_face set to 100000000
(with nmeshvert == 4) loads via MjModel.from_binary_path with no error, and a
single mj_ray against the mesh segfaults.

Fix

Add content bounds-checks for mesh_face, mesh_polyvert and mesh_polymap to
mj_validateReferences, mirroring the compiler's checks. The *adr/*num arrays
iterated here are already validated by MJMODEL_REFERENCES earlier in the same
function, so the iteration is safe.

Follow-ups (not in this PR)

mesh_graph, bvh_child/bvh_nodeid, and the skin_*/flex_* index arrays share
the same root cause and warrant equivalent validation.

Testing

Adds ValidateReferencesTest.Mesh in test/engine/engine_io_test.cc, which corrupts
mesh_face to an out-of-range and to a negative index and expects
mj_validateReferences to report mesh_face (and to return null once restored),
following the existing GeomCondim/Texture cases.

src/engine/engine_io.c was syntax/type-checked locally with
gcc -fsyntax-only -Iinclude -Isrc (clean). The full test suite was not run in my
environment (no MSVC/Clang toolchain set up here); please rely on CI to execute the
added test.

@google-cla

google-cla Bot commented Sep 9, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@aWarmWalrus

aWarmWalrus commented Sep 9, 2026

Copy link
Copy Markdown
Author

@google-cla check

CLA should work now.

@aWarmWalrus aWarmWalrus closed this Sep 9, 2026
@aWarmWalrus aWarmWalrus reopened this Sep 9, 2026
The binary (MJB) model loader reads mesh_face, mesh_polyvert and
mesh_polymap verbatim from the file and only runs mj_validateReferences,
which previously checked the *adr/*num arrays but not the contents of
these index arrays. At runtime these values are used as unchecked offsets
(e.g. mesh_vert + 3*mesh_face[...] in engine_ray.c / render_context.c /
engine_collision_*.c), so a crafted .mjb with an out-of-bounds vertex
index loads successfully and then causes an out-of-bounds read when the
mesh is ray-cast, rendered or collided.

The XML compiler already rejects such indices (mjCMesh::CheckMesh in
user_mesh.cc), but that path is bypassed when loading a binary model.
Add equivalent bounds checks to mj_validateReferences so both load paths
are covered.
Covers the mesh_face out-of-bounds case in mj_validateReferences: a mesh
face vertex index that is >= mesh_vertnum or negative must be rejected,
matching the checks added for the binary (MJB) load path.
@aWarmWalrus
aWarmWalrus force-pushed the fix/validate-mesh-face-indices branch from 7e562ba to 3cbe397 Compare September 9, 2026 06:00

@sylvesterkaczmarek sylvesterkaczmarek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaves the same MJB out-of-bounds-read class open through mesh_facenormal. Binary models load that array verbatim too, and both classic and Filament rendering use each entry as an unchecked local index into mesh_normal (for example mesh_normal + 3 * (mesh_facenormal[...] + normaladr)). A crafted MJB can therefore pass these new checks with a valid mesh_face but an oversized face-normal index and still read outside the normal buffer. Could mj_validateReferences also validate each mesh_facenormal entry against the owning mesh's mesh_normalnum, with a corruption regression alongside mesh_face? mesh_facetexcoord appears worth auditing for the same reason, accounting for its no-texcoord sentinel semantics.

Follow-up to the mesh_face/mesh_polyvert/mesh_polymap checks: the same
MJB out-of-bounds-read class was still open through mesh_facenormal and
mesh_facetexcoord. Both are read verbatim from a binary model and used as
unchecked mesh-local indices by the classic and Filament renderers
(mesh_normal + 3*(mesh_facenormal[...] + normaladr), and likewise for
mesh_texcoord), so a crafted .mjb with valid mesh_face but an oversized
face-normal or face-texcoord index could still read outside those buffers.

Check every mesh_facenormal entry against the owning mesh's
mesh_normalnum. Check mesh_facetexcoord entries against mesh_texcoordnum
only for meshes that have texcoords (mesh_texcoordadr != -1), since the
array is never read for meshes without them and the compiler leaves it
unset in that case.

Extend the regression test to cover both arrays, including the
no-texcoord sentinel case.
@aWarmWalrus
aWarmWalrus force-pushed the fix/validate-mesh-face-indices branch from 0fa386c to 2cf65cd Compare September 12, 2026 22:21
@aWarmWalrus

Copy link
Copy Markdown
Author

Ah yes good point, updated with mesh_facenormal checks and mesh_facetexcoord checks as well.

@sylvesterkaczmarek sylvesterkaczmarek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed current 2cf65cd. The added validation now covers mesh_facenormal and mesh_facetexcoord with the correct no-texcoord guard, and the regression exercises oversized and negative indices plus the mesh_texcoordadr == -1 case. My earlier blocker is resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants