tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

symbolication.sys.mjs (12657B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 // @ts-check
      5 
      6 /** @type {any} */
      7 const lazy = {};
      8 
      9 /**
     10 * @typedef {import("perf").Library} Library
     11 * @typedef {import("perf").PerfFront} PerfFront
     12 * @typedef {import("perf").SymbolTableAsTuple} SymbolTableAsTuple
     13 * @typedef {import("perf").SymbolicationService} SymbolicationService
     14 * @typedef {import("perf").SymbolicationWorkerInitialMessage} SymbolicationWorkerInitialMessage
     15 */
     16 
     17 /**
     18 * @template R
     19 * @typedef {import("perf").SymbolicationWorkerReplyData<R>} SymbolicationWorkerReplyData<R>
     20 */
     21 
     22 ChromeUtils.defineESModuleGetters(
     23  lazy,
     24  {
     25    clearTimeout: "resource://gre/modules/Timer.sys.mjs",
     26    setTimeout: "resource://gre/modules/Timer.sys.mjs",
     27  },
     28  { global: "contextual" }
     29 );
     30 
     31 /** @type {any} */
     32 const global = globalThis;
     33 
     34 // This module obtains symbol tables for binaries.
     35 // It does so with the help of a WASM module which gets pulled in from the
     36 // internet on demand. We're doing this purely for the purposes of saving on
     37 // code size. The contents of the WASM module are expected to be static, they
     38 // are checked against the hash specified below.
     39 // The WASM code is run on a ChromeWorker thread. It takes the raw byte
     40 // contents of the to-be-dumped binary (and of an additional optional pdb file
     41 // on Windows) as its input, and returns a set of typed arrays which make up
     42 // the symbol table.
     43 
     44 // Don't let the strange looking URLs and strings below scare you.
     45 // The hash check ensures that the contents of the wasm module are what we
     46 // expect them to be.
     47 // The source code is at https://github.com/mstange/profiler-get-symbols/ .
     48 // Documentation is at https://docs.rs/samply-api/ .
     49 // The sha384 sum can be computed with the following command (tested on macOS):
     50 // shasum -b -a 384 profiler_get_symbols_wasm_bg.wasm | awk '{ print $1 }' | xxd -r -p | base64
     51 
     52 // Generated from https://github.com/mstange/profiler-get-symbols/commit/390b8c4be82c720dd3977ff205fb34bd7d0e00ba
     53 const WASM_MODULE_URL =
     54  "https://storage.googleapis.com/firefox-profiler-get-symbols/390b8c4be82c720dd3977ff205fb34bd7d0e00ba.wasm";
     55 const WASM_MODULE_INTEGRITY =
     56  "sha384-P8j6U9jY+M4zSfJKXb1ECjsTPkzQ0hAvgb4zv3gHvlg+THRtVpOrDSywHJBhin00";
     57 
     58 const EXPIRY_TIME_IN_MS = 5 * 60 * 1000; // 5 minutes
     59 
     60 /** @type {Promise<WebAssembly.Module> | null} */
     61 let gCachedWASMModulePromise = null;
     62 let gCachedWASMModuleExpiryTimer = 0;
     63 
     64 function clearCachedWASMModule() {
     65  gCachedWASMModulePromise = null;
     66  gCachedWASMModuleExpiryTimer = 0;
     67 }
     68 
     69 function getWASMProfilerGetSymbolsModule() {
     70  if (!gCachedWASMModulePromise) {
     71    gCachedWASMModulePromise = (async function () {
     72      const request = new Request(WASM_MODULE_URL, {
     73        integrity: WASM_MODULE_INTEGRITY,
     74        credentials: "omit",
     75      });
     76      return WebAssembly.compileStreaming(fetch(request));
     77    })();
     78  }
     79 
     80  // Reset expiry timer.
     81  lazy.clearTimeout(gCachedWASMModuleExpiryTimer);
     82  gCachedWASMModuleExpiryTimer = lazy.setTimeout(
     83    clearCachedWASMModule,
     84    EXPIRY_TIME_IN_MS
     85  );
     86 
     87  return gCachedWASMModulePromise;
     88 }
     89 
     90 /**
     91 * Handle the entire life cycle of a worker, and report its result.
     92 * This method creates a new worker, sends the initial message to it, handles
     93 * any errors, and accepts the result.
     94 * Returns a promise that resolves with the contents of the (singular) result
     95 * message or rejects with an error.
     96 *
     97 * @template M
     98 * @template R
     99 * @param {string} workerURL
    100 * @param {M} initialMessageToWorker
    101 * @returns {Promise<R>}
    102 */
    103 async function getResultFromWorker(workerURL, initialMessageToWorker) {
    104  return new Promise((resolve, reject) => {
    105    const worker = new ChromeWorker(workerURL);
    106 
    107    /** @param {MessageEvent<SymbolicationWorkerReplyData<R>>} msg */
    108    worker.onmessage = msg => {
    109      if ("error" in msg.data) {
    110        const error = msg.data.error;
    111        if (error.name) {
    112          // Turn the JSON error object into a real Error object.
    113          const { name, message, fileName, lineNumber } = error;
    114          const ErrorObjConstructor =
    115            name in global && Error.isPrototypeOf(global[name])
    116              ? global[name]
    117              : Error;
    118          const e = new ErrorObjConstructor(message, fileName, lineNumber);
    119          e.name = name;
    120          reject(e);
    121        } else {
    122          reject(error);
    123        }
    124        return;
    125      }
    126      resolve(msg.data.result);
    127    };
    128 
    129    // Handle uncaught errors from the worker script. onerror is called if
    130    // there's a syntax error in the worker script, for example, or when an
    131    // unhandled exception is thrown, but not for unhandled promise
    132    // rejections. Without this handler, mistakes during development such as
    133    // syntax errors can be hard to track down.
    134    worker.onerror = errorEvent => {
    135      worker.terminate();
    136      if (ErrorEvent.isInstance(errorEvent)) {
    137        const { message, filename, lineno } = errorEvent;
    138        const error = new Error(`${message} at ${filename}:${lineno}`);
    139        error.name = "WorkerError";
    140        reject(error);
    141      } else {
    142        reject(new Error("Error in worker " + String(errorEvent)));
    143      }
    144    };
    145 
    146    // Handle errors from messages that cannot be deserialized. I'm not sure
    147    // how to get into such a state, but having this handler seems like a good
    148    // idea.
    149    worker.onmessageerror = () => {
    150      worker.terminate();
    151      reject(new Error("Error in worker"));
    152    };
    153 
    154    worker.postMessage(initialMessageToWorker);
    155  });
    156 }
    157 
    158 /**
    159 * @param {PerfFront} perfFront
    160 * @param {string} path
    161 * @param {string} breakpadId
    162 * @returns {Promise<SymbolTableAsTuple>}
    163 */
    164 async function getSymbolTableFromDebuggee(perfFront, path, breakpadId) {
    165  const [addresses, index, buffer] = await perfFront.getSymbolTable(
    166    path,
    167    breakpadId
    168  );
    169  // The protocol transmits these arrays as plain JavaScript arrays of
    170  // numbers, but we want to pass them on as typed arrays. Convert them now.
    171  return [
    172    new Uint32Array(addresses),
    173    new Uint32Array(index),
    174    new Uint8Array(buffer),
    175  ];
    176 }
    177 
    178 /**
    179 * Profiling through the DevTools remote debugging protocol supports multiple
    180 * different modes. This class is specialized to handle various profiling
    181 * modes such as:
    182 *
    183 *   1) Profiling the same browser on the same machine.
    184 *   2) Profiling a remote browser on the same machine.
    185 *   3) Profiling a remote browser on a different device.
    186 *
    187 * It's also built to handle symbolication requests for both Gecko libraries and
    188 * system libraries. However, it only handles cases where symbol information
    189 * can be found in a local file on this machine. There is one case that is not
    190 * covered by that restriction: Android system libraries. That case requires
    191 * the help of the perf actor and is implemented in
    192 * LocalSymbolicationServiceWithRemoteSymbolTableFallback.
    193 */
    194 class LocalSymbolicationService {
    195  /**
    196   * @param {Library[]} sharedLibraries - Information about the shared libraries.
    197   *   This allows mapping (debugName, breakpadId) pairs to the absolute path of
    198   *   the binary and/or PDB file, and it ensures that these absolute paths come
    199   *   from a trusted source and not from the profiler UI.
    200   * @param {string[]} objdirs - An array of objdir paths
    201   *   on the host machine that should be searched for relevant build artifacts.
    202   */
    203  constructor(sharedLibraries, objdirs) {
    204    this._libInfoMap = new Map(
    205      sharedLibraries.map(lib => {
    206        const { debugName, breakpadId } = lib;
    207        const key = `${debugName}:${breakpadId}`;
    208        return [key, lib];
    209      })
    210    );
    211    this._objdirs = objdirs;
    212  }
    213 
    214  /**
    215   * @param {string} debugName
    216   * @param {string} breakpadId
    217   * @returns {Promise<SymbolTableAsTuple>}
    218   */
    219  async getSymbolTable(debugName, breakpadId) {
    220    const module = await getWASMProfilerGetSymbolsModule();
    221    /** @type {SymbolicationWorkerInitialMessage} */
    222    const initialMessage = {
    223      request: {
    224        type: "GET_SYMBOL_TABLE",
    225        debugName,
    226        breakpadId,
    227      },
    228      libInfoMap: this._libInfoMap,
    229      objdirs: this._objdirs,
    230      module,
    231    };
    232    return getResultFromWorker(
    233      "resource://devtools/shared/performance-new/symbolication.worker.js",
    234      initialMessage
    235    );
    236  }
    237 
    238  /**
    239   * @param {string} path
    240   * @param {string} requestJson
    241   * @returns {Promise<string>}
    242   */
    243  async querySymbolicationApi(path, requestJson) {
    244    const module = await getWASMProfilerGetSymbolsModule();
    245    /** @type {SymbolicationWorkerInitialMessage} */
    246    const initialMessage = {
    247      request: {
    248        type: "QUERY_SYMBOLICATION_API",
    249        path,
    250        requestJson,
    251      },
    252      libInfoMap: this._libInfoMap,
    253      objdirs: this._objdirs,
    254      module,
    255    };
    256    return getResultFromWorker(
    257      "resource://devtools/shared/performance-new/symbolication.worker.js",
    258      initialMessage
    259    );
    260  }
    261 }
    262 
    263 /**
    264 * An implementation of the SymbolicationService interface which also
    265 * covers the Android system library case.
    266 * We first try to get symbols from the wrapped SymbolicationService.
    267 * If that fails, we try to get the symbol table through the perf actor.
    268 */
    269 class LocalSymbolicationServiceWithRemoteSymbolTableFallback {
    270  /**
    271   * @param {SymbolicationService} symbolicationService - The regular symbolication service.
    272   * @param {Library[]} sharedLibraries - Information about the shared libraries
    273   * @param {PerfFront} perfFront - A perf actor, to obtain symbol
    274   *   tables from remote targets
    275   */
    276  constructor(symbolicationService, sharedLibraries, perfFront) {
    277    this._symbolicationService = symbolicationService;
    278    this._libs = sharedLibraries;
    279    this._perfFront = perfFront;
    280  }
    281 
    282  /**
    283   * @param {string} debugName
    284   * @param {string} breakpadId
    285   * @returns {Promise<SymbolTableAsTuple>}
    286   */
    287  async getSymbolTable(debugName, breakpadId) {
    288    try {
    289      return await this._symbolicationService.getSymbolTable(
    290        debugName,
    291        breakpadId
    292      );
    293    } catch (errorFromLocalFiles) {
    294      // Try to obtain the symbol table on the debuggee. We get into this
    295      // branch in the following cases:
    296      //  - Android system libraries
    297      //  - Firefox binaries that have no matching equivalent on the host
    298      //    machine, for example because the user didn't point us at the
    299      //    corresponding objdir, or if the build was compiled somewhere
    300      //    else, or if the build on the device is outdated.
    301      // For now, the "debuggee" is never a Windows machine, which is why we don't
    302      // need to pass the library's debugPath. (path and debugPath are always the
    303      // same on non-Windows.)
    304      const lib = this._libs.find(
    305        l => l.debugName === debugName && l.breakpadId === breakpadId
    306      );
    307      if (!lib) {
    308        let errorMessage;
    309        if (errorFromLocalFiles instanceof Error) {
    310          errorMessage = errorFromLocalFiles.message;
    311        } else {
    312          errorMessage = `${errorFromLocalFiles}`;
    313        }
    314 
    315        throw new Error(
    316          `Could not find the library for "${debugName}", "${breakpadId}" after falling ` +
    317            `back to remote symbol table querying because regular getSymbolTable failed ` +
    318            `with error: ${errorMessage}.`
    319        );
    320      }
    321      return getSymbolTableFromDebuggee(this._perfFront, lib.path, breakpadId);
    322    }
    323  }
    324 
    325  /**
    326   * @param {string} path
    327   * @param {string} requestJson
    328   * @returns {Promise<string>}
    329   */
    330  async querySymbolicationApi(path, requestJson) {
    331    return this._symbolicationService.querySymbolicationApi(path, requestJson);
    332  }
    333 }
    334 
    335 /**
    336 * Return an object that implements the SymbolicationService interface.
    337 *
    338 * @param {Library[]} sharedLibraries - Information about the shared libraries
    339 * @param {string[]} objdirs - An array of objdir paths
    340 *   on the host machine that should be searched for relevant build artifacts.
    341 * @param {PerfFront} [perfFront] - An optional perf actor, to obtain symbol
    342 *   tables from remote targets
    343 * @return {SymbolicationService}
    344 */
    345 export function createLocalSymbolicationService(
    346  sharedLibraries,
    347  objdirs,
    348  perfFront
    349 ) {
    350  const service = new LocalSymbolicationService(sharedLibraries, objdirs);
    351  if (perfFront) {
    352    return new LocalSymbolicationServiceWithRemoteSymbolTableFallback(
    353      service,
    354      sharedLibraries,
    355      perfFront
    356    );
    357  }
    358  return service;
    359 }
    360 
    361 // This file also exports a named object containing other exports to play well
    362 // with defineESModuleGetters.
    363 export const Symbolication = {
    364  createLocalSymbolicationService,
    365 };