From 8ebd4cec8143b90b8aefda2b234a9366fc7beb36 Mon Sep 17 00:00:00 2001 From: shahinsamiur Date: Thu, 29 Oct 2026 15:34:44 +0600 Subject: [PATCH] fix: escape strings in worker CPU profile JSON Signed-off-by: shahinsamiur --- deps/v8/BUILD.bazel | 1 + deps/v8/BUILD.gn | 1 + deps/v8/src/profiler/output-stream-writer.cc | 73 +++++++++++++++++++ deps/v8/src/profiler/output-stream-writer.h | 1 + deps/v8/src/profiler/profile-generator.cc | 19 ++--- .../test-worker-cpu-profile-regexp-frame.js | 38 ++++++++++ 6 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 deps/v8/src/profiler/output-stream-writer.cc create mode 100644 test/parallel/test-worker-cpu-profile-regexp-frame.js diff --git a/deps/v8/BUILD.bazel b/deps/v8/BUILD.bazel index af2395eafce9..d9475cc01eba 100644 --- a/deps/v8/BUILD.bazel +++ b/deps/v8/BUILD.bazel @@ -2393,6 +2393,7 @@ filegroup( "src/profiler/heap-profiler.h", "src/profiler/heap-snapshot-common.h", "src/profiler/heap-snapshot-generator.cc", + "src/profiler/output-stream-writer.cc", "src/profiler/heap-snapshot-generator.h", "src/profiler/heap-snapshot-generator-inl.h", "src/profiler/output-stream-writer.h", diff --git a/deps/v8/BUILD.gn b/deps/v8/BUILD.gn index 560d4d42a94f..f845cb2e9af9 100644 --- a/deps/v8/BUILD.gn +++ b/deps/v8/BUILD.gn @@ -6100,6 +6100,7 @@ v8_source_set("v8_base_without_compiler") { "src/profiler/cpu-profiler.cc", "src/profiler/heap-profiler.cc", "src/profiler/heap-snapshot-generator.cc", + "src/profiler/output-stream-writer.cc", "src/profiler/profile-generator.cc", "src/profiler/profiler-listener.cc", "src/profiler/profiler-stats.cc", diff --git a/deps/v8/src/profiler/output-stream-writer.cc b/deps/v8/src/profiler/output-stream-writer.cc new file mode 100644 index 000000000000..84c40ce74ce1 --- /dev/null +++ b/deps/v8/src/profiler/output-stream-writer.cc @@ -0,0 +1,73 @@ +// Copyright 2026 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "src/profiler/output-stream-writer.h" + +#include "src/strings/unicode-inl.h" + +namespace v8 { +namespace internal { + +namespace { + +void WriteUChar(OutputStreamWriter* w, unibrow::uchar u) { + static const char hex_chars[] = "0123456789ABCDEF"; + w->AddString("\\u"); + w->AddCharacter(hex_chars[(u >> 12) & 0xF]); + w->AddCharacter(hex_chars[(u >> 8) & 0xF]); + w->AddCharacter(hex_chars[(u >> 4) & 0xF]); + w->AddCharacter(hex_chars[u & 0xF]); +} + +} // namespace + +void OutputStreamWriter::AddJsonEscapedString(const unsigned char* s) { + AddCharacter('"'); + for (; *s != '\0'; ++s) { + switch (*s) { + case '\b': + AddString("\\b"); + continue; + case '\f': + AddString("\\f"); + continue; + case '\n': + AddString("\\n"); + continue; + case '\r': + AddString("\\r"); + continue; + case '\t': + AddString("\\t"); + continue; + case '"': + case '\\': + AddCharacter('\\'); + AddCharacter(*s); + continue; + default: + if (*s > 31 && *s < 128) { + AddCharacter(*s); + } else if (*s <= 31) { + WriteUChar(this, *s); + } else { + size_t length = 1, cursor = 0; + for (; length <= 4 && *(s + length) != '\0'; ++length) { + } + unibrow::uchar c = unibrow::Utf8::CalculateValue(s, length, &cursor); + if (c != unibrow::Utf8::kBadChar) { + WriteUChar(this, c); + DCHECK_NE(cursor, 0); + s += cursor - 1; + } else { + AddCharacter('?'); + } + } + } + } + AddCharacter('"'); +} + +} // namespace internal +} // namespace v8 diff --git a/deps/v8/src/profiler/output-stream-writer.h b/deps/v8/src/profiler/output-stream-writer.h index 208018d3f5a3..c06b042abdd2 100644 --- a/deps/v8/src/profiler/output-stream-writer.h +++ b/deps/v8/src/profiler/output-stream-writer.h @@ -67,6 +67,7 @@ class OutputStreamWriter { AddNumber(n); } } + void AddJsonEscapedString(const unsigned char* s); void Finalize() { if (aborted_) return; DCHECK(chunk_pos_ < chunk_size_); diff --git a/deps/v8/src/profiler/profile-generator.cc b/deps/v8/src/profiler/profile-generator.cc index 42f63c770727..27e2f2539f82 100644 --- a/deps/v8/src/profiler/profile-generator.cc +++ b/deps/v8/src/profiler/profile-generator.cc @@ -846,17 +846,18 @@ void CpuProfileJSONSerializer::SerializePositionTicks( void CpuProfileJSONSerializer::SerializeCallFrame( const v8::CpuProfileNode* node) { - writer_->AddString("\"functionName\":\""); - writer_->AddString(node->GetFunctionNameStr()); - writer_->AddString("\",\"lineNumber\":"); + writer_->AddString("\"functionName\":"); + writer_->AddJsonEscapedString( + reinterpret_cast(node->GetFunctionNameStr())); + writer_->AddString(",\"lineNumber\":"); writer_->AddNumber(node->GetLineNumber() - 1); writer_->AddString(",\"columnNumber\":"); writer_->AddNumber(node->GetColumnNumber() - 1); writer_->AddString(",\"scriptId\":"); writer_->AddNumber(node->GetScriptId()); - writer_->AddString(",\"url\":\""); - writer_->AddString(node->GetScriptResourceNameStr()); - writer_->AddCharacter('"'); + writer_->AddString(",\"url\":"); + writer_->AddJsonEscapedString( + reinterpret_cast(node->GetScriptResourceNameStr())); } void CpuProfileJSONSerializer::SerializeChildren(const v8::CpuProfileNode* node, @@ -888,9 +889,9 @@ void CpuProfileJSONSerializer::SerializeNode(const v8::CpuProfileNode* node) { const char* deoptReason = node->GetBailoutReason(); if (deoptReason && deoptReason[0] && strcmp(deoptReason, "no reason")) { - writer_->AddString(",\"deoptReason\":\""); - writer_->AddString(deoptReason); - writer_->AddCharacter('"'); + writer_->AddString(",\"deoptReason\":"); + writer_->AddJsonEscapedString( + reinterpret_cast(deoptReason)); } unsigned lineCount = node->GetHitLineCount(); diff --git a/test/parallel/test-worker-cpu-profile-regexp-frame.js b/test/parallel/test-worker-cpu-profile-regexp-frame.js new file mode 100644 index 000000000000..ae8dd58d60ca --- /dev/null +++ b/test/parallel/test-worker-cpu-profile-regexp-frame.js @@ -0,0 +1,38 @@ +'use strict'; +// Regression test for https://github.com/nodejs/node/issues/67097 +// worker.startCpuProfile() must return valid JSON when the profile contains a +// RegExp code frame whose name includes characters that need JSON escaping. +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +const pattern = '^(\\d{6})'; +const expectedFunctionName = `RegExp: ${pattern}`; + +const worker = new Worker(` + const re = new RegExp(${JSON.stringify(pattern)}); + setInterval(() => { + let matches = 0; + for (let i = 0; i < 30000; i++) { + if (re.test(String(i))) matches++; + } + return matches; + }, 0); +`, { eval: true }); + +worker.on('online', common.mustCall(async () => { + try { + let found = false; + for (let attempt = 0; attempt < 50 && !found; attempt++) { + const handle = await worker.startCpuProfile(); + await new Promise((resolve) => setTimeout(resolve, 100)); + const profile = JSON.parse(await handle.stop()); + found = profile.nodes.some( + ({ callFrame }) => callFrame.functionName === expectedFunctionName); + } + assert.ok(found, + `No "${expectedFunctionName}" frame found in the CPU profile`); + } finally { + await worker.terminate(); + } +}));