Skip to content

fix: don't treat Object.prototype members as reserved words - #5

Merged
webpro merged 1 commit into
webpro-nl:mainfrom
mindlace:worktree-fix-prototype-reserved-words
Aug 2, 2026
Merged

fix: don't treat Object.prototype members as reserved words#5
webpro merged 1 commit into
webpro-nl:mainfrom
mindlace:worktree-fix-prototype-reserved-words

Conversation

@mindlace

@mindlace mindlace commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Problem

readWord classifies a command-start word as a keyword with text in RESERVED_WORDS. in walks the prototype chain, so any Object.prototype member name matches, and RESERVED_WORDS[text] then hands setToken a function instead of a Token.

Two names get past the existing fast-path guard (fc >= CH_a && fc <= CH_z && text.length <= 8): toString and valueOf. 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:

parse("toString foo")                     // commands: []
parse("valueOf x")                        // commands: []
parse("toString() { echo hi; }")          // commands: []
parse("echo hi; toString foo; echo bye")  // commands: [echo hi]   <- rest dropped

Inside a compound command it instead produces spurious errors:

parse("if toString; then echo a; fi")
// errors: ["expected 'then'", "expected 'fi' to close 'if'"]

The blast radius happens to be limited to those two names only because of the length <= 8 fast-path check, which is a performance heuristic — not something that should be load-bearing for correctness. Widen or drop that guard and constructor, hasOwnProperty, toLocaleString, etc. become reachable too.

Fix

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 removes one hash lookup from a hot path — bench/self.ts is unchanged to slightly faster on every group (e.g. short 11.67 → 10.76 µs/iter, large 6.75 → 6.39 ms/iter, within noise but never worse).

I considered Object.create(null) / __proto__: null for RESERVED_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:

Table Lookup Reachable?
RESERVED_WORDS text in RESERVED_WORDS yes — fixed here
UNARY_TEST_OPS UNARY_TEST_OPS[val] === 1 no — value-guarded
BINARY_TEST_OPS BINARY_TEST_OPS[nt.value] === 1 no — value-guarded
REDIRECT_OPS REDIRECT_OPS[t.value] ?? ">" no — keys are only lexer-produced operator strings, never user words

REDIRECT_OPS would 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.prototype member names parse as ordinary command names (toString, valueOf, constructor, hasOwnProperty, __proto__, isPrototypeOf)
  • an Object.prototype member name doesn't truncate the rest of the script

Full suite: 1443/1443 passing. tsc --noEmit, oxlint, and oxfmt --check clean on the touched files.

`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
mindlace force-pushed the worktree-fix-prototype-reserved-words branch from c5415a2 to 24c8440 Compare August 2, 2026 02:06
@webpro

webpro commented Aug 2, 2026

Copy link
Copy Markdown
Member

Oof, nasty. Thanks for the find + PR!

@webpro
webpro merged commit 22a56ac into webpro-nl:main Aug 2, 2026
1 check passed
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