Repository navigation
macOS 27 arm64: bundled TCC links binaries with garbage in __bss (v up hangs, make panics) #29744
Description
Describe the bug
On macOS 27 (arm64), executables linked by the bundled TCC can start with garbage in __bss
instead of zeros. When the V compiler itself is TCC-linked (the default make / v self result
on Apple Silicon), its zero-initialized __globals are corrupted, so:
v uphangs forever (it is compiling thevuptool; the compiler spins in
flat.node_payload_lockbecauseg_node_payload_lockstarts as0xffffffff);- compiling a hello world panics with
V panic: fixed array index out of range (index: -1, len: 64)in
workers.register_open_pool(v3_open_pools_lenstarts as-1); makeon master ends with that same panic in thecmd/tools/detect_tcc.vstep.
Whether a given binary is hit depends only on its layout (see below), so it comes and goes
with unrelated source or flag changes. #28030 (empty main segfaults with -cc tcc on macOS
27.0, goes away with -g / -gc boehm, closed as not reproducible on macOS 26) very likely has
the same root cause; I could not confirm that exact case, because today's V no longer produces a
triggering layout for an empty main.
Reproduction Steps
On macOS 27, Apple Silicon, at 1b4ecb9 with the current bundled TCC:
make # ends with: V panic: fixed array index out of range (index: -1, len: 64)
./v run hello.v # same panic
./v -o t t.v # t.v: `import os` + main -> hangs forever
./v up # hangs (building the vup tool)A deterministic reproducer of the bad layout, independent of how the current ./v was built:
v -cc tcc -nocache -gc none -o v_tcc cmd/v produces a __DATA segment with
filesize 0x3ff0, vmsize 0x5438, and v_tcc then panics/hangs as above.
Expected Behavior
__bss is zero-filled; TCC-linked programs (and a TCC-linked V) work as on macOS 26.
Current Behavior
__bss pages after the end of __DATA's file contents hold the bytes that follow __DATA in the
file: the start of __LINKEDIT (the first words read back as a dyld_chained_fixups_header:
starts_offset 0x20, imports_offset 0xad8, symbols_offset 0xd68, imports_count 0xa4).
Possible Solution
Root cause is in TinyCC's Mach-O writer (tccmacho.c), not in V. With
CONFIG_NEW_MACHO, calc_fixup_size() and bind_rebase_import() compute each segment's
dyld_chained_starts_in_segment.page_count from vmsize, so for __DATA it also lists the
zero-fill __bss pages past the end of the segment's file contents. ld64 lists only the
file-backed pages. On macOS 27, dyld's page-in linking maps every listed page from the file.
So whenever __DATA's file contents end at or before a page boundary and __bss continues into
the next page, that page is mapped from the file (the following __LINKEDIT bytes) instead of
being zero-filled.
Evidence:
vmmapatprocess launch -s:__DATA 1014a0000-1014a4000 SM=COW(file) +
1014a4000-1014a6000 SM=NUL(zero fill). Atmain:__DATA 1014a0000-1014a6000 SM=COW+
mapped file 1014a6000-1014a8000, i.e. 2 pages remapped from the file, exactly the
page_count = 2thatotool -chained_fixupsshows for__DATA. A hardware watchpoint on the
corrupted global never fires: the page is remapped, not written.- Building that same
cmd/vwith TCC variants (same layout every time):
| TCC used | __bss section offset |
__DATA page_count |
hello world | import os |
|---|---|---|---|---|
bundled tcc.exe (tccbin 274abd2, tinycc 85ba3ae8) |
non-zero | 2 | panic | hang |
tinycc 85ba3ae8 built locally, unpatched |
non-zero | 2 | panic | hang |
only __bss offset set to 0 (like ld64) |
0 | 2 | panic | hang |
only page_count from filesize (patch below) |
non-zero | 1 | ok | ok |
So the non-zero file offset TCC gives __bss (ld64 writes 0) is harmless; only page_count
matters.
Patch (against tinycc mob 43c7708b, also applies to 85ba3ae8). It is not upstream yet:
diff --git a/tccmacho.c b/tccmacho.c
index cee09fd7..ecd467da 100644
--- a/tccmacho.c
+++ b/tccmacho.c
@@ -1272,6 +1272,15 @@ const struct {
#define N_SEGMENT (sizeof(all_segment)/sizeof(all_segment[0]))
#ifdef CONFIG_NEW_MACHO
+/* Only the pages of a segment that are backed by the file can hold fixups.
+ dyld's page-in linking maps every page listed in a segment's chained starts
+ from the file, so listing the zero-fill tail (__bss) as well would fill it
+ with the bytes that follow the segment in the file, instead of zeros. */
+static int fixup_page_count(struct segment_command_64 *seg)
+{
+ return (seg->filesize + SEG_PAGE_SIZE - 1) / SEG_PAGE_SIZE;
+}
+
static void calc_fixup_size(TCCState *s1, struct macho *mo)
{
int i, size;
@@ -1279,7 +1288,9 @@ static void calc_fixup_size(TCCState *s1, struct macho *mo)
size = (sizeof(struct dyld_chained_fixups_header) + 7) & -8;
size += (sizeof(struct dyld_chained_starts_in_image) + (mo->nseg - 1) * sizeof(uint32_t) + 7) & -8;
for (i = (s1->output_type == TCC_OUTPUT_EXE); i < mo->nseg - 1; i++) {
- int page_count = (get_segment(mo, i)->vmsize + SEG_PAGE_SIZE - 1) / SEG_PAGE_SIZE;
+ int page_count = fixup_page_count(get_segment(mo, i));
+ if (page_count == 0)
+ continue;
size += (sizeof(struct dyld_chained_starts_in_segment) + (page_count - 1) * sizeof(uint16_t) + 7) & -8;
}
size += mo->n_bind * sizeof (struct dyld_chained_import) + 1;
@@ -2059,10 +2070,13 @@ ST_FUNC void bind_rebase_import(TCCState *s1, struct macho *mo)
data += (size + 7) & -8;
image->seg_count = mo->nseg;
for (i = (s1->output_type == TCC_OUTPUT_EXE); i < mo->nseg - 1; i++) {
+ seg = get_segment(mo, i);
+ page_count = fixup_page_count(seg);
+ /* No file-backed pages, so no fixups: an offset of 0 says so. */
+ if (page_count == 0)
+ continue;
image->seg_info_offset[i] = (data - mo->chained_fixups->data) -
header->starts_offset;
- seg = get_segment(mo, i);
- page_count = (seg->vmsize + SEG_PAGE_SIZE - 1) / SEG_PAGE_SIZE;
size = sizeof(struct dyld_chained_starts_in_segment) +
(page_count - 1) * sizeof(uint16_t);
segment = (struct dyld_chained_starts_in_segment *) data;Verified locally on macOS 27.0.1 / arm64:
- tinycc's own
make test: "ALL TESTS PASSED", same as unpatched. - Unpatched V with the patched
tcc.exeswapped intothirdparty/tcc:makecompletes
(includingdetect_tcc) with a TCC-linked./vthat has the exact failing layout above but
page_count = 1. Building thevuptool (the original hang) takes about 1s, and
v test vlib/v/pref/ vlib/os/ vlib/v/driver/c_compiler_flags_test.vpasses. - Edge cases built with the patched TCC: a
-shareddylib loaded withdlopen(zero__bss,
correct rebased pointer), a__DATAwith only__bss(no starts entry,seg_offset 0),
__DATAending exactly on a page boundary;dyld_info -fixupsparses all of them and
codesign -vpasses.
Suggested path: get the patch into tinycc mob, then dispatch update_tccbin.yml for
macos-arm64 with the new tcc_commit (the monthly run would pick it up otherwise).
macos-amd64 uses the same writer code.
If an interim guard is wanted until the bundle is rebuilt, I have a V-side change ready that
stops V from selecting TCC by itself on Darwin >= 27 (driver default, the v self default on
Apple Silicon, and the GNUmakefile bootstrap), while still honoring an explicit -cc tcc.
Additional Information/Context
Workaround for affected users (verified on unpatched master): build and update with the
system compiler.
export VFLAGS='-cc cc'
make # produces a clang-linked ./v
v up # `v self` then also uses -cc ccV version
V 0.5.2 1b4ecb9
Environment details (OS name and version, etc.)
macOS 27.0.1 (26A434), Darwin 27.0.0, arm64. Bundled TCC:
tcc version 0.9.28rc 2026-07-24 HEAD@85ba3ae8 (AArch64 Darwin), tccbin
thirdparty-macos-arm64 at 1d0ad0e.
Note
You can use the 👍 reaction to increase the issue's priority for developers.
Please note that only the 👍 reaction to the issue itself counts as a vote.
Other reactions and those to comments will not be taken into account.
Activity
driver, tools: do not pick TCC implicitly on macOS 27 (#29745)
quaesitor-scientiam commentedon Oct 8, 2026
Plain C reproducer (no V involved)
Running a TCC-linked C program is enough to reproduce this. Two conditions have to hold:
__bsshas to spill past the first 16K page of__DATA.__DATAlies inside it. A bigsymbol table, and therefore a big
__LINKEDIT, does that.Small binaries don't trigger it, which is why a minimal C test looks fine. Run this from the V repo
root on macOS 27 / arm64.
CCdefaults to the bundled TCC:Results on macOS 27.0.1 (Darwin 27.0.0), arm64:
bss_big(~738K file)bss_small(~51K file)tcc.exe(tccbin1d0ad0e, tinycc85ba3ae8)__bssbytesmob43c7708b, unpatchedmob43c7708b+ the patch aboveIn both TCC builds
__DATAhasfilesize 0x70andvmsize 0x8070, andotool -chained_fixupsreports
page_count 3; with the patch it reportspage_count 1.bss_big:__DATAis at file offset0x4c000, so the 3 listed pages (0x4c000-0x58000) areinside the 738K file. They get mapped from the file, and
__bssreceives__LINKEDITbytes.bss_small:__DATAis at0x8000, so the listed pages (0x8000-0x14000) run past the endof the 51K file, and
__bssstays zero.So, refining the description above: dyld does not map every listed page in every binary. It looks
like page-in linking only happens when all listed pages are inside the file, and that otherwise
dyld falls back to applying the fixups itself. I inferred that from these two layouts; I haven't
checked it against dyld's source. Either way, the patch fixes both cases, and this gives tinycc a
test case that doesn't need V.
quaesitor-scientiam commentedon Oct 8, 2026
Alternative fix that needs no tinycc change: build tccbin with
--config-new_macho=noTinyCC can still write executables in the older Mach-O format (
LC_DYLD_INFO_ONLYrebase/bind opcodes) instead of chained fixups. With that format dyld applies the fixups itself
and never maps any
__DATApages from the file, so the badpage_countis never written at all.The tccbin build script asks for the new format explicitly, at
thirdparty/build_scripts/thirdparty-macos-arm64_tcc.shline 206:Changing that to
--config-new_macho=nofixes this with unmodified tinyccmob.update_tccbin.ymlruns that script from the vlang/v checkout, so the rebuilt bundle wouldstill come from a published repo.or.cz hash.
Tested on macOS 27.0.1 (Darwin 27.0.0), arm64, with tinycc
mob43c7708bconfigured with--config-new_macho=no --config-codesign:bss_big(C reproducer above)bss_small43c7708b,new_macho=yes(current setting)__bssbytes43c7708b,new_macho=noLC_DYLD_INFO_ONLYWith that
tcc.exe(and itslibtcc1.a,bt-*.o,bcheck.o,runmain.o) swapped intothirdparty/tccof current master:v -cc tcc -nocache -o v_tcc cmd/vbuilds a TCC-linked compiler, with both-gc noneand thedefault Boehm GC;
v_tcc versionworks.import osprogram, both with-cc tccandwith its default C compiler.
-cc tcc, and that second compiler works too.v_tcc -cc tcc -silent test vlib/strings/: 5 passed, 4 skipped.-gpanic prints the same output as with the current bundle; neither one prints a backtrace.Trade-offs:
tccmacho.cpatch above is still worth sendingupstream for other TCC users. This change only stops the bundled TCC from depending on it.
Possible path:
--config-new_macho=no.update_tccbin.ymlformacos-arm64(or wait for the monthly run).v uprunsmake latest_tccfirst), remove the Darwin >= 27 guardfrom driver, tools: do not pick TCC implicitly on macOS 27 #29745.
thirdparty-macos-amd64_tcc.shsets the same flag. I could only test arm64.thirdparty: build the macOS arm64 tcc with the older Mach-O fixup for…
thirdparty: add tccmacho page_count patch for macOS 27 bss (vlang/v#2…
Merge pull request #109 from quaesitor-scientiam/tccmacho-page-count-…
tcc: count only file-backed pages in chained-fixups page_count (#29744)…