diff --git a/syntax/lexer.go b/syntax/lexer.go
index <HASH>..<HASH> 100644
--- a/syntax/lexer.go
+++ b/syntax/lexer.go
@@ -676,7 +676,7 @@ func (p *parser) newLit(r rune) {
 	if r <= utf8.RuneSelf {
 		p.litBs = p.litBuf[:1]
 		p.litBs[0] = byte(r)
-	} else {
+	} else if p.npos < len(p.bs) {
 		w := utf8.RuneLen(r)
 		p.litBs = append(p.litBuf[:0], p.bs[p.npos-w:p.npos]...)
 	}
diff --git a/syntax/parser_test.go b/syntax/parser_test.go
index <HASH>..<HASH> 100644
--- a/syntax/parser_test.go
+++ b/syntax/parser_test.go
@@ -785,6 +785,10 @@ var shellTests = []errorCase{
 		`if; then bar; fi; ;`,
 		`1:19: ; can only immediately follow a statement`,
 	},
+	{
+		"<<$\xc8",
+		`1:3: expansions not allowed in heredoc words`,
+	},
 }
 
 func checkError(in, want string, mode ParseMode) func(*testing.T) {
syntax: don't slice past p.bs in p.newLit

If we see encounter an error right before its call, p.npos might be set
to len(p.bs)<I>.