From 595aea6b64d61b9047b9180ee8586c88f7f92986 Mon Sep 17 00:00:00 2001 From: Jae Gangemi Date: Sun, 19 Apr 2026 15:25:05 -0600 Subject: [PATCH 1/2] fix: flush SLIP reader after ReadFlash to prevent stale data corruption - add slipReader.reset() to clear leftover buffer - update flushInput() to also reset SLIP reader state - call flushInput() after ReadFlash's raw block protocol completes - add public FlushInput() method for callers reusing connections - rename GetMD5 to FlashMD5 ReadFlash uses a non-standard SLIP protocol (block reads + ACKs + MD5 frame) that can leave stale bytes in the SLIP reader's leftover buffer. These bytes corrupt subsequent command responses when the flasher connection is reused across multiple operations. --- pkg/espflasher/flasher.go | 17 ++++++++++--- pkg/espflasher/flasher_test.go | 4 ++-- pkg/espflasher/protocol.go | 3 ++- pkg/espflasher/protocol_test.go | 42 +++++++++++++++++++++++++++++++++ pkg/espflasher/slip.go | 5 ++++ pkg/espflasher/slip_test.go | 20 ++++++++++++++++ tools/update-stubs.go | 8 +++++-- 7 files changed, 91 insertions(+), 8 deletions(-) diff --git a/pkg/espflasher/flasher.go b/pkg/espflasher/flasher.go index 32ea28a..20adc2d 100644 --- a/pkg/espflasher/flasher.go +++ b/pkg/espflasher/flasher.go @@ -178,6 +178,12 @@ func (f *Flasher) Close() error { return f.port.Close() } +// FlushInput discards any unread data from the serial port and SLIP reader. +// Useful when reusing a flasher connection across multiple operations. +func (f *Flasher) FlushInput() { + f.conn.flushInput() +} + // reopenPort closes and reopens the serial port after a USB device // re-enumeration. TinyUSB CDC devices may briefly disappear during reset. func (f *Flasher) reopenPort() error { @@ -799,9 +805,9 @@ func (f *Flasher) GetSecurityInfo() (*SecurityInfo, error) { return f.readSecurityInfo() } -// GetMD5 returns the MD5 hash of a flash region. +// FlashMD5 returns the MD5 hash of a flash region. // Requires the stub loader to be running. -func (f *Flasher) GetMD5(offset, size uint32) (string, error) { +func (f *Flasher) FlashMD5(offset, size uint32) (string, error) { if !f.conn.isStub() { return "", &UnsupportedCommandError{Command: "flash MD5 (requires stub)"} } @@ -828,7 +834,12 @@ func (f *Flasher) ReadFlash(offset, size uint32) ([]byte, error) { return nil, err } - return f.conn.readFlash(offset, size) + data, err := f.conn.readFlash(offset, size) + // Clear any stale data left in the serial buffer and SLIP reader + // after the raw block-read protocol. Without this, leftover bytes + // can corrupt subsequent command responses. + f.conn.flushInput() + return data, err } // Reset performs a hard reset of the device, causing it to run user code. diff --git a/pkg/espflasher/flasher_test.go b/pkg/espflasher/flasher_test.go index 99b9dd0..a2f1032 100644 --- a/pkg/espflasher/flasher_test.go +++ b/pkg/espflasher/flasher_test.go @@ -434,11 +434,11 @@ func TestFlashSizeFromJEDECMatchesChipSizes(t *testing.T) { } } -func TestGetMD5RequiresStub(t *testing.T) { +func TestFlashMD5RequiresStub(t *testing.T) { mock := &mockConnection{} mock.stubMode = false // ROM mode f := &Flasher{conn: mock, chip: chipDefs[ChipESP32]} - _, err := f.GetMD5(0, 1024) + _, err := f.FlashMD5(0, 1024) if err == nil { t.Fatal("expected error when stub is not running") } diff --git a/pkg/espflasher/protocol.go b/pkg/espflasher/protocol.go index 1c25e81..e1296bb 100644 --- a/pkg/espflasher/protocol.go +++ b/pkg/espflasher/protocol.go @@ -630,9 +630,10 @@ func (c *conn) flashWriteSize() uint32 { return flashWriteSizeROM } -// flushInput discards any unread data from the serial port. +// flushInput discards any unread data from the serial port and SLIP reader. func (c *conn) flushInput() { c.port.ResetInputBuffer() //nolint:errcheck + c.reader.reset() } // eraseTimeoutForSize calculates an appropriate timeout for erase operations. diff --git a/pkg/espflasher/protocol_test.go b/pkg/espflasher/protocol_test.go index b2cd3cd..dcef2d3 100644 --- a/pkg/espflasher/protocol_test.go +++ b/pkg/espflasher/protocol_test.go @@ -737,3 +737,45 @@ func TestReadFlashParameterValidation(t *testing.T) { t.Errorf("cmdReadFlash opcode mismatch") } } + +// flushTrackingPort embeds mockPort and records ResetInputBuffer calls. +type flushTrackingPort struct { + mockPort + resetCount int +} + +func (f *flushTrackingPort) ResetInputBuffer() error { + f.resetCount++ + return nil +} + +func TestConnFlushInputResetsPortAndReader(t *testing.T) { + port := &flushTrackingPort{} + c := newConn(port) + c.reader.leftover = []byte{0xDE, 0xAD, 0xBE, 0xEF} + + c.flushInput() + + if port.resetCount != 1 { + t.Errorf("ResetInputBuffer called %d times, want 1", port.resetCount) + } + if c.reader.leftover != nil { + t.Errorf("reader.leftover = %X after flushInput, want nil", c.reader.leftover) + } +} + +func TestFlasherFlushInputDelegatesToConn(t *testing.T) { + port := &flushTrackingPort{} + c := newConn(port) + c.reader.leftover = []byte{0x01, 0x02} + f := &Flasher{conn: c} + + f.FlushInput() + + if port.resetCount != 1 { + t.Errorf("ResetInputBuffer called %d times, want 1", port.resetCount) + } + if c.reader.leftover != nil { + t.Errorf("reader.leftover not cleared by Flasher.FlushInput") + } +} diff --git a/pkg/espflasher/slip.go b/pkg/espflasher/slip.go index b991df6..0a7b367 100644 --- a/pkg/espflasher/slip.go +++ b/pkg/espflasher/slip.go @@ -77,6 +77,11 @@ func newSlipReader(port serial.Port) *slipReader { return &slipReader{port: port} } +// reset clears any buffered data from the SLIP reader. +func (r *slipReader) reset() { + r.leftover = nil +} + // ReadFrame reads a single SLIP-framed packet from the serial port. // It blocks until a complete frame is received or the timeout expires. func (r *slipReader) ReadFrame(timeout time.Duration) ([]byte, error) { diff --git a/pkg/espflasher/slip_test.go b/pkg/espflasher/slip_test.go index 948837e..b1f0299 100644 --- a/pkg/espflasher/slip_test.go +++ b/pkg/espflasher/slip_test.go @@ -155,3 +155,23 @@ t.Errorf("raw 0xC0 found at inner byte %d in encoded data %X", i, encoded) } } } + +func TestSlipReaderResetClearsLeftover(t *testing.T) { + r := newSlipReader(nil) + r.leftover = []byte{0xDE, 0xAD, 0xBE, 0xEF} + + r.reset() + + if r.leftover != nil { + t.Errorf("leftover after reset = %X, want nil", r.leftover) + } +} + +func TestSlipReaderResetIsIdempotent(t *testing.T) { + r := newSlipReader(nil) + r.reset() + r.reset() + if r.leftover != nil { + t.Errorf("leftover after double reset = %X, want nil", r.leftover) + } +} diff --git a/tools/update-stubs.go b/tools/update-stubs.go index ccde755..7e42fe5 100644 --- a/tools/update-stubs.go +++ b/tools/update-stubs.go @@ -54,7 +54,9 @@ func download(url, dest string) error { if err != nil { return err } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if resp.StatusCode != http.StatusOK { return fmt.Errorf("HTTP %s for %s", resp.Status, url) @@ -64,7 +66,9 @@ func download(url, dest string) error { if err != nil { return err } - defer f.Close() + defer func() { + _ = f.Close() + }() _, err = io.Copy(f, resp.Body) return err From bbfebc8006938cffbc167b81dc417626bcfb9bef Mon Sep 17 00:00:00 2001 From: Jae Gangemi Date: Fri, 24 Apr 2026 09:01:13 -0600 Subject: [PATCH 2/2] refactor: rename FlashMD5 to GetFlashMD5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #34 — FlashMD5 suggested flashing behavior; GetFlashMD5 makes the read intent explicit. --- pkg/espflasher/flasher.go | 4 ++-- pkg/espflasher/flasher_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/espflasher/flasher.go b/pkg/espflasher/flasher.go index 20adc2d..1280a65 100644 --- a/pkg/espflasher/flasher.go +++ b/pkg/espflasher/flasher.go @@ -805,9 +805,9 @@ func (f *Flasher) GetSecurityInfo() (*SecurityInfo, error) { return f.readSecurityInfo() } -// FlashMD5 returns the MD5 hash of a flash region. +// GetFlashMD5 returns the MD5 hash of a flash region. // Requires the stub loader to be running. -func (f *Flasher) FlashMD5(offset, size uint32) (string, error) { +func (f *Flasher) GetFlashMD5(offset, size uint32) (string, error) { if !f.conn.isStub() { return "", &UnsupportedCommandError{Command: "flash MD5 (requires stub)"} } diff --git a/pkg/espflasher/flasher_test.go b/pkg/espflasher/flasher_test.go index a2f1032..e344471 100644 --- a/pkg/espflasher/flasher_test.go +++ b/pkg/espflasher/flasher_test.go @@ -438,7 +438,7 @@ func TestFlashMD5RequiresStub(t *testing.T) { mock := &mockConnection{} mock.stubMode = false // ROM mode f := &Flasher{conn: mock, chip: chipDefs[ChipESP32]} - _, err := f.FlashMD5(0, 1024) + _, err := f.GetFlashMD5(0, 1024) if err == nil { t.Fatal("expected error when stub is not running") }