Skip to content

Commit

Permalink
Bug 1775132 - Part 2: Allow background tasks to have non-ephemeral pe…
Browse files Browse the repository at this point in the history
…rsistent profiles. r=mossop

This is a little tricky since some behaviour must be in the profile
service while some is more naturally in the background tasks mechanism.
Essentially, some background tasks will have a consistent profile
location determined by their "profile prefix", which includes vendor,
installation hash, and background task name.  Right now, those tasks are
determined by their task name, but in the future we could make this more
flexible.

A few technical notes:

1.  I elected to not assume (or provide) a directory service provider
    in the relevant helper, mostly to ease future commits that might
    pull this functionality forward in the startup process.
2.  These background task profiles are placed in "Background Tasks
    Profiles" on relevant platforms (non-Unix and macOS), sibling to
    "Profiles".
3.  To avoid any possible vulnerability with predictable profile
    directories, these non-ephemeral background task profiles are
    salted.  An entry is placed in the `BackgroundTasksProfiles` section
    of `profiles.ini` mapping the profile prefix to the relative salted
    path.

Differential Revision: https://phabricator.services.mozilla.com/D149919
  • Loading branch information
ncalexan committed Jul 13, 2022
1 parent cf7750a commit ffc393b
Show file tree
Hide file tree
Showing 18 changed files with 561 additions and 49 deletions.
141 changes: 117 additions & 24 deletions toolkit/components/backgroundtasks/BackgroundTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
#include "nsXULAppAPI.h"
#include "prenv.h"
#include "prtime.h"
#include "SpecialSystemDirectory.h"

#include "mozilla/CmdLineAndEnvUtils.h"
#include "mozilla/LateWriteChecks.h"
Expand All @@ -25,7 +24,7 @@ namespace mozilla {
NS_IMPL_ISUPPORTS(BackgroundTasks, nsIBackgroundTasks);

BackgroundTasks::BackgroundTasks(Maybe<nsCString> aBackgroundTask)
: mBackgroundTask(std::move(aBackgroundTask)) {
: mBackgroundTask(std::move(aBackgroundTask)), mIsEphemeralProfile(false) {
// Log when a background task is created.
if (mBackgroundTask.isSome()) {
MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Info,
Expand Down Expand Up @@ -55,17 +54,29 @@ void BackgroundTasks::Shutdown() {
!EnvHasValue("MOZ_BACKGROUNDTASKS_NO_REMOVE_PROFILE")) {
AutoSuspendLateWriteChecks suspend;

// Log that the ephemeral profile is being removed.
if (MOZ_LOG_TEST(sBackgroundTasksLog, mozilla::LogLevel::Info)) {
nsAutoString path;
if (NS_SUCCEEDED(sSingleton->mProfD->GetPath(path))) {
MOZ_LOG(
sBackgroundTasksLog, mozilla::LogLevel::Info,
("Removing profile: %s", NS_LossyConvertUTF16toASCII(path).get()));
if (sSingleton->mIsEphemeralProfile) {
// Log that the ephemeral profile is being removed.
if (MOZ_LOG_TEST(sBackgroundTasksLog, mozilla::LogLevel::Info)) {
nsAutoString path;
if (NS_SUCCEEDED(sSingleton->mProfD->GetPath(path))) {
MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Info,
("Removing profile: %s",
NS_LossyConvertUTF16toASCII(path).get()));
}
}
}

Unused << sSingleton->mProfD->Remove(/* aRecursive */ true);
Unused << sSingleton->mProfD->Remove(/* aRecursive */ true);
} else {
// Log that the non-ephemeral profile is not being removed.
if (MOZ_LOG_TEST(sBackgroundTasksLog, mozilla::LogLevel::Debug)) {
nsAutoString path;
if (NS_SUCCEEDED(sSingleton->mProfD->GetPath(path))) {
MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Debug,
("Not removing non-ephemeral profile: %s",
NS_LossyConvertUTF16toASCII(path).get()));
}
}
}
}

sSingleton = nullptr;
Expand Down Expand Up @@ -105,13 +116,19 @@ bool BackgroundTasks::IsBackgroundTaskMode() {
}

nsresult BackgroundTasks::CreateEphemeralProfileDirectory(
const nsCString& aInstallHash, nsIFile** aFile) {
nsIFile* aRootDir, const nsCString& aProfilePrefix, nsIFile** aFile) {
if (!XRE_IsParentProcess()) {
return NS_ERROR_NOT_AVAILABLE;
}

nsresult rv =
GetSingleton()->CreateEphemeralProfileDirectoryImpl(aInstallHash, aFile);
Maybe<nsCString> task = GetBackgroundTasks();
sSingleton->mIsEphemeralProfile =
task.isSome() && IsEphemeralProfileTaskName(task.ref());

MOZ_RELEASE_ASSERT(sSingleton->mIsEphemeralProfile);

nsresult rv = sSingleton->CreateEphemeralProfileDirectoryImpl(
aRootDir, aProfilePrefix, aFile);

// Log whether the ephemeral profile was created.
if (NS_WARN_IF(NS_FAILED(rv))) {
Expand All @@ -131,8 +148,41 @@ nsresult BackgroundTasks::CreateEphemeralProfileDirectory(
return rv;
}

nsresult BackgroundTasks::CreateNonEphemeralProfileDirectory(
nsIFile* aRootDir, const nsCString& aProfilePrefix, nsIFile** aFile) {
if (!XRE_IsParentProcess()) {
return NS_ERROR_NOT_AVAILABLE;
}

Maybe<nsCString> task = GetBackgroundTasks();
sSingleton->mIsEphemeralProfile =
task.isSome() && IsEphemeralProfileTaskName(task.ref());

MOZ_RELEASE_ASSERT(!sSingleton->mIsEphemeralProfile);

nsresult rv = sSingleton->CreateNonEphemeralProfileDirectoryImpl(
aRootDir, aProfilePrefix, aFile);

// Log whether the non-ephemeral profile was created.
if (NS_WARN_IF(NS_FAILED(rv))) {
MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Warning,
("Failed to create non-ephemeral profile directory!"));
} else {
if (MOZ_LOG_TEST(sBackgroundTasksLog, mozilla::LogLevel::Info)) {
nsAutoString path;
if (aFile && *aFile && NS_SUCCEEDED((*aFile)->GetPath(path))) {
MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Info,
("Non-ephemeral profile directory existed or was created: %s",
NS_LossyConvertUTF16toASCII(path).get()));
}
}
}

return rv;
}

bool BackgroundTasks::IsEphemeralProfile() {
return sSingleton && sSingleton->mProfD;
return sSingleton && sSingleton->mIsEphemeralProfile && sSingleton->mProfD;
}

nsresult BackgroundTasks::RunBackgroundTask(nsICommandLine* aCmdLine) {
Expand All @@ -157,8 +207,18 @@ bool BackgroundTasks::IsUpdatingTaskName(const nsCString& aName) {
aName.EqualsLiteral("shouldprocessupdates");
}

nsresult BackgroundTasks::CreateEphemeralProfileDirectoryImpl(
const nsCString& aInstallHash, nsIFile** aFile) {
bool BackgroundTasks::IsEphemeralProfileTaskName(const nsCString& aName) {
return !(aName.EqualsLiteral("backgroundupdate") ||
aName.EqualsLiteral("not_ephemeral_profile"));
}

nsCString BackgroundTasks::GetProfilePrefix(const nsCString& aInstallHash) {
return nsPrintfCString("%sBackgroundTask-%s-%s", MOZ_APP_VENDOR,
aInstallHash.get(), GetBackgroundTasks().ref().get());
}

nsresult BackgroundTasks::CreateNonEphemeralProfileDirectoryImpl(
nsIFile* aRootDir, const nsCString& aProfilePrefix, nsIFile** aFile) {
if (mBackgroundTask.isNothing()) {
return NS_ERROR_NOT_AVAILABLE;
}
Expand All @@ -170,25 +230,58 @@ nsresult BackgroundTasks::CreateEphemeralProfileDirectoryImpl(
rv = mProfD->Clone(getter_AddRefs(file));
NS_ENSURE_SUCCESS(rv, rv);
} else {
// We don't have the directory service at this point.
rv = GetSpecialSystemDirectory(OS_TemporaryDirectory, getter_AddRefs(file));
file = aRootDir;

// The base path is
// /{UAppData}/Background Tasks
// Profiles/[salt].[vendor]BackgroundTask-[pathHash]-[taskName].
rv = file->AppendNative(aProfilePrefix);
NS_ENSURE_SUCCESS(rv, rv);

// Create the persistent profile directory if it does not exist.
bool exists;
rv = file->Exists(&exists);
NS_ENSURE_SUCCESS(rv, rv);

if (!exists) {
rv = file->Create(nsIFile::DIRECTORY_TYPE, 0700);
NS_ENSURE_SUCCESS(rv, rv);
}

rv = file->Clone(getter_AddRefs(mProfD));
NS_ENSURE_SUCCESS(rv, rv);
}

file.forget(aFile);
return NS_OK;
}

nsCString profilePrefix =
nsPrintfCString("%sBackgroundTask-%s-%s", MOZ_APP_VENDOR,
aInstallHash.get(), mBackgroundTask.ref().get());
nsresult BackgroundTasks::CreateEphemeralProfileDirectoryImpl(
nsIFile* aRootDir, const nsCString& aProfilePrefix, nsIFile** aFile) {
if (mBackgroundTask.isNothing()) {
return NS_ERROR_NOT_AVAILABLE;
}

nsresult rv;

nsCOMPtr<nsIFile> file;
if (mProfD) {
rv = mProfD->Clone(getter_AddRefs(file));
NS_ENSURE_SUCCESS(rv, rv);
} else {
file = aRootDir;

// Windows file cleanup is unreliable, so let's take a moment to clean up
// any prior background task profiles. We can continue if there was an error
// as creating a new ephemeral profile does not require cleaning up the old.
rv = RemoveStaleEphemeralProfileDirectories(file, profilePrefix);
rv = RemoveStaleEphemeralProfileDirectories(file, aProfilePrefix);
if (NS_WARN_IF(NS_FAILED(rv))) {
MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Warning,
("Error cleaning up stale ephemeral profile directories."));
}

// The base path is /tmp/[vendor]BackgroundTask-[pathHash]-[taskName].
rv = file->AppendNative(profilePrefix);
rv = file->AppendNative(aProfilePrefix);
NS_ENSURE_SUCCESS(rv, rv);

// Create a unique profile directory. This can fail if there are too many
Expand Down
34 changes: 29 additions & 5 deletions toolkit/components/backgroundtasks/BackgroundTasks.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ class BackgroundTasks final : public nsIBackgroundTasks {

static bool IsBackgroundTaskMode();

static nsresult CreateEphemeralProfileDirectory(const nsCString& aInstallHash,
nsIFile** aFile);
static nsresult CreateEphemeralProfileDirectory(
nsIFile* aRootDir, const nsCString& aProfilePrefix, nsIFile** aFile);

static nsresult CreateNonEphemeralProfileDirectory(
nsIFile* aRootDir, const nsCString& aProfilePrefix, nsIFile** aFile);

static bool IsEphemeralProfile();

Expand All @@ -60,21 +63,42 @@ class BackgroundTasks final : public nsIBackgroundTasks {
* Whether the given task name should process updates. Most tasks should not
* process updates to avoid Firefox being updated unexpectedly.
*
* Right now, we only process updates for the `backgroundupdate` task and the
* test-only `shouldprocessupdates` task.
* At the time of writing, we only process updates for the `backgroundupdate`
* task and the test-only `shouldprocessupdates` task.
*/
static bool IsUpdatingTaskName(const nsCString& aName);

/**
* Whether the given task name should use a temporary ephemeral
* profile. Most tasks should use a temporary ephemeral profile to
* allow concurrent task invocation and to simplify reasoning.
*
* At the time of writing, we use temporary ephemeral profiles for all tasks
* save the `backgroundupdate` task and the test-only `notephemeralprofile`
* task.
*/
static bool IsEphemeralProfileTaskName(const nsCString& aName);

/**
* Get the installation-specific profile prefix for the current task name and
* the given install hash.
*/
static nsCString GetProfilePrefix(const nsCString& aInstallHash);

protected:
static StaticRefPtr<BackgroundTasks> sSingleton;
static LazyLogModule sBackgroundTasksLog;

Maybe<nsCString> mBackgroundTask;
bool mIsEphemeralProfile;
nsCOMPtr<nsIFile> mProfD;

nsresult CreateEphemeralProfileDirectoryImpl(const nsCString& aInstallHash,
nsresult CreateEphemeralProfileDirectoryImpl(nsIFile* aRootDir,
const nsCString& aProfilePrefix,
nsIFile** aFile);

nsresult CreateNonEphemeralProfileDirectoryImpl(
nsIFile* aRootDir, const nsCString& aProfilePrefix, nsIFile** aFile);
/*
* Iterates children of `aRoot` and removes unlocked profiles matching
* `aPrefix`.
Expand Down
4 changes: 4 additions & 0 deletions toolkit/components/backgroundtasks/BackgroundTasksManager.jsm
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ class BackgroundTasksManager {
`${Services.appinfo.processID}: Running background task named '${name}'` +
` (with ${commandLine.length} arguments)`
);
lazy.log.debug(
`${Services.appinfo.processID}: Background task using profile` +
` '${Services.dirsvc.get("ProfD", Ci.nsIFile).path}'`
);

let exitCode = BackgroundTasksManager.EXIT_CODE.NOT_FOUND;
try {
Expand Down
6 changes: 4 additions & 2 deletions toolkit/components/backgroundtasks/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@ Background task mode supports using the JavaScript debugger and the Firefox Devt

## The background task mode runtime environment

### Background tasks run in ephemeral temporary profiles
### Most background tasks run in ephemeral temporary profiles

Background tasks are intended for periodic maintenance tasks, especially global/per-installation maintenance tasks. To allow background tasks to run at the same time as regular, headed Firefox browsing sessions, they run with an ephemeral temporary profile. This ephemeral profile is deleted when the background task main process exits. Every background task applies the preferences in [`backgroundtasks/defaults/backgroundtasks.js`](https://searchfox.org/mozilla-central/source/toolkit/components/backgroundtasks/defaults/backgroundtasks.js), but any additional preference configuration must be handled by the individual task. Over time, we anticipate a small library of background task functionality to grow to make it easier to lock and read specific prefs from the default browsing profile, to declare per-installation prefs, etc.
Background tasks are intended for periodic maintenance tasks, especially global/per-installation maintenance tasks. To allow background tasks to run at the same time as regular, headed Firefox browsing sessions, by default they run with an ephemeral temporary profile. This ephemeral profile is deleted when the background task main process exits. Every background task applies the preferences in [`backgroundtasks/defaults/backgroundtasks.js`](https://searchfox.org/mozilla-central/source/toolkit/components/backgroundtasks/defaults/backgroundtasks.js), but any additional preference configuration must be handled by the individual task. Over time, we anticipate a small library of background task functionality to grow to make it easier to lock and read specific prefs from the default browsing profile, to declare per-installation prefs, etc.

It is possible to run background tasks in non-emphemeral, i.e., persistent, profiles. See [Bug 1775132](https://bugzilla.mozilla.org/show_bug.cgi?id=1775132) for details.

### Background tasks limit the XPCOM instance graph by default

Expand Down
1 change: 1 addition & 0 deletions toolkit/components/backgroundtasks/moz.build
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ TESTING_JS_MODULES.backgroundtasks += [
"tests/BackgroundTask_file_exists.jsm",
"tests/BackgroundTask_jsdebugger.jsm",
"tests/BackgroundTask_localization.jsm",
"tests/BackgroundTask_not_ephemeral_profile.jsm",
"tests/BackgroundTask_policies.jsm",
"tests/BackgroundTask_profile_is_slim.jsm",
"tests/BackgroundTask_shouldnotprocessupdates.jsm",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,19 @@ async function runBackgroundTask(commandLine) {
let exitCode = Services.prefs.getIntPref(pref, 4);

console.error(
`runBackgroundTask: backgroundtask_specific_pref read pref '${pref}', exiting with exitCode ${exitCode}`
`runBackgroundTask: backgroundtask_specific_pref read pref '${pref}' with value ${exitCode}`
);

if (commandLine.length > 1) {
let newValue = Number.parseInt(commandLine.getArgument(1), 10);
console.error(
`runBackgroundTask: backgroundtask_specific_pref wrote pref '${pref}' with value ${newValue}`
);
Services.prefs.setIntPref(pref, newValue);
}

console.error(
`runBackgroundTask: backgroundtask_specific_pref exiting with exitCode ${exitCode}`
);

return exitCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
* 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/. */

var EXPORTED_SYMBOLS = ["runBackgroundTask"];

async function runBackgroundTask(commandLine) {
// Exact same behaviour as `backgroundtask_specific_pref`, but with
// a task name that is recognized as a task that should not use an
// ephemeral profile.
const taskModule = ChromeUtils.import(
"resource://testing-common/backgroundtasks/BackgroundTask_backgroundtask_specific_pref.jsm"
);
return taskModule.runBackgroundTask(commandLine);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
* vim: sw=4 ts=4 sts=4 et
* 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/. */

add_task(async function test_not_ephemeral_profile() {
// Get the pref, and then update its value. We ignore the initial return,
// since the persistent profile may already exist.
let exitCode = await do_backgroundtask("not_ephemeral_profile", {
extraArgs: ["test.backgroundtask_specific_pref.exitCode", "80"],
});

// Do it again, confirming that the profile is persistent.
exitCode = await do_backgroundtask("not_ephemeral_profile", {
extraArgs: ["test.backgroundtask_specific_pref.exitCode", "81"],
});
Assert.equal(80, exitCode);

exitCode = await do_backgroundtask("not_ephemeral_profile", {
extraArgs: ["test.backgroundtask_specific_pref.exitCode", "82"],
});
Assert.equal(81, exitCode);
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ support-files =
[test_backgroundtask_help.js]
[test_backgroundtask_localization.js]
[test_backgroundtask_locked_profile.js]
[test_backgroundtask_not_ephemeral_profile.js]
run-sequentially = Uses global profile directory `DefProfRt`
[test_backgroundtask_policies.js]
[test_backgroundtask_profile_is_slim.js]
[test_backgroundtask_profile_service_configuration.js]
Expand Down
8 changes: 5 additions & 3 deletions toolkit/mozapps/update/docs/BackgroundUpdates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,13 @@ The task then fishes configuration settings from the default profile, namely:
- The (legacy) Telemetry client ID, so that background update Telemetry
can be correlated with other Firefox Telemetry

The background task creates a temporary profile for itself to load, because a
The background task creates a distinct profile for itself to load, because a
profile must be present in order for most of the Firefox code that it relies on
to function.
to function. This distinct profile is non-ephemeral, i.e., persistent, but not
visible to users: see `bug 1775132
<https://bugzilla.mozilla.org/show_bug.cgi?id=1775132>`__

After setting up the temporary profile and reading all the configuration we need
After setting up this profile and reading all the configuration we need
into it, the regular
`UpdateService.jsm <https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/UpdateService.jsm>`__
check process is initiated. To the greatest extent possible, this process is
Expand Down
Loading

0 comments on commit ffc393b

Please sign in to comment.