Bug 296410 - sprintf/vfprintf not thread safe on AArch64 due to localeconv_l using atomic_store_int, with relaxed semantics
Summary: sprintf/vfprintf not thread safe on AArch64 due to localeconv_l using atomic_...
Status: Closed FIXED
Alias: None
Product: Base System
Classification: Unclassified
Component: threads (show other bugs)
Version: 14.4-RELEASE
Hardware: arm64 Any
: --- Affects Some People
Assignee: Konstantin Belousov
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2026-06-30 16:59 UTC by Tomas Vondra
Modified: 2026-07-15 16:33 UTC (History)
2 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Tomas Vondra 2026-06-30 16:59:38 UTC
While running tests (for ECPG in Postgres), we're getting occasional segfaults in tests using threads. This seems to affect only the AArch64 machine (which is an rpi5), while the two x86_64 machines don't seem to have this issue.

The crashes generally look like this:

------
Program terminated with signal SIGSEGV, Segmentation fault.
Address not mapped to object.
#0  __vfprintf (fp=fp@entry=0x889dab00, locale=locale@entry=0x841dfcf0
<__xlocale_global_locale>, serrno=0, fmt0=fmt0@entry=0x20087a
"Connection: %d", ap=...) at /usr/src/lib/libc/stdio/vfprintf.c:477

warning: 477	/usr/src/lib/libc/stdio/vfprintf.c: No such file or directory
[Current thread is 1 (LWP 933707)]
(gdb) bt
#0  __vfprintf (fp=fp@entry=0x889dab00, locale=locale@entry=0x841dfcf0
<__xlocale_global_locale>, serrno=0, fmt0=fmt0@entry=0x20087a
"Connection: %d", ap=...) at /usr/src/lib/libc/stdio/vfprintf.c:477
#1  0x000000008411d434 in vsprintf_l (locale=0x841dfcf0
<__xlocale_global_locale>, ap=..., str=<optimized out>, fmt=<optimized
out>) at /usr/src/lib/libc/stdio/vsprintf.c:61
#2  vsprintf (str=0x889daefc "", fmt=<optimized out>, ap=...) at
/usr/src/lib/libc/stdio/vsprintf.c:68
#3  0x00000000841123f4 in sprintf (str=0x0, str@entry=0x889daefc "",
fmt=0x841dfcf0 <__xlocale_global_locale> "") at
/usr/src/lib/libc/stdio/sprintf.c:56
#4  0x0000000000210ed8 in fn (arg=0x0) at prep.pgc:39
#5  0x00000000847844fc in thread_start (curthread=0x63044546d010) at
/usr/src/lib/libthr/thread/thr_create.c:291
#6  0x0000000000000000 in ?? ()
------

That is, it's a crash in vfprintf, on this line:

decpt_len = (decimal_point[1] == '\0' ? 1 : strlen(decimal_point));

After investigating a bit, I believe this is due to localeconv_l using atomics in relaxed mode when updating the decimal_point:

    if (atomic_load_acq_int(&loc->numeric_locale_changed) != 0) {
        ...
        N_ASSIGN_STR(decimal_point);
        ...
        atomic_store_int(&loc->numeric_locale_changed, 0);
    }

On architectures with weakly-ordered memory model (like the AArch64), this means the other threads may see the update to numeric_locale_changed, without seeing the updated decimal_point pointer. Which leads to the crash.

This can be reproduced by a simple program:

------
#include <stdio.h>
#include <pthread.h>

void *fn(void *arg) {
    char buf[128];
    for(int i=0; i<10000; i++)
        sprintf(buf, "%d", 1);
    return NULL;
}

int main() {
    pthread_t th[100];

    for(int i=0; i<100; i++)
        pthread_create(&th[i], NULL, fn, NULL);

    for(int i=0; i<100; i++)
        pthread_join(th[i], NULL);

    return 0;
}
------

It's a race condition, so it may not crash. But for me it usually takes only ~10-20 tries to crash with a segfault.

I believe the code should use atomic_store_rel_int instead, to get the proper memory ordering guarantees. Not just for decimal_point, but for the other fields too. (I haven't looked at other places using the relaxed atomics variants, but there might be more similar bugs there.)

In practice, this race condition may be fairly unlikely to hit - the window is fairly narrow. And it's probably enough for the "main" process to do a single sprintf (which initializes the cache) before starting the threads. Which our tests don't do, but bigger apps probably do. Adding such sprintf call to the reproducer seems to fix it.
Comment 1 Konstantin Belousov freebsd_committer freebsd_triage 2026-06-30 23:01:16 UTC
It is easy to change to store_rel in lib/libc/locale/localeconv.c, but is it
enough?

Suppose that we did not observed numeric_locale_changed != 0, but did observed
new values from some loaded fields.  Could this inconsistency cause issues?

If yes, we need something like seqlocks, as used by timecounters and
filedescriptors.
Comment 2 Tomas Vondra 2026-07-01 09:18:36 UTC
(In reply to Konstantin Belousov from comment #1)

I'm not very familiar with how the locales work, but the way I understood localeconv_l is that it rebuilds the cached fields after a change (or on first access). And I thought maybe it's harmless if two threads rebuilt it concurrently, or something like that.

But maybe you're right and after a locale change a thread could see a mix of old and new values for a given locale? I guess that'd mean using atomics for protecting other fields (as implemented here) is not quite sufficient.
Comment 3 Konstantin Belousov freebsd_committer freebsd_triage 2026-07-05 03:18:37 UTC
(In reply to Tomas Vondra from comment #2)
I am not sure.
For instance one thread might have noticed the change in the locale and started the
rebuild, while other did not and load the partially updated data.

Anyway, I think that the change to use the release semantic is fine on its own.
Comment 4 commit-hook freebsd_committer freebsd_triage 2026-07-05 03:19:21 UTC
A commit in branch main references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=4efbcf36a0d49ab142023a767871532f515f1381

commit 4efbcf36a0d49ab142023a767871532f515f1381
Author:     Konstantin Belousov <kib@FreeBSD.org>
AuthorDate: 2026-07-05 02:50:27 +0000
Commit:     Konstantin Belousov <kib@FreeBSD.org>
CommitDate: 2026-07-05 03:18:50 +0000

    libc locale/localeconv.c: use release semantic when clearing locale_changed

    PR:     296410
    Submitted by:   Tomas Vondra <tomas@vondra.me>
    MFC after:      1 week

 lib/libc/locale/localeconv.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Comment 5 commit-hook freebsd_committer freebsd_triage 2026-07-12 08:35:57 UTC
A commit in branch stable/15 references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=b34fd6017a572b3f62885cc8976df7c2449834cf

commit b34fd6017a572b3f62885cc8976df7c2449834cf
Author:     Konstantin Belousov <kib@FreeBSD.org>
AuthorDate: 2026-07-05 02:50:27 +0000
Commit:     Konstantin Belousov <kib@FreeBSD.org>
CommitDate: 2026-07-12 08:34:45 +0000

    libc locale/localeconv.c: use release semantic when clearing locale_changed

    PR:     296410

    (cherry picked from commit 4efbcf36a0d49ab142023a767871532f515f1381)

 lib/libc/locale/localeconv.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Comment 6 Tomas Vondra 2026-07-15 13:22:00 UTC
Do I understand correctly the fix went only to main, and not to the stable (14 and 15) branches? If that's the case, we'll probably need to apply some sort of workaround on our side, to prevent the failures on the older releases.
Comment 7 Tomas Vondra 2026-07-15 13:24:37 UTC
(In reply to Tomas Vondra from comment #6)

Ah, damn. Now I noticed the second commit in stable/15. Still, my test machine is running 14.4, so that may still need the workaround.
Comment 8 commit-hook freebsd_committer freebsd_triage 2026-07-15 13:37:25 UTC
A commit in branch stable/14 references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=00e880819cc5b8249222792ec82c45444a50eb2d

commit 00e880819cc5b8249222792ec82c45444a50eb2d
Author:     Konstantin Belousov <kib@FreeBSD.org>
AuthorDate: 2026-07-05 02:50:27 +0000
Commit:     Konstantin Belousov <kib@FreeBSD.org>
CommitDate: 2026-07-15 13:36:50 +0000

    libc locale/localeconv.c: use release semantic when clearing locale_changed

    PR:     296410

    (cherry picked from commit 4efbcf36a0d49ab142023a767871532f515f1381)

 lib/libc/locale/localeconv.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Comment 9 Mark Linimon freebsd_committer freebsd_triage 2026-07-15 16:33:38 UTC
^Triage: committed and (partially) MFCed.