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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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( _______________________________________________ llvm-branch-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits
