Skip to content

Commit 8ebd4ce

Browse files
committed
fix: escape strings in worker CPU profile JSON
Signed-off-by: shahinsamiur <shahinsamiur@yahoo.com>
1 parent 458a0a8 commit 8ebd4ce

6 files changed

Lines changed: 124 additions & 9 deletions

File tree

‎deps/v8/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2393,6 +2393,7 @@ filegroup(
23932393
"src/profiler/heap-profiler.h",
23942394
"src/profiler/heap-snapshot-common.h",
23952395
"src/profiler/heap-snapshot-generator.cc",
2396+
"src/profiler/output-stream-writer.cc",
23962397
"src/profiler/heap-snapshot-generator.h",
23972398
"src/profiler/heap-snapshot-generator-inl.h",
23982399
"src/profiler/output-stream-writer.h",

‎deps/v8/BUILD.gn‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6100,6 +6100,7 @@ v8_source_set("v8_base_without_compiler") {
61006100
"src/profiler/cpu-profiler.cc",
61016101
"src/profiler/heap-profiler.cc",
61026102
"src/profiler/heap-snapshot-generator.cc",
6103+
"src/profiler/output-stream-writer.cc",
61036104
"src/profiler/profile-generator.cc",
61046105
"src/profiler/profiler-listener.cc",
61056106
"src/profiler/profiler-stats.cc",
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2026 the V8 project authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#include "src/profiler/output-stream-writer.h"
6+
7+
#include "src/strings/unicode-inl.h"
8+
9+
namespace v8 {
10+
namespace internal {
11+
12+
namespace {
13+
14+
void WriteUChar(OutputStreamWriter* w, unibrow::uchar u) {
15+
static const char hex_chars[] = "0123456789ABCDEF";
16+
w->AddString("\\u");
17+
w->AddCharacter(hex_chars[(u >> 12) & 0xF]);
18+
w->AddCharacter(hex_chars[(u >> 8) & 0xF]);
19+
w->AddCharacter(hex_chars[(u >> 4) & 0xF]);
20+
w->AddCharacter(hex_chars[u & 0xF]);
21+
}
22+
23+
} // namespace
24+
25+
void OutputStreamWriter::AddJsonEscapedString(const unsigned char* s) {
26+
AddCharacter('"');
27+
for (; *s != '\0'; ++s) {
28+
switch (*s) {
29+
case '\b':
30+
AddString("\\b");
31+
continue;
32+
case '\f':
33+
AddString("\\f");
34+
continue;
35+
case '\n':
36+
AddString("\\n");
37+
continue;
38+
case '\r':
39+
AddString("\\r");
40+
continue;
41+
case '\t':
42+
AddString("\\t");
43+
continue;
44+
case '"':
45+
case '\\':
46+
AddCharacter('\\');
47+
AddCharacter(*s);
48+
continue;
49+
default:
50+
if (*s > 31 && *s < 128) {
51+
AddCharacter(*s);
52+
} else if (*s <= 31) {
53+
WriteUChar(this, *s);
54+
} else {
55+
size_t length = 1, cursor = 0;
56+
for (; length <= 4 && *(s + length) != '\0'; ++length) {
57+
}
58+
unibrow::uchar c = unibrow::Utf8::CalculateValue(s, length, &cursor);
59+
if (c != unibrow::Utf8::kBadChar) {
60+
WriteUChar(this, c);
61+
DCHECK_NE(cursor, 0);
62+
s += cursor - 1;
63+
} else {
64+
AddCharacter('?');
65+
}
66+
}
67+
}
68+
}
69+
AddCharacter('"');
70+
}
71+
72+
} // namespace internal
73+
} // namespace v8

‎deps/v8/src/profiler/output-stream-writer.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class OutputStreamWriter {
6767
AddNumber(n);
6868
}
6969
}
70+
void AddJsonEscapedString(const unsigned char* s);
7071
void Finalize() {
7172
if (aborted_) return;
7273
DCHECK(chunk_pos_ < chunk_size_);

‎deps/v8/src/profiler/profile-generator.cc‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -846,17 +846,18 @@ void CpuProfileJSONSerializer::SerializePositionTicks(
846846

847847
void CpuProfileJSONSerializer::SerializeCallFrame(
848848
const v8::CpuProfileNode* node) {
849-
writer_->AddString("\"functionName\":\"");
850-
writer_->AddString(node->GetFunctionNameStr());
851-
writer_->AddString("\",\"lineNumber\":");
849+
writer_->AddString("\"functionName\":");
850+
writer_->AddJsonEscapedString(
851+
reinterpret_cast<const unsigned char*>(node->GetFunctionNameStr()));
852+
writer_->AddString(",\"lineNumber\":");
852853
writer_->AddNumber(node->GetLineNumber() - 1);
853854
writer_->AddString(",\"columnNumber\":");
854855
writer_->AddNumber(node->GetColumnNumber() - 1);
855856
writer_->AddString(",\"scriptId\":");
856857
writer_->AddNumber(node->GetScriptId());
857-
writer_->AddString(",\"url\":\"");
858-
writer_->AddString(node->GetScriptResourceNameStr());
859-
writer_->AddCharacter('"');
858+
writer_->AddString(",\"url\":");
859+
writer_->AddJsonEscapedString(
860+
reinterpret_cast<const unsigned char*>(node->GetScriptResourceNameStr()));
860861
}
861862

862863
void CpuProfileJSONSerializer::SerializeChildren(const v8::CpuProfileNode* node,
@@ -888,9 +889,9 @@ void CpuProfileJSONSerializer::SerializeNode(const v8::CpuProfileNode* node) {
888889

889890
const char* deoptReason = node->GetBailoutReason();
890891
if (deoptReason && deoptReason[0] && strcmp(deoptReason, "no reason")) {
891-
writer_->AddString(",\"deoptReason\":\"");
892-
writer_->AddString(deoptReason);
893-
writer_->AddCharacter('"');
892+
writer_->AddString(",\"deoptReason\":");
893+
writer_->AddJsonEscapedString(
894+
reinterpret_cast<const unsigned char*>(deoptReason));
894895
}
895896

896897
unsigned lineCount = node->GetHitLineCount();
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
// Regression test for https://github.com/nodejs/node/issues/67097
3+
// worker.startCpuProfile() must return valid JSON when the profile contains a
4+
// RegExp code frame whose name includes characters that need JSON escaping.
5+
const common = require('../common');
6+
const assert = require('assert');
7+
const { Worker } = require('worker_threads');
8+
9+
const pattern = '^(\\d{6})';
10+
const expectedFunctionName = `RegExp: ${pattern}`;
11+
12+
const worker = new Worker(`
13+
const re = new RegExp(${JSON.stringify(pattern)});
14+
setInterval(() => {
15+
let matches = 0;
16+
for (let i = 0; i < 30000; i++) {
17+
if (re.test(String(i))) matches++;
18+
}
19+
return matches;
20+
}, 0);
21+
`, { eval: true });
22+
23+
worker.on('online', common.mustCall(async () => {
24+
try {
25+
let found = false;
26+
for (let attempt = 0; attempt < 50 && !found; attempt++) {
27+
const handle = await worker.startCpuProfile();
28+
await new Promise((resolve) => setTimeout(resolve, 100));
29+
const profile = JSON.parse(await handle.stop());
30+
found = profile.nodes.some(
31+
({ callFrame }) => callFrame.functionName === expectedFunctionName);
32+
}
33+
assert.ok(found,
34+
`No "${expectedFunctionName}" frame found in the CPU profile`);
35+
} finally {
36+
await worker.terminate();
37+
}
38+
}));

0 commit comments

Comments
 (0)