v0.4.3 prevent input stall on long or malformed sequence
This commit is contained in:
2 files changed
+173
-65
No files matched your search
@@ -57,6 +57,9 @@ type inputReader struct {
|
|||||||
paste []byte
|
paste []byte
|
||||||
pasteSeen time.Time
|
pasteSeen time.Time
|
||||||
|
|
||||||
|
// An over-long CSI sequence is being dropped up to its final byte
|
||||||
|
dropCSI bool
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
running bool
|
running bool
|
||||||
}
|
}
|
||||||
@@ -186,6 +189,19 @@ func (r *inputReader) parseInput(data []byte) int {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.dropCSI {
|
||||||
|
for i < n && data[i] >= 0x20 && data[i] <= 0x3f {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i < n {
|
||||||
|
r.dropCSI = false
|
||||||
|
if data[i] >= 0x40 && data[i] <= 0x7e {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if r.pasting {
|
if r.pasting {
|
||||||
end := bytes.Index(data[i:], pasteEnd)
|
end := bytes.Index(data[i:], pasteEnd)
|
||||||
if end < 0 {
|
if end < 0 {
|
||||||
@@ -321,7 +337,7 @@ func (r *inputReader) parseEscape(data []byte) (int, Event) {
|
|||||||
return 0, Event{}
|
return 0, Event{}
|
||||||
case bytes.HasPrefix(rest, pasteStart):
|
case bytes.HasPrefix(rest, pasteStart):
|
||||||
return 1, Event{Type: EventKey, Key: KeyEscape}
|
return 1, Event{Type: EventKey, Key: KeyEscape}
|
||||||
case rest[1] == '[' || rest[1] == 'O':
|
case rest[1] == 'O', rest[1] == '[' && csiLen(rest) != csiTyped:
|
||||||
n, ev := r.parseEscape(rest)
|
n, ev := r.parseEscape(rest)
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return 0, Event{}
|
return 0, Event{}
|
||||||
@@ -351,55 +367,79 @@ func (r *inputReader) parseEscape(data []byte) (int, Event) {
|
|||||||
return 2, Event{Type: EventKey, Key: KeyRune, Rune: rune(data[1]), Modifiers: ModAlt}
|
return 2, Event{Type: EventKey, Key: KeyRune, Rune: rune(data[1]), Modifiers: ModAlt}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, Event{}
|
// Alt+Backspace
|
||||||
|
if data[1] == 0x7f {
|
||||||
|
return 2, Event{Type: EventKey, Key: KeyBackspace, Modifiers: ModAlt}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alt+multibyte character; a byte no character starts with leaves Escape
|
||||||
|
size := utf8SeqLen(data[1])
|
||||||
|
if size == 0 {
|
||||||
|
return 1, Event{Type: EventKey, Key: KeyEscape}
|
||||||
|
}
|
||||||
|
if len(data) < 1+size {
|
||||||
|
return 0, Event{}
|
||||||
|
}
|
||||||
|
rn, n := decodeRune(data[1:])
|
||||||
|
return 1 + n, Event{Type: EventKey, Key: KeyRune, Rune: rn, Modifiers: ModAlt}
|
||||||
|
}
|
||||||
|
|
||||||
|
// csiMaxLen bounds a CSI sequence a terminal sends, with ESC [ and its final
|
||||||
|
// byte; a longer one is dropped up to its final byte
|
||||||
|
const csiMaxLen = 256
|
||||||
|
|
||||||
|
// csiLen results other than a length: more may be arriving, a byte no
|
||||||
|
// sequence holds came (ESC [ was typed, as Alt+[), or no final byte came
|
||||||
|
// within csiMaxLen
|
||||||
|
const (
|
||||||
|
csiPartial = 0
|
||||||
|
csiTyped = -1
|
||||||
|
csiOverlong = -2
|
||||||
|
)
|
||||||
|
|
||||||
|
// csiLen returns the length of the CSI sequence data starts with, or why it
|
||||||
|
// has none yet. Parameter and intermediate bytes run to a final byte as in
|
||||||
|
// ECMA-48, but for the Linux console's F1-F5, ESC [ [ A to E.
|
||||||
|
func csiLen(data []byte) int {
|
||||||
|
i := 2
|
||||||
|
if len(data) > 2 && data[2] == '[' {
|
||||||
|
i = 3
|
||||||
|
}
|
||||||
|
for ; i < len(data); i++ {
|
||||||
|
if i == csiMaxLen {
|
||||||
|
return csiOverlong
|
||||||
|
}
|
||||||
|
switch b := data[i]; {
|
||||||
|
case b >= 0x40 && b <= 0x7e:
|
||||||
|
return i + 1
|
||||||
|
case b < 0x20 || b > 0x3f:
|
||||||
|
return csiTyped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return csiPartial
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseCSI parses CSI sequence without allocation
|
// parseCSI parses CSI sequence without allocation
|
||||||
func (r *inputReader) parseCSI(data []byte) (int, Event) {
|
func (r *inputReader) parseCSI(data []byte) (int, Event) {
|
||||||
if len(data) < 3 {
|
n := csiLen(data)
|
||||||
|
switch {
|
||||||
|
case n == csiPartial:
|
||||||
return 0, Event{}
|
return 0, Event{}
|
||||||
|
case n == csiTyped:
|
||||||
|
return 2, Event{Type: EventKey, Key: KeyRune, Rune: '[', Modifiers: ModAlt}
|
||||||
|
case n == csiOverlong:
|
||||||
|
r.dropCSI = true
|
||||||
|
return csiMaxLen, Event{Type: EventKey, Key: KeyNone}
|
||||||
|
case data[2] == '<' && (data[n-1] == 'M' || data[n-1] == 'm'):
|
||||||
|
return n, parseSGRMouse(data[3:n-1], data[n-1])
|
||||||
}
|
}
|
||||||
|
|
||||||
// SGR mouse: ESC [ < Btn ; X ; Y M/m
|
if key, mod, ok := lookupCSI(data[2:n]); ok {
|
||||||
if data[2] == '<' {
|
return n, Event{Type: EventKey, Key: key, Modifiers: mod}
|
||||||
return r.parseSGRMouse(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
end := 2
|
|
||||||
maxScan := len(data)
|
|
||||||
if maxScan > 16 {
|
|
||||||
maxScan = 16
|
|
||||||
}
|
|
||||||
|
|
||||||
for end < maxScan {
|
|
||||||
b := data[end]
|
|
||||||
if (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') || b == '~' {
|
|
||||||
end++
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if b < 0x20 || b > 0x7e {
|
|
||||||
return 0, Event{}
|
|
||||||
}
|
|
||||||
end++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we found a terminator or ran out of data
|
|
||||||
if end <= 2 || end > maxScan {
|
|
||||||
return 0, Event{} // Incomplete
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check last byte is valid terminator
|
|
||||||
lastByte := data[end-1]
|
|
||||||
if !((lastByte >= 'A' && lastByte <= 'Z') || (lastByte >= 'a' && lastByte <= 'z') || lastByte == '~') {
|
|
||||||
return 0, Event{} // Incomplete, no terminator found
|
|
||||||
}
|
|
||||||
|
|
||||||
if key, mod, ok := lookupCSI(data[2:end]); ok {
|
|
||||||
return end, Event{Type: EventKey, Key: key, Modifiers: mod}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown but valid CSI syntax - consume and return KeyNone
|
// Unknown but valid CSI syntax - consume and return KeyNone
|
||||||
return end, Event{Type: EventKey, Key: KeyNone}
|
return n, Event{Type: EventKey, Key: KeyNone}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSS3 parses SS3 sequence without allocation, returns length even for unknown sequences
|
// parseSS3 parses SS3 sequence without allocation, returns length even for unknown sequences
|
||||||
@@ -483,31 +523,12 @@ func (r *inputReader) parseControl(b byte) Event {
|
|||||||
return Event{Type: EventKey, Key: KeyNone}
|
return Event{Type: EventKey, Key: KeyNone}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSGRMouse parses mouse SGR sequences
|
// parseSGRMouse parses mouse SGR sequences, ESC [ < Btn ; X ; Y M/m, from
|
||||||
func (r *inputReader) parseSGRMouse(data []byte) (int, Event) {
|
// the parameters and final byte; a malformed one is dropped as KeyNone
|
||||||
// Format: ESC [ < Btn ; X ; Y M/m
|
func parseSGRMouse(params []byte, final byte) Event {
|
||||||
// Minimum: ESC [ < 0 ; 1 ; 1 M = 10 bytes
|
|
||||||
if len(data) < 10 {
|
|
||||||
return 0, Event{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find terminator M or m
|
|
||||||
end := 3
|
|
||||||
for end < len(data) && end < 32 {
|
|
||||||
if data[end] == 'M' || data[end] == 'm' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
end++
|
|
||||||
}
|
|
||||||
if end >= len(data) || (data[end] != 'M' && data[end] != 'm') {
|
|
||||||
return 0, Event{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse: Btn;X;Y
|
|
||||||
params := data[3:end]
|
|
||||||
btn, x, y, ok := parseSGRParams(params)
|
btn, x, y, ok := parseSGRParams(params)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, Event{}
|
return Event{Type: EventKey, Key: KeyNone}
|
||||||
}
|
}
|
||||||
|
|
||||||
ev := Event{Type: EventMouse, MouseX: x - 1, MouseY: y - 1} // Convert to 0-indexed
|
ev := Event{Type: EventMouse, MouseX: x - 1, MouseY: y - 1} // Convert to 0-indexed
|
||||||
@@ -541,7 +562,7 @@ func (r *inputReader) parseSGRMouse(data []byte) (int, Event) {
|
|||||||
ev.MouseBtn = MouseBtnNone // Release with no specific button
|
ev.MouseBtn = MouseBtnNone // Release with no specific button
|
||||||
}
|
}
|
||||||
|
|
||||||
if data[end] == 'M' {
|
if final == 'M' {
|
||||||
if isMotion {
|
if isMotion {
|
||||||
if ev.MouseBtn != MouseBtnNone {
|
if ev.MouseBtn != MouseBtnNone {
|
||||||
ev.MouseAction = MouseActionDrag
|
ev.MouseAction = MouseActionDrag
|
||||||
@@ -567,7 +588,7 @@ func (r *inputReader) parseSGRMouse(data []byte) (int, Event) {
|
|||||||
ev.Modifiers |= ModCtrl
|
ev.Modifiers |= ModCtrl
|
||||||
}
|
}
|
||||||
|
|
||||||
return end + 1, ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSGRParams extracts btn, x, y from "Btn;X;Y" format
|
// parseSGRParams extracts btn, x, y from "Btn;X;Y" format
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package terminal
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"cmp"
|
"cmp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -152,6 +153,92 @@ func TestDoubleEscape(t *testing.T) {
|
|||||||
|
|
||||||
func key(r rune) Event { return Event{Type: EventKey, Key: KeyRune, Rune: r} }
|
func key(r rune) Event { return Event{Type: EventKey, Key: KeyRune, Rune: r} }
|
||||||
|
|
||||||
|
// parsed is what a reader sends for input arriving in chunks without pauses,
|
||||||
|
// and how many bytes it still holds back for the rest of a sequence
|
||||||
|
func parsed(chunks ...string) (events []Event, held int) {
|
||||||
|
r := &inputReader{eventCh: make(chan Event, 4096)}
|
||||||
|
r.pasteMode.Store(true)
|
||||||
|
var buf []byte
|
||||||
|
for _, c := range chunks {
|
||||||
|
buf = append(buf, c...)
|
||||||
|
buf = buf[r.parseInput(buf):]
|
||||||
|
}
|
||||||
|
close(r.eventCh)
|
||||||
|
for ev := range r.eventCh {
|
||||||
|
events = append(events, ev)
|
||||||
|
}
|
||||||
|
return events, len(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every sequence the key tables hold reads as its key, and the key after it
|
||||||
|
// as itself
|
||||||
|
func TestKnownSequencesKeepTheirKeys(t *testing.T) {
|
||||||
|
for _, s := range csiSequences {
|
||||||
|
want := []Event{{Type: EventKey, Key: s.key, Modifiers: s.mod}, key('x')}
|
||||||
|
if got, _ := parsed("\x1b[" + s.seq + "x"); !slices.Equal(got, want) {
|
||||||
|
t.Errorf("ESC [ %s: got %+v, want %+v", s.seq, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No input stops the reader: a reply of any length, Alt with Backspace or a
|
||||||
|
// character past ASCII, a malformed mouse report, or ESC [ before a byte no
|
||||||
|
// sequence holds (typed Alt+[) leave the keys after them read. An unknown or
|
||||||
|
// over-long sequence is dropped up to its final byte, by ECMA-48.
|
||||||
|
func TestNoInputStopsTheReader(t *testing.T) {
|
||||||
|
altBracket := Event{Type: EventKey, Key: KeyRune, Rune: '[', Modifiers: ModAlt}
|
||||||
|
enter := Event{Type: EventKey, Key: KeyEnter}
|
||||||
|
for _, c := range []struct {
|
||||||
|
name string
|
||||||
|
chunks []string
|
||||||
|
want []Event
|
||||||
|
}{
|
||||||
|
{"long reply", []string{"\x1b[?65;1;9;15;18;21;22cx"}, []Event{key('x')}},
|
||||||
|
{"split reply", []string{"\x1b[?65;1;9;", "15;18;21;22", "cx"}, []Event{key('x')}},
|
||||||
|
{"longest reply", []string{"\x1b[" + strings.Repeat("1", csiMaxLen-3) + "zx"}, []Event{key('x')}},
|
||||||
|
{"over-long reply", []string{"\x1b[" + strings.Repeat("1", csiMaxLen) + ";2", "3zx"}, []Event{key('x')}},
|
||||||
|
{"over-long cut", []string{"\x1b[" + strings.Repeat("1", csiMaxLen) + "\rx"}, []Event{enter, key('x')}},
|
||||||
|
{"unknown final", []string{"\x1b[2^x"}, []Event{key('x')}},
|
||||||
|
{"alt+backspace", []string{"\x1b\x7fx"}, []Event{{Type: EventKey, Key: KeyBackspace, Modifiers: ModAlt}, key('x')}},
|
||||||
|
{"alt+ö", []string{"\x1b\xc3", "\xb6x"}, []Event{{Type: EventKey, Key: KeyRune, Rune: 'ö', Modifiers: ModAlt}, key('x')}},
|
||||||
|
{"stray byte", []string{"\x1b\x80x"}, []Event{{Type: EventKey, Key: KeyEscape}, key('x')}},
|
||||||
|
{"alt+[ enter", []string{"\x1b[\rx"}, []Event{altBracket, enter, key('x')}},
|
||||||
|
{"alt+[ 1 é", []string{"\x1b[1éx"}, []Event{altBracket, key('1'), key('é'), key('x')}},
|
||||||
|
{"esc alt+[ enter", []string{"\x1b\x1b[\rx"}, []Event{{Type: EventKey, Key: KeyEscape, Modifiers: ModAlt}, key('['), enter, key('x')}},
|
||||||
|
{"bad mouse", []string{"\x1b[<0;1;1;1Mx"}, []Event{key('x')}},
|
||||||
|
{"mouse", []string{"\x1b[<0;5;3M\x1b[<0;5;3m"}, []Event{
|
||||||
|
{Type: EventMouse, MouseX: 4, MouseY: 2, MouseBtn: MouseBtnLeft, MouseAction: MouseActionPress},
|
||||||
|
{Type: EventMouse, MouseX: 4, MouseY: 2, MouseBtn: MouseBtnLeft, MouseAction: MouseActionRelease}}},
|
||||||
|
} {
|
||||||
|
if got, held := parsed(c.chunks...); !slices.Equal(got, c.want) || held != 0 {
|
||||||
|
t.Errorf("%s: got %+v holding %d, want %+v", c.name, got, held, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whatever bytes arrive, however reads split them, the reader sends the same
|
||||||
|
// events, holds back no more than ESC and the longest sequence, and holds
|
||||||
|
// nothing once four DEL, which end any sequence or character, and a paste
|
||||||
|
// end follow
|
||||||
|
func FuzzInputNeverStalls(f *testing.F) {
|
||||||
|
for _, s := range []string{"\x1b[?65;1;9;15;18;21;22cx", "\x1b\x7fx", "\x1b\xc3\xb6", "\x1b[\rx", "\x1b[<0;1;1;1Mx", "\x1b\x1b[A", "a\x1b[200~\x1b[A\x1b[201~b"} {
|
||||||
|
f.Add(s, uint(len(s)/2))
|
||||||
|
}
|
||||||
|
f.Fuzz(func(t *testing.T, in string, cut uint) {
|
||||||
|
cut %= uint(len(in)) + 1
|
||||||
|
whole, held := parsed(in)
|
||||||
|
if split, _ := parsed(in[:cut], in[cut:]); !slices.Equal(whole, split) {
|
||||||
|
t.Fatalf("%q: whole %+v, split at %d %+v", in, whole, cut, split)
|
||||||
|
}
|
||||||
|
if held > csiMaxLen+1 {
|
||||||
|
t.Fatalf("%q: holds %d bytes", in, held)
|
||||||
|
}
|
||||||
|
if _, held := parsed(in, "\x7f\x7f\x7f\x7f\x1b[201~"); held != 0 {
|
||||||
|
t.Fatalf("%q: stalled holding %d bytes", in, held)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// input is what a reader is given, in chunks, and the events it must send
|
// input is what a reader is given, in chunks, and the events it must send
|
||||||
type input struct {
|
type input struct {
|
||||||
off bool
|
off bool
|
||||||
|
|||||||
Reference in new issue
Block a user