Development record
The shell guard's unknown-word mark could be forged from the command line: an ANSI-C escape that decodes to NUL (a hex, octal, unicode or control escape) put the tokenizer's substitution mark into a word after Check had stripped the line's own NUL bytes, so an ANSI-C NUL glued before a blocked command's name made that name an unknown word compared as text, and every blocker allowed. bash ends an ANSI-C string at its first NUL and runs the name that follows. Found by review3-guard finding 1.
issueresolvedmajoriss-2609252020432185
The shell guard's unknown-word mark could be forged from the command line: an ANSI-C escape that decodes to NUL (a hex, octal, unicode or control escape) put the tokenizer's substitution mark into a word after Check had stripped the line's own NUL bytes, so an ANSI-C NUL glued before a blocked command's name made that name an unknown word compared as text, and every blocker allowed. bash ends an ANSI-C string at its first NUL and runs the name that follows. Found by review3-guard finding 1.
Grounds
- pursued: every escaped-NUL spelling ahead of a blocked command name blocks, and no ANSI-C escape form puts the mark into a word (TestForgedMarkIsTruncatedLikeBash, TestAnsiCEscapeNeverDecodesTheMark); a decoded NUL reaching a word would show it wrong.
Frontmatter
| id | iss-2609252020432185 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | major |
.abcd/work/issues/resolved/iss-2609252020432185-the-shell-guard-s-unknown-word-mark.md
open on GitHub ↗ · commit history ↗
mentions1
- dcn-26092500000000152026-09-25 — Correction to the entry above on the shell guard's unknown word (fix round 3 of lane guard, run A, closing review3-guard). Its mark was not unforgeable as `internal/core/guard/unknown.go` claimed: an ANSI-C escape that decodes to NUL (`$'\x00'`, `$'\0'`, `$'\u0000'`) put the mark into a word after Check had dropped the line's own NUL bytes, and a blocked command's name behind one allowed (iss-2609252020432185). It is unforgeable by construction now: Check drops the line's NULs, and an ANSI-C string ends at its first decoded NUL, as bash ends it. An out-of-band flag on the word was weighed and not taken, because every reader in the package holds words as strings and the change would not have been contained. The same round makes the rule total: every reader of a word or a command name reads through unknown.go, an unknown dash-word is read both as a flag that stands alone and one that takes a value (and as a shell's `-c` or a verb's payload flag) before command position as after it (iss-2609252020507464), and a substitution in command position is every program its known tail allows, a shell, a wrapper and git among them, which resolves the command-position half of iss-2609251824244354; that record now names only the parameter-expansion half (`--$X`, `${…}`), still deferred with its reason, so allow (4) above is that half alone. A test parses the package and fails on a reader of a word's dash or a command's name that is not listed with how it reads the rule. Four over-blocks are accepted in the fail-closed direction, each recorded so it is not mistaken for a defect. (1) An unknown operand can be any subcommand, so a read-only command whose subcommand a substitution prints reads as the hazard its entry names: `gh repo $(echo view) o/r` blocks under gh-repo-delete and `git $(echo status)` warns under git-clean. (2) A program name nothing fixes can be `pkill` or `killall`, whose entries name nothing but the program and an operand, so an unknown name with any operand blocks under them (`"$(which python3)" script.py`; spelling the program's name is the way past); a markdown line whose backtick span stands in command position reads the same way, which is the only change the 4,110-line false-positive sweep showed (two prose lines, no command). (3) An unknown name followed by `-c '<string>'` reads the string as a shell's, so one the guard cannot parse warns as a shell payload would. (4) More than eight substitutions where a command's program name could be, and an unknown dash-word that can be env's `-S` with a value glued on that the guard cannot read, are refused: the first because every such name costs a read of the whole line, the second because the value is not a plain command.