Skip to content

Commit e05eb01

Browse files
edisigcidustinbyrne
authored andcommitted
fix: Crashes due to shadowed global scope object
Changes instrumentation to prevent crashes in environments/code where global or globalThis objects are shadowed or manipulated.
1 parent 2b31e0f commit e05eb01

File tree

6 files changed

+152
-12
lines changed

6 files changed

+152
-12
lines changed

src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export class Config {
3030
// If it's 0 then no async tracking.
3131
public readonly asyncTrackingTimeout: number;
3232

33+
// This flag controls whether a check is generated for the existence
34+
// of the AppMap record function in the global namespace. The check prevents
35+
// crashes in environments where the global/globalThis object is shadowed or
36+
// manipulated. This flag allows easy toggling of the check during testing.
37+
public readonly generateGlobalRecordHookCheck: boolean = true;
38+
3339
private readonly document?: Document;
3440
private migrationPending = false;
3541

src/hooks/__tests__/instrument.test.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@ describe(instrument.transform, () => {
5252
"static": true
5353
}];
5454
55+
${instrument.__appmapRecordInitFunctionDeclarationCode}
56+
${instrument.__appmapRecordVariableDeclarationCode}
57+
5558
function testFun(arg) {
56-
return global.AppMapRecordHook.call(this, () => {
59+
return __appmapRecord.call(this, () => {
5760
return arg + 1;
5861
}, arguments, __appmapFunctionRegistry[0]);
5962
}
@@ -92,9 +95,12 @@ describe(instrument.transform, () => {
9295
}
9396
}];
9497
98+
${instrument.__appmapRecordInitFunctionDeclarationCode}
99+
${instrument.__appmapRecordVariableDeclarationCode}
100+
95101
class TestClass {
96102
foo(value) {
97-
return global.AppMapRecordHook.call(this, () => {
103+
return __appmapRecord.call(this, () => {
98104
return value + 1;
99105
}, arguments, __appmapFunctionRegistry[0]);
100106
}
@@ -175,21 +181,24 @@ describe(instrument.transform, () => {
175181
},
176182
];
177183
178-
const outer = (...$appmap$args) => global.AppMapRecordHook.call(
184+
${instrument.__appmapRecordInitFunctionDeclarationCode}
185+
${instrument.__appmapRecordVariableDeclarationCode}
186+
187+
const outer = (...$appmap$args) => __appmapRecord.call(
179188
undefined,
180189
(arg) => arg + 42,
181190
$appmap$args,
182191
__appmapFunctionRegistry[0],
183192
);
184193
185194
export const testFun = (...$appmap$args) =>
186-
global.AppMapRecordHook.call(
195+
__appmapRecord.call(
187196
undefined,
188197
(arg) => {
189198
var s42 = y => y - 42;
190199
let s43 = y => y - 43;
191200
const inner = (...$appmap$args) =>
192-
global.AppMapRecordHook.call(
201+
__appmapRecord.call(
193202
undefined,
194203
(x) => s42(x) * 2,
195204
$appmap$args,
@@ -255,12 +264,15 @@ describe(instrument.transform, () => {
255264
},
256265
];
257266
267+
${instrument.__appmapRecordInitFunctionDeclarationCode}
268+
${instrument.__appmapRecordVariableDeclarationCode}
269+
258270
exports.testFun = (...$appmap$args) =>
259-
global.AppMapRecordHook.call(
271+
__appmapRecord.call(
260272
undefined,
261273
(arg) => {
262274
const inner = (...$appmap$args) =>
263-
global.AppMapRecordHook.call(
275+
__appmapRecord.call(
264276
undefined,
265277
(x) => x * 2,
266278
$appmap$args,
@@ -303,8 +315,11 @@ describe(instrument.transform, () => {
303315
"klassOrFile": "test",
304316
"static": true
305317
}];
306-
307-
const testFun = (...$appmap$args) => global.AppMapRecordHook.call(undefined, x => x * 2,
318+
319+
${instrument.__appmapRecordInitFunctionDeclarationCode}
320+
${instrument.__appmapRecordVariableDeclarationCode}
321+
322+
const testFun = (...$appmap$args) => __appmapRecord.call(undefined, x => x * 2,
308323
$appmap$args, __appmapFunctionRegistry[0]);
309324
310325
exports.testFun = testFun;

src/hooks/instrument.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,12 @@ export function transform(
176176

177177
if (transformedFunctionInfos.length === 0) return program;
178178

179+
// Add these to prevent crash from shadowing of "global" and/or "globalThis".
180+
if (config().generateGlobalRecordHookCheck) {
181+
program.body.unshift(__appmapRecordVariableDeclaration);
182+
program.body.unshift(__appmapRecordInitFunctionDeclaration);
183+
}
184+
179185
// Add a global variable to hold the function registry (see recorder.ts)
180186
const functionRegistryAssignment: ESTree.VariableDeclaration = {
181187
type: "VariableDeclaration",
@@ -253,12 +259,12 @@ function wrapCallable(
253259
): ESTree.CallExpression {
254260
// (1) Pass the function as an arrow function expression even if the original
255261
// function is not an arrow function, because of a potential super keyword inside.
256-
// global.AppMapRecordHook.call(this|undefined, () => {...}, arguments, __appmapFunctionRegistry[i])
262+
// __appmapRecord.call(this|undefined, () => {...}, arguments, __appmapFunctionRegistry[i])
257263
//
258264
// (2) If it's a generator function then pass it as a generator function since
259265
// yield keyword cannot be used inside an arrow function. We don't care about (1)
260266
// since we don't transform generator methods due to the potential super keyword inside.
261-
// yield* global.AppMapRecordHook.call(this|undefined, function* f() {...}, arguments, __appmapFunctionRegistry[i])
267+
// yield* __appmapRecord.call(this|undefined, function* f() {...}, arguments, __appmapFunctionRegistry[i])
262268

263269
let functionArgument: ESTree.Expression =
264270
fd.type === "ArrowFunctionExpression"
@@ -279,7 +285,9 @@ function wrapCallable(
279285
if (fd.type != "ArrowFunctionExpression") functionArgument = { ...functionArgument, params: [] };
280286

281287
return call_(
282-
memberId("global", "AppMapRecordHook", "call"),
288+
config().generateGlobalRecordHookCheck
289+
? memberId("__appmapRecord", "call")
290+
: memberId("global", "AppMapRecordHook", "call"),
283291
thisArg,
284292
functionArgument,
285293
argsArg,
@@ -352,3 +360,27 @@ function exportName(expr: ESTree.Expression): ESTree.Identifier | undefined {
352360
}
353361
return undefined;
354362
}
363+
364+
export const __appmapRecordVariableDeclarationCode = `const __appmapRecord = __appmapRecordInit()`;
365+
const __appmapRecordVariableDeclaration = parse(__appmapRecordVariableDeclarationCode, {
366+
module: true,
367+
}).body[0] as ESTree.VariableDeclaration;
368+
369+
export const __appmapRecordInitFunctionDeclarationCode = `
370+
function __appmapRecordInit() {
371+
let g = null;
372+
try {
373+
g = global.AppMapRecordHook;
374+
} catch (e) {
375+
try {
376+
g = globalThis.AppMapRecordHook;
377+
} catch (e) {}
378+
// If global/globalThis is shadowed in the top level, we'll get:
379+
// ReferenceError: Cannot access 'global' before initialization
380+
}
381+
// Bypass recording if we can't access recorder to prevent a crash.
382+
return g ?? ((fun, argsArg) => fun.apply(this, argsArg));
383+
}`;
384+
const __appmapRecordInitFunctionDeclaration = parse(__appmapRecordInitFunctionDeclarationCode, {
385+
module: true,
386+
}).body[0] as ESTree.FunctionDeclaration;

test/__snapshots__/simple.test.ts.snap

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,81 @@ exports[`mapping a script with import attributes/assertions 1`] = `
828828
}
829829
`;
830830

831+
exports[`mapping a script with shadowed global object 1`] = `
832+
{
833+
"classMap": [
834+
{
835+
"children": [
836+
{
837+
"children": [
838+
{
839+
"location": "global.js:3",
840+
"name": "shadow",
841+
"static": true,
842+
"type": "function",
843+
},
844+
],
845+
"name": "global",
846+
"type": "class",
847+
},
848+
],
849+
"name": "simple",
850+
"type": "package",
851+
},
852+
],
853+
"events": [
854+
{
855+
"defined_class": "global",
856+
"event": "call",
857+
"id": 1,
858+
"lineno": 3,
859+
"method_id": "shadow",
860+
"parameters": [
861+
{
862+
"class": "String",
863+
"name": "global",
864+
"value": "'Hello'",
865+
},
866+
{
867+
"class": "String",
868+
"name": "globalThis",
869+
"value": "'World'",
870+
},
871+
],
872+
"path": "global.js",
873+
"static": true,
874+
"thread_id": 0,
875+
},
876+
{
877+
"elapsed": 31.337,
878+
"event": "return",
879+
"id": 2,
880+
"parent_id": 1,
881+
"thread_id": 0,
882+
},
883+
],
884+
"metadata": {
885+
"app": "simple",
886+
"client": {
887+
"name": "appmap-node",
888+
"url": "https://github.com/getappmap/appmap-node",
889+
"version": "test node-appmap version",
890+
},
891+
"language": {
892+
"engine": "Node.js",
893+
"name": "javascript",
894+
"version": "test node version",
895+
},
896+
"name": "test process recording",
897+
"recorder": {
898+
"name": "process",
899+
"type": "process",
900+
},
901+
},
902+
"version": "1.12",
903+
}
904+
`;
905+
831906
exports[`mapping a script with tangled async functions 1`] = `
832907
{
833908
"classMap": [

test/simple.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ integrationTest("mapping a custom Error class with a message property", () => {
8787
expect(readAppmap()).toMatchSnapshot();
8888
});
8989

90+
integrationTest("mapping a script with shadowed global object", () => {
91+
expect(runAppmapNode("global.js").status).toBe(0);
92+
expect(readAppmap()).toMatchSnapshot();
93+
});
94+
9095
integrationTestSkipOnWindows("finish signal is handled", async () => {
9196
const server = spawnAppmapNode("server.mjs");
9297
await new Promise<void>((r) =>

test/simple/global.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const global = "Hello";
2+
3+
function shadow(global, globalThis) {
4+
console.log(global, globalThis);
5+
}
6+
7+
shadow(global, "World");

0 commit comments

Comments
 (0)