[llvm] [CAS] Support validation and recovery for plugin CAS in llvm-cas (PR #226291)

Steven Wu via llvm-commits llvm-commits at lists.llvm.org
Thu Sep 24 14:14:58 PDT 2026


https://github.com/cachemeifyoucan updated https://github.com/llvm/llvm-project/pull/226291

>From f665aafddbdf68ed04e993aa93ed27ac790c03e0 Mon Sep 17 00:00:00 2001
From: Steven Wu <stevenwu at apple.com>
Date: Thu, 24 Sep 2026 13:13:50 -0700
Subject: [PATCH 1/2] =?UTF-8?q?[=F0=9D=98=80=F0=9D=97=BD=F0=9D=97=BF]=20in?=
 =?UTF-8?q?itial=20version?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Created using spr 1.3.7
---
 llvm/include/llvm-c/CAS/PluginAPI_functions.h |  47 +++
 llvm/include/llvm-c/CAS/PluginAPI_types.h     |  28 +-
 .../llvm/CAS/BuiltinUnifiedCASDatabases.h     |  29 +-
 llvm/include/llvm/CAS/ObjectStore.h           |  39 ++
 llvm/include/llvm/CAS/OnDiskCASLogger.h       |   7 +-
 llvm/include/llvm/CAS/UnifiedOnDiskCache.h    |  43 +-
 llvm/include/llvm/CAS/ValidationResult.h      |   7 +-
 llvm/lib/CAS/BuiltinUnifiedCASDatabases.cpp   |  19 +-
 llvm/lib/CAS/OnDiskCASLogger.cpp              |  22 +-
 llvm/lib/CAS/PluginAPI.h                      |   7 +
 llvm/lib/CAS/PluginAPI_functions.def          |   2 +
 llvm/lib/CAS/PluginCAS.cpp                    | 102 ++++-
 llvm/lib/CAS/UnifiedOnDiskCache.cpp           | 376 +++++++++++-------
 llvm/test/CMakeLists.txt                      |  12 +
 llvm/test/lit.site.cfg.py.in                  |   1 +
 llvm/test/tools/llvm-cas/lit.local.cfg        |   4 +
 llvm/test/tools/llvm-cas/logging.test         |   8 +-
 .../llvm-cas/plugin-validation-crash.test     |  59 +++
 .../tools/llvm-cas/plugin-validation.test     |  73 ++++
 .../libCASPluginTest/libCASPluginTest.cpp     |  65 +++
 .../libCASPluginTest/libCASPluginTest.exports |   4 +
 llvm/tools/llvm-cas/Options.td                |   8 +
 llvm/tools/llvm-cas/llvm-cas.cpp              | 125 ++++--
 llvm/unittests/CAS/PluginCASTest.cpp          |  91 +++++
 llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp | 104 ++++-
 25 files changed, 1036 insertions(+), 246 deletions(-)
 create mode 100644 llvm/test/tools/llvm-cas/plugin-validation-crash.test
 create mode 100644 llvm/test/tools/llvm-cas/plugin-validation.test

diff --git a/llvm/include/llvm-c/CAS/PluginAPI_functions.h b/llvm/include/llvm-c/CAS/PluginAPI_functions.h
index 94ef0759873b3..b73872655c690 100644
--- a/llvm/include/llvm-c/CAS/PluginAPI_functions.h
+++ b/llvm/include/llvm-c/CAS/PluginAPI_functions.h
@@ -154,6 +154,53 @@ LLCAS_PUBLIC bool llcas_cas_prune_ondisk_data(llcas_cas_t, char **error);
 LLCAS_PUBLIC bool llcas_cas_validate(llcas_cas_t, bool check_hash,
                                      char **error);
 
+/**
+ * Validate the on-disk CAS and action cache contents in-process, if needed.
+ *
+ * This is called without a \c llcas_cas_t being created for the given
+ * options. The implementation decides whether validation is needed, e.g.
+ * whether the contents have already been validated since the last system boot,
+ * and should record a successful validation so that it can be skipped next
+ * time. A validation that fails or crashes should be detectable by
+ * \c llcas_cas_recover_ondisk_data, e.g. by recording that validation is
+ * pending before validating and only clearing it once validation succeeds.
+ *
+ * Validation may crash on invalid data, so clients may call this from a
+ * separate process and call \c llcas_cas_recover_ondisk_data if it fails or
+ * crashes.
+ *
+ * \param check_hash if true, the hash of each object is recomputed and compared
+ * against the one it is stored under.
+ * \param force if true, validation is performed even if it is not needed.
+ * \param error optional pointer to receive an error message if an error
+ * occurred. If set, the memory it points to needs to be released via
+ * \c llcas_string_dispose.
+ * \returns \c LLCAS_VALIDATION_RESULT_VALID or
+ * \c LLCAS_VALIDATION_RESULT_SKIPPED, or \c LLCAS_VALIDATION_RESULT_ERROR if
+ * validation could not be performed or the data is invalid.
+ */
+LLCAS_PUBLIC llcas_validation_result_t llcas_cas_validate_if_needed(
+    llcas_cas_options_t, bool check_hash, bool force, char **error);
+
+/**
+ * Recover from invalid on-disk CAS and action cache contents after
+ * \c llcas_cas_validate_if_needed failed or crashed, e.g. by discarding them.
+ *
+ * This is called without a \c llcas_cas_t being created for the given
+ * options. Multiple processes may attempt recovery concurrently after a failed
+ * validation; the implementation should skip recovery if the contents have
+ * been recovered or successfully validated in the meantime.
+ *
+ * \param error optional pointer to receive an error message if an error
+ * occurred. If set, the memory it points to needs to be released via
+ * \c llcas_string_dispose.
+ * \returns \c LLCAS_VALIDATION_RESULT_RECOVERED or
+ * \c LLCAS_VALIDATION_RESULT_SKIPPED, or \c LLCAS_VALIDATION_RESULT_ERROR if
+ * recovery could not be performed.
+ */
+LLCAS_PUBLIC llcas_validation_result_t
+llcas_cas_recover_ondisk_data(llcas_cas_options_t, char **error);
+
 /**
  * \returns the hash schema name that the plugin is using. The string memory it
  * points to needs to be released via \c llcas_string_dispose.
diff --git a/llvm/include/llvm-c/CAS/PluginAPI_types.h b/llvm/include/llvm-c/CAS/PluginAPI_types.h
index 2d45461aea28a..19fc2a27ec222 100644
--- a/llvm/include/llvm-c/CAS/PluginAPI_types.h
+++ b/llvm/include/llvm-c/CAS/PluginAPI_types.h
@@ -20,7 +20,7 @@
 #include <stdint.h>
 
 #define LLCAS_VERSION_MAJOR 0
-#define LLCAS_VERSION_MINOR 2
+#define LLCAS_VERSION_MINOR 3
 
 typedef struct llcas_cas_options_s *llcas_cas_options_t;
 typedef struct llcas_cas_s *llcas_cas_t;
@@ -84,6 +84,32 @@ typedef enum {
   LLCAS_LOOKUP_RESULT_ERROR = 2,
 } llcas_lookup_result_t;
 
+/**
+ * Return values for \c llcas_cas_validate_if_needed and
+ * \c llcas_cas_recover_ondisk_data.
+ */
+typedef enum {
+  /**
+   * The data is valid.
+   */
+  LLCAS_VALIDATION_RESULT_VALID = 0,
+
+  /**
+   * The data was invalid, but was recovered.
+   */
+  LLCAS_VALIDATION_RESULT_RECOVERED = 1,
+
+  /**
+   * Validation or recovery was skipped, as it was not needed.
+   */
+  LLCAS_VALIDATION_RESULT_SKIPPED = 2,
+
+  /**
+   * An error occurred.
+   */
+  LLCAS_VALIDATION_RESULT_ERROR = 3,
+} llcas_validation_result_t;
+
 /**
  * Callback for \c llcas_cas_load_object_async.
  *
diff --git a/llvm/include/llvm/CAS/BuiltinUnifiedCASDatabases.h b/llvm/include/llvm/CAS/BuiltinUnifiedCASDatabases.h
index e95e50b46e863..3a317f7177a8f 100644
--- a/llvm/include/llvm/CAS/BuiltinUnifiedCASDatabases.h
+++ b/llvm/include/llvm/CAS/BuiltinUnifiedCASDatabases.h
@@ -23,26 +23,17 @@ LLVM_ABI
 Expected<std::pair<std::unique_ptr<ObjectStore>, std::unique_ptr<ActionCache>>>
 createOnDiskUnifiedCASDatabases(StringRef Path);
 
-/// Validate the data in \p Path, if needed to ensure correctness.
-///
-/// \param Path directory for the on-disk database.
-/// \param CheckHash Whether to validate hashes match the data.
-/// \param AllowRecovery Whether to automatically recover from invalid data by
-/// marking the files for garbage collection.
-/// \param ForceValidation Whether to force validation to occur even if it
-/// should not be necessary.
-/// \param LLVMCasBinaryPath If provided, validation is performed out-of-process
-/// using the given \c llvm-cas executable which protects against crashes
-/// during validation. Otherwise validation is performed in-process.
-///
-/// \returns \c Valid if the data is already valid, \c Recovered if data
-/// was invalid but has been cleared, \c Skipped if validation is not needed,
-/// or an \c Error if validation cannot be performed or if the data is left
-/// in an invalid state because \p AllowRecovery is false.
+/// Validate the builtin on-disk CAS in \p Path in-process, if needed. See
+/// \c ondisk::UnifiedOnDiskCache::validateIfNeeded.
 LLVM_ABI
-Expected<ValidationResult> validateOnDiskUnifiedCASDatabasesIfNeeded(
-    StringRef Path, bool CheckHash, bool AllowRecovery, bool ForceValidation,
-    std::optional<StringRef> LLVMCasBinaryPath);
+Expected<ValidationResult>
+validateOnDiskUnifiedCASDatabasesIfNeeded(StringRef Path, bool CheckHash,
+                                          bool ForceValidation);
+
+/// Recover the builtin on-disk CAS in \p Path after a failed validation. See
+/// \c ondisk::UnifiedOnDiskCache::recover.
+LLVM_ABI
+Expected<ValidationResult> recoverOnDiskUnifiedCASDatabases(StringRef Path);
 
 } // namespace llvm::cas
 
diff --git a/llvm/include/llvm/CAS/ObjectStore.h b/llvm/include/llvm/CAS/ObjectStore.h
index 972d8eb02b724..394eb9d53d7a6 100644
--- a/llvm/include/llvm/CAS/ObjectStore.h
+++ b/llvm/include/llvm/CAS/ObjectStore.h
@@ -17,6 +17,7 @@
 #include "llvm/ADT/StringRef.h"
 #include "llvm/CAS/CASID.h"
 #include "llvm/CAS/CASReference.h"
+#include "llvm/CAS/ValidationResult.h"
 #include "llvm/Support/Error.h"
 #include "llvm/Support/FileSystem.h"
 #include <cstddef>
@@ -408,6 +409,44 @@ createPluginCASDatabases(
     StringRef PluginPath, StringRef OnDiskPath,
     ArrayRef<std::pair<std::string, std::string>> PluginArgs);
 
+/// Validate the on-disk data of a plugin-backed CAS in-process if needed, by
+/// calling the plugin's \c llcas_cas_validate_if_needed. The plugin decides
+/// whether validation is needed.
+///
+/// Validation can crash on invalid data. Clients that want to be resilient to
+/// that should call this from a separate process (e.g. via
+/// \c llvm-cas -validate-if-needed) and call \c recoverPluginCASDatabases if
+/// it fails.
+///
+/// \param PluginPath path of the dynamic library to load.
+/// \param OnDiskPath local path that the plugin uses for any on-disk
+/// resources/caches.
+/// \param PluginArgs name/value pairs passed to the plugin as custom options;
+/// they are opaque to the client.
+/// \param CheckHash Whether to validate hashes match the data.
+/// \param ForceValidation Whether to force validation to occur even if it
+/// should not be necessary.
+///
+/// \returns \c Valid if the data is valid, \c Skipped if validation is not
+/// needed, or an \c Error if validation cannot be performed (including if the
+/// plugin does not support it) or the data is invalid.
+LLVM_ABI Expected<ValidationResult> validatePluginCASDatabasesIfNeeded(
+    StringRef PluginPath, StringRef OnDiskPath,
+    ArrayRef<std::pair<std::string, std::string>> PluginArgs, bool CheckHash,
+    bool ForceValidation);
+
+/// Recover the on-disk data of a plugin-backed CAS after a failed
+/// \c validatePluginCASDatabasesIfNeeded, by calling the plugin's
+/// \c llcas_cas_recover_ondisk_data.
+///
+/// \returns \c Recovered if the data has been recovered, \c Skipped if
+/// recovery is not needed (e.g. a concurrent process already recovered), or an
+/// \c Error if recovery cannot be performed (including if the plugin does not
+/// support it).
+LLVM_ABI Expected<ValidationResult> recoverPluginCASDatabases(
+    StringRef PluginPath, StringRef OnDiskPath,
+    ArrayRef<std::pair<std::string, std::string>> PluginArgs);
+
 } // namespace cas
 } // namespace llvm
 
diff --git a/llvm/include/llvm/CAS/OnDiskCASLogger.h b/llvm/include/llvm/CAS/OnDiskCASLogger.h
index 1bea1e082c2f9..d54bdf7258cdf 100644
--- a/llvm/include/llvm/CAS/OnDiskCASLogger.h
+++ b/llvm/include/llvm/CAS/OnDiskCASLogger.h
@@ -72,9 +72,10 @@ class OnDiskCASLogger {
   LLVM_ABI void logUnifiedOnDiskCacheCollectGarbage(StringRef Path);
   LLVM_ABI void logUnifiedOnDiskCacheValidateIfNeeded(
       StringRef Path, uint64_t BootTime, uint64_t ValidationTime,
-      bool CheckHash, bool AllowRecovery, bool Force,
-      std::optional<StringRef> LLVMCas, StringRef ValidationError, bool Skipped,
-      bool Recovered);
+      bool CheckHash, bool Force, StringRef ValidationError, bool Skipped);
+  LLVM_ABI void logUnifiedOnDiskCacheRecover(StringRef Path, uint64_t BootTime,
+                                             StringRef RecoveryError,
+                                             bool Skipped);
   LLVM_ABI void logTempFileCreate(StringRef Name);
   LLVM_ABI void logTempFileKeep(StringRef TmpName, StringRef Name,
                                 std::error_code EC);
diff --git a/llvm/include/llvm/CAS/UnifiedOnDiskCache.h b/llvm/include/llvm/CAS/UnifiedOnDiskCache.h
index 24bc4a36883a2..554ca5e0cceba 100644
--- a/llvm/include/llvm/CAS/UnifiedOnDiskCache.h
+++ b/llvm/include/llvm/CAS/UnifiedOnDiskCache.h
@@ -70,34 +70,47 @@ class UnifiedOnDiskCache {
        OnDiskGraphDB::FaultInPolicy FaultInPolicy =
            OnDiskGraphDB::FaultInPolicy::FullTree);
 
-  /// Validate the data in \p Path, if needed to ensure correctness.
+  /// Validate the data in \p Path in-process, if it has not been validated
+  /// since the last system boot. A successful validation is recorded so that
+  /// subsequent calls can skip it; a failed or crashed one is recorded as
+  /// pending for \c recover, and is not skipped by subsequent calls.
   ///
-  /// Note: if invalid data is detected and \p AllowRecovery is true, then
-  /// recovery requires exclusive access to the CAS and it is an error to
-  /// attempt recovery if there is concurrent use of the CAS.
+  /// Validation can crash on invalid data. Clients that want to be resilient
+  /// to that should call this from a separate process (e.g. via
+  /// \c llvm-cas -validate-if-needed) and call \c recover if it fails.
   ///
   /// \param Path directory for the on-disk database.
   /// \param HashName Identifier name for the hashing algorithm that is going to
   /// be used.
   /// \param HashByteSize Size for the object digest hash bytes.
   /// \param CheckHash Whether to validate hashes match the data.
-  /// \param AllowRecovery Whether to automatically recover from invalid data by
-  /// marking the files for garbage collection.
   /// \param ForceValidation Whether to force validation to occur even if it
   /// should not be necessary.
-  /// \param LLVMCasBinary If provided, validation is performed out-of-process
-  /// using the given \c llvm-cas executable which protects against crashes
-  /// during validation. Otherwise validation is performed in-process.
   ///
-  /// \returns \c Valid if the data is already valid, \c Recovered if data
-  /// was invalid but has been cleared, \c Skipped if validation is not needed,
-  /// or an \c Error if validation cannot be performed or if the data is left
-  /// in an invalid state because \p AllowRecovery is false.
+  /// \returns \c Valid if the data is valid, \c Skipped if validation is not
+  /// needed, or an \c Error if validation cannot be performed or the data is
+  /// invalid.
   LLVM_ABI static Expected<ValidationResult>
   validateIfNeeded(StringRef Path, StringRef HashName, unsigned HashByteSize,
                    bool CheckHash, OnDiskGraphDB::HashingFuncT HashFn,
-                   bool AllowRecovery, bool ForceValidation,
-                   std::optional<StringRef> LLVMCasBinary);
+                   bool ForceValidation);
+
+  /// Recover from invalid data in \p Path after a failed \c validateIfNeeded,
+  /// by marking all the data for garbage collection.
+  ///
+  /// Recovery requires exclusive access to the CAS and it is an error to
+  /// attempt recovery if there is concurrent use of the CAS.
+  ///
+  /// Recovery is serialized with \c validateIfNeeded, and only happens if the
+  /// last validation failed or crashed. If the data has been recovered or
+  /// validated successfully since, e.g. by a concurrent process, recovery is
+  /// skipped.
+  ///
+  /// \param Path directory for the on-disk database.
+  ///
+  /// \returns \c Recovered if the data has been cleared, \c Skipped if
+  /// recovery is not needed, or an \c Error if recovery cannot be performed.
+  LLVM_ABI static Expected<ValidationResult> recover(StringRef Path);
 
   /// Validate the action cache only.
   LLVM_ABI Error validateActionCache() const;
diff --git a/llvm/include/llvm/CAS/ValidationResult.h b/llvm/include/llvm/CAS/ValidationResult.h
index bd3d2999bda9e..81b25d8a3b170 100644
--- a/llvm/include/llvm/CAS/ValidationResult.h
+++ b/llvm/include/llvm/CAS/ValidationResult.h
@@ -11,8 +11,9 @@
 
 namespace llvm::cas {
 
-/// Represents the result of validating the contents using
-/// \c validateOnDiskUnifiedCASDatabasesIfNeeded.
+/// Represents the result of validating the contents, e.g. using
+/// \c validateOnDiskUnifiedCASDatabasesIfNeeded, or of recovering from a
+/// failed validation, e.g. using \c recoverOnDiskUnifiedCASDatabases.
 ///
 /// Note: invalid results are handled as an \c Error.
 enum class ValidationResult {
@@ -20,7 +21,7 @@ enum class ValidationResult {
   Valid,
   /// The data was invalid, but was recovered.
   Recovered,
-  /// Validation was skipped, as it was not needed.
+  /// Validation or recovery was skipped, as it was not needed.
   Skipped,
 };
 
diff --git a/llvm/lib/CAS/BuiltinUnifiedCASDatabases.cpp b/llvm/lib/CAS/BuiltinUnifiedCASDatabases.cpp
index 8283e981b1099..11fd2507bcb3a 100644
--- a/llvm/lib/CAS/BuiltinUnifiedCASDatabases.cpp
+++ b/llvm/lib/CAS/BuiltinUnifiedCASDatabases.cpp
@@ -24,14 +24,23 @@ cas::createOnDiskUnifiedCASDatabases(StringRef Path) {
   return std::make_pair(std::move(CAS), std::move(AC));
 }
 
-Expected<ValidationResult> cas::validateOnDiskUnifiedCASDatabasesIfNeeded(
-    StringRef Path, bool CheckHash, bool AllowRecovery, bool ForceValidation,
-    std::optional<StringRef> LLVMCasBinary) {
+Expected<ValidationResult>
+cas::validateOnDiskUnifiedCASDatabasesIfNeeded(StringRef Path, bool CheckHash,
+                                               bool ForceValidation) {
 #if LLVM_ENABLE_ONDISK_CAS
   return ondisk::UnifiedOnDiskCache::validateIfNeeded(
       Path, builtin::BuiltinCASContext::getHashName(),
-      sizeof(builtin::HashType), CheckHash, builtin::hashingFunc, AllowRecovery,
-      ForceValidation, LLVMCasBinary);
+      sizeof(builtin::HashType), CheckHash, builtin::hashingFunc,
+      ForceValidation);
+#else
+  return createStringError(inconvertibleErrorCode(), "OnDiskCache is disabled");
+#endif
+}
+
+Expected<ValidationResult>
+cas::recoverOnDiskUnifiedCASDatabases(StringRef Path) {
+#if LLVM_ENABLE_ONDISK_CAS
+  return ondisk::UnifiedOnDiskCache::recover(Path);
 #else
   return createStringError(inconvertibleErrorCode(), "OnDiskCache is disabled");
 #endif
diff --git a/llvm/lib/CAS/OnDiskCASLogger.cpp b/llvm/lib/CAS/OnDiskCASLogger.cpp
index 8ff63a8f85248..33d09489b6dc4 100644
--- a/llvm/lib/CAS/OnDiskCASLogger.cpp
+++ b/llvm/lib/CAS/OnDiskCASLogger.cpp
@@ -225,23 +225,29 @@ void OnDiskCASLogger::logUnifiedOnDiskCacheCollectGarbage(StringRef Path) {
 
 void OnDiskCASLogger::logUnifiedOnDiskCacheValidateIfNeeded(
     StringRef Path, uint64_t BootTime, uint64_t ValidationTime, bool CheckHash,
-    bool AllowRecovery, bool Force, std::optional<StringRef> LLVMCas,
-    StringRef ValidationError, bool Skipped, bool Recovered) {
+    bool Force, StringRef ValidationError, bool Skipped) {
   TextLogLine Log(OS);
   Log << "validate-if-needed '" << Path << "'";
   Log << " boot=" << BootTime << " last-valid=" << ValidationTime;
-  Log << " check-hash=" << CheckHash << " allow-recovery=" << AllowRecovery;
-  Log << " force=" << Force;
-  if (LLVMCas)
-    Log << " llvm-cas=" << *LLVMCas;
+  Log << " check-hash=" << CheckHash << " force=" << Force;
   if (Skipped)
     Log << " skipped";
-  if (Recovered)
-    Log << " recovered";
   if (!ValidationError.empty())
     Log << " data was invalid " << ValidationError;
 }
 
+void OnDiskCASLogger::logUnifiedOnDiskCacheRecover(StringRef Path,
+                                                   uint64_t BootTime,
+                                                   StringRef RecoveryError,
+                                                   bool Skipped) {
+  TextLogLine Log(OS);
+  Log << "recover '" << Path << "' boot=" << BootTime;
+  if (Skipped)
+    Log << " skipped";
+  if (!RecoveryError.empty())
+    Log << " failed " << RecoveryError;
+}
+
 void OnDiskCASLogger::logTempFileCreate(StringRef Name) {
   TextLogLine Log(OS);
   Log << "standalone file create '" << Name << "'";
diff --git a/llvm/lib/CAS/PluginAPI.h b/llvm/lib/CAS/PluginAPI.h
index 769020403464b..28a7931f6f4e2 100644
--- a/llvm/lib/CAS/PluginAPI.h
+++ b/llvm/lib/CAS/PluginAPI.h
@@ -53,6 +53,13 @@ struct llcas_functions_t {
 
   bool (*cas_validate)(llcas_cas_t, bool check_hash, char **error);
 
+  llcas_validation_result_t (*cas_validate_if_needed)(llcas_cas_options_t,
+                                                      bool check_hash,
+                                                      bool force, char **error);
+
+  llcas_validation_result_t (*cas_recover_ondisk_data)(llcas_cas_options_t,
+                                                       char **error);
+
   unsigned (*digest_parse)(llcas_cas_t, const char *printed_digest,
                            uint8_t *bytes, size_t bytes_size, char **error);
 
diff --git a/llvm/lib/CAS/PluginAPI_functions.def b/llvm/lib/CAS/PluginAPI_functions.def
index 8e779b68d8288..c12ccaf1a3714 100644
--- a/llvm/lib/CAS/PluginAPI_functions.def
+++ b/llvm/lib/CAS/PluginAPI_functions.def
@@ -36,10 +36,12 @@ CASPLUGINAPI_FUNCTION(cas_options_set_client_version, true)
 CASPLUGINAPI_FUNCTION(cas_options_set_ondisk_path, true)
 CASPLUGINAPI_FUNCTION(cas_options_set_option, true)
 CASPLUGINAPI_FUNCTION(cas_prune_ondisk_data, false)
+CASPLUGINAPI_FUNCTION(cas_recover_ondisk_data, false)
 CASPLUGINAPI_FUNCTION(cas_set_ondisk_size_limit, false)
 CASPLUGINAPI_FUNCTION(cas_store_from_filepath, false)
 CASPLUGINAPI_FUNCTION(cas_store_object, true)
 CASPLUGINAPI_FUNCTION(cas_validate, false)
+CASPLUGINAPI_FUNCTION(cas_validate_if_needed, false)
 CASPLUGINAPI_FUNCTION(digest_parse, true)
 CASPLUGINAPI_FUNCTION(digest_print, true)
 CASPLUGINAPI_FUNCTION(get_plugin_version, true)
diff --git a/llvm/lib/CAS/PluginCAS.cpp b/llvm/lib/CAS/PluginCAS.cpp
index 945a5ac43f56e..682a674a984dc 100644
--- a/llvm/lib/CAS/PluginCAS.cpp
+++ b/llvm/lib/CAS/PluginCAS.cpp
@@ -17,6 +17,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "PluginAPI.h"
+#include "llvm/ADT/STLFunctionalExtras.h"
 #include "llvm/ADT/ScopeExit.h"
 #include "llvm/CAS/ActionCache.h"
 #include "llvm/CAS/ObjectStore.h"
@@ -69,9 +70,8 @@ void PluginCASContext::printIDImpl(raw_ostream &OS, const CASID &ID) const {
   Functions.string_dispose(c_printed_id);
 }
 
-Expected<std::shared_ptr<PluginCASContext>> PluginCASContext::create(
-    StringRef PluginPath, StringRef OnDiskPath,
-    ArrayRef<std::pair<std::string, std::string>> PluginArgs) {
+/// Loads the plugin library at \p PluginPath and looks up its functions.
+static Expected<llcas_functions_t> loadPluginFunctions(StringRef PluginPath) {
   auto reportError = [PluginPath](const Twine &Description) -> Error {
     std::error_code EC = inconvertibleErrorCode();
     return createStringError(EC, "error loading '" + PluginPath +
@@ -96,9 +96,15 @@ Expected<std::shared_ptr<PluginCASContext>> PluginCASContext::create(
 #include "PluginAPI_functions.def"
 #undef CASPLUGINAPI_FUNCTION
 
-  llcas_cas_options_t c_opts = Functions.cas_options_create();
-  scope_exit DisposeOptions([&]() { Functions.cas_options_dispose(c_opts); });
+  return Functions;
+}
 
+/// Creates a \c llcas_cas_options_t for \p OnDiskPath and \p PluginArgs. On
+/// success the caller is responsible for disposing it.
+static Expected<llcas_cas_options_t>
+createPluginOptions(const llcas_functions_t &Functions, StringRef OnDiskPath,
+                    ArrayRef<std::pair<std::string, std::string>> PluginArgs) {
+  llcas_cas_options_t c_opts = Functions.cas_options_create();
   Functions.cas_options_set_client_version(c_opts, LLCAS_VERSION_MAJOR,
                                            LLCAS_VERSION_MINOR);
   SmallString<256> OnDiskPathBuf = OnDiskPath;
@@ -106,9 +112,26 @@ Expected<std::shared_ptr<PluginCASContext>> PluginCASContext::create(
   for (const auto &Pair : PluginArgs) {
     char *c_err = nullptr;
     if (Functions.cas_options_set_option(c_opts, Pair.first.c_str(),
-                                         Pair.second.c_str(), &c_err))
-      return errorAndDispose(c_err, Functions);
+                                         Pair.second.c_str(), &c_err)) {
+      Functions.cas_options_dispose(c_opts);
+      return PluginCASContext::errorAndDispose(c_err, Functions);
+    }
   }
+  return c_opts;
+}
+
+Expected<std::shared_ptr<PluginCASContext>> PluginCASContext::create(
+    StringRef PluginPath, StringRef OnDiskPath,
+    ArrayRef<std::pair<std::string, std::string>> PluginArgs) {
+  llcas_functions_t Functions{};
+  if (Error E = loadPluginFunctions(PluginPath).moveInto(Functions))
+    return std::move(E);
+
+  llcas_cas_options_t c_opts = nullptr;
+  if (Error E = createPluginOptions(Functions, OnDiskPath, PluginArgs)
+                    .moveInto(c_opts))
+    return std::move(E);
+  scope_exit DisposeOptions([&]() { Functions.cas_options_dispose(c_opts); });
 
   char *c_err = nullptr;
   llcas_cas_t c_cas = Functions.cas_create(c_opts, &c_err);
@@ -563,3 +586,68 @@ cas::createPluginCASDatabases(
   auto AC = std::make_shared<PluginActionCache>(std::move(Ctx));
   return std::make_pair(std::move(CAS), std::move(AC));
 }
+
+/// Loads the plugin and calls \p Fn with a \c llcas_cas_options_t for
+/// \p OnDiskPath and \p PluginArgs, converting the returned
+/// \c llcas_validation_result_t.
+static Expected<ValidationResult> callPluginValidationFunction(
+    StringRef PluginPath, StringRef OnDiskPath,
+    ArrayRef<std::pair<std::string, std::string>> PluginArgs,
+    function_ref<Expected<llcas_validation_result_t>(
+        const llcas_functions_t &, llcas_cas_options_t, char **)>
+        Fn) {
+  llcas_functions_t Functions{};
+  if (Error E = loadPluginFunctions(PluginPath).moveInto(Functions))
+    return std::move(E);
+
+  llcas_cas_options_t c_opts = nullptr;
+  if (Error E = createPluginOptions(Functions, OnDiskPath, PluginArgs)
+                    .moveInto(c_opts))
+    return std::move(E);
+  scope_exit DisposeOptions([&]() { Functions.cas_options_dispose(c_opts); });
+
+  char *c_err = nullptr;
+  llcas_validation_result_t Result;
+  if (Error E = Fn(Functions, c_opts, &c_err).moveInto(Result))
+    return std::move(E);
+  switch (Result) {
+  case LLCAS_VALIDATION_RESULT_VALID:
+    return ValidationResult::Valid;
+  case LLCAS_VALIDATION_RESULT_RECOVERED:
+    return ValidationResult::Recovered;
+  case LLCAS_VALIDATION_RESULT_SKIPPED:
+    return ValidationResult::Skipped;
+  case LLCAS_VALIDATION_RESULT_ERROR:
+    return PluginCASContext::errorAndDispose(c_err, Functions);
+  }
+  llvm_unreachable("unknown llcas_validation_result_t value");
+}
+
+Expected<ValidationResult> cas::validatePluginCASDatabasesIfNeeded(
+    StringRef PluginPath, StringRef OnDiskPath,
+    ArrayRef<std::pair<std::string, std::string>> PluginArgs, bool CheckHash,
+    bool ForceValidation) {
+  return callPluginValidationFunction(
+      PluginPath, OnDiskPath, PluginArgs,
+      [&](const llcas_functions_t &Functions, llcas_cas_options_t c_opts,
+          char **c_err) -> Expected<llcas_validation_result_t> {
+        if (!Functions.cas_validate_if_needed)
+          return createStringError(
+              "plugin cas doesn't support validate-if-needed");
+        return Functions.cas_validate_if_needed(c_opts, CheckHash,
+                                                ForceValidation, c_err);
+      });
+}
+
+Expected<ValidationResult> cas::recoverPluginCASDatabases(
+    StringRef PluginPath, StringRef OnDiskPath,
+    ArrayRef<std::pair<std::string, std::string>> PluginArgs) {
+  return callPluginValidationFunction(
+      PluginPath, OnDiskPath, PluginArgs,
+      [&](const llcas_functions_t &Functions, llcas_cas_options_t c_opts,
+          char **c_err) -> Expected<llcas_validation_result_t> {
+        if (!Functions.cas_recover_ondisk_data)
+          return createStringError("plugin cas doesn't support recovery");
+        return Functions.cas_recover_ondisk_data(c_opts, c_err);
+      });
+}
diff --git a/llvm/lib/CAS/UnifiedOnDiskCache.cpp b/llvm/lib/CAS/UnifiedOnDiskCache.cpp
index 3d0a8b3eba135..ce921a1ef7f43 100644
--- a/llvm/lib/CAS/UnifiedOnDiskCache.cpp
+++ b/llvm/lib/CAS/UnifiedOnDiskCache.cpp
@@ -53,8 +53,18 @@
 /// the underlying on-disk storage. The low-level storage is designed to remain
 /// coherent across regular process crashes, but may be invalid after power loss
 /// or similar system failures. \c UnifiedOnDiskCache::validateIfNeeded allows
-/// validating the contents once per boot and can recover by marking invalid
-/// data for garbage collection.
+/// validating the contents once per boot, and if validation fails (or crashes,
+/// when performed in a separate process) \c UnifiedOnDiskCache::recover can
+/// recover by marking invalid data for garbage collection.
+///
+/// Validation and recovery are serialized by an exclusive lock on the
+/// "v1.validation" file, which records the boot time of the last successful
+/// validation or recovery. Before validating, the file is marked as validation
+/// pending, and the boot time is only written once validation succeeds; a
+/// validation that fails or crashes leaves it pending, so the next validation
+/// is not skipped. Recovery only happens while validation is pending, so when
+/// multiple processes attempt recovery after a failed validation only the first
+/// one recovers.
 ///
 /// The data recovery described above requires exclusive access to the CAS, and
 /// it is an error to attempt recovery if the CAS is open in any process/thread.
@@ -80,11 +90,8 @@
 #include "llvm/Support/Errc.h"
 #include "llvm/Support/Error.h"
 #include "llvm/Support/FileSystem.h"
-#include "llvm/Support/FileUtilities.h"
 #include "llvm/Support/IOSandbox.h"
-#include "llvm/Support/MemoryBuffer.h"
 #include "llvm/Support/Path.h"
-#include "llvm/Support/Program.h"
 #include "llvm/Support/raw_ostream.h"
 #include <optional>
 
@@ -207,50 +214,6 @@ static void getNextDBDirName(StringRef DBDir, llvm::raw_ostream &OS) {
   OS << DBDirPrefix << Count + 1;
 }
 
-static Error validateOutOfProcess(StringRef LLVMCasBinary, StringRef RootPath,
-                                  bool CheckHash) {
-  SmallVector<StringRef> Args{LLVMCasBinary, "-cas", RootPath, "-validate"};
-  if (CheckHash)
-    Args.push_back("-check-hash");
-
-  llvm::SmallString<128> StdErrPath;
-  int StdErrFD = -1;
-  if (std::error_code EC = sys::fs::createTemporaryFile(
-          "llvm-cas-validate-stderr", "txt", StdErrFD, StdErrPath,
-          llvm::sys::fs::OF_Text))
-    return createStringError(EC, "failed to create temporary file");
-  FileRemover OutputRemover(StdErrPath.c_str());
-
-  std::optional<llvm::StringRef> Redirects[] = {
-      {""}, // stdin = /dev/null
-      {""}, // stdout = /dev/null
-      StdErrPath.str(),
-  };
-
-  std::string ErrMsg;
-  int Result =
-      sys::ExecuteAndWait(LLVMCasBinary, Args, /*Env=*/std::nullopt, Redirects,
-                          /*SecondsToWait=*/120, /*MemoryLimit=*/0, &ErrMsg);
-
-  if (Result == -1)
-    return createStringError("failed to exec " + join(Args, " ") + ": " +
-                             ErrMsg);
-  if (Result != 0) {
-    llvm::SmallString<64> Err("cas contents invalid");
-    if (!ErrMsg.empty()) {
-      Err += ": ";
-      Err += ErrMsg;
-    }
-    auto StdErrBuf = MemoryBuffer::getFile(StdErrPath.c_str());
-    if (StdErrBuf && !(*StdErrBuf)->getBuffer().empty()) {
-      Err += ": ";
-      Err += (*StdErrBuf)->getBuffer();
-    }
-    return createStringError(Err);
-  }
-  return Error::success();
-}
-
 Error UnifiedOnDiskCache::validateActionCache() const {
   return getKeyValueDB().validate();
 }
@@ -270,27 +233,166 @@ static Error validateInProcess(StringRef RootPath, StringRef HashName,
   return Error::success();
 }
 
-Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
-    StringRef RootPath, StringRef HashName, unsigned HashByteSize,
-    bool CheckHash, OnDiskGraphDB::HashingFuncT HashFn, bool AllowRecovery,
-    bool ForceValidation, std::optional<StringRef> LLVMCasBinaryPath) {
-  if (std::error_code EC = sys::fs::create_directories(RootPath))
-    return createFileError(RootPath, EC);
+static Expected<uint64_t> getCachedBootTime() {
+  static uint64_t BootTime = 0;
+  if (BootTime == 0)
+    if (Error E = getBootTime().moveInto(BootTime))
+      return std::move(E);
+  return BootTime;
+}
+
+namespace {
+/// The validation file records the state of validation for the data:
+/// - empty: never validated.
+/// - \c ValidationPending: a validation started but did not succeed, i.e. it
+///   failed or crashed, and the data has not been recovered since.
+/// - a boot time: the data was validated or recovered during that boot.
+///
+/// While this object is alive it holds an exclusive lock on the file, which
+/// serializes validation and recovery across processes and threads.
+///
+/// Lock ordering: the validation file lock is always acquired before the
+/// top-level "lock" file, and the latter is only ever acquired exclusively via
+/// a non-blocking try-lock, so that validation and recovery cannot deadlock.
+class LockedValidationFile {
+public:
+  /// Written before validating. It is an integer so that older versions, which
+  /// only know about boot times, still parse the file and treat it as not
+  /// validated. It never matches a boot time.
+  static constexpr uint64_t ValidationPending = 0;
+
+  static Expected<std::unique_ptr<LockedValidationFile>>
+  open(StringRef RootPath) {
+    if (std::error_code EC = sys::fs::create_directories(RootPath))
+      return createFileError(RootPath, EC);
+
+    SmallString<256> PathBuf(RootPath);
+    sys::path::append(PathBuf, ValidationFilename);
+    int FD = -1;
+    if (std::error_code EC = sys::fs::openFileForReadWrite(
+            PathBuf, FD, sys::fs::CD_OpenAlways, sys::fs::OF_None))
+      return createFileError(PathBuf, EC);
+    assert(FD != -1);
+    std::unique_ptr<LockedValidationFile> VF(
+        new LockedValidationFile(PathBuf, FD));
+
+    if (std::error_code EC =
+            lockFileThreadSafe(FD, sys::fs::LockKind::Exclusive))
+      return createFileError(PathBuf, EC);
+    VF->Locked = true;
+
+    SmallString<8> Bytes;
+    if (Error E = sys::fs::readNativeFileToEOF(VF->File, Bytes))
+      return createFileError(PathBuf, std::move(E));
+    if (!Bytes.empty()) {
+      uint64_t Value;
+      if (StringRef(Bytes).trim().getAsInteger(10, Value))
+        return createFileError(PathBuf, errc::illegal_byte_sequence,
+                               "expected integer");
+      VF->State = Value;
+    }
+    return std::move(VF);
+  }
 
+  ~LockedValidationFile() {
+    if (Locked)
+      unlockFileThreadSafe(FD);
+    sys::fs::closeFile(File);
+  }
+
+  /// \returns the boot time of the last successful validation or recovery, or
+  /// 0 if there is none.
+  uint64_t getLastValidBootTime() const { return State.value_or(0); }
+
+  bool isValidationPending() const { return State == ValidationPending; }
+
+  Error setValidationPending() { return write(ValidationPending); }
+
+  Error setLastValidBootTime(uint64_t BootTime) { return write(BootTime); }
+
+private:
+  LockedValidationFile(StringRef Path, int FD)
+      : Path(Path), FD(FD), File(sys::fs::convertFDToNativeFile(FD)) {}
+
+  Error write(uint64_t Value) {
+    if (State == Value)
+      return Error::success();
+    if (std::error_code EC = sys::fs::resize_file(FD, 0))
+      return createFileError(Path, EC);
+    raw_fd_ostream OS(FD, /*shouldClose=*/false);
+    OS.seek(0); // resize does not reset position
+    OS << Value << '\n';
+    if (OS.has_error())
+      return createFileError(Path, OS.error());
+    State = Value;
+    return Error::success();
+  }
+
+  SmallString<256> Path;
+  int FD;
+  sys::fs::file_t File;
+  bool Locked = false;
+  std::optional<uint64_t> State;
+};
+} // namespace
+
+/// Marks all the database directories in \p RootPath as corrupt, which makes
+/// them eligible for garbage collection. Requires exclusive access to the CAS.
+static Error markAllDBDirsCorrupt(StringRef RootPath) {
   SmallString<256> PathBuf(RootPath);
-  sys::path::append(PathBuf, ValidationFilename);
-  int FD = -1;
+  sys::path::append(PathBuf, "lock");
+
+  int LockFD = -1;
   if (std::error_code EC = sys::fs::openFileForReadWrite(
-          PathBuf, FD, sys::fs::CD_OpenAlways, sys::fs::OF_None))
+          PathBuf, LockFD, sys::fs::CD_OpenAlways, sys::fs::OF_None))
     return createFileError(PathBuf, EC);
-  assert(FD != -1);
+  sys::fs::file_t LockFile = sys::fs::convertFDToNativeFile(LockFD);
+  llvm::scope_exit CloseLock([&]() { sys::fs::closeFile(LockFile); });
+  if (std::error_code EC = tryLockFileThreadSafe(LockFD)) {
+    if (EC == std::errc::no_lock_available)
+      return createFileError(
+          PathBuf, EC,
+          "CAS recovery requires exclusive access but CAS was in use");
+    return createFileError(PathBuf, EC);
+  }
+  llvm::scope_exit UnlockFD([&]() { unlockFileThreadSafe(LockFD); });
 
-  sys::fs::file_t File = sys::fs::convertFDToNativeFile(FD);
-  llvm::scope_exit CloseFile([&]() { sys::fs::closeFile(File); });
+  auto DBDirs = getAllDBDirs(RootPath);
+  if (!DBDirs)
+    return DBDirs.takeError();
 
-  if (std::error_code EC = lockFileThreadSafe(FD, sys::fs::LockKind::Exclusive))
-    return createFileError(PathBuf, EC);
-  llvm::scope_exit UnlockFD([&]() { unlockFileThreadSafe(FD); });
+  for (StringRef DBDir : *DBDirs) {
+    sys::path::remove_filename(PathBuf);
+    sys::path::append(PathBuf, DBDir);
+    std::error_code EC;
+    int Attempt = 0, MaxAttempts = 100;
+    SmallString<128> GCPath;
+    for (; Attempt < MaxAttempts; ++Attempt) {
+      GCPath.assign(RootPath);
+      sys::path::append(GCPath,
+                        CorruptPrefix + std::to_string(Attempt) + "." + DBDir);
+      EC = sys::fs::rename(PathBuf, GCPath);
+      // Darwin uses ENOTEMPTY. Linux may return either ENOTEMPTY or EEXIST.
+      if (EC != errc::directory_not_empty && EC != errc::file_exists)
+        break;
+    }
+    if (Attempt == MaxAttempts)
+      return createStringError(
+          EC, "rename " + PathBuf +
+                  " failed: too many CAS directories awaiting pruning");
+    if (EC)
+      return createStringError(EC, "rename " + PathBuf + " to " + GCPath +
+                                       " failed: " + EC.message());
+  }
+  return Error::success();
+}
+
+Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
+    StringRef RootPath, StringRef HashName, unsigned HashByteSize,
+    bool CheckHash, OnDiskGraphDB::HashingFuncT HashFn, bool ForceValidation) {
+  std::unique_ptr<LockedValidationFile> VF;
+  if (Error E = LockedValidationFile::open(RootPath).moveInto(VF))
+    return std::move(E);
 
   std::shared_ptr<ondisk::OnDiskCASLogger> Logger;
 #ifndef _WIN32
@@ -299,22 +401,11 @@ Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
     return std::move(E);
 #endif
 
-  SmallString<8> Bytes;
-  if (Error E = sys::fs::readNativeFileToEOF(File, Bytes))
-    return createFileError(PathBuf, std::move(E));
-
-  uint64_t ValidationBootTime = 0;
-  if (!Bytes.empty() &&
-      StringRef(Bytes).trim().getAsInteger(10, ValidationBootTime))
-    return createFileError(PathBuf, errc::illegal_byte_sequence,
-                           "expected integer");
-
-  static uint64_t BootTime = 0;
-  if (BootTime == 0)
-    if (Error E = getBootTime().moveInto(BootTime))
-      return std::move(E);
+  uint64_t BootTime = 0;
+  if (Error E = getCachedBootTime().moveInto(BootTime))
+    return std::move(E);
+  uint64_t ValidationBootTime = VF->getLastValidBootTime();
 
-  bool Recovered = false;
   bool Skipped = false;
   std::string LogValidationError;
 
@@ -322,9 +413,8 @@ Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
     if (!Logger)
       return;
     Logger->logUnifiedOnDiskCacheValidateIfNeeded(
-        RootPath, BootTime, ValidationBootTime, CheckHash, AllowRecovery,
-        ForceValidation, LLVMCasBinaryPath, LogValidationError, Skipped,
-        Recovered);
+        RootPath, BootTime, ValidationBootTime, CheckHash, ForceValidation,
+        LogValidationError, Skipped);
   });
 
   if (ValidationBootTime == BootTime && !ForceValidation) {
@@ -332,86 +422,66 @@ Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
     return ValidationResult::Skipped;
   }
 
-  // Validate!
-  bool NeedsRecovery = false;
-  Error E = LLVMCasBinaryPath
-                ? validateOutOfProcess(*LLVMCasBinaryPath, RootPath, CheckHash)
-                : validateInProcess(RootPath, HashName, HashByteSize, CheckHash,
-                                    HashFn);
-  if (E) {
+  // Mark validation as pending until it succeeds, so that a failed or crashed
+  // validation is detected by recovery and by the next validation.
+  if (Error E = VF->setValidationPending())
+    return std::move(E);
+
+  if (Error E = validateInProcess(RootPath, HashName, HashByteSize, CheckHash,
+                                  HashFn)) {
     if (Logger)
       LogValidationError = toStringWithoutConsuming(E);
-    if (AllowRecovery) {
-      consumeError(std::move(E));
-      NeedsRecovery = true;
-    } else {
-      return std::move(E);
-    }
+    return std::move(E);
   }
 
-  if (NeedsRecovery) {
-    sys::path::remove_filename(PathBuf);
-    sys::path::append(PathBuf, "lock");
+  if (Error E = VF->setLastValidBootTime(BootTime))
+    return std::move(E);
+  return ValidationResult::Valid;
+}
 
-    int LockFD = -1;
-    if (std::error_code EC = sys::fs::openFileForReadWrite(
-            PathBuf, LockFD, sys::fs::CD_OpenAlways, sys::fs::OF_None))
-      return createFileError(PathBuf, EC);
-    sys::fs::file_t LockFile = sys::fs::convertFDToNativeFile(LockFD);
-    llvm::scope_exit CloseLock([&]() { sys::fs::closeFile(LockFile); });
-    if (std::error_code EC = tryLockFileThreadSafe(LockFD)) {
-      if (EC == std::errc::no_lock_available)
-        return createFileError(
-            PathBuf, EC,
-            "CAS validation requires exclusive access but CAS was in use");
-      return createFileError(PathBuf, EC);
-    }
-    llvm::scope_exit UnlockFD([&]() { unlockFileThreadSafe(LockFD); });
-
-    auto DBDirs = getAllDBDirs(RootPath);
-    if (!DBDirs)
-      return DBDirs.takeError();
-
-    for (StringRef DBDir : *DBDirs) {
-      sys::path::remove_filename(PathBuf);
-      sys::path::append(PathBuf, DBDir);
-      std::error_code EC;
-      int Attempt = 0, MaxAttempts = 100;
-      SmallString<128> GCPath;
-      for (; Attempt < MaxAttempts; ++Attempt) {
-        GCPath.assign(RootPath);
-        sys::path::append(GCPath, CorruptPrefix + std::to_string(Attempt) +
-                                      "." + DBDir);
-        EC = sys::fs::rename(PathBuf, GCPath);
-        // Darwin uses ENOTEMPTY. Linux may return either ENOTEMPTY or EEXIST.
-        if (EC != errc::directory_not_empty && EC != errc::file_exists)
-          break;
-      }
-      if (Attempt == MaxAttempts)
-        return createStringError(
-            EC, "rename " + PathBuf +
-                    " failed: too many CAS directories awaiting pruning");
-      if (EC)
-        return createStringError(EC, "rename " + PathBuf + " to " + GCPath +
-                                         " failed: " + EC.message());
-    }
-    Recovered = true;
+Expected<ValidationResult> UnifiedOnDiskCache::recover(StringRef RootPath) {
+  std::unique_ptr<LockedValidationFile> VF;
+  if (Error E = LockedValidationFile::open(RootPath).moveInto(VF))
+    return std::move(E);
+
+  std::shared_ptr<ondisk::OnDiskCASLogger> Logger;
+#ifndef _WIN32
+  if (Error E =
+          ondisk::OnDiskCASLogger::openIfEnabled(RootPath).moveInto(Logger))
+    return std::move(E);
+#endif
+
+  uint64_t BootTime = 0;
+  if (Error E = getCachedBootTime().moveInto(BootTime))
+    return std::move(E);
+
+  bool Skipped = false;
+  std::string LogRecoveryError;
+
+  llvm::scope_exit Log([&] {
+    if (!Logger)
+      return;
+    Logger->logUnifiedOnDiskCacheRecover(RootPath, BootTime, LogRecoveryError,
+                                         Skipped);
+  });
+
+  // Unless validation is still pending, the data has been recovered or
+  // successfully validated since the failed validation, e.g. by a concurrent
+  // process.
+  if (!VF->isValidationPending()) {
+    Skipped = true;
+    return ValidationResult::Skipped;
   }
 
-  if (ValidationBootTime != BootTime) {
-    // Fix filename in case we have error to report.
-    sys::path::remove_filename(PathBuf);
-    sys::path::append(PathBuf, ValidationFilename);
-    if (std::error_code EC = sys::fs::resize_file(FD, 0))
-      return createFileError(PathBuf, EC);
-    raw_fd_ostream OS(FD, /*shouldClose=*/false);
-    OS.seek(0); // resize does not reset position
-    OS << BootTime << '\n';
-    if (OS.has_error())
-      return createFileError(PathBuf, OS.error());
+  if (Error E = markAllDBDirsCorrupt(RootPath)) {
+    if (Logger)
+      LogRecoveryError = toStringWithoutConsuming(E);
+    return std::move(E);
   }
 
-  return NeedsRecovery ? ValidationResult::Recovered : ValidationResult::Valid;
+  if (Error E = VF->setLastValidBootTime(BootTime))
+    return std::move(E);
+  return ValidationResult::Recovered;
 }
 
 Expected<std::unique_ptr<UnifiedOnDiskCache>>
diff --git a/llvm/test/CMakeLists.txt b/llvm/test/CMakeLists.txt
index 18f176c3fe17f..56f96b4f3de92 100644
--- a/llvm/test/CMakeLists.txt
+++ b/llvm/test/CMakeLists.txt
@@ -46,6 +46,14 @@ llvm_canonicalize_cmake_booleans(
   LLVM_HAVE_OPENCSD
   )
 
+# libCASPluginTest is placed next to the CASTests executable, see
+# unittests/CAS/CMakeLists.txt.
+if (TARGET CASPluginTest)
+  set_llvm_build_mode()
+  set(LLVM_CAS_PLUGIN_TEST_PATH
+    "${LLVM_BINARY_DIR}/unittests/CAS/${LLVM_BUILD_MODE}/${CMAKE_SHARED_LIBRARY_PREFIX}CASPluginTest${LLVM_PLUGIN_EXT}")
+endif()
+
 configure_lit_site_cfg(
   ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in
   ${CMAKE_CURRENT_BINARY_DIR}/lit.site.cfg.py
@@ -207,6 +215,10 @@ if (TARGET CGTestPlugin)
   list(APPEND LLVM_TEST_DEPENDS CGTestPlugin)
 endif()
 
+if (TARGET CASPluginTest)
+  list(APPEND LLVM_TEST_DEPENDS CASPluginTest)
+endif()
+
 if(LLVM_INCLUDE_EXAMPLES)
   list(APPEND LLVM_TEST_DEPENDS
     Kaleidoscope-Ch3
diff --git a/llvm/test/lit.site.cfg.py.in b/llvm/test/lit.site.cfg.py.in
index 60b22ef5a7a85..f4821eea6d95b 100644
--- a/llvm/test/lit.site.cfg.py.in
+++ b/llvm/test/lit.site.cfg.py.in
@@ -75,6 +75,7 @@ config.have_vc_rev = @LLVM_APPEND_VC_REV@
 config.force_vc_rev = "@LLVM_FORCE_VC_REVISION@"
 config.has_logf128 = @LLVM_HAS_LOGF128@
 config.have_ondisk_cas = @LLVM_ENABLE_ONDISK_CAS@
+config.llvm_cas_plugin_test_path = lit_config.substitute(r"@LLVM_CAS_PLUGIN_TEST_PATH@")
 config.have_opencsd = @LLVM_HAVE_OPENCSD@
 
 import lit.llvm
diff --git a/llvm/test/tools/llvm-cas/lit.local.cfg b/llvm/test/tools/llvm-cas/lit.local.cfg
index 379945b68925d..0f6e925d6b4c7 100644
--- a/llvm/test/tools/llvm-cas/lit.local.cfg
+++ b/llvm/test/tools/llvm-cas/lit.local.cfg
@@ -1,2 +1,6 @@
 if not config.have_ondisk_cas:
     config.unsupported = True
+
+if config.llvm_cas_plugin_test_path:
+    config.available_features.add("cas-plugin")
+    config.substitutions.append(("%cas_plugin", config.llvm_cas_plugin_test_path))
diff --git a/llvm/test/tools/llvm-cas/logging.test b/llvm/test/tools/llvm-cas/logging.test
index 5dc0955eabcb8..df7fc43048ae2 100644
--- a/llvm/test/tools/llvm-cas/logging.test
+++ b/llvm/test/tools/llvm-cas/logging.test
@@ -7,6 +7,8 @@ RUN: env LLVM_CAS_LOG=2 llvm-cas --cas %t/cas --make-blob --data %t/input/a
 RUN: env LLVM_CAS_LOG=2 llvm-cas --cas %t/cas --make-blob --data %t/input/large
 RUN: env LLVM_CAS_LOG=2 llvm-cas --cas %t/cas --validate-if-needed -check-hash
 RUN: env LLVM_CAS_LOG=2 llvm-cas --cas %t/cas --validate-if-needed -force -allow-recovery
+RUN: rm %t/cas/v1.1/data.v1
+RUN: env LLVM_CAS_LOG=2 llvm-cas --cas %t/cas --validate-if-needed -force -allow-recovery
 RUN: FileCheck %s --input-file %t/cas/v1.log
 RUN: FileCheck %s --input-file %t/cas/v1.log --check-prefix=STANDALONE
 
@@ -30,8 +32,10 @@ RUN: FileCheck %s --input-file %t/cas/v1.log --check-prefix=STANDALONE
 // CHECK: resize mapped file '{{.*}}index.v{{[0-9]+}}'
 // CHECK: close mmap '{{.*}}index.v{{[0-9]+}}'
 
-// CHECK: validate-if-needed '{{.*}}cas' boot=[[BOOT:[0-9]+]] last-valid=0 check-hash=1 allow-recovery=0 force=0 llvm-cas={{.*}}llvm-cas
-// CHECK: validate-if-needed '{{.*}}cas' boot=[[BOOT]] last-valid=[[BOOT]] check-hash=0 allow-recovery=1 force=1 llvm-cas={{.*}}llvm-cas
+// CHECK: validate-if-needed '{{.*}}cas' boot=[[BOOT:[0-9]+]] last-valid=0 check-hash=1 force=0{{$}}
+// CHECK: validate-if-needed '{{.*}}cas' boot=[[BOOT]] last-valid=[[BOOT]] check-hash=0 force=1{{$}}
+// CHECK: validate-if-needed '{{.*}}cas' boot=[[BOOT]] last-valid=[[BOOT]] check-hash=0 force=1 data was invalid
+// CHECK: recover '{{.*}}cas' boot=[[BOOT]]{{$}}
 
 // STANDALONE: standalone file create '[[PATH:.*leaf.[0-9a-f]*.v[0-9]+]].[[SUFFIX:[0-9a-f]*]]'
 // STANDALONE: standalone file rename '[[PATH]].[[SUFFIX]]' to '[[PATH]]'
diff --git a/llvm/test/tools/llvm-cas/plugin-validation-crash.test b/llvm/test/tools/llvm-cas/plugin-validation-crash.test
new file mode 100644
index 0000000000000..7f55f98a14e70
--- /dev/null
+++ b/llvm/test/tools/llvm-cas/plugin-validation-crash.test
@@ -0,0 +1,59 @@
+REQUIRES: cas-plugin
+# HWASan does not tag the globals of a dlopen'ed library with glibc, see
+# https://github.com/llvm/llvm-project/issues/57206.
+UNSUPPORTED: hwasan
+
+# Tests recovery from a validation that crashes, using the test plugin's
+# crash-on-validate option which crashes while checking hashes.
+
+RUN: rm -rf %t
+RUN: mkdir %t
+
+RUN: echo "abc" | llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --make-blob --data - > %t/abc.casid
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --put-cache-key @%t/abc.casid @%t/abc.casid
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed | FileCheck %s --check-prefix=VALID
+
+# Without --allow-recovery a crash in the validation process is an error.
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --fcas-plugin-option crash-on-validate \
+RUN:   --validate-if-needed --force --check-hash 2>&1 \
+RUN:   | FileCheck %s --check-prefix=INVALID
+RUN: FileCheck %s --check-prefix=PENDING --input-file %t/cas/v1.validation
+
+# The crashed validation is not skipped even though the data was validated
+# earlier during this boot.
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed | FileCheck %s --check-prefix=VALID
+RUN: FileCheck %s --check-prefix=VALIDATED --input-file %t/cas/v1.validation
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed | FileCheck %s --check-prefix=SKIPPED
+
+# An in-process validation that crashes also leaves validation pending.
+RUN: not --crash llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --fcas-plugin-option crash-on-validate \
+RUN:   --validate-if-needed --in-process --force --check-hash
+RUN: FileCheck %s --check-prefix=PENDING --input-file %t/cas/v1.validation
+
+# With --allow-recovery, llvm-cas recovers after the validation process crashes.
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --fcas-plugin-option crash-on-validate \
+RUN:   --validate-if-needed --force --check-hash --allow-recovery \
+RUN:   | FileCheck %s --check-prefix=RECOVERED
+RUN: ls %t/cas | FileCheck %s --check-prefix=RECOVERED-DIRS
+RUN: FileCheck %s --check-prefix=VALIDATED --input-file %t/cas/v1.validation
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed | FileCheck %s --check-prefix=SKIPPED
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --get-cache-result @%t/abc.casid | FileCheck %s --check-prefix=NOT-FOUND
+
+VALID: validated successfully
+SKIPPED: validation skipped
+INVALID: llvm-cas: validate-if-needed: cas contents invalid
+PENDING: {{^0$}}
+VALIDATED: {{^[1-9][0-9]*$}}
+RECOVERED: recovered from invalid data
+RECOVERED-DIRS: corrupt.0.v1.1
+NOT-FOUND: result not found
diff --git a/llvm/test/tools/llvm-cas/plugin-validation.test b/llvm/test/tools/llvm-cas/plugin-validation.test
new file mode 100644
index 0000000000000..6c7ef67a0024e
--- /dev/null
+++ b/llvm/test/tools/llvm-cas/plugin-validation.test
@@ -0,0 +1,73 @@
+REQUIRES: cas-plugin
+# HWASan does not tag the globals of a dlopen'ed library with glibc, see
+# https://github.com/llvm/llvm-project/issues/57206.
+UNSUPPORTED: hwasan
+
+RUN: rm -rf %t
+RUN: mkdir %t
+
+RUN: echo "abc" | llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --make-blob --data - > %t/abc.casid
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --make-node --data /dev/null @%t/abc.casid > %t/node.casid
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --put-cache-key @%t/abc.casid @%t/node.casid
+
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin --validate \
+RUN:   | FileCheck %s --check-prefix=VALID
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin --validate \
+RUN:   --check-hash | FileCheck %s --check-prefix=VALID
+
+# Validate once per boot unless forced. The output of the validation process
+# is forwarded.
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed --check-hash | FileCheck %s --check-prefix=VALID
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed --check-hash | FileCheck %s --check-prefix=SKIPPED
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed --force | FileCheck %s --check-prefix=VALID
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed --in-process | FileCheck %s --check-prefix=SKIPPED
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed --in-process --force \
+RUN:   | FileCheck %s --check-prefix=VALID
+
+# Nothing to recover from.
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed --force --allow-recovery \
+RUN:   | FileCheck %s --check-prefix=VALID
+
+# Plugin options are passed through.
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --fcas-plugin-option bogus=1 --validate-if-needed 2>&1 \
+RUN:   | FileCheck %s --check-prefix=BAD-OPTION
+
+RUN: rm %t/cas/v1.1/data.v1
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin --validate
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed --force 2>&1 | FileCheck %s --check-prefix=INVALID
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed --in-process --force 2>&1 \
+RUN:   | FileCheck %s --check-prefix=INVALID
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed --force --allow-recovery \
+RUN:   | FileCheck %s --check-prefix=RECOVERED
+RUN: ls %t/cas | FileCheck %s --check-prefix=CORRUPT-DIR
+
+# Recovery also counts as validation for this boot.
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --validate-if-needed | FileCheck %s --check-prefix=SKIPPED
+
+# The invalid data was discarded.
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin --validate \
+RUN:   | FileCheck %s --check-prefix=VALID
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN:   --get-cache-result @%t/abc.casid | FileCheck %s --check-prefix=NOT-FOUND
+
+VALID: validated successfully
+SKIPPED: validation skipped
+BAD-OPTION: llvm-cas: validate-if-needed: unknown option: bogus
+INVALID: llvm-cas: validate-if-needed:
+RECOVERED: recovered from invalid data
+CORRUPT-DIR: corrupt.0.v1.1
+NOT-FOUND: result not found
diff --git a/llvm/tools/libCASPluginTest/libCASPluginTest.cpp b/llvm/tools/libCASPluginTest/libCASPluginTest.cpp
index 282760073962c..ec71dc3a57c6c 100644
--- a/llvm/tools/libCASPluginTest/libCASPluginTest.cpp
+++ b/llvm/tools/libCASPluginTest/libCASPluginTest.cpp
@@ -100,6 +100,9 @@ struct CASPluginOptions {
   std::string SecondPrefix;
   bool SimulateMissingObjects = false;
   bool Logging = true;
+  /// If true, \c llcas_cas_validate_if_needed crashes while checking hashes,
+  /// for testing recovery from a crashed validation.
+  bool CrashOnValidate = false;
 
   Error setOption(StringRef Name, StringRef Value);
 };
@@ -119,6 +122,8 @@ Error CASPluginOptions::setOption(StringRef Name, StringRef Value) {
     SimulateMissingObjects = true;
   else if (Name == "no-logging")
     Logging = false;
+  else if (Name == "crash-on-validate")
+    CrashOnValidate = true;
   else
     return createStringError(errc::invalid_argument,
                              Twine("unknown option: ") + Name);
@@ -449,6 +454,60 @@ bool llcas_cas_prune_ondisk_data(llcas_cas_t c_cas, char **error) {
   return false;
 }
 
+static void hashObject(ArrayRef<ArrayRef<uint8_t>> Refs, ArrayRef<char> Data,
+                       SmallVectorImpl<uint8_t> &Result) {
+  HashType Digest = BuiltinObjectHasher<HasherT>::hashObject(Refs, Data);
+  Result.assign(Digest.begin(), Digest.end());
+}
+
+bool llcas_cas_validate(llcas_cas_t c_cas, bool check_hash, char **error) {
+  if (Error E =
+          unwrap(c_cas)->DB->getGraphDB().validate(check_hash, hashObject))
+    return reportError(std::move(E), error, true);
+  return false;
+}
+
+static llcas_validation_result_t
+toValidationResult(Expected<ValidationResult> Result, char **error) {
+  if (!Result)
+    return reportError(Result.takeError(), error,
+                       LLCAS_VALIDATION_RESULT_ERROR);
+  switch (*Result) {
+  case ValidationResult::Valid:
+    return LLCAS_VALIDATION_RESULT_VALID;
+  case ValidationResult::Recovered:
+    return LLCAS_VALIDATION_RESULT_RECOVERED;
+  case ValidationResult::Skipped:
+    return LLCAS_VALIDATION_RESULT_SKIPPED;
+  }
+  llvm_unreachable("unknown ValidationResult value");
+}
+
+llcas_validation_result_t
+llcas_cas_validate_if_needed(llcas_cas_options_t c_opts, bool check_hash,
+                             bool force, char **error) {
+  auto &Opts = *unwrap(c_opts);
+  setSmallMaxMappingSize();
+  auto HashFn = [&](ArrayRef<ArrayRef<uint8_t>> Refs, ArrayRef<char> Data,
+                    SmallVectorImpl<uint8_t> &Result) {
+    if (Opts.CrashOnValidate)
+      abort();
+    hashObject(Refs, Data, Result);
+  };
+  return toValidationResult(UnifiedOnDiskCache::validateIfNeeded(
+                                Opts.OnDiskPath,
+                                PluginCASContext::getHashName(),
+                                sizeof(HashType), check_hash, HashFn, force),
+                            error);
+}
+
+llcas_validation_result_t
+llcas_cas_recover_ondisk_data(llcas_cas_options_t c_opts, char **error) {
+  auto &Opts = *unwrap(c_opts);
+  return toValidationResult(UnifiedOnDiskCache::recover(Opts.OnDiskPath),
+                            error);
+}
+
 void llcas_cas_options_set_client_version(llcas_cas_options_t, unsigned major,
                                           unsigned minor) {
   // Ignore for now.
@@ -820,3 +879,9 @@ void llcas_actioncache_put_for_digest_async(
     cb(ctx_cb, failed, c_err);
   });
 }
+
+bool llcas_actioncache_validate(llcas_cas_t c_cas, char **error) {
+  if (Error E = unwrap(c_cas)->DB->validateActionCache())
+    return reportError(std::move(E), error, true);
+  return false;
+}
diff --git a/llvm/tools/libCASPluginTest/libCASPluginTest.exports b/llvm/tools/libCASPluginTest/libCASPluginTest.exports
index 1c292e4da1ccf..6ecfae9093aa7 100644
--- a/llvm/tools/libCASPluginTest/libCASPluginTest.exports
+++ b/llvm/tools/libCASPluginTest/libCASPluginTest.exports
@@ -2,6 +2,7 @@ llcas_actioncache_get_for_digest
 llcas_actioncache_get_for_digest_async
 llcas_actioncache_put_for_digest
 llcas_actioncache_put_for_digest_async
+llcas_actioncache_validate
 llcas_cancellable_cancel
 llcas_cancellable_dispose
 llcas_cas_contains_object
@@ -18,8 +19,11 @@ llcas_cas_options_set_client_version
 llcas_cas_options_set_ondisk_path
 llcas_cas_options_set_option
 llcas_cas_prune_ondisk_data
+llcas_cas_recover_ondisk_data
 llcas_cas_set_ondisk_size_limit
 llcas_cas_store_object
+llcas_cas_validate
+llcas_cas_validate_if_needed
 llcas_digest_parse
 llcas_digest_print
 llcas_get_plugin_version
diff --git a/llvm/tools/llvm-cas/Options.td b/llvm/tools/llvm-cas/Options.td
index 5ae64c104fdb6..9e8eb0a242f10 100644
--- a/llvm/tools/llvm-cas/Options.td
+++ b/llvm/tools/llvm-cas/Options.td
@@ -44,6 +44,14 @@ def cas_path : Separate<["-", "--"], "cas">,
                MetaVarName<"<path>">,
                HelpText<"Path to CAS on disk">;
 
+def cas_plugin_path : Separate<["-", "--"], "fcas-plugin-path">,
+                      MetaVarName<"<path>">,
+                      HelpText<"Path to plugin CAS library">;
+
+def cas_plugin_option : Separate<["-", "--"], "fcas-plugin-option">,
+                        MetaVarName<"<name>=<value>">,
+                        HelpText<"Option passed to the plugin CAS">;
+
 def upstream_cas : Separate<["-", "--"], "upstream-cas">,
                    MetaVarName<"<path>">,
                    HelpText<"Path to another upstream CAS">;
diff --git a/llvm/tools/llvm-cas/llvm-cas.cpp b/llvm/tools/llvm-cas/llvm-cas.cpp
index d4d79336b5d75..f996c3d3a79f3 100644
--- a/llvm/tools/llvm-cas/llvm-cas.cpp
+++ b/llvm/tools/llvm-cas/llvm-cas.cpp
@@ -10,6 +10,7 @@
 ///
 //===----------------------------------------------------------------------===//
 
+#include "llvm/ADT/StringExtras.h"
 #include "llvm/CAS/ActionCache.h"
 #include "llvm/CAS/BuiltinUnifiedCASDatabases.h"
 #include "llvm/CAS/ObjectStore.h"
@@ -20,6 +21,7 @@
 #include "llvm/Support/Error.h"
 #include "llvm/Support/InitLLVM.h"
 #include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/Program.h"
 #include "llvm/Support/raw_ostream.h"
 
 using namespace llvm;
@@ -62,6 +64,8 @@ struct CommandOptions {
   CommandKind Command = CommandKind::Invalid;
   std::vector<std::string> Inputs;
   std::string CASPath;
+  std::string CASPluginPath;
+  SmallVector<std::pair<std::string, std::string>> CASPluginOpts;
   std::string UpstreamCASPath;
   std::string DataPath;
   bool CheckHash;
@@ -122,9 +126,7 @@ static int putCacheKey(ObjectStore &CAS, ActionCache &AC,
 static int getCacheResult(ObjectStore &CAS, ActionCache &AC, const CASID &ID);
 static int validateObject(ObjectStore &CAS, const CASID &ID);
 static int validate(ObjectStore &CAS, ActionCache &AC, bool CheckHash);
-static int validateIfNeeded(StringRef Path, bool CheckHash, bool Force,
-                            bool AllowRecovery, bool InProcess,
-                            const char *Argv0);
+static int validateIfNeeded(const CommandOptions &Opts, const char *Argv0);
 static int prune(cas::ObjectStore &CAS);
 
 static Expected<CommandOptions> parseOptions(int Argc, char **Argv) {
@@ -160,6 +162,11 @@ static Expected<CommandOptions> parseOptions(int Argc, char **Argv) {
   for (auto *File : Args.filtered(OPT_INPUT))
     Opts.Inputs.push_back(File->getValue());
   Opts.CASPath = Args.getLastArgValue(OPT_cas_path);
+  Opts.CASPluginPath = Args.getLastArgValue(OPT_cas_plugin_path);
+  for (StringRef PluginOpt : Args.getAllArgValues(OPT_cas_plugin_option)) {
+    auto [Name, Value] = PluginOpt.split('=');
+    Opts.CASPluginOpts.emplace_back(Name, Value);
+  }
   Opts.UpstreamCASPath = Args.getLastArgValue(OPT_upstream_cas);
   Opts.DataPath = Args.getLastArgValue(OPT_data);
   Opts.CheckHash = Args.hasArg(OPT_check_hash);
@@ -184,10 +191,16 @@ int main(int Argc, char **Argv) {
   auto Opts = ExitOnErr(parseOptions(Argc, Argv));
 
   if (Opts.Command == CommandKind::ValidateIfNeeded)
-    return validateIfNeeded(Opts.CASPath, Opts.CheckHash, Opts.Force,
-                            Opts.AllowRecovery, Opts.InProcess, Argv[0]);
-
-  auto [CAS, AC] = ExitOnErr(createOnDiskUnifiedCASDatabases(Opts.CASPath));
+    return validateIfNeeded(Opts, Argv[0]);
+
+  std::shared_ptr<ObjectStore> CAS;
+  std::shared_ptr<ActionCache> AC;
+  if (!Opts.CASPluginPath.empty())
+    std::tie(CAS, AC) = ExitOnErr(createPluginCASDatabases(
+        Opts.CASPluginPath, Opts.CASPath, Opts.CASPluginOpts));
+  else
+    std::tie(CAS, AC) =
+        ExitOnErr(createOnDiskUnifiedCASDatabases(Opts.CASPath));
   assert(CAS);
 
   if (Opts.Command == CommandKind::Dump)
@@ -361,28 +374,86 @@ int validate(ObjectStore &CAS, ActionCache &AC, bool CheckHash) {
   return 0;
 }
 
-int validateIfNeeded(StringRef Path, bool CheckHash, bool Force,
-                     bool AllowRecovery, bool InProcess, const char *Argv0) {
-  ExitOnError ExitOnErr("llvm-cas: validate-if-needed: ");
-  std::string ExecStorage;
-  std::optional<StringRef> Exec;
-  if (!InProcess) {
-    ExecStorage = sys::fs::getMainExecutable(Argv0, (void *)validateIfNeeded);
-    Exec = ExecStorage;
+/// Validates the CAS in this process and prints the result.
+static Error validateInProcess(const CommandOptions &Opts) {
+  ValidationResult Result;
+  if (Error E = (Opts.CASPluginPath.empty()
+                     ? validateOnDiskUnifiedCASDatabasesIfNeeded(
+                           Opts.CASPath, Opts.CheckHash, Opts.Force)
+                     : validatePluginCASDatabasesIfNeeded(
+                           Opts.CASPluginPath, Opts.CASPath, Opts.CASPluginOpts,
+                           Opts.CheckHash, Opts.Force))
+                    .moveInto(Result))
+    return E;
+  outs() << (Result == ValidationResult::Skipped ? "validation skipped\n"
+                                                 : "validated successfully\n");
+  return Error::success();
+}
+
+/// Validates the CAS by re-executing llvm-cas with --in-process, which
+/// protects against crashes during validation. The output of the child process
+/// is forwarded.
+///
+/// \returns false if validation failed or crashed.
+static Expected<bool> validateOutOfProcess(const CommandOptions &Opts,
+                                           const char *Argv0) {
+  std::string Exec =
+      sys::fs::getMainExecutable(Argv0, (void *)validateOutOfProcess);
+  SmallVector<std::string> PluginOpts;
+  for (const auto &[Name, Value] : Opts.CASPluginOpts)
+    PluginOpts.push_back(Name + "=" + Value);
+
+  SmallVector<StringRef> Args{Exec, "--cas", Opts.CASPath};
+  if (!Opts.CASPluginPath.empty()) {
+    Args.append({"--fcas-plugin-path", Opts.CASPluginPath});
+    for (StringRef PluginOpt : PluginOpts)
+      Args.append({"--fcas-plugin-option", PluginOpt});
   }
-  ValidationResult Result = ExitOnErr(validateOnDiskUnifiedCASDatabasesIfNeeded(
-      Path, CheckHash, AllowRecovery, Force, Exec));
-  switch (Result) {
-  case ValidationResult::Valid:
-    outs() << "validated successfully\n";
-    break;
-  case ValidationResult::Recovered:
-    outs() << "recovered from invalid data\n";
-    break;
-  case ValidationResult::Skipped:
-    outs() << "validation skipped\n";
-    break;
+  Args.append({"--validate-if-needed", "--in-process"});
+  if (Opts.CheckHash)
+    Args.push_back("--check-hash");
+  if (Opts.Force)
+    Args.push_back("--force");
+
+  outs().flush();
+  std::string ErrMsg;
+  int Result = sys::ExecuteAndWait(Exec, Args, /*Env=*/std::nullopt,
+                                   /*Redirects=*/{}, /*SecondsToWait=*/120,
+                                   /*MemoryLimit=*/0, &ErrMsg);
+  if (Result == -1)
+    return createStringError("failed to exec " + join(Args, " ") + ": " +
+                             ErrMsg);
+  if (Result == -2)
+    errs() << "llvm-cas: validate-if-needed: validation crashed: " << ErrMsg
+           << "\n";
+  return Result == 0;
+}
+
+int validateIfNeeded(const CommandOptions &Opts, const char *Argv0) {
+  ExitOnError ExitOnErr("llvm-cas: validate-if-needed: ");
+  if (Opts.InProcess) {
+    Error E = validateInProcess(Opts);
+    if (!E)
+      return 0;
+    if (!Opts.AllowRecovery)
+      ExitOnErr(std::move(E));
+    errs() << "llvm-cas: validate-if-needed: " << toString(std::move(E))
+           << "\n";
+  } else {
+    if (ExitOnErr(validateOutOfProcess(Opts, Argv0)))
+      return 0;
+    if (!Opts.AllowRecovery)
+      ExitOnErr(createStringError("cas contents invalid"));
   }
+
+  ValidationResult Result = ExitOnErr(
+      Opts.CASPluginPath.empty()
+          ? recoverOnDiskUnifiedCASDatabases(Opts.CASPath)
+          : recoverPluginCASDatabases(Opts.CASPluginPath, Opts.CASPath,
+                                      Opts.CASPluginOpts));
+  outs() << (Result == ValidationResult::Skipped
+                 ? "recovery skipped\n"
+                 : "recovered from invalid data\n");
   return 0;
 }
 
diff --git a/llvm/unittests/CAS/PluginCASTest.cpp b/llvm/unittests/CAS/PluginCASTest.cpp
index 376779a387a01..d417d6c6b7bf5 100644
--- a/llvm/unittests/CAS/PluginCASTest.cpp
+++ b/llvm/unittests/CAS/PluginCASTest.cpp
@@ -131,4 +131,95 @@ TEST(PluginCASTest, isMaterialized) {
   }
 }
 
+TEST(PluginCASTest, validate) {
+  unittest::TempDir Temp("plugin-cas", /*Unique=*/true);
+
+  auto validateIfNeeded = [&](bool Force) {
+    return validatePluginCASDatabasesIfNeeded(getCASPluginPath(), Temp.path(),
+                                              /*PluginArgs=*/{},
+                                              /*CheckHash=*/true, Force);
+  };
+  auto recover = [&]() {
+    return recoverPluginCASDatabases(getCASPluginPath(), Temp.path(),
+                                     /*PluginArgs=*/{});
+  };
+  auto openCAS = [&]() {
+    std::optional<
+        std::pair<std::shared_ptr<ObjectStore>, std::shared_ptr<ActionCache>>>
+        DBs;
+    EXPECT_THAT_ERROR(createPluginCASDatabases(getCASPluginPath(), Temp.path(),
+                                               /*PluginArgs=*/{})
+                          .moveInto(DBs),
+                      Succeeded());
+    return DBs;
+  };
+
+  std::optional<ValidationResult> Result;
+  ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/false).moveInto(Result),
+                    Succeeded());
+  EXPECT_EQ(Result, ValidationResult::Valid);
+
+  {
+    auto DBs = openCAS();
+    ASSERT_TRUE(DBs);
+    auto &[CAS, AC] = *DBs;
+
+    std::optional<CASID> ID1, ID2;
+    ASSERT_THAT_ERROR(CAS->createProxy({}, "1").moveInto(ID1), Succeeded());
+    ASSERT_THAT_ERROR(CAS->createProxy({}, "2").moveInto(ID2), Succeeded());
+    ASSERT_THAT_ERROR(AC->put(*ID1, *ID2), Succeeded());
+
+    EXPECT_THAT_ERROR(CAS->validate(/*CheckHash=*/true), Succeeded());
+    EXPECT_THAT_ERROR(AC->validate(), Succeeded());
+  }
+
+  // Already validated since boot.
+  ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/false).moveInto(Result),
+                    Succeeded());
+  EXPECT_EQ(Result, ValidationResult::Skipped);
+
+  ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/true).moveInto(Result),
+                    Succeeded());
+  EXPECT_EQ(Result, ValidationResult::Valid);
+
+  // Nothing to recover from after a successful validation.
+  ASSERT_THAT_ERROR(recover().moveInto(Result), Succeeded());
+  EXPECT_EQ(Result, ValidationResult::Skipped);
+  EXPECT_TRUE(sys::fs::exists(Temp.path("v1.1")));
+
+  // Invalidate the data.
+  ASSERT_FALSE(sys::fs::remove(Temp.path("v1.1/data.v1")));
+  EXPECT_THAT_EXPECTED(validateIfNeeded(/*Force=*/true), Failed());
+
+  // Recovery requires exclusive access, and fails rather than waiting for it.
+  {
+    auto DBs = openCAS();
+    ASSERT_TRUE(DBs);
+    EXPECT_THAT_EXPECTED(recover(), Failed());
+  }
+
+  ASSERT_THAT_ERROR(recover().moveInto(Result), Succeeded());
+  EXPECT_EQ(Result, ValidationResult::Recovered);
+  EXPECT_FALSE(sys::fs::exists(Temp.path("v1.1")));
+
+  // A concurrent recovery for the same failed validation is skipped.
+  ASSERT_THAT_ERROR(recover().moveInto(Result), Succeeded());
+  EXPECT_EQ(Result, ValidationResult::Skipped);
+
+  // Recovery counts as validation for this boot.
+  ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/false).moveInto(Result),
+                    Succeeded());
+  EXPECT_EQ(Result, ValidationResult::Skipped);
+
+  std::pair<std::string, std::string> BadOpts[] = {{"bogus", ""}};
+  EXPECT_THAT_EXPECTED(
+      validatePluginCASDatabasesIfNeeded(getCASPluginPath(), Temp.path(),
+                                         BadOpts, /*CheckHash=*/false,
+                                         /*ForceValidation=*/true),
+      Failed());
+  EXPECT_THAT_EXPECTED(
+      recoverPluginCASDatabases(getCASPluginPath(), Temp.path(), BadOpts),
+      Failed());
+}
+
 #endif /* !LLVM_HWADDRESS_SANITIZER_BUILD */
diff --git a/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp b/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp
index 084a61d90e879..800760ac569ca 100644
--- a/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp
+++ b/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp
@@ -9,6 +9,7 @@
 #include "llvm/CAS/UnifiedOnDiskCache.h"
 #include "CASTestConfig.h"
 #include "OnDiskCommonUtils.h"
+#include "llvm/Support/ThreadPool.h"
 #include "llvm/Testing/Support/Error.h"
 #include "llvm/Testing/Support/SupportHelpers.h"
 #include "gtest/gtest.h"
@@ -134,9 +135,7 @@ TEST_P(CustomHasherOnDiskCASTest, UnifiedOnDiskCacheTest) {
   std::optional<ValidationResult> ValidationRes;
   ASSERT_THAT_ERROR(UnifiedOnDiskCache::validateIfNeeded(
                         Temp.path(), HashName, HashSize, /*CheckHash=*/true,
-                        HashFn, /*AllowRecovery=*/false,
-                        /*ForceValidation=*/true,
-                        /*LLVMCasBinary=*/std::nullopt)
+                        HashFn, /*ForceValidation=*/true)
                         .moveInto(ValidationRes),
                     Succeeded());
   ASSERT_EQ(ValidationRes, ValidationResult::Valid);
@@ -211,3 +210,102 @@ TEST_P(CustomHasherOnDiskCASTest, UnifiedOnDiskCacheTest) {
     EXPECT_FALSE(Val.has_value());
   }
 }
+
+TEST_P(CustomHasherOnDiskCASTest, UnifiedOnDiskCacheConcurrentValidation) {
+  auto HashFn = GetParam().HashFn;
+  StringRef HashName = GetParam().HashName;
+  size_t HashSize = GetParam().HashSize;
+
+  auto createCAS = [&](StringRef Path) {
+    std::unique_ptr<UnifiedOnDiskCache> UniDB;
+    ASSERT_THAT_ERROR(UnifiedOnDiskCache::open(Path, /*SizeLimit=*/std::nullopt,
+                                               HashName, HashSize)
+                          .moveInto(UniDB),
+                      Succeeded());
+    std::optional<ObjectID> ID;
+    ASSERT_THAT_ERROR(store(UniDB->getGraphDB(), "1", {}).moveInto(ID),
+                      Succeeded());
+  };
+  auto validate = [&](StringRef Path, bool Force) {
+    return UnifiedOnDiskCache::validateIfNeeded(
+        Path, HashName, HashSize, /*CheckHash=*/true, HashFn, Force);
+  };
+  auto countCorruptDirs = [](StringRef Path) {
+    unsigned Count = 0;
+    std::error_code EC;
+    for (sys::fs::directory_iterator DirI(Path, EC), DirE; !EC && DirI != DirE;
+         DirI.increment(EC))
+      if (sys::path::filename(DirI->path()).starts_with("corrupt."))
+        ++Count;
+    EXPECT_FALSE(EC);
+    return Count;
+  };
+
+  // Runs \p Fn from multiple threads concurrently, and returns the number of
+  // times each result occurred. Errors are reported as test failures.
+  constexpr unsigned NumTasks = 16;
+  auto runConcurrently = [&](function_ref<Expected<ValidationResult>()> Fn) {
+    std::optional<ValidationResult> Results[NumTasks];
+    std::string Errors[NumTasks];
+    DefaultThreadPool Pool;
+    for (unsigned I = 0; I != NumTasks; ++I)
+      Pool.async([&, I] {
+        if (Error E = Fn().moveInto(Results[I]))
+          Errors[I] = toString(std::move(E));
+      });
+    Pool.wait();
+
+    std::map<ValidationResult, unsigned> Counts;
+    for (unsigned I = 0; I != NumTasks; ++I) {
+      EXPECT_EQ(Errors[I], "");
+      if (Results[I])
+        ++Counts[*Results[I]];
+    }
+    return Counts;
+  };
+
+  // Only one of the concurrent validations of valid data is performed, the
+  // rest see that it has been validated during this boot.
+  {
+    unittest::TempDir Temp("ondisk-unified", /*Unique=*/true);
+    createCAS(Temp.path());
+    auto Counts =
+        runConcurrently([&] { return validate(Temp.path(), /*Force=*/false); });
+    EXPECT_EQ(Counts[ValidationResult::Valid], 1u);
+    EXPECT_EQ(Counts[ValidationResult::Skipped], NumTasks - 1);
+
+    Counts =
+        runConcurrently([&] { return validate(Temp.path(), /*Force=*/true); });
+    EXPECT_EQ(Counts[ValidationResult::Valid], NumTasks);
+  }
+
+  // Concurrently validate invalid data and recover if validation fails, as
+  // done by `llvm-cas -validate-if-needed -allow-recovery`. Exactly one of the
+  // recoveries is performed and none of them fail, regardless of how the
+  // validations and recoveries interleave.
+  for (bool Force : {false, true}) {
+    SCOPED_TRACE(Force ? "Force" : "NoForce");
+    unittest::TempDir Temp("ondisk-unified", /*Unique=*/true);
+    createCAS(Temp.path());
+    ASSERT_FALSE(sys::fs::remove(Temp.path("v1.1/data.v1")));
+
+    auto Counts = runConcurrently([&]() -> Expected<ValidationResult> {
+      Expected<ValidationResult> Result = validate(Temp.path(), Force);
+      if (Result)
+        return Result;
+      consumeError(Result.takeError());
+      return UnifiedOnDiskCache::recover(Temp.path());
+    });
+    EXPECT_EQ(Counts[ValidationResult::Recovered], 1u);
+    EXPECT_EQ(countCorruptDirs(Temp.path()), 1u);
+
+    // Recovery counts as validation for this boot.
+    std::optional<ValidationResult> Result;
+    ASSERT_THAT_ERROR(validate(Temp.path(), /*Force=*/false).moveInto(Result),
+                      Succeeded());
+    EXPECT_EQ(Result, ValidationResult::Skipped);
+    ASSERT_THAT_ERROR(UnifiedOnDiskCache::recover(Temp.path()).moveInto(Result),
+                      Succeeded());
+    EXPECT_EQ(Result, ValidationResult::Skipped);
+  }
+}

>From 1724e3667a56e0834ef208ef317a565c2ec78bec Mon Sep 17 00:00:00 2001
From: Steven Wu <stevenwu at apple.com>
Date: Thu, 24 Sep 2026 14:13:20 -0700
Subject: [PATCH 2/2] fix tests on windows (that are pre-existing issues)

Created using spr 1.3.7
---
 llvm/include/llvm/CAS/UnifiedOnDiskCache.h    |  3 +-
 llvm/lib/CAS/OnDiskCommon.h                   |  2 +-
 llvm/lib/CAS/UnifiedOnDiskCache.cpp           | 46 +++++++++++------
 llvm/test/tools/llvm-cas/lit.local.cfg        |  8 +++
 .../llvm-cas/plugin-validation-crash.test     |  9 +++-
 .../tools/llvm-cas/plugin-validation.test     |  3 ++
 llvm/unittests/CAS/OnDiskCommonUtils.h        | 13 +++++
 llvm/unittests/CAS/PluginCASTest.cpp          |  7 ++-
 llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp | 51 +++++++++++++++++--
 9 files changed, 117 insertions(+), 25 deletions(-)

diff --git a/llvm/include/llvm/CAS/UnifiedOnDiskCache.h b/llvm/include/llvm/CAS/UnifiedOnDiskCache.h
index 554ca5e0cceba..5aa03130c8844 100644
--- a/llvm/include/llvm/CAS/UnifiedOnDiskCache.h
+++ b/llvm/include/llvm/CAS/UnifiedOnDiskCache.h
@@ -73,7 +73,8 @@ class UnifiedOnDiskCache {
   /// Validate the data in \p Path in-process, if it has not been validated
   /// since the last system boot. A successful validation is recorded so that
   /// subsequent calls can skip it; a failed or crashed one is recorded as
-  /// pending for \c recover, and is not skipped by subsequent calls.
+  /// pending for \c recover, and is not skipped by subsequent calls. Where the
+  /// boot time is not known validation is never skipped.
   ///
   /// Validation can crash on invalid data. Clients that want to be resilient
   /// to that should call this from a separate process (e.g. via
diff --git a/llvm/lib/CAS/OnDiskCommon.h b/llvm/lib/CAS/OnDiskCommon.h
index 4d2661c7b842e..c1d8adeac4240 100644
--- a/llvm/lib/CAS/OnDiskCommon.h
+++ b/llvm/lib/CAS/OnDiskCommon.h
@@ -68,7 +68,7 @@ Expected<size_t> preallocateFileTail(int FD, size_t CurrentSize,
 ///
 /// \returns the boot time in seconds (0 if operation not supported), or an \c
 /// Error.
-Expected<uint64_t> getBootTime();
+LLVM_ABI_FOR_TEST Expected<uint64_t> getBootTime();
 
 /// Helper RAII class for copying a file to a unique file path. At destruction
 /// time it will delete any new temporary files created.
diff --git a/llvm/lib/CAS/UnifiedOnDiskCache.cpp b/llvm/lib/CAS/UnifiedOnDiskCache.cpp
index ce921a1ef7f43..ebc545e9da6ee 100644
--- a/llvm/lib/CAS/UnifiedOnDiskCache.cpp
+++ b/llvm/lib/CAS/UnifiedOnDiskCache.cpp
@@ -53,7 +53,8 @@
 /// the underlying on-disk storage. The low-level storage is designed to remain
 /// coherent across regular process crashes, but may be invalid after power loss
 /// or similar system failures. \c UnifiedOnDiskCache::validateIfNeeded allows
-/// validating the contents once per boot, and if validation fails (or crashes,
+/// validating the contents once per boot (or every time, where the boot time is
+/// not known), and if validation fails (or crashes,
 /// when performed in a separate process) \c UnifiedOnDiskCache::recover can
 /// recover by marking invalid data for garbage collection.
 ///
@@ -93,6 +94,7 @@
 #include "llvm/Support/IOSandbox.h"
 #include "llvm/Support/Path.h"
 #include "llvm/Support/raw_ostream.h"
+#include <limits>
 #include <optional>
 
 using namespace llvm;
@@ -233,11 +235,11 @@ static Error validateInProcess(StringRef RootPath, StringRef HashName,
   return Error::success();
 }
 
-static Expected<uint64_t> getCachedBootTime() {
-  static uint64_t BootTime = 0;
-  if (BootTime == 0)
-    if (Error E = getBootTime().moveInto(BootTime))
-      return std::move(E);
+/// \returns the boot time, or 0 if it is not known, including if getting it
+/// failed. Validation is never skipped where the boot time is not known.
+static uint64_t getCachedBootTime() {
+  static const uint64_t BootTime =
+      expectedToOptional(getBootTime()).value_or(0);
   return BootTime;
 }
 
@@ -259,7 +261,8 @@ class LockedValidationFile {
   /// Written before validating. It is an integer so that older versions, which
   /// only know about boot times, still parse the file and treat it as not
   /// validated. It never matches a boot time.
-  static constexpr uint64_t ValidationPending = 0;
+  static constexpr uint64_t ValidationPending =
+      std::numeric_limits<uint64_t>::max();
 
   static Expected<std::unique_ptr<LockedValidationFile>>
   open(StringRef RootPath) {
@@ -302,7 +305,16 @@ class LockedValidationFile {
 
   /// \returns the boot time of the last successful validation or recovery, or
   /// 0 if there is none.
-  uint64_t getLastValidBootTime() const { return State.value_or(0); }
+  uint64_t getLastValidBootTime() const {
+    return isValidationPending() ? 0 : State.value_or(0);
+  }
+
+  /// Whether the data was validated or recovered during the boot with
+  /// \p BootTime. Always false where the boot time is not known, i.e. 0,
+  /// since it cannot be told whether that was during the current boot.
+  bool isValidAtBoot(uint64_t BootTime) const {
+    return BootTime != 0 && State == BootTime;
+  }
 
   bool isValidationPending() const { return State == ValidationPending; }
 
@@ -371,6 +383,14 @@ static Error markAllDBDirsCorrupt(StringRef RootPath) {
       GCPath.assign(RootPath);
       sys::path::append(GCPath,
                         CorruptPrefix + std::to_string(Attempt) + "." + DBDir);
+      // Skip names that are taken by earlier recoveries. Checking the error of
+      // the rename is not enough since Windows reports permission denied when
+      // the destination directory exists. Only garbage collection removes
+      // these directories concurrently, and it never creates them.
+      if (sys::fs::exists(GCPath)) {
+        EC = errc::file_exists;
+        continue;
+      }
       EC = sys::fs::rename(PathBuf, GCPath);
       // Darwin uses ENOTEMPTY. Linux may return either ENOTEMPTY or EEXIST.
       if (EC != errc::directory_not_empty && EC != errc::file_exists)
@@ -401,9 +421,7 @@ Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
     return std::move(E);
 #endif
 
-  uint64_t BootTime = 0;
-  if (Error E = getCachedBootTime().moveInto(BootTime))
-    return std::move(E);
+  uint64_t BootTime = getCachedBootTime();
   uint64_t ValidationBootTime = VF->getLastValidBootTime();
 
   bool Skipped = false;
@@ -417,7 +435,7 @@ Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
         LogValidationError, Skipped);
   });
 
-  if (ValidationBootTime == BootTime && !ForceValidation) {
+  if (VF->isValidAtBoot(BootTime) && !ForceValidation) {
     Skipped = true;
     return ValidationResult::Skipped;
   }
@@ -451,9 +469,7 @@ Expected<ValidationResult> UnifiedOnDiskCache::recover(StringRef RootPath) {
     return std::move(E);
 #endif
 
-  uint64_t BootTime = 0;
-  if (Error E = getCachedBootTime().moveInto(BootTime))
-    return std::move(E);
+  uint64_t BootTime = getCachedBootTime();
 
   bool Skipped = false;
   std::string LogRecoveryError;
diff --git a/llvm/test/tools/llvm-cas/lit.local.cfg b/llvm/test/tools/llvm-cas/lit.local.cfg
index 0f6e925d6b4c7..2ee6eb53c7d54 100644
--- a/llvm/test/tools/llvm-cas/lit.local.cfg
+++ b/llvm/test/tools/llvm-cas/lit.local.cfg
@@ -1,6 +1,14 @@
+import platform
+
 if not config.have_ondisk_cas:
     config.unsupported = True
 
 if config.llvm_cas_plugin_test_path:
     config.available_features.add("cas-plugin")
     config.substitutions.append(("%cas_plugin", config.llvm_cas_plugin_test_path))
+
+# Validation is only skipped where the boot time is known, see getBootTime().
+# This may not list all such platforms, but tests that require it only need to
+# run on some.
+if platform.system() in ("Darwin", "Linux"):
+    config.available_features.add("cas-boot-time")
diff --git a/llvm/test/tools/llvm-cas/plugin-validation-crash.test b/llvm/test/tools/llvm-cas/plugin-validation-crash.test
index 7f55f98a14e70..611432397f1bd 100644
--- a/llvm/test/tools/llvm-cas/plugin-validation-crash.test
+++ b/llvm/test/tools/llvm-cas/plugin-validation-crash.test
@@ -1,4 +1,7 @@
 REQUIRES: cas-plugin
+# Checks that validation is skipped once the data has been validated since boot,
+# which requires the boot time to be known.
+REQUIRES: cas-boot-time
 # HWASan does not tag the globals of a dlopen'ed library with glibc, see
 # https://github.com/llvm/llvm-project/issues/57206.
 UNSUPPORTED: hwasan
@@ -52,8 +55,10 @@ RUN:   --get-cache-result @%t/abc.casid | FileCheck %s --check-prefix=NOT-FOUND
 VALID: validated successfully
 SKIPPED: validation skipped
 INVALID: llvm-cas: validate-if-needed: cas contents invalid
-PENDING: {{^0$}}
-VALIDATED: {{^[1-9][0-9]*$}}
+# Validation pending is recorded as UINT64_MAX, a boot time is shorter. The boot
+# time is 0 on platforms where it is not known.
+PENDING: {{^18446744073709551615$}}
+VALIDATED: {{^[0-9]{1,19}$}}
 RECOVERED: recovered from invalid data
 RECOVERED-DIRS: corrupt.0.v1.1
 NOT-FOUND: result not found
diff --git a/llvm/test/tools/llvm-cas/plugin-validation.test b/llvm/test/tools/llvm-cas/plugin-validation.test
index 6c7ef67a0024e..71510cf2afa3b 100644
--- a/llvm/test/tools/llvm-cas/plugin-validation.test
+++ b/llvm/test/tools/llvm-cas/plugin-validation.test
@@ -1,4 +1,7 @@
 REQUIRES: cas-plugin
+# Checks that validation is skipped once the data has been validated since boot,
+# which requires the boot time to be known.
+REQUIRES: cas-boot-time
 # HWASan does not tag the globals of a dlopen'ed library with glibc, see
 # https://github.com/llvm/llvm-project/issues/57206.
 UNSUPPORTED: hwasan
diff --git a/llvm/unittests/CAS/OnDiskCommonUtils.h b/llvm/unittests/CAS/OnDiskCommonUtils.h
index 303415630c939..9365c6c211ff3 100644
--- a/llvm/unittests/CAS/OnDiskCommonUtils.h
+++ b/llvm/unittests/CAS/OnDiskCommonUtils.h
@@ -17,8 +17,21 @@
 #include "llvm/Support/BLAKE3.h"
 #include "llvm/Testing/Support/Error.h"
 
+namespace llvm::cas::ondisk {
+/// Declared in the private "OnDiskCommon.h".
+Expected<uint64_t> getBootTime();
+} // namespace llvm::cas::ondisk
+
 namespace llvm::unittest::cas {
 
+/// \returns whether the boot time is known, i.e. it is supported on this
+/// platform and getting it succeeds. Validation of on-disk data is only ever
+/// skipped if it is, since otherwise it cannot be told whether the data has
+/// been validated since boot.
+inline bool isBootTimeKnown() {
+  return expectedToOptional(llvm::cas::ondisk::getBootTime()).value_or(0) != 0;
+}
+
 using namespace llvm::cas;
 using namespace llvm::cas::ondisk;
 
diff --git a/llvm/unittests/CAS/PluginCASTest.cpp b/llvm/unittests/CAS/PluginCASTest.cpp
index d417d6c6b7bf5..5dc4400b2a62b 100644
--- a/llvm/unittests/CAS/PluginCASTest.cpp
+++ b/llvm/unittests/CAS/PluginCASTest.cpp
@@ -13,6 +13,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "CASTestConfig.h"
+#include "OnDiskCommonUtils.h"
 #include "llvm/CAS/ActionCache.h"
 #include "llvm/CAS/ObjectStore.h"
 #include "llvm/Config/config.h"
@@ -174,9 +175,11 @@ TEST(PluginCASTest, validate) {
   }
 
   // Already validated since boot.
+  const ValidationResult ValidatedSinceBoot =
+      isBootTimeKnown() ? ValidationResult::Skipped : ValidationResult::Valid;
   ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/false).moveInto(Result),
                     Succeeded());
-  EXPECT_EQ(Result, ValidationResult::Skipped);
+  EXPECT_EQ(Result, ValidatedSinceBoot);
 
   ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/true).moveInto(Result),
                     Succeeded());
@@ -209,7 +212,7 @@ TEST(PluginCASTest, validate) {
   // Recovery counts as validation for this boot.
   ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/false).moveInto(Result),
                     Succeeded());
-  EXPECT_EQ(Result, ValidationResult::Skipped);
+  EXPECT_EQ(Result, ValidatedSinceBoot);
 
   std::pair<std::string, std::string> BadOpts[] = {{"bogus", ""}};
   EXPECT_THAT_EXPECTED(
diff --git a/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp b/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp
index 800760ac569ca..f89cfb8556725 100644
--- a/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp
+++ b/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp
@@ -265,14 +265,17 @@ TEST_P(CustomHasherOnDiskCASTest, UnifiedOnDiskCacheConcurrentValidation) {
   };
 
   // Only one of the concurrent validations of valid data is performed, the
-  // rest see that it has been validated during this boot.
+  // rest see that it has been validated during this boot. Where the boot time
+  // is not known they are all performed.
+  const bool BootTimeKnown = isBootTimeKnown();
   {
     unittest::TempDir Temp("ondisk-unified", /*Unique=*/true);
     createCAS(Temp.path());
     auto Counts =
         runConcurrently([&] { return validate(Temp.path(), /*Force=*/false); });
-    EXPECT_EQ(Counts[ValidationResult::Valid], 1u);
-    EXPECT_EQ(Counts[ValidationResult::Skipped], NumTasks - 1);
+    EXPECT_EQ(Counts[ValidationResult::Valid], BootTimeKnown ? 1u : NumTasks);
+    EXPECT_EQ(Counts[ValidationResult::Skipped],
+              BootTimeKnown ? NumTasks - 1 : 0u);
 
     Counts =
         runConcurrently([&] { return validate(Temp.path(), /*Force=*/true); });
@@ -303,9 +306,49 @@ TEST_P(CustomHasherOnDiskCASTest, UnifiedOnDiskCacheConcurrentValidation) {
     std::optional<ValidationResult> Result;
     ASSERT_THAT_ERROR(validate(Temp.path(), /*Force=*/false).moveInto(Result),
                       Succeeded());
-    EXPECT_EQ(Result, ValidationResult::Skipped);
+    EXPECT_EQ(Result, BootTimeKnown ? ValidationResult::Skipped
+                                    : ValidationResult::Valid);
     ASSERT_THAT_ERROR(UnifiedOnDiskCache::recover(Temp.path()).moveInto(Result),
                       Succeeded());
     EXPECT_EQ(Result, ValidationResult::Skipped);
   }
 }
+
+TEST_P(CustomHasherOnDiskCASTest, UnifiedOnDiskCacheRepeatedRecovery) {
+  auto HashFn = GetParam().HashFn;
+  StringRef HashName = GetParam().HashName;
+  size_t HashSize = GetParam().HashSize;
+
+  unittest::TempDir Temp("ondisk-unified", /*Unique=*/true);
+
+  // Each recovery moves the data aside under a new name, even though the
+  // directory names of earlier recoveries are still taken.
+  for (unsigned I = 0; I != 3; ++I) {
+    SCOPED_TRACE(I);
+    {
+      std::unique_ptr<UnifiedOnDiskCache> UniDB;
+      ASSERT_THAT_ERROR(UnifiedOnDiskCache::open(Temp.path(),
+                                                 /*SizeLimit=*/std::nullopt,
+                                                 HashName, HashSize)
+                            .moveInto(UniDB),
+                        Succeeded());
+      std::optional<ObjectID> ID;
+      ASSERT_THAT_ERROR(store(UniDB->getGraphDB(), "1", {}).moveInto(ID),
+                        Succeeded());
+    }
+    ASSERT_FALSE(sys::fs::remove(Temp.path("v1.1/data.v1")));
+    EXPECT_THAT_EXPECTED(
+        UnifiedOnDiskCache::validateIfNeeded(Temp.path(), HashName, HashSize,
+                                             /*CheckHash=*/true, HashFn,
+                                             /*ForceValidation=*/true),
+        Failed());
+
+    std::optional<ValidationResult> Result;
+    ASSERT_THAT_ERROR(UnifiedOnDiskCache::recover(Temp.path()).moveInto(Result),
+                      Succeeded());
+    EXPECT_EQ(Result, ValidationResult::Recovered);
+    EXPECT_TRUE(
+        sys::fs::exists(Temp.path("corrupt." + std::to_string(I) + ".v1.1")));
+    EXPECT_FALSE(sys::fs::exists(Temp.path("v1.1")));
+  }
+}



More information about the llvm-commits mailing list