为什么这段 Golang 代码可能会泄露存储在内存中的变量值

3
该代码可能会泄露存储在内存中的变量的值。
我认为可能是因为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语言版本

go version go1.12.5 windows/amd64

输出结果为:

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

如您所见,变量在内存中的一部分值:

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

缓冲区泄漏。

3个回答

2
在Go语言中,表达式s.w.Write(p[:64])可以将切片扩展到其长度之外而不会出错(直到切片的容量)。在这种情况下,提供的缓冲区长度只有1,但是您将其扩展到了64(如输出的第二行所示)。额外的63字节内容未定义,恰好是一些fmt方法的输出。
解决方案是检查切片的长度。如果想使切片防错并确保无法看到其长度之外的内容,可以使用切片的三个索引语法来设置其容量,例如p = p[::len(p)]

2

证明了没有内存泄漏,这里 有证据支持。
但是存在一个问题:在 func Fprint(w io.Writer, a ...interface{}) (n int, err error) 中初始化的 p := newPrinter(); p.fmt.init(&p.buf) 返回了没有初始化为零的空闲内存 (切片的基础数组),这可能是由于性能原因而没有被正确初始化 - 我们预期该值应该都是零。

简而言之:
两种解决方法:
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
}

在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() 三次。这里有趣的部分是第二次调用 my.Write() 显示了一个编译器生成的具有不同切片容量的切片,该切片未初始化为零,“也许这只是一个无害的小问题issue”:

如果您想窥探计算机的内存,请尝试这个

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更改为此处尝试 有趣的是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),所以模板引擎生成了长度为3、容量为128的 p

在调试位于 /usr/local/go/src/fmt/print.go 文件中第230行的第二个代码后,似乎是 fmt.buffer 的长度为3,容量为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(&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中,并且在上一个循环中已经写入了stdout。因此,我不认为这是由于模板使用的切片未初始化造成的。 - Iv4n
t.Execute(my, my) 调用 func (m *myWriter) Write(p []byte),因此由模板引擎生成的 p 具有 len=3cap=128 - wasmup
看看解决方案,自己尝试一下,然后让我知道。你的代码输出与解决方案:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 64 64 1 1 128 buf: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1 - wasmup
很抱歉我没有及时回复你。感谢你的努力!所以如果我理解正确:在第一次调用myWriter.Write时,fmt.Sprint将缓冲区设置为“info {SSSSS}”,然后在第二次调用时,fmt.printer从ppFree sync.Pool获取缓存,并且不会将缓冲区全部设置为byte(0)。但仍有一些让我困惑的地方:1.如果我在函数index中调用fmt.Sprintln而不是Write(https://play.golang.org/p/u6jJDaBHfk5),则fmt.buffer中的内存不会泄漏;2.为什么模板引擎生成的本地变量`p []byte`中读取了fmt.buffer中的内存? - Iv4n
你代码中的 "info{SSSSS}" 或者我的代码中的 fmt.Println("gen:", no, gen()) 是函数调用参数,编译器将其放入堆栈中以调用函数 fmt.Sprintln() 或者我的代码中的 fmt.Println()。因此,当函数调用完成(从该函数返回)时,堆栈(内存)占用仍然存在(没有任何问题),新切片的下一个内存会使用它(这是可以的)。但我们期望新的切片被初始化为零(由于性能原因可能没有被正确初始化)。 - wasmup
显示剩余2条评论

0

如果您直接分配给变量而不是使用fmt.Sprintln,则变量将不会泄漏。

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

因此,我认为fmt.Sprintln导致了泄漏。该函数调用另一个未公开的函数newPrinter以获取一个printer,后者又维护其自己的池和缓存。我还没有深入研究,但我的猜测是手动创建的缓冲区可能在这里被重叠/共享。

(如果我发现其他内容,我会更新答案)


我尝试在没有使用Sprintln的情况下运行,似乎已经从输出中删除了"info{SSSSSSSSSSSSSSSSSSSSSSSSSSS}"这部分。 https://play.golang.org/p/Nz0y_MfDjP1 - masnun
1
是的,我认为这与XprintY函数的缓冲区有关。而在使用fmt.Println将值打印到标准输出时,该值也会泄露。https://play.golang.org/p/cmMwp6Tidnr。看起来只有`printer.buf`将与局部变量`p[:64]`共享内存。 - Iv4n

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接