rustyconover commented on issue #397:
URL: https://github.com/apache/arrow-dotnet/issues/397#issuecomment-5738548624

   `ArrowStreamWriter` hits this hazard internally. A caller that writes a 
batch it built only to write gets a `NullReferenceException`, or silently wrong 
bytes, with no span in its own code. The writer case also shows that preferring 
`Memory<byte>` over `Span` isn't enough on `main`.
   
   ### Where it happens
   
   In `WriteRecordBatchInternal`, the last use of `recordBatch` is 
`recordBatch.Length`, when the header is built. The bodies are copied later, by 
`WriteBufferData(recordBatchBuilder.Buffers)`. In optimized code, if the caller 
holds no other reference to the batch, it is unreachable during that copy.
   
   `recordBatchBuilder.Buffers` holds `ReadOnlyMemory<byte>` views. They root 
the `NativeMemoryManager`, but not the `SharedMemoryHandle` that owns it. A 
collection in that window finalizes the handles, `SharedMemoryHandle.Release()` 
disposes the manager, and `WriteBufferData` copies from freed memory. Keeping 
`Memory<byte>` alive does not help, because the finalizer that frees the memory 
belongs to the handle, not to the manager.
   
   ### Repro (Apache.Arrow 23.0.0 from NuGet, .NET 10, linux-arm64)
   
   ```csharp
   // A destination whose writes allocate, e.g. a network or compression stream.
   sealed class CollectingStream : MemoryStream
   {
       static void Collect() { GC.Collect(); GC.WaitForPendingFinalizers(); 
GC.Collect(); }
       public override void Write(byte[] b, int o, int c) { Collect(); 
base.Write(b, o, c); }
       public override void Write(ReadOnlySpan<byte> b) { Collect(); 
base.Write(b); }
   }
   
   [MethodImpl(MethodImplOptions.NoInlining)]
   static RecordBatch Build()   // Int64 + String columns, 1000 rows, from the 
builders
   { ... }
   
   [MethodImpl(MethodImplOptions.NoInlining)]
   static byte[] WriteTemporary()
   {
       using var stream = new CollectingStream();
       using (var writer = new ArrowStreamWriter(stream, schema, leaveOpen: 
true))
       {
           writer.WriteRecordBatch(Build());   // referenced by nothing but the 
call
           writer.WriteEnd();
       }
       return stream.ToArray();
   }
   ```
   
   Twenty iterations with `DOTNET_TieredCompilation=0`, each result read back 
and checked value by value:
   
   ```
   temporary batch (no keep-alive):   NullReferenceException: 20
   batch kept alive by the caller:    ok: 20
   ```
   
   ```
   System.NullReferenceException: Object reference not set to an instance of an 
object.
      at System.SpanHelpers.Memmove(Byte& dest, Byte& src, UIntPtr len)
      at System.IO.Stream.Write(ReadOnlySpan`1 buffer)
      at CollectingStream.Write(ReadOnlySpan`1 buffer)
      at Apache.Arrow.Ipc.ArrowStreamWriter.WriteBufferData(IReadOnlyList`1 
buffers)
      at 
Apache.Arrow.Ipc.ArrowStreamWriter.WriteRecordBatchInternal(RecordBatch 
recordBatch)
   ```
   
   With default tiering it passes, because tier-0 keeps locals alive to the end 
of the method.
   
   We found this in a multithreaded HTTP server. Each response writes a few 
hundred single-row batches built only to be written, and about 0.7% of our 
integration test files failed intermittently. Code shaped like 
`writer.WriteRecordBatch(BuildRow(...))` is common and looks correct.
   
   ### What fixed it for us
   
   `GC.KeepAlive(recordBatch)` at the end of `WriteRecordBatchInternal` and 
`WriteRecordBatchInternalAsync`, after the bodies are copied. We added the same 
guard for `dictionary` in `WriteDictionary` / `WriteDictionaryAsync`. The 
dictionary memo already holds it, so that one is defensive.
   
   This is the second case in the repro, moved into the writer. It is carried 
as a patch in our fork. With the patched writer, repeated runs of the server 
scenario went from 23 crashes in 144 to 0 in 144. I'm happy to open a PR with 
that change and a regression test that forces a collection from the destination 
stream, if that's useful while the broader ownership question in this issue is 
worked out.
   
   Main is unchanged as of 75718c9: no `GC.KeepAlive` in 
`ArrowStreamWriter.cs`, and `SharedMemoryHandle` still has its finalizer.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to