Stage 2 of the mesh loader's admission gate: post-decode value checks.
These are the checks that genuinely need the materialized arrays, so they run
after fetch + decode — but only ever on data Stage 1
(preflight.ts) already admitted, so the fetch they gate is bounded
before it happens.
Two things are checked, and each closes a hole the other cannot see.
Materialized lengths
Stage 1 vets the declared.zarray shape. A store that declares correctly
but materializes short — a raw or mis-sized chunk, a non-compliant decoder —
would otherwise resurrect exactly the undersized-attribute drawElements
over-read that Stage 1's shape cross-checks exist to close.
Face indices, checked on the SOURCE-TYPED values
Every index must land in [0, V), and the check must be two-sided and run
before the integer→u32 coercion, because each side of that cast hides its
own wrap-around:
an externally produced signed store's -1 passes a one-sided < V
pre-cast check and then wraps to 0xffffffff;
a 64-bit store's 2^32 + 1 survives a check run only after the cast —
it wraps to 1, lands inside [0, V), and silently rewrites topology
instead of trapping.
The two-sided source-value check rejects both. And because Stage 1 admits only
V <= 2^27, every index it passes is preserved bit-for-bit by the u32 cast —
so the values checked here are exactly the values the kernels receive.
Getting this wrong is not a cosmetic bug. An out-of-range index panics the
Rust kernel — the crate is panic = "abort", so the trap escapes as an
opaque, uncatchable RuntimeError: unreachable that takes down the whole WASM
module rather than one node — and silently corrupts the TypeScript backend,
whose out-of-bounds reads yield undefined.
What is deliberately NOT checked
Stage 2 does not finite-scan the float arrays (vertices, normals,
colors, scalars). A non-finite value can neither trap a kernel nor
over-read a buffer, and its blast radius is already per-node without a gate: a
NaN/±Inf coordinate on a hidden dimension hides the vertex (the #806
rule the cull kernels enforce), a non-finite displayed coordinate corrupts
at most that node's rasterization and bounding sphere (which the depth-sort
coordinator already refuses to sort by), non-finite colours are clamped by the
shared shader sanitizers, and a non-finite stored normal degrades only that
node's shading. No sibling loader finite-scans its decoded positions either;
mesh matches that policy rather than inventing a stricter one.
Stage 2 of the mesh loader's admission gate: post-decode value checks.
These are the checks that genuinely need the materialized arrays, so they run after fetch + decode — but only ever on data Stage 1 (
preflight.ts) already admitted, so the fetch they gate is bounded before it happens.Two things are checked, and each closes a hole the other cannot see.
Materialized lengths
Stage 1 vets the declared
.zarrayshape. A store that declares correctly but materializes short — a raw or mis-sized chunk, a non-compliant decoder — would otherwise resurrect exactly the undersized-attributedrawElementsover-read that Stage 1's shape cross-checks exist to close.Face indices, checked on the SOURCE-TYPED values
Every index must land in
[0, V), and the check must be two-sided and run before the integer→u32 coercion, because each side of that cast hides its own wrap-around:-1passes a one-sided< Vpre-cast check and then wraps to0xffffffff;2^32 + 1survives a check run only after the cast — it wraps to1, lands inside[0, V), and silently rewrites topology instead of trapping.The two-sided source-value check rejects both. And because Stage 1 admits only
V <= 2^27, every index it passes is preserved bit-for-bit by the u32 cast — so the values checked here are exactly the values the kernels receive.Getting this wrong is not a cosmetic bug. An out-of-range index panics the Rust kernel — the crate is
panic = "abort", so the trap escapes as an opaque, uncatchableRuntimeError: unreachablethat takes down the whole WASM module rather than one node — and silently corrupts the TypeScript backend, whose out-of-bounds reads yieldundefined.What is deliberately NOT checked
Stage 2 does not finite-scan the float arrays (
vertices,normals,colors,scalars). A non-finite value can neither trap a kernel nor over-read a buffer, and its blast radius is already per-node without a gate: aNaN/±Infcoordinate on a hidden dimension hides the vertex (the #806 rule the cull kernels enforce), a non-finite displayed coordinate corrupts at most that node's rasterization and bounding sphere (which the depth-sort coordinator already refuses to sort by), non-finite colours are clamped by the shared shader sanitizers, and a non-finite stored normal degrades only that node's shading. No sibling loader finite-scans its decoded positions either; mesh matches that policy rather than inventing a stricter one.