diff --git a/core/core.services.yml b/core/core.services.yml
index 9b1ab37..6cbcea2 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -293,7 +293,7 @@ services:
     arguments: ['@stream_wrapper_manager', '@settings', '@logger.channel.file']
   form_builder:
     class: Drupal\Core\Form\FormBuilder
-    arguments: ['@form_validator', '@form_submitter', '@form_cache', '@module_handler', '@event_dispatcher', '@request_stack', '@class_resolver', '@element_info', '@theme.manager', '@?csrf_token']
+    arguments: ['@form_validator', '@form_submitter', '@form_cache', '@module_handler', '@event_dispatcher', '@request_stack', '@class_resolver', '@element_info', '@theme.manager', '@private_key', '@keyvalue.expirable', '@?csrf_token']
   form_validator:
     class: Drupal\Core\Form\FormValidator
     arguments: ['@request_stack', '@string_translation', '@csrf_token', '@logger.channel.form']
@@ -844,6 +844,10 @@ services:
     class: Drupal\Core\EventSubscriber\ContentControllerSubscriber
     tags:
       - { name: event_subscriber }
+  route_form_controller_subscriber:
+    class: Drupal\Core\EventSubscriber\FormControllerSubscriber
+    tags:
+      - { name: event_subscriber }
   route_special_attributes_subscriber:
     class: Drupal\Core\EventSubscriber\SpecialAttributesRouteSubscriber
     tags:
diff --git a/core/lib/Drupal/Core/Controller/FormController.php b/core/lib/Drupal/Core/Controller/FormController.php
index 6a9b9d5..a5cca68 100644
--- a/core/lib/Drupal/Core/Controller/FormController.php
+++ b/core/lib/Drupal/Core/Controller/FormController.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\Controller;
 
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Drupal\Core\Form\FormAndFormState;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -76,7 +77,11 @@ public function getContentResult(Request $request, RouteMatchInterface $route_ma
     unset($args[0], $args[1]);
     $form_state->addBuildInfo('args', array_values($args));
 
-    return $this->formBuilder->buildForm($form_object, $form_state);
+    // @todo.
+    $form_state->setPostToOriginalUrl();
+
+    $form_array = $this->formBuilder->buildForm($form_object, $form_state);
+    return new FormAndFormState($form_array, $form_state);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/EventSubscriber/FormControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FormControllerSubscriber.php
new file mode 100644
index 0000000..107c67f
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/FormControllerSubscriber.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\FormControllerSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Form\FormAndFormState;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
+use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * @todo.
+ */
+class FormControllerSubscriber implements EventSubscriberInterface {
+
+  /**
+   * @todo.
+   */
+  public function onViewRenderArray(GetResponseForControllerResultEvent $event) {
+    $request = $event->getRequest();
+
+    if (($request->attributes->has('_form') || $request->attributes->has('_entity_form')) && ($result = $event->getControllerResult()) instanceof FormAndFormState) {
+      $event->setControllerResult($result->getForm());
+    }
+  }
+
+  /**
+   * @todo.
+   */
+  public function onKernelController(FilterControllerEvent $event) {
+    $request = $event->getRequest();
+    if ($request->query->get('magic_string_to_be_renamed') === 'drupal_ajax_post') {
+      $original_controller = $event->getController();
+
+      $controller = function (Request $request) use ($original_controller) {
+        $controller_resolver = \Drupal::service('controller_resolver');
+        $class_resolver = \Drupal::service('class_resolver');
+
+        $arguments = $controller_resolver->getArguments($request, $original_controller);
+        $response = call_user_func_array($original_controller, $arguments);
+
+        $controller = $class_resolver->getInstanceFromDefinition('\Drupal\system\Controller\FormAjaxController');
+        $response = call_user_func_array([$controller, 'contentWithFormAndFormState'], [$request, $response]);
+        return $response;
+      };
+      $event->setController($controller);
+    }
+  }
+
+  /**
+   * Registers the methods in this class that should be listeners.
+   *
+   * @return array
+   *   An array of event listener definitions.
+   */
+  public static function getSubscribedEvents() {
+    $events[KernelEvents::VIEW][] = ['onViewRenderArray'];
+    $events[KernelEvents::CONTROLLER][] = ['onKernelController'];
+
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormAndFormState.php b/core/lib/Drupal/Core/Form/FormAndFormState.php
new file mode 100644
index 0000000..2a01b52
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormAndFormState.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormAndFormState.
+ */
+
+namespace Drupal\Core\Form;
+
+/**
+ * @todo.
+ */
+class FormAndFormState {
+
+  /**
+   * The form to cache.
+   *
+   * @var array
+   */
+  protected $form;
+
+  /**
+   * The form state.
+   *
+   * @var \Drupal\Core\Form\FormStateInterface
+   */
+  protected $formState;
+
+  /**
+   * Constructs a FormAndFormState object.
+   *
+   * @param array $form
+   *   The form definition.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   */
+  public function __construct(array $form, FormStateInterface $form_state) {
+    $this->form = $form;
+    $this->formState = $form_state;
+  }
+
+  /**
+   * Gets the form definition.
+   *
+   * @return array
+   */
+  public function getForm() {
+    return $this->form;
+  }
+
+  /**
+   * Gets the form state.
+   *
+   * @return \Drupal\Core\Form\FormStateInterface
+   */
+  public function getFormState() {
+    return $this->formState;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index 4b995c1..36df847 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -15,8 +15,11 @@
 use Drupal\Core\Access\CsrfTokenGenerator;
 use Drupal\Core\DependencyInjection\ClassResolverInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
+use Drupal\Core\PrivateKey;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\ElementInfoManagerInterface;
+use Drupal\Core\Site\Settings;
 use Drupal\Core\Theme\ThemeManagerInterface;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\RequestStack;
@@ -103,6 +106,20 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
   protected $formCache;
 
   /**
+   * The private key service.
+   *
+   * @var \Drupal\Core\PrivateKey
+   */
+  protected $privateKey;
+
+  /**
+   * The expirable key value factory.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface
+   */
+  protected $keyValueExpirableFactory;
+
+  /**
    * Constructs a new FormBuilder.
    *
    * @param \Drupal\Core\Form\FormValidatorInterface $form_validator
@@ -123,10 +140,12 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
    *   The element info manager.
    * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
    *   The theme manager.
+   * @param \Drupal\Core\PrivateKey $private_key
+   * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory
    * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
    *   The CSRF token generator.
    */
-  public function __construct(FormValidatorInterface $form_validator, FormSubmitterInterface $form_submitter, FormCacheInterface $form_cache, ModuleHandlerInterface $module_handler, EventDispatcherInterface $event_dispatcher, RequestStack $request_stack, ClassResolverInterface $class_resolver, ElementInfoManagerInterface $element_info, ThemeManagerInterface $theme_manager, CsrfTokenGenerator $csrf_token = NULL) {
+  public function __construct(FormValidatorInterface $form_validator, FormSubmitterInterface $form_submitter, FormCacheInterface $form_cache, ModuleHandlerInterface $module_handler, EventDispatcherInterface $event_dispatcher, RequestStack $request_stack, ClassResolverInterface $class_resolver, ElementInfoManagerInterface $element_info, ThemeManagerInterface $theme_manager, PrivateKey $private_key, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CsrfTokenGenerator $csrf_token = NULL) {
     $this->formValidator = $form_validator;
     $this->formSubmitter = $form_submitter;
     $this->formCache = $form_cache;
@@ -135,6 +154,8 @@ public function __construct(FormValidatorInterface $form_validator, FormSubmitte
     $this->requestStack = $request_stack;
     $this->classResolver = $class_resolver;
     $this->elementInfo = $element_info;
+    $this->privateKey = $private_key;
+    $this->keyValueExpirableFactory = $key_value_expirable_factory;
     $this->csrfToken = $csrf_token;
     $this->themeManager = $theme_manager;
   }
@@ -179,6 +200,19 @@ public function getForm($form_arg) {
    * {@inheritdoc}
    */
   public function buildForm($form_id, FormStateInterface &$form_state) {
+    if (!$form_state->willPostToOriginalUrl()) {
+      $temp_form_id = is_object($form_id) ? serialize($form_id) : $form_id;
+      try {
+        $hash_salt = Settings::getHashSalt();
+      }
+      catch (\RuntimeException $e) {
+        // During the installer no hash salt is defined yet.
+        $hash_salt = Crypt::randomBytes(8);
+      }
+      $form_build_key = 'form_build_key:' . Crypt::hmacBase64($temp_form_id . serialize($form_state), 'form_key' . $this->privateKey->get() . $hash_salt);
+      $this->keyValueExpirableFactory->get('form_build_key')->setWithExpire($form_build_key, [$temp_form_id, $form_state], 21600);
+    }
+
     // Ensure the form ID is prepared.
     $form_id = $this->getFormId($form_id, $form_state);
 
@@ -221,6 +255,15 @@ public function buildForm($form_id, FormStateInterface &$form_state) {
       $form = $this->retrieveForm($form_id, $form_state);
       $this->prepareForm($form_id, $form, $form_state);
 
+      if (isset($form_build_key)) {
+        $form['form_build_key'] = [
+          '#type' => 'hidden',
+          '#value' => $form_build_key,
+          '#name' => 'form_build_key',
+          '#parents' => ['form_build_key'],
+        ];
+      }
+
       // self::setCache() removes uncacheable $form_state keys (see properties
       // in \Drupal\Core\Form\FormState) in order for multi-step forms to work
       // properly. This means that form processing logic for single-step forms
diff --git a/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php
index 9d651cd..fe70c3a 100644
--- a/core/lib/Drupal/Core/Form/FormState.php
+++ b/core/lib/Drupal/Core/Form/FormState.php
@@ -211,6 +211,7 @@ class FormState implements FormStateInterface {
     'form_id',
     'form_token',
     'form_build_id',
+    'form_build_key',
     'op',
   ];
 
@@ -427,6 +428,13 @@ class FormState implements FormStateInterface {
   protected $submit_handlers = [];
 
   /**
+   * @todo.
+   *
+   * @var bool
+   */
+  protected $postToOriginalUrl = FALSE;
+
+  /**
    * {@inheritdoc}
    */
   public function setFormState(array $form_state_additions) {
@@ -1245,6 +1253,17 @@ public function cleanValues() {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function setPostToOriginalUrl($post_to_original_url = TRUE) {
+    $this->postToOriginalUrl = (bool) $post_to_original_url;
+    return $this;
+  }
+  public function willPostToOriginalUrl() {
+    return $this->postToOriginalUrl;
+  }
+
+  /**
    * Wraps drupal_set_message().
    *
    * @return array|null
diff --git a/core/lib/Drupal/Core/Form/FormStateInterface.php b/core/lib/Drupal/Core/Form/FormStateInterface.php
index 5e44ecb..5af31d0 100644
--- a/core/lib/Drupal/Core/Form/FormStateInterface.php
+++ b/core/lib/Drupal/Core/Form/FormStateInterface.php
@@ -1074,4 +1074,14 @@ public function addCleanValueKey($key);
    */
   public function cleanValues();
 
+  /**
+   * @todo.
+   */
+  public function setPostToOriginalUrl($post_to_original_url = TRUE);
+
+  /**
+   * @todo.
+   */
+  public function willPostToOriginalUrl();
+
 }
diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php
index 87b9f02..41bfd86 100644
--- a/core/lib/Drupal/Core/Render/Element/RenderElement.php
+++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php
@@ -128,8 +128,14 @@ public static function preRenderGroup($element) {
    */
   public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) {
     $element = static::preRenderAjaxForm($element);
-    if (!empty($element['#ajax_processed'])) {
-      $form_state->setCached();
+    if (!empty($element['#ajax_processed']) && isset($element['#ajax']['url'])) {
+      if ($form_state->willPostToOriginalUrl()) {
+        $element['#ajax']['url'] = Url::fromRouteMatch(\Drupal::routeMatch());
+        $element['#ajax']['options']['query']['magic_string_to_be_renamed'] = 'drupal_ajax_post';
+      }
+      elseif (isset($element['#ajax']['url'])) {
+        $form_state->setCached();
+      }
     }
     return $element;
   }
diff --git a/core/modules/filter/src/Tests/TextFormatElementFormTest.php b/core/modules/filter/src/Tests/TextFormatElementFormTest.php
index b889c4a..3bd0c43 100644
--- a/core/modules/filter/src/Tests/TextFormatElementFormTest.php
+++ b/core/modules/filter/src/Tests/TextFormatElementFormTest.php
@@ -41,7 +41,7 @@ class TextFormatElementFormTest extends KernelTestBase implements FormInterface
   protected function setUp() {
     parent::setUp();
     $this->installEntitySchema('user');
-    $this->installSchema('system', ['sequences', 'router']);
+    $this->installSchema('system', ['sequences', 'router', 'key_value_expire']);
     $this->installConfig(['filter', 'filter_test']);
     // Filter tips link to the full-page.
     \Drupal::service('router.builder')->rebuild();
diff --git a/core/modules/options/src/Tests/OptionsFieldTest.php b/core/modules/options/src/Tests/OptionsFieldTest.php
index d70ec01..a626ac8 100644
--- a/core/modules/options/src/Tests/OptionsFieldTest.php
+++ b/core/modules/options/src/Tests/OptionsFieldTest.php
@@ -27,6 +27,8 @@ class OptionsFieldTest extends OptionsFieldUnitTestBase {
    * Test that allowed values can be updated.
    */
   function testUpdateAllowedValues() {
+    $this->installSchema('system', ['key_value_expire']);
+
     // All three options appear.
     $entity = entity_create('entity_test');
     $form = \Drupal::service('entity.form_builder')->getForm($entity);
diff --git a/core/modules/search/search.module b/core/modules/search/search.module
index 62db411..4cafb3e 100644
--- a/core/modules/search/search.module
+++ b/core/modules/search/search.module
@@ -853,19 +853,3 @@ function _search_find_match_with_simplify($key, $text, $boundary, $langcode = NU
   // If we get here, we couldn't find a match.
   return NULL;
 }
-
-/**
- * Implements hook_form_FORM_ID_alter() for the search_block_form form.
- *
- * Since the exposed form is a GET form, we don't want it to send the form
- * tokens. However, you cannot make this happen in the form builder function
- * itself, because the tokens are added to the form after the builder function
- * is called. So, we have to do it in a form_alter.
- *
- * @see \Drupal\search\Form\SearchBlockForm
- */
-function search_form_search_block_form_alter(&$form, FormStateInterface $form_state) {
-  $form['form_build_id']['#access'] = FALSE;
-  $form['form_token']['#access'] = FALSE;
-  $form['form_id']['#access'] = FALSE;
-}
diff --git a/core/modules/search/src/Form/SearchBlockForm.php b/core/modules/search/src/Form/SearchBlockForm.php
index e154863..b07557e 100644
--- a/core/modules/search/src/Form/SearchBlockForm.php
+++ b/core/modules/search/src/Form/SearchBlockForm.php
@@ -64,6 +64,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     }
 
     $route = 'search.view_' . $entity_id;
+    $form['#pre_render'][] = [get_class(), 'removeFormTokens'];
     $form['#action'] = $this->url($route);
     $form['#token'] = FALSE;
     $form['#method'] = 'get';
@@ -89,6 +90,22 @@ public function buildForm(array $form, FormStateInterface $form_state) {
   }
 
   /**
+   * Remove form tokens from the search form.
+   *
+   * Since the exposed form is a GET form, we don't want it to send the form
+   * tokens. However, you cannot make this happen in the form builder function
+   * itself, because the tokens are added to the form after the builder function
+   * is called. So, we have to do it in a #pre_render.
+   */
+  public static function removeFormTokens($element) {
+    $element['form_build_key']['#access'] = FALSE;
+    $element['form_build_id']['#access'] = FALSE;
+    $element['form_token']['#access'] = FALSE;
+    $element['form_id']['#access'] = FALSE;
+    return $element;
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
diff --git a/core/modules/search/src/Tests/SearchBlockTest.php b/core/modules/search/src/Tests/SearchBlockTest.php
index bf18056..e2b7265 100644
--- a/core/modules/search/src/Tests/SearchBlockTest.php
+++ b/core/modules/search/src/Tests/SearchBlockTest.php
@@ -69,11 +69,7 @@ public function testSearchFormBlock() {
     /** @var $search_page_repository \Drupal\search\SearchPageRepositoryInterface */
     $search_page_repository = \Drupal::service('search.search_page_repository');
     $entity_id = $search_page_repository->getDefaultSearchPage();
-    $this->assertEqual(
-      $this->getUrl(),
-      \Drupal::url('search.view_' . $entity_id, array(), array('query' => array('keys' => $terms['keys']), 'absolute' => TRUE)),
-      'Submitted to correct url.'
-    );
+    $this->assertUrl(\Drupal::url('search.view_' . $entity_id, array(), array('query' => array('keys' => $terms['keys']), 'absolute' => TRUE)));
 
     // Test an empty search via the block form, from the front page.
     $terms = array('keys' => '');
@@ -83,11 +79,7 @@ public function testSearchFormBlock() {
 
     // Confirm that the user is redirected to the search page, when form is
     // submitted empty.
-    $this->assertEqual(
-      $this->getUrl(),
-      \Drupal::url('search.view_' . $entity_id, array(), array('query' => array('keys' => ''), 'absolute' => TRUE)),
-      'Redirected to correct url.'
-    );
+    $this->assertUrl(\Drupal::url('search.view_' . $entity_id, array(), array('query' => array('keys' => ''), 'absolute' => TRUE)));
 
     // Test that after entering a too-short keyword in the form, you can then
     // search again with a longer keyword. First test using the block form.
diff --git a/core/modules/search/src/Tests/SearchConfigSettingsFormTest.php b/core/modules/search/src/Tests/SearchConfigSettingsFormTest.php
index a99ea45..6466014 100644
--- a/core/modules/search/src/Tests/SearchConfigSettingsFormTest.php
+++ b/core/modules/search/src/Tests/SearchConfigSettingsFormTest.php
@@ -178,9 +178,7 @@ function testSearchModuleDisabling() {
       // to this plugin's search results page.
       $terms = array('keys' => $info['keys']);
       $this->submitGetForm('node', $terms, t('Search'));
-      $current = $this->getURL();
-      $expected = \Drupal::url('search.view_' . $entity->id(), array(), array('query' => array('keys' => $info['keys']), 'absolute' => TRUE));
-      $this->assertEqual( $current, $expected, 'Block redirected to right search page');
+      $this->assertUrl(\Drupal::url('search.view_' . $entity->id(), array(), array('query' => array('keys' => $info['keys']), 'absolute' => TRUE)));
 
       // Try an invalid search path, which should 404.
       $this->drupalGet('search/not_a_plugin_path');
diff --git a/core/modules/system/src/Controller/FormAjaxController.php b/core/modules/system/src/Controller/FormAjaxController.php
index 0c29322..3fe8eee 100644
--- a/core/modules/system/src/Controller/FormAjaxController.php
+++ b/core/modules/system/src/Controller/FormAjaxController.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\UpdateBuildIdCommand;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Form\FormAndFormState;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Render\MainContent\MainContentRendererInterface;
@@ -98,6 +99,24 @@ public static function create(ContainerInterface $container) {
   }
 
   /**
+   * @todo.
+   */
+  public function contentWithFormAndFormState(Request $request, FormAndFormState $form_and_form_state) {
+    $form = $form_and_form_state->getForm();
+    $form_state = $form_and_form_state->getFormState();
+    $form_build_id = $request->request->get('form_build_id');
+    $form_id = $form['#form_id'];
+
+    $commands = [];
+    if ($form_build_id != $form['#build_id']) {
+      // If the form build ID has changed, issue an Ajax command to update it.
+      $commands[] = new UpdateBuildIdCommand($form_build_id, $form['#build_id']);
+    }
+    $ajax_form = new FileAjaxForm($form, $form_state, $form_id, $form['#build_id'], $commands);
+    return $this->doContent($request, $ajax_form);
+  }
+
+  /**
    * Processes an Ajax form submission.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
@@ -114,13 +133,18 @@ public static function create(ContainerInterface $container) {
    * @throws \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface
    */
   public function content(Request $request) {
-    $ajax_form = $this->getForm($request);
+    $ajax_form = $this->buildForm($request);
+    return $this->doContent($request, $ajax_form);
+  }
+
+  /**
+   * @todo.
+   */
+  protected function doContent(Request $request, FileAjaxForm $ajax_form) {
     $form = $ajax_form->getForm();
     $form_state = $ajax_form->getFormState();
     $commands = $ajax_form->getCommands();
 
-    $this->formBuilder->processForm($form['#form_id'], $form, $form_state);
-
     // We need to return the part of the form (or some other content) that needs
     // to be re-rendered so the browser can update the page with changed content.
     // Since this is the generic menu callback used by many Ajax elements, it is
@@ -216,4 +240,46 @@ protected function getForm(Request $request) {
     return new FileAjaxForm($form, $form_state, $form_id, $form_build_id, $commands);
   }
 
+  protected function buildForm(Request $request) {
+    $form_build_id = $request->request->get('form_build_id');
+    $form_build_key = $request->request->get('form_build_key');
+
+    $keyvalue = \Drupal::service('keyvalue.expirable');
+    $result = $keyvalue->get('form_build_key')->get($form_build_key);
+    if ($result === NULL) {
+      $this->logger->warning('Expired form.');
+      throw new BadRequestHttpException();
+    }
+    else {
+      /** @var \Drupal\Core\Form\FormStateInterface $form_state */
+      list($form_id, $form_state) = $result;
+      $form = $this->formBuilder->buildForm($form_id, $form_state);
+
+      $commands = [];
+      if ($form_build_id != $form['#build_id']) {
+        // If the form build ID has changed, issue an Ajax command to update it.
+        $commands[] = new UpdateBuildIdCommand($form_build_id, $form['#build_id']);
+        $form_build_id = $form['#build_id'];
+      }
+
+      // Since some of the submit handlers are run, redirects need to be disabled.
+      $form_state->disableRedirect();
+
+      // When a form is rebuilt after Ajax processing, its #build_id and #action
+      // should not change.
+      // @see \Drupal\Core\Form\FormBuilderInterface::rebuildForm()
+      $form_state->addRebuildInfo('copy', [
+        '#build_id' => TRUE,
+        '#action' => TRUE,
+      ]);
+
+      // The form needs to be processed; prepare for that by setting a few
+      // internal variables.
+      $form_state->setUserInput($request->request->all());
+      $form_id = $form['#form_id'];
+
+      return new FileAjaxForm($form, $form_state, $form_id, $form_build_id, $commands);
+    }
+  }
+
 }
diff --git a/core/modules/system/src/FileAjaxForm.php b/core/modules/system/src/FileAjaxForm.php
index 1be14b4..749369c 100644
--- a/core/modules/system/src/FileAjaxForm.php
+++ b/core/modules/system/src/FileAjaxForm.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system;
 
+use Drupal\Core\Form\FormAndFormState;
 use Drupal\Core\Form\FormStateInterface;
 
 /**
@@ -14,21 +15,7 @@
  *
  * @ingroup ajax
  */
-class FileAjaxForm {
-
-  /**
-   * The form to cache.
-   *
-   * @var array
-   */
-  protected $form;
-
-  /**
-   * The form state.
-   *
-   * @var \Drupal\Core\Form\FormStateInterface
-   */
-  protected $formState;
+class FileAjaxForm extends FormAndFormState {
 
   /**
    * The unique form ID.
@@ -66,8 +53,7 @@ class FileAjaxForm {
    *   The ajax commands.
    */
   public function __construct(array $form, FormStateInterface $form_state, $form_id, $form_build_id, array $commands) {
-    $this->form = $form;
-    $this->formState = $form_state;
+    parent::__construct($form, $form_state);
     $this->formId = $form_id;
     $this->formBuildId = $form_build_id;
     $this->commands = $commands;
@@ -84,15 +70,6 @@ public function getCommands() {
   }
 
   /**
-   * Gets the form definition.
-   *
-   * @return array
-   */
-  public function getForm() {
-    return $this->form;
-  }
-
-  /**
    * Gets the unique form build ID.
    *
    * @return string
@@ -110,13 +87,4 @@ public function getFormId() {
     return $this->formId;
   }
 
-  /**
-   * Gets the form state.
-   *
-   * @return \Drupal\Core\Form\FormStateInterface
-   */
-  public function getFormState() {
-    return $this->formState;
-  }
-
 }
diff --git a/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php b/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php
index 0fdd9af..e561fe6 100644
--- a/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php
+++ b/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Entity\Element;
 
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Entity\Element\EntityAutocomplete;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormInterface;
@@ -23,6 +24,8 @@
  */
 class EntityAutocompleteElementFormTest extends EntityUnitTestBase implements FormInterface {
 
+  use DependencySerializationTrait;
+
   /**
    * User for testing.
    *
diff --git a/core/modules/user/src/Tests/UserAccountFormFieldsTest.php b/core/modules/user/src/Tests/UserAccountFormFieldsTest.php
index 679eec0..f16dc8e 100644
--- a/core/modules/user/src/Tests/UserAccountFormFieldsTest.php
+++ b/core/modules/user/src/Tests/UserAccountFormFieldsTest.php
@@ -29,6 +29,8 @@ class UserAccountFormFieldsTest extends KernelTestBase {
    * Tests the root user account form section in the "Configure site" form.
    */
   function testInstallConfigureForm() {
+    $this->installSchema('system', ['key_value_expire']);
+
     require_once \Drupal::root() . '/core/includes/install.core.inc';
     require_once \Drupal::root() . '/core/includes/install.inc';
     $install_state = install_state_defaults();
@@ -53,6 +55,7 @@ function testInstallConfigureForm() {
   function testUserRegistrationForm() {
     // Install default configuration; required for AccountFormController.
     $this->installConfig(array('user'));
+    $this->installSchema('system', ['key_value_expire']);
 
     // Disable email confirmation to unlock the password field.
     $this->config('user.settings')
@@ -79,7 +82,7 @@ function testUserEditForm() {
     $this->installConfig(array('user'));
 
     // Install the router table and then rebuild.
-    $this->installSchema('system', ['router']);
+    $this->installSchema('system', ['router', 'key_value_expire']);
     \Drupal::service('router.builder')->rebuild();
 
     $form = $this->buildAccountForm('default');
diff --git a/core/modules/views/src/Form/ViewsExposedForm.php b/core/modules/views/src/Form/ViewsExposedForm.php
index 29300af..358ac02 100644
--- a/core/modules/views/src/Form/ViewsExposedForm.php
+++ b/core/modules/views/src/Form/ViewsExposedForm.php
@@ -164,7 +164,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     $view->exposed_data = $form_state->getValues();
     $view->exposed_raw_input = [];
 
-    $exclude = array('submit', 'form_build_id', 'form_id', 'form_token', 'exposed_form_plugin', 'reset');
+    $exclude = array('submit', 'form_build_id', 'form_build_key', 'form_id', 'form_token', 'exposed_form_plugin', 'reset');
     /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase $exposed_form_plugin */
     $exposed_form_plugin = $view->display_handler->getPlugin('exposed_form');
     $exposed_form_plugin->exposedFormSubmit($form, $form_state, $exclude);
diff --git a/core/tests/Drupal/Tests/Core/Form/FormStateTest.php b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
index 24c4ddd..ce1ce61 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
@@ -535,7 +535,7 @@ public function testTemporaryValue() {
    */
   public function testGetCleanValueKeys() {
     $form_state = new FormState();
-    $this->assertSame($form_state->getCleanValueKeys(), ['form_id', 'form_token', 'form_build_id', 'op']);
+    $this->assertSame($form_state->getCleanValueKeys(), ['form_id', 'form_token', 'form_build_id', 'form_build_key', 'op']);
   }
 
   /**
@@ -554,7 +554,7 @@ public function testAddCleanValueKey() {
     $form_state = new FormState();
     $form_state->setValue('value_to_clean', 'rainbow_sprinkles');
     $form_state->addCleanValueKey('value_to_clean');
-    $this->assertSame($form_state->getCleanValueKeys(), ['form_id', 'form_token', 'form_build_id', 'op', 'value_to_clean']);
+    $this->assertSame($form_state->getCleanValueKeys(), ['form_id', 'form_token', 'form_build_id', 'form_build_key', 'op', 'value_to_clean']);
     return $form_state;
   }
 
diff --git a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
index 12b3260..8ae2abb 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Site\Settings;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\RequestStack;
@@ -133,11 +134,6 @@
   protected $translationManager;
 
   /**
-   * @var \Drupal\Core\DrupalKernelInterface|\PHPUnit_Framework_MockObject_MockObject
-   */
-  protected $kernel;
-
-  /**
    * @var \PHPUnit_Framework_MockObject_MockObject|\Psr\Log\LoggerInterface
    */
   protected $logger;
@@ -149,6 +145,16 @@
    */
   protected $themeManager;
 
+  /**
+   * The expirable key value factory.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $keyValueExpirableFactory;
+
+  /**
+   * {@inheritdoc}
+   */
   protected function setUp() {
     $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
 
@@ -172,6 +178,21 @@ protected function setUp() {
       ->disableOriginalConstructor()
       ->getMock();
     $this->account = $this->getMock('Drupal\Core\Session\AccountInterface');
+
+    $form_build_key_store = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface');
+    $this->keyValueExpirableFactory = $this->getMock('Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface');
+    $this->keyValueExpirableFactory->expects($this->any())
+      ->method('get')
+      ->with('form_build_key')
+      ->willReturn($form_build_key_store);
+    // Set up the settings singleton.
+    new Settings([
+      'hash_salt' => $this->randomMachineName(),
+    ]);
+    $private_key = $this->getMockBuilder('Drupal\Core\PrivateKey')
+      ->disableOriginalConstructor()
+      ->getMock();
+
     $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
     $this->request = new Request();
     $this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
@@ -188,7 +209,7 @@ protected function setUp() {
       ->getMock();
     $this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))));
 
-    $this->formBuilder = new FormBuilder($this->formValidator, $this->formSubmitter, $this->formCache, $this->moduleHandler, $this->eventDispatcher, $this->requestStack, $this->classResolver, $this->elementInfo, $this->themeManager, $this->csrfToken, $this->kernel);
+    $this->formBuilder = new FormBuilder($this->formValidator, $this->formSubmitter, $this->formCache, $this->moduleHandler, $this->eventDispatcher, $this->requestStack, $this->classResolver, $this->elementInfo, $this->themeManager, $private_key, $this->keyValueExpirableFactory, $this->csrfToken);
   }
 
   /**
