Bug 912717 - Don't let SessionCookie collection jank the chrome process r=mikedeboer
authorTim Taubert <ttaubert@mozilla.com>
Fri, 07 Apr 2017 14:41:38 +0200
changeset 351722 93d54f86f07e3a8760817dc8e4f7c04ea9d5aa9d
parent 351721 d09d07b4d50d1971beedcb085d74fe8f34db8ec0
child 351723 3dd31cc99bb3375e360165f1cec43c7adcd95258
push id88944
push userttaubert@mozilla.com
push dateFri, 07 Apr 2017 12:57:09 +0000
treeherdermozilla-inbound@93d54f86f07e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer
bugs912717
milestone55.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 912717 - Don't let SessionCookie collection jank the chrome process r=mikedeboer Current state: -------------- Session cookies - those that have no Expires or Max-Age directive, sent as a header or set via document.cookie - are meant to live for the duration of a session. SessionStore is a feature that aims to enable users to resume where they left off last time they closed the browser. So SessionStore will persist and restore those cookies that the cookie service only keeps in memory. SessionCookies.jsm registers observers with the cookie service and is thus notified of cookie additions, deletions, and modifications as-it-happens. It has its own internal storage that we could easily serialize and write to disk together with the rest of the session data. The hangs shown in various profiles stem from the fact that since the inception of SessionStore as an add-on around Firefox 2, cookies have been tacked to windows. This means that whenever we collect session data for a specific window (i.e. tabs, their shistory entries, etc.) we have to iterate *all* its tabs and *all* their shistory entries to enumerate the hosts contained in that window. We will then ask the internal cookie store in SessionCookies.jsm to give us all cookies for these hosts and then store them together with the window. This way we filter out cookies from tabs/hosts that have no active documents (BFCache counts as "active"). Changes in this patch: ---------------------- Instead of trying to only retain cookies from “active” documents, i.e. those contained somewhere in the shistory of a tab, we now simply save all session cookies of the session. This will surely reduce user complaints about us "logging them out" too fast because we discard cookies from tabs they open only once in a while, although those definitely belong to the browsing session. Instead of storing the cookies per each window we now have a top-level "cookies" attribute that is a list of cookies. These get restored whenever we restore a session. Legacy window.cookies attributes will still be restored to support older session formats for a while. The DEFER_SESSION startup mode is active by default when a user choses not to restore their whole session automatically but they still have one or more pinned tabs. These pinned tabs are restored automatically and split off of the rest of the session. The rest can be restored manually if the user chooses to do so. In the past, we here extracted and restored only the pinned tabs' cookies from the last session. This filtering also works against how some sites (e.g. Google) use session cookies. It also means we have to iterate all windows, tabs, shistory entries, and cookies to find the data we want. This patch changes our past behavior so that we now restore only pinned tabs but all session cookies. So we don't have to filter, and pages will break less likely. We hereby assume that a user having pinned tabs wants to continue their browsing session partially, although without Firefox remembering the exact list of tabs. Or they simply like starting off of a clean slate.
browser/components/sessionstore/SessionCookies.jsm
browser/components/sessionstore/SessionStore.jsm
browser/components/sessionstore/test/browser.ini
browser/components/sessionstore/test/browser_423132.js
browser/components/sessionstore/test/browser_cookies.js
browser/components/sessionstore/test/browser_cookies.sjs
browser/components/sessionstore/test/browser_cookies_legacy.js
browser/components/sessionstore/test/browser_cookies_privacy.js
browser/components/sessionstore/test/browser_sessionStoreContainer.js
--- a/browser/components/sessionstore/SessionCookies.jsm
+++ b/browser/components/sessionstore/SessionCookies.jsm
@@ -19,22 +19,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision.
 const MAX_EXPIRY = Math.pow(2, 62);
 
 /**
  * The external API implemented by the SessionCookies module.
  */
 this.SessionCookies = Object.freeze({
-  update(windows) {
-    SessionCookiesInternal.update(windows);
-  },
-
-  getHostsForWindow(window) {
-    return SessionCookiesInternal.getHostsForWindow(window);
+  collect() {
+    return SessionCookiesInternal.collect();
   },
 
   restore(cookies) {
     SessionCookiesInternal.restore(cookies);
   }
 });
 
 /**
@@ -42,90 +38,40 @@ this.SessionCookies = Object.freeze({
  */
 var SessionCookiesInternal = {
   /**
    * Stores whether we're initialized, yet.
    */
   _initialized: false,
 
   /**
-   * Retrieve the list of all hosts contained in the given windows' session
-   * history entries (per window) and collect the associated cookies for those
-   * hosts, if any. The given state object is being modified.
-   *
-   * @param windows
-   *        Array of window state objects.
-   *        [{ tabs: [...], cookies: [...] }, ...]
+   * Retrieve an array of all stored session cookies.
    */
-  update(windows) {
+  collect() {
     this._ensureInitialized();
-
-    // Check whether we're allowed to store cookies.
-    let storeAnyCookies = PrivacyLevel.canSave(false);
-    let storeSecureCookies = PrivacyLevel.canSave(true);
-
-    for (let window of windows) {
-      let cookies = [];
-
-      if (storeAnyCookies) {
-        // Collect all cookies for the current window.
-        for (let host of this.getHostsForWindow(window)) {
-          for (let cookie of CookieStore.getCookiesForHost(host)) {
-            if (!cookie.secure || storeSecureCookies) {
-              cookies.push(cookie);
-            }
-          }
-        }
-      }
-
-      // Don't include/keep empty cookie sections.
-      if (cookies.length) {
-        window.cookies = cookies;
-      } else if ("cookies" in window) {
-        delete window.cookies;
-      }
-    }
-  },
-
-  /**
-   * Returns a map of all hosts for a given window that we might want to
-   * collect cookies for.
-   *
-   * @param window
-   *        A window state object containing tabs with history entries.
-   * @return {set} A set of hosts for a given window state object.
-   */
-  getHostsForWindow(window) {
-    let hosts = new Set();
-
-    for (let tab of window.tabs) {
-      for (let entry of tab.entries) {
-        this._extractHostsFromEntry(entry, hosts);
-      }
-    }
-
-    return hosts;
+    return CookieStore.toArray();
   },
 
   /**
    * Restores a given list of session cookies.
    */
   restore(cookies) {
-
     for (let cookie of cookies) {
       let expiry = "expiry" in cookie ? cookie.expiry : MAX_EXPIRY;
       let cookieObj = {
         host: cookie.host,
         path: cookie.path || "",
         name: cookie.name || ""
       };
-      if (!Services.cookies.cookieExists(cookieObj, cookie.originAttributes || {})) {
+
+      let originAttributes = cookie.originAttributes || {};
+      if (!Services.cookies.cookieExists(cookieObj, originAttributes)) {
         Services.cookies.add(cookie.host, cookie.path || "", cookie.name || "",
                              cookie.value, !!cookie.secure, !!cookie.httponly,
-                             /* isSession = */ true, expiry, cookie.originAttributes || {});
+                             /* isSession = */ true, expiry, originAttributes);
       }
     }
   },
 
   /**
    * Handles observers notifications that are sent whenever cookies are added,
    * changed, or removed. Ensures that the storage is updated accordingly.
    */
@@ -141,81 +87,59 @@ var SessionCookiesInternal = {
         this._removeCookie(subject);
         break;
       case "cleared":
         CookieStore.clear();
         break;
       case "batch-deleted":
         this._removeCookies(subject);
         break;
-      case "reload":
-        CookieStore.clear();
-        this._reloadCookies();
-        break;
       default:
         throw new Error("Unhandled cookie-changed notification.");
     }
   },
 
   /**
    * If called for the first time in a session, iterates all cookies in the
    * cookies service and puts them into the store if they're session cookies.
    */
   _ensureInitialized() {
     if (!this._initialized) {
       this._reloadCookies();
       this._initialized = true;
       Services.obs.addObserver(this, "cookie-changed", false);
-    }
-  },
 
-  /**
-   * Fill a given map with hosts found in the given entry's session history and
-   * any child entries.
-   *
-   * @param entry
-   *        the history entry, serialized
-   * @param hosts
-   *        the set that will be used to store hosts
-   */
-  _extractHostsFromEntry(entry, hosts) {
-    try {
-      // It's alright if this throws for about: URIs.
-      let {host, scheme} = Utils.makeURI(entry.url);
-      if (/^(file|https?)$/.test(scheme)) {
-        hosts.add(host);
-      }
-    } catch (ex) { }
-
-    if (entry.children) {
-      for (let child of entry.children) {
-        this._extractHostsFromEntry(child, hosts);
-      }
+      // Listen for privacy level changes to reload cookies when needed.
+      Services.prefs.addObserver("browser.sessionstore.privacy_level", () => {
+        this._reloadCookies();
+      }, false);
     }
   },
 
   /**
    * Adds a given cookie to the store.
    */
   _addCookie(cookie) {
     cookie.QueryInterface(Ci.nsICookie2);
 
-    if (cookie.isSession) {
-      CookieStore.set(cookie);
+    // Store only session cookies, obey the privacy level.
+    if (cookie.isSession && PrivacyLevel.canSave(cookie.isSecure)) {
+      CookieStore.add(cookie);
     }
   },
 
   /**
    * Updates a given cookie.
    */
   _updateCookie(cookie) {
     cookie.QueryInterface(Ci.nsICookie2);
 
-    if (cookie.isSession) {
-      CookieStore.set(cookie);
+    // Store only session cookies, obey the privacy level.
+    if (cookie.isSession && PrivacyLevel.canSave(cookie.isSecure)) {
+      CookieStore.add(cookie);
     } else {
       CookieStore.delete(cookie);
     }
   },
 
   /**
    * Removes a given cookie from the store.
    */
@@ -233,137 +157,49 @@ var SessionCookiesInternal = {
   _removeCookies(cookies) {
     for (let i = 0; i < cookies.length; i++) {
       this._removeCookie(cookies.queryElementAt(i, Ci.nsICookie2));
     }
   },
 
   /**
    * Iterates all cookies in the cookies service and puts them into the store
-   * if they're session cookies.
+   * if they're session cookies. Obeys the user's chosen privacy level.
    */
   _reloadCookies() {
+    CookieStore.clear();
+
+    // Bail out if we're not supposed to store cookies at all.
+    if (!PrivacyLevel.canSave(false)) {
+      return;
+    }
+
     let iter = Services.cookies.enumerator;
     while (iter.hasMoreElements()) {
       this._addCookie(iter.getNext());
     }
   }
 };
 
 /**
- * Generates all possible subdomains for a given host and prepends a leading
- * dot to all variants.
- *
- * See http://tools.ietf.org/html/rfc6265#section-5.1.3
- *     http://en.wikipedia.org/wiki/HTTP_cookie#Domain_and_Path
- *
- * All cookies belonging to a web page will be internally represented by a
- * nsICookie object. nsICookie.host will be the request host if no domain
- * parameter was given when setting the cookie. If a specific domain was given
- * then nsICookie.host will contain that specific domain and prepend a leading
- * dot to it.
- *
- * We thus generate all possible subdomains for a given domain and prepend a
- * leading dot to them as that is the value that was used as the map key when
- * the cookie was set.
- */
-function* getPossibleSubdomainVariants(host) {
-  // Try given domain with a leading dot (.www.example.com).
-  yield "." + host;
-
-  // Stop if there are only two parts left (e.g. example.com was given).
-  let parts = host.split(".");
-  if (parts.length < 3) {
-    return;
-  }
-
-  // Remove the first subdomain (www.example.com -> example.com).
-  let rest = parts.slice(1).join(".");
-
-  // Try possible parent subdomains.
-  yield* getPossibleSubdomainVariants(rest);
-}
-
-/**
- * The internal cookie storage that keeps track of every active session cookie.
- * These are stored using maps per host, path, and cookie name.
+ * The internal storage that keeps track of session cookies.
  */
 var CookieStore = {
   /**
-   * The internal structure holding all known cookies.
-   *
-   * Host =>
-   *  Path =>
-   *    Name => {path: "/", name: "sessionid", secure: true}
-   *
-   * Maps are used for storage but the data structure is equivalent to this:
-   *
-   * this._hosts = {
-   *   "www.mozilla.org": {
-   *     "/": {
-   *       "username": {name: "username", value: "my_name_is", etc...},
-   *       "sessionid": {name: "sessionid", value: "1fdb3a", etc...}
-   *     }
-   *   },
-   *   "tbpl.mozilla.org": {
-   *     "/path": {
-   *       "cookiename": {name: "cookiename", value: "value", etc...}
-   *     }
-   *   },
-   *   ".example.com": {
-   *     "/path": {
-   *       "cookiename": {name: "cookiename", value: "value", etc...}
-   *     }
-   *   }
-   * };
+   * The internal map holding all known session cookies.
    */
-  _hosts: new Map(),
-
-  /**
-   * Returns the list of stored session cookies for a given host.
-   *
-   * @param mainHost
-   *        A string containing the host name we want to get cookies for.
-   */
-  getCookiesForHost(mainHost) {
-    let cookies = [];
-
-    let appendCookiesForHost = host => {
-      if (!this._hosts.has(host)) {
-        return;
-      }
-
-      for (let pathToNamesMap of this._hosts.get(host).values()) {
-        for (let nameToCookiesMap of pathToNamesMap.values()) {
-          cookies.push(...nameToCookiesMap.values());
-        }
-      }
-    }
-
-    // Try to find cookies for the given host, e.g. <www.example.com>.
-    // The full hostname will be in the map if the Set-Cookie header did not
-    // have a domain= attribute, i.e. the cookie will only be stored for the
-    // request domain. Also, try to find cookies for subdomains, e.g.
-    // <.example.com>. We will find those variants with a leading dot in the
-    // map if the Set-Cookie header had a domain= attribute, i.e. the cookie
-    // will be stored for a parent domain and we send it for any subdomain.
-    for (let variant of [mainHost, ...getPossibleSubdomainVariants(mainHost)]) {
-      appendCookiesForHost(variant);
-    }
-
-    return cookies;
-  },
+  _entries: new Map(),
 
   /**
    * Stores a given cookie.
    *
    * @param cookie
    *        The nsICookie2 object to add to the storage.
    */
-  set(cookie) {
+  add(cookie) {
     let jscookie = {host: cookie.host, value: cookie.value};
 
     // Only add properties with non-default values to save a few bytes.
     if (cookie.path) {
       jscookie.path = cookie.path;
     }
 
     if (cookie.name) {
@@ -381,75 +217,53 @@ var CookieStore = {
     if (cookie.expiry < MAX_EXPIRY) {
       jscookie.expiry = cookie.expiry;
     }
 
     if (cookie.originAttributes) {
       jscookie.originAttributes = cookie.originAttributes;
     }
 
-    this._ensureMap(cookie).set(cookie.name, jscookie);
+    this._entries.set(this._getKeyForCookie(cookie), jscookie);
   },
 
   /**
    * Removes a given cookie.
    *
    * @param cookie
    *        The nsICookie2 object to be removed from storage.
    */
   delete(cookie) {
-    // Deletion shouldn't create new maps.
-    // Bail out whenever we find that an entry or a map doesn't exist.
-    let map = this._hosts.get(cookie.host);
-    if (!map) {
-      return;
-    }
-
-    map = map.get(ChromeUtils.originAttributesToSuffix(cookie.originAttributes));
-    if (!map) {
-      return;
-    }
-
-    map = map.get(cookie.path);
-    if (!map) {
-      return;
-    }
-
-    map.delete(cookie.name);
+    this._entries.delete(this._getKeyForCookie(cookie));
   },
 
   /**
    * Removes all cookies.
    */
   clear() {
-    this._hosts.clear();
+    this._entries.clear();
+  },
+
+  /**
+   * Return all cookies as an array.
+   */
+  toArray() {
+    return [...this._entries.values()];
   },
 
   /**
-   * Creates all maps necessary to store a given cookie.
+   * Returns the key needed to properly store and identify a given cookie.
+   * A cookie is uniquely identified by the combination of its host, name,
+   * path, and originAttributes properties.
    *
    * @param cookie
-   *        The nsICookie2 object to create maps for.
-   *
-   * @return The newly created Map instance mapping cookie names to
-   *         internal jscookies, in the given path of the given host.
+   *        The nsICookie2 object to compute a key for.
+   * @return string
    */
-  _ensureMap(cookie) {
-    if (!this._hosts.has(cookie.host)) {
-      this._hosts.set(cookie.host, new Map());
-    }
-
-    let originAttributesMap = this._hosts.get(cookie.host);
-    // If cookie.originAttributes is null, originAttributes will be an empty string.
-    let originAttributes = ChromeUtils.originAttributesToSuffix(cookie.originAttributes);
-    if (!originAttributesMap.has(originAttributes)) {
-      originAttributesMap.set(originAttributes, new Map());
-    }
-
-    let pathToNamesMap = originAttributesMap.get(originAttributes);
-
-    if (!pathToNamesMap.has(cookie.path)) {
-      pathToNamesMap.set(cookie.path, new Map());
-    }
-
-    return pathToNamesMap.get(cookie.path);
+  _getKeyForCookie(cookie) {
+    return JSON.stringify({
+      host: cookie.host,
+      name: cookie.name,
+      path: cookie.path,
+      attr: ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
+    });
   }
 };
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -629,20 +629,25 @@ var SessionStoreInternal = {
     if (state) {
       try {
         // If we're doing a DEFERRED session, then we want to pull pinned tabs
         // out so they can be restored.
         if (ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) {
           let [iniState, remainingState] = this._prepDataForDeferredRestore(state);
           // If we have a iniState with windows, that means that we have windows
           // with app tabs to restore.
-          if (iniState.windows.length)
+          if (iniState.windows.length) {
+            // Move cookies over from the remaining state so that they're
+            // restored right away, and pinned tabs will load correctly.
+            iniState.cookies = remainingState.cookies;
+            delete remainingState.cookies;
             state = iniState;
-          else
+          } else {
             state = null;
+          }
 
           if (remainingState.windows.length) {
             LastSession.setState(remainingState);
           }
         } else {
           // Get the last deferred session in case the user still wants to
           // restore it
           LastSession.setState(state.lastSessionState);
@@ -1148,16 +1153,19 @@ var SessionStoreInternal = {
         } else {
           TelemetryTimestamps.add("sessionRestoreRestoring");
           this._restoreCount = aInitialState.windows ? aInitialState.windows.length : 0;
 
           // global data must be restored before restoreWindow is called so that
           // it happens before observers are notified
           this._globalState.setFromState(aInitialState);
 
+          // Restore session cookies before loading any tabs.
+          SessionCookies.restore(aInitialState.cookies || []);
+
           let overwrite = this._isCmdLineEmpty(aWindow, aInitialState);
           let options = {firstWindow: true, overwriteTabs: overwrite};
           this.restoreWindows(aWindow, aInitialState, options);
         }
       } else {
         // Nothing to restore, notify observers things are complete.
         Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
       }
@@ -1376,17 +1384,16 @@ var SessionStoreInternal = {
         let permanentKey = tab.linkedBrowser.permanentKey;
         this._closedWindowTabs.set(permanentKey, tabData);
       }
 
       if (isFullyLoaded) {
         winData.title = tabbrowser.selectedBrowser.contentTitle || tabbrowser.selectedTab.label;
         winData.title = this._replaceLoadingTitle(winData.title, tabbrowser,
                                                   tabbrowser.selectedTab);
-        SessionCookies.update([winData]);
       }
 
       if (AppConstants.platform != "macosx") {
         // Until we decide otherwise elsewhere, this window is part of a series
         // of closing windows to quit.
         winData._shouldRestore = true;
       }
 
@@ -2216,16 +2223,19 @@ var SessionStoreInternal = {
 
     // determine how many windows are meant to be restored
     this._restoreCount = state.windows ? state.windows.length : 0;
 
     // global data must be restored before restoreWindow is called so that
     // it happens before observers are notified
     this._globalState.setFromState(state);
 
+    // Restore session cookies.
+    SessionCookies.restore(state.cookies || []);
+
     // restore to the given state
     this.restoreWindows(window, state, {overwriteTabs: true});
 
     // Notify of changes to closed objects.
     this._notifyOfClosedObjectsChange();
   },
 
   getWindowState: function ssi_getWindowState(aWindow) {
@@ -2681,16 +2691,19 @@ var SessionStoreInternal = {
     let lastWindow = this._getMostRecentBrowserWindow();
     let canUseLastWindow = lastWindow &&
                            !lastWindow.__SS_lastSessionWindowID;
 
     // global data must be restored before restoreWindow is called so that
     // it happens before observers are notified
     this._globalState.setFromState(lastSessionState);
 
+    // Restore session cookies.
+    SessionCookies.restore(lastSessionState.cookies || []);
+
     // Restore into windows or open new ones as needed.
     for (let i = 0; i < lastSessionState.windows.length; i++) {
       let winState = lastSessionState.windows[i];
       let lastSessionWindowID = winState.__lastSessionWindowID;
       // delete lastSessionWindowID so we don't add that to the window again
       delete winState.__lastSessionWindowID;
 
       // See if we can use an open window. First try one that is associated with
@@ -3041,20 +3054,16 @@ var SessionStoreInternal = {
       if (this._windows[ix]._restoring) // window data is still in _statesToRestore
         continue;
       total.push(this._windows[ix]);
       ids.push(ix);
       if (!this._windows[ix].isPopup)
         nonPopupCount++;
     }
 
-    TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
-    SessionCookies.update(total);
-    TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
-
     // collect the data for all windows yet to be restored
     for (ix in this._statesToRestore) {
       for (let winData of this._statesToRestore[ix].windows) {
         total.push(winData);
         if (!winData.isPopup)
           nonPopupCount++;
       }
     }
@@ -3097,16 +3106,21 @@ var SessionStoreInternal = {
       version: ["sessionrestore", FORMAT_VERSION],
       windows: total,
       selectedWindow: ix + 1,
       _closedWindows: lastClosedWindowsCopy,
       session,
       global: this._globalState.getState()
     };
 
+    // Collect and store session cookies.
+    TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
+    state.cookies = SessionCookies.collect();
+    TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
+
     if (Cu.isModuleLoaded("resource://devtools/client/scratchpad/scratchpad-manager.jsm")) {
       // get open Scratchpad window states too
       let scratchpads = ScratchpadManager.getSessionState();
       if (scratchpads && scratchpads.length) {
         state.scratchpads = scratchpads;
       }
     }
 
@@ -3134,20 +3148,17 @@ var SessionStoreInternal = {
   _getWindowState: function ssi_getWindowState(aWindow) {
     if (!this._isWindowLoaded(aWindow))
       return this._statesToRestore[aWindow.__SS_restoreID];
 
     if (RunState.isRunning) {
       this._collectWindowData(aWindow);
     }
 
-    let windows = [this._windows[aWindow.__SSi]];
-    SessionCookies.update(windows);
-
-    return { windows };
+    return { windows: [this._windows[aWindow.__SSi]] };
   },
 
   /**
    * Gathers data about a window and its tabs, and updates its
    * entry in this._windows.
    *
    * @param aWindow
    *        Window references.
@@ -3381,19 +3392,20 @@ var SessionStoreInternal = {
       Array.slice(tabbrowser.tabs, newTabCount, openTabCount)
            .forEach(tabbrowser.removeTab, tabbrowser);
     }
 
     if (overwriteTabs) {
       this.restoreWindowFeatures(aWindow, winData);
       delete this._windows[aWindow.__SSi].extData;
     }
-    if (winData.cookies) {
-      SessionCookies.restore(winData.cookies);
-    }
+
+    // Restore cookies from legacy sessions, i.e. before bug 912717.
+    SessionCookies.restore(winData.cookies || []);
+
     if (winData.extData) {
       if (!this._windows[aWindow.__SSi].extData) {
         this._windows[aWindow.__SSi].extData = {};
       }
       for (var key in winData.extData) {
         this._windows[aWindow.__SSi].extData[key] = winData.extData[key];
       }
     }
@@ -4307,18 +4319,17 @@ var SessionStoreInternal = {
   },
 
   /**
    * This is going to take a state as provided at startup (via
    * nsISessionStartup.state) and split it into 2 parts. The first part
    * (defaultState) will be a state that should still be restored at startup,
    * while the second part (state) is a state that should be saved for later.
    * defaultState will be comprised of windows with only pinned tabs, extracted
-   * from state. It will contain the cookies that go along with the history
-   * entries in those tabs. It will also contain window position information.
+   * from state. It will also contain window position information.
    *
    * defaultState will be restored at startup. state will be passed into
    * LastSession and will be kept in case the user explicitly wants
    * to restore the previous session (publicly exposed as restoreLastSession).
    *
    * @param state
    *        The state, presumably from nsISessionStartup.state
    * @returns [defaultState, state]
@@ -4333,17 +4344,17 @@ var SessionStoreInternal = {
     state.selectedWindow = state.selectedWindow || 1;
 
     // Look at each window, remove pinned tabs, adjust selectedindex,
     // remove window if necessary.
     for (let wIndex = 0; wIndex < state.windows.length;) {
       let window = state.windows[wIndex];
       window.selected = window.selected || 1;
       // We're going to put the state of the window into this object
-      let pinnedWindowState = { tabs: [], cookies: []};
+      let pinnedWindowState = { tabs: [] };
       for (let tIndex = 0; tIndex < window.tabs.length;) {
         if (window.tabs[tIndex].pinned) {
           // Adjust window.selected
           if (tIndex + 1 < window.selected)
             window.selected -= 1;
           else if (tIndex + 1 == window.selected)
             pinnedWindowState.selected = pinnedWindowState.tabs.length + 2;
             // + 2 because the tab isn't actually in the array yet
@@ -4374,19 +4385,16 @@ var SessionStoreInternal = {
         // - isPopup
         // - hidden
 
         // Assign a unique ID to correlate the window to be opened with the
         // remaining data
         window.__lastSessionWindowID = pinnedWindowState.__lastSessionWindowID
                                      = "" + Date.now() + Math.random();
 
-        // Extract the cookies that belong with each pinned tab
-        this._splitCookiesFromWindow(window, pinnedWindowState);
-
         // Actually add this window to our defaultState
         defaultState.windows.push(pinnedWindowState);
         // Remove the window from the state if it doesn't have any tabs
         if (!window.tabs.length) {
           if (wIndex + 1 <= state.selectedWindow)
             state.selectedWindow -= 1;
           else if (wIndex + 1 == state.selectedWindow)
             defaultState.selectedIndex = defaultState.windows.length + 1;
@@ -4399,46 +4407,16 @@ var SessionStoreInternal = {
 
       }
       wIndex++;
     }
 
     return [defaultState, state];
   },
 
-  /**
-   * Splits out the cookies from aWinState into aTargetWinState based on the
-   * tabs that are in aTargetWinState.
-   * This alters the state of aWinState and aTargetWinState.
-   */
-  _splitCookiesFromWindow:
-    function ssi_splitCookiesFromWindow(aWinState, aTargetWinState) {
-    if (!aWinState.cookies || !aWinState.cookies.length)
-      return;
-
-    // Get the hosts for history entries in aTargetWinState
-    let cookieHosts = SessionCookies.getHostsForWindow(aTargetWinState);
-
-    // By creating a regex we reduce overhead and there is only one loop pass
-    // through either array (cookieHosts and aWinState.cookies).
-    let hosts = [...cookieHosts].join("|").replace(/\./g, "\\.");
-    // If we don't actually have any hosts, then we don't want to do anything.
-    if (!hosts.length)
-      return;
-    let cookieRegex = new RegExp(".*(" + hosts + ")");
-    for (let cIndex = 0; cIndex < aWinState.cookies.length;) {
-      if (cookieRegex.test(aWinState.cookies[cIndex].host)) {
-        aTargetWinState.cookies =
-          aTargetWinState.cookies.concat(aWinState.cookies.splice(cIndex, 1));
-        continue;
-      }
-      cIndex++;
-    }
-  },
-
   _sendRestoreCompletedNotifications: function ssi_sendRestoreCompletedNotifications() {
     // not all windows restored, yet
     if (this._restoreCount > 1) {
       this._restoreCount--;
       return;
     }
 
     // observers were already notified
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -7,17 +7,16 @@
 # browser_589246.js is disabled for leaking browser windows (bug 752467)
 # browser_580512.js is disabled for leaking browser windows (bug 752467)
 
 [DEFAULT]
 support-files =
   head.js
   content.js
   content-forms.js
-  browser_cookies.sjs
   browser_formdata_sample.html
   browser_formdata_xpath_sample.html
   browser_frametree_sample.html
   browser_frametree_sample_frameset.html
   browser_frame_history_index.html
   browser_frame_history_index2.html
   browser_frame_history_index_blank.html
   browser_frame_history_a.html
@@ -75,17 +74,16 @@ skip-if = debug # bug 1167933
 [browser_async_remove_tab.js]
 run-if = e10s
 skip-if = debug # bug 1211084
 [browser_attributes.js]
 [browser_backup_recovery.js]
 [browser_broadcast.js]
 [browser_capabilities.js]
 [browser_cleaner.js]
-[browser_cookies.js]
 [browser_crashedTabs.js]
 skip-if = !e10s || !crashreporter
 [browser_unrestored_crashedTabs.js]
 skip-if = !e10s || !crashreporter
 [browser_revive_crashed_bg_tabs.js]
 skip-if = !e10s || !crashreporter
 [browser_dying_cache.js]
 [browser_dynamic_frames.js]
@@ -246,8 +244,12 @@ skip-if = debug
 [browser_grouped_session_store.js]
 skip-if = !e10s # GroupedSHistory is e10s-only
 
 [browser_closed_objects_changed_notifications_tabs.js]
 [browser_closed_objects_changed_notifications_windows.js]
 [browser_duplicate_history.js]
 [browser_tabicon_after_bg_tab_crash.js]
 skip-if = !e10s # Tabs can't crash without e10s
+
+[browser_cookies.js]
+[browser_cookies_legacy.js]
+[browser_cookies_privacy.js]
--- a/browser/components/sessionstore/test/browser_423132.js
+++ b/browser/components/sessionstore/test/browser_423132.js
@@ -10,50 +10,47 @@ add_task(function*() {
 
   Services.cookies.removeAll();
   // make sure that sessionstore.js can be forced to be created by setting
   // the interval pref to 0
   yield SpecialPowers.pushPrefEnv({
     set: [["browser.sessionstore.interval", 0]]
   });
 
-  let win = yield BrowserTestUtils.openNewBrowserWindow();
-  let browser = win.gBrowser.selectedBrowser;
-  browser.loadURI(testURL);
-  yield BrowserTestUtils.browserLoaded(browser);
-
-  yield TabStateFlusher.flush(browser);
+  let tab = gBrowser.addTab(testURL);
+  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield TabStateFlusher.flush(tab.linkedBrowser);
 
   // get the sessionstore state for the window
-  let state = ss.getWindowState(win);
+  let state = ss.getBrowserState();
 
   // verify our cookie got set during pageload
   let enumerator = Services.cookies.enumerator;
   let cookie;
   let i = 0;
   while (enumerator.hasMoreElements()) {
     cookie = enumerator.getNext().QueryInterface(Ci.nsICookie);
     i++;
   }
   Assert.equal(i, 1, "expected one cookie");
 
   // remove the cookie
   Services.cookies.removeAll();
 
   // restore the window state
-  ss.setWindowState(win, state, true);
+  ss.setBrowserState(state);
 
   // at this point, the cookie should be restored...
   enumerator = Services.cookies.enumerator;
   let cookie2;
   while (enumerator.hasMoreElements()) {
     cookie2 = enumerator.getNext().QueryInterface(Ci.nsICookie);
     if (cookie.name == cookie2.name)
       break;
   }
   is(cookie.name, cookie2.name, "cookie name successfully restored");
   is(cookie.value, cookie2.value, "cookie value successfully restored");
   is(cookie.path, cookie2.path, "cookie path successfully restored");
 
   // clean up
   Services.cookies.removeAll();
-  yield BrowserTestUtils.closeWindow(win);
+  yield promiseRemoveTab(tab);
 });
--- a/browser/components/sessionstore/test/browser_cookies.js
+++ b/browser/components/sessionstore/test/browser_cookies.js
@@ -1,275 +1,73 @@
 "use strict";
 
-const PATH = "/browser/browser/components/sessionstore/test/";
-
-/**
- * Remove all cookies to start off a clean slate.
- */
-add_task(function* test_setup() {
-  requestLongerTimeout(3);
-  Services.cookies.removeAll();
-});
-
-/**
- * Test multiple scenarios with different Set-Cookie header domain= params.
- */
-add_task(function* test_run() {
-  // Set-Cookie: foobar=random()
-  // The domain of the cookie should be the request domain (www.example.com).
-  // We should collect data only for the request domain, no parent or subdomains.
-  yield testCookieCollection({
-    host: "http://www.example.com",
-    cookieHost: "www.example.com",
-    cookieURIs: ["http://www.example.com" + PATH],
-    noCookieURIs: ["http://example.com/" + PATH]
-  });
-
-  // Set-Cookie: foobar=random()
-  // The domain of the cookie should be the request domain (example.com).
-  // We should collect data only for the request domain, no parent or subdomains.
-  yield testCookieCollection({
-    host: "http://example.com",
-    cookieHost: "example.com",
-    cookieURIs: ["http://example.com" + PATH],
-    noCookieURIs: ["http://www.example.com/" + PATH]
-  });
-
-  // Set-Cookie: foobar=random(); Domain=example.com
-  // The domain of the cookie should be the given one (.example.com).
-  // We should collect data for the given domain and its subdomains.
-  yield testCookieCollection({
-    host: "http://example.com",
-    domain: "example.com",
-    cookieHost: ".example.com",
-    cookieURIs: ["http://example.com" + PATH, "http://www.example.com/" + PATH],
-    noCookieURIs: ["about:robots"]
-  });
-
-  // Set-Cookie: foobar=random(); Domain=.example.com
-  // The domain of the cookie should be the given one (.example.com).
-  // We should collect data for the given domain and its subdomains.
-  yield testCookieCollection({
-    host: "http://example.com",
-    domain: ".example.com",
-    cookieHost: ".example.com",
-    cookieURIs: ["http://example.com" + PATH, "http://www.example.com/" + PATH],
-    noCookieURIs: ["about:robots"]
-  });
-
-  // Set-Cookie: foobar=random(); Domain=www.example.com
-  // The domain of the cookie should be the given one (.www.example.com).
-  // We should collect data for the given domain and its subdomains.
-  yield testCookieCollection({
-    host: "http://www.example.com",
-    domain: "www.example.com",
-    cookieHost: ".www.example.com",
-    cookieURIs: ["http://www.example.com/" + PATH],
-    noCookieURIs: ["http://example.com"]
-  });
-
-  // Set-Cookie: foobar=random(); Domain=.www.example.com
-  // The domain of the cookie should be the given one (.www.example.com).
-  // We should collect data for the given domain and its subdomains.
-  yield testCookieCollection({
-    host: "http://www.example.com",
-    domain: ".www.example.com",
-    cookieHost: ".www.example.com",
-    cookieURIs: ["http://www.example.com/" + PATH],
-    noCookieURIs: ["http://example.com"]
-  });
-});
-
-/**
- * Test multiple scenarios with different privacy levels.
- */
-add_task(function* test_run_privacy_level() {
-  registerCleanupFunction(() => {
-    Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
-  });
-
-  // With the default privacy level we collect all cookies.
-  yield testCookieCollection({
-    host: "http://example.com",
-    domain: ".example.com",
-    cookieHost: ".example.com",
-    cookieURIs: ["https://example.com/" + PATH, "http://example.com/" + PATH],
-    noCookieURIs: ["about:robots"]
-  });
-
-  // With the default privacy level we collect all cookies.
-  yield testCookieCollection({
-    host: "https://example.com",
-    domain: ".example.com",
-    cookieHost: ".example.com",
-    cookieURIs: ["https://example.com/" + PATH, "http://example.com/" + PATH],
-    noCookieURIs: ["about:robots"]
-  });
-
-  // With the default privacy level we collect all cookies.
-  yield testCookieCollection({
-    isSecure: true,
-    host: "https://example.com",
-    domain: ".example.com",
-    cookieHost: ".example.com",
-    cookieURIs: ["https://example.com/" + PATH, "http://example.com/" + PATH],
-    noCookieURIs: ["about:robots"]
-  });
-
-  // Set level=encrypted, don't store any secure cookies.
-  Services.prefs.setIntPref("browser.sessionstore.privacy_level", 1);
-
-  // With level=encrypted, non-secure cookies will be stored.
-  yield testCookieCollection({
-    host: "http://example.com",
-    domain: ".example.com",
-    cookieHost: ".example.com",
-    cookieURIs: ["https://example.com/" + PATH, "http://example.com/" + PATH],
-    noCookieURIs: ["about:robots"]
-  });
+function promiseSetCookie(cookie) {
+  info(`Set-Cookie: ${cookie}`);
+  return Promise.all([
+    waitForCookieChanged(),
+    ContentTask.spawn(gBrowser.selectedBrowser, cookie,
+      passedCookie => content.document.cookie = passedCookie)
+  ]);
+}
 
-  // With level=encrypted, non-secure cookies will be stored,
-  // even if sent by an HTTPS site.
-  yield testCookieCollection({
-    host: "https://example.com",
-    domain: ".example.com",
-    cookieHost: ".example.com",
-    cookieURIs: ["https://example.com/" + PATH, "http://example.com/" + PATH],
-    noCookieURIs: ["about:robots"]
-  });
-
-  // With level=encrypted, secure cookies will NOT be stored.
-  yield testCookieCollection({
-    isSecure: true,
-    host: "https://example.com",
-    domain: ".example.com",
-    cookieHost: ".example.com",
-    noCookieURIs: ["https://example.com/" + PATH, "http://example.com/" + PATH]
-  });
-
-  // Set level=full, don't store any cookies.
-  Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);
-
-  // With level=full we must not store any cookies.
-  yield testCookieCollection({
-    host: "http://example.com",
-    domain: ".example.com",
-    cookieHost: ".example.com",
-    noCookieURIs: ["https://example.com/" + PATH, "http://example.com/" + PATH]
-  });
-
-  // With level=full we must not store any cookies.
-  yield testCookieCollection({
-    host: "https://example.com",
-    domain: ".example.com",
-    cookieHost: ".example.com",
-    noCookieURIs: ["https://example.com/" + PATH, "http://example.com/" + PATH]
-  });
-
-  // With level=full we must not store any cookies.
-  yield testCookieCollection({
-    isSecure: true,
-    host: "https://example.com",
-    domain: ".example.com",
-    cookieHost: ".example.com",
-    noCookieURIs: ["https://example.com/" + PATH, "http://example.com/" + PATH]
-  });
-
-  Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
-});
-
-/**
- * Generic test function to check sessionstore's cookie collection module with
- * different cookie domains given in the Set-Cookie header. See above for some
- * usage examples.
- */
-var testCookieCollection = async function(params) {
-  let tab = gBrowser.addTab("about:blank");
-  let browser = tab.linkedBrowser;
-
-  let urlParams = new URLSearchParams();
-  let value = Math.random();
-  urlParams.append("value", value);
-
-  if (params.domain) {
-    urlParams.append("domain", params.domain);
-  }
-
-  if (params.isSecure) {
-    urlParams.append("secure", "1");
-  }
-
-  // Construct request URI.
-  let requestUri = `${params.host}${PATH}browser_cookies.sjs?${urlParams}`;
-
-  // Wait for the browser to load and the cookie to be set.
-  // These two events can probably happen in no particular order,
-  // so let's wait for them in parallel.
-  await Promise.all([
-    waitForNewCookie(),
-    replaceCurrentURI(browser, requestUri)
-  ]);
-
-  // Check all URIs for which the cookie should be collected.
-  for (let uri of params.cookieURIs || []) {
-    await replaceCurrentURI(browser, uri);
-
-    // Check the cookie.
-    let cookie = getCookie();
-    is(cookie.host, params.cookieHost, "cookie host is correct");
-    is(cookie.path, PATH, "cookie path is correct");
-    is(cookie.name, "foobar", "cookie name is correct");
-    is(cookie.value, value, "cookie value is correct");
-  }
-
-  // Check all URIs for which the cookie should NOT be collected.
-  for (let uri of params.noCookieURIs || []) {
-    await replaceCurrentURI(browser, uri);
-
-    // Cookie should be ignored.
-    ok(!getCookie(), "no cookie collected");
-  }
-
-  // Clean up.
-  gBrowser.removeTab(tab);
-  Services.cookies.removeAll();
-};
-
-/**
- * Replace the current URI of the given browser by loading a new URI. The
- * browser's session history will be completely replaced. This function ensures
- * that the parent process has the lastest shistory data before resolving.
- */
-var replaceCurrentURI = async function(browser, uri) {
-  // Replace the tab's current URI with the parent domain.
-  let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
-  browser.loadURIWithFlags(uri, flags);
-  await promiseBrowserLoaded(browser);
-
-  // Ensure the tab's session history is up-to-date.
-  await TabStateFlusher.flush(browser);
-};
-
-/**
- * Waits for a new "*example.com" cookie to be added.
- */
-function waitForNewCookie() {
+function waitForCookieChanged() {
   return new Promise(resolve => {
     Services.obs.addObserver(function observer(subj, topic, data) {
-      let cookie = subj.QueryInterface(Ci.nsICookie2);
-      if (data == "added" && cookie.host.endsWith("example.com")) {
-        Services.obs.removeObserver(observer, topic);
-        resolve();
-      }
+      Services.obs.removeObserver(observer, topic);
+      resolve();
     }, "cookie-changed", false);
   });
 }
 
-/**
- * Retrieves the first cookie in the first window from the current sessionstore
- * state.
- */
-function getCookie() {
-  let state = JSON.parse(ss.getWindowState(window));
-  let cookies = state.windows[0].cookies || [];
-  return cookies[0] || null;
+function cookieExists(host, name, value) {
+  let {cookies: [c]} = JSON.parse(ss.getBrowserState());
+  return c && c.host == host && c.name == name && c.value == value;
 }
+
+// Setup and cleanup.
+add_task(function* test_setup() {
+  registerCleanupFunction(() => {
+    Services.cookies.removeAll();
+  });
+});
+
+// Test session cookie storage.
+add_task(function* test_run() {
+  Services.cookies.removeAll();
+
+  // Add a new tab for testing.
+  gBrowser.selectedTab = gBrowser.addTab("http://example.com/");
+  yield promiseBrowserLoaded(gBrowser.selectedBrowser);
+
+  // Add a session cookie.
+  yield promiseSetCookie("foo=bar");
+  ok(cookieExists("example.com", "foo", "bar"), "cookie was added");
+
+  // Modify a session cookie.
+  yield promiseSetCookie("foo=baz");
+  ok(cookieExists("example.com", "foo", "baz"), "cookie was modified");
+
+  // Turn the session cookie into a normal one.
+  let expiry = new Date();
+  expiry.setDate(expiry.getDate() + 2);
+  yield promiseSetCookie(`foo=baz; Expires=${expiry.toUTCString()}`);
+  ok(!cookieExists("example.com", "foo", "baz"), "no longer a session cookie");
+
+  // Turn it back into a session cookie.
+  yield promiseSetCookie("foo=bar");
+  ok(cookieExists("example.com", "foo", "bar"), "again a session cookie");
+
+  // Remove the session cookie.
+  yield promiseSetCookie("foo=; Expires=Thu, 01 Jan 1970 00:00:00 GMT");
+  ok(!cookieExists("example.com", "foo", ""), "cookie was removed");
+
+  // Add a session cookie.
+  yield promiseSetCookie("foo=bar");
+  ok(cookieExists("example.com", "foo", "bar"), "cookie was added");
+
+  // Clear all session cookies.
+  Services.cookies.removeAll();
+  ok(!cookieExists("example.com", "foo", "bar"), "cookies were cleared");
+
+  // Cleanup.
+  yield promiseRemoveTab(gBrowser.selectedTab);
+});
deleted file mode 100644
--- a/browser/components/sessionstore/test/browser_cookies.sjs
+++ /dev/null
@@ -1,26 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-Components.utils.importGlobalProperties(["URLSearchParams"]);
-
-function handleRequest(req, resp) {
-  resp.setStatusLine(req.httpVersion, 200);
-
-  let params = new URLSearchParams(req.queryString);
-  let value = params.get("value");
-
-  let domain = "";
-  if (params.has("domain")) {
-    domain = `; Domain=${params.get("domain")}`;
-  }
-
-  let secure = "";
-  if (params.has("secure")) {
-    secure = "; Secure";
-  }
-
-  resp.setHeader("Set-Cookie", `foobar=${value}${domain}${secure}`);
-  resp.write("<meta charset=utf-8>hi");
-}
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_cookies_legacy.js
@@ -0,0 +1,71 @@
+"use strict";
+
+function createTestState() {
+  let r = Math.round(Math.random() * 100000);
+
+  let cookie = {
+    host: "http://example.com",
+    path: "/",
+    name: `name${r}`,
+    value: `value${r}`
+  };
+
+  let state = {
+    windows: [{
+      tabs: [{entries: [{url: "about:robots", triggeringPrincipal_base64}]}],
+      cookies: [cookie]
+    }]
+  };
+
+  return [state, cookie];
+}
+
+function waitForNewCookie({host, name, value}) {
+  info(`waiting for cookie ${name}=${value} from ${host}...`);
+
+  return new Promise(resolve => {
+    Services.obs.addObserver(function observer(subj, topic, data) {
+      if (data != "added") {
+        return;
+      }
+
+      let cookie = subj.QueryInterface(Ci.nsICookie2);
+      if (cookie.host == host && cookie.name == name && cookie.value == value) {
+        ok(true, "cookie added by the cookie service");
+        Services.obs.removeObserver(observer, topic);
+        resolve();
+      }
+    }, "cookie-changed", false);
+  });
+}
+
+// Setup and cleanup.
+add_task(function* test_setup() {
+  Services.cookies.removeAll();
+
+  registerCleanupFunction(() => {
+    Services.cookies.removeAll();
+  });
+});
+
+// Test that calling ss.setWindowState() with a legacy state object that
+// contains cookies in the window state restores session cookies properly.
+add_task(function* test_window() {
+  let [state, cookie] = createTestState();
+  let win = yield promiseNewWindowLoaded();
+
+  let promiseCookie = waitForNewCookie(cookie);
+  ss.setWindowState(win, JSON.stringify(state), true);
+  yield promiseCookie;
+
+  yield BrowserTestUtils.closeWindow(win);
+});
+
+// Test that calling ss.setBrowserState() with a legacy state object that
+// contains cookies in the window state restores session cookies properly.
+add_task(function* test_browser() {
+  let backupState = ss.getBrowserState();
+  let [state, cookie] = createTestState();
+  yield Promise.all([waitForNewCookie(cookie), promiseBrowserState(state)]);
+  yield promiseBrowserState(backupState);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_cookies_privacy.js
@@ -0,0 +1,110 @@
+"use strict";
+
+// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision.
+const MAX_EXPIRY = Math.pow(2, 62);
+
+function addCookie(scheme, secure = false) {
+  let cookie = createTestCookie("http", secure);
+  Services.cookies.add(cookie.host, cookie.path, cookie.name, cookie.value,
+                       cookie.secure, /* isHttpOnly = */ false,
+                       /* isSession = */ true, MAX_EXPIRY,
+                       /* originAttributes = */ {});
+  return cookie;
+}
+
+function createTestCookie(scheme, secure = false) {
+  let r = Math.round(Math.random() * 100000);
+
+  let cookie = {
+    host: `${scheme}://example.com`,
+    path: "/",
+    name: `name${r}`,
+    value: `value${r}`,
+    secure: secure
+  };
+
+  return cookie;
+}
+
+function getCookie() {
+  let state = JSON.parse(ss.getBrowserState());
+  let cookies = state.cookies || [];
+  return cookies[0];
+}
+
+function compareCookies(a) {
+  let b = getCookie();
+  return a.host == b.host && a.name == b.name && a.value == b.value;
+}
+
+// Setup and cleanup.
+add_task(function* test_setup() {
+  Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+    Services.cookies.removeAll();
+  });
+});
+
+// Test privacy_level=none (default). We store all session cookies.
+add_task(function* test_level_none() {
+  Services.cookies.removeAll();
+
+  // Set level=none, store all cookies.
+  Services.prefs.setIntPref("browser.sessionstore.privacy_level", 0);
+
+  // With the default privacy level we collect all cookies.
+  ok(compareCookies(addCookie("http")), "non-secure http cookie stored");
+  Services.cookies.removeAll();
+
+  // With the default privacy level we collect all cookies.
+  ok(compareCookies(addCookie("https")), "non-secure https cookie stored");
+  Services.cookies.removeAll();
+
+  // With the default privacy level we collect all cookies.
+  ok(compareCookies(addCookie("https", true)), "secure https cookie stored");
+  Services.cookies.removeAll();
+});
+
+// Test privacy_level=encrypted. We store all non-secure session cookies.
+add_task(function* test_level_encrypted() {
+  Services.cookies.removeAll();
+
+  // Set level=encrypted, don't store any secure cookies.
+  Services.prefs.setIntPref("browser.sessionstore.privacy_level", 1);
+
+  // With level=encrypted, non-secure cookies will be stored.
+  ok(compareCookies(addCookie("http")), "non-secure http cookie stored");
+  Services.cookies.removeAll();
+
+  // With level=encrypted, non-secure cookies will be stored,
+  // even if sent by an HTTPS site.
+  ok(compareCookies(addCookie("https")), "non-secure https cookie stored");
+  Services.cookies.removeAll();
+
+  // With level=encrypted, non-secure cookies will be stored,
+  // even if sent by an HTTPS site.
+  ok(addCookie("https", true) && !getCookie(), "secure https cookie not stored");
+  Services.cookies.removeAll();
+});
+
+// Test privacy_level=full. We store no session cookies.
+add_task(function* test_level_full() {
+  Services.cookies.removeAll();
+
+  // Set level=full, don't store any cookies.
+  Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);
+
+  // With level=full we must not store any cookies.
+  ok(addCookie("http") && !getCookie(), "non-secure http cookie not stored");
+  Services.cookies.removeAll();
+
+  // With level=full we must not store any cookies.
+  ok(addCookie("https") && !getCookie(), "non-secure https cookie not stored");
+  Services.cookies.removeAll();
+
+  // With level=full we must not store any cookies.
+  ok(addCookie("https", true) && !getCookie(), "secure https cookie not stored");
+  Services.cookies.removeAll();
+});
--- a/browser/components/sessionstore/test/browser_sessionStoreContainer.js
+++ b/browser/components/sessionstore/test/browser_sessionStoreContainer.js
@@ -108,17 +108,18 @@ add_task(function* test() {
   const ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
   const { TabStateFlusher } = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
 
   // Make sure userContext is enabled.
   yield SpecialPowers.pushPrefEnv({
     "set": [ [ "privacy.userContext.enabled", true ] ]
   });
 
-  let lastSessionRestore;
+  Services.cookies.removeAll();
+
   for (let userContextId of Object.keys(USER_CONTEXTS)) {
     // Load the page in 3 different contexts and set a cookie
     // which should only be visible in that context.
     let cookie = USER_CONTEXTS[userContextId];
 
     // Open our tab in the given user context.
     let { tab, browser } = yield* openTabInUserContext(userContextId);
 
@@ -126,18 +127,16 @@ add_task(function* test() {
       waitForNewCookie(),
       ContentTask.spawn(browser, cookie,
         passedCookie => content.document.cookie = passedCookie)
     ]);
 
     // Ensure the tab's session history is up-to-date.
     yield TabStateFlusher.flush(browser);
 
-    lastSessionRestore = ss.getWindowState(window);
-
     // Remove the tab.
     gBrowser.removeTab(tab);
   }
 
-  let state = JSON.parse(lastSessionRestore);
-  is(state.windows[0].cookies.length, USER_CONTEXTS.length,
+  let state = JSON.parse(ss.getBrowserState());
+  is(state.cookies.length, USER_CONTEXTS.length,
     "session restore should have each container's cookie");
 });