Skip to content

Commit 25abcc6

Browse files
committed
fix: promote ghc-internal to RTLD_GLOBAL to prevent duplicate loading
When ghc-iserv loads user code that depends on ghc-internal via dlopen, the dynamic linker may load a second copy of libHSghc-internal.so if the original was loaded with RTLD_LOCAL (which is required by Note [RTLD_LOCAL] in rts/Linker.c for symbol override behavior). This causes heap corruption because: 1. The first copy's init_ghc_hs_iface() initializes ghc_hs_iface in the RTS 2. User code compiled against the second copy creates closures with info tables from the second copy 3. The GC uses ghc_hs_iface (pointing to first copy's info tables) to validate closures 4. The mismatch causes "strange closure type" errors during GC This fix uses dladdr to find the shared library containing init_ghc_hs_iface (a known symbol from ghc-internal), then uses dlopen with RTLD_NOLOAD | RTLD_GLOBAL to promote it to global scope before running any Haskell code. See Note [Promoting ghc-internal to RTLD_GLOBAL] in iservmain.c.
1 parent 1f0df60 commit 25abcc6

File tree

1 file changed

+81
-0
lines changed

1 file changed

+81
-0
lines changed

utils/ghc-iserv/cbits/iservmain.c

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,91 @@
44

55
#include <HsFFI.h>
66

7+
#if !defined(mingw32_HOST_OS)
8+
#include <dlfcn.h>
9+
#endif
10+
11+
/*
12+
* Note [Promoting ghc-internal to RTLD_GLOBAL]
13+
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14+
* When ghc-iserv loads user code that depends on ghc-internal, the dynamic
15+
* linker may load a second copy of libHSghc-internal.so if the original copy
16+
* was loaded with RTLD_LOCAL (the default).
17+
*
18+
* This causes heap corruption because:
19+
* 1. The first copy's init_ghc_hs_iface() initializes ghc_hs_iface in the RTS
20+
* 2. User code compiled against the second copy creates closures with info
21+
* tables from the second copy
22+
* 3. The GC uses ghc_hs_iface (pointing to first copy's info tables) to
23+
* validate closures
24+
* 4. The mismatch causes "strange closure type" errors during garbage collection
25+
*
26+
* To fix this, we use dladdr to find the shared library containing a known
27+
* symbol from ghc-internal (init_ghc_hs_iface), then use dlopen with
28+
* RTLD_NOLOAD | RTLD_GLOBAL to promote it to global scope. This ensures
29+
* that when user code's dependencies are resolved, the dynamic linker uses
30+
* the already-loaded copy instead of loading a fresh one.
31+
*
32+
* Note: RTLD_NOLOAD is a GNU extension (and available on macOS since 10.3)
33+
* that returns a handle to an already-loaded library without incrementing
34+
* its reference count. Combined with RTLD_GLOBAL, it changes the library's
35+
* visibility to global.
36+
*
37+
* See also:
38+
* - Note [RTLD_LOCAL] in rts/Linker.c
39+
* - Note [RTS/ghc-internal interface] in rts/RtsToHsIface.c
40+
* - Note [ghc-iserv and dynamic symbol export] in utils/ghc-iserv/ghc-iserv.cabal.in
41+
*/
42+
#if !defined(mingw32_HOST_OS) && defined(RTLD_NOLOAD)
43+
static void promote_ghc_internal_to_global(void)
44+
{
45+
/*
46+
* Find ghc-internal's shared library by looking up init_ghc_hs_iface,
47+
* which is a symbol we know exists in ghc-internal. Then use dladdr
48+
* to get the library path, and reopen it with RTLD_GLOBAL.
49+
*/
50+
51+
/* init_ghc_hs_iface is defined in ghc-internal's cbits/RtsIface.c */
52+
extern void init_ghc_hs_iface(void);
53+
54+
Dl_info info;
55+
if (dladdr((void*)&init_ghc_hs_iface, &info) == 0) {
56+
/* dladdr failed - symbol might be in the executable itself (static link).
57+
* In that case, symbols are already global via -rdynamic. */
58+
return;
59+
}
60+
61+
if (info.dli_fname == NULL) {
62+
/* No filename available - shouldn't happen but handle gracefully */
63+
return;
64+
}
65+
66+
/* Reopen the library with RTLD_GLOBAL to promote its symbols.
67+
* RTLD_NOLOAD ensures we don't load a new copy - we just change
68+
* the visibility of the already-loaded library. */
69+
void *handle = dlopen(info.dli_fname, RTLD_NOW | RTLD_NOLOAD | RTLD_GLOBAL);
70+
if (handle != NULL) {
71+
/* Successfully promoted to global scope.
72+
* Don't dlclose - we want the library to stay loaded and global. */
73+
#if defined(DEBUG)
74+
fprintf(stderr, "ghc-iserv: promoted %s to RTLD_GLOBAL\n", info.dli_fname);
75+
#endif
76+
}
77+
/* If dlopen failed, that's OK - might be statically linked or
78+
* the -rdynamic/-flat_namespace linker flags should help. */
79+
}
80+
#endif /* !mingw32_HOST_OS && RTLD_NOLOAD */
81+
782
int main (int argc, char *argv[])
883
{
984
RtsConfig conf = defaultRtsConfig;
1085

86+
#if !defined(mingw32_HOST_OS) && defined(RTLD_NOLOAD)
87+
/* Promote ghc-internal to RTLD_GLOBAL before running any Haskell code.
88+
* See Note [Promoting ghc-internal to RTLD_GLOBAL] */
89+
promote_ghc_internal_to_global();
90+
#endif
91+
1192
// We never know what symbols GHC will look up in the future, so
1293
// we must retain CAFs for running interpreted code.
1394
conf.keep_cafs = 1;

0 commit comments

Comments
 (0)