summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter/test/unit/head_win64cfi.js
blob: 4df99213dbedd6c8483b4b18e5b7f49b63afbe1e (plain)
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
/* import-globals-from head_crashreporter.js */

let gTestCrasherSyms = null;
let gModules = null;

// Returns the offset (int) of an IP with a given base address.
// This is effectively (ip - base), except a bit more complication due to
// Javascript's shaky handling of 64-bit integers.
// base & ip are passed as hex strings.
function getModuleOffset(base, ip) {
  let i = 0;
  // Find where the two addresses diverge, which enables us to perform a 32-bit
  // subtraction.
  // e.g.    "0x1111111111112222"
  //       - "0x1111111111111111"
  // becomes 2222 - 1111
  for (; i < base.length; ++i) {
    if (base[i] != ip[i]) {
      break;
    }
  }
  if (i == base.length) {
    return 0;
  }
  let lhs2 = "0x" + base.substring(i);
  let rhs2 = "0x" + ip.substring(i);
  return parseInt(rhs2) - parseInt(lhs2);
}

// Uses gTestCrasherSyms to convert an address to a symbol.
function findNearestTestCrasherSymbol(addr) {
  addr += 1; // Breakpad sometimes offsets addresses; correct for this.
  let closestDistance = null;
  let closestSym = null;
  for (let sym in gTestCrasherSyms) {
    if (addr >= gTestCrasherSyms[sym]) {
      let thisDistance = addr - gTestCrasherSyms[sym];
      if (closestDistance === null || thisDistance < closestDistance) {
        closestDistance = thisDistance;
        closestSym = sym;
      }
    }
  }
  if (closestSym === null) {
    return null;
  }
  return { symbol: closestSym, offset: closestDistance };
}

// Populate known symbols for testcrasher.dll.
// Use the same prop names as from CrashTestUtils to avoid the need for mapping.
function initTestCrasherSymbols() {
  gTestCrasherSyms = {};
  for (let k in CrashTestUtils) {
    // Not all keys here are valid symbol names. getWin64CFITestFnAddrOffset
    // will return 0 in those cases, no need to filter here.
    if (Number.isInteger(CrashTestUtils[k])) {
      let t = CrashTestUtils.getWin64CFITestFnAddrOffset(CrashTestUtils[k]);
      if (t > 0) {
        gTestCrasherSyms[k] = t;
      }
    }
  }
}

function stackFrameToString(frameIndex, frame) {
  // Calculate the module offset.
  let ip = frame.ip;
  let symbol = "";
  let moduleOffset = "unknown_offset";
  let filename = "unknown_module";

  if (
    typeof frame.module_index !== "undefined" &&
    frame.module_index >= 0 &&
    frame.module_index < gModules.length
  ) {
    let base = gModules[frame.module_index].base_addr;
    moduleOffset = getModuleOffset(base, ip);
    filename = gModules[frame.module_index].filename;

    if (filename === "testcrasher.dll") {
      let nearestSym = findNearestTestCrasherSymbol(moduleOffset);
      if (nearestSym !== null) {
        symbol = nearestSym.symbol + "+" + nearestSym.offset.toString(16);
      }
    }
  }

  let ret =
    "frames[" +
    frameIndex +
    "] ip=" +
    ip +
    " " +
    symbol +
    ", module:" +
    filename +
    ", trust:" +
    frame.trust +
    ", moduleOffset:" +
    moduleOffset.toString(16);
  return ret;
}

function dumpStackFrames(frames, maxFrames) {
  for (let i = 0; i < Math.min(maxFrames, frames.length); ++i) {
    info(stackFrameToString(i, frames[i]));
  }
}

// Test that the top of the given stack (from extra data) matches the given
// expected frames.
//
// expected is { symbol: "", trust: "" }
function assertStack(stack, expected) {
  for (let i = 0; i < stack.length; ++i) {
    if (i >= expected.length) {
      ok("Top stack frames were expected");
      return;
    }
    let frame = stack[i];
    let expectedFrame = expected[i];
    let dumpThisFrame = function () {
      info("  Actual frame: " + stackFrameToString(i, frame));
      info(
        "Expected { symbol: " +
          expectedFrame.symbol +
          ", trust: " +
          expectedFrame.trust +
          "}"
      );
    };

    if (expectedFrame.trust) {
      if (expectedFrame.trust.startsWith("!")) {
        // A "!" prefix on the frame trust matching is a logical "not".
        if (frame.trust === expectedFrame.trust.substring(1)) {
          dumpThisFrame();
          info("Expected frame trust matched when it should not have.");
          ok(false);
        }
      } else if (frame.trust !== expectedFrame.trust) {
        dumpThisFrame();
        info("Expected frame trust did not match.");
        ok(false);
      }
    }

    if (expectedFrame.symbol) {
      if (typeof frame.module_index === "undefined") {
        // Without a module_index, it happened in an unknown module. Currently
        // you can't specify an expected "unknown" module.
        info("Unknown symbol in unknown module.");
        ok(false);
      }
      if (frame.module_index < 0 || frame.module_index >= gModules.length) {
        dumpThisFrame();
        info("Unknown module.");
        ok(false);
        return;
      }
      let base = gModules[frame.module_index].base_addr;
      let moduleOffset = getModuleOffset(base, frame.ip);
      let filename = gModules[frame.module_index].filename;
      if (filename == "testcrasher.dll") {
        let nearestSym = findNearestTestCrasherSymbol(moduleOffset);
        if (nearestSym === null) {
          dumpThisFrame();
          info("Unknown symbol.");
          ok(false);
          return;
        }

        if (nearestSym.symbol !== expectedFrame.symbol) {
          dumpThisFrame();
          info("Mismatching symbol.");
          ok(false);
        }
      }
    }
  }
}

// Performs a crash, runs minidump-analyzer, and checks expected stack analysis.
//
// how: The crash to perform. Constants defined in both CrashTestUtils.jsm
//   and nsTestCrasher.cpp (i.e. CRASH_X64CFI_PUSH_NONVOL)
// expectedStack: An array of {"symbol", "trust"} where trust is "cfi",
//   "context", "scan", et al. May be null if you don't need to check the stack.
// minidumpAnalyzerArgs: An array of additional arguments to pass to
//   minidump-analyzer.exe.
async function do_x64CFITest(how, expectedStack, minidumpAnalyzerArgs) {
  // Setup is run in the subprocess so we cannot use any closures.
  let setupFn = "crashType = CrashTestUtils." + how + ";";

  let callbackFn = async function (minidumpFile, extra, extraFile) {
    runMinidumpAnalyzer(minidumpFile, minidumpAnalyzerArgs);

    // Refresh updated extra data
    extra = await IOUtils.readJSON(extraFile.path);

    initTestCrasherSymbols();
    let stackTraces = extra.StackTraces;
    let crashingThreadIndex = stackTraces.crash_info.crashing_thread;
    gModules = stackTraces.modules;
    let crashingFrames = stackTraces.threads[crashingThreadIndex].frames;

    dumpStackFrames(crashingFrames, 10);

    assertStack(crashingFrames, expectedStack);
  };

  do_crash(setupFn, callbackFn, true, true);
}