Skip to content

Drop support for the Elixir config format - #1230

Merged
ericmj merged 2 commits into
mainfrom
drop-elixir-config-format
Aug 24, 2026
Merged

Drop support for the Elixir config format#1230
ericmj merged 2 commits into
mainfrom
drop-elixir-config-format

Conversation

@ericmj

@ericmj ericmj commented Aug 23, 2026

Copy link
Copy Markdown
Member

Hex.Config.do_read/0 fell back to Code.eval_string/1 when hex.config failed Erlang term decoding, so an unreadable config was evaluated as Elixir source. That file holds the API key and the OAuth tokens.

The fallback dates to v0.5.0 (2014-09-19), which switched the write format to Erlang terms and kept an Elixir reader for existing files. v0.13.0 removed the reader and v0.13.1 reverted that, so the fallback has been in place continuously since 2014. It never converted anything on read, but update/1 is read-modify-write against the term writer, so any config change (mix hex.user auth, adding an organization, anything touching $repos) has rewritten the file in the term format for the last ten years. A config still in the Elixir format is one no hex has written in that time.

The branch was also reached by any term decode failure, not only old-format files, so a truncated or corrupt config took the eval path too. It now raises and names the recovery step:

Could not read ~/.hex/hex.config, remove the file and run `mix hex.user auth` to authenticate again

The second commit closes the other half of the same read path. :io.read/2 answers a bare :error atom rather than an {:error, reason} tuple when a term is syntactically valid but cannot be read back, and consult/3 had no clause for it, so the whole of mix raised CaseClauseError on every invocation until the file was deleted by hand. Hex.State.start_link/1 calls Hex.Config.read/0, so there was no way to run the command that would have fixed it.

A non-UTF-8 organization name in sso_reauth_required gets there, which needs a hostile HEX_API_URL since hexpm validates organization names, but the same exposure already applied to access_token and refresh_token. It now raises the same Mix.raise as any other unreadable config.

ericmj added 2 commits August 24, 2026 00:10
hex.config has been written as Erlang terms since v0.5.0 in 2014, and
update/1 is read-modify-write, so every config change since then has
rewritten the file in the new format. A config still in the Elixir
format is one no hex has written in about ten years.

Reading it went through Code.eval_string, which evaluates whatever the
file contains, so a term decode failure was a code execution path into
the file that holds the API key and the OAuth tokens. That branch caught
a truncated or corrupt config too, not only an old-format one. An
unreadable config now raises and names the way out.
:io.read/2 answers a bare :error atom, not {:error, reason}, when a term
decodes structurally but holds a binary that is not valid UTF-8. consult/3
matched neither, so read/0 raised CaseClauseError from Hex.State.start_link/1
and every later mix invocation failed until the file was deleted by hand.

A server-supplied value reaches the config this way: hex stores the
organization names from sso_reauth_required, and hex_core admits any binary.
@ericmj
ericmj marked this pull request as ready for review August 24, 2026 16:57
@ericmj
ericmj merged commit 9a9be7a into main Aug 24, 2026
22 checks passed
@ericmj
ericmj deleted the drop-elixir-config-format branch August 24, 2026 17:47
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.

1 participant