SIGGRAPH 2023 shipped an unfuzzed ingest path
Gaussian splats don't break browser memory protection. Their untrusted parsers do: integer overflow to OOB write in splat viewers, CWE-190 into CWE-787.
3D Gaussian splatting renders a scene as several million anisotropic Gaussians. Each stores a position, a covariance assembled from a scale vector and a rotation quaternion, an opacity, and spherical harmonic coefficients for view-dependent color. Kerbl et al. published the method at SIGGRAPH 2023. Browser viewers followed within months. Adoption moved fast. Real-estate walkthroughs, cultural-heritage scans, product viewers, VR capture. Every one embeds a web viewer that an untrusted user can point at an untrusted file. The security-relevant surface lives there, and it is not where the premise puts it.
Start with the correction. Gaussian splats do not bypass memory protection. A splat is a data format and a rasterization technique. Nothing in the math defeats ASLR, DEP, or the V8 sandbox. The claim that the format reliably produces out-of-bounds writes by design is false. The accurate version is narrower and more useful. The pipeline that ingests untrusted splat data is the exposure. Parser, typed-array packing, GPU upload, per-frame depth sort. That path is young, hand-rolled, memory-unsafe, and barely fuzzed. The risk is ingest. Not the rendering.
Look at the formats. The reference release used .ply, a thirty-year-old format with an ASCII header that declares element counts and a per-element property list, followed by a binary body. Compressed variants followed. The .splat layout packs a splat into 32 bytes. mkkellogg’s .ksplat adds bucketed quantization. Niantic open-sourced .spz. Every one of these is parsed client-side, in JavaScript or a WASM module, before a single Gaussian reaches the GPU. The browser’s own memory-safe asset decoders are not in this path. The viewer library wrote its own. WASM parsers compile C or Rust to linear memory, and a C parser with no bounds checks carries them into the browser unchanged.
The bug class is integer overflow leading to a heap out-of-bounds write. CWE-190 into CWE-787. The mechanism is specific. The parser reads a vertex count and a per-vertex stride from the header. It computes an allocation size as count multiplied by stride. Both values are attacker-controlled through a crafted file. An uncompressed splat at spherical harmonic degree three carries fifty-nine floats. Three position, three scale, four rotation, one opacity, forty-eight SH coefficients. Roughly 236 bytes of stride. Multiply a thirty-two-bit count near 2^24 by that stride and the product passes 2^32 and wraps. The allocation returns undersized. The population loop still writes the full declared count. The writes run off the end of the backing store into adjacent heap. That is a deterministic OOB write driven by header fields, reachable with no understanding of V8 internals. The premise is right that it is reliable and wrong about why. The reliability comes from unchecked size arithmetic in a custom parser, not from anything inherent to splats.
Reaching code execution from that write is the established part. In V8, the high-value targets next to a controlled buffer are an ArrayBuffer backing-store pointer and a TypedArray length field. An OOB write that lands on a length field turns a bounded array into an unbounded one. From there the attacker has relative read/write inside the renderer, promotes it with the standard addrof and fakeobj construction, leaks pointers to defeat ASLR, and assembles an arbitrary read/write primitive. These techniques have been public since 2017. None are novel. The parser only has to place the first corrupting write in a predictable location, and grooming the heap with millions of fixed-size splat records is well suited to shaping that layout.
The GPU stages add a second surface. A splat scene is depth-sorted every frame. Viewers do this with a radix sort, sometimes in WASM linear memory, increasingly in a WebGPU compute shader. Index and instance counts flow from the same untrusted header into draw calls and buffer bindings. WASM linear-memory corruption stays inside the module sandbox, but it still corrupts the JS-visible state the module exports and feeds malformed sizes into the graphics API. Below the API sits ANGLE, translating WebGL to D3D, Metal, or Vulkan, and SwiftShader, the CPU fallback when no GPU is present. Both are native C++ with their own history of out-of-bounds writes. CVE-2023-2929 and the SwiftShader cluster around it are out-of-bounds writes in exactly that translation layer. A malformed splat draw that drives a degenerate buffer size is a plausible trigger into that code.
WebGPU widens this further. Dawn is Chrome’s WebGPU implementation, native C++, exposing compute pipelines that splat viewers increasingly use for sorting and projection. WebGPU hands the page direct control over buffer allocation, bind-group layouts, and dispatch dimensions. Validation is supposed to reject out-of-range bindings. Validation has bugs. Each compute path the viewer drives is a new sequence of buffer sizes and dispatch counts the Dawn validator and the underlying driver must handle correctly, and the inputs to those sizes trace back to the same untrusted header.
The process boundary is what makes the GPU path attractive. A renderer compromise still sits inside Chrome’s restricted token, job object, and seccomp-bpf filter. WebGL and WebGPU calls cross into the GPU process, which historically runs with weaker confinement because it needs driver and graphics-kernel access. A memory-corruption bug reached through the graphics API executes closer to the host than the same bug in the renderer. That is why WebGPU and ANGLE bugs keep showing up in escape chains. Splat rendering exercises the renderer JIT path through V8 typed arrays and the GPU path through WebGL or WebGPU, in one page load, against parsers that did not exist two years ago.
Map it to delivery. The hostile asset is a file, not a script, so it moves through channels that asset files are trusted on. A malicious page hosting a viewer and a crafted scene is MITRE T1189, drive-by compromise. The corruption-to-execution step is T1203, exploitation for client execution. Post-exploitation injection and process activity falls under T1055 and T1059. The file can also arrive through T1608, staged on a compromised CDN or model host and pulled by a legitimate viewer that trusts the origin. The viewer does not validate that the .ply it fetched is benign. It validates that it parsed.
There is no confirmed in-the-wild campaign against splat parsers. State that plainly. The precedent that matters is CVE-2023-4863, the libwebp heap buffer overflow, CVSS 8.8, weaponized as a zero-click against image parsing and tied to commercial spyware delivery. The shape is identical. Untrusted binary asset, complex format, native or WASM decoder, no memory safety in the decode path, broad reach because every product embeds the same handful of libraries. Splat viewers are at the stage libwebp was before anyone audited it. A few dominant open-source implementations, embedded widely, parsing attacker-supplied files, with thin fuzzing coverage. That is the condition that produces an asset-parser zero-day, and it is present now.
Telemetry is where defenders should set expectations honestly. The corruption happens inside the browser’s address space. No new process, no file write, no outbound connection at the moment of exploitation. Sysmon does not see a heap overflow. Endpoint sensors do not hook V8’s allocator. What surfaces first is a crash. A killed GPU process or a renderer fault shows up as a Windows Application Error, Event ID 1000, and in Chrome’s own crash reporting. Repeated GPU process termination tied to one origin is a weak signal worth correlating, not an alert by itself. The first real detection opportunity is the second stage. When chrome.exe spawns a child or injects, Sysmon Event ID 1 and Event ID 10 carry it, mapping to T1055 and T1059. The network IOC is the asset fetch. A .ply, .splat, .ksplat, or .spz pulled over HTTPS from an origin with no business serving 3D scenes, anomalously sized or malformed against the format spec. Before execution, the defender is blind by construction. The corruption lives in memory the EDR does not instrument.
A detection worth building keys on the sequence, not the crash. Asset fetch of a splat extension or matching magic bytes from an origin outside an allow-list, followed in the same browser session by a GPU or renderer crash, followed by any child process under the browser. Each event alone is noise. The ordered chain is not. Subresource integrity on the viewer bundle and a content-security-policy that constrains asset origins do not stop a parser overflow, but they shrink the set of origins that can deliver one, which is what makes the network signal tractable.
Close on the patch boundary, because it is split. Bugs in V8, ANGLE, SwiftShader, and Dawn ship fixes in Chrome Stable point releases, and a current browser closes them. That is the covered half. The uncovered half is the viewer library. GaussianSplats3D, gsplat.js, antimatter15/splat, and the custom WASM parsers behind them do their own size arithmetic and their own buffer population, outside Chrome’s patch cadence. A browser update does not fix an integer overflow in third-party JavaScript. That code is versioned by whoever embedded it, updated rarely, audited less. The residual exposure is the entire client-side ingest path for a file format that is two years old, hand-parsed across a dozen implementations, and fuzzed by almost no one. Patch boundary on the engine. Open boundary on the parser. The premise had the conclusion before it had the mechanism. The mechanism is the parser, and the parser is the part nobody owns.
Keep Reading
linux kernelKees Cook deleted strncpy from the Linux kernel
Linux removed strncpy after six years and 360+ patches. The mechanism: missing NUL termination, out-of-bounds reads, kernel info leaks, and why strscpy replaces it.
linux kernelLinux kernel deleted strncpy across 360 patches
Linux removed strncpy across 360 patches over six years. The exposure: a bounded write primitive used as a safety control it never implemented.
legacy systems1992 hardware, no MMU, every payload lands
The Game Boy Work Boy exposes a system with no MMU, DEP, or ASLR - flat executable memory and a fixed layout where any write becomes code execution.
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.