fix(runtime, build): the dlopen surface is walked, and one unwinder per process (2026.9.10.1) - #598
Merged
Conversation
Sunrisepeak
force-pushed
the
feat/dlopen-surface-closure-and-one-unwinder
branch
from
September 9, 2026 16:25
3d49785 to
8f202a8
Compare
Sunrisepeak
force-pushed
the
feat/dlopen-surface-closure-and-one-unwinder
branch
2 times, most recently
from
September 9, 2026 16:37
933c255 to
094d0e9
Compare
…er process (2026.9.10.1) A SYCL project built cleanly and then terminated with exit code 134 and no exception text (#596). The trigger was in the ecosystem -- an adapter's farm was missing one driver library -- but the reasons it presented as a silent abort are two independent gaps in mcpp, and both are repaired here. THE SURFACE NOTHING WALKED. `resolve_runtime_closure` is seeded with the artifact and follows DT_NEEDED. A library a package publishes through `runtime.library_dirs` exists precisely because something will dlopen it, so no link edge names it and it is outside that closure BY CONSTRUCTION. Measured: a farm of twenty-five libraries, two of which could not load at all, while the build reported nothing -- because nothing had asked. `inspect_dlopen_surface` reads each such library's own DT_NEEDED and resolves it against the search path the artifact actually carries, separating three states: resolved, present as a dangling link (the machine has no driver), and absent (a packaging gap). Only the third is reported, and it is a warning: a dangling link is the documented shape of a host driver that is not installed, and failing there would turn a supported configuration into a failed build. `runtime.dlopen_surface` in resolution.json carries the findings and both denominators -- a surface that failed to build enumerates nothing, and "no findings" must not read like "nothing was examined". WHAT "THE SEARCH PATH THE ARTIFACT ACTUALLY CARRIES" TURNED OUT TO MEAN. Three corrections, all of them false positives, none visible until a project with a shared dependency was measured: * `$ORIGIN` leads every artifact's DT_RPATH and a shared dependency is deployed BESIDE the executable. `runtime_search_dirs` cannot carry that -- `$ORIGIN` is a property of each artifact, not of the plan -- so the artifacts' own directories are added in `check_dlopen_surface`. * A plan that produces no program has no surface to judge. The adapter package is `kind = "lib"`; reporting a consumer's surface against an archive's non-existent search path named a library the consumer resolves. * A SONAME is not a filename. mcpp links `bin/libopencl.so` whose SONAME is `libOpenCL.so.1`, and the alias appears later; `mcpp test` calls the check twice and only the second call saw it. The SONAMEs this build produces are read from the objects and passed in. TWO UNWINDERS IN ONE PROCESS. A lane whose device compiler is configured against libstdc++ puts libstdc++ on the link line while the artifact links libc++ statically. `hide_static_cxx_runtime` skipped executables on the premise that "ld exports only what a loaded object references, and mcpp passes no -rdynamic" -- a correct premise with a wrong conclusion, because a loaded libstdc++ DOES reference them. Measured: 89 exported symbols, 68 of them also defined by libstdc++ or libgcc_s. Ten of libgcc's eighteen `_Unwind_*` entry points came from the artifact and eight stayed in libgcc_s, including the accessors libstdc++'s personality routine calls. It read an LLVM libunwind context through libgcc's accessors, recovered a meaningless IP, found no landing pad, and `__cxa_call_terminate` ran past a handler three frames up; the verbose terminate handler's rethrow then terminated as well, which is why nothing was printed. Such a link now takes `--unwindlib=libgcc` and hides the static archives' symbols. libgcc_s is in the process either way -- libstdc++ needs it -- so this names a library rather than adding one, and the C++ runtime stays embedded. A link with no second runtime on it is byte-for-byte unchanged. Measured on one machine, same source, both situations: before exit 134, no output after `sycl: no usable device: ...` / `device unavailable`, exit 1 device `12 24 36 48` in both `-Wl,--exclude-libs` alone was written here as a prediction from the mechanism and then measured: exit 139 instead of 134. Hiding the exports makes libstdc++ bind to libgcc while the artifact's own libc++abi still calls its statically linked libunwind, so the mismatch reproduces in the other direction. The reason is recorded in the design record so the flag is not proposed again. The duplicate-symbol warning now states that consequence when the conflicting set includes the unwinder family, instead of describing it as one more copy that is never called. One documented claim was refuted along the way: the SYCL example states that a missing device image is the one failure its island cannot turn into a return code. Measured by compiling it for sm_90 and running on an sm_89 device -- it was not outside the catches; no catch worked. The example's comment and README are corrected. Design record: .agents/docs/2026-09-09-dlopen-surface-and-two-unwinders.md Ecosystem halves: openxlings/xim-pkgindex#796 #798, mcpplibs/mcpp-index#375 #377 #378
Sunrisepeak
force-pushed
the
feat/dlopen-surface-closure-and-one-unwinder
branch
from
September 9, 2026 17:08
094d0e9 to
4f4cff0
Compare
…ackage's images, and vague linkage is not a second provider Two defects that serving the SYCL runtime's OpenCL adapter made active. Both were latent, neither is caused by #596, and both were found by the checks this branch sharpened rather than by reading. A BUILD PROGRAM'S OBJECTS REACHED A DEPENDENCY'S IMAGE. `role = "object"` with no named target attaches to "every linked image", and that read as "every link unit in this plan" -- which includes the shared library a dependency contributes. Measured: `compat:opencl`'s ICD loader, a C library, came out of the link carrying `saxpy_device` and thirty-seven `sycl::` instantiations, 193 dynamic symbols where its own API is 154, and the process held two copies of the device island. `LinkUnit::dependencyOwned` now separates the images this package produces from the ones a dependency contributes. After: 0 sycl symbols, 154 exports, all of them its own `cl*` entry points. Latent until a SYCL project first had a shared dependency, which is what declaring `compat:opencl` did. VAGUE LINKAGE IS NOT A SECOND PROVIDER. `DynamicSymbol` recorded the symbol's TYPE and not its BINDING, so template instantiations, inline functions and vtables -- which the C++ ABI emits into every image and expects the loader to unify -- were counted as a second provider. `hide_static_cxx_runtime` had already written that rule in a comment; nothing enforced it one layer up. The binding is recorded now, weak definitions are counted rather than reported, and the count is printed for the reason every denominator in this area is printed. Both were invisible while the same artifact still had 68 real findings on top of them. One test moved from positional to designated initialisation: adding a field to `Conflict` bound the provider list to a bool -- a string literal converts to one, so it compiled and the list silently became empty.
Two cross-build jobs died in setup with
E: Failed to fetch https://dl.google.com/linux/chrome-stable/deb/.../Packages.gz Hash Sum mismatch
E: Some index files failed to download.
before a single byte was compiled. The runner image carries third-party apt
lists that these jobs never install from, and `apt-get update` fails the whole
run when any one of them is transiently inconsistent -- so a red that says
nothing about the change under test.
The lists a job has no use for are removed before the update. What remains is
Ubuntu's own, which is what `qemu-user-static`, `wine` and `build-essential`
come from.
Left alone: the two container-based jobs, whose images carry no third-party
lists, and the `|| { ... }` fallback beside the wine dpkg path, which already
continues past a failed update.
The first attempt named `google-chrome.list`, and `apt-get update` failed on the same URL: on ubuntu-24.04 the runner writes deb822 `.sources` files, so the filename was a guess and the guess was wrong. Selected by what the file CONTAINS now. That is the same correction `libs/hostlib.lua` records for library directories: a layout you assume is a layout you are wrong about on some machine.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #596.
A SYCL project built cleanly and terminated with exit code 134 and no exception text. The trigger was in the ecosystem — an adapter's farm was missing one driver library, repaired in openxlings/xim-pkgindex#796 and mcpplibs/mcpp-index#375 — but the reasons it presented as a silent abort are two independent gaps in mcpp.
Design record:
.agents/docs/2026-09-09-dlopen-surface-and-two-unwinders.md.1. The surface nothing walked
resolve_runtime_closureis seeded with the artifact and followsDT_NEEDED. A library a package publishes throughruntime.library_dirsexists precisely because something willdlopenit, so no link edge names it: it is outside that closure by construction, not by oversight. Measured — a farm of twenty-five libraries, two of which could not load at all, with no build diagnostic, because nothing had asked.inspect_dlopen_surfacereads each such library's ownDT_NEEDEDand resolves it against the search path the artifact actually carries:Advisory, never blocking: a dangling link is the documented self-heal shape of a host driver that is not installed, and failing there would turn a supported CPU-only configuration into a failed build.
runtime.dlopen_surfaceinresolution.jsoncarries the findings and both denominators. A surface that failed to build enumerates nothing, and "no findings" must not read like "nothing was examined".2. Two unwinders in one process
hide_static_cxx_runtimeskipped executables on the premise that "an executable's static libstdc++ is already local (ld exports only what a loaded object references, and mcpp passes no-rdynamic)". The premise is correct and the conclusion is not: when a loaded object does reference them, the linker puts them in.dynsym. Measured on the SYCL artifact — 89 exported symbols, 68 also defined by libstdc++ or libgcc_s.A static archive contributes only the members something references, so the interposition is partial by construction:
libstdc++'s
__gxx_personality_v0therefore read an LLVM libunwind_Unwind_Contextthrough libgcc's_Unwind_GetIPInfo, recovered a meaningless IP, found no landing pad, and__cxa_call_terminateran past a handler three frames up.__verbose_terminate_handlerrethrows to name the exception's type; that rethrow terminated too, which is why nothing printed.Such a link now takes
--unwindlib=libgccand hides the static archives' symbols. libgcc_s is in the process either way — libstdc++ needs it — so this names a library rather than adding one, and the C++ runtime stays embedded. A link with no second runtime on it is byte-for-byte unchanged, asserted intest_distribution.cpp.Measured on one machine, same source:
12 24 36 48sycl: no usable device: …/device unavailable, exit 112 24 36 48-Wl,--exclude-libsalone is a regression, not a repair — written here as a prediction from the mechanism and then measured: exit 139 instead of 134. Hiding the exports makes libstdc++ bind to libgcc while the artifact's own libc++abi still calls its statically linked libunwind, so the mismatch reproduces in the other direction. The reason is recorded so the flag is not proposed again.3. The warning says what it means
The duplicate-symbol check already detected the condition and described the impact as "the library's own copy is never called". For the
_Unwind_*family the impact is that exception handling does not work. It now says so, and only when that family is in the conflicting set.A documented claim, refuted
The SYCL example states that a missing device image is the one failure its island cannot turn into a return code. Compiled for
sm_90and run on an sm_89 device: with two unwinders,terminate called after throwing an instance of 'ur_result_t', exit 134; with one, the island's handler prints the build log andmainprintsdevice unavailable, exit 1. It was not outside the catches — no catch worked. The comment and the README are corrected.Tests
test_elf_runtime.cpp: three states plus both denominators; and one library counted once when two names link to it.test_distribution.cpp: the mechanism table's new cell asserted byte-for-byte, plus the unchanged cells beside it.33-authoring-an-adapter,42-heterogeneous-builds); style and structure checks pass.