diff --git a/core/core.services.yml b/core/core.services.yml
index 1d4d172..72a4a1d 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -737,6 +737,17 @@ services:
     factory_service: authentication
     arguments: ['@request']
     synchronized: true
+  session_handler:
+    class: Drupal\Core\Session\SessionHandler
+    arguments: ['@request', '@database']
+    #scope: request
+    #synchronized: true
+    # Since PHP 5, write and close handlers are called after destructing objects,
+    # so destructors can use sessions, but the session handler cannot use
+    # objects. Thus, terminate with the kernel, before objects are destructed.
+    tags:
+      - { name: persist }
+      - { name: needs_destruction }
   asset.css.collection_renderer:
     class: Drupal\Core\Asset\CssCollectionRenderer
     arguments: [ '@state' ]
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 5244ced..a7f3c4b 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -14,6 +14,7 @@
 use Drupal\Core\Installer\Exception\NoProfilesException;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Language\LanguageManager;
+use Drupal\Core\Session\UserSession;
 use Drupal\Core\StringTranslation\Translator\FileTranslation;
 use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
@@ -123,6 +124,17 @@ function install_drupal($settings = array()) {
   // installations can send output to the browser or redirect the user to the
   // next page.
   if ($state['interactive']) {
+    // Manually write and close the session, because the installer does not use
+    // a proper kernel yet, which would trigger the terminate event.
+    // @see drupal_handle_request()
+    // @todo Kernelize the installer.
+    if (\Drupal::hasService('session_handler')) {
+      // @todo SessionHandler::destruct() calls drupal_session_commit(), but
+      //   calling destruct() here causes the installer batch to stop with
+      //   "No active batch" before it starts.
+      // \Drupal::service('session_handler')->destruct();
+      drupal_session_commit();
+    }
     if ($state['parameters_changed']) {
       // Redirect to the correct page if the URL parameters have changed.
       install_goto(install_redirect_url($state));
@@ -1776,9 +1788,23 @@ function install_load_profile(&$install_state) {
  *   An array of information about the current installation state.
  */
 function install_bootstrap_full() {
+  global $user;
+
   drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
+
+  // Masquerade as uid 1. This session will be converted into the actual session
+  // of uid 1 after submitting the site configuration form.
+  // @see install_configure_form_submit()
+  $user = new UserSession(array(
+    'uid' => 1,
+    'hostname' => '',
+    'roles' => array(DRUPAL_AUTHENTICATED_RID),
+  ));
+
   require_once DRUPAL_ROOT . '/' . Settings::get('session_inc', 'core/includes/session.inc');
   drupal_session_initialize();
+  // Immediately start a session; the next installer steps need one.
+  drupal_session_start();
 }
 
 /**
diff --git a/core/includes/session.inc b/core/includes/session.inc
index 8e95036..eb63d11 100644
--- a/core/includes/session.inc
+++ b/core/includes/session.inc
@@ -3,248 +3,27 @@
 /**
  * @file
  * User session handling functions.
- *
- * The user-level session storage handlers:
- * - _drupal_session_open()
- * - _drupal_session_close()
- * - _drupal_session_read()
- * - _drupal_session_write()
- * - _drupal_session_destroy()
- * - _drupal_session_garbage_collection()
- * are assigned by session_set_save_handler() in bootstrap.inc and are called
- * automatically by PHP. These functions should not be called directly. Session
- * data should instead be accessed via the $_SESSION superglobal.
  */
 
 use Drupal\Component\Utility\Crypt;
-use Drupal\Component\Utility\Settings;
-use Drupal\Core\Session\UserSession;
+use Drupal\Core\Session\SessionHandler;
 use Drupal\Core\Utility\Error;
 
 /**
- * Session handler assigned by session_set_save_handler().
- *
- * This function is used to handle any initialization, such as file paths or
- * database connections, that is needed before accessing session data. Drupal
- * does not need to initialize anything in this function.
- *
- * This function should not be called directly.
- *
- * @return
- *   This function will always return TRUE.
- */
-function _drupal_session_open() {
-  return TRUE;
-}
-
-/**
- * Session handler assigned by session_set_save_handler().
- *
- * This function is used to close the current session. Because Drupal stores
- * session data in the database immediately on write, this function does
- * not need to do anything.
- *
- * This function should not be called directly.
- *
- * @return
- *   This function will always return TRUE.
- */
-function _drupal_session_close() {
-  return TRUE;
-}
-
-/**
- * Reads an entire session from the database (internal use only).
- *
- * Also initializes the $user object for the user associated with the session.
- * This function is registered with session_set_save_handler() to support
- * database-backed sessions. It is called on every page load when PHP sets
- * up the $_SESSION superglobal.
- *
- * This function is an internal function and must not be called directly.
- * Doing so may result in logging out the current user, corrupting session data
- * or other unexpected behavior. Session data must always be accessed via the
- * $_SESSION superglobal.
- *
- * @param $sid
- *   The session ID of the session to retrieve.
- *
- * @return
- *   The user's session, or an empty string if no session exists.
- */
-function _drupal_session_read($sid) {
-  global $user;
-
-  // Write and Close handlers are called after destructing objects
-  // since PHP 5.0.5.
-  // Thus destructors can use sessions but session handler can't use objects.
-  // So we are moving session closure before destructing objects.
-  drupal_register_shutdown_function('session_write_close');
-
-  // Handle the case of first time visitors and clients that don't store
-  // cookies (eg. web crawlers).
-  $insecure_session_name = substr(session_name(), 1);
-  $cookies = \Drupal::request()->cookies;
-  if (!$cookies->has(session_name()) && !$cookies->has($insecure_session_name)) {
-    $user = new UserSession();
-    return '';
-  }
-
-  // Otherwise, if the session is still active, we have a record of the
-  // client's session in the database. If it's HTTPS then we are either have
-  // a HTTPS session or we are about to log in so we check the sessions table
-  // for an anonymous session with the non-HTTPS-only cookie. The session ID
-  // that is in the user's cookie is hashed before being stored in the database
-  // as a security measure. Thus, we have to hash it to match the database.
-  if (\Drupal::request()->isSecure()) {
-    $values = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array(':ssid' => Crypt::hashBase64($sid)))->fetchAssoc();
-    if (!$values) {
-      if ($cookies->has($insecure_session_name)) {
-        $values = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid AND s.uid = 0", array(
-        ':sid' => Crypt::hashBase64($cookies->get($insecure_session_name))))
-        ->fetchAssoc();
-      }
-    }
-  }
-  else {
-    $values = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => Crypt::hashBase64($sid)))->fetchAssoc();
-  }
-
-  // We found the client's session record and they are an authenticated,
-  // active user.
-  if ($values && $values['uid'] > 0 && $values['status'] == 1) {
-    // Add roles element to $user.
-    $rids = db_query("SELECT ur.rid FROM {users_roles} ur WHERE ur.uid = :uid", array(':uid' => $values['uid']))->fetchCol();
-    $values['roles'] = array_merge(array(DRUPAL_AUTHENTICATED_RID), $rids);
-    $user = new UserSession($values);
-  }
-  elseif ($values) {
-    // The user is anonymous or blocked. Only preserve two fields from the
-    // {sessions} table.
-    $user = new UserSession(array(
-      'session' => $values['session'],
-      'access' => $values['access'],
-    ));
-  }
-  else {
-    // The session has expired.
-    $user = new UserSession();
-  }
-
-  // Store the session that was read for comparison in _drupal_session_write().
-  $last_read = &drupal_static('drupal_session_last_read');
-  $last_read = array(
-    'sid' => $sid,
-    'value' => $user->session,
-  );
-
-  return $user->session;
-}
-
-/**
- * Writes an entire session to the database (internal use only).
- *
- * This function is registered with session_set_save_handler() to support
- * database-backed sessions.
- *
- * This function is an internal function and must not be called directly.
- * Doing so may result in corrupted session data or other unexpected behavior.
- * Session data must always be accessed via the $_SESSION superglobal.
- *
- * @param $sid
- *   The session ID of the session to write to.
- * @param $value
- *   Session data to write as a serialized string.
- *
- * @return
- *   Always returns TRUE.
- */
-function _drupal_session_write($sid, $value) {
-  global $user;
-
-  // The exception handler is not active at this point, so we need to do it
-  // manually.
-  try {
-    if (!drupal_save_session()) {
-      // We don't have anything to do if we are not allowed to save the session.
-      return;
-    }
-
-    // Check whether $_SESSION has been changed in this request.
-    $last_read = &drupal_static('drupal_session_last_read');
-    $is_changed = !isset($last_read) || $last_read['sid'] != $sid || $last_read['value'] !== $value;
-
-    // For performance reasons, do not update the sessions table, unless
-    // $_SESSION has changed or more than 180 has passed since the last update.
-    if ($is_changed || !$user->getLastAccessedTime() || REQUEST_TIME - $user->getLastAccessedTime() > Settings::get('session_write_interval', 180)) {
-      // Either ssid or sid or both will be added from $key below.
-      $fields = array(
-        'uid' => $user->id(),
-        'hostname' => \Drupal::request()->getClientIP(),
-        'session' => $value,
-        'timestamp' => REQUEST_TIME,
-      );
-
-      // Use the session ID as 'sid' and an empty string as 'ssid' by default.
-      // _drupal_session_read() does not allow empty strings so that's a safe
-      // default.
-      $key = array('sid' => Crypt::hashBase64($sid), 'ssid' => '');
-      // On HTTPS connections, use the session ID as both 'sid' and 'ssid'.
-      if (\Drupal::request()->isSecure()) {
-        $key['ssid'] = Crypt::hashBase64($sid);
-        $cookies = \Drupal::request()->cookies;
-        // The "secure pages" setting allows a site to simultaneously use both
-        // secure and insecure session cookies. If enabled and both cookies are
-        // presented then use both keys. The session ID from the cookie is
-        // hashed before being stored in the database as a security measure.
-        if (Settings::get('mixed_mode_sessions', FALSE)) {
-          $insecure_session_name = substr(session_name(), 1);
-          if ($cookies->has($insecure_session_name)) {
-            $key['sid'] = Crypt::hashBase64($cookies->get($insecure_session_name));
-          }
-        }
-      }
-      elseif (Settings::get('mixed_mode_sessions', FALSE)) {
-        unset($key['ssid']);
-      }
-
-      db_merge('sessions')
-        ->keys($key)
-        ->fields($fields)
-        ->execute();
-    }
-
-    // Likewise, do not update access time more than once per 180 seconds.
-    if ($user->isAuthenticated() && REQUEST_TIME - $user->getLastAccessedTime() > Settings::get('session_write_interval', 180)) {
-      db_update('users')
-        ->fields(array(
-          'access' => REQUEST_TIME
-        ))
-        ->condition('uid', $user->id())
-        ->execute();
-    }
-
-    return TRUE;
-  }
-  catch (Exception $exception) {
-    require_once __DIR__ . '/errors.inc';
-    // If we are displaying errors, then do so with no possibility of a further
-    // uncaught exception being thrown.
-    if (error_displayable()) {
-      print '<h1>Uncaught exception thrown in session handler.</h1>';
-      print '<p>' . Error::renderExceptionSafe($exception) . '</p><hr />';
-    }
-    return FALSE;
-  }
-}
-
-/**
  * Initializes the session handler, starting a session if needed.
  */
 function drupal_session_initialize() {
   global $user;
 
-  session_set_save_handler('_drupal_session_open', '_drupal_session_close', '_drupal_session_read', '_drupal_session_write', '_drupal_session_destroy', '_drupal_session_garbage_collection');
+  if (drupal_is_cli()) {
+    return;
+  }
+
+  // Register the default session handler.
+  // The session handler manually shuts down with the kernel.
+  // @see \Drupal\Core\Session\SessionHandler::destruct()
+  $handler = \Drupal::service('session_handler');
+  session_set_save_handler($handler, FALSE);
 
   $is_https = \Drupal::request()->isSecure();
   $cookies = \Drupal::request()->cookies;
@@ -307,12 +86,12 @@ function drupal_session_start() {
 function drupal_session_commit() {
   global $user;
 
-  if (!drupal_save_session()) {
+  if (!drupal_save_session() || drupal_is_cli()) {
     // We don't have anything to do if we are not allowed to save the session.
     return;
   }
 
-  if ($user->isAnonymous() && empty($_SESSION)) {
+  if ($user && $user->isAnonymous() && empty($_SESSION)) {
     // There is no session data to store, destroy the session if it was
     // previously started.
     if (drupal_session_started()) {
@@ -345,7 +124,7 @@ function drupal_session_started($set = NULL) {
   if (isset($set)) {
     $session_started = $set;
   }
-  return $session_started && session_id();
+  return $session_started && session_status() === \PHP_SESSION_ACTIVE;
 }
 
 /**
@@ -357,7 +136,7 @@ function drupal_session_regenerate() {
   global $user;
 
   // Nothing to do if we are not allowed to change the session.
-  if (!drupal_save_session()) {
+  if (!drupal_save_session() || drupal_is_cli()) {
     return;
   }
 
@@ -423,63 +202,6 @@ function drupal_session_regenerate() {
 }
 
 /**
- * Session handler assigned by session_set_save_handler().
- *
- * Cleans up a specific session.
- *
- * @param $sid
- *   Session ID.
- */
-function _drupal_session_destroy($sid) {
-  global $user;
-
-  // Nothing to do if we are not allowed to change the session.
-  if (!drupal_save_session()) {
-    return;
-  }
-
-  $is_https = \Drupal::request()->isSecure();
-  // Delete session data.
-  db_delete('sessions')
-    ->condition($is_https ? 'ssid' : 'sid', Crypt::hashBase64($sid))
-    ->execute();
-
-  // Reset $_SESSION and $user to prevent a new session from being started
-  // in drupal_session_commit().
-  $_SESSION = array();
-  $user = drupal_anonymous_user();
-
-  // Unset the session cookies.
-  _drupal_session_delete_cookie(session_name());
-  if ($is_https) {
-    _drupal_session_delete_cookie(substr(session_name(), 1), FALSE);
-  }
-  elseif (Settings::get('mixed_mode_sessions', FALSE)) {
-    _drupal_session_delete_cookie('S' . session_name(), TRUE);
-  }
-}
-
-/**
- * Deletes the session cookie.
- *
- * @param $name
- *   Name of session cookie to delete.
- * @param boolean $secure
- *   Force the secure value of the cookie.
- */
-function _drupal_session_delete_cookie($name, $secure = NULL) {
-  $cookies = \Drupal::request()->cookies;
-  if ($cookies->has($name) || (!\Drupal::request()->isSecure() && $secure === TRUE)) {
-    $params = session_get_cookie_params();
-    if ($secure !== NULL) {
-      $params['secure'] = $secure;
-    }
-    setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
-    $cookies->remove($name);
-  }
-}
-
-/**
  * Ends a specific user's session(s).
  *
  * @param $uid
@@ -497,27 +219,6 @@ function drupal_session_destroy_uid($uid) {
 }
 
 /**
- * Session handler assigned by session_set_save_handler().
- *
- * Cleans up stalled sessions.
- *
- * @param $lifetime
- *   The value of session.gc_maxlifetime, passed by PHP.
- *   Sessions not updated for more than $lifetime seconds will be removed.
- */
-function _drupal_session_garbage_collection($lifetime) {
-  // Be sure to adjust 'php_value session.gc_maxlifetime' to a large enough
-  // value. For example, if you want user sessions to stay in your database
-  // for three weeks before deleting them, you need to set gc_maxlifetime
-  // to '1814400'. At that value, only after a user doesn't log in after
-  // three weeks (1814400 seconds) will his/her session be removed.
-  db_delete('sessions')
-    ->condition('timestamp', REQUEST_TIME - $lifetime, '<')
-    ->execute();
-  return TRUE;
-}
-
-/**
  * Determines whether to save session data of the current request.
  *
  * This function allows the caller to temporarily disable writing of
diff --git a/core/lib/Drupal/Core/Session/SessionHandler.php b/core/lib/Drupal/Core/Session/SessionHandler.php
new file mode 100644
index 0000000..3049556
--- /dev/null
+++ b/core/lib/Drupal/Core/Session/SessionHandler.php
@@ -0,0 +1,295 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Session\SessionHandler.
+ */
+
+namespace Drupal\Core\Session;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\Settings;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\DestructableInterface;
+use Drupal\Core\Utility\Error;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Default session handler.
+ */
+class SessionHandler implements \SessionHandlerInterface, DestructableInterface {
+
+  /**
+   * The request.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * An array containing the sid and data from last read.
+   *
+   * @var array
+   */
+  protected $lastRead;
+
+  /**
+   * Constructs a new SessionHandler instance.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The database connection.
+   */
+  public function __construct(Request $request, Connection $connection) {
+    $this->request = $request;
+    $this->connection = $connection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function open($save_path, $name) {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Initializes the global $user object for the user associated with the
+   * session.
+   */
+  public function read($sid) {
+    global $user;
+
+    // Handle the case of first time visitors and clients that don't store
+    // cookies (eg. web crawlers).
+    $insecure_session_name = substr(session_name(), 1);
+    $cookies = $this->request->cookies;
+    if (!$cookies->has(session_name()) && !$cookies->has($insecure_session_name)) {
+      $user = new UserSession();
+      return '';
+    }
+
+    // Otherwise, if the session is still active, we have a record of the
+    // client's session in the database. If it's HTTPS then we are either have
+    // a HTTPS session or we are about to log in so we check the sessions table
+    // for an anonymous session with the non-HTTPS-only cookie. The session ID
+    // that is in the user's cookie is hashed before being stored in the database
+    // as a security measure. Thus, we have to hash it to match the database.
+    if ($this->request->isSecure()) {
+      $values = $this->connection->query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array(
+        ':ssid' => Crypt::hashBase64($sid),
+      ))->fetchAssoc();
+      if (!$values) {
+        if ($cookies->has($insecure_session_name)) {
+          $values = $this->connection->query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid AND s.uid = 0", array(
+            ':sid' => Crypt::hashBase64($cookies->get($insecure_session_name)),
+          ))->fetchAssoc();
+        }
+      }
+    }
+    else {
+      $values = $this->connection->query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(
+        ':sid' => Crypt::hashBase64($sid),
+      ))->fetchAssoc();
+    }
+
+    // We found the client's session record and they are an authenticated,
+    // active user.
+    if ($values && $values['uid'] > 0 && $values['status'] == 1) {
+      // Add roles element to $user.
+      $rids = $this->connection->query("SELECT ur.rid FROM {users_roles} ur WHERE ur.uid = :uid", array(
+        ':uid' => $values['uid'],
+      ))->fetchCol();
+      $values['roles'] = array_merge(array(DRUPAL_AUTHENTICATED_RID), $rids);
+      $user = new UserSession($values);
+    }
+    elseif ($values) {
+      // The user is anonymous or blocked. Only preserve two fields from the
+      // {sessions} table.
+      $user = new UserSession(array(
+        'session' => $values['session'],
+        'access' => $values['access'],
+      ));
+    }
+    else {
+      // The session has expired.
+      $user = new UserSession();
+    }
+
+    // Store the session that was read for comparison in self::write().
+    $this->lastRead = array(
+      'sid' => $sid,
+      'value' => $user->session,
+    );
+    return $user->session;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function write($sid, $value) {
+    global $user;
+
+    // The exception handler is not active at this point, so we need to do it
+    // manually.
+    try {
+      if (!drupal_save_session()) {
+        // We don't have anything to do if we are not allowed to save the session.
+        return TRUE;
+      }
+      // Check whether $_SESSION has been changed in this request.
+      $is_changed = empty($this->lastRead) || $this->lastRead['sid'] != $sid || $this->lastRead['value'] !== $value;
+
+      // For performance reasons, do not update the sessions table, unless
+      // $_SESSION has changed or more than 180 has passed since the last update.
+      $needs_update = !$user->getLastAccessedTime() || REQUEST_TIME - $user->getLastAccessedTime() > Settings::get('session_write_interval', 180);
+
+      if ($is_changed || $needs_update) {
+        // Either ssid or sid or both will be added from $key below.
+        $fields = array(
+          'uid' => $user->id(),
+          'hostname' => $this->request->getClientIP(),
+          'session' => $value,
+          'timestamp' => REQUEST_TIME,
+        );
+        // Use the session ID as 'sid' and an empty string as 'ssid' by default.
+        // _drupal_session_read() does not allow empty strings so that's a safe
+        // default.
+        $key = array('sid' => Crypt::hashBase64($sid), 'ssid' => '');
+        // On HTTPS connections, use the session ID as both 'sid' and 'ssid'.
+        if ($this->request->isSecure()) {
+          $key['ssid'] = Crypt::hashBase64($sid);
+          $cookies = $this->request->cookies;
+          // The "secure pages" setting allows a site to simultaneously use both
+          // secure and insecure session cookies. If enabled and both cookies are
+          // presented then use both keys. The session ID from the cookie is
+          // hashed before being stored in the database as a security measure.
+          if (Settings::get('mixed_mode_sessions', FALSE)) {
+            $insecure_session_name = substr(session_name(), 1);
+            if ($cookies->has($insecure_session_name)) {
+              $key['sid'] = Crypt::hashBase64($cookies->get($insecure_session_name));
+            }
+          }
+        }
+        elseif (Settings::get('mixed_mode_sessions', FALSE)) {
+          unset($key['ssid']);
+        }
+        $this->connection->merge('sessions')
+          ->keys($key)
+          ->fields($fields)
+          ->execute();
+      }
+      // Likewise, do not update access time more than once per 180 seconds.
+      if ($user->isAuthenticated() && REQUEST_TIME - $user->getLastAccessedTime() > Settings::get('session_write_interval', 180)) {
+        $this->connection->update('users')
+          ->fields(array(
+            'access' => REQUEST_TIME
+          ))
+          ->condition('uid', $user->id())
+          ->execute();
+      }
+      return TRUE;
+    }
+    catch (\Exception $exception) {
+      require_once DRUPAL_ROOT . '/core/includes/errors.inc';
+      // If we are displaying errors, then do so with no possibility of a further
+      // uncaught exception being thrown.
+      if (error_displayable()) {
+        print '<h1>Uncaught exception thrown in session handler.</h1>';
+        print '<p>' . Error::renderExceptionSafe($exception) . '</p><hr />';
+      }
+      return FALSE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function close() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function destroy($sid) {
+    global $user;
+
+    // Nothing to do if we are not allowed to change the session.
+    if (!drupal_save_session()) {
+      return TRUE;
+    }
+    $is_https = $this->request->isSecure();
+    // Delete session data.
+    $this->connection->delete('sessions')
+      ->condition($is_https ? 'ssid' : 'sid', Crypt::hashBase64($sid))
+      ->execute();
+
+    // Reset $_SESSION and $user to prevent a new session from being started
+    // in drupal_session_commit().
+    $_SESSION = array();
+    $user = drupal_anonymous_user();
+
+    // Unset the session cookies.
+    $this->deleteCookie(session_name());
+    if ($is_https) {
+      $this->deleteCookie(substr(session_name(), 1), FALSE);
+    }
+    elseif (Settings::get('mixed_mode_sessions', FALSE)) {
+      $this->deleteCookie('S' . session_name(), TRUE);
+    }
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function gc($lifetime) {
+    // Be sure to adjust 'php_value session.gc_maxlifetime' to a large enough
+    // value. For example, if you want user sessions to stay in your database
+    // for three weeks before deleting them, you need to set gc_maxlifetime
+    // to '1814400'. At that value, only after a user doesn't log in after
+    // three weeks (1814400 seconds) will his/her session be removed.
+    $this->connection->delete('sessions')
+      ->condition('timestamp', REQUEST_TIME - $lifetime, '<')
+      ->execute();
+    return TRUE;
+  }
+
+  /**
+   * Deletes a session cookie.
+   *
+   * @param string $name
+   *   Name of session cookie to delete.
+   * @param bool $secure
+   *   Force the secure value of the cookie.
+   */
+  protected function deleteCookie($name, $secure = NULL) {
+    $cookies = $this->request->cookies;
+    if ($cookies->has($name) || (!$this->request->isSecure() && $secure === TRUE)) {
+      $params = session_get_cookie_params();
+      if ($secure !== NULL) {
+        $params['secure'] = $secure;
+      }
+      setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
+      $cookies->remove($name);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function destruct() {
+    session_write_close();
+  }
+
+}
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
index 8efa40a..b812b1d 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -1098,10 +1098,12 @@ protected function curlInitialize() {
     if (!isset($this->curlHandle)) {
       $this->curlHandle = curl_init();
 
-      // Some versions/configurations of cURL break on a NULL cookie jar, so
-      // supply a real file.
+      // CURLOPT_COOKIEJAR needs to be set for cURL to parse cookies and cURL
+      // will save cookies into this file upon curl_close().
+      // The pathname must be absolute on Windows.
+      // @see http://php.net/manual/en/function.curl-setopt.php#75525
       if (empty($this->cookieFile)) {
-        $this->cookieFile = $this->public_files_directory . '/cookie.jar';
+        $this->cookieFile = DRUPAL_ROOT . '/' . $this->public_files_directory . '/cookie.jar';
       }
 
       $curl_options = array(
diff --git a/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php b/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php
index 16d3eec..8e1823c 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php
@@ -20,6 +20,8 @@ class SessionTest extends WebTestBase {
 
   protected $dumpHeaders = TRUE;
 
+  protected $loggedInUsers = array();
+
   public static function getInfo() {
     return array(
       'name' => 'Session tests',
@@ -273,15 +275,32 @@ function testEmptySessionID() {
    */
   function sessionReset($uid = 0) {
     // Close the internal browser.
+    // This causes cURL to write out the CURLOPT_COOKIEJAR file.
     $this->curlClose();
-    $this->loggedInUser = FALSE;
+
+    if (!empty($this->loggedInUser)) {
+      $this->loggedInUsers[$this->loggedInUser->id()] = $this->loggedInUser;
+    }
+    if ($uid) {
+      $this->loggedInUser = isset($this->loggedInUsers[$uid]) ? $this->loggedInUsers[$uid] : FALSE;
+    }
+    else {
+      $this->loggedInUser = FALSE;
+    }
 
     // Change cookie file for user.
-    $this->cookieFile = file_stream_wrapper_get_instance_by_scheme('temporary')->getDirectoryPath() . '/cookie.' . $uid . '.txt';
+    // $this->cookieFile is set as CURLOPT_COOKIEJAR, into which cURL will save
+    // cookies upon curl_close(). The pathname must be absolute on Windows.
+    // @see WebTestBase::curlInitialize()
+    // @see http://php.net/manual/en/function.curl-setopt.php#75525
+    $this->cookieFile = DRUPAL_ROOT . '/' . $this->public_files_directory . '/cookie.' . $uid . '.jar';
+    // The CURLOPT_COOKIEFILE option specifies a file from which cURL will read
+    // cookies to send with requests. By using the same file as for
+    // CURLOPT_COOKIEJAR, cURL will effectively read and write to the same file.
     $this->additionalCurlOptions[CURLOPT_COOKIEFILE] = $this->cookieFile;
     $this->additionalCurlOptions[CURLOPT_COOKIESESSION] = TRUE;
     $this->drupalGet('session-test/get');
-    $this->assertResponse(200, 'Session test module is correctly enabled.', 'Session');
+    $this->assertResponse(200);
   }
 
   /**
