From ade8f3c370d2614c1f2a4965e202b6696ecf13e6 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Mon, 29 Jun 2026 17:00:53 +0000 Subject: [PATCH 1/4] Improve crash call stack data. --- Extension/src/LanguageServer/extension.ts | 42 +++++++++-------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 55516f626..f2ac50d39 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -1165,13 +1165,10 @@ export function watchForCrashes(crashDirectory: string): void { let previousCrashData: string; let previousCrashCount: number = 0; -function logCrashTelemetry(data: string, type: string, offsetData?: string, crashLog?: string): void { +function logCrashTelemetry(data: string, type: string, crashLog?: string): void { const crashObject: Record = {}; const crashCountObject: Record = {}; crashObject.CrashingThreadCallStack = data; - if (offsetData !== undefined) { - crashObject.CrashingThreadCallStackOffsets = offsetData; - } if (crashLog !== undefined) { crashObject.CrashLog = crashLog; } @@ -1185,8 +1182,8 @@ function logMacCrashTelemetry(data: string): void { logCrashTelemetry(data, "MacCrash"); } -function logCppCrashTelemetry(data: string, offsetData?: string, crashLog?: string): void { - logCrashTelemetry(data, "CppCrash", offsetData, crashLog); +function logCppCrashTelemetry(data: string, crashLog?: string): void { + logCrashTelemetry(data, "CppCrash", crashLog); } function handleMacCrashFileRead(err: NodeJS.ErrnoException | undefined | null, data: string): void { @@ -1301,16 +1298,15 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr } const lines: string[] = data.split("\n"); - let addressData: string; + let signalInfo: string; const isCppToolsSrv2: boolean = crashFile.startsWith("cpptools-srv2"); const isCppToolsSrv: boolean = crashFile.startsWith("cpptools-srv"); - const telemetryHeader: string = (isCppToolsSrv2 ? "cpptools-srv2.txt" : isCppToolsSrv ? "cpptools-srv.txt" : crashFile) + "\n"; + const telemetryHeader: string = (isCppToolsSrv2 ? "cpptools-srv2 process" : isCppToolsSrv ? "cpptools-srv process" : "cpptools process") + "\n"; const filtPath: string | null = which.sync("c++filt", { nothrow: true }); const isMac: boolean = process.platform === "darwin"; const startStr: string = isMac ? " _" : "<"; const offsetStr: string = isMac ? " + " : "+"; const endOffsetStr: string = isMac ? " " : " <"; - const dotStr: string = "…\n"; let signalType: string; let crashLog: string = ""; let crashStackStartLine: number = 0; @@ -1333,16 +1329,16 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr } if (lines[crashStackStartLine].startsWith("SIG")) { signalType = `${lines[crashStackStartLine]}\n`; - addressData = `${lines[crashStackStartLine + 1]}:${lines[crashStackStartLine + 2]}\n`; // signalCode:signalAddr + signalInfo = `si_code=${lines[crashStackStartLine + 1]}, si_addr=${lines[crashStackStartLine + 2]}\n`; crashStackStartLine += 3; } else { // The signal type may fail to be written. // Intentionally different from SIGUNKNOWN from cpptools, // and not SIG-? to avoid matching the regex in containsFilteredTelemetryData. signalType = "SIGMISSING\n"; - addressData = ".\n"; + signalInfo = ""; } - data = telemetryHeader + signalType; + data = telemetryHeader + signalType + signalInfo; let crashCallStack: string = ""; let validFrameFound: boolean = false; for (let lineNum: number = crashStackStartLine; lineNum < lines.length - 3; ++lineNum) { // skip last lines @@ -1350,23 +1346,21 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr const startPos: number = line.indexOf(startStr); let pendingCallStack: string = ""; if (startPos === -1 || line[startPos + (isMac ? 1 : 4)] === "+") { - pendingCallStack = dotStr; const startAddressPos: number = line.indexOf("0x"); const endAddressPos: number = line.indexOf(endOffsetStr, startAddressPos + 2); if (startAddressPos === -1 || endAddressPos === -1 || startAddressPos >= endAddressPos) { - addressData += "Unexpected offset\n"; + pendingCallStack = "Unexpected offset\n"; } else { let pendingAddressData: string = line.substring(startAddressPos, endAddressPos) + "\n"; if (containsFilteredTelemetryData(pendingAddressData)) { pendingAddressData = "?\n"; } - addressData += pendingAddressData; + pendingCallStack = pendingAddressData; } } else { const offsetPos: number = line.indexOf(offsetStr, startPos + startStr.length); if (offsetPos === -1) { pendingCallStack = "Missing offsetStr\n"; - addressData += "\n"; } else { const startPos2: number = startPos + 1; let funcStr: string = line.substring(startPos2, offsetPos); @@ -1400,14 +1394,13 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr const startAddressPos: number = line.indexOf("0x"); if (startAddressPos === -1 || startAddressPos >= startPos) { // unexpected - pendingOffset += ""; - addressData += "\n"; + pendingOffset += " "; } else { - let pendingAddressData: string = line.substring(startAddressPos, startPos) + "\n"; + let pendingAddressData: string = line.substring(startAddressPos, startPos).trimEnd(); if (containsFilteredTelemetryData(pendingAddressData)) { - pendingAddressData = "?\n"; + pendingAddressData = "?"; } - addressData += pendingAddressData; + pendingOffset += " " + pendingAddressData; } } else { const endPos: number = line.indexOf(">", offsetPos2); @@ -1416,8 +1409,6 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr } else { pendingOffset += line.substring(offsetPos2, endPos); } - addressData += "\n"; - // TODO: It seems like addressData should be obtained on Linux in case the function is filtered. } pendingOffset += "\n"; pendingCallStack = funcStr + pendingOffset; @@ -1447,19 +1438,18 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr } crashCallStack = crashCallStack.trimEnd(); - addressData = addressData.trimEnd(); if (crashCallStack !== prevCppCrashCallStackData) { prevCppCrashCallStackData = crashCallStack; if (lines.length >= 6 && util.getLoggingLevel() >= 1) { - getCrashCallStacksChannel().appendLine(`\n${isCppToolsSrv2 ? "cpptools-srv2" : isCppToolsSrv ? "cpptools-srv" : "cpptools"}\n${crashDate.toLocaleString()}\n${signalType}${crashCallStack}${crashLog.length > 0 ? "\n\n" + crashLog : ""}`); + getCrashCallStacksChannel().appendLine(`\n${isCppToolsSrv2 ? "cpptools-srv2" : isCppToolsSrv ? "cpptools-srv" : "cpptools"}\n${crashDate.toLocaleString()}\n${signalType}${signalInfo}${crashCallStack}${crashLog.length > 0 ? "\n\n" + crashLog : ""}`); } } data += crashCallStack; - logCppCrashTelemetry(data, addressData, crashLog); + logCppCrashTelemetry(data, crashLog); await util.deleteFile(path.resolve(crashDirectory, crashFile)).catch(logAndReturn.undefined); if (crashFile === "cpptools.txt") { From 09fd26c6ecb4dc2e484ef60a2475b899f7142b20 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Mon, 29 Jun 2026 17:54:58 +0000 Subject: [PATCH 2/4] Copilot review fixes. --- Extension/src/LanguageServer/extension.ts | 29 +++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index f2ac50d39..3fb2b9780 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -1289,6 +1289,22 @@ function containsFilteredTelemetryData(str: string): boolean { return regex.test(str); } +// Non-null fault addresses are randomized by ASLR (and use-after-free/wild pointers vary run to +// run), so embedding the raw value in CrashingThreadCallStack would fragment crash buckets and +// make CrashCount meaningless. Preserve near-null addresses (typical null-pointer dereferences, +// which are stable and useful for bucketing), but replace arbitrary addresses with a stable +// placeholder so identical crashes still de-duplicate. +function bucketSignalAddress(address: string): string { + let value: bigint; + try { + value = BigInt(address.trim()); + } catch { + return address; // Not a parseable address; leave it untouched. + } + // 0x10000 (64 KB) covers null plus small member/array offsets off a null pointer. + return value < 0x10000n ? address : ""; +} + async function handleCrashFileRead(crashDirectory: string, crashFile: string, crashDate: Date, err: NodeJS.ErrnoException | undefined | null, data: string): Promise { if (err) { if (err.code === "ENOENT") { @@ -1329,7 +1345,7 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr } if (lines[crashStackStartLine].startsWith("SIG")) { signalType = `${lines[crashStackStartLine]}\n`; - signalInfo = `si_code=${lines[crashStackStartLine + 1]}, si_addr=${lines[crashStackStartLine + 2]}\n`; + signalInfo = `si_code=${lines[crashStackStartLine + 1]}, si_addr=${bucketSignalAddress(lines[crashStackStartLine + 2])}\n`; crashStackStartLine += 3; } else { // The signal type may fail to be written. @@ -1391,17 +1407,6 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr // Compute pendingOffset. if (isMac) { pendingOffset += line.substring(offsetPos2); - const startAddressPos: number = line.indexOf("0x"); - if (startAddressPos === -1 || startAddressPos >= startPos) { - // unexpected - pendingOffset += " "; - } else { - let pendingAddressData: string = line.substring(startAddressPos, startPos).trimEnd(); - if (containsFilteredTelemetryData(pendingAddressData)) { - pendingAddressData = "?"; - } - pendingOffset += " " + pendingAddressData; - } } else { const endPos: number = line.indexOf(">", offsetPos2); if (endPos === -1) { From a2931314aec04034c0215bdfce286298ded765a3 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Mon, 29 Jun 2026 19:10:43 +0000 Subject: [PATCH 3/4] Fix bucketing of addresses that can change. Fix the process name logging. --- Extension/src/LanguageServer/extension.ts | 25 +++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 3fb2b9780..9d24af661 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -1305,6 +1305,23 @@ function bucketSignalAddress(address: string): string { return value < 0x10000n ? address : ""; } +// An unsymbolized frame is reported as a raw runtime address. Addresses in the fixed-base main +// executable (non-PIE on Linux) stay constant across runs and are useful for bucketing, but +// addresses in the ASLR-randomized shared-library/mmap region (Linux 0x7f..., and on macOS the +// PIE main image and dyld shared cache) shift every launch and would fragment crash buckets. Keep +// the low, fixed addresses but replace high (relocated) ones with a stable placeholder. 4 GB is a +// safe cut: a non-PIE executable's own code loads well below it, while the relocated region is far +// above it. +function bucketFrameAddress(address: string): string { + let value: bigint; + try { + value = BigInt(address.trim()); + } catch { + return address; // Not a parseable address; leave it untouched. + } + return value < 0x100000000n ? address : ""; +} + async function handleCrashFileRead(crashDirectory: string, crashFile: string, crashDate: Date, err: NodeJS.ErrnoException | undefined | null, data: string): Promise { if (err) { if (err.code === "ENOENT") { @@ -1317,7 +1334,7 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr let signalInfo: string; const isCppToolsSrv2: boolean = crashFile.startsWith("cpptools-srv2"); const isCppToolsSrv: boolean = crashFile.startsWith("cpptools-srv"); - const telemetryHeader: string = (isCppToolsSrv2 ? "cpptools-srv2 process" : isCppToolsSrv ? "cpptools-srv process" : "cpptools process") + "\n"; + const processName: string = (isCppToolsSrv2 ? "cpptools-srv2 process" : isCppToolsSrv ? "cpptools-srv process" : "cpptools process") + "\n"; const filtPath: string | null = which.sync("c++filt", { nothrow: true }); const isMac: boolean = process.platform === "darwin"; const startStr: string = isMac ? " _" : "<"; @@ -1354,7 +1371,7 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr signalType = "SIGMISSING\n"; signalInfo = ""; } - data = telemetryHeader + signalType + signalInfo; + data = processName + signalType + signalInfo; let crashCallStack: string = ""; let validFrameFound: boolean = false; for (let lineNum: number = crashStackStartLine; lineNum < lines.length - 3; ++lineNum) { // skip last lines @@ -1367,7 +1384,7 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr if (startAddressPos === -1 || endAddressPos === -1 || startAddressPos >= endAddressPos) { pendingCallStack = "Unexpected offset\n"; } else { - let pendingAddressData: string = line.substring(startAddressPos, endAddressPos) + "\n"; + let pendingAddressData: string = bucketFrameAddress(line.substring(startAddressPos, endAddressPos)) + "\n"; if (containsFilteredTelemetryData(pendingAddressData)) { pendingAddressData = "?\n"; } @@ -1448,7 +1465,7 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr prevCppCrashCallStackData = crashCallStack; if (lines.length >= 6 && util.getLoggingLevel() >= 1) { - getCrashCallStacksChannel().appendLine(`\n${isCppToolsSrv2 ? "cpptools-srv2" : isCppToolsSrv ? "cpptools-srv" : "cpptools"}\n${crashDate.toLocaleString()}\n${signalType}${signalInfo}${crashCallStack}${crashLog.length > 0 ? "\n\n" + crashLog : ""}`); + getCrashCallStacksChannel().appendLine(`\n${processName}${crashDate.toLocaleString()}\n${signalType}${signalInfo}${crashCallStack}${crashLog.length > 0 ? "\n\n" + crashLog : ""}`); } } From 4918373ca055bd4c0b114606c73521c780296749 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Tue, 30 Jun 2026 00:30:47 +0000 Subject: [PATCH 4/4] Fix. --- Extension/src/LanguageServer/extension.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 9d24af661..91584f13c 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -1332,9 +1332,9 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr const lines: string[] = data.split("\n"); let signalInfo: string; - const isCppToolsSrv2: boolean = crashFile.startsWith("cpptools-srv2"); - const isCppToolsSrv: boolean = crashFile.startsWith("cpptools-srv"); - const processName: string = (isCppToolsSrv2 ? "cpptools-srv2 process" : isCppToolsSrv ? "cpptools-srv process" : "cpptools process") + "\n"; + const processName: string = (crashFile.startsWith("cpptools-srv2") ? "cpptools-srv2 process" : + crashFile.startsWith("cpptools-srv") ? "cpptools-srv process" : + crashFile.startsWith("cpptools-wordexp") ? "cpptools-wordexp process" : "cpptools process") + "\n"; const filtPath: string | null = which.sync("c++filt", { nothrow: true }); const isMac: boolean = process.platform === "darwin"; const startStr: string = isMac ? " _" : "<";