Skip to content

Commit bf89d95

Browse files
authored
fix(go): bullet-proof against nil dereferences + more fuzzers (#244)
This change fixes a bunch of issues identified by Orijtech Inc's audit of ics23 which is a critical cosmos-sdk dependency and as per reports about the Dragonberry & Elderberry vulnerability reports, this package was put back on our radar to further audit and voila that uncovered some issues, some of which have beenfixed in this change. While here also added more fuzzers. To ensure that the fuzzers can run alright, added -short to any invocations of "go test". Fixes #241 Fixes #242 Fixes #243
1 parent c7c7288 commit bf89d95

File tree

12 files changed

+184
-36
lines changed

12 files changed

+184
-36
lines changed

go/fuzz_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ics23
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"os"
67
"path/filepath"
@@ -40,6 +41,38 @@ func FuzzExistenceProofCalculate(f *testing.F) {
4041
})
4142
}
4243

44+
func FuzzExistenceProofCheckAgainstSpec(f *testing.F) {
45+
if testing.Short() {
46+
f.Skip("in -short mode")
47+
}
48+
49+
seedDataMap := CheckAgainstSpecTestData(f)
50+
for _, seed := range seedDataMap {
51+
if seed.IsErr {
52+
// Erroneous data, skip it.
53+
continue
54+
}
55+
if blob, err := json.Marshal(seed); err == nil {
56+
f.Add(blob)
57+
}
58+
}
59+
60+
// 2. Now run the fuzzer.
61+
f.Fuzz(func(t *testing.T, fJSON []byte) {
62+
pvs := new(CheckAgainstSpecTestStruct)
63+
if err := json.Unmarshal(fJSON, pvs); err != nil {
64+
return
65+
}
66+
if pvs.Proof == nil {
67+
// No need checking from a nil ExistenceProof.
68+
return
69+
}
70+
71+
ep, spec := pvs.Proof, pvs.Spec
72+
_ = ep.CheckAgainstSpec(spec)
73+
})
74+
}
75+
4376
var batchVectorDataSeeds []*BatchVectorData
4477

4578
func init() {
@@ -89,6 +122,57 @@ func FuzzVerifyNonMembership(f *testing.F) {
89122
})
90123
}
91124

125+
// verifyJSON is necessary so we already have sources of Proof, Ref and Spec
126+
// that'll be mutated by the fuzzer to get much closer to values for greater coverage.
127+
type verifyJSON struct {
128+
Proof *CommitmentProof
129+
Ref *RefData
130+
Spec *ProofSpec
131+
}
132+
133+
func FuzzVerifyMembership(f *testing.F) {
134+
seeds := VectorsTestData()
135+
136+
// VerifyMembership requires:
137+
// Spec, ExistenceProof, CommitmentRootref.
138+
for _, seed := range seeds {
139+
proof, ref := LoadFile(f, seed.Dir, seed.Filename)
140+
root, err := proof.Calculate()
141+
if err != nil {
142+
continue
143+
}
144+
if !bytes.Equal(ref.RootHash, root) {
145+
continue
146+
}
147+
148+
if ref.Value == nil {
149+
continue
150+
}
151+
152+
// Now serialize this value as a seed.
153+
// The use of already calculated values is necessary
154+
// for the fuzzers to have a basis of much better coverage
155+
// generating values.
156+
blob, err := json.Marshal(&verifyJSON{Proof: proof, Ref: ref, Spec: seed.Spec})
157+
if err == nil {
158+
f.Add(blob)
159+
}
160+
}
161+
162+
// 2. Now let's run the fuzzer.
163+
f.Fuzz(func(t *testing.T, input []byte) {
164+
var con verifyJSON
165+
if err := json.Unmarshal(input, &con); err != nil {
166+
return
167+
}
168+
spec, ref, proof := con.Spec, con.Ref, con.Proof
169+
if ref == nil {
170+
return
171+
}
172+
_ = VerifyMembership(spec, ref.RootHash, proof, ref.Key, ref.Value)
173+
})
174+
}
175+
92176
func FuzzCombineProofs(f *testing.F) {
93177
// 1. Load in the CommitmentProofs
94178
baseDirs := []string{"iavl", "tendermint", "smt"}

go/ics23.go

+4
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ func CombineProofs(proofs []*CommitmentProof) (*CommitmentProof, error) {
132132
}
133133

134134
func getExistProofForKey(proof *CommitmentProof, key []byte) *ExistenceProof {
135+
if proof == nil {
136+
return nil
137+
}
138+
135139
switch p := proof.Proof.(type) {
136140
case *CommitmentProof_Exist:
137141
ep := p.Exist

go/ops.go

+20
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,13 @@ func (op *InnerOp) Apply(child []byte) ([]byte, error) {
9494

9595
// CheckAgainstSpec will verify the LeafOp is in the format defined in spec
9696
func (op *LeafOp) CheckAgainstSpec(spec *ProofSpec) error {
97+
if spec == nil {
98+
return errors.New("op and spec must be non-nil")
99+
}
97100
lspec := spec.LeafSpec
101+
if lspec == nil {
102+
return errors.New("spec.LeafSpec must be non-nil")
103+
}
98104

99105
if validateSpec(spec) {
100106
err := validateIavlOps(op, 0)
@@ -123,6 +129,16 @@ func (op *LeafOp) CheckAgainstSpec(spec *ProofSpec) error {
123129

124130
// CheckAgainstSpec will verify the InnerOp is in the format defined in spec
125131
func (op *InnerOp) CheckAgainstSpec(spec *ProofSpec, b int) error {
132+
if spec == nil {
133+
return errors.New("op and spec must be both non-nil")
134+
}
135+
if spec.InnerSpec == nil {
136+
return errors.New("spec.InnerSpec must be non-nil")
137+
}
138+
if spec.LeafSpec == nil {
139+
return errors.New("spec.LeafSpec must be non-nil")
140+
}
141+
126142
if op.Hash != spec.InnerSpec.Hash {
127143
return fmt.Errorf("unexpected HashOp: %d", op.Hash)
128144
}
@@ -146,6 +162,10 @@ func (op *InnerOp) CheckAgainstSpec(spec *ProofSpec, b int) error {
146162
return fmt.Errorf("innerOp prefix too long (%d)", len(op.Prefix))
147163
}
148164

165+
if spec.InnerSpec.ChildSize <= 0 {
166+
return errors.New("spec.InnerSpec.ChildSize must be >= 1")
167+
}
168+
149169
// ensures soundness, with suffix having to be of correct length
150170
if len(op.Suffix)%int(spec.InnerSpec.ChildSize) != 0 {
151171
return fmt.Errorf("InnerOp suffix malformed")

go/proof.go

+40-12
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,11 @@ func (p *NonExistenceProof) Calculate() (CommitmentRoot, error) {
180180

181181
// CheckAgainstSpec will verify the leaf and all path steps are in the format defined in spec
182182
func (p *ExistenceProof) CheckAgainstSpec(spec *ProofSpec) error {
183-
if p.GetLeaf() == nil {
183+
leaf := p.GetLeaf()
184+
if leaf == nil {
184185
return errors.New("existence Proof needs defined LeafOp")
185186
}
186-
err := p.Leaf.CheckAgainstSpec(spec)
187-
if err != nil {
187+
if err := leaf.CheckAgainstSpec(spec); err != nil {
188188
return fmt.Errorf("leaf, %w", err)
189189
}
190190
if spec.MinDepth > 0 && len(p.Path) < int(spec.MinDepth) {
@@ -452,13 +452,41 @@ func orderFromPadding(spec *InnerSpec, inner *InnerOp) (int32, error) {
452452

453453
// over-declares equality, which we cosnider fine for now.
454454
func (p *ProofSpec) SpecEquals(spec *ProofSpec) bool {
455-
return p.LeafSpec.Hash == spec.LeafSpec.Hash &&
456-
p.LeafSpec.PrehashKey == spec.LeafSpec.PrehashKey &&
457-
p.LeafSpec.PrehashValue == spec.LeafSpec.PrehashValue &&
458-
p.LeafSpec.Length == spec.LeafSpec.Length &&
459-
p.InnerSpec.Hash == spec.InnerSpec.Hash &&
460-
p.InnerSpec.MinPrefixLength == spec.InnerSpec.MinPrefixLength &&
461-
p.InnerSpec.MaxPrefixLength == spec.InnerSpec.MaxPrefixLength &&
462-
p.InnerSpec.ChildSize == spec.InnerSpec.ChildSize &&
463-
len(p.InnerSpec.ChildOrder) == len(spec.InnerSpec.ChildOrder)
455+
// 1. Compare LeafSpecs values.
456+
switch {
457+
case (p.LeafSpec == nil) != (spec.LeafSpec == nil): // One of them is nil.
458+
return false
459+
460+
case p.LeafSpec != nil && spec.LeafSpec != nil:
461+
ok := p.LeafSpec.Hash == spec.LeafSpec.Hash &&
462+
p.LeafSpec.PrehashKey == spec.LeafSpec.PrehashKey &&
463+
p.LeafSpec.PrehashValue == spec.LeafSpec.PrehashValue &&
464+
p.LeafSpec.Length == spec.LeafSpec.Length
465+
if !ok {
466+
return false
467+
}
468+
469+
default: // Both are nil, hence LeafSpec values are equal.
470+
}
471+
472+
// 2. Compare InnerSpec values.
473+
switch {
474+
case (p.InnerSpec == nil) != (spec.InnerSpec == nil): // One of them is not nil.
475+
return false
476+
477+
case p.InnerSpec != nil && spec.InnerSpec != nil: // Both are non-nil
478+
ok := p.InnerSpec.Hash == spec.InnerSpec.Hash &&
479+
p.InnerSpec.MinPrefixLength == spec.InnerSpec.MinPrefixLength &&
480+
p.InnerSpec.MaxPrefixLength == spec.InnerSpec.MaxPrefixLength &&
481+
p.InnerSpec.ChildSize == spec.InnerSpec.ChildSize &&
482+
len(p.InnerSpec.ChildOrder) == len(spec.InnerSpec.ChildOrder)
483+
if !ok {
484+
return false
485+
}
486+
487+
default: // Both are nil, hence InnerSpec values are equal.
488+
}
489+
490+
// By this point all the above conditions pass so they are equal.
491+
return true
464492
}

go/proof_data_test.go

+12-12
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ type ExistenceProofTestStruct struct {
1313
Expected []byte
1414
}
1515

16-
func ExistenceProofTestData(t *testing.T) map[string]ExistenceProofTestStruct {
17-
t.Helper()
16+
func ExistenceProofTestData(tb testing.TB) map[string]ExistenceProofTestStruct {
17+
tb.Helper()
1818
fname := filepath.Join("..", "testdata", "TestExistenceProofData.json")
1919
ffile, err := os.Open(fname)
2020
if err != nil {
21-
t.Fatal(err)
21+
tb.Fatal(err)
2222
}
2323
var cases map[string]ExistenceProofTestStruct
2424
jsonDecoder := json.NewDecoder(ffile)
2525
err = jsonDecoder.Decode(&cases)
2626
if err != nil {
27-
t.Fatal(err)
27+
tb.Fatal(err)
2828
}
2929
return cases
3030
}
@@ -35,18 +35,18 @@ type CheckLeafTestStruct struct {
3535
IsErr bool
3636
}
3737

38-
func CheckLeafTestData(t *testing.T) map[string]CheckLeafTestStruct {
39-
t.Helper()
38+
func CheckLeafTestData(tb testing.TB) map[string]CheckLeafTestStruct {
39+
tb.Helper()
4040
fname := filepath.Join("..", "testdata", "TestCheckLeafData.json")
4141
ffile, err := os.Open(fname)
4242
if err != nil {
43-
t.Fatal(err)
43+
tb.Fatal(err)
4444
}
4545
var cases map[string]CheckLeafTestStruct
4646
jsonDecoder := json.NewDecoder(ffile)
4747
err = jsonDecoder.Decode(&cases)
4848
if err != nil {
49-
t.Fatal(err)
49+
tb.Fatal(err)
5050
}
5151
return cases
5252
}
@@ -57,18 +57,18 @@ type CheckAgainstSpecTestStruct struct {
5757
IsErr bool
5858
}
5959

60-
func CheckAgainstSpecTestData(t *testing.T) map[string]CheckAgainstSpecTestStruct {
61-
t.Helper()
60+
func CheckAgainstSpecTestData(tb testing.TB) map[string]CheckAgainstSpecTestStruct {
61+
tb.Helper()
6262
fname := filepath.Join("..", "testdata", "TestCheckAgainstSpecData.json")
6363
ffile, err := os.Open(fname)
6464
if err != nil {
65-
t.Fatal(err)
65+
tb.Fatal(err)
6666
}
6767
var cases map[string]CheckAgainstSpecTestStruct
6868
jsonDecoder := json.NewDecoder(ffile)
6969
err = jsonDecoder.Decode(&cases)
7070
if err != nil {
71-
t.Fatal(err)
71+
tb.Fatal(err)
7272
}
7373
return cases
7474
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
[]byte("{\"Proof\":{\"leAf\":{\"prehAsh_vAlue\":1,\"length\":1,\"prefiX\":\"AA00\"},\"pAth\":[{\"hAsh\":1}]},\"SpeC\":{\"leAf_speC\":{\"prehAsh_vAlue\":1,\"length\":1,\"prefiX\":\"AA00\"},\"inner_speC\":{\"hAsh\":1}}}")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
[]byte("{\"Proof\":{\"leAf\":{},\"pAth\":[{}]},\"SpeC\":{\"leAf_speC\":{}}}")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
[]byte("{\"Proof\":{\"leAf\":{}},\"SpeC\":{}}")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
[]byte("{\"Proof\":{\"leAf\":{}}}")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
[]byte("{}")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
[]byte("{\"Ref\":{}}")

go/vectors_data_test.go

+12-12
Original file line numberDiff line numberDiff line change
@@ -165,42 +165,42 @@ func DecompressBatchVectorsTestData(t *testing.T) map[string]*CommitmentProof {
165165
}
166166
}
167167

168-
func LoadFile(t *testing.T, dir string, filename string) (*CommitmentProof, *RefData) {
169-
t.Helper()
168+
func LoadFile(tb testing.TB, dir string, filename string) (*CommitmentProof, *RefData) {
169+
tb.Helper()
170170
// load the file into a json struct
171171
name := filepath.Join(dir, filename)
172172
bz, err := os.ReadFile(name)
173173
if err != nil {
174-
t.Fatalf("Read file: %+v", err)
174+
tb.Fatalf("Read file: %+v", err)
175175
}
176176
var data TestVector
177177
err = json.Unmarshal(bz, &data)
178178
if err != nil {
179-
t.Fatalf("Unmarshal json: %+v", err)
179+
tb.Fatalf("Unmarshal json: %+v", err)
180180
}
181181
// parse the protobuf object
182182
var proof CommitmentProof
183-
err = proof.Unmarshal(mustHex(t, data.Proof))
183+
err = proof.Unmarshal(mustHex(tb, data.Proof))
184184
if err != nil {
185-
t.Fatalf("Unmarshal protobuf: %+v", err)
185+
tb.Fatalf("Unmarshal protobuf: %+v", err)
186186
}
187187
var ref RefData
188-
ref.RootHash = CommitmentRoot(mustHex(t, data.RootHash))
189-
ref.Key = mustHex(t, data.Key)
188+
ref.RootHash = CommitmentRoot(mustHex(tb, data.RootHash))
189+
ref.Key = mustHex(tb, data.Key)
190190
if data.Value != "" {
191-
ref.Value = mustHex(t, data.Value)
191+
ref.Value = mustHex(tb, data.Value)
192192
}
193193
return &proof, &ref
194194
}
195195

196-
func mustHex(t *testing.T, data string) []byte {
197-
t.Helper()
196+
func mustHex(tb testing.TB, data string) []byte {
197+
tb.Helper()
198198
if data == "" {
199199
return nil
200200
}
201201
res, err := hex.DecodeString(data)
202202
if err != nil {
203-
t.Fatalf("decoding hex: %v", err)
203+
tb.Fatalf("decoding hex: %v", err)
204204
}
205205
return res
206206
}

0 commit comments

Comments
 (0)