diff --git a/browser/components/resistfingerprinting/test/browser/browser.ini b/browser/components/resistfingerprinting/test/browser/browser.ini index 0b879410b34d..65e19604d1a5 100644 --- a/browser/components/resistfingerprinting/test/browser/browser.ini +++ b/browser/components/resistfingerprinting/test/browser/browser.ini @@ -2,6 +2,7 @@ tags = resistfingerprinting support-files = browser_navigator_header.sjs + coop_header.sjs file_dummy.html file_keyBoardEvent.sjs file_navigator.html @@ -34,4 +35,5 @@ skip-if = os == "linux" && bits == 64 && os_version == "18.04" [browser_spoofing_keyboard_event.js] skip-if = (debug || asan) && os == "linux" && bits == 64 #Bug 1518179 [browser_timezone.js] +[browser_timing_precision_cross_origin_isolated.js] [browser_bug1369357_site_specific_zoom_level.js] diff --git a/browser/components/resistfingerprinting/test/browser/browser_performanceAPI.js b/browser/components/resistfingerprinting/test/browser/browser_performanceAPI.js index 96afa0f745e2..e11d58d8b2a3 100644 --- a/browser/components/resistfingerprinting/test/browser/browser_performanceAPI.js +++ b/browser/components/resistfingerprinting/test/browser/browser_performanceAPI.js @@ -80,6 +80,7 @@ let isRounded = (x, expectedPrecision) => { let setupTest = async function( resistFingerprinting, reduceTimerPrecision, + crossOriginIsolated, expectedPrecision, runTests, workerCall @@ -92,14 +93,19 @@ let setupTest = async function( "privacy.resistFingerprinting.reduceTimerPrecision.microseconds", expectedPrecision * 1000, ], + ["browser.tabs.remote.useCrossOriginOpenerPolicy", crossOriginIsolated], + ["browser.tabs.remote.useCrossOriginEmbedderPolicy", crossOriginIsolated], + ["browser.tabs.documentchannel", crossOriginIsolated], ], }); + let url = crossOriginIsolated + ? `https://example.com/browser/browser/components/resistfingerprinting` + + `/test/browser/coop_header.sjs?crossOriginIsolated=${crossOriginIsolated}` + : TEST_PATH + "file_dummy.html"; + let win = await BrowserTestUtils.openNewBrowserWindow(); - let tab = await BrowserTestUtils.openNewForegroundTab( - win.gBrowser, - TEST_PATH + "file_dummy.html" - ); + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url); // No matter what we set the precision to, if we're in ResistFingerprinting mode // we use the larger of the precision pref and the constant 100ms @@ -118,6 +124,15 @@ let setupTest = async function( ], runTests ); + + if (crossOriginIsolated) { + let remoteType = tab.linkedBrowser.remoteType; + ok( + remoteType.startsWith(E10SUtils.WEB_REMOTE_COOP_COEP_TYPE_PREFIX), + `${remoteType} expected to be coop+coep` + ); + } + await BrowserTestUtils.closeWindow(win); }; // ================================================================================================ @@ -181,10 +196,13 @@ add_task(async function runRPTests() { ); }; - await setupTest(true, true, 200, runTests); - await setupTest(true, true, 100, runTests); - await setupTest(true, false, 13, runTests); - await setupTest(true, false, 0.13, runTests); + await setupTest(true, true, false, 200, runTests); + await setupTest(true, true, false, 100, runTests); + await setupTest(true, false, false, 13, runTests); + await setupTest(true, false, false, 0.13, runTests); + await setupTest(true, true, true, 100, runTests); + await setupTest(true, false, true, 13, runTests); + await setupTest(true, false, true, 0.13, runTests); }); // ================================================================================================ @@ -264,9 +282,10 @@ add_task(async function runRTPTests() { content.performance.clearResourceTimings(); }; - await setupTest(false, true, 100, runTests); - await setupTest(false, true, 13, runTests); - await setupTest(false, true, 0.13, runTests); + await setupTest(false, true, false, 100, runTests); + await setupTest(false, true, false, 13, runTests); + await setupTest(false, true, false, 0.13, runTests); + await setupTest(false, true, true, 0.005, runTests); }); // ================================================================================================ @@ -293,13 +312,13 @@ let runWorkerTest = async function(data) { }; add_task(async function runRPTestsForWorker() { - await setupTest(true, true, 100, runWorkerTest, "runRPTests"); - await setupTest(true, false, 13, runWorkerTest, "runRPTests"); - await setupTest(true, true, 0.13, runWorkerTest, "runRPTests"); + await setupTest(true, true, false, 100, runWorkerTest, "runRPTests"); + await setupTest(true, false, false, 13, runWorkerTest, "runRPTests"); + await setupTest(true, true, false, 0.13, runWorkerTest, "runRPTests"); }); add_task(async function runRTPTestsForWorker() { - await setupTest(false, true, 100, runWorkerTest, "runRTPTests"); - await setupTest(false, true, 13, runWorkerTest, "runRTPTests"); - await setupTest(false, true, 0.13, runWorkerTest, "runRTPTests"); + await setupTest(false, true, false, 100, runWorkerTest, "runRTPTests"); + await setupTest(false, true, false, 13, runWorkerTest, "runRTPTests"); + await setupTest(false, true, false, 0.13, runWorkerTest, "runRTPTests"); }); diff --git a/browser/components/resistfingerprinting/test/browser/browser_timing_precision_cross_origin_isolated.js b/browser/components/resistfingerprinting/test/browser/browser_timing_precision_cross_origin_isolated.js new file mode 100644 index 000000000000..d93aa2af03a8 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_timing_precision_cross_origin_isolated.js @@ -0,0 +1,381 @@ +/** + * Bug 1621677 - A test for making sure getting the correct (higher) precision + * when it's cross-origin-isolated. + */ + +let isRounded = (x, expectedPrecision) => { + let rounded = Math.floor(x / expectedPrecision) * expectedPrecision; + // First we do the perfectly normal check that should work just fine + if (rounded === x || x === 0) { + return true; + } + + // When we're diving by non-whole numbers, we may not get perfect + // multiplication/division because of floating points. + // When dealing with ms since epoch, a double's precision is on the order + // of 1/5 of a microsecond, so we use a value a little higher than that as + // our epsilon. + // To be clear, this error is introduced in our re-calculation of 'rounded' + // above in JavaScript. + if (Math.abs(rounded - x + expectedPrecision) < 0.0005) { + return true; + } else if (Math.abs(rounded - x) < 0.0005) { + return true; + } + + // Then we handle the case where you're sub-millisecond and the timer is not + // We check that the timer is not sub-millisecond by assuming it is not if it + // returns an even number of milliseconds + if (expectedPrecision < 1 && Math.round(x) == x) { + if (Math.round(rounded) == x) { + return true; + } + } + + ok( + false, + "Looming Test Failure, Additional Debugging Info: Expected Precision: " + + expectedPrecision + + " Measured Value: " + + x + + " Rounded Vaue: " + + rounded + + " Fuzzy1: " + + Math.abs(rounded - x + expectedPrecision) + + " Fuzzy 2: " + + Math.abs(rounded - x) + ); + + return false; +}; + +let setupTest = async function( + resistFingerprinting, + reduceTimerPrecision, + crossOriginIsolated, + expectedPrecision, + runTests, + workerCall +) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", resistFingerprinting], + ["privacy.reduceTimerPrecision", reduceTimerPrecision], + [ + "privacy.resistFingerprinting.reduceTimerPrecision.microseconds", + expectedPrecision * 1000, + ], + ["browser.tabs.remote.useCrossOriginOpenerPolicy", crossOriginIsolated], + ["browser.tabs.remote.useCrossOriginEmbedderPolicy", crossOriginIsolated], + ["browser.tabs.documentchannel", crossOriginIsolated], + ], + }); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + `https://example.com/browser/browser/components/resistfingerprinting` + + `/test/browser/coop_header.sjs?crossOriginIsolated=${crossOriginIsolated}` + ); + + // No matter what we set the precision to, if we're in ResistFingerprinting mode + // we use the larger of the precision pref and the constant 100ms + if (resistFingerprinting) { + expectedPrecision = expectedPrecision < 100 ? 100 : expectedPrecision; + } + await SpecialPowers.spawn( + tab.linkedBrowser, + [ + { + precision: expectedPrecision, + isRoundedFunc: isRounded.toString(), + workerCall, + }, + ], + runTests + ); + + if (crossOriginIsolated) { + let remoteType = tab.linkedBrowser.remoteType; + ok( + remoteType.startsWith(E10SUtils.WEB_REMOTE_COOP_COEP_TYPE_PREFIX), + `${remoteType} expected to be coop+coep` + ); + } + + await BrowserTestUtils.closeWindow(win); +}; + +// ================================================================================================ +// ================================================================================================ +// This test case is mostly copy-and-paste from the forth test case in +// browser_performanceAPI.js. The main difference is this test case verifies +// performance API have more precsion when it's in cross-origin-isolated and +// cross-origin-isolated doesn't affect RFP. +let runWorkerTest = async function(data) { + let expectedPrecision = data.precision; + await new Promise(resolve => { + // eslint beleives that isrounded is available in this scope, but if you + // remove the assignment, you will see it is not + // eslint-disable-next-line + let isRounded = eval(data.isRoundedFunc); + + let worker = new content.Worker( + "coop_header.sjs?crossOriginIsolated=true&worker=true" + ); + + worker.postMessage({ + type: "runCmdAndGetResult", + cmd: `performance.timeOrigin`, + }); + + const expectedAllEntriesLength = data.workerCall == "runRPTests" ? 0 : 3; + const expectedResourceEntriesLength = + data.workerCall == "runRPTests" ? 0 : 2; + const expectedTestAndMarkEntriesLength = + data.workerCall == "runRPTests" ? 0 : 1; + + worker.onmessage = function(e) { + if (e.data.type == "result") { + if (e.data.resultOf == "performance.timeOrigin") { + ok( + isRounded(e.data.result, expectedPrecision), + `In a worker, for reduceTimerPrecision, performance.timeOrigin is` + + ` not correctly rounded: ${e.data.result}` + ); + + worker.postMessage({ + type: "runCmds", + cmds: [ + `performance.mark("Test");`, + `performance.mark("Test-End");`, + `performance.measure("Test-Measure", "Test", "Test-End");`, + ], + }); + } else if (e.data.resultOf == "entriesLength") { + is( + e.data.result, + expectedAllEntriesLength, + `In a worker, for reduceTimerPrecision: Incorrect number of ` + + `entries for performance.getEntries() for workers: ` + + `${e.data.result}` + ); + + if (data.workerCall == "runRTPTests") { + worker.postMessage({ + type: "getResult", + resultOf: "startTimeAndDuration", + num: 0, + }); + } else { + worker.postMessage({ + type: "getResult", + resultOf: "getEntriesByTypeLength", + }); + } + } else if (e.data.resultOf == "startTimeAndDuration") { + let index = e.data.result.index; + let startTime = e.data.result.startTime; + let duration = e.data.result.duration; + ok( + isRounded(startTime, expectedPrecision), + `In a worker, for reduceTimerPrecision(${expectedPrecision}, ` + + `performance.getEntries(${index}).startTime is not rounded: ` + + `${startTime}` + ); + ok( + isRounded(duration, expectedPrecision), + `In a worker, for reduceTimerPrecision(${expectedPrecision}), ` + + `performance.getEntries(${index}).duration is not rounded: ` + + `${duration}` + ); + + if (index < 2) { + worker.postMessage({ + type: "getResult", + resultOf: "startTimeAndDuration", + num: index + 1, + }); + } else { + worker.postMessage({ + type: "getResult", + resultOf: "getEntriesByTypeLength", + }); + } + } else if (e.data.resultOf == "entriesByTypeLength") { + is( + e.data.result.markLength, + expectedResourceEntriesLength, + `In a worker, for reduceTimerPrecision: Incorrect number of ` + + `entries for performance.getEntriesByType() for workers: ` + + `${e.data.result.resourceLength}` + ); + is( + e.data.result.testAndMarkLength, + expectedTestAndMarkEntriesLength, + `In a worker, for reduceTimerPrecision: Incorrect number of ` + + `entries for performance.getEntriesByName() for workers: ` + + `${e.data.result.testAndMarkLength}` + ); + + worker.terminate(); + resolve(); + } + } else { + ok(false, `Unknown message type got ${e.data.type}`); + worker.terminate(); + resolve(); + } + }; + }); +}; + +add_task(async function runTestsForWorker() { + // RFP + await setupTest(true, true, true, 100, runWorkerTest, "runRPTests"); + await setupTest(true, false, true, 13, runWorkerTest, "runRPTests"); + await setupTest(true, true, true, 0.13, runWorkerTest, "runRPTests"); + + // RTP + await setupTest(false, true, false, 0.13, runWorkerTest, "runRTPTests"); + await setupTest(false, true, true, 0.005, runWorkerTest, "runRTPTests"); +}); + +// ================================================================================================ +// ================================================================================================ +// This test case is mostly copy-and-paste from the test case for window in +// test_reduce_time_precision.html. The main difference is this test case +// verifies DOM API has more precsion when it's in cross-origin-isolated and +// cross-origin-isolated doesn't affect RFP. +add_task(async function runRTPTestDOM() { + let runTests = async function(data) { + let expectedPrecision = data.precision; + // eslint beleives that isrounded is available in this scope, but if you + // remove the assignment, you will see it is not + // eslint-disable-next-line + let isRounded = eval(data.isRoundedFunc); + + // Prepare for test of AudioContext.currentTime + // eslint-disable-next-line + let audioContext = new content.AudioContext(); + + // Known ways to generate time stamps, in milliseconds + const timeStampCodes = [ + "content.performance.now()", + "new content.Date().getTime()", + 'new content.Event("").timeStamp', + 'new content.File([], "").lastModified', + ]; + // These are measured in seconds, so we need to scale them up + var timeStampCodesDOM = timeStampCodes.concat([ + "audioContext.currentTime * 1000", + ]); + + // Loop through each timeStampCode, evaluate it, + // and check if it is rounded + for (let timeStampCode of timeStampCodesDOM) { + // eslint-disable-next-line + let timeStamp = eval(timeStampCode); + + // Audio Contexts increment in intervals of (minimum) 5.4ms, so we don't + // clamp/jitter if the timer precision is les than that. + // (Technically on MBPs they increment in intervals of 2.6 but this is + // non-standard and will eventually be changed. We don't cover this situation + // because we don't really support arbitrary Timer Precision, especially in + // the 2.6 - 5.4ms interval.) + if (timeStampCode.includes("audioContext") && expectedPrecision < 5.4) { + continue; + } + + ok( + isRounded(timeStamp, expectedPrecision), + `Should be rounded to nearest ${expectedPrecision} ms; saw ${timeStamp}` + ); + } + }; + + // RFP + await setupTest(true, true, true, 100, runTests); + await setupTest(true, false, true, 13, runTests); + await setupTest(true, false, true, 0.13, runTests); + + // RTP + await setupTest(false, true, false, 0.13, runTests); + await setupTest(false, true, true, 0.005, runTests); +}); + +// ================================================================================================ +// ================================================================================================ +// This test case is mostly copy-and-paste from the test case for worker in +// test_reduce_time_precision.html. The main difference is this test case +// verifies DOM API has more precsion when it's in cross-origin-isolated and +// cross-origin-isolated doesn't affect RFP. +let runWorkerTest1 = async function(data) { + let expectedPrecision = data.precision; + await new Promise(resolve => { + // eslint beleives that isrounded is available in this scope, but if you + // remove the assignment, you will see it is not + // eslint-disable-next-line + let isRounded = eval(data.isRoundedFunc); + + let worker = new content.Worker( + "coop_header.sjs?crossOriginIsolated=true&worker=true" + ); + + // Known ways to generate time stamps, in milliseconds + const timeStampCodes = [ + "performance.now()", + "new Date().getTime()", + 'new Event("").timeStamp', + 'new File([], "").lastModified', + ]; + + let promises = []; + for (let timeStampCode of timeStampCodes) { + promises.push( + new Promise(res => { + worker.postMessage({ + type: "runCmdAndGetResult", + cmd: timeStampCode, + }); + + worker.addEventListener("message", function(e) { + if (e.data.type == "result") { + if (e.data.resultOf == timeStampCode) { + ok( + isRounded(e.data.result, expectedPrecision), + `The result of ${e.data.resultOf} should be rounded to ` + + ` nearest ${expectedPrecision} ms in workers; saw ` + + `${e.data.result}` + ); + worker.removeEventListener("message", this); + res(); + } + + return; + } + + ok(false, `Unknown message type. Got ${e.data.type}`); + res(); + }); + }) + ); + } + + Promise.all(promises).then(_ => { + worker.terminate(); + resolve(); + }); + }); +}; + +add_task(async function runRTPTestsForWorker() { + // RFP + await setupTest(true, true, true, 100, runWorkerTest1); + await setupTest(true, false, true, 13, runWorkerTest1); + await setupTest(true, false, true, 0.13, runWorkerTest1); + + // RTP + await setupTest(false, true, false, 0.13, runWorkerTest1); + await setupTest(false, true, true, 0.005, runWorkerTest1); +}); diff --git a/browser/components/resistfingerprinting/test/browser/coop_header.sjs b/browser/components/resistfingerprinting/test/browser/coop_header.sjs new file mode 100644 index 000000000000..c02d9b8fd3c5 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/coop_header.sjs @@ -0,0 +1,77 @@ +const HTML_DATA = ` + + + + Dummy test page + + + +

Dummy test page

+ + + `; + +const WORKER = ` + self.onmessage = function(e) { + if (e.data.type == "runCmdAndGetResult") { + let result = eval(e.data.cmd); + + postMessage({ type: "result", resultOf: e.data.cmd, result: result }); + return; + } else if (e.data.type == "runCmds") { + for (let cmd of e.data.cmds) { + eval(cmd); + } + + postMessage({ type: "result", resultOf: "entriesLength", result: performance.getEntries().length }); + return; + } else if (e.data.type == "getResult") { + if (e.data.resultOf == "startTimeAndDuration") { + postMessage({ type: "result", + resultOf: "startTimeAndDuration", + result: { + index: e.data.num, + startTime: performance.getEntries()[e.data.num].startTime, + duration: performance.getEntries()[e.data.num].duration, + } + }); + return; + } else if (e.data.resultOf == "getEntriesByTypeLength") { + postMessage({ + type: "result", + resultOf: "entriesByTypeLength", + result: { + markLength: performance.getEntriesByType("mark").length, + resourceLength: performance.getEntriesByType("resource").length, + testAndMarkLength: performance.getEntriesByName("Test", "mark").length, + } + }); + return; + } + } + + postMessage({ type: "unexpectedMessageType", data: e.data.type }); + }; + `; + +function handleRequest(request, response) +{ + Components.utils.importGlobalProperties(["URLSearchParams"]); + let query = new URLSearchParams(request.queryString); + + if (query.get("crossOriginIsolated") === "true") { + response.setHeader("Cross-Origin-Opener-Policy", "same-origin", false); + response.setHeader("Cross-Origin-Embedder-Policy", "require-corp", false); + } + + if (query.get("worker")) { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "application/javascript"); + response.write(WORKER); + return; + } + + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + response.write(HTML_DATA); +}