tor-browser

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

head.js (16461B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const utilityProcessTest = () => {
      7  return Cc["@mozilla.org/utility-process-test;1"].createInstance(
      8    Ci.nsIUtilityProcessTest
      9  );
     10 };
     11 
     12 const kGenericUtilitySandbox = 0;
     13 const kGenericUtilityActor = "unknown";
     14 
     15 // Start a generic utility process with the given array of utility actor names
     16 // registered.
     17 async function startUtilityProcess(actors = []) {
     18  info("Start a UtilityProcess");
     19  return utilityProcessTest().startProcess(actors);
     20 }
     21 
     22 // Returns an array of process infos for utility processes of the given type
     23 // or all utility processes if actor is not defined.
     24 async function getUtilityProcesses(actor = undefined, options = {}) {
     25  let procInfos = (await ChromeUtils.requestProcInfo()).children.filter(p => {
     26    return (
     27      p.type === "utility" &&
     28      (actor == undefined ||
     29        p.utilityActors.find(a => a.actorName.startsWith(actor)))
     30    );
     31  });
     32 
     33  if (!options?.quiet) {
     34    info(`Utility process infos = ${JSON.stringify(procInfos)}`);
     35  }
     36  return procInfos;
     37 }
     38 
     39 async function tryGetUtilityPid(actor, options = {}) {
     40  let process = await getUtilityProcesses(actor, options);
     41  if (!options?.quiet) {
     42    Assert.lessOrEqual(
     43      process.length,
     44      1,
     45      `at most one ${actor} process exists`
     46    );
     47  }
     48  return process[0]?.pid;
     49 }
     50 
     51 async function checkUtilityExists(actor) {
     52  info(`Looking for a running ${actor} utility process`);
     53  const utilityPid = await tryGetUtilityPid(actor);
     54  Assert.greater(utilityPid, 0, `Found ${actor} utility process ${utilityPid}`);
     55  return utilityPid;
     56 }
     57 
     58 // "Cleanly stop" a utility process.  This will never leave a crash dump file.
     59 // preferKill will "kill" the process (e.g. SIGABRT) instead of using the
     60 // UtilityProcessManager.
     61 // To "crash" -- i.e. shutdown and generate a crash dump -- use
     62 // crashSomeUtility().
     63 async function cleanUtilityProcessShutdown(actor, preferKill = false) {
     64  info(`${preferKill ? "Kill" : "Clean shutdown"} Utility Process ${actor}`);
     65 
     66  const utilityPid = await tryGetUtilityPid(actor);
     67  Assert.notStrictEqual(
     68    utilityPid,
     69    undefined,
     70    `Must have PID for ${actor} utility process`
     71  );
     72 
     73  const utilityProcessGone = TestUtils.topicObserved(
     74    "ipc:utility-shutdown",
     75    (subject, data) => parseInt(data, 10) === utilityPid
     76  );
     77 
     78  if (preferKill) {
     79    SimpleTest.expectChildProcessCrash();
     80    info(`Kill Utility Process ${utilityPid}`);
     81    const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
     82      Ci.nsIProcessToolsService
     83    );
     84    ProcessTools.kill(utilityPid);
     85  } else {
     86    info(`Stopping Utility Process ${utilityPid}`);
     87    await utilityProcessTest().stopProcess(actor);
     88  }
     89 
     90  let [subject, data] = await utilityProcessGone;
     91  ok(
     92    subject instanceof Ci.nsIPropertyBag2,
     93    "Subject needs to be a nsIPropertyBag2 to clean up properly"
     94  );
     95  is(
     96    parseInt(data, 10),
     97    utilityPid,
     98    `Should match the crashed PID ${utilityPid} with ${data}`
     99  );
    100 
    101  // Make sure the process is dead, otherwise there is a risk of race for
    102  // writing leak logs
    103  utilityProcessTest().noteIntentionalCrash(utilityPid);
    104 
    105  ok(!subject.hasKey("dumpID"), "There should be no dumpID");
    106 }
    107 
    108 async function killUtilityProcesses() {
    109  let utilityProcesses = await getUtilityProcesses();
    110  for (const utilityProcess of utilityProcesses) {
    111    for (const actor of utilityProcess.utilityActors) {
    112      info(`Stopping ${actor.actorName} utility process`);
    113      await cleanUtilityProcessShutdown(actor.actorName, /* preferKill */ true);
    114    }
    115  }
    116 }
    117 
    118 function audioTestData() {
    119  return [
    120    {
    121      src: "small-shot.ogg",
    122      expectations: {
    123        Android: {
    124          process: "Utility Generic",
    125          decoder: "ffvpx audio decoder",
    126        },
    127        Linux: {
    128          process: "Utility Generic",
    129          decoder: "ffvpx audio decoder",
    130        },
    131        WINNT: {
    132          process: "Utility Generic",
    133          decoder: "ffvpx audio decoder",
    134        },
    135        Darwin: {
    136          process: "Utility Generic",
    137          decoder: "ffvpx audio decoder",
    138        },
    139      },
    140    },
    141    {
    142      src: "small-shot.mp3",
    143      expectations: {
    144        Android: { process: "Utility Generic", decoder: "ffvpx audio decoder" },
    145        Linux: {
    146          process: "Utility Generic",
    147          decoder: "ffvpx audio decoder",
    148        },
    149        WINNT: {
    150          process: "Utility Generic",
    151          decoder: "ffvpx audio decoder",
    152        },
    153        Darwin: {
    154          process: "Utility Generic",
    155          decoder: "ffvpx audio decoder",
    156        },
    157      },
    158    },
    159    {
    160      src: "small-shot.m4a",
    161      expectations: {
    162        // Add Android after Bug 1934009
    163        Linux: {
    164          process: "Utility Generic",
    165          decoder: "ffmpeg audio decoder",
    166        },
    167        WINNT: {
    168          process: "Utility WMF",
    169          decoder: "wmf audio decoder",
    170        },
    171        Darwin: {
    172          process: "Utility AppleMedia",
    173          decoder: "apple coremedia decoder",
    174        },
    175      },
    176    },
    177    {
    178      src: "small-shot.flac",
    179      expectations: {
    180        Android: { process: "Utility Generic", decoder: "ffvpx audio decoder" },
    181        Linux: {
    182          process: "Utility Generic",
    183          decoder: "ffvpx audio decoder",
    184        },
    185        WINNT: {
    186          process: "Utility Generic",
    187          decoder: "ffvpx audio decoder",
    188        },
    189        Darwin: {
    190          process: "Utility Generic",
    191          decoder: "ffvpx audio decoder",
    192        },
    193      },
    194    },
    195  ];
    196 }
    197 
    198 function audioTestDataEME() {
    199  return [
    200    {
    201      src: {
    202        audioFile:
    203          "https://example.com/browser/ipc/glue/test/browser/short-aac-encrypted-audio.mp4",
    204        sourceBuffer: "audio/mp4",
    205      },
    206      expectations: {
    207        Linux: {
    208          process: "Utility Generic",
    209          decoder: "ffmpeg audio decoder",
    210        },
    211        WINNT: {
    212          process: "Utility WMF",
    213          decoder: "wmf audio decoder",
    214        },
    215        Darwin: {
    216          process: "Utility AppleMedia",
    217          decoder: "apple coremedia decoder",
    218        },
    219      },
    220    },
    221  ];
    222 }
    223 
    224 async function addMediaTab(src) {
    225  const tab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
    226    forceNewProcess: true,
    227  });
    228  const browser = gBrowser.getBrowserForTab(tab);
    229  await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:blank" });
    230  await SpecialPowers.spawn(browser, [src], createAudioElement);
    231  return tab;
    232 }
    233 
    234 async function addMediaTabWithEME(sourceBuffer, audioFile) {
    235  const tab = BrowserTestUtils.addTab(
    236    gBrowser,
    237    "https://example.com/browser/",
    238    {
    239      forceNewProcess: true,
    240    }
    241  );
    242  const browser = gBrowser.getBrowserForTab(tab);
    243  await BrowserTestUtils.browserLoaded(browser);
    244  await SpecialPowers.spawn(
    245    browser,
    246    [sourceBuffer, audioFile],
    247    createAudioElementEME
    248  );
    249  return tab;
    250 }
    251 
    252 async function play(
    253  tab,
    254  expectUtility,
    255  expectDecoder,
    256  expectContent = false,
    257  expectJava = false,
    258  expectError = false,
    259  withEME = false
    260 ) {
    261  let browser = tab.linkedBrowser;
    262  return SpecialPowers.spawn(
    263    browser,
    264    [
    265      expectUtility,
    266      expectDecoder,
    267      expectContent,
    268      expectJava,
    269      expectError,
    270      withEME,
    271    ],
    272    checkAudioDecoder
    273  );
    274 }
    275 
    276 async function stop(tab) {
    277  let browser = tab.linkedBrowser;
    278  await SpecialPowers.spawn(browser, [], async function () {
    279    let audio = content.document.querySelector("audio");
    280    audio.pause();
    281  });
    282 }
    283 
    284 async function createAudioElement(src) {
    285  const doc = typeof content !== "undefined" ? content.document : document;
    286  const ROOT = "https://example.com/browser/ipc/glue/test/browser";
    287  let audio = doc.createElement("audio");
    288  audio.setAttribute("controls", "true");
    289  audio.setAttribute("loop", true);
    290  audio.src = `${ROOT}/${src}`;
    291  doc.body.appendChild(audio);
    292 }
    293 
    294 async function createAudioElementEME(sourceBuffer, audioFile) {
    295  // Helper to clone data into content so the EME helper can use the data.
    296  function cloneIntoContent(data) {
    297    return Cu.cloneInto(data, content.wrappedJSObject);
    298  }
    299 
    300  // Load the EME helper into content.
    301  Services.scriptloader.loadSubScript(
    302    "chrome://mochitests/content/browser/ipc/glue/test/browser/eme_standalone.js",
    303    content
    304  );
    305 
    306  let audio = content.document.createElement("audio");
    307  audio.setAttribute("controls", "true");
    308  audio.setAttribute("loop", true);
    309  audio.setAttribute("_sourceBufferType", sourceBuffer);
    310  audio.setAttribute("_audioUrl", audioFile);
    311  content.document.body.appendChild(audio);
    312 
    313  let emeHelper = new content.wrappedJSObject.EmeHelper();
    314  emeHelper.SetKeySystem(
    315    content.wrappedJSObject.EmeHelper.GetClearkeyKeySystemString()
    316  );
    317  emeHelper.SetInitDataTypes(cloneIntoContent(["keyids", "cenc"]));
    318  emeHelper.SetAudioCapabilities(
    319    cloneIntoContent([{ contentType: 'audio/mp4; codecs="mp4a.40.2"' }])
    320  );
    321  emeHelper.AddKeyIdAndKey(
    322    "2cdb0ed6119853e7850671c3e9906c3c",
    323    "808B9ADAC384DE1E4F56140F4AD76194"
    324  );
    325  emeHelper.onerror = error => {
    326    is(false, `Got unexpected error from EME helper: ${error}`);
    327  };
    328  await emeHelper.ConfigureEme(audio);
    329  // Done setting up EME.
    330 }
    331 
    332 async function checkAudioDecoder(
    333  expectedProcess,
    334  expectedDecoder,
    335  expectContent = false,
    336  expectJava = false,
    337  expectError = false,
    338  withEME = false
    339 ) {
    340  const doc = typeof content !== "undefined" ? content.document : document;
    341  let audio = doc.querySelector("audio");
    342  const checkPromise = new Promise((resolve, reject) => {
    343    const timeUpdateHandler = async () => {
    344      const debugInfo = await SpecialPowers.wrap(audio).mozRequestDebugInfo();
    345      const audioDecoderName = debugInfo.decoder.reader.audioDecoderName;
    346 
    347      const isExpectedDecoder =
    348        audioDecoderName.indexOf(`${expectedDecoder}`) == 0;
    349      ok(
    350        isExpectedDecoder,
    351        `playback ${audio.src} was from decoder '${audioDecoderName}', expected '${expectedDecoder}'`
    352      );
    353 
    354      const isExpectedProcess =
    355        audioDecoderName.indexOf(`(${expectedProcess} remote)`) > 0;
    356      const isJavaRemote = audioDecoderName.indexOf("(remote)") > 0;
    357      const isOk =
    358        (isExpectedProcess && !isJavaRemote && !expectContent && !expectJava) || // Running in Utility
    359        (expectJava && !isExpectedProcess && isJavaRemote) || // Running in Java remote
    360        (expectContent && !isExpectedProcess && !isJavaRemote); // Running in Content
    361 
    362      ok(
    363        isOk,
    364        `playback ${audio.src} was from process '${audioDecoderName}', expected '${expectedProcess}'`
    365      );
    366 
    367      if (isOk) {
    368        resolve();
    369      } else {
    370        reject();
    371      }
    372    };
    373 
    374    const startPlaybackHandler = async () => {
    375      ok(
    376        await audio.play().then(
    377          _ => true,
    378          _ => false
    379        ),
    380        "audio started playing"
    381      );
    382 
    383      audio.addEventListener("timeupdate", timeUpdateHandler, { once: true });
    384    };
    385 
    386    audio.addEventListener("error", async () => {
    387      info(
    388        `Received HTML media error: ${audio.error.code}: ${audio.error.message}`
    389      );
    390      if (expectError) {
    391        const w = typeof content !== "undefined" ? content.window : window;
    392        ok(
    393          audio.error.code === w.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED ||
    394            w.MediaError.MEDIA_ERR_DECODE,
    395          "Media supported but decoding failed"
    396        );
    397        resolve();
    398      } else {
    399        info(`Unexpected error`);
    400        reject();
    401      }
    402    });
    403 
    404    audio.addEventListener("canplaythrough", startPlaybackHandler, {
    405      once: true,
    406    });
    407  });
    408 
    409  if (!withEME) {
    410    // We need to make sure the decoder is ready before play()ing otherwise we
    411    // could get into bad situations
    412    audio.load();
    413  } else {
    414    // For EME we need to create and load content ourselves. We do this here
    415    // because if we do it in createAudioElementEME() above then we end up
    416    // with events fired before we get a chance to listen to them here
    417    async function once(target, name) {
    418      return new Promise(r => target.addEventListener(name, r, { once: true }));
    419    }
    420 
    421    // Setup MSE.
    422    const ms = new content.wrappedJSObject.MediaSource();
    423    audio.src = content.wrappedJSObject.URL.createObjectURL(ms);
    424    await once(ms, "sourceopen");
    425    const sb = ms.addSourceBuffer(audio.getAttribute("_sourceBufferType"));
    426    let fetchResponse = await content.fetch(audio.getAttribute("_audioUrl"));
    427    let dataBuffer = await fetchResponse.arrayBuffer();
    428    sb.appendBuffer(dataBuffer);
    429    await once(sb, "updateend");
    430    ms.endOfStream();
    431    await once(ms, "sourceended");
    432  }
    433 
    434  return checkPromise;
    435 }
    436 
    437 async function runMochitestUtilityAudio(
    438  src,
    439  {
    440    expectUtility,
    441    expectDecoder,
    442    expectContent = false,
    443    expectJava = false,
    444    expectError = false,
    445  } = {}
    446 ) {
    447  info(`Add media: ${src}`);
    448  await createAudioElement(src);
    449  let audio = document.querySelector("audio");
    450  ok(audio, "Found an audio element created");
    451 
    452  info(`Play media: ${src}`);
    453  await checkAudioDecoder(
    454    expectUtility,
    455    expectDecoder,
    456    expectContent,
    457    expectJava,
    458    expectError
    459  );
    460 
    461  info(`Pause media: ${src}`);
    462  await audio.pause();
    463 
    464  info(`Remove media: ${src}`);
    465  document.body.removeChild(audio);
    466 }
    467 
    468 async function crashSomeUtility(utilityPid, actorsCheck) {
    469  SimpleTest.expectChildProcessCrash();
    470 
    471  const crashMan = Services.crashmanager;
    472  const utilityProcessGone = TestUtils.topicObserved(
    473    "ipc:utility-shutdown",
    474    (subject, data) => {
    475      info(`ipc:utility-shutdown: data=${data} subject=${subject}`);
    476      return parseInt(data, 10) === utilityPid;
    477    }
    478  );
    479 
    480  info("prune any previous crashes");
    481  const future = new Date(Date.now() + 1000 * 60 * 60 * 24);
    482  await crashMan.pruneOldCrashes(future);
    483 
    484  info("crash Utility Process");
    485  const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
    486    Ci.nsIProcessToolsService
    487  );
    488 
    489  info(`Crash Utility Process ${utilityPid}`);
    490  ProcessTools.crash(utilityPid);
    491 
    492  info(`Waiting for utility process ${utilityPid} to go away.`);
    493  let [subject, data] = await utilityProcessGone;
    494  Assert.strictEqual(
    495    parseInt(data, 10),
    496    utilityPid,
    497    `Should match the crashed PID ${utilityPid} with ${data}`
    498  );
    499  ok(
    500    subject instanceof Ci.nsIPropertyBag2,
    501    "Subject needs to be a nsIPropertyBag2 to clean up properly"
    502  );
    503 
    504  // Make sure the process is dead, otherwise there is a risk of race for
    505  // writing leak logs
    506  utilityProcessTest().noteIntentionalCrash(utilityPid);
    507 
    508  const dumpID = subject.getPropertyAsAString("dumpID");
    509  ok(dumpID, "There should be a dumpID");
    510 
    511  await crashMan.ensureCrashIsPresent(dumpID);
    512  await crashMan.getCrashes().then(crashes => {
    513    is(crashes.length, 1, "There should be only one record");
    514    const crash = crashes[0];
    515    ok(
    516      crash.isOfType(
    517        crashMan.processTypes[Ci.nsIXULRuntime.PROCESS_TYPE_UTILITY],
    518        crashMan.CRASH_TYPE_CRASH
    519      ),
    520      "Record should be a utility process crash"
    521    );
    522    Assert.strictEqual(crash.id, dumpID, "Record should have an ID");
    523    ok(
    524      actorsCheck(crash.metadata.UtilityActorsName),
    525      `Record should have the correct actors name for: ${crash.metadata.UtilityActorsName}`
    526    );
    527  });
    528 
    529  let minidumpDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
    530  minidumpDirectory.append("minidumps");
    531 
    532  let dumpfile = minidumpDirectory.clone();
    533  dumpfile.append(dumpID + ".dmp");
    534  if (dumpfile.exists()) {
    535    info(`Removal of ${dumpfile.path}`);
    536    dumpfile.remove(false);
    537  }
    538 
    539  let extrafile = minidumpDirectory.clone();
    540  extrafile.append(dumpID + ".extra");
    541  info(`Removal of ${extrafile.path}`);
    542  if (extrafile.exists()) {
    543    extrafile.remove(false);
    544  }
    545 }
    546 
    547 // Crash a utility process and generate a crash dump.  To close a utility
    548 // process (forcefully or not) without a generating a crash, use
    549 // cleanUtilityProcessShutdown.
    550 async function crashSomeUtilityActor(
    551  actor,
    552  actorsCheck = () => {
    553    return true;
    554  }
    555 ) {
    556  // Get PID for utility type
    557  const procInfos = await getUtilityProcesses(actor);
    558  Assert.equal(
    559    procInfos.length,
    560    1,
    561    `exactly one ${actor} utility process should be found`
    562  );
    563  const utilityPid = procInfos[0].pid;
    564  return crashSomeUtility(utilityPid, actorsCheck);
    565 }
    566 
    567 function isNightlyOnly() {
    568  const { AppConstants } = ChromeUtils.importESModule(
    569    "resource://gre/modules/AppConstants.sys.mjs"
    570  );
    571  return AppConstants.NIGHTLY_BUILD;
    572 }