1
1
Fork 0
mirror of https://github.com/NixOS/nix.git synced 2025-11-21 09:49:36 +01:00
nix/src/libstore/aws-creds.cc
Bernardo Meurer Costa f0e1f65260
fix(libstore): fix race condition in AWS credential provider caching
The previous implementation had a check-then-create race condition where
multiple threads could simultaneously:
1. Check the cache and find no provider (line 122)
2. Create their own providers (lines 126-145)
3. Insert into cache (line 161)

This resulted in multiple credential providers being created when
downloading multiple packages in parallel, as each .narinfo download
would trigger provider creation on its own thread.

Fix by using boost::concurrent_flat_map's try_emplace_and_cvisit, which
provides atomic get-or-create semantics:
- f1 callback: Called atomically during insertion, creates the provider
- f2 callback: Called if key exists, returns cached provider
- Other threads are blocked during f1, so no nullptr is ever visible
2025-10-12 20:16:02 +00:00

191 lines
6.9 KiB
C++

#include "nix/store/aws-creds.hh"
#if NIX_WITH_CURL_S3
# include <aws/crt/Types.h>
# include "nix/store/s3-url.hh"
# include "nix/util/finally.hh"
# include "nix/util/logging.hh"
# include "nix/util/url.hh"
# include "nix/util/util.hh"
# include <aws/crt/Api.h>
# include <aws/crt/auth/Credentials.h>
# include <aws/crt/io/Bootstrap.h>
# include <boost/unordered/concurrent_flat_map.hpp>
# include <chrono>
# include <future>
# include <memory>
# include <unistd.h>
namespace nix {
namespace {
// Global credential provider cache using boost's concurrent map
// Key: profile name (empty string for default profile)
using CredentialProviderCache =
boost::concurrent_flat_map<std::string, std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider>>;
static CredentialProviderCache credentialProviderCache;
/**
* Clear all cached credential providers.
* Called automatically by CrtWrapper destructor during static destruction.
*/
static void clearAwsCredentialsCache()
{
credentialProviderCache.clear();
}
static void initAwsCrt()
{
struct CrtWrapper
{
Aws::Crt::ApiHandle apiHandle;
CrtWrapper()
{
apiHandle.InitializeLogging(Aws::Crt::LogLevel::Warn, static_cast<FILE *>(nullptr));
}
~CrtWrapper()
{
try {
// CRITICAL: Clear credential provider cache BEFORE AWS CRT shuts down
// This ensures all providers (which hold references to ClientBootstrap)
// are destroyed while AWS CRT is still valid
clearAwsCredentialsCache();
// Now it's safe for ApiHandle destructor to run
} catch (...) {
ignoreExceptionInDestructor();
}
}
};
static CrtWrapper crt;
}
static AwsCredentials getCredentialsFromProvider(std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider> provider)
{
if (!provider || !provider->IsValid()) {
throw AwsAuthError("AWS credential provider is invalid");
}
auto prom = std::make_shared<std::promise<AwsCredentials>>();
auto fut = prom->get_future();
provider->GetCredentials([prom](std::shared_ptr<Aws::Crt::Auth::Credentials> credentials, int errorCode) {
if (errorCode != 0 || !credentials) {
prom->set_exception(
std::make_exception_ptr(AwsAuthError("Failed to resolve AWS credentials: error code %d", errorCode)));
} else {
auto accessKeyId = Aws::Crt::ByteCursorToStringView(credentials->GetAccessKeyId());
auto secretAccessKey = Aws::Crt::ByteCursorToStringView(credentials->GetSecretAccessKey());
auto sessionToken = Aws::Crt::ByteCursorToStringView(credentials->GetSessionToken());
std::optional<std::string> sessionTokenStr;
if (!sessionToken.empty()) {
sessionTokenStr = std::string(sessionToken.data(), sessionToken.size());
}
prom->set_value(AwsCredentials(
std::string(accessKeyId.data(), accessKeyId.size()),
std::string(secretAccessKey.data(), secretAccessKey.size()),
sessionTokenStr));
}
});
// AWS CRT GetCredentials is asynchronous and only guarantees the callback will be
// invoked if the initial call returns success. There's no documented timeout mechanism,
// so we add a timeout to prevent indefinite hanging if the callback is never called.
auto timeout = std::chrono::seconds(30);
if (fut.wait_for(timeout) == std::future_status::timeout) {
throw AwsAuthError(
"Timeout waiting for AWS credentials (%d seconds)",
std::chrono::duration_cast<std::chrono::seconds>(timeout).count());
}
return fut.get(); // This will throw if set_exception was called
}
} // anonymous namespace
AwsCredentials getAwsCredentials(const std::string & profile)
{
// Get or create credential provider with caching
std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider> provider;
// Use try_emplace_and_cvisit for atomic get-or-create
// This prevents race conditions where multiple threads create providers
credentialProviderCache.try_emplace_and_cvisit(
profile,
nullptr, // Placeholder - will be replaced in f1 before any thread can see it
[&](auto & kv) {
// f1: Called atomically during insertion with non-const reference
// Other threads are blocked until we finish, so nullptr is never visible
debug(
"[pid=%d] creating new AWS credential provider for profile '%s'",
getpid(),
profile.empty() ? "(default)" : profile.c_str());
try {
initAwsCrt();
if (profile.empty()) {
Aws::Crt::Auth::CredentialsProviderChainDefaultConfig config;
config.Bootstrap = Aws::Crt::ApiHandle::GetOrCreateStaticDefaultClientBootstrap();
kv.second = Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderChainDefault(config);
} else {
Aws::Crt::Auth::CredentialsProviderProfileConfig config;
config.Bootstrap = Aws::Crt::ApiHandle::GetOrCreateStaticDefaultClientBootstrap();
// This is safe because the underlying C library will copy this string
// c.f. https://github.com/awslabs/aws-c-auth/blob/main/source/credentials_provider_profile.c#L220
config.ProfileNameOverride = Aws::Crt::ByteCursorFromCString(profile.c_str());
kv.second = Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderProfile(config);
}
if (!kv.second) {
throw AwsAuthError(
"Failed to create AWS credentials provider for %s",
profile.empty() ? "default profile" : fmt("profile '%s'", profile));
}
provider = kv.second;
} catch (Error & e) {
// Exception during creation - remove the entry to allow retry
credentialProviderCache.erase(profile);
e.addTrace({}, "for AWS profile: %s", profile.empty() ? "(default)" : profile);
throw;
} catch (...) {
// Non-Error exception - still need to clean up
credentialProviderCache.erase(profile);
throw;
}
},
[&](const auto & kv) {
// f2: Called if key already exists (const reference)
provider = kv.second;
});
return getCredentialsFromProvider(provider);
}
void invalidateAwsCredentials(const std::string & profile)
{
credentialProviderCache.erase(profile);
}
AwsCredentials preResolveAwsCredentials(const ParsedS3URL & s3Url)
{
std::string profile = s3Url.profile.value_or("");
// Get credentials (automatically cached)
return getAwsCredentials(profile);
}
} // namespace nix
#endif