This is an automated email from the ASF dual-hosted git repository.
zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-go.git
The following commit(s) were added to refs/heads/main by this push:
new 3886c0a0 fix(arrow/array): `Resize(0)` on non-empty builder causes
memory leak (#995)
3886c0a0 is described below
commit 3886c0a0162d17b9852b4bb8dc1872b4a0ee21c5
Author: Lucas Valente <[email protected]>
AuthorDate: Fri Jul 24 18:11:11 2026 +0200
fix(arrow/array): `Resize(0)` on non-empty builder causes memory leak (#995)
### Rationale for this change
I found a memory leak while working on the [JSON nullable parity
PR](https://github.com/apache/arrow-go/pull/833). It's triggered
whenever a non-empty builder gets resized to zero.
This memory leak can be achieved with public APIs only (`Append` and
`Resize`), meaning it's an actual bug in the public API and not a misuse
of internal APIs where I didn't check for an invariant.
In the specific case of the JSON PR, whenever we found an error in the
first row that would call `Resize(-1)`, which in turn calls `Resize(0)`
on all the previous fields, and the bug gets triggered.
You can checkout to each commit to see the test failing then passing
after the fix.
### What changes are included in this PR?
- A new unit test to repro the memory leak.
- The fix: always respect `minBuilderCapacity` when calling `Resize()`
### Are these changes tested?
Yes.
### Are there any user-facing changes?
No.
---
arrow/array/builder.go | 5 +-
arrow/array/numericbuilder.gen_test.go | 183 ++++++++++++++++++++++++++++
arrow/array/numericbuilder.gen_test.go.tmpl | 17 +++
3 files changed, 203 insertions(+), 2 deletions(-)
diff --git a/arrow/array/builder.go b/arrow/array/builder.go
index ed4f08e7..517feec5 100644
--- a/arrow/array/builder.go
+++ b/arrow/array/builder.go
@@ -164,10 +164,11 @@ func (b *builder) resize(newBits int, init func(int)) {
return
}
- newBytesN := bitutil.CeilByte(newBits) / 8
+ allocBits := max(newBits, minBuilderCapacity)
+ newBytesN := bitutil.CeilByte(allocBits) / 8
oldBytesN := b.nullBitmap.Len()
b.nullBitmap.Resize(newBytesN)
- b.capacity = newBits
+ b.capacity = allocBits
if oldBytesN < newBytesN {
// TODO(sgc): necessary?
memory.Set(b.nullBitmap.Buf()[oldBytesN:], 0)
diff --git a/arrow/array/numericbuilder.gen_test.go
b/arrow/array/numericbuilder.gen_test.go
index 2ca56329..c54d8cf2 100644
--- a/arrow/array/numericbuilder.gen_test.go
+++ b/arrow/array/numericbuilder.gen_test.go
@@ -230,6 +230,18 @@ func TestInt64Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestInt64Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ ab := array.NewInt64Builder(mem)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestInt64BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -457,6 +469,18 @@ func TestUint64Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestUint64Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ ab := array.NewUint64Builder(mem)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestUint64BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -684,6 +708,18 @@ func TestFloat64Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestFloat64Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ ab := array.NewFloat64Builder(mem)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestFloat64BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -909,6 +945,18 @@ func TestInt32Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestInt32Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ ab := array.NewInt32Builder(mem)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestInt32BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -1136,6 +1184,18 @@ func TestUint32Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestUint32Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ ab := array.NewUint32Builder(mem)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestUint32BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -1363,6 +1423,18 @@ func TestFloat32Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestFloat32Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ ab := array.NewFloat32Builder(mem)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestFloat32BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -1588,6 +1660,18 @@ func TestInt16Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestInt16Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ ab := array.NewInt16Builder(mem)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestInt16BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -1815,6 +1899,18 @@ func TestUint16Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestUint16Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ ab := array.NewUint16Builder(mem)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestUint16BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -2042,6 +2138,18 @@ func TestInt8Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestInt8Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ ab := array.NewInt8Builder(mem)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestInt8BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -2269,6 +2377,18 @@ func TestUint8Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestUint8Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ ab := array.NewUint8Builder(mem)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestUint8BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -2501,6 +2621,19 @@ func TestTime32Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestTime32Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ dtype := &arrow.Time32Type{Unit: arrow.Second}
+ ab := array.NewTime32Builder(mem, dtype)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestTime32BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -2734,6 +2867,19 @@ func TestTime64Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestTime64Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ dtype := &arrow.Time64Type{Unit: arrow.Second}
+ ab := array.NewTime64Builder(mem, dtype)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestTime64BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -2962,6 +3108,18 @@ func TestDate32Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestDate32Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ ab := array.NewDate32Builder(mem)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestDate32BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -3196,6 +3354,18 @@ func TestDate64Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestDate64Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ ab := array.NewDate64Builder(mem)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestDate64BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
@@ -3428,6 +3598,19 @@ func TestDurationBuilder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func TestDurationBuilder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ dtype := &arrow.DurationType{Unit: arrow.Second}
+ ab := array.NewDurationBuilder(mem, dtype)
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func TestDurationBuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
diff --git a/arrow/array/numericbuilder.gen_test.go.tmpl
b/arrow/array/numericbuilder.gen_test.go.tmpl
index 07d5fc8a..e3422496 100644
--- a/arrow/array/numericbuilder.gen_test.go.tmpl
+++ b/arrow/array/numericbuilder.gen_test.go.tmpl
@@ -272,6 +272,23 @@ func Test{{.Name}}Builder_Resize(t *testing.T) {
assert.Equal(t, 5, ab.Len())
}
+func Test{{.Name}}Builder_ResizeToZeroThenAppend(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+{{if .Opt.Parametric -}}
+ dtype := &arrow.{{.Name}}Type{Unit: arrow.Second}
+ ab := array.New{{.Name}}Builder(mem, dtype)
+{{else}}
+ ab := array.New{{.Name}}Builder(mem)
+{{end -}}
+ defer ab.Release()
+
+ ab.Append(0)
+ ab.Resize(0)
+ ab.Append(0)
+}
+
func Test{{.Name}}BuilderUnmarshalJSON(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)