]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
locale-util: dlopen() libintl instead of linking against it
authorDaan De Meyer <daan@amutable.com>
Fri, 15 May 2026 18:33:43 +0000 (18:33 +0000)
committerDaan De Meyer <daan@amutable.com>
Mon, 18 May 2026 21:17:38 +0000 (21:17 +0000)
dgettext() lives in libc on glibc and in libintl.so.8 on musl with
gettext. Resolve it via dlsym() so neither configuration produces a
hard link-time dependency on libintl: try libintl.so.8 first and fall
back to RTLD_DEFAULT (which finds dgettext in libc on glibc).

The _() macro now expands to a runtime check that returns the
untranslated string if dlopen_libintl() has not run successfully, so
callers don't have to gate every translatable message on a runtime
check. pam_systemd_home — currently the only consumer of _() — calls
dlopen_libintl() best-effort from each PAM entry point.

The meson find_library('intl') dance is replaced with a has_header()
check; the only thing we need at build time is the prototype.

meson.build
src/basic/locale-util.c
src/basic/locale-util.h
src/home/meson.build
src/home/pam_systemd_home.c
src/test/test-dlopen-so.c

index 05739e517b27461ab5c68e7ac00fc9d285f0962e..b26ef7979c996f99313ca8efdd1e132dea90cffc 100644 (file)
@@ -983,20 +983,11 @@ librt = cc.find_library('rt')
 libm = cc.find_library('m')
 libdl = cc.find_library('dl')
 
-# On some distributions that use musl (e.g. Alpine), libintl.h may be provided by gettext rather than musl.
-# In that case, we need to explicitly link with libintl.so.
-if cc.has_function('dgettext',
-                   prefix : '''#include <libintl.h>''',
-                   args : '-D_GNU_SOURCE')
-        libintl = []
-else
-        libintl = cc.find_library('intl')
-        if not cc.has_function('dgettext',
-                               prefix : '''#include <libintl.h>''',
-                               args : '-D_GNU_SOURCE',
-                               dependencies : libintl)
-                error('dgettext() not found')
-        endif
+# Header presence check only — dgettext itself is resolved via dlopen_libintl() at runtime, so we never
+# link against libintl. On glibc dgettext lives in libc; on musl gettext-dev provides libintl.h alongside
+# libintl.so.8 which we dlopen() if present.
+if not cc.has_header('libintl.h')
+        error('libintl.h not found (install gettext / gettext-dev)')
 endif
 
 # On some architectures, libatomic is required. But on some installations,
index 3b869d70f9747ea81923996547698da5842f9631..6d4493c0bb450fef4a5930709707327292c837e9 100644 (file)
@@ -7,7 +7,12 @@
 #include <sys/stat.h>
 #include <unistd.h>
 
+#ifndef __GLIBC__
+#include "sd-dlopen.h"
+#endif
+
 #include "dirent-util.h"
+#include "dlfcn-util.h"
 #include "env-util.h"
 #include "fd-util.h"
 #include "fileio.h"
 #include "strv.h"
 #include "utf8.h"
 
+#ifdef __GLIBC__
+DLSYM_PROTOTYPE(dgettext) = dgettext;
+#else
+DLSYM_PROTOTYPE(dgettext) = NULL;
+#endif
+
+int dlopen_libintl(int log_level) {
+#ifdef __GLIBC__
+        return 1;
+#else
+        static void *libintl_dl = NULL;
+
+        SD_ELF_NOTE_DLOPEN(
+                        "intl",
+                        "Support for message translation via gettext",
+                        SD_ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED,
+                        "libintl.so.8");
+
+        return dlopen_many_sym_or_warn(
+                        &libintl_dl,
+                        "libintl.so.8",
+                        log_level,
+                        DLSYM_ARG(dgettext));
+#endif
+}
+
 static char* normalize_locale(const char *name) {
         const char *e;
 
index bf5cbcb439220b8bf98ec3bb49e13fba781a1c94..fb22953f16f0ef910d6ec899e30a686cd213605f 100644 (file)
@@ -1,9 +1,18 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 #pragma once
 
+#include <libintl.h>    /* IWYU pragma: export */
 #include <locale.h>     /* IWYU pragma: export */
 
 #include "basic-forward.h"
+#include "dlfcn-util.h"
+
+/* format_arg(2) propagates the format-string nature of the second argument to the return value, so that
+ * printf(_("Hello %s"), name) still gets checked. It survives both DLSYM_PROTOTYPE's typeof() and the
+ * ternary in _() below — verified on gcc and clang. */
+extern DLSYM_PROTOTYPE(dgettext) __attribute__((format_arg(2)));
+
+int dlopen_libintl(int log_level);
 
 typedef enum LocaleVariable {
         /* We don't list LC_ALL here on purpose. People should be
@@ -31,7 +40,9 @@ int get_locales(char ***ret);
 bool locale_is_valid(const char *name);
 int locale_is_installed(const char *name);
 
-#define _(String) dgettext(GETTEXT_PACKAGE, String)
+/* Falls back to the untranslated string if dlopen_libintl() hasn't run or has failed, so callers don't have
+ * to gate every translatable message on a runtime check. */
+#define _(String) (sym_dgettext ? sym_dgettext(GETTEXT_PACKAGE, (String)) : (String))
 #define N_(String) String
 
 bool is_locale_utf8(void);
index 631eeb13aa879890aba329cf3458a7de0fa10150..0b27001c5a04bba0008a6958076f0e7362c4a957 100644 (file)
@@ -115,7 +115,6 @@ modules += [
                 'conditions' : ['HAVE_PAM'],
                 'sources' : pam_systemd_home_sources,
                 'dependencies' : [
-                        libintl,
                         libpam_cflags,
                         threads,
                 ],
index 5fe6dcbec20fc24c4711aeff68bf1e0b759bf758..6e24924e91cfc0ffe8bc5a331a7f44c37c2af29a 100644 (file)
@@ -1,7 +1,5 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
-#include <libintl.h>
-
 #include "sd-bus.h"
 
 #include "alloc-util.h"
@@ -792,6 +790,8 @@ _public_ PAM_EXTERN int pam_sm_authenticate(
         if (r < 0)
                 return PAM_SERVICE_ERR;
 
+        (void) dlopen_libintl(LOG_DEBUG); /* best-effort: messages won't be translated if this fails */
+
         pam_log_setup();
 
         if (parse_env(pamh, &flags) < 0)
@@ -857,6 +857,8 @@ _public_ PAM_EXTERN int pam_sm_open_session(
         if (r < 0)
                 return PAM_SERVICE_ERR;
 
+        (void) dlopen_libintl(LOG_DEBUG); /* best-effort: messages won't be translated if this fails */
+
         pam_log_setup();
 
         if (parse_env(pamh, &flags) < 0)
@@ -914,6 +916,8 @@ _public_ PAM_EXTERN int pam_sm_close_session(
         if (r < 0)
                 return PAM_SERVICE_ERR;
 
+        (void) dlopen_libintl(LOG_DEBUG); /* best-effort: messages won't be translated if this fails */
+
         pam_log_setup();
 
         if (parse_argv(pamh,
@@ -979,6 +983,8 @@ _public_ PAM_EXTERN int pam_sm_acct_mgmt(
         if (r < 0)
                 return PAM_SERVICE_ERR;
 
+        (void) dlopen_libintl(LOG_DEBUG); /* best-effort: messages won't be translated if this fails */
+
         pam_log_setup();
 
         if (parse_env(pamh, &flags) < 0)
@@ -1098,6 +1104,8 @@ _public_ PAM_EXTERN int pam_sm_chauthtok(
         if (r < 0)
                 return PAM_SERVICE_ERR;
 
+        (void) dlopen_libintl(LOG_DEBUG); /* best-effort: messages won't be translated if this fails */
+
         pam_log_setup();
 
         if (parse_argv(pamh,
index 0eca1356c0313c31e4ccbf6e9e47927ddd655066..de1d7671392e8c3045a21b496038e2f6f7a5ce87 100644 (file)
@@ -18,6 +18,7 @@
 #include "libcrypt-util.h"
 #include "libfido2-util.h"
 #include "libmount-util.h"
+#include "locale-util.h"
 #include "main-func.h"
 #include "microhttpd-util.h"
 #include "module-util.h"
@@ -65,6 +66,7 @@ static int run(int argc, char **argv) {
         ASSERT_DLOPEN(dlopen_libblkid, HAVE_BLKID);
         ASSERT_DLOPEN(dlopen_libcrypt, HAVE_LIBCRYPT);
         ASSERT_DLOPEN(dlopen_libfido2, HAVE_LIBFIDO2);
+        ASSERT_OK(dlopen_libintl(LOG_DEBUG)); /* Required to be available at build time. */
         ASSERT_DLOPEN(dlopen_libkmod, HAVE_KMOD);
         ASSERT_DLOPEN(dlopen_libmount, HAVE_LIBMOUNT);
         ASSERT_DLOPEN(dlopen_libpam, HAVE_PAM);