Hi,

On macOS, the setlocale replacement in libintl (and in gnulib's
setlocale module) calls CFLocaleCopyPreferredLanguages() every time
setlocale (LC_ALL, "") falls back for LC_MESSAGES. Programs that fork()
without exec() and change the locale again in the child then call
CoreFoundation in the child. Apple's fork(2) says frameworks are unsafe
there, and with bash it intermittently crashes with SIGSEGV. Shells do
this all the time: bash restores the locale with setlocale (LC_ALL, "")
after `LANG=C some_builtin` in a subshell.

It happens when all of these hold:
- AppleLocale is a name that libc rejects. This is the case when the
  Region in System Settings is not the language's default region:
  English + Finland gives "en_US@rg=fizzzz", English + Germany gives
  "en_DE" (see bash bug https://savannah.gnu.org/bugs/?68453).
- LANG and LC_* are unset.
- setlocale (LC_ALL, "") runs in a fork()ed child.

setlocale_single (LC_MESSAGES, "en_US@rg=fizzzz") then fails, and the
fallback "Take the primary language preference" calls
CFLocaleCopyPreferredLanguages(). The AppleLocale name and the
language list in langprefs.c are cached, but this result is not, so it
is looked up again on every call, including in the child. It is in
gettext-runtime/intl/setlocale.c (line 1514) and in gnulib
lib/setlocale.c (line 1515) in current git.

Crash stack, the same in all 24 crash reports I have (macOS 27.2,
build 26B123, Apple M3 Max):

  libsystem_trace  _os_log_preferences_refresh  <- EXC_BAD_ACCESS
  libsystem_trace  os_log_type_enabled
  CoreFoundation   -[CFPrefsSearchListSource alreadylocked_copyValueForKey:]
  ...
  CoreFoundation   CFLocaleCopyPreferredLanguages
  bash             libintl_setlocale
  bash             reset_locale_vars
  bash             sv_locale
  bash             dispose_temporary_env

The crash reports also say "*** single-threaded process forked ***"
(CoreFoundation) and "crashed on child side of fork pre-exec"
(libsystem_c). The faulting access is a byte read in an unmapped gap
next to read-only shared memory regions, so it looks like os_log keeps
a pointer to memory that the child did not inherit. It is
intermittent because os_log refreshes its preferences only now and
then: in one process, 5000 restores called
CFLocaleCopyPreferredLanguages 5004 times but
_os_log_preferences_refresh only 7 times.

Reproducer, with any bash that uses libintl on macOS (for example
Homebrew's); the loop is from https://github.com/NixOS/nixpkgs/pull/565511:

  LOOP='find /usr/bin -maxdepth 1 -type f -print0 | head -c 4000 |
    while IFS= read -r -d "" f; do
      LANG=C read -r -n 4 -u 3 m 3<"$f" 2>/dev/null
    done'
  for i in $(seq 1 200); do
    env -i HOME=/homeless-shelter PATH=/usr/bin:/bin bash -c "$LOOP"
  done 2>&1 | grep -c 'Segmentation fault'

Crashes in 200 runs:
- bash 5.3.20 linked against gettext 1.0's libintl.8.dylib: 15 to 23
- bash 5.3.20 with its bundled libintl (from gettext 0.21.1): 28 to 36
- bash devel, whose lib/intl/setlocale.c is the one from gettext 1.0: 28

I could not make a small C program crash that only calls
setlocale (LC_ALL, "") in fork()ed children, so the reproducer needs
bash.

Suggested fix: cache the result of that lookup, as langprefs.c does. The
first setlocale (LC_ALL, "") normally happens at program start, before
any fork(), so children would reuse the cached value. The attached patch
against current git does that. I tested the same change on gettext 1.0:
bash linked against the patched libintl had 0 crashes in 600 runs,
CoreFoundation was no longer called when the locale was restored,
LC_MESSAGES still ends up as "en", and msgfmt's German and Finnish
messages are unchanged. The patch is meant as an example of how this
could be fixed.

One limitation of a lazy cache: it does not help if the first lookup
happens in a child, for example when the program starts with LANG set
and a child later unsets it.

AI disclosure: Claude Opus 5.5 has been used in the process.

Thanks,
Mika Tammi

Attachment: 0001-intl-Don-t-call-CoreFoundation-in-fork-ed-children-o.patch
Description: Binary data

Reply via email to