Skip to content

fix(win): ship uniquely named bundled DLLs to avoid foreign DLL conflicts - #254

Merged
MaxRev-Dev merged 9 commits into
mainfrom
fix/win-unique-dll-names
Oct 31, 2026
Merged

MaxRev-Dev merged 9 commits into
mainfrom
fix/win-unique-dll-names

Conversation

@MaxRev-Dev

@MaxRev-Dev MaxRev-Dev commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Fixes #253
Refs #9 (Windows part)

Change

Windows resolves a DLL's dependencies by base name and reuses a same-named module that is already loaded, so a foreign, older libcrypto-3-x64.dll, hdf5.dll, zlib.dll, sqlite3.dll, ... (already loaded in the process, or found via System32/PATH) can be bound instead of the copy we ship. With OpenSSL 3.6 and HDF5 2.x that fails with "The specified procedure could not be found".

The Windows runtime now ships every bundled third-party DLL as <name>-maxrev.dll, and every packaged binary imports the new names. Unique names also stop our DLLs from overwriting, or being overwritten by, same-named DLLs from other NuGet packages in the shared runtimes/win-x64/native output folder on Windows.

  • win/PeImportRenamer.cs (new): renames DLL-name strings in PE import and delay-import tables, plus the export name of a renamed DLL. Longer names go into a small appended read-only section; if the PE header has no room for another section header, they go into the zero padding at the end of existing read-only data sections. The PE checksum is recomputed. Authenticode-signed files are never modified. Same approach as delvewheel on Windows and patchelf on Linux ([BUG] Bundled libssl/libcrypto conflicts with .NET OpenSSL on Linux #224).
  • win/partials.psm1:
    • Rename-BundledDLLs: renames every top-level DLL in the native folder except the keep-list below, and patches the imports of every packaged DLL and EXE (native folder including subdirectories, and the tools folder). Running it again is a no-op.
    • Confirm-PackagedDllImports: fails the build if a bundled DLL still ships under its original name, if any packaged binary imports an original name, or if it imports a DLL that is neither packaged nor a system DLL. Missing or empty output folders fail as well.
  • win/collect-deps-makefile.vc, win/README.md: updated for the new names.
  • win/functions.psm1: registers the default PSGallery repository when a runner starts without it. The previous check called Set-PSRepository, which cannot create a missing repository, and failed the Windows test job before any test ran.
  • Tests: BinaryLoadingTests (Windows-only) asserts that every native module loaded from the package folder is either on the keep-list or carries the -maxrev suffix, and that no renamed DLL is also packaged under its original name. PeImportRenamerTests covers the appended-section path, the section-padding fallback, the no-space error and signed files.

Kept under their original names:

  • gdal.dll and the SWIG bindings gdal_wrap.dll, ogr_wrap.dll, osr_wrap.dll, gdalconst_wrap.dll (loaded by name from managed code and by consumers)
  • the VC++ runtime (vcruntime140*.dll, msvcp140*.dll, concrt140.dll, vccorlib140.dll, vcomp*.dll) and msodbcsql17.dll (Microsoft-signed redistributables)
  • OpenCL.dll if collected, and any other Authenticode-signed DLL
  • DLLs in subdirectories (e.g. GDAL plugins loaded by path) keep their names but get their imports rewritten

Code that loads a bundled DLL by its old name (for example NativeLibrary.Load("libcrypto-3-x64.dll") or [DllImport("sqlite3")], or externally built GDAL/HDF5 plugins importing hdf5.dll, proj_9.dll, ...) no longer binds to our copies.

Verification

  • Ran Rename-BundledDLLs and Confirm-PackagedDllImports through the PowerShell SDK on the native DLLs from MaxRev.Gdal.WindowsRuntime.Minimal 3.13.3.583, plus stand-in tool EXEs and a plugin in a subdirectory: 53 DLLs renamed, 16 kept, 56 binaries patched, all have header room for the appended section. Checked with pefile: no imports of original names, every import resolves to a packaged or system DLL, imported and exported symbols unchanged, export names updated, valid checksums and section layout, byte changes limited to headers, name fields and the new names. A second run leaves every file byte-identical. objdump parses all patched files.
  • Repeated the run with the header slack of every DLL filled so that the padding fallback is used: all 56 binaries patched without adding sections, same pefile checks pass.
  • Confirm-PackagedDllImports fails for an unpatched DLL put back under its original name, an unpatched EXE, a missing renamed DLL, and missing or empty folders.
  • BinaryLoadingTests and PeImportRenamerTests 11/11 locally (the loading test is Windows-only). Native loading on Windows is covered by the Windows CI run.

…conflicts (#253)

Windows binds a DLL's imports by base name and reuses any same-named module
already loaded in the process. A foreign libcrypto-3-x64.dll (OpenSSL < 3.6)
or hdf5.dll (< 1.14.4) from another application could therefore satisfy
gdal.dll, libssl and hdf5_hl imports and fail with "procedure could not be
found".

The packaged libcrypto, libssl, hdf5 and hdf5_hl DLLs are now renamed to
<name>-maxrev.dll after dependency collection, and the import tables of
every packaged DLL/EXE are rewritten to match (PeImportRenamer.cs, loaded
via Add-Type). This mirrors the patchelf rename of libssl/libcrypto on
Linux. Confirm-PackagedDllImports then fails the build if a generic name is
still shipped or imported, or if any import is neither packaged nor a
system DLL. A Windows-only test asserts the renamed modules are what gets
loaded.
Rename every packaged DLL to <name>-maxrev.dll except a keep-list (gdal.dll,
the SWIG *_wrap.dll, the Microsoft-signed VC++ runtime and ODBC driver,
OpenCL.dll and any other signed DLL), and rewrite the imports of every
packaged DLL and tool, including DLLs in subdirectories that are loaded by
path. Confirm-PackagedDllImports now fails if any bundled DLL still ships or
is imported under its original name.

PeImportRenamer falls back to the zero padding of read-only data sections
when the PE header has no room for another section header.
@MaxRev-Dev MaxRev-Dev changed the title fix(win): ship uniquely named OpenSSL/HDF5 DLLs to avoid foreign DLL conflicts fix(win): ship uniquely named bundled DLLs to avoid foreign DLL conflicts Oct 19, 2026
@MaxRev-Dev
MaxRev-Dev requested a balanced review from Copilot October 25, 2026 00:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Packaging validation can accept empty tools output and unshipped third-party dependencies found only on the build machine.

2 open issues
What changed in this PR

This PR isolates bundled Windows dependencies with -maxrev.dll names to prevent foreign DLL conflicts and shared NuGet output collisions, addressing #253 and the Windows portion of #9.

Changes:

  • Adds PE import, delay-import, and export-name rewriting with signature protection.
  • Integrates DLL renaming and dependency validation into Windows packaging.
  • Documents compatibility changes and adds renaming and native-loading tests.
File Description
win/​README.md Documents unique names, exceptions, and compatibility effects.
win/​PeImportRenamer.cs Implements PE name rewriting and checksum updates.
win/​partials.psm1 Adds packaging rename and dependency-validation steps.
win/​collect-deps-makefile.vc Exposes the tools output directory.
tests/​MaxRev.Gdal.Core.Tests.XUnit/​PeImportRenamerTests.cs Tests name placement, idempotency, and failure handling.
tests/​MaxRev.Gdal.Core.Tests.XUnit/​MaxRev.Gdal.Core.Tests.XUnit.csproj Conditionally includes the PE editor and its tests.
tests/​MaxRev.Gdal.Core.Tests.XUnit/​BinaryLoadingTests.cs Checks loaded DLL names and duplicate packaged names.

Review effort: Balanced

Comment thread win/partials.psm1 Outdated
elseif ($shipped.ContainsKey($import) -or $import -like "api-ms-win-*" -or $import -like "ext-ms-*") {
continue
}
elseif (Test-Path -Path (Join-Path $systemDir $import) -PathType Leaf) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ed84de3: unshipped imports are now classified as a Windows system dependency only if they are an API set name (api-ms-win-/ext-ms-) or exist in System32 with a valid Microsoft Windows catalog signature; everything else fails the build.

Comment thread win/partials.psm1 Outdated
if (-not (Test-Path -Path $dir -PathType Container)) {
throw "Expected a packaged output directory at '$dir' but it does not exist."
}
Get-ChildItem -Path $dir -File -Recurse -ErrorAction Stop | Where-Object { $_.Extension -in ".dll", ".exe" }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ed84de3: Get-PackagedPeFiles now throws if a requested directory exists but contains zero DLL/EXE files.

Install-PwshModuleRequirements called Set-PSRepository when PSGallery was
not registered, which cannot create a repository and throws. Runners can
intermittently start without PSGallery registered, which failed
TestPackages-Windows before any test ran. Register the default repository
with a bounded retry instead.
MaxRev-Dev added a commit that referenced this pull request Oct 30, 2026
The extract step created its container from the shared :latest tag, which a concurrent PR run can overwrite between push and extract. Run 594 (PR #254) picked up run 593's MaxRev.Gdal.Core package this way and failed restore in TestPackages-Linux.
MaxRev-Dev added a commit that referenced this pull request Oct 31, 2026
…to the run's image (#258)

* ci(macos): move arm64 jobs from retiring macos-14 to macos-15

GitHub retires macOS 14 runner images on 2026-11-02 and is running scheduled brownouts that cancel macos-14 jobs. Binaries still target macOS 11.0 via OSX_DEPLOYMENT_TARGET.

* ci(linux): extract build artifacts from this run's image digest

The extract step created its container from the shared :latest tag, which a concurrent PR run can overwrite between push and extract. Run 594 (PR #254) picked up run 593's MaxRev.Gdal.Core package this way and failed restore in TestPackages-Linux.

* ci(linux): fail the extract step if the digest container cannot be created

docker create inside the argument list did not stop the step on failure, and an empty container name made the extractor fall back to :latest.
MaxRev-Dev

This comment was marked as outdated.

@MaxRev-Dev
MaxRev-Dev merged commit ed4ced1 into main Oct 31, 2026
15 checks passed
@MaxRev-Dev
MaxRev-Dev deleted the fix/win-unique-dll-names branch October 31, 2026 03:38
MaxRev-Dev added a commit that referenced this pull request Oct 31, 2026
* fix(osx): point CLI tools at shipped PROJ/libgdal dylibs, test gdalsrsinfo

CLI tools like gdalsrsinfo link @rpath/libproj.25.dylib but the macOS
runtime package only ships the fully versioned real file, so they
failed to load at runtime. dylibbundler never patched tool binaries,
only the wrap/driver dylibs.

Patch every tool's @rpath references in copy-tools to the real shipped
file names via install_name_tool -change, failing the build if a
reference cannot be resolved against a shipped library (check-tool-deps
target). This also lets us drop the separate libgdal.<major>.dylib
copy in copy-deps, since dylibbundler already points the wrap libs at
the fully versioned file and tools are now patched too; NuGet doesn't
preserve symlinks so that copy was a duplicate ~30MB binary per
package.

Add gdalsrsinfo to the CLI smoke test so CI covers a tool that links
PROJ directly instead of only libgdal.

* feat(unix): uniquely name all bundled shared libraries (#9)

The runtime ships its libraries flat in runtimes/<rid>/native, so a
same-named .so from another NuGet package (NetVips, SkiaSharp, ...) can
overwrite ours in the app output, or the other way round. glibc also
reuses any loaded object whose SONAME matches a DT_NEEDED entry,
regardless of RUNPATH, so a libz.so.1 loaded first by other code
satisfied our imports too.

unique-native-names.sh renames every bundled library except libgdal,
the *_wrap libs and libgomp to <name>-maxrev (libproj-maxrev.so.25),
sets the SONAME to match and rewrites DT_NEEDED and version
requirements of all packaged libraries and tools. This replaces the
OpenSSL-only rename from #224; libssl-maxrev.so.3 still cannot shadow
the system libssl.so.3. patchelf 0.14 garbles the SONAME when
--set-soname and --replace-needed share one call, so each edit gets its
own call.

The build then fails if any library ships under its original name or
any packaged ELF needs a name that is neither packaged nor glibc/GCC
runtime, and loads the result and runs gdalinfo and gdalsrsinfo. CI
runs the same verification on the finished nupkgs.

* feat(osx): uniquely name all bundled dylibs (#9)

Rename every bundled dylib except libgdal and the *_wrap libs to
<name>-maxrev (libproj-maxrev.25.9.8.1.dylib), change its install id to
match and rewrite the load commands of all packaged dylibs and tools,
so other NuGet packages' same-named dylibs cannot overwrite ours in
the flat runtimes/<rid>/native output, or the other way round.

The same pass replaces the tool-only rewrite and check-tool-deps from
the previous commit: tool references such as @rpath/libproj.25.dylib
are resolved through the build lib symlinks (or the one matching
shipped file) and pointed at the renamed file. All edits are resolved
before any file is touched, every patched binary is ad-hoc re-signed,
and install_name_tool and codesign failures now fail the build instead
of being ignored.

The build then fails if any dylib ships under its original name or
with a foreign install id, any packaged Mach-O loads a name that is
neither packaged nor in /usr/lib or /System/Library, or a signature
does not verify, and loads the result and runs gdalinfo and
gdalsrsinfo. CI runs the same verification on the finished nupkgs.

* test: check Linux and macOS load uniquely named bundled libraries

Mirror the Windows check from #254: after ConfigureAll, every library
mapped from the runtime's native folder (/proc/self/maps on Linux, dyld
image list on macOS) must be libgdal, a *_wrap lib, libgomp or carry
the -maxrev suffix, at least one renamed library must be loaded, and
no renamed library may also ship under its original name.

* docs: explain the -maxrev names of bundled native libraries

* fix(unix,osx): clean up native-name script shell checks

* fix(osx): keep the libgdal ABI install name for external plugins [skip unix] [skip windows]

* fix(osx): ad-hoc sign Mach-O files that needed no patching [skip unix] [skip windows]

x86_64 links are not signed by the linker, and tools that already load @rpath/libgdal.N.dylib were left untouched, so verify failed on 28 unsigned macos-15-intel tools.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] SpatialReference..ctor() throws TypeInitializationException

2 participants