tor-browser

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

commit ca1d56427df8e17b48b6c85cd74aaa410ba1bc86
parent f37ec715c24cbb7e207768ae7ef1b0e45ec07d6c
Author: Johannes Salas Schmidt <joschmidt@mozilla.com>
Date:   Mon, 24 Nov 2025 10:26:47 +0000

Bug 2001366 - restructure telemetry r=theidkamp,credential-management-reviewers,dimi

Differential Revision: https://phabricator.services.mozilla.com/D273430

Diffstat:
Mtoolkit/components/passwordmgr/LoginManagerRustMirror.sys.mjs | 158+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mtoolkit/components/passwordmgr/metrics.yaml | 121++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mtoolkit/components/passwordmgr/test/browser/browser_rust_mirror.js | 137+++++++++++++++++++++++++++++++++++++++++++------------------------------------
3 files changed, 229 insertions(+), 187 deletions(-)

diff --git a/toolkit/components/passwordmgr/LoginManagerRustMirror.sys.mjs b/toolkit/components/passwordmgr/LoginManagerRustMirror.sys.mjs @@ -7,8 +7,10 @@ ChromeUtils.defineESModuleGetters(lazy, { LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", }); +const rustMirrorTelemetryVersion = "2"; + /* Check if an url has punicode encoded hostname */ -function isPunycode(origin) { +function isPunycodeOrigin(origin) { try { return origin && new URL(origin).hostname.startsWith("xn--"); } catch (_) { @@ -16,64 +18,91 @@ function isPunycode(origin) { } } -function recordIncompatibleFormats(runId, operation, loginInfo) { - if (isPunycode(loginInfo.origin)) { - Glean.pwmgr.rustIncompatibleLoginFormat.record({ - run_id: runId, - issue: "nonAsciiOrigin", - operation, - }); - } - if (isPunycode(loginInfo.formActionOrigin)) { - Glean.pwmgr.rustIncompatibleLoginFormat.record({ - run_id: runId, - issue: "nonAsciiFormAction", - operation, - }); - } +/* Check if an origin is a single dot */ +function isSingleDot(str) { + return str === "."; +} - if (loginInfo.origin === ".") { - Glean.pwmgr.rustIncompatibleLoginFormat.record({ - run_id: runId, - issue: "dotOrigin", - operation, - }); - } +/* Check if an origin does not contain http protocol */ +function isHttpOrigin(str) { + return str.startsWith("https://") || str.startsWith("http://"); +} - if ( - loginInfo.username?.includes("\n") || - loginInfo.username?.includes("\r") - ) { - Glean.pwmgr.rustIncompatibleLoginFormat.record({ - run_id: runId, - issue: "usernameLineBreak", - operation, - }); - } +/* Check if a string contains line breaks */ +function containsLineBreaks(str) { + return str.includes("\n") || str.includes("\r"); +} + +/* Check if a string contains Nul string */ +function containsNul(str) { + return str.includes("\0"); } -function recordMirrorStatus(runId, operation, status, error = null) { +/* Normalize different errors */ +function normalizeRustStorageErrorMessage(error) { + const message = error?.message || String(error); + + return message + .replace(/^reason: /, "") + .replace(/^Invalid login: /, "") + .replace(/\{[0-9a-fA-F-]{36}\}/, "{UUID}"); +} + +function recordMirrorFailure(runId, operation, error, login = null) { + // lookup poisoned status const poisoned = Services.prefs.getBoolPref( "signon.rustMirror.poisoned", false ); - let errorMessage = ""; - if (error) { - errorMessage = error.message ?? String(error); + const data = { + metric_version: rustMirrorTelemetryVersion, + run_id: runId, + operation, + poisoned, + + error_message: normalizeRustStorageErrorMessage(error), + + is_deleted: false, + has_empty_password: false, + has_punycode_origin: false, + has_single_dot_origin: false, + has_non_http_origin: false, + has_username_line_break: false, + has_username_nul: false, + }; + + if (login) { + data.is_deleted = login.deleted; + data.has_empty_password = !login.password; + data.has_punycode_origin = isPunycodeOrigin( + login.origin || login.formActionOrigin + ); + data.has_single_dot_origin = isSingleDot( + login.origin || login.formActionOrigin + ); + data.has_non_http_origin = !isHttpOrigin( + login.origin || login.formActionOrigin + ); + data.has_username_line_break = containsLineBreaks(login.username); + data.has_username_nul = containsNul(login.username); } + Glean.pwmgr.rustWriteFailure.record(data); + + // set poisoned status on error + if (!poisoned) { + Services.prefs.setBoolPref("signon.rustMirror.poisoned", true); + } +} + +function recordMirrorStatus(runId, operation, status) { Glean.pwmgr.rustMirrorStatus.record({ + metric_version: rustMirrorTelemetryVersion, run_id: runId, operation, status, - error_message: errorMessage, - poisoned, }); - - if (status === "failure" && !poisoned) { - Services.prefs.setBoolPref("signon.rustMirror.poisoned", true); - } } function recordMigrationStatus( @@ -85,23 +114,13 @@ function recordMigrationStatus( const had_errors = numberOfLoginsMigrated < numberOfLoginsToMigrate; Glean.pwmgr.rustMigrationStatus.record({ + metric_version: rustMirrorTelemetryVersion, run_id: runId, duration_ms: duration, number_of_logins_to_migrate: numberOfLoginsToMigrate, number_of_logins_migrated: numberOfLoginsMigrated, had_errors, }); - - if (had_errors) { - Services.prefs.setBoolPref("signon.rustMirror.poisoned", true); - } -} - -function recordMigrationFailure(runId, error) { - Glean.pwmgr.rustMigrationFailure.record({ - run_id: runId, - error_message: error.message ?? String(error), - }); } export class LoginManagerRustMirror { @@ -166,7 +185,6 @@ export class LoginManagerRustMirror { this.#logger.log("Rust Mirror is enabled."); } catch (e) { this.#logger.error("Login migration failed", e); - recordMirrorStatus("migration-enable", "failure", e); } } @@ -201,19 +219,20 @@ export class LoginManagerRustMirror { const runId = Services.uuid.generateUUID(); let loginToModify; let newLoginData; + let status = "success"; switch (eventName) { case "addLogin": this.#logger.log(`adding login ${subject.guid}...`); try { - recordIncompatibleFormats(runId, "add", subject); await this.#rustStorage.addLoginsAsync([subject]); - recordMirrorStatus(runId, "add", "success"); this.#logger.log(`added login ${subject.guid}.`); } catch (e) { + status = "failure"; + recordMirrorFailure(runId, "add", e, subject); this.#logger.error("mirror-error:", e); - recordMirrorStatus(runId, "add", "failure", e); } + recordMirrorStatus(runId, "add", status); break; case "modifyLogin": @@ -221,38 +240,40 @@ export class LoginManagerRustMirror { newLoginData = subject.queryElementAt(1, Ci.nsILoginInfo); this.#logger.log(`modifying login ${loginToModify.guid}...`); try { - recordIncompatibleFormats(runId, "modify", newLoginData); this.#rustStorage.modifyLogin(loginToModify, newLoginData); - recordMirrorStatus(runId, "modify", "success"); this.#logger.log(`modified login ${loginToModify.guid}.`); } catch (e) { + status = "failure"; + recordMirrorFailure(runId, "modify", e, newLoginData); this.#logger.error("error: modifyLogin:", e); - recordMirrorStatus(runId, "modify", "failure", e); } + recordMirrorStatus(runId, "modify", status); break; case "removeLogin": this.#logger.log(`removing login ${subject.guid}...`); try { this.#rustStorage.removeLogin(subject); - recordMirrorStatus(runId, "remove", "success"); this.#logger.log(`removed login ${subject.guid}.`); } catch (e) { + status = "failure"; + recordMirrorFailure(runId, "remove", e, subject); this.#logger.error("error: removeLogin:", e); - recordMirrorStatus(runId, "remove", "failure", e); } + recordMirrorStatus(runId, "remove", status); break; case "removeAllLogins": this.#logger.log("removing all logins..."); try { this.#rustStorage.removeAllLogins(); - recordMirrorStatus(runId, "remove-all", "success"); this.#logger.log("removed all logins."); } catch (e) { + status = "failure"; this.#logger.error("error: removeAllLogins:", e); - recordMirrorStatus(runId, "remove-all", "failure", e); + recordMirrorFailure(runId, "remove-all", e); } + recordMirrorStatus(runId, "remove-all", status); break; // re-migrate on importLogins event @@ -318,14 +339,15 @@ export class LoginManagerRustMirror { Services.prefs.setBoolPref("signon.rustMirror.poisoned", false); - const logins = await this.#jsonStorage.getAllLogins(); + // get all logins; exclude deletion stubs + const logins = await this.#jsonStorage.getAllLogins(false); numberOfLoginsToMigrate = logins.length; const results = await this.#rustStorage.addLoginsAsync(logins, true); - for (const { error } of results) { + for (const [i, { error }] of results.entries()) { if (error) { this.#logger.error("error during migration:", error.message); - recordMigrationFailure(runId, error); + recordMirrorFailure(runId, "migration", error, logins[i]); } else { numberOfLoginsMigrated += 1; } diff --git a/toolkit/components/passwordmgr/metrics.yaml b/toolkit/components/passwordmgr/metrics.yaml @@ -850,6 +850,9 @@ pwmgr: - passwords-dev@mozilla.org expires: never extra_keys: + metric_version: + type: string + description: Identifier for rust mirror telemetry version run_id: type: string description: Unique identifier for the login change event @@ -861,98 +864,104 @@ pwmgr: description: > The result of the operation ("success" or "failure"). type: string - error_message: - description: > - The error message or exception string from the failure. - type: string - poisoned: - description: > - Whether there was a previous error, meaning that this event must be viewed with caution. - type: boolean - rust_migration_failure: + rust_migration_status: type: event description: > - Records errors during a migration run. + One record per rolling migration run, recording duration, total number + of logins migrated and whether there have been errors. bugs: - - https://bugzil.la/1991676 + - https://bugzil.la/1985800 data_reviews: - - https://bugzil.la/1991676 + - https://bugzil.la/1985800 data_sensitivity: - technical notification_emails: - passwords-dev@mozilla.org expires: never extra_keys: + metric_version: + type: string + description: Identifier for rust mirror telemetry version run_id: type: string description: Unique identifier for the migration run. - error_message: - type: string - description: The error message from the failed login entry. + number_of_logins_to_migrate: + description: > + Total number of logins to be migrated during this run. + type: quantity + number_of_logins_migrated: + description: > + Number of logins migrated during this run. + type: quantity + duration_ms: + description: > + Duration of the migration in milliseconds. + type: quantity + had_errors: + description: > + Whether the migration had errors. + type: boolean - rust_incompatible_login_format: + rust_write_failure: type: event description: > - Records whenever a login format is incompatible with the Rust password - store. Includes which field was affected and during which operation. + Records errors during mirror or migration run. bugs: - - https://bugzil.la/1981814 + - https://bugzil.la/1991676 data_reviews: - - https://bugzil.la/1981814 + - https://bugzil.la/1991676 data_sensitivity: - technical notification_emails: - passwords-dev@mozilla.org expires: never extra_keys: + metric_version: + type: string + description: Identifier for rust mirror telemetry version run_id: type: string description: Unique identifier for the login change event - issue: - description: > - Which kind of incompatible format (dotOrigin, nonAsciiOrigin, - nonAsciiFormAction, usernameLineBreak). - type: string operation: description: > - The operation being performed when the incompatible format was found - (e.g. addLogin, modifyLogin, removeLogin). + The type of operation that failed (e.g. "add", "modify", "remove", "remove-all", "migration", etc.). type: string - - rust_migration_status: - type: event - description: > - One record per rolling migration run, recording duration, total number - of logins migrated and whether there have been errors. - bugs: - - https://bugzil.la/1985800 - data_reviews: - - https://bugzil.la/1985800 - data_sensitivity: - - technical - notification_emails: - - passwords-dev@mozilla.org - expires: never - extra_keys: - run_id: + poisoned: + description: > + Whether there was a previous error, meaning that this event must be viewed with caution. + type: boolean + error_message: + description: > + The error message or exception string from the failure. type: string - description: Unique identifier for the migration run. - number_of_logins_to_migrate: + has_empty_password: description: > - Total number of logins to be migrated during this run. - type: quantity - number_of_logins_migrated: + Whether the login to add/modify/remove has an empty password + type: boolean + has_punycode_origin: description: > - Number of logins migrated during this run. - type: quantity - duration_ms: + Whether the login to add/modify/remove has a punycode origin or form_action_origin + type: boolean + has_single_dot_origin: description: > - Duration of the migration in milliseconds. - type: quantity - had_errors: + Whether the login to add/modify/remove has an origin or form_action_origin consisting of a single dot only + type: boolean + has_non_http_origin: description: > - Whether the migration had errors. + Whether the login to add/modify/remove has an origin or form_action_origin which does not start with http:// or https:// + type: boolean + has_username_line_break: + description: > + Whether the login to add/modify/remove has a username containing a line break + type: boolean + has_username_nul: + description: > + Whether the login to add/modify/remove has a username containing a Nul char + type: boolean + is_deleted: + description: > + Whether the login to add/modify/remove is a deletion stub type: boolean saving_enabled: diff --git a/toolkit/components/passwordmgr/test/browser/browser_rust_mirror.js b/toolkit/components/passwordmgr/test/browser/browser_rust_mirror.js @@ -434,24 +434,25 @@ add_task(async function test_rust_migration_failure_event() { }); await Services.logins.addLoginAsync(login_bad); + const waitForGleanEvent = BrowserTestUtils.waitForCondition( + () => Glean.pwmgr.rustWriteFailure.testGetValue()?.length == 1, + "event has been emitted" + ); + // Trigger migration await SpecialPowers.pushPrefEnv({ set: [["signon.rustMirror.enabled", true]], }); - await BrowserTestUtils.waitForCondition( - () => Glean.pwmgr.rustMigrationFailure.testGetValue()?.length == 1, - "event has been emitted" - ); + await waitForGleanEvent; - const [evt] = Glean.pwmgr.rustMigrationFailure.testGetValue(); + const [evt] = Glean.pwmgr.rustWriteFailure.testGetValue(); Assert.ok(evt.extra?.run_id, "event has a run_id"); Assert.equal( evt.extra?.error_message, "simulated migration failure", "event has the expected error message" ); - Assert.equal(evt.name, "rust_migration_failure", "event has correct name"); sinon.restore(); LoginTestUtils.clearData(); @@ -515,6 +516,11 @@ add_task(async function test_rust_mirror_addLogin_failure() { ], }); Services.fog.testResetFOG(); + const waitForGleanEvent = BrowserTestUtils.waitForCondition( + () => Glean.pwmgr.rustMirrorStatus.testGetValue()?.length == 1, + "rust_mirror_status event has been emitted" + ); + // This login will be accepted by JSON but rejected by Rust const badLogin = LoginTestUtils.testData.formLogin({ origin: ".", @@ -529,13 +535,9 @@ add_task(async function test_rust_mirror_addLogin_failure() { "single dot origin login saved to JSON" ); - await BrowserTestUtils.waitForCondition( - () => Glean.pwmgr.rustMirrorStatus.testGetValue()?.length == 1, - "event has been emitted" - ); + await waitForGleanEvent; const rustStorage = new LoginManagerRustStorage(); - const allLogins = await rustStorage.getAllLogins(); Assert.equal( allLogins.length, @@ -544,18 +546,34 @@ add_task(async function test_rust_mirror_addLogin_failure() { ); const [evt] = Glean.pwmgr.rustMirrorStatus.testGetValue(); - Assert.ok(evt, "event has been emitted"); - Assert.equal(evt.extra?.operation, "add", "event has operation"); - Assert.equal(evt.extra?.status, "failure", "event has status=failure"); Assert.equal( - evt.extra?.error_message, - "Invalid login: Login has illegal origin", + evt.extra?.operation, + "add", + "rust_mirror_status event has operation" + ); + Assert.equal( + evt.extra?.status, + "failure", + "rust_mirror_status event has status=failure" + ); + + const [evt1] = Glean.pwmgr.rustWriteFailure.testGetValue(); + Assert.equal( + evt1.extra?.error_message, + "Login has illegal origin", "event has error_message" ); - Assert.equal(evt.extra?.poisoned, "false", "event is not poisoned"); - Assert.equal(evt.name, "rust_mirror_status", "event has name"); + Assert.equal( + evt1.extra?.poisoned, + "false", + "rust_write_failure event is not poisoned" + ); // produce another failure + const waitForSecondGleanEvent = BrowserTestUtils.waitForCondition( + () => Glean.pwmgr.rustMirrorStatus.testGetValue()?.length == 2, + "two events have been emitted" + ); const badLogin2 = LoginTestUtils.testData.formLogin({ username: "another-bad-login", origin: ".", @@ -563,21 +581,22 @@ add_task(async function test_rust_mirror_addLogin_failure() { }); await Services.logins.addLoginAsync(badLogin2); - await BrowserTestUtils.waitForCondition( - () => Glean.pwmgr.rustMirrorStatus.testGetValue()?.length == 2, - "two events have been emitted" - ); + await waitForSecondGleanEvent; // eslint-disable-next-line no-unused-vars - const [_, evt2] = Glean.pwmgr.rustMirrorStatus.testGetValue(); - Assert.equal(evt2.extra?.poisoned, "true", "event is poisoned now"); + const [_, evt3] = Glean.pwmgr.rustWriteFailure.testGetValue(); + Assert.equal( + evt3.extra?.poisoned, + "true", + "rust_write_failure event is poisoned now" + ); LoginTestUtils.clearData(); await SpecialPowers.flushPrefEnv(); }); /* - * Tests that we collect telemetry if non-ASCII origins get punycoded. + * Tests that we store non-ASCII origins get punycoded. */ add_task(async function test_punycode_origin_metric() { // ensure mirror is on @@ -595,13 +614,15 @@ add_task(async function test_punycode_origin_metric() { password: "pass1", }); - await Services.logins.addLoginAsync(login); - - await BrowserTestUtils.waitForCondition( - () => Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue()?.length == 1, + const waitForGleanEvent = BrowserTestUtils.waitForCondition( + () => Glean.pwmgr.rustMirrorStatus.testGetValue()?.length == 1, "event has been emitted" ); + await Services.logins.addLoginAsync(login); + + await waitForGleanEvent; + const rustStorage = new LoginManagerRustStorage(); const allLogins = await rustStorage.getAllLogins(); @@ -613,18 +634,13 @@ add_task(async function test_punycode_origin_metric() { "origin has been punicoded on the Rust side" ); - const [evt] = Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue(); - Assert.equal(evt.extra?.issue, "nonAsciiOrigin"); - Assert.equal(evt.extra?.operation, "add"); - Assert.ok("run_id" in evt.extra); - LoginTestUtils.clearData(); rustStorage.removeAllLogins(); await SpecialPowers.flushPrefEnv(); }); /* - * Tests that we collect telemetry if non-ASCII formorigins get punycoded. + * Tests that we store non-ASCII formorigins get punycoded. */ add_task(async function test_punycode_formActionOrigin_metric() { // ensure mirror is on @@ -642,15 +658,16 @@ add_task(async function test_punycode_formActionOrigin_metric() { password: "pass1", }); - await Services.logins.addLoginAsync(login); - - await BrowserTestUtils.waitForCondition( - () => Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue()?.length == 1, + const waitForGleanEvent = BrowserTestUtils.waitForCondition( + () => Glean.pwmgr.rustMirrorStatus.testGetValue()?.length == 1, "event has been emitted" ); - const rustStorage = new LoginManagerRustStorage(); + await Services.logins.addLoginAsync(login); + + await waitForGleanEvent; + const rustStorage = new LoginManagerRustStorage(); const allLogins = await rustStorage.getAllLogins(); Assert.equal(allLogins.length, 1, "punicode origin login saved to Rust"); const [rustLogin] = allLogins; @@ -660,11 +677,6 @@ add_task(async function test_punycode_formActionOrigin_metric() { "origin has been punicoded on the Rust side" ); - const [evt] = Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue(); - Assert.equal(evt.extra?.issue, "nonAsciiFormAction"); - Assert.equal(evt.extra?.operation, "add"); - Assert.ok("run_id" in evt.extra); - LoginTestUtils.clearData(); rustStorage.removeAllLogins(); await SpecialPowers.flushPrefEnv(); @@ -689,17 +701,15 @@ add_task(async function test_single_dot_in_origin() { password: "pass1", }); - await Services.logins.addLoginAsync(login); - - await BrowserTestUtils.waitForCondition( - () => Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue()?.length == 1, + const waitForGleanEvent = BrowserTestUtils.waitForCondition( + () => Glean.pwmgr.rustMirrorStatus.testGetValue()?.length == 1, "event has been emitted" ); + await Services.logins.addLoginAsync(login); + await waitForGleanEvent; - const [evt] = Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue(); - Assert.equal(evt.extra?.issue, "dotOrigin"); - Assert.equal(evt.extra?.operation, "add"); - Assert.ok("run_id" in evt.extra); + const [evt] = Glean.pwmgr.rustWriteFailure.testGetValue(); + Assert.ok(evt.extra?.has_single_dot_origin); LoginTestUtils.clearData(); await SpecialPowers.flushPrefEnv(); @@ -723,20 +733,21 @@ add_task(async function test_username_linebreak_metric() { password: "pass1", }); - await Services.logins.addLoginAsync(login); - - await BrowserTestUtils.waitForCondition( - () => Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue()?.length == 1, + const waitForGleanEvent = BrowserTestUtils.waitForCondition( + () => Glean.pwmgr.rustMirrorStatus.testGetValue()?.length == 1, "event has been emitted" ); - - const [evt] = Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue(); - Assert.equal(evt.extra?.issue, "usernameLineBreak"); - Assert.equal(evt.extra?.operation, "add"); - Assert.ok("run_id" in evt.extra); + await Services.logins.addLoginAsync(login); + await waitForGleanEvent; + const rustStorage = new LoginManagerRustStorage(); + const allLogins = await rustStorage.getAllLogins(); + Assert.equal( + allLogins.length, + 1, + "line break username origin login saved to Rust" + ); LoginTestUtils.clearData(); - const rustStorage = new LoginManagerRustStorage(); rustStorage.removeAllLogins(); await SpecialPowers.flushPrefEnv(); });