This is an automated email from the ASF dual-hosted git repository.

CurtHagenlocher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-dotnet.git


The following commit(s) were added to refs/heads/main by this push:
     new 08b2b16  fix: Add memory lifetime warnings to ReadOnlySpan properties 
(#406)
08b2b16 is described below

commit 08b2b16269a146cdc1495d04c2aaf0900f37b949
Author: Abhilash S Hathwar <[email protected]>
AuthorDate: Wed Aug 19 04:05:50 2026 +0530

    fix: Add memory lifetime warnings to ReadOnlySpan properties (#406)
    
    ## What's Changed
    
    Adds explicit XML documentation remarks (`<remarks>`) to properties and
    methods that return a `ReadOnlySpan` over managed or unmanaged memory
    across Apache Arrow array and buffer classes.
    
    ### Motivation & Relation to PR #393
    When an `ArrowBuffer` or `Array` is backed by unmanaged memory (e.g.,
    allocated via a custom `MemoryAllocator` or `NativeMemoryManager`),
    extracting a `ReadOnlySpan` into a local variable and subsequently
    disposing the parent buffer/array can lead to use-after-free memory
    access issues.
    
    While PR #393 explores structural lifecycle/pinning mechanisms, this PR
    provides an immediate, non-breaking developer safety improvement:
    - Added `<remarks>` warning callouts to `ArrowBuffer.Span`,
    `PrimitiveArray.Values`, `BooleanArray.Values`, and
    `BinaryArray.GetBytes`. These surface as visible safety notes in IDE
    tooltips and IntelliSense (Visual Studio, VS Code, Rider) without
    breaking builds or requiring API breaking changes under
    `TreatWarningsAsErrors=true`.
    - Added `PoisonMemoryAllocator` and unit test
    `TestNativeMemoryManagerUseAfterFree` to `Apache.Arrow.Tests` to verify
    memory poisoning on buffer release.
    
    Relates to #397 and #393.
---
 src/Apache.Arrow/Arrays/BinaryArray.cs           |  6 +-
 src/Apache.Arrow/Arrays/BooleanArray.cs          | 12 ++-
 src/Apache.Arrow/Arrays/PrimitiveArray.cs        |  9 ++-
 src/Apache.Arrow/ArrowBuffer.cs                  |  9 ++-
 test/Apache.Arrow.Tests/ArrowBufferTests.cs      | 24 +++++-
 test/Apache.Arrow.Tests/PoisonMemoryAllocator.cs | 93 ++++++++++++++++++++++++
 6 files changed, 147 insertions(+), 6 deletions(-)

diff --git a/src/Apache.Arrow/Arrays/BinaryArray.cs 
b/src/Apache.Arrow/Arrays/BinaryArray.cs
index 80985d0..387c644 100644
--- a/src/Apache.Arrow/Arrays/BinaryArray.cs
+++ b/src/Apache.Arrow/Arrays/BinaryArray.cs
@@ -1,4 +1,4 @@
-// Licensed to the Apache Software Foundation (ASF) under one or more
+// Licensed to the Apache Software Foundation (ASF) under one or more
 // contributor license agreements. See the NOTICE file distributed with
 // this work for additional information regarding copyright ownership.
 // The ASF licenses this file to You under the Apache License, Version 2.0
@@ -383,6 +383,10 @@ namespace Apache.Arrow
         /// <summary>
         /// Get the collection of bytes, as a read-only span, at a given index 
in the array.
         /// </summary>
+        /// <remarks>
+        /// CAUTION: The returned ReadOnlySpan is not GC-tracked if backed by 
unmanaged memory.
+        /// Ensure the BinaryArray object remains in scope and undisposed 
while accessing this span.
+        /// </remarks>
         /// <param name="index">Index at which to get bytes.</param>
         /// <param name="isNull">Set to <see langword="true"/> if the value at 
the given index is null.</param>
         /// <returns>Returns a <see cref="ReadOnlySpan{Byte}"/> 
object.</returns>
diff --git a/src/Apache.Arrow/Arrays/BooleanArray.cs 
b/src/Apache.Arrow/Arrays/BooleanArray.cs
index f87c2ec..32824a1 100644
--- a/src/Apache.Arrow/Arrays/BooleanArray.cs
+++ b/src/Apache.Arrow/Arrays/BooleanArray.cs
@@ -1,4 +1,4 @@
-// Licensed to the Apache Software Foundation (ASF) under one or more
+// Licensed to the Apache Software Foundation (ASF) under one or more
 // contributor license agreements. See the NOTICE file distributed with
 // this work for additional information regarding copyright ownership.
 // The ASF licenses this file to You under the Apache License, Version 2.0
@@ -162,7 +162,15 @@ namespace Apache.Arrow
         }
 
         public ArrowBuffer ValueBuffer => Data.Buffers[1];
-        public ReadOnlySpan<byte> Values => ValueBuffer.Span.Slice(0, 
(int)Math.Ceiling(Length / 8.0));
+
+        /// <summary>
+        /// Gets the boolean array values as a span of bytes (a bitmap).
+        /// </summary>
+        /// <remarks>
+        /// CAUTION: The returned ReadOnlySpan is not GC-tracked if backed by 
unmanaged memory.
+        /// Ensure the BooleanArray object remains in scope and undisposed 
while accessing this span.
+        /// </remarks>
+        public ReadOnlySpan<byte> Values => ValueBuffer.Span.Slice(0, 
(int)Math.Ceiling((double)Length / 8));
 
         public BooleanArray(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
diff --git a/src/Apache.Arrow/Arrays/PrimitiveArray.cs 
b/src/Apache.Arrow/Arrays/PrimitiveArray.cs
index 05d659b..992ffb6 100644
--- a/src/Apache.Arrow/Arrays/PrimitiveArray.cs
+++ b/src/Apache.Arrow/Arrays/PrimitiveArray.cs
@@ -1,4 +1,4 @@
-// Licensed to the Apache Software Foundation (ASF) under one or more
+// Licensed to the Apache Software Foundation (ASF) under one or more
 // contributor license agreements. See the NOTICE file distributed with
 // this work for additional information regarding copyright ownership.
 // The ASF licenses this file to You under the Apache License, Version 2.0
@@ -31,6 +31,13 @@ namespace Apache.Arrow
 
         public ArrowBuffer ValueBuffer => Data.Buffers[1];
 
+        /// <summary>
+        /// Gets the array values as a span.
+        /// </summary>
+        /// <remarks>
+        /// CAUTION: The returned ReadOnlySpan is not GC-tracked if backed by 
unmanaged memory.
+        /// Ensure the PrimitiveArray object remains in scope and undisposed 
while accessing this span.
+        /// </remarks>
         public ReadOnlySpan<T> Values => 
ValueBuffer.Span.CastTo<T>().Slice(Offset, Length);
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/src/Apache.Arrow/ArrowBuffer.cs b/src/Apache.Arrow/ArrowBuffer.cs
index d3cd939..c2f531f 100644
--- a/src/Apache.Arrow/ArrowBuffer.cs
+++ b/src/Apache.Arrow/ArrowBuffer.cs
@@ -1,4 +1,4 @@
-// Licensed to the Apache Software Foundation (ASF) under one or more
+// Licensed to the Apache Software Foundation (ASF) under one or more
 // contributor license agreements. See the NOTICE file distributed with
 // this work for additional information regarding copyright ownership.
 // The ASF licenses this file to You under the Apache License, Version 2.0
@@ -52,6 +52,13 @@ namespace Apache.Arrow
 
         public int Length => Memory.Length;
 
+        /// <summary>
+        /// Gets a read-only span over the buffer contents.
+        /// </summary>
+        /// <remarks>
+        /// CAUTION: The returned ReadOnlySpan points directly to memory 
managed by this ArrowBuffer.
+        /// Ensure the ArrowBuffer instance remains rooted and undisposed 
while using this span to prevent use-after-free.
+        /// </remarks>
         public ReadOnlySpan<byte> Span
         {
             [MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/test/Apache.Arrow.Tests/ArrowBufferTests.cs 
b/test/Apache.Arrow.Tests/ArrowBufferTests.cs
index 28f22c2..a9b0e77 100644
--- a/test/Apache.Arrow.Tests/ArrowBufferTests.cs
+++ b/test/Apache.Arrow.Tests/ArrowBufferTests.cs
@@ -1,4 +1,4 @@
-// Licensed to the Apache Software Foundation (ASF) under one or more
+// Licensed to the Apache Software Foundation (ASF) under one or more
 // contributor license agreements. See the NOTICE file distributed with
 // this work for additional information regarding copyright ownership.
 // The ASF licenses this file to You under the Apache License, Version 2.0
@@ -14,7 +14,9 @@
 // limitations under the License.
 
 using System;
+using System.Buffers;
 using System.Threading;
+using Apache.Arrow.Memory;
 using Apache.Arrow.Tests.Fixtures;
 using Xunit;
 
@@ -111,6 +113,26 @@ namespace Apache.Arrow.Tests
             Assert.Equal(10, buffer.Span.CastTo<int>()[2]);
         }
 
+        [Fact]
+        public void TestNativeMemoryManagerUseAfterFree()
+        {
+            using var allocator = new PoisonMemoryAllocator();
+            // Allocate using the Builder pattern
+            var builder = new ArrowBuffer.Builder<byte>(100000);
+            builder.Append(new byte[100000]);
+            ArrowBuffer buffer = builder.Build(allocator);
+
+            // Extract the unmanaged Span
+            ReadOnlySpan<byte> span = buffer.Span;
+
+            // Dispose the buffer to trigger memory poisoning and release
+            buffer.Dispose();
+
+            // span[50000] is poisoned with 0xFF after free, it's not the 
initial 0 value
+            byte b = span[50000];
+            Assert.Equal(0xFF, b);
+        }
+
         public class Retain
         {
             [Fact]
diff --git a/test/Apache.Arrow.Tests/PoisonMemoryAllocator.cs 
b/test/Apache.Arrow.Tests/PoisonMemoryAllocator.cs
new file mode 100644
index 0000000..8ed050b
--- /dev/null
+++ b/test/Apache.Arrow.Tests/PoisonMemoryAllocator.cs
@@ -0,0 +1,93 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using Apache.Arrow.Memory;
+
+namespace Apache.Arrow.Tests
+{
+    public class PoisonMemoryAllocator : MemoryAllocator, IDisposable
+    {
+        private readonly List<IMemoryOwner<byte>> _allocatedOwners = new 
List<IMemoryOwner<byte>>();
+        private bool _disposed;
+
+        public PoisonMemoryAllocator(int alignment = DefaultAlignment) : 
base(alignment)
+        {
+        }
+
+        protected override IMemoryOwner<byte> AllocateInternal(int length, out 
int bytesAllocated)
+        {
+            var innerOwner = 
NativeMemoryAllocator.Default.Value.Allocate(length);
+            bytesAllocated = length;
+            lock (_allocatedOwners)
+            {
+                _allocatedOwners.Add(innerOwner);
+            }
+            return new PoisonMemoryOwner(innerOwner);
+        }
+
+        public void Dispose()
+        {
+            if (_disposed)
+            {
+                return;
+            }
+            _disposed = true;
+            lock (_allocatedOwners)
+            {
+                foreach (IMemoryOwner<byte> owner in _allocatedOwners)
+                {
+                    owner.Dispose();
+                }
+                _allocatedOwners.Clear();
+            }
+        }
+
+        private sealed class PoisonMemoryOwner : IMemoryOwner<byte>
+        {
+            private readonly IMemoryOwner<byte> _inner;
+            private bool _disposed;
+
+            public PoisonMemoryOwner(IMemoryOwner<byte> inner)
+            {
+                _inner = inner ?? throw new 
ArgumentNullException(nameof(inner));
+            }
+
+            public Memory<byte> Memory
+            {
+                get
+                {
+                    if (_disposed)
+                    {
+                        throw new 
ObjectDisposedException(nameof(PoisonMemoryOwner));
+                    }
+                    return _inner.Memory;
+                }
+            }
+
+            public void Dispose()
+            {
+                if (_disposed)
+                {
+                    return;
+                }
+                _disposed = true;
+                _inner.Memory.Span.Fill(0xFF);
+            }
+        }
+    }
+}

Reply via email to