# **Security Report: AdditiveSafeInteger Plain Conversion Loses Range/NaN Guard and Forges Out-of-Range SlicedString Views, Enabling a V8 Heap Out-of-Bounds Write**

**Reporter:** OpenAI Codex Security  
**Organization:** OpenAI  
**Component:** V8 JavaScript Engine (TurboFan simplified lowering / `String.prototype.slice`)  
**Affected Area:** `PlainPrimitiveToNumber`, `SpeculativeAdditiveSafeIntegerAdd`, inline `String.prototype.slice` lowering, `SlicedString` creation  
**Bug Class:** Lost checked-safe-integer conversion -> typer/current mismatch -> malformed `SlicedString` -> heap out-of-bounds write  

---

## **Summary**

TurboFan's additive-safe-integer lowering intentionally requires
`CheckedSafeIntTruncatingWord32` when a `SpeculativeAdditiveSafeIntegerAdd`
result is consumed as Word32. That checked conversion is supposed to
deoptimize when an operand is not an integer or is outside the additive-safe
range.

`PlainPrimitiveToNumber` does not preserve that requirement. When its consumer
requests a Word32 value, the `NumberOrOddball` fast path unconditionally lowers
through `UseInfo::TruncatingWord32()`. That drops the
`truncation.check_safe_integer()` bit and turns `undefined` into machine Word32
`0`, even though JavaScript `ToNumber(undefined)` is `NaN`.

The submitted exploit feeds that silent current/target mismatch into the inline
reducer for ordinary `String.prototype.slice(start, end)`. Typer proves the
slice endpoints are strictly negative under JavaScript semantics, so the
reducer selects the negative-index path and then installs unchecked
`TypeGuard<UnsignedSmall>` nodes before `StringSubstring`. The malformed
machine current instead supplies non-negative endpoints, so the optimized code
constructs `length + start` and `length + end` values that lie beyond the
original string.

With default `string_slices` behavior enabled, `StringSubstring` creates a
valid-looking `SlicedString` whose stored offset extends outside its parent.
That malformed `SlicedString` is the bug's resulting memory-safety primitive.
To demonstrate write impact, the PoC feeds it into the ordinary `RegExp`
constructor. `RegExp`'s source-escaping helper counts required escapes in one
pass, allocates its result from that count, and then walks the source a second
time to write the escaped bytes. Because the forged slice points outside its
parent, the two passes can observe different neighboring heap contents across
the allocation and GC point. The first pass can size the result for one slash
while the second pass sees thousands of newline or `\u2028` characters and
writes the expanded escape sequences past the end of the allocated result
string.

The exploit reproduces that out-of-bounds heap write impact in two
forms: a one-byte version where bytes after the undersized result visibly
contain the continuing escaped output, and a two-byte variant that reaches a
deterministic write fault inside the inlined `WriteEscapedRegExpSource()` store
sequence in `RegExp::Compile`.

The same malformed slice also provides a reusable out-of-bounds read through
ordinary `charCodeAt()` calls. That read primitive is useful for locating and
validating the corrupted neighborhood, but it is not the primary impact claim
of this report.

A no-flag malformed-slice/disclosure PoC was confirmed on current upstream main.
The deterministic write witness described below uses
native syntax only as a development aid to stabilize and inspect the heap
layout around the same unmodified engine bug; it does not rely on the memory
corruption API or a patched V8.

## **Impact**

Malicious JavaScript running in the default-configuration V8 engine can turn
the malformed `SlicedString` produced by this compiler bug into a real heap
out-of-bounds write. The demonstrated sink is ordinary `RegExp` source
escaping: it allocates from a first-pass size calculation and then emits more
bytes than were reserved during the second pass.

Current-main one-byte writer exploit:

```text
$ out/additivesafe-main-release/d8 --allow-natives-syntax regexp-escape-window-observe.js -- 14250
N=14250 PRE=16383,0 POST=0,8788 RE=16385,0,0,4395 OOB=3@0:6e5c6e0501000003000000004000002f414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141
```

The escaped result still has the short first-pass length (`16385`), while the
second pass emits thousands of backslash escapes. The bytes immediately after
the nominal result already contain the continuing `n\\n` escape pattern,
proving that the write crossed the result boundary.

The two-byte variant reaches a deterministic write fault from the same
`WriteEscapedRegExpSource()` path:

```text
Received signal 11 SEGV_ACCERR 26f201240000
...
RegExp::Compile+0xe17
...
```

## **Root Cause (High Level)**

TurboFan reasons about the same value through two incompatible views:

```text
consumer view:
    AdditiveSafeInteger + Word32 truncation requires a checked-safe conversion

producer view:
    PlainPrimitiveToNumber sees only "used as Word32"
    -> emits plain TruncatingWord32
    -> identifies undefined/NaN with integer 0
```

The additive consumer explicitly requests a deoptimizing checked conversion,
but `PlainPrimitiveToNumber` forwards only the Word32 shape and silently drops
the safety bit. The resulting machine current no longer matches the optimistic
type information still attached to the graph.

`String.prototype.slice` then consumes the stale type facts. The reducer
chooses the negative-index branch, creates unchecked unsigned endpoint guards,
and feeds them directly into `StringSubstring`. Because the forged current is
non-negative, the generated code produces `length + attacker_delta` endpoints
that are outside the original string while still satisfying the reducer's
local ordering checks.

Once the forged slice exists, it can be used against consumers that rely on
string immutability. The demonstrated write sink is `RegExp`: its first and
second source-escape passes can observe different heap bytes around the
allocation point, so the size calculation and the write pass disagree and the
second pass writes beyond the under-allocated result.

It is likely that this, or some other sink, can be used to achieve more targeted
corruption and potentially arbitrary read/write in the v8 heap sandbox, although
that has not yet been demonstrated here.

## **Affected Versions**

The vulnerability is present when the following source properties are present:

```text
- SpeculativeAdditiveSafeIntegerAdd propagates CheckedSafeIntTruncatingWord32.
- PlainPrimitiveToNumber lowers NumberOrOddball Word32 uses via TruncatingWord32.
- That branch does not preserve truncation.check_safe_integer().
- String.prototype.slice lowers negative endpoints through TypeGuardUnsignedSmall.
- StringSubstring creates slices from the current endpoints without a bounds check.
- StringCharCodeAt also trusts the forged slice's own length and direct-string offset.
```

Exploitability was confirmed on:

```text
V8 main: a90254a7f732826783513c7b58de48debce4cd0d
V8 version: 15.2.0 (candidate)
Architecture: x86-64, pointer compression and V8 sandbox enabled
Build: is_debug=false, dcheck_always_on=false, memory_corruption_api=false
```

The oldest V8 line that contains the buggy lowering is 13.5. The introducing
commit lands after the 13.4 branch point (`main@{#98459}`) and before the 13.5
branch point (`main@{#99020}`), so the 13.5 branch is the first line where the
bug exists in source. That line is not default-reachable, however:
`additive_safe_int_feedback` remains `false` by default on the 13.5, 13.6,
13.7, 13.8, and 13.9 branches and is only reachable there if an embedder opts
into the feature flag.

The default-on Stable boundary is V8 14.0. The permanent enablement change is:

```text
ea1d2d78e05d90caf4e78fe7dfd6acb0c4f7e07e
[turbofan] Enable additive safe int feedback
Cr-Commit-Position: refs/heads/main@{#100967}
Commit date: 2025-06-23
```

The 13.9 branch was cut immediately before that change
(`main@{#100941}`) and still carries:

```cpp
DEFINE_BOOL(additive_safe_int_feedback, false, ...)
```

The 14.0 branch was cut later (`main@{#101731}`) and carries:

```cpp
DEFINE_BOOL(additive_safe_int_feedback, true, ...)
```

Therefore this is not limited to Dev/Beta-only builds. The first affected
default-configuration Stable V8 release is:

```text
V8 14.0.365.4
Chrome Stable 140.0.7339.80 (Linux)
Release date: 2025-09-02
```

Relevant release and source references:

- [V8 13.9 branch head](https://chromium.googlesource.com/v8/v8/+/refs/heads/13.9-lkgr)
- [V8 13.9 default-off flag definition](https://chromium.googlesource.com/v8/v8/+/refs/heads/13.9-lkgr/src/flags/flag-definitions.h#726)
- [V8 14.0 branch head](https://chromium.googlesource.com/v8/v8/+/refs/heads/14.0-lkgr)
- [V8 14.0 default-on flag definition](https://chromium.googlesource.com/v8/v8/+/refs/heads/14.0-lkgr/src/flags/flag-definitions.h#746)
- [Default-on enablement change](https://chromium.googlesource.com/v8/v8/+/ea1d2d78e05d90caf4e78fe7dfd6acb0c4f7e07e%5E%21/)
- [V8 version numbering and Chrome branch mapping](https://v8.dev/docs/version-numbers)
- [Chrome 140 Stable desktop release announcement](https://chromereleases.googleblog.com/2025/09/stable-channel-update-for-desktop.html)

The submitted archive had originally been validated on:

```text
V8 revision: 5068658e0b754814453e62dfe5898027ee052506
V8 version: 15.1.0 (candidate)
```

Current main is 607 commits after that initially tested revision, and the same
vulnerable lowering structure remains present.

The source change that first makes this mismatch possible is:

```text
0a1fae9e77c6d8e85d8197b4f4396815ec9194b9
[turbofan] Use AdditiveSafeInt feedback for faster int add/sub
Cr-Commit-Position: refs/heads/main@{#98643}
Commit date: 2025-02-11
```

That change adds the additive-safe feedback mode and the
`CheckedSafeIntTruncatingWord32` consumer requirement. The pre-existing
`PlainPrimitiveToNumber` Word32 path already downgraded
`NumberOrOddball` inputs to `TruncatingWord32`, so the commit introduces the
incompatible producer/consumer combination.

The source change is available at:

[https://chromium.googlesource.com/v8/v8/+/0a1fae9e77c6d8e85d8197b4f4396815ec9194b9%5E%21/](https://chromium.googlesource.com/v8/v8/+/0a1fae9e77c6d8e85d8197b4f4396815ec9194b9%5E%21/)

---

## Detailed Analysis

### AdditiveSafeInteger Requires a Checked Word32 Conversion

`UseInfo::CheckedSafeIntTruncatingWord32` explicitly carries a
`Word32WithSafeIntCheck()` truncation and the
`kAdditiveSafeInteger` type-check kind:

[https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/compiler/use-info.h#L35-L40](https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/compiler/use-info.h#L35-L40)

[https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/compiler/use-info.h#L275-L286](https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/compiler/use-info.h#L275-L286)

```cpp
static Truncation Word32WithSafeIntCheck() {
  return Truncation(TruncationKind::kWord32, kIdentifyZeros, true);
}

static UseInfo CheckedSafeIntTruncatingWord32(
    const FeedbackSource& feedback) {
  return UseInfo(MachineRepresentation::kWord32,
                 Truncation::Word32WithSafeIntCheck(),
                 TypeCheckKind::kAdditiveSafeInteger, feedback);
}
```

The additive-safe reducer uses that checked conversion when the result is
consumed as Word32:

[https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/compiler/simplified-lowering.cc#L1876-L1888](https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/compiler/simplified-lowering.cc#L1876-L1888)

```cpp
if (truncation.IsUsedAsWord32() && !TypeOf(node).IsNone()) {
  // We *must* propagate the CheckedSafeIntTruncatingWord32 information.
  VisitBinop<T>(node,
                UseInfo::CheckedSafeIntTruncatingWord32(FeedbackSource{}),
                MachineRepresentation::kWord32);
  if (lower<T>()) ChangeToPureOp(node, Int32Op(node));
  return;
}
```

The safety invariant is clear: before replacing the operation with pure
wrapping `Int32Add`, each input must still deoptimize if it is not an integer
or is outside the intended additive-safe range.

### `PlainPrimitiveToNumber` Drops the Safety Bit

On current main, the `PlainPrimitiveToNumber` Word32 branch checks only
`truncation.IsUsedAsWord32()` and then lowers `NumberOrOddball` inputs through
plain `TruncatingWord32()`:

[https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/compiler/simplified-lowering.cc#L4431-L4478](https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/compiler/simplified-lowering.cc#L4431-L4478)

```cpp
} else if (truncation.IsUsedAsWord32()) {
  if (InputIs(node, Type::NumberOrOddball())) {
    VisitUnop<T>(node, UseInfo::TruncatingWord32(),
                 MachineRepresentation::kWord32);
    if (lower<T>()) {
      DeferReplacement(node, InsertSemanticsHintForVerifier(
                                 node->op(), node->InputAt(0)));
    }
  }
```

That branch does not inspect `truncation.check_safe_integer()` and does not
preserve the checked-safe conversion requested by the consumer. This is
especially conspicuous because several neighboring lowering paths were
recently hardened to guard integer shortcuts with
`!truncation.check_safe_integer()`.

For the submitted trigger:

```javascript
let n = +(route ? TK : undefined);
let base = (n + C) | 0;
```

the JavaScript target value for the malicious route is `NaN`, but the lowered
current becomes Word32 `0`. The following additive-safe add therefore executes
on a forged integer current even though the graph still carries the
`NaN | Range(0, 0)` target type.

### The Current Trace Still Shows the Bad Conversion on Main

The fresh TurboFan trace captured from the current-main build contains the same
decisive chain:

```text
===== V8.TFTypedLowering =====
37 PlainPrimitiveToNumber
58 SpeculativeAdditiveSafeIntegerAdd[AdditiveSafeInteger]
113 TypeGuard[Unsigned30]
126 TypeGuard[Unsigned30]
132 StringSubstring

===== V8.TFSimplifiedLowering =====
58 Int32Add
113 TypeGuard[Unsigned30]
126 TypeGuard[Unsigned30]
132 StringSubstring
151 TruncateNumberOrOddballToWord32

===== schedule =====
151: TruncateNumberOrOddballToWord32(34)
58: Int32Add(151, 153) : (NaN | Range(0, 0))
113: TypeGuard[Unsigned30](107, 86, 41) : Range(0, 536870871)
126: TypeGuard[Unsigned30](120, 113, 41) : Range(0, 536870886)
132: StringSubstring(86, 113, 126, 126, 129) : String
```

This is the concrete lowering failure: a plain oddball-to-Word32 truncation
directly feeds the additive-safe `Int32Add`, and the malformed current survives
all the way into `StringSubstring`.

### `String.prototype.slice` Trusts the Stale Type Facts

The inline reducer for `String.prototype.slice` computes negative indices as
`max(length + index, 0)` and then applies only
`TypeGuardUnsignedSmall` before calling `StringSubstring`:

[https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/compiler/js-call-reducer.cc#L1337-L1362](https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/compiler/js-call-reducer.cc#L1337-L1362)

```cpp
TNode<Number> from_untyped =
    SelectIf<Number>(NumberLessThan(start_smi, zero))
        .Then(_ { return NumberMax(NumberAdd(length, start_smi), zero); })
        .Else(_ { return NumberMin(start_smi, length); })
        .Value();
TNode<Smi> from = TypeGuardUnsignedSmall(from_untyped);

TNode<Number> to_untyped =
    SelectIf<Number>(NumberLessThan(end_smi, zero))
        .Then(_ { return NumberMax(NumberAdd(length, end_smi), zero); })
        .Else(_ { return NumberMin(end_smi, length); })
        .Value();
TNode<Smi> to = TypeGuardUnsignedSmall(to_untyped);

return SelectIf<String>(NumberLessThan(from, to))
    .Then(_ { return StringSubstring(receiver_string, from, to); })
    .Else(_ { return EmptyStringConstant(); })
    .Value();
```

For the submitted payload, typer proves:

```text
start target = [-4112, -17]
end target   = [-4097,  -2]
```

The malformed current instead becomes:

```text
start current = delta
end current   = delta + 15
compiled from = string.length + delta
compiled to   = string.length + delta + 15
```

The surviving `Uint32LessThan(from, to)` proves only ordering. It does not
prove that either endpoint is still within the parent string.

### `StringSubstring` Produces the Malformed Slice Capability

`StringSubstring` simply forwards the current endpoints to `SubString`:

[https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/builtins/builtins-string-gen.cc#L1727-L1733](https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/builtins/builtins-string-gen.cc#L1727-L1733)

```cpp
TF_BUILTIN(StringSubstring, StringBuiltinsAssembler) {
  auto string = Parameter<String>(Descriptor::kString);
  auto from = UncheckedParameter<IntPtrT>(Descriptor::kFrom);
  auto to = UncheckedParameter<IntPtrT>(Descriptor::kTo);
  Return(SubString(string, from, to));
}
```

`SubString` checks only the slice width against the source length. For widths
in the normal sliced-string range, it directly allocates a `SlicedString`
using `offset = from + parent_offset`:

[https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/builtins/builtins-string-gen.cc#L2072-L2132](https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/builtins/builtins-string-gen.cc#L2072-L2132)

```cpp
const TNode<IntPtrT> substr_length = IntPtrSub(to, from);
const TNode<IntPtrT> string_length = LoadStringLengthAsWord(string);
GotoIf(UintPtrGreaterThanOrEqual(substr_length, string_length),
       &original_string_or_invalid_length);
...
TNode<IntPtrT> offset = IntPtrAdd(from, to_direct.offset());
...
var_result = AllocateSlicedOneByteString(
    Unsigned(TruncateIntPtrToInt32(substr_length)), direct_string,
    SmiTag(offset));
```

Later consumers trust the forged slice's own length and direct-string offset.
For example, `StringCharCodeAt` checks the queried index against the forged
slice's short visible length and then loads from the direct parent plus the
stored offset:

[https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/codegen/code-stub-assembler.cc#L9371-L9412](https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/codegen/code-stub-assembler.cc#L9371-L9412)

```cpp
CSA_DCHECK(this, UintPtrLessThan(index, LoadStringLengthAsWord(string)));
...
const TNode<UintPtrT> offset =
    UintPtrAdd(index, Unsigned(to_direct.offset()));
...
var_result = Load<Uint8T>(string_data, offset);
```

In the release build used for validation, the `CSA_DCHECK` is not a runtime
mitigation. The forged slice remains a JavaScript-visible capability that can
be queried repeatedly and, more importantly for this report, can be handed to
`RegExp` as if it were an ordinary immutable source string.

### Demonstrated Write Impact Through `RegExp`'s Two-Pass Escaping Writer

This is the primary demonstrated impact of the bug, not the root cause. The
root cause is the lost checked-safe-integer conversion and the resulting
out-of-range `SlicedString`. That malformed `SlicedString` can then violate an
assumption used by the source-escaping helper in `src/regexp/regexp.cc`: the
helper counts required escapes in one pass, allocates the output string from
that count, and then walks the source again to write the escaped bytes.

[https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/regexp/regexp.cc#L231-L380](https://github.com/v8/v8/blob/a90254a7f732826783513c7b58de48debce4cd0d/src/regexp/regexp.cc#L231-L380)

```cpp
uint32_t additional_escape_chars =
    one_byte ? CountAdditionalEscapeChars<uint8_t>(source, &needs_escapes)
             : CountAdditionalEscapeChars<base::uc16>(source, &needs_escapes);
...
ASSIGN_RETURN_ON_EXCEPTION(isolate, result,
                           isolate->factory()->NewRawOneByteString(length));
return WriteEscapedRegExpSource<uint8_t>(source, result);
```

`CountAdditionalEscapeChars()` and `WriteEscapedRegExpSource()` each obtain a
fresh flat-character view of the same `String`. That is normally safe because
strings are immutable. It is not safe for a forged slice whose stored parent
offset points outside the parent object. If a GC occurs at the allocation point
between the two passes, the same forged slice can observe different neighboring
heap bytes before and after the allocation.

The practical write setup is:

1. make the first pass see a source containing only one unescaped `/`, so the
   escaped result is allocated with essentially the original length;
2. make the second pass see a different neighboring byte pattern containing
   many `\n` or `\u2028` characters; and
3. let the writer emit `\\n` or `\\u2028` sequences past the end of the
   under-allocated result object.

The development harness
`regexp-escape-window-observe.js` repeatedly reproduced the one-byte form on
current main. A representative run showed the same malformed source changing
from one slash plus `A` bytes to thousands of newline bytes across the
allocation boundary:

```text
PRE=16383,0 POST=0,8788 RE=16385,0,0,4395 OOB=3@0:6e5c6e...
```

The escaped result still had the short first-pass length (`16385`), but the
second pass wrote thousands of backslash escape bytes. The immediate bytes
after the result already contained the continuing `n\\n` pattern, proving a
heap out-of-bounds write rather than merely a stale read or parser oddity.

The two-byte variant in `regexp-escape-two-byte-observe.js` uses `\u2028`,
whose escape expands by five extra code units. On current main it reaches a
deterministic write fault inside the inlined `WriteEscapedRegExpSource()` store
sequence in `RegExp::Compile`, confirming that the write sink is not an
observer-only artifact and does not depend on a later read primitive.

This write primitive is also the path now being used to turn the bug into a
targeted in-cage corruption primitive. The most useful surgical target is a
nearby `FixedDoubleArray::length` field: changing that length while preserving
the map lets JavaScript grow the corresponding `JSArray.length` without
reallocating the backing store, which in turn yields an out-of-bounds double
array suitable for the usual addrof/fakeobj and cage-ARW upgrade. The remaining
engineering work is heap grooming: making the escaped result land immediately
before a live `FixedDoubleArray` so that the overwrite hits its header rather
than unused tail space.

## **Exploitation Strategy**

The no-flag PoC first warms the additive-safe path and the slice reducer using
ordinary JavaScript. Once optimized, it switches the input from a large
numeric constant to `undefined`, causing the lost checked conversion to produce
the forged Word32 current.

The exploit keeps the forged slice length fixed at 15 bytes and varies a
12-bit `delta` coordinate:

```javascript
let raw = ((base >>> 8) & 0xffff) + (delta & 4095);
let start = raw - 4112;
let end = raw - 4097;
return s.slice(start, end);
```

The target type still says both endpoints are strictly negative. The machine
current instead places the resulting slice at `parent.length + delta`, so each
returned `SlicedString` reads 15 bytes beyond the parent.

For the write exploit, the exploit positions that malformed
slice over a neighboring string layout whose bytes change across the allocation
inside `RegExp` source escaping. The first pass sees a nearly unescaped source,
the second pass sees escape-heavy data, and the result object is under-allocated
for the bytes that the second pass emits. The one-byte witness shows the
continuing escaped output immediately after the result object; the two-byte
variant drives the same store sequence to a deterministic fault.

The separate disclosure PoC allocates a normal public JSON string and then a
closure-held secret token string, scans 600 bytes using forged views, and
verifies the recovered token through a closure oracle. That read exploit is
supporting evidence that the malformed slice is a stable cross-object
capability.

## **Proof of Concept**

The reporting package contains these primary artifacts:

```text
regexp-escape-window-observe.js   one-byte heap OOB-write witness
regexp-escape-two-byte-observe.js two-byte write-fault witness
exploit.js                       no-V8-flag supporting disclosure exploit
verify.py                        expected-output verifier
trace-trigger.js                 tracing-only optimized twin
collect-trace.sh                 trace collection helper
extract-evidence.py              scoped graph extractor
evidence/                        original submitted trace and output
```

Current main was built with:

```text
is_debug=false
dcheck_always_on=false
symbol_level=0
v8_enable_memory_corruption_api=false
```

The generated build configuration additionally confirms:

```text
pointer_compression=true
sandbox=true
memory_corruption_api=false
```

```bash
out/additivesafe-main-release/d8 --allow-natives-syntax regexp-escape-window-observe.js -- 14250
out/additivesafe-main-release/d8 --allow-natives-syntax regexp-escape-two-byte-observe.js -- 14000
```

Expected one-byte output includes:

```text
N=14250 PRE=16383,0 POST=0,8788 RE=16385,0,0,4395 OOB=3@0:6e5c6e...
```

The two-byte variant reaches a deterministic fault in the RegExp escape writer.
The disclosure witness remains available as:

```bash
out/additivesafe-main-release/d8 exploit.js
```

with expected supporting output:

```text
PROCESS_MEMORY_LEAK_OFFSET=<small positive offset>
LEAKED="TOPSECRET_<runtime milliseconds>_<runtime random>_END"
SUCCESS: private application token passed oracle after OOB exfiltration
```

The write-witness helpers use `--allow-natives-syntax` only to make the heap
layout and neighborhood inspection deterministic during development. The
supporting disclosure exploit and the underlying malformed-slice trigger do
not use native syntax, any exposed corruption API, or experimental V8 features.

## **Suggested Fix**

The root issue is that `PlainPrimitiveToNumber` forwards only the coarse
Word32 shape of the requested truncation and discards the checked-safe-integer
requirement.

The smallest robust fix is to make the `PlainPrimitiveToNumber` Word32 path
preserve `truncation.check_safe_integer()`. If the consumer requested
`CheckedSafeIntTruncatingWord32`, the producer must emit a conversion that
retains the additive-safe check instead of unconditionally selecting
`UseInfo::TruncatingWord32()`.

This should be accompanied by an audit of other truncation-forwarding sites for
the same mistake: several current-main branches already explicitly gate
integer shortcuts with `!truncation.check_safe_integer()`, and
`PlainPrimitiveToNumber` should obey the same invariant.

As defense in depth, the string reducers should apply explicit dynamic bounds
checks before constructing direct-string or sliced-string views from optimized
indices. `StringSubstring` should not be able to allocate a `SlicedString` from
current values that are outside `0 <= from <= to <= string.length`, even when
the typer believed those relations were already proven.

Useful regression coverage would include:

```text
1. A TurboFan test where PlainPrimitiveToNumber feeds
   SpeculativeAdditiveSafeIntegerAdd under a checked Word32 truncation.
2. A semantic JS test that verifies undefined does not become an unchecked
   integer current before additive-safe arithmetic.
3. A slice-specific test that ensures the optimized trigger deoptimizes before
   reaching StringSubstring when the producer loses the required check.
```
