Skip to content

Commit dad393a

Browse files
authored
tflog+tfsdklog: Prevent slice/map leaks when setting LoggerOpts (#132)
Reference: #126 Reference: #131 Since the `LoggerOpts` struct contains slice and map fields, it is important to ensure any modifications occur on copies of those slices and maps, otherwise the memory reference can wind up being shared. Consumers should always be able to create a new `context.Context` without worrying about shared data. This change introduces a `Copy()` method for `LoggerOpts` and implements it for option modifier functions which adjust a map or slice.
1 parent 3ca5214 commit dad393a

File tree

11 files changed

+1849
-36
lines changed

11 files changed

+1849
-36
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: BUG FIXES
2+
body: 'tflog+tflogsdk: Prevented data race conditions when using SetField and other
3+
option functions'
4+
time: 2023-02-07T16:55:20.433603-05:00
5+
custom:
6+
Issue: "132"

internal/logging/options.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,47 @@ type LoggerOpts struct {
157157
MaskMessageStrings []string
158158
}
159159

160+
// Copy creates a duplicate LoggerOpts. This should be used to ensure
161+
// safe LoggerOpts modification when the LoggerOpts could be saved into a
162+
// new context.Context.
163+
func (o LoggerOpts) Copy() LoggerOpts {
164+
result := LoggerOpts{
165+
AdditionalLocationOffset: o.AdditionalLocationOffset,
166+
Fields: make(map[string]any, len(o.Fields)),
167+
IncludeLocation: o.IncludeLocation,
168+
IncludeRootFields: o.IncludeRootFields,
169+
IncludeTime: o.IncludeTime,
170+
Level: o.Level,
171+
MaskAllFieldValuesRegexes: make([]*regexp.Regexp, len(o.MaskAllFieldValuesRegexes)),
172+
MaskAllFieldValuesStrings: make([]string, len(o.MaskAllFieldValuesStrings)),
173+
MaskFieldValuesWithFieldKeys: make([]string, len(o.MaskFieldValuesWithFieldKeys)),
174+
MaskMessageRegexes: make([]*regexp.Regexp, len(o.MaskMessageRegexes)),
175+
MaskMessageStrings: make([]string, len(o.MaskMessageStrings)),
176+
Name: o.Name,
177+
OmitLogWithFieldKeys: make([]string, len(o.OmitLogWithFieldKeys)),
178+
OmitLogWithMessageRegexes: make([]*regexp.Regexp, len(o.OmitLogWithMessageRegexes)),
179+
OmitLogWithMessageStrings: make([]string, len(o.OmitLogWithMessageStrings)),
180+
Output: o.Output,
181+
}
182+
183+
// Copy all slice/map contents to prevent leaking memory references
184+
// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
185+
for key, value := range o.Fields {
186+
result.Fields[key] = value
187+
}
188+
189+
copy(result.MaskAllFieldValuesRegexes, o.MaskAllFieldValuesRegexes)
190+
copy(result.MaskAllFieldValuesStrings, o.MaskAllFieldValuesStrings)
191+
copy(result.MaskFieldValuesWithFieldKeys, o.MaskFieldValuesWithFieldKeys)
192+
copy(result.MaskMessageRegexes, o.MaskMessageRegexes)
193+
copy(result.MaskMessageStrings, o.MaskMessageStrings)
194+
copy(result.OmitLogWithFieldKeys, o.OmitLogWithFieldKeys)
195+
copy(result.OmitLogWithMessageRegexes, o.OmitLogWithMessageRegexes)
196+
copy(result.OmitLogWithMessageStrings, o.OmitLogWithMessageStrings)
197+
198+
return result
199+
}
200+
160201
// ApplyLoggerOpts generates a LoggerOpts out of a list of Option
161202
// implementations. By default, AdditionalLocationOffset is 1, IncludeLocation
162203
// is true, IncludeTime is true, and Output is os.Stderr.

internal/logging/options_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package logging_test
2+
3+
import (
4+
"os"
5+
"regexp"
6+
"testing"
7+
8+
"github.com/google/go-cmp/cmp"
9+
"github.com/hashicorp/go-hclog"
10+
"github.com/hashicorp/terraform-plugin-log/internal/logging"
11+
)
12+
13+
func TestLoggerOptsCopy(t *testing.T) {
14+
t.Parallel()
15+
16+
regex1 := regexp.MustCompile("regex1")
17+
regex2 := regexp.MustCompile("regex2")
18+
19+
// Populate all fields.
20+
originalLoggerOpts := logging.LoggerOpts{
21+
AdditionalLocationOffset: 1,
22+
Fields: map[string]any{"key1": "value1"},
23+
IncludeLocation: true,
24+
IncludeRootFields: true,
25+
IncludeTime: true,
26+
Level: hclog.Error,
27+
MaskAllFieldValuesRegexes: []*regexp.Regexp{regex1},
28+
MaskAllFieldValuesStrings: []string{"string1"},
29+
MaskFieldValuesWithFieldKeys: []string{"string1"},
30+
MaskMessageRegexes: []*regexp.Regexp{regex1},
31+
MaskMessageStrings: []string{"string1"},
32+
Name: "name1",
33+
OmitLogWithFieldKeys: []string{"string1"},
34+
OmitLogWithMessageRegexes: []*regexp.Regexp{regex1},
35+
OmitLogWithMessageStrings: []string{"string1"},
36+
Output: os.Stdout,
37+
}
38+
39+
// Expected LoggerOpts should exactly match original.
40+
expectedLoggerOpts := logging.LoggerOpts{
41+
AdditionalLocationOffset: 1,
42+
Fields: map[string]any{"key1": "value1"},
43+
IncludeLocation: true,
44+
IncludeRootFields: true,
45+
IncludeTime: true,
46+
Level: hclog.Error,
47+
MaskAllFieldValuesRegexes: []*regexp.Regexp{regex1},
48+
MaskAllFieldValuesStrings: []string{"string1"},
49+
MaskFieldValuesWithFieldKeys: []string{"string1"},
50+
MaskMessageRegexes: []*regexp.Regexp{regex1},
51+
MaskMessageStrings: []string{"string1"},
52+
Name: "name1",
53+
OmitLogWithFieldKeys: []string{"string1"},
54+
OmitLogWithMessageRegexes: []*regexp.Regexp{regex1},
55+
OmitLogWithMessageStrings: []string{"string1"},
56+
Output: os.Stdout,
57+
}
58+
59+
// Create a copy before modifying the original LoggerOpts. This will be
60+
// checked against the expected LoggerOpts after modifications.
61+
copiedLoggerOpts := originalLoggerOpts.Copy()
62+
63+
// Ensure modifications of original does not effect copy.
64+
originalLoggerOpts.AdditionalLocationOffset = 2
65+
originalLoggerOpts.Fields["key2"] = "value2"
66+
originalLoggerOpts.IncludeLocation = false
67+
originalLoggerOpts.IncludeRootFields = false
68+
originalLoggerOpts.IncludeTime = false
69+
originalLoggerOpts.Level = hclog.Debug
70+
originalLoggerOpts.MaskAllFieldValuesRegexes = append(originalLoggerOpts.MaskAllFieldValuesRegexes, regex2)
71+
originalLoggerOpts.MaskAllFieldValuesStrings = append(originalLoggerOpts.MaskAllFieldValuesStrings, "string2")
72+
originalLoggerOpts.MaskFieldValuesWithFieldKeys = append(originalLoggerOpts.MaskFieldValuesWithFieldKeys, "string2")
73+
originalLoggerOpts.MaskMessageRegexes = append(originalLoggerOpts.MaskMessageRegexes, regex2)
74+
originalLoggerOpts.MaskMessageStrings = append(originalLoggerOpts.MaskMessageStrings, "string2")
75+
originalLoggerOpts.Name = "name2"
76+
originalLoggerOpts.OmitLogWithFieldKeys = append(originalLoggerOpts.OmitLogWithFieldKeys, "string2")
77+
originalLoggerOpts.OmitLogWithMessageRegexes = append(originalLoggerOpts.OmitLogWithMessageRegexes, regex2)
78+
originalLoggerOpts.OmitLogWithMessageStrings = append(originalLoggerOpts.OmitLogWithMessageStrings, "string2")
79+
originalLoggerOpts.Output = os.Stderr
80+
81+
// Prevent go-cmp errors.
82+
cmpOpts := []cmp.Option{
83+
cmp.Comparer(func(i, j *os.File) bool {
84+
if i == nil {
85+
return j == nil
86+
}
87+
88+
if j == nil {
89+
return false
90+
}
91+
92+
// Simple comparison test is good enough for our purposes.
93+
return i.Fd() == j.Fd()
94+
}),
95+
cmp.Comparer(func(i, j *regexp.Regexp) bool {
96+
if i == nil {
97+
return j == nil
98+
}
99+
100+
if j == nil {
101+
return false
102+
}
103+
104+
// Simple comparison test is good enough for our purposes.
105+
return i.String() == j.String()
106+
}),
107+
}
108+
109+
if diff := cmp.Diff(copiedLoggerOpts, expectedLoggerOpts, cmpOpts...); diff != "" {
110+
t.Errorf("unexpected difference: %s", diff)
111+
}
112+
}

tflog/provider.go

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import (
1515
func SetField(ctx context.Context, key string, value interface{}) context.Context {
1616
lOpts := logging.GetProviderRootTFLoggerOpts(ctx)
1717

18-
lOpts = logging.WithField(key, value)(lOpts)
18+
// Copy to prevent slice/map aliasing issues.
19+
// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
20+
lOpts = logging.WithField(key, value)(lOpts.Copy())
1921

2022
return logging.SetProviderRootTFLoggerOpts(ctx, lOpts)
2123
}
@@ -152,7 +154,9 @@ func Error(ctx context.Context, msg string, additionalFields ...map[string]inter
152154
func OmitLogWithFieldKeys(ctx context.Context, keys ...string) context.Context {
153155
lOpts := logging.GetProviderRootTFLoggerOpts(ctx)
154156

155-
lOpts = logging.WithOmitLogWithFieldKeys(keys...)(lOpts)
157+
// Copy to prevent slice/map aliasing issues.
158+
// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
159+
lOpts = logging.WithOmitLogWithFieldKeys(keys...)(lOpts.Copy())
156160

157161
return logging.SetProviderRootTFLoggerOpts(ctx, lOpts)
158162
}
@@ -174,7 +178,9 @@ func OmitLogWithFieldKeys(ctx context.Context, keys ...string) context.Context {
174178
func OmitLogWithMessageRegexes(ctx context.Context, expressions ...*regexp.Regexp) context.Context {
175179
lOpts := logging.GetProviderRootTFLoggerOpts(ctx)
176180

177-
lOpts = logging.WithOmitLogWithMessageRegexes(expressions...)(lOpts)
181+
// Copy to prevent slice/map aliasing issues.
182+
// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
183+
lOpts = logging.WithOmitLogWithMessageRegexes(expressions...)(lOpts.Copy())
178184

179185
return logging.SetProviderRootTFLoggerOpts(ctx, lOpts)
180186
}
@@ -195,7 +201,9 @@ func OmitLogWithMessageRegexes(ctx context.Context, expressions ...*regexp.Regex
195201
func OmitLogWithMessageStrings(ctx context.Context, matchingStrings ...string) context.Context {
196202
lOpts := logging.GetProviderRootTFLoggerOpts(ctx)
197203

198-
lOpts = logging.WithOmitLogWithMessageStrings(matchingStrings...)(lOpts)
204+
// Copy to prevent slice/map aliasing issues.
205+
// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
206+
lOpts = logging.WithOmitLogWithMessageStrings(matchingStrings...)(lOpts.Copy())
199207

200208
return logging.SetProviderRootTFLoggerOpts(ctx, lOpts)
201209
}
@@ -217,7 +225,9 @@ func OmitLogWithMessageStrings(ctx context.Context, matchingStrings ...string) c
217225
func MaskFieldValuesWithFieldKeys(ctx context.Context, keys ...string) context.Context {
218226
lOpts := logging.GetProviderRootTFLoggerOpts(ctx)
219227

220-
lOpts = logging.WithMaskFieldValuesWithFieldKeys(keys...)(lOpts)
228+
// Copy to prevent slice/map aliasing issues.
229+
// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
230+
lOpts = logging.WithMaskFieldValuesWithFieldKeys(keys...)(lOpts.Copy())
221231

222232
return logging.SetProviderRootTFLoggerOpts(ctx, lOpts)
223233
}
@@ -241,7 +251,9 @@ func MaskFieldValuesWithFieldKeys(ctx context.Context, keys ...string) context.C
241251
func MaskAllFieldValuesRegexes(ctx context.Context, expressions ...*regexp.Regexp) context.Context {
242252
lOpts := logging.GetProviderRootTFLoggerOpts(ctx)
243253

244-
lOpts = logging.WithMaskAllFieldValuesRegexes(expressions...)(lOpts)
254+
// Copy to prevent slice/map aliasing issues.
255+
// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
256+
lOpts = logging.WithMaskAllFieldValuesRegexes(expressions...)(lOpts.Copy())
245257

246258
return logging.SetProviderRootTFLoggerOpts(ctx, lOpts)
247259
}
@@ -265,7 +277,9 @@ func MaskAllFieldValuesRegexes(ctx context.Context, expressions ...*regexp.Regex
265277
func MaskAllFieldValuesStrings(ctx context.Context, matchingStrings ...string) context.Context {
266278
lOpts := logging.GetProviderRootTFLoggerOpts(ctx)
267279

268-
lOpts = logging.WithMaskAllFieldValuesStrings(matchingStrings...)(lOpts)
280+
// Copy to prevent slice/map aliasing issues.
281+
// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
282+
lOpts = logging.WithMaskAllFieldValuesStrings(matchingStrings...)(lOpts.Copy())
269283

270284
return logging.SetProviderRootTFLoggerOpts(ctx, lOpts)
271285
}
@@ -287,7 +301,9 @@ func MaskAllFieldValuesStrings(ctx context.Context, matchingStrings ...string) c
287301
func MaskMessageRegexes(ctx context.Context, expressions ...*regexp.Regexp) context.Context {
288302
lOpts := logging.GetProviderRootTFLoggerOpts(ctx)
289303

290-
lOpts = logging.WithMaskMessageRegexes(expressions...)(lOpts)
304+
// Copy to prevent slice/map aliasing issues.
305+
// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
306+
lOpts = logging.WithMaskMessageRegexes(expressions...)(lOpts.Copy())
291307

292308
return logging.SetProviderRootTFLoggerOpts(ctx, lOpts)
293309
}
@@ -309,7 +325,9 @@ func MaskMessageRegexes(ctx context.Context, expressions ...*regexp.Regexp) cont
309325
func MaskMessageStrings(ctx context.Context, matchingStrings ...string) context.Context {
310326
lOpts := logging.GetProviderRootTFLoggerOpts(ctx)
311327

312-
lOpts = logging.WithMaskMessageStrings(matchingStrings...)(lOpts)
328+
// Copy to prevent slice/map aliasing issues.
329+
// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
330+
lOpts = logging.WithMaskMessageStrings(matchingStrings...)(lOpts.Copy())
313331

314332
return logging.SetProviderRootTFLoggerOpts(ctx, lOpts)
315333
}

0 commit comments

Comments
 (0)