Embedded LLD Phase 7 Prerequisite: Native sanitizer linking (Linux) #5399
Replies: 2 comments
|
Update on the capture mechanism, now that it's implemented and verified. The original plan above captured the sanitizer link fragment for Clang only and left macOS, the BSDs, and GCC on the legacy path. That last one is a problem: the whole point of this work is to let Phase 7 delete the legacy Both compilers go through the same mechanism: at configure time, run the driver twice with Two wrinkles surfaced while making the diff work for both. The link line has to be tokenized respecting shell quoting, because Clang double-quotes every argument and GCC leaves most bare; If a future driver ever violates the supersequence assumption — a new non-deterministic token, or reordered arguments — the capture stops at configure time with a clear error rather than emitting a fragment it can't trust. Verified on this host with both compilers: full builds, a program linking through embedded LLD with the sanitizer runtime live (for GCC, Still scoped to native Linux. macOS, the BSDs, FreeBSD |
|
Implemented and verified (clang + gcc): PR #5409. |
Uh oh!
There was an error while loading. Please reload this page.
Implementation plan for Embedded LLD,
the prerequisite work for Phase 7 (Cleanup). Phase 7 removes the legacy
system()linker and the--linker/--link-ldcmdoptions. That removalcan't happen while live code still routes through the legacy path on its own,
without anyone asking for
--linker. ThePhase 7 scoping comment
identified two things that still do: native sanitizer builds, and FreeBSD
use=dtrace.This document covers the native sanitizer half, and Linux only. macOS
and the BSDs run the same legacy fallback for native sanitizer builds, but the
plan is to land Linux first, where the one sanitizer CI job actually runs and
the work can be verified end to end, then take what's learned there to macOS
and the BSDs as separate follow-ups. The FreeBSD
dtracequestion and thelegacy-code deletion itself are also separate follow-ups, not part of this
work.
Divergences
One behavior change from what exists today.
system()), so the driver can pass-fsanitize=…and pull in the sanitizer runtimelink_exe_lld_elf), with the sanitizer runtime linked explicitlyNon-sanitizer native Linux builds already link through embedded LLD and don't
change. Sanitizer cross-compilation, macOS, and the BSDs don't change either:
their guards stay in place.
The problem
link_exe(src/libponyc/codegen/genexe.cc:1945) dispatches to embedded LLD.The Linux branch reads:
In a non-sanitizer build the
#ifblock is absent, so native Linux alreadyuses embedded LLD. The guard only bites in a sanitizer build: there, a native
link falls through to the legacy compiler-driver path so the driver can pass
-fsanitize=….That flag isn't decoration. When ponyc is built with sanitizers, libponyrt's C
code is instrumented, so every executable ponyc links references
__asan_*(or the ubsan equivalents) and has to link the sanitizer runtime. The embedded
ELF path doesn't add that runtime today. Drop the guard without adding it and
every native sanitizer link fails with undefined
__asan_*symbols.PONY_SANITIZERis a build-time string ("address,undefined", "thread", and soon), set in
CMakeLists.txtfromuse=address_sanitizer/undefined_behavior_sanitizer/thread_sanitizer. The only sanitizer buildCI exercises is the weekly
with_sanitizersjob: x86-64 Linux glibc,address_sanitizer,undefined_behavior_sanitizer, native, debug and release.What the compiler driver emits
Verified with
clang -fsanitize=address,undefined -###on x86-64 Linux. Thedriver inserts, immediately before the object file:
and, after the object, the system libraries
-lrt -lresolvon top of the-lpthread -lm -ldlponyc already emits.undefinedalone resolves toubsan_standalone;threadtotsan; combiningundefinedwithaddress/threadfolds ubsan into the larger runtime, so there's no separateubsan_standalone. Every one of these flags is accepted by LLD's ELF driver.Approach: capture the sanitizer link fragment at build time
This follows the recommendation in section 9 of #4941: query the compiler at
build time for the runtime paths and embed them as compile-time constants.
PONY_SANITIZERis already a build-time decision, and a sanitizer-enabledponyc is a developer and CI artifact that runs on the machine that built it, so
build-machine-absolute paths in the binary are fine.
Asking the driver for the fragment, rather than resolving each library by name
with
--print-file-name, is the point. The driver owns the mapping from-fsanitize=Xto a set of libraries, the ubsan-folding rule, and thewhole-archive/dynamic-list wrapping. Resolving libraries by name means
re-implementing all of that in ponyc's build, and that hidden knowledge is
exactly what rots when a compiler version changes its sanitizer runtime layout.
Capture sidesteps it: whatever combination
PONY_SANITIZERnames, the driverreports the exact fragment, address and undefined and thread and any future
combination alike.
CMake
Done in pure CMake. Python is not a ponyc build dependency, and this work
doesn't make it one; CMake's list operations are enough.
-###emits a link line onlyfor a real input.
-marchthe native builduses: one with
-fsanitize=<value>, one without, as a baseline. Query the Ccompiler (the one that compiles and instruments libponyrt), so the resolved
runtime matches the instrumentation.
of the baseline (a set-difference confirms nothing is removed). A two-pointer
ordered scan emits each sanitized token that doesn't match the next
unconsumed baseline token: exactly what
-fsanitizeinjected, in driverorder, the whole-archive/dynamic-list block and the extra system libraries
both. No library names or wrapping flags are hardcoded. The implementation
confirms the driver doesn't reorder the non-sanitizer tokens (the scan
consumes every baseline token in order); if that ever failed, the fallback is
to extract the contiguous
clang_rtblock and append the glibc-standardsanitizer system libraries explicitly.
static const char* const[]plus its count, only inside the sanitizerbranch, and add its directory to libponyc's include path.
genexe.cc
Remove the
is_cross_compilingguard from the Linux dispatch branch. Splice thecaptured arguments into
link_exe_lld_elfimmediately before the object file,gated on
!is_cross_compiling(c). The gate matters becauselink_exe_lld_elfserves both the Linux and BSD branches, and the captured paths are
host-arch-absolute, so they must not reach a cross link. The four cases:
the fix.
host-arch paths in a cross link. Unexercised by CI and without a sanitizer
runtime today either, so behavior is preserved, not regressed.
native BSD sanitizer build still falls to legacy.
Linux.
The splice carries a comment recording that the function serves both branches,
why the gate is there, and the ordering. The whole-archived
clang_rtstaticarchives precede the object so their members are pulled in. The injected
-lrtand
-lresolvaren't re-emitted later by ponyc, so they appear only here,before the object, which is safe because they're shared libraries: DT_NEEDED
placement isn't position-sensitive the way static-archive symbol resolution is.
The header include is guarded to match the generated header's sanitizer-only
existence, which also keeps the array out of non-sanitizer translation units and
clear of the unused-variable warning under
-Werror.The legacy path keeps its own
-fsanitize=handling. Legacy stays reachablethrough
--linker, and a sanitizer build forced down that path still needs it.The flag goes dead only for the default native case, which is the whole point.
Verification
All on an x86-64 Linux host, mirroring the weekly
with_sanitizersjob.native sanitizer link must fail with undefined
__asan_*symbols. Confirmsthe splice is load-bearing.
(
use=pool_memalign,address_sanitizer,undefined_behavior_sanitizer), linkand run a small Pony program through it, confirm it links via embedded LLD
(verbose output shows
ld.lldwith theclang_rtarguments and nosystem()driver call) and that the sanitizer runtime is present. Pony codeisn't instrumented, only libponyrt's C is, so the live signal is the
runtime's presence and a fault triggered in instrumented runtime code, not a
Pony-level fault.
test-ci-coredebug and release under the sameASAN_OPTIONSandUBSAN_OPTIONSthe CI job uses; its full-program tests are the ones thatlink executables through
link_exe.No CI workflow change is needed;
with_sanitizersalready builds and tests, andwill now link through embedded LLD instead of the driver. The change is internal
build and linker plumbing with no user-facing difference, so there are no
release notes.
Out of scope, tracked as follow-ups
use=dtraceon embedded LLD.system()linker and the--linker/--link-ldcmdoptions (Phase 7 proper).
All reactions