Skip to content

dln_c_loader_interpose: all-series patches + the spec-22 acceptance harness — DRAFT, chain-gated - #55

Draft
ronaldtse wants to merge 8 commits into
mainfrom
feat/dln-c-loader-interpose
Draft

dln_c_loader_interpose: all-series patches + the spec-22 acceptance harness — DRAFT, chain-gated#55
ronaldtse wants to merge 8 commits into
mainfrom
feat/dln-c-loader-interpose

Conversation

@ronaldtse

Copy link
Copy Markdown
Contributor

What

Spec 22 phase 1 (loader interposition) — the ruby-side pieces:

  • patches/<series>/dln_c_loader_interpose.patch for all five series (3.1, 3.2, 3.3, 3.4, 4.0 — 3.2's body is shared by 3.2–4.0 via manifest references): routes dln_open's dlopen through tfs_dlopen_route so payload C-extensions resolve against the VFS; a miss is a named dlerror, never a segfault.
  • ci/spec22/ — the acceptance harness (run.sh + fixtures + README): factory-builds a runtime locally from the patched source mirror and runs the jailed probe (fiddle / cext self-dlopen / named dlerror / jail EPERM).

Evidence (macOS arm64)

  • All-version roll: 37/37 versions apply clean (tfs_dlopen_route present in every dln.c) — mirror + SHA256SUMS in the run artifacts.
  • Line tips factory-built + jailed-probed 5/5 green: 3.1.7, 3.2.11, 3.3.12, 3.4.10, 4.0.6 (SPEC22-ACCEPTANCE-OK, 4 assertions each).

Deviations (documented in ci/spec22/README)

  • macOS delivery is the driver's self-insertion, not this patch.
  • dlsym is not interposed (dlopen only).

Status

DRAFT — chain-gated. Do not merge until the whole spec-22 chain is green — tebako #396, tebako-runtime #28, tebako-runtime-ruby #95, plus the ELF acceptance leg — and the owner calls the merge. The previous tebako release is still publishing; nothing lands on main before the full chain is validated end-to-end.

…ce harness

Spec 22 class L, ELF (linux gnu/musl): the runtime binary's own
dlopen/dlerror definitions preempt process-wide (the interpreter, its C
extensions, and any library they pull), so every native-library load --
dln_load, ffi, fiddle, C-extension self-loads -- routes a VFS-resident
path through tebako_fs_dlmap2file (library + dependency closure to the
exec cache) and loads the real file. Host paths pass through;
covered-but-not-held answers ENOENT and the raw call serves it (the
dln_load precedent). Materialization failures are named errors
(library, mount, verdict) through the dlerror channel. dlsym is not
interposed (real handles make it unnecessary; a main-binary dlsym
cannot resolve its original on musl).

macOS is inert in this TU: dyld applies __interpose tuples only from
dylibs, never from the main executable (verified), so the macOS
interposition is the driver's self-insertion at boot
(crates/tebako-driver). Inert where USE_DLN_DLOPEN is undefined
(windows).

One body serves 3.2-4.0 (home 3.2); 3.1 carries its diverged context.
The ci/spec22 harness is the local reproducible acceptance proof
(jailed fiddle + C-ext self-dlopen probe).
The harness README's platform notes still described the pre-discovery
design (an __interpose section in the runtime binary). dyld honors
tuples only from dylib images (verified empirically, spec 22 'Phase 1
delivery'): the macOS interposition is the driver writing its embedded
interpose dylib and re-execing with DYLD_INSERT_LIBRARIES; the ELF
delivery is this patch family.
- prefer homebrew bison: stock macOS ships 2.3, ruby regenerates
  parse.c (needs >= 3.0) when a patch nudges the dependency graph —
  breaks 3.2.x builds
- package layout: accept the sibling form (exe IS $RUNTIME_PKG,
  image $RUNTIME_PKG.tfs beside it) as well as the directory form
- probe natives: canonical header root deps/src/_ruby_$VERSION; the
  blanket find can land on the deps/stash_* installed-headers copy
  (include/ruby-X/ruby.h — one level deeper), breaking the dirname
  arithmetic
- derive the .ext/include arch dir instead of hardcoding
  arm64-darwin24
ruby compiles dln.c with -fvisibility=hidden (XCFLAGS); upstream symbols
get default visibility from the RUBY_SYMBOL_EXPORT pragmas in dln.h, but
the patch's new definitions carried no export marker, so they stayed
GLOBAL HIDDEN in dln.o and LOCAL in the linked exe. -Wl,-export-dynamic
cannot export a hidden symbol, so the exe-preempt design (spec 22 phase
1, class L, ELF delivery) never engaged: nm -D showed no dlopen/dlerror
export (gate exit 66 on the 4.0.6 linux-gnu build).

Annotate both definitions __attribute__((visibility("default"))) (the
3.1 copy and the 3.2 body shared by 3.2-4.0 via manifest refs) and
document the constraint in the patch comment. macOS is unaffected (its
delivery is the driver's self-insertion dylib, not this TU).
The linux/amd64 twin of run.sh: one docker container (ubuntu-20.04
factory CI image, linux/amd64 under qemu-user on Apple Silicon), one
host-side driver (run-elf-leg.sh) — toolchain setup, source roll from
this checkout, adapter-less gem repo staging, link unit build with the
GNU_UNIQUE gate, factory runtime build with the nm -D dlopen/dlerror
export gate, and the same four jailed assertions as the macOS leg.
Proven green on 4.0.6 (SPEC22-ACCEPTANCE-OK).

The container-local deviations are documented in elf/README.md and
scripted where automatable: the distro libjemalloc.a neuter (qemu-user
hangs miniruby otherwise; setup-toolchain.sh, idempotent) and the
libtfs.a reseal recipe for gcc's GNU_UNIQUE template statics in the
vendored rnp closure (gated in build-link-unit.sh; reseal.py +
swap-members.py + ar-rebuild.py included; the durable fix is upstream
arscope work, flagged in the README).
…2 §3 SSOT)

tfs_spawn_prepare now resolves the preload shim's in-VFS path from
TEBAKO_PRELOAD_SHIM (the driver's flow of the env image's layout.yaml
preload_shim grant) and falls back to the literal
/__tfs__/lib/tebako/libtfs_preload.{so,dylib} for images predating the
grant. The env var is authoritative; the literal keeps older runtime
images working through the transition.

Same transform applied to patches/{3.1,3.2,3.3,3.4,4.0} (byte-identical
hook across versions); hunk headers recomputed arithmetically, verified
by line recount. Apply-fixture suite: 110 examples, 0 failures.
…targets

Factory run 31699651270 (macos arm64, darwin24) failed the shell-string
class-E example with EMPTY output and killed the probe's diagnostic cc:
dyld TERMINATES an Apple-restricted binary at exec when the inherited
DYLD_INSERT_LIBRARIES names a foreign dylib. darwin23 stripped the
variable instead (the behavior spec 22 §3.1 pinned), so under the
driver's process-wide export every system()/backtick of a packaged
interpreter killed /bin/sh on that host.

The hook now drops the inherited variable per spawn whose target is
restricted: any shell form (the shell is always a platform binary) and
any target resolving into Apple's system binary dirs (/bin, /sbin,
/usr/bin, /usr/sbin, /usr/libexec, /System), with an execvp-equivalent
parent-side PATH walk for bare names. The nil env pair deletes the
inherited entry (rb_execarg_parent_start1's NIL_P branch, st_delete —
verified against ruby_4_0 process.c). A restricted target can never
honor the insertion on any host, so the scrub loses nothing; a
non-restricted host target (a third-party JRE) keeps the delivery —
spec 22 §3.1's array form stays green.

Patch verified: applies cleanly against ruby_4_0 process.c (patch
--dry-run); the PATH-walk + prefix logic passes a 12-case standalone
oracle (sh/cc resolve restricted, temurin's hostedtoolcache path and
/usr/local stay delivered). All five version patches kept byte-identical.
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.

2 participants