# v8-path-b-cdp-leak — VALIDATION RESULTS

**Spike:** `spikes/v8-path-b-cdp-leak/`
**Date:** 2026-06-25
**Source claim:** NIGHTLY-FINDINGS-2026-06-25 — May 2025 V8 patch closed own-property getter path (Path A from v8-cdp-leak); alleged Path B (inherited/prototype getter via console.debug) still fires on CDP-attached Chrome.

---

## Verdict: ❌ REJECTED

**Reason:** Path B fires identically on vanilla and CDP-attached Chrome. Only callCount differs (2 vs 4), which is not a clean discriminator — both are CDP-mode behaviors of `console.debug`, not a stealth-browser signal.

---

## Method

Three-condition test runner (`test_runner.py`) hits the local probe page at `http://127.0.0.1:3457/v8-path-b-cdp-leak/demo.html`:

1. **Vanilla Playwright (headless)** — no CDP session
2. **Playwright + explicit CDP session** with `Runtime.enable()` sent
3. **Vanilla Playwright (headful/Xvfb)** — cross-check

The probe (`demo.html`) checks:

- **Path B (inherited getters):** `Error.prototype.stack` getter firing via `console.debug` and `console.error`
- **Path A (own getters, reference):** `Error.prototype.message` own getter via `console.debug` and `console.error`
- **Prototype-proxy ownKeys:** across 6 console methods (groupEnd, debug, error, log, dir, table)
- **Positive control:** `chromeKeyCount` (must equal 3 in stock Chrome)

---

## Results

| Signal | Vanilla | CDP-attached | Δ | Verdict |
|---|---|---|---|---|
| `pathB_stackGetterFired` | true | true | none | ❌ Not a discriminator |
| `pathB_stackGetterFired_viaError` | true | true | none | ❌ Not a discriminator |
| `pathB_stackCallCount` | 2 | 4 | +2 | ⚠️ Difference exists but not clean |
| `pathA_messageFired` (own) | false | false | none | ❌ Closed (May 2025 patch confirmed) |
| `pathA_messageFired_viaError` | false | false | none | ❌ Closed |
| `proxyOwnKeys_*` (×6 methods) | identical | identical | none | ❌ Universal V8 behavior |
| `chromeKeyCount` | 3 | 3 | none | ✅ Control passes |

---

## Why this is rejected

The `Error.prototype.stack` getter fires on both vanilla and CDP-attached Chrome via `console.debug` because `console.debug` *always* invokes the stack getter to format its frame — it's a universal V8 console behavior, not a CDP-only path.

The 2x callCount difference on CDP-attached is real but not a **stealth-browser signal**:
- It's a property of `console.debug` in CDP-mode (frame formatting requires an extra pass)
- A real human browser with DevTools open produces the same callCount
- A C++-fork stealth browser (Camoufox, CloakBrowser, patchright) that suppresses console output entirely would skip this signal anyway

The May 2025 V8 patch that closed Path A appears to have closed Path B at the same time. The "Path B bypass" claim in the nightly findings is misinformation.

---

## Why this matters

This is the **second** V8 CDP-leak probe to reject on identical-evidence grounds (cf. `spikes/v8-cdp-leak/VALIDATION-RESULTS.md`). The pattern is now clear:

- V8 console methods (debug/error/log) all invoke stack/message getters as part of formatting
- These getters fire universally — there is no "CDP-only" path that survives modern V8
- Any signal derived from `console.*` getter-firing behavior is environmental, not a stealth detector

**Action:** Both V8 CDP-leak probes should be marked closed. If a new V8 console-API signal emerges in future nightly findings, it must show evidence of discrimination between vanilla and CDP-attached at the same V8 version before validation.

---

## Files

- `demo.html` — local probe page (217 lines, no external deps)
- `test_runner.py` — three-condition dual test runner (212 lines)
- `RESULTS.json` — captured output from last session run (pathB fired on both, callCount 2/4)