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

Steven Wu via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Mon Sep 28 15:46:52 PDT 2026


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

>From c996e184a41dfec3acbb55cf06b0c44f0ddb21ef Mon Sep 17 00:00:00 2001
From: Steven Wu <stevenwu at apple.com>
Date: Mon, 28 Sep 2026 10:29:13 -0700
Subject: [PATCH 1/3] [CAS] Support validation and recovery for plugin CAS in
 llvm-cas

Add the -fcas-plugin-path and -fcas-plugin-option options to llvm-cas,
and optional llcas_cas_validate_if_needed() and
llcas_cas_recover_ondisk_data() functions to the plugin API, exposed
via cas::validatePluginCASDatabasesIfNeeded() and
cas::recoverPluginCASDatabases(). Implement them, along with
llcas_cas_validate() and llcas_actioncache_validate(), in
libCASPluginTest and make the plugin available to lit tests.
---
 llvm/include/llvm-c/CAS/PluginAPI_functions.h |  47 ++++++++
 llvm/include/llvm-c/CAS/PluginAPI_types.h     |  28 ++++-
 llvm/include/llvm/CAS/ObjectStore.h           |  39 +++++++
 llvm/lib/CAS/PluginAPI.h                      |   7 ++
 llvm/lib/CAS/PluginAPI_functions.def          |   2 +
 llvm/lib/CAS/PluginCAS.cpp                    | 102 ++++++++++++++++--
 llvm/test/CMakeLists.txt                      |  12 +++
 llvm/test/lit.site.cfg.py.in                  |   1 +
 llvm/test/tools/llvm-cas/lit.local.cfg        |  12 +++
 .../llvm-cas/plugin-validation-crash.test     |  64 +++++++++++
 .../tools/llvm-cas/plugin-validation.test     |  76 +++++++++++++
 .../libCASPluginTest/libCASPluginTest.cpp     |  65 +++++++++++
 .../libCASPluginTest/libCASPluginTest.exports |   4 +
 llvm/tools/llvm-cas/Options.td                |   8 ++
 llvm/tools/llvm-cas/llvm-cas.cpp              |  44 ++++++--
 llvm/unittests/CAS/PluginCASTest.cpp          |  94 ++++++++++++++++
 16 files changed, 590 insertions(+), 15 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/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/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/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..2ee6eb53c7d54 100644
--- a/llvm/test/tools/llvm-cas/lit.local.cfg
+++ b/llvm/test/tools/llvm-cas/lit.local.cfg
@@ -1,2 +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
new file mode 100644
index 0000000000000..611432397f1bd
--- /dev/null
+++ b/llvm/test/tools/llvm-cas/plugin-validation-crash.test
@@ -0,0 +1,64 @@
+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
+
+# 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
+# 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
new file mode 100644
index 0000000000000..71510cf2afa3b
--- /dev/null
+++ b/llvm/test/tools/llvm-cas/plugin-validation.test
@@ -0,0 +1,76 @@
+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
+
+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 a69cc9a5fe2b9..f996c3d3a79f3 100644
--- a/llvm/tools/llvm-cas/llvm-cas.cpp
+++ b/llvm/tools/llvm-cas/llvm-cas.cpp
@@ -64,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;
@@ -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);
@@ -186,7 +193,14 @@ int main(int Argc, char **Argv) {
   if (Opts.Command == CommandKind::ValidateIfNeeded)
     return validateIfNeeded(Opts, Argv[0]);
 
-  auto [CAS, AC] = ExitOnErr(createOnDiskUnifiedCASDatabases(Opts.CASPath));
+  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)
@@ -363,8 +377,12 @@ int validate(ObjectStore &CAS, ActionCache &AC, bool CheckHash) {
 /// Validates the CAS in this process and prints the result.
 static Error validateInProcess(const CommandOptions &Opts) {
   ValidationResult Result;
-  if (Error E = validateOnDiskUnifiedCASDatabasesIfNeeded(
-                    Opts.CASPath, Opts.CheckHash, Opts.Force)
+  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"
@@ -381,8 +399,17 @@ static Expected<bool> validateOutOfProcess(const CommandOptions &Opts,
                                            const char *Argv0) {
   std::string Exec =
       sys::fs::getMainExecutable(Argv0, (void *)validateOutOfProcess);
-  SmallVector<StringRef> Args{Exec, "--cas", Opts.CASPath,
-                              "--validate-if-needed", "--in-process"};
+  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});
+  }
+  Args.append({"--validate-if-needed", "--in-process"});
   if (Opts.CheckHash)
     Args.push_back("--check-hash");
   if (Opts.Force)
@@ -419,8 +446,11 @@ int validateIfNeeded(const CommandOptions &Opts, const char *Argv0) {
       ExitOnErr(createStringError("cas contents invalid"));
   }
 
-  ValidationResult Result =
-      ExitOnErr(recoverOnDiskUnifiedCASDatabases(Opts.CASPath));
+  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");
diff --git a/llvm/unittests/CAS/PluginCASTest.cpp b/llvm/unittests/CAS/PluginCASTest.cpp
index 376779a387a01..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"
@@ -131,4 +132,97 @@ 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.
+  const ValidationResult ValidatedSinceBoot =
+      isBootTimeKnown() ? ValidationResult::Skipped : ValidationResult::Valid;
+  ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/false).moveInto(Result),
+                    Succeeded());
+  EXPECT_EQ(Result, ValidatedSinceBoot);
+
+  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, ValidatedSinceBoot);
+
+  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 */

>From f6f868b98c01652d760d4779c51ba73b91c086d1 Mon Sep 17 00:00:00 2001
From: Steven Wu <stevenwu at apple.com>
Date: Mon, 28 Sep 2026 15:43:25 -0700
Subject: [PATCH 2/3] Address review feedback

---
 llvm/include/llvm-c/CAS/PluginAPI_functions.h | 6 +++---
 llvm/include/llvm/CAS/ObjectStore.h           | 4 ++--
 llvm/lib/CAS/PluginCAS.cpp                    | 8 +++++++-
 3 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/llvm/include/llvm-c/CAS/PluginAPI_functions.h b/llvm/include/llvm-c/CAS/PluginAPI_functions.h
index b73872655c690..4abecc3f638df 100644
--- a/llvm/include/llvm-c/CAS/PluginAPI_functions.h
+++ b/llvm/include/llvm-c/CAS/PluginAPI_functions.h
@@ -165,9 +165,9 @@ LLCAS_PUBLIC bool llcas_cas_validate(llcas_cas_t, bool check_hash,
  * \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.
+ * Clients that want to be resilient to unexpected crashes during validation
+ * 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.
diff --git a/llvm/include/llvm/CAS/ObjectStore.h b/llvm/include/llvm/CAS/ObjectStore.h
index 394eb9d53d7a6..064117335ba78 100644
--- a/llvm/include/llvm/CAS/ObjectStore.h
+++ b/llvm/include/llvm/CAS/ObjectStore.h
@@ -413,8 +413,8 @@ createPluginCASDatabases(
 /// 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
+/// Clients that want to be resilient to unexpected crashes during validation
+/// may call this from a separate process (e.g. via
 /// \c llvm-cas -validate-if-needed) and call \c recoverPluginCASDatabases if
 /// it fails.
 ///
diff --git a/llvm/lib/CAS/PluginCAS.cpp b/llvm/lib/CAS/PluginCAS.cpp
index 682a674a984dc..f132a847f4a3e 100644
--- a/llvm/lib/CAS/PluginCAS.cpp
+++ b/llvm/lib/CAS/PluginCAS.cpp
@@ -620,7 +620,13 @@ static Expected<ValidationResult> callPluginValidationFunction(
   case LLCAS_VALIDATION_RESULT_ERROR:
     return PluginCASContext::errorAndDispose(c_err, Functions);
   }
-  llvm_unreachable("unknown llcas_validation_result_t value");
+  // The plugin is outside of our control, so an unknown result is an error
+  // rather than unreachable.
+  if (c_err)
+    Functions.string_dispose(c_err);
+  return createStringError("unknown validation result " +
+                           Twine(static_cast<int>(Result)) + " from '" +
+                           PluginPath + "'");
 }
 
 Expected<ValidationResult> cas::validatePluginCASDatabasesIfNeeded(

>From 3865d8288f3e8ff24ec06b1f0ae3291ec1dc5039 Mon Sep 17 00:00:00 2001
From: Steven Wu <stevenwu at apple.com>
Date: Mon, 28 Sep 2026 15:46:31 -0700
Subject: [PATCH 3/3] Include the plugin's error message with an unknown
 validation result

---
 llvm/lib/CAS/PluginCAS.cpp | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/llvm/lib/CAS/PluginCAS.cpp b/llvm/lib/CAS/PluginCAS.cpp
index f132a847f4a3e..8b31df1eae6bc 100644
--- a/llvm/lib/CAS/PluginCAS.cpp
+++ b/llvm/lib/CAS/PluginCAS.cpp
@@ -621,12 +621,16 @@ static Expected<ValidationResult> callPluginValidationFunction(
     return PluginCASContext::errorAndDispose(c_err, Functions);
   }
   // The plugin is outside of our control, so an unknown result is an error
-  // rather than unreachable.
-  if (c_err)
+  // rather than unreachable. Include the plugin's error message, if any.
+  std::string Msg;
+  raw_string_ostream OS(Msg);
+  OS << "unknown validation result " << static_cast<int>(Result) << " from '"
+     << PluginPath << "'";
+  if (c_err) {
+    OS << ": " << c_err;
     Functions.string_dispose(c_err);
-  return createStringError("unknown validation result " +
-                           Twine(static_cast<int>(Result)) + " from '" +
-                           PluginPath + "'");
+  }
+  return createStringError(Msg);
 }
 
 Expected<ValidationResult> cas::validatePluginCASDatabasesIfNeeded(



More information about the llvm-branch-commits mailing list