# Full-Chain Exploitation Notes (Hardware Required)

This document describes how to take the validation gap demonstrated by the
unit-test POC all the way to GPU-process memory corruption on real hardware.
This requires an Intel/AMD machine with VA-API video encode support — it
cannot be done in the headless test container.

## Hardware Prerequisites

- Linux x86_64 with `/dev/dri/renderD128` present
- Intel GPU (Gen9+ preferred — KBL/CFL/TGL all have iHD driver) **OR**
  AMD GPU with Mesa Gallium VA support
- Working VA-API encode: `vainfo` must show at least one of
  `VAProfileH264*` / `VAProfileVP8*` / `VAProfileVP9*` with `VAEntrypointEncSlice`
- Chrome built **without** `dcheck_always_on=true` (the production code path —
  `VerifyGpuMemoryBufferHandle` at `platform_video_frame_utils.cc:536-543` is
  gated behind `#if DCHECK_IS_ON()`, and even when present only produces a
  `DLOG_IF(WARNING)` rather than rejecting)

## Why Chrome-level reproduction needs Strategy B (renderer-code patch)

The attack requires sending a `media::mojom::VideoFrame` over Mojo where:
- `frame->HasSharedImage()` returns true (to bypass the size check at
  `mojo_video_encode_accelerator_service.cc:170-178`)
- The SharedImage's `metadata.size` is large (e.g., 1920×1080)
- The SharedImage's underlying `NativePixmapHandle` wraps a tiny dmabuf

A legitimate renderer cannot construct this — `gpu::SharedImageInterface::
CreateSharedImage` allocates a buffer that matches the requested dimensions.
The attacker needs renderer code execution (assumed in Chromium's threat model
for renderer→GPU sandbox escape) to either:

**Option A — Forge the ExportedSharedImage struct directly:**
After creating a small legitimate SharedImage (32×32), patch the
`gpu::ExportedSharedImage` struct in renderer memory to claim large
`metadata_.size` before it's serialized over Mojo. The Mojo struct traits at
`exported_shared_image_mojom_traits.h:80-97` do not lseek-validate against
`buffer_handle`.

**Option B — Bypass renderer-side wrapping:**
Build a `media::VideoFrame` directly with `WrapMappableSharedImage` using
manually-constructed `gpu::ClientSharedImage` whose `mailbox_` references
a small backing but whose `metadata_.size` is large.

## Renderer-side patch sketch (Option A)

Apply to `media/mojo/clients/mojo_video_encoder_client.cc` or wherever the
renderer wraps frames before calling the Mojo `Encode`:

```cpp
// Just before remote_->Encode(frame, ...):
//
// 1. Allocate a TINY shared image (32x32 NV12 → ~1.5 KB Y + ~768 B UV).
//    The GPU process allocates a real (small) GBM buffer and returns the
//    mailbox + the dmabuf fd.
//
// 2. Export it via ClientSharedImage::Export() → ExportedSharedImage.
//    This struct holds:
//      - metadata_ (SharedImageMetadata with size, format, etc.)
//      - buffer_handle_ (gfx::GpuMemoryBufferHandle wrapping NativePixmapHandle)
//
// 3. ATTACK: Mutate exported.metadata_.size = gfx::Size(1920, 1080)
//    Leave the buffer_handle_ untouched (still the small fd).
//    Optionally also tamper planes[].stride/offset for the wild-offset variant.
//
// 4. Reconstruct a ClientSharedImage from the tampered export.
//    ClientSharedImage::ImportUnowned does no size→fd validation.
//
// 5. Wrap it into a VideoFrame via WrapMappableSharedImage.
//    video_frame.cc:407: coded_size = shared_image->size()  ← attacker's lie
//
// 6. Send via remote_->Encode(frame, ...).
//    GPU side: HasSharedImage() == true → coded_size check bypassed.
```

## What to look for on hardware

### Intel iHD driver behavior

Intel iHD (`/usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so`) at
`media_libva.cpp:DdiMedia_CreateSurfaces2` does have *some* validation:

- It checks `objects[i].size >= offset + height * pitch` for some formats
- BUT historically has had gaps for tiled formats and DRM_PRIME_2 path

Expected outcome: either `vaCreateSurfaces` fails with `VA_STATUS_ERROR_*`
(safe — the bug becomes a "DoS only" finding) **OR** the surface is created
and the **encode** operation reads past the 4KB mapping. The latter is the
exploitable outcome.

To detect the OOB read:
1. Fill the small dmabuf with a known marker pattern (e.g., `0xDEADBEEF`).
2. Allocate a large heap object in the GPU process immediately after.
3. Submit the malformed frame for encoding.
4. Examine the encoded H.264/VP9 bitstream returned to the renderer.
5. If the bitstream encodes bytes that aren't `0xDEADBEEF`, OOB read confirmed.

### Mesa Gallium driver behavior

Mesa (`/usr/lib/x86_64-linux-gnu/dri/radeonsi_drv_video.so` or
`nouveau_drv_video.so`) at `src/gallium/frontends/va/surface.c:
vlVaCreateSurfaces2` historically does even less validation — it largely
trusts `width`/`height` and uses them directly to compute mapping sizes via
`pipe_resource_create`.

## Detecting via dmesg / GPU hang

Even without ASAN, the OOB on a GBM-mapped buffer typically produces:
- GPU page fault in `dmesg`: look for `i915` or `amdgpu` page fault entries
- VA driver internal error: `VA_STATUS_ERROR_OPERATION_FAILED` from
  `vaEndPicture` or `vaSyncSurface`
- In the worst (best for attacker) case: silent OOB read, no crash, leaked
  GPU-process memory in encoded output

## Why ASAN can't help even with hardware

The VA-API driver shared library is loaded via `dlopen` at runtime
(`va_stubs` / `libva_stubs` dynamically loads `libva.so`). It is compiled
by the distro, not by Chromium's build system, so it has no ASAN
instrumentation. The OOB happens entirely inside that uninstrumented `.so`.

You *can* get an ASAN signal if:
- The driver `mmap`s the small buffer with `MAP_FIXED` adjacent to ASAN-tracked
  allocations, AND
- The OOB read crosses into an ASAN red-zone

…but this is not reliable. The deterministic POC is the unit test.
