Bug 1264192 - Adjust cookie eviction heuristics when exceeding the maximum cookies allowed per host. r=ehsan a=sylvestre
authorJosh Matthews <josh@joshmatthews.net>
Fri, 09 Sep 2016 16:29:15 -0400
changeset 333134 56321464405dc99b8a9b68c0fc60e9d628f75bf5
parent 333133 8663d41e015f3c9933a08a65c8349b86f2263354
child 333135 2f0c6a3164536083c2ba2e3c0f55b993f27f2085
push id10006
push userkwierso@gmail.com
push dateMon, 12 Sep 2016 19:20:48 +0000
treeherdermozilla-aurora@2f0c6a316453 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan, sylvestre
bugs1264192
milestone50.0a2
Bug 1264192 - Adjust cookie eviction heuristics when exceeding the maximum cookies allowed per host. r=ehsan a=sylvestre If no expired cookies exist, in order of preference, evict the oldest: * session cookie with a non-matching path * session cookie with a matching path * non-session cookie with a non-matching path * non-session cookie with a matching path This replaces the previous heuristic of evicting the oldest cookie, irregardless of any other attributes, if no expired cookies were present. This ensures that cookies that are already considered transient by web applications will be removed first, followed by cookies that are unrelated to the response that is adding new cookies. * * * Bug 1264192 - Interdiff
netwerk/cookie/nsCookie.cpp
netwerk/cookie/nsCookie.h
netwerk/cookie/nsCookieService.cpp
netwerk/cookie/nsCookieService.h
netwerk/cookie/test/unit/test_eviction.js
netwerk/cookie/test/unit/xpcshell.ini
--- a/netwerk/cookie/nsCookie.cpp
+++ b/netwerk/cookie/nsCookie.cpp
@@ -4,18 +4,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/ToJSValue.h"
 #include "nsAutoPtr.h"
 #include "nsCookie.h"
 #include "nsUTF8ConverterService.h"
 #include <stdlib.h>
 
-static const int64_t kCookieStaleThreshold = 60 * PR_USEC_PER_SEC; // 1 minute in microseconds
-
 /******************************************************************************
  * nsCookie:
  * string helper impl
  ******************************************************************************/
 
 // copy aSource strings into contiguous storage provided in aDest1,
 // providing terminating nulls for each destination string.
 static inline void
@@ -125,17 +123,17 @@ nsCookie::SizeOfIncludingThis(mozilla::M
     return aMallocSizeOf(this);
 }
 
 bool
 nsCookie::IsStale() const
 {
   int64_t currentTimeInUsec = PR_Now();
 
-  return currentTimeInUsec - LastAccessed() > kCookieStaleThreshold;
+  return currentTimeInUsec - LastAccessed() > mCookieStaleThreshold * PR_USEC_PER_SEC;
 }
 
 /******************************************************************************
  * nsCookie:
  * xpcom impl
  ******************************************************************************/
 
 // xpcom getters
--- a/netwerk/cookie/nsCookie.h
+++ b/netwerk/cookie/nsCookie.h
@@ -7,16 +7,17 @@
 #define nsCookie_h__
 
 #include "nsICookie.h"
 #include "nsICookie2.h"
 #include "nsString.h"
 
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/BasePrincipal.h"
+#include "mozilla/Preferences.h"
 
 using mozilla::OriginAttributes;
 
 /** 
  * The nsCookie class is the main cookie storage medium for use within cookie
  * code. It implements nsICookie2, which extends nsICookie, a frozen interface
  * for xpcom access of cookie objects.
  */
@@ -51,16 +52,18 @@ class nsCookie : public nsICookie2
      : mName(aName)
      , mValue(aValue)
      , mHost(aHost)
      , mPath(aPath)
      , mEnd(aEnd)
      , mExpiry(aExpiry)
      , mLastAccessed(aLastAccessed)
      , mCreationTime(aCreationTime)
+       // Defaults to 60s
+     , mCookieStaleThreshold(mozilla::Preferences::GetInt("network.cookie.staleThreshold", 60))
      , mIsSession(aIsSession)
      , mIsSecure(aIsSecure)
      , mIsHttpOnly(aIsHttpOnly)
      , mOriginAttributes(aOriginAttributes)
     {
     }
 
   public:
@@ -122,15 +125,16 @@ class nsCookie : public nsICookie2
     const char  *mName;
     const char  *mValue;
     const char  *mHost;
     const char  *mPath;
     const char  *mEnd;
     int64_t      mExpiry;
     int64_t      mLastAccessed;
     int64_t      mCreationTime;
+    int64_t      mCookieStaleThreshold;
     bool mIsSession;
     bool mIsSecure;
     bool mIsHttpOnly;
     mozilla::OriginAttributes mOriginAttributes;
 };
 
 #endif // nsCookie_h__
--- a/netwerk/cookie/nsCookieService.cpp
+++ b/netwerk/cookie/nsCookieService.cpp
@@ -3482,17 +3482,17 @@ nsCookieService::AddInternal(const nsCoo
         "cookie has already expired");
       return;
     }
 
     // check if we have to delete an old cookie.
     nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey);
     if (entry && entry->GetCookies().Length() >= mMaxCookiesPerHost) {
       nsListIter iter;
-      FindStaleCookie(entry, currentTime, iter);
+      FindStaleCookie(entry, currentTime, aHostURI, iter);
       oldCookie = iter.Cookie();
 
       // remove the oldest cookie from the domain
       RemoveCookieFromList(iter);
       COOKIE_LOGEVICTED(oldCookie, "Too many cookies for this domain");
       purgedList = CreatePurgeList(oldCookie);
 
     } else if (mDBState->cookieCount >= ADD_TEN_PERCENT(mMaxNumberOfCookies)) {
@@ -4350,39 +4350,119 @@ nsCookieService::CookieExists(nsICookie2
   return NS_OK;
 }
 
 // For a given base domain, find either an expired cookie or the oldest cookie
 // by lastAccessed time.
 void
 nsCookieService::FindStaleCookie(nsCookieEntry *aEntry,
                                  int64_t aCurrentTime,
+                                 nsIURI* aSource,
                                  nsListIter &aIter)
 {
-  aIter.entry = nullptr;
-
-  int64_t oldestTime = 0;
+  bool requireHostMatch = true;
+  nsAutoCString baseDomain, sourceHost, sourcePath;
+  if (aSource) {
+    GetBaseDomain(aSource, baseDomain, requireHostMatch);
+    aSource->GetAsciiHost(sourceHost);
+    aSource->GetPath(sourcePath);
+  }
+
   const nsCookieEntry::ArrayType &cookies = aEntry->GetCookies();
+
+  int64_t oldestNonMatchingSessionCookieTime = 0;
+  nsListIter oldestNonMatchingSessionCookie;
+  oldestNonMatchingSessionCookie.entry = nullptr;
+
+  int64_t oldestSessionCookieTime = 0;
+  nsListIter oldestSessionCookie;
+  oldestSessionCookie.entry = nullptr;
+
+  int64_t oldestNonMatchingNonSessionCookieTime = 0;
+  nsListIter oldestNonMatchingNonSessionCookie;
+  oldestNonMatchingNonSessionCookie.entry = nullptr;
+
+  int64_t oldestCookieTime = 0;
+  nsListIter oldestCookie;
+  oldestCookie.entry = nullptr;
+
   for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
     nsCookie *cookie = cookies[i];
 
     // If we found an expired cookie, we're done.
     if (cookie->Expiry() <= aCurrentTime) {
       aIter.entry = aEntry;
       aIter.index = i;
       return;
     }
 
+    // Update our various records of oldest cookies fitting several restrictions:
+    // * session cookies
+    // * non-session cookies
+    // * cookies with paths and domains that don't match the cookie triggering this purge
+
+    uint32_t cookiePathLen = cookie->Path().Length();
+    if (cookiePathLen > 0 && cookie->Path().Last() == '/')
+      --cookiePathLen;
+
+    // This cookie is a candidate for eviction if we have no information about
+    // the source request, or if it is not a path or domain match against the
+    // source request.
+    bool isPrimaryEvictionCandidate = true;
+    if (aSource) {
+      bool pathMatches = StringBeginsWith(sourcePath, Substring(cookie->Path(), 0, cookiePathLen));
+      bool domainMatches = cookie->RawHost() == sourceHost ||
+          (cookie->IsDomain() && StringEndsWith(sourceHost, cookie->Host()));
+      isPrimaryEvictionCandidate = !pathMatches || !domainMatches;
+    }
+
+    int64_t lastAccessed = cookie->LastAccessed();
+    if (cookie->IsSession()) {
+      if (!oldestSessionCookie.entry || oldestSessionCookieTime > lastAccessed) {
+        oldestSessionCookieTime = lastAccessed;
+        oldestSessionCookie.entry = aEntry;
+        oldestSessionCookie.index = i;
+      }
+
+      if (isPrimaryEvictionCandidate &&
+          (!oldestNonMatchingSessionCookie.entry ||
+           oldestNonMatchingSessionCookieTime > lastAccessed)) {
+        oldestNonMatchingSessionCookieTime = lastAccessed;
+        oldestNonMatchingSessionCookie.entry = aEntry;
+        oldestNonMatchingSessionCookie.index = i;
+      }
+    } else if (isPrimaryEvictionCandidate &&
+               (!oldestNonMatchingNonSessionCookie.entry ||
+                oldestNonMatchingNonSessionCookieTime > lastAccessed)) {
+      oldestNonMatchingNonSessionCookieTime = lastAccessed;
+      oldestNonMatchingNonSessionCookie.entry = aEntry;
+      oldestNonMatchingNonSessionCookie.index = i;
+    }
+
     // Check if we've found the oldest cookie so far.
-    if (!aIter.entry || oldestTime > cookie->LastAccessed()) {
-      oldestTime = cookie->LastAccessed();
-      aIter.entry = aEntry;
-      aIter.index = i;
+    if (!oldestCookie.entry || oldestCookieTime > lastAccessed) {
+      oldestCookieTime = lastAccessed;
+      oldestCookie.entry = aEntry;
+      oldestCookie.index = i;
     }
   }
+
+  // Prefer to evict the oldest session cookies with a non-matching path/domain,
+  // followed by the oldest session cookie with a matching path/domain,
+  // followed by the oldest non-session cookie with a non-matching path/domain,
+  // resorting to the oldest non-session cookie with a matching path/domain.
+  if (oldestNonMatchingSessionCookie.entry) {
+    aIter = oldestNonMatchingSessionCookie;
+  } else if (oldestSessionCookie.entry) {
+    aIter = oldestSessionCookie;
+  } else if (oldestNonMatchingNonSessionCookie.entry) {
+    aIter = oldestNonMatchingNonSessionCookie;
+  } else {
+    aIter = oldestCookie;
+  }
 }
 
 // count the number of cookies stored by a particular host. this is provided by the
 // nsICookieManager2 interface.
 NS_IMETHODIMP
 nsCookieService::CountCookiesFromHost(const nsACString &aHost,
                                       uint32_t         *aCountFromHost)
 {
--- a/netwerk/cookie/nsCookieService.h
+++ b/netwerk/cookie/nsCookieService.h
@@ -307,17 +307,17 @@ class nsCookieService final : public nsI
     CookieStatus                  CheckPrefs(nsIURI *aHostURI, bool aIsForeign, bool aRequireHostMatch, const char *aCookieHeader);
     bool                          CheckDomain(nsCookieAttributes &aCookie, nsIURI *aHostURI, const nsCString &aBaseDomain, bool aRequireHostMatch);
     static bool                   CheckPath(nsCookieAttributes &aCookie, nsIURI *aHostURI);
     static bool                   CheckPrefixes(nsCookieAttributes &aCookie, bool aSecureRequest);
     static bool                   GetExpiry(nsCookieAttributes &aCookie, int64_t aServerTime, int64_t aCurrentTime);
     void                          RemoveAllFromMemory();
     already_AddRefed<nsIArray>    PurgeCookies(int64_t aCurrentTimeInUsec);
     bool                          FindCookie(const nsCookieKey& aKey, const nsAFlatCString &aHost, const nsAFlatCString &aName, const nsAFlatCString &aPath, nsListIter &aIter);
-    static void                   FindStaleCookie(nsCookieEntry *aEntry, int64_t aCurrentTime, nsListIter &aIter);
+    void                          FindStaleCookie(nsCookieEntry *aEntry, int64_t aCurrentTime, nsIURI* aSource, nsListIter &aIter);
     void                          NotifyRejected(nsIURI *aHostURI);
     void                          NotifyThirdParty(nsIURI *aHostURI, bool aAccepted, nsIChannel *aChannel);
     void                          NotifyChanged(nsISupports *aSubject, const char16_t *aData);
     void                          NotifyPurged(nsICookie2* aCookie);
     already_AddRefed<nsIArray>    CreatePurgeList(nsICookie2* aCookie);
     void                          UpdateCookieOldestTime(DBState* aDBState, nsCookie* aCookie);
 
     nsresult                      GetCookiesWithOriginAttributes(const mozilla::OriginAttributesPattern& aPattern, nsISimpleEnumerator **aEnumerator);
new file mode 100644
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_eviction.js
@@ -0,0 +1,272 @@
+var {utils: Cu, interfaces: Ci, classes: Cc} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const BASE_HOSTNAMES = ["example.org", "example.co.uk"];
+const SUBDOMAINS = ["", "pub.", "www.", "other."];
+
+const cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService);
+const cm = cs.QueryInterface(Ci.nsICookieManager2);
+
+function run_test() {
+    var tests = [];
+    Services.prefs.setIntPref("network.cookie.staleThreshold", 0);
+    for (var host of BASE_HOSTNAMES) {
+        var base = SUBDOMAINS[0] + host;
+        var sub = SUBDOMAINS[1] + host;
+        var other = SUBDOMAINS[2] + host;
+        var another = SUBDOMAINS[3] + host;
+        tests.push([host, test_basic_eviction.bind(this, base, sub, other, another)]);
+        add_task(function* a() {
+            var t = tests.splice(0, 1)[0];
+            do_print('testing with host ' + t[0]);
+            yield t[1]();
+            cm.removeAll();
+        });
+        tests.push([host, test_domain_or_path_matches_not_both.bind(this, base, sub, other, another)]);
+        add_task(function*() {
+            var t = tests.splice(0, 1)[0];
+            do_print('testing with host ' + t[0]);
+            yield t[1]();
+            cm.removeAll();
+        });
+    }
+    add_task(function*() {
+        yield test_localdomain();
+    });
+
+    run_next_test();
+}
+
+// Verify that subdomains of localhost are treated as separate hosts and aren't considered
+// candidates for eviction.
+function* test_localdomain() {
+    Services.prefs.setIntPref("network.cookie.maxPerHost", 2);
+
+    const BASE_URI = Services.io.newURI("http://localhost", null, null);
+    const BASE_BAR = Services.io.newURI("http://localhost/bar", null, null);
+    const OTHER_URI = Services.io.newURI("http://other.localhost", null, null);
+    const OTHER_BAR = Services.io.newURI("http://other.localhost/bar", null, null);
+    
+    yield setCookie("session_no_path", null, null, null, BASE_URI);
+    yield setCookie("session_bar_path", null, "/bar", null, BASE_BAR);
+
+    yield setCookie("session_no_path", null, null, null, OTHER_URI);
+    yield setCookie("session_bar_path", null, "/bar", null, OTHER_BAR);
+
+    verifyCookies(['session_no_path',
+                   'session_bar_path'], BASE_URI);
+    verifyCookies(['session_no_path',
+                   'session_bar_path'], OTHER_URI);
+
+    yield setCookie("session_another_no_path", null, null, null, BASE_URI);
+    verifyCookies(['session_no_path',
+                   'session_another_no_path'], BASE_URI);
+
+    yield setCookie("session_another_no_path", null, null, null, OTHER_URI);
+    verifyCookies(['session_no_path',
+                   'session_another_no_path'], OTHER_URI);
+}
+
+// Ensure that cookies are still considered candidates for eviction if either the domain
+// or path matches, but not both.
+function* test_domain_or_path_matches_not_both(base_host,
+                                               subdomain_host,
+                                               other_subdomain_host,
+                                               another_subdomain_host) {
+    Services.prefs.setIntPref("network.cookie.maxPerHost", 2);
+
+    const BASE_URI = Services.io.newURI("http://" + base_host, null, null);
+    const PUB_FOO_PATH = Services.io.newURI("http://" + subdomain_host + "/foo", null, null);
+    const WWW_BAR_PATH = Services.io.newURI("http://" + other_subdomain_host + "/bar", null, null);
+    const OTHER_BAR_PATH = Services.io.newURI("http://" + another_subdomain_host + "/bar", null, null);
+    const PUB_BAR_PATH = Services.io.newURI("http://" + subdomain_host + "/bar", null, null);
+    const WWW_FOO_PATH = Services.io.newURI("http://" + other_subdomain_host + "/foo", null, null);
+
+    yield setCookie("session_pub_with_foo_path", subdomain_host, "/foo", null, PUB_FOO_PATH);
+    yield setCookie("session_www_with_bar_path", other_subdomain_host, "/bar", null, WWW_BAR_PATH);
+    verifyCookies(['session_pub_with_foo_path',
+                   'session_www_with_bar_path'], BASE_URI);
+
+    yield setCookie("session_pub_with_bar_path", subdomain_host, "/bar", null, PUB_BAR_PATH);
+    verifyCookies(['session_www_with_bar_path',
+                   'session_pub_with_bar_path'], BASE_URI);
+
+    yield setCookie("session_other_with_bar_path", another_subdomain_host, "/bar", null, OTHER_BAR_PATH);
+    verifyCookies(['session_pub_with_bar_path',
+                   'session_other_with_bar_path'], BASE_URI);
+}
+
+function* test_basic_eviction(base_host, subdomain_host, other_subdomain_host) {
+    Services.prefs.setIntPref("network.cookie.maxPerHost", 5);
+
+    const BASE_URI = Services.io.newURI("http://" + base_host, null, null);
+    const SUBDOMAIN_URI = Services.io.newURI("http://" + subdomain_host, null, null);
+    const OTHER_SUBDOMAIN_URI = Services.io.newURI("http://" + other_subdomain_host, null, null);
+    const FOO_PATH = Services.io.newURI("http://" + base_host + "/foo", null, null);
+    const BAR_PATH = Services.io.newURI("http://" + base_host + "/bar", null, null);
+    const ALL_SUBDOMAINS = '.' + base_host;
+    const OTHER_SUBDOMAIN = other_subdomain_host;
+
+    // Initialize the set of cookies with a mix of non-session cookies with no path,
+    // and session cookies with explicit paths. Any subsequent cookies added will cause
+    // existing cookies to be evicted.
+    yield setCookie("non_session_non_path_non_domain", null, null, 100000, BASE_URI);
+    yield setCookie("non_session_non_path_subdomain", ALL_SUBDOMAINS, null, 100000, SUBDOMAIN_URI);
+    yield setCookie("session_non_path_pub_domain", OTHER_SUBDOMAIN, null, null, OTHER_SUBDOMAIN_URI);
+    yield setCookie("session_foo_path", null, "/foo", null, FOO_PATH);
+    yield setCookie("session_bar_path", null, "/bar", null, BAR_PATH);
+    verifyCookies(['non_session_non_path_non_domain',
+                   'non_session_non_path_subdomain',
+                   'session_non_path_pub_domain',
+                   'session_foo_path',
+                   'session_bar_path'], BASE_URI);
+
+    // Ensure that cookies set for the / path appear more recent.
+    cs.getCookieString(OTHER_SUBDOMAIN_URI, null)
+    verifyCookies(['non_session_non_path_non_domain',
+                   'session_foo_path',
+                   'session_bar_path',
+                   'non_session_non_path_subdomain',
+                   'session_non_path_pub_domain'], BASE_URI);
+
+    // Evict oldest session cookie that does not match example.org/foo (session_bar_path)
+    yield setCookie("session_foo_path_2", null, "/foo", null, FOO_PATH);
+    verifyCookies(['non_session_non_path_non_domain',
+                   'session_foo_path',
+                   'non_session_non_path_subdomain',
+                   'session_non_path_pub_domain',
+                   'session_foo_path_2'], BASE_URI);
+
+    // Evict oldest session cookie that does not match example.org/bar (session_foo_path)
+    yield setCookie("session_bar_path_2", null, "/bar", null, BAR_PATH);
+    verifyCookies(['non_session_non_path_non_domain',
+                   'non_session_non_path_subdomain',
+                   'session_non_path_pub_domain',
+                   'session_foo_path_2',
+                   'session_bar_path_2'], BASE_URI);
+
+    // Evict oldest session cookie that does not match example.org/ (session_non_path_pub_domain)
+    yield setCookie("non_session_non_path_non_domain_2", null, null, 100000, BASE_URI);
+    verifyCookies(['non_session_non_path_non_domain',
+                   'non_session_non_path_subdomain',
+                   'session_foo_path_2',
+                   'session_bar_path_2',
+                   'non_session_non_path_non_domain_2'], BASE_URI);
+
+    // Evict oldest session cookie that does not match example.org/ (session_foo_path_2)
+    yield setCookie("session_non_path_non_domain_3", null, null, null, BASE_URI);
+    verifyCookies(['non_session_non_path_non_domain',
+                   'non_session_non_path_subdomain',
+                   'session_bar_path_2',
+                   'non_session_non_path_non_domain_2',
+                   'session_non_path_non_domain_3'], BASE_URI);
+
+    // Evict oldest session cookie; all such cookies match example.org/bar (session_bar_path_2)
+    yield setCookie("non_session_non_path_non_domain_3", null, null, 100000, BAR_PATH);
+    verifyCookies(['non_session_non_path_non_domain',
+                   'non_session_non_path_subdomain',
+                   'non_session_non_path_non_domain_2',
+                   'session_non_path_non_domain_3',
+                   'non_session_non_path_non_domain_3'], BASE_URI);
+
+    // Evict oldest session cookie, even though it matches pub.example.org (session_non_path_non_domain_3)
+    yield setCookie("non_session_non_path_pub_domain", null, null, 100000, OTHER_SUBDOMAIN_URI);
+    verifyCookies(['non_session_non_path_non_domain',
+                   'non_session_non_path_subdomain',
+                   'non_session_non_path_non_domain_2',
+                   'non_session_non_path_non_domain_3',
+                   'non_session_non_path_pub_domain'], BASE_URI);
+
+    // All session cookies have been evicted.
+    // Evict oldest non-session non-domain-matching cookie (non_session_non_path_pub_domain)
+    yield setCookie("non_session_bar_path_non_domain", null, '/bar', 100000, BAR_PATH);
+    verifyCookies(['non_session_non_path_non_domain',
+                   'non_session_non_path_subdomain',
+                   'non_session_non_path_non_domain_2',
+                   'non_session_non_path_non_domain_3',
+                   'non_session_bar_path_non_domain'], BASE_URI);
+
+    // Evict oldest non-session non-path-matching cookie (non_session_bar_path_non_domain)
+    yield setCookie("non_session_non_path_non_domain_4", null, null, 100000, BASE_URI);
+    verifyCookies(['non_session_non_path_non_domain',
+                   'non_session_non_path_subdomain',
+                   'non_session_non_path_non_domain_2',
+                   'non_session_non_path_non_domain_3',
+                   'non_session_non_path_non_domain_4'], BASE_URI);
+
+    // At this point all remaining cookies are non-session cookies, have a path of /,
+    // and either don't have a domain or have one that matches subdomains.
+    // They will therefore be evicted from oldest to newest if all new cookies added share
+    // similar characteristics.
+}
+
+// Verify that the given cookie names exist, and are ordered from least to most recently accessed
+function verifyCookies(names, uri) {
+    do_check_eq(cm.countCookiesFromHost(uri.host), names.length);
+    let cookies = cm.getCookiesFromHost(uri.host, {});
+    let actual_cookies = [];
+    while (cookies.hasMoreElements()) {
+        let cookie = cookies.getNext().QueryInterface(Ci.nsICookie2);
+        actual_cookies.push(cookie);
+    }
+    if (names.length != actual_cookies.length) {
+        let left = names.filter(function(n) {
+            return actual_cookies.findIndex(function(c) {
+                return c.name == n;
+            }) == -1;
+        });
+        let right = actual_cookies.filter(function(c) {
+            return names.findIndex(function(n) {
+                return c.name == n;
+            }) == -1;
+        }).map(function(c) { return c.name });
+        if (left.length) {
+            do_print("unexpected cookies: " + left);
+        }
+        if (right.length) {
+            do_print("expected cookies: " + right);
+        }
+    }
+    do_check_eq(names.length, actual_cookies.length);
+    actual_cookies.sort(function(a, b) {
+        if (a.lastAccessed < b.lastAccessed)
+            return -1;
+        if (a.lastAccessed > b.lastAccessed)
+            return 1;
+        return 0;
+    });
+    for (var i = 0; i < names.length; i++) {
+        do_check_eq(names[i], actual_cookies[i].name);
+        do_check_eq(names[i].startsWith('session'), actual_cookies[i].isSession);
+    }
+}
+
+var lastValue = 0
+function* setCookie(name, domain, path, maxAge, url) {
+    let value = name + "=" + ++lastValue;
+    var s = 'setting cookie ' + value;
+    if (domain) {
+        value += "; Domain=" + domain;
+        s += ' (d=' + domain + ')';
+    }
+    if (path) {
+        value += "; Path=" + path;
+        s += ' (p=' + path + ')';
+    }
+    if (maxAge) {
+        value += "; Max-Age=" + maxAge;
+        s += ' (non-session)';
+    } else {
+        s += ' (session)';
+    }
+    s += ' for ' + url.spec;
+    do_print(s);
+    cs.setCookieStringFromHttp(url, null, null, value, null, null);
+    return new Promise(function(resolve) {
+        // Windows XP has low precision timestamps that cause our cookie eviction
+        // algorithm to produce different results from other platforms. We work around
+        // this by ensuring that there's a clear gap between each cookie update.
+        do_timeout(10, resolve);
+    })
+}
--- a/netwerk/cookie/test/unit/xpcshell.ini
+++ b/netwerk/cookie/test/unit/xpcshell.ini
@@ -3,8 +3,9 @@ head =
 tail = 
 skip-if = toolkit == 'gonk'
 
 [test_bug643051.js]
 [test_bug1155169.js]
 [test_bug1267910.js]
 [test_parser_0001.js]
 [test_parser_0019.js]
+[test_eviction.js]
\ No newline at end of file