Per design §3.2 line 359, all WAL CRC computations must use crc32c
(Castagnoli polynomial 0x82F63B78). The previous code used crc32.IEEE
(Ethernet/PNG polynomial 0xEDB88320) in 5 places. While the system was
self-consistent (encode + decode both used IEEE), it diverged from the
design spec and lost SSE4.2 hardware acceleration (the native CRC32
instruction only supports Castagnoli).
Changes:
- wal/crc32c.go (new): package-level crc32cTable = crc32.MakeTable(
crc32.Castagnoli). Central definition prevents future drift.
- wal/header.go: 2 ChecksumIEEE calls replaced with crc32.Checksum(
data, crc32cTable). Comment updated to reference design §3.2 line 341, 359.
- wal/record.go: 2 ChecksumIEEE calls replaced.
- wal/block_writer_test.go: 1 ChecksumIEEE call in test helper replaced.
- wal/crc32c_test.go (new): 4 regression guards:
- TestCRC32CStandardVector: RFC 3720 fixed vector (crc32c("123456789")
= 0xE3069283).
- TestCRC32CEdistinctFromIEEE: confirms IEEE produces different value.
- TestHeaderCRCUsesCastagnoli: direct assertion on stored header CRC
(catches paired encode/decode reversion that round-trip tests miss).
- TestPhysicalRecordCRCUsesCastagnoli: same for physical record CRC.
BREAKING CHANGE: WAL files written before this fix (with IEEE CRC)
cannot be read after this fix (expects crc32c). Phase 1 has not been
released, so no real data migration is needed. Production users
post-release would need to drain + re-create the database.
Developers pulling this change should delete any local Phase-1 WAL
directories (`rm -rf <db-dir>/segment-*.wal`) before running the code;
old IEEE-encoded WALs will fail recovery on local dev machines.
Verified: all existing round-trip tests pass (encode + decode both use
crc32c, still self-consistent). Full suite green including
go test -race ./... .
Audit context: docs/audit-3.2.md C1.
11 KiB
C1 修复方案:CRC 多项式从 IEEE 改为 crc32c
TL;DR
目标:把 WAL 所有 CRC 计算从
crc32.ChecksumIEEE(IEEE 802.3,多项式 0xEDB88320)改为crc32.Checksum(data, crc32cTable)(Castagnoli,多项式 0x82F63B78)。设计文档明确要求 crc32c,当前代码自洽但和规范脱钩。交付:
- 新建包级
crc32cTable变量(crc32.MakeTable(crc32.Castagnoli))- 5 处
ChecksumIEEE调用替换为Checksum(data, crc32cTable)- 1 个固定向量测试(防回归关键)
- 单次 commit
预估工时:1 小时 风险:低。改动局限在 5 处函数调用。Breaking change:旧 WAL 文件无法读,但 Phase 1 未 release
Context
Bug 摘要
5 处 CRC 计算用错多项式:
| 文件 | 行 | 用途 |
|---|---|---|
wal/header.go:44 |
encode WAL header CRC | |
wal/header.go:83 |
decode verify WAL header CRC | |
wal/record.go:30 |
encode physical record CRC | |
wal/record.go:55 |
decode verify physical record CRC | |
wal/block_writer_test.go:349 |
test helper |
当前都用 crc32.ChecksumIEEE,但设计要求 crc32c。
设计依据
docs/design.md §3.2 line 359(Physical Record 字段说明):
| crc32c | 校验
length + type + payload,用于识别 torn write、partial write 和数据损坏 |
WAL 格式中所有 CRC 都是 crc32c(Castagnoli),headerCRC 也应该一致。
为什么这是 bug(即使当前能跑)
- 不符合设计:文档代码不一致
- 失去硬件加速:Intel SSE4.2 的
CRC32指令只支持 Castagnoli 多项式 - 未来兼容性:跨实现 / 跨工具读写需要正确的 CRC
- 行业标准:ext4 / SQLite / RocksDB / LevelDB 都用 crc32c
Phase 1 修复成本最低
- Phase 1 未 release,没有真实数据需要迁移
- 越晚改,迁移成本越高(要维护 v1/v2 两套 CRC)
执行计划
Phase A:代码改动(15 分钟)
A.1 新建 wal/crc32c.go
package wal
import "hash/crc32"
// crc32cTable is the CRC-32 table using the Castagnoli polynomial (0x82F63B78
// reflected). Required by design §3.2 line 359 for all WAL CRC computations.
//
// Distinct from crc32.IEEE (Ethernet/PNG polynomial 0xEDB88320) — the two
// produce unrelated checksums for the same input. SSE4.2 native CRC32
// instruction only supports Castagnoli, so this table also enables hardware
// acceleration via Go's standard library internals.
var crc32cTable = crc32.MakeTable(crc32.Castagnoli)
必要 docstring:解释为什么单独抽这个变量(C1 bug 防回归),引用设计行号。属于 security-related 必要注释。
A.2 替换 5 处 ChecksumIEEE
wal/header.go:44:
// 改前:
h.HeaderCRC = crc32.ChecksumIEEE(buf[0:28])
// 改后:
h.HeaderCRC = crc32.Checksum(buf[0:28], crc32cTable)
wal/header.go:83:
// 改前:
gotCRC := crc32.ChecksumIEEE(data[0:28])
// 改后:
gotCRC := crc32.Checksum(data[0:28], crc32cTable)
wal/record.go:30:
// 改前:
crc := crc32.ChecksumIEEE(buf[4:])
// 改后:
crc := crc32.Checksum(buf[4:], crc32cTable)
wal/record.go:55:
// 改前:
expectedCRC := crc32.ChecksumIEEE(data[4 : 7+length])
// 改后:
expectedCRC := crc32.Checksum(data[4 : 7+length], crc32cTable)
wal/block_writer_test.go:349:
// 改前:
computedCRC := crc32.ChecksumIEEE(crcData)
// 改后:
computedCRC := crc32.Checksum(crcData, crc32cTable)
注释更新:wal/header.go:43 注释 // CRC32 IEEE over bytes 0–27 改成 // CRC32C over bytes 0–27 (per design §3.2 line 341, 359)。
Oracle 修订(bg_0323e6b9):原计划只引用 line 359,但 line 359 描述的是 physical record CRC,header CRC 应该引用 line 341(headerCRC 字段定义)。改成同时引用两条,或写"per design §3.2 CRC field spec"。
Phase B:测试(30 分钟)
B.1 固定向量测试(关键防回归)
新增 wal/crc32c_test.go:
package wal
import (
"hash/crc32"
"testing"
)
// Regression guard for C1: verify all WAL CRC uses Castagnoli polynomial
// (crc32c), not IEEE. Per design §3.2 line 359.
//
// The standard CRC-32C test vector from RFC 3720 Appendix B is the 9-byte
// ASCII string "123456789":
// - crc32c: 0xE3069283
// - crc32 IEEE: 0xCBF43926
//
// If anyone changes crc32cTable back to IEEE, this test fails immediately.
func TestCRC32CStandardVector(t *testing.T) {
got := crc32.Checksum([]byte("123456789"), crc32cTable)
const want = uint32(0xE3069283)
if got != want {
t.Errorf("crc32c('123456789') = 0x%X, want 0x%X (Castagnoli)", got, want)
}
}
// Negative regression: confirm IEEE would produce a DIFFERENT value. This
// catches the case where someone "fixes" crc32cTable to use IEEE by mistake.
func TestCRC32CEdistinctFromIEEE(t *testing.T) {
data := []byte("123456789")
ieee := crc32.ChecksumIEEE(data)
castagnoli := crc32.Checksum(data, crc32cTable)
if ieee == castagnoli {
t.Errorf("IEEE and Castagnoli produced the same CRC (impossible unless table is wrong); both = 0x%X", ieee)
}
}
B.2 直接断言 header CRC 用的是 crc32c(Oracle 新增)
Oracle 修订(bg_0323e6b9)NICE-TO-HAVE:现有
TestHeaderRoundtrip不能抓"encode + decode 同时改回 IEEE"(自洽)。需要直接断言存储的 CRC 值。
新增到 wal/header_test.go(或 crc32c_test.go):
// Regression guard for C1: header CRC must be computed with crc32c, not
// IEEE. TestHeaderRoundtrip is self-consistent (encode + decode use same
// polynomial), so a paired reversion to IEEE would pass it. This test
// directly asserts the stored CRC matches Castagnoli.
func TestHeaderCRCUsesCastagnoli(t *testing.T) {
hdr := &WalFileHeader{
BlockSize: 32 * 1024,
SegmentID: 42,
StartSequence: 100,
}
encoded := EncodeWalHeader(hdr)
want := crc32.Checksum(encoded[0:28], crc32cTable)
got := encoded[28] | uint32(encoded[29])<<8 | uint32(encoded[30])<<16 | uint32(encoded[31])<<24
if got != want {
t.Errorf("stored headerCRC = 0x%X, want crc32c value 0x%X", got, want)
}
// Sanity: confirm IEEE would produce a different value (catches paired reversion).
ieeeValue := crc32.ChecksumIEEE(encoded[0:28])
if got == ieeeValue {
t.Errorf("stored headerCRC = 0x%X matches IEEE value (C1 regression)", got)
}
}
同理 physical record 也加一个(可选,但便宜):
// Regression guard for C1: physical record CRC must be Castagnoli.
func TestPhysicalRecordCRCUsesCastagnoli(t *testing.T) {
payload := []byte("test-payload")
encoded := EncodePhysicalRecord(RecFull, payload)
// Record format: [crc u32][length u16][type u8][payload]
want := crc32.Checksum(encoded[4:7+len(payload)], crc32cTable)
got := encoded[0] | uint32(encoded[1])<<8 | uint32(encoded[2])<<16 | uint32(encoded[3])<<24
if got != want {
t.Errorf("stored record CRC = 0x%X, want crc32c value 0x%X", got, want)
}
ieeeValue := crc32.ChecksumIEEE(encoded[4 : 7+len(payload)])
if got == ieeeValue {
t.Errorf("stored record CRC = 0x%X matches IEEE value (C1 regression)", got)
}
}
B.3 现有测试自动通过(无需改动)
所有 round-trip 测试(encode → decode → verify)自洽:encode 和 decode 用同一个多项式。改成 crc32c 后两端都用 crc32c,仍然自洽。
需要确认:
wal/header_test.goround-trip 测试通过wal/record_test.goround-trip 测试通过wal/block_writer_test.goround-trip 测试通过wal/recover_test.go等端到端 recovery 测试通过db_test.go/db_e2e_test.go通过
Phase C:验证(15 分钟)
# 1. 编译
go build ./...
# 2. 重点测试(C1 新增)
go test ./wal -run 'TestCRC32C' -count=1 -v
# 3. wal 包全量
go test ./wal/... -count=1
# 4. 全仓
go test ./... -count=1
# 5. race
go test -race ./... -count=1
# 6. vet
go vet ./...
Phase D:Commit message draft
fix: use CRC-32C (Castagnoli) instead of IEEE for WAL (C1)
Per design §3.2 line 359, all WAL CRC computations must use crc32c
(Castagnoli polynomial 0x82F63B78). The previous code used crc32.IEEE
(Ethernet/PNG polynomial 0xEDB88320) in 5 places. While the system was
self-consistent (encode + decode both used IEEE), it diverged from the
design spec and lost SSE4.2 hardware acceleration (the native CRC32
instruction only supports Castagnoli).
Changes:
- wal/crc32c.go (new): package-level crc32cTable = crc32.MakeTable(
crc32.Castagnoli). Central definition prevents future drift.
- wal/header.go: 2 ChecksumIEEE calls replaced with crc32.Checksum(
data, crc32cTable). Comment updated to reference design §3.2 line 359.
- wal/record.go: 2 ChecksumIEEE calls replaced.
- wal/block_writer_test.go: 1 ChecksumIEEE call in test helper replaced.
- wal/crc32c_test.go (new): standard CRC-32C test vector (RFC 3720:
crc32c("123456789") = 0xE3069283) + negative test confirming IEEE
produces different value. These guards catch any future regression
to IEEE.
BREAKING CHANGE: WAL files written before this fix (with IEEE CRC)
cannot be read after this fix (expects crc32c). Phase 1 has not been
released, so no real data migration is needed. Production users
post-release would need to drain + re-create the database.
Developers pulling this change should delete any local Phase-1 WAL
directories (`rm -rf <db-dir>/segment-*.wal`) before running the code;
old IEEE-encoded WALs will fail recovery on local dev machines.
Verified: all existing round-trip tests pass (encode + decode both use
crc32c, still self-consistent). Full suite green including
go test -race ./... .
Audit context: docs/audit-3.2.md C1.
验收清单
- Phase A.1:
wal/crc32c.go存在,定义crc32cTable - Phase A.2:5 处
ChecksumIEEE全部替换为Checksum(data, crc32cTable) - Phase A.2:
wal/header.go:43注释更新引用设计行号 - Phase B.1:
TestCRC32CStandardVector+TestCRC32CEdistinctFromIEEE存在 go test ./wal/... -count=1全绿go test ./... -count=1全绿go test -race ./... -count=1全绿go vet ./...无新增警告- 单次 commit,message 引用 audit C1 + 标注 BREAKING CHANGE
不在本次范围内(后续 issue)
| 编号 | 为什么不放进来 |
|---|---|
| C7 | Put/Close 竞态,独立 |
| H1-H7 | 其他 High,独立 |
| WAL 格式版本号 | Phase 1 未 release,不需要 v2 迁移机制 |
修订记录
- v1(原始):C1 修复方案初稿,送 Momus 审
- v1.0(Momus 审核 bg_5a6543ac):[OKAY],无 blocking。验证全仓只有 5 处
ChecksumIEEE,无遗漏 - v1.1(Oracle 审核 bg_0323e6b9):approve(无 blocking)。3 个 nice-to-have 已采纳:
- 加直接断言测试(header / physical record CRC 用的是 crc32c 值,不是 IEEE)——
TestHeaderRoundtrip抓不到"encode + decode 同时改回 IEEE" - header.go 注释引用设计行号改成
line 341, 359(line 359 是 physical record,header CRC 应引 line 341) - commit message 加开发者提醒:删本地 Phase-1 WAL 目录
- 加直接断言测试(header / physical record CRC 用的是 crc32c 值,不是 IEEE)——