【问题标题】:Why this Golang code could leak value of variable which is in memory为什么这个 Golang 代码会泄漏内存中的变量值
【发布时间】:2020-02-03 22:08:58
【问题描述】:

此代码可能会泄漏内存中变量的值。

我认为fmt.XprintY 可能没有重置缓冲区,但我的调试尝试没有结果。

package main

import (
    "bytes"
    "fmt"
    "io"
    "text/template"
)

type SecWriter struct {
    w io.Writer
}

func (s *SecWriter) Write(p []byte) (n int, err error) {
    fmt.Println(string(p), len(p), cap(p))

    // here
    tmp := fmt.Sprintln("info{SSSSSSSSSSSSSSSSSSSSSSSSSSS}")
    if tmp == ""{}

    s.w.Write(p[:64])
    return 64, nil
}

func index() {
    exp := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA{{1}}"

    b := &bytes.Buffer{}
    s := &SecWriter{
        w: b,
    }


    t := template.Must(template.New("index").Parse(exp))
    t.Execute(s, nil)

    fmt.Println("buf: ", b.String())
}

func main() {
    index()
}

我的go env

set GOARCH=amd64
set GOOS=windows

去版本

go version go1.12.5 windows/amd64

输出是:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 64 64
1 1 128
buf: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1nfo{SSSSSSSSSSSSSSSSSSSSSSSSSSS}                 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

如你所见,内存中变量的部分值:

tmp := fmt.Sprintln("info{SSSSSSSSSSSSSSSSSSSSSSSSSSS}")

泄漏到缓冲区。

【问题讨论】:

    标签: go memory slice


    【解决方案1】:

    在 Go 中,表达式 s.w.Write(p[:64]) 可以将切片扩展到超出其长度而不会出错(直到切片的容量)。在这种情况下,提供的缓冲区长度仅为 1,但您将其扩展为 64(如输出的第二行所示)。额外的 63 个字节中的内容是未定义的,它恰好是一些 fmt 方法的输出。

    解决方案是检查切片的长度。如果想让切片万无一失,确保超出长度的内容看不到,可以使用切片的三索引语法来设置其容量,例如p = p[::len(p)]

    【讨论】:

      【解决方案2】:

      如果你直接赋值给变量而不是做fmt.Sprintln,变量不会泄漏。

      代码:https://play.golang.org/p/Nz0y_MfDjP1

      所以我相信fmt.Sprintln 导致了泄漏。该函数调用另一个未导出的函数newPrinter 来获取printer,而printer 又维护它自己的池和缓存。我还不够深入,但我的猜测是您手动创建的缓冲区可能会以某种方式在这里重叠/共享。

      (如果发现其他问题,我会更新答案)

      【讨论】:

      • 我试图在没有Sprintln 的情况下运行,它似乎已经从输出中删除了"info{SSSSSSSSSSSSSSSSSSSSSSSSSSS}" 部分。 play.golang.org/p/Nz0y_MfDjP1
      • 是的,所以我认为这是关于 XprintY 函数的缓冲区。并且在使用fmt.Println 打印到标准输出时,值也被泄露了。 play.golang.org/p/cmMwp6Tidnr。好像只有printer.buf会和局部变量p[:64]共享内存
      【解决方案3】:

      没有内存泄漏this 证明了这一点。
      但是有一个issue:在func Fprint(w io.Writer, a ...interface{}) (n int, err error) 中对p := newPrinter() 的调用在此处初始化p.fmt.init(&p.buf) 返回空闲内存(切片的底层数组)而不将其初始化为零(可能出于性能原因未初始化 - 我们预计会全部为零)。

      TL;DR:
      两种解决方案:
      1. 解决方法:使用s.w.Write(p) 而不是s.w.Write(p[:64]),或者编辑您的代码并将p[len(p):cap(p)] 全部设置为零(如果您不使用或无法使用第二种解决方案):

      func (s *SecWriter) Write(p []byte) (n int, err error) {
          b := p[len(p):cap(p)]
          for i := range b {
              b[i] = 0
          }
          fmt.Println(string(p), len(p), cap(p))
          // here
          tmp := fmt.Sprintln("info{SSSSSSSSSSSSSSSSSSSSSSSSSSS}")
          if tmp == "" {
          }
          s.w.Write(p[:64])
          return 64, nil
      }
      

      1. 在 Windows (C:\Go\src\fmt\format.go) 或 Linux (/usr/local/go/src/fmt/format.go) 文件的第 58 行将缓冲区设置为全零:
          b := (*buf)[:cap(*buf)]
          for i := range b {
              b[i] = 0
          }
      

      这个函数内部:

      func (f *fmt) init(buf *buffer) {
          b := (*buf)[:cap(*buf)]
          for i := range b {
              b[i] = 0
          }
          f.buf = buf
          f.clearflags()
      }
      

      您的代码输出与此应用:

      AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 64 64
      1 1 128
      buf:  AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1
      

      长答案:
      您正在观看超出指定长度的切片数据,并且您可以看到切片数据到切片容量。
      您可以将:m.Writer.Write(p[:8]) 替换为:m.Writer.Write(p),这会使您的代码正常工作。以下代码显示template.Parse() 将模板标记为3 个部分,并调用my.Write() 3 次。这里有趣的部分是对my.Write()第二次 调用显示了一个编译器生成的具有不同切片容量的切片,该切片容量未初始化为零,“也许这是一个小无害issue”:

      如果你想窥探你电脑的内存,试试this

      package main
      
      import (
          "bytes"
          "fmt"
          "io"
          "text/template"
      )
      
      func main() {
          buf := &bytes.Buffer{}
          my := &myWriter{"You", buf}
          template.Must(template.New("my").Parse("Hi{{.Name}}Bye.")).Execute(my, my)
          fmt.Printf("<<%q>>\n", buf.String())
      }
      func (m *myWriter) Write(p []byte) (n int, err error) {
          fmt.Printf("len=%v cap=%v\t%v %v\n", len(p), cap(p), string(p), p[:cap(p)])
          no++
          fmt.Println("gen:", no, gen())
          m.Writer.Write(p)
          // m.Writer.Write(p[:8])
          return 8, nil
      }
      
      type myWriter struct {
          Name string
          io.Writer
      }
      
      const genLen = 8
      
      func gen() string {
          b := [genLen]byte{}
          for i := range b {
              b[i] = no
          }
          return string(b[:])
      }
      
      var no = byte(49) //'1'
      

      输出:

      len=2 cap=8 Hi [72 105 0 0 0 0 0 0]
      gen: 50 22222222
      len=3 cap=64    You [89 111 117 58 32 53 48 32 50 50 50 50 50 50 50 50 10 50 32 49 48 53 32 48 32 48 32 48 32 48 32 48 32 48 93 10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
      gen: 51 33333333
      len=4 cap=8 Bye. [66 121 101 46 0 0 0 0]
      gen: 52 44444444
      <<"HiYouBye.">>
      

      然后改const genLen = 64试试this 有趣cap=64 更改为 cap=128(这不是预期的):

      输出:

      len=2 cap=8 Hi [72 105 0 0 0 0 0 0]
      gen: 50 2222222222222222222222222222222222222222222222222222222222222222
      len=3 cap=128   You [89 111 117 58 32 53 48 32 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
      gen: 51 3333333333333333333333333333333333333333333333333333333333333333
      len=4 cap=8 Bye. [66 121 101 46 0 0 0 0]
      gen: 52 4444444444444444444444444444444444444444444444444444444444444444
      <<"HiYouBye.">>
      

      t.Execute(my, my) 调用func (m *myWriter) Write(p []byte),所以plen=3cap=128 由模板引擎生成。

      在第 230 行调试 /usr/local/go/src/fmt/print.go 文件中的第二个代码后,它似乎是 fmt.bufferlength=3cap=128,这里:

      func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
          p := newPrinter()
          p.doPrint(a)
          n, err = w.Write(p.buf)
          p.free()
          return
      }
      

      p := newPrinter()的调用在这里初始化p.fmt.init(&amp;p.buf)

      // newPrinter allocates a new pp struct or grabs a cached one.
      func newPrinter() *pp {
          p := ppFree.Get().(*pp)
          p.panicking = false
          p.erroring = false
          p.wrapErrs = false
          p.fmt.init(&p.buf)
          return p
      }
      

      获取并返回空闲内存而不将其初始化为零。

      【讨论】:

      • 我知道模板引擎会将“Hi{{.Name}}Bye”识别为TextNode, ActionNode, TextNode,并会写入 3 次。但是,在您的代码中,在第二次写入时:[]byte("50 22222222") 位于局部变量 p 中,并且在最后一个循环中写入标准输出。所以我认为不是因为模板使用的切片没有初始化。
      • t.Execute(my, my) 调用func (m *myWriter) Write(p []byte),所以plen=3cap=128 由模板引擎生成。
      • 查看解决方案,自己尝试并告诉我。您的代码输出与解决方案:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 64 64 1 1 128 buf: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1
      • 很抱歉没有及时回复您。感谢你付出的努力!因此,如果我理解正确:在第一次调用myWriter.Write 时,fmt.Sprint 将缓冲区设置为“info{SSSSS}”,然后在第二次调用时,fmt.printer 从ppFree sync.Pool 获取缓存而不设置缓冲区全部到字节(0)。但是还有一点让我困惑: 1、如果我在函数index而不是Write(play.golang.org/p/u6jJDaBHfk5)中调用fmt.Sprintln,fmt.buffer中的内存不会泄露; 2.为什么fmt.buffer中的内存被读取到模板引擎生成的局部变量p []byte中?
      • 代码中的 "info{SSSSS}" tmp := fmt.Sprintln("info{SSSSSSSSSSSSSSSSSSSSSSSSSSS}") 或我的代码中的 `fmt.Println("gen:", no, gen())` 是编译器放入的函数调用参数堆栈(如预期的那样)调用函数fmt.Sprintln() 或我的代码fmt.Println() 所以当函数调用完成时(从该函数返回)堆栈(内存)足迹就在那里(没有任何问题)和下一个内存新切片接受并使用它(没关系),但我们预计新切片初始化为零(可能出于性能原因未初始化)。
      猜你喜欢
      • 2011-10-21
      • 1970-01-01
      • 2017-07-01
      • 2011-01-17
      • 1970-01-01
      • 2016-04-12
      • 2013-04-27
      相关资源
      最近更新 更多