From dcef86db1c35a03a65c10ed4682bcf4f2cb3bcad Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Sat, 4 Dec 2021 21:20:00 -0800 Subject: [PATCH 1/2] 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. --- funcr/example_test.go | 14 ++++++++++++++ funcr/funcr.go | 34 +++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/funcr/example_test.go b/funcr/example_test.go index 9197a9a..53373a2 100644 --- a/funcr/example_test.go +++ b/funcr/example_test.go @@ -111,3 +111,17 @@ func ExamplePseudoStruct() { log.Info("the message", "key", funcr.PseudoStruct(kv)) // Output: {"logger":"","level":0,"msg":"the message","key":{"field1":12345,"field2":true}} } + +func ExampleOptions_maxLogDepth() { + type List struct { + Next *List + } + l := List{} + l.Next = &l // recursive + + var log logr.Logger = funcr.NewJSON( + func(obj string) { fmt.Println(obj) }, + funcr.Options{MaxLogDepth: 4}) + log.Info("recursive", "list", l) + // Output: {"logger":"","level":0,"msg":"recursive","list":{"Next":{"Next":{"Next":{"Next":{"Next":""}}}}}} +} diff --git a/funcr/funcr.go b/funcr/funcr.go index 4727d27..c1a4154 100644 --- a/funcr/funcr.go +++ b/funcr/funcr.go @@ -126,6 +126,14 @@ type Options struct { // called for key-value pairs passed directly to Info and Error. See // RenderBuiltinsHook for more details. RenderArgsHook func(kvList []interface{}) []interface{} + + // MaxLogDepth tells funcr how many levels of nested fields (e.g. 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. + MaxLogDepth int } // MessageClass indicates which category or categories of messages to consider. @@ -194,11 +202,15 @@ func NewFormatterJSON(opts Options) Formatter { } const defaultTimestampFmt = "2006-01-02 15:04:05.000000" +const defaultMaxDepth = 16 func newFormatter(opts Options, outfmt outputFormat) Formatter { if opts.TimestampFormat == "" { opts.TimestampFormat = defaultTimestampFmt } + if opts.MaxLogDepth == 0 { + opts.MaxLogDepth = defaultMaxDepth + } f := Formatter{ outputFormat: outfmt, prefix: "", @@ -321,7 +333,7 @@ func (f Formatter) flatten(buf *bytes.Buffer, kvList []interface{}, continuing b } func (f Formatter) pretty(value interface{}) string { - return f.prettyWithFlags(value, 0) + return f.prettyWithFlags(value, 0, 0) } const ( @@ -329,7 +341,11 @@ const ( ) // TODO: This is not fast. Most of the overhead goes here. -func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string { +func (f Formatter) prettyWithFlags(value interface{}, flags uint32, depth int) string { + if depth > f.opts.MaxLogDepth { + return `""` + } + // Handle types that take full control of logging. if v, ok := value.(logr.Marshaler); ok { // Replace the value with what the type wants to get logged. @@ -394,7 +410,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string { // arbitrary keys might need escaping buf.WriteString(prettyString(v[i].(string))) buf.WriteByte(':') - buf.WriteString(f.pretty(v[i+1])) + buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1)) } if flags&flagRawStruct == 0 { buf.WriteByte('}') @@ -464,7 +480,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string { buf.WriteByte(',') } if fld.Anonymous && fld.Type.Kind() == reflect.Struct && name == "" { - buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct)) + buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1)) continue } if name == "" { @@ -475,7 +491,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string { buf.WriteString(name) buf.WriteByte('"') buf.WriteByte(':') - buf.WriteString(f.pretty(v.Field(i).Interface())) + buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1)) } if flags&flagRawStruct == 0 { buf.WriteByte('}') @@ -488,7 +504,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string { buf.WriteByte(',') } e := v.Index(i) - buf.WriteString(f.pretty(e.Interface())) + buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1)) } buf.WriteByte(']') return buf.String() @@ -513,7 +529,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string { keystr = prettyString(keystr) } else { // prettyWithFlags will produce already-escaped values - keystr = f.prettyWithFlags(it.Key().Interface(), 0) + keystr = f.prettyWithFlags(it.Key().Interface(), 0, depth+1) if t.Key().Kind() != reflect.String { // JSON only does string keys. Unlike Go's standard JSON, we'll // convert just about anything to a string. @@ -522,7 +538,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string { } buf.WriteString(keystr) buf.WriteByte(':') - buf.WriteString(f.pretty(it.Value().Interface())) + buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1)) i++ } buf.WriteByte('}') @@ -531,7 +547,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string { if v.IsNil() { return "null" } - return f.pretty(v.Elem().Interface()) + return f.prettyWithFlags(v.Elem().Interface(), 0, depth) } return fmt.Sprintf(`""`, t.Kind().String()) } From 2ccfbf60909e32b5f5cd5f55d268c9696790c713 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Sat, 4 Dec 2021 21:24:40 -0800 Subject: [PATCH 2/2] Fix some internal names --- funcr/funcr.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/funcr/funcr.go b/funcr/funcr.go index c1a4154..b23ab96 100644 --- a/funcr/funcr.go +++ b/funcr/funcr.go @@ -201,15 +201,16 @@ func NewFormatterJSON(opts Options) Formatter { return newFormatter(opts, outputJSON) } -const defaultTimestampFmt = "2006-01-02 15:04:05.000000" -const defaultMaxDepth = 16 +// Defaults for Options. +const defaultTimestampFormat = "2006-01-02 15:04:05.000000" +const defaultMaxLogDepth = 16 func newFormatter(opts Options, outfmt outputFormat) Formatter { if opts.TimestampFormat == "" { - opts.TimestampFormat = defaultTimestampFmt + opts.TimestampFormat = defaultTimestampFormat } if opts.MaxLogDepth == 0 { - opts.MaxLogDepth = defaultMaxDepth + opts.MaxLogDepth = defaultMaxLogDepth } f := Formatter{ outputFormat: outfmt,