Skip to content

Commit 69e5cf7

Browse files
authored
Checksums in default VFS. (#177)
1 parent 75c1dbb commit 69e5cf7

File tree

17 files changed

+305
-467
lines changed

17 files changed

+305
-467
lines changed

config.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package sqlite3
22

33
import (
44
"context"
5+
"strconv"
56

67
"github.com/tetratelabs/wazero/api"
78

@@ -327,3 +328,46 @@ func (c *Conn) SoftHeapLimit(n int64) int64 {
327328
func (c *Conn) HardHeapLimit(n int64) int64 {
328329
return int64(c.call("sqlite3_hard_heap_limit64", uint64(n)))
329330
}
331+
332+
// EnableChecksums enables checksums on a database.
333+
//
334+
// https://sqlite.org/cksumvfs.html
335+
func (c *Conn) EnableChecksums(schema string) error {
336+
r, err := c.FileControl(schema, FCNTL_RESERVE_BYTES)
337+
if err != nil {
338+
return err
339+
}
340+
if r == 8 {
341+
// Correct value, enabled.
342+
return nil
343+
}
344+
if r == 0 {
345+
// Default value, enable.
346+
_, err = c.FileControl(schema, FCNTL_RESERVE_BYTES, 8)
347+
if err != nil {
348+
return err
349+
}
350+
r, err = c.FileControl(schema, FCNTL_RESERVE_BYTES)
351+
if err != nil {
352+
return err
353+
}
354+
}
355+
if r != 8 {
356+
// Invalid value.
357+
return util.ErrorString("sqlite3: reserve bytes must be 8, is: " + strconv.Itoa(r.(int)))
358+
}
359+
360+
// VACUUM the database.
361+
if schema != "" {
362+
err = c.Exec(`VACUUM ` + QuoteIdentifier(schema))
363+
} else {
364+
err = c.Exec(`VACUUM`)
365+
}
366+
if err != nil {
367+
return err
368+
}
369+
370+
// Checkpoint the WAL.
371+
_, _, err = c.WALCheckpoint(schema, CHECKPOINT_RESTART)
372+
return err
373+
}

tests/cksm_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package tests
2+
3+
import (
4+
_ "embed"
5+
"strings"
6+
"testing"
7+
8+
"github.com/ncruces/go-sqlite3"
9+
"github.com/ncruces/go-sqlite3/driver"
10+
_ "github.com/ncruces/go-sqlite3/embed"
11+
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
12+
"github.com/ncruces/go-sqlite3/util/ioutil"
13+
"github.com/ncruces/go-sqlite3/vfs/memdb"
14+
"github.com/ncruces/go-sqlite3/vfs/readervfs"
15+
)
16+
17+
//go:embed testdata/cksm.db
18+
var cksmDB string
19+
20+
func Test_fileformat(t *testing.T) {
21+
t.Parallel()
22+
23+
readervfs.Create("test.db", ioutil.NewSizeReaderAt(strings.NewReader(cksmDB)))
24+
25+
db, err := driver.Open("file:test.db?vfs=reader")
26+
if err != nil {
27+
t.Fatal(err)
28+
}
29+
defer db.Close()
30+
31+
var enabled bool
32+
err = db.QueryRow(`PRAGMA checksum_verification`).Scan(&enabled)
33+
if err != nil {
34+
t.Fatal(err)
35+
}
36+
if !enabled {
37+
t.Error("want true")
38+
}
39+
40+
db.SetMaxIdleConns(0) // Clears the page cache.
41+
42+
_, err = db.Exec(`PRAGMA integrity_check`)
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
}
47+
48+
func Test_enable(t *testing.T) {
49+
t.Parallel()
50+
51+
db, err := driver.Open(memdb.TestDB(t),
52+
func(db *sqlite3.Conn) error {
53+
return db.EnableChecksums("main")
54+
})
55+
if err != nil {
56+
t.Fatal(err)
57+
}
58+
defer db.Close()
59+
60+
var enabled bool
61+
err = db.QueryRow(`PRAGMA checksum_verification`).Scan(&enabled)
62+
if err != nil {
63+
t.Fatal(err)
64+
}
65+
if !enabled {
66+
t.Error("want true")
67+
}
68+
69+
db.SetMaxIdleConns(0) // Clears the page cache.
70+
71+
_, err = db.Exec(`PRAGMA integrity_check`)
72+
if err != nil {
73+
t.Fatal(err)
74+
}
75+
}
File renamed without changes.

vfs/README.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ The main differences are [file locking](#file-locking) and [WAL mode](#write-ahe
1616
POSIX advisory locks, which SQLite uses on Unix, are
1717
[broken by design](https://github.com/sqlite/sqlite/blob/b74eb0/src/os_unix.c#L1073-L1161).
1818

19-
On Linux and macOS, this module uses
19+
On Linux and macOS, this package uses
2020
[OFD locks](https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html)
2121
to synchronize access to database files.
2222
OFD locks are fully compatible with POSIX advisory locks.
2323

24-
This module can also use
24+
This package can also use
2525
[BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2),
2626
albeit with reduced concurrency (`BEGIN IMMEDIATE` behaves like `BEGIN EXCLUSIVE`).
2727
On BSD, macOS, and illumos, BSD locks are fully compatible with POSIX advisory locks;
@@ -30,7 +30,7 @@ elsewhere, they are very likely broken.
3030
BSD locks are the default on BSD and illumos,
3131
but you can opt into them with the `sqlite3_flock` build tag.
3232

33-
On Windows, this module uses `LockFileEx` and `UnlockFileEx`,
33+
On Windows, this package uses `LockFileEx` and `UnlockFileEx`,
3434
like SQLite.
3535

3636
Otherwise, file locking is not supported, and you must use
@@ -46,7 +46,7 @@ to check if your build supports file locking.
4646

4747
### Write-Ahead Logging
4848

49-
On little-endian Unix, this module uses `mmap` to implement
49+
On little-endian Unix, this package uses `mmap` to implement
5050
[shared-memory for the WAL-index](https://sqlite.org/wal.html#implementation_of_shared_memory_for_the_wal_index),
5151
like SQLite.
5252

@@ -67,9 +67,22 @@ to check if your build supports shared memory.
6767

6868
### Batch-Atomic Write
6969

70-
On 64-bit Linux, this module supports [batch-atomic writes](https://sqlite.org/cgi/src/technote/714)
70+
On 64-bit Linux, this package supports
71+
[batch-atomic writes](https://sqlite.org/cgi/src/technote/714)
7172
on the F2FS filesystem.
7273

74+
### Checksums
75+
76+
This package can be [configured](https://pkg.go.dev/github.com/ncruces/go-sqlite3#Conn.EnableChecksums)
77+
to add an 8-byte checksum to the end of every page in an SQLite database.
78+
The checksum is added as each page is written
79+
and verified as each page is read.\
80+
The checksum is intended to help detect database corruption
81+
caused by random bit-flips in the mass storage device.
82+
83+
The implementation is compatible with SQLite's
84+
[Checksum VFS Shim](https://sqlite.org/cksumvfs.html).
85+
7386
### Build Tags
7487

7588
The VFS can be customized with a few build tags:

vfs/adiantum/adiantum_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
var testDB string
2121

2222
func Test_fileformat(t *testing.T) {
23+
t.Parallel()
24+
2325
readervfs.Create("test.db", ioutil.NewSizeReaderAt(strings.NewReader(testDB)))
2426
vfs.Register("radiantum", adiantum.Wrap(vfs.Find("reader"), nil))
2527

vfs/api.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,8 @@ type blockingSharedMemory interface {
186186
SharedMemory
187187
shmEnableBlocking(block bool)
188188
}
189+
190+
type fileControl interface {
191+
File
192+
fileControl(ctx context.Context, mod api.Module, op _FcntlOpcode, pArg uint32) _ErrorCode
193+
}

vfs/cksm.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package vfs
2+
3+
import (
4+
"bytes"
5+
"context"
6+
_ "embed"
7+
"encoding/binary"
8+
"strconv"
9+
10+
"github.com/tetratelabs/wazero/api"
11+
12+
"github.com/ncruces/go-sqlite3/internal/util"
13+
"github.com/ncruces/go-sqlite3/util/sql3util"
14+
)
15+
16+
func cksmWrapFile(name *Filename, flags OpenFlag, file File) File {
17+
// Checksum only main databases and WALs.
18+
if flags&(OPEN_MAIN_DB|OPEN_WAL) == 0 {
19+
return file
20+
}
21+
22+
cksm := cksmFile{File: file}
23+
24+
if flags&OPEN_WAL != 0 {
25+
main, _ := name.DatabaseFile().(cksmFile)
26+
cksm.cksmFlags = main.cksmFlags
27+
} else {
28+
cksm.cksmFlags = new(cksmFlags)
29+
cksm.isDB = true
30+
}
31+
32+
return cksm
33+
}
34+
35+
type cksmFile struct {
36+
File
37+
*cksmFlags
38+
isDB bool
39+
}
40+
41+
type cksmFlags struct {
42+
computeCksm bool
43+
verifyCksm bool
44+
inCkpt bool
45+
pageSize int
46+
}
47+
48+
func (c cksmFile) ReadAt(p []byte, off int64) (n int, err error) {
49+
n, err = c.File.ReadAt(p, off)
50+
51+
// SQLite is reading the header of a database file.
52+
if c.isDB && off == 0 && len(p) >= 100 &&
53+
bytes.HasPrefix(p, []byte("SQLite format 3\000")) {
54+
c.init(p)
55+
}
56+
57+
// Verify checksums.
58+
if c.verifyCksm && !c.inCkpt && len(p) == c.pageSize {
59+
cksm1 := cksmCompute(p[:len(p)-8])
60+
cksm2 := *(*[8]byte)(p[len(p)-8:])
61+
if cksm1 != cksm2 {
62+
return 0, _IOERR_DATA
63+
}
64+
}
65+
return n, err
66+
}
67+
68+
func (c cksmFile) WriteAt(p []byte, off int64) (n int, err error) {
69+
// SQLite is writing the first page of a database file.
70+
if c.isDB && off == 0 && len(p) >= 100 &&
71+
bytes.HasPrefix(p, []byte("SQLite format 3\000")) {
72+
c.init(p)
73+
}
74+
75+
// Compute checksums.
76+
if c.computeCksm && !c.inCkpt && len(p) == c.pageSize {
77+
*(*[8]byte)(p[len(p)-8:]) = cksmCompute(p[:len(p)-8])
78+
}
79+
80+
return c.File.WriteAt(p, off)
81+
}
82+
83+
func (c cksmFile) Pragma(name string, value string) (string, error) {
84+
switch name {
85+
case "checksum_verification":
86+
b, ok := sql3util.ParseBool(value)
87+
if ok {
88+
c.verifyCksm = b && c.computeCksm
89+
}
90+
if !c.verifyCksm {
91+
return "0", nil
92+
}
93+
return "1", nil
94+
95+
case "page_size":
96+
if c.computeCksm {
97+
// Do not allow page size changes on a checksum database.
98+
return strconv.Itoa(c.pageSize), nil
99+
}
100+
}
101+
return "", _NOTFOUND
102+
}
103+
104+
func (c cksmFile) fileControl(ctx context.Context, mod api.Module, op _FcntlOpcode, pArg uint32) _ErrorCode {
105+
switch op {
106+
case _FCNTL_CKPT_START:
107+
c.inCkpt = true
108+
case _FCNTL_CKPT_DONE:
109+
c.inCkpt = false
110+
}
111+
if rc := vfsFileControlImpl(ctx, mod, c, op, pArg); rc != _NOTFOUND {
112+
return rc
113+
}
114+
return vfsFileControlImpl(ctx, mod, c.File, op, pArg)
115+
}
116+
117+
func (f *cksmFlags) init(header []byte) {
118+
f.pageSize = 256 * int(binary.LittleEndian.Uint16(header[16:18]))
119+
if r := header[20] == 8; r != f.computeCksm {
120+
f.computeCksm = r
121+
f.verifyCksm = r
122+
}
123+
}
124+
125+
func cksmCompute(a []byte) (cksm [8]byte) {
126+
var s1, s2 uint32
127+
for len(a) >= 8 {
128+
s1 += binary.LittleEndian.Uint32(a[0:4]) + s2
129+
s2 += binary.LittleEndian.Uint32(a[4:8]) + s1
130+
a = a[8:]
131+
}
132+
if len(a) != 0 {
133+
panic(util.AssertErr())
134+
}
135+
binary.LittleEndian.PutUint32(cksm[0:4], s1)
136+
binary.LittleEndian.PutUint32(cksm[4:8], s2)
137+
return
138+
}
139+
140+
func (c cksmFile) SharedMemory() SharedMemory {
141+
if f, ok := c.File.(FileSharedMemory); ok {
142+
return f.SharedMemory()
143+
}
144+
return nil
145+
}
146+
147+
func (c cksmFile) Unwrap() File {
148+
return c.File
149+
}

vfs/cksmvfs/README.md

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)