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-adbc.git


The following commit(s) were added to refs/heads/main by this push:
     new 57cb036137 fix(csharp): prevent freeing driver library twice (#4976)
57cb036137 is described below

commit 57cb036137c7640400a1d5d27754aa7fabb6e376
Author: Bryce Mecum <[email protected]>
AuthorDate: Wed Oct 28 16:55:02 2026 -0700

    fix(csharp): prevent freeing driver library twice (#4976)
    
    When `CAdbcDriverImporter.Load` is called with `canUnload: true` and the
    library doesn't export the requested entrypoint, the netstandard2.0
    build (used on .NET Framework) frees the library twice: once before
    throwing and again in the `finally` block. That build uses its own
    `NativeLibrary` shim, whose `GetExport` returns `IntPtr.Zero` for a
    missing export instead of throwing, so it reaches a branch the
    net8.0/net10.0 builds never do. Freeing twice can unload a library that
    something else in the process still uses.
    
    This PR adds a test for this case and leaves freeing the library to the
    `finally` block.
    
    Bug found and PR created with Opus 5.5.
---
 .../src/Apache.Arrow.Adbc/C/CAdbcDriverImporter.cs |   1 -
 .../CAdbcDriverImporterUnloadTests.cs              | 150 +++++++++++++++++++++
 2 files changed, 150 insertions(+), 1 deletion(-)

diff --git a/csharp/src/Apache.Arrow.Adbc/C/CAdbcDriverImporter.cs 
b/csharp/src/Apache.Arrow.Adbc/C/CAdbcDriverImporter.cs
index 57e96ef721..603e4d7e49 100644
--- a/csharp/src/Apache.Arrow.Adbc/C/CAdbcDriverImporter.cs
+++ b/csharp/src/Apache.Arrow.Adbc/C/CAdbcDriverImporter.cs
@@ -98,7 +98,6 @@ namespace Apache.Arrow.Adbc.C
                 }
                 if (export == IntPtr.Zero)
                 {
-                    if (canUnload) { NativeLibrary.Free(library); }
                     throw new ArgumentException($"Unable to find {entryPoint} 
export", nameof(file));
                 }
 
diff --git 
a/csharp/test/Apache.Arrow.Adbc.Tests/CAdbcDriverImporterUnloadTests.cs 
b/csharp/test/Apache.Arrow.Adbc.Tests/CAdbcDriverImporterUnloadTests.cs
new file mode 100644
index 0000000000..c7fe6ec834
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/CAdbcDriverImporterUnloadTests.cs
@@ -0,0 +1,150 @@
+/*
+ * 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.IO;
+using System.Runtime.InteropServices;
+using Apache.Arrow.Adbc.C;
+using Xunit;
+
+namespace Apache.Arrow.Adbc.Tests
+{
+    public class CAdbcDriverImporterUnloadTests : IDisposable
+    {
+        private readonly string _tempDir =
+            Path.Combine(Path.GetTempPath(), "adbc_importer_unload_" + 
Guid.NewGuid().ToString("N"));
+
+        public CAdbcDriverImporterUnloadTests()
+        {
+            Directory.CreateDirectory(_tempDir);
+        }
+
+        public void Dispose()
+        {
+            try { Directory.Delete(_tempDir, true); } catch { }
+        }
+
+        // Loading with canUnload when the library doesn't export the 
entrypoint
+        // should release the importer's own reference exactly once, leaving 
any
+        // other reference to the library in place
+        [Fact]
+        public void Load_CanUnload_MissingEntrypoint_KeepsOtherReferences()
+        {
+            // A copy of the DuckDB library under a unique name, so nothing 
else in
+            // the process has it loaded and this test holds the only other 
reference
+            string duckDb = Path.Combine(Directory.GetCurrentDirectory(), 
OsLibrary.DuckDbFileName);
+            string library = Path.Combine(_tempDir, "adbc_test_unload_" + 
Guid.NewGuid().ToString("N") + Path.GetExtension(duckDb));
+            File.Copy(duckDb, library);
+
+            IntPtr handle = OsLibrary.Load(library);
+            Assert.NotEqual(IntPtr.Zero, handle);
+            try
+            {
+                Assert.ThrowsAny<Exception>(
+                    () => CAdbcDriverImporter.Load(library, canUnload: true, 
"AdbcTestNoSuchEntrypoint"));
+
+                Assert.True(
+                    OsLibrary.IsLoaded(library),
+                    "The library was unloaded while another reference to it 
was still held");
+            }
+            finally
+            {
+                OsLibrary.Free(handle);
+            }
+        }
+
+        /// <summary>
+        /// Loads and checks native libraries directly with the OS loader, 
which is
+        /// also available on .NET Framework.
+        /// </summary>
+        private static class OsLibrary
+        {
+            public static string DuckDbFileName =>
+                RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 
"duckdb.dll"
+                : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 
"libduckdb.dylib"
+                : "libduckdb.so";
+
+            public static IntPtr Load(string path) =>
+                RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 
Windows.LoadLibraryW(path)
+                : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 
Mac.dlopen(path, Mac.RTLD_NOW)
+                : Linux.dlopen(path, Linux.RTLD_NOW);
+
+            public static void Free(IntPtr handle)
+            {
+                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 
Windows.FreeLibrary(handle);
+                else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 
Mac.dlclose(handle);
+                else Linux.dlclose(handle);
+            }
+
+            public static bool IsLoaded(string path)
+            {
+                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                {
+                    return Windows.GetModuleHandleW(path) != IntPtr.Zero;
+                }
+
+                // RTLD_NOLOAD returns a new reference only if the library is 
already loaded
+                IntPtr handle = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
+                    ? Mac.dlopen(path, Mac.RTLD_NOW | Mac.RTLD_NOLOAD)
+                    : Linux.dlopen(path, Linux.RTLD_NOW | Linux.RTLD_NOLOAD);
+                if (handle == IntPtr.Zero)
+                {
+                    return false;
+                }
+                Free(handle);
+                return true;
+            }
+
+            private static class Windows
+            {
+                [DllImport("kernel32.dll", CharSet = CharSet.Unicode, 
SetLastError = true)]
+                public static extern IntPtr LoadLibraryW(string fileName);
+
+                [DllImport("kernel32.dll", SetLastError = true)]
+                [return: MarshalAs(UnmanagedType.Bool)]
+                public static extern bool FreeLibrary(IntPtr module);
+
+                [DllImport("kernel32.dll", CharSet = CharSet.Unicode, 
SetLastError = true)]
+                public static extern IntPtr GetModuleHandleW(string 
moduleName);
+            }
+
+            private static class Mac
+            {
+                public const int RTLD_NOW = 0x2;
+                public const int RTLD_NOLOAD = 0x10;
+
+                [DllImport("libdl.dylib")]
+                public static extern IntPtr dlopen(string fileName, int flags);
+
+                [DllImport("libdl.dylib")]
+                public static extern int dlclose(IntPtr handle);
+            }
+
+            private static class Linux
+            {
+                public const int RTLD_NOW = 0x2;
+                public const int RTLD_NOLOAD = 0x4;
+
+                [DllImport("libdl.so.2")]
+                public static extern IntPtr dlopen(string fileName, int flags);
+
+                [DllImport("libdl.so.2")]
+                public static extern int dlclose(IntPtr handle);
+            }
+        }
+    }
+}

Reply via email to