Repository navigation
Stop a cloned repository from writing outside itself or choosing the model - #176
Merged
Merged
Conversation
The model a reader opens came from .repograph/vectors.json, which a clone can commit, without the check repograph.toml's embed_model passes. A clone could therefore name any Hugging Face repository for this machine to download and hand to onnxruntime. The recorded model is now used only when it is catalogued or trusted on this machine; otherwise the configured model is opened and a notice says how to trust the recorded one.
A cloned repository controls every entry under .repograph/. A committed symlink there carried this process's writes to a file outside the repository. The detached refresh appended its log (including lines a crafted file name chose) to the link's target; append_after truncated and extended vectors.f32's target; the lock and .gitignore opens followed links too. Store files are now opened through open_unlinked: refused when the path is a symlink, and opened with O_NOFOLLOW (FILE_FLAG_OPEN_REPARSE_POINT on Windows) to close the gap after the check. Temporary files are created with create_new, and a .repograph that is itself a link is refused. Repository paths printed on stderr are escaped or passed through terminal_safe.
- set_embed_model's temporary file is opened unlinked too. - serve refuses a linked .repograph before it unlinks or binds its socket. - A temporary name already taken is skipped, never written through or removed, and the test that claimed to cover it now does. - A failed .gitignore write removes the empty file it created. - The override is weighed before the trust gate, and the untrusted-model notice reaches ask's notices instead of the server's stderr. - Model names read from a store are escaped once where they are read, and the notice suggests `repograph model` only for a plain token. - Repository file names on stderr pass through log_line, so a name with a newline cannot forge a log line the MCP progress reader parses. - `repograph model` says when the store's model is not trusted.
Opening the configured model in place of an untrusted recorded one searched the store's rows with another model's queries, and a refreshing reader re-embedded the whole store under it. Readers now answer lexical-only and say why; dump and a dense bench fail. Writers keep the configured model through writer_model. A writer.lock that is a symlink is now read as a barred store rather than a read-only checkout, so a reader no longer refreshes and saves without the lock, and it promises no background refresh that would be refused.
5 tasks done
This was referenced Oct 14, 2026
devmaxxx
added a commit
that referenced
this pull request
Oct 16, 2026
…model (#176) * fix(embed): gate the store's recorded model through the trust list The model a reader opens came from .repograph/vectors.json, which a clone can commit, without the check repograph.toml's embed_model passes. A clone could therefore name any Hugging Face repository for this machine to download and hand to onnxruntime. The recorded model is now used only when it is catalogued or trusted on this machine; otherwise the configured model is opened and a notice says how to trust the recorded one. * fix(store): never write through a symlink a clone committed A cloned repository controls every entry under .repograph/. A committed symlink there carried this process's writes to a file outside the repository. The detached refresh appended its log (including lines a crafted file name chose) to the link's target; append_after truncated and extended vectors.f32's target; the lock and .gitignore opens followed links too. Store files are now opened through open_unlinked: refused when the path is a symlink, and opened with O_NOFOLLOW (FILE_FLAG_OPEN_REPARSE_POINT on Windows) to close the gap after the check. Temporary files are created with create_new, and a .repograph that is itself a link is refused. Repository paths printed on stderr are escaped or passed through terminal_safe. * fix(store): close the gaps the review found in the link and trust fixes - set_embed_model's temporary file is opened unlinked too. - serve refuses a linked .repograph before it unlinks or binds its socket. - A temporary name already taken is skipped, never written through or removed, and the test that claimed to cover it now does. - A failed .gitignore write removes the empty file it created. - The override is weighed before the trust gate, and the untrusted-model notice reaches ask's notices instead of the server's stderr. - Model names read from a store are escaped once where they are read, and the notice suggests `repograph model` only for a plain token. - Repository file names on stderr pass through log_line, so a name with a newline cannot forge a log line the MCP progress reader parses. - `repograph model` says when the store's model is not trusted. * fix(embed): read a store whose model is untrusted lexical-only Opening the configured model in place of an untrusted recorded one searched the store's rows with another model's queries, and a refreshing reader re-embedded the whole store under it. Readers now answer lexical-only and say why; dump and a dense bench fail. Writers keep the configured model through writer_model. A writer.lock that is a symlink is now read as a barred store rather than a read-only checkout, so a reader no longer refreshes and saves without the lock, and it promises no background refresh that would be refused.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A cloned repository controls everything under its own
.repograph/. Four paths treated that directory as trusted; this closes them.background.log,vectors.f32,writer.lock,.gitignoreand the temp files were opened by path, so a link to~/.zshrccarried a detached refresh's output — including a line a crafted file name chose — outside the repository. Every store file now opens throughopen_unlinked(a link check, thenO_NOFOLLOW/FILE_FLAG_OPEN_REPARSE_POINT); a.repographthat is itself a link is refused, byservebefore it touches its socket too. A taken temp name is skipped, never written through or removed.vectors.jsoncould name any hub repo andaskwould download it and hand it to onnxruntime. The recorded model now passesmodel_is_trusted; when it does not, readers answer lexical-only and say why,dumpand a densebenchfail, and no reader re-embeds the store under another model.writer.lockis read as a barred store, not a read-only checkout, so a reader never refreshes and saves without the lock.background.log, so a newline in a name cannot forge a log line the MCP progress reader parses.Known and left out: reads still follow links (a
graph.json -> /dev/zerois a denial of service), andinstall-agentfollows a linked.claudeparent, which users with dotfile links rely on.Test plan
cargo test— all suites pass; new unix tests plant links at each store file, the store dir, the lock and the logcargo clippy --all-targets -- -D warningsupdate --detachis refused and the link's target is unchanged