CVE-2026-7903
Overview
Background
- ANGLE
- Chrome’s library that translates OpenGL ES calls onto native backends such as Direct3D 11 (
d3d11), used to service WebGL from untrusted web content. - Staging / conversion buffer
- an intermediate CPU-side allocation (
conversionBuffer) ANGLE fills when an uploaded texture’s pixel format must be converted before being handed to D3D11. - `CheckedNumeric`
- an
anglebase/basenumerics helper that performs arithmetic while tracking overflow, exposingIsValid<T>()andValueOrDie<T>()so a result can be range-validated before use. - `ANGLE_CHECK_GL_MATH`
- an ANGLE macro that converts a failed math/overflow validation into a GL error return rather than letting a bad value flow downstream.
Root Cause Analysis
The texture data-conversion path in TextureStorage11.cpp computed the intermediate buffer geometry with native-width integers: bufferRowPitch was static_cast<unsigned int>(outputPixelSize) * width (a 32-bit UINT), bufferDepthPitch was bufferRowPitch * height, and neededSize was bufferDepthPitch * depth. Because width, height, and depth are attacker-influenced (bounded only by GL_MAX_TEXTURE_SIZE and the chosen internal format’s pixel size), these products can exceed 32 bits and wrap, so neededSize can be far smaller than the real byte count required. The undersized value was then passed to getScratchMemoryBuffer, allocating a conversion buffer too small for the data the subsequent loadFunction writes using the (also wrapped) bufferRowPitch/bufferDepthPitch.
The fix redoes every multiplication in 64-bit CheckedNumeric<uint64_t> and gates allocation on ANGLE_CHECK_GL_MATH(... checkedNeededSize.IsValid<size_t>() && checkedBufferRowPitch.IsValid<UINT>() && checkedBufferDepthPitch.IsValid<UINT>()), so an overflowing size produces a GL error instead of a truncated allocation. It also moves the computation inside the if (loadFunctionInfo.requiresConversion) branch, so the math only runs when a conversion buffer is actually needed.
UINT) integers over dimensions the caller controls, so the product silently wrapped to a small value; the fix computes in checked 64-bit and refuses to proceed unless the results fit their destination types.Attack Path
- Issue a large conversion-requiring upload
Web content calls
glTexImage2D/glTexSubImage2D(or the 3D variants) with near-maximum dimensions and a wide internal format (e.g.GL_RGB32F) whosegetLoadFunctions()reportsrequiresConversion. - Overflow the pitch/size math
outputPixelSize * width * height * depthexceeds 2^32, so the 32-bitbufferRowPitch/bufferDepthPitchand the derivedneededSizewrap to values much smaller than the true data size. - Allocate an undersized conversion buffer
getScratchMemoryBufferis given the wrappedneededSizeand returns aconversionBuffertoo small for the pixels about to be written. - Write past the allocation
loadFunctionInfo.loadFunction(...)writes converted pixels intoconversionBuffer->data()using the realwidth/height/depth, producing a heap out-of-bounds write (inferred; the shipped test only asserts the operation does not crash).
Impact Assessment
LargeTextureOverflow verifies only that the max-size upload no longer crashes). The advisory rates this high severity and labels it an integer overflow in ANGLE; it does not report in-the-wild exploitation. Preconditions are a D3D11 backend and an upload whose format requires conversion at near-maximum dimensions, all reachable from an ordinary web page.Files Changed
src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cppsrc/tests/gl_tests/TextureTest.cpp
Audit Directions
- Allocation-size arithmetic in narrow integersflag any
UINT/int/size_tproduct of width × height × depth × bytes-per-pixel (or pitch × rows) used as an allocation size without aCheckedNumeric/CheckMulguard, especially in the other D3D and GL backend upload paths. - Compute-before-branch orderinglook for size math executed before the branch that decides whether the buffer is even allocated, since the overflow can occur regardless of the later
requiresConversiondecision. - Loader/pitch mismatchaudit callers of
loadFunctionfamily routines where the destination buffer size and the row/depth pitches are derived from separate, independently-overflowing computations.
Patch
From 88ff9f4e6924c230bc924ab7dd9ab1f78068bf12 Mon Sep 17 00:00:00 2001 From: Geoff Lang <[email protected]> Date: Mon, 23 Mar 2026 17:32:05 -0400 Subject: [PATCH] D3D11: Protect against overflows for texture staging buffers When TextureStorage11 needs to calculate sizes of intermediate buffers for data conversion, do the math in 64-bit using CheckedNumerics. Validate that the results fit into 32 bits. Bug: chromium:491760376 Change-Id: Ie5942291fe7790c229cb1070fa9a2325fa40ac2f Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7695232 Reviewed-by: Shahbaz Youssefi <[email protected]> Commit-Queue: Geoff Lang <[email protected]> --- diff --git a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp index ebe2cdf..2088634 100644 --- a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp +++ b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp @@ -17,6 +17,7 @@ #include <tuple> #include "common/MemoryBuffer.h" +#include "common/base/anglebase/numerics/checked_math.h" #include "common/utilities.h" #include "image_util/loadimage.h" #include "libANGLE/Context.h" @@ -902,22 +903,35 @@ const d3d11::Format &d3d11Format = d3d11::Format::Get(image->getInternalFormat(), mRenderer->getRenderer11DeviceCaps()); - const d3d11::DXGIFormatSize &dxgiFormatInfo = - d3d11::GetDXGIFormatSizeInfo(d3d11Format.texFormat); - const size_t outputPixelSize = dxgiFormatInfo.pixelBytes; - - UINT bufferRowPitch = static_cast<unsigned int>(outputPixelSize) * width; - UINT bufferDepthPitch = bufferRowPitch * height; - - const size_t neededSize = bufferDepthPitch * depth; angle::MemoryBuffer *conversionBuffer = nullptr; const uint8_t *data = nullptr; + UINT bufferRowPitch = 0; + UINT bufferDepthPitch = 0; LoadImageFunctionInfo loadFunctionInfo = d3d11Format.getLoadFunctions()(type); if (loadFunctionInfo.requiresConversion) { - ANGLE_TRY(mRenderer->getScratchMemoryBuffer(context11, neededSize, &conversionBuffer)); + const d3d11::DXGIFormatSize &dxgiFormatInfo = + d3d11::GetDXGIFormatSizeInfo(d3d11Format.texFormat); + + const size_t outputPixelSize = dxgiFormatInfo.pixelBytes; + + angle::CheckedNumeric<uint64_t> checkedBufferRowPitch = outputPixelSize; + checkedBufferRowPitch *= static_cast<uint64_t>(width); + + angle::CheckedNumeric<uint64_t> checkedBufferDepthPitch = checkedBufferRowPitch * height; + angle::CheckedNumeric<uint64_t> checkedNeededSize = checkedBufferDepthPitch * depth; + + ANGLE_CHECK_GL_MATH(context11, checkedNeededSize.IsValid<size_t>() && + checkedBufferRowPitch.IsValid<UINT>() && + checkedBufferDepthPitch.IsValid<UINT>()); + + bufferRowPitch = checkedBufferRowPitch.ValueOrDie<UINT>(); + bufferDepthPitch = checkedBufferDepthPitch.ValueOrDie<UINT>(); + + ANGLE_TRY(mRenderer->getScratchMemoryBuffer( + context11, checkedNeededSize.ValueOrDie<size_t>(), &conversionBuffer)); loadFunctionInfo.loadFunction(mRenderer->getDisplay()->getImageLoadContext(), width, height, depth, pixelData + srcSkipBytes, srcRowPitch, srcDepthPitch, conversionBuffer->data(), bufferRowPitch, bufferDepthPitch); diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp index 7137425..11cb22f 100644 --- a/src/tests/gl_tests/TextureTest.cpp +++ b/src/tests/gl_tests/TextureTest.cpp @@ -17393,6 +17393,23 @@ } } +// Test that uploading a max size texture with large formats does not crash. +TEST_P(Texture2DTestES3, LargeTextureOverflow) +{ + // Some backends generate internal errors for OOM. That's OK as long as they don't crash. + ScopedIgnorePlatformMessages ignore; + + GLint maxTextureSize; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize); + + GLTexture texture; + glBindTexture(GL_TEXTURE_2D, texture); + + // This may succeed or generate an error about overflows but it should not crash. + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, maxTextureSize, maxTextureSize, 0, GL_RGB, GL_FLOAT, + nullptr); +} + // Create an integer format texture but specify a FLOAT sampler. OpenGL // tolerates this but it causes a Vulkan validation error. TEST_P(Texture2DTestES3, TexImageFormatMismatch)
Regression Test / PoC
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 7137425..11cb22f 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -17393,6 +17393,23 @@
}
}
+// Test that uploading a max size texture with large formats does not crash.
+TEST_P(Texture2DTestES3, LargeTextureOverflow)
+{
+ // Some backends generate internal errors for OOM. That's OK as long as they don't crash.
+ ScopedIgnorePlatformMessages ignore;
+
+ GLint maxTextureSize;
+ glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize);
+
+ GLTexture texture;
+ glBindTexture(GL_TEXTURE_2D, texture);
+
+ // This may succeed or generate an error about overflows but it should not crash.
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, maxTextureSize, maxTextureSize, 0, GL_RGB, GL_FLOAT,
+ nullptr);
+}
+
// Create an integer format texture but specify a FLOAT sampler. OpenGL
// tolerates this but it causes a Vulkan validation error.
TEST_P(Texture2DTestES3, TexImageFormatMismatch)
Original Bug Report
Integer overflow in ANGLE D3D11 TextureStorage11::setData() leads to heap buffer overflow via WebGL2
Security Bug
Important: Please do not change the component of this bug manually.
Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md
Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs
Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp
NOTE: Security bugs are normally made public once a fix has been widely deployed.
VULNERABILITY DETAILS
Integer overflow in ANGLE D3D11 backend TextureStorage11::setData() leads to heap buffer overflow in the GPU process via WebGL2. This vulnerability primarily affects Chrome on Windows (D3D11 is the default ANGLE backend).
When a WebGL2 texture with RGB32F internal format is uploaded via texSubImage2D, ANGLE’s D3D11 backend converts the source data from RGB to RGBA format. The destination staging buffer size calculation at TextureStorage11.cpp:910-913 uses UINT (uint32_t) arithmetic that silently overflows, producing a zero-byte allocation. The subsequent LoadToNative3To4 call writes up to 4 GB of data into this zero-byte buffer, causing a heap buffer overflow in the GPU process.
This is triggerable from any web page using WebGL2 JavaScript, with no user interaction beyond visiting the page. A GPU with >= 4 GB VRAM is required.
ROOT CAUSE
File: third_party/angle/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp Function: TextureStorage11::setData() Lines: 910-913
Vulnerable code:
UINT bufferRowPitch = static_cast<unsigned int>(outputPixelSize) * width;
UINT bufferDepthPitch = bufferRowPitch * height;
const size_t neededSize = bufferDepthPitch * depth;
Type analysis (C++ usual arithmetic conversions):
Line 910: bufferRowPitch static_cast<unsigned int>(outputPixelSize) * width = (unsigned int)16 * (int)16384 -> unsigned int promotion: 16u * 16384u = 262,144 Fits in UINT. Correct.
Line 911: bufferDepthPitch bufferRowPitch * height = (UINT)262,144 * (int)16384 -> unsigned int promotion: 262,144u * 16384u = 4,294,967,296 = 2^32 -> WRAPS TO 0 (defined behavior for unsigned int)
Line 913: neededSize bufferDepthPitch * depth = (UINT)0 * (int)1 -> unsigned int: 0u * 1u = 0 -> zero-extended to size_t: neededSize = 0
The overflowed neededSize (0) is passed to getScratchMemoryBuffer() at line 920, which allocates a zero-byte (or minimal) scratch buffer. Then the load function LoadToNative3To4 at line 921-923 iterates over all 16384*16384 pixels, writing 4 components * sizeof(float) = 16 bytes per pixel using the CORRECT (non-overflowed) bufferRowPitch of 262,144. This writes 16384 * 262,144 = 4,294,967,296 bytes (4 GB) to the zero-byte buffer, causing a massive heap buffer overflow.
Key detail: bufferRowPitch (262,144) is correct and does NOT overflow. The load function uses it to compute write offsets for each row: Row 0: offset 0 Row 1: offset 262,144 Row 16383: offset 4,294,705,152 Each row writes 262,144 bytes. Total written: ~4 GB to a ~0-byte buffer.
TRIGGER PATH (WebGL2 JavaScript -> GPU process heap overflow)
-
Create a TEXTURE_2D with gl.RGB32F internal format at 16384x16384: gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB32F, 16384, 16384);
On D3D11, ANGLE maps GL_RGB32F to DXGI_FORMAT_R32G32B32A32_FLOAT (confirmed in texture_format_map.json: “GL_RGB32F”: “R32G32B32A32_FLOAT”). The D3D11 format has pixelBytes = 16 (128 bits / 8).
-
Upload texture data with RGB/FLOAT format: gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 16384, 16384, gl.RGB, gl.FLOAT, data);
The source format is RGB FLOAT (12 bytes/pixel). The D3D11 destination format is RGBA FLOAT (16 bytes/pixel). The load function RGB32F_to_R32G32B32A32_FLOAT(GL_FLOAT) returns: LoadToNative3To4<GLfloat, gl::Float32One> with requiresConversion = true (confirmed in load_functions_table_autogen.cpp:3189-3194)
-
Since requiresConversion is true, TextureStorage11::setData() takes the conversion branch (line 918-924): a. Line 908: outputPixelSize = dxgiFormatInfo.pixelBytes = 16 b. Line 910: bufferRowPitch = 16 * 16384 = 262,144 (fits in UINT) c. Line 911: bufferDepthPitch = 262,144 * 16384 = 2^32 -> WRAPS TO 0 d. Line 913: neededSize = 0 * 1 = 0 e. Line 920: getScratchMemoryBuffer(context11, 0, &conversionBuffer) -> allocates 0-byte (or minimal) scratch buffer f. Line 921-923: LoadToNative3To4 writes 4 GB to 0-byte buffer -> HEAP BUFFER OVERFLOW
ADDITIONAL OVERFLOW INSTANCES (same pattern)
a) BlitGL.cpp:831-833 - copySubTexture() [OpenGL backend] size_t destBufferSize = readPixelsArea.width * readPixelsArea.height * destInternalFormatInfo.pixelBytes; Type chain: int * int -> int (268M, fits), then int * GLuint -> unsigned int For pixelBytes=16 at 16384x16384: unsigned int overflow to 0. Affects Chrome on Linux with native OpenGL (non-Vulkan ANGLE).
b) FrameBufferMtl.mm:82 - CopyTextureSliceLevelToTempBuffer() [Metal backend] uint32_t sizeInBytes = width * height * angleFormat.pixelBytes; All three operands are uint32_t. For RGBA32F at 16384x16384: overflows to 0. Note: Only reachable when copyTextureToBufferForReadOptimization feature is enabled, which is restricted to AMD GPUs (DisplayMtl.mm:1265). Not reachable on Apple Silicon by default. Triggerable via readPixels on AMD-based Macs.
c) Image11.cpp:428 - copyFromFramebuffer() [D3D11, low exploitability] size_t bufferSize = destFormatInfo.pixelBytes * sourceArea.width * sourceArea.height; The overflow pattern exists, but exploitability is limited: for RGBA32F (16 bpp), the load function RGBA32F_to_R32G32B32A32_FLOAT returns requiresConversion=false (load_functions_table_autogen.cpp:3622), so the vulnerable scratch buffer path is not taken. For RGB32F, destFormatInfo.pixelBytes=12 (GL-side), which does not overflow at 16384x16384. No known exploitable format/type combination found.
None of these use CheckedNumeric or any overflow validation. The D3D11 and OpenGL instances are independent from the previously reported Vulkan backend overflow in TextureVk::reinitImageAsRenderable() — different functions, different trigger paths, different backends.
IMPACT
- Heap buffer overflow in Chrome’s GPU process on Windows (D3D11)
- Up to 4 GB of data written to a zero-byte buffer
- The overflow data comes from the LoadToNative3To4 conversion function, which copies source texture data (attacker-controlled via texSubImage2D) and adds a fourth component (1.0f). Attacker controls 3/4 of the overflow data.
- The GPU process has weaker sandboxing than the renderer process (broader syscall allowlist on Windows)
- No user interaction required beyond visiting a malicious web page
- Requires >= 4 GB GPU VRAM (common: GTX 1070+, RTX 2060+, etc.)
AFFECTED PLATFORMS
Primary: Windows with any GPU supporting D3D11 Feature Level 11_0
- MAX_TEXTURE_SIZE = 16384 on FL 11_0 (all modern GPUs since ~2013)
- D3D11 is the default ANGLE backend on Windows Chrome
- Requires >= 4 GB GPU VRAM
- NVIDIA GTX 1070+ (8 GB), RTX 2060+ (6-24 GB): AFFECTED
- AMD RX 580+ (8 GB), RX 5600+ (6-16 GB): AFFECTED
- Intel Arc A380+ (6 GB): AFFECTED
- GPUs with < 4 GB VRAM: NOT affected (texStorage2D fails with OUT_OF_MEMORY)
Secondary: Linux with ANGLE OpenGL backend (same pattern in BlitGL.cpp)
- Less common: Linux Chrome defaults to ANGLE Vulkan, not OpenGL
Tertiary: macOS with AMD GPU only (FrameBufferMtl.mm:82)
- Overflow exists but gated behind copyTextureToBufferForReadOptimization feature, enabled only for AMD GPUs (DisplayMtl.mm:1265)
- Apple Silicon Macs: NOT affected (feature disabled)
- Intel Macs with AMD dGPU: potentially affected via readPixels path
RELATIONSHIP TO PREVIOUS REPORT
This vulnerability shares the same ROOT CAUSE PATTERN (uint32 overflow in buffer size calculations using GLuint/UINT types) as the previously reported TextureVk::reinitImageAsRenderable() overflow in the ANGLE Vulkan backend. However, the bugs are in completely independent code:
TextureVk (Vulkan): reinitImageAsRenderable() — format fallback conversion TextureStorage11 (D3D11): setData() — texSubImage2D upload conversion
The D3D11 backend was NOT mentioned in the TextureVk report’s “additional overflow instances” list, which only covered Vulkan-specific files (TextureVk.cpp, vk_helpers.cpp). If the TextureVk fix was scoped to only the Vulkan backend, these D3D11/OpenGL overflow instances remain unfixed.
SUGGESTED FIX
-
Use checked arithmetic for staging buffer size (TextureStorage11.cpp:910-913):
angle::CheckedNumeric<UINT> bufferRowPitch = angle::CheckedNumeric<UINT>(static_cast<unsigned int>(outputPixelSize)) * width; angle::CheckedNumeric<UINT> bufferDepthPitch = bufferRowPitch * height; angle::CheckedNumeric<size_t> neededSize = angle::CheckedNumeric<size_t>(bufferDepthPitch.ValueOrDie()) * depth; if (!bufferRowPitch.IsValid() || !bufferDepthPitch.IsValid() || !neededSize.IsValid()) { ANGLE_TRY_HR(context11, E_OUTOFMEMORY, “Integer overflow in buffer size”); }
-
Fix BlitGL.cpp:831-833:
angle::CheckedNumeric<size_t> destBufferSize = angle::CheckedNumeric<size_t>(readPixelsArea.width) * readPixelsArea.height * destInternalFormatInfo.pixelBytes;
-
Fix FrameBufferMtl.mm:82 (defense-in-depth, currently AMD-gated):
angle::CheckedNumeric<uint32_t> sizeInBytes = angle::CheckedNumeric<uint32_t>(width) * height * angleFormat.pixelBytes;
-
Fix Image11.cpp:428 (defense-in-depth, no known exploitable path):
angle::CheckedNumeric<size_t> bufferSize = angle::CheckedNumeric<size_t>(destFormatInfo.pixelBytes) * sourceArea.width * sourceArea.height;
VERSION
Chrome Version: 147.0.7682.0 (dev) Operating System: Windows 10/11 with D3D11 Feature Level 11_0 GPU (>= 4 GB VRAM) ANGLE Backend: D3D11 (Windows default)
REPRODUCTION CASE
Prerequisites:
- Windows system with GPU having >= 4 GB VRAM
- Chrome using D3D11 ANGLE backend (default on Windows)
- GPU MAX_TEXTURE_SIZE >= 16384 (all D3D11 FL11_0 GPUs)
Steps:
-
Serve poc.html locally: python -m http.server 8080
-
Open Chrome on Windows: chrome.exe http://localhost:8080/poc.html
-
Click “Check Environment” to verify:
- WebGL2 is available with D3D11 backend
- MAX_TEXTURE_SIZE >= 16384
- RGB32F requires format conversion (estimated by checking GL_RENDERER)
-
Click “Run PoC” to trigger the overflow:
- Expected: GPU process crashes, WebGL context is lost
For ASAN builds (recommended for clear crash report): gn gen out/asan –args=' is_asan = true is_debug = false is_component_build = false dcheck_always_on = true target_cpu = “x64” angle_enable_d3d11 = true ' autoninja -C out/asan chrome out/asan/chrome.exe –no-sandbox –disable-gpu-sandbox ^ http://localhost:8080/poc.html
Expected ASAN output: ==GPU_PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x… WRITE of size … at 0x… thread T… #0 in LoadToNative3To4Impl<float>(…) #1 in LoadToNative3To4<float, …>(…) #2 in rx::TextureStorage11::setData(…)
Standalone arithmetic verification (no GPU required): cl /Fe:verify_overflow.exe verify_overflow.c && verify_overflow.exe (or: cc -o verify_overflow verify_overflow.c && ./verify_overflow) This confirms the integer overflow using the exact C++ type promotion rules.
FOR CRASHES
Type of crash: GPU process crash (not tab, not browser) Crash state:
Expected crash stack: LoadToNative3To4Impl<float>(…) <- writes 4 GB to 0-byte buffer LoadToNative3To4<float, gl::Float32One>(…) <- loadimage.inc:88-102 rx::TextureStorage11::setData(…) <- TextureStorage11.cpp:921 rx::TextureD3D_2D::setSubImage(…) gl::Texture::setSubImage(…)
Root cause frame: TextureStorage11.cpp:910-911 UINT bufferRowPitch = static_cast<unsigned int>(outputPixelSize) * width; UINT bufferDepthPitch = bufferRowPitch * height; // bufferDepthPitch = 262,144 * 16,384 = 2^32 -> 0
Allocation frame: TextureStorage11.cpp:920 getScratchMemoryBuffer(context11, neededSize=0, &conversionBuffer) // Allocates 0-byte (or minimal) scratch buffer
Overflow frame: TextureStorage11.cpp:921-923 loadFunctionInfo.loadFunction(… width=16384, height=16384, depth=1, … conversionBuffer->data(), bufferRowPitch=262144, bufferDepthPitch=0); // LoadToNative3To4 writes 163841638416 = 4,294,967,296 bytes to 0-byte buffer
The GPU process will crash with access violation (release build) or report heap-buffer-overflow (ASAN build). In a release build, the GPU process restarts and the WebGL context is reported as lost.
ATTACHED FILES
- poc.html - Self-contained WebGL2 PoC with environment check
- verify_overflow.c - Standalone C program proving the integer overflow (reproduces exact C++ type promotion chain)
CREDIT INFORMATION Reporter credit: heesun