Skip to content

Bound the Guard's Stdin Descriptor Before int() - #2033

Merged
ptr727 merged 1 commit into
developfrom
feature/auto-2028
Sep 29, 2026
Merged

ptr727 merged 1 commit into
developfrom
feature/auto-2028

Conversation

@ptr727

@ptr727 ptr727 commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

_redirects_stdin in the write guard converted a redirect's descriptor prefix with int() after only an isdecimal() check. A zero-padded descriptor over Python's 4300-digit conversion limit raised ValueError, the hook exited non-blocking, and the command ran with no check. Bash reads such a descriptor as descriptor 0, so the crash was reachable with a command bash runs.

  • The descriptor-0 test now asks whether any digit is nonzero, via unicodedata.decimal, so no digit string is converted. It gives the same verdict int() gave for every isdecimal() string.
  • Two self-test rows pin a 4400-zero descriptor on a while read loop as descriptor 0: denied when it reads /dev/zero, allowed when it reads a file.

Closes on promotion: #2028

Verification

  • gh-write-guard.py --selftest passes. With the fix reverted it fails on the ValueError, and a plain text compare fails the new allow row.
  • An exhaustive check over every codepoint isdecimal() accepts, in single, padded, and repeated forms, found no verdict that differs from int().
  • ruff format and check, mypy, prose lint, the eol gate, spec/validate.py, and build_dist.py --check pass.
  • Two local-strict-review passes ran. The first found two introduced findings, both fixed: an ASCII-only descriptor match that allowed a runnable command the old code denied, and a deny row that passed whichever way the descriptor was read, now paired with the allow row. It also found one pre-existing defect, filed as The Guard's Stdin Redirect Scan Runs Past a Reserved Word #2032. The second found nothing.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of very long, zero-padded file descriptors in stdin-redirection checks, including descriptors bound to /dev/zero or a file.

A redirect's descriptor prefix reached int() after only an isdecimal()
check, so a zero-padded descriptor over Python's 4300-digit conversion
limit raised ValueError and the hook exited non-blocking. The descriptor
is now tested digit by digit for a nonzero value, which reads every
decimal digit exactly as int() did without converting the string.

Self-test rows pin a 4400-zero descriptor on a while-read loop as
descriptor 0: denied reading /dev/zero, allowed reading a file.
Copilot AI balanced review requested due to automatic review settings September 29, 2026 02:30
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: ptr727/ProjectTemplate/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 0a47c32c-d4d9-496d-899a-5bab2e2e47ce

📥 Commits

Reviewing files that changed from the base of the PR and between c0467a5 and 3e285f3.

📒 Files selected for processing (1)
  • host-setup/agent-safety/claude/gh-write-guard.py

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The stdin-redirection check now detects zero-valued descriptors by inspecting their decimal digits instead of converting them to integers. Self-tests cover long zero-padded descriptors bound to /dev/zero and to a file.

Changes

Descriptor Zero Detection

Layer / File(s) Summary
Descriptor check and self-tests
host-setup/agent-safety/claude/gh-write-guard.py
The guard skips descriptors with nonzero decimal digits and treats all-zero descriptors as descriptor 0. Self-tests check a 4,400-zero descriptor bound to /dev/zero and a file.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~8 minutes

Merge Risk: ⚪ Minimal · up to 3e285

No supported change-specific risk remains; the guard handles the oversized zero-padded descriptor cases described for this change.

Architecture Summary

Architecture risk: 🔵 Low · up to 3e285

The change affects 1 system.

Changed systems: host-setup

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — host-setup (service) was modified; 1 changed file maps to changed impact.

Before / after behavior

  • observed — Modified behavior in host-setup/agent-safety/claude/gh-write-guard.py: Imports unicodedata for decimal-digit inspection in descriptor handling.
  • observed — Modified behavior in host-setup/agent-safety/claude/gh-write-guard.py: Replaces integer conversion of the descriptor with a decimal-digit check: descriptors containing any nonzero decimal digit are skipped; zero-valued descriptors, including very long zero-padded values, are treated as descriptor 0.
  • observed — Modified behavior in host-setup/agent-safety/claude/gh-write-guard.py: Adds self-test cases asserting that a 4,400-zero descriptor binds /dev/zero and is denied, while the same descriptor bound to a file is allowed.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing oversized stdin descriptors from reaching int() conversion.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 55.66%. Comparing base (c0467a5) to head (3e285f3).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #2033   +/-   ##
========================================
  Coverage    55.66%   55.66%           
========================================
  Files           16       16           
  Lines         7317     7317           
========================================
  Hits          4073     4073           
  Misses        3244     3244           
Flag Coverage Δ
python-3.13 55.66% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

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.

Copilot review overview

🟢 Approval recommended

The one-line logic change is provably equivalent to the prior check (verified across all Unicode codepoints), removes a real crash-to-unchecked-command leak, and is covered by targeted self-test rows with no defects found.

Review effort: Balanced
Findings: None

What changed in this PR

This PR hardens _redirects_stdin in the Claude write guard (host-setup/agent-safety/claude/gh-write-guard.py). Previously the function converted a redirect's descriptor prefix with int(fd) after only an isdecimal() check. A zero-padded descriptor exceeding Python's 4300-digit int() conversion limit raised ValueError, which made the hook exit non-blocking and let the command run entirely unchecked. Since bash resolves a zero-padded descriptor as descriptor 0, the crash was reachable through a command bash actually executes. This guard is part of the fleet's agent-safety layer, so an unchecked-command leak is a security-relevant defect.

Changes:

  • Replace the int(fd) != 0 descriptor-0 test with any(unicodedata.decimal(c) for c in fd), which never converts the digit string and returns the same verdict int() gave for every isdecimal()-accepted string.
  • Add import unicodedata (correctly ordered in the stdlib import block).
  • Add two self-test rows pinning a 4400-zero descriptor as descriptor 0: deny when it binds /dev/zero (a stream), allow when it binds a file.

I verified the key assumptions independently:

  • str.isdecimal() and unicodedata.decimal() have identical domains across all 0x110000 codepoints (0 mismatches), so unicodedata.decimal(c) cannot raise on fd, which the tok.isdecimal() guard (line 1887) plus the {-prefix handling (line 1902) guarantee is empty or all-decimal.
  • any(unicodedata.decimal(c) for c in fd) matches int(fd) != 0 for all tested inputs, including all-zero and Arabic-Indic digits, and preserves the empty-fd case as descriptor 0 (any([]) is False, matching the old if fd and ...).
  • The allow row's yes | prefix is meaningful: it proves the padded descriptor is recognized as descriptor 0 (overriding the pipe), which a naive text compare would fail.
File Description
host-setup/​agent-safety/​claude/​gh-write-guard.py Replaces the unbounded int(fd) descriptor-0 test with an unicodedata.decimal-based check that never converts the digit string, adds the required import, and adds two self-test rows pinning the 4400-zero descriptor behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI left a comment

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.

Copilot review overview

🔵 Needs a closer look

The change alters fail-open behavior in a security-critical agent-safety write guard, which warrants final human sign-off despite the fix being small and verified equivalent.

Review effort: Balanced
Findings: 1 Low severity

Open (1)

Comment thread host-setup/agent-safety/claude/gh-write-guard.py
@ptr727

ptr727 commented Sep 29, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ptr727
ptr727 merged commit dd4ca98 into develop Sep 29, 2026
12 checks passed
ptr727 added a commit that referenced this pull request Sep 29, 2026
…d Effort (#2104)

## Summary

Copilot code review's default effort moved from Lite to Balanced on
2026-09-28. Balanced rounds write the `ccr-overview-v2` body with a
per-file table and almost never a coverage statement. So `pr_review.py`
read every such pull request as `COVERAGE_IS_UNSTATED` (exit 45) and
parked it on a maintainer decision (#2103, #2095, #2089, #2082, #2077).

Measured over the Copilot rounds on the 40 newest pull requests here, 25
of 29 Lite rounds carried the `fleet-review` marker, against 1 of 22
Balanced rounds. Stating the marker literally in the runbook did not
help. A Balanced round on this branch read that instruction, quoted it
in its own summary, and still left the marker out. So the tooling now
accepts a coverage reading that works at any effort level, instead of
depending on one setting.

### The rule

A head that no coverage statement reaches, meaning none is stated on it
and none is carried to it, now reads `coverage=table` and passes when
all of these hold:

- A round covering the head has its own file table naming exactly the
changed files.
- No Copilot round on the pull request, on any commit, states or appears
to state partial coverage. The scan reads each round body in 12 ways:
raw, entity-decoded, and compatibility-folded, each with tags kept and
stripped three ways. A partial found in any reading counts.
- The whole review history is in view.
- The changed-file list is present, well-formed, non-empty, and within
the 100-file window.

A statement that reaches the head always wins, so a stated partial still
blocks. A pull request that ever had a partial round goes to the
maintainer. When no table stands in, the digest prints the reason as `NO
FILE TABLE STANDS IN: <reason>`.

The rule is stated in `GOVERNANCE.md` "PR Review Etiquette", in the
`pr-review-conduct` Merge Gate (source and generated copies), in the
runbook, in `scripts/README.md`, and in the script's own docstring and
exit-45 message.

Measured live with this branch: #2102, #2094, #2088, #2081, #2076,
#2050, #2046, #2039 and #2033 read `coverage=table`. #2098, which
carries the marker, still reads `full`.

## Known limits, all failing closed unless noted

- **Glob rows.** Copilot sometimes collapses generated skill copies into
glob rows such as `.../skills/*/references/*.md`, so on those pull
requests the table does not match and they go to the maintainer. #2053
and #2021 are examples.
- **Marker quoted near digits.** A round that quotes the marker next to
digits reads as a partial. That includes this pull request's own first
round, so this PR goes to the maintainer.
- **Early-closed fences.** A fence that Markdown closes early, through a
list item or an HTML block, masks a real table after it.
- **Prose partials (fail-open).** A partial worded in prose that none of
the patterns anticipate can still pass. The scan narrows this gap but
cannot close it.
- **Malformed marker before a full one (pre-existing, not introduced
here).** On the head, a malformed marker followed by a well-formed full
marker is read as full by the existing coverage reader.
- **Unpinned readings.** Tests do not pin each of the 12 readings
individually, the echoed snippet can carry raw control characters into
the digest line, and two docstrings overstate slightly. None of these
affects a verdict.

## Verification

- `python3 -m unittest discover -s tests`: 1841 tests pass. The new
tests were mutation-checked, and each guard fails its test when removed.
- ruff, mypy, the prose gate, markdownlint, `build_dist.py --check`,
`spec/validate.py`, and the eol gate are clean.
- The local strict review ran across several rounds, and the last pass
found no fail-open. Its differential fuzz over 150k bodies found nothing
the previous commit flagged that this one misses. Its receipt is
recorded.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Sep 29, 2026
…pilot Coverage at Balanced Effort (#2105)

## Summary

Promotes develop to main, carrying 14 pull requests:

- [#2076](#2076) Read the
Guard's Token Fallback With a Bash-Aware Context Scan
- [#2081](#2081) Keep the
Guard's Quote Mask When an Escape Sits Outside the Loop Condition
- [#2094](#2094)
Reconfigure pr_review.py's Console Streams to UTF-8
- [#2088](#2088) Guard
check_settings' Live Read of the Repository Response
- [#2102](#2102) Skip a
Blockquoted List Marker in the Prose Gate's Dash Rule
- [#2104](#2104) Accept a
Head Round's Exact File Table as Copilot Coverage at Balanced Effort
- [#2098](#2098) Quote the
Review Engine's Remaining Bare Shell Placeholders
- [#2050](#2050) Hand
Unstated Coverage to the Maintainer Instead of Prescribing a Re-Request
- [#2046](#2046) Report a
Copilot Review Request That Records Nothing Instead of Waiting It Out
- [#2053](#2053) State the
Python Build Profile by Its Trait, Not One of Its Forms
- [#2068](#2068) Strip the
CR From configure.sh's Environment-Name Encoding
- [#2033](#2033) Bound the
Guard's Stdin Descriptor Before int()
- [#2036](#2036) Guard
ruleset_id() Against Malformed Array Elements
- [#2039](#2039) Strip the
CR From configure.sh's Workflow-Model Read

The lead change is #2104. Copilot code review's default effort moved
from Lite to Balanced on 2026-09-28, and Balanced rounds write a
per-file table but almost never a coverage statement. `pr_review.py` now
accepts a head round's file table as coverage when it names exactly the
changed files and no Copilot round on the pull request has ever stated
or appeared to state partial coverage. Before this, every pull request
reviewed at Balanced effort parked on a maintainer decision. The other
pull requests are guard, configure, prose-gate, and review-engine fixes
that were waiting on that decision or landed earlier.

#2076 leaves #1910, #1958, and #1959 open, per #1960.

Closes #1998
Closes #2091
Closes #1260
Closes #1190
Closes #1652
Closes #2047
Closes #1904
Closes #1947
Closes #1882
Closes #2028
Closes #1987
Closes #1881

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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