Low watch CVSS 2.9 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
2.9
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUnchecked return value in Performance
DescriptionUnchecked return value in Performance
ComponentPerformance
Sub-componentD8
Bug ClassLogic Error
Action tierwatch — dos-class only — no demonstrated corruption path
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker552023752
Fix commitf6bc03dde92d (v8/v8) +48/-18
CWECWE-252
CVSS vectorCVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-08-24 (per vendor advisory)
Fix landed2026-08-25 (public git)
Disclosed2026-09-22
Patch gap28 days — fix public in source before the advisory

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 GetMoreData supplies 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 String for positions and error reporting.
`Shell::ReadFile` / `ConvertToString`
helpers that read a file’s contents into a V8 String, sizing the read from stat/ftell reported 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.

Key insight
The single mistake was reading the file twice and deriving the source 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

  1. Supply a special-file source Run d8 in streaming-compile mode against a file (e.g. under /proc/*) whose stat/ftell reports a size of 0 while still yielding real bytes on read.
  2. Stream delivers bytes, second read returns empty FileSourceStream::GetMoreData feeds actual source to the background compiler, but source.ConvertToString via Shell::ReadFile produces an empty String because the reported size is 0.
  3. 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.
  4. Position invariant fails During profiling or error reporting, SetPosition hits position mismatches and triggers a DCHECK failure, terminating the process in debug/checked builds (inferred: any further consequence in release builds).

Impact Assessment

An attacker (or misconfigured input) able to point 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

FunctionChangeNotes
DummySourceStream
src/d8/d8.cc
modified
CompileSourceStream
src/d8/d8.cc
modified
FileSourceStream
src/d8/d8.cc
modified
FileSourceStream
src/d8/d8.cc
modified
if
src/d8/d8.cc
modified

Files Changed

  • src/d8/d8.cc

Audit Directions

  • Double-read source inconsistency
    Look 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 assumptions
    Audit readers that size buffers from stat/ftell/reported length and return empty or truncated data for special files (/proc, pipes, sockets) that report size 0.
  • Unchecked return values feeding invariants
    Flag callers that ignore or under-check return values (empty String, failed ToLocal) whose result later feeds position/consistency invariants enforced only by DCHECK.
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) {
Loading diff…

Original Bug Report

reported by 24...@project.gserviceaccount.com

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.

View on issue tracker