Conversation
…381) `git fetch --quiet <remote> <branch>` reads argv position 6 as an option when the value starts with a dash, and `--upload-pack=<cmd>` in that position runs <cmd> -- measured on git 2.46.2, not reasoned. `resolve_base` now consults a new `remote_problem()` before building any argv, alongside the `default_branch` verdict #368 already added two positions over. The docstring beside it claimed `default_branch` was the only value reaching git's argv unprefixed. That was false, and a sentence that stops the next guard sweep from looking costs more than the value it describes. It now names both guarded values and both unguarded ones with the reason each is safe -- `branch_occupancy` prefixes `refs/heads/` and `refs/remotes/`, `--repo` is `-C`'s argument which git consumes literally -- both re-measured rather than inherited from the audit. tests/test_lane_setup_381.py asserts on captured argv rather than the exit code, with a well-formed non-default remote as the positive control, and ends with a sweep over every argument the module hands to `_git` for a hostile value at each of the four input sites -- the previous guard was written for the position somebody enumerated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015HuQoqn4iLmW2ULpvhKnwj
Both review spawns caught this independently and both were right. The site built its hostile repository as `tmp_path / "-repo"` -- absolute, so it does not start with a dash at all -- and asserted only over `call["args"]` while the recorder files the repository under `call["repo"]`. Two ways of measuring nothing in one three-line site, in the very test whose subject is a guard written for the position somebody enumerated. The value is now relative, from a chdir, so it genuinely is `-repo`; the site asserts the repository stays out of the arguments and reaches `call["repo"]`; every site now asserts its own premise, that its value is dash-prefixed at all, because neither the loop nor a green run would otherwise say so. `test_the_repo_argument_is_always_preceded_by_dash_C` is the positive control one layer down: the sweep only sees what `_git` was called with, and what makes a dash-prefixed repository safe is the argv `_git` builds around it. Its failure mode is reachable -- moving `str(repo)` out of `-C`'s slot in a scratch mutation reddens it with `['/usr/bin/git', '--no-pager', 'rev-parse', 'HEAD', '-repo']`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015HuQoqn4iLmW2ULpvhKnwj
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.
Closes #381.
What was wrong
scripts/lane_setup.py'sresolve_base()handed the--remotevalue straight intogit fetch --quiet <remote> <branch>, where argv position 6 is read as an option if it starts with a dash. Two positions over,default_branchwas already refused before any argv was built (#368) -- so a guard and a bypass sat in one command line.The load-bearing half was the sentence beside it. The docstring claimed
default_branchwas the one value here that reaches git's argv unprefixed. That was false when it was written, and it is the kind of sentence that stops the next guard sweep from looking.Severity is worse than the issue body says, and it was measured
The issue describes the argv position. Against git 2.46.2 on darwin, the injected command is executed:
So this is arbitrary command execution from that position, not a confusing flag. Reachability is unchanged and is not claimed to be worse: nothing in this repository passes
--remote-- a grep over the whole tree finds theadd_argumentand nothing else -- so the value arrives only on the maintainer's own command line. Thesplicesrank stands.The judgment call, and why it went this way
The brief left open whether to guard at all or to correct the docstring and record a decision not to. Guarded, for three reasons: the execution measurement above; ten lines and one call site; and nobody types that is a fact about today, which is the category of claim this repository distrusts by construction.
git fetch --quiet -- <remote> <branch>was measured as an alternative and does work -- git refuses the dash-prefixed repository itself withfatal: strange pathname ... blockedwhile a well-formed remote still fetches. Declined, and the reasons are in the docstring rather than only here: it makes git the thing that reports the refusal, in a sentence this script would then have to interpret to filldetail; and beside a value already refused above it could never fire, and an unfireable guard is the thing this repository keeps finding instead of a fix.The fix
remote_problem()is consulted before any argv is built, besideoss_config.default_branch_problem(). The rule is a leading dash and nothing else -- the harm is the option position, and a value whose legitimate forms include a bare name, an ssh URL and a filesystem path has no shape this file has authority to invent.It lives in
lane_setup.pyrather thanoss_config.pybecause the value does.remoteis--remoteargv only and is never config-sourced, so a verdict in the config validator would be a rule for a key no config carries anddoctorwould have no occasion to print it. #345's one-value-one-rule constraint points the other way fordefault_branch, which is config-sourced.remote_problem's docstring names the migration if that ever changes.The docstring now names all four values, not two
Re-derived rather than inherited from the audit. Two values are guarded by a rule (
remote,default_branch); two are safe by the shape of their argv and are now named as such:branch_occupancyprefixesrefs/heads/andrefs/remotes/, and--repois-C's argument, which git consumes literally (git --no-pager -C -x loganswersfatal: cannot change to '-x'). No third unguarded argv site exists -- that was checked rather than assumed, andread_boardinterpolates nothing.Tests
tests/test_lane_setup_381.pyasserts on captured argv, never on the exit code: a version that ran the fetch and then failed would pass the weaker form. Every silence is paired with a positive control in the same fixture, including a well-formed non-default remote, so a guard that refused everything would not pass either.The last test is a sweep rather than a case -- every argument the module hands to
_git, for a hostile value at each of the four input sites, must not begin with a dash unless the module wrote it as a literal flag. #381 exists because the previous guard was written for the position somebody enumerated; a sweep cannot be wrong about a site nobody thought of.Red before the fix:
7 failed, 1 passed-- the pass is the recorder control, which is what makes the seven silences mean anything. Green after:2637 passed, 2 skipped, coverage 92.71%.Self-review found a real one
Both review spawns independently flagged the same defect in the first draft of the sweep, and both were right: the
reposite measured nothing, twice over. It built its hostile path astmp_path / -repo, which is absolute and therefore does not start with a dash at all, and it asserted only over the recordedargswhile the recorder files the repository under a separaterepokey. Two ways of measuring nothing in one three-line site, inside the very test whose subject is a guard written for the position somebody enumerated.Fixed in the second commit: the value is now relative from a chdir so it genuinely is
-repo; the site asserts the repository stays out of the arguments and reaches therepokey; and every site now asserts its own premise, that its value is dash-prefixed at all. A newtest_the_repo_argument_is_always_preceded_by_dash_Cmeasures the argv_gitbuilds directly, and its failure mode was proven reachable with a scratch mutation of_git.Platform
Observed on darwin / Python 3.13.15. Reasoned for the other twelve legs: the diff adds a string-prefix check and prose, with no path separator, suffix match, drive letter, platform-specific exception, spawn change or non-ASCII output anywhere in it -- both changed files were checked and are pure printable ASCII. The tests patch module-level names looked up at call time, not ones captured through an object attribute at import, so the injection takes on 3.9-3.12 -- reasoned, not run there.
Verified by the maintainer
The escalation is real, and I reproduced it a different way. The report shows the injected script
printing its own marker. Mine went through git's protocol parser instead, which is the harder evidence:
EXECis the first four bytes of the executed script's stdout. git ran it and then choked trying toread that output as pack protocol — so execution is not inferred from a message the script chose, it
is visible in git's own parse failure. git 2.46.2. The control, a well-formed remote name, executes
nothing and fails at resolution.
That is arbitrary command execution from the position
--remoteoccupied, and it is why guardingbeat documenting. The rank is unchanged and correct: nothing in this repository passes
--remote,so the value still arrives only on the maintainer's own command line, which is the
splicesrow'sstated bound.
v0.8.0was not held for it and did not need to be.Red re-run: this branch's test file on
origin/mainatd6f56c4, product code untouched —6 failed, 3 passed. That is a different split from the report's7 failed, 1 passedand not acontradiction: the report's red was watched against this branch's own base before its second commit,
mine is the final file against a
mainthat has since taken #368, #382 and #387. Recorded so the twonumbers are not read later as one of them being wrong.
The branch was not rebased and that was the right call.
origin/mainmoved tod6f56c4mid-lane;the author merge-tested in a detached probe (
2692 passed, probe removed) and left the rebasedecision to me rather than rewriting a branch I had briefed. The base-moved handling is #389.
The doubly-vacuous
reposweep site both review spawns caught — absolute path so never dash-prefixed,and asserting over
argswhile the recorder files it underrepo— is the finding of this diff. Itwas inside the test whose whole subject is that a guard written for the position somebody enumerated
misses the one they did not.