Page Checksums
Every page carries a checksum of itself, so a damaged page is an error instead of a wrong answer
Give every page a 4-byte trailer: the CRC32 of the other 4092 bytes. Write it whenever a page goes to the file, check it whenever a page comes back, and refuse to use a page that does not match. Move the end of every slotted page from byte 4096 to 4092 to make room, and add db verify, which checks the whole file.
- Silent corruption: a disk, a cable or a bug can change a byte, and nothing notices unless something checks
- A checksum is a short summary of the bytes: change any one of them and the summary changes
- Written on the way out, checked on the way in: the checksum belongs to the file's copy of a page, not to the page in memory
- Detection, not repair: a bad page becomes a loud error, never a wrong row
- A format change has a cost: every layout that ran to the end of the page moves four bytes
Why this stage exists
Every stage so far has trusted the file. The program writes a page, reads it back later, and uses whatever bytes come back. Usually they are the bytes it wrote. Not always.
Disks return the wrong sector. A cable flips a bit. A power cut leaves half a page new and half old. A bug in some other program, or in this one, writes over the wrong offset. None of these announce themselves. The program reads the damaged page, finds a slot count that is a little off or a key that is not the one it stored, and does what those bytes say: returns a row that was never inserted, skips one that was, or follows a child pointer into the middle of nowhere. The worst outcome is not a crash. It is an answer that looks right and is not.
This stage makes damage loud. Every page carries a checksum of itself, written each time the page goes to the file and checked each time it comes back. A page that does not match is an error the moment it is read, before any of its bytes are believed.
It comes last in Flexible Storage because it changes a page layout, the subject of this part, and it comes before the write-ahead log because recovering from a crash starts with knowing which pages a crash has torn.
What changes: every page ends with a trailer
The last four bytes of every page, 4092 to 4095, are its trailer: the CRC32 of the page's other 4092 bytes, stored little-endian like every integer in the file.
- …bytes 0–3 (4 bytes) · the last bytes of the header page: zeros, bytes 4088 to 4091
- trailerbytes 4–7 (4 bytes) · CRC32 of bytes 0 to 4091: 0xdf4774a3, little-endian
Every page has one, whatever it holds: the header, the catalog and table leaves, internal nodes, slotted pages made by page init, and a page that only pager write ever touched. The program does not need to know what a page is to check it, which is the point: a damaged header must be caught before the program reads the header to find out what everything else is.
When it is written. Whenever a page goes to the file, the trailer is computed from the bytes the page holds at that moment and written with it. In this part that is the end of the session. The copy of a page in memory is never checked and its last four bytes mean nothing; only the file's copy has to be right.
When it is checked. Whenever a page comes from the file, the first time a session needs it, its trailer is checked before any byte of it is used. A page that does not match is 00609. The command that needed it prints that one line and nothing else: no rows, no partial result, and nothing changes. The bad page is not kept in memory, so the next command that needs it reads it and fails again, and a command that does not need it works as before. Opening checks page 0 the same way, after its magic, version and page size: a damaged header is 00609, the only line, with exit status 1.
CRC32 is not cryptography. It catches accidents, every change of a single byte and every burst of changes up to 32 bits long, but anyone who wants to change a page on purpose can recompute it. PostgreSQL uses a 16-bit checksum in each page header; InnoDB keeps its checksum in a trailer at the end of each page, as this tutorial does, and SQLite can with its checksum extension.
What changes: pages end at 4092
A layout that ran to the last byte of the page has to stop four bytes earlier. Internal nodes, the header's fields and the text pager write stores never come near the end, so they do not change. Slotted pages do: their records grew from byte 4096 down, and now they grow from 4092.
So:
- An empty slotted page has a record start of 4092 and
free_space=4076. The first record ends at byte 4092. - A new root leaf is
01 01, zeros up to byte 14, thenfc 0f: a record start of 4092. - A leaf holds 4076 bytes of cells and slots. Rows of 21 bytes still fit 140 to a leaf, since
4076 / 29rounds down to 140 as4080 / 29did, but rows whose cells and slots take 510 bytes now fit seven, not eight. - Any four cells still fit in an empty leaf: four of the largest take 4032 bytes, well within 4076.
Earlier cases that pinned 4096 or 4080, or compared a whole page of the file byte for byte, are retired here by name in the manifest. What they checked about SQL still has to pass.
What changes: db verify
db verify checks the whole file, and is how you find out which pages are damaged without tripping over them one statement at a time. For each page, in order, a page already in memory counts as good: it was checked when it was read, or made in this session and not written yet. Every other page is read and checked as any read is, so a good page stays in memory afterwards and a bad one does not. It changes nothing, and on a file with no pages it reports none and creates nothing.
The algorithm in words
Compute a CRC32
The same checksum zlib, PNG and Ethernet use, so you can test yours against any of them.
- 1.Start from 0xFFFFFFFF. For each byte, combine it into the low 8 bits, then eight times: shift right one bit, and when the bit shifted out was 1, combine the result with 0xEDB88320.
- 2.Invert every bit at the end. The CRC32 of the nine ASCII bytes 123456789 is 0xCBF43926: check yours against it before using it anywhere else.
- 3.A table of the 256 possible results for one byte makes it eight times faster; the answers are the same.
Seal a page on its way to the file
The trailer describes the bytes as they are written, never as they were.
- 1.Whenever a page is written to the file, compute the CRC32 of its bytes 0 to 4091 and store it in bytes 4092 to 4095, little-endian, in the copy being written.
- 2.Every page, whatever it holds: the header, catalog and table pages, slotted pages, and pages that only pager write touched.
- 3.Nothing checks the copy in memory. Only the file's copy has to be right.
Check a page on its way back
No byte of a page from the file is used before its trailer says it is whole.
- 1.Whenever a page is read from the file, compute the CRC32 of its bytes 0 to 4091 and compare it with its trailer.
- 2.A mismatch is 00609 for the command that needed the page: it prints that one line, prints no rows and changes nothing. The page is not kept in memory, so the next command that needs it reads it and checks it again.
- 3.On open, check page 0 after its magic, version and page size: a mismatch there is 00609, the only line, exit status 1, the file untouched.
End every layout at 4092
The last four bytes of a page belong to the trailer.
- 1.A slotted page's records end at byte 4092. Its record start is 4092 when it has no records, and its free space is counted from 4092.
- 2.An empty page has 4076 bytes free, and a new root leaf records a start of 4092, the bytes fc 0f.
- 3.The header's fields, internal nodes and the text of pager write never come near byte 4092, so they do not change.
Verify the file
db verify checks every page and names the bad ones.
- 1.For each page of the file, in order: a page already in memory counts as good; any other page is read and checked as any read is.
- 2.Print the number of pages and of bad ones, then one line per bad page. Change nothing.
Output contract for this stage
The full definition is on the output contract page, section 20.
| Input | Output |
|---|---|
| reading a page whose trailer does not match | [ERROR:00609] <message>, and nothing else from that command |
| opening a file whose page 0 does not match | [ERROR:00609] <message>, the only line, exit status 1 |
db verify | [DB] Verify: pages=<total_pages> bad=<k>, then [DB] Page <p>: checksum mismatch for each bad page, ascending |
page init <n> | [PAGE] Init page <n>: free_space=4076 |
btree dump of an empty leaf | [BTREE] Page <p>: type=LEAF num_cells=0 free_space=4076 |
Errors are checked by code, never by message. A wrong magic, version or page size is still 00608, checked before the trailer.
What you should see when done
A table, a restart, a check of the whole file:
Welcome to droid!>create table users (id int, name varchar(1000), email varchar(1000));Create table statement executed successfully>insert into users (id, name, email) values (1, 'dan', 'd@e');Insert statement executed successfully (1 rows)>btree dump 2[BTREE] Page 2: type=LEAF num_cells=1 free_space=4047[BTREE] Cell 0: key=1 (21 bytes)>.exitexiting.. good bye!
Welcome to droid!>db verify[DB] Verify: pages=3 bad=0>.exitexiting.. good bye!
Now damage one byte of page 2 in the file, anywhere, even in the empty middle of the page, and open it again:
Welcome to droid!>select * from users;[ERROR:00609] Page 2 failed its checksum>select * from users;[ERROR:00609] Page 2 failed its checksum>db verify[DB] Verify: pages=3 bad=1[DB] Page 2: checksum mismatch>.exitexiting.. good bye!
Things to notice. The SELECT printed no header and no rows, not even (0 rows): it did not get far enough to have a result. The second SELECT failed again, because the bad page was never kept. And db verify found page 2 without being told where to look.
Damage page 0 instead, and the program never gets as far as the welcome line:
$droid --db notes.db[ERROR:00609] Page 0 failed its checksum$echo $?1
Common bugs in this stage
- Checking the page in memory. The trailer is written when the page goes to the file. A page you changed a moment ago has a stale trailer in memory, and that is fine; checking it there raises alarms on every write.
- Computing the checksum over 4096 bytes. The trailer cannot cover itself. It is the CRC32 of bytes 0 to 4091.
- Forgetting a kind of page. The header, the catalog, raw pages from
pager write: every page the file holds gets a trailer, or the firstdb verifyafter a restart finds it bad. - Writing the trailer big-endian. Every integer in the file is little-endian, this one too.
- A slotted page that still ends at 4096. The first record goes over the trailer, and the page fails its checksum after the next restart, or the record is cut when the trailer is written. Every place that knew the page ended at 4096 now needs 4092: the empty record start, free space, compaction and the capacity of a leaf.
- Keeping the bad page. A page that failed its check must not stay in memory, or the second statement reads it without complaint.
- Checking the header before its magic. A file that is not a droid database at all is still
00608; only a droid header with a wrong trailer is00609.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C: there is no CRC32 in the standard library. Write the bitwise version first and check it against
0xCBF43926, then add a 256-entry table if you like. Useuint32_tthroughout so the shifts are unsigned. - C++: the same as C;
std::array<uint32_t, 256>built once holds the table. - Rust: write it yourself with
u32and>>, or compare yours with thecrc32fastcrate in a test.u32::to_le_bytesgives the trailer's four bytes. - Zig:
std.hash.Crc32computes exactly this checksum; writing your own is still worth doing once.std.mem.writeIntwith.littlestores the trailer.
Checklist
One line per thing the tests check. The tags name the test cases.
- Every page in the file ends with the CRC32 of its first 4092 bytestrailer-on-every-page
- A new root leaf records a start of 4092 and has 4076 bytes freeempty-leaf-ends-at-4092
- A leaf holds 4076 bytes: eight 502-byte rows no longer fitleaf-holds-4076-bytes
- The worked slotted page, four bytes lowerworked-page-ends-at-4092
- db verify counts the pages and finds none bad, in the session and after a restartverify-clean
- db verify on a file with no pages creates nothingverify-empty-file
- db verify names each bad page, in order, and keeps none of themverify-lists-bad-pages
- A page that fails its checksum is 00609 every time, and the rest of the file still worksflipped-byte-detected
- A statement that needs a bad page changes nothingbad-page-changes-nothing
- A damaged page 0 is 00609 at open; a wrong magic is still 00608header-corruption-detected
- Pages rewritten later carry their new checksums: no false alarmchecksum-updated-on-write