Skip to content

dlopen: expose the main program symbol scope - #879

Closed
brandonpayton wants to merge 1 commit into
mainfrom
fix/dlopen-null-main-handle
Closed

dlopen: expose the main program symbol scope#879
brandonpayton wants to merge 1 commit into
mainfrom
fix/dlopen-null-main-handle

Conversation

@brandonpayton

@brandonpayton brandonpayton commented Jul 11, 2026

Copy link
Copy Markdown
Member

Why

Dynamic loading lets a running program open a shared module and look up symbols by name. GNU Make uses this for plugins. dlopen(NULL) represents the main executable, while RTLD_DEFAULT asks the loader to search the program's default symbol scope.

GNU Make and other plugin hosts use dlopen(NULL, ...) and dlsym(RTLD_DEFAULT, ...) to expose executable symbols to loaded modules. Kandelo's libc glue hard-coded dlopen(NULL) as unsupported even though the shared host runtime already records the main Wasm instance's exported function and data symbols.

This is the platform root cause behind GNU Make needing --disable-load; leaving that configure switch in a package formula would hide a Kandelo dynamic-linking gap.

What changed

  • Reserve a stable opaque handle for the main program without changing the existing __wasm_dlopen import signature.
  • Resolve exported functions to indirect-table indices and exported data globals to linear-memory addresses for both the main handle and RTLD_DEFAULT.
  • Keep side-module handles disjoint and preserve deterministic fork replay ordering.
  • Exercise the same compiled C fixture through Node and Chromium, including function and data lookup.
  • Document --export-dynamic, default symbol visibility, and the remaining RTLD_NEXT limitation.

Host parity

host/src/worker-main.ts is the shared Node/browser process worker path. The new fixture passes through CentralizedKernelHost on Node and BrowserKernel in Chromium; neither host has a separate compatibility path.

ABI and artifacts

This is a backward-compatible semantic addition over the existing host import signature. No channel layout, import signature, kernel export, generated binding, or snapshot entry changed, and existing binaries remain valid. ABI_VERSION therefore remains 18 under the additive-within-an-epoch policy.

No package archives, indexes, VFS images, or committed Wasm artifacts are changed. Programs that use the new behavior must be rebuilt with the current libc glue and run with the current host, as with other additive changes in an ABI epoch.

Validation

On the rebased ABI 18 head:

  • ./scripts/dev-shell.sh bash scripts/build-musl.sh
  • ./scripts/dev-shell.sh bash -c 'cargo build --release -p kandelo -Z build-std=core,alloc'
  • ./scripts/dev-shell.sh npm --prefix host test -- --run test/dylink.test.ts test/dlopen-e2e.test.ts test/fork-dlopen-replay-e2e.test.ts (22 tests passed after installing the current kernel; the first attempt failed only because local-binaries/kernel.wasm was absent)
  • ./scripts/dev-shell.sh npm --prefix host run typecheck
  • ./scripts/dev-shell.sh bash scripts/check-abi-version.sh
  • git diff --check origin/main...HEAD

The same Chromium fixture passed on the pre-rebase semantic head. On the rebased head, local fetch-only setup correctly rejected the released node archive because rebuilding libc changes the package cache key. I did not bypass that check or source-build this incidental browser-demo dependency; hosted staging must rebuild the affected package graph and rerun the Chromium suite on this exact head.

I did not run the full host, browser, libc-test, POSIX, or Sortix suites locally. libc-test explicitly skips its DSO/dlopen .mk cases, while the added end-to-end fixture directly covers this glue and host path.

Remaining boundary

RTLD_NEXT lookup remains unsupported and is documented as such.

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Phase B-1 matrix build status — pr-879-staging

ABI v18. 69 built, 0 failed, 69 total.

Package Arch Status Sha
libcurl wasm32 built a7ec8384
libcxx wasm32 built 09c06607
libcxx wasm64 built d5709528
libpng wasm32 built 47444054
libxml2 wasm32 built 993a7a2c
libxml2 wasm64 built c8f4e029
openssl wasm32 built d2fc84e8
openssl wasm64 built 985da291
sqlite wasm32 built 6b0137a9
sqlite wasm64 built 4825f6a1
zlib wasm32 built e9fdd5b1
zlib wasm64 built 1c77cf7b
bc wasm32 built 85ddd078
bzip2 wasm32 built fc8c7fb4
coreutils wasm32 built a8e4d1f7
curl wasm32 built e5be2304
dash wasm32 built 31be600e
diffutils wasm32 built ec140b77
dinit wasm32 built 88603967
fbdoom wasm32 built 95fede2e
file wasm32 built d652f794
findutils wasm32 built a5fd70de
gawk wasm32 built 977c32f0
git wasm32 built 50e7bf19
grep wasm32 built a7e0cfd5
gzip wasm32 built c7e2e85f
hello wasm32 built 65e2176c
kandelo-sdk wasm32 built c6ff9328
kernel wasm32 built 4844b391
less wasm32 built c5939334
lsof wasm32 built 2f1c6304
m4 wasm32 built f552cabe
make wasm32 built 26155b53
mariadb wasm32 built f861a897
mariadb wasm64 built 3c4aa9de
modeset wasm32 built 7f3bcc5a
msmtpd wasm32 built 1f8d13ee
nano wasm32 built f5d649f5
ncurses wasm32 built dc9ea43f
netcat wasm32 built fdbc86ec
nginx wasm32 built ef785fc2
php wasm32 built d103a42f
posix-utils-lite wasm32 built 587aa7b5
ruby wasm32 built 4ff5cc56
sed wasm32 built 7177e695
spidermonkey wasm32 built 43de30ca
tar wasm32 built debfa58a
tcl wasm32 built dd1a64f9
unzip wasm32 built d3254898
userspace wasm32 built 2ff5a537
vim wasm32 built b6cacaf9
wget wasm32 built 47794347
xz wasm32 built 60075411
zip wasm32 built 57fc3ddd
zstd wasm32 built 75e9bd9e
bash wasm32 built 7280a58f
mariadb-test wasm32 built dc75215f
mariadb-vfs wasm32 built 1afaa3af
mariadb-vfs wasm64 built 13215777
nethack wasm32 built 9e34b035
node wasm32 built 598f6735
spidermonkey-node wasm32 built 0e109cc0
vim-browser-bundle wasm32 built 62ea9614
nethack-browser-bundle wasm32 built 15edeab6
rootfs wasm32 built 6f214852
shell wasm32 built 24b8e787
lamp wasm32 built 4832a8bf
node-vfs wasm32 built 357db97e
wordpress wasm32 built bceb6b71

Auto-generated; replaced on each push. Raw data in the publish-status workflow artifact.

GNU Make and other plugin hosts use dlopen(NULL) plus RTLD_DEFAULT to resolve symbols exported by the executable. Kandelo's libc glue rejected that operation even though the host already tracked the main instance's global symbols.

Reserve a stable main-program handle, resolve function exports to table indices and data exports to addresses, and preserve side-module replay handle ordering. Cover the behavior in Node and Chromium with the same compiled fixture and document export visibility requirements.

This is a backward-compatible semantic addition over the existing host import signature. The ABI snapshot is unchanged, so ABI_VERSION remains 18.
@brandonpayton

Copy link
Copy Markdown
Member Author

Absorbed by rebase-merged #967. Main is now 6efb411 with the sealed tree b133bfa018f7e2bc84c4845edf2f945b2f844954, and ABI 41 activation completed successfully in run 29550069085. The absorption audit confirmed this PR\x27s behavior and tests are present in that merged tree, so this source PR is closed and its branch is deleted.

@brandonpayton
brandonpayton deleted the fix/dlopen-null-main-handle branch July 17, 2026 02:38
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.

1 participant