← All issues

[16] ANGLE Metal: initialize missing fragment shader output components

Severity: Medium | Component: ANGLE MSL translator (ModifyStruct) | 3043c63

Rated Medium because the observable effect is uninitialised G/B/A bytes from a narrow fragment output reaching readPixels (per-pixel one-byte/four-byte residue), passively readable from any web origin; controllability is low (depends on Metal register allocator state).

Source/ThirdParty/ANGLE/src/compiler/translator/msl/ModifyStruct.cpp

+ for (uint8_t d = dim; d < saturation; ++d)
+ {
+ state.addConversion([=](Access::Env &, OriginalAccess &, ModifiedAccess &m) {
+ auto &m_ = AccessIndex(m, d);
+ return Access{*CreateZeroNode(m_.getType()), m_};
+ });
+ }

A second loop in SaturateScalarOrVectorCommon emits zero-initialisation conversion lambdas for component indices [dim, saturation) — components present in the expanded Metal-friendly struct but absent from the original GLSL fragment shader output. For each missing component d, it registers a conversion wrapping AccessIndex(m, d) with CreateZeroNode of the matching type. A regression test declares layout(location=0) out float outColor;, draws to an RGBA target, and asserts readPixels returns (255, 0, 0, 0).

Uninitialized expanded-output components in a shader-translation widening pass leak through to attacker-readable framebuffer pixels.

WebGL2 lets a fragment shader declare an output with fewer components than the bound color attachment; the spec requires missing components to read as zero. To satisfy Metal's stricter struct-layout rules, ANGLE's ModifyStruct pass widens the user's output struct: ConvertStructState accumulates ConversionFunc lambdas via state.addConversion(...) that the pass later emits as MSL assignments from original to modified struct fields. SaturateScalarOrVectorCommon widens a scalar/vector field from dim to saturation.

Pre-fix the helper emitted conversion lambdas only for [0, dim) — copying outColor into modified[0]. The components [dim, saturation) had no conversion registered, so the generated MSL wrote whatever the shader's local/register state contained into the framebuffer.

🔒

Detailed look at how a shader-translation widening pass turns into a cross-process information-disclosure primitive readable from JavaScript, and the bounds of what an attacker can actually scrape.

Subscribe to read more

🔒

Multiple reusable audit patterns identified — translation-layer widening, readback-surface guarantees, and cross-backend parity — each with concrete starting points in ANGLE's translator tree.

Subscribe to read more