diff --git a/includes/ajax.inc b/includes/ajax.inc
index 49ff7e46b0f007e0efdfa040e10f6a0854054a20..e1ea518d78328b72645fa2a56adbfb80d1955a86 100644
--- a/includes/ajax.inc
+++ b/includes/ajax.inc
@@ -239,6 +239,12 @@ function ajax_get_form() {
   // Since some of the submit handlers are run, redirects need to be disabled.
   $form_state['no_redirect'] = TRUE;
 
+  // When a form is rebuilt after AJAX processing, its #build_id and #action
+  // should not change.
+  // @see drupal_rebuild_form()
+  $form_state['rebuild_info']['copy']['#build_id'] = TRUE;
+  $form_state['rebuild_info']['copy']['#action'] = TRUE;
+
   // The form needs to be processed; prepare for that by setting a few internal
   // variables.
   $form_state['input'] = $_POST;
@@ -263,18 +269,15 @@ function ajax_get_form() {
  * enhanced function.
  */
 function ajax_form_callback() {
-  list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
-
-  // Build, validate and if possible, submit the form.
-  drupal_process_form($form_id, $form, $form_state);
-
-  // This call recreates the form relying solely on the $form_state that
-  // drupal_process_form() set up.
-  $form = drupal_rebuild_form($form_id, $form_state, $form);
-
-  // As part of drupal_process_form(), the element that triggered the form
-  // submission is determined, and in the case of AJAX, it might not be a
-  // button. This lets us route to the appropriate callback.
+  list($form, $form_state) = ajax_get_form();
+  drupal_process_form($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
+  // up to the #ajax['callback'] function of the element (may or may not be a
+  // button) that triggered the AJAX request to determine what needs to be
+  // rendered.
   if (!empty($form_state['triggering_element'])) {
     $callback = $form_state['triggering_element']['#ajax']['callback'];
   }
diff --git a/includes/batch.inc b/includes/batch.inc
index 7fcc915668cf2a8b8c309e5d2bad4ed0fba52883..d847646bf36ad561793529006ab0da1c6d779f3e 100644
--- a/includes/batch.inc
+++ b/includes/batch.inc
@@ -495,9 +495,12 @@ function _batch_finished() {
     // Use drupal_redirect_form() to handle the redirection logic.
     drupal_redirect_form($_batch['form_state']);
 
-    // If no redirection happened, save the final $form_state value to be
-    // retrieved by drupal_get_form() and redirect to the originating page.
-    $_SESSION['batch_form_state'] = $_batch['form_state'];
+    // If no redirection happened, redirect to the originating page. In case the
+    // form needs to be rebuilt, save the final $form_state for
+    // drupal_build_form().
+    if (!empty($_batch['form_state']['rebuild'])) {
+      $_SESSION['batch_form_state'] = $_batch['form_state'];
+    }
     $function = $_batch['redirect_callback'];
     if (function_exists($function)) {
       $function($_batch['source_url'], array('query' => array('op' => 'finish', 'id' => $_batch['id'])));
diff --git a/includes/form.inc b/includes/form.inc
index e2909c64bac11f8e83b51e30a4d6afb50035331e..0c50e190b1da0653c3e3eacc74e95987bf141a12 100644
--- a/includes/form.inc
+++ b/includes/form.inc
@@ -275,153 +275,79 @@ function drupal_build_form($form_id, &$form_state) {
   }
 
   if (isset($_SESSION['batch_form_state'])) {
-    // We've been redirected here after a batch processing : the form has
-    // already been processed, so we grab the post-process $form_state value
-    // and move on to form display. See _batch_finished() function.
+    // We've been redirected here after a batch processing. The form has
+    // already been processed, but needs to be rebuilt. See _batch_finished().
     $form_state = $_SESSION['batch_form_state'];
     unset($_SESSION['batch_form_state']);
-  }
-  else {
-    // If the incoming input contains a form_build_id, we'll check the
-    // cache for a copy of the form in question. If it's there, we don't
-    // have to rebuild the form to proceed. In addition, if there is stored
-    // form_state data from a previous step, we'll retrieve it so it can
-    // be passed on to the form processing code.
-    $check_cache = isset($form_state['input']['form_id']) && $form_state['input']['form_id'] == $form_id && !empty($form_state['input']['form_build_id']);
+    return drupal_rebuild_form($form_id, $form_state);
+  }
+
+  // If the incoming input contains a form_build_id, we'll check the cache for a
+  // copy of the form in question. If it's there, we don't have to rebuild the
+  // form to proceed. In addition, if there is stored form_state data from a
+  // previous step, we'll retrieve it so it can be passed on to the form
+  // processing code.
+  $check_cache = isset($form_state['input']['form_id']) && $form_state['input']['form_id'] == $form_id && !empty($form_state['input']['form_build_id']);
+  if ($check_cache) {
+    $form = form_get_cache($form_state['input']['form_build_id'], $form_state);
+  }
+
+  // If the previous bit of code didn't result in a populated $form object, we
+  // are hitting the form for the first time and we need to build it from
+  // scratch.
+  if (!isset($form)) {
+    // If we attempted to serve the form from cache, uncacheable $form_state
+    // keys need to be removed after retrieving and preparing the form, except
+    // any that were already set prior to retrieving the form.
     if ($check_cache) {
-      $form_build_id = $form_state['input']['form_build_id'];
-      $form = form_get_cache($form_build_id, $form_state);
-    }
-
-    // If the previous bit of code didn't result in a populated $form
-    // object, we're hitting the form for the first time and we need
-    // to build it from scratch.
-    if (!isset($form)) {
-      // Record the filepath of the include file containing the original form,
-      // so the form builder callbacks can be loaded when the form is being
-      // rebuilt from cache on a different path (such as 'system/ajax'). See
-      // form_get_cache().
-      // $menu_get_item() is not available at installation time.
-      if (!isset($form_state['build_info']['files']['menu']) && !defined('MAINTENANCE_MODE')) {
-        $item = menu_get_item();
-        if (!empty($item['include_file'])) {
-          $form_state['build_info']['files']['menu'] = $item['include_file'];
-        }
-      }
-
-      // If we attempted to serve the form from cache, uncacheable $form_state
-      // keys need to be removed after retrieving and preparing the form, except
-      // any that were already set prior to retrieving the form.
-      if ($check_cache) {
-        $form_state_before_retrieval = $form_state;
-      }
-
-      $form = drupal_retrieve_form($form_id, $form_state);
-      $form_build_id = 'form-' . drupal_hash_base64(uniqid(mt_rand(), TRUE) . mt_rand());
-      $form['#build_id'] = $form_build_id;
-
-      // Fix the form method, if it is 'get' in $form_state, but not in $form.
-      if ($form_state['method'] == 'get' && !isset($form['#method'])) {
-        $form['#method'] = 'get';
-      }
-
-      drupal_prepare_form($form_id, $form, $form_state);
-      // Store a copy of the unprocessed form to cache in case
-      // $form_state['cache'] is set.
-      $original_form = $form;
-
-      // form_set_cache() removes uncacheable $form_state keys defined in
-      // form_state_keys_no_cache() in order for multi-step forms to work
-      // properly. This means that form processing logic for single-step forms
-      // using $form_state['cache'] may depend on data stored in those keys
-      // during drupal_retrieve_form()/drupal_prepare_form(), but form
-      // processing should not depend on whether the form is cached or not, so
-      // $form_state is adjusted to match what it would be after a
-      // form_set_cache()/form_get_cache() sequence. These exceptions are
-      // allowed to survive here:
-      // - always_process: Does not make sense in conjunction with form caching
-      //   in the first place, since passing form_build_id as a GET parameter is
-      //   not desired.
-      // - temporary: Any assigned data is expected to survives within the same
-      //   page request.
-      if ($check_cache) {
-        $form_state = array_diff_key($form_state, array_flip(array_diff(form_state_keys_no_cache(), array('always_process', 'temporary')))) + $form_state_before_retrieval;
-      }
-    }
-
-    // Now that we know we have a form, we'll process it (validating,
-    // submitting, and handling the results returned by its submission
-    // handlers. Submit handlers accumulate data in the form_state by
-    // altering the $form_state variable, which is passed into them by
-    // reference.
-    drupal_process_form($form_id, $form, $form_state);
-  }
-
-  // Most simple, single-step forms will be finished by this point --
-  // drupal_process_form() usually redirects to another page (or to
-  // a 'fresh' copy of the form) once processing is complete. If one
-  // of the form's handlers has set $form_state['redirect'] to FALSE,
-  // the form will simply be re-rendered with the values still in its
-  // fields.
-  //
-  // If $form_state['rebuild'] has been set and input has been processed, we
-  // know that we're in a multi-part process of some sort and the form's
-  // workflow is not complete. We need to construct a fresh copy of the form,
-  // passing in the latest $form_state in addition to any other variables passed
-  // into drupal_get_form().
-  if ($form_state['rebuild'] && $form_state['process_input'] && !form_get_errors()) {
-    $form = drupal_rebuild_form($form_id, $form_state);
-  }
-  // After processing the form, the form builder or a #process callback may
-  // have set $form_state['cache'] to indicate that the original form and the
-  // $form_state shall be cached. But the form may only be cached if the
-  // special 'no_cache' property is not set to TRUE and we are not rebuilding.
-  elseif (isset($form_build_id) && $form_state['cache'] && empty($form_state['no_cache'])) {
-    // Cache the original, unprocessed form upon initial build of the form.
-    if (isset($original_form)) {
-      form_set_cache($form_build_id, $original_form, $form_state);
-    }
-    // After processing a cached form, only update the cached form state.
-    else {
-      form_set_cache($form_build_id, NULL, $form_state);
-    }
-  }
-
-  // Check theme functions for this form.
-  // theme_form() itself is in #theme_wrappers and not #theme. Therefore, the
-  // #theme function only has to care for rendering the inner form elements,
-  // not the form itself.
-  drupal_theme_initialize();
-  $registry = theme_get_registry();
-  // If #theme has been set, check whether the theme function(s) exist, or
-  // remove the suggestion(s), so drupal_render() renders the children.
-  if (isset($form['#theme'])) {
-    if (is_array($form['#theme'])) {
-      foreach ($form['#theme'] as $key => $suggestion) {
-        if (!isset($registry[$suggestion])) {
-          unset($form['#theme'][$key]);
-        }
-      }
-      if (empty($form['#theme'])) {
-        unset($form['#theme']);
-      }
-    }
-    else {
-      if (!isset($registry[$form['#theme']])) {
-        unset($form['#theme']);
-      }
-    }
-  }
-  // Only try to auto-suggest theme functions, if #theme has not been set.
-  else {
-    if (isset($registry[$form_id])) {
-      $form['#theme'] = $form_id;
-    }
-    elseif (isset($form_state['build_info']['base_form_id']) && isset($registry[$form_state['build_info']['base_form_id']])) {
-      $form['#theme'] = $form_state['build_info']['base_form_id'];
-    }
-  }
+      $form_state_before_retrieval = $form_state;
+    }
+
+    $form = drupal_retrieve_form($form_id, $form_state);
+    drupal_prepare_form($form_id, $form, $form_state);
+
+    // form_set_cache() removes uncacheable $form_state keys defined in
+    // form_state_keys_no_cache() in order for multi-step forms to work
+    // properly. This means that form processing logic for single-step forms
+    // using $form_state['cache'] may depend on data stored in those keys
+    // during drupal_retrieve_form()/drupal_prepare_form(), but form
+    // processing should not depend on whether the form is cached or not, so
+    // $form_state is adjusted to match what it would be after a
+    // form_set_cache()/form_get_cache() sequence. These exceptions are
+    // allowed to survive here:
+    // - always_process: Does not make sense in conjunction with form caching
+    //   in the first place, since passing form_build_id as a GET parameter is
+    //   not desired.
+    // - temporary: Any assigned data is expected to survives within the same
+    //   page request.
+    if ($check_cache) {
+      $uncacheable_keys = array_flip(array_diff(form_state_keys_no_cache(), array('always_process', 'temporary')));
+      $form_state = array_diff_key($form_state, $uncacheable_keys);
+      $form_state += $form_state_before_retrieval;
+    }
+  }
+
+  // Now that we have a constructed form, process it. This is where:
+  // - Element #process functions get called to further refine $form.
+  // - User input, if any, gets incorporated in the #value property of the
+  //   corresponding elements and into $form_state['values'].
+  // - Validation and submission handlers are called.
+  // - If this submission is part of a multistep workflow, the form is rebuilt
+  //   to contain the information of the next step.
+  // - If necessary, the form and form state are cached or re-cached, so that
+  //   appropriate information persists to the next page request.
+  // All of the handlers in the pipeline receive $form_state by reference and
+  // can use it to know or update information about the state of the form.
+  drupal_process_form($form_id, $form, $form_state);
 
+  // If this was a successful submission of a single-step form or the last step
+  // of a multi-step form, then drupal_process_form() issued a redirect to
+  // another page, or back to this page, but as a new request. Therefore, if
+  // we're here, it means that this is either a form being viewed initially
+  // before any user input, or there was a validation error requiring the form
+  // to be re-displayed, or we're in a multi-step workflow and need to display
+  // the form's next step. In any case, we have what we need in $form, and can
+  // return it for rendering.
   return $form;
 }
 
@@ -431,6 +357,7 @@ function drupal_build_form($form_id, &$form_state) {
 function form_state_defaults() {
   return array(
     'rebuild' => FALSE,
+    'rebuild_info' => array(),
     'redirect' => NULL,
     'build_info' => array('args' => array()),
     'temporary' => array(),
@@ -444,16 +371,19 @@ function form_state_defaults() {
 }
 
 /**
- * Retrieves a form, caches it and processes it again.
+ * Constructs a new $form from the information in $form_state.
+ *
+ * This is the key function for making multi-step forms advance from step to
+ * step. It is called by drupal_process_form() when all user input processing,
+ * including calling validation and submission handlers, for the request is
+ * finished. If a validate or submit handler set $form_state['rebuild'] to TRUE,
+ * and if other conditions don't preempt a rebuild from happening, then this
+ * function is called to generate a new $form, the next step in the form
+ * workflow, to be returned for rendering.
  *
- * If your AJAX callback simulates the pressing of a button, then your AJAX
- * callback will need to do the same as what drupal_get_form() would do when the
- * button is pressed: get the form from the cache, run drupal_process_form over
- * it and then if it needs rebuild, run drupal_rebuild_form() over it. Then send
- * back a part of the returned form.
- * $form_state['triggering_element']['#array_parents'] will help you to find
- * which part.
- * @see ajax_form_callback() for an example.
+ * AJAX form submissions are almost always multi-step workflows, so that is one
+ * common use-case during which form rebuilding occurs. See ajax_form_callback()
+ * for more information about creating AJAX-enabled forms.
  *
  * @param $form_id
  *   The unique string identifying the desired form. If a function
@@ -466,21 +396,20 @@ function form_state_defaults() {
  *   A keyed array containing the current state of the form.
  * @param $old_form
  *   (optional) A previously built $form. Used to retain the #build_id and
- *   #action properties in AJAX callbacks and similar partial form rebuilds.
- *   Should not be passed for regular rebuilds, for which the entire $form
- *   should be rebuilt freshly.
+ *   #action properties in AJAX callbacks and similar partial form rebuilds. The
+ *   only properties copied from $old_form are the ones which both exist in
+ *   $old_form and for which $form_state['rebuild_info']['copy'][PROPERTY] is
+ *   TRUE. If $old_form is not passed, the entire $form is rebuilt freshly.
+ *   'rebuild_info' needs to be a separate top-level property next to
+ *   'build_info', since the contained data must not be cached.
  *
  * @return
  *   The newly built form.
+ *
+ * @see drupal_process_form()
+ * @see ajax_form_callback()
  */
 function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
-  // AJAX and other contexts may call drupal_rebuild_form() even when
-  // $form_state['rebuild'] isn't set, but _form_builder_handle_input_element()
-  // needs to distinguish a rebuild from an initial build in order to process
-  // user input correctly. Form constructors and form processing functions may
-  // also need to handle a rebuild differently than an initial build.
-  $form_state['rebuild'] = TRUE;
-
   $form = drupal_retrieve_form($form_id, $form_state);
 
   // If only parts of the form will be returned to the browser (e.g. AJAX or
@@ -489,20 +418,28 @@ function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
   // Otherwise, a new #build_id is generated, to not clobber the previous
   // build's data in the form cache; also allowing the user to go back to an
   // earlier build, make changes, and re-submit.
-  $form['#build_id'] = isset($old_form['#build_id']) ? $old_form['#build_id'] : 'form-' . drupal_hash_base64(uniqid(mt_rand(), TRUE) . mt_rand());
+  // @see drupal_prepare_form()
+  if (isset($old_form['#build_id']) && !empty($form_state['rebuild_info']['copy']['#build_id'])) {
+    $form['#build_id'] = $old_form['#build_id'];
+  }
+  else {
+    $form['#build_id'] = 'form-' . drupal_hash_base64(uniqid(mt_rand(), TRUE) . mt_rand());
+  }
 
   // #action defaults to request_uri(), but in case of AJAX and other partial
   // rebuilds, the form is submitted to an alternate URL, and the original
   // #action needs to be retained.
-  if (isset($old_form['#action'])) {
+  if (isset($old_form['#action']) && !empty($form_state['rebuild_info']['copy']['#action'])) {
     $form['#action'] = $old_form['#action'];
   }
 
   drupal_prepare_form($form_id, $form, $form_state);
 
+  // Caching is normally done in drupal_process_form(), but what needs to be
+  // cached is the $form structure before it passes through form_builder(),
+  // so we need to do it here.
+  // @todo For Drupal 8, find a way to avoid this code duplication.
   if (empty($form_state['no_cache'])) {
-    // We cache the form structure and the form state so it can be retrieved
-    // later for validation.
     form_set_cache($form['#build_id'], $form, $form_state);
   }
 
@@ -510,10 +447,8 @@ function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
   // re-rendering the form.
   $form_state['groups'] = array();
 
-  // Do not call drupal_process_form(), since it would prevent the rebuilt form
-  // to submit.
-  $form = form_builder($form_id, $form, $form_state);
-  return $form;
+  // Return a fully built form that is ready for rendering.
+  return form_builder($form_id, $form, $form_state);
 }
 
 /**
@@ -577,6 +512,7 @@ function form_state_keys_no_cache() {
     'always_process',
     'must_validate',
     'rebuild',
+    'rebuild_info',
     'redirect',
     'no_redirect',
     'temporary',
@@ -691,6 +627,18 @@ function drupal_form_submit($form_id, &$form_state) {
 function drupal_retrieve_form($form_id, &$form_state) {
   $forms = &drupal_static(__FUNCTION__);
 
+  // Record the filepath of the include file containing the original form, so
+  // the form builder callbacks can be loaded when the form is being rebuilt
+  // from cache on a different path (such as 'system/ajax'). See
+  // form_get_cache().
+  // $menu_get_item() is not available at installation time.
+  if (!isset($form_state['build_info']['files']['menu']) && !defined('MAINTENANCE_MODE')) {
+    $item = menu_get_item();
+    if (!empty($item['include_file'])) {
+      $form_state['build_info']['files']['menu'] = $item['include_file'];
+    }
+  }
+
   // We save two copies of the incoming arguments: one for modules to use
   // when mapping form ids to constructor functions, and another to pass to
   // the constructor function itself.
@@ -758,7 +706,7 @@ function drupal_retrieve_form($form_id, &$form_state) {
  * Processes a form submission.
  *
  * This function is the heart of form API. The form gets built, validated and in
- * appropriate cases, submitted.
+ * appropriate cases, submitted and rebuilt.
  *
  * @param $form_id
  *   The unique string identifying the current form.
@@ -787,7 +735,11 @@ function drupal_process_form($form_id, &$form, &$form_state) {
     }
   }
 
-  // Build the form.
+  // form_builder() finishes building the form by calling element #process
+  // functions and mapping user input, if any, to #value properties, and also
+  // storing the values in $form_state['values']. We need to retain the
+  // unprocessed $form in case it needs to be cached.
+  $unprocessed_form = $form;
   $form = form_builder($form_id, $form, $form_state);
 
   // Only process the input if we have a correct form submission.
@@ -847,6 +799,29 @@ function drupal_process_form($form_id, &$form, &$form_state) {
       // Redirect the form based on values in $form_state.
       drupal_redirect_form($form_state);
     }
+
+    // Don't rebuild or cache form submissions invoked via drupal_form_submit().
+    if (!empty($form_state['programmed'])) {
+      return;
+    }
+  }
+
+  // If $form_state['rebuild'] has been set and input has been processed without
+  // validation errors, we're in a multi-step workflow that is not yet complete.
+  // We need to construct a new $form based on the changes made to $form_state
+  // during this request.
+  if ($form_state['rebuild'] && $form_state['process_input'] && !form_get_errors()) {
+    $form = drupal_rebuild_form($form_id, $form_state, $form);
+  }
+  // After processing the form, the form builder or a #process callback may
+  // have set $form_state['cache'] to indicate that the form and form state
+  // shall be cached. But the form may only be cached if the 'no_cache' property
+  // is not set to TRUE. Only cache $form as it was prior to form_builder(),
+  // because form_builder() must run for each request to accomodate new user
+  // input. We do not cache here for forms that have been rebuilt, because
+  // drupal_rebuild_form() takes care of that.
+  elseif ($form_state['cache'] && empty($form_state['no_cache'])) {
+    form_set_cache($form['#build_id'], $unprocessed_form, $form_state);
   }
 }
 
@@ -870,15 +845,27 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
   $form['#type'] = 'form';
   $form_state['programmed'] = isset($form_state['programmed']) ? $form_state['programmed'] : FALSE;
 
-  if (isset($form['#build_id'])) {
-    $form['form_build_id'] = array(
-      '#type' => 'hidden',
-      '#value' => $form['#build_id'],
-      '#id' => $form['#build_id'],
-      '#name' => 'form_build_id',
-    );
+  // Fix the form method, if it is 'get' in $form_state, but not in $form.
+  if ($form_state['method'] == 'get' && !isset($form['#method'])) {
+    $form['#method'] = 'get';
   }
 
+  // Generate a new #build_id for this form, if none has been set already. The
+  // form_build_id is used as key to cache a particular build of the form. For
+  // multi-step forms, this allows the user to go back to an earlier build, make
+  // changes, and re-submit.
+  // @see drupal_build_form()
+  // @see drupal_rebuild_form()
+  if (!isset($form['#build_id'])) {
+    $form['#build_id'] = 'form-' . drupal_hash_base64(uniqid(mt_rand(), TRUE) . mt_rand());
+  }
+  $form['form_build_id'] = array(
+    '#type' => 'hidden',
+    '#value' => $form['#build_id'],
+    '#id' => $form['#build_id'],
+    '#name' => 'form_build_id',
+  );
+
   // Add a token, based on either #token or form_id, to any form displayed to
   // authenticated users. This ensures that any submitted form was actually
   // requested previously by the user and protects against cross site request
@@ -942,6 +929,41 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
     }
   }
 
+  // Check theme functions for this form.
+  // theme_form() itself is in #theme_wrappers and not #theme. Therefore, the
+  // #theme function only has to care for rendering the inner form elements,
+  // not the form itself.
+  drupal_theme_initialize();
+  $registry = theme_get_registry();
+  // If #theme has been set, check whether the theme function(s) exist, or
+  // remove the suggestion(s), so drupal_render() renders the children.
+  if (isset($form['#theme'])) {
+    if (is_array($form['#theme'])) {
+      foreach ($form['#theme'] as $key => $suggestion) {
+        if (!isset($registry[$suggestion])) {
+          unset($form['#theme'][$key]);
+        }
+      }
+      if (empty($form['#theme'])) {
+        unset($form['#theme']);
+      }
+    }
+    else {
+      if (!isset($registry[$form['#theme']])) {
+        unset($form['#theme']);
+      }
+    }
+  }
+  // Only try to auto-suggest theme functions, if #theme has not been set.
+  else {
+    if (isset($registry[$form_id])) {
+      $form['#theme'] = $form_id;
+    }
+    elseif (isset($form_state['build_info']['base_form_id']) && isset($registry[$form_state['build_info']['base_form_id']])) {
+      $form['#theme'] = $form_state['build_info']['base_form_id'];
+    }
+  }
+
   // Invoke hook_form_alter(), hook_form_BASE_FORM_ID_alter(), and
   // hook_form_FORM_ID_alter() implementations.
   $hooks = array('form');
diff --git a/modules/file/file.module b/modules/file/file.module
index 1388f179be84b8a8612e8ecc6917ff5cd4893d96..5d76b6c82e02b339a134e79b2558a6ca95fb89fd 100644
--- a/modules/file/file.module
+++ b/modules/file/file.module
@@ -254,7 +254,7 @@ function file_ajax_upload() {
     return array('#type' => 'ajax', '#commands' => $commands, '#header' => FALSE);
   }
 
-  list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
+  list($form, $form_state) = ajax_get_form();
 
   if (!$form) {
     // Invalid form_build_id.
@@ -271,12 +271,8 @@ function file_ajax_upload() {
   }
   $current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0;
 
-  // Build, validate and if possible, submit the form.
-  drupal_process_form($form_id, $form, $form_state);
-
-  // This call recreates the form relying solely on the form_state that the
-  // drupal_process_form() set up.
-  $form = drupal_rebuild_form($form_id, $form_state, $form);
+  // Process user input. $form and $form_state are modified in the process.
+  drupal_process_form($form['#form_id'], $form, $form_state);
 
   // Retrieve the element to be rendered.
   foreach ($form_parents as $parent) {
diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test
index d628f488b281c1b1d57c0a47061a9bdd181a9c93..ce4bc9681f932b490f1029b5b90326f00ba983a7 100644
--- a/modules/simpletest/tests/form.test
+++ b/modules/simpletest/tests/form.test
@@ -1011,6 +1011,10 @@ class FormsRebuildTestCase extends DrupalWebTestCase {
     $this->drupalPost(NULL, array(), t('Save'));
     $this->assertText('Title field is required.', t('Non-AJAX submission correctly triggered a validation error.'));
 
+    // Ensure that the form contains two items in the multi-valued field, so we
+    // know we're testing a form that was correctly retrieved from cache.
+    $this->assert(count($this->xpath('//form[contains(@id, "page-node-form")]//div[contains(@class, "form-item-field-ajax-test")]//input[@type="text"]')) == 2, t('Form retained its state from cache.'));
+
     // Ensure that the form's action is correct.
     $forms = $this->xpath('//form[contains(@class, "node-page-form")]');
     $this->assert(count($forms) == 1 && $forms[0]['action'] == url('node/add/page'), t('Re-rendered form contains the correct action value.'));