Skip to content

Fix out-of-bounds UDF read causing non-deterministic mesh corruption - #39

Open
rwfsmith wants to merge 1 commit into
JeffreyXiang:mainfrom
rwfsmith:fix-oob-udf-read
Open

Fix out-of-bounds UDF read causing non-deterministic mesh corruption#39
rwfsmith wants to merge 1 commit into
JeffreyXiang:mainfrom
rwfsmith:fix-oob-udf-read

Conversation

@rwfsmith

Copy link
Copy Markdown

Summary

get_vertex_val() in src/remesh/simple_dual_contour.cu uses the result of a hashmap lookup as an array index without checking whether the lookup succeeded. On a miss this reads far out of bounds of udf.

This is undefined behavior on all platforms — it isn't Windows- or Blackwell-specific. It just happens to be visible when the out-of-bounds memory contains something that flips a comparison.

The bug

uint32_t idx = linear_probing_lookup(hashmap_keys, hashmap_vals, key, N_vert);
return udf[idx];

linear_probing_lookup signals "not found" with a sentinel (src/hash/hash.cuh):

if (prev == std::numeric_limits<K>::max()) {
    return std::numeric_limits<V>::max();
}

With V = uint32_t that's UINT32_MAX, so a miss makes the line above evaluate udf[4294967295] — a read roughly 16 GB past the base pointer.

A miss is reachable in normal use: get_vertex_val is called for all 8 corners of each voxel, and corners just outside the voxelized narrow band were never inserted into the hashmap.

Worth noting this is the only call site that dereferences the result directly. The two call sites in src/hash/hash.cu just store the returned value and let the caller handle the sentinel, which suggests this one is an oversight rather than an intentional invariant.

Impact

Whatever garbage lands in idx's slot participates in the isosurface crossing test, so it can inject spurious dual-contouring vertices and edge intersections. Downstream in TRELLIS.2 this showed up as to_glb(..., remesh=True) non-deterministically producing shredded/spiky meshes — the same code and the same latents would give a clean mesh on one run and a corrupted one on the next, because the result depended on whatever happened to be in that memory at the time.

That non-determinism is what makes it worth fixing rather than working around: it's silent, intermittent, and very easy to misattribute to the model.

Fix

Treat a missing corner as "far outside the surface" so the edge simply produces no intersection, instead of reading out-of-bounds memory:

if (idx == std::numeric_limits<uint32_t>::max()) {
    return 1e6f;
}

9 added lines (mostly the explanatory comment), one file, no API or behavior change for corners that are present.

Verification

Reproduced and confirmed fixed on Windows 11 / CUDA 13.0 / RTX 5090 via TRELLIS.2's to_glb(remesh=True): corrupted exports across repeated runs before, consistently clean meshes after. Extracted GLBs check out (e.g. 808k verts / 973k faces, 0 NaNs, 6 degenerate faces, sane extents).

I have not built on Linux, but the change is platform-independent and only adds a bounds guard on a path that was previously UB.

get_vertex_val() looked up a voxel corner's UDF value via the hashmap but
never checked whether the lookup actually found the key. When a corner
lies just outside the voxelized narrow band, linear_probing_lookup
returns the sentinel UINT32_MAX, and udf[UINT32_MAX] reads far out of
bounds GPU memory. The resulting garbage value could push the isosurface
crossing test either way, non-deterministically injecting bogus dual
contouring vertices/intersections into the remeshed output.

This explained the shredded/spiky mesh corruption seen when using
o_voxel's to_glb(remesh=True) on Windows/Blackwell: identical code and
latents sometimes produced a clean mesh and sometimes produced garbage,
because the corruption depended on whatever was in that out-of-bounds
GPU memory at the time.

Fix: treat a missing hashmap entry as "far outside the surface" (a large
positive UDF value) so no bogus intersection is produced for that edge,
instead of reading uninitialized/out-of-bounds memory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

1 participant