Skip to content

Commit 546d6f4

Browse files
bassosimoneMurphy-OrangeMud
authored andcommitted
fix(enginenetx): gracefully handle more nil cases (ooni#1316)
This diff adjusts code inside the enginenetx package such that we gracefully deal with nil values in more cases. Part of ooni/probe#2531
1 parent b725a06 commit 546d6f4

File tree

4 files changed

+85
-16
lines changed

4 files changed

+85
-16
lines changed

internal/enginenetx/statsmanager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ func statsContainerRemoveOldEntries(input *statsContainer) (output *statsContain
211211
// At the name implies, this function MUST be called while holding the [*statsManager] mutex.
212212
func (c *statsContainer) GetStatsTacticLocked(tactic *httpsDialerTactic) (*statsTactic, bool) {
213213
domainEpntRecord, found := c.DomainEndpoints[tactic.domainEndpointKey()]
214-
if !found {
214+
if !found || domainEpntRecord == nil {
215215
return nil, false
216216
}
217217
tacticRecord, found := domainEpntRecord.Tactics[tactic.tacticSummaryKey()]

internal/enginenetx/statsmanager_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,3 +1016,30 @@ func TestStatsSafeIncrementMapStringInt64(t *testing.T) {
10161016
}
10171017
})
10181018
}
1019+
1020+
func TestStatsContainer(t *testing.T) {
1021+
t.Run("GetStatsTacticLocked", func(t *testing.T) {
1022+
t.Run("is robust with respect to c.DomainEndpoints containing a nil entry", func(t *testing.T) {
1023+
sc := &statsContainer{
1024+
DomainEndpoints: map[string]*statsDomainEndpoint{
1025+
"api.ooni.io:443": nil,
1026+
},
1027+
Version: statsContainerVersion,
1028+
}
1029+
tactic := &httpsDialerTactic{
1030+
Address: "162.55.247.208",
1031+
InitialDelay: 0,
1032+
Port: "443",
1033+
SNI: "www.example.com",
1034+
VerifyHostname: "api.ooni.io",
1035+
}
1036+
record, good := sc.GetStatsTacticLocked(tactic)
1037+
if good {
1038+
t.Fatal("expected not good")
1039+
}
1040+
if record != nil {
1041+
t.Fatal("expected nil")
1042+
}
1043+
})
1044+
})
1045+
}

internal/enginenetx/statspolicy.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func (p *statsPolicy) LookupTactics(ctx context.Context, domain string, port str
6060
}
6161

6262
// give priority to what we know from stats
63-
for _, t := range p.statsLookupTactics(domain, port) {
63+
for _, t := range statsPolicyPostProcessTactics(p.Stats.LookupTactics(domain, port)) {
6464
maybeEmitTactic(t)
6565
}
6666

@@ -73,39 +73,40 @@ func (p *statsPolicy) LookupTactics(ctx context.Context, domain string, port str
7373
return out
7474
}
7575

76-
func (p *statsPolicy) statsLookupTactics(domain string, port string) (out []*httpsDialerTactic) {
77-
78-
// obtain information from the stats--here the result may be false if the
79-
// stats do not contain any information about the domain and port
80-
tactics, good := p.Stats.LookupTactics(domain, port)
76+
func statsPolicyPostProcessTactics(tactics []*statsTactic, good bool) (out []*httpsDialerTactic) {
77+
// when good is false, it means p.Stats.LookupTactics failed
8178
if !good {
8279
return
8380
}
8481

85-
// successRate is a convenience function for computing the success rate
86-
successRate := func(t *statsTactic) (rate float64) {
87-
if t.CountStarted > 0 {
82+
// nilSafeSuccessRate is a convenience function for computing the success rate
83+
// which returns zero as the success rate if CountStarted is zero
84+
//
85+
// for robustness, be paranoid about nils here because the stats are
86+
// written on the disk and a user could potentially edit them
87+
nilSafeSuccessRate := func(t *statsTactic) (rate float64) {
88+
if t != nil && t.CountStarted > 0 {
8889
rate = float64(t.CountSuccess) / float64(t.CountStarted)
8990
}
9091
return
9192
}
9293

93-
// Implementation note: the function should implement the "less" semantics
94-
// but we want descending sorting not ascending, so we're using a "more" semantics
94+
// Implementation note: the function should implement the "less" semantics for
95+
// ascending sorting, but we want descending sorting, so we use `>` instead
9596
sort.SliceStable(tactics, func(i, j int) bool {
9697
// TODO(bassosimone): should we also consider the number of samples
9798
// we have and how recent a sample is?
98-
return successRate(tactics[i]) > successRate(tactics[j])
99+
return nilSafeSuccessRate(tactics[i]) > nilSafeSuccessRate(tactics[j])
99100
})
100101

101102
for _, t := range tactics {
102103
// make sure we only include samples with 1+ successes; we don't want this policy
103104
// to return what we already know it's not working and it will be the purpose of the
104105
// fallback policy to generate new tactics to test
105106
//
106-
// additionally, as a precautionary and defensive measure, make sure t.Tactic
107-
// is not nil before adding the real tactic to the return list
108-
if t.CountSuccess > 0 && t.Tactic != nil {
107+
// additionally, as a precautionary and defensive measure, make sure t and t.Tactic
108+
// are not nil before adding a malformed tactic to the return list
109+
if t != nil && t.CountSuccess > 0 && t.Tactic != nil {
109110
out = append(out, t.Tactic)
110111
}
111112
}

internal/enginenetx/statspolicy_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/ooni/probe-cli/v3/internal/netemx"
1414
"github.com/ooni/probe-cli/v3/internal/netxlite"
1515
"github.com/ooni/probe-cli/v3/internal/runtimex"
16+
"github.com/ooni/probe-cli/v3/internal/testingx"
1617
)
1718

1819
func TestStatsPolicyWorkingAsIntended(t *testing.T) {
@@ -314,3 +315,43 @@ var _ httpsDialerPolicy = &mocksPolicy{}
314315
func (p *mocksPolicy) LookupTactics(ctx context.Context, domain string, port string) <-chan *httpsDialerTactic {
315316
return p.MockLookupTactics(ctx, domain, port)
316317
}
318+
319+
func TestStatsPolicyPostProcessTactics(t *testing.T) {
320+
t.Run("we do nothing when good is false", func(t *testing.T) {
321+
tactics := statsPolicyPostProcessTactics(nil, false)
322+
if len(tactics) != 0 {
323+
t.Fatal("expected zero-lenght return value")
324+
}
325+
})
326+
327+
t.Run("we filter out cases in which t or t.Tactic are nil", func(t *testing.T) {
328+
expected := &statsTactic{}
329+
ff := &testingx.FakeFiller{}
330+
ff.Fill(&expected)
331+
332+
input := []*statsTactic{nil, {
333+
CountStarted: 0,
334+
CountTCPConnectError: 0,
335+
CountTCPConnectInterrupt: 0,
336+
CountTLSHandshakeError: 0,
337+
CountTLSHandshakeInterrupt: 0,
338+
CountTLSVerificationError: 0,
339+
CountSuccess: 0,
340+
HistoTCPConnectError: map[string]int64{},
341+
HistoTLSHandshakeError: map[string]int64{},
342+
HistoTLSVerificationError: map[string]int64{},
343+
LastUpdated: time.Time{},
344+
Tactic: nil,
345+
}, nil, expected}
346+
347+
got := statsPolicyPostProcessTactics(input, true)
348+
349+
if len(got) != 1 {
350+
t.Fatal("expected just one element")
351+
}
352+
353+
if diff := cmp.Diff(expected.Tactic, got[0]); diff != "" {
354+
t.Fatal(diff)
355+
}
356+
})
357+
}

0 commit comments

Comments
 (0)