MVF-2026-0009 — an inherited environment variable chose the signing key and the trust anchor

The local keystore directory was built from the process's idea of the home directory, which on Unix is $HOME and on Windows is %USERPROFILE% — a variable inherited from whatever launched Mutant and…

| Identifier | MVF-2026-0009 | | Internal row | M26-DOC3-001 | | Severity | S3, priority P1 | | Affected | 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0 | | Not affected | 1.0.0, 1.0.1, 2.0.0, 2.0.1 — the keystore and the signing key pair do not exist in them | | Fixed in | 2.6.0 | | Reported | internally; Stage B3 audit, confirmed 2026-09-25 | | Published | 2026-10-05 | | CVE / GHSA | none assigned | | CVSS:3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N | | CVSS:4.0 | CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N |

Summary#

The local keystore directory was built from the process's idea of the home directory, which on Unix is $HOME and on Windows is %USERPROFILE% — a variable inherited from whatever launched Mutant and recorded nowhere in the invocation. That directory supplies the private key artefacts are signed with and, when no key is named on the command line, the public key used as the verification trust anchor. So one recorded command line could sign with one key or another, and could accept or refuse the same artefact, depending on a variable that the case record does not contain.

This project's standing rule is that no configuration comes from an environment variable. This was a breach of that rule which the rule's own machine guard did not catch, because the guard looked for direct environment reads and this was a library call that performs one.

Impact, scoped to what actually shipped#

This is the part of the page to read carefully, because the internal finding overstated it. The internal row listed five places the environment-chosen key is used. Three of those files exist only on the development branch and are in no released version: the ledger commit path, the record file writer and the record crypto layer. Checked at the tag, git cat-file -e v2.5.0: fails for all three.

On released software the environment chose:

release what the environment-chosen key signed the default trust anchor
2.1.0 – 2.4.0 a compiled .mu artefact yes
2.5.0 a compiled .mu artefact, and a custody seal / case manifest yes

It did not choose a key that signed a ledger commit or a .mrec evidence record in any release, because neither exists in one. Telling examiners that their archived evidence records were signed under an environment-chosen key would send them to re-open case material that cannot carry this defect, which is the worst error available on this page.

This is not a privilege boundary, and the advisory does not claim one. Setting a variable in the examiner's launch environment presupposes control of that environment, where PATH is a stronger lever. What was lost is reproducibility and a stated configuration guarantee: the invocation does not record what it depended on.

Details#

ResolveLocalKeyStoreDir joined the OS home-directory lookup with .mutant/keys. The standard library's lookup returns $HOME on Unix and %USERPROFILE% on Windows. The same directory holds the signing key pair the generator uses, and the public half is handed to the runner as the trust anchor when no key is named with a flag.

The policy guard missed it because it matched direct environment reads, and this was a library wrapper around the same read — which is the general lesson, and why the fix widened the guard rather than only changing the one call.

The fix#

The one call is replaced with a per-platform accountHomeDir() that asks the operating system's account database instead of the environment:

  • Windows — the current process token's profile directory, which resolves the token's SID against the registry profile list rather than reading %USERPROFILE%.
  • macOS — the account lookup through libc.
  • Linux — a direct read of /etc/passwd, because the Go os/user package built without cgo (which is how Mutant ships) answers from $HOME for a uid the file does not list, and returns that same answer for the caller's own uid. Going through it would have reintroduced the defect behind a different name.

A new check refuses an empty or non-absolute home rather than rooting the keystore under whatever directory Mutant happened to start in, and an account the database cannot answer for is refused instead of guessed at.

The guard was widened in the same change to flag the library wrappers — home, cache, config and temp directory lookups, the temp-file helpers, and the account lookups in files that build for Linux without cgo — with named allowlist categories for the legitimate readers, so that a future call of this shape is caught at the gate rather than in an audit.

Fixture#

security/key_bootstrap_test.go, TestTheKeystoreDoesNotFollowTheEnvironment; security/account_home_test.go, TestThePasswdFileIsReadAsOsUserReadsIt and TestAnAccountWithoutAnAbsoluteHomeIsRefused; and policy/env_guard_test.go, TestTheGuardSeesLibraryCallsThatReadTheEnvironment and TestTheUserLookupRuleFollowsTheLibraryItNames. The last pair is the half that stops the defect returning in a new wrapper.

Workarounds#

Partial on 2.5.0; none on 2.1.0 – 2.4.0.

On 2.5.0 only, --trusted-key <path> names the verification anchor on the command line, which takes the environment out of the verify decision entirely. It does not cover signing: at 2.5.0 both the generator and the custody seal ask for the signing key pair with no path argument, and there is no --signing-key or --keystore flag at any tag, so which key signs a .mu artefact or a custody seal still comes from the environment even with --trusted-key given.

On 2.1.0 – 2.4.0 there is no command-line lever at all.

The only remaining measure on any affected release is to pin HOME (Unix) or USERPROFILE (Windows) explicitly in the launch wrapper and record that value beside the command line. State plainly what that does not do: it makes the dependency deliberate but still not visible in the invocation, so a case file holding only the command line remains incomplete — which is the exact promise the defect breaks.

On 2.1.0 alone there is an environment variable that pins the verify anchor. It is not recommended: it is itself an environment variable, it still does not appear in the recorded command line, and it puts key material in the environment, where every child process inherits it.

Upgrading to 2.6.0 is the only complete remedy. There is no patch release for the 2.5 line.

What changes for programs that worked before#

A keystore that was being found through $HOME or %USERPROFILE% is now found through the account database. Where those agree — the ordinary case — nothing changes. Where they disagree, Mutant now uses the account's real home, so a wrapper that set HOME to redirect the keystore no longer redirects it, and a key pair kept under the redirected path will not be found. An account whose home the database cannot answer for, or whose home is not absolute, is now refused rather than silently rooted somewhere else.

Credit#

Found by this project's own 2.6.0 security review, Stage B3, on 2026-09-25, and confirmed in Stage B4 the same day. No external reporter.

References#

Related reading