fix: flush records queued before an S3 Select error message

The writer goroutine selects between the record channel and the error
channel at random. When Evaluate had just queued a record and then reported
an error, the error case could win first; it flushed only the staging
buffer, so the queued record was dropped in the exit drain and the client
saw the error without the records that preceded it. Stage whatever is queued
before flushing and writing the error. The CVE-2026-39414 regression test
asserted this contract and failed once under the race detector in CI; a new
unit test exercises the ordering directly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QwirCh4nsrJCQp2WaXoVLK
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-09-03 06:43:49 +08:00
parent 94fbb6df6d
commit 3598c4305d
2 changed files with 79 additions and 18 deletions
+31 -18
View File
@@ -295,7 +295,15 @@ func (writer *messageWriter) start() {
select {
case data := <-writer.errCh:
quitFlag = true
// Flush collected records before sending error message
// A record accepted by SendRecord may still be queued when the
// error arrives, because select picks between the two channels
// at random. Stage it first so every record produced before the
// error precedes the error message instead of being dropped.
for len(writer.payloadCh) > 0 {
if !writer.stageRecord(<-writer.payloadCh) {
break
}
}
if !writer.flushRecords() {
break
}
@@ -316,23 +324,8 @@ func (writer *messageWriter) start() {
break
}
writer.write(endMessage)
} else {
for payload.Len() > 0 {
copiedLen := copy(writer.payloadBuffer[writer.payloadBufferIndex:], payload.Bytes())
writer.payloadBufferIndex += copiedLen
payload.Next(copiedLen)
// If buffer is filled, flush it now!
freeSpace := bufLength - writer.payloadBufferIndex
if freeSpace == 0 {
if !writer.flushRecords() {
quitFlag = true
break
}
}
}
bufPool.Put(payload)
} else if !writer.stageRecord(payload) {
quitFlag = true
}
case <-recordStagingTicker.C:
@@ -368,6 +361,26 @@ func (writer *messageWriter) start() {
}
}
// stageRecord copies a record into the payload buffer, flushing whenever
// the buffer fills, and returns the buffer to the pool. It reports false when
// a flush failed.
func (writer *messageWriter) stageRecord(payload *bytes.Buffer) bool {
defer bufPool.Put(payload)
for payload.Len() > 0 {
copiedLen := copy(writer.payloadBuffer[writer.payloadBufferIndex:], payload.Bytes())
writer.payloadBufferIndex += copiedLen
payload.Next(copiedLen)
// If buffer is filled, flush it now!
if bufLength-writer.payloadBufferIndex == 0 {
if !writer.flushRecords() {
return false
}
}
}
return true
}
// Sends a single whole record.
func (writer *messageWriter) SendRecord(payload *bytes.Buffer) error {
select {