90123314693b2bb25e5f93e343f37745d2940f3f Resolve UNSAFE_TODO in DOMSharedArrayBuffer::Create(const void*, ...)
Triage note: Mechanical UNSAFE_TODO/spanification of a raw copy in SharedArrayBuffer creation; sizes are equal by construction so behavior is unchanged. Memory-safety hardening, not exploitable.
The bug at a glance
This is a spatial-safety refactor, not a live bug fix. At the moment it landed, the single caller path allocates the destination ArrayBufferContents with exactly byte_length bytes and immediately copies byte_length bytes from source, so destination and source sizes are in lock-step and no out-of-bounds access occurs in tree. The change is graded low/watch-tier because it removes a latent footgun class rather than closing an exploitable primitive: the old signature Create(const void* source, unsigned byte_length) carried the pointer and its length as two independent arguments with no compiler-enforced relationship, so any future or refactored caller that passed a byte_length larger than the real source allocation would produce an over-read of the source region (an info-leak read primitive, since the extra bytes get copied into a SharedArrayBuffer that JS can read). Because no such mismatched caller exists today, there is no CVE and no exploit; the fix is preventative. It is watch-tier rather than ignore-tier because DOMSharedArrayBuffer content is directly readable by attacker JavaScript, so a length desync here would be a real (not theoretical) heap disclosure sink, and SharedArrayBuffer sits in a security-sensitive area (cross-origin isolation, Spectre mitigations).
For a defender, the value here is understanding the sink, not the (absent) bug. DOMSharedArrayBuffer::Create copies raw source bytes into a shared backing store that attacker-controlled JavaScript can read directly and share across agents. The pre-patch API accepted a bare (pointer, length) pair with the length trusted blindly by memcpy. Anyone auditing Blink for heap-disclosure primitives should treat ‘raw memcpy into a JS-visible buffer where the length comes from a different source than the pointer’ as a high-value pattern: if the length ever exceeds the true source allocation, the excess adjacent heap bytes are copied into a buffer JS reads at leisure. This commit is Opera/Blink’s ongoing spanification campaign eliminating exactly that shape, replacing the trusted-length memcpy with span::copy_from, whose length-equality CHECK converts a would-be silent over-read into a hard, non-exploitable crash.
Root cause
The root cause being addressed is API-level pointer/length desynchronization risk. The original static factory took Create(const void* source, unsigned byte_length). It allocated an ArrayBufferContents of byte_length bytes (kShared, kDontInitialize), then performed UNSAFE_TODO(memcpy(contents.DataShared(), source, byte_length)). memcpy performs no validation: it trusts that source points to at least byte_length readable bytes. The UNSAFE_TODO macro is Chromium’s explicit marker that this call has not yet been proven memory-safe by the spanification effort and needs review. Because the source pointer and its length were passed as two decoupled scalars, the type system could not enforce that byte_length matched the real extent of the source buffer.
The fix changes the parameter to a single base::span source, which bundles pointer and length into one object whose length is derived from the source itself. The destination is now allocated with source.size() (the span’s own length), and the copy is performed with contents.ByteSpanShared().copy_from(source). base::span::copy_from CHECK-fails if the source and destination span sizes differ, so a mismatch can no longer silently over-read into the destination. A new helper ArrayBufferContents::ByteSpanShared() is added as the shared-memory analog of ByteSpanMaybeShared(), with a DCHECK(IsShared()) to assert the backing store is actually shared. This mirrors the equivalent already-spanified DOMArrayBuffer::Create overload, so the change is a consistency/parity fix as much as a safety fix.
Net effect: the destination and source are now guaranteed by construction to be the same length (destination sized from source.size()), and the only path that could produce a size mismatch is one where a caller constructs a span whose stated length exceeds its true backing — which is itself the safer, more auditable failure mode because copy_from will CHECK rather than memcpy silently reading OOB.
Key code
Trusted-length memcpy replaced by a length-checked span copy; pointer+length collapsed into one span
- static DOMSharedArrayBuffer* Create(const void* source,
- unsigned byte_length) {
+ static DOMSharedArrayBuffer* Create(base::span<const uint8_t> source) {
ArrayBufferContents contents(
- byte_length, 1, ArrayBufferContents::kShared,
+ source.size(), 1, ArrayBufferContents::kShared,
ArrayBufferContents::kDontInitialize,
ArrayBufferContents::AllocationFailureBehavior::kCrash);
CHECK(contents.IsValid());
- UNSAFE_TODO(memcpy(contents.DataShared(), source, byte_length));
+ contents.ByteSpanShared().copy_from(source);
return Create(std::move(contents));
Patch walkthrough
third_party/blink/renderer/core/typed_arrays/array_buffer/array_buffer_contents.h— Adds ByteSpanShared() returning base::span<uint8_t> over the shared backing store, with DCHECK(IsShared()) to assert the store is shared. It delegates to the existing ByteSpanMaybeShared(), which itself wraps Data()/DataLength() in a documented SAFETY block. This gives Create a length-carrying accessor for the destination so it can call copy_from instead of taking a raw DataShared() pointer.third_party/blink/renderer/core/typed_arrays/dom_shared_array_buffer.h— Changes the factory signature from (const void*, unsigned) to (base::span), sizes the destination allocation from source.size(), replaces the UNSAFE_TODO memcpy with contents.ByteSpanShared().copy_from(source), and drops the now-unneeded base/compiler_specific.h include (which provided UNSAFE_TODO). Callers must now pass a span, forcing pointer+length to travel together.
Background
UNSAFE_TODO — A Chromium macro that marks a pointer-arithmetic or raw-memory call site (like memcpy) that has not yet been converted to a memory-safe span-based form. It compiles normally but flags the code as owing a spatial-safety review. Resolving UNSAFE_TODO means proving or enforcing the bounds and removing the raw call.
base::span::copy_from — Copies from a source span into a destination span, CHECK-failing at runtime if the two spans differ in size. This is the key safety upgrade over memcpy: a length mismatch becomes a controlled crash instead of a silent over-read or over-write.
SharedArrayBuffer as a disclosure sink — Bytes copied into a DOMSharedArrayBuffer are directly readable by JavaScript and shareable across same-agent-cluster contexts. Any surplus heap bytes copied in during construction would be observable to script, which is why a length desync in this factory would be an information-disclosure primitive, not a mere crash.
kDontInitialize allocation — The destination is allocated without zero-initialization for performance, on the assumption the following copy fully overwrites it. If the copy ever wrote fewer bytes than the allocation, uninitialized heap would remain readable by JS — another reason correct length coupling matters here.
Vulnerability window
- Introduced — The (const void*, unsigned) factory with a trusted-length memcpy predates the spanification effort; it was tagged UNSAFE_TODO as part of Chromium’s ongoing raw-pointer audit.
- Fixed — 2026-01-20, authored by Fredrik Soderquist (Opera), reviewed by Kent Tamura. Tracked under crbug 451652367 as a spatial-safety cleanup mirroring the already-spanified DOMArrayBuffer overload. Landed on main@{#1571914}.
- Backport — None indicated. No [M###] milestone tag on the subject; this landed on trunk as ordinary hardening and was not cherry-picked.
Proof of concept
No test ships in this diff and no exploit is possible against in-tree code, because every caller allocates the destination to exactly the source length, so no out-of-bounds copy occurs. To demonstrate the class the fix guards against, one would have to reintroduce a caller that passed a byte_length larger than the real source allocation (the pre-patch shape), which cannot exist post-patch since the span carries its own length. No exploit is demonstrated and none is claimed.
Exploitation
- Reach the sink — DOMSharedArrayBuffer::Create(span) is reachable from internal Blink code that materializes SharedArrayBuffers from source bytes. An attacker does not call it directly; they would need a caller with an attacker-influenced length that outruns the source buffer.
- Trigger the over-read (pre-patch class only) — Blocked in tree: no caller supplies a mismatched length. Post-patch it is structurally blocked because the length comes from source.size() and copy_from CHECKs equality. There is no path from web content to an exploitable state.
- Exfiltrate — Had a desync existed, adjacent heap bytes would be copied into a JS-readable SharedArrayBuffer and read out with a typed-array view. This is the theoretical payoff and the reason the sink is worth hardening, but it is not achievable against current or patched code.
Detection & hunting
For defenders and detection engineers:
- copy_from CHECK crash in ArrayBufferContents/DOMSharedArrayBuffer::Create — Post-patch, a renderer crash inside base::span::copy_from on the Create path indicates a caller with a genuine size mismatch — investigate that caller as a potential corruption or logic bug rather than assuming benign.
- Reintroduced raw memcpy into shared buffers — Flag any new code copying into DataShared()/DataMaybeShared() via memcpy with a separately-supplied length; that is the regressed pattern this CL removed.
Audit directions
- Blink typed_arrays factories — Audit remaining ArrayBuffer/SharedArrayBuffer construction paths for any surviving (void*, length) signatures or UNSAFE_TODO(memcpy(…)) into JS-visible backing stores; those are the same pointer/length-desync footgun this CL closed.
- Callers of DOMSharedArrayBuffer::Create — Verify each caller builds its source span from the true backing extent (not a separately tracked length variable), since the span’s stated size now drives the destination allocation and any lying span would CHECK at copy_from.