fix(authority): stop the PID-proof scan from slicing inside a multi-byte character

Issue #7048: a Cyrillic session panics and loses the whole TUI, ten times in
two weeks, always "start byte index N is not a char boundary". The reported
line moved between versions, so this was found from the signature rather than
the line number.

`pid_var_proofs` walks the command by byte to find `$` sigils, then asked
whether the two bytes before the sigil are a `++` or `--` prefix:

    let prefix_mutated = i >= 2 && matches!(&stmt[i - 2..i], "++" | "--");

`i` is always a character boundary, because it indexes an ASCII `$`. `i - 2` is
not: a two-byte Cyrillic character followed by one ASCII byte and then `$` puts
`i - 2` inside that character, and slicing a `&str` there panics. Latin text
never hits it, which is why this survived to a release and why the reporter
saw it only in Russian.

The comparison now reads the same two bytes as `&[u8]`, which has no boundary
requirement:

    let prefix_mutated = i >= 2 && matches!(&bytes[i - 2..i], [b'+', b'+'] | [b'-', b'-']);

`bytes` is already `stmt.as_bytes()` from the loop above, so nothing new is
allocated.

Verification: a standalone harness ran both forms over five inputs. The old
form panics on `яX$p`, `тоY$p` and `имяZ$p`; the new form returns false on all
three and still returns true for `++$p`, so the proof semantics are unchanged.
That harness compiles the exact expression used here, so it also checks the
slice pattern's types. `rustfmt --check` on the file exits 0.

Not verified in-crate: `cargo check -p codewhale-tui --lib` could not run - the
shared build lock was held by another session's test for the whole attempt and
the command timed out waiting. CI is the verifier for the crate build.

The reporter also describes a second site where evidence preparation slices
command output by byte offset. I did not find it: `AutoReviewContext::from_tool_call_inner`
and everything it calls in this file contain no byte-index text slice, and the
service-level output truncation in tools/shell_output.rs already walks
`char_indices`. That path stays open and is worth its own report.

Refs #7048

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-22 01:59:23 -07:00
1 parent ba0db02e67
commit 26155e9bd0
1 file changed
+6 -1
+6 -1
View File
@@ -1523,7 +1523,12 @@ fn pid_var_proofs(command: &str) -> HashMap<String, PidVarSource> {
// Prefix `++`/`--`, destructuring (`$a, $b = ...` proves nothing
// about `$b`), postfix `++`/`--`, and compound assignment all
// mutate without proving: mark touched, keep no proof.
let prefix_mutated = i >= 2 && matches!(&stmt[i - 2..i], "++" | "--");
//
// Compare bytes, not a `&str` slice: `i` is a boundary because it
// points at an ASCII `$`, but `i - 2` can land inside a multi-byte
// character. Slicing the string there panicked for any Cyrillic or
// CJK input (#7048).
let prefix_mutated = i >= 2 && matches!(&bytes[i - 2..i], [b'+', b'+'] | [b'-', b'-']);
let destructured = stmt[..i].trim_end().ends_with(',');
while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t') {
j += 1;