Skip to content

Commit 3058d38

Browse files
committed
strings: fix two Builder bugs allowing mutation of strings, remove ReadFrom
The Builder's ReadFrom method allows the underlying unsafe slice to escape, and for callers to subsequently modify memory that had been unsafely converted into an immutable string. In the original proposal for Builder (#18990), I'd noted there should be no Read methods: > There would be no Reset or Bytes or Truncate or Read methods. > Nothing that could mutate the []byte once it was unsafely converted > to a string. And in my prototype (https://golang.org/cl/37767), I handled ReadFrom properly, but when https://golang.org/cl/74931 arrived, I missed that it had a ReadFrom method and approved it. Because we're so close to the Go 1.10 release, just remove the ReadFrom method rather than think about possible fixes. It has marginal utility in a Builder anyway. Also, fix a separate bug that also allowed mutation of a slice's backing array after it had been converted into a slice by disallowing copies of the Builder by value. Updates #18990 Fixes #23083 Fixes #23084 Change-Id: Id1f860f8a4f5f88b32213cf85108ebc609acb95f Reviewed-on: https://go-review.googlesource.com/83255 Reviewed-by: Keith Randall <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent 29be20a commit 3058d38

File tree

3 files changed

+120
-127
lines changed

3 files changed

+120
-127
lines changed

api/go1.10.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,6 @@ pkg os, method (*SyscallError) Timeout() bool
594594
pkg os, var ErrNoDeadline error
595595
pkg strings, method (*Builder) Grow(int)
596596
pkg strings, method (*Builder) Len() int
597-
pkg strings, method (*Builder) ReadFrom(io.Reader) (int64, error)
598597
pkg strings, method (*Builder) Reset()
599598
pkg strings, method (*Builder) String() string
600599
pkg strings, method (*Builder) Write([]uint8) (int, error)

src/strings/builder.go

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,24 @@
55
package strings
66

77
import (
8-
"errors"
9-
"io"
108
"unicode/utf8"
119
"unsafe"
1210
)
1311

1412
// A Builder is used to efficiently build a string using Write methods.
1513
// It minimizes memory copying. The zero value is ready to use.
14+
// Do not copy a non-zero Builder.
1615
type Builder struct {
17-
buf []byte
16+
addr *Builder // of receiver, to detect copies by value
17+
buf []byte
18+
}
19+
20+
func (b *Builder) copyCheck() {
21+
if b.addr == nil {
22+
b.addr = b
23+
} else if b.addr != b {
24+
panic("strings: illegal use of non-zero Builder copied by value")
25+
}
1826
}
1927

2028
// String returns the accumulated string.
@@ -26,7 +34,10 @@ func (b *Builder) String() string {
2634
func (b *Builder) Len() int { return len(b.buf) }
2735

2836
// Reset resets the Builder to be empty.
29-
func (b *Builder) Reset() { b.buf = nil }
37+
func (b *Builder) Reset() {
38+
b.addr = nil
39+
b.buf = nil
40+
}
3041

3142
// grow copies the buffer to a new, larger buffer so that there are at least n
3243
// bytes of capacity beyond len(b.buf).
@@ -40,6 +51,7 @@ func (b *Builder) grow(n int) {
4051
// another n bytes. After Grow(n), at least n bytes can be written to b
4152
// without another allocation. If n is negative, Grow panics.
4253
func (b *Builder) Grow(n int) {
54+
b.copyCheck()
4355
if n < 0 {
4456
panic("strings.Builder.Grow: negative count")
4557
}
@@ -51,20 +63,23 @@ func (b *Builder) Grow(n int) {
5163
// Write appends the contents of p to b's buffer.
5264
// Write always returns len(p), nil.
5365
func (b *Builder) Write(p []byte) (int, error) {
66+
b.copyCheck()
5467
b.buf = append(b.buf, p...)
5568
return len(p), nil
5669
}
5770

5871
// WriteByte appends the byte c to b's buffer.
5972
// The returned error is always nil.
6073
func (b *Builder) WriteByte(c byte) error {
74+
b.copyCheck()
6175
b.buf = append(b.buf, c)
6276
return nil
6377
}
6478

6579
// WriteRune appends the UTF-8 encoding of Unicode code point r to b's buffer.
6680
// It returns the length of r and a nil error.
6781
func (b *Builder) WriteRune(r rune) (int, error) {
82+
b.copyCheck()
6883
if r < utf8.RuneSelf {
6984
b.buf = append(b.buf, byte(r))
7085
return 1, nil
@@ -81,38 +96,7 @@ func (b *Builder) WriteRune(r rune) (int, error) {
8196
// WriteString appends the contents of s to b's buffer.
8297
// It returns the length of s and a nil error.
8398
func (b *Builder) WriteString(s string) (int, error) {
99+
b.copyCheck()
84100
b.buf = append(b.buf, s...)
85101
return len(s), nil
86102
}
87-
88-
// minRead is the minimum slice passed to a Read call by Builder.ReadFrom.
89-
// It is the same as bytes.MinRead.
90-
const minRead = 512
91-
92-
// errNegativeRead is the panic value if the reader passed to Builder.ReadFrom
93-
// returns a negative count.
94-
var errNegativeRead = errors.New("strings.Builder: reader returned negative count from Read")
95-
96-
// ReadFrom reads data from r until EOF and appends it to b's buffer.
97-
// The return value n is the number of bytes read.
98-
// Any error except io.EOF encountered during the read is also returned.
99-
func (b *Builder) ReadFrom(r io.Reader) (n int64, err error) {
100-
for {
101-
l := len(b.buf)
102-
if cap(b.buf)-l < minRead {
103-
b.grow(minRead)
104-
}
105-
m, e := r.Read(b.buf[l:cap(b.buf)])
106-
if m < 0 {
107-
panic(errNegativeRead)
108-
}
109-
b.buf = b.buf[:l+m]
110-
n += int64(m)
111-
if e == io.EOF {
112-
return n, nil
113-
}
114-
if e != nil {
115-
return n, e
116-
}
117-
}
118-
}

src/strings/builder_test.go

Lines changed: 100 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,9 @@ package strings_test
66

77
import (
88
"bytes"
9-
"errors"
10-
"io"
119
"runtime"
1210
. "strings"
1311
"testing"
14-
"testing/iotest"
1512
)
1613

1714
func check(t *testing.T, b *Builder, want string) {
@@ -169,93 +166,6 @@ func TestBuilderWriteByte(t *testing.T) {
169166
check(t, &b, "a\x00")
170167
}
171168

172-
func TestBuilderReadFrom(t *testing.T) {
173-
for _, tt := range []struct {
174-
name string
175-
fn func(io.Reader) io.Reader
176-
}{
177-
{"Reader", func(r io.Reader) io.Reader { return r }},
178-
{"DataErrReader", iotest.DataErrReader},
179-
{"OneByteReader", iotest.OneByteReader},
180-
} {
181-
t.Run(tt.name, func(t *testing.T) {
182-
var b Builder
183-
184-
r := tt.fn(NewReader("hello"))
185-
n, err := b.ReadFrom(r)
186-
if err != nil {
187-
t.Fatalf("first call: got %s", err)
188-
}
189-
if n != 5 {
190-
t.Errorf("first call: got n=%d; want 5", n)
191-
}
192-
check(t, &b, "hello")
193-
194-
r = tt.fn(NewReader(" world"))
195-
n, err = b.ReadFrom(r)
196-
if err != nil {
197-
t.Fatalf("first call: got %s", err)
198-
}
199-
if n != 6 {
200-
t.Errorf("first call: got n=%d; want 6", n)
201-
}
202-
check(t, &b, "hello world")
203-
})
204-
}
205-
}
206-
207-
var errRead = errors.New("boom")
208-
209-
// errorReader sends reads to the underlying reader
210-
// but returns errRead instead of io.EOF.
211-
type errorReader struct {
212-
r io.Reader
213-
}
214-
215-
func (r errorReader) Read(b []byte) (int, error) {
216-
n, err := r.r.Read(b)
217-
if err == io.EOF {
218-
err = errRead
219-
}
220-
return n, err
221-
}
222-
223-
func TestBuilderReadFromError(t *testing.T) {
224-
var b Builder
225-
r := errorReader{NewReader("hello")}
226-
n, err := b.ReadFrom(r)
227-
if n != 5 {
228-
t.Errorf("got n=%d; want 5", n)
229-
}
230-
if err != errRead {
231-
t.Errorf("got err=%q; want %q", err, errRead)
232-
}
233-
check(t, &b, "hello")
234-
}
235-
236-
type negativeReader struct{}
237-
238-
func (r negativeReader) Read([]byte) (int, error) { return -1, nil }
239-
240-
func TestBuilderReadFromNegativeReader(t *testing.T) {
241-
var b Builder
242-
defer func() {
243-
switch err := recover().(type) {
244-
case nil:
245-
t.Fatal("ReadFrom didn't panic")
246-
case error:
247-
wantErr := "strings.Builder: reader returned negative count from Read"
248-
if err.Error() != wantErr {
249-
t.Fatalf("recovered panic: got %v; want %v", err.Error(), wantErr)
250-
}
251-
default:
252-
t.Fatalf("unexpected panic value: %#v", err)
253-
}
254-
}()
255-
256-
b.ReadFrom(negativeReader{})
257-
}
258-
259169
func TestBuilderAllocs(t *testing.T) {
260170
var b Builder
261171
b.Grow(5)
@@ -280,3 +190,103 @@ func numAllocs(fn func()) uint64 {
280190
runtime.ReadMemStats(&m2)
281191
return m2.Mallocs - m1.Mallocs
282192
}
193+
194+
func TestBuilderCopyPanic(t *testing.T) {
195+
tests := []struct {
196+
name string
197+
fn func()
198+
wantPanic bool
199+
}{
200+
{
201+
name: "String",
202+
wantPanic: false,
203+
fn: func() {
204+
var a Builder
205+
a.WriteByte('x')
206+
b := a
207+
_ = b.String() // appease vet
208+
},
209+
},
210+
{
211+
name: "Len",
212+
wantPanic: false,
213+
fn: func() {
214+
var a Builder
215+
a.WriteByte('x')
216+
b := a
217+
b.Len()
218+
},
219+
},
220+
{
221+
name: "Reset",
222+
wantPanic: false,
223+
fn: func() {
224+
var a Builder
225+
a.WriteByte('x')
226+
b := a
227+
b.Reset()
228+
b.WriteByte('y')
229+
},
230+
},
231+
{
232+
name: "Write",
233+
wantPanic: true,
234+
fn: func() {
235+
var a Builder
236+
a.Write([]byte("x"))
237+
b := a
238+
b.Write([]byte("y"))
239+
},
240+
},
241+
{
242+
name: "WriteByte",
243+
wantPanic: true,
244+
fn: func() {
245+
var a Builder
246+
a.WriteByte('x')
247+
b := a
248+
b.WriteByte('y')
249+
},
250+
},
251+
{
252+
name: "WriteString",
253+
wantPanic: true,
254+
fn: func() {
255+
var a Builder
256+
a.WriteString("x")
257+
b := a
258+
b.WriteString("y")
259+
},
260+
},
261+
{
262+
name: "WriteRune",
263+
wantPanic: true,
264+
fn: func() {
265+
var a Builder
266+
a.WriteRune('x')
267+
b := a
268+
b.WriteRune('y')
269+
},
270+
},
271+
{
272+
name: "Grow",
273+
wantPanic: true,
274+
fn: func() {
275+
var a Builder
276+
a.Grow(1)
277+
b := a
278+
b.Grow(2)
279+
},
280+
},
281+
}
282+
for _, tt := range tests {
283+
didPanic := make(chan bool)
284+
go func() {
285+
defer func() { didPanic <- recover() != nil }()
286+
tt.fn()
287+
}()
288+
if got := <-didPanic; got != tt.wantPanic {
289+
t.Errorf("%s: panicked = %v; want %v", tt.name, got, tt.wantPanic)
290+
}
291+
}
292+
}

0 commit comments

Comments
 (0)