-
Notifications
You must be signed in to change notification settings - Fork 462
Expand file tree
/
Copy pathmozilla-symbolication-api.ts
More file actions
226 lines (205 loc) · 8.41 KB
/
mozilla-symbolication-api.ts
File metadata and controls
226 lines (205 loc) · 8.41 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
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import * as v from 'valibot';
import type {
AddressResult,
LibSymbolicationRequest,
LibSymbolicationResponse,
} from './symbol-store';
// This file handles requesting symbolication information using the API described
// on https://tecken.readthedocs.io/en/latest/symbolication.html .
// Specifically, it uses version 5 of the API, which was implemented in
// https://bugzilla.mozilla.org/show_bug.cgi?id=1377479 .
//
// The actual request happens via a callback. We use two different callbacks:
//
// - When requesting symbols via HTTP, we use a callback which calls fetch.
// This is used to request symbols from the Mozilla Symbolication Server,
// or from an alternative symbol server which was configured with a
// ?symbolServer URL parameter.
// - When requesting symbols from the browser, we use a callback which sends a
// WebChannel message. The WebChannel has a querySymbolicationApi entry point
// which uses the same API as the server.
// The type for the callback, see comment above.
export type QuerySymbolicationApiCallback = (
path: string,
requestJson: string
) => Promise<unknown>;
// Valibot schemas for API response validation
// For every requested library in the memoryMap, this object contains a string
// key of the form `${debugName}/${breakpadId}`. The value is null if no
// address with the module index was requested, and otherwise a boolean that
// says whether the symbol server had symbols for this library.
const APIFoundModulesV5Schema = v.record(v.string(), v.nullable(v.boolean()));
// Information about functions that were inlined at this address.
const APIInlineFrameInfoV5Schema = v.object({
// The name of the function this inline frame was in, if known.
function: v.optional(v.string()),
// The path of the file that contains the function this inline frame was in, optional.
file: v.optional(v.string()),
// The line number that contains the source code for this inline frame that
// contributed to the instruction at the looked-up address, optional.
// e.g. 543
line: v.optional(v.number()),
});
const APIFrameInfoV5Schema = v.object({
// The hex version of the address that we requested (e.g. "0x5ab").
module_offset: v.string(),
// The debugName of the library that this frame was in.
module: v.string(),
// The index of this APIFrameInfo in its enclosing APIStack.
frame: v.number(),
// The name of the function this frame was in, if symbols were found.
function: v.optional(v.string()),
// The hex offset between the requested address and the start of the function,
// e.g. "0x3c".
function_offset: v.optional(v.string()),
// An optional size, in bytes, of the machine code of the outer function that
// this address belongs to, as a hex string, e.g. "0x270".
function_size: v.optional(v.string()),
// The path of the file that contains the function this frame was in, optional.
file: v.optional(v.string()),
// The line number that contains the source code that generated the instructions at the address, optional.
line: v.optional(v.number()),
// Information about functions that were inlined at this address.
// Ordered from inside to outside.
inlines: v.optional(v.array(APIInlineFrameInfoV5Schema)),
});
const APIStackV5Schema = v.array(APIFrameInfoV5Schema);
const APIJobResultV5Schema = v.object({
found_modules: APIFoundModulesV5Schema,
stacks: v.array(APIStackV5Schema),
});
const APIResultV5Schema = v.object({
results: v.array(APIJobResultV5Schema),
});
type APIJobResultV5 = v.InferOutput<typeof APIJobResultV5Schema>;
type APIResultV5 = v.InferOutput<typeof APIResultV5Schema>;
// Make sure that the JSON blob we receive from the API conforms to our
// type definition using valibot validation.
function _ensureIsAPIResultV5(result: unknown): APIResultV5 {
return v.parse(APIResultV5Schema, result);
}
function getV5ResultForLibRequest(
symbolSupplierName: string,
request: LibSymbolicationRequest,
addressArray: number[],
json: APIJobResultV5
): Map<number, AddressResult> {
const { lib } = request;
const { debugName, breakpadId } = lib;
if (!json.found_modules[`${debugName}/${breakpadId}`]) {
throw new Error(
`The ${symbolSupplierName} does not have symbols for ${debugName}/${breakpadId}.`
);
}
const addressInfo = json.stacks[0];
if (addressInfo.length !== addressArray.length) {
throw new Error(
`The result from the ${symbolSupplierName} has an unexpected length.`
);
}
const results = new Map<number, AddressResult>();
for (let i = 0; i < addressInfo.length; i++) {
const address = addressArray[i];
const info = addressInfo[i];
let addressResult;
if (info.function !== undefined && info.function_offset !== undefined) {
const name = info.function;
const functionOffset = parseInt(info.function_offset.substr(2), 16);
// Some symbolication API implementations return an inline stack for each
// address. These are function calls that were inlined into the outer
// function by the compiler.
let inlines;
if (info.inlines !== undefined) {
const inlineCount = info.inlines.length;
inlines = info.inlines.map(({ function: name, file, line }, i) => {
const depth = inlineCount - i;
return {
name:
name ??
`<unknown at ${info.module_offset} at inline depth ${depth}>`,
file,
line,
};
});
}
let functionSize;
if (info.function_size !== undefined) {
functionSize = parseInt(info.function_size.substr(2), 16);
}
addressResult = {
name,
symbolAddress: address - functionOffset,
file: info.file,
line: info.line,
inlines,
functionSize,
};
} else {
// This can happen if the address is between functions, or before the first
// or after the last function.
addressResult = {
name: `<unknown at ${info.module_offset}>`,
symbolAddress: address,
};
}
results.set(address, addressResult);
}
return results;
}
// Request symbols for the given addresses and libraries using the Mozilla
// symbolication API.
// Returns a promise that resolves to an array LibSymbolicationResponses,
// one response per LibSymbolicationRequest in requests. If the server does
// not have symbol information for a given library, the LibSymbolicationResponse
// for that library will have .type === 'ERROR'.
// `querySymbolicationApiCallback` should be a callback that receives the actual
// request in the Mozilla symbolication API format. See the comment about the
// callback at the top of this file for more details.
export async function requestSymbols(
symbolSupplierName: string,
requests: LibSymbolicationRequest[],
querySymbolicationApiCallback: QuerySymbolicationApiCallback
): Promise<LibSymbolicationResponse[]> {
// For each request, turn its set of addresses into an array.
// We need there to be a defined order in each addressArray so that we can
// match the results to the request.
const requestsWithAddressArrays = requests.map((request) => ({
request,
addressArray: Array.from(request.addresses),
}));
// Construct the API request body. We make one "job" per LibSymbolicatioRequest.
// Each "job" has a module list with just one "module" (the lib), and a list
// of stacks with just one "stack", which contains all addresses for that lib.
const body = {
jobs: requestsWithAddressArrays.map(({ request, addressArray }) => {
const { debugName, breakpadId } = request.lib;
return {
memoryMap: [[debugName, breakpadId]],
stacks: [addressArray.map((addr) => [0, addr])],
};
}),
};
const responseJson = _ensureIsAPIResultV5(
await querySymbolicationApiCallback('/symbolicate/v5', JSON.stringify(body))
);
return requestsWithAddressArrays.map(
({ request, addressArray }, requestIndex) => {
const json = responseJson.results[requestIndex];
try {
const { lib } = request;
const results = getV5ResultForLibRequest(
symbolSupplierName,
request,
addressArray,
json
);
return { type: 'SUCCESS', lib, results };
} catch (error) {
return { type: 'ERROR', request, error };
}
}
);
}