Repository navigation
fix(security): re-judge a remote hosts alias after a mid-parse publish - #21726
Conversation
Sensitive change review
|
UX Review (Opus 5.5) — ⏭️ skippedRevision |
Sensitive Change ReviewSensitive files changed:
|
Design Review (Opus 5.5) — ✅ PASSDesign-level review of Design-Verdict: PASS Check that the three-pass bound rests on a real limit: "one enrichment pass moves the key at most twice" should be stated in the worker code, not only in this comment. [DESIGN-REVIEWED] a61afcd Description read[DESCRIPTION-READ] fc08bdfcaa868669f18b9b7167599eaf3194875d577db7c109bfdaf95baa4f33 That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
First Principles Review (Opus 5.5) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS Check the bound before merging: three passes assumes "one enrichment pass moves the key at most twice (netlink publish, then DNS merge)". Writes at What this change shipsInventory (4 items) — 4 justifiedIntent: the first
[FIRST-PRINCIPLES-REVIEWED] a61afcd Description read[DESCRIPTION-READ] 6e5947f43405ba55f99b1f5b19a3261470f23522e52d4d59ebcb882e0aa13fdf That sha256 names the PR description this verdict read. Recompute it with |
Opus 5.5 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 6.1 Review — ✅ no blocking findingsGPT 6.1 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
|
|
Security Scope Review -- ✅ PASSWhich legitimate operations does Verdict basis: adjudicated, zero confirmed regressions. Security scope review -- adjudicated candidates
No candidate flipped from allowed to refused. Scope-Verdict: PASS TIGHTENING: the hosts-file part of the ssh-to-self host check ( ADJUDICATED: 16 candidates, 0 confirmed newly-refused PLATFORM COVERAGE: The diff makes no new assumption about paths, separators or executable names. It reuses the existing
One limit applies to every platform: the retry loop only runs when the enrichment worker publishes during the gate's parse. A static classification cannot trigger that race. The patch's own unit tests are what exercise it. [SCOPE-REVIEWED] a61afcd |
On a cold worker the first dotless ssh check starts the own-host enrichment worker and parses the hosts file in the same call. When the worker publishes while that parse runs, the table comes back uncached, judged against the older own set. An absent name or a local entry is answered from that table; a remote entry was refused as pending because its key no longer matched the own state at return. _hosts_file_verdict now judges every path again on the current key in that one case, at most _HOSTS_KEY_MOVED_ATTEMPTS passes per call, and answers pending after that. A remote answer is still served only when publication and the own set at return match the key its table was judged under, so an alias for a newly published own address is refused on the retry. A file that changes mid-parse and a failed re-stat stay pending at once. Fixes #21228 Follow-up to #20964 Supersedes #20861 Co-authored-by: Will Laws <will.laws@me.com>
75d828b to
a61afcd
Compare
Sensitive change review
|
|
HOLD at a61afcd: human review is required by policy. This PR changes I found no defect. Each pass re-reads the key, so a stale table is parsed again under the new own set. A local verdict on any line still denies first. An exhausted bound still denies. The file-changed, re-stat-failure, unreadable and oversized exits are unchanged. Security Scope Review reports 0 newly refused commands, and the new tests pin each exit. The Design and First Principles punchlines on the three-pass bound were rebutted on the pre-rebase head. That rebuttal does not name the To clear: a maintainer reviews and approves this sandbox-gate change. — crew-pr-reviewer (r110) |
Problem / Motivation
Goal: On a cold worker, the first
sshto a dotless name the hosts file maps to a remote address (10.4.4.4 devbox, thenssh devbox uptime) is allowed even when the own-host enrichment worker publishes while that call is parsing the hosts file.Maintainer takeover of #20861 by @jeeshofone, carried on a branch of this repository because the Sensitive Change Review gate refuses a fork PR that touches
src/kiro_crew/security/. The code and tests are the author's commit, unchanged; the author is credited withCo-authored-by:.Supersedes #20861
Follow-up to #20964. That PR made the in-call parse hand back its table uncached when only the own-address half of the hosts-cache key moves mid-parse, so a name the file does not hold (the case #20842 reported) or a local entry is answered from it. A remote entry in that table was judged against the older own set, so the own-state re-check before return still refuses it as pending. Reported as #21228.
Why it matters
A user whose hosts file names a remote machine gets a false sandbox-escape refusal on the first
sshafter the gateway starts, and the same command passes on the next call. The refusal comes from a security gate, so it reads as an attack rather than a race.Not a goal
main.What changed (motivation → approach → change)
A cold
sshto a remote hosts-file alias is no longer refused when the own-host worker publishes during the gate's first parse; an alias for this host's own address is still refused.Root cause:
_hosts_file_verdictinsrc/kiro_crew/security/argv_floor.pyjudged each table under the key it read before parsing. When the worker published mid-parse, a remote entry's key no longer matched the own state at return, so the gate answered pending and refused.Change: the body of
_hosts_file_verdictruns in a loop of at most_HOSTS_KEY_MOVED_ATTEMPTS = 3passes. The only retry is a remote entry whose own state moved: every path is judged again on the current key, so an alias for a newly published own address is refused on the retry. After the last pass the answer is pending and the warm is scheduled, as before. Every other exit is unchanged. One enrichment pass moves the key at most twice (netlink publish, then DNS merge), so three passes see a settled key. Tests intest/test_denied_commands_security.pypin each exit.Backwards compatibility
Compatible: every answer
maingives is unchanged, except a remote hosts-file entry whose own state moves during the call. That one is judged again on the current key instead of being refused as pending. A name that maps local on any line is still refused.Removes nothing: no function, constant, key, field or record goes away.
Tests
In
TestHostsFileWarmUp(test/test_denied_commands_security.py):test_a_cold_ssh_to_a_remote_alias_is_allowed_when_publication_lands_mid_parse(new, the regression)ssh farbox uptimeallowed after exactly two parses, new key cached, no warmtest_the_second_judgement_uses_the_newly_published_own_set(new)test_an_own_set_that_moves_before_a_remote_answer_is_judged_again(new)test_a_key_still_moving_after_every_attempt_is_pending(new)test_a_file_edited_during_the_gate_parse_is_pending_at_once,test_a_failed_re_stat_after_the_parse_is_pending_at_once(new)test_own_set_merge_after_the_key_check_is_a_deny(changed pin)Determinism:
TestHostsFileWarmUp(40 tests) passed 10 of 10 serial repeats, under-n 4, and in 3 shuffled orders (seeds 1, 2, 3). No clock or sleep is used; the race is forced by running the real publish step inside the parse.prove.py --base origin/mainreverts the production hunk and reports PROVEN (the new tests go red).Suites run on this head:
test/test_denied_commands_security.py(1327 passed, 1 skipped),test/test_security.py(1455 passed, 1 skipped),test/test_mcp_cron_security.py(425),test/test_agent_refactor_facade.py(49), and every other test file that namesargv_floor(all green), plustest/test_flake_pattern_ratchet.pyand Semgrep with the repo rules (0 findings). The full static gate floor (black, isort, flake8, mypy, comment history, file line cap and the rest) passed.Manual verification
Real-process check: a fresh Python process per call with the real enrichment worker, real netlink read and real threads; only the hosts file path is pointed at a temp file (
10.4.4.4 devboxplus a line aliasing this host's own interface address asownalias) and its parse slowed by a sleep. 20 calls per cell:mainallowedssh devbox uptimessh devbox uptimessh devbox uptimessh ownalias uptimessh -o ConnectTimeout=10 backup@host uptime(#20842)The namespace sandbox lane (
Backend Tests (namespace sandbox)) cannot run on the host used here (unprivileged user namespaces are refused); CI runs it on this head.Related Issues
Fixes #21228
Refs #20842
Supersedes #20861
Follow-up to #20964, which covers the case #20842 reported.
Known limit, not changed here: #21051 (on Linux a same-size rewrite within one coarse ctime tick can keep the hosts-cache key; the same on
main).#17666 (draft) moves this code to its own module. Whichever lands second re-homes the loop.
Pattern harvest
Rule candidate: review-prompt
Pattern: a check-then-use answer of "pending" returned on the first race, when re-reading the moved state and judging again a bounded number of times would answer correctly.
Checklist
feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)Contribution License Agreement
Contributions are accepted under the project's Apache-2.0 license; see the Licensing section of CONTRIBUTING.md. Original work by @jeeshofone in #20861.