fix: don't treat Object.prototype members as reserved words - #5
Merged
webpro merged 1 commit intoAug 2, 2026
Merged
Conversation
`readWord` classified a command-start word as a keyword with
`text in RESERVED_WORDS`. `in` walks the prototype chain, so any
Object.prototype member name matched, and `RESERVED_WORDS[text]` then
handed `setToken` a function instead of a `Token`.
Two names reach the lookup through the existing fast-path guard
(lowercase first char, length <= 8): `toString` and `valueOf`. Both are
plausible shell command/function names, and the failure is silent — the
bogus token makes the parser abandon the rest of the script with no
entry in `errors`:
parse("toString foo") // commands: []
parse("echo hi; toString foo; echo bye") // commands: [echo hi]
Inside a compound command it instead produces spurious errors
(`if toString; then echo a; fi` reports "expected 'then'").
Replace the `in` test plus second lookup with a single lookup guarded by
`typeof reserved === "number"`. Own entries are always numeric `Token`
values, inherited members never are. This also drops one hash lookup
from the hot path; bench/self.ts is unchanged-to-slightly-faster.
The other string-keyed lookup tables were checked: `UNARY_TEST_OPS` and
`BINARY_TEST_OPS` compare `=== 1`, and `REDIRECT_OPS` is keyed only by
lexer-produced operator strings, so none are reachable this way.
mindlace
force-pushed
the
worktree-fix-prototype-reserved-words
branch
from
August 2, 2026 02:06
c5415a2 to
24c8440
Compare
Member
|
Oof, nasty. Thanks for the find + PR! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
readWordclassifies a command-start word as a keyword withtext in RESERVED_WORDS.inwalks the prototype chain, so anyObject.prototypemember name matches, andRESERVED_WORDS[text]then handssetTokena function instead of aToken.Two names get past the existing fast-path guard (
fc >= CH_a && fc <= CH_z && text.length <= 8):toStringandvalueOf. Both are plausible names for a shell command or function.The failure is silent — the bogus token makes the parser abandon the rest of the script, with nothing added to
errors:Inside a compound command it instead produces spurious errors:
The blast radius happens to be limited to those two names only because of the
length <= 8fast-path check, which is a performance heuristic — not something that should be load-bearing for correctness. Widen or drop that guard andconstructor,hasOwnProperty,toLocaleString, etc. become reachable too.Fix
Replace the
intest plus second lookup with a single lookup guarded bytypeof reserved === "number". Own entries are always numericTokenvalues; inherited members never are.This also removes one hash lookup from a hot path —
bench/self.tsis unchanged to slightly faster on every group (e.g.short11.67 → 10.76 µs/iter,large6.75 → 6.39 ms/iter, within noise but never worse).I considered
Object.create(null)/__proto__: nullforRESERVED_WORDS, but null-prototype objects drop to dictionary mode in V8, which would cost more than it saves here.Scope check
I swept the other string-keyed lookup tables for the same defect class:
RESERVED_WORDStext in RESERVED_WORDSUNARY_TEST_OPSUNARY_TEST_OPS[val] === 1BINARY_TEST_OPSBINARY_TEST_OPS[nt.value] === 1REDIRECT_OPSREDIRECT_OPS[t.value] ?? ">"REDIRECT_OPSwould return the inherited function rather than falling back to">"if a prototype key ever reached it, but no code path can supply one, so I left it alone rather than adding an unreachable check to the hot path. Happy to harden it if you'd prefer belt-and-braces.Tests
Two tests added to
test/parser.test.ts, both failing before the change:Object.prototypemember names parse as ordinary command names (toString,valueOf,constructor,hasOwnProperty,__proto__,isPrototypeOf)Object.prototypemember name doesn't truncate the rest of the scriptFull suite: 1443/1443 passing.
tsc --noEmit,oxlint, andoxfmt --checkclean on the touched files.