Repository navigation
Expand file tree
/
Copy pathcoverage_wrapper.js
More file actions
311 lines (275 loc) · 11.1 KB
/
Copy pathcoverage_wrapper.js
File metadata and controls
311 lines (275 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
import { Readable, Writable } from 'node:stream';
import inspector from 'node:inspector';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
if (process.argv.length < 3) {
console.error("Usage: node coverage_wrapper.js <target_script> [args...]");
process.exit(1);
}
const targetScript = path.resolve(process.argv[2]);
const targetArgs = process.argv.slice(3);
// Set process.argv so the target script gets its expected arguments
process.argv = [process.argv[0], targetScript, ...targetArgs];
// Setup V8 Inspector session
const session = new inspector.Session();
session.connect();
// Enable Profiler and start precise coverage once at startup
session.post('Profiler.enable');
session.post('Profiler.startPreciseCoverage', { callCount: true, detailed: true });
// Track and profile Node.js Worker threads dynamically
const workerSessions = new Map();
session.on('NodeWorker.attachedToWorker', (event) => {
const info = event.params || event;
const workerSession = new inspector.Session();
try {
workerSession.connect(info.sessionId);
workerSession.post('Profiler.enable');
workerSession.post('Profiler.startPreciseCoverage', { callCount: true, detailed: true });
workerSessions.set(info.sessionId, workerSession);
} catch (e) {}
});
session.on('NodeWorker.detachedFromWorker', (event) => {
const info = event.params || event;
workerSessions.delete(info.sessionId);
});
session.post('NodeWorker.enable', { waitForDebuggerOnStart: false });
// Intercept stdin and stdout
const realStdin = process.stdin;
const realStdout = process.stdout;
// Custom streams that the target script will think are the standard ones
const customStdin = new Readable({
read() {}
});
const customStdout = new Writable({
write(chunk, encoding, callback) {
handleTargetStdout(chunk);
callback();
}
});
Object.defineProperty(process, 'stdin', { value: customStdin, writable: true, configurable: true });
Object.defineProperty(process, 'stdout', { value: customStdout, writable: true, configurable: true });
// ORPHAN GUARD (2026-09-23). The fuzzer is our only reason to exist. It used to
// die without us noticing -- libFuzzer's C exit() skips JSRunner.stop(), and
// the listening socket keeps this event loop alive forever -- which left 663
// servers (~47 GB) orphaned on the VM. When the fuzzer goes away, take down our
// whole process group (JSRunner makes us a session leader, so pgid == our pid)
// and so every child the target spawned. Two signals, either suffices:
// - EOF on the real stdin: JSRunner holds the write end and never writes, so
// the kernel closes it exactly when the fuzzer process dies (any cause).
// - our parent pid changes (reparented to init/a subreaper).
// Plus SIGTERM, which JSRunner.stop() sends and which Linux delivers as our
// PDEATHSIG (procreap.pdeathsig_prefix): node's default SIGTERM exits node
// alone and orphans the target's children.
// Set TOFU_NO_ORPHAN_GUARD=1 to run the wrapper by hand with stdin closed.
if (!process.env.TOFU_NO_ORPHAN_GUARD) {
const parentPid = process.ppid;
const reapGroup = () => {
// stop() never ran, so nobody else will remove the socket (1641 stale
// /tmp/mcp_fuzz_*.sock on the VM). Only fires from events, by which time
// socketPath below is initialised.
try { fs.unlinkSync(socketPath); } catch (e) {}
try { process.kill(-process.pid, 'SIGKILL'); } catch (e) {}
process.exit(137); // not a group leader (run by hand): at least exit
};
realStdin.on('end', reapGroup);
realStdin.on('close', reapGroup);
realStdin.on('error', reapGroup);
realStdin.on('data', () => {}); // flowing mode, so 'end' is delivered
process.on('SIGTERM', reapGroup);
setInterval(() => { if (process.ppid !== parentPid) reapGroup(); }, 1000).unref();
}
import net from 'node:net';
const socketPath = process.env.FUZZER_SOCKET_PATH || '/tmp/mcp_fuzzer.sock';
const socketServer = net.createServer((socket) => {
let socketBuffer = '';
socket.on('data', (chunk) => {
socketBuffer += chunk.toString();
let lineEnd;
while ((lineEnd = socketBuffer.indexOf('\n')) !== -1) {
const line = socketBuffer.slice(0, lineEnd).trim();
socketBuffer = socketBuffer.slice(lineEnd + 1);
if (line) {
handleSocketMessage(socket, line);
}
}
});
});
try {
if (fs.existsSync(socketPath)) {
fs.unlinkSync(socketPath);
}
} catch (e) {}
socketServer.listen(socketPath, () => {
realStdout.write(`SOCKET_READY:${socketPath}\n`);
});
function handleSocketMessage(socket, line) {
try {
const msg = JSON.parse(line);
if (msg.method === 'tools/call') {
activeSocket = socket;
isFuzzingCall = true;
}
} catch (e) {}
// Forward to target script stdin
customStdin.push(line + '\n');
}
let activeSocket = null;
let isFuzzingCall = false;
function handleTargetStdout(chunk) {
const dataStr = chunk.toString();
if (isFuzzingCall && activeSocket) {
const targetSocket = activeSocket;
isFuzzingCall = false;
// Take precise coverage without stopping it
session.post('Profiler.takePreciseCoverage', (err, covRes) => {
const combinedResults = [];
if (!err && covRes && covRes.result) {
combinedResults.push(...covRes.result);
}
const coverageReport = processCoverage(combinedResults);
const payload = JSON.stringify({
response: dataStr.trim(),
coverage: coverageReport
}) + '\n';
targetSocket.write(payload);
});
} else {
realStdout.write(chunk);
}
}
// Line offset caching logic
const fileLineOffsets = new Map();
function getLineOffsets(filePath) {
if (fileLineOffsets.has(filePath)) {
return fileLineOffsets.get(filePath);
}
try {
const content = fs.readFileSync(filePath, 'utf8');
const offsets = [0];
for (let i = 0; i < content.length; i++) {
if (content[i] === '\n') {
offsets.push(i + 1);
}
}
fileLineOffsets.set(filePath, offsets);
return offsets;
} catch (e) {
return null;
}
}
function offsetToLine(offsets, offset) {
let low = 0;
let high = offsets.length - 1;
while (low <= high) {
const mid = (low + high) >> 1;
if (offsets[mid] <= offset) {
if (mid === offsets.length - 1 || offsets[mid + 1] > offset) {
return mid + 1; // 1-based line number
}
low = mid + 1;
} else {
high = mid - 1;
}
}
return 1;
}
// Find package root (directory containing package.json or the parent of build/src)
let targetDir = path.dirname(targetScript);
while (targetDir !== path.parse(targetDir).root) {
if (fs.existsSync(path.join(targetDir, 'package.json'))) {
break;
}
targetDir = path.dirname(targetDir);
}
const localFileCache = new Map();
function isLocalFile(fileUrl) {
if (!fileUrl.startsWith('file://')) return false;
if (localFileCache.has(fileUrl)) return localFileCache.get(fileUrl);
const filePath = fileURLToPath(fileUrl);
const matched = filePath.startsWith(targetDir) && !filePath.includes('node_modules') && !filePath.includes('.venv');
localFileCache.set(fileUrl, matched);
return matched;
}
// PER-CALL coverage. V8 ALREADY gives us this -- do not delta it again.
//
// MEASURED 2026-09-10 with a standalone inspector.Session: running the same
// 1000-iteration loop before three consecutive takePreciseCoverage calls gives
// take #1: nonzero=8 maxCount=1000
// take #2: nonzero=8 maxCount=1000
// take #3: nonzero=8 maxCount=1000
// Counts do NOT accumulate -- takePreciseCoverage RESETS the execution
// counters. range.count is therefore already "executed by THIS call".
//
// An earlier fix assumed the opposite (nazmul.todo item 10 flagged that
// assumption as unverified, and it was wrong). It kept a prevCounts map and
// reported a range only when count > prevCount, which after the first call
// suppressed nearly everything: cov: 1388 stayed identical from execution #13
// to #8192 while corp: held at 1/32b, on twelve semantically distinct seeds.
// Reverted to reporting every executed range.
//
// WHAT V8 BLOCK COVERAGE ACTUALLY REPORTS, AND WHY count>0 THREW IT AWAY.
//
// MEASURED 2026-09-10 on one function with three branches, detailed:true:
// [routes ] isBlockCoverage=true ranges= 157-265:1 207-263:0
// [services] isBlockCoverage=true ranges= 157-265:1 194-207:0 244-263:0
// [zzz ] isBlockCoverage=true ranges= 157-265:1 194-207:0 231-244:0
//
// V8 emits ONE range covering the whole function with its execution count, then
// carves out the branches that did NOT run as nested count:0 sub-ranges. So the
// set of count>0 ranges is IDENTICAL for all three inputs (just 157-265) and
// every bit of branch information lives in the count:0 ranges the old filter
// discarded. Three inputs taking three different branches produced one
// identical coverage set, which is why libFuzzer's cov: never moved:
// before: cov: 1388 ft: 1388 corp: 1/32b, unchanged #13 -> #8192
//
// Correct semantics: a region executed iff it is inside a count>0 range and NOT
// inside a nested count:0 range. executedIntervals() does that subtraction and
// returns disjoint executed intervals, keyed by byte offset.
//
// Byte offsets, not line numbers. The previous code expanded every range to
// whole lines and unioned them, which re-merges branches that share a line --
// and most Set A targets ship a minified/transpiled dist/index.js where an
// entire handler is one line.
function executedIntervals(ranges) {
// V8 ranges within a function are properly nested; outermost first.
const sorted = [...ranges].sort(
(a, b) => a.startOffset - b.startOffset || b.endOffset - a.endOffset);
const out = [];
for (let i = 0; i < sorted.length; i++) {
const r = sorted[i];
if (r.count <= 0) continue; // not executed; its own children are moot
// Subtract this range's IMMEDIATE children. A child with count>0 emits its
// own intervals when it is reached as `r`, so subtracting it here is right
// either way; what matters is that a count:0 child leaves a hole.
let cursor = r.startOffset;
for (let j = i + 1; j < sorted.length; j++) {
const c = sorted[j];
if (c.startOffset >= r.endOffset) break; // past the end of r
if (c.endOffset > r.endOffset) continue; // overlaps out of r, not nested
if (c.startOffset < cursor) continue; // nested inside an earlier child
if (c.startOffset > cursor) out.push([cursor, c.startOffset]);
cursor = c.endOffset;
}
if (cursor < r.endOffset) out.push([cursor, r.endOffset]);
}
return out;
}
function processCoverage(result) {
const covered = [];
for (const script of result) {
if (!isLocalFile(script.url)) continue;
const fileName = path.basename(fileURLToPath(script.url));
for (const func of script.functions) {
for (const [a, b] of executedIntervals(func.ranges)) {
covered.push(`${fileName}|${a}|${b}`);
}
}
}
return [...new Set(covered)];
}
// Dynamically import the target script (supports both ESM and CommonJS via dynamic import)
import(pathToFileURL(targetScript).href).catch((err) => {
console.error("Failed to load target script:", err);
process.exit(1);
});