Skip to content

Commit dcef86d

Browse files
committed
funcr: Prevent stack overflow on recursive structs
Add a new Option `MaxLogDepth` which tells funcr how many levels of nested fields (e.g. a struct that contains a struct that contains a struct, etc.) it may log. Every time it finds a struct, slice, array, or map the depth is increased by one. When the maximum is reached, the value will be converted to a string indicating that the max depth has been exceeded. If this field is not specified, a default value will be used.
1 parent ebe3534 commit dcef86d

File tree

2 files changed

+39
-9
lines changed

2 files changed

+39
-9
lines changed

Diff for: funcr/example_test.go

+14
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,17 @@ func ExamplePseudoStruct() {
111111
log.Info("the message", "key", funcr.PseudoStruct(kv))
112112
// Output: {"logger":"","level":0,"msg":"the message","key":{"field1":12345,"field2":true}}
113113
}
114+
115+
func ExampleOptions_maxLogDepth() {
116+
type List struct {
117+
Next *List
118+
}
119+
l := List{}
120+
l.Next = &l // recursive
121+
122+
var log logr.Logger = funcr.NewJSON(
123+
func(obj string) { fmt.Println(obj) },
124+
funcr.Options{MaxLogDepth: 4})
125+
log.Info("recursive", "list", l)
126+
// Output: {"logger":"","level":0,"msg":"recursive","list":{"Next":{"Next":{"Next":{"Next":{"Next":"<max-log-depth-exceeded>"}}}}}}
127+
}

Diff for: funcr/funcr.go

+25-9
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,14 @@ type Options struct {
126126
// called for key-value pairs passed directly to Info and Error. See
127127
// RenderBuiltinsHook for more details.
128128
RenderArgsHook func(kvList []interface{}) []interface{}
129+
130+
// MaxLogDepth tells funcr how many levels of nested fields (e.g. a struct
131+
// that contains a struct, etc.) it may log. Every time it finds a struct,
132+
// slice, array, or map the depth is increased by one. When the maximum is
133+
// reached, the value will be converted to a string indicating that the max
134+
// depth has been exceeded. If this field is not specified, a default
135+
// value will be used.
136+
MaxLogDepth int
129137
}
130138

131139
// MessageClass indicates which category or categories of messages to consider.
@@ -194,11 +202,15 @@ func NewFormatterJSON(opts Options) Formatter {
194202
}
195203

196204
const defaultTimestampFmt = "2006-01-02 15:04:05.000000"
205+
const defaultMaxDepth = 16
197206

198207
func newFormatter(opts Options, outfmt outputFormat) Formatter {
199208
if opts.TimestampFormat == "" {
200209
opts.TimestampFormat = defaultTimestampFmt
201210
}
211+
if opts.MaxLogDepth == 0 {
212+
opts.MaxLogDepth = defaultMaxDepth
213+
}
202214
f := Formatter{
203215
outputFormat: outfmt,
204216
prefix: "",
@@ -321,15 +333,19 @@ func (f Formatter) flatten(buf *bytes.Buffer, kvList []interface{}, continuing b
321333
}
322334

323335
func (f Formatter) pretty(value interface{}) string {
324-
return f.prettyWithFlags(value, 0)
336+
return f.prettyWithFlags(value, 0, 0)
325337
}
326338

327339
const (
328340
flagRawStruct = 0x1 // do not print braces on structs
329341
)
330342

331343
// TODO: This is not fast. Most of the overhead goes here.
332-
func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
344+
func (f Formatter) prettyWithFlags(value interface{}, flags uint32, depth int) string {
345+
if depth > f.opts.MaxLogDepth {
346+
return `"<max-log-depth-exceeded>"`
347+
}
348+
333349
// Handle types that take full control of logging.
334350
if v, ok := value.(logr.Marshaler); ok {
335351
// Replace the value with what the type wants to get logged.
@@ -394,7 +410,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
394410
// arbitrary keys might need escaping
395411
buf.WriteString(prettyString(v[i].(string)))
396412
buf.WriteByte(':')
397-
buf.WriteString(f.pretty(v[i+1]))
413+
buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1))
398414
}
399415
if flags&flagRawStruct == 0 {
400416
buf.WriteByte('}')
@@ -464,7 +480,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
464480
buf.WriteByte(',')
465481
}
466482
if fld.Anonymous && fld.Type.Kind() == reflect.Struct && name == "" {
467-
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct))
483+
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1))
468484
continue
469485
}
470486
if name == "" {
@@ -475,7 +491,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
475491
buf.WriteString(name)
476492
buf.WriteByte('"')
477493
buf.WriteByte(':')
478-
buf.WriteString(f.pretty(v.Field(i).Interface()))
494+
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1))
479495
}
480496
if flags&flagRawStruct == 0 {
481497
buf.WriteByte('}')
@@ -488,7 +504,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
488504
buf.WriteByte(',')
489505
}
490506
e := v.Index(i)
491-
buf.WriteString(f.pretty(e.Interface()))
507+
buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1))
492508
}
493509
buf.WriteByte(']')
494510
return buf.String()
@@ -513,7 +529,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
513529
keystr = prettyString(keystr)
514530
} else {
515531
// prettyWithFlags will produce already-escaped values
516-
keystr = f.prettyWithFlags(it.Key().Interface(), 0)
532+
keystr = f.prettyWithFlags(it.Key().Interface(), 0, depth+1)
517533
if t.Key().Kind() != reflect.String {
518534
// JSON only does string keys. Unlike Go's standard JSON, we'll
519535
// convert just about anything to a string.
@@ -522,7 +538,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
522538
}
523539
buf.WriteString(keystr)
524540
buf.WriteByte(':')
525-
buf.WriteString(f.pretty(it.Value().Interface()))
541+
buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1))
526542
i++
527543
}
528544
buf.WriteByte('}')
@@ -531,7 +547,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
531547
if v.IsNil() {
532548
return "null"
533549
}
534-
return f.pretty(v.Elem().Interface())
550+
return f.prettyWithFlags(v.Elem().Interface(), 0, depth)
535551
}
536552
return fmt.Sprintf(`"<unhandled-%s>"`, t.Kind().String())
537553
}

0 commit comments

Comments
 (0)