https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126723
Bug ID: 126723
Summary: [16/17 Regression] aarch64-darwin: temporary L.str
labels miscompile literals after a weak_definition
Product: gcc
Version: 16.1.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: c++
Assignee: unassigned at gcc dot gnu.org
Reporter: skarnproject at gmail dot com
Target Milestone: ---
Created attachment 65271
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65271&action=edit
example
On aarch64-apple-darwin, string constants are emitted with assembler-temporary
("L"-prefixed) labels. Under .subsections_via_symbols a temporary label does
not
open a Mach-O atom, so the literal is absorbed into the atom of the preceding
real
symbol. When that symbol is a .weak_definition, ld64 keeps one TU's copy and
discards the others, and every literal that rode along resolves into the
surviving
atom at the same offset.
The result is silent wrong-code : correct-looking source reads the wrong bytes,
with
no diagnostic at any stage.
THIS CONTRADICTS THE DARWIN PORT'S OWN DOCUMENTED RULE
gcc/config/darwin.h (~line 999) deliberately makes constant labels
linker-visible,
and states why:
/* Make local constant labels linker-visible, so that if one follows a
weak_global constant, ld64 will be able to separate the atoms. */
#undef ASM_GENERATE_INTERNAL_LABEL
#define ASM_GENERATE_INTERNAL_LABEL(LABEL,PREFIX,NUM) \
do { \
if (strcmp ("LC", PREFIX) == 0) \
sprintf (LABEL, "*%s%ld", "lC", (long)(NUM)); \
But gcc/config/darwin.cc, darwin_encode_section_info (~line 1314 on master),
takes
that already-safe "lC" name and renames it back to uppercase for string
constants
unless ASan is enabled:
gcc_checking_assert (strncmp ("*lC", name, 3) == 0);
char *buf;
if (is_str)
{
bool for_asan = (flag_sanitize & SANITIZE_ADDRESS)
&& asan_protect_global (const_cast<tree> (decl));
/* When we are generating code for sanitized strings, the string
internal symbols are made visible in the object. */
buf = xasprintf ("*%c.str.%s", for_asan ? 'l' : 'L', &name[3]);
The assert confirms the incoming label is the protected lowercase form; the
next
line discards that protection for every non-ASan build. The same reasoning is
already recorded in machopic_indirection_name for PR 71767 ("ld64 will be
unable to
split this into two atoms ... legitimate direct accesses to the second symbol
will
appear to be direct accesses to an atom of type weak").
Note the polarity, which is the reverse of what the introducing commit's title
suggests: for_asan selects the SAFE lowercase 'l'; the ": 'L'" fallback is the
broken one. Ordinary builds are affected and an ASan build is accidentally
correct:
$ g++-16 -std=c++26 -O2 -S t.cpp -> L.str.0:
(temporary, unsafe)
$ g++-16 -std=c++26 -O2 -fsanitize=address -S t.cpp -> l.str.0:
(linker-private, safe)
This appears to originate in r16-2939-g4db9571488eb ("Darwin: Handle string
constants specially when asan is enabled"). Before that, string constants kept
the
lC<N> name. I have not run a GCC 15 build to confirm it is unaffected -- only
read
the release-branch sources, which contain no equivalent block.
EXPOSURE
Only literals in regular sections are at risk. __TEXT,__cstring is a literal
section (ld64 coalesces it by content and never atomizes it by symbol), so
literals
landing there are fine. The damage is done by literals GCC emits into
__TEXT,__const, which is atomized by symbol -- in practice non-NUL-terminated
char
arrays, including libstdc++'s own to_chars tables.
REAL-WORLD EVIDENCE
A TU including <format> alongside a large header set emits (-O2 -S, GCC 16.1.0,
aarch64-apple-darwin25):
.globl __ZNSt9__unicode9__v16_0_014__xpicto_edgesE
.weak_definition __ZNSt9__unicode9__v16_0_014__xpicto_edgesE
.const
__ZNSt9__unicode9__v16_0_014__xpicto_edgesE:
...
.const
.align 3
L.str.31:
.ascii "0123456789abcdef" ; to_chars digit table
...
.const
.align 3
L.str.32:
.ascii "0123456789abcdefghijklmnopqrstuvwxyz"
Assembling that TU confirms the literals are folded away and addressed through
the
weak symbol:
$ nm -m t.o | grep ' L\.str' | grep -c __const
0 # no literal survives as its own atom
$ otool -rv t.o | grep -c __xpicto_edges
6 # accesses encoded as __xpicto_edges + offset
Once a second TU defines __xpicto_edges and wins coalescing, those relocations
point
into the wrong atom.
OBSERVED SYMPTOM
In a C++26 codebase (heavy -freflection use, which places many weak blobs in
__TEXT,__const), std::format("{:016x}", value) returned NUL-riddled hex,
because
libstdc++'s "0123456789abcdef" table had shifted relative to the weak symbol it
was
anchored to. This corrupted downstream file parsing and was slow to trace,
since
nothing near the failure site was wrong. Small test binaries linked correctly
--
the outcome depends on which TU wins coalescing in a given link.
MINIMAL REPRODUCER
Hand-written to model exactly what GCC emits above (identical weak symbol in
both
TUs, each followed by a temporary-labelled literal), so it is deterministic.
a.s:
.section __TEXT,__const
.globl _wk
.weak_definition _wk
_wk:
.ascii "WWWW"
L.str.900:
.ascii "0123456789abcdef"
.text
.align 2
.globl _get_a
_get_a:
adrp x0, L.str.900@PAGE
add x0, x0, L.str.900@PAGEOFF
ret
.subsections_via_symbols
b.s: identical, but _get_b / L.str.901 / .ascii "ZZZZZZZZZZZZZZZZ"
m.c:
#include <stdio.h>
const char *get_a(void); const char *get_b(void);
int main(void){ printf("a=[%.16s]\nb=[%.16s]\n", get_a(), get_b()); }
$ as -arch arm64 -o a.o a.s && as -arch arm64 -o b.o b.s && cc -o t m.c a.o
b.o && ./t
a=[0123456789abcdef]
b=[0123456789abcdef] <-- expected ZZZZZZZZZZZZZZZZ
b's literal was discarded along with b.o's copy of _wk, and its reference --
encoded
as _wk + 4 -- resolves into a's surviving atom, so b silently returns a's
string.
Renaming both labels to lowercase l.str.* fixes it, and they are still stripped
from
the final image:
$ sed -i '' 's/L\.str\./l.str./g' a.s b.s
$ as -arch arm64 -o a.o a.s && as -arch arm64 -o b.o b.s && cc -o t m.c a.o
b.o && ./t
a=[0123456789abcdef]
b=[ZZZZZZZZZZZZZZZZ]
$ nm t | grep -c 'l\.str'
0
SUGGESTED FIX
Drop the conditional so string constants always use the linker-private prefix,
matching what ASM_GENERATE_INTERNAL_LABEL already does for lC and what the
darwin.h
comment requires:
buf = xasprintf ("*l.str.%s", &name[3]);
ASan already takes that branch today, so this only restores the non-ASan path.
WORKAROUND
For anyone hitting this before a fix lands: an "as" wrapper on -B that rewrites
L.str.* to l.str.* in the assembly is sufficient. It has to be that narrow --
-Wa,-L (make all temporary labels real) also promotes local branch targets, and
aarch64 cannot relocate a conditional branch against an external symbol, so
ordinary
code stops assembling.
VERSION
g++-16 (Homebrew GCC 16.1.0) 16.1.0
Target: aarch64-apple-darwin25
Apple clang version 17.0.0 (clang-1700.6.3.2) -- /usr/bin/as, ld-1200+
macOS 26.2 (Darwin 25.2.0), Apple M3 Pro