Skip to content

fix(runtime, build): the dlopen surface is walked, and one unwinder per process (2026.9.10.1) - #598

Merged
Sunrisepeak merged 4 commits into
mainfrom
feat/dlopen-surface-closure-and-one-unwinder
Sep 9, 2026
Merged

fix(runtime, build): the dlopen surface is walked, and one unwinder per process (2026.9.10.1)#598
Sunrisepeak merged 4 commits into
mainfrom
feat/dlopen-surface-closure-and-one-unwinder

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

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_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: 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_surface reads each such library's own DT_NEEDED and resolves it against the search path the artifact actually carries:

state meaning reported
resolved nothing to say no
present as a dangling link the machine has no such driver no
absent packaging gap warning

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_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".

2. Two unwinders in one process

hide_static_cxx_runtime skipped 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:

libgcc_s _Unwind_* entry points           18
defined by the executable (libunwind.a)   10
still resolved in libgcc_s                 8   incl. _Unwind_GetIPInfo, _Unwind_GetCFA

libstdc++'s __gxx_personality_v0 therefore read an LLVM libunwind _Unwind_Context through libgcc's _Unwind_GetIPInfo, recovered a meaningless IP, found no landing pad, and __cxa_call_terminate ran past a handler three frames up. __verbose_terminate_handler rethrows to name the exception's type; that rethrow terminated too, which is why nothing 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, asserted in test_distribution.cpp.

Measured on one machine, same source:

error leg device leg
before exit 134, no output 12 24 36 48
after sycl: no usable device: … / device unavailable, exit 1 12 24 36 48

-Wl,--exclude-libs alone 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_90 and 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 and main prints device 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.
  • Full unit suite: 108 passed, 0 failed.
  • Docs updated in both languages (33-authoring-an-adapter, 42-heterogeneous-builds); style and structure checks pass.

@Sunrisepeak
Sunrisepeak force-pushed the feat/dlopen-surface-closure-and-one-unwinder branch from 3d49785 to 8f202a8 Compare September 9, 2026 16:25
@Sunrisepeak
Sunrisepeak force-pushed the feat/dlopen-surface-closure-and-one-unwinder branch 2 times, most recently from 933c255 to 094d0e9 Compare September 9, 2026 16:37
…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
Sunrisepeak force-pushed the feat/dlopen-surface-closure-and-one-unwinder branch from 094d0e9 to 4f4cff0 Compare September 9, 2026 17:08
…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.
@Sunrisepeak
Sunrisepeak merged commit 1046c22 into main Sep 9, 2026
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SYCL 示例在 NVIDIA 显卡上运行即崩溃:compat.sycl-runtime 缺 libnvidia-ml.so.1

2 participants