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 Goos/userpackage built without cgo (which is how Mutant ships) answers from$HOMEfor 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#
- The entry in CHANGELOG.md under 2.6.0, Security.
- SECURITY.md for the severity scale.
- The advisory register.