firstMatching silently skips a record that fails its checksum
Observed behavior
An ordered consumer silently skips a file-store record with an invalid checksum and successfully delivers the next record, without reporting an error or sequence gap.
On a standalone server, I created stream T with subjects t.>, file storage, and no per-subject limit. Two publishes were acknowledged:
Aont.a, sequence 1Bont.b, sequence 2
After stopping the server, I flipped one bit in sequence 1's trailing 8-byte record checksum. This changes no length, sequence, timestamp, or subject bytes. After restarting, an ordered consumer with filter t.> and DeliverAll delivered only sequence 2. No error or gap was reported, and the ordered consumer's gap detection did not trigger.
Output:
A=seq 1 B=seq 2
delivered seq 2 subject t.b data "B"
delivered=[2]The same enumeration on an undamaged stream delivers both records.
Source inspection points to the following path in server/filestore.go:
indexCacheBufperforms record-length sanity checks, deferring the per-message checksum validation until the record is read.msgFromBufExreturnserrBadMsg{"invalid checksum"}for the damaged record.msgBlock.firstMatchingreturns early forerrPartialCacheanderrNoCache, but continues past othercacheLookuperrors, including the checksum failure.msgBlock.firstMatchingMultialso discards errors at its threecacheLookupsites. Two continue on any error; the third ignores the failed lookup and returns true.
These functions were identical in v2.15.0 and the main branch checked on 2026-09-21.
Skipping an unreadable record may be an intentional resilience choice. The problem is that the enumeration succeeds without making the skipped record visible to the caller.
Expected behavior
An unreadable record should not silently disappear from a successful enumeration.
The consumer should receive an explicit error or a reported sequence gap, or both records should be delivered if recovery is possible. If skipping is intentional, there should be an observable indication of the skipped record.
Server and client version
Server: v2.15.0 Client: nats.go V1.53.1
Host environment
Linux/arm64, server embedded in a go test process. File storage in a temporary directory created by t.TempDir(). Standalone, single server. No clustering.
Steps to reproduce
Use repro_test.go (in zip above) with this go.mod:
module repro
go 1.26.5
require (
github.com/nats-io/nats-server/v2 v2.15.0
github.com/nats-io/nats.go v1.53.1
)Run: go test -v -count=1 ./...
The reproduction performs these steps:
- Start an embedded server.
- Create stream T with subjects t.> and file storage, leaving other settings at their defaults
- Publish A to t.a and B to t.b, receiving acknowledgments for sequences 1 and 2
- Stop the server
- Walk
<StoreDir>/jetstream/$G/streams/T/msgs/*.blk, locate the record for sequence 1, and XOR its last byte with 0x01. This changes only one bit of its trailing checksum. - Restart the server using the same store directory
- Create the subscription:
js.SubscribeSync("t.>", nats.OrderedConsumer(), nats.DeliverAll())- Collect deliveries and check for errors or a reported gap. The defect test fails by design when the issue reproduces. The undamaged control passes:
--- FAIL: TestEnumerationSilentlySkipsUnreadableRecord
seq 1 was never delivered and no error or gap was reported ... the walk returned seq 2 successfully
--- PASS: TestControlUndamagedDeliversBothPossibly related
#7549 reports acknowledged writes lost after low-level .blk damage on a replicated stream. The symptoms may be related, but this reproduction does not establish a shared cause.
Submission acknowledgment
- I am a human being writing in my own words and not an AI agent. I will not use an AI agent to communicate on my behalf in this issue, either directly or via copy-paste.
Source: nats-io/nats-server