Development record
Two more paths by which a process search's output reaches a kill are not read by kill-by-search: an unquoted here-document whose body holds a command substitution, redirected into an xargs kill (a here-string of the same substitution blocks), and a command substitution inside a command that reads a pipe, which inherits that pipe as its standard input (an xargs kill in the substitution reads the search piped into its command). A third, a search handed to a shell string as a positional parameter the string's kill reads, is not read either: it is a pid carried through a variable, the half [dcn-2609250000000016](../../decisions/dcn-2609250000000016-correction-to-the-two-entries-above-on.md) (c) defers with iss-2609251824244354, unless it is read the way xargs's input to a string is ([dcn-2609270000000001](../../decisions/dcn-2609270000000001-the-kill-by-search-reading-follows-a.md)), which is a call for the next guard lane. Found while fixing iss-2609270028388291.
Two more paths by which a process search's output reaches a kill are not read by kill-by-search: an unquoted here-document whose body holds a command substitution, redirected into an xargs kill (a here-string of the same substitution blocks), and a command substitution inside a command that reads a pipe, which inherits that pipe as its standard input (an xargs kill in the substitution reads the search piped into its command). A third, a search handed to a shell string as a positional parameter the string's kill reads, is not read either: it is a pid carried through a variable, the half dcn-2609250000000016 (c) defers with iss-2609251824244354, unless it is read the way xargs's input to a string is (dcn-2609270000000001), which is a call for the next guard lane. Found while fixing iss-2609270028388291.
Grounds
- pursued: each path from a search to a kill through a here-document, an inherited pipe or a string parameter blocks, and the no-leak shapes stay allowed; an allowed spelling of one of the three, or a kill of a named pid that now blocks outside the recorded string over-block, would show it wrong
Frontmatter
| id | iss-2609270036253187 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | minor |
.abcd/work/issues/resolved/iss-2609270036253187-two-more-paths-by-which-a-process-search.md
open on GitHub ↗ · commit history ↗
mentions5
- dcn-26092500000000162026-09-25 — Correction to the two entries above on the shell guard's unknown word (fix round 4 of lane guard, run A, closing review4-guard). Over-block (2) of the round-3 entry is wider than it says: any command word whose basename ends in a substitution (`$(date) x`, `"$(which go)" build ./...`, `$(command -v X) anything`, `/usr/bin/$(x) y`) followed by any one operand blocks, because killall-by-name and pkill-by-pattern name only a program and an operand; and the verdict carried killall's own lesson ("stop it by pid"). Where the only entries a command fires are ones its unknown program name can be, the verdict is now reported under the reserved id `program-name-unknown`, in the substitution family, naming the entry the line reads as and the way past, spelling the program's name; the entries stay in its matches, and an entry the line does name outranks it. A narrower `$(which NAME)`/`$(command -v NAME)` reading was weighed and not taken. Three further rulings of the round. (a) A line the tokenizer cannot split is BLOCKED on the hook (`command-unparsable`), not failed open: where the tokenizer is right no shell runs the line either, so the block costs nothing, and where it is wrong (the `$'\c'` misreading this round fixed, which swallowed the string's closing quote) a fail-open is a bypass of every blocker by construction. itd-103 ac-1 names the binary failing to run, which still fails open loudly, as do an unreadable payload and a registry that will not load; the check verb keeps reporting the error as a fault (exit 2); the 961-line repository corpus holds no unparsable line. (b) An unquoted here-document body is read as bash expands it: the command substitutions in it are commands of the redirecting command's chain and its text is data, and a delimiter written with a backslash (`<<\EOF`, `<<E\OF`) is quoted, as bash reads it. (c) A `${…}` in which a substitution's output lands is unknown from its `${` on, so a word that is wholly such an expansion falls under allow (1) of the round-2 entry exactly as `"$(x)"` does, and `git push "${X:-$(echo --force)}" origin main` is not seen. The plain-variable half (`$X`, `${X:-git}`, a `${…}` holding no substitution) stays iss-2609251824244354, deferred with its reason; it is not seen as the program name, as a flag, or inside a payload.
- dcn-26092700000000012026-09-27 — The kill-by-search reading follows a search into and out of shell strings, and keeps one over-block, recorded so it is not mistaken for a defect (lane drainG2, autonomous run A, on iss-2609262259360005 and the review of the first reading). Every command of a string that `xargs` runs is read as handed xargs's input, so `pgrep make | xargs sh -c 'kill 4242'` blocks as `kill-by-search` though its kill names a pid: `xargs -I{}` replaces the input into any part of the string, and telling a command that reads `"$@"` or `$1` from one that does not would be a text match on the string, which the feed mechanism does not make. The same holds for the standard input a shell passes to the commands of its string (`pgrep make | sh -c 'xargs kill'`). The accepted over-block of the first reading, that a lower-case signal name (`pkill -term -g <pgid>`) blocked as `pkill-by-owner` because its letters read as the `-t` and `-u` selectors, is removed rather than recorded: `pkill`'s first `-NAME` word naming a signal is read as its signal, in any case and with or without `SIG`, as procps-ng and BSD pkill read it, which is also what lets `-U` and `-G` be read attached (`pkill -Ubob`) without taking `-HUP`, `-USR1` or `-SIGTERM` for them. Only the first such word is the signal, so `pkill -9 -term -g <pgid>` still blocks: both implementations hand the second word to their option parser as `-t erm`. `killall` is not read this way, because psmisc killall reads a signal name only when it begins with a capital letter and parses `killall -term` as `killall -t erm`.
- dcn-26092800000000062026-09-28 — A parameter expansion is an unknown word, read for what its value can spell and not for what an earlier command carried into it, and the reading keeps its over-reads, recorded so they are not mistaken for defects (lane drainG3, autonomous run A, on iss-2609251824244354; this supersedes allow (4) of the first 2026-09-25 entry and ruling (c) of the third, which parked the plain-variable half). `$X`, `$1`, `$@`, `$*`, `$-` and a `${…}` read whole to its own `}` leave the unknown word's mark where the value goes, so `--$X` is every flag it could become and `$GIT` in command position is every program its known text allows; `$$`, `$!`, `$?` and `$#` print numbers and stay text, as an arithmetic expansion's output is a number. Allow (1) of the first 2026-09-25 entry extends to a word that is wholly a variable: it is one operand, so `git push origin "$branch"` stays allowed and `git push $X origin main` is not seen. A string handed to a shell is read with each variable-only word written back out (`sh -c "… $X …"` reads `$X` as that shell does), so a bare variable in a string raises no new warn, which keeps the gap `shellRawUninspectable` names: a value holding shell syntax is not read. The 4,315-input false-positive sweep (Makefile recipes, script lines and whole scripts, the repo-mined and adversarial corpora, and 81 everyday variable lines) moved from 4,000 allow / 47 block / 19 warn to 3,991 / 49 / 26 with 249 unparsable lines unchanged, after three readings of a carried value were left out because together they added 37 blocks on ordinary work: a variable handed to a shell or `source` as its script is not a stream (`bash "$script"`), and a variable standing as the program fires no entry that names only its program and an operand (pkill-by-pattern, killall-by-name) and is not a bare interpreter inside a string (`"$GO" build`, `$EDITOR notes.md`). Those, a pid list carried through a variable, and `eval "$X"` (which allows, as it did) are iss-2609281134544802, deferred past v0.11.0 for a ruling. Over-reads kept, each the variable twin of a ruled substitution over-read: a variable program name with a variable first operand can be `git clean` and warns (`exec "$BIN" "$@"`, `"$BASH" "$GATE"`; five lines of the sweep), `git -c core.quotePath=false "$@"` warns under git-clean, `git grep` whose pattern holds a variable and a `(` warns under the fail-safe as its substitution twin does, two printf continuation lines of a script read on their own (`"$n" "$n" "$spec" "$n"`) block as gh-repo-delete, `git -c "$KV" commit` blocks as a commit that may move core.hooksPath, and a stream piped into a shell whose script is a variable (`curl … | bash "$f"`) blocks, because a word wholly an expansion may be no word. The kill-by-search reading gains three feeds in the same lane (iss-2609270036253187): an unquoted here-document's substitutions are the standard input of the command that opened it, a substitution runs with the pipe into its own command as its input, and a shell string is handed the output of the substitutions in its command's words as its positional parameters or text, so every command of such a string is read as handed it (`sh -c 'kill 4242' _ "$(pgrep make)"` blocks), the over-block the 2026-09-27 entry accepts for a string xargs runs.
- iss-2609251824244354The shell guard reads a command substitution's output as an unknown word, but not a parameter expansion's. A dash glued to a variable (git push --$X origin main, rm -$F after a cd) is read as the literal text --$X, which names no flag, so every blocker allows it while its --$(echo x) twin blocks; and ${GIT:-git} standing in command position is compared as text. bash builds the hazard from either. Found while closing review2-guard (its finding 1 names --$X as pre-existing). The record named a second half, a substitution standing in command position, which fix round 3 of lane guard resolved (a name a substitution prints is every program its known tail allows); this record is the parameter-expansion half alone. Fix round 4 made a ${…} that carries a command substitution (${X:-$(…)}, --${X:-$(…)}) unknown from its ${ on (iss-2609252120211621). What remains is exactly a parameter expansion with no substitution in it: $X, ${X}, ${X:-word} and the other operators with literal words, and $@ / $*, standing as the command's program name, as a flag or glued to one, as an operand an entry constrains, or inside a payload the guard reads.
- iss-2609270028388291The kill-by-search reading loses a pipe that feeds a group or a command string from outside it. A pipe INTO a brace or paren group feeds every command in the group, but the tokenizer hands it only to the commands before the first separator inside, so a process search piped into a group whose later command is an xargs kill (after a sleep, a read, or an and-list) allows silently while the one-command group blocks. In the same class, payloadInput hands a command string only its running shell's pipe, so a here-string or a process-substitution redirect carrying a search into a shell string whose xargs kills allows, while the same redirect into a plain xargs kill blocks. Found by review-drainG2 (tokenize.go group open/close, payload.go payloadInput).