From eef8a1a5ff03aad775ec4df66ff98e980cc458bc Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Costa Date: Fri, 13 Feb 2026 07:47:13 +0000 Subject: [PATCH 01/18] chore: bump version --- .version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.version b/.version index dc148c42f45d..02eb44632167 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.33.3 +2.33.4 From 6e88d0cf6c8a2d8cdebc5c33c9207161e81b3e0c Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Mon, 16 Feb 2026 13:33:25 -0500 Subject: [PATCH 02/18] upload-release: disable containerd image store to preserve gzip layer compression (cherry picked from commit 2ccb8a9a562ea666ba4b3718f8820324f4763368) --- .github/workflows/upload-release.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/upload-release.yml b/.github/workflows/upload-release.yml index 82ce0c40b74d..01a9ecac3ca4 100644 --- a/.github/workflows/upload-release.yml +++ b/.github/workflows/upload-release.yml @@ -39,6 +39,17 @@ jobs: role-to-assume: "arn:aws:iam::080433136561:role/nix-release" role-session-name: nix-release-oidc-${{ github.run_id }} aws-region: eu-west-1 + - name: Disable containerd image store + run: | + # Docker 28+ defaults to the containerd image store, which + # pushes layers uncompressed instead of gzip. OCI clients + # that only support gzip (e.g. go-containerregistry) fail + # with "gzip: invalid header". Disabling the containerd + # snapshotter restores the classic storage driver, which + # preserves gzip-compressed layers through the + # `docker load` / `docker push` pipeline. + echo '{"features":{"containerd-snapshotter":false}}' | sudo tee /etc/docker/daemon.json > /dev/null + sudo systemctl restart docker - name: Login to Docker Hub uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: From e2af95376a1c001bf73b8255e02e1dc816d16e80 Mon Sep 17 00:00:00 2001 From: dramforever Date: Thu, 26 Feb 2026 00:51:31 +0800 Subject: [PATCH 03/18] libutil/logging: Generalize writeToStderr into writeFullLogging Generalize writeToStderr into writeFullLogging, with similar behavior but taking an arbitrary fd, and reimplement writeToStderr with it as a convenient wrapper. (cherry picked from commit 473d54ed5f1798e43fe93b8383c3b2c06826a4d1) --- src/libutil/logging.cc | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/libutil/logging.cc b/src/libutil/logging.cc index 8f7ec2d294ed..c814e96f11be 100644 --- a/src/libutil/logging.cc +++ b/src/libutil/logging.cc @@ -150,18 +150,23 @@ class SimpleLogger : public Logger Verbosity verbosity = lvlInfo; -void writeToStderr(std::string_view s) +static void writeFullLogging(Descriptor fd, std::string_view s) { try { - writeFull(getStandardError(), s, false); + writeFull(fd, s, false); } catch (SystemError & e) { - /* Ignore failing writes to stderr. We need to ignore write - errors to ensure that cleanup code that logs to stderr runs - to completion if the other side of stderr has been closed - unexpectedly. */ + /* Ignore failing logging writes. We need to ignore write + errors to ensure that cleanup code that writes logs runs + to completion if the other side of the logging fd has + been closed unexpectedly. */ } } +void writeToStderr(std::string_view s) +{ + writeFullLogging(getStandardError(), s); +} + std::unique_ptr makeSimpleLogger(bool printBuildLogs) { return std::make_unique(printBuildLogs); From e258c32fa645b5c75dad45a425529ac9283c73f7 Mon Sep 17 00:00:00 2001 From: dramforever Date: Thu, 26 Feb 2026 01:04:05 +0800 Subject: [PATCH 04/18] libutil/logging: Use writeFullLogging in JSONLogger::write Logging should not check for interrupts. Use the new writeFullLogging function to write JSON output to get similar behavior to SimpleLogger. This avoids the logger itself throwing Interrupted exceptions in handleExceptions. (cherry picked from commit 12101004216089329cc2831608be07ef84cd7404) --- src/libutil/logging.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libutil/logging.cc b/src/libutil/logging.cc index c814e96f11be..c8d70fc7f1d7 100644 --- a/src/libutil/logging.cc +++ b/src/libutil/logging.cc @@ -250,15 +250,15 @@ struct JSONLogger : Logger void write(const nlohmann::json & json) { - auto line = - (includeNixPrefix ? "@nix " : "") + json.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace); + auto line = (includeNixPrefix ? "@nix " : "") + + json.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace) + "\n"; /* Acquire a lock to prevent log messages from clobbering each other. */ try { auto state(_state.lock()); if (state->enabled) - writeLine(fd, line); + writeFullLogging(fd, line); } catch (...) { bool enabled = false; std::swap(_state.lock()->enabled, enabled); From 296b8d7bb977fefac0a3b6b488659da3be14d30d Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 25 Apr 2025 16:50:02 +0200 Subject: [PATCH 05/18] Fix the nix-community/patsh/0.2.1 flake regression test (again) (cherry picked from commit 23349778632379b3863d5498b3b583de2a9a5a8f) --- src/libfetchers/fetchers.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libfetchers/fetchers.cc b/src/libfetchers/fetchers.cc index 7e091ef1071e..a3090fba08fe 100644 --- a/src/libfetchers/fetchers.cc +++ b/src/libfetchers/fetchers.cc @@ -236,6 +236,9 @@ void Input::checkLocks(Input specified, Input & result) if (auto prevNarHash = specified.getNarHash()) specified.attrs.insert_or_assign("narHash", prevNarHash->to_string(HashFormat::SRI, true)); + if (auto narHash = result.getNarHash()) + result.attrs.insert_or_assign("narHash", narHash->to_string(HashFormat::SRI, true)); + for (auto & field : specified.attrs) { auto field2 = result.attrs.find(field.first); if (field2 != result.attrs.end() && field.second != field2->second) From f543870f81da203f33821757986f56a0a48422a7 Mon Sep 17 00:00:00 2001 From: Luna Nova Date: Sat, 7 Mar 2026 19:09:27 -0800 Subject: [PATCH 06/18] libstore: handle root path in RemoteFSAccessor::maybeLstat RemoteFSAccessor::fetch() crashes when called with the root path. $ nix build nixpkgs#hello --store ssh-ng://host error: path '/nix/store/' is not in the Nix store Add path.isRoot() guard like LocalStoreAccessor::maybeLstat Fixes: eb643d034 ("`Store::getFSAccessor`: Do not include the store dir") (cherry picked from commit b523564eb3b5ec5070d5de82d19906a06726c9ea) --- src/libstore/remote-fs-accessor.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libstore/remote-fs-accessor.cc b/src/libstore/remote-fs-accessor.cc index 51bab9953548..fc1dee92eaf7 100644 --- a/src/libstore/remote-fs-accessor.cc +++ b/src/libstore/remote-fs-accessor.cc @@ -94,6 +94,8 @@ std::shared_ptr RemoteFSAccessor::accessObject(const StorePath & std::optional RemoteFSAccessor::maybeLstat(const CanonPath & path) { + if (path.isRoot()) + return Stat{.type = tDirectory}; auto res = fetch(path); return res.first->maybeLstat(res.second); } From 1bd5fc56a9c43f8dc6f1bf8398776e880a2564ac Mon Sep 17 00:00:00 2001 From: Lisanna Dettwyler Date: Sat, 14 Mar 2026 17:16:01 -0400 Subject: [PATCH 07/18] Fix compatibility with lowdown 3 Resolves #15420 Signed-off-by: Lisanna Dettwyler (cherry picked from commit 342faaa1fa0aa3accc3027081b66c7c2ca36670b) --- src/libcmd/markdown.cc | 4 +++- src/libcmd/meson.build | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libcmd/markdown.cc b/src/libcmd/markdown.cc index c3341da73a26..2cdc8e38c234 100644 --- a/src/libcmd/markdown.cc +++ b/src/libcmd/markdown.cc @@ -38,7 +38,9 @@ static std::string doRenderMarkdownToTerminal(std::string_view markdown) # endif .feat = LOWDOWN_COMMONMARK | LOWDOWN_FENCED | LOWDOWN_DEFLIST | LOWDOWN_TABLES, .oflags = -# if HAVE_LOWDOWN_1_4 +# if HAVE_LOWDOWN_3 + LOWDOWN_NORELLINK +# elif HAVE_LOWDOWN_1_4 LOWDOWN_TERM_NORELLINK // To render full links while skipping relative ones # else LOWDOWN_TERM_NOLINK diff --git a/src/libcmd/meson.build b/src/libcmd/meson.build index f553afa0ba17..07c899a9128c 100644 --- a/src/libcmd/meson.build +++ b/src/libcmd/meson.build @@ -44,6 +44,10 @@ configdata.set( 'HAVE_LOWDOWN_1_4', lowdown.version().version_compare('>= 1.4.0').to_int(), ) +configdata.set( + 'HAVE_LOWDOWN_3', + lowdown.version().version_compare('>= 3.0.0').to_int(), +) readline_flavor = get_option('readline-flavor') if readline_flavor == 'editline' From fa15a4cdccf0ede84b08090fef52248c08d9fb3a Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Costa Date: Tue, 17 Mar 2026 23:10:30 +0000 Subject: [PATCH 08/18] fix(libstore/aws-creds): add STS WebIdentity provider to credential chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nix 2.33 replaced aws-sdk-cpp with aws-crt-cpp for S3 credentials. The custom credential chain (Environment → SSO → Profile → IMDS) omitted the STS WebIdentity provider that was part of the old DefaultAWSCredentialsProviderChain, breaking EKS IRSA, GitHub Actions OIDC, and any other sts:AssumeRoleWithWebIdentity flow. The observed failure mode on EKS pods using IRSA was a misleading 'Valid credentials could not be sourced by the IMDS provider' error — IMDS is merely the last provider tried after WebIdentity was skipped. aws-c-auth (already linked) exposes aws_credentials_provider_new_sts_web_identity, which reads AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, and AWS_REGION from the environment (falling back to the profile config) and returns nullptr at construction time when they aren't set. This is wired into the chain after Profile and before IMDS, matching the pre-2.33 ordering. (cherry picked from commit 9ae4b5021ce721a4699b90227178085eebbee33f) --- src/libstore/aws-creds.cc | 43 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/libstore/aws-creds.cc b/src/libstore/aws-creds.cc index 9b0ddefdca85..495e8ae8fcfa 100644 --- a/src/libstore/aws-creds.cc +++ b/src/libstore/aws-creds.cc @@ -170,6 +170,31 @@ static std::shared_ptr createSSOProvider( return createWrappedProvider(aws_credentials_provider_new_sso(allocator, &options), allocator); } +/** + * Create an STS WebIdentity credentials provider using the C library directly. + * This reads AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, + * and AWS_REGION from the environment (falling back to the profile config). + * Used by EKS IRSA, GitHub Actions OIDC, and other sts:AssumeRoleWithWebIdentity flows. + * Returns nullptr if the required parameters can't be resolved. + */ +static std::shared_ptr createSTSWebIdentityProvider( + const std::string & profileName, + Aws::Crt::Io::ClientBootstrap * bootstrap, + Aws::Crt::Io::TlsContext * tlsContext, + Aws::Crt::Allocator * allocator = Aws::Crt::ApiAllocator()) +{ + aws_credentials_provider_sts_web_identity_options options; + AWS_ZERO_STRUCT(options); + + options.bootstrap = bootstrap->GetUnderlyingHandle(); + options.tls_ctx = tlsContext ? tlsContext->GetUnderlyingHandle() : nullptr; + if (!profileName.empty()) { + options.profile_name_override = aws_byte_cursor_from_c_str(profileName.c_str()); + } + + return createWrappedProvider(aws_credentials_provider_new_sts_web_identity(allocator, &options), allocator); +} + static AwsCredentials getCredentialsFromProvider(std::shared_ptr provider) { if (!provider || !provider->IsValid()) { @@ -229,7 +254,8 @@ class AwsCredentialProviderImpl : public AwsCredentialProvider tlsContext = std::make_shared(tlsCtxOptions, Aws::Crt::Io::TlsMode::CLIENT, allocator); if (!tlsContext || !*tlsContext) { - warn("failed to create TLS context for AWS SSO; SSO authentication will be unavailable"); + warn( + "failed to create TLS context for AWS credential providers; SSO and STS WebIdentity authentication will be unavailable"); tlsContext = nullptr; } @@ -273,7 +299,7 @@ AwsCredentialProviderImpl::createProviderForProfile(const std::string & profile) debug("[pid=%d] creating new AWS credential provider for profile '%s'", getpid(), profileDisplayName); - // Build a custom credential chain: Environment → SSO → Profile → IMDS + // Build a custom credential chain: Environment → SSO → Profile → STS WebIdentity → IMDS // This works for both default and named profiles, ensuring consistent behavior // including SSO support and proper TLS context for STS-based role assumption. Aws::Crt::Auth::CredentialsProviderChainConfig chainConfig; @@ -311,7 +337,18 @@ AwsCredentialProviderImpl::createProviderForProfile(const std::string & profile) return Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderProfile(profileConfig, allocator); }); - // 4. IMDS provider (for EC2 instances, lowest priority) + // 4. STS WebIdentity (AWS_WEB_IDENTITY_TOKEN_FILE + AWS_ROLE_ARN — EKS IRSA, GitHub Actions OIDC) + if (tlsContext) { + addProviderToChain("STS WebIdentity", [&]() { + return createSTSWebIdentityProvider(profile, bootstrap, tlsContext.get(), allocator); + }); + } else { + debug( + "Skipped AWS STS WebIdentity Credential Provider for profile '%s': TLS context unavailable", + profileDisplayName); + } + + // 5. IMDS provider (for EC2 instances, lowest priority) addProviderToChain("IMDS", [&]() { Aws::Crt::Auth::CredentialsProviderImdsConfig imdsConfig; imdsConfig.Bootstrap = bootstrap; From ddae831bd31a5fe0e666413086f724de3aab8291 Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Costa Date: Tue, 17 Mar 2026 23:12:34 +0000 Subject: [PATCH 09/18] fix(libstore/aws-creds): add ECS container provider to credential chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same regression class as the preceding commit. The pre-2.33 aws-sdk-cpp credential chain included the ECS container metadata provider, which reads AWS_CONTAINER_CREDENTIALS_RELATIVE_URI (or _FULL_URI) and is used by ECS tasks and EKS Pod Identity. aws_credentials_provider_new_ecs_from_environment reads the relevant env vars and returns nullptr at construction time when none are set, so this slots into the chain with zero runtime cost when unused. Wired after STS WebIdentity and before IMDS, matching the pre-2.33 ordering. The full credential chain is now: Environment → SSO → Profile → STS WebIdentity → ECS → IMDS (cherry picked from commit eec6d92b291322d532b8706589e02b85bb98feec) --- src/libstore/aws-creds.cc | 65 ++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/src/libstore/aws-creds.cc b/src/libstore/aws-creds.cc index 495e8ae8fcfa..1fa84ac0e2cf 100644 --- a/src/libstore/aws-creds.cc +++ b/src/libstore/aws-creds.cc @@ -10,7 +10,7 @@ # include # include -// C library headers for SSO provider support +// C library headers for SSO, STS WebIdentity, and ECS credential providers # include // C library headers for custom logging @@ -195,6 +195,28 @@ static std::shared_ptr createSTSWebIdentit return createWrappedProvider(aws_credentials_provider_new_sts_web_identity(allocator, &options), allocator); } +/** + * Create an ECS container credentials provider using the C library directly. + * This reads AWS_CONTAINER_CREDENTIALS_RELATIVE_URI or + * AWS_CONTAINER_CREDENTIALS_FULL_URI (plus the optional + * AWS_CONTAINER_AUTHORIZATION_TOKEN / _TOKEN_FILE) from the environment. + * Used by ECS tasks and EKS Pod Identity. + * Returns nullptr if neither URI env var is set. + */ +static std::shared_ptr createECSProvider( + Aws::Crt::Io::ClientBootstrap * bootstrap, + Aws::Crt::Io::TlsContext * tlsContext, + Aws::Crt::Allocator * allocator = Aws::Crt::ApiAllocator()) +{ + aws_credentials_provider_ecs_environment_options options; + AWS_ZERO_STRUCT(options); + + options.bootstrap = bootstrap->GetUnderlyingHandle(); + options.tls_ctx = tlsContext ? tlsContext->GetUnderlyingHandle() : nullptr; + + return createWrappedProvider(aws_credentials_provider_new_ecs_from_environment(allocator, &options), allocator); +} + static AwsCredentials getCredentialsFromProvider(std::shared_ptr provider) { if (!provider || !provider->IsValid()) { @@ -248,14 +270,14 @@ class AwsCredentialProviderImpl : public AwsCredentialProvider // This ensures AWS logs respect Nix's verbosity settings and are formatted consistently. initialiseAwsLogger(); - // Create a shared TLS context for SSO (required for HTTPS connections) + // Create a shared TLS context for SSO, STS WebIdentity, and ECS providers (required for HTTPS) auto allocator = Aws::Crt::ApiAllocator(); auto tlsCtxOptions = Aws::Crt::Io::TlsContextOptions::InitDefaultClient(allocator); tlsContext = std::make_shared(tlsCtxOptions, Aws::Crt::Io::TlsMode::CLIENT, allocator); if (!tlsContext || !*tlsContext) { warn( - "failed to create TLS context for AWS credential providers; SSO and STS WebIdentity authentication will be unavailable"); + "failed to create TLS context for AWS credential providers; SSO, STS WebIdentity, and ECS container authentication will be unavailable"); tlsContext = nullptr; } @@ -299,19 +321,20 @@ AwsCredentialProviderImpl::createProviderForProfile(const std::string & profile) debug("[pid=%d] creating new AWS credential provider for profile '%s'", getpid(), profileDisplayName); - // Build a custom credential chain: Environment → SSO → Profile → STS WebIdentity → IMDS + // Build a custom credential chain: Environment → SSO → Profile → STS WebIdentity → ECS → IMDS // This works for both default and named profiles, ensuring consistent behavior // including SSO support and proper TLS context for STS-based role assumption. Aws::Crt::Auth::CredentialsProviderChainConfig chainConfig; auto allocator = Aws::Crt::ApiAllocator(); - auto addProviderToChain = [&](std::string_view name, auto createProvider) { + auto addProviderToChain = [&](std::string_view name, auto createProvider) -> bool { if (auto provider = createProvider()) { chainConfig.Providers.push_back(provider); debug("Added AWS %s Credential Provider to chain for profile '%s'", name, profileDisplayName); - } else { - debug("Skipped AWS %s Credential Provider for profile '%s'", name, profileDisplayName); + return true; } + debug("Skipped AWS %s Credential Provider for profile '%s'", name, profileDisplayName); + return false; }; // 1. Environment variables (highest priority) @@ -338,22 +361,36 @@ AwsCredentialProviderImpl::createProviderForProfile(const std::string & profile) }); // 4. STS WebIdentity (AWS_WEB_IDENTITY_TOKEN_FILE + AWS_ROLE_ARN — EKS IRSA, GitHub Actions OIDC) + // 5. ECS container metadata (AWS_CONTAINER_CREDENTIALS_RELATIVE_URI — ECS tasks, EKS Pod Identity) + // ECS and IMDS are mutually exclusive per both the aws-c-auth default chain and the + // pre-2.33 aws-sdk-cpp DefaultAWSCredentialsProviderChain: when container credential + // env vars are set, IMDS is skipped so a transient ECS endpoint failure can't silently + // fall through to the (typically broader) EC2 instance profile. + bool ecsAdded = false; if (tlsContext) { addProviderToChain("STS WebIdentity", [&]() { return createSTSWebIdentityProvider(profile, bootstrap, tlsContext.get(), allocator); }); + ecsAdded = + addProviderToChain("ECS", [&]() { return createECSProvider(bootstrap, tlsContext.get(), allocator); }); } else { debug( - "Skipped AWS STS WebIdentity Credential Provider for profile '%s': TLS context unavailable", + "Skipped AWS STS WebIdentity and ECS Credential Providers for profile '%s': TLS context unavailable", profileDisplayName); } - // 5. IMDS provider (for EC2 instances, lowest priority) - addProviderToChain("IMDS", [&]() { - Aws::Crt::Auth::CredentialsProviderImdsConfig imdsConfig; - imdsConfig.Bootstrap = bootstrap; - return Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderImds(imdsConfig, allocator); - }); + // 6. IMDS provider (for EC2 instances, lowest priority) — only if ECS didn't claim the slot + if (!ecsAdded) { + addProviderToChain("IMDS", [&]() { + Aws::Crt::Auth::CredentialsProviderImdsConfig imdsConfig; + imdsConfig.Bootstrap = bootstrap; + return Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderImds(imdsConfig, allocator); + }); + } else { + debug( + "Skipped AWS IMDS Credential Provider for profile '%s': ECS provider is active (mutually exclusive)", + profileDisplayName); + } return Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderChain(chainConfig, allocator); } From cb0e7fcb54251eaedac98692d5853d68be4a00e2 Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Costa Date: Tue, 17 Mar 2026 23:12:54 +0000 Subject: [PATCH 10/18] docs(rl-next): note restored S3 WebIdentity and ECS credential providers (cherry picked from commit ddd84dcaf80d078de98f095cf79c7c6b128f4e2d) --- .../s3-credential-chain-web-identity.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 doc/manual/rl-next/s3-credential-chain-web-identity.md diff --git a/doc/manual/rl-next/s3-credential-chain-web-identity.md b/doc/manual/rl-next/s3-credential-chain-web-identity.md new file mode 100644 index 000000000000..4dfece0e3b7d --- /dev/null +++ b/doc/manual/rl-next/s3-credential-chain-web-identity.md @@ -0,0 +1,26 @@ +--- +synopsis: "S3: restore STS WebIdentity and ECS container credential providers" +prs: [15507] +--- + +Nix 2.33 replaced the S3 backend's `aws-sdk-cpp` credential chain with a +custom chain built on `aws-c-auth`. That chain omitted two providers, +breaking S3 binary cache access in container workloads: + +- **STS WebIdentity** (`AWS_WEB_IDENTITY_TOKEN_FILE`, `AWS_ROLE_ARN`, + `AWS_ROLE_SESSION_NAME`) — used by EKS IRSA, GitHub Actions OIDC, and + any `sts:AssumeRoleWithWebIdentity` federation. +- **ECS container metadata** (`AWS_CONTAINER_CREDENTIALS_RELATIVE_URI`, + `AWS_CONTAINER_CREDENTIALS_FULL_URI`) — used by ECS tasks and EKS Pod + Identity. + +The typical symptom was a misleading IMDS error +(`Valid credentials could not be sourced by the IMDS provider`), because +IMDS is the last provider tried after the correct one was skipped. + +Both providers are now part of the chain, ordered to match the +pre-2.33 `DefaultAWSCredentialsProviderChain`: +`Environment → SSO → Profile → STS WebIdentity → (ECS | IMDS)`. +As in both the old and new AWS SDK default chains, ECS and IMDS are +mutually exclusive: when container credential environment variables are +set, IMDS is skipped. From d94e72ac45f650cc44ad6c8eb2ff5c715e4ad568 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 25 Mar 2026 06:33:09 -0700 Subject: [PATCH 11/18] Don't destroy windowSize mutex This fixes a a destructor ordering issue where during shutdown, ~ProgressBar() calls getWindowSize() after the windowSize object (and thus its mutex) has been destroyed. This happens when a draw() call happens to be active during destruction. Fixes https://github.com/DeterminateSystems/nix-src/issues/361, https://github.com/NixOS/nix/issues/14300, https://github.com/NixOS/nix/issues/14361. (cherry picked from commit cf44b7c49cf4d80cbb90b72a99c10142e4f9cd94) --- src/libutil/terminal.cc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libutil/terminal.cc b/src/libutil/terminal.cc index 401ce16043d1..48d692872712 100644 --- a/src/libutil/terminal.cc +++ b/src/libutil/terminal.cc @@ -161,14 +161,16 @@ std::string filterANSIEscapes(std::string_view s, bool filterAll, unsigned int w ////////////////////////////////////////////////////////////////////// -static Sync> windowSize{{0, 0}}; +// Note: this object intentionally leaks to avoid a destructor ordering issue (specifically, ~ProgressBar() calling +// getWindowSize() after windowSize has been destroyed). +static auto * const windowSize = new Sync>{{0, 0}}; void updateWindowSize() { #ifndef _WIN32 struct winsize ws; if (ioctl(2, TIOCGWINSZ, &ws) == 0) { - auto windowSize_(windowSize.lock()); + auto windowSize_(windowSize->lock()); windowSize_->first = ws.ws_row; windowSize_->second = ws.ws_col; } @@ -176,7 +178,7 @@ void updateWindowSize() CONSOLE_SCREEN_BUFFER_INFO info; // From https://stackoverflow.com/a/12642749 if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info) != 0) { - auto windowSize_(windowSize.lock()); + auto windowSize_(windowSize->lock()); // From https://github.com/libuv/libuv/blob/v1.48.0/src/win/tty.c#L1130 windowSize_->first = info.srWindow.Bottom - info.srWindow.Top + 1; windowSize_->second = info.dwSize.X; @@ -186,7 +188,7 @@ void updateWindowSize() std::pair getWindowSize() { - return *windowSize.lock(); + return *windowSize->lock(); } #ifndef _WIN32 From c4917eee5e1e6355abd9a4a861d7d209646c5501 Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Thu, 26 Mar 2026 12:02:21 +0300 Subject: [PATCH 12/18] packaging: Make binaryTarball more reproducible While debugging https://github.com/NixOS/nix/issues/15564 I noticed that our release tarball don't seem to be reproducible. This doesn't explain why hydra calculated the different hash of the tarball, which should only be built once (???). Regardless, I noticed that checking the build reproducibility against hydra built tarballs fails with e.g. nix build .\#hydraJobs.binaryTarball.x86_64-linux -L --keep-failed --rebuild Looking with diffoscope, it was the entry ordering issue. Let's nip this in the bud. (cherry picked from commit 4c41c5f4ad8135a21d9b49a92438408163f9a9ff) --- packaging/binary-tarball.nix | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packaging/binary-tarball.nix b/packaging/binary-tarball.nix index 86aae0ac524c..859cb6cd6d6e 100644 --- a/packaging/binary-tarball.nix +++ b/packaging/binary-tarball.nix @@ -74,7 +74,8 @@ runCommand "nix-binary-tarball-${version}" env '' fn=$out/$dir.tar.xz mkdir -p $out/nix-support echo "file binary-dist $fn" >> $out/nix-support/hydra-build-products - tar cfJ $fn \ + tar cf - \ + --sort=name \ --owner=0 --group=0 --mode=u+rw,uga+r \ --mtime='1970-01-01' \ --absolute-names \ @@ -90,5 +91,5 @@ runCommand "nix-binary-tarball-${version}" env '' $TMPDIR/install-freebsd-multi-user.sh \ $TMPDIR/install-multi-user \ $TMPDIR/reginfo \ - $(cat ${installerClosureInfo}/store-paths) + $(cat ${installerClosureInfo}/store-paths) | xz --threads=1 > $fn '' From ab2c750c58d5b88c97525b857f7330590e697184 Mon Sep 17 00:00:00 2001 From: dramforever Date: Thu, 26 Mar 2026 18:03:58 +0800 Subject: [PATCH 13/18] tests/functional/stale-file-handle: Skip if the error doesn't happen This error no longer seems to occur on Linux 6.19+ anymore. Skip in that case to fix build. (cherry picked from commit 2e6a03e264007df21c38be3270a4e06b27bf4599) --- .../local-overlay-store/stale-file-handle-inner.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/functional/local-overlay-store/stale-file-handle-inner.sh b/tests/functional/local-overlay-store/stale-file-handle-inner.sh index d38f00cdc1b5..2c42d984c1ca 100755 --- a/tests/functional/local-overlay-store/stale-file-handle-inner.sh +++ b/tests/functional/local-overlay-store/stale-file-handle-inner.sh @@ -36,8 +36,12 @@ triggerStaleFileHandle () { buildInStore "$storeB" } -# Without remounting, we should encounter errors -expectStderr 1 triggerStaleFileHandle | grepQuiet 'Stale file handle' +# Without remounting, we should encounter errors. However, this doesn't seem to +# happen on Linux 6.19+ anymore. +# +# See https://github.com/NixOS/nixpkgs/issues/496466 +( expectStderr 1 triggerStaleFileHandle | grepQuiet 'Stale file handle' ) || \ + skipTest "Couldn't trigger the error" # Configure remount-hook and reset OverlayFS storeB="$storeB&remount-hook=$PWD/remount.sh" From 2c5ce4cb96e8dc20b866aa40fa680e4c09e2bb8a Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Fri, 3 Apr 2026 00:21:21 +0300 Subject: [PATCH 14/18] libstore: Make temporary in-store directory not world-readable (cherry picked from commit 0e3412a93f43a017342c267000c152e5c45327e7) --- src/libstore/local-store.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index b625b6c1bf2a..216a3376d89d 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1330,8 +1330,9 @@ std::pair LocalStore::createTempDirInStore() do { /* There is a slight possibility that `tmpDir' gets deleted by the GC between createTempDir() and when we acquire a lock on it. - We'll repeat until 'tmpDir' exists and we've locked it. */ - tmpDirFn = createTempDir(std::filesystem::path{config->realStoreDir.get()}, "tmp"); + We'll repeat until 'tmpDir' exists and we've locked it. + Make the directory accessible only to the current user. */ + tmpDirFn = createTempDir(std::filesystem::path{config->realStoreDir.get()}, "tmp", /*mode=*/0700); tmpDirFd = openDirectory(tmpDirFn); if (!tmpDirFd) { continue; From e7bf1cdda30f2856fc8a0a81f4722bc4ef6f3002 Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Fri, 3 Apr 2026 00:21:31 +0300 Subject: [PATCH 15/18] derivation-builder: Don't use copyFile for FOD output copying, put the output in a temporary directory in the store Puts the temporary FOD output copies in a temporary directory inside the store instead of the (for Linux sandboxed builds) chroot. This prevents file overwrite due to symlink following that std::filesystem::copy_file does. Also applies the same output copying approach for impure derivations that don't have network sandboxing and thus are subject to FD smuggling. Fixes GHSA-g3g9-5vj6-r3gj. (cherry picked from commit a760af86b3a42aa5ac9d9002929107fe357bf128) --- src/libstore/include/nix/store/local-store.hh | 2 ++ src/libstore/unix/build/derivation-builder.cc | 36 ++++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh index 7d93d7045f46..f5164252eb28 100644 --- a/src/libstore/include/nix/store/local-store.hh +++ b/src/libstore/include/nix/store/local-store.hh @@ -459,6 +459,8 @@ private: friend struct PathSubstitutionGoal; friend struct DerivationGoal; + /* Only used for createTempDirInStore. */ + friend class DerivationBuilderImpl; }; } // namespace nix diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc index 360b6a484871..6622ae8fde4b 100644 --- a/src/libstore/unix/build/derivation-builder.cc +++ b/src/libstore/unix/build/derivation-builder.cc @@ -1537,6 +1537,13 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() assert(output && scratchPath); auto actualPath = realPathInSandbox(store.printStorePath(*scratchPath)); + /* An optional file descriptor of a directory used for intermediate + operations. */ + AutoCloseFD tempDirFd; + /* RAII cleanup of a temporary directory inside the store that is used + for intermediate operations. */ + std::optional delTempDir; + auto finish = [&](StorePath finalStorePath) { /* Store the final path */ finalOutputs.insert_or_assign(outputName, finalStorePath); @@ -1672,6 +1679,25 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() return newInfo0; }; + auto moveOutputToTempDir = [&]() -> void { + std::filesystem::path tempDir; + std::tie(tempDir, tempDirFd) = store.createTempDirInStore(); + delTempDir.emplace(tempDir); + + auto tmpOutput = tempDir / "x"; + + /* Serialise and create a fresh copy of the output to break + any stale writable file descriptors. Copy through the + serialisation/deserialisation. TODO: Use copyRecursive here and + make use of reflinking. */ + auto source = sinkToSource([&](Sink & nextSink) { dumpPath(actualPath, nextSink); }); + restorePath(tmpOutput, *source, settings.fsyncStorePaths); + /* This makes it slightly harder to make sense of the control flow. The rule + of thumb is that actualPath points to the current location of the stuff + that we'll end up registering. */ + actualPath = std::move(tmpOutput); + }; + ValidPathInfo newInfo = std::visit( overloaded{ @@ -1699,14 +1725,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() [&](const DerivationOutput::CAFixed & dof) { auto & wanted = dof.ca.hash; - - // Replace the output by a fresh copy of itself to make sure - // that there's no stale file descriptor pointing to it - Path tmpOutput = actualPath + ".tmp"; - copyFile(std::filesystem::path(actualPath), std::filesystem::path(tmpOutput), true); - - std::filesystem::rename(tmpOutput, actualPath); - + moveOutputToTempDir(); return newInfoFromCA( DerivationOutput::CAFloating{ .method = dof.ca.method, @@ -1723,6 +1742,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() }, [&](const DerivationOutput::Impure & doi) { + moveOutputToTempDir(); return newInfoFromCA( DerivationOutput::CAFloating{ .method = doi.method, From e4491cf47709d0c944637ed886abdfb95ed1593b Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Sun, 5 Apr 2026 16:39:58 +0300 Subject: [PATCH 16/18] libstore: Use landlock with LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET for new enough kernels This partially fixes the issue with cooperating processes being able to communicate via abstract sockets. The fix is partial, because processes outside the landlock domain of the sandboxed process can still connect to a socket created by the FOD. There's no equivalent way of restricting inbound connections. This closes the gap when there's no cooperating process on the host (i.e. 2 separate FODs). >= 6.12 kernel is widespread enough (NixOS 25.11 ships it by default) that we have no reason not to apply this hardening, even though it's incomplete. ca-fd-leak test exercises this exact code path and now the smuggling process fails with (on new enough kernels that have landlock support enabled): vm-test-run-ca-fd-leak> machine # sandbox setup: applied landlock sandboxing vm-test-run-ca-fd-leak> machine # building '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv'... vm-test-run-ca-fd-leak> machine # building derivation '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv': woken up vm-test-run-ca-fd-leak> machine # connect: Operation not permitted vm-test-run-ca-fd-leak> machine # sendmsg: Socket not connected (cherry picked from commit 44017ca497c8b44d5dac179f5afc63e91fe45ed6) --- src/libstore/meson.build | 5 + .../unix/build/linux-derivation-builder.cc | 103 ++++++++++++++++++ tests/nixos/ca-fd-leak/default.nix | 8 +- 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/libstore/meson.build b/src/libstore/meson.build index 088d8bcbb591..24f9453b82aa 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -77,6 +77,11 @@ foreach funcspec : check_funcs configdata_priv.set(define_name, define_value) endforeach +if host_machine.system() == 'linux' + has_landlock = cxx.has_header('linux/landlock.h') + configdata_priv.set('HAVE_LANDLOCK', has_landlock.to_int()) +endif + has_acl_support = cxx.has_header('sys/xattr.h') \ and cxx.has_function('llistxattr') \ and cxx.has_function('lremovexattr') diff --git a/src/libstore/unix/build/linux-derivation-builder.cc b/src/libstore/unix/build/linux-derivation-builder.cc index d15e6e1ae7a3..1249b6251355 100644 --- a/src/libstore/unix/build/linux-derivation-builder.cc +++ b/src/libstore/unix/build/linux-derivation-builder.cc @@ -1,11 +1,17 @@ #ifdef __linux__ +# include "store-config-private.hh" + # include "nix/store/personality.hh" # include "nix/util/cgroup.hh" # include "nix/util/linux-namespaces.hh" # include "nix/util/logging.hh" # include "linux/fchmodat2-compat.hh" +# include +# include +# include + # include # include # include @@ -14,11 +20,16 @@ # include # include # include +# include # if HAVE_SECCOMP # include # endif +# if HAVE_LANDLOCK +# include +# endif + # define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) namespace nix { @@ -122,6 +133,77 @@ static void setupSeccomp() # endif } +# if HAVE_LANDLOCK && defined(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET) + +# define DO_LANDLOCK 1 + +/* We are using LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET on best-effort basis. There are no glibc wrappers for now. */ + +static int landlockCreateRuleset(const ::landlock_ruleset_attr * attr, std::size_t size, std::uint32_t flags) +{ + return ::syscall(__NR_landlock_create_ruleset, attr, size, flags); +} + +static int landlockRestrictSelf(Descriptor rulesetFd, std::uint32_t flags) +{ + return ::syscall(__NR_landlock_restrict_self, rulesetFd, flags); +} + +static int getLandlockAbiVersion() +{ + int abiVersion = landlockCreateRuleset(nullptr, 0, LANDLOCK_CREATE_RULESET_VERSION); + return abiVersion; +} + +static void setupLandlock() +{ + bool landlockSupportsScopeAbstractUnixSocket = []() { + int abiVersion = getLandlockAbiVersion(); + if (abiVersion >= 6) + /* All good, we can use LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET. See + https://docs.kernel.org/userspace-api/landlock.html#abstract-unix-socket-abi-6 */ + return true; + + if (abiVersion == -1) { + debug("landlock is not available"); + return false; + } + + debug("landlock version %d does not support LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", abiVersion); + return false; + }(); + + /* Bail out early if landlock is not enabled or LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET wouldn't work. + TODO: Consider adding more landlock rules for filesystem access as defense-in-depth on top. */ + if (!landlockSupportsScopeAbstractUnixSocket) + return; + + ::landlock_ruleset_attr attr = { + /* This prevents multiple FODs from communicating with each other + via abstract sockets. Note that cooperating processes outside the + sandbox can still connect to an abstract socket created by the FOD. To + mitigate that issue entirely we'd still need network namespaces. */ + .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, + }; + + /* This better not fail - if the kernel reports a new enough ABI version we + should treat any errors as fatal from now on. */ + AutoCloseFD rulesetFd = landlockCreateRuleset(&attr, sizeof(attr), 0); + if (!rulesetFd) + throw SysError("failed to create a landlock ruleset"); + + if (landlockRestrictSelf(rulesetFd.get(), 0) == -1) + throw SysError("failed to apply landlock"); + + debug("applied landlock sandboxing"); +} + +# else + +# define DO_LANDLOCK 0 + +# endif + static void doBind(const Path & source, const Path & target, bool optional = false) { debug("bind mounting '%1%' to '%2%'", source, target); @@ -160,8 +242,27 @@ struct LinuxDerivationBuilder : virtual DerivationBuilderImpl void enterChroot() override { + /* Set the NO_NEW_PRIVS before doing seccomp/landlock setup. + landlock_restrict_self requires either NO_NEW_PRIVS or CAP_SYS_ADMIN. + With user namespaces we do get CAP_SYS_ADMIN. */ + if (!settings.allowNewPrivileges) + if (::prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) + throw SysError("failed to set PR_SET_NO_NEW_PRIVS"); + setupSeccomp(); +# if DO_LANDLOCK + try { + setupLandlock(); + } catch (SysError & e) { + if (e.errNo != EPERM) + throw; + /* If allowNewPrivileges is true and we don't have CAP_SYS_ADMIN + this code path might be hit. */ + warn("setting up landlock: %s", e.message()); + } +# endif + linux::setPersonality(drv.platform); } }; @@ -737,4 +838,6 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu } // namespace nix +# undef DO_LANDLOCK + #endif diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix index 902aacdc650f..dc944290f7e2 100644 --- a/tests/nixos/ca-fd-leak/default.nix +++ b/tests/nixos/ca-fd-leak/default.nix @@ -78,7 +78,7 @@ in # Build the smuggled derivation. # This will connect to the smuggler server and send it the file descriptor - machine.succeed(r""" + sender_output = machine.succeed(r""" nix-build -E ' builtins.derivation { name = "smuggled"; @@ -89,9 +89,13 @@ in outputHash = builtins.hashString "sha256" "hello, world\n"; builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ]; - }' + }' 2>&1 """.strip()) + # Landlock's LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET prevents a sandboxed process + # from connecting to an abstract socket created in an unrelated landlock domain. + # There's no such flag for preventing inbound connections. + assert "connect: Operation not permitted" in sender_output # Tell the smuggler server that we're done machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}") From 7d8efbcfe12bc7bb1a11c7462be6bd2446ebeff2 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 23 Apr 2026 17:45:53 +0200 Subject: [PATCH 17/18] sentry-native: Enable separate debug info --- packaging/sentry-native.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packaging/sentry-native.nix b/packaging/sentry-native.nix index 06accdd5b18a..0e21e9be55c2 100644 --- a/packaging/sentry-native.nix +++ b/packaging/sentry-native.nix @@ -49,4 +49,6 @@ stdenv.mkDerivation rec { "out" "dev" ]; + + separateDebugInfo = true; } From dd0920f1326390581c5df1839b668180b0641233 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 23 Apr 2026 17:55:34 +0200 Subject: [PATCH 18/18] upload-debug-info-to-sentry.py: Upload .so files without separate debug info --- maintainers/upload-debug-info-to-sentry.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/maintainers/upload-debug-info-to-sentry.py b/maintainers/upload-debug-info-to-sentry.py index e1c242fd2fee..4e315c65e872 100755 --- a/maintainers/upload-debug-info-to-sentry.py +++ b/maintainers/upload-debug-info-to-sentry.py @@ -130,7 +130,8 @@ def main(): for lib in libs: build_id = get_build_id(lib) if build_id is None: - print(f" {lib} (no build ID)", file=sys.stderr) + print(f" {lib} (no build ID, uploading binary)", file=sys.stderr) + debug_files.append(lib) continue local = find_debug_file_in_dirs(build_id, args.debug_dir) @@ -141,7 +142,8 @@ def main(): debuginfo = fetch_debuginfo(build_id) if debuginfo is None: - print(f" {lib} ({build_id}, no debug info in cache)", file=sys.stderr) + print(f" {lib} ({build_id}): no separate debug info, uploading binary", file=sys.stderr) + debug_files.append(lib) continue print(f" {lib} ({build_id}): member={debuginfo['member']}", file=sys.stderr) nar_path = download_nar(build_id, debuginfo["archive"])