Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions internal/config/secretkeys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,17 @@ func TestMaskURLCredentialsNoPanic(t *testing.T) {
}
}

// schemelessTimeOf is the fastest of five runs, so one scheduler or GC stall on
// a loaded runner does not read as super-linear growth.
func schemelessTimeOf(unit string, size int) time.Duration {
v := strings.Repeat(unit, size/len(unit))
start := time.Now()
_ = MaskURLCredentials(v)
return time.Since(start)
best := time.Duration(1<<63 - 1)
for i := 0; i < 5; i++ {
start := time.Now()
_ = MaskURLCredentials(v)
if d := time.Since(start); d < best {
best = d
}
}
return best
}
41 changes: 41 additions & 0 deletions internal/credmask/linear_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package credmask

import (
"strings"
"testing"
"time"
)

// bestOf returns the fastest of n runs of Schemeless over unit repeated to
// about size bytes; the minimum filters scheduler and GC noise on a loaded CI
// runner.
func bestOf(unit string, size, n int) time.Duration {
v := strings.Repeat(unit, size/len(unit))
best := time.Duration(1<<63 - 1)
for i := 0; i < n; i++ {
start := time.Now()
_ = Schemeless(v)
if d := time.Since(start); d < best {
best = d
}
}
return best
}

// TestSchemelessLinear: adversarial shapes must scale linearly. 10x the input
// may take about 10x as long; a quadratic scan takes about 100x, so the bound
// is 50x plus a 2 ms floor for timer noise. The 40 KB to 400 KB step is where
// a quadratic rescan stands clear of that noise.
func TestSchemelessLinear(t *testing.T) {
units := []string{"@a(", ":@a(", "a:@", "a:b@c(", "x=y@a(", "password=", "aaaaaaaa", "::::", "@@@@"}
for _, unit := range units {
prev := bestOf(unit, 4_000, 7)
for _, size := range []int{40_000, 400_000} {
cur := bestOf(unit, size, 7)
if cur > 50*prev+2*time.Millisecond {
t.Errorf("unit %q: %d B took %v, %d B took %v: not linear", unit, size/10, prev, size, cur)
}
prev = cur
}
}
}
24 changes: 18 additions & 6 deletions internal/credmask/schemeless.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,30 +58,42 @@ func maskDSNUserinfo(v string) string {
}
var b strings.Builder
done := 0
// One forward pass tracks where the token holding each match starts, so a
// long run without whitespace is not rescanned per match.
// One forward pass tracks where the token holding each match starts, its
// first `:` and its first `://`, so a long run without whitespace is not
// rescanned per match.
pos, tokenStart := 0, 0
colonAt, schemeAt := -1, -1
for _, loc := range locs {
at := loc[0]
for ; pos < at; pos++ {
if strings.IndexByte(" \t\r\n\"'", v[pos]) >= 0 {
switch c := v[pos]; c {
case ' ', '\t', '\r', '\n', '"', '\'':
tokenStart = pos + 1
colonAt, schemeAt = -1, -1
case ':':
if colonAt < 0 {
colonAt = pos
}
if schemeAt < 0 && strings.HasPrefix(v[pos:], "://") {
schemeAt = pos
}
}
}
if at < done {
continue
}
start := max(tokenStart, done)
token := v[start:at]
colon := strings.IndexByte(token, ':')
if colon < 0 || strings.Contains(token, "://") {
if colonAt < 0 || (schemeAt >= 0 && schemeAt+3 <= at) {
continue
}
token := v[start:at]
colon := colonAt - start
keep := token[:strings.LastIndexByte(token[:colon], '=')+1]
b.WriteString(v[done:start])
b.WriteString(keep)
b.WriteString("****")
done = at
colonAt, schemeAt = -1, -1
}
b.WriteString(v[done:])
return b.String()
Expand Down
Loading