diff --git a/docs/integrating.md b/docs/integrating.md
index 6be7606..0a8ef58 100644
--- a/docs/integrating.md
+++ b/docs/integrating.md
@@ -121,6 +121,28 @@ target is the bundled dispatcher whenever the bundle *or* the host has a vendor
library for it, and the host's own library only otherwise; `examples/` shows the
failure this rule exists to prevent.
+### VA-API drivers
+
+VA-API needs one thing the library path cannot give. `libva.so.2` never opens
+its driver by soname: it constructs `
/_drv_video.so` and opens that
+absolute path, walking `LIBVA_DRIVERS_PATH` or a default compiled into the
+libva being run. That default names the layout of the distro that built the
+library, so a bundled libva looks for the runtime host's drivers in the wrong
+place and no `--library-path` can fix it.
+
+When `libva.so.2` is in the process, the preload assembles the answer: every
+host `/dri` directory it finds is appended to `LIBVA_DRIVERS_PATH`,
+behind anything already set. A process that never loads libva is not touched.
+The bundle's own `lib/dri` is never added. A bundle that ships VA drivers
+manages the variable itself, and its entries stay ahead of anything appended
+here.
+
+Measured with a stand-in driver by E95 through E100 in
+[`experiments/30-run-tests.sh`](../experiments/30-run-tests.sh), on the glibc
+2.31 floor. ⚠ **A real `iHD_drv_video.so` or `i965_drv_video.so` carried
+across a libc boundary is UNVERIFIED.** What that needs is a host with one
+installed, and [`report/README.md`](report/README.md) does not have that run.
+
### A plain binary, no bundle anywhere
```bash
diff --git a/docs/traps.md b/docs/traps.md
index d901c0e..bb7ad5b 100644
--- a/docs/traps.md
+++ b/docs/traps.md
@@ -71,6 +71,18 @@ about it.
---
+### A `dlopen` by soname can stop finding a library your own RUNPATH names
+
+The `dlopen` this project interposes runs with the interposer as its caller,
+and the search consults the **caller's** `DT_RPATH` and `DT_RUNPATH`. A binary
+that reaches a plugin by soname through its own runpath gets
+`cannot open shared object file` under the preload and works without it. Reach
+the library through `LD_LIBRARY_PATH` instead, or `dlopen` an object that
+NEEDs the soname you want, which is what gstreamer does to its va plugin.
+Measured while writing E97, in [`experiments/30-run-tests.sh`](../experiments/30-run-tests.sh).
+
+---
+
### `couldn't get an RGB, Double-buffered visual` is not about visuals
Or about libc. It is the message a glvnd dispatcher gives when the host ships no
diff --git a/experiments/30-run-tests.sh b/experiments/30-run-tests.sh
index bf4e6fc..6480f0b 100644
--- a/experiments/30-run-tests.sh
+++ b/experiments/30-run-tests.sh
@@ -1003,6 +1003,184 @@ else
cd ..
fi
+echo
+echo "-- Q. libva's driver search: LIBVA_DRIVERS_PATH is ours to assemble"
+#
+# libva never dlopens its driver by soname. va_openDriver() walks a search
+# list and dlopens the ABSOLUTE path it constructs from each entry,
+# /_drv_video.so. The list is LIBVA_DRIVERS_PATH, or the
+# VA_DRIVERS_PATH compiled into whichever libva is running. That compiled
+# default names the layout of the distro that BUILT it, so a bundled libva
+# carries its build host's answer into a process on a different one. No
+# library path can correct that, because no soname lookup ever happens.
+#
+# The feature under test: when libva.so.2 is in the process, the preload
+# appends the host's /dri directories to LIBVA_DRIVERS_PATH,
+# behind anything already set, and touches nothing in a process that never
+# loads libva. The bundle's own lib/dri is never added: a bundle that
+# ships VA drivers manages this variable itself, and those entries already
+# sit ahead of anything appended here.
+#
+# The fake libva below implements va_openDriver's CONTRACT: getenv,
+# colon-split, constructed absolute path, RTLD_NOW|RTLD_GLOBAL|RTLD_NODELETE,
+# dlsym __vaDriverInit_1_0. The fake drivers answer with the directory
+# they were built for, so the verdict line names WHICH directory won rather
+# than merely that a directory did. The answer round-trips through the
+# driver's own function, so a pass is not "a string appeared".
+mkdir -p /work/va/host/lib/dri /work/va/userdri /work/va/bundle/lib/dri
+cat > va_drv.c <<'CEOF'
+#include
+static const char *const va_vendor = VA_VENDOR;
+int __vaDriverInit_1_0(void *ctx) { (void)ctx; return 0; }
+const char *va_driver_vendor(void) { return va_vendor; }
+CEOF
+gcc -shared -fPIC -O2 -DVA_VENDOR='"HOST"' va_drv.c -o /work/va/host/lib/dri/vastub_drv_video.so 2>"$BERR" || bfail vastub-HOST
+gcc -shared -fPIC -O2 -DVA_VENDOR='"USER"' va_drv.c -o /work/va/userdri/vastub_drv_video.so 2>"$BERR" || bfail vastub-USER
+gcc -shared -fPIC -O2 -DVA_VENDOR='"BUNDLE"' va_drv.c -o /work/va/bundle/lib/dri/vastub_drv_video.so 2>"$BERR" || bfail vastub-BUNDLE
+
+cat > va_libva.c <<'CEOF'
+#define _GNU_SOURCE
+#include
+#include
+#include
+static void *drv;
+int vaInitialize(void) {
+ const char *search = secure_getenv("LIBVA_DRIVERS_PATH");
+ if (!search || !*search) return 1;
+ char *copy = strdup(search);
+ if (!copy) return 1;
+ int rc = 1;
+ for (char *dir = strtok(copy, ":"); dir; dir = strtok(NULL, ":")) {
+ char path[4096];
+ snprintf(path, sizeof path, "%s/%s%s", dir, "vastub", "_drv_video.so");
+ void *h = dlopen(path, RTLD_NOW | RTLD_GLOBAL | RTLD_NODELETE);
+ if (!h) continue; /* silent on files that are not there, as libva is */
+ int (*init)(void *) = (int (*)(void *))dlsym(h, "__vaDriverInit_1_0");
+ if (init && init(NULL) == 0) { drv = h; rc = 0; break; }
+ dlclose(h);
+ }
+ free(copy);
+ return rc;
+}
+const char *va_stub_vendor(void) {
+ if (!drv) return "(no driver)";
+ const char *(*f)(void) = (const char *(*)(void))dlsym(drv, "va_driver_vendor");
+ return f ? f() : "(no vendor)";
+}
+CEOF
+gcc -shared -fPIC -O2 va_libva.c -o /work/va/libva.so.2 \
+ -Wl,-soname,libva.so.2 -ldl 2>"$BERR" || bfail libva.so.2
+
+# Consumer A links libva at startup, the shape of every ffmpeg/mpv/browser
+cat > va_consumer.c <<'CEOF'
+#include
+#include
+extern int vaInitialize(void);
+extern const char *va_stub_vendor(void);
+int main(void) {
+ const char *e = getenv("LIBVA_DRIVERS_PATH");
+ if (vaInitialize() != 0) { printf("NO-DRIVER env=[%s]\n", e ? e : "(unset)"); return 1; }
+ printf("DRIVER=%s env=[%s]\n", va_stub_vendor(), e ? e : "(unset)");
+ return 0;
+}
+CEOF
+gcc -O2 va_consumer.c -o va_consumer \
+ -L/work/va -l:libva.so.2 -Wl,-rpath,/work/va 2>"$BERR" || bfail va_consumer
+
+# Consumer B never loads libva: the guard's other arm
+cat > va_nolib.c <<'CEOF'
+#include
+#include
+int main(void) {
+ const char *e = getenv("LIBVA_DRIVERS_PATH");
+ printf("env=[%s]\n", e ? e : "(unset)");
+ return 0;
+}
+CEOF
+gcc -O2 va_nolib.c -o va_nolib 2>"$BERR" || bfail va_nolib
+
+# Consumer C dlopens a PLUGIN that NEEDs libva, the shape of gstreamer
+# loading libgstva.so: libva rides in as a dependency of the dlopened
+# object, after main. Direct references keep the NEEDED alive against
+# --as-needed. (dlopen("libva.so.2") directly from the consumer does NOT
+# work here, and the reason is its own finding: an interposed dlopen's
+# caller is the preload, so the consumer's own RUNPATH is not searched.
+# A bundle-shaped process reaches libva through LD_LIBRARY_PATH or through
+# a dlopened object's dependency, which is the shape below.)
+cat > va_late.c <<'CEOF'
+#define _GNU_SOURCE
+#include
+#include
+#include
+int main(void) {
+ void *h = dlopen("/work/va/va_plugin.so", RTLD_NOW);
+ if (!h) { printf("NO-PLUGIN %s\n", dlerror()); return 1; }
+ int (*init)(void) = (int (*)(void))dlsym(h, "plugin_va_init");
+ const char *(*vend)(void) = (const char *(*)(void))dlsym(h, "plugin_va_vendor");
+ const char *e = getenv("LIBVA_DRIVERS_PATH");
+ if (!e || !*e) { printf("NO-DRIVER env=[(unset)]\n"); return 1; }
+ if (init() != 0) { printf("NO-DRIVER env=[%s]\n", e); return 1; }
+ printf("DRIVER=%s env=[%s]\n", vend(), e);
+ return 0;
+}
+CEOF
+cat > va_plugin.c <<'CEOF'
+extern int vaInitialize(void);
+extern const char *va_stub_vendor(void);
+int plugin_va_init(void) { return vaInitialize(); }
+const char *plugin_va_vendor(void) { return va_stub_vendor(); }
+CEOF
+gcc -shared -fPIC -O2 va_plugin.c -o /work/va/va_plugin.so \
+ -L/work/va -l:libva.so.2 -Wl,-rpath,/work/va 2>"$BERR" || bfail va_plugin
+gcc -O2 va_late.c -o va_late -ldl 2>"$BERR" || bfail va_late
+
+# E95: libva linked at startup. /work/va/host/lib stands in for the host's
+# libdir on the process's search list; the conventional directories
+# cannot know /work, so only the assembled list can answer.
+run E95 OK "DRIVER=HOST env=[/work/va/host/lib/dri" \
+ env LD_LIBRARY_PATH=/work/va/host/lib \
+ LD_PRELOAD=/work/cross-libc-dlopen.so ./va_consumer
+
+# E96: the control, and the case that fails without the feature. Feature
+# off means the variable stays unset, the fake libva walks nothing,
+# and the driver is never found.
+run E96 FAIL "NO-DRIVER env=[(unset)]" \
+ env CROSS_LIBC_DLOPEN=0 LD_LIBRARY_PATH=/work/va/host/lib \
+ LD_PRELOAD=/work/cross-libc-dlopen.so ./va_consumer
+
+# E97: the late load. gstreamer dlopens its va plugin, which pulls libva in
+# after main; the check that assembles the list runs after that very
+# dlopen returns, which is still before vaInitialize can read it.
+run E97 OK "DRIVER=HOST env=[/work/va/host/lib/dri" \
+ env LD_LIBRARY_PATH=/work/va/host/lib \
+ LD_PRELOAD=/work/cross-libc-dlopen.so ./va_late
+
+# E98: a value already set keeps its place. The user's directory is tried
+# first and answers USER; the host directory is APPENDED behind it,
+# the same place the conventions put every appended path entry.
+run E98 OK "DRIVER=USER env=[/work/va/userdri:/work/va/host/lib/dri" \
+ env LIBVA_DRIVERS_PATH=/work/va/userdri LD_LIBRARY_PATH=/work/va/host/lib \
+ LD_PRELOAD=/work/cross-libc-dlopen.so ./va_consumer
+
+# E99: the bundle's own dri directory never enters the list. An absence,
+# and `run` can only assert presence, so the absence is turned into a
+# word, the way E85 does.
+run E99 OK "bundle-dri-absent" sh -c \
+ 'out=$(env CROSS_LIBC_DLOPEN_ROOT=/work/va/bundle \
+ LD_LIBRARY_PATH=/work/va/host/lib \
+ LD_PRELOAD=/work/cross-libc-dlopen.so \
+ ./va_consumer 2>&1)
+ case "$out" in
+ */work/va/bundle/*) echo "bundle-dri-PRESENT: $out" ;;
+ *"DRIVER=HOST"*) echo "bundle-dri-absent" ;;
+ *) echo "no-driver: $out" ;;
+ esac'
+
+# E100: no libva in the process, so the variable is not ours to write.
+# Consumer B links nothing of the kind and the feature is ON.
+run E100 OK "env=[(unset)]" \
+ env LD_PRELOAD=/work/cross-libc-dlopen.so ./va_nolib
+
echo
echo "================================================================"
echo " predictions matched: $PASS mismatched: $FAIL"
diff --git a/src/cross-libc-dlopen.c b/src/cross-libc-dlopen.c
index 3151367..7660ddc 100644
--- a/src/cross-libc-dlopen.c
+++ b/src/cross-libc-dlopen.c
@@ -1434,6 +1434,201 @@ VISIBLE void cross_libc_dlopen_init_now(void) {
cld_load_global_scope_libs();
}
+// ---------------------------------------------------------------------------
+// libva's driver search list (LIBVA_DRIVERS_PATH)
+//
+// libva never dlopens its driver by soname. va_openDriver() walks a search
+// list and dlopens the ABSOLUTE path it constructs from each entry,
+// /_drv_video.so. The list is LIBVA_DRIVERS_PATH, or the
+// VA_DRIVERS_PATH compiled into the libva being run. That compiled default
+// names the layout of whatever distro BUILT that libva, and a bundled
+// libva therefore carries its build host's answer into a process running
+// on a different one. No library path can correct this, because no soname
+// lookup happens; the list is ours to assemble, the same act sharun
+// performs for ld.so.
+//
+// Why only when libva is loaded: the variable is libva's, and a process
+// that never loads libva gets nothing written into its environment. The
+// check is dl_iterate_phdr, once from the constructor (libva linked at
+// startup) and once after every successful dlopen until it fires (libva
+// dlopened late, as gstreamer loads its va plugin). Either route has fired
+// before vaInitialize can read the variable: a dlopen of libva itself is
+// the last load that can precede that read, and the check runs after that
+// very dlopen returns.
+//
+// Nothing here opens a library (conventions/code.md): appending directories
+// to a search list is not searching it, and libva does its own searching
+// from the result. The bundle's own lib/dri is deliberately never included:
+// a bundle that ships VA drivers manages LIBVA_DRIVERS_PATH itself, and
+// whatever it set already sits ahead of anything appended here.
+//
+// The existing value keeps priority and is never clobbered: anything this
+// assembles goes at the END, the same place the conventions put appended
+// library-path entries, so a user's or a launcher's choice always wins.
+#define CLD_VA_LIBVA_SONAME "libva.so."
+#define CLD_VA_DRIVERS_PATH "LIBVA_DRIVERS_PATH"
+
+#if defined(__x86_64__)
+# define CLD_TRIPLET "x86_64-linux-gnu"
+#elif defined(__aarch64__)
+# define CLD_TRIPLET "aarch64-linux-gnu"
+#elif defined(__i386__)
+# define CLD_TRIPLET "i386-linux-gnu"
+#else
+# define CLD_TRIPLET "unknown"
+#endif
+
+// The conventional libdirs of the hosts this runs on, each probed as
+// /dri. Debian and Ubuntu keep their VA drivers under the triplet
+// directory, Alpine in /usr/lib/dri, Fedora in /usr/lib64/dri. A missed
+// access() costs nothing.
+static const char *const cld_va_libdirs[] = {
+ "/usr/lib/" CLD_TRIPLET, "/lib/" CLD_TRIPLET,
+ "/usr/lib64", "/lib64",
+ "/usr/lib", "/lib",
+ "/usr/local/lib", "/usr/local/lib64",
+ NULL
+};
+
+static int cld_va_phdr_cb(struct dl_phdr_info *info, size_t size, void *data) {
+ (void)size;
+ const char *name = info->dlpi_name;
+ const char *base = strrchr(name, '/');
+ base = base ? base + 1 : name;
+ if (strncmp(base, CLD_VA_LIBVA_SONAME,
+ sizeof(CLD_VA_LIBVA_SONAME) - 1) == 0) {
+ *(int *)data = 1;
+ return 1;
+ }
+ return 0;
+}
+
+static int cld_va_libva_loaded(void) {
+ // dlpi_name is the soname for DT_NEEDED libraries and a path for
+ // dlopened ones, so the basename is what has to match either way
+ int found = 0;
+ dl_iterate_phdr(cld_va_phdr_cb, &found);
+ return found;
+}
+
+struct cld_va_list {
+ char buf[4096];
+ size_t used;
+};
+
+static int cld_va_has(const struct cld_va_list *l, const char *dir) {
+ size_t len = strlen(dir);
+ const char *p = l->buf;
+ while (*p) {
+ const char *end = strchr(p, ':');
+ if (!end) end = p + strlen(p);
+ if ((size_t)(end - p) == len && strncmp(p, dir, len) == 0)
+ return 1;
+ p = *end ? end + 1 : end;
+ }
+ return 0;
+}
+
+static void cld_va_add(struct cld_va_list *l, const char *dir) {
+ char path[PATH_MAX];
+ int n = snprintf(path, sizeof(path), "%s/dri", dir);
+ if (n < 0 || n >= (int)sizeof(path))
+ return;
+ if (access(path, X_OK) != 0)
+ return;
+ if (cld_va_has(l, path))
+ return;
+ int w = snprintf(l->buf + l->used, sizeof(l->buf) - l->used,
+ "%s%s", l->used ? ":" : "", path);
+ if (w < 0 || (size_t)w >= sizeof(l->buf) - l->used) {
+ DEBUG_PRINT("LIBVA_DRIVERS_PATH is full; %s omitted\n", path);
+ return;
+ }
+ l->used += (size_t)w;
+}
+
+// One guarded run. ⛔ The guard means "settled", not "ran": a constructor
+// that latched on "libva absent" would disarm the post-dlopen scan for the
+// process's whole life, and the late-load case would never fire. Measured
+// here as E97, which went exactly that way on the first revision. So the
+// scan re-runs after every successful dlopen until libva is found; before
+// that, each pass costs one phdr walk per dlopen, and a dlopen is already
+// the more expensive of the two.
+static int cld_va_done;
+
+static void cld_va_setup(void) {
+ if (cld_va_done)
+ return;
+
+ if (!cross_libc_dlopen_mode()) {
+ cld_va_done = 1;
+ return;
+ }
+
+ // Not loaded yet is NOT settled: the next dlopen may bring libva in.
+ if (!cld_va_libva_loaded()) {
+ DEBUG_PRINT("LIBVA_DRIVERS_PATH: libva not loaded, untouched\n");
+ return;
+ }
+ cld_va_done = 1;
+
+ struct cld_va_list l = { { 0 }, 0 };
+
+ // The value already set, the user's or the launcher's, keeps its
+ // place at the front of libva's walk.
+ const char *cur = getenv(CLD_VA_DRIVERS_PATH);
+ if (cur && *cur) {
+ int w = snprintf(l.buf, sizeof(l.buf), "%s", cur);
+ if (w < 0 || (size_t)w >= sizeof(l.buf)) {
+ DEBUG_PRINT("LIBVA_DRIVERS_PATH already overlong, left alone\n");
+ return;
+ }
+ l.used = (size_t)w;
+ }
+
+ // The process's own search list first: a launcher that already
+ // assembled the host library path (sharun) has written the host's
+ // answers there, and /dri is where those answers keep VA.
+ const char *lp = getenv("LD_LIBRARY_PATH");
+ if (lp && *lp) {
+ char *copy = strdup(lp);
+ if (copy) {
+ for (char *p = strtok(copy, ":"); p; p = strtok(NULL, ":"))
+ if (is_host_library_path(p))
+ cld_va_add(&l, p);
+ free(copy);
+ }
+ }
+
+ for (size_t i = 0; cld_va_libdirs[i]; i++)
+ cld_va_add(&l, cld_va_libdirs[i]);
+
+ if (!l.used) {
+ DEBUG_PRINT("LIBVA_DRIVERS_PATH: libva loaded, no host dri "
+ "directory found, untouched\n");
+ return;
+ }
+
+ if (cld_dryrun_enabled()) {
+ fprintf(stderr,
+ " [cross-libc-dlopen.so] >> DRYRUN LIBVA_DRIVERS_PATH "
+ "would be: %s\n", l.buf);
+ return;
+ }
+
+ if (setenv(CLD_VA_DRIVERS_PATH, l.buf, 1) != 0) {
+ DEBUG_PRINT("LIBVA_DRIVERS_PATH setenv failed: %s\n",
+ strerror(errno));
+ return;
+ }
+ DEBUG_PRINT("LIBVA_DRIVERS_PATH=%s\n", l.buf);
+}
+
+__attribute__((constructor))
+static void cld_va_init(void) {
+ cld_va_setup();
+}
+
// core libraries are never stripped nor loaded twice, rewriting the
// dynamic linker or libc is a one way ticket to segfault city. ld-linux
// carries no soname so RTLD_NOLOAD cannot catch it, hence this list
@@ -1801,13 +1996,20 @@ VISIBLE void *dlopen(const char *filename, int flags) {
int handled = 0;
void *host = cld_attempt(dlopen_orig, filename, flags, &handled);
if (handled) {
- if (host)
+ if (host) {
+ // a successful load may have brought libva in; the call is
+ // one branch once it has fired (see the LIBVA section above)
+ cld_va_setup();
DEBUG_PRINT("cross-libc dlopen success: %s\n", filename);
- else
+ } else {
DEBUG_PRINT("cross-libc dlopen failed: %s\n", filename);
+ }
return host;
}
DEBUG_PRINT("dlopen pass-through: %s\n", filename);
- return dlopen_orig(filename, flags);
+ void *pass = dlopen_orig(filename, flags);
+ if (pass)
+ cld_va_setup();
+ return pass;
}