Skip to content

Conversation

@EricLendvai
Copy link
Contributor

2026-01-16 01:40 UTC-0800 Eric Lendvai (harbour.wiki)

  • utils/hbmk2/hbmk2.prg
    • hbmk2 (mingw): Prevent (null) DLL name in generated import libraries
      Harden MinGW dlltool path in win_implib_def() to avoid creating
      import libraries with embedded DLL name (null) when the source .def
      contains LIBRARY (null) or lacks a LIBRARY line.
      + Sanitize .def to ensure LIBRARY ""
      + Always pass -D "" to dlltool
      + Scoped to MinGW flow; MSVC unaffected
      Fixes executables importing (null).dll (observed with libcurl and
      other dependencies built via hbmk2).

@alcz
Copy link
Contributor

alcz commented Jan 17, 2026

Hello Eric!
It seems like a significant amount of mangling needed. Can you outline the steps to reproduce this issue with MinGW toolchain? dlltool is possibly also used not only in MinGW + GCC but also with clang.

@EricLendvai
Copy link
Contributor Author

Hello Aleksander,

Here is a concrete reproduction outline and the underlying mechanism.

Summary of the issue

On MinGW (both 32-bit and 64-bit), hbmk2 can generate an import library via dlltool using a .def file. If that .def file contains an invalid/missing LIBRARY header (most commonly LIBRARY (null)), dlltool bakes that value into the import library. Any executable linked against that import library will then import a DLL named (null) (or (null).dll depending on tooling), producing runtime failures unless the real DLL is renamed to match.

This does not appear with MSVC because it uses lib.exe/link.exe style import-lib generation, not dlltool.

Minimal reproduction steps (works on mingw32 and mingw64)

The core is: make hbmk2 go down the win_implib_def() path (i.e., a .def file exists for a DLL), and ensure that .def has LIBRARY (null) or no LIBRARY line.

A) Prepare a DLL and a broken .def

  1. Ensure MinGW toolchain is present (MSYS2 is easiest):
  • Use MSYS2 “MinGW 32-bit” (mingw32) or “MinGW 64-bit” (mingw64).
  1. Pick any real DLL to create an import library for. libcurl is a common example:
  • mingw32: C:\msys64\mingw32\bin\libcurl-4.dll
  • mingw64: C:\msys64\mingw64\bin\libcurl-4.dll
  1. Create a .def file with the same base name as the DLL (hbmk2 looks for it), but with a bad header. Example for libcurl-4.dll:

libcurl-4.def:

LIBRARY (null)
EXPORTS
  curl_easy_init
  curl_easy_cleanup

(For reproduction, the export list does not need to be exhaustive. It only needs to be syntactically acceptable for dlltool.)

B) Generate an import library with hbmk2 (unpatched)

  1. Run hbmk2 in “implib generation” mode to create an import library for the DLL. The exact command varies depending on how you invoke hbmk2’s implib helper on your platform/build, but the observable effect is: hbmk2 ends up calling something equivalent to:
dlltool -d libcurl-4.def -l libcurl-4.dll.a

Because the .def says LIBRARY (null), the resulting import library will embed (null) as the imported module name.

(For clarity: the same thing can be reproduced directly with dlltool without hbmk2; hbmk2 is simply the orchestrator that triggers this in Harbour’s dependency build workflows.)

C) Link a trivial program against the generated import library

  1. Link any executable against that generated import library. For example, a trivial C “hello” that does not even call curl is enough if the import lib is linked in; or a Harbour program that pulls -llibcurl due to hbcurl usage.

  2. Inspect the executable import table:

objdump -p your.exe | grep -i "DLL Name"

You will see:

DLL Name: (null)

At runtime, Windows loader will attempt to load (null) / (null).dll.

This is exactly what I observed in a real build scenario: hbmk2 linked with -llibcurl which resolved to liblibcurl.a under C:\Harbour\lib\win\mingw, and that import library contained (null) as the embedded DLL name.

Why this happens (root cause)

  • On MinGW, the imported DLL name recorded into the .a import library is derived from the .def file’s LIBRARY line unless overridden.
  • If the LIBRARY line is malformed ((null) or blank), the import library records that invalid value.
  • When the linker uses that import library, it copies that module name into the final EXE’s import table.

This is why the patch does two things:

  1. sanitize .def so the LIBRARY line is always valid, and
  2. pass dlltool -D "<dll filename>" to force the correct module name even if .def is defective.

Why this matters in hbmk2 specifically

This can happen in real Harbour builds because .def files may be generated (or copied) by third-party packaging steps, and some tooling does produce LIBRARY (null) under certain circumstances. Once such a .def lands next to a DLL, hbmk2’s “prefer same-name .def” logic will use it, and dlltool will embed the bad name.

The fix is defensive: hbmk2 should not propagate (null) into generated import libraries when it can trivially infer the real DLL filename it is processing.

Toolchain note: clang

Yes, this is relevant beyond GCC. The underlying issue is dlltool + .def semantics, not GCC specifically.

  • clang on Windows (when targeting MinGW/COFF) typically uses the same binutils (ld, dlltool, etc.) from the MinGW environment.
  • If hbmk2 uses the same dlltool path for clang-based MinGW builds, the hardening is still applicable and beneficial.

In other words: any hbmk2 flow that ends up running dlltool -d <def> -l <implib> can be affected; this patch protects that entire class of flows.

What the patch changes in observable behavior

  • If .def has LIBRARY (null) or no LIBRARY, hbmk2 will generate a temporary corrected .def using the real DLL filename.
  • hbmk2 will also always add -D "<dll filename>" when invoking dlltool.
  • The resulting import library will embed the correct DLL name (e.g., libcurl.dll), and executables linked against it will import the correct module.

@alcz
Copy link
Contributor

alcz commented Jan 17, 2026

As far as I worked with multiple build systems: in my opinion we shouldn't defensively add -D dllname argument when calling dlltool. You can't be sure about llvm-dlltool or other non-GNU flavor exact behavior when adding extra flags . The same applies adding a LIBRARY line when there is no such line in the file.

LIBRARY (null) detection and warning could be added in some simplified form, possibly with such scheme:
c := hb_MemoRead() ; hb_AtI("LIBRARY (null)" > 0 ; output warning about malformed .def file ; StrTran(...) or better Stuff() to not play with detecting the case of this keyword ; write temporary .def?

@EricLendvai EricLendvai force-pushed the fix_hbmk2_mingw_imported_libraries branch from 022d1ab to faf688c Compare January 20, 2026 06:09
import libraries generated from .def files on the dlltool path.

This is opt-in only (no behavior change unless HBMK_IMPLIBDLLMAP is set),
and fixes cases where a .def lacks a LIBRARY directive and dlltool would
otherwise produce an import library referencing (null).dll.
@EricLendvai EricLendvai force-pushed the fix_hbmk2_mingw_imported_libraries branch from faf688c to 88e73a6 Compare January 20, 2026 06:15
@EricLendvai
Copy link
Contributor Author

Hello Aleksander,

I reworked the solution to address the root cause while keeping hbmk2 behavior stable by default.

Problem: In MinGW import-lib generation (dlltool path), if the input .def lacks a LIBRARY directive (e.g. starts with EXPORTS), dlltool can generate an import library that embeds a dependency on (null).dll. Any executable linked against that import library then imports (null).dll.

Why prior fixes were controversial: Unconditionally passing dlltool -D <name or automatically injecting LIBRARY lines changes behavior for valid .def files and may differ across dlltool variants.

New approach (this PR): Add an opt-in environment variable HBMK_IMPLIBDLLMAP that maps defname:dllname (supports multiple entries separated by ;). When set, hbmk2 uses it only on the dlltool import-lib path to supply a concrete DLL name by generating a temporary .def containing LIBRARY "<dllname" when the source .def has no usable LIBRARY name.

Safety / compatibility:

  • No unconditional -D injection.
  • No behavior change unless the environment variable is explicitly set.
  • Existing builds remain unaffected; users who hit (null).dll can fix it without patching third-party .def files.

Repro: Use any .def without a LIBRARY line and run the hbmk2 import-lib generation on MinGW; without the override, the produced import lib may embed (null) and executables import (null).dll.

Usage example:

  • MinGW32: set HBMK_IMPLIBDLLMAP=libcurl.def:libcurl-4.dll
  • MinGW64: set HBMK_IMPLIBDLLMAP=libcurl.def:libcurl-x64.dll
  • MSVC64: set HBMK_IMPLIBDLLMAP=libcurl.def:libcurl-x64.dll
    Then delete the generated import lib (e.g. liblibcurl.a) and rebuild so it regenerates with the correct embedded DLL name.

Also updated ChangeLog.txt to reflect the final implementation: opt-in HBMK_IMPLIBDLLMAP DEF-DLL mapping for dlltool import-lib generation (no default behavior change).

I tested this is Mingw, Mingw64 and MSVC64

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.

2 participants