Skip to content

fix(config): resolve trust hash collision for same-name directories#8628

Merged
jdx merged 1 commit intojdx:mainfrom
tdragon:fix/trust-hash-collision
Mar 19, 2026
Merged

fix(config): resolve trust hash collision for same-name directories#8628
jdx merged 1 commit intojdx:mainfrom
tdragon:fix/trust-hash-collision

Conversation

@tdragon
Copy link
Copy Markdown
Contributor

@tdragon tdragon commented Mar 17, 2026

Summary

  • PathBuf::with_extension("hash") replaces everything after the last . in the trust filename, but hashed filenames contain dots (e.g. infra-mise.toml-a1b2c3d4)
  • This causes all configs sharing the same parent directory leaf name to map to a single hash file (e.g. both /projectA/infra/mise.toml and /projectB/infra/mise.toml produce infra-mise.hash), silently breaking paranoid mode trust verification
  • Same bug affects monorepo marker files (.monorepo extension)
  • Replace with_extension() with a helper that appends the extension to the full filename instead of replacing it
  • Fix untrust() to also remove .hash and .monorepo sidecar files, preventing stale trust state in paranoid mode

Related: #4499

Note: Existing .hash and .monorepo files from prior versions use a different filename convention and will be silently ignored after upgrade. Previously trusted configs will need a one-time mise trust after upgrading. This is acceptable since the old files were unreliable due to the collision bug.

Test plan

  • Reproduced the collision with a standalone Rust script confirming with_extension produces identical paths for different configs
  • Added unit test for with_appended_extension helper
  • cargo test passes (532 tests)
  • cargo clippy clean
  • CI passes

🤖 Generated with Claude Code

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a subtle but critical bug in how trust hash and monorepo marker files were named, which could lead to hash collisions and compromise the paranoid mode's trust verification. The issue stemmed from PathBuf::with_extension incorrectly parsing filenames that already contained dots. The solution introduces a dedicated helper function to ensure extensions are always appended, thereby preventing filename conflicts and restoring the integrity of the trust system.

Highlights

  • Bug Fix: Resolved a critical bug where PathBuf::with_extension caused trust hash and monorepo marker filename collisions due to incorrect handling of dots in hashed filenames.
  • New Helper Function: Introduced a new helper function, with_appended_extension, to correctly append extensions to filenames without replacing existing parts of the name.
  • Code Refactor: Updated all relevant calls to use the new with_appended_extension helper for hash and monorepo file paths, ensuring correct path generation.
Changelog
  • src/config/config_file/mod.rs
    • Replaced PathBuf::with_extension with with_appended_extension for monorepo marker path generation.
    • Replaced PathBuf::with_extension with with_appended_extension for hash file path generation in trust and trust_file_hash functions.
    • Added a new private helper function with_appended_extension to correctly append extensions to Path objects.
    • Added a unit test test_with_appended_extension to verify the new helper function's behavior.
Activity
  • The author reproduced the collision issue using a standalone Rust script.
  • A new unit test for the with_appended_extension helper was added.
  • All cargo test and cargo clippy checks passed.
  • CI checks are expected to pass.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 17, 2026

Greptile Summary

This PR fixes a trust-hash filename collision in mise's paranoid-mode trust verification. Path::with_extension("hash") was replacing everything after the last dot in a trust filename (e.g. infra-mise.toml-a1b2c3d4infra-mise.hash), meaning every config file that shared the same parent-directory leaf name (e.g. /projectA/infra/mise.toml and /projectB/infra/mise.toml) would map to the same .hash/.monorepo file, silently invalidating or mixing up trust state. The fix introduces with_appended_extension, a thin helper that appends .{ext} to the full OsString path instead of replacing the last component, and applies it consistently across all four call-sites (trust, untrust, mark_as_monorepo_root, trust_file_hash, and the monorepo-root walk in is_trusted).

Key changes:

  • New with_appended_extension helper preserves the full filename when adding .hash/.monorepo suffixes
  • All four previous Path::with_extension call-sites updated consistently
  • untrust now also removes the corresponding .hash and .monorepo auxiliary files (previously it only removed the primary trust symlink)
  • One-time migration impact documented in code: existing users in paranoid mode will need to re-run mise trust after upgrading because old .hash files (written under the buggy naming) will no longer be found
  • Unit test added for the helper function

Confidence Score: 4/5

  • Safe to merge — the fix is correct, all callsites are consistently updated, and the migration impact is documented.
  • The PR surgically fixes a real collision bug with a minimal, well-scoped helper. All four with_extension callsites are updated, the untrust cleanup is improved, and the with_appended_extension function is unit-tested. One point withheld because the change silently invalidates existing .hash files for users in paranoid mode (a one-time re-trust is needed), which is a meaningful user-visible behavior change, though it is acknowledged in the code comments.
  • No files require special attention — the single changed file is clean and consistent.

Important Files Changed

Filename Overview
src/config/config_file/mod.rs Replaces all four Path::with_extension callsites for .hash/.monorepo files with the new with_appended_extension helper, fixes untrust to also remove the auxiliary files, and adds a unit test for the helper.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[mise trust path] --> B[trust_path: hash path filename]
    B --> C["hashed_path = ~/.mise/trusted-configs/dir-file-HASH"]
    C --> D[write symlink at hashed_path]
    D --> E{paranoid mode?}
    E -- yes --> F["with_appended_extension(hashed_path, 'hash')"]
    F --> G["writes ~/.mise/trusted-configs/dir-file-HASH.hash"]
    E -- no --> Z[done]

    H[mise untrust path] --> I[trust_path]
    I --> J[remove hashed_path if exists]
    J --> K["with_appended_extension(hashed_path, 'hash') → remove .hash file"]
    K --> L["with_appended_extension(hashed_path, 'monorepo') → remove .monorepo file"]

    M[is_trusted path] --> N{paranoid?}
    N -- yes --> O["trust_file_hash: with_appended_extension(trust_path, 'hash')"]
    O --> P[read stored hash, compare with current file hash]
    N -- no --> Q{monorepo walk?}
    Q -- yes --> R["with_appended_extension(trust_path dir, 'monorepo') exists?"]
    R -- yes --> S[trusted]
    R -- no --> T[walk to parent dir]
Loading

Last reviewed commit: e09b622

Comment thread src/config/config_file/mod.rs
@tdragon tdragon force-pushed the fix/trust-hash-collision branch from 8abad7f to 58121ba Compare March 17, 2026 14:22
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request correctly fixes a hash collision bug by introducing a with_appended_extension helper function to append extensions to filenames instead of replacing them. The change is applied consistently across all relevant locations for handling trust hashes and monorepo markers. The addition of a unit test for the new helper function is a great way to ensure its correctness. I have one suggestion to make the new helper function more robust.

Comment on lines 483 to +488

/// Appends an extension to a path without replacing existing dots in the filename.
/// Unlike `Path::with_extension`, this preserves the full filename.
/// e.g. "foo-bar.toml-abc123" + "hash" → "foo-bar.toml-abc123.hash"
///
/// NOTE: This changes the filename convention for .hash and .monorepo files.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

While the current implementation is correct for its usage context, it could be made more robust and idiomatic by operating on the OsString of the whole path. This avoids the unwrap() on file_name(), which could panic if the function is ever called with a path that has no filename component (e.g., / or . or ..).

Suggested change
/// Appends an extension to a path without replacing existing dots in the filename.
/// Unlike `Path::with_extension`, this preserves the full filename.
/// e.g. "foo-bar.toml-abc123" + "hash" → "foo-bar.toml-abc123.hash"
///
/// NOTE: This changes the filename convention for .hash and .monorepo files.
fn with_appended_extension(path: &Path, ext: &str) -> PathBuf {
let mut os_string = path.as_os_str().to_owned();
os_string.push(".");
os_string.push(ext);
PathBuf::from(os_string)
}

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.

thanks, improved

PathBuf::with_extension("hash") replaces everything after the last dot
in the trust filename. Since hashed filenames contain dots (e.g.
"infra-mise.toml-a1b2c3d4"), this causes all configs with the same
parent directory leaf name to share a single hash file, silently
breaking paranoid mode trust verification and monorepo markers.

Replace with_extension() with a helper that appends the extension to
the full filename instead of replacing.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@tdragon tdragon force-pushed the fix/trust-hash-collision branch from 58121ba to e09b622 Compare March 17, 2026 14:31
@jdx jdx merged commit 7554b17 into jdx:main Mar 19, 2026
33 of 35 checks passed
jdx pushed a commit that referenced this pull request Mar 21, 2026
### 🐛 Bug Fixes

- **(config)** resolve trust hash collision for same-name directories by
@tdragon in [#8628](#8628)
- **(docs)** fix width of tools table by @himkt in
[#8625](#8625)
- **(docs)** prevent homepage hero atmosphere overflow by @nygmaaa in
[#8642](#8642)
- **(doctor)** detect PATH ordering issues when mise is activated by
@jdx in [#8585](#8585)
- **(git)** use origin as remote name by @bentinata in
[#8626](#8626)
- **(installer)** normalize current version before comparison by @tak848
in [#8649](#8649)
- **(lockfile)** Resolve symlink when updating lockfiles by @chancez in
[#8589](#8589)
- **(python)** verify checksums for precompiled binary downloads by
@malept in [#8593](#8593)
- **(rust)** resolve relative CARGO_HOME/RUSTUP_HOME to absolute paths
by @simonepri in [#8604](#8604)
- **(task)** correctly resolve task name for _default files with
extensions by @youta1119 in
[#8646](#8646)
- **(tasks)** global file tasks not properly marked as such by @roele in
[#8618](#8618)
- **(tasks)** handle broken pipe in table print and task completion
output by @vmaleze in [#8608](#8608)
- use dark/light logo variants in README for GitHub dark mode by @jdx in
[#8656](#8656)
- failing rebuild of runtime symlinks for shared tools by @roele in
[#8647](#8647)
- add spaces around current element operator in flutter version_expr by
@roele in [#8616](#8616)
- complete task arguments correctly by @KevSlashNull in
[#8601](#8601)

### 📚 Documentation

- switch body font to DM Sans and darken dark mode background by @jdx in
[6e3ad34](6e3ad34)
- use Cormorant Garamond for headers and Roc Grotesk for body text by
@jdx in
[010812a](010812a)
- resolve chaotic heading hierarchy in task-arguments.md by @muzimuzhi
in [#8644](#8644)

### 🧪 Testing

- fix test_java and mark as slow by @roele in
[#8634](#8634)

### 📦️ Dependency Updates

- update docker/dockerfile:1 docker digest to 4a43a54 by @renovate[bot]
in [#8657](#8657)
- update ghcr.io/jdx/mise:alpine docker digest to 2584470 by
@renovate[bot] in [#8658](#8658)

### 📦 Registry

- add viteplus (npm:vite-plus) by @risu729 in
[#8594](#8594)
- remove backend.options for podman by @roele in
[#8633](#8633)
- add pi.dev coding agent by @dector in
[#8635](#8635)
- add ormolu ([github:tweag/ormolu](https://github.com/tweag/ormolu)) by
@3w36zj6 in [#8617](#8617)
- use version_expr for dart and sort versions by @roele in
[#8631](#8631)

### New Contributors

- @bentinata made their first contribution in
[#8626](#8626)
- @tdragon made their first contribution in
[#8628](#8628)
- @nygmaaa made their first contribution in
[#8642](#8642)
- @youta1119 made their first contribution in
[#8646](#8646)
- @chancez made their first contribution in
[#8589](#8589)
- @dector made their first contribution in
[#8635](#8635)
- @tak848 made their first contribution in
[#8649](#8649)

## 📦 Aqua Registry Updates

#### New Packages (5)

- [`acsandmann/rift`](https://github.com/acsandmann/rift)
-
[`alltuner/mise-completions-sync`](https://github.com/alltuner/mise-completions-sync)
- [`berbicanes/apiark`](https://github.com/berbicanes/apiark)
-
[`gitlab.com/graphviz/graphviz`](https://github.com/gitlab.com/graphviz/graphviz)
-
[`jorgelbg/pinentry-touchid`](https://github.com/jorgelbg/pinentry-touchid)

#### Updated Packages (7)

- [`UpCloudLtd/upcloud-cli`](https://github.com/UpCloudLtd/upcloud-cli)
- [`aquaproj/registry-tool`](https://github.com/aquaproj/registry-tool)
- [`go-swagger/go-swagger`](https://github.com/go-swagger/go-swagger)
-
[`gopinath-langote/1build`](https://github.com/gopinath-langote/1build)
- [`sassman/t-rec-rs`](https://github.com/sassman/t-rec-rs)
- [`sharkdp/fd`](https://github.com/sharkdp/fd)
- [`temporalio/cli`](https://github.com/temporalio/cli)
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