Skip to content

libstore: read SSL_CERT_FILE after main, not in a static initializer - #16383

Open
awsmadi wants to merge 2 commits into
NixOS:masterfrom
awsmadi:pr/ssl-cert-file-out-of-static-init
Open

libstore: read SSL_CERT_FILE after main, not in a static initializer#16383
awsmadi wants to merge 2 commits into
NixOS:masterfrom
awsmadi:pr/ssl-cert-file-out-of-static-init

Conversation

@awsmadi

@awsmadi awsmadi commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Follow-up to xokdvium's review comment on the merged #16364, which
asked whether we could avoid running this
in a static initializer rather than defending against it. That's the right call, and this does it.

NIX_SSL_CERT_FILE / SSL_CERT_FILE were read in FileTransferSettings' constructor. That object is a
global, so the read ran during static initialization, where a bad value cannot be reported: on Unix an
escaping exception reaches std::terminate before main, and on Windows the loader absorbs it and the
process dies having printed nothing at all (#16356). #16364 worked around that by catching and warning.

Every other config source is converted post-main through Setting::set(), so this one is now applied
there too — in loadConfFile(), next to the existing NIX_CONFIG handling. The constructor goes away
entirely, along with its try/catch.

Behaviour

A non-absolute value is an ordinary reportable error again, which is what the trace text added in #16364
said should eventually happen:

before:  warning: … while applying the 'NIX_SSL_CERT_FILE' or 'SSL_CERT_FILE' environment
                  variable; ignoring it for now, but this may become an error again in the future
after:   error: not an absolute path: "relative/ca.crt"

Everything else is unchanged. Measured with nix config show ssl-cert-file against master:

case                        this branch                        master
env only, absolute          /etc/from-env.crt                  same
no env (default)            /etc/ssl/certs/ca-certificates.crt same
ssl-cert-file in nix.conf   /etc/from-nix-conf.crt             same
  + env var set             (nix.conf wins)                    same

The override is applied before the config files so that nix.conf keeps the precedence it had when the
constructor assigned during static initialization — verified above rather than assumed.

One behaviour change worth flagging

Callers using initLibStore(false) no longer get the override, where previously it applied
unconditionally because it happened in a constructor. That seems consistent with asking not to load config,
but it is a change.

Verified nix build .#checks.x86_64-linux.pre-commit (7/7 hooks, no diff) and that both nix-store and
nix-store-x86_64-w64-mingw32 compile.

Follow-up to review feedback on NixOS#16364 from xokdvium: that change caught the
symptom rather than removing it.

NIX_SSL_CERT_FILE / SSL_CERT_FILE were read in FileTransferSettings'
constructor, and that object is a global, so the read ran during static
initialization. A bad value could not be reported there: on Unix an escaping
exception reaches std::terminate before main, and on Windows the loader absorbs
it and the process dies having printed nothing at all (NixOS#16356). NixOS#16364 worked
around that by catching and warning.

Every other config source is converted post-main through Setting::set(), so
apply this one there too, in loadConfFile() next to the existing NIX_CONFIG
handling. A non-absolute path is then an ordinary reportable error again rather
than a startup warning, which is what the trace text added in NixOS#16364 said should
eventually happen, and the constructor's try/catch is no longer needed.

Applied before the config files, so an ssl-cert-file in nix.conf keeps the
precedence it had when the constructor assigned during static initialization.

Note that callers using initLibStore(false) no longer get the override, where
before it applied unconditionally.

Assisted-by: Claude Code (claude-opus-5)
@awsmadi
awsmadi requested a review from Ericson2314 as a code owner August 28, 2026 16:28

@Ericson2314 Ericson2314 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hehe @xokdvium I asked it not to move this code before, because I didn't like where it moved it, but I like this new location better. Let's ship it

Comment thread src/libstore/globals.cc Outdated
Comment on lines +132 to +134
if (auto sslCertFile = getEnvOsNonEmpty(OS_STR("NIX_SSL_CERT_FILE")).or_else([] {
return getEnvOsNonEmpty(OS_STR("SSL_CERT_FILE"));
}))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually agent, the way you did *NonEmpty changes the semantics subtly. We do not want to fall back to the next variable if it is set, even if it is empty.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, and it's a real regression rather than just a wording thing. getEnvOsNonEmpty returns {}
for set-but-empty, so it is indistinguishable from unset and or_else falls through. The original chain did
not: or_else saw NIX_SSL_CERT_FILE as present, so SSL_CERT_FILE was never consulted, and the empty
check afterwards turned it into "no override" — an empty NIX_SSL_CERT_FILE suppresses the fallback.

Restored getEnvOs with the empty check after the fallback, so it is the original chain relocated rather
than a rewrite. Verified against master: NIX_SSL_CERT_FILE= with SSL_CERT_FILE set gives the built-in
default on both, where my previous version picked up SSL_CERT_FILE.

@Ericson2314 Ericson2314 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh did find an issue

Comment thread src/libstore/filetransfer.cc
Both from Ericson2314's review.

getEnvOsNonEmpty reports set-but-empty and unset identically, so
getEnvOsNonEmpty(NIX_SSL_CERT_FILE).or_else(getEnvOsNonEmpty(SSL_CERT_FILE))
fell through to SSL_CERT_FILE when NIX_SSL_CERT_FILE was set to the empty
string. The original chain did not: or_else saw the variable as present, so the
empty check afterwards yielded "no override" and SSL_CERT_FILE stayed
suppressed. Restored getEnvOs with the empty check after the fallback.

Also restored the addTrace, as add-trace-and-rethrow rather than the
add-trace-and-warn it was in NixOS#16364, so an invalid value still says which
environment variable it came from:

    error: … while applying the 'NIX_SSL_CERT_FILE' or 'SSL_CERT_FILE'
             environment variable
    error: not an absolute path: "rel/ca.crt"

Measured against master: empty-suppresses-fallback, both-unset, and
NIX_-unset-SSL_-set all agree; only the invalid-value case differs, which is the
point of the change.

Assisted-by: Claude Code (claude-opus-5)
@awsmadi

awsmadi commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

The red eval here is infrastructure, not this change — a re-run should clear it, and I don't have rights to
trigger one.

It fails in install-nix-action's dogfood step, which picks the newest successful master ci.yml run and
downloads its installer artifact:

Download nix install artifact from master
  no valid artifacts found to download
  ##[error]Process completed with exit code 1

The artifact it wanted is present and unexpired — run 33135116639 (master df1878b6d) has installer-linux,
59 MB, expiring 2026-11-26 — and that is still the run the step would select, so the download itself flaked
rather than the selection being wrong. aggregate basic checks is just cascading from it.

For what it's worth the previous head of this PR, 907c459aa, passed that same step 58 minutes earlier with
identical CI config, and the step compiles nothing, so globals.cc can't reach it.

Update — discriminated rather than assumed. Merge-queue run 33195884177, created 15 minutes after this
one on a different commit, ran the same eval job through the same install-nix-action step and passed
(eval: success, aggregate basic checks: success). The artifact also downloads fine on request now. So the
step was momentarily unable to fetch an artifact that exists — transient, and a re-run of 33194720856 should
be all it needs. I don't have rights to trigger that.

@awsmadi

awsmadi commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

@Ericson2314 both points from your review are already in 386984208, which I pushed about twenty minutes after you left them -- I think the CHANGES_REQUESTED state just predates it.

The *NonEmpty semantics. You were right and it was a real regression, not a wording issue. getEnvOsNonEmpty returns {} for set-but-empty, so it is indistinguishable from unset and or_else falls through to SSL_CERT_FILE -- which is exactly the behaviour you said we do not want. It now reads:

if (auto sslCertFile = getEnvOs(OS_STR("NIX_SSL_CERT_FILE"))
                           .or_else([] { return getEnvOs(OS_STR("SSL_CERT_FILE")); })
                           .and_then([](OsString s) -> std::optional<OsString> {
                               return s.empty() ? std::nullopt : std::optional{std::move(s)};
                           })) {

getEnvOs rather than getEnvOsNonEmpty, with the empty check after the fallback, so setting NIX_SSL_CERT_FILE= suppresses SSL_CERT_FILE instead of falling through to it. The reasoning is in a comment above it so the next reader does not re-introduce the shorter spelling.

The addTrace. Restored as add-trace-and-rethrow rather than add-trace-and-warn, and I dropped the "ignoring it for now" clause since it no longer is.

On the red CI, which is not this change. The only failing run on this head is 33194720856, and the job that failed is eval, at the step Run ./.github/actions/install-nix-action:

no valid artifacts found to download
##[error]Process completed with exit code 1.
##[end-action id=__self.download-nix-installer;outcome=failure;conclusion=failure

That action downloads a prebuilt installer and compiles nothing, so no content of globals.cc can reach it. aggregate basic checks then failed only by cascading from it ("Exit with any errors"), and every other check reports SKIPPED because eval gates the matrix -- so nothing in this PR has actually been exercised on this head yet.

For what it is worth the same step passed on merge-queue run 33195884177 fifteen minutes later under identical CI config, which says the step is flaky rather than broken. I should be straight that this is weaker evidence than it looks: that run's head is 7f711bef5 and does not contain 386984208, so it does not demonstrate that this change passes -- only that the step does. The load-bearing argument is the one above, that the step runs before anything is built.

I do not have re-run rights on this repo. A re-run of 33194720856 is all this needs; if it would be easier I am happy to push an empty commit to retrigger instead, just say which you prefer.

@awsmadi

awsmadi commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

The red eval here is still the four-day-old attempt 1 from 2026-08-28, and it is not reachable from this branch's code. I traced the mechanism rather than just asserting it was infrastructure: the failing step is install-nix-action, which under dogfood: true resolves the installer by running gh run list --repo NixOS/nix --workflow ci.yml --branch master --status success and then downloading that run's installer-linux artifact. So it installs a Nix built from upstream master, never one built from this PR — this branch's SSL_CERT_FILE change is not compiled at the point where the job fails.

That also means the failure is not specific to me: once the newest successful master run ages past the artifact retention window, gh run download has nothing to fetch and the step fails for any PR until master produces a fresh one. aggregate basic checks is red only because it aggregates eval, so there is one root failure here, not two.

A re-run should clear it and I can't trigger one. The two review points are already in 386984208.

@awsmadi
awsmadi force-pushed the pr/ssl-cert-file-out-of-static-init branch from 3869842 to 4cac5cc Compare September 2, 2026 17:05
@Ericson2314

Copy link
Copy Markdown
Member

@awsmadi Good new this is passing CI now. Can you rebase and squash down the fixup?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants