Protect the kernel version space from redefinition.

Review URL: http://codereview.chromium.org/2786005
diff --git a/vboot_firmware/lib/rollback_index.c b/vboot_firmware/lib/rollback_index.c
index e15b07e..3d1553c 100644
--- a/vboot_firmware/lib/rollback_index.c
+++ b/vboot_firmware/lib/rollback_index.c
@@ -19,27 +19,44 @@
 uint16_t g_kernel_key_version = 0;
 uint16_t g_kernel_version = 0;
 
-#define RETURN_ON_FAILURE(tpm_command) do {     \
-    uint32_t result;                            \
-    if ((result = tpm_command) != TPM_SUCCESS) {\
-      return result;                            \
-    }                                           \
+#define RETURN_ON_FAILURE(tpm_command) do {             \
+    uint32_t result;                                    \
+    if ((result = (tpm_command)) != TPM_SUCCESS) {      \
+      return result;                                    \
+    }                                                   \
   } while (0)
 
+static uint32_t InitializeKernelVersionsSpaces(void) {
+  RETURN_ON_FAILURE(TlclDefineSpace(KERNEL_VERSIONS_NV_INDEX,
+                                    TPM_NV_PER_PPWRITE, KERNEL_SPACE_SIZE));
+  RETURN_ON_FAILURE(TlclWrite(KERNEL_VERSIONS_NV_INDEX, KERNEL_SPACE_INIT_DATA,
+                              KERNEL_SPACE_SIZE));
+  return TPM_SUCCESS;
+}
+
+static uint32_t GetSpacesInitialized(int* initialized) {
+  uint32_t space_holder;
+  uint32_t result;
+  result = TlclRead(TPM_IS_INITIALIZED_NV_INDEX,
+                    (uint8_t*) &space_holder, sizeof(space_holder));
+  switch (result) {
+  case TPM_SUCCESS:
+    *initialized = 1;
+    break;
+  case TPM_E_BADINDEX:
+    *initialized = 0;
+    result = TPM_SUCCESS;
+    break;
+  }
+  return result;
+}
+
 static uint32_t InitializeSpaces(void) {
   uint32_t zero = 0;
-  uint32_t space_holder;
   uint32_t firmware_perm = TPM_NV_PER_GLOBALLOCK | TPM_NV_PER_PPWRITE;
-  uint32_t kernel_perm = TPM_NV_PER_PPWRITE;
 
   debug("Initializing spaces\n");
 
-  if (TlclRead(TPM_IS_INITIALIZED_NV_INDEX,
-               (uint8_t*) &space_holder, sizeof(space_holder)) == TPM_SUCCESS) {
-    /* Spaces are already initialized, so this is an error */
-    return 0;
-  }
-
   RETURN_ON_FAILURE(TlclSetNvLocked());
 
   RETURN_ON_FAILURE(TlclDefineSpace(FIRMWARE_VERSIONS_NV_INDEX,
@@ -47,23 +64,20 @@
   RETURN_ON_FAILURE(TlclWrite(FIRMWARE_VERSIONS_NV_INDEX,
                               (uint8_t*) &zero, sizeof(uint32_t)));
 
-  RETURN_ON_FAILURE(TlclDefineSpace(KERNEL_VERSIONS_NV_INDEX,
-                                    kernel_perm, sizeof(uint32_t)));
-  RETURN_ON_FAILURE(TlclWrite(KERNEL_VERSIONS_NV_INDEX, (uint8_t*) &zero,
-                              sizeof(uint32_t)));
+  RETURN_ON_FAILURE(InitializeKernelVersionsSpaces());
 
   /* The space KERNEL_VERSIONS_BACKUP_NV_INDEX is used to protect the kernel
    * versions when entering recovery mode.  The content of space
-   * KERNEL_BACKUP_IS_VALID determines whether the backup value (1) or the
+   * KERNEL_MUST_USE_BACKUP determines whether the backup value (1) or the
    * regular value (0) should be trusted.
    */
   RETURN_ON_FAILURE(TlclDefineSpace(KERNEL_VERSIONS_BACKUP_NV_INDEX,
                                     firmware_perm, sizeof(uint32_t)));
   RETURN_ON_FAILURE(TlclWrite(KERNEL_VERSIONS_BACKUP_NV_INDEX,
                               (uint8_t*) &zero, sizeof(uint32_t)));
-  RETURN_ON_FAILURE(TlclDefineSpace(KERNEL_BACKUP_IS_VALID_NV_INDEX,
+  RETURN_ON_FAILURE(TlclDefineSpace(KERNEL_MUST_USE_BACKUP_NV_INDEX,
                                     firmware_perm, sizeof(uint32_t)));
-  RETURN_ON_FAILURE(TlclWrite(KERNEL_BACKUP_IS_VALID_NV_INDEX,
+  RETURN_ON_FAILURE(TlclWrite(KERNEL_MUST_USE_BACKUP_NV_INDEX,
                               (uint8_t*) &zero, sizeof(uint32_t)));
 
   /* The space TPM_IS_INITIALIZED_NV_INDEX is used to indicate that the TPM
@@ -80,99 +94,67 @@
  * the TPM, so do not attempt to do any more TPM operations, and particularly
  * do not set bGlobalLock.
  */
-static void EnterRecovery(int unlocked) {
+void EnterRecovery(int unlocked) {
   uint32_t combined_versions;
   uint32_t backup_versions;
-  uint32_t backup_is_valid;
+  uint32_t must_use_backup;
+  uint32_t result;
 
   if (!unlocked) {
     /* Saves the kernel versions and indicates that we should trust the saved
      * ones.
      */
-    TlclRead(KERNEL_VERSIONS_NV_INDEX,
-             (uint8_t*) &combined_versions, sizeof(uint32_t));
-    TlclRead(KERNEL_VERSIONS_BACKUP_NV_INDEX,
-             (uint8_t*) &backup_versions, sizeof(uint32_t));
-    /* We could unconditional writes of both KERNEL_VERSIONS_BACKUP and
-     * KERNEL_BACKUP_IS_VALID, but this is more robust.
-     */
+    if (TlclRead(KERNEL_VERSIONS_NV_INDEX, (uint8_t*) &combined_versions,
+                 sizeof(uint32_t)) != TPM_SUCCESS)
+      goto recovery_mode;
+    if (TlclRead(KERNEL_VERSIONS_BACKUP_NV_INDEX, (uint8_t*) &backup_versions,
+                 sizeof(uint32_t)) != TPM_SUCCESS)
+      goto recovery_mode;
+    /* Avoids idempotent writes. */
     if (combined_versions != backup_versions) {
-      TlclWrite(KERNEL_VERSIONS_BACKUP_NV_INDEX,
-                (uint8_t*) &combined_versions, sizeof(uint32_t));
+      result = TlclWrite(KERNEL_VERSIONS_BACKUP_NV_INDEX,
+                         (uint8_t*) &combined_versions, sizeof(uint32_t));
+      if (result == TPM_E_MAXNVWRITES) {
+        goto forceclear_and_reboot;
+      } else if (result != TPM_SUCCESS) {
+        goto recovery_mode;
+      }
     }
 
-    TlclRead(KERNEL_BACKUP_IS_VALID_NV_INDEX,
-             (uint8_t*) &backup_is_valid, sizeof(uint32_t));
-    if (backup_is_valid != 1) {
-      backup_is_valid = 1;
-      TlclWrite(KERNEL_BACKUP_IS_VALID_NV_INDEX, (uint8_t*) &backup_is_valid,
-                sizeof(uint32_t));
+    if (TlclRead(KERNEL_MUST_USE_BACKUP_NV_INDEX, (uint8_t*) &must_use_backup,
+                 sizeof(uint32_t)) != TPM_SUCCESS)
+      goto recovery_mode;
+    if (must_use_backup != 1) {
+      must_use_backup = 1;
+      result = TlclWrite(KERNEL_MUST_USE_BACKUP_NV_INDEX,
+                         (uint8_t*) &must_use_backup, sizeof(uint32_t));
+      if (result == TPM_E_MAXNVWRITES) {
+        goto forceclear_and_reboot;
+      } else if (result != TPM_SUCCESS) {
+        goto recovery_mode;
+      }
     }
     /* Protects the firmware and backup kernel versions. */
-    LockFirmwareVersions();
+    if (LockFirmwareVersions() != TPM_SUCCESS)
+      goto recovery_mode;
   }
+
+ recovery_mode:
   debug("entering recovery mode");
 
   /* TODO(nelson): code for entering recovery mode. */
+
+ forceclear_and_reboot:
+  if (TlclForceClear() != TPM_SUCCESS) {
+    goto recovery_mode;
+  }
+  /* TODO: reboot */
 }
 
 static uint32_t GetTPMRollbackIndices(void) {
-  uint32_t backup_is_valid;
   uint32_t firmware_versions;
   uint32_t kernel_versions;
 
-  if (TlclRead(KERNEL_BACKUP_IS_VALID_NV_INDEX, (uint8_t*) &backup_is_valid,
-               sizeof(uint32_t)) != TPM_SUCCESS) {
-    EnterRecovery(1);
-  }
-  if (backup_is_valid) {
-    /* We reach this path if the previous boot went into recovery mode and we
-     * made a copy of the kernel versions to protect them.
-     */
-    uint32_t protected_combined_versions;
-    uint32_t unsafe_combined_versions;
-    uint32_t result;
-    uint32_t zero = 0;
-    if (TlclRead(KERNEL_VERSIONS_BACKUP_NV_INDEX,
-                 (uint8_t*) &protected_combined_versions,
-                 sizeof(uint32_t)) != TPM_SUCCESS) {
-      EnterRecovery(1);
-    }
-    result = TlclRead(KERNEL_VERSIONS_NV_INDEX,
-                      (uint8_t*) &unsafe_combined_versions, sizeof(uint32_t));
-    if (result == TPM_E_BADINDEX) {
-      /* Jeez, someone removed the space.  This is either hostile or extremely
-       * incompetent.  Foo to them.  Politeness and lack of an adequate
-       * character set prevent me from expressing my true feelings.
-       */
-      RETURN_ON_FAILURE(TlclDefineSpace(KERNEL_VERSIONS_NV_INDEX,
-                                        TPM_NV_PER_PPWRITE,
-                                        sizeof(uint32_t)));
-    } else if (result != TPM_SUCCESS) {
-      EnterRecovery(1);
-    }
-    if (result == TPM_E_BADINDEX ||
-        protected_combined_versions != unsafe_combined_versions) {
-      RETURN_ON_FAILURE(TlclWrite(KERNEL_VERSIONS_NV_INDEX,
-                                  (uint8_t*) &protected_combined_versions,
-                                  sizeof(uint32_t)));
-    }
-    /* We recovered the backed-up versions and now we can reset the
-     * BACKUP_IS_VALID flag.
-     */
-    RETURN_ON_FAILURE(TlclWrite(KERNEL_BACKUP_IS_VALID_NV_INDEX,
-                                (uint8_t*) &zero, 0));
-
-    if (!TlclIsOwned()) {
-      /* Must ForceClear and reboot to prevent from running into the 64-write
-       * limit.
-       */
-      RETURN_ON_FAILURE(TlclForceClear());
-      /* Reboot!  No return */
-      return 9999;
-    }
-  }
-
   /* We perform the reads, making sure they succeed. A failure means that the
    * rollback index locations are missing or somehow messed up.  We let the
    * caller deal with that.
@@ -192,35 +174,118 @@
   return TPM_SUCCESS;
 }
 
+/* Checks if the kernel version space has been mucked with.  If it has,
+ * reconstructs it using the backup value.
+ */
+uint32_t RecoverKernelSpace(void) {
+  uint32_t perms = 0;
+  uint8_t buffer[KERNEL_SPACE_SIZE];
+  int read_OK = 0;
+  int perms_OK = 0;
+  uint32_t backup_combined_versions;
+  uint32_t must_use_backup;
 
-uint32_t SetupTPM(void) {
+  RETURN_ON_FAILURE(TlclRead(KERNEL_MUST_USE_BACKUP_NV_INDEX,
+                             (uint8_t*) &must_use_backup, sizeof(uint32_t)));
+  /* must_use_backup is true if the previous boot entered recovery mode. */
+
+  read_OK = TlclRead(KERNEL_VERSIONS_NV_INDEX, (uint8_t*) &buffer,
+                     KERNEL_SPACE_SIZE) == TPM_SUCCESS;
+  if (read_OK) {
+    RETURN_ON_FAILURE(TlclGetPermissions(KERNEL_VERSIONS_NV_INDEX, &perms));
+    perms_OK = perms == TPM_NV_PER_PPWRITE;
+  }
+  if (!must_use_backup && read_OK && perms_OK &&
+      !Memcmp(buffer + sizeof(uint32_t), KERNEL_SPACE_UID,
+              KERNEL_SPACE_UID_SIZE)) {
+    /* Everything is fine.  This is the normal, frequent path. */
+    return TPM_SUCCESS;
+  }
+
+  /* Either we detected that something went wrong, or we cannot trust the
+   * PP-protected kernel space.  Attempts to fix.  It is not always necessary
+   * to redefine the space, but we might as well, since this path should be
+   * taken quite seldom (after recovery mode and after an attack).
+   */
+  RETURN_ON_FAILURE(InitializeKernelVersionsSpaces());
+  RETURN_ON_FAILURE(TlclRead(KERNEL_VERSIONS_BACKUP_NV_INDEX,
+                             (uint8_t*) &backup_combined_versions,
+                             sizeof(uint32_t)));
+  RETURN_ON_FAILURE(TlclWrite(KERNEL_VERSIONS_NV_INDEX,
+                              (uint8_t*) &backup_combined_versions,
+                              sizeof(uint32_t)));
+  if (must_use_backup) {
+    uint32_t zero = 0;
+    RETURN_ON_FAILURE(TlclWrite(KERNEL_MUST_USE_BACKUP_NV_INDEX,
+                                (uint8_t*) &zero, 0));
+    
+  }
+  return TPM_SUCCESS;
+}
+
+static uint32_t BackupKernelSpace(void) {
+  uint32_t kernel_versions;
+  uint32_t backup_versions;
+  RETURN_ON_FAILURE(TlclRead(KERNEL_VERSIONS_NV_INDEX,
+                             (uint8_t*) &kernel_versions, sizeof(uint32_t)));
+  RETURN_ON_FAILURE(TlclRead(KERNEL_VERSIONS_BACKUP_NV_INDEX,
+                             (uint8_t*) &backup_versions, sizeof(uint32_t)));
+  if (kernel_versions == backup_versions) {
+    return TPM_SUCCESS;
+  } else if (kernel_versions < backup_versions) {
+    /* This cannot happen.  We're screwed. */
+    return TPM_E_INTERNAL_INCONSISTENCY;
+  }
+  RETURN_ON_FAILURE(TlclWrite(KERNEL_VERSIONS_BACKUP_NV_INDEX,
+                              (uint8_t*) &kernel_versions, sizeof(uint32_t)));
+  return TPM_SUCCESS;
+}
+
+static uint32_t SetupTPM_(void) {
   uint8_t disable;
   uint8_t deactivated;
   TlclLibInit();
   RETURN_ON_FAILURE(TlclStartup());
   RETURN_ON_FAILURE(TlclContinueSelfTest());
   RETURN_ON_FAILURE(TlclAssertPhysicalPresence());
-  /* Check that the TPM is enabled and activated. */
+  /* Checks that the TPM is enabled and activated. */
   RETURN_ON_FAILURE(TlclGetFlags(&disable, &deactivated));
   if (disable || deactivated) {
     RETURN_ON_FAILURE(TlclSetEnable());
     RETURN_ON_FAILURE(TlclSetDeactivated(0));
-    /* TODO: Reboot now */
+    /* TODO: Reboot */
     return 9999;
   }
   /* We expect this to fail the first time we run on a device, indicating that
    * the TPM has not been initialized yet. */
-  if (GetTPMRollbackIndices() != TPM_SUCCESS) {
-    /* If InitializeSpaces() fails (possibly because it had been executed
-     * already), something is wrong. */
-    RETURN_ON_FAILURE(InitializeSpaces());
-    /* Try again. */
-    RETURN_ON_FAILURE(GetTPMRollbackIndices());
+  if (RecoverKernelSpace() != TPM_SUCCESS) {
+    int initialized = 0;
+    RETURN_ON_FAILURE(GetSpacesInitialized(&initialized));
+    if (initialized) {
+      return TPM_E_ALREADY_INITIALIZED;
+    } else {
+      RETURN_ON_FAILURE(InitializeSpaces());
+      RETURN_ON_FAILURE(RecoverKernelSpace());
+    }
   }
+  RETURN_ON_FAILURE(BackupKernelSpace());
+  RETURN_ON_FAILURE(GetTPMRollbackIndices());
 
   return TPM_SUCCESS;
 }
 
+uint32_t SetupTPM(void) {
+  uint32_t result = SetupTPM_();
+  if (result == TPM_E_MAXNVWRITES) {
+    /* ForceClears and reboots */
+    RETURN_ON_FAILURE(TlclForceClear());
+    /* TODO: reboot */
+    return 9999;
+  } else {
+    return result;
+  }
+}
+
 uint32_t GetStoredVersions(int type, uint16_t* key_version, uint16_t* version) {
 
   /* TODO: should verify that SetupTPM() has been called.  Note that
@@ -257,15 +322,6 @@
       RETURN_ON_FAILURE(TlclWrite(KERNEL_VERSIONS_NV_INDEX,
                                   (uint8_t*) &combined_version,
                                   sizeof(uint32_t)));
-      break;
-  }
-  /* TODO(semenzato): change TlclIsOwned to return a TPM status directly and
-   * the "owned" value by reference.
-   */
-  if (!TlclIsOwned()) {
-    RETURN_ON_FAILURE(TlclForceClear());
-    /* TODO: Reboot here.  No return. */
-    return 9999;
   }
   return TPM_SUCCESS;
 }