Bug 1474285: Implement dedicated profiles per install. r=froydnj, r=Gijs
authorDave Townsend <dtownsend@oxymoronical.com>
Fri, 25 Jan 2019 16:02:28 -0800
changeset 456132 58babf220962945b5ed8b4af5309d81eb8376571
parent 456131 d3b849c06ebc602a15d086f3778f92880ca4f0fd
child 456133 28e889fa43224adcc585bdccbe37a35307796a09
push id111592
push userdtownsend@mozilla.com
push dateThu, 31 Jan 2019 03:35:12 +0000
treeherdermozilla-inbound@86b7743d7a65 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfroydnj, Gijs
bugs1474285, 1518634, 1522751, 1518632, 1523024
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1474285: Implement dedicated profiles per install. r=froydnj, r=Gijs Uses a different profile depending on the install directory of the application. installs.ini is used to map a hash of the install directory to a profile directory. If no profile is marked as default for the current install we use a heuristic explained in the code to decide whether to use the profile that would have been used before this feature. The feature is disabled in snap builds where the install directory changes for every version of the app, but multiple instances cannot share profiles anyway. A boolean flag is used to turn on the feature because in a later patch we need to be able to turn off the behaviour at runtime. Includes code folded in from bug 1518634, bug 1522751, bug 1518632 and bug 1523024.
browser/components/preferences/in-content/main.js
browser/components/preferences/in-content/main.xul
toolkit/profile/nsIToolkitProfileService.idl
toolkit/profile/nsToolkitProfileService.cpp
toolkit/profile/nsToolkitProfileService.h
toolkit/profile/xpcshell/head.js
toolkit/profile/xpcshell/test_claim_locked.js
toolkit/profile/xpcshell/test_clean.js
toolkit/profile/xpcshell/test_create_default.js
toolkit/profile/xpcshell/test_lock.js
toolkit/profile/xpcshell/test_new_default.js
toolkit/profile/xpcshell/test_previous_dedicated.js
toolkit/profile/xpcshell/test_profile_reset.js
toolkit/profile/xpcshell/test_remove_default.js
toolkit/profile/xpcshell/test_select_default.js
toolkit/profile/xpcshell/test_single_profile_selected.js
toolkit/profile/xpcshell/test_single_profile_unselected.js
toolkit/profile/xpcshell/test_snap.js
toolkit/profile/xpcshell/test_snap_empty.js
toolkit/profile/xpcshell/test_steal_inuse.js
toolkit/profile/xpcshell/test_update_selected_dedicated.js
toolkit/profile/xpcshell/test_update_unknown_dedicated.js
toolkit/profile/xpcshell/test_update_unselected_dedicated.js
toolkit/profile/xpcshell/test_use_dedicated.js
toolkit/profile/xpcshell/xpcshell.ini
toolkit/xre/ProfileReset.cpp
toolkit/xre/ProfileReset.h
toolkit/xre/nsAppRunner.cpp
toolkit/xre/nsXREDirProvider.cpp
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -405,36 +405,16 @@ var gMainPane = {
     if (navigator.platform.toLowerCase().startsWith("win")) {
       emeUIEnabled = emeUIEnabled && parseFloat(Services.sysinfo.get("version")) >= 6;
     }
     if (!emeUIEnabled) {
       // Don't want to rely on .hidden for the toplevel groupbox because
       // of the pane hiding/showing code potentially interfering:
       document.getElementById("drmGroup").setAttribute("style", "display: none !important");
     }
-
-    if (AppConstants.MOZ_DEV_EDITION) {
-      let uAppData = OS.Constants.Path.userApplicationDataDir;
-      let ignoreSeparateProfile = OS.Path.join(uAppData, "ignore-dev-edition-profile");
-
-      setEventListener("separateProfileMode", "command", gMainPane.separateProfileModeChange);
-      let separateProfileModeCheckbox = document.getElementById("separateProfileMode");
-      setEventListener("getStarted", "click", gMainPane.onGetStarted);
-
-      OS.File.stat(ignoreSeparateProfile).then(() => separateProfileModeCheckbox.checked = false,
-        () => separateProfileModeCheckbox.checked = true);
-
-      if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
-        document.getElementById("sync-dev-edition-root").hidden = false;
-        fxAccounts.getSignedInUser().then(data => {
-          document.getElementById("getStarted").selectedIndex = data ? 1 : 0;
-        }).catch(Cu.reportError);
-      }
-    }
-
     // Initialize the Firefox Updates section.
     let version = AppConstants.MOZ_APP_VERSION_DISPLAY;
 
     // Include the build ID if this is an "a#" (nightly) build
     if (/a\d+$/.test(version)) {
       let buildID = Services.appinfo.appBuildID;
       let year = buildID.slice(0, 4);
       let month = buildID.slice(4, 6);
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -25,30 +25,16 @@
 </hbox>
 
 <!-- Startup -->
 <groupbox id="startupGroup"
           data-category="paneGeneral"
           hidden="true">
   <label><html:h2 data-l10n-id="startup-header"/></label>
 
-#ifdef MOZ_DEV_EDITION
-  <vbox id="separateProfileBox">
-    <checkbox id="separateProfileMode"
-              data-l10n-id="separate-profile-mode"/>
-    <hbox id="sync-dev-edition-root" align="center" class="indent" hidden="true">
-      <label id="useFirefoxSync" data-l10n-id="use-firefox-sync"/>
-      <deck id="getStarted">
-        <label class="text-link" data-l10n-id="get-started-not-logged-in"/>
-        <label class="text-link" data-l10n-id="get-started-configured"/>
-      </deck>
-    </hbox>
-  </vbox>
-#endif
-
   <vbox id="startupPageBox">
     <checkbox id="browserRestoreSession"
               data-l10n-id="startup-restore-previous-session"/>
     <hbox class="indent">
       <checkbox id="browserRestoreSessionQuitWarning"
                 preference="browser.sessionstore.warnOnQuit"
                 disabled="true"
                 data-l10n-id="startup-restore-warn-on-quit"/>
--- a/toolkit/profile/nsIToolkitProfileService.idl
+++ b/toolkit/profile/nsIToolkitProfileService.idl
@@ -29,16 +29,22 @@ interface nsIToolkitProfileService : nsI
      * arguments or environment variables. Setting this will change the profile
      * used by default the next time the application is started.
      * Attempting to change the default may throw an exception on builds that do
      * not support changing the default profile, such as developer edition.
      */
     attribute nsIToolkitProfile defaultProfile;
 
     /**
+     * True if during startup a new profile was created for this install instead
+     * of using the profile that was the default for older versions.
+     */
+    readonly attribute boolean createdAlternateProfile;
+
+    /**
      * Selects or creates a profile to use based on the profiles database, any
      * environment variables and any command line arguments. Will not create
      * a profile if aIsResetting is true. The profile is selected based on this
      * order of preference:
      * * Environment variables (set when restarting the application).
      * * --profile command line argument.
      * * --createprofile command line argument (this also causes the app to exit).
      * * -p command line argument.
--- a/toolkit/profile/nsToolkitProfileService.cpp
+++ b/toolkit/profile/nsToolkitProfileService.cpp
@@ -24,30 +24,32 @@
 #include "nsIFile.h"
 
 #ifdef XP_MACOSX
 #  include <CoreFoundation/CoreFoundation.h>
 #  include "nsILocalFileMac.h"
 #endif
 
 #include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceDefs.h"
 #include "nsNetCID.h"
 #include "nsXULAppAPI.h"
 #include "nsThreadUtils.h"
 
 #include "nsIRunnable.h"
-#include "nsINIParser.h"
 #include "nsXREDirProvider.h"
 #include "nsAppRunner.h"
 #include "nsString.h"
 #include "nsReadableUtils.h"
 #include "nsNativeCharsetUtils.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Sprintf.h"
 #include "nsPrintfCString.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIToolkitShellService.h"
 
 using namespace mozilla;
 
 #define DEV_EDITION_NAME "dev-edition-default"
 #define DEFAULT_NAME "default"
 
 nsToolkitProfile::nsToolkitProfile(const nsACString& aName, nsIFile* aRootDir,
                                    nsIFile* aLocalDir, nsToolkitProfile* aPrev)
@@ -167,16 +169,19 @@ nsresult nsToolkitProfile::RemoveInterna
   mNext = nullptr;
 
   if (nsToolkitProfileService::gService->mNormalDefault == this) {
     nsToolkitProfileService::gService->mNormalDefault = nullptr;
   }
   if (nsToolkitProfileService::gService->mDevEditionDefault == this) {
     nsToolkitProfileService::gService->mDevEditionDefault = nullptr;
   }
+  if (nsToolkitProfileService::gService->mDedicatedProfile == this) {
+    nsToolkitProfileService::gService->SetDefaultProfile(nullptr);
+  }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsToolkitProfile::Remove(bool removeFiles) {
   return RemoveInternal(removeFiles, false /* in background */);
 }
@@ -286,76 +291,268 @@ nsToolkitProfileLock::~nsToolkitProfileL
 nsToolkitProfileService* nsToolkitProfileService::gService = nullptr;
 
 NS_IMPL_ISUPPORTS(nsToolkitProfileService, nsIToolkitProfileService)
 
 nsToolkitProfileService::nsToolkitProfileService()
     : mStartupProfileSelected(false),
       mStartWithLast(true),
       mIsFirstRun(true),
-      mUseDevEditionProfile(false) {
+      mUseDevEditionProfile(false),
+#ifdef MOZ_DEDICATED_PROFILES
+      mUseDedicatedProfile(!IsSnapEnvironment()),
+#else
+      mUseDedicatedProfile(false),
+#endif
+      mCreatedAlternateProfile(false) {
 #ifdef MOZ_DEV_EDITION
   mUseDevEditionProfile = true;
 #endif
   gService = this;
 }
 
 nsToolkitProfileService::~nsToolkitProfileService() { gService = nullptr; }
 
+// Tests whether the passed profile was last used by this install.
+bool nsToolkitProfileService::IsProfileForCurrentInstall(
+    nsIToolkitProfile* aProfile) {
+  nsCOMPtr<nsIFile> profileDir;
+  nsresult rv = aProfile->GetRootDir(getter_AddRefs(profileDir));
+  NS_ENSURE_SUCCESS(rv, false);
+
+  nsCOMPtr<nsIFile> compatFile;
+  rv = profileDir->Clone(getter_AddRefs(compatFile));
+  NS_ENSURE_SUCCESS(rv, false);
+
+  rv = compatFile->Append(NS_LITERAL_STRING("compatibility.ini"));
+  NS_ENSURE_SUCCESS(rv, false);
+
+  nsINIParser compatData;
+  rv = compatData.Init(compatFile);
+  // If the file is missing then either this is an empty profile (likely
+  // generated by bug 1518591) or it is from an ancient version. We'll opt to
+  // use it in this case.
+  if (NS_FAILED(rv)) {
+    return true;
+  }
+
+  /**
+   * In xpcshell gDirServiceProvider doesn't have all the correct directories
+   * set so using NS_GetSpecialDirectory works better there. But in a normal
+   * app launch the component registry isn't initialized so
+   * NS_GetSpecialDirectory doesn't work. So we have to use two different
+   * paths to support testing.
+   */
+  nsCOMPtr<nsIFile> currentGreDir;
+  rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(currentGreDir));
+  if (rv == NS_ERROR_NOT_INITIALIZED) {
+    currentGreDir = gDirServiceProvider->GetGREDir();
+    MOZ_ASSERT(currentGreDir, "No GRE dir found.");
+  } else if (NS_FAILED(rv)) {
+    return false;
+  }
+
+  nsCString greDirPath;
+  rv = compatData.GetString("Compatibility", "LastPlatformDir", greDirPath);
+  // If this string is missing then this profile is from an ancient version.
+  // We'll opt to use it in this case.
+  if (NS_FAILED(rv)) {
+    return true;
+  }
+
+  nsCOMPtr<nsIFile> greDir;
+  rv = NS_NewNativeLocalFile(EmptyCString(), false, getter_AddRefs(greDir));
+  NS_ENSURE_SUCCESS(rv, false);
+
+  rv = greDir->SetPersistentDescriptor(greDirPath);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  bool equal;
+  rv = greDir->Equals(currentGreDir, &equal);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  return equal;
+}
+
+/**
+ * Used the first time an install with dedicated profile support runs. Decides
+ * whether to mark the passed profile as the default for this install.
+ *
+ * The goal is to reduce disruption but ideally end up with the OS default
+ * install using the old default profile.
+ *
+ * If the decision is to use the profile then it will be unassigned as the
+ * dedicated default for other installs.
+ *
+ * We won't attempt to use the profile if it was last used by a different
+ * install.
+ *
+ * If the profile is currently in use by an install that was either the OS
+ * default install or the profile has been explicitely chosen by some other
+ * means then we won't use it.
+ *
+ * Returns true if we chose to make the profile the new dedicated default.
+ */
+bool nsToolkitProfileService::MaybeMakeDefaultDedicatedProfile(
+    nsIToolkitProfile* aProfile) {
+  nsresult rv;
+
+  // If the profile was last used by a different install then we won't use it.
+  if (!IsProfileForCurrentInstall(aProfile)) {
+    return false;
+  }
+
+  nsCString descriptor;
+  rv = GetProfileDescriptor(aProfile, descriptor, nullptr);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  // Get a list of all the installs.
+  nsTArray<nsCString> installs = GetKnownInstalls();
+
+  // Cache the installs that use the profile.
+  nsTArray<nsCString> inUseInstalls;
+
+  // See if the profile is already in use by an install that hasn't locked it.
+  for (uint32_t i = 0; i < installs.Length(); i++) {
+    const nsCString& install = installs[i];
+
+    nsCString path;
+    rv = mInstallData.GetString(install.get(), "Default", path);
+    if (NS_FAILED(rv)) {
+      continue;
+    }
+
+    // Is this install using the profile we care about?
+    if (!descriptor.Equals(path)) {
+      continue;
+    }
+
+    // Is this profile locked to this other install?
+    nsCString isLocked;
+    rv = mInstallData.GetString(install.get(), "Locked", isLocked);
+    if (NS_SUCCEEDED(rv) && isLocked.Equals("1")) {
+      return false;
+    }
+
+    inUseInstalls.AppendElement(install);
+  }
+
+  // At this point we've decided to take the profile. Strip it from other
+  // installs.
+  for (uint32_t i = 0; i < inUseInstalls.Length(); i++) {
+    // Removing the default setting entirely will make the install go through
+    // the first run process again at startup and create itself a new profile.
+    mInstallData.DeleteString(inUseInstalls[i].get(), "Default");
+  }
+
+  // Set this as the default profile for this install.
+  SetDefaultProfile(aProfile);
+
+  bool isDefaultApp = false;
+
+  nsCOMPtr<nsIToolkitShellService> shell =
+      do_GetService(NS_TOOLKITSHELLSERVICE_CONTRACTID);
+  if (shell) {
+    rv = shell->IsDefaultApplication(&isDefaultApp);
+    // If the shell component is following XPCOM rules then this shouldn't be
+    // needed, but let's be safe.
+    if (NS_FAILED(rv)) {
+      isDefaultApp = false;
+    }
+  }
+
+  if (!isDefaultApp) {
+    // SetDefaultProfile will have locked this profile to this install so no
+    // other installs will steal it, but this was auto-selected so we want to
+    // unlock it so that the OS default install can take it at a later time.
+    mInstallData.DeleteString(mInstallHash.get(), "Locked");
+  }
+
+  // Persist the changes.
+  Flush();
+
+  return true;
+}
+
 nsresult nsToolkitProfileService::Init() {
   NS_ASSERTION(gDirServiceProvider, "No dirserviceprovider!");
   nsresult rv;
 
   rv = nsXREDirProvider::GetUserAppDataDirectory(getter_AddRefs(mAppData));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = nsXREDirProvider::GetUserLocalDataDirectory(getter_AddRefs(mTempData));
   NS_ENSURE_SUCCESS(rv, rv);
 
+  nsCString installProfilePath;
+
+  if (mUseDedicatedProfile) {
+    // Load the dedicated profiles database.
+    rv = mAppData->Clone(getter_AddRefs(mInstallFile));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = mInstallFile->AppendNative(NS_LITERAL_CSTRING("installs.ini"));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsString installHash;
+    rv = gDirServiceProvider->GetInstallHash(installHash);
+    NS_ENSURE_SUCCESS(rv, rv);
+    CopyUTF16toUTF8(installHash, mInstallHash);
+
+    rv = mInstallData.Init(mInstallFile);
+    if (NS_SUCCEEDED(rv)) {
+      // Try to find the descriptor for the default profile for this install.
+      rv = mInstallData.GetString(mInstallHash.get(), "Default",
+                                  installProfilePath);
+      // Not having a value means this install doesn't appear in installs.ini so
+      // this is the first run for this install.
+      mIsFirstRun = NS_FAILED(rv);
+    }
+  }
+
   rv = mAppData->Clone(getter_AddRefs(mListFile));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = mListFile->AppendNative(NS_LITERAL_CSTRING("profiles.ini"));
   NS_ENSURE_SUCCESS(rv, rv);
 
+  nsINIParser parser;
+
   bool exists;
   rv = mListFile->IsFile(&exists);
-  if (NS_FAILED(rv) || !exists) {
-    return NS_OK;
+  if (NS_SUCCEEDED(rv) && exists) {
+    rv = parser.Init(mListFile);
+    // Init does not fail on parsing errors, only on OOM/really unexpected
+    // conditions.
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
   }
 
-  int64_t size;
-  rv = mListFile->GetFileSize(&size);
-  if (NS_FAILED(rv) || !size) {
-    return NS_OK;
-  }
-
-  nsINIParser parser;
-  rv = parser.Init(mListFile);
-  // Init does not fail on parsing errors, only on OOM/really unexpected
-  // conditions.
-  if (NS_FAILED(rv)) return rv;
-
   nsAutoCString buffer;
   rv = parser.GetString("General", "StartWithLastProfile", buffer);
   if (NS_SUCCEEDED(rv) && buffer.EqualsLiteral("0")) mStartWithLast = false;
 
   nsToolkitProfile* currentProfile = nullptr;
 
 #ifdef MOZ_DEV_EDITION
-  nsCOMPtr<nsIFile> ignoreSeparateProfile;
-  rv = mAppData->Clone(getter_AddRefs(ignoreSeparateProfile));
-  if (NS_FAILED(rv)) return rv;
+  nsCOMPtr<nsIFile> ignoreDevEditionProfile;
+  rv = mAppData->Clone(getter_AddRefs(ignoreDevEditionProfile));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
 
-  rv = ignoreSeparateProfile->AppendNative(
+  rv = ignoreDevEditionProfile->AppendNative(
       NS_LITERAL_CSTRING("ignore-dev-edition-profile"));
-  if (NS_FAILED(rv)) return rv;
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
 
   bool shouldIgnoreSeparateProfile;
-  rv = ignoreSeparateProfile->Exists(&shouldIgnoreSeparateProfile);
+  rv = ignoreDevEditionProfile->Exists(&shouldIgnoreSeparateProfile);
   if (NS_FAILED(rv)) return rv;
 
   mUseDevEditionProfile = !shouldIgnoreSeparateProfile;
 #endif
 
   nsCOMPtr<nsIToolkitProfile> autoSelectProfile;
 
   unsigned int nonDevEditionProfiles = 0;
@@ -411,36 +608,45 @@ nsresult nsToolkitProfileService::Init()
         new nsToolkitProfile(name, rootDir, localDir, currentProfile);
     NS_ENSURE_TRUE(currentProfile, NS_ERROR_OUT_OF_MEMORY);
 
     rv = parser.GetString(profileID.get(), "Default", buffer);
     if (NS_SUCCEEDED(rv) && buffer.EqualsLiteral("1")) {
       mNormalDefault = currentProfile;
     }
 
+    // Is this the default profile for this install?
+    if (mUseDedicatedProfile && !mDedicatedProfile &&
+        installProfilePath.Equals(filePath)) {
+      // Found a profile for this install.
+      mDedicatedProfile = currentProfile;
+    }
+
     if (name.EqualsLiteral(DEV_EDITION_NAME)) {
       mDevEditionDefault = currentProfile;
     } else {
       nonDevEditionProfiles++;
       autoSelectProfile = currentProfile;
     }
   }
 
   // If there is only one non-dev-edition profile then mark it as the default.
   if (!mNormalDefault && nonDevEditionProfiles == 1) {
     mNormalDefault = autoSelectProfile;
   }
 
-  if (mUseDevEditionProfile) {
-    // When using the separate dev-edition profile not finding it means this is
-    // a first run.
-    mIsFirstRun = !mDevEditionDefault;
-  } else {
-    // If there are no normal profiles then this is a first run.
-    mIsFirstRun = nonDevEditionProfiles == 0;
+  if (!mUseDedicatedProfile) {
+    if (mUseDevEditionProfile) {
+      // When using the separate dev-edition profile not finding it means this
+      // is a first run.
+      mIsFirstRun = !mDevEditionDefault;
+    } else {
+      // If there are no normal profiles then this is a first run.
+      mIsFirstRun = nonDevEditionProfiles == 0;
+    }
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsToolkitProfileService::SetStartWithLastProfile(bool aValue) {
   if (mStartWithLast != aValue) {
@@ -483,36 +689,101 @@ nsToolkitProfileService::ProfileEnumerat
 NS_IMETHODIMP
 nsToolkitProfileService::GetCurrentProfile(nsIToolkitProfile** aResult) {
   NS_IF_ADDREF(*aResult = mCurrent);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsToolkitProfileService::GetDefaultProfile(nsIToolkitProfile** aResult) {
+  if (mUseDedicatedProfile) {
+    NS_IF_ADDREF(*aResult = mDedicatedProfile);
+    return NS_OK;
+  }
+
   if (mUseDevEditionProfile) {
     NS_IF_ADDREF(*aResult = mDevEditionDefault);
     return NS_OK;
   }
 
   NS_IF_ADDREF(*aResult = mNormalDefault);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsToolkitProfileService::SetDefaultProfile(nsIToolkitProfile* aProfile) {
+  if (mUseDedicatedProfile) {
+    if (mDedicatedProfile != aProfile) {
+      if (!aProfile) {
+        // Setting this to the empty string means no profile will be found on
+        // startup but we'll recognise that this install has been used
+        // previously.
+        mInstallData.SetString(mInstallHash.get(), "Default", "");
+      } else {
+        nsCString profilePath;
+        nsresult rv = GetProfileDescriptor(aProfile, profilePath, nullptr);
+        NS_ENSURE_SUCCESS(rv, rv);
+
+        mInstallData.SetString(mInstallHash.get(), "Default",
+                               profilePath.get());
+      }
+      mDedicatedProfile = aProfile;
+
+      // Some kind of choice has happened here, lock this profile to this
+      // install.
+      mInstallData.SetString(mInstallHash.get(), "Locked", "1");
+    }
+    return NS_OK;
+  }
+
   if (mUseDevEditionProfile && aProfile != mDevEditionDefault) {
     // The separate profile is hardcoded.
     return NS_ERROR_FAILURE;
   }
 
   mNormalDefault = aProfile;
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsToolkitProfileService::GetCreatedAlternateProfile(bool* aResult) {
+  *aResult = mCreatedAlternateProfile;
+  return NS_OK;
+}
+
+// Gets the profile root directory descriptor for storing in profiles.ini or
+// installs.ini.
+nsresult nsToolkitProfileService::GetProfileDescriptor(
+    nsIToolkitProfile* aProfile, nsACString& aDescriptor, bool* aIsRelative) {
+  nsCOMPtr<nsIFile> profileDir;
+  nsresult rv = aProfile->GetRootDir(getter_AddRefs(profileDir));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // if the profile dir is relative to appdir...
+  bool isRelative;
+  rv = mAppData->Contains(profileDir, &isRelative);
+
+  nsCString profilePath;
+  if (NS_SUCCEEDED(rv) && isRelative) {
+    // we use a relative descriptor
+    rv = profileDir->GetRelativeDescriptor(mAppData, profilePath);
+  } else {
+    // otherwise, a persistent descriptor
+    rv = profileDir->GetPersistentDescriptor(profilePath);
+  }
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  aDescriptor.Assign(profilePath);
+  if (aIsRelative) {
+    *aIsRelative = isRelative;
+  }
+
+  return NS_OK;
+}
+
 /**
  * An implementation of SelectStartupProfile callable from JavaScript via XPCOM.
  * See nsIToolkitProfileService.idl.
  */
 NS_IMETHODIMP
 nsToolkitProfileService::SelectStartupProfile(
     const nsTArray<nsCString>& aArgv, bool aIsResetting, nsIFile** aRootDir,
     nsIFile** aLocalDir, nsIToolkitProfile** aProfile, bool* aDidCreate) {
@@ -723,51 +994,85 @@ nsresult nsToolkitProfileService::Select
   if (mIsFirstRun) {
     if (aIsResetting) {
       // We don't want to create a fresh profile when we're attempting a
       // profile reset so just bail out here, the calling code will handle it.
       *aProfile = nullptr;
       return NS_OK;
     }
 
-    // create a default profile
+    if (mUseDedicatedProfile) {
+      // This is the first run of a dedicated profile install. We have to decide
+      // whether to use the default profile used by non-dedicated-profile
+      // installs or to create a new profile.
+
+      // Find what would have been the default profile for old installs.
+      nsCOMPtr<nsIToolkitProfile> profile = mNormalDefault;
+      if (mUseDevEditionProfile) {
+        profile = mDevEditionDefault;
+      }
+
+      if (profile && MaybeMakeDefaultDedicatedProfile(profile)) {
+        mCurrent = profile;
+        profile->GetRootDir(aRootDir);
+        profile->GetLocalDir(aLocalDir);
+        profile.forget(aProfile);
+        return NS_OK;
+      }
+
+      // We're going to create a new profile for this install. If there was a
+      // potential previous default to use then the user may be confused over
+      // why we're not using that anymore so set a flag for the front-end to use
+      // to notify the user about what has happened.
+      mCreatedAlternateProfile = !!profile;
+    }
+
+    // Create a new default profile
     nsAutoCString name;
-    if (mUseDevEditionProfile) {
+    if (mUseDedicatedProfile) {
+      name.AssignLiteral("default-" NS_STRINGIFY(MOZ_UPDATE_CHANNEL));
+    } else if (mUseDevEditionProfile) {
       name.AssignLiteral(DEV_EDITION_NAME);
     } else {
       name.AssignLiteral(DEFAULT_NAME);
     }
 
-    nsresult rv = CreateProfile(nullptr, name, getter_AddRefs(mCurrent));
+    rv = CreateProfile(nullptr, name, getter_AddRefs(mCurrent));
     if (NS_SUCCEEDED(rv)) {
-      if (mUseDevEditionProfile) {
+      if (mUseDedicatedProfile) {
+        SetDefaultProfile(mCurrent);
+      } else if (mUseDevEditionProfile) {
         mDevEditionDefault = mCurrent;
-
-        // If the only profile is the new dev-edition-profile then older
-        // versions may try to auto-select it. Create a default profile for them
-        // to use instead.
-        if (mFirst && !mFirst->mNext) {
-          CreateProfile(nullptr, NS_LITERAL_CSTRING(DEFAULT_NAME),
-                        getter_AddRefs(mNormalDefault));
-        }
       } else {
         mNormalDefault = mCurrent;
       }
+
+      // If there is only one profile and it isn't meant to be the profile that
+      // older versions of Firefox use then we must create a default profile
+      // for older versions of Firefox to avoid the existing profile being
+      // auto-selected.
+      if ((mUseDedicatedProfile || mUseDevEditionProfile) && mFirst &&
+          !mFirst->mNext) {
+        CreateProfile(nullptr, NS_LITERAL_CSTRING(DEFAULT_NAME),
+                      getter_AddRefs(mNormalDefault));
+      }
+
       Flush();
 
+      // Use the new profile.
       mCurrent->GetRootDir(aRootDir);
       mCurrent->GetLocalDir(aLocalDir);
       NS_ADDREF(*aProfile = mCurrent);
 
       *aDidCreate = true;
       return NS_OK;
     }
   }
 
-  // There are multiple profiles available.
+  // We've been told not to use the selected profile automatically.
   if (!mStartWithLast) {
     return NS_ERROR_SHOW_PROFILE_MANAGER;
   }
 
   GetDefaultProfile(getter_AddRefs(mCurrent));
 
   // None of the profiles was marked as default (generally only happens if the
   // user modifies profiles.ini manually). Let the user choose.
@@ -810,16 +1115,60 @@ nsresult nsToolkitProfileService::Create
   if (NS_FAILED(rv)) return rv;
 
   mCurrent = newProfile;
   newProfile.forget(aNewProfile);
 
   return NS_OK;
 }
 
+/**
+ * This is responsible for deleting the old profile, copying its name to the
+ * current profile and if the old profile was default making the new profile
+ * default as well.
+ */
+nsresult nsToolkitProfileService::ApplyResetProfile(
+    nsIToolkitProfile* aOldProfile) {
+  // If the old profile would have been the default for old installs then mark
+  // the new profile as such.
+  if (mNormalDefault == aOldProfile) {
+    mNormalDefault = mCurrent;
+  }
+
+  if (mUseDedicatedProfile && mDedicatedProfile == aOldProfile) {
+    bool wasLocked = false;
+    nsCString val;
+    if (NS_SUCCEEDED(
+            mInstallData.GetString(mInstallHash.get(), "Locked", val))) {
+      wasLocked = val.Equals("1");
+    }
+
+    SetDefaultProfile(mCurrent);
+
+    // Make the locked state match if necessary.
+    if (!wasLocked) {
+      mInstallData.DeleteString(mInstallHash.get(), "Locked");
+    }
+  }
+
+  nsCString name;
+  nsresult rv = aOldProfile->GetName(name);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = aOldProfile->Remove(false);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Switching the name will make this the default for dev-edition if
+  // appropriate.
+  rv = mCurrent->SetName(name);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return Flush();
+}
+
 NS_IMETHODIMP
 nsToolkitProfileService::GetProfileByName(const nsACString& aName,
                                           nsIToolkitProfile** aResult) {
   nsToolkitProfile* curP = mFirst;
   while (curP) {
     if (curP->mName.Equals(aName)) {
       NS_ADDREF(*aResult = curP);
       return NS_OK;
@@ -973,16 +1322,59 @@ nsToolkitProfileService::CreateProfile(n
   if (aName.Equals(DEV_EDITION_NAME)) {
     mDevEditionDefault = profile;
   }
 
   profile.forget(aResult);
   return NS_OK;
 }
 
+/**
+ * Snaps (https://snapcraft.io/) use a different installation directory for
+ * every version of an application. Since dedicated profiles uses the
+ * installation directory to determine which profile to use this would lead
+ * snap users getting a new profile on every application update.
+ *
+ * However the only way to have multiple installation of a snap is to install
+ * a new snap instance. Different snap instances have different user data
+ * directories and so already will not share profiles, in fact one instance
+ * will not even be able to see the other instance's profiles since
+ * profiles.ini will be stored in different places.
+ *
+ * So we can just disable dedicated profile support in this case and revert
+ * back to the old method of just having a single default profile and still
+ * get essentially the same benefits as dedicated profiles provides.
+ */
+bool nsToolkitProfileService::IsSnapEnvironment() {
+  return !!PR_GetEnv("SNAP_NAME");
+}
+
+struct FindInstallsClosure {
+  nsINIParser* installData;
+  nsTArray<nsCString>* installs;
+};
+
+static bool FindInstalls(const char* aSection, void* aClosure) {
+  FindInstallsClosure* closure = static_cast<FindInstallsClosure*>(aClosure);
+
+  nsCString install(aSection);
+  closure->installs->AppendElement(install);
+
+  return true;
+}
+
+nsTArray<nsCString> nsToolkitProfileService::GetKnownInstalls() {
+  nsTArray<nsCString> result;
+  FindInstallsClosure closure = {&mInstallData, &result};
+
+  mInstallData.GetSections(&FindInstalls, &closure);
+
+  return result;
+}
+
 nsresult nsToolkitProfileService::CreateTimesInternal(nsIFile* aProfileDir) {
   nsresult rv = NS_ERROR_FAILURE;
   nsCOMPtr<nsIFile> creationLog;
   rv = aProfileDir->Clone(getter_AddRefs(creationLog));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = creationLog->AppendNative(NS_LITERAL_CSTRING("times.json"));
   NS_ENSURE_SUCCESS(rv, rv);
@@ -1018,21 +1410,27 @@ nsToolkitProfileService::GetProfileCount
     profile = profile->mNext;
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsToolkitProfileService::Flush() {
+  nsresult rv;
+
+  if (mUseDedicatedProfile) {
+    rv = mInstallData.WriteToFile(mInstallFile);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
   // Errors during writing might cause unhappy semi-written files.
   // To avoid this, write the entire thing to a buffer, then write
   // that buffer to disk.
 
-  nsresult rv;
   uint32_t pCount = 0;
   nsToolkitProfile* cur;
 
   for (cur = mFirst; cur != nullptr; cur = cur->mNext) ++pCount;
 
   uint32_t length;
   const int bufsize = 100 + MAXPATHLEN * pCount;
   auto buffer = MakeUnique<char[]>(bufsize);
@@ -1045,27 +1443,19 @@ nsToolkitProfileService::Flush() {
                   "StartWithLastProfile=%s\n\n",
                   mStartWithLast ? "1" : "0");
 
   nsAutoCString path;
   cur = mFirst;
   pCount = 0;
 
   while (cur) {
-    // if the profile dir is relative to appdir...
     bool isRelative;
-    rv = mAppData->Contains(cur->mRootDir, &isRelative);
-    if (NS_SUCCEEDED(rv) && isRelative) {
-      // we use a relative descriptor
-      rv = cur->mRootDir->GetRelativeDescriptor(mAppData, path);
-    } else {
-      // otherwise, a persistent descriptor
-      rv = cur->mRootDir->GetPersistentDescriptor(path);
-      NS_ENSURE_SUCCESS(rv, rv);
-    }
+    nsresult rv = GetProfileDescriptor(cur, path, &isRelative);
+    NS_ENSURE_SUCCESS(rv, rv);
 
     pos +=
         snprintf(pos, end - pos,
                  "[Profile%u]\n"
                  "Name=%s\n"
                  "IsRelative=%s\n"
                  "Path=%s\n",
                  pCount, cur->mName.get(), isRelative ? "1" : "0", path.get());
--- a/toolkit/profile/nsToolkitProfileService.h
+++ b/toolkit/profile/nsToolkitProfileService.h
@@ -8,16 +8,17 @@
 #ifndef nsToolkitProfileService_h
 #define nsToolkitProfileService_h
 
 #include "nsIToolkitProfileService.h"
 #include "nsIToolkitProfile.h"
 #include "nsIFactory.h"
 #include "nsSimpleEnumerator.h"
 #include "nsProfileLock.h"
+#include "nsINIParser.h"
 
 class nsToolkitProfile final : public nsIToolkitProfile {
  public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSITOOLKITPROFILE
 
   friend class nsToolkitProfileService;
   RefPtr<nsToolkitProfile> mNext;
@@ -72,54 +73,79 @@ class nsToolkitProfileService final : pu
  public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSITOOLKITPROFILESERVICE
 
   nsresult SelectStartupProfile(int* aArgc, char* aArgv[], bool aIsResetting,
                                 nsIFile** aRootDir, nsIFile** aLocalDir,
                                 nsIToolkitProfile** aProfile, bool* aDidCreate);
   nsresult CreateResetProfile(nsIToolkitProfile** aNewProfile);
+  nsresult ApplyResetProfile(nsIToolkitProfile* aOldProfile);
 
  private:
   friend class nsToolkitProfile;
   friend class nsToolkitProfileFactory;
   friend nsresult NS_NewToolkitProfileService(nsIToolkitProfileService**);
 
   nsToolkitProfileService();
   ~nsToolkitProfileService();
 
   nsresult Init();
 
   nsresult CreateTimesInternal(nsIFile* profileDir);
   void GetProfileByDir(nsIFile* aRootDir, nsIFile* aLocalDir,
                        nsIToolkitProfile** aResult);
 
+  nsresult GetProfileDescriptor(nsIToolkitProfile* aProfile,
+                                nsACString& aDescriptor, bool* aIsRelative);
+  bool IsProfileForCurrentInstall(nsIToolkitProfile* aProfile);
+  void ClearProfileFromOtherInstalls(nsIToolkitProfile* aProfile);
+  bool MaybeMakeDefaultDedicatedProfile(nsIToolkitProfile* aProfile);
+  bool IsSnapEnvironment();
+
+  // Returns the known install hashes from the installs database. Modifying the
+  // installs database is safe while iterating the returned array.
+  nsTArray<nsCString> GetKnownInstalls();
+
   // Tracks whether SelectStartupProfile has been called.
   bool mStartupProfileSelected;
   // The first profile in a linked list of profiles loaded from profiles.ini.
   RefPtr<nsToolkitProfile> mFirst;
   // The profile selected for use at startup, if it exists in profiles.ini.
   nsCOMPtr<nsIToolkitProfile> mCurrent;
+  // The profile selected for this install in installs.ini.
+  nsCOMPtr<nsIToolkitProfile> mDedicatedProfile;
   // The default profile used by non-dev-edition builds.
   nsCOMPtr<nsIToolkitProfile> mNormalDefault;
   // The profile used if mUseDevEditionProfile is true (the default on
   // dev-edition builds).
   nsCOMPtr<nsIToolkitProfile> mDevEditionDefault;
   // The directory that holds profiles.ini and profile directories.
   nsCOMPtr<nsIFile> mAppData;
   // The directory that holds the cache files for profiles.
   nsCOMPtr<nsIFile> mTempData;
   // The location of profiles.ini.
   nsCOMPtr<nsIFile> mListFile;
+  // The location of installs.ini.
+  nsCOMPtr<nsIFile> mInstallFile;
+  // The data loaded from installs.ini.
+  nsINIParser mInstallData;
+  // The install hash for the currently running install.
+  nsCString mInstallHash;
   // Whether to start with the selected profile by default.
   bool mStartWithLast;
   // True if during startup it appeared that this is the first run.
   bool mIsFirstRun;
   // True if the default profile is the separate dev-edition-profile.
   bool mUseDevEditionProfile;
+  // True if this install should use a dedicated default profile.
+  const bool mUseDedicatedProfile;
+  // True if during startup no dedicated profile was already selected, an old
+  // default profile existed but was rejected so a new profile was created.
+  bool mCreatedAlternateProfile;
 
   static nsToolkitProfileService* gService;
 
   class ProfileEnumerator final : public nsSimpleEnumerator {
    public:
     NS_DECL_NSISIMPLEENUMERATOR
 
     const nsID& DefaultInterface() override {
--- a/toolkit/profile/xpcshell/head.js
+++ b/toolkit/profile/xpcshell/head.js
@@ -18,26 +18,68 @@ let gDataHomeLocal = gProfD.clone();
 gDataHomeLocal.append("local");
 gDataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
 
 let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].
                      getService(Ci.nsIXREDirProvider);
 xreDirProvider.setUserDataDirectory(gDataHome, false);
 xreDirProvider.setUserDataDirectory(gDataHomeLocal, true);
 
+let gIsDefaultApp = false;
+
+const ShellService = {
+  register() {
+    let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+
+    let factory = {
+      createInstance(outer, iid) {
+        if (outer != null) {
+          throw Cr.NS_ERROR_NO_AGGREGATION;
+        }
+
+        return ShellService.QueryInterface(iid);
+      },
+    };
+
+    registrar.registerFactory(this.ID, "ToolkitShellService", this.CONTRACT, factory);
+  },
+
+  isDefaultApplication() {
+    return gIsDefaultApp;
+  },
+
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIToolkitShellService]),
+  ID: Components.ID("{ce724e0c-ed70-41c9-ab31-1033b0b591be}"),
+  CONTRACT: "@mozilla.org/toolkit/shell-service;1",
+};
+
+ShellService.register();
+
+let gIsSnap = false;
+
+function simulateSnapEnvironment() {
+  let env = Cc["@mozilla.org/process/environment;1"].
+          getService(Ci.nsIEnvironment);
+  env.set("SNAP_NAME", "foo");
+
+  gIsSnap = true;
+}
+
 function getProfileService() {
   return Cc["@mozilla.org/toolkit/profile-service;1"].
          getService(Ci.nsIToolkitProfileService);
 }
 
 let PROFILE_DEFAULT = "default";
 if (AppConstants.MOZ_DEV_EDITION) {
   PROFILE_DEFAULT = "dev-edition-default";
 }
 
+let DEDICATED_NAME = `default-${AppConstants.MOZ_UPDATE_CHANNEL}`;
+
 /**
  * Creates a random profile path for use.
  */
 function makeRandomProfileDir(name) {
   let file = gDataHome.clone();
   file.append(name);
   file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
   return file;
@@ -84,16 +126,39 @@ function safeGet(ini, section, key) {
   try {
     return ini.getString(section, key);
   } catch (e) {
     return null;
   }
 }
 
 /**
+ * Writes a compatibility.ini file that marks the give profile directory as last
+ * used by the given install path.
+ */
+function writeCompatibilityIni(dir, appDir = FileUtils.getDir("CurProcD", []),
+                                    greDir = FileUtils.getDir("GreD", [])) {
+  let target = dir.clone();
+  target.append("compatibility.ini");
+
+  let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
+                getService(Ci.nsIINIParserFactory);
+  let ini = factory.createINIParser().QueryInterface(Ci.nsIINIParserWriter);
+
+  // The profile service doesn't care about these so just use fixed values
+  ini.setString("Compatibility", "LastVersion", "64.0a1_20180919123806/20180919123806");
+  ini.setString("Compatibility", "LastOSABI", "Darwin_x86_64-gcc3");
+
+  ini.setString("Compatibility", "LastPlatformDir", greDir.persistentDescriptor);
+  ini.setString("Compatibility", "LastAppDir", appDir.persistentDescriptor);
+
+  ini.writeFile(target);
+}
+
+/**
  * Writes a profiles.ini based on the passed profile data.
  * profileData should contain two properties, options and profiles.
  * options contains a single property, startWithLastProfile.
  * profiles is an array of profiles each containing name, path and default
  * properties.
  */
 function writeProfilesIni(profileData) {
   let target = gDataHome.clone();
@@ -130,17 +195,19 @@ function writeProfilesIni(profileData) {
  * because the order is irrelevant and it makes testing easier if we can make
  * that assumption.
  */
 function readProfilesIni() {
   let target = gDataHome.clone();
   target.append("profiles.ini");
 
   let profileData = {
-    options: {},
+    options: {
+      startWithLastProfile: true,
+    },
     profiles: [],
   };
 
   if (!target.exists()) {
     return profileData;
   }
 
   let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
@@ -174,45 +241,123 @@ function readProfilesIni() {
   }
 
   profileData.profiles.sort((a, b) => a.name.localeCompare(b.name));
 
   return profileData;
 }
 
 /**
+ * Writes an installs.ini based on the supplied data. Should be an object with
+ * keys for every installation hash each mapping to an object. Each object
+ * should have a default property for the relative path to the profile.
+ */
+function writeInstallsIni(installData) {
+  let target = gDataHome.clone();
+  target.append("installs.ini");
+
+  const { installs = {} } = installData;
+
+  let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
+                getService(Ci.nsIINIParserFactory);
+  let ini = factory.createINIParser(null).QueryInterface(Ci.nsIINIParserWriter);
+
+  for (let hash of Object.keys(installs)) {
+    ini.setString(hash, "Default", installs[hash].default);
+    if ("locked" in installs[hash]) {
+      ini.setString(hash, "Locked", installs[hash].locked ? "1" : "0");
+    }
+  }
+
+  ini.writeFile(target);
+}
+
+/**
+ * Reads installs.ini into a structure like that used in the above function.
+ */
+function readInstallsIni() {
+  let target = gDataHome.clone();
+  target.append("installs.ini");
+
+  let installData = {
+    installs: {},
+  };
+
+  if (!target.exists()) {
+    return installData;
+  }
+
+  let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
+                getService(Ci.nsIINIParserFactory);
+  let ini = factory.createINIParser(target);
+
+  let sections = ini.getSections();
+  while (sections.hasMore()) {
+    let hash = sections.getNext();
+    if (hash != "General") {
+      installData.installs[hash] = {
+        default: safeGet(ini, hash, "Default"),
+        locked: safeGet(ini, hash, "Locked") == 1,
+      };
+    }
+  }
+
+  return installData;
+}
+
+/**
  * Checks that the profile service seems to have the right data in it compared
  * to profile and install data structured as in the above functions.
  */
-function checkProfileService(profileData = readProfilesIni()) {
+function checkProfileService(profileData = readProfilesIni(), installData = readInstallsIni()) {
   let service = getProfileService();
 
   let serviceProfiles = Array.from(service.profiles);
 
   Assert.equal(serviceProfiles.length, profileData.profiles.length, "Should be the same number of profiles.");
 
   // Sort to make matching easy.
   serviceProfiles.sort((a, b) => a.name.localeCompare(b.name));
   profileData.profiles.sort((a, b) => a.name.localeCompare(b.name));
 
-  let defaultProfile = null;
+  let hash = xreDirProvider.getInstallHash();
+  let defaultPath = hash in installData.installs ? installData.installs[hash].default : null;
+  let dedicatedProfile = null;
+  let snapProfile = null;
 
   for (let i = 0; i < serviceProfiles.length; i++) {
     let serviceProfile = serviceProfiles[i];
     let expectedProfile = profileData.profiles[i];
 
     Assert.equal(serviceProfile.name, expectedProfile.name, "Should have the same name.");
 
     let expectedPath = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
     expectedPath.setRelativeDescriptor(gDataHome, expectedProfile.path);
     Assert.equal(serviceProfile.rootDir.path, expectedPath.path, "Should have the same path.");
 
+    if (expectedProfile.path == defaultPath) {
+      dedicatedProfile = serviceProfile;
+    }
+
     if (AppConstants.MOZ_DEV_EDITION) {
       if (expectedProfile.name == PROFILE_DEFAULT) {
-        defaultProfile = serviceProfile;
+        snapProfile = serviceProfile;
       }
     } else if (expectedProfile.default) {
-      defaultProfile = serviceProfile;
+      snapProfile = serviceProfile;
     }
   }
 
-  Assert.equal(service.defaultProfile, defaultProfile, "Should have seen the right profile as default.");
+  if (gIsSnap) {
+    Assert.equal(service.defaultProfile, snapProfile, "Should have seen the right profile selected.");
+  } else {
+    Assert.equal(service.defaultProfile, dedicatedProfile, "Should have seen the right profile selected.");
+  }
 }
+
+/**
+ * Asynchronously reads an nsIFile from disk.
+ */
+async function readFile(file) {
+  let decoder = new TextDecoder();
+  let data = await OS.File.read(file.path);
+  return decoder.decode(data);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_claim_locked.js
@@ -0,0 +1,46 @@
+/*
+ * Tests that an old-style default profile already locked to a different install
+ * isn't claimed by this install.
+ */
+
+add_task(async () => {
+  let defaultProfile = makeRandomProfileDir("default");
+
+  writeCompatibilityIni(defaultProfile);
+
+  writeProfilesIni({
+    profiles: [{
+      name: PROFILE_DEFAULT,
+      path: defaultProfile.leafName,
+      default: true,
+    }],
+  });
+
+  let hash = xreDirProvider.getInstallHash();
+  writeProfilesIni({
+    installs: {
+      other: {
+        default: defaultProfile.leafName,
+        locked: true,
+      },
+    },
+  });
+
+  let { profile: selectedProfile, didCreate } = selectStartupProfile();
+
+  let profileData = readProfilesIni();
+  let installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 2, "Should have the right number of profiles.");
+
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should be two known installs.");
+  Assert.notEqual(installData.installs[hash].default, defaultProfile.leafName, "Should not have marked the original default profile as the default for this install.");
+  Assert.ok(installData.installs[hash].locked, "Should have locked as we created this profile for this install.");
+
+  checkProfileService(profileData, installData);
+
+  Assert.ok(didCreate, "Should have created a new profile.");
+  Assert.ok(!selectedProfile.rootDir.equals(defaultProfile), "Should be using a different directory.");
+  Assert.equal(selectedProfile.name, DEDICATED_NAME);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_clean.js
@@ -0,0 +1,110 @@
+/*
+ * Tests from a clean state.
+ * Then does some testing that creating new profiles and marking them as
+ * selected works.
+ */
+
+add_task(async () => {
+  let service = getProfileService();
+
+  let target = gDataHome.clone();
+  target.append("profiles.ini");
+  Assert.ok(!target.exists(), "profiles.ini should not exist yet.");
+  target.leafName = "installs.ini";
+  Assert.ok(!target.exists(), "installs.ini should not exist yet.");
+
+  // Create a new profile to use.
+  let newProfile = service.createProfile(null, "dedicated");
+  service.flush();
+
+  let profileData = readProfilesIni();
+  let installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
+
+  let profile = profileData.profiles[0];
+  Assert.equal(profile.name, "dedicated", "Should have the right name.");
+  Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+  // The new profile hasn't been marked as the default yet!
+  Assert.equal(Object.keys(installData.installs).length, 0, "Should be no defaults for installs yet.");
+
+  checkProfileService(profileData, installData);
+
+  service.defaultProfile = newProfile;
+  service.flush();
+
+  profileData = readProfilesIni();
+  installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
+
+  profile = profileData.profiles[0];
+  Assert.equal(profile.name, "dedicated", "Should have the right name.");
+  Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+  let hash = xreDirProvider.getInstallHash();
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
+  Assert.equal(installData.installs[hash].default, profileData.profiles[0].path, "Should have marked the new profile as the default for this install.");
+
+  checkProfileService(profileData, installData);
+
+  let otherProfile = service.createProfile(null, "another");
+  service.defaultProfile = otherProfile;
+
+  service.flush();
+
+  profileData = readProfilesIni();
+  installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 2, "Should have the right number of profiles.");
+
+  profile = profileData.profiles[0];
+  Assert.equal(profile.name, "another", "Should have the right name.");
+  Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+  profile = profileData.profiles[1];
+  Assert.equal(profile.name, "dedicated", "Should have the right name.");
+  Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
+  Assert.equal(installData.installs[hash].default, profileData.profiles[0].path, "Should have marked the new profile as the default for this install.");
+
+  checkProfileService(profileData, installData);
+
+  newProfile.remove(true);
+  service.flush();
+
+  profileData = readProfilesIni();
+  installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
+
+  profile = profileData.profiles[0];
+  Assert.equal(profile.name, "another", "Should have the right name.");
+  Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
+  Assert.equal(installData.installs[hash].default, profileData.profiles[0].path, "Should have marked the new profile as the default for this install.");
+
+  checkProfileService(profileData, installData);
+
+  otherProfile.remove(true);
+  service.flush();
+
+  profileData = readProfilesIni();
+  installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 0, "Should have the right number of profiles.");
+
+  // We leave a reference to the missing profile to stop us trying to steal the
+  // old-style default profile on next startup.
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
+
+  checkProfileService(profileData, installData);
+});
--- a/toolkit/profile/xpcshell/test_create_default.js
+++ b/toolkit/profile/xpcshell/test_create_default.js
@@ -1,18 +1,31 @@
 /*
  * Tests that from an empty database a default profile is created.
  */
 
 add_task(async () => {
   let service = getProfileService();
   let { profile, didCreate } = selectStartupProfile();
-  checkProfileService();
+
+  let profileData = readProfilesIni();
+  let installData = readInstallsIni();
+  checkProfileService(profileData, installData);
 
   Assert.ok(didCreate, "Should have created a new profile.");
-  if (AppConstants.MOZ_DEV_EDITION) {
-    Assert.equal(service.profileCount, 2, "Should be two profiles.");
-  } else {
-    Assert.equal(service.profileCount, 1, "Should be only one profile.");
-    Assert.equal(profile, service.defaultProfile, "Should now be the default profile.");
-  }
-  Assert.equal(profile.name, PROFILE_DEFAULT, "Should have created a new profile with the right name.");
+  Assert.equal(profile, service.defaultProfile, "Should now be the default profile.");
+  Assert.equal(profile.name, DEDICATED_NAME, "Should have created a new profile with the right name.");
+  Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 2, "Should have the right number of profiles.");
+
+  profile = profileData.profiles[0];
+  Assert.equal(profile.name, "default", "Should have the right name.");
+  Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+  profile = profileData.profiles[1];
+  Assert.equal(profile.name, DEDICATED_NAME, "Should have the right name.");
+  Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+  let hash = xreDirProvider.getInstallHash();
+  Assert.ok(installData.installs[hash].locked, "Should have locked the profile");
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_lock.js
@@ -0,0 +1,43 @@
+/*
+ * Tests that when the default application claims the old-style default profile
+ * it locks it to itself.
+ */
+
+add_task(async () => {
+  gIsDefaultApp = true;
+  let defaultProfile = makeRandomProfileDir("default");
+
+  writeCompatibilityIni(defaultProfile);
+
+  writeProfilesIni({
+    profiles: [{
+      name: "default",
+      path: defaultProfile.leafName,
+      default: true,
+    }],
+  });
+
+  let { profile: selectedProfile, didCreate } = selectStartupProfile();
+
+  let hash = xreDirProvider.getInstallHash();
+  let profileData = readProfilesIni();
+  let installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
+
+  let profile = profileData.profiles[0];
+  Assert.equal(profile.name, "default", "Should have the right name.");
+  Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
+  Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
+  Assert.equal(installData.installs[hash].default, defaultProfile.leafName, "Should have marked the original default profile as the default for this install.");
+  Assert.ok(installData.installs[hash].locked, "Should have locked as we're the default app.");
+
+  checkProfileService(profileData, installData);
+
+  Assert.ok(!didCreate, "Should not have created a new profile.");
+  Assert.ok(selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
+  Assert.equal(selectedProfile.name, "default");
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_new_default.js
@@ -0,0 +1,73 @@
+/*
+ * Tests that an old-style default profile previously used by this build gets
+ * updated to a dedicated profile for this build.
+ */
+
+add_task(async () => {
+  let mydefaultProfile = makeRandomProfileDir("mydefault");
+  let defaultProfile = makeRandomProfileDir("default");
+  let devDefaultProfile = makeRandomProfileDir("devedition");
+
+  writeCompatibilityIni(mydefaultProfile);
+  writeCompatibilityIni(devDefaultProfile);
+
+  writeProfilesIni({
+    profiles: [{
+      name: "mydefault",
+      path: mydefaultProfile.leafName,
+      default: true,
+    }, {
+      name: "default",
+      path: defaultProfile.leafName,
+    }, {
+      name: "dev-edition-default",
+      path: devDefaultProfile.leafName,
+    }],
+  });
+
+  let service = getProfileService();
+  let { profile: selectedProfile, didCreate } = selectStartupProfile();
+
+  let hash = xreDirProvider.getInstallHash();
+  let profileData = readProfilesIni();
+  let installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 3, "Should have the right number of profiles.");
+
+  let profile = profileData.profiles[0];
+  Assert.equal(profile.name, "default", "Should have the right name.");
+  Assert.equal(profile.path, defaultProfile.leafName, "Should be the original non-default profile.");
+  Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+  profile = profileData.profiles[1];
+  Assert.equal(profile.name, "dev-edition-default", "Should have the right name.");
+  Assert.equal(profile.path, devDefaultProfile.leafName, "Should be the original dev default profile.");
+  Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+  profile = profileData.profiles[2];
+  Assert.equal(profile.name, "mydefault", "Should have the right name.");
+  Assert.equal(profile.path, mydefaultProfile.leafName, "Should be the original default profile.");
+  Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
+  if (AppConstants.MOZ_DEV_EDITION) {
+    Assert.equal(installData.installs[hash].default, devDefaultProfile.leafName, "Should have marked the original dev default profile as the default for this install.");
+  } else {
+    Assert.equal(installData.installs[hash].default, mydefaultProfile.leafName, "Should have marked the original default profile as the default for this install.");
+  }
+
+  Assert.ok(!installData.installs[hash].locked, "Should not be locked as we're not the default app.");
+
+  checkProfileService(profileData, installData);
+
+  Assert.ok(!didCreate, "Should not have created a new profile.");
+  if (AppConstants.MOZ_DEV_EDITION) {
+    Assert.ok(selectedProfile.rootDir.equals(devDefaultProfile), "Should be using the right directory.");
+    Assert.equal(selectedProfile.name, "dev-edition-default");
+  } else {
+    Assert.ok(selectedProfile.rootDir.equals(mydefaultProfile), "Should be using the right directory.");
+    Assert.equal(selectedProfile.name, "mydefault");
+  }
+  Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_previous_dedicated.js
@@ -0,0 +1,48 @@
+/**
+ * If install.ini lists a default profile for this build but that profile no
+ * longer exists don't try to steal the old-style default even if it was used
+ * by this build. It means this install has previously used dedicated profiles.
+ */
+
+add_task(async () => {
+  let hash = xreDirProvider.getInstallHash();
+  let defaultProfile = makeRandomProfileDir("default");
+
+  writeCompatibilityIni(defaultProfile);
+
+  writeProfilesIni({
+    profiles: [{
+      name: "default",
+      path: defaultProfile.leafName,
+      default: true,
+    }],
+  });
+
+  writeInstallsIni({
+    installs: {
+      [hash]: {
+        default: "foobar",
+      },
+    },
+  });
+
+  let service = getProfileService();
+  testStartsProfileManager();
+
+  let profileData = readProfilesIni();
+  let installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
+  Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
+
+  let profile = profileData.profiles[0];
+  Assert.equal(profile.name, "default", "Should have the right name.");
+  Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
+  Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+  // We keep the data here so we don't steal on the next reboot...
+  Assert.equal(Object.keys(installData.installs).length, 1, "Still list the broken reference.");
+
+  checkProfileService(profileData, installData);
+});
--- a/toolkit/profile/xpcshell/test_profile_reset.js
+++ b/toolkit/profile/xpcshell/test_profile_reset.js
@@ -6,9 +6,10 @@ add_task(async () => {
   let service = getProfileService();
 
   let { profile, didCreate } = selectStartupProfile([], true);
   checkProfileService();
 
   Assert.ok(!didCreate, "Should not have created a new profile.");
   Assert.ok(!profile, "Should not be a returned profile.");
   Assert.equal(service.profileCount, 0, "Still should be no profiles.");
+  Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_remove_default.js
@@ -0,0 +1,61 @@
+/**
+ * Tests that calling nsIToolkitProfile.remove on the default profile correctly
+ * removes the profile.
+ */
+
+add_task(async () => {
+  let hash = xreDirProvider.getInstallHash();
+  let defaultProfile = makeRandomProfileDir("default");
+
+  let profilesIni = {
+    profiles: [{
+      name: "default",
+      path: defaultProfile.leafName,
+      default: true,
+    }],
+  };
+  writeProfilesIni(profilesIni);
+
+  let installsIni = {
+    installs: {
+      [hash]: {
+        default: defaultProfile.leafName,
+      },
+    },
+  };
+  writeInstallsIni(installsIni);
+
+  let service = getProfileService();
+  checkProfileService(profilesIni, installsIni);
+
+  let { profile, didCreate } = selectStartupProfile();
+  Assert.ok(!didCreate, "Should have not created a new profile.");
+  Assert.equal(profile.name, "default", "Should have selected the default profile.");
+  Assert.equal(profile, service.defaultProfile, "Should have selected the default profile.");
+
+  checkProfileService(profilesIni, installsIni);
+
+  // In an actual run of Firefox we wouldn't be able to delete the profile in
+  // use because it would be locked. But we don't actually lock the profile in
+  // tests.
+  profile.remove(false);
+
+  Assert.ok(!service.defaultProfile, "Should no longer be a default profile.");
+  Assert.equal(profile, service.currentProfile, "Should still be the profile in use.");
+
+  // These are the modifications that should have been made.
+  profilesIni.profiles.pop();
+  installsIni.installs[hash].default = "";
+
+  checkProfileService(profilesIni, installsIni);
+
+  service.flush();
+
+  // And that should have flushed to disk correctly.
+  checkProfileService();
+
+  // checkProfileService doesn't differentiate between a blank default profile
+  // for the install and a missing install.
+  let installs = readInstallsIni();
+  Assert.equal(installs.installs[hash].default, "", "Should be a blank default profile.");
+});
--- a/toolkit/profile/xpcshell/test_select_default.js
+++ b/toolkit/profile/xpcshell/test_select_default.js
@@ -1,25 +1,34 @@
 /*
  * Tests that from a database of profiles the default profile is selected.
  */
 
 add_task(async () => {
+  let hash = xreDirProvider.getInstallHash();
+
   let profileData = {
     options: {
       startWithLastProfile: true,
     },
     profiles: [{
       name: "Profile1",
       path: "Path1",
     }, {
       name: "Profile3",
       path: "Path3",
     }],
   };
+  let installData = {
+    installs: {
+      [hash]: {
+        default: "Path2",
+      },
+    },
+  };
 
   if (AppConstants.MOZ_DEV_EDITION) {
     profileData.profiles.push({
       name: "default",
       path: "Path2",
       default: true,
     }, {
       name: PROFILE_DEFAULT,
@@ -29,18 +38,20 @@ add_task(async () => {
     profileData.profiles.push({
       name: PROFILE_DEFAULT,
       path: "Path2",
       default: true,
     });
   }
 
   writeProfilesIni(profileData);
+  writeInstallsIni(installData);
+
+  let { profile, didCreate } = selectStartupProfile();
 
   let service = getProfileService();
   checkProfileService(profileData);
 
-  let { profile, didCreate } = selectStartupProfile();
-
   Assert.ok(!didCreate, "Should not have created a new profile.");
   Assert.equal(profile, service.defaultProfile, "Should have returned the default profile.");
-  Assert.equal(profile.name, PROFILE_DEFAULT, "Should have selected the right profile");
+  Assert.equal(profile.name, "default", "Should have selected the right profile");
+  Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_single_profile_selected.js
@@ -0,0 +1,45 @@
+/*
+ * Previous versions of Firefox automatically used a single profile even if it
+ * wasn't marked as the default. So we should try to upgrade that one if it was
+ * last used by this build. This test checks the case where it was.
+ */
+
+add_task(async () => {
+  let defaultProfile = makeRandomProfileDir("default");
+
+  writeCompatibilityIni(defaultProfile);
+
+  writeProfilesIni({
+    profiles: [{
+      name: "default",
+      path: defaultProfile.leafName,
+      default: false,
+    }],
+  });
+
+  let { profile: selectedProfile, didCreate } = selectStartupProfile();
+
+  let hash = xreDirProvider.getInstallHash();
+  let profileData = readProfilesIni();
+  let installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
+
+  let profile = profileData.profiles[0];
+  Assert.equal(profile.name, "default", "Should have the right name.");
+  Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
+  Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
+  Assert.equal(installData.installs[hash].default, defaultProfile.leafName, "Should have marked the original default profile as the default for this install.");
+  Assert.ok(!installData.installs[hash].locked, "Should not have locked as we're not the default app.");
+
+  checkProfileService(profileData, installData);
+
+  Assert.ok(!didCreate, "Should not have created a new profile.");
+  let service = getProfileService();
+  Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
+  Assert.ok(selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
+  Assert.equal(selectedProfile.name, "default");
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_single_profile_unselected.js
@@ -0,0 +1,54 @@
+/*
+ * Previous versions of Firefox automatically used a single profile even if it
+ * wasn't marked as the default. So we should try to upgrade that one if it was
+ * last used by this build. This test checks the case where it wasn't.
+ */
+
+add_task(async () => {
+  let defaultProfile = makeRandomProfileDir("default");
+
+  // Just pretend this profile was last used by something in the profile dir.
+  let greDir = gProfD.clone();
+  greDir.append("app");
+  writeCompatibilityIni(defaultProfile, greDir, greDir);
+
+  writeProfilesIni({
+    profiles: [{
+      name: "default",
+      path: defaultProfile.leafName,
+      default: false,
+    }],
+  });
+
+  let service = getProfileService();
+
+  let profileData = readProfilesIni();
+  let installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
+
+  let profile = profileData.profiles[0];
+  Assert.equal(profile.name, "default", "Should have the right name.");
+  Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
+  Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+  Assert.equal(Object.keys(installData.installs).length, 0, "Should be no defaults for installs yet.");
+
+  checkProfileService(profileData, installData);
+
+  let { profile: selectedProfile, didCreate } = selectStartupProfile();
+  Assert.ok(didCreate, "Should have created a new profile.");
+  Assert.ok(service.createdAlternateProfile, "Should have created an alternate profile.");
+  Assert.ok(!selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
+  Assert.equal(selectedProfile.name, DEDICATED_NAME);
+
+  profileData = readProfilesIni();
+
+  profile = profileData.profiles[0];
+  Assert.equal(profile.name, "default", "Should have the right name.");
+  Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
+  Assert.ok(profile.default, "Should now be marked as the old-style default.");
+
+  checkProfileService(profileData);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_snap.js
@@ -0,0 +1,44 @@
+/*
+ * Tests that an old-style default profile not previously used by this build gets
+ * used in a snap environment.
+ */
+
+add_task(async () => {
+  let defaultProfile = makeRandomProfileDir("default");
+
+  // Just pretend this profile was last used by something in the profile dir.
+  let greDir = gProfD.clone();
+  greDir.append("app");
+  writeCompatibilityIni(defaultProfile, greDir, greDir);
+
+  writeProfilesIni({
+    profiles: [{
+      name: PROFILE_DEFAULT,
+      path: defaultProfile.leafName,
+      default: true,
+    }],
+  });
+
+  simulateSnapEnvironment();
+
+  let { profile: selectedProfile, didCreate } = selectStartupProfile();
+
+  let profileData = readProfilesIni();
+  let installsINI = gDataHome.clone();
+  installsINI.append("installs.ini");
+  Assert.ok(!installsINI.exists(), "Installs database should not have been created.");
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
+
+  let profile = profileData.profiles[0];
+  Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
+  Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
+  Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+  checkProfileService(profileData);
+
+  Assert.ok(!didCreate, "Should not have created a new profile.");
+  Assert.ok(selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
+  Assert.equal(selectedProfile.name, PROFILE_DEFAULT);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_snap_empty.js
@@ -0,0 +1,20 @@
+/*
+ * Tests that from a clean slate snap builds create an appropriate profile.
+ */
+
+add_task(async () => {
+  simulateSnapEnvironment();
+
+  let service = getProfileService();
+  let { profile, didCreate } = selectStartupProfile();
+
+  Assert.ok(didCreate, "Should have created a new profile.");
+  Assert.equal(profile.name, PROFILE_DEFAULT, "Should have used the normal name.");
+  if (AppConstants.MOZ_DEV_EDITION) {
+    Assert.equal(service.profileCount, 2, "Should be two profiles.");
+  } else {
+    Assert.equal(service.profileCount, 1, "Should be only one profile.");
+  }
+
+  checkProfileService();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_steal_inuse.js
@@ -0,0 +1,51 @@
+/*
+ * Tests that an old-style default profile previously used by this build but
+ * that has already been claimed by a different build gets stolen by this build.
+ */
+
+add_task(async () => {
+  let defaultProfile = makeRandomProfileDir("default");
+
+  writeCompatibilityIni(defaultProfile);
+
+  writeProfilesIni({
+    profiles: [{
+      name: PROFILE_DEFAULT,
+      path: defaultProfile.leafName,
+      default: true,
+    }],
+  });
+  writeInstallsIni({
+    installs: {
+      otherhash: {
+        default: defaultProfile.leafName,
+      },
+    },
+  });
+
+  let service = getProfileService();
+  let { profile: selectedProfile, didCreate } = selectStartupProfile();
+
+  let hash = xreDirProvider.getInstallHash();
+  let profileData = readProfilesIni();
+  let installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
+
+  let profile = profileData.profiles[0];
+  Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
+  Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
+  Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should only be one known installs.");
+  Assert.equal(installData.installs[hash].default, defaultProfile.leafName, "Should have taken the original default profile as the default for the current install.");
+  Assert.ok(!installData.installs[hash].locked, "Should not have locked as we're not the default app.");
+
+  checkProfileService(profileData, installData);
+
+  Assert.ok(!didCreate, "Should not have created a new profile.");
+  Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
+  Assert.ok(selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
+  Assert.equal(selectedProfile.name, PROFILE_DEFAULT);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_update_selected_dedicated.js
@@ -0,0 +1,44 @@
+/*
+ * Tests that an old-style default profile previously used by this build gets
+ * updated to a dedicated profile for this build.
+ */
+
+add_task(async () => {
+  let defaultProfile = makeRandomProfileDir("default");
+
+  writeCompatibilityIni(defaultProfile);
+
+  writeProfilesIni({
+    profiles: [{
+      name: PROFILE_DEFAULT,
+      path: defaultProfile.leafName,
+      default: true,
+    }],
+  });
+
+  let service = getProfileService();
+  let { profile: selectedProfile, didCreate } = selectStartupProfile();
+
+  let hash = xreDirProvider.getInstallHash();
+  let profileData = readProfilesIni();
+  let installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
+
+  let profile = profileData.profiles[0];
+  Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
+  Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
+  Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
+  Assert.equal(installData.installs[hash].default, defaultProfile.leafName, "Should have marked the original default profile as the default for this install.");
+  Assert.ok(!installData.installs[hash].locked, "Should not have locked as we're not the default app.");
+
+  checkProfileService(profileData, installData);
+
+  Assert.ok(!didCreate, "Should not have created a new profile.");
+  Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
+  Assert.ok(selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
+  Assert.equal(selectedProfile.name, PROFILE_DEFAULT);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_update_unknown_dedicated.js
@@ -0,0 +1,42 @@
+/*
+ * Tests that an old-style default profile not previously used by any build gets
+ * updated to a dedicated profile for this build.
+ */
+
+add_task(async () => {
+  let hash = xreDirProvider.getInstallHash();
+  let defaultProfile = makeRandomProfileDir("default");
+
+  writeProfilesIni({
+    profiles: [{
+      name: PROFILE_DEFAULT,
+      path: defaultProfile.leafName,
+      default: true,
+    }],
+  });
+
+  let service = getProfileService();
+  let { profile: selectedProfile, didCreate } = selectStartupProfile();
+
+  let profileData = readProfilesIni();
+  let installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
+
+  let profile = profileData.profiles[0];
+  Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
+  Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
+  Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should be a default for installs.");
+  Assert.equal(installData.installs[hash].default, profile.path, "Should have the right default profile.");
+  Assert.ok(!installData.installs[hash].locked, "Should not have locked as we didn't create this profile for this install.");
+
+  checkProfileService(profileData, installData);
+
+  Assert.ok(!didCreate, "Should not have created a new profile.");
+  Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
+  Assert.ok(selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
+  Assert.equal(selectedProfile.name, PROFILE_DEFAULT);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_update_unselected_dedicated.js
@@ -0,0 +1,56 @@
+/*
+ * Tests that an old-style default profile not previously used by this build gets
+ * ignored.
+ */
+
+add_task(async () => {
+  let hash = xreDirProvider.getInstallHash();
+  let defaultProfile = makeRandomProfileDir("default");
+
+  // Just pretend this profile was last used by something in the profile dir.
+  let greDir = gProfD.clone();
+  greDir.append("app");
+  writeCompatibilityIni(defaultProfile, greDir, greDir);
+
+  writeProfilesIni({
+    profiles: [{
+      name: PROFILE_DEFAULT,
+      path: defaultProfile.leafName,
+      default: true,
+    }],
+  });
+
+  let service = getProfileService();
+  let { profile: selectedProfile, didCreate } = selectStartupProfile();
+
+  let profileData = readProfilesIni();
+  let installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 2, "Should have the right number of profiles.");
+
+  // The name ordering is different for dev edition.
+  if (AppConstants.MOZ_DEV_EDITION) {
+    profileData.profiles.reverse();
+  }
+
+  let profile = profileData.profiles[0];
+  Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
+  Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
+  Assert.ok(profile.default, "Should be marked as the old-style default.");
+  profile = profileData.profiles[1];
+  Assert.equal(profile.name, DEDICATED_NAME, "Should have the right name.");
+  Assert.notEqual(profile.path, defaultProfile.leafName, "Should not be the original default profile.");
+  Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should be a default for this install.");
+  Assert.equal(installData.installs[hash].default, profile.path, "Should have marked the new profile as the default for this install.");
+  Assert.ok(installData.installs[hash].locked, "Should have locked as we created this profile for this install.");
+
+  checkProfileService(profileData, installData);
+
+  Assert.ok(didCreate, "Should have created a new profile.");
+  Assert.ok(service.createdAlternateProfile, "Should have created an alternate profile.");
+  Assert.ok(!selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
+  Assert.equal(selectedProfile.name, DEDICATED_NAME);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_use_dedicated.js
@@ -0,0 +1,66 @@
+/**
+ * Tests that if installs.ini lists a profile we use it as the default.
+ */
+
+add_task(async () => {
+  let hash = xreDirProvider.getInstallHash();
+  let defaultProfile = makeRandomProfileDir("default");
+  let dedicatedProfile = makeRandomProfileDir("dedicated");
+  let devProfile = makeRandomProfileDir("devedition");
+
+  // Make sure we don't steal the old-style default.
+  writeCompatibilityIni(defaultProfile);
+
+  writeProfilesIni({
+    profiles: [{
+      name: "default",
+      path: defaultProfile.leafName,
+      default: true,
+    }, {
+      name: "dedicated",
+      path: dedicatedProfile.leafName,
+    }, {
+      name: "dev-edition-default",
+      path: devProfile.leafName,
+    }],
+  });
+
+  writeInstallsIni({
+    installs: {
+      [hash]: {
+        default: dedicatedProfile.leafName,
+      },
+      "otherhash": {
+        default: "foobar",
+      },
+    },
+  });
+
+  let { profile: selectedProfile, didCreate } = selectStartupProfile();
+
+  let profileData = readProfilesIni();
+  let installData = readInstallsIni();
+
+  Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
+  Assert.equal(profileData.profiles.length, 3, "Should have the right number of profiles.");
+
+  let profile = profileData.profiles[0];
+  Assert.equal(profile.name, `dedicated`, "Should have the right name.");
+  Assert.equal(profile.path, dedicatedProfile.leafName, "Should be the expected dedicated profile.");
+  Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+  profile = profileData.profiles[1];
+  Assert.equal(profile.name, "default", "Should have the right name.");
+  Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
+  Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+  Assert.equal(Object.keys(installData.installs).length, 2, "Should be two known installs.");
+  Assert.equal(installData.installs[hash].default, dedicatedProfile.leafName, "Should have kept the default for this install.");
+  Assert.equal(installData.installs.otherhash.default, "foobar", "Should have kept the default for the other install.");
+
+  checkProfileService(profileData, installData);
+
+  Assert.ok(!didCreate, "Should not have created a new profile.");
+  Assert.ok(selectedProfile.rootDir.equals(dedicatedProfile), "Should be using the right directory.");
+  Assert.equal(selectedProfile.name, "dedicated");
+});
--- a/toolkit/profile/xpcshell/xpcshell.ini
+++ b/toolkit/profile/xpcshell/xpcshell.ini
@@ -6,8 +6,25 @@ skip-if = toolkit == 'android'
 [test_select_profilemanager.js]
 [test_select_named.js]
 [test_select_missing.js]
 [test_select_noname.js]
 [test_create_default.js]
 [test_select_environment.js]
 [test_select_environment_named.js]
 [test_profile_reset.js]
+[test_clean.js]
+[test_previous_dedicated.js]
+[test_single_profile_selected.js]
+skip-if = devedition
+[test_single_profile_unselected.js]
+skip-if = devedition
+[test_update_selected_dedicated.js]
+[test_update_unknown_dedicated.js]
+[test_update_unselected_dedicated.js]
+[test_use_dedicated.js]
+[test_new_default.js]
+[test_steal_inuse.js]
+[test_snap.js]
+[test_snap_empty.js]
+[test_remove_default.js]
+[test_claim_locked.js]
+[test_lock.js]
--- a/toolkit/xre/ProfileReset.cpp
+++ b/toolkit/xre/ProfileReset.cpp
@@ -26,20 +26,22 @@
 using namespace mozilla;
 
 extern const XREAppData* gAppData;
 
 static const char kProfileProperties[] =
     "chrome://mozapps/locale/profile/profileSelection.properties";
 
 /**
- * Delete the profile directory being reset after a backup and delete the local
- * profile directory.
+ * Spin up a thread to backup the old profile's main directory and delete the
+ * profile's local directory. Once complete have the profile service remove the
+ * old profile and if necessary make the new profile the default.
  */
-nsresult ProfileResetCleanup(nsIToolkitProfile* aOldProfile) {
+nsresult ProfileResetCleanup(nsToolkitProfileService* aService,
+                             nsIToolkitProfile* aOldProfile) {
   nsresult rv;
   nsCOMPtr<nsIFile> profileDir;
   rv = aOldProfile->GetRootDir(getter_AddRefs(profileDir));
   if (NS_FAILED(rv)) return rv;
 
   nsCOMPtr<nsIFile> profileLocalDir;
   rv = aOldProfile->GetLocalDir(getter_AddRefs(profileLocalDir));
   if (NS_FAILED(rv)) return rv;
@@ -137,15 +139,10 @@ nsresult ProfileResetCleanup(nsIToolkitP
     gProfileResetCleanupCompleted = true;
     NS_WARNING("Cleanup thread creation failed");
     return rv;
   }
   // Close the progress window now that the cleanup thread is done.
   auto* piWindow = nsPIDOMWindowOuter::From(progressWindow);
   piWindow->Close();
 
-  // Delete the old profile from profiles.ini. The folder was already deleted by
-  // the thread above.
-  rv = aOldProfile->Remove(false);
-  if (NS_FAILED(rv)) NS_WARNING("Could not remove the profile");
-
-  return rv;
+  return aService->ApplyResetProfile(aOldProfile);
 }
--- a/toolkit/xre/ProfileReset.h
+++ b/toolkit/xre/ProfileReset.h
@@ -1,22 +1,23 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
 
-#include "nsIToolkitProfileService.h"
+#include "nsToolkitProfileService.h"
 #include "nsIFile.h"
 #include "nsThreadUtils.h"
 
 static bool gProfileResetCleanupCompleted = false;
 static const char kResetProgressURL[] =
     "chrome://global/content/resetProfileProgress.xul";
 
-nsresult ProfileResetCleanup(nsIToolkitProfile* aOldProfile);
+nsresult ProfileResetCleanup(nsToolkitProfileService* aService,
+                             nsIToolkitProfile* aOldProfile);
 
 class ProfileResetCleanupResultTask : public mozilla::Runnable {
  public:
   ProfileResetCleanupResultTask()
       : mozilla::Runnable("ProfileResetCleanupResultTask"),
         mWorkerThread(do_GetCurrentThread()) {
     MOZ_ASSERT(!NS_IsMainThread());
   }
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -2045,51 +2045,16 @@ static ReturnAbortOnError ShowProfileMan
     gRestartArgv = newArgv;
     gRestartArgv[gRestartArgc++] = const_cast<char*>("-os-restarted");
     gRestartArgv[gRestartArgc] = nullptr;
   }
 
   return LaunchChild(aNative);
 }
 
-/**
- * Get the currently running profile using its root directory.
- *
- * @param aProfileSvc         The profile service
- * @param aCurrentProfileRoot The root directory of the current profile.
- * @param aProfile            Out-param that returns the profile object.
- * @return an error if aCurrentProfileRoot is not found
- */
-static nsresult GetCurrentProfile(nsIToolkitProfileService* aProfileSvc,
-                                  nsIFile* aCurrentProfileRoot,
-                                  nsIToolkitProfile** aProfile) {
-  NS_ENSURE_ARG_POINTER(aProfileSvc);
-  NS_ENSURE_ARG_POINTER(aProfile);
-
-  nsCOMPtr<nsISimpleEnumerator> profiles;
-  nsresult rv = aProfileSvc->GetProfiles(getter_AddRefs(profiles));
-  if (NS_FAILED(rv)) return rv;
-
-  bool foundMatchingProfile = false;
-  nsCOMPtr<nsISupports> supports;
-  rv = profiles->GetNext(getter_AddRefs(supports));
-  while (NS_SUCCEEDED(rv)) {
-    nsCOMPtr<nsIToolkitProfile> profile = do_QueryInterface(supports);
-    nsCOMPtr<nsIFile> profileRoot;
-    profile->GetRootDir(getter_AddRefs(profileRoot));
-    profileRoot->Equals(aCurrentProfileRoot, &foundMatchingProfile);
-    if (foundMatchingProfile) {
-      profile.forget(aProfile);
-      return NS_OK;
-    }
-    rv = profiles->GetNext(getter_AddRefs(supports));
-  }
-  return rv;
-}
-
 static bool gDoMigration = false;
 static bool gDoProfileReset = false;
 static nsCOMPtr<nsIToolkitProfile> gResetOldProfile;
 
 // Pick a profile. We need to end up with a profile lock.
 //
 // 1) check for --profile <path>
 // 2) check for -P <name>
@@ -4125,28 +4090,16 @@ nsresult XREMain::XRE_mainRun() {
         if (buf[0] == '0' || buf[0] == 'f' || buf[0] == 'F') {
           gDoMigration = false;
         }
       }
     }
   }
 
   {
-    bool profileWasDefault = false;
-    if (gDoProfileReset) {
-      nsCOMPtr<nsIToolkitProfile> defaultProfile;
-      // This can fail if there is no default profile.
-      // That shouldn't stop reset from proceeding.
-      nsresult gotDefault =
-          mProfileSvc->GetDefaultProfile(getter_AddRefs(defaultProfile));
-      if (NS_SUCCEEDED(gotDefault)) {
-        profileWasDefault = defaultProfile == gResetOldProfile;
-      }
-    }
-
     // Profile Migration
     if (mAppData->flags & NS_XRE_ENABLE_PROFILE_MIGRATOR && gDoMigration) {
       gDoMigration = false;
       nsCOMPtr<nsIProfileMigrator> pm(
           do_CreateInstance(NS_PROFILEMIGRATOR_CONTRACTID));
       if (pm) {
         nsAutoCString aKey;
         nsAutoCString aName;
@@ -4156,43 +4109,21 @@ nsresult XREMain::XRE_mainRun() {
           aKey = MOZ_APP_NAME;
           gResetOldProfile->GetName(aName);
         }
         pm->Migrate(&mDirProvider, aKey, aName);
       }
     }
 
     if (gDoProfileReset) {
-      nsresult backupCreated = ProfileResetCleanup(gResetOldProfile);
+      nsresult backupCreated = ProfileResetCleanup(
+          static_cast<nsToolkitProfileService*>(mProfileSvc.get()),
+          gResetOldProfile);
       if (NS_FAILED(backupCreated))
         NS_WARNING("Could not cleanup the profile that was reset");
-
-      nsCOMPtr<nsIToolkitProfile> newProfile;
-      rv = GetCurrentProfile(mProfileSvc, mProfD, getter_AddRefs(newProfile));
-      if (NS_SUCCEEDED(rv)) {
-        nsAutoCString name;
-        gResetOldProfile->GetName(name);
-        newProfile->SetName(name);
-        mProfileName.Assign(name);
-        // Set the new profile as the default after we're done cleaning up the
-        // old profile, iff that profile was already the default
-        if (profileWasDefault) {
-          rv = mProfileSvc->SetDefaultProfile(newProfile);
-          if (NS_FAILED(rv))
-            NS_WARNING("Could not set current profile as the default");
-        }
-      } else {
-        NS_WARNING(
-            "Could not find current profile to set as default / change name.");
-      }
-
-      // Need to write out the fact that the profile has been removed, the new
-      // profile renamed, and potentially that the selected/default profile
-      // changed.
-      mProfileSvc->Flush();
     }
   }
 
 #ifndef XP_WIN
   nsCOMPtr<nsIFile> profileDir;
   nsAutoCString path;
   rv = mDirProvider.GetProfileStartupDir(getter_AddRefs(profileDir));
   if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(profileDir->GetNativePath(path)) &&
--- a/toolkit/xre/nsXREDirProvider.cpp
+++ b/toolkit/xre/nsXREDirProvider.cpp
@@ -1,15 +1,18 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
 
 #include "nsAppRunner.h"
 #include "nsXREDirProvider.h"
+#ifndef ANDROID
+#  include "commonupdatedir.h"
+#endif
 
 #include "jsapi.h"
 #include "xpcpublic.h"
 
 #include "nsIAppStartup.h"
 #include "nsIDirectoryEnumerator.h"
 #include "nsIFile.h"
 #include "nsIObserver.h"
@@ -1616,17 +1619,17 @@ nsresult nsXREDirProvider::AppendProfile
   nsAutoCString vendor;
   if (gAppData->profile) {
     profile = gAppData->profile;
   } else {
     appName = gAppData->name;
     vendor = gAppData->vendor;
   }
 
-  nsresult rv;
+  nsresult rv = NS_OK;
 
 #if defined(XP_MACOSX)
   if (!profile.IsEmpty()) {
     rv = AppendProfileString(aFile, profile.get());
   } else {
     // Note that MacOS ignores the vendor when creating the profile hierarchy -
     // all application preferences directories live alongside one another in
     // ~/Library/Application Support/
@@ -1678,20 +1681,23 @@ nsresult nsXREDirProvider::AppendProfile
       ToLowerCase(folder);
 
       rv = aFile->AppendNative(folder);
       NS_ENSURE_SUCCESS(rv, rv);
 
       folder.Truncate();
     }
 
-    folder.Append(appName);
-    ToLowerCase(folder);
+    // This can be the case in tests.
+    if (!appName.IsEmpty()) {
+      folder.Append(appName);
+      ToLowerCase(folder);
 
-    rv = aFile->AppendNative(folder);
+      rv = aFile->AppendNative(folder);
+    }
   }
   NS_ENSURE_SUCCESS(rv, rv);
 
 #else
 #  error "Don't know how to get profile path on your platform"
 #endif
   return NS_OK;
 }