Skip to content

ci(freebsd-amd64): flip libgc.a lane from XFAIL to blocking (etext/end fix validated) #108

ci(freebsd-amd64): flip libgc.a lane from XFAIL to blocking (etext/end fix validated)

ci(freebsd-amd64): flip libgc.a lane from XFAIL to blocking (etext/end fix validated) #108

Workflow file for this run

name: build and test (freebsd-amd64)
on:
push:
branches: [thirdparty-freebsd-amd64]
pull_request:
branches: [thirdparty-freebsd-amd64]
workflow_dispatch: {}
jobs:
build:
runs-on: ubuntu-latest
# `github.sha` and `github.event.repository.clone_url` are used
# uniformly across all 3 trigger event types, and correctly cover
# each:
# - pull_request: `github.sha` is GitHub's own synthesized MERGE
# commit (what the target branch would actually look like if
# this PR were merged right now), not the PR branch's own head -
# testing only the head tree in isolation could pass a PR whose
# OWN commit works fine, while the actual merge result (combined
# with whatever the target branch has since gained) is broken
# (Codex pullrequestreview-4781865117 on vlang/tccbin#75). That
# merge commit exists ONLY as an object in the base repo
# (vlang/tccbin, exposed via `refs/pull/<n>/merge`) - never in a
# contributor's fork - so this must clone from
# `github.event.repository.clone_url` (the base repo), not the
# fork's URL, regardless of where the PR originated.
# - push/workflow_dispatch: `github.sha`/`github.event.repository`
# are simply the pushed/dispatched commit and the repo it lives
# in - already exactly what should be tested.
# In every case, `github.sha` is fixed for this run's entire
# lifetime once the triggering event fires (a later push creates a
# brand new run with its own new `github.sha`, it doesn't mutate
# this one) - so this is exactly as race-safe against a
# mid-queue push as the earlier head.sha-based approach was (Codex
# pullrequestreview-4780048339 on vlang/tccbin#75), while also
# testing the actual merge result instead of the head tree alone.
# Set at job level (matching update_tccbin.yml's own working BSD
# job env, which only ever references github.*/secrets.*, never
# steps.* - step-level env on the "Start VM" step was tried first
# and empirically did NOT get forwarded into the VM by
# environment_variables, unlike this job-level placement).
env:
TCCBIN_CLONE_URL: ${{ github.event.repository.clone_url }}
TCCBIN_CLONE_SHA: ${{ github.sha }}
steps:
# cross-platform-actions/action boots a real FreeBSD VM inside this
# Linux runner - the same action vlang/v's own update_tccbin.yml
# uses to rebuild this branch's binaries. Cloning both repos
# directly inside the VM (rather than actions/checkout on the
# runner + sync_files) mirrors that proven-working pattern instead
# of relying on an unverified file-permission-preserving sync.
# Pinned to the v1.3.0 tag's commit (resolved via the GitHub API)
# rather than the mutable tag itself, so a retag upstream can't
# silently swap in different third-party code here without a
# review-visible diff (Codex pullrequestreview-4780048339 on
# vlang/tccbin#75).
- name: Start freebsd-amd64 VM
uses: cross-platform-actions/action@4347c661239bf5dcd0cd77708061488d907343d6 # v1.3.0
with:
operating_system: freebsd
version: '15.1'
memory: 4G
shell: sh
sync_files: runner-to-vm
environment_variables: TCCBIN_CLONE_URL TCCBIN_CLONE_SHA
- name: fetch shared conformance suite and this branch's binaries
shell: cpa.sh {0}
run: |
set -eu
sudo pkg install -y bash git
# Fetch the exact commit that triggered this run, not a branch
# name - `git clone --branch <name>` would resolve whatever
# that name points to *at clone time*, which can race a push
# that lands after this run started (Codex pullrequestreview-
# 4780048339 on vlang/tccbin#75).
mkdir -p thirdparty/tcc && cd thirdparty/tcc
git init -q
git remote add origin "$TCCBIN_CLONE_URL"
git fetch --depth=1 origin "$TCCBIN_CLONE_SHA"
git checkout -q FETCH_HEAD
cd ../..
# thirdparty/libgc/include (gc.h) and thirdparty/tccbin_tests
# (the shared cross-platform conformance suite) both live in
# the main v repo, not here. A plain `git clone --depth=1`
# only ever contains the CURRENT tip commit's objects - once
# vlang/v advances past this pinned SHA, that historical commit
# object won't exist in a fresh shallow clone and this checkout
# would fail (Codex P1, pullrequestreview-4780048339 on
# vlang/tccbin#75, line 60). Fetch that exact SHA directly
# instead of cloning HEAD and hoping it's still there.
mkdir -p vsrc && cd vsrc
git init -q
git remote add origin https://github.com/vlang/v.git
git sparse-checkout init --no-cone
git sparse-checkout set thirdparty/libgc thirdparty/tccbin_tests
git fetch --filter=blob:none --depth=1 origin c82d3f08271e9324d6fb8de3c251e9c0e1a9154b
git checkout -q FETCH_HEAD
cd ..
chmod +x thirdparty/tcc/tcc.exe
# HISTORY: tcc on FreeBSD used to be unable to link the bundled
# libgc.a at all - its linker only ever defined the glibc-style
# _etext/_edata/_end (see tccelf.c's tcc_add_linker_symbols), never
# the plain BSD-style etext/edata/end that BDWGC's FreeBSD
# data-segment-scanning code references directly, so any
# GC-dependent program failed with "undefined symbol 'etext'"/
# "'end'" (later builds reworded this to "unresolved reference
# to ..."). That is FIXED upstream as of tinycc 384614a, "tccelf:
# provide complete ELF boundary symbols", which gives the plain
# etext/edata/end names PROVIDE semantics on every ELF target.
#
# The GC lane below is therefore a normal BLOCKING lane now, not an
# XFAIL - see the comment above it.
#
# crash.c doesn't touch the GC at all, so it's checked directly
# (bypassing run.sh, which would otherwise apply the same GC
# flags to every test) as an independent, blocking regression check
# for this platform's tcc/libtcc1.a/crt pairing - it still catches a
# broken toolchain even if the GC lane were to fail for its own
# unrelated reasons.
- name: run shared conformance tests (crash only, no GC - blocking)
shell: cpa.sh {0}
run: |
set -eu
thirdparty/tcc/tcc.exe vsrc/thirdparty/tccbin_tests/shared/crash.c \
-o /tmp/crash_nogc
set +e
/tmp/crash_nogc
code=$?
set -e
# Any nonzero exit isn't enough: an exec-format error, a
# missing dynamic loader, or an unrelated miscompilation that
# just exit(1)s would also satisfy that and silently mask a
# real regression. crash.c's null-pointer dereference is
# expected to be killed by SIGSEGV specifically, which a
# POSIX shell reports as exit code 128+11=139 - verify that
# exact signal-terminated signature, not just "nonzero".
if [ "$code" -ne 139 ]; then
echo "expected exit 139 (killed by SIGSEGV) from an unguarded null-pointer dereference, got $code" >&2
exit 1
fi
echo "no-GC compile+crash check passed (killed by SIGSEGV as expected)"
# crash.c exiting 139 only proves SOME crash happened - if a
# broken libtcc1.a/crt or startup path made EVERY program
# segfault immediately (before ever reaching the intentional
# null-pointer dereference), this check would still see exit
# 139 and wrongly call it "expected" (Codex pullrequestreview-
# 4781468264 on vlang/tccbin#75). Keep this positive check
# independent of the GC lane below rather than relying on it:
# this lane's whole purpose is to stay meaningful on its own,
# covering the plain no-GC toolchain path that a GC-linked test
# never exercises. Compile and run a trivial, non-crashing
# program with the same no-GC flags, requiring it to actually
# complete and exit 0 - proving the toolchain can still produce
# a genuinely working binary, not just one that crashes
# regardless of source.
cat > /tmp/nogc_trivial.c <<'TRIVIALEOF'
int main(void) { return 0; }
TRIVIALEOF
thirdparty/tcc/tcc.exe /tmp/nogc_trivial.c -o /tmp/nogc_trivial
set +e
/tmp/nogc_trivial
trivial_code=$?
set -e
if [ "$trivial_code" -ne 0 ]; then
echo "expected the trivial no-GC program to exit 0, got $trivial_code - the no-GC toolchain path may be broken (e.g. crashing at startup) even though the crash.c check above happened to pass" >&2
exit 1
fi
echo "trivial no-GC success check passed (exit=0)"
# BLOCKING as of tinycc 384614a ("tccelf: provide complete ELF
# boundary symbols"), which is what this branch's tcc.exe must now
# be built from or later. This lane was previously a
# signature-checked XFAIL asserting that gc_alloc.c/hello.c MUST
# still fail on unresolved etext/end - correct while the bug was
# live, but it would hard-fail the moment a fixed tcc.exe is
# published here (its own "unexpected full pass" guard fired on
# exactly that), breaking CI for the opposite reason.
#
# This STRENGTHENS the gate rather than relaxing it: a full pass of
# all three shared tests is now the required baseline, where before
# only a very specific failure was accepted. Nothing here is
# downgraded to a warning, and no failure shape is tolerated.
#
# Verified on a real FreeBSD 15.1 VM before flipping: with this
# branch's EXACT libgc.a held byte-identical, the shipped tcc.exe
# (85ba3ae8, predating the fix) fails on unresolved etext/end and
# nothing else, while a tcc.exe rebuilt from tinycc 2be0218b (which
# contains 384614a) reports "3 passed, 0 failed". tcc.exe was the
# only variable between the two.
- name: run shared conformance tests (libgc.a - blocking)
shell: cpa.sh {0}
run: |
set -eu
bash vsrc/thirdparty/tccbin_tests/run.sh "$PWD/thirdparty/tcc/tcc.exe" freebsd -- \
-DGC_BUILTIN_ATOMIC=1 -DBUS_PAGE_FAULT=T_PAGEFLT -DALL_INTERIOR_POINTERS=1 \
-I "$PWD/vsrc/thirdparty/libgc/include" \
"$PWD/thirdparty/tcc/lib/libgc.a" \
-lpthread
# Positive proof of the MECHANISM, not just the outcome. The lane
# above passing means BDWGC linked, but it does not by itself prove
# WHY - a future tcc.exe could conceivably satisfy the suite while
# regressing the boundary symbols BDWGC's FreeBSD data-segment scan
# depends on, or provide them with a nonsensical range. Reference
# all three directly from C: if the linker fails to provide them,
# this simply does not link, which is precisely the original bug.
#
# Deliberately NOT an `nm` symbol-table check - that was tried while
# validating this flip and is the wrong instrument: tcc emits no
# full .symtab by default, and linker-PROVIDEd symbols need not
# appear in the output binary's symbol table at all, so `nm`
# reported all three "absent" for a binary that had just linked and
# run correctly.
- name: verify the linker provides etext/edata/end (blocking)
shell: cpa.sh {0}
if: ${{ !cancelled() }}
run: |
set -eu
cat > /tmp/boundary_probe.c <<'PROBEEOF'
#include <stdio.h>
/* The plain BSD-style names BDWGC's FreeBSD data-segment scan
references, and that tcc did not provide before 384614a. */
extern char etext[], edata[], end[];
int main(void) {
printf("etext=%p edata=%p end=%p\n",
(void *)etext, (void *)edata, (void *)end);
/* Sanity-check the advertised range, not merely the
symbols' existence: 384614a is specifically about
boundary symbols being finalized from the ordered
loadable sections, so provided-but-wrong is a distinct
failure mode from not-provided. */
if (!((void *)etext < (void *)end)) {
fprintf(stderr, "bad boundary range: etext is not below end\n");
return 1;
}
return 0;
}
PROBEEOF
thirdparty/tcc/tcc.exe /tmp/boundary_probe.c -o /tmp/boundary_probe
/tmp/boundary_probe
echo "confirmed: the linker provides etext/edata/end, and the advertised range is ordered sanely"