[Git][ghc/ghc][master] 2 commits: rts: add a few missing i386 relocations in the rts linker
Marge Bot pushed to branch master at Glasgow Haskell Compiler / GHC
Commits:
04d143c0 by Luite Stegeman at 2026-04-21T14:05:33-04:00
rts: add a few missing i386 relocations in the rts linker
- - - - -
014087e7 by Luite Stegeman at 2026-04-21T14:05:34-04:00
CodeOutput: Fix finalizers on multiple platforms
- ELF platforms: emit .fini_array section
- wasm32/Darwin: emit initializer with __cxa_atexit call
- Windows: use -Wl,--whole-archive to prevent dropping finalizer symbols
- rts linker: fix crash/assertion failure unloading objects with finalizers
fixes #27072
- - - - -
21 changed files:
- + changelog.d/fix-finalizers-27072
- compiler/GHC/Driver/CodeOutput.hs
- compiler/GHC/Linker/Executable.hs
- compiler/GHC/Types/ForeignStubs.hs
- rts/Linker.c
- rts/LinkerInternals.h
- rts/linker/Elf.c
- + testsuite/tests/codeGen/should_run/T27072d.hs
- + testsuite/tests/codeGen/should_run/T27072d.stdout
- + testsuite/tests/codeGen/should_run/T27072d_c.c
- + testsuite/tests/codeGen/should_run/T27072d_check.c
- + testsuite/tests/codeGen/should_run/T27072w.hs
- + testsuite/tests/codeGen/should_run/T27072w.stdout
- + testsuite/tests/codeGen/should_run/T27072w_c.c
- testsuite/tests/codeGen/should_run/all.T
- + testsuite/tests/rts/linker/T27072/Lib.c
- + testsuite/tests/rts/linker/T27072/Makefile
- + testsuite/tests/rts/linker/T27072/T27072.stdout
- + testsuite/tests/rts/linker/T27072/all.T
- + testsuite/tests/rts/linker/T27072/main.c
- testsuite/tests/th/all.T
Changes:
=====================================
changelog.d/fix-finalizers-27072
=====================================
@@ -0,0 +1,10 @@
+section: codegen
+synopsis: Fix module finalizers on multiple platforms
+description: {
+ GHC-generated module finalizers (e.g. ``hs_spt_remove`` for the Static
+ Pointer Table) now run correctly on ELF platforms, darwin, wasm32 and
+ Windows. Also fixes running finalizers when unloading objects with the
+ RTS linker.
+}
+issues: #27072
+mrs: !15762
=====================================
compiler/GHC/Driver/CodeOutput.hs
=====================================
@@ -119,6 +119,7 @@ codeOutput logger tmpfs llvm_config dflags unit_state this_mod filenm location g
{ a <- linted_cmm_stream
; let stubs = genForeignStubs a
; emitInitializerDecls this_mod stubs
+ ; emitFinalizerDecls this_mod stubs
; return (stubs, a) }
; let dus1 = newTagDUniqSupply CodeGenTag dus0
@@ -133,19 +134,23 @@ codeOutput logger tmpfs llvm_config dflags unit_state this_mod filenm location g
}
-- | See Note [Initializers and finalizers in Cmm] in GHC.Cmm.InitFini for details.
-emitInitializerDecls :: Module -> ForeignStubs -> CgStream RawCmmGroup ()
-emitInitializerDecls this_mod (ForeignStubs _ cstub)
- | initializers <- getInitializers cstub
- , not $ null initializers =
- let init_array = CmmData sect statics
- lbl = mkInitializerArrayLabel this_mod
- sect = Section InitArray lbl
+emitInitializerDecls, emitFinalizerDecls :: Module -> ForeignStubs -> CgStream RawCmmGroup ()
+emitInitializerDecls = emitInitFiniArrayDecls InitArray mkInitializerArrayLabel getInitializers
+emitFinalizerDecls = emitInitFiniArrayDecls FiniArray mkFinalizerArrayLabel getFinalizers
+
+emitInitFiniArrayDecls :: SectionType -> (Module -> CLabel) -> (CStub -> [CLabel])
+ -> Module -> ForeignStubs -> CgStream RawCmmGroup ()
+emitInitFiniArrayDecls sect_type mk_lbl get_labels this_mod (ForeignStubs _ cstub)
+ | labels <- get_labels cstub
+ , not $ null labels =
+ let lbl = mk_lbl this_mod
+ sect = Section sect_type lbl
statics = CmmStaticsRaw lbl
[ CmmStaticLit $ CmmLabel fn_name
- | fn_name <- initializers
+ | fn_name <- labels
]
- in Stream.yield [init_array]
-emitInitializerDecls _ _ = return ()
+ in Stream.yield [CmmData sect statics]
+emitInitFiniArrayDecls _ _ _ _ _ = return ()
doOutput :: String -> (Handle -> IO a) -> IO a
doOutput filenm io_action = bracket (openFile filenm WriteMode) hClose io_action
=====================================
compiler/GHC/Linker/Executable.hs
=====================================
@@ -300,7 +300,19 @@ linkExecutable logger tmpfs opts unit_env o_files dep_units = do
then ["-Wl,--gc-sections"]
else [])
+ -- On Windows, module .o files may be archives (see
+ -- Note [Object merging] in GHC.Driver.Pipeline.Execute).
+ -- Use --whole-archive to ensure all archive members are
+ -- included, especially those containing .ctors/.dtors
+ -- initializer/finalizer sections. See Note [Initializers and
+ -- finalizers in Cmm] in GHC.Cmm.InitFini.
+ ++ (if platformOS platform == OSMinGW32
+ then ["-Wl,--whole-archive"]
+ else [])
++ o_files
+ ++ (if platformOS platform == OSMinGW32
+ then ["-Wl,--no-whole-archive"]
+ else [])
++ lib_path_opts)
++ extra_ld_inputs
++ map GHC.SysTools.Option (
=====================================
compiler/GHC/Types/ForeignStubs.hs
=====================================
@@ -59,11 +59,85 @@ initializerCStub platform clbl declarations body =
-- | @finalizerCStub fn_nm decls body@ is a 'CStub' containing C finalizer
-- function (e.g. an entry of the @.fini_array@ section) named
-- @fn_nm@ with the given body and the given set of declarations.
+--
+-- See Note [Finalizers via __cxa_atexit]
finalizerCStub :: Platform -> CLabel -> SDoc -> SDoc -> CStub
-finalizerCStub platform clbl declarations body =
- functionCStub platform clbl declarations body
+finalizerCStub platform clbl declarations body
+ | ArchWasm32 <- platformArch platform
+ = -- See Note [Finalizers via __cxa_atexit]
+ cxaAtexitFinalizerCStub platform clbl declarations body
+finalizerCStub platform clbl declarations body
+ | OSDarwin <- platformOS platform
+ = -- See Note [Finalizers via __cxa_atexit]
+ cxaAtexitFinalizerCStub platform clbl declarations body
+finalizerCStub platform clbl declarations body
+ = functionCStub platform clbl declarations body
`mappend` CStub empty [] [clbl]
+-- | Generate a @__cxa_atexit@-based finalizer.
+-- See Note [Finalizers via __cxa_atexit]
+cxaAtexitFinalizerCStub :: Platform -> CLabel -> SDoc -> SDoc -> CStub
+cxaAtexitFinalizerCStub platform clbl declarations body =
+ let clbl_pretty = pprCLabel platform clbl
+ fini_name = hcat [clbl_pretty, text "$fini"]
+ wrapper_name = hcat [clbl_pretty, text "$fini_atexit"]
+ c_code = vcat
+ [ declarations
+ , text "int __cxa_atexit(void (*)(void *), void *, void *);"
+ , hcat [text "static void ", fini_name, text "(void)"]
+ , braces body
+ , hcat [text "static void ", wrapper_name, text "(void *arg __attribute__((unused)))"]
+ , braces (hcat [fini_name, text "();"])
+ , hsep [text "void", clbl_pretty, text "(void)"]
+ , braces (hcat [text "__cxa_atexit(", wrapper_name, text ", 0, 0);"])
+ ]
+ in CStub c_code [clbl] []
+
+{-
+Note [Finalizers via __cxa_atexit]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+On some platforms, placing a function pointer in the .fini_array /
+__mod_term_func section is not sufficient to have it called on exit.
+On these platforms we instead lower finalizers as initializers that register
+the actual finalizer function via __cxa_atexit.
+
+Affected platforms:
+
+ Wasm32: does not support .fini_array sections.
+
+ Darwin: modern macOS dyld no longer processes __DATA,__mod_term_func entries.
+ Clang now lowers __attribute__((destructor)) as an initializer that calls
+ __cxa_atexit, placing the initializer in __DATA,__mod_init_func (which the
+ linker converts to __TEXT,__init_offsets). GHC must follow the same pattern.
+
+For a finalizer with label `clbl` and body `body`, on these platforms we
+generate:
+
+ static void clbl$fini(void) {
+ <body>
+ }
+ static void clbl$fini_atexit(void *arg) {
+ clbl$fini();
+ }
+ void clbl(void) {
+ __cxa_atexit(clbl$fini_atexit, 0, 0);
+ }
+
+The function `clbl` is placed in the initializers list (getInitializers)
+instead of the finalizers list (getFinalizers). During code output,
+emitInitializerDecls places it in .init_array / __mod_init_func, so the
+registration runs at startup.
+
+The actual finalizer body is in the static helper `clbl$fini`. A separate
+wrapper `clbl$fini_atexit` with the void(*)(void*) signature expected by
+__cxa_atexit is needed because some platforms (e.g. wasm32) enforce exact
+function signature matching at call sites — a simple cast would trap at
+runtime.
+
+This matches what clang does when lowering __attribute__((destructor)) on
+these platforms.
+-}
+
newtype CHeader = CHeader { getCHeader :: SDoc }
instance Monoid CHeader where
=====================================
rts/Linker.c
=====================================
@@ -1117,6 +1117,27 @@ freePreloadObjectFile (ObjectCode *oc)
oc->fileSize = 0;
}
+/* Note [Object unloading and finalizers]
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ * An ObjectCode may contain .fini_array/.dtors sections with finalizers that
+ * should run when the object is unloaded. However, we must only run these
+ * finalizers if the corresponding initializers (.init_array/.ctors) have
+ * actually been executed.
+ *
+ * Archive members start in OBJECT_LOADED state and only progress to
+ * OBJECT_NEEDED -> OBJECT_RESOLVED -> OBJECT_READY when a symbol from
+ * them is actually required. An archive member that was never needed never
+ * has its relocations applied, so its .fini_array section data still
+ * contains zeros (unresolved relocation targets). Running those finalizers
+ * would dereference NULL function pointers.
+ *
+ * When unloadObj sets an object's status to OBJECT_UNLOADED, it does so
+ * regardless of the previous state, so we cannot rely on the status alone
+ * to decide whether finalizers should run. Instead, we track whether
+ * initializers were executed via the initializersRan flag, which is set in
+ * ocRunInit after successfully running the initializers.
+ */
+
/*
* freeObjectCode() releases all the pieces of an ObjectCode. It is called by
* the GC when a previously unloaded ObjectCode has been determined to be
@@ -1126,11 +1147,9 @@ void freeObjectCode (ObjectCode *oc)
{
IF_DEBUG(linker, ocDebugBelch(oc, "freeObjectCode: start\n"));
- // Run finalizers
- if (oc->type == STATIC_OBJECT &&
- (oc->status == OBJECT_READY || oc->status == OBJECT_UNLOADED)) {
- // Only run finalizers if the initializers have also been run, which
- // happens when we resolve the object.
+ // Run finalizers only if initializers have been run.
+ // See Note [Object unloading and finalizers].
+ if (oc->type == STATIC_OBJECT && oc->initializersRan) {
#if defined(OBJFORMAT_ELF)
ocRunFini_ELF(oc);
#elif defined(OBJFORMAT_PEi386)
@@ -1295,6 +1314,7 @@ mkOc( ObjectType type, pathchar *path, char *image, int imageSize,
oc->imageMapped = mapped;
oc->misalignment = misalignment;
+ oc->initializersRan = false;
oc->cxa_finalize = NULL;
oc->extraInfos = NULL;
@@ -1691,6 +1711,7 @@ int ocRunInit(ObjectCode *oc)
foreignExportsFinishedLoadingObject();
if (!r) { return r; }
+ oc->initializersRan = true;
oc->status = OBJECT_READY;
return 1;
=====================================
rts/LinkerInternals.h
=====================================
@@ -268,6 +268,12 @@ struct _ObjectCode {
after allocation, so that we can use realloc */
int misalignment;
+ /* Set to true after initializers (.init_array, .ctors, etc.) have been
+ * executed. Used by freeObjectCode to decide whether finalizers should
+ * run: only objects whose initializers ran should have their finalizers
+ * executed. See Note [Object unloading and finalizers]. */
+ bool initializersRan;
+
/* The address of __cxa_finalize; set when at least one finalizer was
* register and therefore we must call __cxa_finalize before unloading.
* See Note [Resolving __dso_handle]. */
=====================================
rts/linker/Elf.c
=====================================
@@ -1308,6 +1308,16 @@ do_Elf_Rel_relocations ( ObjectCode* oc, char* ehdrC,
case COMPAT_R_386_NONE: break;
case COMPAT_R_386_32: *pP = value; break;
case COMPAT_R_386_PC32: *pP = value - P; break;
+ case COMPAT_R_386_PLT32: *pP = value - P; break;
+ case COMPAT_R_386_GOTOFF: *pP = value - (Elf_Addr)oc->info->got_start; break;
+ case COMPAT_R_386_GOTPC: *pP = (Elf_Addr)oc->info->got_start + A - P; break;
+ case COMPAT_R_386_GOT32:
+ case COMPAT_R_386_GOT32X:
+ CHECK(symbol);
+ CHECK(symbol->got_addr);
+ *pP = (Elf_Addr)symbol->got_addr
+ - (Elf_Addr)oc->info->got_start + A;
+ break;
# endif
# if defined(arm_HOST_ARCH)
=====================================
testsuite/tests/codeGen/should_run/T27072d.hs
=====================================
@@ -0,0 +1,10 @@
+{-# LANGUAGE StaticPointers #-}
+module T27072d where
+
+import GHC.StaticPtr
+
+f :: StaticPtr Int
+f = static 1
+
+g :: StaticPtr Int
+g = static 2
=====================================
testsuite/tests/codeGen/should_run/T27072d.stdout
=====================================
@@ -0,0 +1,2 @@
+SPT entries after init: 2
+SPT entries after finalizer: 0
=====================================
testsuite/tests/codeGen/should_run/T27072d_c.c
=====================================
@@ -0,0 +1,38 @@
+// Test that GHC-generated module initializers and finalizer registrations
+// work correctly on Darwin.
+//
+// On Darwin, GHC lowers finalizers as __cxa_atexit registrations from an
+// initializer placed in __DATA,__mod_init_func (see Note [Finalizers via
+// __cxa_atexit] in GHC.Types.ForeignStubs).
+//
+// This test verifies the mechanism by checking that:
+// 1. The SPT initializer runs at load time (entries are inserted).
+// 2. The SPT finalizer (registered via __cxa_atexit from __mod_init_func)
+// fires during exit() and removes the entries.
+//
+// We verify (2) by registering our own __cxa_atexit checker from a
+// constructor in a dylib that is loaded before the main executable's
+// initializers run. Since __cxa_atexit handlers fire in LIFO order,
+// a handler registered earlier runs later — so our checker runs after the
+// GHC-generated finalizer, and can observe that SPT entries were removed.
+//
+// The Apple linker does not support --wrap, so this is the Darwin
+// equivalent of T27072w's approach.
+
+#include "Rts.h"
+#include
participants (1)
-
Marge Bot (@marge-bot)