Skip to content

What the kernel trusts, held by the data: invariants in the builders, input finite where it enters, checks in tests - #79

Merged
yujiaxu-ops merged 2 commits into
mainfrom
kernel-data
Nov 14, 2026
Merged

yujiaxu-ops merged 2 commits into
mainfrom
kernel-data

Conversation

@yujiaxu-ops

@yujiaxu-ops yujiaxu-ops commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

The kernel follows indices it reads from buffers and checks none of them, so until now the loop guard was the only thing between bad data and a dispatch that never ends. I checked what the references do: pbrt-v4, Embree, Cycles, three-mesh-bvh, Falcor, the DXR and Vulkan specs, wgpu/naga/Tint. They all do three things:

  • hold the invariants in the builders (children after the parent, a depth cap tied to the traversal stack, forced or median leaves);
  • clean input where it enters (Embree and Cycles drop non-finite primitives, DXR makes them inactive);
  • keep structural checks to debug builds and tests.

None of them checks a tree before every upload, and none caps a traversal's iterations: the 2^64 loop bound that naga and Tint insert does not stop a hang, and clamping a bad child index can close a cycle. This PR does the same three things here.

What changes

Builders

  • bvh.rs: no tree is deeper than the kernel's 48-entry stack. Where the levels left could not finish the split SAH wants, the builder splits at the median instead.
  • Items whose box is not finite go in no leaf. They sit after every leaf's range and keep their index.
  • check_tree (moved here from units.rs) runs on every tree in tests and debug builds.
  • A build reports its depth (bvh_depth in the load timings). Today's scenes: Bistro 18, Kabukichō 26, the district 33. No current tree changes.
  • ltree.rs: LightTree::check runs after every build, growth and refit in tests and debug builds. It checks that children come after their parent, that leaves stay inside the entries, and that each entry's code leads to its own leaf. Note that the light tree's walks end whatever the data, because their loops are bounded by the encoding. The risk the check covers is out-of-range reads.

Where data enters

  • World::set_geometry refuses positions that are not finite.
  • A volume's majorant must be a finite number > 0, in Rust and in packages/scene. Infinity used to pass both checks, and delta tracking then never stepped: the walk through the volume never ended.
  • A world matrix that is not finite places its object at a point, the same convention scene::instance_world already uses for hidden instances. Transforms are still kept as set.
  • scene::blas_tri makes the hardware acceleration structures' vertices finite. DXR and Vulkan leave a non-finite vertex as undefined behaviour and define a degenerate triangle as never hit, so a triangle with a non-finite vertex becomes three vertices at the origin and keeps its index.

Tests and docs

  • Garbage-input tests for the BVH builder and the light tree: NaN, infinite and near-limit values, a thousand copies of one triangle, a needle, garbage in the slot for emitters added in place.
  • A depth-cap test: boxes spaced in powers of two make SAH deep; the capped tree stays within the cap and is unchanged where it fits.
  • Tests for the input checks, in Rust and TS.
  • docs/RUNTIME.md has a new section, "What the kernel trusts", with a table of every structure the kernel walks and what holds it. There is also a ROADMAP record.

Checks

  • cargo test --release with the loop guard (GPU tests included): 138 passed.

  • render-identity against main, 190 outputs each, guarded runs first:

    run byte-identical within run-to-run spread changed
    native + guard 175 15 0
    merged + guard 171 19 0
    software traversal (THREERT_HWRT=0) + guard 171 19 0
    native, no guard 172 18 0
  • tools/web-hello.ts in Chrome: pass.

  • Load time: the district's tree builds in 333 ms against main's 321 ms, one load each. An earlier load of the same build took 338 ms, so the difference is within run-to-run variation.

  • No shader changed.

🤖 Generated with Claude Code


Note

Medium Risk
Changes core BVH construction and scene-prep invariants that can affect traversal correctness and performance, though shaders are unchanged and identity checks report no output drift.

Overview
Shifts responsibility for safe GPU traversal from the browser loop guard alone to data the kernel consumes: BVH/light-tree builders enforce walk-ending structure, and inputs are sanitized where they enter the pipeline.

BVH (bvh.rs) trees are capped to the kernel’s 48-entry stack, SAH splits fall back to median splits when depth would exceed the cap, triangles/boxes with non-finite bounds are excluded from leaves (kept in order after leaf ranges), check_tree lives in bvh.rs and runs in tests/debug builds, and builds report bvh_depth in load timings.

Light tree (ltree.rs) adds LightTree::check (topology, leaf ranges, path codes) after build/growth/refit in tests/debug.

Input gates: finite geometry positions (World::set_geometry), finite volume majorant > 0 (Rust + VolumeMesh in TS), non-finite world matrices collapse to a point (like hidden instances), and scene::blas_tri replaces non-finite BLAS vertices with origin degenerates for hardware RT upload paths.

Adds garbage-input / depth-cap tests and documents the contract in docs/RUNTIME.md (“What the kernel trusts”) and docs/ROADMAP.md.

Reviewed by Cursor Bugbot for commit b0672ee. Bugbot is set up for automated code reviews on this repo. Configure here.

yujiaxu-ops and others added 2 commits November 14, 2026 13:00
…s, input is finite where it enters, checks in tests and debug builds

The kernel follows indices from buffers and checks none of them; the loop guard was the only thing between bad
data and a dispatch that never ends. The references (pbrt-v4, Embree, Cycles, three-mesh-bvh, Falcor, the DXR and
Vulkan specs, wgpu) hold the invariants in their builders, clean input where it enters, and keep structural checks
to debug builds and tests. The same here:

- bvh.rs: trees no deeper than the kernel's stack (48; where the levels left could not finish the split SAH wants,
  the median); items whose box is not finite in no leaf, after every leaf's range, their index kept; check_tree
  (moved from units.rs) on every tree in tests and debug builds; a build reports its depth (bvh_depth in the load
  timings). Bistro 18, Kabukicho 26, the district 33: no tree of today's scenes changes.
- ltree.rs: LightTree::check (children after the parent, leaves inside the entries, each entry's code leads to its
  leaf) after every build, growth and refit in tests and debug builds.
- Where data enters: World::set_geometry refuses positions that are not finite; a volume's majorant must be a finite
  number > 0 (Rust and packages/scene: Infinity passed both, and delta tracking never stepped); a world matrix that
  is not finite places its object at a point, as a hidden instance is; the hardware acceleration structures'
  vertices are finite (scene::blas_tri: a triangle with a vertex that is not becomes a degenerate one, its index
  kept).
- Garbage-input tests for the BVH builder and the light tree (NaN, infinite and huge values, coincident and
  collinear items), a depth-cap test, the input checks' tests.
- docs/RUNTIME.md "What the kernel trusts"; ROADMAP record.

Checks: cargo test --release with the loop guard, 138 passed; render-identity against main guarded (native, merged,
software traversal) and native: 0 of 190 changed; web-hello in Chrome passes. No shader changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reduced box

CI (x86_64) failed garbage_in_makes_a_tree_the_kernel_walks where this Mac passed it: a min or max with a NaN gives
the NaN under NEON and the other operand under SSE, so on x86 a triangle with one NaN vertex got a finite box and
went into a leaf. A triangle is now finite when its three vertices are, a box when its six numbers are; one that is
not gets the empty box, which no leaf takes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yujiaxu-ops
yujiaxu-ops merged commit b14f98c into main Nov 14, 2026
9 checks passed
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