Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.487
diff -u -p -r1.487 form.inc
--- includes/form.inc	30 Aug 2010 17:07:49 -0000	1.487
+++ includes/form.inc	5 Sep 2010 22:51:41 -0000
@@ -242,10 +242,6 @@ function drupal_get_form($form_id) {
  *     write data, as this could lead to security issues. It is useful so that
  *     searches do not need to have a form_id in their query arguments to
  *     trigger the search.
- *   - must_validate: Ordinarily, a form is only validated once but there are
- *     times when a form is resubmitted internally and should be validated
- *     again. Setting this to TRUE will force that to happen. This is most
- *     likely to occur during AHAH or AJAX operations.
  *   - temporary: An array holding temporary data accessible during the current
  *     page request only. It may be used to temporary save any data that doesn't
  *     need to or shouldn't be cached during the whole form workflow, e.g. data
@@ -549,7 +545,6 @@ function form_state_keys_no_cache() {
   return array(
     // Public properties defined by form constructors and form handlers.
     'always_process',
-    'must_validate',
     'rebuild',
     'redirect',
     'no_redirect',
@@ -641,7 +636,6 @@ function drupal_form_submit($form_id, &$
   $form_state['submitted'] = TRUE;
 
   // Reset form validation.
-  $form_state['must_validate'] = TRUE;
   form_clear_error();
 
   drupal_prepare_form($form_id, $form, $form_state);
@@ -701,6 +695,15 @@ function drupal_retrieve_form($form_id, 
     }
   }
 
+  // To support multiple forms of the same $form_id on the same page, the forms
+  // need to be distinguished when the form is submitted. The arguments passed
+  // to build the form need to be different, or otherwise, it would not matter,
+  // which build of the form is actually submitted.
+  // Dynamically defined arguments of hook_forms() are taken into account.
+  // @see drupal_prepare_form()
+  // @see form_builder()
+  $form_state['build_info']['instance'] = hash('sha256', serialize($args));
+
   $form = array();
   // We need to pass $form_state by reference in order for forms to modify it,
   // since call_user_func_array() requires that referenced variables are passed
@@ -755,6 +758,9 @@ function drupal_process_form($form_id, &
     if (!isset($form_state['input']['form_id'])) {
       $form_state['input']['form_id'] = $form_id;
     }
+    if (!isset($form_state['input']['form_instance'])) {
+      $form_state['input']['form_instance'] = $form_state['build_info']['instance'];
+    }
     if (!isset($form_state['input']['form_token']) && isset($form['#token'])) {
       $form_state['input']['form_token'] = drupal_get_token($form['#token']);
     }
@@ -884,6 +890,13 @@ function drupal_prepare_form($form_id, &
       '#id' => drupal_html_id("edit-$form_id"),
     );
   }
+  if (isset($form_state['build_info']['instance'])) {
+    $form['form_instance'] = array(
+      '#type' => 'hidden',
+      '#value' => $form_state['build_info']['instance'],
+    );
+  }
+
   if (!isset($form['#id'])) {
     $form['#id'] = drupal_html_id($form_id);
   }
@@ -936,12 +949,6 @@ function drupal_prepare_form($form_id, &
  *   not be repeated in the submission step.
  */
 function drupal_validate_form($form_id, &$form, &$form_state) {
-  $validated_forms = &drupal_static(__FUNCTION__, array());
-
-  if (isset($validated_forms[$form_id]) && empty($form_state['must_validate'])) {
-    return;
-  }
-
   // If the session token was set by drupal_prepare_form(), ensure that it
   // matches the current user's session.
   if (isset($form['#token'])) {
@@ -952,7 +959,6 @@ function drupal_validate_form($form_id, 
   }
 
   _form_validate($form, $form_state, $form_id);
-  $validated_forms[$form_id] = TRUE;
 
   // If validation errors are limited then remove any non validated form values,
   // so that only values that passed validation are left for submit callbacks.
@@ -1510,9 +1516,26 @@ function form_builder($form_id, $element
     // Store a complete copy of the form in form_state prior to building the form.
     $form_state['complete form'] = $element;
     // Set a flag if we have a correct form submission. This is always TRUE for
-    // programmed forms coming from drupal_form_submit(), or if the form_id coming
-    // from the POST data is set and matches the current form_id.
-    if ($form_state['programmed'] || (!empty($form_state['input']) && (isset($form_state['input']['form_id']) && ($form_state['input']['form_id'] == $form_id)))) {
+    // programmed forms coming from drupal_form_submit(), or if the form_id and
+    // form_instance coming from the POST data indicate that it was this form
+    // that was submitted. When processing a POST request for a page containing
+    // multiple forms of the same form_id, the form_instance ensures that the
+    // Form API input processing logic only runs for the form that was actually
+    // submitted. GET requests and advanced use-cases may unset
+    // $form_state['build_info']['instance'] to bypass that check.
+    // @see drupal_retrieve_form()
+    $input = FALSE;
+    if (!empty($form_state['input'])) {
+      $input = $form_state['input'] + array('form_id' => '', 'form_instance' => '');
+      // $install_state changes during installation.
+      if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'install') {
+        $instance = $input['form_instance'];
+      }
+      else {
+        $instance = (isset($form_state['build_info']['instance']) ? $form_state['build_info']['instance'] : $input['form_instance']);
+      }
+    }
+    if ($form_state['programmed'] || ($input && $input['form_id'] == $form_id && $input['form_instance'] == $instance)) {
       $form_state['process_input'] = TRUE;
     }
     else {
@@ -1891,7 +1914,7 @@ function _form_button_was_clicked($eleme
  */
 function form_state_values_clean(&$form_state) {
   // Remove internal Form API values.
-  unset($form_state['values']['form_id'], $form_state['values']['form_token'], $form_state['values']['form_build_id'], $form_state['values']['op']);
+  unset($form_state['values']['form_id'], $form_state['values']['form_token'], $form_state['values']['form_build_id'], $form_state['values']['form_instance'], $form_state['values']['op']);
 
   // Remove button values.
   // form_builder() collects all button elements in a form. We remove the button
@@ -3489,7 +3512,7 @@ function _form_set_class(&$element, $cla
   if ($element['#required']) {
     $element['#attributes']['class'][] = 'required';
   }
-  if (form_get_error($element)) {
+  if (!empty($element['#validated']) && form_get_error($element)) {
     $element['#attributes']['class'][] = 'error';
   }
 }
Index: modules/simpletest/tests/ajax.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax.test,v
retrieving revision 1.14
diff -u -p -r1.14 ajax.test
--- modules/simpletest/tests/ajax.test	5 Aug 2010 23:53:38 -0000	1.14
+++ modules/simpletest/tests/ajax.test	5 Sep 2010 22:15:12 -0000
@@ -268,7 +268,7 @@ class AJAXMultiFormTestCase extends AJAX
     // Ensure the initial page contains both node forms and the correct number
     // of field items and "add more" button for the multi-valued field within
     // each form.
-    $this->drupalGet('form-test/two-instances-of-same-form');
+    $this->drupalGet('form-test/multiple-instances-of-node-form');
     foreach ($field_xpaths as $form_id => $field_xpath) {
       $this->assert(count($this->xpath($field_xpath . $field_items_xpath_suffix)) == 1, t('Found the correct number of field items on the initial page.'));
       $this->assertFieldByXPath($field_xpath . $button_xpath_suffix, NULL, t('Found the "add more" button on the initial page.'));
Index: modules/simpletest/tests/form.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v
retrieving revision 1.62
diff -u -p -r1.62 form.test
--- modules/simpletest/tests/form.test	27 Aug 2010 11:54:32 -0000	1.62
+++ modules/simpletest/tests/form.test	5 Sep 2010 22:15:12 -0000
@@ -907,6 +907,7 @@ class FormStateValuesCleanTestCase exten
     $this->assertFalse(isset($values['form_id']), t('%element was removed.', array('%element' => 'form_id')));
     $this->assertFalse(isset($values['form_token']), t('%element was removed.', array('%element' => 'form_token')));
     $this->assertFalse(isset($values['form_build_id']), t('%element was removed.', array('%element' => 'form_build_id')));
+    $this->assertFalse(isset($values['form_instance']), t('%element was removed.', array('%element' => 'form_instance')));
     $this->assertFalse(isset($values['op']), t('%element was removed.', array('%element' => 'op')));
 
     // Verify that all buttons were removed.
@@ -1294,3 +1295,39 @@ class FormsFileInclusionTestCase extends
     $this->assertText('Submit callback called.');
   }
 }
+
+/**
+ * Tests multiple identical forms on a page.
+ */
+class FormMultipleTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Multiple forms',
+      'description' => 'Tests form handling of multiple identical forms on a page.',
+      'group' => 'Form API',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('form_test');
+  }
+
+  /**
+   * Basic tests of multiple identical forms on a page.
+   */
+  function testMultipleFormsOnPage() {
+    // Ensure that the Form API processes the form that was submitted and only
+    // that one.
+    $expected = array(
+      'form-test-multiple-instances-testing-form' => 'Form Instance 1 was processed.',
+      'form-test-multiple-instances-testing-form--2' => 'Form Instance 2 was processed.',
+    );
+    foreach ($expected as $form_html_id => $expected_message) {
+      $this->drupalPost('form-test/multiple-instances-of-simple-form', array(), 'Submit', array(), array(), $form_html_id);
+      $this->assertText($expected_message);
+      foreach (array_diff($expected, array($expected_message)) as $other_message) {
+        $this->assertNoText($other_message);
+      }
+    }
+  }
+}
Index: modules/simpletest/tests/form_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form_test.module,v
retrieving revision 1.48
diff -u -p -r1.48 form_test.module
--- modules/simpletest/tests/form_test.module	27 Aug 2010 11:54:32 -0000	1.48
+++ modules/simpletest/tests/form_test.module	5 Sep 2010 22:15:12 -0000
@@ -141,10 +141,16 @@ function form_test_menu() {
     'type' => MENU_CALLBACK,
   );
 
+  $items['form-test/multiple-instances-of-simple-form'] = array(
+    'title' => 'Form Test: Multiple identical forms on page',
+    'page callback' => 'form_test_multiple_instances_simple',
+    'access callback' => TRUE,
+  );
+
   if (module_exists('node')) {
-    $items['form-test/two-instances-of-same-form'] = array(
-      'title' => 'AJAX test with two form instances',
-      'page callback' => 'form_test_two_instances',
+    $items['form-test/multiple-instances-of-node-form'] = array(
+      'title' => 'AJAX test with multiple form instances',
+      'page callback' => 'form_test_multiple_node_forms',
       'access callback' => 'node_access',
       'access arguments' => array('create', 'page'),
       'file path' => drupal_get_path('module', 'node'),
@@ -1263,9 +1269,55 @@ function form_test_user_register_form_re
 }
 
 /**
- * Menu callback that returns two instances of the node form.
+ * Menu callback; Returns multiple instances of a simple form.
+ */
+function form_test_multiple_instances_simple() {
+  return array(
+    drupal_get_form('form_test_multiple_instances_testing_form', 'Form Instance 1'),
+    drupal_get_form('form_test_multiple_instances_testing_form', 'Form Instance 2'),
+  );
+}
+
+/**
+ * Form builder to test pages containing multiple forms using the same form_id.
+ *
+ * @param $arg
+ *   A build argument to distinguish each instance of this form on the page.
+ *
+ * @see form_test_multiple_instances_simple()
+ */
+function form_test_multiple_instances_testing_form($form, &$form_state, $arg) {
+  // Take over the additional build $arg as enforced #value for a text field,
+  // which is output in the submit handler.
+  $form['instance'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Form instance name',
+    '#value' => $arg,
+    '#disabled' => TRUE,
+  );
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => 'Submit',
+  );
+  return $form;
+}
+
+/**
+ * Form submission handler for form_test_multiple_instances_testing_form().
+ */
+function form_test_multiple_instances_testing_form_submit($form, &$form_state) {
+  drupal_set_message(check_plain($form_state['values']['instance']) . ' was processed.');
+}
+
+/**
+ * Menu callback that returns multiple instances of the node form.
+ *
+ * Simple Form API tests can use form_test_multiple_instances_simple() instead
+ * of this page. This page instantiates multiple node forms for testing more
+ * complex functionality (e.g., the "Add another item" button of a multi-valued
+ * field).
  */
-function form_test_two_instances() {
+function form_test_multiple_node_forms() {
   global $user;
   $node1 = (object) array(
     'uid' => $user->uid,
