Skip to content

fix(wal): use io.ReadFull instead of f.Read for WAL header reads - #484

Merged
xe-nvdk merged 5 commits into
mainfrom
fix/wal-readfull-header
Jun 5, 2026
Merged

xe-nvdk merged 5 commits into
mainfrom
fix/wal-readfull-header

Conversation

@xe-nvdk

@xe-nvdk xe-nvdk commented Jun 5, 2026

Copy link
Copy Markdown
Member

Fixes #341. The WAL reader used f.Read to read fixed-size headers. f.Read may return fewer bytes than the buffer size without error, causing partial reads that cascade into misaligned subsequent reads. Both header reads now use io.ReadFull.

  • WAL file header (7 bytes): f.Read → io.ReadFull
  • WAL entry header (16 bytes): f.Read → io.ReadFull, io.ErrUnexpectedEOF mapped to io.EOF
  • Payload read: = → := (n was previously declared by the removed header f.Read)

All 40 WAL tests pass. gofmt/go vet clean. No configuration-interaction risk.

Ignacio Van Droogenbroeck added 2 commits June 5, 2026 15:24
f.Read may return fewer bytes than the buffer size without error,
causing partial reads that cascade into misaligned subsequent reads.
Replace both WAL file header and entry header reads with io.ReadFull
which guarantees the buffer is filled completely or returns an error.

Fixes #341

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the WAL reader to use io.ReadFull instead of f.Read for fixed-size file and entry header reads to prevent partial reads. However, the review feedback highlights two critical issues: first, swallowing all read errors during the file header read is dangerous because transient I/O errors could be mistaken for an empty file, potentially leading to data loss; second, mapping io.ErrUnexpectedEOF to io.EOF when reading entry headers silently ignores truncated or corrupted entries at the end of the WAL file, bypassing corruption logging and metrics.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread internal/wal/reader.go Outdated
Comment thread internal/wal/reader.go Outdated
…ader read

- File header: separate io.EOF/ErrUnexpectedEOF (short/empty file, benign)
  from actual I/O errors (disk failure, must surface).
- Entry header: route io.EOF cleanly to caller (0 bytes read = end of WAL)
  and io.ErrUnexpectedEOF (truncated entry) through corruption logging
  instead of silently treating it as clean EOF.
@xe-nvdk

xe-nvdk commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@gemini-code-assist please review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request improves the robustness of the WAL reader by replacing f.Read with io.ReadFull for fixed-size file and entry header reads, preventing potential corruption from partial reads. The review feedback suggests optimizing performance and reducing heap allocations by using stack-allocated fixed-size arrays instead of slices for the header buffers.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread internal/wal/reader.go Outdated
Comment thread internal/wal/reader.go Outdated
Replace make([]byte, WALFileHeaderSize) and make([]byte, WALEntryHeaderSize)
with var header [WALFileHeaderSize]byte / [WALEntryHeaderSize]byte.
Both sizes (7, 16) are known at compile time — no heap alloc needed in
the hot recovery path.
@xe-nvdk

xe-nvdk commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@gemini-code-assist please review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the WAL reader to use io.ReadFull with fixed-size arrays for reading file and entry headers, preventing partial reads from corrupting subsequent entries. The review feedback highlights a critical issue in readEntry where io.ErrUnexpectedEOF is not handled. If a crash results in a partially written entry header, the reader will fail with a hard error instead of gracefully stopping at the end of the valid entries. A code suggestion is provided to map io.ErrUnexpectedEOF to io.EOF to ensure robust recovery.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread internal/wal/reader.go
io.ReadFull returns three outcomes:
  n=16, err=nil                  → full header, proceed
  n=0,  err=io.EOF               → clean end of WAL, stop
  1≤n≤15, err=io.ErrUnexpectedEOF → crash during write, truncated header

Without the io.ErrUnexpectedEOF→io.EOF mapping, case 3 hits fmt.Errorf,
the caller's err==io.EOF check fails, and continue tries to read the
next entry at a misaligned offset — the exact cascading corruption bug
#341 describes. The file offset is mid-header and unrecoverable.

The previous review incorrectly claimed io.EOF from io.ReadFull was dead
code. It is not: ReadAtLeast preserves io.EOF when 0 bytes are read.
@xe-nvdk

xe-nvdk commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@gemini-code-assist please review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the WAL reader to use io.ReadFull for fixed-size file and entry header reads to prevent partial reads from causing misaligned subsequent reads. It also updates the release notes to document this change. Feedback on the changes suggests that mapping io.ErrUnexpectedEOF to io.EOF when reading entry headers will cause truncated entry headers (e.g., from a crash mid-write) to be silently ignored instead of being flagged as corrupted. It is recommended to let io.ErrUnexpectedEOF propagate as an error so the caller can properly log and track the corruption.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread internal/wal/reader.go
@xe-nvdk

xe-nvdk commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

mapping io.ErrUnexpectedEOF to io.EOF when reading entry headers will cause truncated entry headers (e.g., from a crash mid-write) to be silently ignored instead of being flagged as corrupted.

Respectfully, this recommendation would reintroduce the cascading misalignment bug #341 describes. Here is the trace:

If io.ErrUnexpectedEOF propagates as an error instead of being mapped to io.EOF:

readEntry → fmt.Errorf("...: %w", io.ErrUnexpectedEOF)
caller:   err == io.EOF?  → NO (wrapped in fmt.Errorf)
caller:   err != nil → CorruptedEntries++ → continue
           ↑ file offset is 1–15 bytes into a partial header
           ↑ next readEntry parses garbage as payloadLen/timestamp/checksum
           ↑ CASCADING MISALIGNMENT — the exact bug #341 exists to fix

The file offset after a partial io.ReadFull on the entry header is unrecoverable. There is no way to skip forward — we do not know the payload length because the header is incomplete. The only correct action is to stop reading.

The io.EOF mapping matches the original code behavior: the original f.Read code checked err == io.EOF before the n < WALEntryHeaderSize check. A f.Read returning (10, io.EOF) would match err == io.EOF first and return io.EOF — treating a truncated header at EOF as clean end-of-file, not as a corrupted entry.

Corruption tracking (CorruptedEntries++) is meant for recoverable corruption within a valid entry (checksum mismatches), not for unrecoverable partial headers at crash points.

@xe-nvdk
xe-nvdk merged commit f0f68f2 into main Jun 5, 2026
5 checks passed
@xe-nvdk
xe-nvdk deleted the fix/wal-readfull-header branch June 5, 2026 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

medium(wal): Partial WAL header uses f.Read instead of io.ReadFull

1 participant