Repository navigation
Fix heap buffer overflow for high valence subdivision vertices - #632
stefanatwork wants to merge 5 commits into
Conversation
The one-ring construction in CatmullClark1RingT::init() and GeneralCatmullClark1RingT::init() wrote into the fixed capacity ring buffers (MAX_RING_FACE_VALENCE / MAX_RING_EDGE_VALENCE) without any runtime bound, only asserts guarded these writes. A vertex shared by more than MAX_RING_FACE_VALENCE faces (or a ring with more than MAX_RING_EDGE_VALENCE edges) thus corrupted the heap in release builds. The ring construction now stops when the ring buffers are exhausted and clamps the evaluation start indices accordingly, so no out of bounds write can happen for any topology. In addition patches whose rings exceed the supported size limits can now get detected in constant time through a flag that is calculated at commit time for every topology. rtcInterpolate and rtcInterpolateN, which did not perform any validity check before, use this flag to reject such patches and return zeros. The ray tracing path already rejected these patches through SubdivMesh::valid. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| PatchType patch_type; //!< stores type of subdiv patch | ||
| VertexType vertex_type; //!< stores type of the start vertex | ||
| char align[2]; | ||
| char valid_sizes; //!< stores if the patch and all its rings are within the supported size limits |
There was a problem hiding this comment.
The validity used by the builder and interpolation is exposed as Topology::valid(). validPatchTopology() is kept as the construction-time topology scan that initializes this bit, rather than a separate hot-path check.
There was a problem hiding this comment.
Changed: valid_patch is now a bool, with the half-edge padding adjusted to preserve its size.
| /*! tests if the ring around the start vertex is within the supported size | ||
| * limits. In contrast to validRing this test only depends on the topology | ||
| * and not on the vertex positions. */ | ||
| __forceinline bool validRingSizes() const |
There was a problem hiding this comment.
I think we do not need that function at all, there is some SubdivMesh::valid() function that is used in the builder to detect valid faces. Same function should also be used to filter out invalid faces for interpolate.
There was a problem hiding this comment.
Agreed that the builder and interpolation should consume the established validity API rather than repeat size checks. The topology-size traversal is retained only during topology construction to precompute the per-topology validity bit; builders see it through SubdivMesh::valid(), and interpolation now checks topo->valid().
|
|
||
| do | ||
| { | ||
| /* stop when the ring buffers are exhausted to avoid writing out of bounds */ |
There was a problem hiding this comment.
Do you understand all these changes in this file? As we now validate the size of the ring upfront, there should be no need for these checks at all!?
There was a problem hiding this comment.
Agreed. I removed these ring-capacity assertions: supported ring sizes are established during topology construction, and invalid patches are filtered through the shared validity checks before ring initialization.
| foreach_unique(valid1,primID,[&](const vbool4& valid1, const unsigned int primID) | ||
| { | ||
| /* patches that exceed the supported valence limits cannot get evaluated */ | ||
| if (unlikely(!topo->getHalfEdge(primID)->hasValidSizes())) |
There was a problem hiding this comment.
Just to be on the save side, pull out topo->getHalfEdge(primID) into variable and use it here and in code below. want to avoid compiler to compute this twice.
There was a problem hiding this comment.
Done. The half-edge pointer is fetched once into halfEdge and reused for the evaluation call.
| /* we have to calculate patch_type last! */ | ||
| HalfEdge::PatchType patch_type = edge->patchType(); | ||
| for (size_t i=0; i<mesh->faceVertices[f]; i++) | ||
| const char valid_sizes = edge->validPatchSizes() ? 1 : 0; |
There was a problem hiding this comment.
Should use the mesh->valid(i) function to tag a valid face/edge. This same function is also used by the builder to filter out invalid faces, we should stay consistent here.
There was a problem hiding this comment.
The builder uses SubdivMesh::valid(), which now reaches the topology validity bit. That bit is intentionally per-topology: using the mesh-wide geometry validity for an attribute topology could reject an otherwise-valid attribute patch. Geometry vertex/hole validity remains tracked separately in invalidFace.
Fold the high-valence subdivision patch check into the existing Topology::valid() path instead of carrying a separate hasValidSizes() query at interpolation sites. The cached half-edge flag is now a bool and interpolation uses the same validity model as the builders, while still checking the selected attribute topology when vertex attributes are bound to a non-default topology. With invalid patches filtered before PatchEval, remove the defensive ring truncation code from the Catmull-Clark ring constructors and keep assertions for the invariants that commit-time validation guarantees. This avoids constructing partially truncated rings and keeps invalid patch handling centralized in the topology validity path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| const HalfEdge* halfEdge = topo->getHalfEdge(primID); | ||
|
|
||
| /* invalid patches cannot get evaluated */ | ||
| if (unlikely(!this->valid(primID) || !topo->valid(primID))) |
There was a problem hiding this comment.
I think here should just be topo->valid(primID)? This->valid() uses the main topology which is for geometry. But even if that is invalid, if the attribute topology is still ok all is fine.
There was a problem hiding this comment.
Agreed. Interpolation now checks only topo->valid(primID), so a valid attribute topology is not rejected solely because the geometry topology is invalid.
| return faceValence <= MAX_RING_FACE_VALENCE && edgeValence <= MAX_RING_EDGE_VALENCE; | ||
| } | ||
|
|
||
| /*! tests if the ring around the start vertex is within the supported size |
There was a problem hiding this comment.
We do not need that function, one can just use topo->valid() ?
There was a problem hiding this comment.
Done for the interpolation paths: they call topo->valid() directly. The topology scan remains only at construction to precompute the validity state that this accessor returns.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| if (!p->validRingTopology()) return false; | ||
| } | ||
| return N >= 3 && N <= MAX_PATCH_VALENCE; | ||
| } |
There was a problem hiding this comment.
This duplicate code is still there.
Keep m128i() and m128d() available when compiling with Emscripten. SSE2 equality, shuffle, and masked integer loads call the explicit accessors regardless of platform. Continue excluding the implicit integer and double conversion operators for Emscripten, and leave the MSVC ARM64 path unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The one-ring construction in CatmullClark1RingT::init() and GeneralCatmullClark1RingT::init() wrote into the fixed capacity ring buffers (MAX_RING_FACE_VALENCE / MAX_RING_EDGE_VALENCE) without any runtime bound, only asserts guarded these writes. A vertex shared by more than MAX_RING_FACE_VALENCE faces (or a ring with more than MAX_RING_EDGE_VALENCE edges) thus corrupted the heap in release builds.
The ring construction now stops when the ring buffers are exhausted and clamps the evaluation start indices accordingly, so no out of bounds write can happen for any topology.
In addition patches whose rings exceed the supported size limits can now get detected in constant time through a flag that is calculated at commit time for every topology. rtcInterpolate and rtcInterpolateN, which did not perform any validity check before, use this flag to reject such patches and return zeros. The ray tracing path already rejected these patches through SubdivMesh::valid.