
June 2nd, 2026 – Alisa Esage
Earlier this year I invested several weeks full-time in zero day engineering for Google Chrome and validating my mental models of frontier browser defenses. I found and reported to Chromium VRP a number of security bugs across renderer, network service, and browser process. This article will focus on three modern Chrome-specific exploit mitigations that I had to confront while writing the exploits: MiraclePtr, v8 sandbox, and PartitionAlloc.
Before getting to those three, I met the old guard: ASLR, CFI, CORS and the sandbox security model itself. These are generally well understood and I will only briefly mention my experience with them here.
As my bugs spanned the full stack of Chrome technology – from a renderer RCE to network service partial-EoP and browser process EoP for sandbox escape, all memory corruption issues – I consider this subset of mitigations somewhat representative of the actual resistance surface that a modern Chrome attacker will face; and a priority for offensive R&D.
Some of my bugs bypassed the relevant Chrome mitigations, while others were blocked by them despite my best effort. I cover both types in this article, as much as the present stage of responsible disclosure permits. Full technical details of the exploits will be disclosed in due course.
For a compressed systematic exposure to prerequisite knowledge, I recommend my Browser Exploit Design course.
Chrome has many exploit mitigations that are doing more work than public knowledge suggests.
The tables below are actual as of current Stable (M148).
| Mitigation | Details | Enabled? | OS | Bypass |
|---|---|---|---|---|
| PartitionAlloc-Everywhere | PA-E routes every renderer and browser allocation through PartitionRoot via the allocator shim, controlled by use_partition_alloc_as_malloc_default and implemented under base/allocator/partition_allocator/. | Default on | All | — |
| Bucketed slot spans | Each partition splits allocations into size-class buckets (PartitionBucket in partition_bucket.cc); requests round up to the bucket's slot_size and slots come from num_system_pages_per_slot_span system pages, isolating size classes from one another. | Default on | All | Partial |
| Address-pool isolation | On 64-bit, PartitionAddressSpace reserves separate virtual-address pools by purpose — regular (kRegularPoolHandle), BRP (kBRPPoolHandle), configurable, and thread-isolated — collectively known as the giga-cage / gigacage. | Default on | 64-bit | Partial |
| Out-of-cage metadata | With move_metadata_outside_gigacage, per-slot metadata (refcounts and tags held in InSlotMetadata / InSlotMetadataTable) lives in a second mapping outside the giga-cage, so in-cage write primitives can't reach it. | Default on | 64-bit non-iOS | Partial |
| Freelist pointer encoding | The freelist next pointer is obfuscated by EncodedNextFreelistEntry::Transform() — ReverseBytes() on little-endian, negation on big-endian — and cross-checked against a shadow copy at a different offset, raising the bar for tcache-style freelist poisoning. | Default on | All | Yes |
| Thread Cache | ThreadCache keeps a per-thread, TLS-backed freelist of recently-freed slots (up to kThreadCacheDefaultSizeThreshold) in front of the central per-bucket freelist; instances coordinate via ThreadCacheRegistry, are sized by SetThreadCacheMultiplier, and drained by RunPeriodicPurge. The same hot path hosts cacheline poisoning, tombstone validation, and SchedulerLoopQuarantine integration (kThreadCacheQuarantineIndex) — added because real-world exploits target tcache/fastbin-style freelist corruption. See base/allocator/partition_allocator/src/partition_alloc/thread_cache.h. | Default on | All | Subsystem — sub-features bypassed individually |
| Cacheline poisoning on free | On free() into the Thread Cache, the cacheline holding the new freelist next is overwritten with poison_16_bytes (0xbadbad00 repeating) so a subsequent UAF read on that line crashes early instead of returning attacker-shaped data; gated by PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY). See thread_cache.h:626-674. | Default on (with shadow entry, LE) | All | Probabilistic — only the touched cacheline |
| ThreadCache tombstone | After a thread exits, its per-thread ThreadCache* is marked with kTombstoneMask low bits; IsValidPtr / IsValid check for the tombstone and reject the access before dereference, preventing use-after-thread-exit on the cache pointer. See thread_cache.h:279-298. | Default on | All | Limited |
| Freelist randomization | Per-bucket randomization of free-entry order would make grooming the freelist harder, but no such shuffle is implemented in M148. | Not found in M148 | — | — |
| Guard pages | Unmapped guard pages flank slot spans and direct maps (DirectMapGuardPages in partition_page.h) so a linear overflow off the end of a slot faults rather than corrupting adjacent metadata. | Default on | All | Partial |
| Partition cookie | A 16-byte canary 0xDE 0xAD 0xBE 0xEF 0xCA 0xFE 0xD0 0x0D 0x13 0x37 0xF0 0x05 0xBA 0x11 0xAB 0x1E (kCookieValue in partition_cookie.h) is stamped at the end of every allocation via PartitionCookieWriteValue; on free, PartitionCookieCheckValue verifies the pattern and calls CookieCorruptionDetected on mismatch. Gated by USE_PARTITION_COOKIE (use_partition_cookie), enabled only in debug / DCHECK builds. | Debug / DCHECK only | All | Limited |
| MiraclePtr | MiraclePtr — also called BRP, BackupRefPtr, or raw_ptr<T> (with raw_ref<T> for references) — wraps non-owning pointer fields; BackupRefPtrImpl increments a refcount in InSlotMetadata and, on free() with a non-zero count, quarantines the slot via request_quarantine and poisons the memory with internal::kQuarantinedByte = 0xEF (so reads return the famous 0xEFEFEFEFEFEFEFEF pattern), turning a UAF into a crash. Gated by kPartitionAllocBackupRefPtr; fields opt out with RAW_PTR_EXCLUSION. | Default on (Android renderer excluded) | All | No bypass of invariant |
| Dangling-pointer detector | When a slot is freed while a raw_ptr<T> still points at it, DanglingPointerDetector raises a DanglingRawPtrChecks failure in DCHECK/debug builds; enable_dangling_raw_ptr_checks_default gates the build, and DanglingUntriaged tags known-but-unaddressed cases. Debug builds (EXPENSIVE_DCHECKS_ARE_ON) also DebugMemset freed slots with kFreedByte = 0xCD and stamp kUninitializedByte = 0xAB on freshly recommitted memory. | Debug / DCHECK only | Linux/CrOS | — |
| PCScan | The briefly-shipped conservative-scanning quarantine — also called *Scan / StarScan — has been removed from M148; no references remain. | Removed | — | — |
| GWP-ASan | enable_gwp_asan_support puts a guard page around a tiny sampled fraction of allocations so heap bugs occasionally crash deterministically and bucket together in crash reports; detection only, not prevention. | Default on (low rate) | All | — |
| Scheduler-loop quarantine | SchedulerLoopQuarantine defers the actual free() until a later scheduler-loop tick so reuse can't happen on the freeing thread's hot path; quarantined memory is poisoned with 0xcdcdcdcd; per-process configuration is JSON-driven and the feature is off by default. | Opt-in | All | New |
| Advanced checks | kPartitionAllocAdvancedChecks (AdvancedChecks) turns on extra runtime integrity assertions, enabled per process (browser / renderer / GPU / non-renderer / all); off by default. | Opt-in | All | New |
| Memory reclaimer | PartitionAllocMemoryReclaimer periodically decommits idle slot spans to reclaim address-space fragmentation. | Default on | All | — |
| Straighten/sort freelist | StraightenLargerSlotSpanFreeLists and SortSmallerSlotSpanFreeLists reorder per-span freelists on purge to improve allocation locality. | Default on | All | — |
| Shadow metadata | EnableShadowMetadata maps PartitionAlloc metadata twice — read-only to the allocator's normal code, and writable behind a PKU key — so corruption from a write primitive in regular code cannot reach metadata. HW-gated. | Default on (HW-gated) | x64 Linux/CrOS | New |
| Thread-isolation pool | A dedicated partition pool (kThreadIsolatedPoolHandle, PA_THREAD_ISOLATED_ALIGN) for sensitive allocations whose pages are guarded by Intel MPK / PKU keys (enable_pkeys); only threads holding the key can write. | Default on (HW-gated) | x64 Linux/CrOS | Yes (PKU) |
| PartitionAlloc MTE | On ARM hardware with MTE, PartitionAllocMTE / kEnableMemoryTaggingChecks tags each slot and matches the tag on access in sync or async mode; mismatched tags fault. | Default on (HW-gated) | Android arm64 | Yes |
| Strict free-size check | kPartitionAllocFreeWithSize enables PartitionRoot::FreeWithSize to verify that the caller-passed size on free matches the slot's actual size, catching size-class confusion and type-confused frees. | Opt-in | All | New |
| ASan-BRP integration | ASan-only checks (kAsanBrpDereferenceCheck, kAsanBrpExtractionCheck, kAsanBrpInstantiationCheck) instrument raw_ptr<T> dereference, extraction, and instantiation against BRP-protected memory to surface misuse during sanitizer runs. | ASan builds only | All | N/A (sanitizer) |
| Mitigation | Details | Enabled? | OS | Bypass |
|---|---|---|---|---|
| V8 Sandbox | The V8 heap sandbox (a.k.a. heap cage) is a 4 GB virtual address range set up by Sandbox::Initialize() in v8/src/sandbox/sandbox.h and gated by v8_enable_sandbox / V8_ENABLE_SANDBOX; all V8 on-heap pointers must point inside it, and stray out-of-cage references are caught by SBXCHECKs. | Default on | 64-bit non-Fuchsia | Yes |
| Pointer compression | Inside the cage, V8 stores pointers as 32-bit cage-relative tagged values (V8_COMPRESS_POINTERS); v8_enable_pointer_compression / _shared_cage and kPtrComprCageBaseAlignment define the shared base alignment. | Default on | x64 / arm64 / loong64 | Foundational |
| External Pointer Table (EPT) | Raw host pointers held by V8 objects ("sandboxed pointers") live in ExternalPointerTable outside the cage and are referenced from inside via tagged handles (kExternalPointerTagShift). See v8/src/sandbox/external-pointer-table.h. | Default on (with sandbox) | All | Yes |
| Code Pointer Table (CPT) | JSFunction::code is indirected through CodePointerTable via a kCodePointerHandle, so a corrupted handle can only resolve to an existing compiled code object rather than an arbitrary address. See v8/src/sandbox/code-pointer-table.h. | Default on (with sandbox) | All | Yes |
| Wasm Code Pointer Table (WCPT) | The Wasm analog of CPT: WasmCodePointerTable stores a signature_hash per entry that's cross-checked at indirect Wasm calls, and the table itself is pkey-write-protected on supported HW. See v8/src/wasm/wasm-code-pointer-table.h. | Default on (with sandbox) | All | Yes |
| Trusted Pointer Table (TPT) | TrustedPointerTable holds pointers to V8-internal trusted objects, dispatched through 15-bit IndirectPointerTags so a corrupted handle can only name a same-type trusted entry. See v8/src/sandbox/trusted-pointer-table.h and indirect-pointer-tag.h. | Default on (with sandbox) | All | Limited |
| Trusted Space | TrustedSpace is a V8 heap region placed outside the sandbox cage; sensitive objects (select Maps, bytecode handlers) live there so in-cage corruption cannot reach them directly. | Default on (with sandbox) | All | Boundary, not bypass target |
| CppHeap Pointer Table | CppHeapPointerTable indirects pointers from V8-heap objects into the Oilpan / cppgc C++ heap, applying the same handle-based protection across the heap boundary. See v8/src/sandbox/cppheap-pointer-table.h. | Default on (with sandbox) | All | Same class |
| JS Dispatch Table | JSDispatchTable indirects every JS function dispatch through a write-protected table indexed by JSDispatchHandle, supporting leaptiering (seamless tier switching) without exposing raw code pointers to sandboxed code. See v8/src/sandbox/js-dispatch-table.h. | Default on | All | Same class |
| Trap-based Wasm bounds | Wasm memory accesses skip explicit bounds-cmp instructions; OOB accesses fault and are caught by kTrapHandler (v8/src/trap-handler/), which converts the signal into a Wasm trap. | Default on | 64-bit | Holds |
| Hardware sandbox (PKU) | V8_ENABLE_SANDBOX_HARDWARE_SUPPORT uses Intel MPK / PKU keys (V8_HAS_PKU_JIT_WRITE_PROTECT) to enforce the sandbox/trusted-region boundary in hardware; M148's CodeSandboxingMode enum (kSandboxed / kUnsandboxed) annotates which code regions run under that enforcement. See v8/src/sandbox/hardware-support.h and code-sandboxing-mode.h. | Experimental | x64 Linux/CrOS | — |
| Mitigation | Details | Enabled? | OS | Bypass |
|---|---|---|---|---|
| MAP_JIT W^X | On Apple Silicon, V8's JIT region is mapped MAP_JIT and toggled per-thread between writable and executable via pthread_jit_write_protect_np; RwxMemoryWriteScope brackets writes, providing per-thread W^X. | Default on | macOS arm64 | Yes |
| Dual-mapping W^X | On Linux and Android, V8's JIT pages are dual-mapped via mmap aliasing — one view writable, one executable — so executable code is never simultaneously writable. See code-space-access.h. | Default on | Linux/Android | Yes |
| Windows JIT W^X | Windows JIT is not W^X-enforced: the renderer uses MITIGATION_DYNAMIC_CODE_DISABLE_WITH_OPT_OUT so V8 can opt out of strict ACG and keep writable+executable JIT pages. | Off | Windows | — |
| CFI-icall in TurboFan/Wasm | Where Clang CFI is on (Linux x64 official), TurboFan- and Wasm-emitted indirect calls get LLVM cfi-icall checks via use_cfi_icall; call sites that can't carry type metadata opt out with V8_CLANG_NO_SANITIZE. | Default on (where CFI on) | Linux x64 official | Yes |
| Short builtin calls | With v8_enable_short_builtin_calls, embedded builtins are laid out close enough to the heap (within kShortBuiltinCallsOldSpaceSizeThreshold) to be reached by direct rather than indirect calls, shrinking the icall surface. | Default on | All | — |
| --jitless | --jitless (FLAG_jitless) runs V8 in interpreter-only mode, disabling TurboFan, Maglev, Sparkplug, and the Wasm baseline JIT — eliminating writable-executable pages and unlocking strict ACG on Windows. | Opt-in | All | — |
| Spectre v1 masking (V8) | Early JIT-level Spectre v1 mitigation (branch load poisoning, --branch-load-poisoning) was retired in favor of Site Isolation; no active masking code remains. | Retired | — | — |
| Mitigation | Details | Enabled? | OS | Bypass |
|---|---|---|---|---|
| Multi-process architecture | Chrome splits responsibilities across renderer, browser, GPU, and utility processes under content/browser and content/renderer; RenderProcessHost and ProcessLauncher manage spawning. | Default on | All | — |
| Site Isolation | Each web site is placed in its own renderer process (--site-per-process, IsolateOrigins); SiteIsolationPolicy (content/browser/site_isolation_policy.cc) enforces strict site-per-process on desktop, partial on low-RAM Android. | Default on (desktop); partial (Android low-RAM) | All | Yes |
| OOPIFs | Out-of-process iframes (OOPIFs) place cross-site iframes in their own renderer process; isolated sandboxed iframes (AreIsolatedSandboxedIframesEnabled) extend this to same-origin sandboxed frames. | Default on | All | Yes |
| Origin-Agent-Cluster (OAC) | A site can opt into per-origin isolation finer than site isolation via the Origin-Agent-Cluster header (kOriginIsolationHeader), reflected in JS as document.originAgentCluster. | Opt-in (by site) | All | — |
| Zygote | On Linux/CrOS/Android, renderers are spawned from a pre-forked, pre-hardened zygote process managed by ZygoteHost; an intermediate kZygoteIntermediateSandbox primes sandbox state before the renderer-specific policy is applied, and the zygote socket carries spawn requests. | Default on | Linux/CrOS/Android | Yes (Zygote socket) |
| seccomp-bpf | On Linux/CrOS/Android, the renderer is constrained by a seccomp-bpf syscall allowlist defined in BaselinePolicy / BaselinePolicyAndroid (sandbox/linux/seccomp-bpf-helpers/); disallowed syscalls return errno or kill the process. | Default on | Linux/CrOS/Android | Yes (via kernel) |
| User namespaces | On Linux/CrOS, the sandbox is built on CLONE_NEWUSER (namespace_sandbox), replacing the older setuid_sandbox helper so no setuid bit is required. | Default on | Linux/CrOS | Yes (via kernel) |
| Landlock | On Linux, the renderer installs a Landlock LSM ruleset (landlock_create_ruleset, linux_landlock.h) restricting filesystem access; enabled since M97. | Default on (M97+) | Linux | Limited |
| AppContainer + restricted token | On Windows, the renderer runs inside an AppContainer (AppContainerBase / AppContainerProfile, sandbox/win/src/app_container_base.cc) at low integrity level with a restricted token, sharply limiting object access. | Default on | Windows | Yes |
| Seatbelt profile | On macOS, the renderer is confined by a Seatbelt .sb profile applied via sandbox_init_with_parameters (sandbox/mac/seatbelt.cc) — the same App Sandbox mechanism Apple uses for sandboxed apps. | Default on | macOS | Yes |
| isolatedProcess / SELinux | On Android, the renderer runs as an isolatedProcess in the isolated_app SELinux domain (seapp_contexts), with Binder and file access denied beyond what's explicitly granted. | Default on | Android | Yes |
| Win32k lockdown | The renderer sets MITIGATION_WIN32K_DISABLE (PROCESS_CREATION_MITIGATION_POLICY_WIN32K_SYSTEM_CALL_DISABLE_ALWAYS_ON) so calls into win32k.sys are blocked, removing a large kernel attack surface. | Default on | Windows | Yes |
| Code Integrity Guard (CIG) | MITIGATION_FORCE_MS_SIGNED_BINS (PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON, MicrosoftSignedOnly) makes the renderer reject load of any non-Microsoft-signed DLL. | Default on (renderer) | Windows | Limited |
| Arbitrary Code Guard (ACG) | Strict ACG (MITIGATION_DYNAMIC_CODE_DISABLE, PROCESS_CREATION_MITIGATION_POLICY_PROHIBIT_DYNAMIC_CODE_ALWAYS_ON, ProhibitDynamicCode) blocks all dynamic code generation; the renderer instead uses the opt-out variant MITIGATION_DYNAMIC_CODE_DISABLE_WITH_OPT_OUT so V8's JIT can keep running. Strict mode is only viable under --jitless. | Off in renderer; opt-in with --jitless | Windows | — |
| Intel CET shadow stack (process) | Intel CET's user shadow stack (PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_ON) is requested per process; MITIGATION_CET_STRICT_MODE opts a process into hard-fail (opt-in), and MITIGATION_CET_DISABLED opts a process out entirely. | Conditional; strict mode opt-in | Windows ≥20H1 x64 | Yes |
| FSCTL syscall disable | MITIGATION_FSCTL_SYSTEM_CALL_DISABLE blocks FSCTL control codes to NtFsControlFile, narrowing a historically rich kernel attack surface; supported on Windows 10 22H2+. | Default on (where supported) | Windows 10 22H2+ | New |
| Restrict core sharing | MITIGATION_RESTRICT_CORE_SHARING (Windows 11 24H2+) prevents the renderer from sharing an SMT/hyperthread core with another security domain, mitigating same-core side-channel leaks. | Default on (where supported) | Windows 11 24H2+ | New |
| Module tampering protection | MITIGATION_MODULE_TAMPERING_PROTECTION detects in-process IAT (Import Address Table) patching and remaps the affected module from a clean on-disk image, defending against import-table hijacking that would otherwise sidestep CIG. Lands in M149. | Emergent (M149+) — not in M148 stable | Windows | New |
| No-child-process policy | The renderer cannot spawn child processes; all process launches go through ChildProcessLauncher in the browser, enforced at the content layer rather than via a single OS mitigation flag. | Default on | All | Yes (browser-side bugs) |
| Misc Windows process mitigations | The renderer also applies MITIGATION_EXTENSION_POINT_DISABLE (no AppInit DLLs), MITIGATION_NONSYSTEM_FONT_DISABLE (no third-party fonts), and MITIGATION_IMAGE_LOAD_NO_REMOTE / _NO_LOW_LABEL (ProcessImageLoadPolicy) to block remote and low-integrity DLL loads. | Default on (renderer) | Windows | Limited |
| Sandbox broker | A privileged broker process (Windows: BrokerServices / TargetProcess in sandbox/win/src/broker_services.cc; Linux: namespace broker) mediates the small set of OS operations the renderer is allowed to perform — broker bugs are sandbox escapes by definition. | Default on | Windows, Linux | Yes |
| Network Service sandbox | The Network Service (services/network/) runs in its own sandboxed process (kNetwork sandbox type, NetworkProcessSandbox) — fully sandboxed on desktop, partial on mobile. | Default on (desktop); partial (mobile) | All | Yes |
| LPAC | On Windows, the network and audio services run inside a Less-Privileged AppContainer (LowPrivilegeAppContainer, IsLpacEnabled) — a tighter variant than the renderer's AppContainer, with only explicitly granted capabilities (lpacCom, lpacPnpNotifications, etc.). | Partial | Windows | Limited |
| GPU process sandbox | The GPU process runs in the kGpu sandbox (GpuSandbox); sandbox strength is constrained by what the platform driver requires the process to access, leaving a substantial attack surface in the driver itself. | Default on | All | Yes |
| Specialized service sandboxes | Newer per-service sandbox types in sandbox/policy/mojom/sandbox.mojom — kOnDeviceModelExecution, kHardwareVideoDecoding / kHardwareVideoEncoding, kPrintBackend, kScreenAI, kIme, kTts, kMirroring, kOnDeviceTranslation, kShapeDetection — give each subsystem its own narrow policy. | Default on per-feature | Platform-specific | — |
| Utility / Audio isolation | Audio (kAudio) and generic utility (kUtility) services run in their own sandboxed processes rather than in the renderer or browser. | Default on | All | Limited |
| Mitigation | Details | Enabled? | OS | Bypass |
|---|---|---|---|---|
| Interface attribute enforcement | Mojom-level capability gating via [RuntimeFeature], [RequireContext], [AllowedContext], and [MinVersion] attributes — enforced at codegen time by mojom_restrictions_check.py and mojom_interface_feature_check.py — restricts which contexts can bind or send which interfaces. | Default on | All | Yes (mis-scoped bindings) |
| Bind-time caller validation | On BindReceiver, Mojo tracks the source node (set_source_node) and lets endpoints reject unexpected callers; mis-routed messages report via NotifyBadMessageFrom / BadMessageCallback, which can kill the offending process. | Default on (partial) | All | Yes |
| Message ordinal scrambling | When enable_mojom_message_id_scrambling is on (official non-CrOS desktop builds), ScrambleMethodOrdinals in mojo/public/tools/bindings/mojom_bindings_generator.py derives each wire method ID as the first 31 bits of SHA-256(salt + interface + index), with the per-build salt sourced from //chrome/VERSION — raising the bar for cross-pipe confusion and blind probing. | Default on | mac/win/linux (official, non-CrOS) | Limited |
| Fuzzing infrastructure | In-tree Mojo fuzzing: MojoLPM (libFuzzer + LPM-driven protocol fuzzing), proto-based fuzzers, mojo_parse_message_fuzzer, and channel_mac_fuzzer, all gated by enable_mojom_fuzzer. | Default on | All | — |
| MojoJS gating | JavaScript-to-Mojo bindings (MojoBindingsController, MojoJsFeatures) are restricted to the main frame's isolated world for WebUI (kMojoWebUi) and only opt in via EnableMojoJsBindings / AllowMojoJSForProcess — they are not exposed to web content in stable. | Default on (off for web) | All | — |
| Capability-based validation | Mojo validates every incoming message at the receiver — typed enums, struct header / version, pointer overflow, ValidateNonNullableUnion (validation_util.h); a malformed message triggers ReportBadMessage / BadMessageCallback, which terminates the sender. | Default on | All | Yes (validation-logic bugs) |
| IsolatedConnection | mojo::IsolatedConnection opens a point-to-point Mojo channel that is not joined to the global Mojo node graph, used where strict cross-process isolation is wanted. | Default on | All | — |
| Mitigation | Details | Enabled? | OS | Bypass |
|---|---|---|---|---|
| LLVM CFI | Clang's LLVM CFI (is_cfi; -fsanitize=cfi-vcall / -cfi-icall / -cfi-cast via build/config/sanitizers/sanitizers.gni) provides forward-edge type-based call protection — vtable calls and indirect calls are checked against the legal type set — using ThinLTO for cross-DSO coverage. On by default only in official Linux x64 and CrOS device builds; use_cfi_icall and use_cfi_cast are narrower still. | Default on (official builds) | Linux x64, CrOS device | Yes |
| ShadowCallStack (SCS) | ShadowCallStack (-fsanitize=shadow-call-stack, enable_shadow_call_stack) keeps a separate read-only return-address stack on arm64, anchored at the reserved x18 register; opt-in per Android arm64 build. | Default on (opt-in per build) | Android arm64 | Limited |
| Intel CET shadow stack (compile) | Windows x64 official binaries are linked CET-compatible (/CETCOMPAT, enable_cet_shadow_stack), letting the OS enable the hardware shadow stack on supported CPUs. | Default on (official) | Windows x64 | Yes |
| Arm PAC | Arm Pointer Authentication / PAUTH (ARMv8.3-A) signs return addresses (PACIASP) and verifies them on return (AUTIASP); enabled via -mbranch-protection=pac-ret under arm_control_flow_integrity=standard on Linux/Android arm64. | Default on | Linux/Android arm64 | Yes |
| Arm BTI | Arm Branch Target Identification (ARMv8.5-A) requires every indirect-branch landing pad to be a BTI instruction; enabled via -mbranch-protection=standard and link-time -Wl,-z,force-bti / lld_branch_target_hardening on Linux/Android arm64. | Default on | Linux/Android arm64 | Yes |
| Stack canaries | Compiler-inserted stack cookies (SSP, also called GS cookies) via -fstack-protector or -fstack-protector-strong depending on platform/config, validated on function return through __stack_chk_fail. | Default on | All | Yes |
| SafeStack | SafeStack (-fsanitize=safe-stack) split the stack into a safe and an unsafe stack; removed from Chrome's build configuration. | Removed | — | — |
| Mitigation | Details | Enabled? | OS | Bypass |
|---|---|---|---|---|
| ASLR | Address Space Layout Randomization for image, heap, and stack — Windows /DYNAMICBASE, POSIX position-independent code/executable (-fPIC / -fPIE, PIE binaries). | Default on | All | Yes |
| High-entropy ASLR | On 64-bit Windows, /HIGHENTROPYVA opts into the wider 64-bit ASLR entropy range; corresponding POSIX defaults apply on Linux/Android. | Default on | 64-bit | Yes |
| DEP / NX | Data Execution Prevention / NX: Windows /NXCOMPAT and POSIX -Wl,-z,noexecstack mark data pages non-executable so injected data can't be jumped to directly. | Default on | All | Yes |
| /SAFESEH | On Windows x86, /SAFESEH (SafeSEH) requires Structured Exception Handlers to live in a known table, validating the SEH chain at dispatch time. | Default on | Windows x86 | Yes |
| RELRO + BIND_NOW | Full RELRO: -Wl,-z,relro makes the GOT read-only after relocation and -Wl,-z,now forces eager symbol resolution, so the PLT no longer needs to write to the GOT at runtime. On for non-component Linux/Android/Fuchsia builds. | Default on (non-component) | Linux/Android/Fuchsia | — |
| _FORTIFY_SOURCE | glibc's fortified-libc wrappers around string and memory functions (__strcpy_chk and friends) — _FORTIFY_SOURCE=2 by default on Linux/Android, =3 on ChromeOS and sysroot+Clang builds, providing compile- and runtime-time bounds checks where the size is statically known. | =2 default; =3 on CrOS and sysroot+Clang | Linux/Android/CrOS | Yes |
| Auto-init stack variables | -ftrivial-auto-var-init=zero (controlled by init_stack_vars) auto-initializes stack-local variables to zero on entry (-ftrivial-auto-var-init=pattern is the fallback), closing the uninitialized-stack-read bug class; on by default everywhere except non-official Android. | Default on | All except non-official Android | Limited |
| ThinLTO + whole-program-vtables | -flto=thin (ThinLTO) plus -fwhole-program-vtables enables link-time optimization and whole-program devirtualization, prerequisites for cross-DSO CFI; on in CFI builds. | Default on (CFI builds) | Linux x64 official, CrOS | — |
| Relative VTables ABI | -fexperimental-relative-c++-abi-vtables switches to a compact vtable layout using relative offsets instead of absolute pointers, hardening vtable layout on Android arm64 component builds. | Default on | Android arm64 component | — |
| libstdc++ / libc++ assertions | Standard-library assertions — _GLIBCXX_DEBUG=1 / _GLIBCXX_ASSERTIONS=1 for libstdc++, _LIBCPP_HARDENING_MODE for libc++ — catch out-of-range access and iterator misuse; enabled in debug and ASan builds. | Debug + ASan only | All | — |
| MTE (toolchain) | Toolchain-level MTE codegen (-march=armv8.5-a+memtag, __arm_mte intrinsics) is not enabled in M148 at the build level; the runtime MTE support in PartitionAlloc covers what's actually deployed. | Not enabled | — | — |
| -fstack-clash-protection | -fstack-clash-protection would insert stack probes to detect stack/heap clash, but no such flag is set in M148's build configuration. | Not found in M148 | — | — |
| HWASan / ASan | Sanitizer builds only — -fsanitize=address (AddressSanitizer) and -fsanitize=hwaddress (HWASan, a hardware-tagged ASan based on Top-Byte-Ignore on arm64); used in dev and fuzz builds, not shipped. | Debug + fuzz only | All (HWASan: Android arm64) | — |
| Mitigation | Details | Enabled? | OS | Bypass |
|---|---|---|---|---|
| CORB | Cross-Origin Read Blocking (orb_api.h) prevents cross-origin responses with protected MIME types (HTML, XML, JSON) from reaching renderers that should not see them; respects X-Content-Type-Options: nosniff. | Default on (coexists with ORB) | All | Partial |
| ORB | Opaque Response Blocking (services/network/orb/, orb_impl.h) is CORB's successor, blocking cross-origin opaque responses from being delivered to renderers based on a stricter classification. | Default on (rolling) | All | Limited |
| COOP / COEP / CORP | Cross-Origin-Opener-Policy, Cross-Origin-Embedder-Policy, and Cross-Origin-Resource-Policy headers (with values such as require-corp and same-origin-allow-popups) gate window.crossOriginIsolated, which in turn gates SharedArrayBuffer and high-resolution timers. | Opt-in (by site) | All | Yes |
| Document Isolation Policy (DIP) | Document Isolation Policy (content/browser/security/dip/, feature kDocumentIsolationPolicyWithoutSiteIsolation) grants a document crossOriginIsolated without requiring full Site Isolation for the embedding context. | Opt-in (feature) | All | New |
| Fetch Metadata | Chrome attaches Sec-Fetch-Site / -Mode / -Dest / -User / -Storage-Access headers (sec_header_helpers.cc) so servers can reject unexpected requests — CSRF, XSSI, Spectre cross-origin loads — at the edge. | Default on (Chrome sends) | All | — |
| Private Network Access (PNA) | Private Network Access (a.k.a. CORS-RFC1918 / Local Network Access; local_network_access_checker) blocks requests from public origins to private/local IP ranges unless preflighted, mitigating router and intranet attacks. | Default on (rolling) | All | Limited |
| HTTPS-Upgrades + HTTPS-First | Chrome automatically attempts HTTPS first for http:// navigations; HTTPS-First Mode (HTTPS-Only Mode) requires explicit user opt-in to fall back to plaintext (force_no_https_upgrade). | Default on | All | Limited |
| Encrypted Client Hello (ECH) | Encrypted Client Hello encrypts the TLS SNI using a public key fetched from the DNS HTTPS resource record (SVCB family), preventing on-path observers from learning the hostname being connected to. | Default on | All | — |
| HSTS preload | Chrome ships an HTTP Strict Transport Security preload list (transport_security_state, AddHSTS) so listed hosts are HTTPS-only from the first request — no opportunity for SSL stripping on initial connect. | Default on | All | Limited |
| Schemeful Same-Site | Schemeful Same-Site (schemeful_site.h) treats http:// and https:// versions of the same eTLD+1 as cross-site for SameSite cookie purposes, closing a downgrade-attack path. | Default on | All | Limited |
| SameSite=Lax default | Cookies without an explicit SameSite attribute default to Lax (CookieSameSite::LAX_MODE, kSameSiteByDefaultCookies), mitigating cross-site request forgery. | Default on | All | Yes |
| First-Party Sets | First-Party Sets, also called Related Website Sets (FPS / RWS — GlobalFirstPartySets, FirstPartySetsHandlerImpl), let a controlling site declare a set of related domains that share certain storage and identity boundaries. | Default on | All | — |
| DIPS | Bounce Tracking Mitigations (DIPS — btm_bounce_detector, Privacy.DIPS UMA) identify domains used purely as redirect-bounce trackers and clear their state. | Default on | All | — |
| BFCache security | Back-Forward Cache (BackForwardCacheImpl) keeps the prior page's renderer process alive but isolated; pages only enter BFCache when no cross-process leaks are possible. | Default on | All | Limited |
| Trusted Types | Trusted Types (TrustedHTML, TrustedScript, TrustedScriptURL, trusted_types_names.h) require typed wrappers before passing strings to DOM-XSS sinks; opt-in for web content, mandatory for chrome:// WebUI. | Opt-in (mandatory for WebUI) | All | — |
| CSP / XFO / nosniff | Content-Security-Policy (with directives such as script-src and frame-ancestors), X-Frame-Options for legacy framing, and X-Content-Type-Options: nosniff to disable MIME sniffing — all enforced where sites set them. | Default on (where set by site) | All | Yes |
| Permissions Policy | Permissions Policy (formerly Feature Policy, with sibling Document Policy; permissions_policy/) lets a document gate which browser features can be used by itself and by embedded frames. | Default on | All | — |
| Mitigation | Details | Enabled? | OS | Bypass |
|---|---|---|---|---|
| Site Isolation (Spectre) | Site Isolation places different sites in different OS processes (ChildProcessSecurityPolicy enforces the boundary), so a Spectre v1/v2 transient-execution read inside a renderer cannot reach another site's data — that data lives in another address space entirely. | Default on (desktop); partial (Android) | All | Yes |
| SAB COOP+COEP gating | SharedArrayBuffer is only available to documents that have set COOP+COEP headers granting crossOriginIsolated (SharedArrayBufferIssueType tracks violations), since SAB enables the high-resolution timers Spectre PoCs depend on. | Default on | All | Yes |
| performance.now() clamp | ClampTimeResolution (time_clamper.h) quantizes performance.now() to kCoarseResolutionMicroseconds = 100µs without crossOriginIsolated and kFineResolutionMicroseconds = 5µs with it, denying easy high-resolution timing for side-channel attacks. | Default on | All | Yes |
| OS microarch mitigations | Microarchitectural mitigations against branch-target-injection and related transient-execution attacks — retpoline, IBRS / IBPB / eIBRS, STIBP — are configured at the OS/kernel level; Chrome inherits whatever the OS provides. | OS-level (Chrome inherits) | All | Yes |
| Cross-domain SMT restriction | MITIGATION_RESTRICT_CORE_SHARING (Windows 11 24H2+) prevents the renderer's threads from sharing an SMT/hyperthread core with another security domain, mitigating cross-domain hyperthread side-channel leaks. | Default on (where supported) | Windows 11 24H2+ | New |
Not all the mitigations at once are at work against every Chrome exploit chain. An exploit would usually hit a small subset of mitigations, which depends on the bug class, operating system, hardware architecture, field trials configuration, user configuration, Chrome version, specific bug instance shape, and other factors.
That said, certain mitigations are more prohibitive while others are mostly obstacles in the exploit engineering process.
Familiar exploit mitigations – such as ASLR and CFI – are still relevant and present a substantial obstacle to exploit engineering for modern Chrome.
Consider ASLR. The common bounce-off judgement from an "exploit expert": "Just find an infoleak bug to bypass it". Technically correct, but... In my experience, reality differs.
First, pure memory disclosure vulnerabilities in Chrome are rare. This is both statistical and structural. I looked at most Chrome security bugs of the past year, plus selectively picked prior samples. I didn't get an impression that a memory disclosure bug in Chrome is easy.
Furthermore, an infoleak bug must be located in the same process as the main bug to be chainable, which constrains the search space. Every process type in Chrome is a different world with its own anti-patterns, architectural models and implementation details. A problem like "I need to find an infoleak in renderer and another one in the browser process" isn't the same problem repeated twice, it's two different specialization projects.
Historically, ASLR in Chrome renderer was weakened by JavaScript engine type confusion vulnerabilities. A typical JSE bug is powerful enough that it can work twice: first to disclose memory and break ASLR, then to execute the code. [1] Enablement of v8 sandbox in M124 broke this convenience, and made ASLR hard again for the Chrome renderer.
In contrast, the broker process, GPU process and other high-privilege browser processes never had this kind of a luxury bug class to coast through ASLR, which is why the majority of wild 0-day chains for Chrome use either logic bugs for the sandbox escape part, or an OS kernel EoP (another restricted attack surface), or settle on a renderer-only payload, or attack a peculiar configuration – all compromises driven by the cost of an honest infoleak chain in the browser process.
Another assumed solution to bypass ASLR is to "make" an infoleak out of a compatible use-after-free bug. In fact, PartitionAlloc blocks many state manipulations on use-after-free bugs, while MiraclePtr shrinks the attack surface.
A similar situation with CFI/CFG (Control Flow Integrity). In Chrome, CFI is baked into every object vtable dispatch by the compiler, excluding a handful of code-annotated exceptions, which makes the coverage broad and solid. It means that any accidental vtable call on a use-after-free object will hit a hard crash, and if that happens early enough in the bug's state machine, it becomes unexploitable.
In general, most of the old-gen mitigations are not invariants, but rather cost-raisers. An encounter with them is often bypassable with the same bug and a bit of luck. Newer Chrome-specific mitigations tend to be more solid. If a bug is blocked by it, it's often game over – go find another one.
Further sections deal with the newer mitigations that I experienced.
MiraclePtr – also known as BackupRefPtr or BRP – is a fairly recent and ambitious addition to Chrome's list of exploit mitigations. It aims to make use-after-free bugs unexploitable without eliminating them, at the cost of manually wrapping each pointer in raw_ptr<T>, plus some runtime overhead. Design docs and developer's instructions are published [1 2 3], although it's best understood by reading the code, which is compact.
The security invariant offered by MiraclePtr: "as long as a dangling raw_ptr<T> exists, the slot is not reused". This is achieved by 1) counting pointer instances in a field under InSlotMetadata, and 2) quarantining the freed memory if InSlotMetadata counter isn't zero on free. Coverage is per-field and per-process, not whole-allocator.
I first encountered MiraclePtr at the stage of vulnerability discovery. I found a bug by static analysis; it looked good until I tested it with Address Sanitizer...
That was surprising: I knew about raw_ptr<T> and didn't see it in the code? Turns out that MiraclePtr was recently (and quietly) rolled into some popular chromium idioms, so it's invisible in the code unless you look deep into familiar system internals. Sneaky!
I spent some time looking at the MiraclePtr implementation, and couldn't find a bypass in the given time. It's quite obvious that the code was heavily scrutinized and hedged against anything that could go wrong. I didn't bother reporting the bug – Google officially considers MiraclePtr-PROTECTED bugs strongly mitigated. On those Chromium builds that don't enable MiraclePtr mitigation, it's still an exploitable 0-day.
Meanwhile, my other bugs escaped MiraclePtr – they are UNPROTECTED and exploitable in release Chrome before the patch.
PartitionAlloc manages all the memory in official Chrome, including C++ new operator, and excluding v8 (which uses Oilpan). PartitionAlloc has been ramping exploit mitigations for years, and by now is a bit of a beast. MiraclePtr implementation is based on PartitionAlloc infrastructure, too. Official documentation is in the chromium docs [4].
Out of many mitigations in PartitionAlloc, Thread Cache itself – as in, separate freelists per thread – doesn't have a reputation as an exploit blocker. Indeed, it was built as a performance optimization and never offered any strong security invariants. In reality, it quietly mitigates many bug shapes in the browser process.
I have dealt with Thread Cache directly and passed by encoded freelist pointers, cacheline poisoning (0xbadbad00), and scheduler loop quarantine (0xcdcdcdcd) when working on the exploit for a double-free issue in the browser process. The latter was surprising, since Scheduler Loop Quarantine isn't supposed to be enabled yet – it's actually enabled in Dev channel via field trials configuration.
Consider the following scenario: a use-after-free bug in the browser process. As of current Chromium, the majority of objects in the browser process live in the same PA root partition, so reclaiming the freed object doesn't look hard in code. The problem is the multi-threading architecture of the browser process combined with the per-thread cache. This combination constrains object reclamation paths to those used in the vulnerable thread, which substantially shrinks the opportunity space to even begin to control the state machine from the bug. The following diagram illustrates the problem.
In this scenario, a bypass of Thread Cache depends on the bug's control flow graph escaping between free and re-use of the vulnerable object to allocate memory elsewhere to force a purge to the shared bucket. If the bug's code site is tight between free and re-use, it becomes unexploitable and heap grooming via another thread doesn't help. The same bug without Thread Cache would be exploitable. The specific bug that I worked with was of the former type and was effectively blocked by the mitigation in official Chrome.
v8 sandbox works by breaking common JavaScript Engine exploit idioms at a deep level of abstraction, which makes it efficient. Consider in-object pointer indirection, one of the key features of v8 sandbox. In a JavaScript engine, many object types have a pointer in them, which points to additional data (such as array contents in an ArrayBuffer). A typical JavaScript bug allows to craft a fake JavaScript object, which the unsuspecting engine then uses – dereferencing the fake inner pointer and thereby reading/writing to arbitrary memory whatever the JavaScript code requires. This allows to build an arbitrary RW primitive through the bug, a canonical JavaScript engine exploitation technique. The v8 sandbox replaces in-object pointers with indexes in out-of-sandbox tables, which breaks the exploit primitive universally. This feature alone isn't sufficient to break every exploit, and v8 sandbox doesn't end on it. Many official design docs are available [5 6].
All known public bypasses of the v8 sandbox, including 0-days, are bugs in the sandbox code. I looked at most of v8 sandbox bypasses (more than 80 issues in 2025), the bug patterns were eliminated systematically, and the implementation is substantially hardened now.
My renderer exploit bypasses the v8 sandbox by using a special kind of v8 bug that isn't fully covered by the sandbox. The exploit achieves arbitrary address read and write. This is an interesting attack surface for lateral thinking about attacking v8 in post-v8 sandbox era.
0xbadd1e) and %rbx (0xc0ffee).Chrome exploit mitigations continue to harden while being asymmetrically concentrated in the browser process and high-privilege processes, with the renderer and v8 now catching up.
Bypassing Chrome-specific mitigations head-on becomes nearly impossible; the open path lies through blind spots in coverage, platform-specific weaknesses, and bugs in mitigation code.
All current mitigations have limits, some of them structural (e.g., optimization). Those limits mark the edge of the practical attack surface.
Logic issues such as UXSS gain importance, as they are the least covered by current exploit mitigations.
Browser Exploit Design — hands-on self-paced training in modern browser exploitation. Focus on class-wide models, canonical bug classes and exploit techniques, and full-chain perspective.
Pi — Pattern Insight — subscription intelligence service with in-depth research and analysis.