CVE-2026-95316
Overview
Background
- `d8`
- V8’s standalone developer shell used to run JavaScript files directly for testing and debugging.
- `ScriptCompiler::ExternalSourceStream`
- an abstraction that feeds source in chunks to V8’s background streaming compiler, whose
GetMoreDatasupplies raw bytes on demand. - Streaming compile
- a code path where V8 parses/compiles source as it is delivered by a stream, then later needs the full source
Stringfor positions and error reporting. - `Shell::ReadFile` / `ConvertToString`
- helpers that read a file’s contents into a V8
String, sizing the read fromstat/ftellreported length.
Root Cause Analysis
In Shell::CompileSource, when options.streaming_compile was set, FileSourceStream::GetMoreData read raw chunks directly from the native file handle via fread, but the source String used for positions and error reporting was obtained separately through source.ConvertToString, which relies on Shell::ReadFile and sizes the buffer from the file’s reported length. For special files such as /proc/*, stat/ftell report a size of 0, so ReadFile returned an empty string even though the stream itself had delivered real bytes to the compiler, associating the compiled code with an empty source string. This inconsistency violated the invariant that the compiled bytecode’s source positions must correspond to a source string of matching length, producing position mismatches and DCHECK failures in SetPosition.
The fix makes FileSourceStream retain every chunk it reads into a data_ buffer and expose it through a new GetSourceString, so the source string is derived from exactly the bytes the streamer consumed rather than from a second, size-dependent read. Because the file is now read only once and the streamer-read data is reused, the empty-string outcome and the resulting inconsistency can no longer occur.
String from a length that can be reported as 0 for special files, decoupling it from the bytes actually compiled; the fix caches the streamed bytes in FileSourceStream and rebuilds the source string from them via GetSourceString, guaranteeing the string matches the compiled input.Attack Path
- Supply a special-file source
Run
d8in streaming-compile mode against a file (e.g. under/proc/*) whosestat/ftellreports a size of0while still yielding real bytes on read. - Stream delivers bytes, second read returns empty
FileSourceStream::GetMoreDatafeeds actual source to the background compiler, butsource.ConvertToStringviaShell::ReadFileproduces an emptyStringbecause the reported size is0. - Mismatched source string bound to code The compiled code is associated with the empty source string, breaking the correspondence between bytecode source positions and source length.
- Position invariant fails
During profiling or error reporting,
SetPositionhits position mismatches and triggers aDCHECKfailure, terminating the process in debug/checked builds (inferred: any further consequence in release builds).
Impact Assessment
d8’s streaming compiler at a zero-reported-size file causes the compiled code to carry an empty source string, leading to position mismatches and DCHECK/SetPosition failures during profiling or error reporting. The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 2.9. The advisory does not state exploited-in-the-wild status or a severity rating. Any escalation beyond the observed DCHECK/consistency failure is (inferred) and not demonstrated by the patch.Changed Functions
| Function | Change | Notes |
|---|---|---|
DummySourceStreamsrc/d8/d8.cc |
modified | |
CompileSourceStreamsrc/d8/d8.cc |
modified | |
FileSourceStreamsrc/d8/d8.cc |
modified | |
FileSourceStreamsrc/d8/d8.cc |
modified | |
ifsrc/d8/d8.cc |
modified |
Files Changed
src/d8/d8.cc
Audit Directions
- Double-read source inconsistencyLook for code that both streams/consumes input and separately re-reads the same source for positions or error reporting, where the two reads can disagree in length or content.
- Size-from-stat assumptionsAudit readers that size buffers from
stat/ftell/reported length and return empty or truncated data for special files (/proc, pipes, sockets) that report size0. - Unchecked return values feeding invariantsFlag callers that ignore or under-check return values (empty
String, failedToLocal) whose result later feeds position/consistency invariants enforced only byDCHECK.
Patch
From f6bc03dde92d94b4354241176fa8412aa14201a8 Mon Sep 17 00:00:00 2001
From: Leszek Swirski <leszeks@chromium.org>
Date: Tue, 25 Aug 2026 15:35:11 +0200
Subject: [PATCH] [d8] Provide GetSourceString on streamed CompileSourceStream
When compiling streamed files, FileSourceStream read chunks directly
from native file handles via fread without buffering, while
Shell::CompileSource subsequently called source.ConvertToString to
obtain the source string via Shell::ReadFile.
For special files (such as /proc/*) where stat/ftell report a size of 0,
ReadFile returned an empty string, causing the streamed code to be
associated with an empty source string. During profiling or error
reporting, this caused position mismatches and DCHECK failures in
SetPosition.
This CL fixes this by making sure to read the file only once, and
saving/reusing streamer-read data for the source string if needed.
TAG=agy
CONV=b30f2ed3-329c-4deb-b12b-ecc7184fe218
Fixed: 552023752
Change-Id: I8f32e20bd8be4fc709cf8d82c47631766ad124bf
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8285581
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Patrick Thier <pthier@chromium.org>
Commit-Queue: Patrick Thier <pthier@chromium.org>
Auto-Submit: Leszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/main@{#109494}
---
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index 6542b36..4cca834 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -55,6 +55,7 @@
#include "src/base/strong-alias.h"
#include "src/base/sys-info.h"
#include "src/base/utils/random-number-generator.h"
+#include "src/codegen/compiler.h"
#include "src/compiler-dispatcher/optimizing-compile-dispatcher.h"
#include "src/d8/d8-console.h"
#include "src/d8/d8-platforms.h"
@@ -716,11 +717,32 @@
ScriptCompiler::CachedData::BufferOwned));
}
+MaybeLocal<String> CreateStringFromExternalData(Isolate* isolate,
+ std::string_view source) {
+ int size = static_cast<int>(source.size());
+ if (i::v8_flags.use_external_strings &&
+ i::String::IsAscii(source.data(), size)) {
+ String::ExternalOneByteStringResource* resource =
+ new i::OwningExternalOneByteStringResource(source);
+ return String::NewExternalOneByte(isolate, resource);
+ }
+ return String::NewFromUtf8(isolate, source.data(), NewStringType::kNormal,
+ size);
+}
+
// Dummy external source stream which returns the whole source in one go.
// TODO(leszeks): Also test chunking the data.
-class DummySourceStream : public v8::ScriptCompiler::ExternalSourceStream {
+class CompileSourceStream : public v8::ScriptCompiler::ExternalSourceStream {
public:
- DummySourceStream(Isolate* isolate, Local<String> source) : done_(false) {
+ virtual MaybeLocal<String> GetSourceString(Isolate* isolate) = 0;
+};
+
+// Dummy external source stream which returns the whole source in one go.
+// TODO(leszeks): Also test chunking the data.
+class DummySourceStream : public CompileSourceStream {
+ public:
+ DummySourceStream(Isolate* isolate, Local<String> source)
+ : source_(source), done_(false) {
source_length_ = source->Length();
source_buffer_ = std::make_unique<uint16_t[]>(source_length_);
source->Write(isolate, 0, source_length_, source_buffer_.get());
@@ -736,13 +758,18 @@
return source_length_ * 2;
}
+ MaybeLocal<String> GetSourceString(Isolate* isolate) override {
+ return source_;
+ }
+
private:
+ Local<String> source_;
uint32_t source_length_;
std::unique_ptr<uint16_t[]> source_buffer_;
bool done_;
};
-class FileSourceStream : public v8::ScriptCompiler::ExternalSourceStream {
+class FileSourceStream : public CompileSourceStream {
public:
FileSourceStream(Isolate* isolate, const char* filename) {
file_ = base::Fopen(filename, "rb");
@@ -768,15 +795,25 @@
return 0;
}
+ if (data_.size() + bytes_read > size_t{String::kMaxLength}) {
+ FATAL("Input file too large (> %d bytes)", String::kMaxLength);
+ }
+ data_.append(reinterpret_cast<const char*>(buffer.get()), bytes_read);
+
*src = buffer.release();
return bytes_read;
}
bool IsValid() const { return file_ != nullptr; }
+ MaybeLocal<String> GetSourceString(Isolate* isolate) override {
+ return CreateStringFromExternalData(isolate, data_);
+ }
+
private:
static constexpr size_t kChunkSize = 4096;
FILE* file_ = nullptr;
+ std::string data_;
};
// Run a ScriptStreamingTask in a separate thread.
@@ -862,14 +899,12 @@
const Source& source,
const ScriptOrigin& origin) {
if (options.streaming_compile) {
- std::unique_ptr<v8::ScriptCompiler::ExternalSourceStream> source_stream;
+ std::unique_ptr<CompileSourceStream> source_stream;
v8::ScriptCompiler::StreamedSource::Encoding encoding;
- Local<String> source_string;
if (source.type() == Source::Type::kString) {
- source_string = source.string();
source_stream =
- std::make_unique<DummySourceStream>(isolate, source_string);
+ std::make_unique<DummySourceStream>(isolate, source.string());
encoding = v8::ScriptCompiler::StreamedSource::TWO_BYTE;
} else {
DCHECK_EQ(source.type(), Source::Type::kFile);
@@ -896,10 +931,11 @@
if (streaming_task) {
StreamerThread::StartThreadForTaskAndJoin(streaming_task.get());
- if (source_string.IsEmpty()) {
- if (!source.ConvertToString(isolate).ToLocal(&source_string)) {
- return MaybeLocal<T>();
- }
+ auto* stream = static_cast<CompileSourceStream*>(
+ streamed_source.impl()->source_stream.get());
+ Local<String> source_string;
+ if (!stream->GetSourceString(isolate).ToLocal(&source_string)) {
+ return MaybeLocal<T>();
}
update_script_size(source_string->Length());
return CompileStreamed<T>(context, &streamed_source, source_string,
@@ -5802,13 +5838,7 @@
static_assert(String::kMaxLength <= i::kMaxInt);
int size = static_cast<int>(full_file_size);
char* chars = static_cast<char*>(file->memory());
- if (i::v8_flags.use_external_strings && i::String::IsAscii(chars, size)) {
- String::ExternalOneByteStringResource* resource =
- new i::OwningExternalOneByteStringResource(
- std::string_view(chars, size));
- return String::NewExternalOneByte(isolate, resource);
- }
- return String::NewFromUtf8(isolate, chars, NewStringType::kNormal, size);
+ return CreateStringFromExternalData(isolate, std::string_view(chars, size));
}
void Shell::WriteChars(const char* name, uint8_t* buffer, size_t buffer_size) {
Original Bug Report
DCHECK failure in pos.line > 0 in profile-generator.cc
Detailed Report: https://clusterfuzz.com/testcase?key=6704429696843776
Fuzzer: big_sleep Job Type: linux_asan_d8_dbg Platform Id: linux
Crash Type: DCHECK failure Crash Address: Crash State: pos.line > 0 in profile-generator.cc v8::internal::SourcePositionTable::SetPosition v8::internal::ProfilerListener::CodeCreateEvent
Sanitizer: address (ASAN)
Regressed: https://clusterfuzz.com/revisions?job=linux_asan_d8_dbg&range=107278:107279
Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=6704429696843776
Issue filed automatically.
To reproduce this, please build the target in this report and run it against the reproducer testcase. Please use the GN arguments provided at bottom of this report when building the binary.
If you have trouble reproducing, please also export the environment variables listed under “[Environment]” in the crash stacktrace.
If you have any feedback on reproducing test cases, let us know at https://forms.gle/Yh3qCYFveHj6E5jz5 so we can improve.