Repository navigation
fix(win): ship uniquely named bundled DLLs to avoid foreign DLL conflicts - #254
Conversation
…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.
There was a problem hiding this comment.
🟡 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
| 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) { |
There was a problem hiding this comment.
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.
| 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" } |
There was a problem hiding this comment.
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.
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.
…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.
# Conflicts: # win/functions.psm1
* 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.


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 sharedruntimes/win-x64/nativeoutput 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 calledSet-PSRepository, which cannot create a missing repository, and failed the Windows test job before any test ran.BinaryLoadingTests(Windows-only) asserts that every native module loaded from the package folder is either on the keep-list or carries the-maxrevsuffix, and that no renamed DLL is also packaged under its original name.PeImportRenamerTestscovers the appended-section path, the section-padding fallback, the no-space error and signed files.Kept under their original names:
gdal.dlland the SWIG bindingsgdal_wrap.dll,ogr_wrap.dll,osr_wrap.dll,gdalconst_wrap.dll(loaded by name from managed code and by consumers)vcruntime140*.dll,msvcp140*.dll,concrt140.dll,vccorlib140.dll,vcomp*.dll) andmsodbcsql17.dll(Microsoft-signed redistributables)OpenCL.dllif collected, and any other Authenticode-signed DLLCode 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 importinghdf5.dll,proj_9.dll, ...) no longer binds to our copies.Verification
Rename-BundledDLLsandConfirm-PackagedDllImportsthrough the PowerShell SDK on the native DLLs fromMaxRev.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.Confirm-PackagedDllImportsfails for an unpatched DLL put back under its original name, an unpatched EXE, a missing renamed DLL, and missing or empty folders.BinaryLoadingTestsandPeImportRenamerTests11/11 locally (the loading test is Windows-only). Native loading on Windows is covered by the Windows CI run.