oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package operator
4
5import (
6 "bytes"
7 "context"
8 "crypto/sha256"
9 "database/sql"
10 "encoding/hex"
11 "encoding/json"
12 "io"
13 "os"
14 "os/exec"
15 "path/filepath"
16 "testing"
17 "time"
18)
19
20func TestReadonlySubprocess(t *testing.T) {
21 if os.Getenv("OFICINA_READONLY_PROBE") == "1" {
22 var input struct {
23 Expected ReadonlyExpectation
24 Mode string
25 }
26 if json.NewDecoder(io.LimitReader(os.Stdin, 16384)).Decode(&input) != nil {
27 t.Fatal("input")
28 }
29 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
30 defer cancel()
31 calls := 0
32 proof := func(context.Context, ReadonlyExpectation) error {
33 calls++
34 if input.Mode == "proof-revoked" && calls > 1 {
35 return ErrRefused
36 }
37 return nil
38 }
39 if input.Mode == "missing-proof" {
40 proof = nil
41 }
42 if input.Mode == "wrong-hash" {
43 input.Expected.SHA256 = string(bytes.Repeat([]byte("0"), 64))
44 }
45 if input.Mode == "wrong-inode" {
46 input.Expected.Inode++
47 }
48 if input.Mode == "expired" {
49 cancel()
50 }
51 if input.Mode == "unbounded" {
52 ctx = context.Background()
53 }
54 var extra *os.File
55 defer func() {
56 if extra != nil {
57 extra.Close()
58 }
59 }()
60 inspect := func(ctx context.Context, c *sql.Conn) error {
61 if input.Mode == "unavailable" {
62 return ErrUnavailable
63 }
64 if input.Mode == "spill" {
65 var e error
66 extra, e = os.Create(input.Expected.Path + ".spill")
67 if e != nil {
68 return e
69 }
70 }
71 var count int
72 if e := c.QueryRowContext(ctx, "SELECT count(*) FROM changes").Scan(&count); e != nil {
73 return e
74 }
75 if count != 0 {
76 return ErrRefused
77 }
78 if input.Mode == "write" {
79 _, e := c.ExecContext(ctx, "INSERT INTO changes VALUES(1)")
80 return e
81 }
82 return nil
83 }
84 lastStage := "before-inspection"
85 hook := func(stage string) {
86 lastStage = stage
87 if input.Mode == "replace-restore" {
88 if stage == "before-driver" {
89 if os.Rename(input.Expected.Path, input.Expected.Path+".saved") != nil || os.Rename(input.Expected.Path+".other", input.Expected.Path) != nil {
90 t.Fatal("swap")
91 }
92 }
93 if stage == "after-open" {
94 if os.Rename(input.Expected.Path, input.Expected.Path+".other") != nil || os.Rename(input.Expected.Path+".saved", input.Expected.Path) != nil {
95 t.Fatal("restore")
96 }
97 }
98 }
99 if input.Mode == "change-after-read" && stage == "after-exec" {
100 // Alter an unused SQLite header byte through an external process: no
101 // extra descriptor is admitted into the checker process.
102 cmd := exec.Command("python3", "-c", "import sys; f=open(sys.argv[1],'r+b'); f.seek(72); f.write(b'\\x01'); f.close()", input.Expected.Path)
103 if e := cmd.Run(); e != nil {
104 t.Fatal(e)
105 }
106 }
107 }
108 err := inspectReadonly(ctx, input.Expected, proof, inspect, hook)
109 if input.Mode == "unavailable" && err != ErrUnavailable {
110 t.Fatalf("lost unavailable outcome: %v", err)
111 }
112 if input.Mode == "valid" {
113 if err != nil {
114 t.Fatalf("inspection refused after %s: %v", lastStage, err)
115 }
116 } else if err == nil {
117 t.Fatal("unsafe success", input.Mode)
118 }
119 return
120 }
121 for _, mode := range []string{"valid", "unavailable", "proof-revoked", "replace-restore", "missing-proof", "wrong-hash", "wrong-inode", "expired", "unbounded", "wal", "shm", "journal", "spill", "write", "change-after-read"} {
122 t.Run(mode, func(t *testing.T) {
123 path := filepath.Join(t.TempDir(), "test.db")
124 seedOpenDB(t, path)
125 if mode == "replace-restore" {
126 seedOpenDB(t, path+".other")
127 }
128 raw, e := os.ReadFile(path)
129 if e != nil {
130 t.Fatal(e)
131 }
132 h := sha256.Sum256(raw)
133 id, e := statExisting(path)
134 if e != nil {
135 t.Fatal(e)
136 }
137 expected := ReadonlyExpectation{path, id.device, id.inode, int64(len(raw)), hex.EncodeToString(h[:])}
138 if mode == "wal" || mode == "shm" || mode == "journal" {
139 if e := os.WriteFile(path+"-"+mode, []byte("retained"), 0600); e != nil {
140 t.Fatal(e)
141 }
142 }
143 input, _ := json.Marshal(struct {
144 Expected ReadonlyExpectation
145 Mode string
146 }{expected, mode})
147 cmd := exec.Command(os.Args[0], "-test.run=^TestReadonlySubprocess$")
148 cmd.Env = append(os.Environ(), "OFICINA_READONLY_PROBE=1")
149 cmd.Stdin = bytes.NewReader(input)
150 if out, e := cmd.CombinedOutput(); e != nil {
151 t.Fatalf("child: %v %s", e, out)
152 }
153 if mode != "change-after-read" {
154 after, e := os.ReadFile(path)
155 if e != nil || !bytes.Equal(raw, after) {
156 t.Fatal("database bytes changed")
157 }
158 }
159 })
160 }
161}
162